@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/broadcast-Gcd-dmC7.js +12 -0
- package/dist/broadcast-Gcd-dmC7.js.map +1 -0
- package/dist/control-C5TfClga.js +17 -0
- package/dist/control-C5TfClga.js.map +1 -0
- package/dist/{index-Crkij3C4.js → index-N-VqrIxN.js} +305 -210
- package/dist/index-N-VqrIxN.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.html +1 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/lumencast.js +14 -9
- package/dist/modes/broadcast.d.ts.map +1 -1
- package/dist/modes/broadcast.js +6 -1
- package/dist/modes/broadcast.js.map +1 -1
- package/dist/modes/control.d.ts.map +1 -1
- package/dist/modes/control.js +6 -1
- package/dist/modes/control.js.map +1 -1
- package/dist/modes/test.d.ts.map +1 -1
- package/dist/modes/test.js +2 -1
- package/dist/modes/test.js.map +1 -1
- package/dist/render/allowed-hosts.d.ts +41 -0
- package/dist/render/allowed-hosts.d.ts.map +1 -0
- package/dist/render/allowed-hosts.js +88 -0
- package/dist/render/allowed-hosts.js.map +1 -0
- package/dist/render/asset-resolve.d.ts +27 -0
- package/dist/render/asset-resolve.d.ts.map +1 -0
- package/dist/render/asset-resolve.js +86 -0
- package/dist/render/asset-resolve.js.map +1 -0
- package/dist/render/blend-mode.d.ts +7 -0
- package/dist/render/blend-mode.d.ts.map +1 -0
- package/dist/render/blend-mode.js +49 -0
- package/dist/render/blend-mode.js.map +1 -0
- package/dist/render/bundle.d.ts +9 -1
- package/dist/render/bundle.d.ts.map +1 -1
- package/dist/render/bundle.js.map +1 -1
- package/dist/render/fill.d.ts +36 -3
- package/dist/render/fill.d.ts.map +1 -1
- package/dist/render/fill.js +222 -23
- package/dist/render/fill.js.map +1 -1
- package/dist/render/headless.d.ts +39 -0
- package/dist/render/headless.d.ts.map +1 -0
- package/dist/render/headless.js +83 -0
- package/dist/render/headless.js.map +1 -0
- package/dist/render/mask.d.ts +87 -0
- package/dist/render/mask.d.ts.map +1 -0
- package/dist/render/mask.js +243 -0
- package/dist/render/mask.js.map +1 -0
- package/dist/render/primitives/frame.d.ts.map +1 -1
- package/dist/render/primitives/frame.js +91 -5
- package/dist/render/primitives/frame.js.map +1 -1
- package/dist/render/primitives/grid.d.ts +1 -1
- package/dist/render/primitives/grid.d.ts.map +1 -1
- package/dist/render/primitives/grid.js +4 -1
- package/dist/render/primitives/grid.js.map +1 -1
- package/dist/render/primitives/image.d.ts +8 -1
- package/dist/render/primitives/image.d.ts.map +1 -1
- package/dist/render/primitives/image.js +17 -3
- package/dist/render/primitives/image.js.map +1 -1
- package/dist/render/primitives/index.d.ts +7 -0
- package/dist/render/primitives/index.d.ts.map +1 -1
- package/dist/render/primitives/index.js.map +1 -1
- package/dist/render/primitives/media.d.ts +11 -2
- package/dist/render/primitives/media.d.ts.map +1 -1
- package/dist/render/primitives/media.js +14 -3
- package/dist/render/primitives/media.js.map +1 -1
- package/dist/render/primitives/shape.d.ts.map +1 -1
- package/dist/render/primitives/shape.js +29 -26
- package/dist/render/primitives/shape.js.map +1 -1
- package/dist/render/primitives/stack.d.ts +1 -1
- package/dist/render/primitives/stack.d.ts.map +1 -1
- package/dist/render/primitives/stack.js +5 -1
- package/dist/render/primitives/stack.js.map +1 -1
- package/dist/render/primitives/text.d.ts.map +1 -1
- package/dist/render/primitives/text.js +0 -1
- package/dist/render/primitives/text.js.map +1 -1
- package/dist/render/prop-allowlist.d.ts.map +1 -1
- package/dist/render/prop-allowlist.js +25 -2
- package/dist/render/prop-allowlist.js.map +1 -1
- package/dist/render/shape-geometry.d.ts +81 -0
- package/dist/render/shape-geometry.d.ts.map +1 -0
- package/dist/render/shape-geometry.js +199 -0
- package/dist/render/shape-geometry.js.map +1 -0
- package/dist/render/shape-index.d.ts +28 -0
- package/dist/render/shape-index.d.ts.map +1 -0
- package/dist/render/shape-index.js +77 -0
- package/dist/render/shape-index.js.map +1 -0
- package/dist/render/tree.d.ts.map +1 -1
- package/dist/render/tree.js +175 -3
- package/dist/render/tree.js.map +1 -1
- package/dist/render/universal-wrapper.d.ts +27 -1
- package/dist/render/universal-wrapper.d.ts.map +1 -1
- package/dist/render/universal-wrapper.js +98 -22
- package/dist/render/universal-wrapper.js.map +1 -1
- package/dist/{status-pill-BT5b-yET.js → status-pill-BaLQoIDl.js} +2 -2
- package/dist/{status-pill-BT5b-yET.js.map → status-pill-BaLQoIDl.js.map} +1 -1
- package/dist/{test-_hh1JvAd.js → test-CA30C2By.js} +51 -51
- package/dist/{test-_hh1JvAd.js.map → test-CA30C2By.js.map} +1 -1
- package/dist/tree-1coZ32nd.js +1777 -0
- package/dist/tree-1coZ32nd.js.map +1 -0
- package/package.json +6 -5
- package/src/index.ts +24 -0
- package/src/modes/broadcast.tsx +12 -1
- package/src/modes/control.tsx +10 -1
- package/src/modes/test.tsx +4 -1
- package/src/render/allowed-hosts.tsx +100 -0
- package/src/render/asset-resolve.ts +97 -0
- package/src/render/blend-mode.ts +50 -0
- package/src/render/bundle.ts +6 -1
- package/src/render/fill.tsx +266 -24
- package/src/render/headless.tsx +129 -0
- package/src/render/mask.tsx +389 -0
- package/src/render/primitives/frame.tsx +101 -5
- package/src/render/primitives/grid.tsx +4 -1
- package/src/render/primitives/image.tsx +17 -3
- package/src/render/primitives/index.ts +7 -0
- package/src/render/primitives/media.tsx +14 -3
- package/src/render/primitives/shape.tsx +39 -75
- package/src/render/primitives/stack.tsx +5 -1
- package/src/render/primitives/text.tsx +0 -1
- package/src/render/prop-allowlist.ts +25 -2
- package/src/render/shape-geometry.tsx +315 -0
- package/src/render/shape-index.tsx +90 -0
- package/src/render/tree.tsx +214 -12
- package/src/render/universal-wrapper.tsx +128 -21
- package/dist/broadcast-DO7jEkix.js +0 -11
- package/dist/broadcast-DO7jEkix.js.map +0 -1
- package/dist/control-BSfl4_cO.js +0 -16
- package/dist/control-BSfl4_cO.js.map +0 -1
- package/dist/index-Crkij3C4.js.map +0 -1
- package/dist/tree-DBj9SJgs.js +0 -1230
- 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.
|
|
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.
|
|
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.
|
|
54
|
-
"@lumencast/server": "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";
|
package/src/modes/broadcast.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/modes/control.tsx
CHANGED
|
@@ -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
|
-
<
|
|
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
|
</>
|
package/src/modes/test.tsx
CHANGED
|
@@ -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
|
-
<
|
|
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
|
+
}
|
package/src/render/bundle.ts
CHANGED
|
@@ -111,7 +111,12 @@ export interface RenderBundle {
|
|
|
111
111
|
root: RenderNode;
|
|
112
112
|
operator_inputs?: OperatorInput[];
|
|
113
113
|
external_adapters?: ExternalAdapter[];
|
|
114
|
-
|
|
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
|