Skip to content

fix(console): insecure origins get a guarded crypto.randomUUID shim — list views stop crashing on LAN IPs (#4563) - #4583

Merged
yinlianghui merged 3 commits into
mainfrom
claude/issue-4563-insecure-origin-uuid
Aug 13, 2026
Merged

fix(console): insecure origins get a guarded crypto.randomUUID shim — list views stop crashing on LAN IPs (#4563)#4583
yinlianghui merged 3 commits into
mainfrom
claude/issue-4563-insecure-origin-uuid

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#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/, the ordinary second-device flow) leaves the method undefined, and every unguarded caller throws TypeError: 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-grid call site. There is noneplugin-grid has no crypto.randomUUID call of its own. The reporter's componentStack names plugin-grid because 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:

LocationPath
packages/plugin-viewsrc/config/view-config-utils.ts:146,160,294
packages/plugin-listsrc/ListView.tsx:242,248,2209
packages/componentssrc/custom/filter-builder.tsx:228, src/custom/sort-builder.tsx:94
packages/app-shellsrc/views/RecordDetailView.tsx:1556,1596

Already 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-grid reaches the unguarded components sites through its @object-ui/components dependency, 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 the window.process polyfill 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:

  • 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.
  • In dist/assets/index-*.js the install sat behind 16 imported chunksvendor-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. Verified on the built artifact — the shim is tag #2 of 4 in dist/index.html, and the only src-carrying tag is #3:

 #0 (inline classic)
#1 (inline classic)
#2 (inline classic) <== #4563 SHIM (inline, synchronous)
#3 type="module" crossorigin src="./assets/index-DFPi5DWy.js"

Behaviour

Guarded on absence (typeof cryptoRef.randomUUID === 'function' returns early), so a native CSPRNG is never replaced. crypto.getRandomValues is not secure-context-gated, so it is present exactly where randomUUID is not and the entropy stays cryptographic; only the RFC 4122 formatting is rebuilt (version nibble 4 in octet 6, variant 10 in octet 8). Object.defineProperty first, 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.html and 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:

Expected: "SENTINEL-capture-the-real-message"
Received: "crypto.randomUUID is not a function"

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 every src-carrying script).

Reverse verification

Four probes, each restored and sha256-verified byte-identical (git checkout / sentinel edits — never git stash):

ProbeExpectationResult
Assert a sentinel messagereveals the real stringcrypto.randomUUID is not a function
Remove the shim from index.htmlshim + generator + installer + consumer-green cases red13 of 20 red
Remove the absence guardnative-identity pin red3 red, exactly the guard-dependent ones
Remove the module-script tag (earlier shape)placement red3 of 4 red

On 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/console to be unpublished. It is not.@object-ui/console is in the fixed group of .changeset/config.json, and scripts/check-changeset-presence.mjs derives its guarded surface from that file precisely so this package is covered — its header says so:

@object-ui/console — the single most-edited published package in this repository, and the one the platform's bump-objectui.sh writes a changeset FOR — lives at apps/console, outside that glob entirely.

So a changeset is owed. Gate output on this branch:

2 source file(s) of 1 released package(s) changed, and this change
declares 1 changeset(s): .changeset/clever-hounds-brake.md.

patch, never major (check:changeset-no-major green).

Verification

pnpm --filter "@object-ui/console^..." build exit 0 (dependency closure)
pnpm --filter "@object-ui/console" build exit 0
pnpm --filter "@object-ui/console" type-check exit 0
pnpm --filter "@object-ui/console" lint exit 0 (0 errors; 0 from these files)
vitest run (the two new files) 20 passed (20)
check-control-bytes / changeset-presence / -no-major / -fixed / phantom-dependencies all green

Heavy steps serialized under the shared flock with a capped heap.

Surface

apps/console/index.html, two test files under apps/console/src/__tests__/, one changeset. Untouched as instructed: packages/react (#4548), core dataset-format / DatasetWidget (#4566), StudioDesignSurface (#4567), ObjectGrid's exportOptions (#4535), the SETTINGS_CRYPTO_UNAVAILABLE refusal surface (#4570), content/docs/releases/.


Generated by Claude Code

…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
@vercel

vercelBot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 13, 2026 1:36pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)24.7 KB350 KB
Entry fileindex-CU4y74xm.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.56KB3.59KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)25.13KB5.40KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)38.46KB10.17KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.88KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)489.33KB108.47KB
core (index.js)3.37KB1.34KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)163.56KB44.83KB
fields (index.js)230.37KB57.17KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)3.35KB1.38KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.12KB7.62KB
i18n (useDisplayLocale.js)2.84KB1.45KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.98KB10.85KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.86KB12.91KB
plugin-charts (index.js)62.10KB17.67KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)121.04KB31.57KB
plugin-designer (index.js)212.58KB42.83KB
plugin-detail (index.js)239.93KB60.01KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.58KB27.68KB
plugin-gantt (index.js)164.30KB40.02KB
plugin-grid (index.js)189.37KB50.33KB
plugin-kanban (index.js)52.74KB14.53KB
plugin-list (index.js)111.13KB27.12KB
plugin-map (index.js)18.16KB5.81KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)41.29KB11.05KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.09KB20.56KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)27.64KB9.44KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.26KB0.67KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghui
yinlianghui marked this pull request as ready for review August 13, 2026 13:48
@yinlianghui
yinlianghui added this pull request to the merge queueAug 13, 2026
Merged via the queue into main with commit 25b9833Aug 13, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4563-insecure-origin-uuid branch August 13, 2026 13:48
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Console crashes with "crypto.randomUUID is not a function" on insecure origins (HTTP + LAN IP)

2 participants

@yinlianghui@claude