From 3a1d9c4542fcd29e2a191e6fe8285a1be7a6bdfa Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Fri, 4 Sep 2026 23:52:41 -0400 Subject: [PATCH 1/3] fix(webv2): mint UUIDs without crypto.randomUUID in insecure contexts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `crypto.randomUUID` is only exposed in secure contexts (HTTPS or localhost), so opening the app over plain HTTP from another device on the LAN — an iPad pointed at a workstation, for example — left the property undefined. The durable persistence layer introduced in #210 mints a writer token during startup, which turned that into a crash before first paint: "crypto.randomUUID is not a function". Add `createUuid` in `@platform/browser/randomUuid`, which uses the native generator when present and otherwise assembles a v4 UUID from `crypto.getRandomValues`, and route every production call site through it. An architecture test now rejects any direct `randomUUID` reference outside the helper so the crash cannot creep back in. Re-record the architecture and browser performance baselines for the new bundled source file. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_019JTauFL2fzFopMq8VmQiE7 --- invokeai/frontend/webv2/package.json | 2 +- .../performance/architecture-baseline.json | 50 +-- .../webv2/performance/browser-baseline.json | 350 +++++++++--------- .../webv2/src/app/GenerationUiAdapter.tsx | 5 +- .../architecture/secureContextApis.test.ts | 28 ++ .../ui/promptFields/PositivePromptActions.tsx | 5 +- .../features/queue/runtime/utilityGraph.ts | 5 +- .../src/platform/browser/randomUuid.test.ts | 70 ++++ .../webv2/src/platform/browser/randomUuid.ts | 38 ++ .../projects/durableSyncedPersistence.ts | 6 +- .../src/workbench/projects/editorSession.ts | 3 +- .../webv2/src/workbench/projects/ids.ts | 4 +- .../workbench/projects/indexedDbDraftStore.ts | 6 +- .../widgets/layers/RunLayerWorkflowDialog.tsx | 3 +- 14 files changed, 362 insertions(+), 213 deletions(-) create mode 100644 invokeai/frontend/webv2/src/architecture/secureContextApis.test.ts create mode 100644 invokeai/frontend/webv2/src/platform/browser/randomUuid.test.ts create mode 100644 invokeai/frontend/webv2/src/platform/browser/randomUuid.ts diff --git a/invokeai/frontend/webv2/package.json b/invokeai/frontend/webv2/package.json index f6585d52cdb..0452b608fcd 100644 --- a/invokeai/frontend/webv2/package.json +++ b/invokeai/frontend/webv2/package.json @@ -7,7 +7,7 @@ "#architecture/source-analysis": "./src/architecture/tsSourceAnalysis.ts" }, "scripts": { - "architecture:check": "vitest run --config vitest.config.mts src/architecture/dependencyPolicy.test.ts src/architecture/workbenchOwnership.test.ts src/architecture/architectureWorkflow.test.ts src/architecture/canvasSeamOwnership.test.ts src/architecture/tsSourceAnalysis.test.ts && node scripts/write-workbench-ownership-inventory.mjs && node scripts/write-contract-caller-matrix.mjs && node scripts/write-canvas-import-matrix.mjs", + "architecture:check": "vitest run --config vitest.config.mts src/architecture/dependencyPolicy.test.ts src/architecture/workbenchOwnership.test.ts src/architecture/architectureWorkflow.test.ts src/architecture/canvasSeamOwnership.test.ts src/architecture/tsSourceAnalysis.test.ts src/architecture/secureContextApis.test.ts && node scripts/write-workbench-ownership-inventory.mjs && node scripts/write-contract-caller-matrix.mjs && node scripts/write-canvas-import-matrix.mjs", "build": "pnpm run lint && vite build", "check:architecture": "pnpm run lint && pnpm run test:all && pnpm run test:performance:architecture && pnpm run test:project-files:browser", "check:release": "pnpm run check:architecture && pnpm run test:accessibility:browser", diff --git a/invokeai/frontend/webv2/performance/architecture-baseline.json b/invokeai/frontend/webv2/performance/architecture-baseline.json index b76b5270428..a2b7b886d3d 100644 --- a/invokeai/frontend/webv2/performance/architecture-baseline.json +++ b/invokeai/frontend/webv2/performance/architecture-baseline.json @@ -2,17 +2,17 @@ "build": { "launchpad": { "baseline": { - "brotliBytes": 848403, + "brotliBytes": 848736, "cssRawBytes": 2159, "fontRawBytes": 219480, - "gzipBytes": 847579, + "gzipBytes": 847916, "imageRawBytes": 156376, - "initialRawBytes": 1982245, + "initialRawBytes": 1982717, "largestAssetRawBytes": 746121, "otherAssetRawBytes": 0, - "ownedRawBytes": 73859, - "requestCount": 42, - "scriptRequestCount": 26, + "ownedRawBytes": 73898, + "requestCount": 43, + "scriptRequestCount": 27, "sourceOwners": [ "package:@ark-ui/react", "package:@chakra-ui/react", @@ -173,6 +173,7 @@ "source:src/features/nodes/ui/NodesPage.tsx", "source:src/platform/browser/downloadBlob.ts", "source:src/platform/browser/knownBrowserIssues.ts", + "source:src/platform/browser/randomUuid.ts", "source:src/platform/core/concurrency.ts", "source:src/platform/i18n/client.ts", "source:src/platform/i18n/languages.ts", @@ -296,17 +297,17 @@ ] }, "limits": { - "brotliBytes": 856888, + "brotliBytes": 857224, "cssRawBytes": 2181, "fontRawBytes": 221675, - "gzipBytes": 856055, + "gzipBytes": 856396, "imageRawBytes": 157940, - "initialRawBytes": 2002068, + "initialRawBytes": 2002545, "largestAssetRawBytes": 753583, "otherAssetRawBytes": 0, - "ownedRawBytes": 73859, - "requestCount": 42, - "scriptRequestCount": 26 + "ownedRawBytes": 73898, + "requestCount": 43, + "scriptRequestCount": 27 }, "owner": "app", "remediationTicket": "set-performance-budgets", @@ -314,17 +315,17 @@ }, "editor": { "baseline": { - "brotliBytes": 1146538, + "brotliBytes": 1146936, "cssRawBytes": 2159, "fontRawBytes": 219480, - "gzipBytes": 1141296, + "gzipBytes": 1141728, "imageRawBytes": 156376, - "initialRawBytes": 2888015, + "initialRawBytes": 2888767, "largestAssetRawBytes": 746121, "otherAssetRawBytes": 0, - "ownedRawBytes": 132798, - "requestCount": 103, - "scriptRequestCount": 87, + "ownedRawBytes": 132886, + "requestCount": 104, + "scriptRequestCount": 88, "sourceOwners": [ "package:@ark-ui/react", "package:@chakra-ui/react", @@ -639,6 +640,7 @@ "source:src/features/workflow/widget.ts", "source:src/platform/browser/downloadBlob.ts", "source:src/platform/browser/knownBrowserIssues.ts", + "source:src/platform/browser/randomUuid.ts", "source:src/platform/browser/webLocks.ts", "source:src/platform/core/concurrency.ts", "source:src/platform/core/json.ts", @@ -967,17 +969,17 @@ ] }, "limits": { - "brotliBytes": 1158004, + "brotliBytes": 1158406, "cssRawBytes": 2181, "fontRawBytes": 221675, - "gzipBytes": 1152709, + "gzipBytes": 1153146, "imageRawBytes": 157940, - "initialRawBytes": 2916896, + "initialRawBytes": 2917655, "largestAssetRawBytes": 753583, "otherAssetRawBytes": 0, - "ownedRawBytes": 132798, - "requestCount": 103, - "scriptRequestCount": 87 + "ownedRawBytes": 132886, + "requestCount": 104, + "scriptRequestCount": 88 }, "owner": "workbench", "remediationTicket": "deepen-widget-registry-loading", diff --git a/invokeai/frontend/webv2/performance/browser-baseline.json b/invokeai/frontend/webv2/performance/browser-baseline.json index 7e4a52e0f06..e6de7fbeba6 100644 --- a/invokeai/frontend/webv2/performance/browser-baseline.json +++ b/invokeai/frontend/webv2/performance/browser-baseline.json @@ -1,5 +1,5 @@ { - "browserExecutable": "/home/toast/.cache/ms-playwright/chromium-1234/chrome-linux64/chrome", + "browserExecutable": "/home/lstein/.cache/ms-playwright/chromium-1234/chrome-linux64/chrome", "capturedAt": "2026-09-05", "routes": [ { @@ -25,13 +25,13 @@ "scriptRequestCount": 0, "totalRawBytes": 0 }, - "domContentLoadedMedianMs": 119.39999999850988, + "domContentLoadedMedianMs": 94.10000002384186, "id": "launchpad", "layoutAckMedianMs": 0, "layoutReturnSwitchMedianMs": 0, "layoutSwitchMedianMs": 0, - "loadMedianMs": 119.5, - "longestTaskMaxMs": 70, + "loadMedianMs": 94.20000004768372, + "longestTaskMaxMs": 55, "owner": "app", "projectSwitchMedianMs": 0, "readyMark": "invokeai:ready:launchpad", @@ -41,24 +41,24 @@ "fontRawBytes": 23664, "imageRawBytes": 0, "largestAssetRawBytes": 746121, - "otherRawBytes": 169527, - "requestCount": 37, - "scriptRawBytes": 1662375, - "scriptRequestCount": 33, - "totalRawBytes": 1857725 + "otherRawBytes": 169609, + "requestCount": 38, + "scriptRawBytes": 1662847, + "scriptRequestCount": 34, + "totalRawBytes": 1858279 }, "resourceLimits": { "cssRawBytes": 2181, "fontRawBytes": 23901, "imageRawBytes": 0, "largestAssetRawBytes": 753583, - "otherRawBytes": 171223, - "requestCount": 37, - "scriptRawBytes": 1678999, - "scriptRequestCount": 33, - "totalRawBytes": 1876303 + "otherRawBytes": 171306, + "requestCount": 38, + "scriptRawBytes": 1679476, + "scriptRequestCount": 34, + "totalRawBytes": 1876862 }, - "routeReadyMedianMs": 268.3999999985099, + "routeReadyMedianMs": 214.89999997615814, "scriptSourceOwnerSet": "launchpad-static", "stateProfile": "empty" }, @@ -85,13 +85,13 @@ "scriptRequestCount": 0, "totalRawBytes": 0 }, - "domContentLoadedMedianMs": 122.60000000149012, + "domContentLoadedMedianMs": 96, "id": "launchpad", "layoutAckMedianMs": 0, "layoutReturnSwitchMedianMs": 0, "layoutSwitchMedianMs": 0, - "loadMedianMs": 122.70000000298023, - "longestTaskMaxMs": 73, + "loadMedianMs": 96.10000002384186, + "longestTaskMaxMs": 56, "owner": "app", "projectSwitchMedianMs": 0, "readyMark": "invokeai:ready:launchpad", @@ -101,24 +101,24 @@ "fontRawBytes": 23664, "imageRawBytes": 0, "largestAssetRawBytes": 746121, - "otherRawBytes": 169527, - "requestCount": 37, - "scriptRawBytes": 1662375, - "scriptRequestCount": 33, - "totalRawBytes": 1857725 + "otherRawBytes": 169609, + "requestCount": 38, + "scriptRawBytes": 1662847, + "scriptRequestCount": 34, + "totalRawBytes": 1858279 }, "resourceLimits": { "cssRawBytes": 2181, "fontRawBytes": 23901, "imageRawBytes": 0, "largestAssetRawBytes": 753583, - "otherRawBytes": 171223, - "requestCount": 37, - "scriptRawBytes": 1678999, - "scriptRequestCount": 33, - "totalRawBytes": 1876303 + "otherRawBytes": 171306, + "requestCount": 38, + "scriptRawBytes": 1679476, + "scriptRequestCount": 34, + "totalRawBytes": 1876862 }, - "routeReadyMedianMs": 282.70000000298023, + "routeReadyMedianMs": 229, "scriptSourceOwnerSet": "launchpad-static", "stateProfile": "representative" }, @@ -145,15 +145,15 @@ "scriptRequestCount": 0, "totalRawBytes": 0 }, - "domContentLoadedMedianMs": 117.89999999850988, + "domContentLoadedMedianMs": 93.30000007152557, "id": "editor-minimal", "layoutAckMedianMs": 0, "layoutReturnSwitchMedianMs": 0, "layoutSwitchMedianMs": 0, - "loadMedianMs": 118, - "longestTaskMaxMs": 125, + "loadMedianMs": 93.39999997615814, + "longestTaskMaxMs": 105, "owner": "workbench", - "projectSwitchMedianMs": 38.30000000447035, + "projectSwitchMedianMs": 29.799999952316284, "readyMark": "invokeai:ready:widget:center:preview", "remediationTicket": "deepen-widget-registry-loading", "resourceBaseline": { @@ -161,24 +161,24 @@ "fontRawBytes": 23664, "imageRawBytes": 156376, "largestAssetRawBytes": 746121, - "otherRawBytes": 169527, - "requestCount": 121, - "scriptRawBytes": 3436667, - "scriptRequestCount": 116, - "totalRawBytes": 3788393 + "otherRawBytes": 169609, + "requestCount": 122, + "scriptRawBytes": 3437481, + "scriptRequestCount": 117, + "totalRawBytes": 3789289 }, "resourceLimits": { "cssRawBytes": 2181, "fontRawBytes": 23901, "imageRawBytes": 157940, "largestAssetRawBytes": 753583, - "otherRawBytes": 171223, - "requestCount": 121, - "scriptRawBytes": 3471034, - "scriptRequestCount": 116, - "totalRawBytes": 3826277 + "otherRawBytes": 171306, + "requestCount": 122, + "scriptRawBytes": 3471856, + "scriptRequestCount": 117, + "totalRawBytes": 3827182 }, - "routeReadyMedianMs": 434.29999999701977, + "routeReadyMedianMs": 352.10000002384186, "scriptSourceOwnerSet": "editor-minimal-static", "stateProfile": "empty" }, @@ -205,15 +205,15 @@ "scriptRequestCount": 0, "totalRawBytes": 0 }, - "domContentLoadedMedianMs": 121.39999999850988, + "domContentLoadedMedianMs": 95.20000004768372, "id": "editor-minimal", "layoutAckMedianMs": 0, "layoutReturnSwitchMedianMs": 0, "layoutSwitchMedianMs": 0, - "loadMedianMs": 121.39999999850988, - "longestTaskMaxMs": 132, + "loadMedianMs": 95.30000007152557, + "longestTaskMaxMs": 106, "owner": "workbench", - "projectSwitchMedianMs": 44.600000001490116, + "projectSwitchMedianMs": 35, "readyMark": "invokeai:ready:widget:center:preview", "remediationTicket": "deepen-widget-registry-loading", "resourceBaseline": { @@ -221,24 +221,24 @@ "fontRawBytes": 23664, "imageRawBytes": 156376, "largestAssetRawBytes": 746121, - "otherRawBytes": 169527, - "requestCount": 121, - "scriptRawBytes": 3436667, - "scriptRequestCount": 116, - "totalRawBytes": 3788393 + "otherRawBytes": 169609, + "requestCount": 122, + "scriptRawBytes": 3437481, + "scriptRequestCount": 117, + "totalRawBytes": 3789289 }, "resourceLimits": { "cssRawBytes": 2181, "fontRawBytes": 23901, "imageRawBytes": 157940, "largestAssetRawBytes": 753583, - "otherRawBytes": 171223, - "requestCount": 121, - "scriptRawBytes": 3471034, - "scriptRequestCount": 116, - "totalRawBytes": 3826277 + "otherRawBytes": 171306, + "requestCount": 122, + "scriptRawBytes": 3471856, + "scriptRequestCount": 117, + "totalRawBytes": 3827182 }, - "routeReadyMedianMs": 439.69999999552965, + "routeReadyMedianMs": 354.60000002384186, "scriptSourceOwnerSet": "editor-minimal-static", "stateProfile": "representative" }, @@ -250,9 +250,9 @@ "largestAssetRawBytes": 314585, "otherRawBytes": 0, "requestCount": 7, - "scriptRawBytes": 481922, + "scriptRawBytes": 481950, "scriptRequestCount": 7, - "totalRawBytes": 481922 + "totalRawBytes": 481950 }, "activatedResourceLimits": { "cssRawBytes": 0, @@ -261,17 +261,17 @@ "largestAssetRawBytes": 317731, "otherRawBytes": 0, "requestCount": 7, - "scriptRawBytes": 486742, + "scriptRawBytes": 486770, "scriptRequestCount": 7, - "totalRawBytes": 486742 + "totalRawBytes": 486770 }, - "domContentLoadedMedianMs": 119.20000000298023, + "domContentLoadedMedianMs": 95.5, "id": "editor-canvas", - "layoutAckMedianMs": 2.7000000029802322, - "layoutReturnSwitchMedianMs": 56.399999998509884, - "layoutSwitchMedianMs": 94.20000000298023, - "loadMedianMs": 119.30000000447035, - "longestTaskMaxMs": 154, + "layoutAckMedianMs": 2.100000023841858, + "layoutReturnSwitchMedianMs": 48.60000002384186, + "layoutSwitchMedianMs": 77.80000007152557, + "loadMedianMs": 95.70000004768372, + "longestTaskMaxMs": 113, "owner": "canvas", "projectSwitchMedianMs": 0, "readyMark": "invokeai:ready:widget:center:canvas", @@ -281,24 +281,24 @@ "fontRawBytes": 23664, "imageRawBytes": 156376, "largestAssetRawBytes": 746121, - "otherRawBytes": 169527, - "requestCount": 128, - "scriptRawBytes": 3918589, - "scriptRequestCount": 123, - "totalRawBytes": 4270315 + "otherRawBytes": 169609, + "requestCount": 129, + "scriptRawBytes": 3919431, + "scriptRequestCount": 124, + "totalRawBytes": 4271239 }, "resourceLimits": { "cssRawBytes": 2181, "fontRawBytes": 23901, "imageRawBytes": 157940, "largestAssetRawBytes": 753583, - "otherRawBytes": 171223, - "requestCount": 128, - "scriptRawBytes": 3957775, - "scriptRequestCount": 123, - "totalRawBytes": 4313019 + "otherRawBytes": 171306, + "requestCount": 129, + "scriptRawBytes": 3958626, + "scriptRequestCount": 124, + "totalRawBytes": 4313952 }, - "routeReadyMedianMs": 1137.7000000029802, + "routeReadyMedianMs": 1025, "scriptSourceOwnerSet": "editor-canvas-static", "stateProfile": "empty" }, @@ -310,9 +310,9 @@ "largestAssetRawBytes": 314585, "otherRawBytes": 0, "requestCount": 7, - "scriptRawBytes": 481922, + "scriptRawBytes": 481950, "scriptRequestCount": 7, - "totalRawBytes": 481922 + "totalRawBytes": 481950 }, "activatedResourceLimits": { "cssRawBytes": 0, @@ -321,17 +321,17 @@ "largestAssetRawBytes": 317731, "otherRawBytes": 0, "requestCount": 7, - "scriptRawBytes": 486742, + "scriptRawBytes": 486770, "scriptRequestCount": 7, - "totalRawBytes": 486742 + "totalRawBytes": 486770 }, - "domContentLoadedMedianMs": 121.69999999552965, + "domContentLoadedMedianMs": 96.29999995231628, "id": "editor-canvas", - "layoutAckMedianMs": 3.3000000044703484, - "layoutReturnSwitchMedianMs": 46.29999999701977, - "layoutSwitchMedianMs": 88.5, - "loadMedianMs": 121.79999999701977, - "longestTaskMaxMs": 193, + "layoutAckMedianMs": 3.399999976158142, + "layoutReturnSwitchMedianMs": 45.700000047683716, + "layoutSwitchMedianMs": 77.39999997615814, + "loadMedianMs": 96.5, + "longestTaskMaxMs": 152, "owner": "canvas", "projectSwitchMedianMs": 0, "readyMark": "invokeai:ready:widget:center:canvas", @@ -341,24 +341,24 @@ "fontRawBytes": 23664, "imageRawBytes": 156376, "largestAssetRawBytes": 746121, - "otherRawBytes": 169527, - "requestCount": 128, - "scriptRawBytes": 3918589, - "scriptRequestCount": 123, - "totalRawBytes": 4270315 + "otherRawBytes": 169609, + "requestCount": 129, + "scriptRawBytes": 3919431, + "scriptRequestCount": 124, + "totalRawBytes": 4271239 }, "resourceLimits": { "cssRawBytes": 2181, "fontRawBytes": 23901, "imageRawBytes": 157940, "largestAssetRawBytes": 753583, - "otherRawBytes": 171223, - "requestCount": 128, - "scriptRawBytes": 3957775, - "scriptRequestCount": 123, - "totalRawBytes": 4313019 + "otherRawBytes": 171306, + "requestCount": 129, + "scriptRawBytes": 3958626, + "scriptRequestCount": 124, + "totalRawBytes": 4313952 }, - "routeReadyMedianMs": 1137.6999999955297, + "routeReadyMedianMs": 1048.5, "scriptSourceOwnerSet": "editor-canvas-static", "stateProfile": "representative" }, @@ -370,9 +370,9 @@ "largestAssetRawBytes": 173980, "otherRawBytes": 0, "requestCount": 7, - "scriptRawBytes": 364370, + "scriptRawBytes": 364400, "scriptRequestCount": 6, - "totalRawBytes": 379783 + "totalRawBytes": 379813 }, "activatedResourceLimits": { "cssRawBytes": 15568, @@ -381,17 +381,17 @@ "largestAssetRawBytes": 175720, "otherRawBytes": 0, "requestCount": 7, - "scriptRawBytes": 368014, + "scriptRawBytes": 368044, "scriptRequestCount": 6, - "totalRawBytes": 383581 + "totalRawBytes": 383612 }, - "domContentLoadedMedianMs": 119.5, + "domContentLoadedMedianMs": 93.60000002384186, "id": "editor-workflow", - "layoutAckMedianMs": 2.600000001490116, - "layoutReturnSwitchMedianMs": 32.70000000298023, - "layoutSwitchMedianMs": 94.60000000149012, - "loadMedianMs": 119.59999999403954, - "longestTaskMaxMs": 142, + "layoutAckMedianMs": 2.100000023841858, + "layoutReturnSwitchMedianMs": 29.299999952316284, + "layoutSwitchMedianMs": 75.5, + "loadMedianMs": 93.70000004768372, + "longestTaskMaxMs": 104, "owner": "workflow", "projectSwitchMedianMs": 0, "readyMark": "invokeai:ready:widget:center:workflow", @@ -401,24 +401,24 @@ "fontRawBytes": 23664, "imageRawBytes": 156376, "largestAssetRawBytes": 746121, - "otherRawBytes": 169527, - "requestCount": 128, - "scriptRawBytes": 3801037, - "scriptRequestCount": 122, - "totalRawBytes": 4168176 + "otherRawBytes": 169609, + "requestCount": 129, + "scriptRawBytes": 3801881, + "scriptRequestCount": 123, + "totalRawBytes": 4169102 }, "resourceLimits": { "cssRawBytes": 17748, "fontRawBytes": 23901, "imageRawBytes": 157940, "largestAssetRawBytes": 753583, - "otherRawBytes": 171223, - "requestCount": 128, - "scriptRawBytes": 3839048, - "scriptRequestCount": 122, - "totalRawBytes": 4209858 + "otherRawBytes": 171306, + "requestCount": 129, + "scriptRawBytes": 3839900, + "scriptRequestCount": 123, + "totalRawBytes": 4210794 }, - "routeReadyMedianMs": 1152, + "routeReadyMedianMs": 1032.8999999761581, "scriptSourceOwnerSet": "editor-workflow-static", "stateProfile": "empty" }, @@ -430,9 +430,9 @@ "largestAssetRawBytes": 173980, "otherRawBytes": 0, "requestCount": 7, - "scriptRawBytes": 364370, + "scriptRawBytes": 364400, "scriptRequestCount": 6, - "totalRawBytes": 379783 + "totalRawBytes": 379813 }, "activatedResourceLimits": { "cssRawBytes": 15568, @@ -441,17 +441,17 @@ "largestAssetRawBytes": 175720, "otherRawBytes": 0, "requestCount": 7, - "scriptRawBytes": 368014, + "scriptRawBytes": 368044, "scriptRequestCount": 6, - "totalRawBytes": 383581 + "totalRawBytes": 383612 }, - "domContentLoadedMedianMs": 120.79999999701977, + "domContentLoadedMedianMs": 96, "id": "editor-workflow", - "layoutAckMedianMs": 3.5, - "layoutReturnSwitchMedianMs": 40.5, - "layoutSwitchMedianMs": 126.10000000149012, - "loadMedianMs": 120.89999999850988, - "longestTaskMaxMs": 375, + "layoutAckMedianMs": 3.299999952316284, + "layoutReturnSwitchMedianMs": 35.299999952316284, + "layoutSwitchMedianMs": 110.10000002384186, + "loadMedianMs": 96.09999990463257, + "longestTaskMaxMs": 277, "owner": "workflow", "projectSwitchMedianMs": 0, "readyMark": "invokeai:ready:widget:center:workflow", @@ -461,24 +461,24 @@ "fontRawBytes": 23664, "imageRawBytes": 156376, "largestAssetRawBytes": 746121, - "otherRawBytes": 169527, - "requestCount": 128, - "scriptRawBytes": 3801037, - "scriptRequestCount": 122, - "totalRawBytes": 4168176 + "otherRawBytes": 169609, + "requestCount": 129, + "scriptRawBytes": 3801881, + "scriptRequestCount": 123, + "totalRawBytes": 4169102 }, "resourceLimits": { "cssRawBytes": 17748, "fontRawBytes": 23901, "imageRawBytes": 157940, "largestAssetRawBytes": 753583, - "otherRawBytes": 171223, - "requestCount": 128, - "scriptRawBytes": 3839048, - "scriptRequestCount": 122, - "totalRawBytes": 4209858 + "otherRawBytes": 171306, + "requestCount": 129, + "scriptRawBytes": 3839900, + "scriptRequestCount": 123, + "totalRawBytes": 4210794 }, - "routeReadyMedianMs": 1177.6000000014901, + "routeReadyMedianMs": 1084.5, "scriptSourceOwnerSet": "editor-workflow-static", "stateProfile": "representative" }, @@ -505,13 +505,13 @@ "scriptRequestCount": 0, "totalRawBytes": 0 }, - "domContentLoadedMedianMs": 120.89999999850988, + "domContentLoadedMedianMs": 93, "id": "editor-gallery", "layoutAckMedianMs": 0, "layoutReturnSwitchMedianMs": 0, - "layoutSwitchMedianMs": 23.5, - "loadMedianMs": 121.10000000149012, - "longestTaskMaxMs": 126, + "layoutSwitchMedianMs": 20.700000047683716, + "loadMedianMs": 93.10000002384186, + "longestTaskMaxMs": 105, "owner": "gallery", "projectSwitchMedianMs": 0, "readyMark": "invokeai:ready:widget:center:gallery", @@ -521,24 +521,24 @@ "fontRawBytes": 23664, "imageRawBytes": 156376, "largestAssetRawBytes": 746121, - "otherRawBytes": 169527, - "requestCount": 121, - "scriptRawBytes": 3436667, - "scriptRequestCount": 116, - "totalRawBytes": 3788393 + "otherRawBytes": 169609, + "requestCount": 122, + "scriptRawBytes": 3437481, + "scriptRequestCount": 117, + "totalRawBytes": 3789289 }, "resourceLimits": { "cssRawBytes": 2181, "fontRawBytes": 23901, "imageRawBytes": 157940, "largestAssetRawBytes": 753583, - "otherRawBytes": 171223, - "requestCount": 121, - "scriptRawBytes": 3471034, - "scriptRequestCount": 116, - "totalRawBytes": 3826277 + "otherRawBytes": 171306, + "requestCount": 122, + "scriptRawBytes": 3471856, + "scriptRequestCount": 117, + "totalRawBytes": 3827182 }, - "routeReadyMedianMs": 1325, + "routeReadyMedianMs": 1232.6000000238419, "scriptSourceOwnerSet": "editor-minimal-static", "stateProfile": "empty" }, @@ -565,13 +565,13 @@ "scriptRequestCount": 0, "totalRawBytes": 0 }, - "domContentLoadedMedianMs": 115, + "domContentLoadedMedianMs": 98.70000004768372, "id": "editor-gallery", "layoutAckMedianMs": 0, "layoutReturnSwitchMedianMs": 0, - "layoutSwitchMedianMs": 30.80000000447035, - "loadMedianMs": 115.10000000149012, - "longestTaskMaxMs": 114, + "layoutSwitchMedianMs": 26.700000047683716, + "loadMedianMs": 98.80000007152557, + "longestTaskMaxMs": 101, "owner": "gallery", "projectSwitchMedianMs": 0, "readyMark": "invokeai:ready:widget:center:gallery", @@ -581,24 +581,24 @@ "fontRawBytes": 23664, "imageRawBytes": 156376, "largestAssetRawBytes": 746121, - "otherRawBytes": 169527, - "requestCount": 121, - "scriptRawBytes": 3436667, - "scriptRequestCount": 116, - "totalRawBytes": 3788393 + "otherRawBytes": 169609, + "requestCount": 122, + "scriptRawBytes": 3437481, + "scriptRequestCount": 117, + "totalRawBytes": 3789289 }, "resourceLimits": { "cssRawBytes": 2181, "fontRawBytes": 23901, "imageRawBytes": 157940, "largestAssetRawBytes": 753583, - "otherRawBytes": 171223, - "requestCount": 121, - "scriptRawBytes": 3471034, - "scriptRequestCount": 116, - "totalRawBytes": 3826277 + "otherRawBytes": 171306, + "requestCount": 122, + "scriptRawBytes": 3471856, + "scriptRequestCount": 117, + "totalRawBytes": 3827182 }, - "routeReadyMedianMs": 1330.2000000029802, + "routeReadyMedianMs": 1265.8000000715256, "scriptSourceOwnerSet": "editor-minimal-static", "stateProfile": "representative" } @@ -803,6 +803,7 @@ "source:src/features/queue/ui/launchpad/QueueStatusBand.tsx", "source:src/platform/browser/downloadBlob.ts", "source:src/platform/browser/knownBrowserIssues.ts", + "source:src/platform/browser/randomUuid.ts", "source:src/platform/core/concurrency.ts", "source:src/platform/i18n/client.ts", "source:src/platform/i18n/languages.ts", @@ -1384,6 +1385,7 @@ "source:src/features/workflow/widget.ts", "source:src/platform/browser/downloadBlob.ts", "source:src/platform/browser/knownBrowserIssues.ts", + "source:src/platform/browser/randomUuid.ts", "source:src/platform/browser/webLocks.ts", "source:src/platform/core/concurrency.ts", "source:src/platform/core/json.ts", @@ -2371,6 +2373,7 @@ "source:src/features/workflow/widget.ts", "source:src/platform/browser/downloadBlob.ts", "source:src/platform/browser/knownBrowserIssues.ts", + "source:src/platform/browser/randomUuid.ts", "source:src/platform/browser/webLocks.ts", "source:src/platform/core/concurrency.ts", "source:src/platform/core/json.ts", @@ -3570,6 +3573,7 @@ "source:src/features/workflow/widget.ts", "source:src/platform/browser/downloadBlob.ts", "source:src/platform/browser/knownBrowserIssues.ts", + "source:src/platform/browser/randomUuid.ts", "source:src/platform/browser/webLocks.ts", "source:src/platform/core/concurrency.ts", "source:src/platform/core/json.ts", diff --git a/invokeai/frontend/webv2/src/app/GenerationUiAdapter.tsx b/invokeai/frontend/webv2/src/app/GenerationUiAdapter.tsx index c1756f94687..c252f88b26c 100644 --- a/invokeai/frontend/webv2/src/app/GenerationUiAdapter.tsx +++ b/invokeai/frontend/webv2/src/app/GenerationUiAdapter.tsx @@ -15,6 +15,7 @@ import { extractGenerationMeta, getResultImageName, } from '@features/queue/contracts'; +import { createUuid } from '@platform/browser/randomUuid'; import { useMountEffect } from '@platform/react/useMountEffect'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { @@ -233,7 +234,7 @@ export const GenerationUiAdapterProvider = ({ children }: { children: ReactNode }); }, save: (label, weights, multiplier) => { - const preset = { id: crypto.randomUUID(), label, multiplier, weights }; + const preset = { id: createUuid(), label, multiplier, weights }; void patchWorkbenchPreferences({ krea2RebalancePresets: [...getWorkbenchPreferences().krea2RebalancePresets, preset], @@ -263,7 +264,7 @@ export const GenerationUiAdapterProvider = ({ children }: { children: ReactNode }); }, save: (label, values) => { - const preset = { id: crypto.randomUUID(), label, values }; + const preset = { id: createUuid(), label, values }; void patchWorkbenchPreferences({ generatePresets: [...getWorkbenchPreferences().generatePresets, preset], diff --git a/invokeai/frontend/webv2/src/architecture/secureContextApis.test.ts b/invokeai/frontend/webv2/src/architecture/secureContextApis.test.ts new file mode 100644 index 00000000000..e9c2470f038 --- /dev/null +++ b/invokeai/frontend/webv2/src/architecture/secureContextApis.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; + +/** + * `crypto.randomUUID` is undefined outside secure contexts, and the app is + * routinely opened over plain HTTP from another device on the LAN. Every id + * mint goes through `@platform/browser/randomUuid`, which falls back to + * `getRandomValues`; a direct call anywhere else reintroduces a startup crash + * that only shows up on a phone or tablet. + */ +const sources = import.meta.glob('../**/*.{ts,tsx}', { + eager: true, + import: 'default', + query: '?raw', +}) as Record; + +const HELPER_PATH = '../platform/browser/randomUuid.ts'; +const isTestFile = (path: string): boolean => /\.(test|spec|stories)\.tsx?$/.test(path); + +describe('secure-context web APIs', () => { + it('routes every production UUID mint through the randomUuid helper', () => { + const offenders = Object.entries(sources) + .filter(([path]) => path !== HELPER_PATH && !isTestFile(path)) + .filter(([, text]) => /\brandomUUID\b/.test(text)) + .map(([path]) => path.replace(/^\.\.\//, '')); + + expect(offenders).toEqual([]); + }); +}); diff --git a/invokeai/frontend/webv2/src/features/generation/ui/promptFields/PositivePromptActions.tsx b/invokeai/frontend/webv2/src/features/generation/ui/promptFields/PositivePromptActions.tsx index 460b124dd7c..42b691d4baf 100644 --- a/invokeai/frontend/webv2/src/features/generation/ui/promptFields/PositivePromptActions.tsx +++ b/invokeai/frontend/webv2/src/features/generation/ui/promptFields/PositivePromptActions.tsx @@ -25,6 +25,7 @@ import { } from '@features/generation/ui/promptFields/promptTriggerOptions'; import { SystemPromptsField } from '@features/generation/ui/promptFields/SystemPromptsField'; import { useSystemPrompts } from '@features/generation/ui/promptFields/useSystemPrompts'; +import { createUuid } from '@platform/browser/randomUuid'; import { useMountEffect } from '@platform/react/useMountEffect'; import { getApiErrorMessage } from '@platform/transport/http'; import { Button, IconButton, PopoverContent, Scrollable, Tooltip } from '@platform/ui'; @@ -435,7 +436,7 @@ const ExpandPromptButton = ({ return; } - const nextTaskId = crypto.randomUUID(); + const nextTaskId = createUuid(); setTaskId(nextTaskId); setIsLoading(true); @@ -608,7 +609,7 @@ const ImageToPromptButton = ({ return; } - const nextTaskId = crypto.randomUUID(); + const nextTaskId = createUuid(); setTaskId(nextTaskId); setIsLoading(true); diff --git a/invokeai/frontend/webv2/src/features/queue/runtime/utilityGraph.ts b/invokeai/frontend/webv2/src/features/queue/runtime/utilityGraph.ts index 31ddb666301..e4e3a374075 100644 --- a/invokeai/frontend/webv2/src/features/queue/runtime/utilityGraph.ts +++ b/invokeai/frontend/webv2/src/features/queue/runtime/utilityGraph.ts @@ -32,6 +32,7 @@ import type { SocketHub } from '@platform/transport/socketHub'; import { buildUtilityQueueItemOrigin } from '@features/queue/data/events'; import { cancelQueueItems, getQueueItem } from '@features/queue/data/serverApi'; import { enqueueUtility } from '@features/queue/data/submissionApi'; +import { createUuid } from '@platform/browser/randomUuid'; import { ApiError } from '@platform/transport/http'; /** The default time a utility graph may run before it is abandoned. */ @@ -133,7 +134,7 @@ export interface RunUtilityGraphOptions { timeoutMs?: number; /** Cancels the await and best-effort cancels accepted backend items. */ signal?: AbortSignal; - /** Injectable id source (defaults to `crypto.randomUUID`). */ + /** Injectable id source (defaults to `createUuid`). */ createId?: () => string; /** Injectable completed-item reconciliation seam. */ reconcileCompletedOutput?: UtilityCompletedOutputReconciler; @@ -232,7 +233,7 @@ export const runUtilityGraph = (options: RunUtilityGraphOptions): Promise crypto.randomUUID()))(); + const utilityId = (options.createId ?? createUuid)(); origin = buildUtilityQueueItemOrigin(utilityId); } catch (cause) { const detail = cause instanceof Error ? cause.message : String(cause); diff --git a/invokeai/frontend/webv2/src/platform/browser/randomUuid.test.ts b/invokeai/frontend/webv2/src/platform/browser/randomUuid.test.ts new file mode 100644 index 00000000000..6e2a6e053f4 --- /dev/null +++ b/invokeai/frontend/webv2/src/platform/browser/randomUuid.test.ts @@ -0,0 +1,70 @@ +import { createUuid, createUuidWith, type UuidCryptoSource } from '@platform/browser/randomUuid'; +import { describe, expect, it, vi } from 'vitest'; + +const UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +/** A `Crypto` as seen from an insecure context: random bytes, no `randomUUID`. */ +const insecureContextCrypto = ( + fill: (bytes: Uint8Array) => void = (bytes) => crypto.getRandomValues(bytes as Uint8Array) +) => { + const source: UuidCryptoSource = { + getRandomValues: ((array: ArrayBufferView) => { + fill(new Uint8Array(array.buffer, array.byteOffset, array.byteLength)); + return array; + }) as Crypto['getRandomValues'], + }; + return source; +}; + +describe('createUuidWith', () => { + it('delegates to the native generator when the context provides one', () => { + const randomUUID = vi.fn( + (): `${string}-${string}-${string}-${string}-${string}` => '11111111-2222-4333-8444-555555555555' + ); + + expect(createUuidWith({ getRandomValues: crypto.getRandomValues.bind(crypto), randomUUID })).toBe( + '11111111-2222-4333-8444-555555555555' + ); + expect(randomUUID).toHaveBeenCalledOnce(); + }); + + it('builds a well-formed version 4 UUID without the native generator', () => { + const source = insecureContextCrypto(); + const seen = new Set(); + + for (let index = 0; index < 200; index += 1) { + const uuid = createUuidWith(source); + expect(uuid).toMatch(UUID_V4); + seen.add(uuid); + } + + expect(seen.size).toBe(200); + }); + + it('forces the version and variant bits regardless of the random bytes', () => { + const allOnes = createUuidWith(insecureContextCrypto((bytes) => bytes.fill(0xff))); + const allZeros = createUuidWith(insecureContextCrypto((bytes) => bytes.fill(0x00))); + + expect(allOnes).toBe('ffffffff-ffff-4fff-bfff-ffffffffffff'); + expect(allZeros).toBe('00000000-0000-4000-8000-000000000000'); + }); +}); + +describe('createUuid', () => { + it('produces a version 4 UUID from the ambient crypto object', () => { + expect(createUuid()).toMatch(UUID_V4); + }); + + it('survives an ambient crypto object that lacks randomUUID', () => { + const original = globalThis.crypto; + vi.stubGlobal( + 'crypto', + insecureContextCrypto((bytes) => original.getRandomValues(bytes as Uint8Array)) + ); + try { + expect(createUuid()).toMatch(UUID_V4); + } finally { + vi.unstubAllGlobals(); + } + }); +}); diff --git a/invokeai/frontend/webv2/src/platform/browser/randomUuid.ts b/invokeai/frontend/webv2/src/platform/browser/randomUuid.ts new file mode 100644 index 00000000000..c2433111ea9 --- /dev/null +++ b/invokeai/frontend/webv2/src/platform/browser/randomUuid.ts @@ -0,0 +1,38 @@ +/** + * Generate an RFC 4122 version 4 UUID. + * + * `crypto.randomUUID` is exposed only in secure contexts — HTTPS, or `localhost` + * over plain HTTP. The app is routinely opened from another device on the LAN + * (an iPad pointed at a workstation, say), and there the property is simply + * undefined, so a bare `crypto.randomUUID()` throws. The durable persistence + * layer mints a writer token during startup, which turned that into a crash + * before first paint. + * + * `crypto.getRandomValues` carries no secure-context restriction, so when the + * native generator is missing the UUID is assembled from sixteen random bytes + * with the version and variant bits set by hand. Every call site that needs a + * fresh id goes through here; `crypto.randomUUID` must not be called directly. + */ + +/** The subset of `Crypto` the generator relies on, so tests can swap it. */ +export type UuidCryptoSource = Pick & Partial>; + +const toHex = (byte: number): string => byte.toString(16).padStart(2, '0'); + +export const createUuidWith = (source: UuidCryptoSource): string => { + if (typeof source.randomUUID === 'function') { + return source.randomUUID(); + } + + const bytes = new Uint8Array(16); + source.getRandomValues(bytes); + // Version 4: the top nibble of byte 6 is 0b0100. + bytes[6] = (bytes[6]! & 0x0f) | 0x40; + // RFC 4122 variant: the top two bits of byte 8 are 0b10. + bytes[8] = (bytes[8]! & 0x3f) | 0x80; + + const hex = Array.from(bytes, toHex).join(''); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; +}; + +export const createUuid = (): string => createUuidWith(globalThis.crypto); diff --git a/invokeai/frontend/webv2/src/workbench/projects/durableSyncedPersistence.ts b/invokeai/frontend/webv2/src/workbench/projects/durableSyncedPersistence.ts index 5b31e68fae4..4b656cbf40d 100644 --- a/invokeai/frontend/webv2/src/workbench/projects/durableSyncedPersistence.ts +++ b/invokeai/frontend/webv2/src/workbench/projects/durableSyncedPersistence.ts @@ -1,6 +1,7 @@ import type { HydratedWorkbenchSnapshot } from '@workbench/persistenceContracts'; import type { Project, ProjectLoadResult, RefusedWorkbenchProject, WorkbenchState } from '@workbench/projectContracts'; +import { createUuid } from '@platform/browser/randomUuid'; import { assertAccountScopeCurrent, type AccountScope } from '@platform/state/accountLifecycle'; import { getProjectCanvasSchemaRequirement, @@ -459,7 +460,7 @@ export const createDurableSyncedWorkbenchPersistence = ( journal.close(); } }; - const writerToken = dependencies.writerToken ?? crypto.randomUUID(); + const writerToken = dependencies.writerToken ?? createUuid(); const saveAsNew = dependencies.saveDraftAsNew ?? saveDraftAsNew; const deleteDatabase = dependencies.deleteDatabase ?? deleteWorkbenchDatabase; const databaseDeleteTimeoutMs = dependencies.databaseDeleteTimeoutMs ?? 5_000; @@ -644,8 +645,7 @@ export const createDurableSyncedWorkbenchPersistence = ( const isolateUnopenableDraft = (projectId: string, editorSessionId: string): string => { const preferred = `${editorSessionId}:writer:${writerToken}`; - const isolated = - draftEditorSessionIds.get(projectId) === preferred ? `${preferred}:${crypto.randomUUID()}` : preferred; + const isolated = draftEditorSessionIds.get(projectId) === preferred ? `${preferred}:${createUuid()}` : preferred; draftEditorSessionIds.set(projectId, isolated); return isolated; }; diff --git a/invokeai/frontend/webv2/src/workbench/projects/editorSession.ts b/invokeai/frontend/webv2/src/workbench/projects/editorSession.ts index 42793a153c0..7d05b82602b 100644 --- a/invokeai/frontend/webv2/src/workbench/projects/editorSession.ts +++ b/invokeai/frontend/webv2/src/workbench/projects/editorSession.ts @@ -1,3 +1,4 @@ +import { createUuid } from '@platform/browser/randomUuid'; import { acquireExclusiveLock, type ExclusiveLockResult } from '@platform/browser/webLocks'; export const EDITOR_SESSION_STORAGE_KEY = 'invokeai:v7:webv2:editor-session'; @@ -18,7 +19,7 @@ type AcquireLock = (name: string) => Promise; export const createEditorSessionProvider = ( storage: SessionStoragePort, acquireLock: AcquireLock = acquireExclusiveLock, - createId: () => string = () => crypto.randomUUID() + createId: () => string = createUuid ): (() => Promise) => { let sessionPromise: Promise | null = null; let currentSession: EditorSession | null = null; diff --git a/invokeai/frontend/webv2/src/workbench/projects/ids.ts b/invokeai/frontend/webv2/src/workbench/projects/ids.ts index 9c8705d0745..0fb44f08446 100644 --- a/invokeai/frontend/webv2/src/workbench/projects/ids.ts +++ b/invokeai/frontend/webv2/src/workbench/projects/ids.ts @@ -1,4 +1,6 @@ -export const createProjectId = (): string => `project-${globalThis.crypto.randomUUID()}`; +import { createUuid } from '@platform/browser/randomUuid'; + +export const createProjectId = (): string => `project-${createUuid()}`; export const createDeterministicProjectId = async (scope: string): Promise => { const digest = new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(scope))); diff --git a/invokeai/frontend/webv2/src/workbench/projects/indexedDbDraftStore.ts b/invokeai/frontend/webv2/src/workbench/projects/indexedDbDraftStore.ts index 4a544f2c4e6..054531bb109 100644 --- a/invokeai/frontend/webv2/src/workbench/projects/indexedDbDraftStore.ts +++ b/invokeai/frontend/webv2/src/workbench/projects/indexedDbDraftStore.ts @@ -1,5 +1,7 @@ import type { AccountScope } from '@platform/state/accountLifecycle'; +import { createUuid } from '@platform/browser/randomUuid'; + import { acquireAccountOwnedWorkbenchDatabase } from './accountOwnedWorkbenchDatabase'; import { clampProjectDraftLimit, @@ -538,9 +540,7 @@ export const createIndexedDbProjectDraftStore = ( state: 'fenced', updatedAt: Date.now(), writerToken: - typeof storedWriterToken === 'string' && storedWriterToken.length > 0 - ? storedWriterToken - : crypto.randomUUID(), + typeof storedWriterToken === 'string' && storedWriterToken.length > 0 ? storedWriterToken : createUuid(), }); await transaction.done; return { kind: 'deleted' }; diff --git a/invokeai/frontend/webv2/src/workbench/widgets/layers/RunLayerWorkflowDialog.tsx b/invokeai/frontend/webv2/src/workbench/widgets/layers/RunLayerWorkflowDialog.tsx index 8b07468e137..4d06871100a 100644 --- a/invokeai/frontend/webv2/src/workbench/widgets/layers/RunLayerWorkflowDialog.tsx +++ b/invokeai/frontend/webv2/src/workbench/widgets/layers/RunLayerWorkflowDialog.tsx @@ -19,6 +19,7 @@ import { type WorkflowImageBinding, } from '@features/workflow/graph'; import { useInvocationTemplatesSnapshot, type InvocationTemplatesSnapshot } from '@features/workflow/react'; +import { createUuid } from '@platform/browser/randomUuid'; import { captureAccountScope, isAccountScopeCurrent } from '@platform/state/accountLifecycle'; import { socketHub } from '@platform/transport/socketHub'; import { Button, CloseButton, Field, Select } from '@platform/ui'; @@ -364,7 +365,7 @@ export const RunLayerWorkflowDialog = ({ canvas.appendStagingCandidate({ candidate, projectId: targetProjectId }), buildGraph: buildLayerWorkflowGraph, commitGenerated: (options) => engine.layers.commitGeneratedImageResult(options), - createRequestId: () => crypto.randomUUID(), + createRequestId: createUuid, exportLayer: (targetLayerId) => engine.exports.exportBakedLayerBlob(targetLayerId, { includeDisabled: true }), getImage: galleryImages.resolve, isGuardCurrent: (guard) => engine.exports.isLayerExportGuardCurrent(guard), From 4985dc419419fe4970ca9bdd44be29db92fe1b11 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Sat, 5 Sep 2026 00:07:17 -0400 Subject: [PATCH 2/3] fix(webv2): hash without crypto.subtle in insecure contexts `crypto.subtle` is gated behind secure contexts exactly like `crypto.randomUUID`, so after the UUID fix the same LAN-over-HTTP session still threw on the first paint-cache flush, composite for generation, or deterministic project id. Add `@platform/browser/sha256` with a pure-JavaScript SHA-256 that is byte-identical to the Web Crypto digest (verified against it across the padding boundaries), route the three digest sites through it, and widen the secure-context architecture test to reject direct `crypto.subtle` use. Re-record the performance baselines for the new source file. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_019JTauFL2fzFopMq8VmQiE7 --- .../performance/architecture-baseline.json | 50 +-- .../webv2/performance/browser-baseline.json | 326 +++++++++--------- .../architecture/secureContextApis.test.ts | 20 +- .../webv2/src/platform/browser/sha256.test.ts | 64 ++++ .../webv2/src/platform/browser/sha256.ts | 137 ++++++++ .../canvas-engine/document/bitmapStore.ts | 17 +- .../compositeForGeneration.ts | 16 +- .../webv2/src/workbench/projects/ids.ts | 3 +- 8 files changed, 414 insertions(+), 219 deletions(-) create mode 100644 invokeai/frontend/webv2/src/platform/browser/sha256.test.ts create mode 100644 invokeai/frontend/webv2/src/platform/browser/sha256.ts diff --git a/invokeai/frontend/webv2/performance/architecture-baseline.json b/invokeai/frontend/webv2/performance/architecture-baseline.json index a2b7b886d3d..bc32888a855 100644 --- a/invokeai/frontend/webv2/performance/architecture-baseline.json +++ b/invokeai/frontend/webv2/performance/architecture-baseline.json @@ -2,17 +2,17 @@ "build": { "launchpad": { "baseline": { - "brotliBytes": 848736, + "brotliBytes": 849939, "cssRawBytes": 2159, "fontRawBytes": 219480, - "gzipBytes": 847916, + "gzipBytes": 849088, "imageRawBytes": 156376, - "initialRawBytes": 1982717, + "initialRawBytes": 1984807, "largestAssetRawBytes": 746121, "otherAssetRawBytes": 0, - "ownedRawBytes": 73898, - "requestCount": 43, - "scriptRequestCount": 27, + "ownedRawBytes": 73933, + "requestCount": 44, + "scriptRequestCount": 28, "sourceOwners": [ "package:@ark-ui/react", "package:@chakra-ui/react", @@ -174,6 +174,7 @@ "source:src/platform/browser/downloadBlob.ts", "source:src/platform/browser/knownBrowserIssues.ts", "source:src/platform/browser/randomUuid.ts", + "source:src/platform/browser/sha256.ts", "source:src/platform/core/concurrency.ts", "source:src/platform/i18n/client.ts", "source:src/platform/i18n/languages.ts", @@ -297,17 +298,17 @@ ] }, "limits": { - "brotliBytes": 857224, + "brotliBytes": 858439, "cssRawBytes": 2181, "fontRawBytes": 221675, - "gzipBytes": 856396, + "gzipBytes": 857579, "imageRawBytes": 157940, - "initialRawBytes": 2002545, + "initialRawBytes": 2004656, "largestAssetRawBytes": 753583, "otherAssetRawBytes": 0, - "ownedRawBytes": 73898, - "requestCount": 43, - "scriptRequestCount": 27 + "ownedRawBytes": 73933, + "requestCount": 44, + "scriptRequestCount": 28 }, "owner": "app", "remediationTicket": "set-performance-budgets", @@ -315,17 +316,17 @@ }, "editor": { "baseline": { - "brotliBytes": 1146936, + "brotliBytes": 1148325, "cssRawBytes": 2159, "fontRawBytes": 219480, - "gzipBytes": 1141728, + "gzipBytes": 1142958, "imageRawBytes": 156376, - "initialRawBytes": 2888767, + "initialRawBytes": 2891084, "largestAssetRawBytes": 746121, "otherAssetRawBytes": 0, - "ownedRawBytes": 132886, - "requestCount": 104, - "scriptRequestCount": 88, + "ownedRawBytes": 132954, + "requestCount": 105, + "scriptRequestCount": 89, "sourceOwners": [ "package:@ark-ui/react", "package:@chakra-ui/react", @@ -641,6 +642,7 @@ "source:src/platform/browser/downloadBlob.ts", "source:src/platform/browser/knownBrowserIssues.ts", "source:src/platform/browser/randomUuid.ts", + "source:src/platform/browser/sha256.ts", "source:src/platform/browser/webLocks.ts", "source:src/platform/core/concurrency.ts", "source:src/platform/core/json.ts", @@ -969,17 +971,17 @@ ] }, "limits": { - "brotliBytes": 1158406, + "brotliBytes": 1159809, "cssRawBytes": 2181, "fontRawBytes": 221675, - "gzipBytes": 1153146, + "gzipBytes": 1154388, "imageRawBytes": 157940, - "initialRawBytes": 2917655, + "initialRawBytes": 2919995, "largestAssetRawBytes": 753583, "otherAssetRawBytes": 0, - "ownedRawBytes": 132886, - "requestCount": 104, - "scriptRequestCount": 88 + "ownedRawBytes": 132954, + "requestCount": 105, + "scriptRequestCount": 89 }, "owner": "workbench", "remediationTicket": "deepen-widget-registry-loading", diff --git a/invokeai/frontend/webv2/performance/browser-baseline.json b/invokeai/frontend/webv2/performance/browser-baseline.json index e6de7fbeba6..6e45b11291c 100644 --- a/invokeai/frontend/webv2/performance/browser-baseline.json +++ b/invokeai/frontend/webv2/performance/browser-baseline.json @@ -25,13 +25,13 @@ "scriptRequestCount": 0, "totalRawBytes": 0 }, - "domContentLoadedMedianMs": 94.10000002384186, + "domContentLoadedMedianMs": 96.39999997615814, "id": "launchpad", "layoutAckMedianMs": 0, "layoutReturnSwitchMedianMs": 0, "layoutSwitchMedianMs": 0, - "loadMedianMs": 94.20000004768372, - "longestTaskMaxMs": 55, + "loadMedianMs": 96.5, + "longestTaskMaxMs": 57, "owner": "app", "projectSwitchMedianMs": 0, "readyMark": "invokeai:ready:launchpad", @@ -41,24 +41,24 @@ "fontRawBytes": 23664, "imageRawBytes": 0, "largestAssetRawBytes": 746121, - "otherRawBytes": 169609, - "requestCount": 38, - "scriptRawBytes": 1662847, - "scriptRequestCount": 34, - "totalRawBytes": 1858279 + "otherRawBytes": 169687, + "requestCount": 39, + "scriptRawBytes": 1664937, + "scriptRequestCount": 35, + "totalRawBytes": 1860447 }, "resourceLimits": { "cssRawBytes": 2181, "fontRawBytes": 23901, "imageRawBytes": 0, "largestAssetRawBytes": 753583, - "otherRawBytes": 171306, - "requestCount": 38, - "scriptRawBytes": 1679476, - "scriptRequestCount": 34, - "totalRawBytes": 1876862 + "otherRawBytes": 171384, + "requestCount": 39, + "scriptRawBytes": 1681587, + "scriptRequestCount": 35, + "totalRawBytes": 1879052 }, - "routeReadyMedianMs": 214.89999997615814, + "routeReadyMedianMs": 219.10000002384186, "scriptSourceOwnerSet": "launchpad-static", "stateProfile": "empty" }, @@ -85,12 +85,12 @@ "scriptRequestCount": 0, "totalRawBytes": 0 }, - "domContentLoadedMedianMs": 96, + "domContentLoadedMedianMs": 95.40000009536743, "id": "launchpad", "layoutAckMedianMs": 0, "layoutReturnSwitchMedianMs": 0, "layoutSwitchMedianMs": 0, - "loadMedianMs": 96.10000002384186, + "loadMedianMs": 95.60000002384186, "longestTaskMaxMs": 56, "owner": "app", "projectSwitchMedianMs": 0, @@ -101,24 +101,24 @@ "fontRawBytes": 23664, "imageRawBytes": 0, "largestAssetRawBytes": 746121, - "otherRawBytes": 169609, - "requestCount": 38, - "scriptRawBytes": 1662847, - "scriptRequestCount": 34, - "totalRawBytes": 1858279 + "otherRawBytes": 169687, + "requestCount": 39, + "scriptRawBytes": 1664937, + "scriptRequestCount": 35, + "totalRawBytes": 1860447 }, "resourceLimits": { "cssRawBytes": 2181, "fontRawBytes": 23901, "imageRawBytes": 0, "largestAssetRawBytes": 753583, - "otherRawBytes": 171306, - "requestCount": 38, - "scriptRawBytes": 1679476, - "scriptRequestCount": 34, - "totalRawBytes": 1876862 + "otherRawBytes": 171384, + "requestCount": 39, + "scriptRawBytes": 1681587, + "scriptRequestCount": 35, + "totalRawBytes": 1879052 }, - "routeReadyMedianMs": 229, + "routeReadyMedianMs": 225.70000004768372, "scriptSourceOwnerSet": "launchpad-static", "stateProfile": "representative" }, @@ -145,15 +145,15 @@ "scriptRequestCount": 0, "totalRawBytes": 0 }, - "domContentLoadedMedianMs": 93.30000007152557, + "domContentLoadedMedianMs": 94.10000002384186, "id": "editor-minimal", "layoutAckMedianMs": 0, "layoutReturnSwitchMedianMs": 0, "layoutSwitchMedianMs": 0, - "loadMedianMs": 93.39999997615814, + "loadMedianMs": 94.20000004768372, "longestTaskMaxMs": 105, "owner": "workbench", - "projectSwitchMedianMs": 29.799999952316284, + "projectSwitchMedianMs": 30.40000009536743, "readyMark": "invokeai:ready:widget:center:preview", "remediationTicket": "deepen-widget-registry-loading", "resourceBaseline": { @@ -161,24 +161,24 @@ "fontRawBytes": 23664, "imageRawBytes": 156376, "largestAssetRawBytes": 746121, - "otherRawBytes": 169609, - "requestCount": 122, - "scriptRawBytes": 3437481, - "scriptRequestCount": 117, - "totalRawBytes": 3789289 + "otherRawBytes": 169687, + "requestCount": 123, + "scriptRawBytes": 3439620, + "scriptRequestCount": 118, + "totalRawBytes": 3791506 }, "resourceLimits": { "cssRawBytes": 2181, "fontRawBytes": 23901, "imageRawBytes": 157940, "largestAssetRawBytes": 753583, - "otherRawBytes": 171306, - "requestCount": 122, - "scriptRawBytes": 3471856, - "scriptRequestCount": 117, - "totalRawBytes": 3827182 + "otherRawBytes": 171384, + "requestCount": 123, + "scriptRawBytes": 3474017, + "scriptRequestCount": 118, + "totalRawBytes": 3829422 }, - "routeReadyMedianMs": 352.10000002384186, + "routeReadyMedianMs": 356.39999997615814, "scriptSourceOwnerSet": "editor-minimal-static", "stateProfile": "empty" }, @@ -205,15 +205,15 @@ "scriptRequestCount": 0, "totalRawBytes": 0 }, - "domContentLoadedMedianMs": 95.20000004768372, + "domContentLoadedMedianMs": 95.40000009536743, "id": "editor-minimal", "layoutAckMedianMs": 0, "layoutReturnSwitchMedianMs": 0, "layoutSwitchMedianMs": 0, - "loadMedianMs": 95.30000007152557, - "longestTaskMaxMs": 106, + "loadMedianMs": 95.5, + "longestTaskMaxMs": 102, "owner": "workbench", - "projectSwitchMedianMs": 35, + "projectSwitchMedianMs": 35.299999952316284, "readyMark": "invokeai:ready:widget:center:preview", "remediationTicket": "deepen-widget-registry-loading", "resourceBaseline": { @@ -221,24 +221,24 @@ "fontRawBytes": 23664, "imageRawBytes": 156376, "largestAssetRawBytes": 746121, - "otherRawBytes": 169609, - "requestCount": 122, - "scriptRawBytes": 3437481, - "scriptRequestCount": 117, - "totalRawBytes": 3789289 + "otherRawBytes": 169687, + "requestCount": 123, + "scriptRawBytes": 3439620, + "scriptRequestCount": 118, + "totalRawBytes": 3791506 }, "resourceLimits": { "cssRawBytes": 2181, "fontRawBytes": 23901, "imageRawBytes": 157940, "largestAssetRawBytes": 753583, - "otherRawBytes": 171306, - "requestCount": 122, - "scriptRawBytes": 3471856, - "scriptRequestCount": 117, - "totalRawBytes": 3827182 + "otherRawBytes": 171384, + "requestCount": 123, + "scriptRawBytes": 3474017, + "scriptRequestCount": 118, + "totalRawBytes": 3829422 }, - "routeReadyMedianMs": 354.60000002384186, + "routeReadyMedianMs": 357.6999999284744, "scriptSourceOwnerSet": "editor-minimal-static", "stateProfile": "representative" }, @@ -265,13 +265,13 @@ "scriptRequestCount": 7, "totalRawBytes": 486770 }, - "domContentLoadedMedianMs": 95.5, + "domContentLoadedMedianMs": 96.90000009536743, "id": "editor-canvas", - "layoutAckMedianMs": 2.100000023841858, - "layoutReturnSwitchMedianMs": 48.60000002384186, - "layoutSwitchMedianMs": 77.80000007152557, - "loadMedianMs": 95.70000004768372, - "longestTaskMaxMs": 113, + "layoutAckMedianMs": 2.0999999046325684, + "layoutReturnSwitchMedianMs": 48, + "layoutSwitchMedianMs": 76.89999997615814, + "loadMedianMs": 97.10000002384186, + "longestTaskMaxMs": 114, "owner": "canvas", "projectSwitchMedianMs": 0, "readyMark": "invokeai:ready:widget:center:canvas", @@ -281,24 +281,24 @@ "fontRawBytes": 23664, "imageRawBytes": 156376, "largestAssetRawBytes": 746121, - "otherRawBytes": 169609, - "requestCount": 129, - "scriptRawBytes": 3919431, - "scriptRequestCount": 124, - "totalRawBytes": 4271239 + "otherRawBytes": 169687, + "requestCount": 130, + "scriptRawBytes": 3921570, + "scriptRequestCount": 125, + "totalRawBytes": 4273456 }, "resourceLimits": { "cssRawBytes": 2181, "fontRawBytes": 23901, "imageRawBytes": 157940, "largestAssetRawBytes": 753583, - "otherRawBytes": 171306, - "requestCount": 129, - "scriptRawBytes": 3958626, - "scriptRequestCount": 124, - "totalRawBytes": 4313952 + "otherRawBytes": 171384, + "requestCount": 130, + "scriptRawBytes": 3960786, + "scriptRequestCount": 125, + "totalRawBytes": 4316191 }, - "routeReadyMedianMs": 1025, + "routeReadyMedianMs": 1025.9000000953674, "scriptSourceOwnerSet": "editor-canvas-static", "stateProfile": "empty" }, @@ -325,13 +325,13 @@ "scriptRequestCount": 7, "totalRawBytes": 486770 }, - "domContentLoadedMedianMs": 96.29999995231628, + "domContentLoadedMedianMs": 96.09999990463257, "id": "editor-canvas", - "layoutAckMedianMs": 3.399999976158142, - "layoutReturnSwitchMedianMs": 45.700000047683716, - "layoutSwitchMedianMs": 77.39999997615814, - "loadMedianMs": 96.5, - "longestTaskMaxMs": 152, + "layoutAckMedianMs": 3.100000023841858, + "layoutReturnSwitchMedianMs": 45.200000047683716, + "layoutSwitchMedianMs": 77.70000004768372, + "loadMedianMs": 96.19999992847443, + "longestTaskMaxMs": 151, "owner": "canvas", "projectSwitchMedianMs": 0, "readyMark": "invokeai:ready:widget:center:canvas", @@ -341,24 +341,24 @@ "fontRawBytes": 23664, "imageRawBytes": 156376, "largestAssetRawBytes": 746121, - "otherRawBytes": 169609, - "requestCount": 129, - "scriptRawBytes": 3919431, - "scriptRequestCount": 124, - "totalRawBytes": 4271239 + "otherRawBytes": 169687, + "requestCount": 130, + "scriptRawBytes": 3921570, + "scriptRequestCount": 125, + "totalRawBytes": 4273456 }, "resourceLimits": { "cssRawBytes": 2181, "fontRawBytes": 23901, "imageRawBytes": 157940, "largestAssetRawBytes": 753583, - "otherRawBytes": 171306, - "requestCount": 129, - "scriptRawBytes": 3958626, - "scriptRequestCount": 124, - "totalRawBytes": 4313952 + "otherRawBytes": 171384, + "requestCount": 130, + "scriptRawBytes": 3960786, + "scriptRequestCount": 125, + "totalRawBytes": 4316191 }, - "routeReadyMedianMs": 1048.5, + "routeReadyMedianMs": 1047.2999999523163, "scriptSourceOwnerSet": "editor-canvas-static", "stateProfile": "representative" }, @@ -370,9 +370,9 @@ "largestAssetRawBytes": 173980, "otherRawBytes": 0, "requestCount": 7, - "scriptRawBytes": 364400, + "scriptRawBytes": 364426, "scriptRequestCount": 6, - "totalRawBytes": 379813 + "totalRawBytes": 379839 }, "activatedResourceLimits": { "cssRawBytes": 15568, @@ -381,17 +381,17 @@ "largestAssetRawBytes": 175720, "otherRawBytes": 0, "requestCount": 7, - "scriptRawBytes": 368044, + "scriptRawBytes": 368071, "scriptRequestCount": 6, - "totalRawBytes": 383612 + "totalRawBytes": 383638 }, - "domContentLoadedMedianMs": 93.60000002384186, + "domContentLoadedMedianMs": 94.79999995231628, "id": "editor-workflow", - "layoutAckMedianMs": 2.100000023841858, - "layoutReturnSwitchMedianMs": 29.299999952316284, + "layoutAckMedianMs": 1.9000000953674316, + "layoutReturnSwitchMedianMs": 29.300000071525574, "layoutSwitchMedianMs": 75.5, - "loadMedianMs": 93.70000004768372, - "longestTaskMaxMs": 104, + "loadMedianMs": 94.79999995231628, + "longestTaskMaxMs": 103, "owner": "workflow", "projectSwitchMedianMs": 0, "readyMark": "invokeai:ready:widget:center:workflow", @@ -401,24 +401,24 @@ "fontRawBytes": 23664, "imageRawBytes": 156376, "largestAssetRawBytes": 746121, - "otherRawBytes": 169609, - "requestCount": 129, - "scriptRawBytes": 3801881, - "scriptRequestCount": 123, - "totalRawBytes": 4169102 + "otherRawBytes": 169687, + "requestCount": 130, + "scriptRawBytes": 3804046, + "scriptRequestCount": 124, + "totalRawBytes": 4171345 }, "resourceLimits": { "cssRawBytes": 17748, "fontRawBytes": 23901, "imageRawBytes": 157940, "largestAssetRawBytes": 753583, - "otherRawBytes": 171306, - "requestCount": 129, - "scriptRawBytes": 3839900, - "scriptRequestCount": 123, - "totalRawBytes": 4210794 + "otherRawBytes": 171384, + "requestCount": 130, + "scriptRawBytes": 3842087, + "scriptRequestCount": 124, + "totalRawBytes": 4213059 }, - "routeReadyMedianMs": 1032.8999999761581, + "routeReadyMedianMs": 1047.6999999284744, "scriptSourceOwnerSet": "editor-workflow-static", "stateProfile": "empty" }, @@ -430,9 +430,9 @@ "largestAssetRawBytes": 173980, "otherRawBytes": 0, "requestCount": 7, - "scriptRawBytes": 364400, + "scriptRawBytes": 364426, "scriptRequestCount": 6, - "totalRawBytes": 379813 + "totalRawBytes": 379839 }, "activatedResourceLimits": { "cssRawBytes": 15568, @@ -441,17 +441,17 @@ "largestAssetRawBytes": 175720, "otherRawBytes": 0, "requestCount": 7, - "scriptRawBytes": 368044, + "scriptRawBytes": 368071, "scriptRequestCount": 6, - "totalRawBytes": 383612 + "totalRawBytes": 383638 }, - "domContentLoadedMedianMs": 96, + "domContentLoadedMedianMs": 93.39999997615814, "id": "editor-workflow", - "layoutAckMedianMs": 3.299999952316284, - "layoutReturnSwitchMedianMs": 35.299999952316284, - "layoutSwitchMedianMs": 110.10000002384186, - "loadMedianMs": 96.09999990463257, - "longestTaskMaxMs": 277, + "layoutAckMedianMs": 2.6999999284744263, + "layoutReturnSwitchMedianMs": 34.60000002384186, + "layoutSwitchMedianMs": 108.5, + "loadMedianMs": 93.39999997615814, + "longestTaskMaxMs": 288, "owner": "workflow", "projectSwitchMedianMs": 0, "readyMark": "invokeai:ready:widget:center:workflow", @@ -461,24 +461,24 @@ "fontRawBytes": 23664, "imageRawBytes": 156376, "largestAssetRawBytes": 746121, - "otherRawBytes": 169609, - "requestCount": 129, - "scriptRawBytes": 3801881, - "scriptRequestCount": 123, - "totalRawBytes": 4169102 + "otherRawBytes": 169687, + "requestCount": 130, + "scriptRawBytes": 3804046, + "scriptRequestCount": 124, + "totalRawBytes": 4171345 }, "resourceLimits": { "cssRawBytes": 17748, "fontRawBytes": 23901, "imageRawBytes": 157940, "largestAssetRawBytes": 753583, - "otherRawBytes": 171306, - "requestCount": 129, - "scriptRawBytes": 3839900, - "scriptRequestCount": 123, - "totalRawBytes": 4210794 + "otherRawBytes": 171384, + "requestCount": 130, + "scriptRawBytes": 3842087, + "scriptRequestCount": 124, + "totalRawBytes": 4213059 }, - "routeReadyMedianMs": 1084.5, + "routeReadyMedianMs": 1069.5, "scriptSourceOwnerSet": "editor-workflow-static", "stateProfile": "representative" }, @@ -505,13 +505,13 @@ "scriptRequestCount": 0, "totalRawBytes": 0 }, - "domContentLoadedMedianMs": 93, + "domContentLoadedMedianMs": 98.09999990463257, "id": "editor-gallery", "layoutAckMedianMs": 0, "layoutReturnSwitchMedianMs": 0, - "layoutSwitchMedianMs": 20.700000047683716, - "loadMedianMs": 93.10000002384186, - "longestTaskMaxMs": 105, + "layoutSwitchMedianMs": 20.300000071525574, + "loadMedianMs": 98.29999995231628, + "longestTaskMaxMs": 103, "owner": "gallery", "projectSwitchMedianMs": 0, "readyMark": "invokeai:ready:widget:center:gallery", @@ -521,24 +521,24 @@ "fontRawBytes": 23664, "imageRawBytes": 156376, "largestAssetRawBytes": 746121, - "otherRawBytes": 169609, - "requestCount": 122, - "scriptRawBytes": 3437481, - "scriptRequestCount": 117, - "totalRawBytes": 3789289 + "otherRawBytes": 169687, + "requestCount": 123, + "scriptRawBytes": 3439620, + "scriptRequestCount": 118, + "totalRawBytes": 3791506 }, "resourceLimits": { "cssRawBytes": 2181, "fontRawBytes": 23901, "imageRawBytes": 157940, "largestAssetRawBytes": 753583, - "otherRawBytes": 171306, - "requestCount": 122, - "scriptRawBytes": 3471856, - "scriptRequestCount": 117, - "totalRawBytes": 3827182 + "otherRawBytes": 171384, + "requestCount": 123, + "scriptRawBytes": 3474017, + "scriptRequestCount": 118, + "totalRawBytes": 3829422 }, - "routeReadyMedianMs": 1232.6000000238419, + "routeReadyMedianMs": 1236.4000000953674, "scriptSourceOwnerSet": "editor-minimal-static", "stateProfile": "empty" }, @@ -565,13 +565,13 @@ "scriptRequestCount": 0, "totalRawBytes": 0 }, - "domContentLoadedMedianMs": 98.70000004768372, + "domContentLoadedMedianMs": 94.90000009536743, "id": "editor-gallery", "layoutAckMedianMs": 0, "layoutReturnSwitchMedianMs": 0, - "layoutSwitchMedianMs": 26.700000047683716, - "loadMedianMs": 98.80000007152557, - "longestTaskMaxMs": 101, + "layoutSwitchMedianMs": 27.200000047683716, + "loadMedianMs": 95, + "longestTaskMaxMs": 99, "owner": "gallery", "projectSwitchMedianMs": 0, "readyMark": "invokeai:ready:widget:center:gallery", @@ -581,24 +581,24 @@ "fontRawBytes": 23664, "imageRawBytes": 156376, "largestAssetRawBytes": 746121, - "otherRawBytes": 169609, - "requestCount": 122, - "scriptRawBytes": 3437481, - "scriptRequestCount": 117, - "totalRawBytes": 3789289 + "otherRawBytes": 169687, + "requestCount": 123, + "scriptRawBytes": 3439620, + "scriptRequestCount": 118, + "totalRawBytes": 3791506 }, "resourceLimits": { "cssRawBytes": 2181, "fontRawBytes": 23901, "imageRawBytes": 157940, "largestAssetRawBytes": 753583, - "otherRawBytes": 171306, - "requestCount": 122, - "scriptRawBytes": 3471856, - "scriptRequestCount": 117, - "totalRawBytes": 3827182 + "otherRawBytes": 171384, + "requestCount": 123, + "scriptRawBytes": 3474017, + "scriptRequestCount": 118, + "totalRawBytes": 3829422 }, - "routeReadyMedianMs": 1265.8000000715256, + "routeReadyMedianMs": 1245.8000000715256, "scriptSourceOwnerSet": "editor-minimal-static", "stateProfile": "representative" } @@ -804,6 +804,7 @@ "source:src/platform/browser/downloadBlob.ts", "source:src/platform/browser/knownBrowserIssues.ts", "source:src/platform/browser/randomUuid.ts", + "source:src/platform/browser/sha256.ts", "source:src/platform/core/concurrency.ts", "source:src/platform/i18n/client.ts", "source:src/platform/i18n/languages.ts", @@ -1386,6 +1387,7 @@ "source:src/platform/browser/downloadBlob.ts", "source:src/platform/browser/knownBrowserIssues.ts", "source:src/platform/browser/randomUuid.ts", + "source:src/platform/browser/sha256.ts", "source:src/platform/browser/webLocks.ts", "source:src/platform/core/concurrency.ts", "source:src/platform/core/json.ts", @@ -2374,6 +2376,7 @@ "source:src/platform/browser/downloadBlob.ts", "source:src/platform/browser/knownBrowserIssues.ts", "source:src/platform/browser/randomUuid.ts", + "source:src/platform/browser/sha256.ts", "source:src/platform/browser/webLocks.ts", "source:src/platform/core/concurrency.ts", "source:src/platform/core/json.ts", @@ -3574,6 +3577,7 @@ "source:src/platform/browser/downloadBlob.ts", "source:src/platform/browser/knownBrowserIssues.ts", "source:src/platform/browser/randomUuid.ts", + "source:src/platform/browser/sha256.ts", "source:src/platform/browser/webLocks.ts", "source:src/platform/core/concurrency.ts", "source:src/platform/core/json.ts", diff --git a/invokeai/frontend/webv2/src/architecture/secureContextApis.test.ts b/invokeai/frontend/webv2/src/architecture/secureContextApis.test.ts index e9c2470f038..a96779b9025 100644 --- a/invokeai/frontend/webv2/src/architecture/secureContextApis.test.ts +++ b/invokeai/frontend/webv2/src/architecture/secureContextApis.test.ts @@ -1,11 +1,12 @@ import { describe, expect, it } from 'vitest'; /** - * `crypto.randomUUID` is undefined outside secure contexts, and the app is - * routinely opened over plain HTTP from another device on the LAN. Every id - * mint goes through `@platform/browser/randomUuid`, which falls back to - * `getRandomValues`; a direct call anywhere else reintroduces a startup crash - * that only shows up on a phone or tablet. + * `crypto.randomUUID` and `crypto.subtle` are undefined outside secure contexts, + * and the app is routinely opened over plain HTTP from another device on the + * LAN. Every id mint goes through `@platform/browser/randomUuid` and every + * digest through `@platform/browser/sha256`, which carry fallbacks; a direct + * call anywhere else reintroduces a crash that only shows up on a phone or + * tablet. */ const sources = import.meta.glob('../**/*.{ts,tsx}', { eager: true, @@ -13,14 +14,15 @@ const sources = import.meta.glob('../**/*.{ts,tsx}', { query: '?raw', }) as Record; -const HELPER_PATH = '../platform/browser/randomUuid.ts'; +const HELPER_PATHS = new Set(['../platform/browser/randomUuid.ts', '../platform/browser/sha256.ts']); +const SECURE_CONTEXT_ONLY = /\brandomUUID\b|\bcrypto\s*\.\s*subtle\b|\bsubtle\s*\.\s*digest\b/; const isTestFile = (path: string): boolean => /\.(test|spec|stories)\.tsx?$/.test(path); describe('secure-context web APIs', () => { - it('routes every production UUID mint through the randomUuid helper', () => { + it('routes every production UUID mint and SHA-256 digest through the platform helpers', () => { const offenders = Object.entries(sources) - .filter(([path]) => path !== HELPER_PATH && !isTestFile(path)) - .filter(([, text]) => /\brandomUUID\b/.test(text)) + .filter(([path]) => !HELPER_PATHS.has(path) && !isTestFile(path)) + .filter(([, text]) => SECURE_CONTEXT_ONLY.test(text)) .map(([path]) => path.replace(/^\.\.\//, '')); expect(offenders).toEqual([]); diff --git a/invokeai/frontend/webv2/src/platform/browser/sha256.test.ts b/invokeai/frontend/webv2/src/platform/browser/sha256.test.ts new file mode 100644 index 00000000000..1e119f84997 --- /dev/null +++ b/invokeai/frontend/webv2/src/platform/browser/sha256.test.ts @@ -0,0 +1,64 @@ +import { sha256, sha256Fallback, sha256Hex } from '@platform/browser/sha256'; +import { describe, expect, it, vi } from 'vitest'; + +const encode = (text: string): Uint8Array => new TextEncoder().encode(text); +const hex = (bytes: Uint8Array): string => Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join(''); + +const nativeHex = async (bytes: Uint8Array): Promise => + hex(new Uint8Array(await crypto.subtle.digest('SHA-256', bytes as Uint8Array))); + +describe('sha256Fallback', () => { + it('matches the published test vectors', () => { + expect(hex(sha256Fallback(encode('')))).toBe('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'); + expect(hex(sha256Fallback(encode('abc')))).toBe('ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'); + expect(hex(sha256Fallback(encode('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq')))).toBe( + '248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1' + ); + }); + + it('agrees with Web Crypto across the padding boundaries', async () => { + for (const length of [0, 1, 55, 56, 57, 63, 64, 65, 119, 120, 128, 1000, 70_000]) { + const bytes = new Uint8Array(length); + for (let index = 0; index < length; index += 1) { + bytes[index] = (index * 31 + length) & 0xff; + } + expect(hex(sha256Fallback(bytes)), `length ${length}`).toBe(await nativeHex(bytes)); + } + }); + + it('hashes a subarray by its own bytes, not the whole backing buffer', async () => { + const backing = new Uint8Array(64).fill(0xab); + const window = backing.subarray(8, 24); + + expect(hex(sha256Fallback(window))).toBe(await nativeHex(new Uint8Array(window))); + }); +}); + +describe('sha256 / sha256Hex', () => { + it('uses the native digest when the context exposes one', async () => { + const digest = vi.fn(crypto.subtle.digest.bind(crypto.subtle)); + const subtle = { digest } as unknown as SubtleCrypto; + + expect(await sha256Hex(encode('abc'), subtle)).toBe( + 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' + ); + expect(digest).toHaveBeenCalledOnce(); + }); + + it('falls back to the JavaScript digest without one, with identical output', async () => { + const bytes = encode('The quick brown fox jumps over the lazy dog'); + + expect(await sha256Hex(bytes, undefined)).toBe(await nativeHex(bytes)); + expect(hex(await sha256(bytes.buffer as ArrayBuffer, undefined))).toBe(await nativeHex(bytes)); + }); + + it('survives an ambient crypto object that lacks subtle', async () => { + const original = globalThis.crypto; + vi.stubGlobal('crypto', { getRandomValues: original.getRandomValues.bind(original) }); + try { + expect(await sha256Hex(encode('abc'))).toBe('ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'); + } finally { + vi.unstubAllGlobals(); + } + }); +}); diff --git a/invokeai/frontend/webv2/src/platform/browser/sha256.ts b/invokeai/frontend/webv2/src/platform/browser/sha256.ts new file mode 100644 index 00000000000..2db33f73676 --- /dev/null +++ b/invokeai/frontend/webv2/src/platform/browser/sha256.ts @@ -0,0 +1,137 @@ +/** + * SHA-256, with a pure-JavaScript fallback for insecure contexts. + * + * `crypto.subtle` is exposed only in secure contexts, exactly like + * `crypto.randomUUID` (see `randomUuid.ts`). Opened over plain HTTP from another + * device on the LAN, `crypto.subtle` is undefined and every content hash — paint + * cache flushes, composites for generation, deterministic project ids — throws. + * The fallback produces byte-identical digests, so ids derived from a hash stay + * stable regardless of which path computed them. + * + * The fallback is slower than the native digest by a wide margin, so it is only + * taken when the native API is absent. All hashing goes through here; + * `crypto.subtle` must not be called directly elsewhere. + */ + +const ROUND_CONSTANTS = new Uint32Array([ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, + 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, + 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, + 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, + 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, + 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, + 0xc67178f2, +]); + +const INITIAL_STATE = new Uint32Array([ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, +]); + +const rotateRight = (value: number, bits: number): number => (value >>> bits) | (value << (32 - bits)); + +/** Pure-JavaScript SHA-256 over `message`, returning the 32-byte digest. */ +export const sha256Fallback = (message: Uint8Array): Uint8Array => { + const length = message.byteLength; + // One 0x80 byte and an 8-byte big-endian bit length, padded to a 64-byte block. + const paddedLength = Math.ceil((length + 9) / 64) * 64; + const padded = new Uint8Array(paddedLength); + padded.set(message); + padded[length] = 0x80; + const view = new DataView(padded.buffer); + // The bit length as two 32-bit words; `length / 2^29` is the high word of `length * 8`. + view.setUint32(paddedLength - 8, Math.floor(length / 0x20000000), false); + view.setUint32(paddedLength - 4, (length << 3) >>> 0, false); + + const state = new Uint32Array(INITIAL_STATE); + const schedule = new Uint32Array(64); + + for (let offset = 0; offset < paddedLength; offset += 64) { + for (let index = 0; index < 16; index += 1) { + schedule[index] = view.getUint32(offset + index * 4, false); + } + for (let index = 16; index < 64; index += 1) { + const w15 = schedule[index - 15]!; + const w2 = schedule[index - 2]!; + const s0 = rotateRight(w15, 7) ^ rotateRight(w15, 18) ^ (w15 >>> 3); + const s1 = rotateRight(w2, 17) ^ rotateRight(w2, 19) ^ (w2 >>> 10); + schedule[index] = (schedule[index - 16]! + s0 + schedule[index - 7]! + s1) >>> 0; + } + + let a = state[0]!; + let b = state[1]!; + let c = state[2]!; + let d = state[3]!; + let e = state[4]!; + let f = state[5]!; + let g = state[6]!; + let h = state[7]!; + + for (let index = 0; index < 64; index += 1) { + const bigSigma1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25); + const choose = (e & f) ^ (~e & g); + const t1 = (h + bigSigma1 + choose + ROUND_CONSTANTS[index]! + schedule[index]!) >>> 0; + const bigSigma0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22); + const majority = (a & b) ^ (a & c) ^ (b & c); + const t2 = (bigSigma0 + majority) >>> 0; + + h = g; + g = f; + f = e; + e = (d + t1) >>> 0; + d = c; + c = b; + b = a; + a = (t1 + t2) >>> 0; + } + + state[0] = (state[0]! + a) >>> 0; + state[1] = (state[1]! + b) >>> 0; + state[2] = (state[2]! + c) >>> 0; + state[3] = (state[3]! + d) >>> 0; + state[4] = (state[4]! + e) >>> 0; + state[5] = (state[5]! + f) >>> 0; + state[6] = (state[6]! + g) >>> 0; + state[7] = (state[7]! + h) >>> 0; + } + + const digest = new Uint8Array(32); + const digestView = new DataView(digest.buffer); + for (let index = 0; index < 8; index += 1) { + digestView.setUint32(index * 4, state[index]!, false); + } + return digest; +}; + +const toBytes = (data: ArrayBuffer | Uint8Array): Uint8Array => + data instanceof Uint8Array ? data : new Uint8Array(data); + +const getSubtleCrypto = (): SubtleCrypto | undefined => { + try { + return globalThis.crypto?.subtle; + } catch { + return undefined; + } +}; + +/** SHA-256 digest bytes of `data`, via Web Crypto when the context exposes it. */ +export const sha256 = async ( + data: ArrayBuffer | Uint8Array, + subtle: SubtleCrypto | undefined = getSubtleCrypto() +): Promise => { + const bytes = toBytes(data); + if (subtle) { + return new Uint8Array(await subtle.digest('SHA-256', bytes as Uint8Array)); + } + return sha256Fallback(bytes); +}; + +/** Lower-case hex SHA-256 of `data`. */ +export const sha256Hex = async (data: ArrayBuffer | Uint8Array, subtle?: SubtleCrypto | undefined): Promise => { + const digest = await sha256(data, subtle); + let hex = ''; + for (const byte of digest) { + hex += byte.toString(16).padStart(2, '0'); + } + return hex; +}; diff --git a/invokeai/frontend/webv2/src/workbench/canvas-engine/document/bitmapStore.ts b/invokeai/frontend/webv2/src/workbench/canvas-engine/document/bitmapStore.ts index 2730dd56bb5..bf35661b3b6 100644 --- a/invokeai/frontend/webv2/src/workbench/canvas-engine/document/bitmapStore.ts +++ b/invokeai/frontend/webv2/src/workbench/canvas-engine/document/bitmapStore.ts @@ -39,6 +39,8 @@ import type { CanvasProjectMutation } from '@workbench/canvas-engine/mutationCon import type { PaintCacheTrim } from '@workbench/canvas-engine/render/paintCacheTrim'; import type { RasterSurface } from '@workbench/canvas-engine/render/raster'; +import { sha256Hex } from '@platform/browser/sha256'; + /** Default idle window before a dirty layer is flushed. */ export const DEFAULT_DEBOUNCE_MS = 1500; /** Default upload attempts per flush (initial try + retries). */ @@ -126,7 +128,7 @@ export interface BitmapStoreDeps { * mask must clear `mask.bitmap` while preserving its `fill`. */ clearBitmap?(layerId: string): boolean; - /** Content-hashes a blob (defaults to SHA-256 hex via `crypto.subtle`). */ + /** Content-hashes a blob (defaults to SHA-256 hex via `@platform/browser/sha256`). */ hashBlob?(blob: Blob): Promise; /** Idle debounce window in ms (default {@link DEFAULT_DEBOUNCE_MS}). */ debounceMs?: number; @@ -200,17 +202,8 @@ const defaultTimers: BitmapStoreTimers = { setTimeout: (handler, ms) => globalThis.setTimeout(handler, ms), }; -/** SHA-256 hex of a blob's bytes, via the Web Crypto API (Node ≥ 20 exposes `crypto.subtle`). */ -const defaultHashBlob = async (blob: Blob): Promise => { - const buffer = await blob.arrayBuffer(); - const digest = await crypto.subtle.digest('SHA-256', buffer); - const bytes = new Uint8Array(digest); - let hex = ''; - for (const byte of bytes) { - hex += byte.toString(16).padStart(2, '0'); - } - return hex; -}; +/** SHA-256 hex of a blob's bytes, via `@platform/browser/sha256`. */ +const defaultHashBlob = async (blob: Blob): Promise => sha256Hex(await blob.arrayBuffer()); /** Creates a bitmap store wired to the given seams. */ export const createBitmapStore = (deps: BitmapStoreDeps): BitmapStore => { diff --git a/invokeai/frontend/webv2/src/workbench/canvas-operations/compositeForGeneration.ts b/invokeai/frontend/webv2/src/workbench/canvas-operations/compositeForGeneration.ts index ffe2c4f755e..c3ecea85107 100644 --- a/invokeai/frontend/webv2/src/workbench/canvas-operations/compositeForGeneration.ts +++ b/invokeai/frontend/webv2/src/workbench/canvas-operations/compositeForGeneration.ts @@ -34,23 +34,15 @@ import type { CompositePlan, } from '@workbench/canvas-operations/generationContracts'; +import { sha256Hex } from '@platform/browser/sha256'; import { fromTRS, multiply } from '@workbench/canvas-engine/math/mat2d'; import { renderRasterComposite } from '@workbench/canvas-engine/render/rasterComposite'; import { getCompositeLayerBounds } from '@workbench/canvas-operations/generationCompositePlan'; type Ctx = RasterSurface['ctx']; -/** SHA-256 hex of a blob's bytes, via the Web Crypto API (matches `bitmapStore`). */ -const defaultHashBlob = async (blob: Blob): Promise => { - const buffer = await blob.arrayBuffer(); - const digest = await crypto.subtle.digest('SHA-256', buffer); - const bytes = new Uint8Array(digest); - let hex = ''; - for (const byte of bytes) { - hex += byte.toString(16).padStart(2, '0'); - } - return hex; -}; +/** SHA-256 hex of a blob's bytes, via `@platform/browser/sha256` (matches `bitmapStore`). */ +const defaultHashBlob = async (blob: Blob): Promise => sha256Hex(await blob.arrayBuffer()); /** Reads a surface's pixels via its 2D context (real DOM path; injectable for tests). */ const defaultReadImageData = (surface: RasterSurface, rect: Rect): ImageData => @@ -129,7 +121,7 @@ export interface ExecuteCompositePlanDeps { uploadImage(blob: Blob): Promise; /** Persistent dedupe state (see {@link CompositeDedupeCache}). */ dedupe: CompositeDedupeCache; - /** Content-hashes a blob (default SHA-256 hex via `crypto.subtle`). */ + /** Content-hashes a blob (default SHA-256 hex via `@platform/browser/sha256`). */ hashBlob?(blob: Blob): Promise; /** Reads a surface region's pixels for the coverage scan (default `getImageData`). */ readImageData?(surface: RasterSurface, rect: Rect): ImageData; diff --git a/invokeai/frontend/webv2/src/workbench/projects/ids.ts b/invokeai/frontend/webv2/src/workbench/projects/ids.ts index 0fb44f08446..f9740a6eb55 100644 --- a/invokeai/frontend/webv2/src/workbench/projects/ids.ts +++ b/invokeai/frontend/webv2/src/workbench/projects/ids.ts @@ -1,9 +1,10 @@ import { createUuid } from '@platform/browser/randomUuid'; +import { sha256 } from '@platform/browser/sha256'; export const createProjectId = (): string => `project-${createUuid()}`; export const createDeterministicProjectId = async (scope: string): Promise => { - const digest = new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(scope))); + const digest = await sha256(new TextEncoder().encode(scope)); const bytes = digest.slice(0, 16); bytes[6] = (bytes[6]! & 0x0f) | 0x80; bytes[8] = (bytes[8]! & 0x3f) | 0x80; From f382fe2bee20f3a989c3ebd82636e88824315e83 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Sun, 6 Sep 2026 11:39:50 -0400 Subject: [PATCH 3/3] docs(frontend): reduce verbosity of inline crypt.* function comments --- .../src/architecture/secureContextApis.test.ts | 6 +----- .../webv2/src/platform/browser/randomUuid.ts | 13 ++----------- .../frontend/webv2/src/platform/browser/sha256.ts | 12 ++---------- 3 files changed, 5 insertions(+), 26 deletions(-) diff --git a/invokeai/frontend/webv2/src/architecture/secureContextApis.test.ts b/invokeai/frontend/webv2/src/architecture/secureContextApis.test.ts index a96779b9025..1d82dd22f3e 100644 --- a/invokeai/frontend/webv2/src/architecture/secureContextApis.test.ts +++ b/invokeai/frontend/webv2/src/architecture/secureContextApis.test.ts @@ -2,11 +2,7 @@ import { describe, expect, it } from 'vitest'; /** * `crypto.randomUUID` and `crypto.subtle` are undefined outside secure contexts, - * and the app is routinely opened over plain HTTP from another device on the - * LAN. Every id mint goes through `@platform/browser/randomUuid` and every - * digest through `@platform/browser/sha256`, which carry fallbacks; a direct - * call anywhere else reintroduces a crash that only shows up on a phone or - * tablet. + * so filter these calls out when running over plain HTTP. */ const sources = import.meta.glob('../**/*.{ts,tsx}', { eager: true, diff --git a/invokeai/frontend/webv2/src/platform/browser/randomUuid.ts b/invokeai/frontend/webv2/src/platform/browser/randomUuid.ts index c2433111ea9..2a0d4d2c6d8 100644 --- a/invokeai/frontend/webv2/src/platform/browser/randomUuid.ts +++ b/invokeai/frontend/webv2/src/platform/browser/randomUuid.ts @@ -1,17 +1,8 @@ /** * Generate an RFC 4122 version 4 UUID. * - * `crypto.randomUUID` is exposed only in secure contexts — HTTPS, or `localhost` - * over plain HTTP. The app is routinely opened from another device on the LAN - * (an iPad pointed at a workstation, say), and there the property is simply - * undefined, so a bare `crypto.randomUUID()` throws. The durable persistence - * layer mints a writer token during startup, which turned that into a crash - * before first paint. - * - * `crypto.getRandomValues` carries no secure-context restriction, so when the - * native generator is missing the UUID is assembled from sixteen random bytes - * with the version and variant bits set by hand. Every call site that needs a - * fresh id goes through here; `crypto.randomUUID` must not be called directly. + * `crypto.randomUUID` is applied in secure contexts (HTTPS or `localhost`). + * `crypto.getRandomValues` is applied in non-secure contexts. */ /** The subset of `Crypto` the generator relies on, so tests can swap it. */ diff --git a/invokeai/frontend/webv2/src/platform/browser/sha256.ts b/invokeai/frontend/webv2/src/platform/browser/sha256.ts index 2db33f73676..c390d1237ba 100644 --- a/invokeai/frontend/webv2/src/platform/browser/sha256.ts +++ b/invokeai/frontend/webv2/src/platform/browser/sha256.ts @@ -1,16 +1,8 @@ /** * SHA-256, with a pure-JavaScript fallback for insecure contexts. * - * `crypto.subtle` is exposed only in secure contexts, exactly like - * `crypto.randomUUID` (see `randomUuid.ts`). Opened over plain HTTP from another - * device on the LAN, `crypto.subtle` is undefined and every content hash — paint - * cache flushes, composites for generation, deterministic project ids — throws. - * The fallback produces byte-identical digests, so ids derived from a hash stay - * stable regardless of which path computed them. - * - * The fallback is slower than the native digest by a wide margin, so it is only - * taken when the native API is absent. All hashing goes through here; - * `crypto.subtle` must not be called directly elsewhere. + * `crypto.subtle` is exposed only in secure contexts, otherwise a slower fallback + * that produces identical digests is applied. */ const ROUND_CONSTANTS = new Uint32Array([