Restore crypto.randomUUID before the Studio bundle evaluates - #9075
Restore crypto.randomUUID before the Studio bundle evaluates#9075Lyxot wants to merge 2 commits into
Conversation
Studio rendered a blank page when reached over plain http on a LAN address. `crypto.randomUUID()` is secure-context only, and the chat runtime store calls it at module scope, so the bundle threw before React mounted. `localhost` is a secure context, which is why local runs never saw it. `main.tsx` already carried a polyfill, but in the module body: a module's static imports evaluate before its own body, so the throwing module always ran first and the polyfill could never precede it. Move it to an external classic script loaded from `index.html` ahead of the module entry, the one position that runs before module evaluation. External rather than inline because the backend CSP is `script-src 'self'`, following the existing `theme-boot.js` pattern. Secure-context features such as dictation and clipboard remain unavailable over plain http by browser design; `--secure` stays the supported path for full remote access.
|
@codex review |
|
Codex Review: Didn't find any major issues. Hooray! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
The preview iframe is sandboxed with an opaque origin, so it has its own globals and the page's boot script cannot reach it. Canvas code calling `crypto.randomUUID()` therefore threw over plain http on a LAN address — the same secure-context gap the app itself hit — and the canvas rendered nothing. Install the fallback in the frame shell, before the message listener that renders the canvas: the first render replaces the document and takes that listener with it. It is inlined rather than sharing the page's `crypto-boot.js` because the strict frame CSP allows no external script at all; a test pins the two copies to each other so they cannot drift, since only the shared one has a golden test over its output. The dev smoke pages load the boot script too, so every HTML entry is covered, and the markup tests iterate all of them rather than `index.html` alone.
|
Added a second commit extending the fix to the artifact preview canvas. The preview iframe is sandboxed with an opaque origin, so it has its own globals and the page's boot script cannot reach it. Canvas code calling Two behaviours were measured rather than assumed: a fallback installed in the shell survives the It is inlined rather than sharing The dev smoke pages also load the boot script now, so every HTML entry is covered, and the markup tests iterate all entries instead of Note this part is a pre-existing limitation rather than part of the #9046 regression — canvases never had @codex review |
|
Codex Review: Something went wrong. Try again later by commenting “@codex review”. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
@codex review |
|
Codex Review: Didn't find any major issues. What shall we delve into next? Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Fixes #9046
The bug
Studio's web UI renders a blank page when it is reached over plain
http://at a LAN address —unsloth studio -H 0.0.0.0, then openinghttp://<LAN-IP>:<PORT>from another machine. The console shows:crypto.randomUUID()is a secure-context API, so it is absent on a page served over plain http from a LAN address.localhostis a secure context by definition, which is why local runs never hit this.Root cause
The frontend has 67
crypto.randomUUID()references, and exactly one of them runs at module scope rather than inside a function:main.tsxalready carried arandomUUIDpolyfill, but in the module body. A module's static imports are evaluated before its own body, and the path from the entry to that store is entirely static (main.tsx→app/app→app/router→routes/chat→features/chat→chat-runtime-store, whereroutes/chat.tsxuses a plain import rather thanlazyRouteComponent). So the throwing module always evaluated first and the polyfill could never precede it, wherever inmain.tsxit was placed. The throw kills the entry chunk, React never mounts, and the page stays blank.The fix
Move the polyfill out of the bundle and into an external classic script loaded from
index.htmlahead of the module entry, which is the one position that runs before module evaluation. The generator itself is unchanged from the one previously inmain.tsx.External rather than inline because the backend sends
script-src 'self'with no'unsafe-inline'and no nonce on normal requests, so an inline bootstrap is blocked. This mirrors the existingpublic/theme-boot.js, which is an external file for exactly the same reason. Files inpublic/are emitted to the build root and served by the frontend catch-all, the same paththeme-boot.jsalready takes in production.Reviewer notes and trade-offs
asyncscript above it runs first.deferon the boot scripts would preserve that too, since deferred classic scripts and the module entry share one execution list in document order;asyncon either would not. The test asserts the tags are non-asyncrather than forbiddingdefer.crypto.randomUUID()is correct everywhere, including at module scope. Seven files that hand-roll their own fallback are now redundant, but removing them is a behavior-neutral refactor and is better done separately than mixed into this fix.unsloth studio --secureremains the supported path for full remote access.Validation
Built the frontend, served the build over a LAN address, and loaded it in a browser with
window.isSecureContext === false.Before, with the boot script omitted from the same build:
After:
Inline was also checked against the running backend rather than assumed — an inline polyfill in the served
index.htmlis rejected:Each added assertion was mutation-checked, and each of these fails exactly one test: removing the script tag, commenting it out, moving it below the module entry, marking either tag
async, changing the nibble mask15to14, dropping the mask, replacing the source withMath.random, folding the drawn bytes together, reusing one byte, and overwriting an existingrandomUUID.deferon the boot script alone still passes, as intended.cd studio/frontend npm run typecheck node --experimental-strip-types --test tests/crypto-uuid-boot.test.ts