@lumencast/runtime 0.6.0 → 0.8.0

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.
Files changed (133) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/broadcast-Gcd-dmC7.js +12 -0
  3. package/dist/broadcast-Gcd-dmC7.js.map +1 -0
  4. package/dist/control-C5TfClga.js +17 -0
  5. package/dist/control-C5TfClga.js.map +1 -0
  6. package/dist/{index-Crkij3C4.js → index-N-VqrIxN.js} +305 -210
  7. package/dist/index-N-VqrIxN.js.map +1 -0
  8. package/dist/index.d.ts +3 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.html +1 -1
  11. package/dist/index.js +13 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/lumencast.js +14 -9
  14. package/dist/modes/broadcast.d.ts.map +1 -1
  15. package/dist/modes/broadcast.js +6 -1
  16. package/dist/modes/broadcast.js.map +1 -1
  17. package/dist/modes/control.d.ts.map +1 -1
  18. package/dist/modes/control.js +6 -1
  19. package/dist/modes/control.js.map +1 -1
  20. package/dist/modes/test.d.ts.map +1 -1
  21. package/dist/modes/test.js +2 -1
  22. package/dist/modes/test.js.map +1 -1
  23. package/dist/render/allowed-hosts.d.ts +41 -0
  24. package/dist/render/allowed-hosts.d.ts.map +1 -0
  25. package/dist/render/allowed-hosts.js +88 -0
  26. package/dist/render/allowed-hosts.js.map +1 -0
  27. package/dist/render/asset-resolve.d.ts +27 -0
  28. package/dist/render/asset-resolve.d.ts.map +1 -0
  29. package/dist/render/asset-resolve.js +86 -0
  30. package/dist/render/asset-resolve.js.map +1 -0
  31. package/dist/render/blend-mode.d.ts +7 -0
  32. package/dist/render/blend-mode.d.ts.map +1 -0
  33. package/dist/render/blend-mode.js +49 -0
  34. package/dist/render/blend-mode.js.map +1 -0
  35. package/dist/render/bundle.d.ts +9 -1
  36. package/dist/render/bundle.d.ts.map +1 -1
  37. package/dist/render/bundle.js.map +1 -1
  38. package/dist/render/fill.d.ts +36 -3
  39. package/dist/render/fill.d.ts.map +1 -1
  40. package/dist/render/fill.js +222 -23
  41. package/dist/render/fill.js.map +1 -1
  42. package/dist/render/headless.d.ts +39 -0
  43. package/dist/render/headless.d.ts.map +1 -0
  44. package/dist/render/headless.js +83 -0
  45. package/dist/render/headless.js.map +1 -0
  46. package/dist/render/mask.d.ts +87 -0
  47. package/dist/render/mask.d.ts.map +1 -0
  48. package/dist/render/mask.js +243 -0
  49. package/dist/render/mask.js.map +1 -0
  50. package/dist/render/primitives/frame.d.ts.map +1 -1
  51. package/dist/render/primitives/frame.js +91 -5
  52. package/dist/render/primitives/frame.js.map +1 -1
  53. package/dist/render/primitives/grid.d.ts +1 -1
  54. package/dist/render/primitives/grid.d.ts.map +1 -1
  55. package/dist/render/primitives/grid.js +4 -1
  56. package/dist/render/primitives/grid.js.map +1 -1
  57. package/dist/render/primitives/image.d.ts +8 -1
  58. package/dist/render/primitives/image.d.ts.map +1 -1
  59. package/dist/render/primitives/image.js +17 -3
  60. package/dist/render/primitives/image.js.map +1 -1
  61. package/dist/render/primitives/index.d.ts +7 -0
  62. package/dist/render/primitives/index.d.ts.map +1 -1
  63. package/dist/render/primitives/index.js.map +1 -1
  64. package/dist/render/primitives/media.d.ts +11 -2
  65. package/dist/render/primitives/media.d.ts.map +1 -1
  66. package/dist/render/primitives/media.js +14 -3
  67. package/dist/render/primitives/media.js.map +1 -1
  68. package/dist/render/primitives/shape.d.ts.map +1 -1
  69. package/dist/render/primitives/shape.js +29 -26
  70. package/dist/render/primitives/shape.js.map +1 -1
  71. package/dist/render/primitives/stack.d.ts +1 -1
  72. package/dist/render/primitives/stack.d.ts.map +1 -1
  73. package/dist/render/primitives/stack.js +5 -1
  74. package/dist/render/primitives/stack.js.map +1 -1
  75. package/dist/render/primitives/text.d.ts.map +1 -1
  76. package/dist/render/primitives/text.js +0 -1
  77. package/dist/render/primitives/text.js.map +1 -1
  78. package/dist/render/prop-allowlist.d.ts.map +1 -1
  79. package/dist/render/prop-allowlist.js +25 -2
  80. package/dist/render/prop-allowlist.js.map +1 -1
  81. package/dist/render/shape-geometry.d.ts +81 -0
  82. package/dist/render/shape-geometry.d.ts.map +1 -0
  83. package/dist/render/shape-geometry.js +199 -0
  84. package/dist/render/shape-geometry.js.map +1 -0
  85. package/dist/render/shape-index.d.ts +28 -0
  86. package/dist/render/shape-index.d.ts.map +1 -0
  87. package/dist/render/shape-index.js +77 -0
  88. package/dist/render/shape-index.js.map +1 -0
  89. package/dist/render/tree.d.ts.map +1 -1
  90. package/dist/render/tree.js +175 -3
  91. package/dist/render/tree.js.map +1 -1
  92. package/dist/render/universal-wrapper.d.ts +27 -1
  93. package/dist/render/universal-wrapper.d.ts.map +1 -1
  94. package/dist/render/universal-wrapper.js +98 -22
  95. package/dist/render/universal-wrapper.js.map +1 -1
  96. package/dist/{status-pill-BT5b-yET.js → status-pill-BaLQoIDl.js} +2 -2
  97. package/dist/{status-pill-BT5b-yET.js.map → status-pill-BaLQoIDl.js.map} +1 -1
  98. package/dist/{test-_hh1JvAd.js → test-CA30C2By.js} +51 -51
  99. package/dist/{test-_hh1JvAd.js.map → test-CA30C2By.js.map} +1 -1
  100. package/dist/tree-1coZ32nd.js +1777 -0
  101. package/dist/tree-1coZ32nd.js.map +1 -0
  102. package/package.json +6 -5
  103. package/src/index.ts +24 -0
  104. package/src/modes/broadcast.tsx +12 -1
  105. package/src/modes/control.tsx +10 -1
  106. package/src/modes/test.tsx +4 -1
  107. package/src/render/allowed-hosts.tsx +100 -0
  108. package/src/render/asset-resolve.ts +97 -0
  109. package/src/render/blend-mode.ts +50 -0
  110. package/src/render/bundle.ts +6 -1
  111. package/src/render/fill.tsx +266 -24
  112. package/src/render/headless.tsx +129 -0
  113. package/src/render/mask.tsx +389 -0
  114. package/src/render/primitives/frame.tsx +101 -5
  115. package/src/render/primitives/grid.tsx +4 -1
  116. package/src/render/primitives/image.tsx +17 -3
  117. package/src/render/primitives/index.ts +7 -0
  118. package/src/render/primitives/media.tsx +14 -3
  119. package/src/render/primitives/shape.tsx +39 -75
  120. package/src/render/primitives/stack.tsx +5 -1
  121. package/src/render/primitives/text.tsx +0 -1
  122. package/src/render/prop-allowlist.ts +25 -2
  123. package/src/render/shape-geometry.tsx +315 -0
  124. package/src/render/shape-index.tsx +90 -0
  125. package/src/render/tree.tsx +214 -12
  126. package/src/render/universal-wrapper.tsx +128 -21
  127. package/dist/broadcast-DO7jEkix.js +0 -11
  128. package/dist/broadcast-DO7jEkix.js.map +0 -1
  129. package/dist/control-BSfl4_cO.js +0 -16
  130. package/dist/control-BSfl4_cO.js.map +0 -1
  131. package/dist/index-Crkij3C4.js.map +0 -1
  132. package/dist/tree-DBj9SJgs.js +0 -1230
  133. package/dist/tree-DBj9SJgs.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumencast/runtime",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Browser runtime for Lumencast — mount(), LSDP/1 transport, leaf-grain store, LSML render, animations, overlays.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -36,7 +36,7 @@
36
36
  "framer-motion": "^12.0.0",
37
37
  "react": "^19.0.0",
38
38
  "react-dom": "^19.0.0",
39
- "@lumencast/protocol": "0.6.0"
39
+ "@lumencast/protocol": "0.8.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@playwright/test": "^1.49.1",
@@ -50,8 +50,8 @@
50
50
  "vite-plugin-dts": "^4.5.0",
51
51
  "vitest": "^4.1.5",
52
52
  "ws": "^8.18.0",
53
- "@lumencast/dev-server": "0.6.0",
54
- "@lumencast/server": "0.6.0"
53
+ "@lumencast/dev-server": "0.8.0",
54
+ "@lumencast/server": "0.8.0"
55
55
  },
56
56
  "scripts": {
57
57
  "dev": "vite",
@@ -60,6 +60,7 @@
60
60
  "test": "vitest run",
61
61
  "test:watch": "vitest",
62
62
  "test:e2e": "playwright test",
63
- "check:bundle": "node scripts/check-bundle-size.mjs"
63
+ "check:bundle": "node scripts/check-bundle-size.mjs",
64
+ "check:no-fetch": "node scripts/check-no-fetch.mjs"
64
65
  }
65
66
  }
package/src/index.ts CHANGED
@@ -46,3 +46,27 @@ export {
46
46
  isAuthoringProfile,
47
47
  validateBundleProfiles,
48
48
  } from "./render/bundle.js";
