Uh oh!
There was an error while loading. Please reload this page.
fix(console): insecure origins get a guarded crypto.randomUUID shim — list views stop crashing on LAN IPs (#4563) - #4583
Merged
Conversation
…4563) crypto.randomUUID is exposed only in secure contexts (HTTPS or http://localhost). Reaching a dev box over plain HTTP from another machine - http://192.168.x.x:4001/_console/ - leaves the method undefined, and every unguarded caller throws TypeError: crypto.randomUUID is not a function which takes the console's list views into the ErrorBoundary. The fix is a GLOBAL shim rather than a shared newId() helper, because a helper only reaches call sites that agree to import it and the crashing ones do not: the console's own graph carries unguarded calls in five packages, and the reporter's stack attributes the throwing frame to a vendored chunk this repository does not author at all. The call sites therefore stay untouched - they call a standard platform API correctly; what was missing is the platform. apps/console/index.html loads the shim from a script type=module placed ahead of the /src/main.tsx entry. Both are deferred and run in document order, so the shim precedes every consumer in the app's module graph; a placement test pins that ordering against a silent regression. The fallback builds RFC 4122 v4 from crypto.getRandomValues, which is not secure-context-gated. Guarded on absence, so a native implementation is never replaced. With no entropy source it installs nothing rather than degrading to Math.random (surfacing that state is objectui#4570). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
…ublic API The first run of these tests was red for four reasons, all of them in the tests rather than the shim, and each one worth keeping written down: - `parseSpecFilter` is not exported from `@object-ui/plugin-view`, so the "real consumer" case threw `parseSpecFilter is not a function` — a red that looks like the card's red and proves nothing. The public entry points that DO reach the unguarded calls are `toFilterGroup` (via parseSpecFilter -> parseTriplet, view-config-utils.ts:146) and `toSortItems` (:294 directly); both are now exercised. - The entropy helper re-read `globalThis.crypto` at call time, so the moment a test stubbed the global with an object whose `getRandomValues` WAS that helper it recursed until the stack blew. It now binds the native function once at module load. - `installRandomUuidShim(undefined)` hits the DEFAULT parameter and so means "use globalThis.crypto" — it cannot pose the no-crypto case. That case is now posed by removing the global. - The placement test compared raw string offsets, and both paths also appear in the explanatory HTML comment above the shim tag, so `indexOf(APP_ENTRY)` found the comment and the ordering assertion inverted while the markup was correct. It now compares script-tag positions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
The module-script shape shipped in the previous commit was TOO LATE, and the console's own build proves it. Vite merges the two HTML module entries into a single chunk, and that merged entry's static imports are hoisted above the shim's body, so in dist/assets/index-*.js the install sat behind 16 imported chunks - vendor-react, ui-components and RecordDetailView among them, all three of which contain randomUUID calls. Document order between module scripts is real in the browser but it does not survive bundling, so it cannot carry this guarantee. A classic inline script runs synchronously during parse, before any module script and therefore before any bundled chunk. That is the only bundler-independent guarantee, and it is the precedent the report named: the window.process polyfill immediately above it. Verified on the built artifact - the shim is tag #2 of 4 in dist/index.html and the only src-carrying tag is #3. The TypeScript module is deleted rather than kept for testability: a second copy of the logic would be graded green while the bootstrap that actually ships drifted away from it. The tests now EXTRACT the inline script from index.html and execute it, so they grade exactly what ships, and they fail loudly ("found 0") rather than vacuously if it is renamed or removed. Two parsing hazards found by these tests and worth keeping written down: prose describing a script tag inside an HTML comment parses as a script tag (the comment above this very shim was once paired with the shim's closing tag, reporting it as type=module), so both tests strip comments before parsing; and this app's tsconfig is browser-only, so index.html is read through Vite's `?raw` rather than node:fs, which tsc rejects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 13, 2026 13:48
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#4563
crypto.randomUUIDis exposed only in secure contexts — HTTPS, orhttp://localhost. Reaching a dev box over plain HTTP from another machine (http://192.168.x.x:4001/_console/, the ordinary second-device flow) leaves the method undefined, and every unguarded caller throwsTypeError: crypto.randomUUID is not a function, taking the console's list views into the ErrorBoundary.Premise: confirmed, with one correction to the ruling's expectation
Verified against current
origin/main. The defect reproduces from a real in-repo consumer, so it needs no vendored frame to explain it.The ruling expected a
plugin-gridcall site. There is none —plugin-gridhas nocrypto.randomUUIDcall of its own. The reporter'scomponentStacknamesplugin-gridbecause that is the component tree the error surfaced in; the calls it reaches live in its dependencies. This does not change the ruling's conclusion (a global shim), only the census that supports it.Caller census (repo-wide, tracked files,
dist/excluded)Unguarded — these throw on an insecure origin:
packages/plugin-viewsrc/config/view-config-utils.ts:146,160,294packages/plugin-listsrc/ListView.tsx:242,248,2209packages/componentssrc/custom/filter-builder.tsx:228,src/custom/sort-builder.tsx:94packages/app-shellsrc/views/RecordDetailView.tsx:1556,1596Already guarded (
typeof crypto.randomUUID === 'function'checks):packages/core/src/actions/TransactionManager.ts:510,packages/plugin-chatbot/src/utils.ts:14,packages/plugin-view/src/SharedViewLink.tsx:34,packages/components/src/custom/sort-builder.tsx:68. Not a console runtime file:public/mockServiceWorker.js:115.Per the ruling, every one of these stays unmodified. They are correct code calling a standard platform API; what was missing is the platform.
plugin-gridreaches the unguardedcomponentssites through its@object-ui/componentsdependency, which is consistent with the reported stack.Module-eval-time callers: none. Every call above sits inside a function or callback, so no consumer runs before the shim.
Placement — the part that was measured, and changed shape once
The shim is an inline classic script in
apps/console/index.html, beside thewindow.processpolyfill the report names.It was first written as a separate module-type entry placed above the app entry. The console's own build proved that too late, so it was replaced:
dist/assets/index-*.jsthe install sat behind 16 imported chunks —vendor-react,ui-componentsandRecordDetailViewamong them, all three of which containrandomUUIDcalls.A classic inline script runs synchronously during parse, before any module script and therefore before any bundled chunk. Verified on the built artifact — the shim is tag #2 of 4 in
dist/index.html, and the onlysrc-carrying tag is #3:Behaviour
Guarded on absence (
typeof cryptoRef.randomUUID === 'function'returns early), so a native CSPRNG is never replaced.crypto.getRandomValuesis not secure-context-gated, so it is present exactly whererandomUUIDis not and the entropy stays cryptographic; only the RFC 4122 formatting is rebuilt (version nibble4in octet 6, variant10in octet 8).Object.definePropertyfirst, plain assignment as fallback, and a target that refuses both is left alone rather than assumed to have worked.With no entropy source it installs nothing rather than degrading to
Math.random— an id generator that only looks like crypto is worse than the honest absence. Surfacing that state to the user is #4570, deliberately untouched here.Tests — they grade the shipped artifact
There is deliberately no TypeScript copy of the logic. A testable duplicate would be graded green while the bootstrap that actually ships drifted away from it, so the tests extract the inline script from
index.htmland execute it, and fail loudly (found 0) rather than vacuously if it is renamed or removed.Red-first, predicted in writing before implementing, driven through
@object-ui/plugin-view's public API against an insecure-origin-shaped crypto. Predicted the card's message verbatim; captured by temporarily asserting a sentinel:Prediction and result match exactly.
Coverage: generator shape / version nibble / variant bits / 1000-sample uniqueness / 16-byte draw; installer absence, native-identity, idempotence, no-entropy refusal, no-crypto-global; both consumer paths (
toFilterGroup,toSortItems) red without the shim and green with it; placement (classic, inline, ahead of everysrc-carrying script).Reverse verification
Four probes, each restored and sha256-verified byte-identical (
git checkout/ sentinel edits — nevergit stash):crypto.randomUUID is not a functionindex.htmlOn probe 2 the 7 survivors are the ones that must survive: the two red-first throw cases (which need no shim), the extractor's own negative test, and the entry-tag checks.
Changeset — gate verdict measured, not assumed
The ruling expected
apps/consoleto be unpublished. It is not.@object-ui/consoleis in thefixedgroup of.changeset/config.json, andscripts/check-changeset-presence.mjsderives its guarded surface from that file precisely so this package is covered — its header says so:So a changeset is owed. Gate output on this branch:
patch, never major (check:changeset-no-majorgreen).Verification
Heavy steps serialized under the shared
flockwith a capped heap.Surface
apps/console/index.html, two test files underapps/console/src/__tests__/, one changeset. Untouched as instructed:packages/react(#4548), core dataset-format / DatasetWidget (#4566), StudioDesignSurface (#4567), ObjectGrid's exportOptions (#4535), theSETTINGS_CRYPTO_UNAVAILABLErefusal surface (#4570),content/docs/releases/.Generated by Claude Code