Skip to content

Commit d893503

Browse files
feat: remove allow-same-origin for better security.
1 parent 72c5ed4 commit d893503

8 files changed

Lines changed: 589 additions & 221 deletions

File tree

docs/preview-sandbox-benchmark.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Preview Sandbox Benchmark
2+
3+
This benchmark captures preview-runtime timing before and after sandbox changes.
4+
5+
## Metrics
6+
7+
- First render latency
8+
- Median auto-render latency across 20 edits
9+
- p95 auto-render latency across 20 edits
10+
- Runtime diagnostic arrival latency after a known runtime error
11+
12+
## Setup
13+
14+
1. Start the app with `npm run dev`.
15+
2. Open the app in a fresh browser tab.
16+
3. Open DevTools console and run:
17+
18+
```js
19+
window.__KNIGHTED_PREVIEW_TELEMETRY__ = []
20+
```
21+
22+
4. Confirm auto-render is enabled.
23+
24+
## Manual run
25+
26+
1. Wait for the initial render to complete.
27+
2. Make 20 quick edits in the component editor (append a character, then remove it).
28+
3. Trigger one known runtime error (for example, throw inside `App`).
29+
4. In the console, run:
30+
31+
```js
32+
const events = Array.isArray(window.__KNIGHTED_PREVIEW_TELEMETRY__)
33+
? window.__KNIGHTED_PREVIEW_TELEMETRY__
34+
: []
35+
36+
const byName = name => events.filter(event => event?.name === name)
37+
38+
const renderStarts = byName('render-start')
39+
const renderCompletes = byName('render-complete')
40+
const iframeReady = byName('iframe-ready')
41+
const rendered = byName('rendered')
42+
const runtimeErrors = byName('runtime-error')
43+
44+
const pairDurations = (starts, ends) => {
45+
const count = Math.min(starts.length, ends.length)
46+
const durations = []
47+
48+
for (let index = 0; index < count; index += 1) {
49+
const start = starts[index]?.at
50+
const end = ends[index]?.at
51+
if (typeof start === 'number' && typeof end === 'number' && end >= start) {
52+
durations.push(end - start)
53+
}
54+
}
55+
56+
return durations
57+
}
58+
59+
const quantile = (values, ratio) => {
60+
if (!Array.isArray(values) || values.length === 0) {
61+
return null
62+
}
63+
64+
const sorted = [...values].sort((a, b) => a - b)
65+
const index = Math.min(
66+
sorted.length - 1,
67+
Math.max(0, Math.ceil(sorted.length * ratio) - 1),
68+
)
69+
70+
return sorted[index]
71+
}
72+
73+
const renderDurations = pairDurations(renderStarts, renderCompletes)
74+
const firstRenderLatency = renderDurations[0] ?? null
75+
const autoRenderDurations = renderDurations.slice(1, 21)
76+
77+
const diagnosticsLatency = (() => {
78+
const firstRuntimeError = runtimeErrors[0]
79+
const firstRendered = rendered[0]
80+
if (
81+
!firstRuntimeError ||
82+
typeof firstRuntimeError.at !== 'number' ||
83+
!firstRendered ||
84+
typeof firstRendered.at !== 'number'
85+
) {
86+
return null
87+
}
88+
89+
return firstRuntimeError.at - firstRendered.at
90+
})()
91+
92+
console.table({
93+
firstRenderLatency,
94+
autoRenderMedian: quantile(autoRenderDurations, 0.5),
95+
autoRenderP95: quantile(autoRenderDurations, 0.95),
96+
diagnosticsLatency,
97+
iframeReadyEvents: iframeReady.length,
98+
runtimeErrorEvents: runtimeErrors.length,
99+
})
100+
```
101+
102+
## Notes
103+
104+
- Rerun if CDN failures occur because runtime imports are network-backed.
105+
- Compare baseline and updated runs using the same browser and machine state.

playwright/rendering-modes.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,38 @@ test('editing-transient missing reference runtime errors are suppressed', async
326326
await expect(page.getByRole('status', { name: 'App status' })).not.toHaveText('Error')
327327
})
328328

329+
test('preview iframe sandbox isolates parent origin access', async ({ page }) => {
330+
await waitForInitialRender(page)
331+
332+
const iframe = page.locator('#preview-host iframe')
333+
const sandbox = await iframe.getAttribute('sandbox')
334+
335+
expect(typeof sandbox).toBe('string')
336+
expect(sandbox?.includes('allow-same-origin')).toBeFalsy()
337+
338+
await setComponentEditorSource(
339+
page,
340+
[
341+
'const canReadParentStorage = (() => {',
342+
' try {',
343+
' return Boolean(window.parent.localStorage)',
344+
' } catch {',
345+
' return false',
346+
' }',
347+
'})()',
348+
'',
349+
'export const App = () => (',
350+
" <button type='button'>",
351+
" {canReadParentStorage ? 'parent-readable' : 'parent-blocked'}",
352+
' </button>',
353+
')',
354+
].join('\n'),
355+
)
356+
357+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
358+
await expect(getPreviewFrame(page).getByRole('button')).toContainText('parent-blocked')
359+
})
360+
329361
test('post-render runtime exceptions from iframe are reported in preview panel', async ({
330362
page,
331363
}) => {

src/app.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,14 @@ const showAppToast = message => {
226226
const previewBackground = createPreviewBackgroundController({
227227
previewBgColorInput,
228228
getPreviewHost: () => previewHost,
229+
onBackgroundColorChange: color => {
230+
if (
231+
renderRuntime &&
232+
typeof renderRuntime.updatePreviewBackgroundColor === 'function'
233+
) {
234+
renderRuntime.updatePreviewBackgroundColor(color)
235+
}
236+
},
229237
getDefaultPreviewBackgroundColor: () => {
230238
if (document.documentElement.dataset.theme === 'light') {
231239
return '#ffffff'

src/modules/preview-background.js

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export const createPreviewBackgroundController = ({
3939
previewBgColorInput,
4040
getPreviewHost,
4141
getDefaultPreviewBackgroundColor,
42+
onBackgroundColorChange,
4243
}) => {
4344
let previewBackgroundColor = null
4445
let previewBackgroundCustomized = false
@@ -66,26 +67,21 @@ export const createPreviewBackgroundController = ({
6667
return
6768
}
6869

69-
const iframe = previewHost.querySelector('iframe')
70-
const iframeDocument = iframe?.contentDocument ?? null
71-
7270
if (typeof color === 'string' && color.length > 0) {
7371
previewHost.style.backgroundColor = color
7472
previewHost.style.setProperty('--preview-iframe-background-color', color)
7573

76-
if (iframeDocument) {
77-
iframeDocument.documentElement.style.backgroundColor = color
78-
iframeDocument.body.style.backgroundColor = color
74+
if (typeof onBackgroundColorChange === 'function') {
75+
onBackgroundColorChange(color)
7976
}
8077
return
8178
}
8279

8380
previewHost.style.removeProperty('background-color')
8481
previewHost.style.removeProperty('--preview-iframe-background-color')
8582

86-
if (iframeDocument) {
87-
iframeDocument.documentElement.style.removeProperty('background-color')
88-
iframeDocument.body.style.removeProperty('background-color')
83+
if (typeof onBackgroundColorChange === 'function') {
84+
onBackgroundColorChange('')
8985
}
9086
}
9187

0 commit comments

Comments
 (0)