49
+
50
+ // Headless render (ADR 003) — render an already-compiled RenderBundle into a
51
+ // live DOM node, no WS, ready when layout + fonts settle. Hosts (Solar headless
52
+ // entry, ZabCanvas render worker, the zero-loss harness) screenshot the target
53
+ // once `ready` resolves. The runtime does DOM + readiness only — no fetch, no
54
+ // screenshot. Dynamically pulls BroadcastMode so it adds no eager weight to the
55
+ // `mount`/broadcast path (RC6).
56
+ export { renderBundleHeadless } from "./render/headless.js";
57
+ export type { HeadlessRenderOptions, HeadlessRenderHandle } from "./render/headless.js";
58
+
59
+ // Asset / font resolution helpers for headless hosts (ADR 003 §3.2). No-fetch:
60
+ // they only rewrite a bundle's `src`s against a caller table and load fonts
61
+ // from caller-supplied `data:` URIs. The host-allow gate stays the sole
62
+ // authority. (`FontFace` is the public type name per ADR 003 RC5; it is the
63
+ // spec object — distinct from the DOM `FontFace` constructor.)
64
+ export {
65
+ resolveSrc,
66
+ rewriteLayoutSrcs,
67
+ rewriteDefaultsSrcs,
68
+ injectFonts,
69
+ type AssetTable,
70
+ type FontFaceSpec,
71
+ type FontFaceSpec as FontFace,
72
+ } from "./render/asset-resolve.js";
@@ -1,8 +1,19 @@
1
+ import { useMemo } from "react";
1
2
  import { Tree } from "../render/tree";
3
+ import { AllowedHostsProvider, readAllowedHosts } from "../render/allowed-hosts";
4
+ import { ShapeIndexProvider, buildShapeIndex } from "../render/shape-index";
2
5
  import { useLumencastRuntime } from "../overlay/runtime-context";
3
6
 
4
7
  /** Broadcast mode : pure scene render, no UI chrome. */
5
8
  export function BroadcastMode() {
6
9
  const { store, bundle } = useLumencastRuntime();
7
- return <Tree node={bundle.root} store={store} />;
10
+ // ADR 002 A2.1 (#K) — build the `id → shape` index once per bundle.
11
+ const shapeIndex = useMemo(() => buildShapeIndex(bundle.root), [bundle.root]);
12
+ return (
13
+ <AllowedHostsProvider hosts={readAllowedHosts(bundle)}>
14
+ <ShapeIndexProvider index={shapeIndex}>
15
+ <Tree node={bundle.root} store={store} />
16
+ </ShapeIndexProvider>
17
+ </AllowedHostsProvider>
18
+ );
8
19
  }
