chainlesschain 0.165.6 → 0.165.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/assets/web-panel/assets/{AIOps-vfoDb6HZ.js → AIOps-B3gWmXjo.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-Cm5f7p93.js → ActionButton-G9pdT-0e.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-yRuvgWxa.js → Analytics-Ca30J7m2.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-BROsC4h1.js → AppLayout-BtVTm8PU.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-CimHApb1.js → Artifacts-BhViNa6u.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-DI8kr8zI.js → Audit-DhKq-FnY.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DSLzKy-n.js → BackgroundAgents-BL0SVbnE.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DkH_n2r1.js → Backup-BWkblnFi.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CKSrUoRA.js → BaseInput-ClaRNrF8.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-ClwfMXUS.js → Chat-sZE2Z77o.js} +4 -4
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CjMaqfL7.js → ChatBubbleRenderer-BAfKOnRe.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BYTehcQG.js → Checkbox-Cvhay5WM.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-B8AMcp6J.js → Codegen-BpejL-4N.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BNSogPo3.js → Col-g2Ki6g_1.js} +1 -1
- package/src/assets/web-panel/assets/{Community-JxPvGb8O.js → Community-D2xKbNo9.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DF7bFAgE.js → Compact-DbL1Pds3.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-Bvhqnwrq.js → Compliance-DGtZCV4c.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BDS0CJvM.js → Cowork-4kQpoNsI.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DqvRfxoQ.js → Cron-BxTYXXAN.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-CDcIvnqS.js → Crosschain-k0J0LT2N.js} +1 -1
- package/src/assets/web-panel/assets/{DID-D7jaUxU3.js → DID-BaqhoPgN.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-B5A18wEV.js → Dashboard-T4RVPIM1.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-_6IVkGag.js → Dropdown-DszOToWq.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-C9PeiKCG.js → EmailListRenderer-C9KJU7HF.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-DXjuEHrX.js → FamilyGuardDashboard-Cmr0JnsA.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-B4IAR9zv.js → Federation-BI-_2xZX.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CxQeSNx1.js → FormItemContext-DsnDEPsl.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-B8GS-Y3N.js → GenericCardRenderer-BLncAKKK.js} +1 -1
- package/src/assets/web-panel/assets/{Git-BS8SouUX.js → Git-BPQhGXDn.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-Dq9aYb5Q.js → Governance-CyElbUQW.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BrCEe9AJ.js → Inference-CcSntBQD.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DybOpiY2.js → KnowledgeGraph-p6e7HZ2r.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DBpzbRfx.js → Logs-CNEtr-wz.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-Bj9xI0YH.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-Cc7CCSCu.js → Marketplace-Dd2notAJ.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DFaIhOfu.js → McpTools-BBsN2CSW.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-KCqlp9ol.js → Memory-Dg2uTvwx.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-Bys84ySN.js → MobileBridge-r1YMRiJV.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-B-6qU-je.js → MobileProjects-C-89kSm4.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BgY-fvCq.js → Mtc-CrPZcVgf.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-B8pCmqQJ.js → MtcAudit-sxi7hqJb.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-BbOmYutA.js → Multisig-CAgOMfF1.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-BocLQz_M.js → NLProgramming-DafaPts3.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CSwbxEop.js → Notes-CJM-xUW_.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-CqFNSYG3.js → NotificationSettings-L0ZjfAh_.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-C6gMfAk7.js → OrderTableRenderer-BB1CvCK0.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-QrDXVdMJ.js → Organization-DeLtgAD8.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Cxntotjt.js → Overflow-CKC2b10z.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CIdBOwNq.js → P2P-eyf2rzY1.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-CT5aP8rD.js → PdhVaultBrowser-5upNIOW8.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-DH7OM8XU.js → Permissions-B8p3W6JK.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-CnJqbWce.js → PersonalDataHub-BnRMxTq3.js} +5 -5
- package/src/assets/web-panel/assets/{Pipeline-5C3HG1kH.js → Pipeline-DjCNI1li.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-B4yIp7xK.js → Privacy-u6gXLbWT.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BMvt-2M0.js → ProjectInit-BZAw4tkM.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CvSTEMRG.js → ProjectSettings-B1Tr72q7.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BKBJxEek.js → Projects-DO7HFuBQ.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-BdwUqFWN.js → Providers-BjCztyhj.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-DtnCKlTm.js → QrScannerModal-ZWyCUbR1.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-sTbgwyNr.js → QuickAsk-DhDjeW4G.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Bzr14gSb.js → Recommend-R_CN2MuU.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-C_TF5IaJ.js → RemoteSession-uxFcujeu.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-GCAM4H2K.js → Reputation-DX8D7rks.js} +1 -1
- package/src/assets/web-panel/assets/{Row-Dir0r6OQ.js → Row-BYahQILD.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-CpGnlecX.js → RssFeed-CUSQ2tm-.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DMMamC6u.js → Search-CnN_S8Cs.js} +1 -1
- package/src/assets/web-panel/assets/{Security-yqay5yG-.js → Security-DcZlh-O4.js} +4 -4
- package/src/assets/web-panel/assets/{Services-C5i2wh9G.js → Services-ZhBOjzIX.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-vUqk35yj.js → Skeleton-D5Qnq_lI.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DzcSkhot.js → Skills-BC9cZ3Be.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-CB6qbYNr.js → Sla-DauJ3E8_.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-DbNeZ_fh.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-D-RrsxTJ.js → SyncSettings-DUvJbSct.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-DFxdOJiq.js → Tasks-BXa-5noQ.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-SKAob_Aq.js → Templates-JxaiUt_3.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BPfsfHQ-.js → Tenant-BW4ERQN8.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-8iw7FpUf.js → Terminal-DltUbdtv.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-C4Gjj5Su.js → TimelineRenderer-CMoZz0Tc.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-COYhN18e.js → Tokens-D_VEV60K.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-B-JzXcf6.js → Trigger-CCXTCs0R.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BJ5bg0nI.js → Trust-lkGRIPcG.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-BG_ByRMf.js → UkeySign-BfGeQ9HX.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B3kOnSiU.js → VideoEditing-BEC7pYvc.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CcoGFnK7.js → Wallet-BZdG24o0.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DdT8vRC_.js → WebAuthn-yoEddedp.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-CZu3M9N8.js → WorkflowEditor-BXZEprOW.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DxJt3Qba.js → chat-gg0bzZal.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DdYfwJaB.js → colors-DjRVRuin.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BG3SUzwj.js → compact-item-su-dieAc.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-By51z2-1.js → createContext-Ci3S8oKF.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CxbFX9uH.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-B0apZ3KJ.js → hasIn-D8F10heH.js} +1 -1
- package/src/assets/web-panel/assets/{index-DE76D-kA.js → index-11wtYQiF.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkvcEo4p.js → index-1IgxgALJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bos5_aMV.js → index-1agckWSq.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfWYsB9t.js → index-5JqhujnR.js} +1 -1
- package/src/assets/web-panel/assets/{index-CAhHirVH.js → index-6Tniqk1s.js} +1 -1
- package/src/assets/web-panel/assets/{index-BYINoRLK.js → index-8Z-8BIlN.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5rLiEo0.js → index-BCI5v0yB.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZ_0qm4g.js → index-BDJntYmJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CWEuZWCK.js → index-BGzixOGZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BLIbGOLX.js → index-BMMOVoLD.js} +1 -1
- package/src/assets/web-panel/assets/{index-bP_VlDb4.js → index-BTFpdBtt.js} +1 -1
- package/src/assets/web-panel/assets/{index-B3rWuPMW.js → index-Ba_HyPnE.js} +1 -1
- package/src/assets/web-panel/assets/{index-Da_WBdLX.js → index-BbF1IHof.js} +1 -1
- package/src/assets/web-panel/assets/{index-RD6O-0C6.js → index-BgT9su_e.js} +1 -1
- package/src/assets/web-panel/assets/{index-CpjZhgNd.js → index-BzPB6kGZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Q2SCX-MI.js → index-C6ygzplr.js} +1 -1
- package/src/assets/web-panel/assets/{index-BIrJfqO4.js → index-CC3ZDjzY.js} +1 -1
- package/src/assets/web-panel/assets/{index-BBPf3TpM.js → index-CEmXRhAE.js} +1 -1
- package/src/assets/web-panel/assets/index-CJGOlduY.js +1 -0
- package/src/assets/web-panel/assets/{index-Cp6e-hGw.js → index-CMPqd5Cv.js} +1 -1
- package/src/assets/web-panel/assets/{index-kf57LJ93.js → index-COWd8uGx.js} +1 -1
- package/src/assets/web-panel/assets/{index-CW93tZDv.js → index-CPIYSp4H.js} +1 -1
- package/src/assets/web-panel/assets/{index-BewKYrX9.js → index-CR0h6seT.js} +1 -1
- package/src/assets/web-panel/assets/{index-p1_8SbI1.js → index-CUmDhC4j.js} +1 -1
- package/src/assets/web-panel/assets/index-CkBCN5yH.js +1 -0
- package/src/assets/web-panel/assets/{index-ZxtP8jG7.js → index-CusgZPtc.js} +1 -1
- package/src/assets/web-panel/assets/{index-BWF8_mkx.js → index-DBAETXft.js} +1 -1
- package/src/assets/web-panel/assets/{index-BP94CrBV.js → index-DIJ2qFOC.js} +1 -1
- package/src/assets/web-panel/assets/{index-CNIxXMmN.js → index-Dbq4NMET.js} +1 -1
- package/src/assets/web-panel/assets/{index-CzmQElZy.js → index-DkMEF5VT.js} +1 -1
- package/src/assets/web-panel/assets/{index-ZFbps9po.js → index-DkfQR3hM.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dz49VOyQ.js → index-DnLucOn9.js} +1 -1
- package/src/assets/web-panel/assets/{index-oxRrmFO9.js → index-DugB_aKa.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bissefox.js → index-DyLUarhZ.js} +3 -3
- package/src/assets/web-panel/assets/{index-DaIz0NAW.js → index-T6_6ygJZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-SQqOlR3d.js → index-Tu8eWqh-.js} +1 -1
- package/src/assets/web-panel/assets/{index-DCn8IcxV.js → index-W7XUgdVL.js} +1 -1
- package/src/assets/web-panel/assets/{index-3jWkIu5j.js → index-dHZinfko.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1C9i79A.js → index-jyNgp5FI.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BWJktcg8.js → initDefaultProps-Cv4p8e4w.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BEXBzUFV.js → motion-B8EwmMOw.js} +1 -1
- package/src/assets/web-panel/assets/{move-BUKwn2sx.js → move-B8R9F8TK.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-FTI7btqQ.js → mtc-parser-DoFYz5Bp.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CsLtSPH3.js → omit-CHWYLBjI.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DWZxsT_S.js → pickAttrs-CXuPDO77.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-Bcfs43CX.js → placementArrow-BaAtUUon.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-a9bcC1p-.js → responsiveObserve-DCc4zcDD.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BDTzgOXN.js → slide-B_lx89oZ.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-Iq1j_HUg.js → statusUtils-DE8DPmR0.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DAZEYxeh.js → styleChecker-1ktXOsL_.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DVkjyDrh.js → useFlexGapSupport-xH89-wjh.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-wXTpNTHt.js → useFs-DscvfW5p.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CtQM_aZr.js → usePersonalDataHub-RdMKQta7.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-hjEoG85v.js → vnode-Z3wTni2T.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-BHVpyKg_.js → zoom-B0iChCsm.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +3 -3
- package/src/commands/agent.js +91 -60
- package/src/commands/git.js +39 -0
- package/src/commands/plugin.js +17 -2
- package/src/commands/session-group.js +131 -0
- package/src/commands/session-location.js +48 -0
- package/src/commands/session.js +11 -0
- package/src/commands/sync-providers.js +4 -1
- package/src/commands/update.js +1 -0
- package/src/data/changelog.json +27 -1
- package/src/gateways/ws/ws-agent-handler.js +20 -12
- package/src/harness/jsonl-session-store.js +279 -37
- package/src/harness/mcp-client.js +701 -91
- package/src/harness/session-index.js +15 -4
- package/src/lib/agent-worktree.js +8 -1
- package/src/lib/auto-mode-safety-classifier.js +3 -17
- package/src/lib/background-agent-keeper-protocol.js +26 -6
- package/src/lib/background-agent-supervisor.js +17 -1
- package/src/lib/browser-evidence.js +1405 -0
- package/src/lib/chrome-connector.js +465 -79
- package/src/lib/claude-project-auto-memory.js +481 -0
- package/src/lib/claude-project-storage-layout.js +104 -0
- package/src/lib/cloud/cloud-client.js +40 -0
- package/src/lib/execution-location-local-supervisor.mjs +226 -0
- package/src/lib/execution-location-runner-lifecycle.js +991 -0
- package/src/lib/execution-location-target-preflight.js +333 -0
- package/src/lib/execution-location-target.js +783 -13
- package/src/lib/file-lines.js +6 -1
- package/src/lib/gateway-client-resilience.js +571 -0
- package/src/lib/gitlab-mr-worktree.js +176 -0
- package/src/lib/hooks-v2-producers.js +119 -15
- package/src/lib/hooks-v2-workspace-context.js +44 -1
- package/src/lib/host-resource-budget.js +295 -0
- package/src/lib/ide-context.js +15 -2
- package/src/lib/llm-config-defaults.js +43 -0
- package/src/lib/mcp-lifecycle-authority.js +887 -0
- package/src/lib/mcp-oauth.js +101 -20
- package/src/lib/mcp-tls.js +103 -3
- package/src/lib/paths.js +260 -32
- package/src/lib/permission-request.js +48 -0
- package/src/lib/plugin-runtime/bin.js +3 -1
- package/src/lib/plugin-runtime/hooks.js +3 -1
- package/src/lib/plugin-runtime/install.js +117 -1
- package/src/lib/plugin-runtime/lsp.js +4 -4
- package/src/lib/plugin-runtime/marketplace-archive-source.js +40 -5
- package/src/lib/plugin-runtime/marketplace-catalog.js +13 -2
- package/src/lib/plugin-runtime/marketplace-command-descriptor.js +343 -0
- package/src/lib/plugin-runtime/marketplace-command-source.js +580 -0
- package/src/lib/plugin-runtime/marketplace-source-adapter.js +33 -8
- package/src/lib/plugin-runtime/mcp.js +3 -1
- package/src/lib/plugin-runtime/monitors.js +3 -1
- package/src/lib/plugin-runtime/remote-source.js +151 -6
- package/src/lib/plugin-runtime/settings.js +3 -1
- package/src/lib/plugin-runtime/trust.js +274 -13
- package/src/lib/process-execution-broker/credential-agent.js +210 -41
- package/src/lib/process-execution-broker/index.js +15 -1
- package/src/lib/process-execution-broker/linux-cgroup-v2.js +391 -0
- package/src/lib/process-execution-broker/platform-sandbox.js +50 -28
- package/src/lib/project-mcp-trust.js +165 -10
- package/src/lib/resume-session.js +9 -6
- package/src/lib/session-core-singletons.js +26 -11
- package/src/lib/session-host-lease.js +52 -16
- package/src/lib/session-lifecycle-scope.js +153 -0
- package/src/lib/session-message-fabric.js +168 -21
- package/src/lib/session-projection.js +244 -4
- package/src/lib/session-store-guard.js +3 -1
- package/src/lib/session-workbench-store.js +466 -0
- package/src/lib/spellcheck.js +211 -0
- package/src/lib/sub-agent-context.js +9 -9
- package/src/lib/sync-cli-db.js +485 -129
- package/src/lib/sync-credentials.js +31 -20
- package/src/lib/web-fetch.js +112 -40
- package/src/lib/web-search.js +127 -61
- package/src/lib/workspace-trust.js +793 -0
- package/src/repl/agent-repl.js +124 -5
- package/src/repl/permission-prompt.js +10 -12
- package/src/runtime/agent-core.js +311 -18
- package/src/runtime/bootstrap.js +37 -13
- package/src/runtime/coding-agent-contract-shared.cjs +11 -4
- package/src/runtime/headless-runner.js +234 -36
- package/src/runtime/headless-stream.js +181 -6
- package/src/runtime/mcp-config.js +298 -8
- package/src/runtime/policies/agent-policy.js +8 -0
- package/src/runtime/status-diagnostics-lite.js +9 -1
- package/src/assets/web-panel/assets/MarkdownRenderer-CFyt8iUd.js +0 -1
- package/src/assets/web-panel/assets/SpeechSettings-BHUI7cox.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DorRZcI9.js +0 -1
- package/src/assets/web-panel/assets/index-BETiL0a3.js +0 -1
- package/src/assets/web-panel/assets/index-BhAVHuW8.js +0 -1
|
@@ -0,0 +1,1405 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { redactSecrets } from "./secret-scan.js";
|
|
3
|
+
|
|
4
|
+
export const BROWSER_EVIDENCE_SCHEMA =
|
|
5
|
+
"chainlesschain.browser-evidence-envelope.v1";
|
|
6
|
+
export const BROWSER_ORIGIN_GRANT_SCHEMA =
|
|
7
|
+
"chainlesschain.browser-origin-grant.v1";
|
|
8
|
+
export const CLAUDE_INCREMENT_AUDIT_FRAGMENT_SCHEMA =
|
|
9
|
+
"chainlesschain.claude-code-increment-audit-fragment.v1";
|
|
10
|
+
|
|
11
|
+
export const BROWSER_ORIGIN_SCOPES = Object.freeze([
|
|
12
|
+
"observe",
|
|
13
|
+
"act",
|
|
14
|
+
"navigate",
|
|
15
|
+
"upload",
|
|
16
|
+
"download",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const EXACT_SHA_RE = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u;
|
|
20
|
+
const SHA256_RE = /^sha256:[a-f0-9]{64}$/u;
|
|
21
|
+
const SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,255}$/u;
|
|
22
|
+
const SIDE_EFFECT_BY_ACTION = Object.freeze({
|
|
23
|
+
assertText: "none",
|
|
24
|
+
screenshot: "none",
|
|
25
|
+
waitForSelector: "none",
|
|
26
|
+
click: "page",
|
|
27
|
+
navigate: "page",
|
|
28
|
+
press: "page",
|
|
29
|
+
type: "page",
|
|
30
|
+
upload: "credential",
|
|
31
|
+
download: "filesystem",
|
|
32
|
+
});
|
|
33
|
+
const SCOPE_BY_ACTION = Object.freeze({
|
|
34
|
+
assertText: "observe",
|
|
35
|
+
screenshot: "observe",
|
|
36
|
+
waitForSelector: "observe",
|
|
37
|
+
click: "act",
|
|
38
|
+
press: "act",
|
|
39
|
+
type: "act",
|
|
40
|
+
navigate: "navigate",
|
|
41
|
+
upload: "upload",
|
|
42
|
+
download: "download",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function actionCarriesCredentialMaterial(action) {
|
|
46
|
+
const type = String(action?.type || "");
|
|
47
|
+
if (["type", "upload"].includes(type)) return true;
|
|
48
|
+
if (type !== "navigate") return false;
|
|
49
|
+
const target = new URL(String(action.url));
|
|
50
|
+
return target.search.length > 0 || target.hash.length > 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function canonicalBrowserEvidenceJson(value) {
|
|
54
|
+
if (value === null || typeof value !== "object") {
|
|
55
|
+
return JSON.stringify(value);
|
|
56
|
+
}
|
|
57
|
+
if (Array.isArray(value)) {
|
|
58
|
+
return `[${value.map((item) => canonicalBrowserEvidenceJson(item)).join(",")}]`;
|
|
59
|
+
}
|
|
60
|
+
return `{${Object.keys(value)
|
|
61
|
+
.sort()
|
|
62
|
+
.map(
|
|
63
|
+
(key) =>
|
|
64
|
+
`${JSON.stringify(key)}:${canonicalBrowserEvidenceJson(value[key])}`,
|
|
65
|
+
)
|
|
66
|
+
.join(",")}}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function browserEvidenceDigest(value) {
|
|
70
|
+
const bytes =
|
|
71
|
+
typeof value === "string" || Buffer.isBuffer(value)
|
|
72
|
+
? value
|
|
73
|
+
: canonicalBrowserEvidenceJson(value);
|
|
74
|
+
return `sha256:${crypto.createHash("sha256").update(bytes).digest("hex")}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function cloneJson(value, label) {
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(JSON.stringify(value));
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new Error(`${label} must be JSON serializable: ${error.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function compareCanonicalText(left, right) {
|
|
86
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function safeId(value, label) {
|
|
90
|
+
const normalized = String(value || "").trim();
|
|
91
|
+
if (!SAFE_ID_RE.test(normalized)) {
|
|
92
|
+
throw new Error(`${label} must be a stable non-secret identifier`);
|
|
93
|
+
}
|
|
94
|
+
return normalized;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function positiveInteger(value, label) {
|
|
98
|
+
const normalized = Number(value);
|
|
99
|
+
if (!Number.isSafeInteger(normalized) || normalized < 1) {
|
|
100
|
+
throw new Error(`${label} must be a positive integer`);
|
|
101
|
+
}
|
|
102
|
+
return normalized;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function exactSha(value, label) {
|
|
106
|
+
const normalized = String(value || "")
|
|
107
|
+
.trim()
|
|
108
|
+
.toLowerCase();
|
|
109
|
+
if (!EXACT_SHA_RE.test(normalized)) {
|
|
110
|
+
throw new Error(`${label} must be an exact 40- or 64-character commit SHA`);
|
|
111
|
+
}
|
|
112
|
+
return normalized;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function sha256(value, label) {
|
|
116
|
+
const normalized = String(value || "")
|
|
117
|
+
.trim()
|
|
118
|
+
.toLowerCase();
|
|
119
|
+
if (!SHA256_RE.test(normalized)) {
|
|
120
|
+
throw new Error(`${label} must be a canonical sha256 digest`);
|
|
121
|
+
}
|
|
122
|
+
return normalized;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function repositoryRelativePosixPath(value, label) {
|
|
126
|
+
const raw = String(value || "");
|
|
127
|
+
const normalized = raw.trim();
|
|
128
|
+
const segments = normalized.split("/");
|
|
129
|
+
if (
|
|
130
|
+
!normalized ||
|
|
131
|
+
normalized !== raw ||
|
|
132
|
+
normalized.startsWith("/") ||
|
|
133
|
+
normalized.includes("\\") ||
|
|
134
|
+
segments.some((segment) => !segment || segment === "." || segment === "..")
|
|
135
|
+
) {
|
|
136
|
+
throw new Error(`${label} must be a repository-relative POSIX path`);
|
|
137
|
+
}
|
|
138
|
+
return normalized;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isoTimestamp(value, label) {
|
|
142
|
+
const date = new Date(value);
|
|
143
|
+
if (!Number.isFinite(date.getTime())) {
|
|
144
|
+
throw new Error(`${label} must be an ISO timestamp`);
|
|
145
|
+
}
|
|
146
|
+
return date.toISOString();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function safeText(value, cap = 500) {
|
|
150
|
+
const redacted = [...redactSecrets(String(value || ""))]
|
|
151
|
+
.filter((character) => {
|
|
152
|
+
const code = character.codePointAt(0);
|
|
153
|
+
return (
|
|
154
|
+
code === 9 || code === 10 || code === 13 || (code >= 32 && code !== 127)
|
|
155
|
+
);
|
|
156
|
+
})
|
|
157
|
+
.join("")
|
|
158
|
+
.trim();
|
|
159
|
+
return redacted.slice(0, cap);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function browserOrigin(value) {
|
|
163
|
+
let parsed;
|
|
164
|
+
try {
|
|
165
|
+
parsed = new URL(String(value || ""));
|
|
166
|
+
} catch {
|
|
167
|
+
throw new Error("browser origin must be an absolute http(s) URL");
|
|
168
|
+
}
|
|
169
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
170
|
+
throw new Error("browser origin must use http or https");
|
|
171
|
+
}
|
|
172
|
+
if (parsed.username || parsed.password) {
|
|
173
|
+
throw new Error("browser origin must not contain credentials");
|
|
174
|
+
}
|
|
175
|
+
return parsed.origin;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function normalizeBrowserEvidenceBinding(binding) {
|
|
179
|
+
if (!binding || typeof binding !== "object" || Array.isArray(binding)) {
|
|
180
|
+
throw new Error("browser evidence requires a binding object");
|
|
181
|
+
}
|
|
182
|
+
const diff = binding.diff;
|
|
183
|
+
const testRun = binding.testRun;
|
|
184
|
+
if (!diff || typeof diff !== "object" || Array.isArray(diff)) {
|
|
185
|
+
throw new Error("browser evidence binding requires diff authority");
|
|
186
|
+
}
|
|
187
|
+
if (!testRun || typeof testRun !== "object" || Array.isArray(testRun)) {
|
|
188
|
+
throw new Error("browser evidence binding requires testRun authority");
|
|
189
|
+
}
|
|
190
|
+
return Object.freeze({
|
|
191
|
+
session: Object.freeze({
|
|
192
|
+
id: safeId(binding.sessionId ?? binding.session?.id, "session id"),
|
|
193
|
+
revision: positiveInteger(
|
|
194
|
+
binding.sessionRevision ?? binding.session?.revision,
|
|
195
|
+
"session revision",
|
|
196
|
+
),
|
|
197
|
+
}),
|
|
198
|
+
diff: Object.freeze({
|
|
199
|
+
baseSha: exactSha(diff.baseSha, "diff base SHA"),
|
|
200
|
+
headSha: exactSha(diff.headSha, "diff head SHA"),
|
|
201
|
+
digest: sha256(diff.digest, "diff digest"),
|
|
202
|
+
}),
|
|
203
|
+
testRun: Object.freeze({
|
|
204
|
+
id: safeId(testRun.id, "test run id"),
|
|
205
|
+
attempt: positiveInteger(testRun.attempt, "test run attempt"),
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function issueBrowserOriginGrant({
|
|
211
|
+
grantId,
|
|
212
|
+
binding,
|
|
213
|
+
origin,
|
|
214
|
+
revision,
|
|
215
|
+
scopes,
|
|
216
|
+
credentialBoundary = "none",
|
|
217
|
+
issuedAt,
|
|
218
|
+
expiresAt,
|
|
219
|
+
} = {}) {
|
|
220
|
+
const normalizedBinding = normalizeBrowserEvidenceBinding(binding);
|
|
221
|
+
const normalizedScopes = [
|
|
222
|
+
...new Set((Array.isArray(scopes) ? scopes : []).map(String)),
|
|
223
|
+
].sort();
|
|
224
|
+
if (
|
|
225
|
+
normalizedScopes.length === 0 ||
|
|
226
|
+
normalizedScopes.some((scope) => !BROWSER_ORIGIN_SCOPES.includes(scope))
|
|
227
|
+
) {
|
|
228
|
+
throw new Error("browser origin grant scopes are missing or unsupported");
|
|
229
|
+
}
|
|
230
|
+
if (!["none", "session-bound"].includes(credentialBoundary)) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
"browser origin grant credentialBoundary must be none or session-bound",
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
const normalizedIssuedAt = isoTimestamp(issuedAt, "grant issuedAt");
|
|
236
|
+
const normalizedExpiresAt = isoTimestamp(expiresAt, "grant expiresAt");
|
|
237
|
+
if (Date.parse(normalizedExpiresAt) <= Date.parse(normalizedIssuedAt)) {
|
|
238
|
+
throw new Error("browser origin grant must expire after it is issued");
|
|
239
|
+
}
|
|
240
|
+
const body = {
|
|
241
|
+
schema: BROWSER_ORIGIN_GRANT_SCHEMA,
|
|
242
|
+
grantId: safeId(grantId, "origin grant id"),
|
|
243
|
+
session: normalizedBinding.session,
|
|
244
|
+
origin: browserOrigin(origin),
|
|
245
|
+
revision: positiveInteger(revision, "origin grant revision"),
|
|
246
|
+
scopes: normalizedScopes,
|
|
247
|
+
credentialBoundary,
|
|
248
|
+
issuedAt: normalizedIssuedAt,
|
|
249
|
+
expiresAt: normalizedExpiresAt,
|
|
250
|
+
};
|
|
251
|
+
return Object.freeze({
|
|
252
|
+
...body,
|
|
253
|
+
grantDigest: browserEvidenceDigest(body),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function verifyBrowserOriginGrant(grant, { now = Date.now() } = {}) {
|
|
258
|
+
if (!grant || grant.schema !== BROWSER_ORIGIN_GRANT_SCHEMA) {
|
|
259
|
+
throw new Error("browser origin grant schema is invalid");
|
|
260
|
+
}
|
|
261
|
+
const binding = normalizeBrowserEvidenceBinding({
|
|
262
|
+
session: grant.session,
|
|
263
|
+
diff: {
|
|
264
|
+
baseSha: "0".repeat(40),
|
|
265
|
+
headSha: "0".repeat(40),
|
|
266
|
+
digest: `sha256:${"0".repeat(64)}`,
|
|
267
|
+
},
|
|
268
|
+
testRun: { id: "grant-verification", attempt: 1 },
|
|
269
|
+
});
|
|
270
|
+
const body = {
|
|
271
|
+
schema: BROWSER_ORIGIN_GRANT_SCHEMA,
|
|
272
|
+
grantId: safeId(grant.grantId, "origin grant id"),
|
|
273
|
+
session: binding.session,
|
|
274
|
+
origin: browserOrigin(grant.origin),
|
|
275
|
+
revision: positiveInteger(grant.revision, "origin grant revision"),
|
|
276
|
+
scopes: [...new Set((grant.scopes || []).map(String))].sort(),
|
|
277
|
+
credentialBoundary: grant.credentialBoundary,
|
|
278
|
+
issuedAt: isoTimestamp(grant.issuedAt, "grant issuedAt"),
|
|
279
|
+
expiresAt: isoTimestamp(grant.expiresAt, "grant expiresAt"),
|
|
280
|
+
};
|
|
281
|
+
if (
|
|
282
|
+
body.scopes.length === 0 ||
|
|
283
|
+
body.scopes.some((scope) => !BROWSER_ORIGIN_SCOPES.includes(scope))
|
|
284
|
+
) {
|
|
285
|
+
throw new Error("browser origin grant scopes are invalid");
|
|
286
|
+
}
|
|
287
|
+
if (!["none", "session-bound"].includes(body.credentialBoundary)) {
|
|
288
|
+
throw new Error("browser origin grant credential boundary is invalid");
|
|
289
|
+
}
|
|
290
|
+
if (Date.parse(body.expiresAt) <= Date.parse(body.issuedAt)) {
|
|
291
|
+
throw new Error("browser origin grant validity window is invalid");
|
|
292
|
+
}
|
|
293
|
+
if (browserEvidenceDigest(body) !== grant.grantDigest) {
|
|
294
|
+
throw new Error("browser origin grant digest mismatch");
|
|
295
|
+
}
|
|
296
|
+
if (Date.parse(body.issuedAt) > Number(now)) {
|
|
297
|
+
throw new Error("browser origin grant is not active yet");
|
|
298
|
+
}
|
|
299
|
+
if (Date.parse(body.expiresAt) <= Number(now)) {
|
|
300
|
+
throw new Error("browser origin grant is expired");
|
|
301
|
+
}
|
|
302
|
+
return Object.freeze({ ...body, grantDigest: grant.grantDigest });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function grantForOrigin(grants, origin, options) {
|
|
306
|
+
const matches = (Array.isArray(grants) ? grants : [])
|
|
307
|
+
.map((grant) => verifyBrowserOriginGrant(grant, options))
|
|
308
|
+
.filter((grant) => grant.origin === origin);
|
|
309
|
+
if (matches.length !== 1) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
matches.length === 0
|
|
312
|
+
? `browser origin ${origin} is not granted`
|
|
313
|
+
: `browser origin ${origin} has ambiguous grants`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
return matches[0];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function authorizeBrowserAction({
|
|
320
|
+
binding,
|
|
321
|
+
grants,
|
|
322
|
+
expectedGrantRevisions,
|
|
323
|
+
action,
|
|
324
|
+
currentUrl,
|
|
325
|
+
now = Date.now(),
|
|
326
|
+
} = {}) {
|
|
327
|
+
const normalizedBinding = normalizeBrowserEvidenceBinding(binding);
|
|
328
|
+
const type = String(action?.type || "");
|
|
329
|
+
const requiredScope = SCOPE_BY_ACTION[type];
|
|
330
|
+
if (!requiredScope) throw new Error(`unsupported browser action: ${type}`);
|
|
331
|
+
const currentOrigin = browserOrigin(currentUrl);
|
|
332
|
+
const currentGrant = grantForOrigin(grants, currentOrigin, { now });
|
|
333
|
+
const assertGrant = (grant) => {
|
|
334
|
+
if (
|
|
335
|
+
grant.session.id !== normalizedBinding.session.id ||
|
|
336
|
+
grant.session.revision !== normalizedBinding.session.revision
|
|
337
|
+
) {
|
|
338
|
+
throw new Error("browser origin grant session/revision mismatch");
|
|
339
|
+
}
|
|
340
|
+
const expected = Number(expectedGrantRevisions?.[grant.origin]);
|
|
341
|
+
if (!Number.isSafeInteger(expected) || expected !== grant.revision) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
`browser origin grant revision mismatch for ${grant.origin}`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
if (!grant.scopes.includes(requiredScope)) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
`browser origin grant for ${grant.origin} lacks ${requiredScope} scope`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
assertGrant(currentGrant);
|
|
353
|
+
|
|
354
|
+
let targetGrant = currentGrant;
|
|
355
|
+
if (type === "navigate") {
|
|
356
|
+
const targetOrigin = browserOrigin(action.url);
|
|
357
|
+
if (targetOrigin !== currentOrigin) {
|
|
358
|
+
targetGrant = grantForOrigin(grants, targetOrigin, { now });
|
|
359
|
+
assertGrant(targetGrant);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const credentialGrant = type === "navigate" ? targetGrant : currentGrant;
|
|
363
|
+
if (
|
|
364
|
+
actionCarriesCredentialMaterial(action) &&
|
|
365
|
+
credentialGrant.credentialBoundary !== "session-bound"
|
|
366
|
+
) {
|
|
367
|
+
throw new Error(
|
|
368
|
+
`browser action ${type} requires a session-bound credential grant`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
return Object.freeze({
|
|
372
|
+
grantId: targetGrant.grantId,
|
|
373
|
+
grantDigest: targetGrant.grantDigest,
|
|
374
|
+
origin: targetGrant.origin,
|
|
375
|
+
revision: targetGrant.revision,
|
|
376
|
+
scope: requiredScope,
|
|
377
|
+
credentialBoundary: targetGrant.credentialBoundary,
|
|
378
|
+
crossOrigin: targetGrant.origin !== currentOrigin,
|
|
379
|
+
...(targetGrant.origin !== currentOrigin
|
|
380
|
+
? {
|
|
381
|
+
sourceGrantId: currentGrant.grantId,
|
|
382
|
+
sourceGrantDigest: currentGrant.grantDigest,
|
|
383
|
+
sourceOrigin: currentGrant.origin,
|
|
384
|
+
sourceRevision: currentGrant.revision,
|
|
385
|
+
sourceCredentialBoundary: currentGrant.credentialBoundary,
|
|
386
|
+
}
|
|
387
|
+
: {}),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function browserActionIntent(action) {
|
|
392
|
+
const type = String(action?.type || "");
|
|
393
|
+
if (!SCOPE_BY_ACTION[type]) {
|
|
394
|
+
throw new Error(`unsupported browser action: ${type}`);
|
|
395
|
+
}
|
|
396
|
+
const intent = { type };
|
|
397
|
+
if (action?.selector) intent.selector = safeText(action.selector, 500);
|
|
398
|
+
if (action?.key) intent.key = safeText(action.key, 80);
|
|
399
|
+
if (type === "navigate") {
|
|
400
|
+
const target = new URL(String(action.url));
|
|
401
|
+
intent.targetOrigin = browserOrigin(target);
|
|
402
|
+
intent.targetPath = safeText(target.pathname, 500) || "/";
|
|
403
|
+
intent.queryKeys = [
|
|
404
|
+
...new Set(
|
|
405
|
+
[...target.searchParams.keys()]
|
|
406
|
+
.map((key) => safeText(key, 120))
|
|
407
|
+
.filter(Boolean),
|
|
408
|
+
),
|
|
409
|
+
].sort();
|
|
410
|
+
intent.queryValuesRetained = false;
|
|
411
|
+
intent.fragmentPresent = target.hash.length > 0;
|
|
412
|
+
intent.fragmentValueRetained = false;
|
|
413
|
+
}
|
|
414
|
+
if (type === "type") intent.textLength = String(action.text || "").length;
|
|
415
|
+
if (type === "assertText") {
|
|
416
|
+
intent.expectedDigest = browserEvidenceDigest(
|
|
417
|
+
String(action.expected || ""),
|
|
418
|
+
);
|
|
419
|
+
intent.expectedLength = String(action.expected || "").length;
|
|
420
|
+
}
|
|
421
|
+
if (type === "upload") {
|
|
422
|
+
intent.artifactId = safeId(action.artifactId, "upload artifact id");
|
|
423
|
+
}
|
|
424
|
+
if (type === "waitForSelector") {
|
|
425
|
+
intent.timeoutMs = Math.max(1, Number(action.timeoutMs) || 1);
|
|
426
|
+
}
|
|
427
|
+
return Object.freeze(intent);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function normalizeRecordedBrowserActionIntent(intent) {
|
|
431
|
+
const type = String(intent?.type || "");
|
|
432
|
+
if (!SCOPE_BY_ACTION[type]) {
|
|
433
|
+
throw new Error(`unsupported browser action: ${type}`);
|
|
434
|
+
}
|
|
435
|
+
const normalized = { type };
|
|
436
|
+
if (
|
|
437
|
+
[
|
|
438
|
+
"assertText",
|
|
439
|
+
"click",
|
|
440
|
+
"download",
|
|
441
|
+
"type",
|
|
442
|
+
"upload",
|
|
443
|
+
"waitForSelector",
|
|
444
|
+
].includes(type)
|
|
445
|
+
) {
|
|
446
|
+
normalized.selector = safeText(intent.selector, 500);
|
|
447
|
+
if (!normalized.selector) {
|
|
448
|
+
throw new Error(`${type} intent requires a selector`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (type === "press") {
|
|
452
|
+
normalized.key = safeText(intent.key, 80);
|
|
453
|
+
if (!normalized.key) throw new Error("press intent requires a key");
|
|
454
|
+
}
|
|
455
|
+
if (type === "navigate") {
|
|
456
|
+
normalized.targetOrigin = browserOrigin(intent.targetOrigin);
|
|
457
|
+
normalized.targetPath = safeText(intent.targetPath, 500) || "/";
|
|
458
|
+
if (!normalized.targetPath.startsWith("/")) {
|
|
459
|
+
throw new Error("navigate intent target path is invalid");
|
|
460
|
+
}
|
|
461
|
+
normalized.queryKeys = [
|
|
462
|
+
...new Set(
|
|
463
|
+
(Array.isArray(intent.queryKeys) ? intent.queryKeys : [])
|
|
464
|
+
.map((key) => safeText(key, 120))
|
|
465
|
+
.filter(Boolean),
|
|
466
|
+
),
|
|
467
|
+
].sort();
|
|
468
|
+
normalized.queryValuesRetained = false;
|
|
469
|
+
if (intent.queryValuesRetained !== false) {
|
|
470
|
+
throw new Error("navigate intent query value boundary is invalid");
|
|
471
|
+
}
|
|
472
|
+
normalized.fragmentPresent = intent.fragmentPresent === true;
|
|
473
|
+
normalized.fragmentValueRetained = false;
|
|
474
|
+
if (intent.fragmentValueRetained !== false) {
|
|
475
|
+
throw new Error("navigate intent fragment boundary is invalid");
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (type === "type") {
|
|
479
|
+
normalized.textLength = Number(intent.textLength);
|
|
480
|
+
if (
|
|
481
|
+
!Number.isSafeInteger(normalized.textLength) ||
|
|
482
|
+
normalized.textLength < 0
|
|
483
|
+
) {
|
|
484
|
+
throw new Error("type intent length is invalid");
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (type === "assertText") {
|
|
488
|
+
normalized.expectedDigest = sha256(
|
|
489
|
+
intent.expectedDigest,
|
|
490
|
+
"browser action expected digest",
|
|
491
|
+
);
|
|
492
|
+
normalized.expectedLength = positiveInteger(
|
|
493
|
+
intent.expectedLength,
|
|
494
|
+
"browser action expected length",
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
if (type === "upload") {
|
|
498
|
+
normalized.artifactId = safeId(intent.artifactId, "upload artifact id");
|
|
499
|
+
}
|
|
500
|
+
if (type === "waitForSelector") {
|
|
501
|
+
normalized.timeoutMs = positiveInteger(
|
|
502
|
+
intent.timeoutMs,
|
|
503
|
+
"browser action timeout",
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
return normalized;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export function describeBrowserAction(action, step, authority, index) {
|
|
510
|
+
const type = String(action?.type || "");
|
|
511
|
+
if (!SCOPE_BY_ACTION[type])
|
|
512
|
+
throw new Error(`unsupported browser action: ${type}`);
|
|
513
|
+
const intent = browserActionIntent(action);
|
|
514
|
+
const row = {
|
|
515
|
+
index: Number(index),
|
|
516
|
+
type,
|
|
517
|
+
intent,
|
|
518
|
+
intentDigest: browserEvidenceDigest(intent),
|
|
519
|
+
sideEffect: SIDE_EFFECT_BY_ACTION[type],
|
|
520
|
+
credentialBoundary: actionCarriesCredentialMaterial(action)
|
|
521
|
+
? "session-bound-payload-not-retained"
|
|
522
|
+
: "none",
|
|
523
|
+
authority: cloneJson(authority, "browser action authority"),
|
|
524
|
+
outcome: {
|
|
525
|
+
ok: step?.ok === true,
|
|
526
|
+
durationMs: Math.max(0, Number(step?.durationMs) || 0),
|
|
527
|
+
detail: safeText(step?.detail, 500),
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
if (action?.selector) row.selector = safeText(action.selector, 500);
|
|
531
|
+
if (action?.key) row.key = safeText(action.key, 80);
|
|
532
|
+
if (type === "navigate") row.targetOrigin = browserOrigin(action.url);
|
|
533
|
+
if (type === "type") row.textLength = String(action.text || "").length;
|
|
534
|
+
if (type === "upload") {
|
|
535
|
+
row.uploadArtifact = {
|
|
536
|
+
id: safeId(
|
|
537
|
+
step?.uploadArtifact?.id || action.artifactId,
|
|
538
|
+
"upload artifact id",
|
|
539
|
+
),
|
|
540
|
+
...(step?.uploadArtifact?.sha256
|
|
541
|
+
? {
|
|
542
|
+
sha256: sha256(
|
|
543
|
+
step.uploadArtifact.sha256,
|
|
544
|
+
"upload artifact digest",
|
|
545
|
+
),
|
|
546
|
+
size: Math.max(0, Number(step.uploadArtifact.size) || 0),
|
|
547
|
+
}
|
|
548
|
+
: {}),
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
for (const kind of ["screenshot", "download"]) {
|
|
552
|
+
const digest = step?.[`${kind}Sha256`];
|
|
553
|
+
if (digest) {
|
|
554
|
+
row[`${kind}Digest`] = sha256(digest, `${kind} digest`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return Object.freeze(row);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function normalizeObservationRows(rows, kind, captureAvailable = false) {
|
|
561
|
+
const normalized = (Array.isArray(rows) ? rows : [])
|
|
562
|
+
.slice(0, 200)
|
|
563
|
+
.map((row) => {
|
|
564
|
+
if (kind === "console") {
|
|
565
|
+
return {
|
|
566
|
+
type: safeText(row?.type, 40),
|
|
567
|
+
text: safeText(row?.text, 500),
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
const normalizedRow = {
|
|
571
|
+
kind: safeText(row?.kind, 40),
|
|
572
|
+
url: safeText(row?.url, 500),
|
|
573
|
+
};
|
|
574
|
+
if (row?.status != null) normalizedRow.status = Number(row.status);
|
|
575
|
+
if (row?.error) normalizedRow.error = safeText(row.error, 300);
|
|
576
|
+
return normalizedRow;
|
|
577
|
+
});
|
|
578
|
+
return Object.freeze({
|
|
579
|
+
captureAvailable: captureAvailable === true,
|
|
580
|
+
count: normalized.length,
|
|
581
|
+
digest: browserEvidenceDigest(normalized),
|
|
582
|
+
records: Object.freeze(normalized),
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function nonNegativeInteger(value, label) {
|
|
587
|
+
const normalized = Number(value);
|
|
588
|
+
if (!Number.isSafeInteger(normalized) || normalized < 0) {
|
|
589
|
+
throw new Error(`${label} must be a non-negative integer`);
|
|
590
|
+
}
|
|
591
|
+
return normalized;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function normalizeDomSnapshot({
|
|
595
|
+
html,
|
|
596
|
+
sourceChars,
|
|
597
|
+
cap,
|
|
598
|
+
truncated,
|
|
599
|
+
redaction,
|
|
600
|
+
captureSucceeded,
|
|
601
|
+
} = {}) {
|
|
602
|
+
const safeHtml = String(html || "");
|
|
603
|
+
const normalizedCap = Math.max(0, Number(cap) || safeHtml.length);
|
|
604
|
+
const normalizedSourceChars = Math.max(
|
|
605
|
+
safeHtml.length,
|
|
606
|
+
Number(sourceChars) || safeHtml.length,
|
|
607
|
+
);
|
|
608
|
+
return Object.freeze({
|
|
609
|
+
digest: browserEvidenceDigest(safeHtml),
|
|
610
|
+
capturedChars: safeHtml.length,
|
|
611
|
+
sourceChars: normalizedSourceChars,
|
|
612
|
+
cap: normalizedCap,
|
|
613
|
+
truncated: truncated === true || normalizedSourceChars > normalizedCap,
|
|
614
|
+
captureSucceeded: captureSucceeded === true,
|
|
615
|
+
redaction: Object.freeze({
|
|
616
|
+
applied: redaction?.applied === true,
|
|
617
|
+
sensitiveFieldValues: nonNegativeInteger(
|
|
618
|
+
redaction?.sensitiveFieldValues ?? 0,
|
|
619
|
+
"DOM sensitive field redaction count",
|
|
620
|
+
),
|
|
621
|
+
urlQueryValues: nonNegativeInteger(
|
|
622
|
+
redaction?.urlQueryValues ?? 0,
|
|
623
|
+
"DOM URL query redaction count",
|
|
624
|
+
),
|
|
625
|
+
secretPatterns: nonNegativeInteger(
|
|
626
|
+
redaction?.secretPatterns ?? 0,
|
|
627
|
+
"DOM secret pattern redaction count",
|
|
628
|
+
),
|
|
629
|
+
}),
|
|
630
|
+
contentRetained: false,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
export function createBrowserEvidenceEnvelope({
|
|
635
|
+
binding,
|
|
636
|
+
originPermissions,
|
|
637
|
+
actions,
|
|
638
|
+
consoleEntries,
|
|
639
|
+
networkEntries,
|
|
640
|
+
pageUrl,
|
|
641
|
+
pageTitle,
|
|
642
|
+
domSnapshot,
|
|
643
|
+
screenshots,
|
|
644
|
+
downloads,
|
|
645
|
+
replay = null,
|
|
646
|
+
observationCaptureAvailable = false,
|
|
647
|
+
capturedAt = new Date().toISOString(),
|
|
648
|
+
} = {}) {
|
|
649
|
+
const normalizedBinding = normalizeBrowserEvidenceBinding(binding);
|
|
650
|
+
const permissionInputs = (
|
|
651
|
+
Array.isArray(originPermissions) ? originPermissions : []
|
|
652
|
+
).flatMap((permission) => [
|
|
653
|
+
permission,
|
|
654
|
+
...(permission?.crossOrigin
|
|
655
|
+
? [
|
|
656
|
+
{
|
|
657
|
+
grantId: permission.sourceGrantId,
|
|
658
|
+
grantDigest: permission.sourceGrantDigest,
|
|
659
|
+
origin: permission.sourceOrigin,
|
|
660
|
+
revision: permission.sourceRevision,
|
|
661
|
+
scope: permission.scope,
|
|
662
|
+
credentialBoundary: permission.sourceCredentialBoundary || "none",
|
|
663
|
+
crossOrigin: false,
|
|
664
|
+
},
|
|
665
|
+
]
|
|
666
|
+
: []),
|
|
667
|
+
]);
|
|
668
|
+
const permissionRows = permissionInputs.map((permission) => ({
|
|
669
|
+
grantId: safeId(permission.grantId, "origin permission grant id"),
|
|
670
|
+
grantDigest: sha256(
|
|
671
|
+
permission.grantDigest,
|
|
672
|
+
"origin permission grant digest",
|
|
673
|
+
),
|
|
674
|
+
origin: browserOrigin(permission.origin),
|
|
675
|
+
revision: positiveInteger(
|
|
676
|
+
permission.revision,
|
|
677
|
+
"origin permission revision",
|
|
678
|
+
),
|
|
679
|
+
scope: String(permission.scope || ""),
|
|
680
|
+
credentialBoundary: String(permission.credentialBoundary || "none"),
|
|
681
|
+
crossOrigin: permission.crossOrigin === true,
|
|
682
|
+
}));
|
|
683
|
+
const normalizedPermissions = [
|
|
684
|
+
...new Map(
|
|
685
|
+
permissionRows.map((permission) => [
|
|
686
|
+
canonicalBrowserEvidenceJson(permission),
|
|
687
|
+
permission,
|
|
688
|
+
]),
|
|
689
|
+
).values(),
|
|
690
|
+
].sort((left, right) =>
|
|
691
|
+
compareCanonicalText(
|
|
692
|
+
`${left.origin}:${left.scope}:${left.revision}:${canonicalBrowserEvidenceJson(left)}`,
|
|
693
|
+
`${right.origin}:${right.scope}:${right.revision}:${canonicalBrowserEvidenceJson(right)}`,
|
|
694
|
+
),
|
|
695
|
+
);
|
|
696
|
+
for (const permission of normalizedPermissions) {
|
|
697
|
+
if (!BROWSER_ORIGIN_SCOPES.includes(permission.scope)) {
|
|
698
|
+
throw new Error("browser evidence origin permission scope is invalid");
|
|
699
|
+
}
|
|
700
|
+
if (!["none", "session-bound"].includes(permission.credentialBoundary)) {
|
|
701
|
+
throw new Error(
|
|
702
|
+
"browser evidence origin permission credential boundary is invalid",
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
const normalizedActions = cloneJson(
|
|
707
|
+
actions || [],
|
|
708
|
+
"browser evidence actions",
|
|
709
|
+
);
|
|
710
|
+
if (!Array.isArray(normalizedActions) || normalizedActions.length === 0) {
|
|
711
|
+
throw new Error("browser evidence requires at least one action result");
|
|
712
|
+
}
|
|
713
|
+
const normalizedScreenshots = (Array.isArray(screenshots) ? screenshots : [])
|
|
714
|
+
.map((row, index) => ({
|
|
715
|
+
index,
|
|
716
|
+
digest: sha256(row.digest, "screenshot digest"),
|
|
717
|
+
actionIndex: Number(row.actionIndex),
|
|
718
|
+
}))
|
|
719
|
+
.sort((left, right) => left.actionIndex - right.actionIndex);
|
|
720
|
+
const normalizedDownloads = (Array.isArray(downloads) ? downloads : [])
|
|
721
|
+
.map((row, index) => ({
|
|
722
|
+
index,
|
|
723
|
+
digest: sha256(row.digest, "download digest"),
|
|
724
|
+
actionIndex: Number(row.actionIndex),
|
|
725
|
+
suggestedName: safeText(row.suggestedName, 200),
|
|
726
|
+
}))
|
|
727
|
+
.sort((left, right) => left.actionIndex - right.actionIndex);
|
|
728
|
+
const replayRecord = replay
|
|
729
|
+
? {
|
|
730
|
+
sourceEnvelopeDigest: sha256(
|
|
731
|
+
replay.sourceEnvelopeDigest,
|
|
732
|
+
"replay source envelope digest",
|
|
733
|
+
),
|
|
734
|
+
sideEffectBoundary: String(replay.sideEffectBoundary),
|
|
735
|
+
credentialBoundary: String(replay.credentialBoundary),
|
|
736
|
+
actionCount: positiveInteger(
|
|
737
|
+
replay.actionCount,
|
|
738
|
+
"browser replay action count",
|
|
739
|
+
),
|
|
740
|
+
}
|
|
741
|
+
: {
|
|
742
|
+
sourceEnvelopeDigest: null,
|
|
743
|
+
sideEffectBoundary: "recorded-not-authorized-for-replay",
|
|
744
|
+
credentialBoundary: "payloads-not-retained",
|
|
745
|
+
actionCount: 0,
|
|
746
|
+
};
|
|
747
|
+
if (
|
|
748
|
+
![
|
|
749
|
+
"recorded-not-authorized-for-replay",
|
|
750
|
+
"deny",
|
|
751
|
+
"explicitly-approved",
|
|
752
|
+
].includes(replayRecord.sideEffectBoundary) ||
|
|
753
|
+
!["payloads-not-retained", "deny", "explicitly-approved"].includes(
|
|
754
|
+
replayRecord.credentialBoundary,
|
|
755
|
+
)
|
|
756
|
+
) {
|
|
757
|
+
throw new Error("browser evidence replay boundary is invalid");
|
|
758
|
+
}
|
|
759
|
+
const body = {
|
|
760
|
+
schema: BROWSER_EVIDENCE_SCHEMA,
|
|
761
|
+
version: 1,
|
|
762
|
+
binding: normalizedBinding,
|
|
763
|
+
originPermissions: normalizedPermissions,
|
|
764
|
+
actions: normalizedActions,
|
|
765
|
+
observations: {
|
|
766
|
+
page: {
|
|
767
|
+
origin: browserOrigin(pageUrl),
|
|
768
|
+
title: safeText(pageTitle, 500),
|
|
769
|
+
queryValueRedactions: [...new URL(String(pageUrl)).searchParams.keys()]
|
|
770
|
+
.length,
|
|
771
|
+
credentialMaterialRetained: false,
|
|
772
|
+
},
|
|
773
|
+
console: normalizeObservationRows(
|
|
774
|
+
consoleEntries,
|
|
775
|
+
"console",
|
|
776
|
+
observationCaptureAvailable,
|
|
777
|
+
),
|
|
778
|
+
network: normalizeObservationRows(
|
|
779
|
+
networkEntries,
|
|
780
|
+
"network",
|
|
781
|
+
observationCaptureAvailable,
|
|
782
|
+
),
|
|
783
|
+
},
|
|
784
|
+
domSnapshot: normalizeDomSnapshot(domSnapshot),
|
|
785
|
+
screenshots: normalizedScreenshots,
|
|
786
|
+
downloads: normalizedDownloads,
|
|
787
|
+
replay: replayRecord,
|
|
788
|
+
capturedAt: isoTimestamp(capturedAt, "browser evidence capturedAt"),
|
|
789
|
+
};
|
|
790
|
+
return Object.freeze({
|
|
791
|
+
...body,
|
|
792
|
+
envelopeDigest: browserEvidenceDigest(body),
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export function verifyBrowserEvidenceEnvelope(envelope) {
|
|
797
|
+
if (!envelope || envelope.schema !== BROWSER_EVIDENCE_SCHEMA) {
|
|
798
|
+
throw new Error("browser evidence envelope schema is invalid");
|
|
799
|
+
}
|
|
800
|
+
const body = { ...cloneJson(envelope, "browser evidence envelope") };
|
|
801
|
+
const digest = body.envelopeDigest;
|
|
802
|
+
delete body.envelopeDigest;
|
|
803
|
+
if (browserEvidenceDigest(body) !== digest) {
|
|
804
|
+
throw new Error("browser evidence envelope digest mismatch");
|
|
805
|
+
}
|
|
806
|
+
if (body.version !== 1) {
|
|
807
|
+
throw new Error("browser evidence envelope version is invalid");
|
|
808
|
+
}
|
|
809
|
+
const envelopeKeys = [
|
|
810
|
+
"actions",
|
|
811
|
+
"binding",
|
|
812
|
+
"capturedAt",
|
|
813
|
+
"domSnapshot",
|
|
814
|
+
"downloads",
|
|
815
|
+
"observations",
|
|
816
|
+
"originPermissions",
|
|
817
|
+
"replay",
|
|
818
|
+
"schema",
|
|
819
|
+
"screenshots",
|
|
820
|
+
"version",
|
|
821
|
+
];
|
|
822
|
+
if (
|
|
823
|
+
canonicalBrowserEvidenceJson(Object.keys(body).sort()) !==
|
|
824
|
+
canonicalBrowserEvidenceJson(envelopeKeys)
|
|
825
|
+
) {
|
|
826
|
+
throw new Error("browser evidence envelope fields are invalid");
|
|
827
|
+
}
|
|
828
|
+
const normalizedBinding = normalizeBrowserEvidenceBinding({
|
|
829
|
+
session: body.binding?.session,
|
|
830
|
+
diff: body.binding?.diff,
|
|
831
|
+
testRun: body.binding?.testRun,
|
|
832
|
+
});
|
|
833
|
+
if (
|
|
834
|
+
canonicalBrowserEvidenceJson(normalizedBinding) !==
|
|
835
|
+
canonicalBrowserEvidenceJson(body.binding)
|
|
836
|
+
) {
|
|
837
|
+
throw new Error("browser evidence binding is not canonical");
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (!Array.isArray(body.originPermissions)) {
|
|
841
|
+
throw new Error("browser evidence origin permissions are invalid");
|
|
842
|
+
}
|
|
843
|
+
const permissionKeys = new Set();
|
|
844
|
+
for (const permission of body.originPermissions) {
|
|
845
|
+
const canonicalPermission = {
|
|
846
|
+
grantId: safeId(permission?.grantId, "origin permission grant id"),
|
|
847
|
+
grantDigest: sha256(
|
|
848
|
+
permission?.grantDigest,
|
|
849
|
+
"origin permission grant digest",
|
|
850
|
+
),
|
|
851
|
+
origin: browserOrigin(permission?.origin),
|
|
852
|
+
revision: positiveInteger(
|
|
853
|
+
permission?.revision,
|
|
854
|
+
"origin permission revision",
|
|
855
|
+
),
|
|
856
|
+
scope: String(permission?.scope || ""),
|
|
857
|
+
credentialBoundary: String(permission?.credentialBoundary || "none"),
|
|
858
|
+
crossOrigin: permission?.crossOrigin === true,
|
|
859
|
+
};
|
|
860
|
+
if (
|
|
861
|
+
!BROWSER_ORIGIN_SCOPES.includes(canonicalPermission.scope) ||
|
|
862
|
+
!["none", "session-bound"].includes(
|
|
863
|
+
canonicalPermission.credentialBoundary,
|
|
864
|
+
) ||
|
|
865
|
+
canonicalBrowserEvidenceJson(canonicalPermission) !==
|
|
866
|
+
canonicalBrowserEvidenceJson(permission)
|
|
867
|
+
) {
|
|
868
|
+
throw new Error("browser evidence origin permission is invalid");
|
|
869
|
+
}
|
|
870
|
+
const key = canonicalBrowserEvidenceJson(canonicalPermission);
|
|
871
|
+
if (permissionKeys.has(key)) {
|
|
872
|
+
throw new Error("browser evidence origin permission is duplicated");
|
|
873
|
+
}
|
|
874
|
+
permissionKeys.add(key);
|
|
875
|
+
}
|
|
876
|
+
const sortedPermissions = [...body.originPermissions].sort((left, right) =>
|
|
877
|
+
compareCanonicalText(
|
|
878
|
+
`${left.origin}:${left.scope}:${left.revision}:${canonicalBrowserEvidenceJson(left)}`,
|
|
879
|
+
`${right.origin}:${right.scope}:${right.revision}:${canonicalBrowserEvidenceJson(right)}`,
|
|
880
|
+
),
|
|
881
|
+
);
|
|
882
|
+
if (
|
|
883
|
+
canonicalBrowserEvidenceJson(sortedPermissions) !==
|
|
884
|
+
canonicalBrowserEvidenceJson(body.originPermissions)
|
|
885
|
+
) {
|
|
886
|
+
throw new Error("browser evidence origin permissions are not canonical");
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (!Array.isArray(body.actions) || body.actions.length === 0) {
|
|
890
|
+
throw new Error("browser evidence actions are invalid");
|
|
891
|
+
}
|
|
892
|
+
for (const [index, action] of body.actions.entries()) {
|
|
893
|
+
const normalizedIntent = normalizeRecordedBrowserActionIntent(
|
|
894
|
+
action?.intent,
|
|
895
|
+
);
|
|
896
|
+
const actionKeys = [
|
|
897
|
+
"authority",
|
|
898
|
+
"credentialBoundary",
|
|
899
|
+
"index",
|
|
900
|
+
"intent",
|
|
901
|
+
"intentDigest",
|
|
902
|
+
"outcome",
|
|
903
|
+
"sideEffect",
|
|
904
|
+
"type",
|
|
905
|
+
...(normalizedIntent.selector ? ["selector"] : []),
|
|
906
|
+
...(normalizedIntent.key ? ["key"] : []),
|
|
907
|
+
...(normalizedIntent.targetOrigin ? ["targetOrigin"] : []),
|
|
908
|
+
...(normalizedIntent.textLength != null ? ["textLength"] : []),
|
|
909
|
+
...(action?.type === "upload" ? ["uploadArtifact"] : []),
|
|
910
|
+
...(action?.screenshotDigest ? ["screenshotDigest"] : []),
|
|
911
|
+
...(action?.downloadDigest ? ["downloadDigest"] : []),
|
|
912
|
+
].sort();
|
|
913
|
+
if (
|
|
914
|
+
action?.index !== index ||
|
|
915
|
+
!SCOPE_BY_ACTION[action?.type] ||
|
|
916
|
+
action.sideEffect !== SIDE_EFFECT_BY_ACTION[action.type] ||
|
|
917
|
+
!action.intent ||
|
|
918
|
+
action.intent.type !== action.type ||
|
|
919
|
+
canonicalBrowserEvidenceJson(normalizedIntent) !==
|
|
920
|
+
canonicalBrowserEvidenceJson(action.intent) ||
|
|
921
|
+
browserEvidenceDigest(normalizedIntent) !== action.intentDigest ||
|
|
922
|
+
action.credentialBoundary !==
|
|
923
|
+
(["type", "upload"].includes(action.type) ||
|
|
924
|
+
(action.type === "navigate" &&
|
|
925
|
+
(normalizedIntent.queryKeys.length > 0 ||
|
|
926
|
+
normalizedIntent.fragmentPresent))
|
|
927
|
+
? "session-bound-payload-not-retained"
|
|
928
|
+
: "none") ||
|
|
929
|
+
(normalizedIntent.selector || undefined) !==
|
|
930
|
+
(action.selector || undefined) ||
|
|
931
|
+
(normalizedIntent.key || undefined) !== (action.key || undefined) ||
|
|
932
|
+
(normalizedIntent.targetOrigin || undefined) !==
|
|
933
|
+
(action.targetOrigin || undefined) ||
|
|
934
|
+
(normalizedIntent.textLength ?? undefined) !==
|
|
935
|
+
(action.textLength ?? undefined) ||
|
|
936
|
+
(normalizedIntent.artifactId || undefined) !==
|
|
937
|
+
(action.uploadArtifact?.id || undefined) ||
|
|
938
|
+
canonicalBrowserEvidenceJson(Object.keys(action).sort()) !==
|
|
939
|
+
canonicalBrowserEvidenceJson(actionKeys) ||
|
|
940
|
+
(action.screenshotDigest != null && action.type !== "screenshot") ||
|
|
941
|
+
(action.downloadDigest != null && action.type !== "download") ||
|
|
942
|
+
action.text != null ||
|
|
943
|
+
action.expected != null ||
|
|
944
|
+
action.url != null
|
|
945
|
+
) {
|
|
946
|
+
throw new Error(`browser evidence action ${index} is invalid`);
|
|
947
|
+
}
|
|
948
|
+
if (action.screenshotDigest) {
|
|
949
|
+
sha256(action.screenshotDigest, "browser action screenshot digest");
|
|
950
|
+
}
|
|
951
|
+
if (action.downloadDigest) {
|
|
952
|
+
sha256(action.downloadDigest, "browser action download digest");
|
|
953
|
+
}
|
|
954
|
+
if (action.type === "upload") {
|
|
955
|
+
const canonicalUpload = {
|
|
956
|
+
id: safeId(action.uploadArtifact?.id, "upload artifact id"),
|
|
957
|
+
...(action.uploadArtifact?.sha256
|
|
958
|
+
? {
|
|
959
|
+
sha256: sha256(
|
|
960
|
+
action.uploadArtifact.sha256,
|
|
961
|
+
"upload artifact digest",
|
|
962
|
+
),
|
|
963
|
+
size: nonNegativeInteger(
|
|
964
|
+
action.uploadArtifact.size,
|
|
965
|
+
"upload artifact size",
|
|
966
|
+
),
|
|
967
|
+
}
|
|
968
|
+
: {}),
|
|
969
|
+
};
|
|
970
|
+
if (
|
|
971
|
+
canonicalBrowserEvidenceJson(canonicalUpload) !==
|
|
972
|
+
canonicalBrowserEvidenceJson(action.uploadArtifact)
|
|
973
|
+
) {
|
|
974
|
+
throw new Error(
|
|
975
|
+
`browser evidence action ${index} upload record is not canonical`,
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
if (action.intent.expectedDigest) {
|
|
980
|
+
sha256(action.intent.expectedDigest, "browser action expected digest");
|
|
981
|
+
}
|
|
982
|
+
if (action.intent.targetOrigin) browserOrigin(action.intent.targetOrigin);
|
|
983
|
+
const canonicalOutcome = {
|
|
984
|
+
ok: action.outcome?.ok === true,
|
|
985
|
+
durationMs: Math.max(0, Number(action.outcome?.durationMs) || 0),
|
|
986
|
+
detail: safeText(action.outcome?.detail, 500),
|
|
987
|
+
};
|
|
988
|
+
if (
|
|
989
|
+
typeof action.outcome?.ok !== "boolean" ||
|
|
990
|
+
!Number.isFinite(Number(action.outcome?.durationMs)) ||
|
|
991
|
+
Number(action.outcome.durationMs) < 0 ||
|
|
992
|
+
canonicalBrowserEvidenceJson(canonicalOutcome) !==
|
|
993
|
+
canonicalBrowserEvidenceJson(action.outcome)
|
|
994
|
+
) {
|
|
995
|
+
throw new Error(`browser evidence action ${index} outcome is invalid`);
|
|
996
|
+
}
|
|
997
|
+
if (action.outcome.ok && !action.authority) {
|
|
998
|
+
throw new Error(
|
|
999
|
+
`browser evidence action ${index} successful outcome lacks authority`,
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
if (action.authority) {
|
|
1003
|
+
const authority = action.authority;
|
|
1004
|
+
const authorityPermission = {
|
|
1005
|
+
grantId: safeId(authority?.grantId, "browser action grant id"),
|
|
1006
|
+
grantDigest: sha256(
|
|
1007
|
+
authority?.grantDigest,
|
|
1008
|
+
"browser action grant digest",
|
|
1009
|
+
),
|
|
1010
|
+
origin: browserOrigin(authority?.origin),
|
|
1011
|
+
revision: positiveInteger(
|
|
1012
|
+
authority?.revision,
|
|
1013
|
+
"browser action grant revision",
|
|
1014
|
+
),
|
|
1015
|
+
scope: String(authority?.scope || ""),
|
|
1016
|
+
credentialBoundary: String(authority?.credentialBoundary || "none"),
|
|
1017
|
+
crossOrigin: authority?.crossOrigin === true,
|
|
1018
|
+
};
|
|
1019
|
+
const authorityKey = canonicalBrowserEvidenceJson(authorityPermission);
|
|
1020
|
+
if (
|
|
1021
|
+
!permissionKeys.has(authorityKey) ||
|
|
1022
|
+
authorityPermission.scope !== SCOPE_BY_ACTION[action.type]
|
|
1023
|
+
) {
|
|
1024
|
+
throw new Error(
|
|
1025
|
+
`browser evidence action ${index} authority is invalid`,
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
const canonicalAuthority = { ...authorityPermission };
|
|
1029
|
+
if (authority.crossOrigin) {
|
|
1030
|
+
const sourceGrantId = safeId(
|
|
1031
|
+
authority.sourceGrantId,
|
|
1032
|
+
"browser cross-origin source grant id",
|
|
1033
|
+
);
|
|
1034
|
+
const sourceOrigin = browserOrigin(authority.sourceOrigin);
|
|
1035
|
+
const sourceGrantDigest = sha256(
|
|
1036
|
+
authority.sourceGrantDigest,
|
|
1037
|
+
"browser cross-origin source grant digest",
|
|
1038
|
+
);
|
|
1039
|
+
const sourceRevision = positiveInteger(
|
|
1040
|
+
authority.sourceRevision,
|
|
1041
|
+
"browser cross-origin source revision",
|
|
1042
|
+
);
|
|
1043
|
+
const sourceCredentialBoundary = String(
|
|
1044
|
+
authority.sourceCredentialBoundary || "none",
|
|
1045
|
+
);
|
|
1046
|
+
Object.assign(canonicalAuthority, {
|
|
1047
|
+
sourceGrantId,
|
|
1048
|
+
sourceGrantDigest,
|
|
1049
|
+
sourceOrigin,
|
|
1050
|
+
sourceRevision,
|
|
1051
|
+
sourceCredentialBoundary,
|
|
1052
|
+
});
|
|
1053
|
+
if (sourceOrigin === authorityPermission.origin) {
|
|
1054
|
+
throw new Error(
|
|
1055
|
+
`browser evidence action ${index} cross-origin authority is invalid`,
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
const sourcePermissionFound = body.originPermissions.some(
|
|
1059
|
+
(permission) =>
|
|
1060
|
+
permission.grantId === authority.sourceGrantId &&
|
|
1061
|
+
permission.grantDigest === authority.sourceGrantDigest &&
|
|
1062
|
+
permission.origin === sourceOrigin &&
|
|
1063
|
+
permission.revision === authority.sourceRevision &&
|
|
1064
|
+
permission.scope === authority.scope &&
|
|
1065
|
+
permission.credentialBoundary === sourceCredentialBoundary &&
|
|
1066
|
+
permission.crossOrigin === false,
|
|
1067
|
+
);
|
|
1068
|
+
if (!sourcePermissionFound) {
|
|
1069
|
+
throw new Error(
|
|
1070
|
+
`browser evidence action ${index} source authority is missing`,
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
if (
|
|
1075
|
+
canonicalBrowserEvidenceJson(canonicalAuthority) !==
|
|
1076
|
+
canonicalBrowserEvidenceJson(authority)
|
|
1077
|
+
) {
|
|
1078
|
+
throw new Error(
|
|
1079
|
+
`browser evidence action ${index} authority is not canonical`,
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
if (action.type !== "upload" && action.uploadArtifact != null) {
|
|
1083
|
+
throw new Error(
|
|
1084
|
+
`browser evidence action ${index} upload is unexpected`,
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if (
|
|
1089
|
+
action.outcome.ok &&
|
|
1090
|
+
action.type === "upload" &&
|
|
1091
|
+
(!action.uploadArtifact?.sha256 ||
|
|
1092
|
+
!Number.isFinite(Number(action.uploadArtifact?.size)))
|
|
1093
|
+
) {
|
|
1094
|
+
throw new Error(
|
|
1095
|
+
`browser evidence action ${index} upload record is invalid`,
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
for (const kind of ["console", "network"]) {
|
|
1101
|
+
const observation = body.observations?.[kind];
|
|
1102
|
+
const normalizedObservation = normalizeObservationRows(
|
|
1103
|
+
observation?.records,
|
|
1104
|
+
kind,
|
|
1105
|
+
observation?.captureAvailable === true,
|
|
1106
|
+
);
|
|
1107
|
+
if (
|
|
1108
|
+
!observation ||
|
|
1109
|
+
!Array.isArray(observation.records) ||
|
|
1110
|
+
canonicalBrowserEvidenceJson(normalizedObservation) !==
|
|
1111
|
+
canonicalBrowserEvidenceJson(observation) ||
|
|
1112
|
+
(observation.captureAvailable !== true && observation.count > 0)
|
|
1113
|
+
) {
|
|
1114
|
+
throw new Error(`browser evidence ${kind} observation is invalid`);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
const canonicalPage = {
|
|
1118
|
+
origin: browserOrigin(body.observations?.page?.origin),
|
|
1119
|
+
title: safeText(body.observations?.page?.title, 500),
|
|
1120
|
+
queryValueRedactions: nonNegativeInteger(
|
|
1121
|
+
body.observations?.page?.queryValueRedactions,
|
|
1122
|
+
"page query redaction count",
|
|
1123
|
+
),
|
|
1124
|
+
credentialMaterialRetained: false,
|
|
1125
|
+
};
|
|
1126
|
+
if (
|
|
1127
|
+
body.observations?.page?.credentialMaterialRetained !== false ||
|
|
1128
|
+
canonicalBrowserEvidenceJson(canonicalPage) !==
|
|
1129
|
+
canonicalBrowserEvidenceJson(body.observations?.page)
|
|
1130
|
+
) {
|
|
1131
|
+
throw new Error("browser evidence page observation is invalid");
|
|
1132
|
+
}
|
|
1133
|
+
const dom = body.domSnapshot;
|
|
1134
|
+
const canonicalRedaction = {
|
|
1135
|
+
applied: dom?.redaction?.applied === true,
|
|
1136
|
+
sensitiveFieldValues: nonNegativeInteger(
|
|
1137
|
+
dom?.redaction?.sensitiveFieldValues,
|
|
1138
|
+
"DOM sensitive field redaction count",
|
|
1139
|
+
),
|
|
1140
|
+
urlQueryValues: nonNegativeInteger(
|
|
1141
|
+
dom?.redaction?.urlQueryValues,
|
|
1142
|
+
"DOM URL query redaction count",
|
|
1143
|
+
),
|
|
1144
|
+
secretPatterns: nonNegativeInteger(
|
|
1145
|
+
dom?.redaction?.secretPatterns,
|
|
1146
|
+
"DOM secret pattern redaction count",
|
|
1147
|
+
),
|
|
1148
|
+
};
|
|
1149
|
+
const canonicalDom = {
|
|
1150
|
+
digest: sha256(dom?.digest, "DOM snapshot digest"),
|
|
1151
|
+
capturedChars: nonNegativeInteger(
|
|
1152
|
+
dom?.capturedChars,
|
|
1153
|
+
"DOM captured character count",
|
|
1154
|
+
),
|
|
1155
|
+
sourceChars: nonNegativeInteger(
|
|
1156
|
+
dom?.sourceChars,
|
|
1157
|
+
"DOM source character count",
|
|
1158
|
+
),
|
|
1159
|
+
cap: nonNegativeInteger(dom?.cap, "DOM capture cap"),
|
|
1160
|
+
truncated: dom?.truncated === true,
|
|
1161
|
+
captureSucceeded: dom?.captureSucceeded === true,
|
|
1162
|
+
redaction: canonicalRedaction,
|
|
1163
|
+
contentRetained: false,
|
|
1164
|
+
};
|
|
1165
|
+
if (
|
|
1166
|
+
!dom ||
|
|
1167
|
+
!SHA256_RE.test(String(dom.digest || "")) ||
|
|
1168
|
+
!Number.isSafeInteger(dom.capturedChars) ||
|
|
1169
|
+
!Number.isSafeInteger(dom.sourceChars) ||
|
|
1170
|
+
!Number.isSafeInteger(dom.cap) ||
|
|
1171
|
+
dom.capturedChars < 0 ||
|
|
1172
|
+
dom.sourceChars < dom.capturedChars ||
|
|
1173
|
+
dom.cap < dom.capturedChars ||
|
|
1174
|
+
typeof dom.truncated !== "boolean" ||
|
|
1175
|
+
typeof dom.captureSucceeded !== "boolean" ||
|
|
1176
|
+
canonicalBrowserEvidenceJson(canonicalRedaction) !==
|
|
1177
|
+
canonicalBrowserEvidenceJson(dom.redaction) ||
|
|
1178
|
+
dom.contentRetained !== false ||
|
|
1179
|
+
canonicalBrowserEvidenceJson(canonicalDom) !==
|
|
1180
|
+
canonicalBrowserEvidenceJson(dom)
|
|
1181
|
+
) {
|
|
1182
|
+
throw new Error("browser evidence DOM snapshot metadata is invalid");
|
|
1183
|
+
}
|
|
1184
|
+
for (const [collection, actionType, digestField] of [
|
|
1185
|
+
[body.screenshots, "screenshot", "screenshotDigest"],
|
|
1186
|
+
[body.downloads, "download", "downloadDigest"],
|
|
1187
|
+
]) {
|
|
1188
|
+
if (!Array.isArray(collection)) {
|
|
1189
|
+
throw new Error(`browser evidence ${actionType} records are invalid`);
|
|
1190
|
+
}
|
|
1191
|
+
for (const [index, record] of collection.entries()) {
|
|
1192
|
+
const action = body.actions[record?.actionIndex];
|
|
1193
|
+
const canonicalRecord = {
|
|
1194
|
+
index,
|
|
1195
|
+
digest: sha256(record?.digest, `${actionType} digest`),
|
|
1196
|
+
actionIndex: nonNegativeInteger(
|
|
1197
|
+
record?.actionIndex,
|
|
1198
|
+
`${actionType} action index`,
|
|
1199
|
+
),
|
|
1200
|
+
...(actionType === "download"
|
|
1201
|
+
? { suggestedName: safeText(record?.suggestedName, 200) }
|
|
1202
|
+
: {}),
|
|
1203
|
+
};
|
|
1204
|
+
if (
|
|
1205
|
+
record?.index !== index ||
|
|
1206
|
+
!action ||
|
|
1207
|
+
action.type !== actionType ||
|
|
1208
|
+
canonicalRecord.digest !== action[digestField] ||
|
|
1209
|
+
canonicalBrowserEvidenceJson(canonicalRecord) !==
|
|
1210
|
+
canonicalBrowserEvidenceJson(record)
|
|
1211
|
+
) {
|
|
1212
|
+
throw new Error(`browser evidence ${actionType} record is invalid`);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
for (const [actionIndex, action] of body.actions.entries()) {
|
|
1216
|
+
const records = collection.filter(
|
|
1217
|
+
(record) => record.actionIndex === actionIndex,
|
|
1218
|
+
);
|
|
1219
|
+
if (
|
|
1220
|
+
action.type === actionType &&
|
|
1221
|
+
action.outcome.ok &&
|
|
1222
|
+
(records.length !== 1 || !action[digestField])
|
|
1223
|
+
) {
|
|
1224
|
+
throw new Error(
|
|
1225
|
+
`browser evidence ${actionType} action record is incomplete`,
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
if (
|
|
1229
|
+
(action.type !== actionType || !action.outcome.ok) &&
|
|
1230
|
+
records.length > 0
|
|
1231
|
+
) {
|
|
1232
|
+
throw new Error(
|
|
1233
|
+
`browser evidence ${actionType} action record is unexpected`,
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
const replay = body.replay;
|
|
1239
|
+
const canonicalReplay = {
|
|
1240
|
+
sourceEnvelopeDigest:
|
|
1241
|
+
replay?.sourceEnvelopeDigest === null
|
|
1242
|
+
? null
|
|
1243
|
+
: sha256(replay?.sourceEnvelopeDigest, "replay source envelope digest"),
|
|
1244
|
+
sideEffectBoundary: String(replay?.sideEffectBoundary || ""),
|
|
1245
|
+
credentialBoundary: String(replay?.credentialBoundary || ""),
|
|
1246
|
+
actionCount: nonNegativeInteger(replay?.actionCount, "replay action count"),
|
|
1247
|
+
};
|
|
1248
|
+
if (
|
|
1249
|
+
!replay ||
|
|
1250
|
+
!Number.isSafeInteger(replay.actionCount) ||
|
|
1251
|
+
replay.actionCount < 0 ||
|
|
1252
|
+
![
|
|
1253
|
+
"recorded-not-authorized-for-replay",
|
|
1254
|
+
"deny",
|
|
1255
|
+
"explicitly-approved",
|
|
1256
|
+
].includes(replay.sideEffectBoundary) ||
|
|
1257
|
+
!["payloads-not-retained", "deny", "explicitly-approved"].includes(
|
|
1258
|
+
replay.credentialBoundary,
|
|
1259
|
+
) ||
|
|
1260
|
+
canonicalBrowserEvidenceJson(canonicalReplay) !==
|
|
1261
|
+
canonicalBrowserEvidenceJson(replay)
|
|
1262
|
+
) {
|
|
1263
|
+
throw new Error("browser evidence replay record is invalid");
|
|
1264
|
+
}
|
|
1265
|
+
if (replay.sourceEnvelopeDigest !== null) {
|
|
1266
|
+
sha256(replay.sourceEnvelopeDigest, "replay source envelope digest");
|
|
1267
|
+
if (
|
|
1268
|
+
replay.actionCount < 1 ||
|
|
1269
|
+
replay.sideEffectBoundary === "recorded-not-authorized-for-replay" ||
|
|
1270
|
+
replay.credentialBoundary === "payloads-not-retained"
|
|
1271
|
+
) {
|
|
1272
|
+
throw new Error("browser evidence replay authority is invalid");
|
|
1273
|
+
}
|
|
1274
|
+
} else if (
|
|
1275
|
+
replay.actionCount !== 0 ||
|
|
1276
|
+
replay.sideEffectBoundary !== "recorded-not-authorized-for-replay" ||
|
|
1277
|
+
replay.credentialBoundary !== "payloads-not-retained"
|
|
1278
|
+
) {
|
|
1279
|
+
throw new Error("browser evidence replay default boundary is invalid");
|
|
1280
|
+
}
|
|
1281
|
+
if (
|
|
1282
|
+
isoTimestamp(body.capturedAt, "browser evidence capturedAt") !==
|
|
1283
|
+
body.capturedAt
|
|
1284
|
+
) {
|
|
1285
|
+
throw new Error("browser evidence capturedAt is not canonical");
|
|
1286
|
+
}
|
|
1287
|
+
return Object.freeze(cloneJson(envelope, "browser evidence envelope"));
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
export function authorizeBrowserReplay({
|
|
1291
|
+
sourceEnvelope,
|
|
1292
|
+
binding,
|
|
1293
|
+
actions,
|
|
1294
|
+
allowSideEffects = false,
|
|
1295
|
+
allowCredentials = false,
|
|
1296
|
+
} = {}) {
|
|
1297
|
+
const source = verifyBrowserEvidenceEnvelope(sourceEnvelope);
|
|
1298
|
+
const normalizedBinding = normalizeBrowserEvidenceBinding(binding);
|
|
1299
|
+
if (
|
|
1300
|
+
canonicalBrowserEvidenceJson(source.binding) !==
|
|
1301
|
+
canonicalBrowserEvidenceJson(normalizedBinding)
|
|
1302
|
+
) {
|
|
1303
|
+
throw new Error("browser replay binding does not match source evidence");
|
|
1304
|
+
}
|
|
1305
|
+
const requested = Array.isArray(actions) ? actions : [];
|
|
1306
|
+
if (requested.length === 0) throw new Error("browser replay has no actions");
|
|
1307
|
+
for (const action of requested) {
|
|
1308
|
+
const effect = SIDE_EFFECT_BY_ACTION[String(action?.type || "")];
|
|
1309
|
+
if (!effect)
|
|
1310
|
+
throw new Error("browser replay contains an unsupported action");
|
|
1311
|
+
if (effect !== "none" && !allowSideEffects) {
|
|
1312
|
+
throw new Error(
|
|
1313
|
+
`browser replay side-effect boundary denied ${action.type}`,
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
if (
|
|
1317
|
+
["type", "upload"].includes(action.type) ||
|
|
1318
|
+
(action.type === "navigate" && actionCarriesCredentialMaterial(action))
|
|
1319
|
+
) {
|
|
1320
|
+
if (!allowCredentials) {
|
|
1321
|
+
throw new Error(
|
|
1322
|
+
`browser replay credential boundary denied ${action.type}`,
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
throw new Error(
|
|
1326
|
+
`browser replay cannot reconstruct ${action.type} payloads because evidence never retains credentials`,
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
const intentDigest = browserEvidenceDigest(browserActionIntent(action));
|
|
1330
|
+
const represented = source.actions.some(
|
|
1331
|
+
(sourceAction) => sourceAction.intentDigest === intentDigest,
|
|
1332
|
+
);
|
|
1333
|
+
if (!represented) {
|
|
1334
|
+
throw new Error(
|
|
1335
|
+
`browser replay action ${action.type} is not represented by source evidence`,
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
return Object.freeze({
|
|
1340
|
+
sourceEnvelopeDigest: source.envelopeDigest,
|
|
1341
|
+
sideEffectBoundary: allowSideEffects ? "explicitly-approved" : "deny",
|
|
1342
|
+
credentialBoundary: allowCredentials ? "explicitly-approved" : "deny",
|
|
1343
|
+
actionCount: requested.length,
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
export function createClaudeIncrementAuditFragment({
|
|
1348
|
+
commitmentId,
|
|
1349
|
+
headSha,
|
|
1350
|
+
os,
|
|
1351
|
+
runtime,
|
|
1352
|
+
profileVersion,
|
|
1353
|
+
thresholds,
|
|
1354
|
+
measurements,
|
|
1355
|
+
testIds,
|
|
1356
|
+
producerDigests,
|
|
1357
|
+
disposition = "required",
|
|
1358
|
+
outcome = "passed",
|
|
1359
|
+
source,
|
|
1360
|
+
} = {}) {
|
|
1361
|
+
if (!["required", "advisory"].includes(disposition)) {
|
|
1362
|
+
throw new Error("audit fragment disposition is invalid");
|
|
1363
|
+
}
|
|
1364
|
+
if (!["passed", "failed"].includes(outcome)) {
|
|
1365
|
+
throw new Error("audit fragment outcome is invalid");
|
|
1366
|
+
}
|
|
1367
|
+
if (disposition === "required" && outcome !== "passed") {
|
|
1368
|
+
throw new Error("required audit fragments must have a passed outcome");
|
|
1369
|
+
}
|
|
1370
|
+
const normalizedDigests = {};
|
|
1371
|
+
for (const [file, digest] of Object.entries(producerDigests || {}).sort()) {
|
|
1372
|
+
normalizedDigests[
|
|
1373
|
+
repositoryRelativePosixPath(file, "producer digest path")
|
|
1374
|
+
] = sha256(digest, `producer digest for ${file}`);
|
|
1375
|
+
}
|
|
1376
|
+
if (Object.keys(normalizedDigests).length === 0) {
|
|
1377
|
+
throw new Error("audit fragment requires producer digests");
|
|
1378
|
+
}
|
|
1379
|
+
return Object.freeze({
|
|
1380
|
+
schema: CLAUDE_INCREMENT_AUDIT_FRAGMENT_SCHEMA,
|
|
1381
|
+
commitmentId: safeId(commitmentId, "commitment id"),
|
|
1382
|
+
headSha: exactSha(headSha, "fragment head SHA"),
|
|
1383
|
+
os: safeId(os, "fragment OS"),
|
|
1384
|
+
runtime: {
|
|
1385
|
+
name: safeId(runtime?.name, "runtime name"),
|
|
1386
|
+
version: safeText(runtime?.version, 100),
|
|
1387
|
+
arch: safeId(runtime?.arch, "runtime architecture"),
|
|
1388
|
+
},
|
|
1389
|
+
profileVersion: safeId(profileVersion, "profile version"),
|
|
1390
|
+
thresholds: cloneJson(thresholds || {}, "fragment thresholds"),
|
|
1391
|
+
measurements: cloneJson(measurements || {}, "fragment measurements"),
|
|
1392
|
+
testIds: (Array.isArray(testIds) ? testIds : []).map((id) =>
|
|
1393
|
+
safeId(id, "test id"),
|
|
1394
|
+
),
|
|
1395
|
+
producerDigests: normalizedDigests,
|
|
1396
|
+
disposition,
|
|
1397
|
+
outcome,
|
|
1398
|
+
source: {
|
|
1399
|
+
workflowId: safeId(source?.workflowId, "workflow id"),
|
|
1400
|
+
runId: safeId(source?.runId, "run id"),
|
|
1401
|
+
jobId: safeId(source?.jobId, "job id"),
|
|
1402
|
+
artifactName: safeId(source?.artifactName, "artifact name"),
|
|
1403
|
+
},
|
|
1404
|
+
});
|
|
1405
|
+
}
|