@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.
Files changed (123) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/broadcast-DUYqvcgo.js +12 -0
  3. package/dist/broadcast-DUYqvcgo.js.map +1 -0
  4. package/dist/control-CL8TWXaE.js +17 -0
  5. package/dist/control-CL8TWXaE.js.map +1 -0
  6. package/dist/{index-CyOlpZAL.js → index-C6viWFcT.js} +216 -182
  7. package/dist/index-C6viWFcT.js.map +1 -0
  8. package/dist/index.html +1 -1
  9. package/dist/lumencast.js +1 -1
  10. package/dist/modes/broadcast.d.ts.map +1 -1
  11. package/dist/modes/broadcast.js +6 -1
  12. package/dist/modes/broadcast.js.map +1 -1
  13. package/dist/modes/control.d.ts.map +1 -1
  14. package/dist/modes/control.js +6 -1
  15. package/dist/modes/control.js.map +1 -1
  16. package/dist/modes/test.d.ts.map +1 -1
  17. package/dist/modes/test.js +2 -1
  18. package/dist/modes/test.js.map +1 -1
  19. package/dist/mount.d.ts.map +1 -1
  20. package/dist/mount.js +5 -0
  21. package/dist/mount.js.map +1 -1
  22. package/dist/render/allowed-hosts.d.ts +41 -0
  23. package/dist/render/allowed-hosts.d.ts.map +1 -0
  24. package/dist/render/allowed-hosts.js +88 -0
  25. package/dist/render/allowed-hosts.js.map +1 -0
  26. package/dist/render/blend-mode.d.ts +7 -0
  27. package/dist/render/blend-mode.d.ts.map +1 -0
  28. package/dist/render/blend-mode.js +49 -0
  29. package/dist/render/blend-mode.js.map +1 -0
  30. package/dist/render/bundle.d.ts +17 -1
  31. package/dist/render/bundle.d.ts.map +1 -1
  32. package/dist/render/bundle.js +15 -1
  33. package/dist/render/bundle.js.map +1 -1
  34. package/dist/render/fill.d.ts +36 -3
  35. package/dist/render/fill.d.ts.map +1 -1
  36. package/dist/render/fill.js +222 -23
  37. package/dist/render/fill.js.map +1 -1
  38. package/dist/render/mask.d.ts +87 -0
  39. package/dist/render/mask.d.ts.map +1 -0
  40. package/dist/render/mask.js +243 -0
  41. package/dist/render/mask.js.map +1 -0
  42. package/dist/render/primitives/frame.d.ts.map +1 -1
  43. package/dist/render/primitives/frame.js +91 -5
  44. package/dist/render/primitives/frame.js.map +1 -1
  45. package/dist/render/primitives/grid.d.ts +1 -1
  46. package/dist/render/primitives/grid.d.ts.map +1 -1
  47. package/dist/render/primitives/grid.js +4 -1
  48. package/dist/render/primitives/grid.js.map +1 -1
  49. package/dist/render/primitives/image.d.ts +8 -1
  50. package/dist/render/primitives/image.d.ts.map +1 -1
  51. package/dist/render/primitives/image.js +17 -3
  52. package/dist/render/primitives/image.js.map +1 -1
  53. package/dist/render/primitives/index.d.ts +7 -0
  54. package/dist/render/primitives/index.d.ts.map +1 -1
  55. package/dist/render/primitives/index.js.map +1 -1
  56. package/dist/render/primitives/shape.d.ts.map +1 -1
  57. package/dist/render/primitives/shape.js +29 -26
  58. package/dist/render/primitives/shape.js.map +1 -1
  59. package/dist/render/primitives/stack.d.ts +1 -1
  60. package/dist/render/primitives/stack.d.ts.map +1 -1
  61. package/dist/render/primitives/stack.js +5 -1
  62. package/dist/render/primitives/stack.js.map +1 -1
  63. package/dist/render/primitives/text.d.ts.map +1 -1
  64. package/dist/render/primitives/text.js +0 -1
  65. package/dist/render/primitives/text.js.map +1 -1
  66. package/dist/render/prop-allowlist.d.ts.map +1 -1
  67. package/dist/render/prop-allowlist.js +25 -2
  68. package/dist/render/prop-allowlist.js.map +1 -1
  69. package/dist/render/shape-geometry.d.ts +81 -0
  70. package/dist/render/shape-geometry.d.ts.map +1 -0
  71. package/dist/render/shape-geometry.js +199 -0
  72. package/dist/render/shape-geometry.js.map +1 -0
  73. package/dist/render/shape-index.d.ts +28 -0
  74. package/dist/render/shape-index.d.ts.map +1 -0
  75. package/dist/render/shape-index.js +77 -0
  76. package/dist/render/shape-index.js.map +1 -0
  77. package/dist/render/tree.d.ts.map +1 -1
  78. package/dist/render/tree.js +175 -3
  79. package/dist/render/tree.js.map +1 -1
  80. package/dist/render/universal-wrapper.d.ts +27 -1
  81. package/dist/render/universal-wrapper.d.ts.map +1 -1
  82. package/dist/render/universal-wrapper.js +98 -22
  83. package/dist/render/universal-wrapper.js.map +1 -1
  84. package/dist/{status-pill-DIpXc5du.js → status-pill-jJT54n07.js} +2 -2
  85. package/dist/{status-pill-DIpXc5du.js.map → status-pill-jJT54n07.js.map} +1 -1
  86. package/dist/{test-ByRec1kd.js → test-84XodL1c.js} +51 -51
  87. package/dist/{test-ByRec1kd.js.map → test-84XodL1c.js.map} +1 -1
  88. package/dist/transport/ws.d.ts +5 -0
  89. package/dist/transport/ws.d.ts.map +1 -1
  90. package/dist/transport/ws.js +7 -0
  91. package/dist/transport/ws.js.map +1 -1
  92. package/dist/tree-BIimahCf.js +1777 -0
  93. package/dist/tree-BIimahCf.js.map +1 -0
  94. package/package.json +4 -4
  95. package/src/modes/broadcast.tsx +12 -1
  96. package/src/modes/control.tsx +10 -1
  97. package/src/modes/test.tsx +4 -1
  98. package/src/mount.ts +5 -0
  99. package/src/render/allowed-hosts.tsx +100 -0
  100. package/src/render/blend-mode.ts +50 -0
  101. package/src/render/bundle.ts +28 -2
  102. package/src/render/fill.tsx +266 -24
  103. package/src/render/mask.tsx +389 -0
  104. package/src/render/primitives/frame.tsx +101 -5
  105. package/src/render/primitives/grid.tsx +4 -1
  106. package/src/render/primitives/image.tsx +17 -3
  107. package/src/render/primitives/index.ts +7 -0
  108. package/src/render/primitives/shape.tsx +39 -75
  109. package/src/render/primitives/stack.tsx +5 -1
  110. package/src/render/primitives/text.tsx +0 -1
  111. package/src/render/prop-allowlist.ts +25 -2
  112. package/src/render/shape-geometry.tsx +315 -0
  113. package/src/render/shape-index.tsx +90 -0
  114. package/src/render/tree.tsx +214 -12
  115. package/src/render/universal-wrapper.tsx +128 -21
  116. package/src/transport/ws.ts +8 -0
  117. package/dist/broadcast-3vYij4k-.js +0 -11
  118. package/dist/broadcast-3vYij4k-.js.map +0 -1
  119. package/dist/control-BFNkY7-6.js +0 -16
  120. package/dist/control-BFNkY7-6.js.map +0 -1
  121. package/dist/index-CyOlpZAL.js.map +0 -1
  122. package/dist/tree-D5wYHpPu.js +0 -1230
  123. 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.5.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.5.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.5.0",
54
- "@lumencast/server": "0.5.0"
53
+ "@lumencast/dev-server": "0.7.0",
54
+ "@lumencast/server": "0.7.0"
55
55
  },
56
56
  "scripts": {
57
57
  "dev": "vite",
@@ -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 />
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
+ }
@@ -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
@@ -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 response = await this.fetchImpl(url);
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
  }