fix(core): don't rely on crypto.randomUUID for asset ids - #681
Conversation
saveAsset() (called from reference-panel.tsx and local-guide-image.ts, i.e. on every file upload/drop) used crypto.randomUUID() to generate the IndexedDB key. That method requires a secure context (HTTPS or localhost) and is undefined otherwise, so it throws on plain-HTTP origins such as an internal-network deployment reached over a bare IP or hostname - every upload failed immediately there. Switched to nanoid's customAlphabet, which has no secure-context requirement and is already used for ids elsewhere in this package (schema/base.ts). Preferred that over the narrower typeof-check + counter fallback used in packages/nodes/src/cabinet/stack.ts: that counter resets to 0 on every reload, which is fine for stack.ts's transient in-memory compartment ids but not here - asset ids are persisted as IndexedDB keys and as asset://<id> references inside saved/exported scenes, so a colliding fallback could silently overwrite a different asset. nanoid gives the same collision resistance on every code path instead of degrading on insecure origins. Also adds a regression test. There was no existing test for this module because idb-keyval (used here) needs a real indexedDB global, which Bun's test runtime doesn't provide - added fake-indexeddb as a devDependency to fill that gap. The new test stubs crypto.randomUUID as unavailable and asserts saveAsset()/loadAssetUrl() still round-trip correctly; confirmed it fails with the old implementation (TypeError: crypto.randomUUID is not a function) and passes with the fix. Full packages/core suite (957 tests) passes.
Aymericr
left a comment
There was a problem hiding this comment.
This is a good catch and a well-argued fix — sorry it sat. I checked every randomUUID call site in the repo and asset-storage.ts was indeed the only unguarded browser-side one; stack.ts and mcp/storage/slug.ts already have their own fallbacks. And your reasoning for not copying the stack.ts counter here is right: these ids persist as IndexedDB keys and inside saved scenes, so a counter that resets on reload would eventually overwrite someone's asset.
customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 16) also happens to be exactly what schema/base.ts already uses for node ids, so this lands right on our existing convention. Existing asset://<uuid> references keep resolving since loadAssetUrl reads by exact key, so both formats coexist safely.
One optional nit: base.ts exports generateId(prefix) already. I'm fine with the local customAlphabet since asset://<id> wants a bare id and I'd rather not pull schema/base into lib/, but flagging it in case you prefer the reuse.
I've approved the CI runs — merging once they're green. Thanks for adding the fake-indexeddb harness too; that module had no test coverage at all, so that's useful beyond this fix.
Summary
saveAsset()(called on every file upload/drop, fromreference-panel.tsxandlocal-guide-image.ts) usedcrypto.randomUUID()to generate the IndexedDB key. That method requires a secure context (HTTPS orlocalhost) and isundefinedotherwise, so it throws on plain-HTTP origins — e.g. an internal-network deployment reached over a bare IP or hostname. Every upload fails immediately there.Switched to
nanoid'scustomAlphabet, which has no secure-context requirement and is already used for ids elsewhere in this package (schema/base.ts). I preferred that over the narrowertypeof-check + counter fallback inpackages/nodes/src/cabinet/stack.ts: that counter resets to 0 on every reload, which is fine forstack.ts's transient in-memory compartment ids but not here — asset ids are persisted as IndexedDB keys and asasset://<id>references inside saved/exported scenes, so a colliding fallback could silently overwrite a different asset.nanoidgives the same collision resistance on every code path instead of degrading on insecure origins.Test plan
packages/core/src/lib/asset-storage.test.ts. There was no existing test for this module becauseidb-keyval(used here) needs a realindexedDBglobal, which Bun's test runtime doesn't provide — addedfake-indexeddbas a devDependency to fill that gap.crypto.randomUUIDas unavailable and assertssaveAsset()/loadAssetUrl()still round-trip correctly. Confirmed it fails against the old implementation (TypeError: crypto.randomUUID is not a function) and passes with the fix.packages/coresuite (957 tests) passes.bun run check(biome) clean across the repo.Note
Low Risk
Small, localized ID generation change with regression tests; no auth or API surface changes, though persisted
asset://id format shifts from UUID to nanoid strings.Overview
Fixes file uploads on plain-HTTP deployments by changing how persisted asset IDs are generated in
saveAsset().saveAsset()previously usedcrypto.randomUUID(), which is unavailable outside a secure context (HTTPS/localhost), so uploads from editor flows that call it could throw immediately on internal HTTP hosts. IDs are now produced withnanoid'scustomAlphabet(16-char alphanumeric), which works in insecure contexts and stays suitable for long-lived IndexedDB keys andasset://references in saved scenes.Test coverage for
asset-storageis added viafake-indexeddbin Bun tests, including a regression that stubscrypto.randomUUIDas missing and asserts save/load still round-trip.Reviewed by Cursor Bugbot for commit cf5611b. Bugbot is set up for automated code reviews on this repo. Configure here.