@@ -1,4 +1,7 @@
1
+ import { useMemo } from "react";
1
2
  import { Tree } from "../render/tree";
3
+ import { AllowedHostsProvider, readAllowedHosts } from "../render/allowed-hosts";
4
+ import { ShapeIndexProvider, buildShapeIndex } from "../render/shape-index";
2
5
  import { ControlPanel } from "../overlay/control";
3
6
  import { StatusPill } from "../overlay/status-pill";
4
7
  import { useLumencastRuntime } from "../overlay/runtime-context";
@@ -7,9 +10,15 @@ import { useLumencastRuntime } from "../overlay/runtime-context";
7
10
  * panel from operator_inputs). */
8
11
  export function ControlMode() {
9
12
  const { store, bundle } = useLumencastRuntime();
13
+ // ADR 002 A2.1 (#K) — build the `id → shape` index once per bundle.
14
+ const shapeIndex = useMemo(() => buildShapeIndex(bundle.root), [bundle.root]);
10
15
  return (
11
16
  <>
12
- <Tree node={bundle.root} store={store} />
17
+ <AllowedHostsProvider hosts={readAllowedHosts(bundle)}>
18
+ <ShapeIndexProvider index={shapeIndex}>
19
+ <Tree node={bundle.root} store={store} />
20
+ </ShapeIndexProvider>
21
+ </AllowedHostsProvider>
13
22
  <StatusPill />
14
23
  <ControlPanel />
15
24
  </>
@@ -1,4 +1,5 @@
1
1
  import { Tree } from "../render/tree";
2
+ import { AllowedHostsProvider, readAllowedHosts } from "../render/allowed-hosts";
2
3
  import { ControlPanel } from "../overlay/control";
3
4
  import { TestPanel } from "../overlay/test";
4
5
  import { StatusPill } from "../overlay/status-pill";
@@ -10,7 +11,9 @@ export function TestMode() {
10
11
  const { store, bundle } = useLumencastRuntime();
11
12
  return (
12
13
  <>
13
- <Tree node={bundle.root} store={store} />
14
+ <AllowedHostsProvider hosts={readAllowedHosts(bundle)}>
15
+ <Tree node={bundle.root} store={store} />
16
+ </AllowedHostsProvider>
14
17
  <StatusPill />
15
18
  <ControlPanel />
16
19
  <TestPanel />
@@ -0,0 +1,100 @@
1
+ // Render-side host-allowlist context (LSML 1.2, ADR 002 #E + #F ; Bastion T1/T2).
2
+ //
3
+ // CANONICAL host-gate module for the runtime. There is exactly ONE
4
+ // `AllowedHostsProvider` and ONE allowlist context for the whole render
5
+ // tree. The bundle's `assets.allowedHosts` rides this context from the
6
+ // render root down to every consumer that places an untrusted asset URL
7
+ // into the DOM :
8
+ //
9
+ // - image-fill `src` on frame / shape backgrounds (#F, via `gateImageFills`
10
+ // / `gateSrc` in `fill.tsx`),
11
+ // - the `<img src>` of the `image` primitive (#F — closes the latent 1.1
12
+ // hole where `image.tsx` placed `src` with no host check at all),
13
+ // - a `mask.source`-image `href` (#E, via `checkHostAllowed` in
14
+ // `mask.tsx`, which reads the allowlist off this same context through
15
+ // `tree.tsx`).
16
+ //
17
+ // The underlying decision is ALWAYS delegated to `@lumencast/protocol`'s
18
+ // `checkHostAllowed` / `isHostAllowed` (the #C foundation, single source of
19
+ // truth for host + scheme matching). This module never re-implements that
20
+ // logic ; it only threads the allowlist and adapts it for each call-site.
21
+ //
22
+ // Deny-by-default : a consumer rendered outside any provider, or one whose
23
+ // bundle declares no `allowedHosts`, sees `undefined` — which
24
+ // `checkHostAllowed` treats as "no allowlist → reject every remote host".
25
+ // There is no path by which a missing provider silently re-opens the gate.
26
+ //
27
+ // The context value is a read-only, mount-stable `string[] | undefined`
28
+ // (the allowlist is part of the content-addressed bundle), so a plain React
29
+ // context is the right tool.
30
+
31
+ import { createContext, useContext, type ReactNode } from "react";
32
+ import { checkHostAllowed } from "@lumencast/protocol";
33
+ import { emitDiagnostic } from "./diagnostics";
34
+
35
+ const AllowedHostsCtx = createContext<readonly string[] | undefined>(undefined);
36
+
37
+ /**
38
+ * Provide the bundle's host allowlist to the render subtree. Mounted ONCE at
39
+ * the render root by each mode (broadcast / control / test), wrapping
40
+ * `<Tree>`. The value should come from {@link readAllowedHosts} so the
41
+ * legacy `Asset[]` and the LSML 1.2 object `assets` shapes are both handled
42
+ * and non-string entries are dropped.
43
+ *
44
+ * Prop name is `hosts` (the canonical render-side spelling, #F).
45
+ */
46
+ export function AllowedHostsProvider({
47
+ hosts,
48
+ children,
49
+ }: {
50
+ hosts: readonly string[] | undefined;
51
+ children: ReactNode;
52
+ }) {
53
+ return <AllowedHostsCtx.Provider value={hosts}>{children}</AllowedHostsCtx.Provider>;
54
+ }
55
+
56
+ /** Read the active host allowlist. `undefined` when no provider is mounted —
57
+ * which `checkHostAllowed` treats as deny-by-default (never a passthrough). */
58
+ export function useAllowedHosts(): readonly string[] | undefined {
59
+ return useContext(AllowedHostsCtx);
60
+ }
61
+
62
+ /** Read `assets.allowedHosts` defensively off a render bundle, for the mode
63
+ * that provides the context. The LSML 1.2 compiler forwards `assets` in the
64
+ * object form `{ allowedHosts?: string[] }` (see `RenderBundle.assets`) ;
65
+ * a legacy bundle may still carry the old `Asset[]` form. Either way we
66
+ * extract a `string[]` of hostnames, or `undefined` (deny-by-default). A
67
+ * non-string entry is dropped — it can never match `new URL().hostname`. */
68
+ export function readAllowedHosts(bundle: { assets?: unknown }): readonly string[] | undefined {
69
+ const assets = bundle.assets as { allowedHosts?: unknown } | undefined;
70
+ const list = assets?.allowedHosts;
71
+ if (!Array.isArray(list)) return undefined;
72
+ const hosts = list.filter((h): h is string => typeof h === "string");
73
+ return hosts.length > 0 ? hosts : undefined;
74
+ }
75
+
76
+ /**
77
+ * Gate an asset `src` against the host + scheme allowlist BEFORE it reaches
78
+ * the DOM (Bastion T1/T2). Returns the `src` unchanged when allowed, or
79
+ * `undefined` when rejected — in which case the caller MUST omit the asset
80
+ * (never passthrough). On rejection an R9-clean diagnostic is emitted : it
81
+ * carries only `{ nodeId, field, reason }`, never the URL itself.
82
+ *
83
+ * The decision is delegated to `checkHostAllowed` and is deny-by-default :
84
+ * an absent / empty `allowedHosts` rejects every remote host.
85
+ * `undefined`/non-string/empty `src` resolves to `undefined` (absent asset)
86
+ * WITHOUT a diagnostic — that is a primitive with no source, not a rejected
87
+ * one.
88
+ */
89
+ export function gateSrc(
90
+ src: unknown,
91
+ allowedHosts: readonly string[] | undefined,
92
+ field: string,
93
+ nodeId?: string,
94
+ ): string | undefined {
95
+ if (typeof src !== "string" || src.length === 0) return undefined;
96
+ const decision = checkHostAllowed(src, allowedHosts);
97
+ if (decision.allowed) return src;
98
+ emitDiagnostic(nodeId, field, decision.reason ?? "asset host/scheme rejected");
99
+ return undefined;
100
+ }
@@ -0,0 +1,97 @@
1
+ // Public asset + font resolution helpers for headless / host render (ADR 003 §3.2).
2
+ //
3
+ // These utilities let a HOST (Solar headless entry, a ZabCanvas render worker,
4
+ // the zero-loss harness) resolve a bundle's content-addressed asset references
5
+ // to concrete `data:` URIs and inject brand `@font-face`s BEFORE the first
6
+ // frame — exactly as the zero-loss harness has always done (ADR 002 #J). They
7
+ // are promoted here verbatim so every host resolves identically and exercises
8
+ // the SAME host-allow gate the runtime applies internally.
9
+ //
10
+ // ── No-fetch contract (ADR 003 §3.2, D3, Bastion R2) ────────────────────────
11
+ // NONE of these helpers performs a network fetch. They only rewrite a bundle's
12
+ // `src` values against a caller-supplied table (`resolveSrc` /
13
+ // `rewriteLayoutSrcs` / `rewriteDefaultsSrcs`) and load `@font-face`s from
14
+ // caller-supplied `data:`/same-document URLs (`injectFonts`). The runtime never
15
+ // reaches the network; the host owns where bytes come from. Both the input
16
+ // table values and the font `src`s are expected to be `data:` (or otherwise
17
+ // already host-allowed) URIs — substituting one already-admitted scheme for
18
+ // another, so the deny-by-default `allowedHosts` gate stays the sole authority.
19
+ // ─────────────────────────────────────────────────────────────────────────────
20
+
21
+ /** A table mapping a bundle `src` reference to its resolved `data:` URI.
22
+ * Keys may be either the full `assets/<hash>.ext` path or a bare `<hash>`. */
23
+ export type AssetTable = Readonly<Record<string, string>>;
24
+
25
+ /** Resolve a single `src` value against a table. Non-string or unmatched values
26
+ * pass through unchanged. Matches both `assets/<hash>.ext` and bare `<hash>`. */
27
+ export function resolveSrc(src: unknown, table: AssetTable): unknown {
28
+ if (typeof src !== "string") return src;
29
+ if (table[src]) return table[src];
30
+ const m = /^assets\/([A-Za-z0-9]+)\.[A-Za-z0-9]+$/.exec(src);
31
+ if (m && m[1] !== undefined && table[m[1]]) return table[m[1]];
32
+ return src;
33
+ }
34
+
35
+ /** Deep-rewrite every `src` (image-fill, mask source) in a layout subtree,
36
+ * in place, against the supplied asset table. */
37
+ export function rewriteLayoutSrcs(node: unknown, table: AssetTable): void {
38
+ if (node === null || typeof node !== "object") return;
39
+ if (Array.isArray(node)) {
40
+ for (const n of node) rewriteLayoutSrcs(n, table);
41
+ return;
42
+ }
43
+ const obj = node as Record<string, unknown>;
44
+ if ("src" in obj) obj["src"] = resolveSrc(obj["src"], table);
45
+ for (const v of Object.values(obj)) {
46
+ if (v && typeof v === "object") rewriteLayoutSrcs(v, table);
47
+ }
48
+ }
49
+
50
+ /** Rewrite the `__lit.image.*` defaults (image-primitive `bind.src` targets)
51
+ * against the asset table, returning a new defaults object. */
52
+ export function rewriteDefaultsSrcs(
53
+ defaults: Record<string, unknown>,
54
+ table: AssetTable,
55
+ ): Record<string, unknown> {
56
+ const out: Record<string, unknown> = { ...defaults };
57
+ for (const [k, v] of Object.entries(out)) {
58
+ if (k.startsWith("__lit.image.")) out[k] = resolveSrc(v, table);
59
+ }
60
+ return out;
61
+ }
62
+
63
+ /** A brand `@font-face` to inject before render. `src` is a `data:` URI (or any
64
+ * same-document URL) so no network/host-allow surface is involved. */
65
+ export interface FontFaceSpec {
66
+ family: string;
67
+ weight: number | string;
68
+ style?: string;
69
+ /** `url(data:font/woff2;base64,…)` content (the value inside `src:`). */
70
+ src: string;
71
+ }
72
+
73
+ /** Inject `@font-face` rules and block until the faces are loaded, so the very
74
+ * first painted frame already uses the brand glyphs (no fallback-font flash).
75
+ * Returns the families that successfully loaded. Loads only from the supplied
76
+ * `src` (expected `data:`); never fetches a remote host on its own behalf. */
77
+ export async function injectFonts(faces: readonly FontFaceSpec[]): Promise<string[]> {
78
+ const loaded: string[] = [];
79
+ for (const f of faces) {
80
+ try {
81
+ const face = new FontFace(f.family, f.src, {
82
+ weight: String(f.weight),
83
+ style: f.style ?? "normal",
84
+ });
85
+ await face.load();
86
+ (document as Document & { fonts: FontFaceSet }).fonts.add(face);
87
+ loaded.push(f.family);
88
+ } catch {
89
+ // A font that fails to load is a documented gap, not a render-breaker:
90
+ // the fallback glyphs paint and the host can detect the missing family
91
+ // in the returned list. Never throw (would abort an otherwise-good
92
+ // render) and never log the value (R9) — the absent family name is in
93
+ // `f.family`, which the caller already holds.
94
+ }
95
+ }
96
+ return loaded;
97
+ }
@@ -0,0 +1,50 @@
1
+ // Strict `mix-blend-mode` gate — the runtime half of the T4 double-gate
2
+ // (Bastion conditions 1.2, ADR 002 §3.2 / #D).
3
+ //
4
+ // The compiler already validates `blendMode` against its closed enum
5
+ // (`parseBlendMode`, @lumencast/compiler) before emitting the universal
6
+ // prop. This module is the INDEPENDENT runtime gate : a bundle prop OR a
7
+ // live LSDP delta value reaching the wrapper is re-validated here against
8
+ // the same closed allowlist before it may touch an inline CSS style.
9
+ // Anything outside the enum is omitted (never passthrough) — mirroring
10
+ // the `css-color.ts` discipline (self-contained second gate, no untrusted
11
+ // string ever interpolated into CSS).
12
+ //
13
+ // The allowlist is intentionally duplicated rather than imported from the
14
+ // compiler : the runtime does not depend on @lumencast/compiler, and the
15
+ // gate must hold even if a hand-rolled / tampered bundle bypasses the
16
+ // compiler entirely. It is a fixed, finite set of CSS keywords (Figma
17
+ // blend modes minus PASS_THROUGH) — the single source of truth for the
18
+ // CSS value is this closed set.
19
+
20
+ /** Closed `mix-blend-mode` allowlist (ADR 002 §3.2 — Figma minus
21
+ * `PASS_THROUGH`). Mirrors the compiler's `BLEND_MODES`. */
22
+ const BLEND_MODES: ReadonlySet<string> = new Set([
23
+ "normal",
24
+ "multiply",
25
+ "screen",
26
+ "overlay",
27
+ "darken",
28
+ "lighten",
29
+ "color-dodge",
30
+ "color-burn",
31
+ "hard-light",
32
+ "soft-light",
33
+ "difference",
34
+ "exclusion",
35
+ "hue",
36
+ "saturation",
37
+ "color",
38
+ "luminosity",
39
+ // Figma LINEAR_DODGE (add) — exact additive blend, gentler than color-dodge.
40
+ "plus-lighter",
41
+ ]);
42
+
43
+ /**
44
+ * Re-validate a resolved `blendMode` against the closed enum. Returns the
45
+ * CSS `mix-blend-mode` keyword when recognised, else `undefined` (caller
46
+ * omits — the value never reaches the style). Never passthrough.
47
+ */
48
+ export function parseBlendMode(value: unknown): string | undefined {
49
+ return typeof value === "string" && BLEND_MODES.has(value) ? value : undefined;
50
+ }
@@ -111,7 +111,12 @@ export interface RenderBundle {
111
111
  root: RenderNode;
112
112
  operator_inputs?: OperatorInput[];
113
113
  external_adapters?: ExternalAdapter[];
114
- assets?: Asset[];
114
+ /** Bundle-level asset declarations. `allowedHosts` is the host
115
+ * allowlist (LSML 1.2 §3.2, Bastion T1) every image / image-fill `src`
116
+ * is gated against at render BEFORE reaching the DOM. Absent / empty =
117
+ * deny every remote host (deny-by-default). Other keys (`fonts`,
118
+ * `preload`) are forwarded opaquely by the compiler. */
119
+ assets?: { allowedHosts?: string[]; [key: string]: unknown };
115
120
  /** LSML 1.1 §17.3 — capability profiles required for correct rendering.
116
121
  * Each entry is an `x-<vendor>.<name>/<version>` string. The runtime
117
122
  * checks every behavioural entry against its supported list ; an