Skip to content

feat(embed): package an opaque bootstrap for bundle sources - #54

Merged
francoischalifour merged 4 commits into
mainfrom
fc/opaque-bootstrap
Oct 9, 2026
Merged

francoischalifour merged 4 commits into
mainfrom
fc/opaque-bootstrap

Conversation

@francoischalifour

@francoischalifour francoischalifour commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Context

The frontend integration for rendering saved data apps in Claude exposed a restriction in Claude’s MCP proxy: it permits data: and blob: child frames but blocks HTTPS child frames, even when the runtime domain is declared in frameDomains.

The SDK currently requires bundle sources to provide an HTTP(S) bootstrapUrl. Its source-mode bridge therefore cannot load the opaque bootstrap document that this integration needs.

The frontend works around that restriction by importing the SDK Worker, obtaining its runtime HTML, inserting a UUID compatibility script, and encoding the result as a data:text/html URL. It then uses connection mode and separately manages the iframe source, random token, initialization timeout, cleanup, and presentation updates.

This duplicates SDK-owned behavior in the host and makes the frontend depend on the structure of the generated bootstrap HTML.

A second issue appears inside opaque data documents: crypto.randomUUID() can be unavailable while crypto.getRandomValues() remains available. Direct UUID calls can consequently prevent the bootstrap from starting or fail later during app requests.

Proposed solution

Make bootstrapUrl optional on the existing bundle source:

attachDataAppBridge({
  iframe,
  source: { type: 'bundle', javascript },
  presentation,
  onMessage: router.dispatch,
  onStatusChange,
});

When bootstrapUrl is omitted, the SDK generates a data: document containing its packaged bootstrap and the exact host origin. When an HTTP(S) URL is supplied, the existing external-bootstrap behavior remains available.

This keeps bootstrap creation within source mode, which already owns iframe loading, sandbox policy, token rotation, initialization timeout, and disposal. It requires no additional public HTML generator or bootstrap lifecycle API.

The Worker’s runtime HTML generation and CSP move into a shared private module. The build compiles the bootstrap once and embeds the same classic script into both the browser SDK and Worker artifacts. The local document needs no runtime script download.

Secure UUID generation

Introduce a private UUID helper that uses native crypto.randomUUID() when available and otherwise generates a UUID v4 from crypto.getRandomValues().

Use it throughout the browser SDK wherever opaque execution can reach identifier generation: bootstrap and document identifiers, transport requests, host tokens and sessions, browser operations, SQL requests, React hook scopes, and annotations.

The helper does not modify the global crypto object and never falls back to Math.random(). Identifier generation fails when cryptographic randomness is unavailable. App-owned code that independently calls native crypto.randomUUID() remains responsible for its own compatibility.

Security and lifecycle

The packaged bootstrap uses the existing authenticated bridge and preserves its boundaries:

  • sandbox="allow-scripts" creates an opaque frame without same-origin privileges.
  • The document embeds the exact parent origin before app execution.
  • Incoming host messages require the expected iframe window, null origin, token, document identifier, and session identifier.
  • The CSP prohibits direct network access through connect-src 'none'; data requests run through the host’s dispatcher.
  • Source mode retains initialization timeout, cancellation, cleanup, reconnection, and live presentation updates.
  • React source changes replace the iframe when the JavaScript bundle changes.

Existing URL sources and explicit HTTP(S) bootstrap sources retain their behavior. Externally served bootstraps continue to own their response CSP.

Frontend migration

After adopting an SDK release containing this change, the frontend sandbox wrapper can render:

<DataAppBridge
  title={title}
  source={{ type: 'bundle', javascript }}
  presentation={presentation}
  onMessage={onMessage}
  onStatusChange={onStatusChange}
  iframeProps={{ hidden }}
/>

This removes the Worker import, HTML patching, UUID polyfill, data URL construction, manual token creation, startup timer, iframe/bridge refs, and lifecycle effects. The frontend continues to own bundle fetching, loading/error UI, and retry controls.

Verification

bun run check passed all 135 tests, including opaque-frame delivery and runtime error attribution, plus package and starter checks. Bootstrap minification reduces script size from 24,767 to 12,273 bytes and URL-encoded content from 43,273 to 17,251 bytes.

Workflow validation pins its fallback go run command to Go 1.27.1 because the runner's older Go version selected an unavailable toolchain download. The pinned actionlint fallback and workflow/shell checks pass locally.

MCP hosts can load bundle apps in data frames by omitting bootstrapUrl. Share the restrictive runtime HTML with the Worker and retain source-owned sandbox, session, cancellation, timeout, and presentation behavior.

Generate browser SDK identifiers with crypto.getRandomValues when opaque documents omit crypto.randomUUID. Keep randomness cryptographic and fail closed when it is unavailable.

Verify opaque-frame queries, cancellation, message isolation, presentation updates, runtime recovery, and UUID fallback through public APIs. bun run check passes all 135 tests plus package and starter checks.
Host bundlers cannot optimize JavaScript embedded inside a string. Minify the classic bootstrap before sharing it with the local data document and Worker to reduce browser payloads.

Bootstrap size drops from 24,767 to 12,273 bytes; URL-encoded content drops from 43,273 to 17,251 bytes. bun run check passes all 135 tests, including runtime error attribution, plus package and starter checks.
The Ubuntu runner toolchain is older than actionlint requires. Automatic toolchain selection attempted to download an unavailable Go release and failed before validation.

Pin the fallback go run command to the published Go 1.27.1 toolchain instead of relying on automatic version selection. Existing installed actionlint behavior remains unchanged.

Verified the pinned actionlint fallback command, workflow and shell checks, formatting, and diff checks.
@francoischalifour
francoischalifour merged commit 5c23250 into main Oct 9, 2026
7 checks passed
@francoischalifour
francoischalifour deleted the fc/opaque-bootstrap branch October 9, 2026 08:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants