@lumencast/runtime 0.5.0 → 0.7.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-DUYqvcgo.js +12 -0
- package/dist/broadcast-DUYqvcgo.js.map +1 -0
- package/dist/control-CL8TWXaE.js +17 -0
- package/dist/control-CL8TWXaE.js.map +1 -0
- package/dist/{index-CyOlpZAL.js → index-C6viWFcT.js} +216 -182
- package/dist/index-C6viWFcT.js.map +1 -0
- package/dist/index.html +1 -1
- package/dist/lumencast.js +1 -1
- 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/mount.d.ts.map +1 -1
- package/dist/mount.js +5 -0
- package/dist/mount.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/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 +17 -1
- package/dist/render/bundle.d.ts.map +1 -1
- package/dist/render/bundle.js +15 -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/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/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-DIpXc5du.js → status-pill-jJT54n07.js} +2 -2
- package/dist/{status-pill-DIpXc5du.js.map → status-pill-jJT54n07.js.map} +1 -1
- package/dist/{test-ByRec1kd.js → test-84XodL1c.js} +51 -51
- package/dist/{test-ByRec1kd.js.map → test-84XodL1c.js.map} +1 -1
- package/dist/transport/ws.d.ts +5 -0
- package/dist/transport/ws.d.ts.map +1 -1
- package/dist/transport/ws.js +7 -0
- package/dist/transport/ws.js.map +1 -1
- package/dist/tree-BIimahCf.js +1777 -0
- package/dist/tree-BIimahCf.js.map +1 -0
- package/package.json +4 -4
- package/src/modes/broadcast.tsx +12 -1
- package/src/modes/control.tsx +10 -1
- package/src/modes/test.tsx +4 -1
- package/src/mount.ts +5 -0
- package/src/render/allowed-hosts.tsx +100 -0
- package/src/render/blend-mode.ts +50 -0
- package/src/render/bundle.ts +28 -2
- package/src/render/fill.tsx +266 -24
- 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/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/src/transport/ws.ts +8 -0
- package/dist/broadcast-3vYij4k-.js +0 -11
- package/dist/broadcast-3vYij4k-.js.map +0 -1
- package/dist/control-BFNkY7-6.js +0 -16
- package/dist/control-BFNkY7-6.js.map +0 -1
- package/dist/index-CyOlpZAL.js.map +0 -1
- package/dist/tree-D5wYHpPu.js +0 -1230
- package/dist/tree-D5wYHpPu.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.7.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.7.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.7.0",
|
|
54
|
+
"@lumencast/server": "0.7.0"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"dev": "vite",
|
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 />
|
package/src/mount.ts
CHANGED
|
@@ -20,9 +20,14 @@ export function mount(options: MountOptions): LumencastHandle {
|
|
|
20
20
|
|
|
21
21
|
const store = createStore();
|
|
22
22
|
const baseUrl = deriveBaseUrl(options.serverUrl);
|
|
23
|
+
// The render-bundle endpoint is auth-gated like the LSDP/1 WS. Resolve the
|
|
24
|
+
// current session token per fetch (mirrors `setToken`) so each bundle GET
|
|
25
|
+
// carries `Authorization: Bearer <token>`. `ws` is assigned below; the
|
|
26
|
+
// closure runs only at fetch time, after assignment.
|
|
23
27
|
const bundleFetcher = createBundleFetcher({
|
|
24
28
|
baseUrl,
|
|
25
29
|
...(options.resolveBundleUrl !== undefined ? { resolveUrl: options.resolveBundleUrl } : {}),
|
|
30
|
+
getAuthToken: () => ws.resolveCurrentToken(),
|
|
26
31
|
});
|
|
27
32
|
|
|
28
33
|
const bundleSignal = signal<RenderBundle | null>(null);
|
|
@@ -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,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
|
|
@@ -251,6 +256,14 @@ export interface BundleFetcherOptions {
|
|
|
251
256
|
* Lets a gateway-prefixed server be addressed without changing the
|
|
252
257
|
* host-root default. */
|
|
253
258
|
resolveUrl?: BundleUrlResolver;
|
|
259
|
+
/** Resolve the bearer token used to authenticate each bundle GET. The
|
|
260
|
+
* render-bundle endpoint is auth-gated identically to the LSDP/1 WS
|
|
261
|
+
* subscription, so the fetch carries the same session token as
|
|
262
|
+
* `Authorization: Bearer <token>`. Resolved per request so a token swap
|
|
263
|
+
* (`setToken`) takes effect on the next fetch ; a `LumencastTokenProvider`
|
|
264
|
+
* is awaited. When omitted — or when it resolves to an empty/undefined
|
|
265
|
+
* value — no `Authorization` header is sent (v0.5.0 behaviour). */
|
|
266
|
+
getAuthToken?: () => string | undefined | Promise<string | undefined>;
|
|
254
267
|
fetchImpl?: typeof fetch;
|
|
255
268
|
}
|
|
256
269
|
|
|
@@ -259,15 +272,27 @@ class FetcherImpl implements BundleFetcher {
|
|
|
259
272
|
private readonly baseUrl: string;
|
|
260
273
|
private readonly pathPrefix: string;
|
|
261
274
|
private readonly resolveUrl: BundleUrlResolver | undefined;
|
|
275
|
+
private readonly getAuthToken: BundleFetcherOptions["getAuthToken"];
|
|
262
276
|
private readonly fetchImpl: typeof fetch;
|
|
263
277
|
|
|
264
278
|
constructor(opts: BundleFetcherOptions) {
|
|
265
279
|
this.baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
266
280
|
this.pathPrefix = (opts.pathPrefix ?? "/lsdp/v1/scenes").replace(/\/$/, "");
|
|
267
281
|
this.resolveUrl = opts.resolveUrl;
|
|
282
|
+
this.getAuthToken = opts.getAuthToken;
|
|
268
283
|
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
269
284
|
}
|
|
270
285
|
|
|
286
|
+
/** Build the request init carrying the bearer token, if any. Returns
|
|
287
|
+
* `undefined` when no token is available — the fetch stays header-less,
|
|
288
|
+
* preserving v0.5.0 behaviour. */
|
|
289
|
+
private async buildInit(): Promise<RequestInit | undefined> {
|
|
290
|
+
if (!this.getAuthToken) return undefined;
|
|
291
|
+
const token = await this.getAuthToken();
|
|
292
|
+
if (!token) return undefined;
|
|
293
|
+
return { headers: { Authorization: `Bearer ${token}` } };
|
|
294
|
+
}
|
|
295
|
+
|
|
271
296
|
private buildUrl(sceneId: string, sceneVersion: string): string {
|
|
272
297
|
if (this.resolveUrl) {
|
|
273
298
|
return this.resolveUrl(sceneId, sceneVersion);
|
|
@@ -287,7 +312,8 @@ class FetcherImpl implements BundleFetcher {
|
|
|
287
312
|
const cached = this.cache.get(sceneVersion);
|
|
288
313
|
if (cached) return cached;
|
|
289
314
|
const url = this.buildUrl(sceneId, sceneVersion);
|
|
290
|
-
const
|
|
315
|
+
const init = await this.buildInit();
|
|
316
|
+
const response = init ? await this.fetchImpl(url, init) : await this.fetchImpl(url);
|
|
291
317
|
if (!response.ok) {
|
|
292
318
|
throw new Error(`bundle fetch failed: ${response.status} ${response.statusText}`);
|
|
293
319
|
}
|