chainlesschain 0.163.8 → 0.165.1
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 +7 -4
- package/scripts/macos-mcp-launcher-build.mjs +317 -0
- package/scripts/macos-mcp-launcher-postinstall.sh +54 -0
- package/scripts/macos-mcp-launcher-preinstall.sh +61 -0
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-CavM7W3k.js → AIOps-CYxFDrcm.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CUvZGabQ.js → ActionButton-CPBtdiU2.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DeBnWnbe.js → Analytics-Dayq7Dp7.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DbI5kxvd.js → AppLayout-6U7bPb2s.js} +4 -4
- package/src/assets/web-panel/assets/{Artifacts-D93bigKm.js → Artifacts-D2Iqg_3D.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CBNvfUZa.js → Audit-CdoOB7WV.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-CJLyh_YU.js → BackgroundAgents-DrnAVB8w.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-D_0yoehD.js → Backup-CesMi5cy.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-TJxmbObW.js → BaseInput-DaX9ptax.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CE0dto1q.js → Chat-OrwOPxDS.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CZ5-yYiD.js → ChatBubbleRenderer-CvPZFRxM.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-Ck1RVIVo.js → Checkbox-DiHbcysC.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-qpoyf6DK.js → Codegen-CbgCkaBJ.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DUyPWjIE.js → Col-Du5hgrh4.js} +1 -1
- package/src/assets/web-panel/assets/{Community-D_S1BemS.js → Community-DsPOBW-p.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BjxeK-Gc.js → Compact-BwHIFkNC.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-Dd3CEJsu.js → Compliance-BvrDQSgd.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-B4QG7-ad.js → Cowork-gwAE6arx.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-BtZnids5.js → Cron-DW-0l4uF.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-CHPfCBFD.js → Crosschain-j-NsLesB.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DT8DCEOK.js → DID-CibVEBOI.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-COQab5_U.js → Dashboard-W4z9vSfe.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BY1PsQuU.js → Dropdown-DAcrkKxG.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CsuAuaq3.js → EmailListRenderer-6GXah-89.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BqpinD-j.js → FamilyGuardDashboard-TKDCq8Ku.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-D0gkfQFv.js → Federation-BXBLlcNm.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BqgE-axK.js → FormItemContext-BoJJBkG3.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-D0hJ2KvC.js → GenericCardRenderer-okjsl46v.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CmMB0uPY.js → Git-Rvs7i6nn.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BnVEI0Dk.js → Governance-CHSUxXXS.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-YTAMihRP.js → Inference-V3BlfxsB.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-0ax7VHop.js → KnowledgeGraph-C_NlD4aR.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-B9TRcokL.js → Logs-Nn4LNxhA.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-SVAgNnkt.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-B6CSuD_H.js → Marketplace-DEmjFcg-.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-CjMXvOVU.js → McpTools-DlopXipI.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-CXRF71U3.js → Memory-BG3ynX60.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DQgNp_oS.js → MobileBridge-DeRzWOfT.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-DlPwcCbq.js → MobileProjects-Tyd3o2A0.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-6hDTRPe1.js → Mtc-BoPbOv1C.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-BlVHU1DS.js → MtcAudit-un-rTHp7.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-DP9pzEbV.js → Multisig-Ds0oveEB.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-Djorh8EV.js → NLProgramming-D09JI-yY.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-oLZ-JkUg.js → Notes-DTRhiKJn.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-Cg288VH3.js → NotificationSettings-BAnnNCim.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-Yr-1fSYi.js +1 -0
- package/src/assets/web-panel/assets/{Organization-CBdo-bHs.js → Organization-S9EAolpj.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-qZdnVumx.js → Overflow-CqcLKqJ8.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-D4kNfGNQ.js → P2P-Dw0F9EcU.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-DnlMemM7.js → PdhVaultBrowser-BDRwUevN.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-DHucHFBJ.js → Permissions-D3_c3nKB.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BovlOIPR.js → PersonalDataHub-CxdMV98x.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-Deru3CQY.js → Pipeline-CgjNQFLI.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-Bq5oTTbU.js → Privacy-BHghIIht.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BfzxVWAz.js → ProjectInit-De6pqaKM.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CKC6XBtH.js → ProjectSettings-Bg4ZkNYQ.js} +2 -2
- package/src/assets/web-panel/assets/Projects-CP8vPvzs.js +1 -0
- package/src/assets/web-panel/assets/{Providers-C_jx4TWX.js → Providers-CUnHbNf-.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BdIHeVGC.js → QrScannerModal-DA2f3Wts.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-BwZwAcPD.js → QuickAsk-C1FeHDqp.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BNTt0L-m.js → Recommend-B5guaUMN.js} +1 -1
- package/src/assets/web-panel/assets/RemoteSession-PDADSds3.js +5 -0
- package/src/assets/web-panel/assets/{Reputation-D6VpG32B.js → Reputation-eTlRhgvK.js} +1 -1
- package/src/assets/web-panel/assets/{Row-D_74OvQQ.js → Row-D7AqSGY0.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BTr0a-BQ.js → RssFeed-JiuLm4Ja.js} +3 -3
- package/src/assets/web-panel/assets/{Search-C3MCFoNU.js → Search-CwruahW3.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CqW9aJ6s.js → Security-CdhqaR0b.js} +4 -4
- package/src/assets/web-panel/assets/{Services-Cw5GNaHF.js → Services-CU0ZIlcT.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DZps5qeY.js → Skeleton-CtduPfAb.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-C6NQ1znT.js → Skills-CROXm6AY.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DFP8G2Sh.js → Sla-DiZiABFD.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-ESt6MB6v.js → SpeechSettings-B5pLcXUw.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BHnN98aM.js → SyncSettings-DBX6J1Oe.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-NiSCaOf-.js → Tasks-BLyRJd3-.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DsKFhW2C.js → Templates-BKa5qzmk.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CQ0E0ze5.js → Tenant-DnD3a4M6.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-D_rBu_by.js → Terminal-DEitQeTb.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-D3Vm4Po4.js → TimelineRenderer-CjHggTDq.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DNTxRH6q.js → Tokens-CsGT7sfl.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-Dgprz1ms.js → Trigger-yr4rFDrW.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-B2rUnVBD.js → Trust-CLvu1Sjp.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DEGQNc6_.js → UkeySign-B0m7yUP8.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-jlsAx3Eh.js → VideoEditing-C0VvP4lc.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-8jQih1ij.js → Wallet-kllBAG1M.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-XLeO6xIB.js → WebAuthn-C54UQNLx.js} +3 -3
- package/src/assets/web-panel/assets/{WorkflowEditor-De_dgDAG.js → WorkflowEditor-B7y8qSvU.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CI19R_Sk.js → chat-DRzTh8CJ.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DsYTOYN3.js → colors-BSMwSnCx.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CM3-t_Qr.js → compact-item-BTg602H5.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BjxlBRZv.js → createContext-B6EGsdvp.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-Dol8jLEP.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-RDaWX_4i.js → hasIn-DDKqJ8RD.js} +1 -1
- package/src/assets/web-panel/assets/{index-nAH87mQP.js → index-323Hhxxt.js} +1 -1
- package/src/assets/web-panel/assets/{index-DIUnNOjG.js → index-BICn3Qh1.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyIWssPC.js → index-BMuGd2--.js} +1 -1
- package/src/assets/web-panel/assets/{index-DX7tuWVJ.js → index-BRqt4fO-.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cd3euP_4.js → index-BVTAl5N2.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cpqm1qfU.js → index-BsR7Y2NG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DcPFB8qJ.js → index-Bx044qDj.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cd9OJhHG.js → index-C1GFfuqN.js} +1 -1
- package/src/assets/web-panel/assets/index-C3QX5tOl.js +1 -0
- package/src/assets/web-panel/assets/{index-B-Z8eFUJ.js → index-C66NpWy5.js} +1 -1
- package/src/assets/web-panel/assets/{index-BlT2VAEK.js → index-CDTvsX8t.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dsij_KsN.js → index-CGNklbP6.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4gkzxkw.js → index-CHfLYwcW.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6sm5-Xq.js → index-CNxg61YP.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0yx-jtQ.js → index-CQlHFN_g.js} +1 -1
- package/src/assets/web-panel/assets/{index-CjTrRt13.js → index-CUEU48o1.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7WBt_4T.js → index-CWGax2QW.js} +1 -1
- package/src/assets/web-panel/assets/{index-IeiL9RKc.js → index-C_JzIXUq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CnHH7bnD.js → index-Cf23wVMs.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKJr-TZp.js → index-CmAInUkX.js} +1 -1
- package/src/assets/web-panel/assets/{index-CiECJi-Q.js → index-CmAubjDi.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCnZyIMA.js → index-Co7oM1qr.js} +1 -1
- package/src/assets/web-panel/assets/{index-Drlnwr8F.js → index-CwPUZbMI.js} +1 -1
- package/src/assets/web-panel/assets/{index-9R6BxAr0.js → index-D1naNj4U.js} +1 -1
- package/src/assets/web-panel/assets/{index-BGqqJfL0.js → index-D5CsBShd.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bi46Xu4u.js → index-DBT3YeQ9.js} +1 -1
- package/src/assets/web-panel/assets/{index-DXXtGMUJ.js → index-DLdrHhML.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0PMlbMs.js → index-Dc77VR3H.js} +1 -1
- package/src/assets/web-panel/assets/{index-D0zcy54x.js → index-Dfv2lKu3.js} +1 -1
- package/src/assets/web-panel/assets/{index-BN6UPD2E.js → index-DiW0sDkz.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dxge-xWl.js → index-DpTHIRfZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CyOGN6a5.js → index-DqFXGzzC.js} +1 -1
- package/src/assets/web-panel/assets/{index-B9O9YyWE.js → index-XFe-1twF.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHNYPO0b.js → index-p2HYn89m.js} +3 -3
- package/src/assets/web-panel/assets/{index-CVQMeiwH.js → index-pqfux8vZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-pPL66J85.js → index-rjzk9z1Q.js} +1 -1
- package/src/assets/web-panel/assets/{index-DoQA0-52.js → index-th2ihDc0.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5REnKt8.js → index-u2YVld9W.js} +1 -1
- package/src/assets/web-panel/assets/index-xYybvEic.js +1 -0
- package/src/assets/web-panel/assets/{initDefaultProps-BTL88of4.js → initDefaultProps-C3Djh9fa.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BUB2pXwB.js → motion-BzqFmbSe.js} +1 -1
- package/src/assets/web-panel/assets/{move-egCkkOMm.js → move-DJo_FFaW.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DAWhZC8c.js → mtc-parser-_tlTiBvk.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CvOxe_6d.js → omit-CB2ZyxUg.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DZUSLZ5Z.js → pickAttrs-DZwIFD4u.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BERxGNMY.js → placementArrow-DxZY0dgk.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DZuDfZ9n.js → responsiveObserve-CzzGt7GL.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DO756sgI.js → slide-BaIK52yz.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BRQlIz28.js → statusUtils-3-FHu9_M.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DCSfdybl.js → styleChecker-BkRT68fK.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-LD8dWVve.js → useFlexGapSupport-DHYwKS-s.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BX9uW2LK.js → useFs-DVVUhV8p.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BPSlPuHU.js → usePersonalDataHub-CkrFKAzQ.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BmOILC9V.js → vnode-BuBLFz0c.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-1iKowKGg.js → zoom-kpm6LtXU.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +3 -3
- package/src/commands/cowork.js +418 -41
- package/src/commands/permissions.js +240 -16
- package/src/commands/plugin.js +1293 -56
- package/src/commands/session-location.js +276 -0
- package/src/commands/session.js +2 -0
- package/src/data/changelog.json +28 -2
- package/src/gateways/ws/action-protocol.js +316 -17
- package/src/gateways/ws/message-dispatcher.js +30 -0
- package/src/gateways/ws/remote-session-protocol.js +565 -35
- package/src/gateways/ws/ws-agent-handler.js +12 -0
- package/src/gateways/ws/ws-approval-gate.js +5 -0
- package/src/gateways/ws/ws-server.js +510 -50
- package/src/harness/jsonl-session-store.js +341 -24
- package/src/harness/mcp-client.js +8 -1
- package/src/harness/remote-session-audit.js +2 -0
- package/src/harness/remote-session-crypto.js +929 -21
- package/src/harness/remote-session-registry.js +945 -24
- package/src/harness/session-list-index.js +233 -14
- package/src/lib/agent-authority.js +6 -0
- package/src/lib/auto-mode-config.js +98 -3
- package/src/lib/background-agent-keeper-client.js +248 -0
- package/src/lib/background-agent-keeper-protocol.js +291 -0
- package/src/lib/background-agent-supervisor.js +1357 -85
- package/src/lib/background-interaction-resolver.js +12 -0
- package/src/lib/background-session-transport.js +23 -6
- package/src/lib/background-turn-bootstrap-protocol.js +190 -0
- package/src/lib/cowork-workflow.js +1188 -76
- package/src/lib/delivery-production-adapter.js +38 -2
- package/src/lib/dynamic-workflow-facade.js +1736 -0
- package/src/lib/execution-location-contract.js +551 -0
- package/src/lib/execution-location-runtime.js +130 -0
- package/src/lib/ide-context.js +94 -6
- package/src/lib/mcp-stdio-package-materialization.js +166 -14
- package/src/lib/packer/pkg-config-generator.js +2 -0
- package/src/lib/permission-authority.js +77 -0
- package/src/lib/permission-side-effect-center.js +495 -0
- package/src/lib/plugin-runtime/install.js +514 -13
- package/src/lib/plugin-runtime/marketplace-artifact-readback.js +900 -0
- package/src/lib/plugin-runtime/marketplace-catalog.js +1181 -0
- package/src/lib/plugin-runtime/marketplace-impact.js +408 -0
- package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +1567 -0
- package/src/lib/plugin-security.js +60 -3
- package/src/lib/process-execution-broker/index.js +1095 -57
- package/src/lib/process-execution-broker/linux-bwrap-descriptor-launch.js +375 -0
- package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +90 -16
- package/src/lib/process-execution-broker/linux-generic-bwrap.js +159 -18
- package/src/lib/process-execution-broker/macos-mcp-launcher-contract.js +278 -0
- package/src/lib/process-execution-broker/macos-mcp-launcher-protocol.json +23 -0
- package/src/lib/process-execution-broker/macos-mcp-launcher.c +1497 -0
- package/src/lib/process-execution-broker/mcp-fd-entry-bootstrap.js +37 -0
- package/src/lib/process-execution-broker/platform-sandbox.js +2330 -279
- package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
- package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
- package/src/lib/process-execution-broker/windows-sandbox.cs +50 -4
- package/src/lib/public-ide-capability-manifest.js +3 -0
- package/src/lib/remote-approval-bridge.js +777 -34
- package/src/lib/remote-control.js +2 -0
- package/src/lib/remote-membership-authority.js +632 -0
- package/src/lib/remote-membership-coordinator.js +2973 -0
- package/src/lib/remote-membership-host-store.js +1799 -0
- package/src/lib/runtime-provenance-ledger.js +171 -46
- package/src/lib/scoped-permission-store.js +369 -0
- package/src/lib/session-anti-rollback-anchor.js +262 -5
- package/src/lib/shell-approval.js +169 -6
- package/src/lib/sub-agent-context.js +4 -0
- package/src/lib/with-file-lock.js +73 -38
- package/src/lib/workflow-definition-contract.js +149 -0
- package/src/repl/agent-repl.js +111 -36
- package/src/repl/chat-repl.js +2 -0
- package/src/repl/clipboard-image.js +735 -16
- package/src/repl/ide-status.js +1 -1
- package/src/repl/prompt-interactions.js +59 -6
- package/src/repl/remote-approval.js +29 -7
- package/src/runtime/agent-core.js +1182 -155
- package/src/runtime/headless-runner.js +52 -30
- package/src/runtime/headless-stream.js +51 -28
- package/src/workers/background-agent-keeper.js +689 -0
- package/src/workers/background-agent-worker.js +365 -13
- package/src/workers/background-turn-bootstrap.js +170 -0
- package/src/assets/web-panel/assets/MarkdownRenderer-yySN5YNM.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-Bb1O3Ofr.js +0 -1
- package/src/assets/web-panel/assets/Projects-CMolGhae.js +0 -1
- package/src/assets/web-panel/assets/RemoteSession-Biyn2-Ro.js +0 -5
- package/src/assets/web-panel/assets/devWarning-S1ld2LwU.js +0 -1
- package/src/assets/web-panel/assets/index-Drz6yq0m.js +0 -1
- package/src/assets/web-panel/assets/index-uWiUs7S_.js +0 -1
|
@@ -12,6 +12,16 @@ import {
|
|
|
12
12
|
|
|
13
13
|
const PROTOCOL = "chainlesschain.remote-session.e2ee.v1";
|
|
14
14
|
const PAIRING_SCHEME = "chainlesschain://remote-session/pair#";
|
|
15
|
+
const RELAY_POSSESSION_SCHEMA =
|
|
16
|
+
"chainlesschain.remote-session.relay-possession-capability/v1";
|
|
17
|
+
const relayPossessionCapabilities = new WeakMap();
|
|
18
|
+
const stagedRelayPairings = new WeakMap();
|
|
19
|
+
const RELAY_AUTHORITY_SCOPES = new Set([
|
|
20
|
+
"observe",
|
|
21
|
+
"prompt",
|
|
22
|
+
"approve",
|
|
23
|
+
"interrupt",
|
|
24
|
+
]);
|
|
15
25
|
|
|
16
26
|
function b64(value) {
|
|
17
27
|
return Buffer.from(value).toString("base64url");
|
|
@@ -34,6 +44,229 @@ function importPublicKey(value) {
|
|
|
34
44
|
return createPublicKey({ key: der, type: "spki", format: "der" });
|
|
35
45
|
}
|
|
36
46
|
|
|
47
|
+
function canonicalPublicKey(value) {
|
|
48
|
+
const key = importPublicKey(value);
|
|
49
|
+
if (key.asymmetricKeyType !== "x25519") {
|
|
50
|
+
throw new Error("Remote Session peer key must be X25519");
|
|
51
|
+
}
|
|
52
|
+
return exportPublicKey(key);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function digest(domain, value) {
|
|
56
|
+
return createHash("sha256")
|
|
57
|
+
.update(domain, "utf8")
|
|
58
|
+
.update("\0", "utf8")
|
|
59
|
+
.update(value)
|
|
60
|
+
.digest("hex");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function remoteSessionPairingTokenDigest(pairingToken) {
|
|
64
|
+
if (typeof pairingToken !== "string" || pairingToken.length === 0) {
|
|
65
|
+
throw new Error("Remote Session pairing token is required");
|
|
66
|
+
}
|
|
67
|
+
return digest("chainlesschain.remote-session.pairing-token.v1", pairingToken);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function relayEnvelopeTranscriptHash(envelope) {
|
|
71
|
+
const normalized = {
|
|
72
|
+
v: envelope.v,
|
|
73
|
+
sessionId: envelope.sessionId,
|
|
74
|
+
senderId: envelope.senderId,
|
|
75
|
+
sequence: envelope.sequence,
|
|
76
|
+
nonce: envelope.nonce,
|
|
77
|
+
ciphertext: envelope.ciphertext,
|
|
78
|
+
tag: envelope.tag,
|
|
79
|
+
};
|
|
80
|
+
return digest(
|
|
81
|
+
"chainlesschain.remote-session.relay-envelope.v1",
|
|
82
|
+
JSON.stringify(normalized),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function consumeRemoteRelayPossessionCapability(
|
|
87
|
+
capability,
|
|
88
|
+
{
|
|
89
|
+
sessionId,
|
|
90
|
+
mobilePeerId,
|
|
91
|
+
mobilePublicKey,
|
|
92
|
+
pairingTokenDigest,
|
|
93
|
+
coordinatorId,
|
|
94
|
+
serverInstanceId,
|
|
95
|
+
authorityVersion,
|
|
96
|
+
} = {},
|
|
97
|
+
) {
|
|
98
|
+
const proof = relayPossessionCapabilities.get(capability);
|
|
99
|
+
// Consume before checking the caller's expectation. A confused-deputy or
|
|
100
|
+
// stale binding attempt cannot probe and then reuse the capability.
|
|
101
|
+
relayPossessionCapabilities.delete(capability);
|
|
102
|
+
const expectedPublicKey = canonicalPublicKey(mobilePublicKey);
|
|
103
|
+
if (
|
|
104
|
+
!proof ||
|
|
105
|
+
proof.sessionId !== sessionId ||
|
|
106
|
+
proof.mobilePeerId !== mobilePeerId ||
|
|
107
|
+
proof.mobilePublicKey !== expectedPublicKey ||
|
|
108
|
+
proof.pairingTokenDigest !== pairingTokenDigest ||
|
|
109
|
+
proof.coordinatorId !== coordinatorId ||
|
|
110
|
+
proof.serverInstanceId !== serverInstanceId ||
|
|
111
|
+
proof.authorityVersion !== authorityVersion
|
|
112
|
+
) {
|
|
113
|
+
throw new Error("Remote relay possession capability is invalid or stale");
|
|
114
|
+
}
|
|
115
|
+
return Object.freeze({
|
|
116
|
+
...proof,
|
|
117
|
+
authorizedScopes: Object.freeze([...proof.authorizedScopes]),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function normalizeRelayAuthorityScopes(scopes) {
|
|
122
|
+
if (!Array.isArray(scopes) || scopes.length === 0) {
|
|
123
|
+
throw new Error("Durable Remote relay scopes are required");
|
|
124
|
+
}
|
|
125
|
+
const normalized = [...new Set(scopes.map(String))].sort();
|
|
126
|
+
if (normalized.some((scope) => !RELAY_AUTHORITY_SCOPES.has(scope))) {
|
|
127
|
+
throw new Error("Durable Remote relay scopes are invalid");
|
|
128
|
+
}
|
|
129
|
+
return Object.freeze(normalized);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeRelayCapabilities(capabilities) {
|
|
133
|
+
if (capabilities == null) return Object.freeze([]);
|
|
134
|
+
if (!Array.isArray(capabilities) || capabilities.length > 32) {
|
|
135
|
+
throw new Error("Durable Remote relay capabilities are invalid");
|
|
136
|
+
}
|
|
137
|
+
const normalized = [...new Set(capabilities)];
|
|
138
|
+
if (
|
|
139
|
+
normalized.some(
|
|
140
|
+
(capability) =>
|
|
141
|
+
typeof capability !== "string" ||
|
|
142
|
+
capability.length === 0 ||
|
|
143
|
+
capability.length > 128 ||
|
|
144
|
+
capability.includes("\0"),
|
|
145
|
+
)
|
|
146
|
+
) {
|
|
147
|
+
throw new Error("Durable Remote relay capabilities are invalid");
|
|
148
|
+
}
|
|
149
|
+
return Object.freeze(normalized.sort());
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function sameStringArray(left, right) {
|
|
153
|
+
return (
|
|
154
|
+
left.length === right.length &&
|
|
155
|
+
left.every((value, index) => value === right[index])
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function relayCredentialDigest(publicKey) {
|
|
160
|
+
return createHash("sha256").update(unb64(publicKey)).digest("hex");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function samePendingRelayRequest(left, right) {
|
|
164
|
+
return (
|
|
165
|
+
left.peerId === right.peerId &&
|
|
166
|
+
left.key.equals(right.key) &&
|
|
167
|
+
left.mobilePublicKey === right.mobilePublicKey &&
|
|
168
|
+
left.pairingTokenDigest === right.pairingTokenDigest &&
|
|
169
|
+
left.initialEnvelopeSequence === right.initialEnvelopeSequence &&
|
|
170
|
+
left.initialEnvelopeTranscriptHash ===
|
|
171
|
+
right.initialEnvelopeTranscriptHash &&
|
|
172
|
+
left.expectedPrincipalId === right.expectedPrincipalId &&
|
|
173
|
+
left.expectedCredentialDigest === right.expectedCredentialDigest &&
|
|
174
|
+
left.expectedSessionEpoch === right.expectedSessionEpoch &&
|
|
175
|
+
left.expectedMembershipEpoch === right.expectedMembershipEpoch &&
|
|
176
|
+
left.baselineAuthorityGeneration === right.baselineAuthorityGeneration &&
|
|
177
|
+
left.baselineMembershipEpoch === right.baselineMembershipEpoch &&
|
|
178
|
+
left.baselineMembershipStatus === right.baselineMembershipStatus &&
|
|
179
|
+
sameStringArray(left.expectedScopes, right.expectedScopes) &&
|
|
180
|
+
sameStringArray(left.expectedCapabilities, right.expectedCapabilities) &&
|
|
181
|
+
sameStringArray(
|
|
182
|
+
left.authority.authorizedScopes,
|
|
183
|
+
right.authority.authorizedScopes,
|
|
184
|
+
) &&
|
|
185
|
+
left.authority.expiresAtMs === right.authority.expiresAtMs &&
|
|
186
|
+
left.authority.coordinatorId === right.authority.coordinatorId &&
|
|
187
|
+
left.authority.serverInstanceId === right.authority.serverInstanceId &&
|
|
188
|
+
left.authority.authorityVersion === right.authority.authorityVersion
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function createRelayMembershipAcceptance(
|
|
193
|
+
sessionId,
|
|
194
|
+
peerId,
|
|
195
|
+
{
|
|
196
|
+
mobilePublicKey,
|
|
197
|
+
pairingTokenDigest,
|
|
198
|
+
principalId,
|
|
199
|
+
sessionEpoch,
|
|
200
|
+
membershipEpoch,
|
|
201
|
+
scopes,
|
|
202
|
+
capabilities,
|
|
203
|
+
statement = null,
|
|
204
|
+
} = {},
|
|
205
|
+
) {
|
|
206
|
+
const normalizedPublicKey = canonicalPublicKey(mobilePublicKey);
|
|
207
|
+
const normalizedScopes = normalizeRelayAuthorityScopes(scopes);
|
|
208
|
+
const normalizedCapabilities = normalizeRelayCapabilities(capabilities);
|
|
209
|
+
if (
|
|
210
|
+
typeof pairingTokenDigest !== "string" ||
|
|
211
|
+
!/^[0-9a-f]{64}$/.test(pairingTokenDigest) ||
|
|
212
|
+
typeof principalId !== "string" ||
|
|
213
|
+
principalId.length === 0 ||
|
|
214
|
+
typeof sessionEpoch !== "string" ||
|
|
215
|
+
!/^[1-9]\d*$/.test(sessionEpoch) ||
|
|
216
|
+
typeof membershipEpoch !== "string" ||
|
|
217
|
+
!/^[1-9]\d*$/.test(membershipEpoch)
|
|
218
|
+
) {
|
|
219
|
+
throw new Error("Remote relay membership acceptance binding is invalid");
|
|
220
|
+
}
|
|
221
|
+
return Object.freeze({
|
|
222
|
+
sessionId,
|
|
223
|
+
mobilePeerId: peerId,
|
|
224
|
+
mobilePublicKey: normalizedPublicKey,
|
|
225
|
+
pairingTokenDigest,
|
|
226
|
+
principalId,
|
|
227
|
+
sessionEpoch,
|
|
228
|
+
membershipEpoch,
|
|
229
|
+
scopes: normalizedScopes,
|
|
230
|
+
capabilities: normalizedCapabilities,
|
|
231
|
+
statement,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function decryptEnvelopeWithKey(
|
|
236
|
+
sessionId,
|
|
237
|
+
key,
|
|
238
|
+
envelope,
|
|
239
|
+
previousSequence = 0,
|
|
240
|
+
) {
|
|
241
|
+
if (envelope?.v !== 1 || envelope.sessionId !== sessionId) {
|
|
242
|
+
throw new Error("Invalid encrypted Remote Session envelope");
|
|
243
|
+
}
|
|
244
|
+
if (
|
|
245
|
+
!Number.isSafeInteger(envelope.sequence) ||
|
|
246
|
+
envelope.sequence <= previousSequence
|
|
247
|
+
) {
|
|
248
|
+
throw new Error("Remote Session replay or out-of-order envelope rejected");
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const decipher = createDecipheriv(
|
|
252
|
+
"aes-256-gcm",
|
|
253
|
+
key,
|
|
254
|
+
unb64(envelope.nonce),
|
|
255
|
+
);
|
|
256
|
+
decipher.setAAD(aad(sessionId, envelope.senderId, envelope.sequence));
|
|
257
|
+
decipher.setAuthTag(unb64(envelope.tag));
|
|
258
|
+
const plaintext = Buffer.concat([
|
|
259
|
+
decipher.update(unb64(envelope.ciphertext)),
|
|
260
|
+
decipher.final(),
|
|
261
|
+
]);
|
|
262
|
+
return JSON.parse(plaintext.toString("utf8"));
|
|
263
|
+
} catch (error) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
`Remote Session envelope authentication failed: ${error.message}`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
37
270
|
function aad(sessionId, senderId, sequence) {
|
|
38
271
|
return Buffer.from(
|
|
39
272
|
`${PROTOCOL}\n${sessionId}\n${senderId}\n${sequence}`,
|
|
@@ -121,7 +354,14 @@ export function parseRemotePairingUri(uri, now = Date.now()) {
|
|
|
121
354
|
}
|
|
122
355
|
|
|
123
356
|
export class RemoteSessionCryptoContext {
|
|
124
|
-
constructor({
|
|
357
|
+
constructor({
|
|
358
|
+
sessionId,
|
|
359
|
+
localPeerId,
|
|
360
|
+
privateKey,
|
|
361
|
+
publicKey,
|
|
362
|
+
now = () => Date.now(),
|
|
363
|
+
faultHooks = null,
|
|
364
|
+
} = {}) {
|
|
125
365
|
if (!sessionId || !localPeerId)
|
|
126
366
|
throw new Error("sessionId and localPeerId are required");
|
|
127
367
|
let keyPair;
|
|
@@ -134,28 +374,113 @@ export class RemoteSessionCryptoContext {
|
|
|
134
374
|
format: "der",
|
|
135
375
|
});
|
|
136
376
|
this.publicKey = publicKey || keyPair.publicKey;
|
|
377
|
+
this._now = now;
|
|
378
|
+
this._faultHooks = faultHooks || {};
|
|
137
379
|
this.keys = new Map();
|
|
380
|
+
this.pairingMaterial = new Map();
|
|
381
|
+
this.relayPeerBindings = new Map();
|
|
382
|
+
this.acceptedRelayMemberships = new Map();
|
|
383
|
+
this.pendingCommittedRelayAcceptances = new Map();
|
|
384
|
+
this.pendingRelayCapabilities = new Map();
|
|
385
|
+
this.outstandingRelayCapabilities = new Map();
|
|
138
386
|
this.sendSequence = 0;
|
|
139
387
|
this.receivedSequences = new Map();
|
|
140
388
|
}
|
|
141
389
|
|
|
142
390
|
pair(peerId, peerPublicKey, pairingToken) {
|
|
391
|
+
return this._pair(peerId, peerPublicKey, pairingToken, null);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
pairForDurableMembership(
|
|
395
|
+
peerId,
|
|
396
|
+
peerPublicKey,
|
|
397
|
+
pairingToken,
|
|
398
|
+
{
|
|
399
|
+
authorizedScopes,
|
|
400
|
+
expiresAtMs,
|
|
401
|
+
coordinatorId,
|
|
402
|
+
serverInstanceId,
|
|
403
|
+
authorityVersion,
|
|
404
|
+
} = {},
|
|
405
|
+
) {
|
|
406
|
+
const normalizedExpiry = Number(expiresAtMs);
|
|
407
|
+
if (
|
|
408
|
+
!Number.isSafeInteger(normalizedExpiry) ||
|
|
409
|
+
normalizedExpiry <= this._now()
|
|
410
|
+
) {
|
|
411
|
+
throw new Error("Durable Remote relay pairing authority is expired");
|
|
412
|
+
}
|
|
413
|
+
if (
|
|
414
|
+
typeof coordinatorId !== "string" ||
|
|
415
|
+
coordinatorId.length === 0 ||
|
|
416
|
+
typeof serverInstanceId !== "string" ||
|
|
417
|
+
serverInstanceId.length === 0 ||
|
|
418
|
+
typeof authorityVersion !== "string" ||
|
|
419
|
+
authorityVersion.length === 0
|
|
420
|
+
) {
|
|
421
|
+
throw new Error("Durable Remote relay authority domain is required");
|
|
422
|
+
}
|
|
423
|
+
return this._pair(peerId, peerPublicKey, pairingToken, {
|
|
424
|
+
authorizedScopes: normalizeRelayAuthorityScopes(authorizedScopes),
|
|
425
|
+
expiresAtMs: normalizedExpiry,
|
|
426
|
+
coordinatorId,
|
|
427
|
+
serverInstanceId,
|
|
428
|
+
authorityVersion,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
_pair(peerId, peerPublicKey, pairingToken, durableAuthority) {
|
|
143
433
|
if (!peerId || !peerPublicKey || !pairingToken)
|
|
144
434
|
throw new Error("Incomplete peer key material");
|
|
435
|
+
// Preserve the legacy re-pair/rotated-token behavior. Re-pairing replaces
|
|
436
|
+
// the transport key, but first invalidates any not-yet-consumed authority
|
|
437
|
+
// capability minted under the old key/token tuple.
|
|
438
|
+
const outstandingCapability = this.outstandingRelayCapabilities.get(peerId);
|
|
439
|
+
if (outstandingCapability) {
|
|
440
|
+
relayPossessionCapabilities.delete(outstandingCapability);
|
|
441
|
+
}
|
|
442
|
+
this.pendingRelayCapabilities.delete(peerId);
|
|
443
|
+
this.outstandingRelayCapabilities.delete(peerId);
|
|
444
|
+
const normalizedPublicKey = canonicalPublicKey(peerPublicKey);
|
|
445
|
+
this.acceptedRelayMemberships.delete(peerId);
|
|
446
|
+
this.discardPendingRelayMembershipCommit(peerId);
|
|
145
447
|
this.keys.set(
|
|
146
448
|
peerId,
|
|
147
449
|
deriveKey(
|
|
148
450
|
this.privateKey,
|
|
149
|
-
importPublicKey(
|
|
451
|
+
importPublicKey(normalizedPublicKey),
|
|
150
452
|
this.sessionId,
|
|
151
453
|
pairingToken,
|
|
152
454
|
),
|
|
153
455
|
);
|
|
456
|
+
this.pairingMaterial.set(peerId, {
|
|
457
|
+
mobilePublicKey: normalizedPublicKey,
|
|
458
|
+
pairingTokenDigest: remoteSessionPairingTokenDigest(pairingToken),
|
|
459
|
+
authorizedScopes: durableAuthority?.authorizedScopes || null,
|
|
460
|
+
expiresAtMs: durableAuthority?.expiresAtMs || null,
|
|
461
|
+
coordinatorId: durableAuthority?.coordinatorId || null,
|
|
462
|
+
serverInstanceId: durableAuthority?.serverInstanceId || null,
|
|
463
|
+
authorityVersion: durableAuthority?.authorityVersion || null,
|
|
464
|
+
});
|
|
465
|
+
this.relayPeerBindings.set(peerId, {
|
|
466
|
+
mobilePublicKey: normalizedPublicKey,
|
|
467
|
+
pairingTokenDigest: remoteSessionPairingTokenDigest(pairingToken),
|
|
468
|
+
});
|
|
154
469
|
}
|
|
155
470
|
|
|
156
471
|
encrypt(peerId, message) {
|
|
472
|
+
const revokesThisPeer =
|
|
473
|
+
message?.type === "session.revoked" &&
|
|
474
|
+
message.remoteSessionId === this.sessionId;
|
|
157
475
|
const key = this.keys.get(peerId);
|
|
158
|
-
if (!key)
|
|
476
|
+
if (!key) {
|
|
477
|
+
// A durable join can be authoritative while its candidate key is still
|
|
478
|
+
// pending publication. A courtesy revoke cannot be encrypted in that
|
|
479
|
+
// window, but it must still retire the process-local recovery marker so
|
|
480
|
+
// a later, legitimately re-authorized generation is not blocked.
|
|
481
|
+
if (revokesThisPeer) this.clearRelayPeer(peerId);
|
|
482
|
+
throw new Error(`No encryption key for peer: ${peerId}`);
|
|
483
|
+
}
|
|
159
484
|
const sequence = ++this.sendSequence;
|
|
160
485
|
const nonce = randomBytes(12);
|
|
161
486
|
const cipher = createCipheriv("aes-256-gcm", key, nonce);
|
|
@@ -164,7 +489,7 @@ export class RemoteSessionCryptoContext {
|
|
|
164
489
|
cipher.update(JSON.stringify(message), "utf8"),
|
|
165
490
|
cipher.final(),
|
|
166
491
|
]);
|
|
167
|
-
|
|
492
|
+
const envelope = {
|
|
168
493
|
v: 1,
|
|
169
494
|
sessionId: this.sessionId,
|
|
170
495
|
senderId: this.localPeerId,
|
|
@@ -173,6 +498,12 @@ export class RemoteSessionCryptoContext {
|
|
|
173
498
|
ciphertext: b64(ciphertext),
|
|
174
499
|
tag: b64(cipher.getAuthTag()),
|
|
175
500
|
};
|
|
501
|
+
if (revokesThisPeer) {
|
|
502
|
+
// The ciphertext is complete; remove every local generation for this
|
|
503
|
+
// peer before the courtesy revoke frame is physically sent.
|
|
504
|
+
this.clearRelayPeer(peerId);
|
|
505
|
+
}
|
|
506
|
+
return envelope;
|
|
176
507
|
}
|
|
177
508
|
|
|
178
509
|
decrypt(envelope) {
|
|
@@ -191,27 +522,604 @@ export class RemoteSessionCryptoContext {
|
|
|
191
522
|
"Remote Session replay or out-of-order envelope rejected",
|
|
192
523
|
);
|
|
193
524
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
525
|
+
const message = decryptEnvelopeWithKey(
|
|
526
|
+
this.sessionId,
|
|
527
|
+
key,
|
|
528
|
+
envelope,
|
|
529
|
+
previous,
|
|
530
|
+
);
|
|
531
|
+
this.receivedSequences.set(envelope.senderId, envelope.sequence);
|
|
532
|
+
const pairing = this.pairingMaterial.get(envelope.senderId);
|
|
533
|
+
if (
|
|
534
|
+
pairing &&
|
|
535
|
+
pairing.authorizedScopes !== null &&
|
|
536
|
+
pairing.coordinatorId !== null &&
|
|
537
|
+
pairing.serverInstanceId !== null &&
|
|
538
|
+
pairing.authorityVersion !== null &&
|
|
539
|
+
pairing.expiresAtMs > this._now() &&
|
|
540
|
+
message?.type === "pair.join" &&
|
|
541
|
+
typeof message.token === "string" &&
|
|
542
|
+
remoteSessionPairingTokenDigest(message.token) ===
|
|
543
|
+
pairing.pairingTokenDigest
|
|
544
|
+
) {
|
|
545
|
+
const proof = Object.freeze({
|
|
546
|
+
schema: RELAY_POSSESSION_SCHEMA,
|
|
547
|
+
sessionId: this.sessionId,
|
|
548
|
+
mobilePeerId: envelope.senderId,
|
|
549
|
+
mobilePublicKey: pairing.mobilePublicKey,
|
|
550
|
+
pairingTokenDigest: pairing.pairingTokenDigest,
|
|
551
|
+
coordinatorId: pairing.coordinatorId,
|
|
552
|
+
serverInstanceId: pairing.serverInstanceId,
|
|
553
|
+
authorityVersion: pairing.authorityVersion,
|
|
554
|
+
authorizedScopes: pairing.authorizedScopes,
|
|
555
|
+
pairingExpiresAtMs: pairing.expiresAtMs,
|
|
556
|
+
envelopeSequence: envelope.sequence,
|
|
557
|
+
envelopeTranscriptHash: relayEnvelopeTranscriptHash(envelope),
|
|
558
|
+
});
|
|
559
|
+
relayPossessionCapabilities.set(proof, { ...proof });
|
|
560
|
+
this.pendingRelayCapabilities.set(envelope.senderId, proof);
|
|
561
|
+
this.outstandingRelayCapabilities.set(envelope.senderId, proof);
|
|
562
|
+
this.pairingMaterial.delete(envelope.senderId);
|
|
563
|
+
}
|
|
564
|
+
return message;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Authenticate a prospective relay pairing without changing the live peer
|
|
569
|
+
* key, replay high-water mark, or an accepted-membership receipt. The caller
|
|
570
|
+
* commits the opaque stage only after the membership transition succeeds.
|
|
571
|
+
*/
|
|
572
|
+
stageRelayPairing(
|
|
573
|
+
peerId,
|
|
574
|
+
peerPublicKey,
|
|
575
|
+
pairingToken,
|
|
576
|
+
envelope,
|
|
577
|
+
durableAuthority = null,
|
|
578
|
+
) {
|
|
579
|
+
if (!peerId || !peerPublicKey || !pairingToken) {
|
|
580
|
+
throw new Error("Incomplete peer key material");
|
|
581
|
+
}
|
|
582
|
+
if (envelope?.senderId !== peerId) {
|
|
583
|
+
throw new Error("Remote Session relay peer binding is invalid");
|
|
584
|
+
}
|
|
585
|
+
const normalizedPublicKey = canonicalPublicKey(peerPublicKey);
|
|
586
|
+
const pairingTokenDigest = remoteSessionPairingTokenDigest(pairingToken);
|
|
587
|
+
let authority = null;
|
|
588
|
+
if (durableAuthority !== null) {
|
|
589
|
+
const normalizedExpiry = Number(durableAuthority?.expiresAtMs);
|
|
590
|
+
if (
|
|
591
|
+
!Number.isSafeInteger(normalizedExpiry) ||
|
|
592
|
+
normalizedExpiry <= this._now()
|
|
593
|
+
) {
|
|
594
|
+
throw new Error("Durable Remote relay pairing authority is expired");
|
|
595
|
+
}
|
|
596
|
+
if (
|
|
597
|
+
typeof durableAuthority?.coordinatorId !== "string" ||
|
|
598
|
+
durableAuthority.coordinatorId.length === 0 ||
|
|
599
|
+
typeof durableAuthority?.serverInstanceId !== "string" ||
|
|
600
|
+
durableAuthority.serverInstanceId.length === 0 ||
|
|
601
|
+
typeof durableAuthority?.authorityVersion !== "string" ||
|
|
602
|
+
durableAuthority.authorityVersion.length === 0
|
|
603
|
+
) {
|
|
604
|
+
throw new Error("Durable Remote relay authority domain is required");
|
|
605
|
+
}
|
|
606
|
+
authority = Object.freeze({
|
|
607
|
+
authorizedScopes: normalizeRelayAuthorityScopes(
|
|
608
|
+
durableAuthority.authorizedScopes,
|
|
609
|
+
),
|
|
610
|
+
expiresAtMs: normalizedExpiry,
|
|
611
|
+
coordinatorId: durableAuthority.coordinatorId,
|
|
612
|
+
serverInstanceId: durableAuthority.serverInstanceId,
|
|
613
|
+
authorityVersion: durableAuthority.authorityVersion,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
const key = deriveKey(
|
|
617
|
+
this.privateKey,
|
|
618
|
+
importPublicKey(normalizedPublicKey),
|
|
619
|
+
this.sessionId,
|
|
620
|
+
pairingToken,
|
|
621
|
+
);
|
|
622
|
+
// A new key/token tuple starts a new encrypted sequence space. Nothing is
|
|
623
|
+
// published to the live maps until commitStagedRelayPairing().
|
|
624
|
+
const message = decryptEnvelopeWithKey(this.sessionId, key, envelope, 0);
|
|
625
|
+
if (
|
|
626
|
+
message?.type !== "pair.join" ||
|
|
627
|
+
message.remoteSessionId !== this.sessionId ||
|
|
628
|
+
message.token !== pairingToken
|
|
629
|
+
) {
|
|
630
|
+
throw new Error("Invalid encrypted Remote Session pairing request");
|
|
631
|
+
}
|
|
632
|
+
let possessionCapability = null;
|
|
633
|
+
if (authority) {
|
|
634
|
+
const proof = Object.freeze({
|
|
635
|
+
schema: RELAY_POSSESSION_SCHEMA,
|
|
636
|
+
sessionId: this.sessionId,
|
|
637
|
+
mobilePeerId: peerId,
|
|
638
|
+
mobilePublicKey: normalizedPublicKey,
|
|
639
|
+
pairingTokenDigest,
|
|
640
|
+
coordinatorId: authority.coordinatorId,
|
|
641
|
+
serverInstanceId: authority.serverInstanceId,
|
|
642
|
+
authorityVersion: authority.authorityVersion,
|
|
643
|
+
authorizedScopes: authority.authorizedScopes,
|
|
644
|
+
pairingExpiresAtMs: authority.expiresAtMs,
|
|
645
|
+
envelopeSequence: envelope.sequence,
|
|
646
|
+
envelopeTranscriptHash: relayEnvelopeTranscriptHash(envelope),
|
|
647
|
+
});
|
|
648
|
+
relayPossessionCapabilities.set(proof, { ...proof });
|
|
649
|
+
possessionCapability = proof;
|
|
650
|
+
}
|
|
651
|
+
const stage = Object.freeze({});
|
|
652
|
+
stagedRelayPairings.set(stage, {
|
|
653
|
+
owner: this,
|
|
654
|
+
peerId,
|
|
655
|
+
key,
|
|
656
|
+
mobilePublicKey: normalizedPublicKey,
|
|
657
|
+
pairingTokenDigest,
|
|
658
|
+
envelopeSequence: envelope.sequence,
|
|
659
|
+
envelopeTranscriptHash: relayEnvelopeTranscriptHash(envelope),
|
|
660
|
+
authority,
|
|
661
|
+
possessionCapability,
|
|
662
|
+
});
|
|
663
|
+
return Object.freeze({
|
|
664
|
+
message,
|
|
665
|
+
stage,
|
|
666
|
+
possessionCapability,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
commitStagedRelayPairing(stage) {
|
|
671
|
+
const candidate = stagedRelayPairings.get(stage);
|
|
672
|
+
stagedRelayPairings.delete(stage);
|
|
673
|
+
if (!candidate || candidate.owner !== this) {
|
|
674
|
+
throw new Error("Remote relay pairing stage is invalid or stale");
|
|
675
|
+
}
|
|
676
|
+
const outstandingCapability = this.outstandingRelayCapabilities.get(
|
|
677
|
+
candidate.peerId,
|
|
678
|
+
);
|
|
679
|
+
if (outstandingCapability) {
|
|
680
|
+
relayPossessionCapabilities.delete(outstandingCapability);
|
|
681
|
+
}
|
|
682
|
+
this.pendingRelayCapabilities.delete(candidate.peerId);
|
|
683
|
+
this.outstandingRelayCapabilities.delete(candidate.peerId);
|
|
684
|
+
this.pairingMaterial.delete(candidate.peerId);
|
|
685
|
+
this.keys.set(candidate.peerId, candidate.key);
|
|
686
|
+
this.relayPeerBindings.set(candidate.peerId, {
|
|
687
|
+
mobilePublicKey: candidate.mobilePublicKey,
|
|
688
|
+
pairingTokenDigest: candidate.pairingTokenDigest,
|
|
689
|
+
});
|
|
690
|
+
this.receivedSequences.set(candidate.peerId, candidate.envelopeSequence);
|
|
691
|
+
this.acceptedRelayMemberships.delete(candidate.peerId);
|
|
692
|
+
this.discardPendingRelayMembershipCommit(candidate.peerId);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Arm recovery before invoking the durable coordinator/registry. This moves
|
|
697
|
+
* the verified candidate out of the opaque stage and binds its exact request,
|
|
698
|
+
* authority instance, and pre-join authority generation without publishing
|
|
699
|
+
* a live peer key, replay high-water mark, or acceptance receipt.
|
|
700
|
+
*/
|
|
701
|
+
armPendingRelayMembershipCommit(
|
|
702
|
+
stage,
|
|
703
|
+
{ authoritySnapshot, scopes, capabilities } = {},
|
|
704
|
+
) {
|
|
705
|
+
const candidate = stagedRelayPairings.get(stage);
|
|
706
|
+
const snapshot = authoritySnapshot;
|
|
707
|
+
if (
|
|
708
|
+
!candidate ||
|
|
709
|
+
candidate.owner !== this ||
|
|
710
|
+
!candidate.authority ||
|
|
711
|
+
snapshot?.sessionId !== this.sessionId ||
|
|
712
|
+
snapshot.status !== "active" ||
|
|
713
|
+
!/^[1-9]\d*$/.test(String(snapshot.sessionEpoch)) ||
|
|
714
|
+
!/^\d+$/.test(String(snapshot.authorityGeneration)) ||
|
|
715
|
+
!Array.isArray(snapshot.members)
|
|
716
|
+
) {
|
|
717
|
+
throw new Error("Remote relay pairing stage is invalid or stale");
|
|
718
|
+
}
|
|
719
|
+
const expectedScopes = normalizeRelayAuthorityScopes(scopes);
|
|
720
|
+
const expectedCapabilities = normalizeRelayCapabilities(capabilities);
|
|
721
|
+
if (
|
|
722
|
+
expectedScopes.some(
|
|
723
|
+
(scope) => !candidate.authority.authorizedScopes.includes(scope),
|
|
724
|
+
)
|
|
725
|
+
) {
|
|
726
|
+
throw new Error("Remote relay membership request exceeds its authority");
|
|
727
|
+
}
|
|
728
|
+
const expectedCredentialDigest = relayCredentialDigest(
|
|
729
|
+
candidate.mobilePublicKey,
|
|
730
|
+
);
|
|
731
|
+
const expectedPrincipalId = `relay-x25519:${expectedCredentialDigest}`;
|
|
732
|
+
const baselineMember = snapshot.members.find(
|
|
733
|
+
(member) => member.principalId === expectedPrincipalId,
|
|
734
|
+
);
|
|
735
|
+
if (baselineMember?.status === "active") {
|
|
736
|
+
throw new Error("Remote relay principal is already active");
|
|
737
|
+
}
|
|
738
|
+
if (
|
|
739
|
+
baselineMember &&
|
|
740
|
+
baselineMember.credentialKeySha256 !== expectedCredentialDigest
|
|
741
|
+
) {
|
|
742
|
+
throw new Error("Remote relay authority credential binding changed");
|
|
743
|
+
}
|
|
744
|
+
const pending = Object.freeze({
|
|
745
|
+
phase: "prepared",
|
|
746
|
+
peerId: candidate.peerId,
|
|
747
|
+
key: candidate.key,
|
|
748
|
+
mobilePublicKey: candidate.mobilePublicKey,
|
|
749
|
+
pairingTokenDigest: candidate.pairingTokenDigest,
|
|
750
|
+
initialEnvelopeSequence: candidate.envelopeSequence,
|
|
751
|
+
initialEnvelopeTranscriptHash: candidate.envelopeTranscriptHash,
|
|
752
|
+
localHighWater: candidate.envelopeSequence,
|
|
753
|
+
authority: candidate.authority,
|
|
754
|
+
possessionCapability: candidate.possessionCapability,
|
|
755
|
+
expectedPrincipalId,
|
|
756
|
+
expectedCredentialDigest,
|
|
757
|
+
expectedSessionEpoch: String(snapshot.sessionEpoch),
|
|
758
|
+
expectedMembershipEpoch: baselineMember
|
|
759
|
+
? String(BigInt(baselineMember.membershipEpoch) + 1n)
|
|
760
|
+
: "1",
|
|
761
|
+
expectedScopes,
|
|
762
|
+
expectedCapabilities,
|
|
763
|
+
baselineAuthorityGeneration: String(snapshot.authorityGeneration),
|
|
764
|
+
baselineMembershipEpoch: baselineMember?.membershipEpoch || null,
|
|
765
|
+
baselineMembershipStatus: baselineMember?.status || null,
|
|
766
|
+
receipt: null,
|
|
767
|
+
});
|
|
768
|
+
const existing = this.pendingCommittedRelayAcceptances.get(
|
|
769
|
+
candidate.peerId,
|
|
770
|
+
);
|
|
771
|
+
if (existing && !samePendingRelayRequest(existing, pending)) {
|
|
772
|
+
throw new Error(
|
|
773
|
+
"Remote relay peer already has a different pending membership commit",
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
if (!existing) {
|
|
777
|
+
this.pendingCommittedRelayAcceptances.set(candidate.peerId, pending);
|
|
778
|
+
} else if (candidate.possessionCapability) {
|
|
779
|
+
relayPossessionCapabilities.delete(candidate.possessionCapability);
|
|
780
|
+
}
|
|
781
|
+
stagedRelayPairings.delete(stage);
|
|
782
|
+
return existing || pending;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
completePendingCommittedRelayAcceptance(peerId, acceptance) {
|
|
786
|
+
const pending = this.pendingCommittedRelayAcceptances.get(peerId);
|
|
787
|
+
if (!pending) {
|
|
788
|
+
throw new Error("Remote relay committed acceptance is unavailable");
|
|
789
|
+
}
|
|
790
|
+
// This boundary is deliberately after RemoteSessionRegistry.joinRelayMember
|
|
791
|
+
// has returned (including its local transport attachment). A coordinator
|
|
792
|
+
// commit that fails before registry attachment remains fail-closed because
|
|
793
|
+
// this three-file slice has no trusted public reattach operation.
|
|
794
|
+
this._faultHooks.afterRelayRegistryCommit?.({
|
|
795
|
+
sessionId: this.sessionId,
|
|
796
|
+
peerId,
|
|
797
|
+
expectedPrincipalId: pending.expectedPrincipalId,
|
|
798
|
+
expectedSessionEpoch: pending.expectedSessionEpoch,
|
|
799
|
+
expectedMembershipEpoch: pending.expectedMembershipEpoch,
|
|
800
|
+
});
|
|
801
|
+
const receipt = createRelayMembershipAcceptance(
|
|
802
|
+
this.sessionId,
|
|
803
|
+
peerId,
|
|
804
|
+
acceptance,
|
|
805
|
+
);
|
|
806
|
+
if (
|
|
807
|
+
receipt.mobilePublicKey !== pending.mobilePublicKey ||
|
|
808
|
+
receipt.pairingTokenDigest !== pending.pairingTokenDigest ||
|
|
809
|
+
receipt.principalId !== pending.expectedPrincipalId ||
|
|
810
|
+
receipt.sessionEpoch !== pending.expectedSessionEpoch ||
|
|
811
|
+
receipt.membershipEpoch !== pending.expectedMembershipEpoch ||
|
|
812
|
+
!sameStringArray(receipt.scopes, pending.expectedScopes) ||
|
|
813
|
+
!sameStringArray(receipt.capabilities, pending.expectedCapabilities)
|
|
814
|
+
) {
|
|
815
|
+
throw new Error("Remote relay membership acceptance binding is invalid");
|
|
816
|
+
}
|
|
817
|
+
this.pendingCommittedRelayAcceptances.set(
|
|
818
|
+
peerId,
|
|
819
|
+
Object.freeze({ ...pending, phase: "committed", receipt }),
|
|
820
|
+
);
|
|
821
|
+
return receipt;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
resolvePendingRelayMembershipCommit(
|
|
825
|
+
peerId,
|
|
826
|
+
authorityRead,
|
|
827
|
+
authorityDescriptor,
|
|
828
|
+
) {
|
|
829
|
+
const pending = this.pendingCommittedRelayAcceptances.get(peerId);
|
|
830
|
+
const session = authorityRead?.session;
|
|
831
|
+
const statement = authorityRead?.statement;
|
|
832
|
+
if (
|
|
833
|
+
!pending ||
|
|
834
|
+
!session ||
|
|
835
|
+
session.sessionId !== this.sessionId ||
|
|
836
|
+
authorityDescriptor?.coordinatorId !== pending.authority.coordinatorId ||
|
|
837
|
+
authorityDescriptor?.serverInstanceId !==
|
|
838
|
+
pending.authority.serverInstanceId ||
|
|
839
|
+
authorityDescriptor?.authorityVersion !==
|
|
840
|
+
pending.authority.authorityVersion ||
|
|
841
|
+
statement?.coordinatorId !== pending.authority.coordinatorId ||
|
|
842
|
+
statement?.authorityVersion !== pending.authority.authorityVersion ||
|
|
843
|
+
statement?.generation !== String(session.authorityGeneration) ||
|
|
844
|
+
statement?.payload?.sessionId !== session.sessionId ||
|
|
845
|
+
statement?.payload?.sessionEpoch !== session.sessionEpoch
|
|
846
|
+
) {
|
|
847
|
+
return Object.freeze({ status: "fenced", receipt: null });
|
|
848
|
+
}
|
|
849
|
+
const member = session.members?.find(
|
|
850
|
+
(candidate) => candidate.principalId === pending.expectedPrincipalId,
|
|
851
|
+
);
|
|
852
|
+
const generationAdvanced =
|
|
853
|
+
/^\d+$/.test(String(session.authorityGeneration)) &&
|
|
854
|
+
BigInt(session.authorityGeneration) >
|
|
855
|
+
BigInt(pending.baselineAuthorityGeneration);
|
|
856
|
+
if (
|
|
857
|
+
generationAdvanced &&
|
|
858
|
+
session.status === "active" &&
|
|
859
|
+
String(session.sessionEpoch) === pending.expectedSessionEpoch &&
|
|
860
|
+
member?.status === "active" &&
|
|
861
|
+
member.credentialKeySha256 === pending.expectedCredentialDigest &&
|
|
862
|
+
String(member.membershipEpoch) === pending.expectedMembershipEpoch &&
|
|
863
|
+
sameStringArray(
|
|
864
|
+
normalizeRelayAuthorityScopes(member.scopes),
|
|
865
|
+
pending.expectedScopes,
|
|
866
|
+
)
|
|
867
|
+
) {
|
|
868
|
+
const receipt = createRelayMembershipAcceptance(this.sessionId, peerId, {
|
|
869
|
+
mobilePublicKey: pending.mobilePublicKey,
|
|
870
|
+
pairingTokenDigest: pending.pairingTokenDigest,
|
|
871
|
+
principalId: pending.expectedPrincipalId,
|
|
872
|
+
sessionEpoch: pending.expectedSessionEpoch,
|
|
873
|
+
membershipEpoch: pending.expectedMembershipEpoch,
|
|
874
|
+
scopes: pending.expectedScopes,
|
|
875
|
+
capabilities: pending.expectedCapabilities,
|
|
876
|
+
statement: authorityRead.statement || null,
|
|
877
|
+
});
|
|
878
|
+
this.pendingCommittedRelayAcceptances.set(
|
|
879
|
+
peerId,
|
|
880
|
+
Object.freeze({ ...pending, phase: "committed", receipt }),
|
|
881
|
+
);
|
|
882
|
+
return Object.freeze({ status: "committed", receipt });
|
|
883
|
+
}
|
|
884
|
+
const baselineMemberUnchanged = pending.baselineMembershipEpoch
|
|
885
|
+
? member?.status === pending.baselineMembershipStatus &&
|
|
886
|
+
String(member.membershipEpoch) === pending.baselineMembershipEpoch &&
|
|
887
|
+
member.credentialKeySha256 === pending.expectedCredentialDigest
|
|
888
|
+
: !member;
|
|
889
|
+
if (
|
|
890
|
+
session.status === "active" &&
|
|
891
|
+
String(session.sessionEpoch) === pending.expectedSessionEpoch &&
|
|
892
|
+
baselineMemberUnchanged
|
|
893
|
+
) {
|
|
894
|
+
return Object.freeze({ status: "not-committed", receipt: null });
|
|
895
|
+
}
|
|
896
|
+
return Object.freeze({ status: "fenced", receipt: null });
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
discardPendingRelayMembershipCommit(peerId) {
|
|
900
|
+
const pending = this.pendingCommittedRelayAcceptances.get(peerId);
|
|
901
|
+
if (!pending) return false;
|
|
902
|
+
if (pending.possessionCapability) {
|
|
903
|
+
relayPossessionCapabilities.delete(pending.possessionCapability);
|
|
904
|
+
}
|
|
905
|
+
this.pendingCommittedRelayAcceptances.delete(peerId);
|
|
906
|
+
return true;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Publish a previously armed durable outcome. Pending is deleted last, so a
|
|
911
|
+
* synchronous failure at either injected boundary can safely re-enter this
|
|
912
|
+
* method without lowering an already-observed replay high-water mark.
|
|
913
|
+
*/
|
|
914
|
+
finalizeCommittedRelayAcceptance(peerId) {
|
|
915
|
+
const pending = this.pendingCommittedRelayAcceptances.get(peerId);
|
|
916
|
+
if (!pending) {
|
|
917
|
+
const accepted = this.acceptedRelayMemberships.get(peerId);
|
|
918
|
+
if (accepted) return accepted;
|
|
919
|
+
throw new Error("Remote relay committed acceptance is unavailable");
|
|
920
|
+
}
|
|
921
|
+
if (pending.phase !== "committed" || !pending.receipt) {
|
|
922
|
+
throw new Error("Remote relay committed acceptance is not authoritative");
|
|
923
|
+
}
|
|
924
|
+
const outstandingCapability = this.outstandingRelayCapabilities.get(peerId);
|
|
925
|
+
if (outstandingCapability) {
|
|
926
|
+
relayPossessionCapabilities.delete(outstandingCapability);
|
|
927
|
+
}
|
|
928
|
+
this.pendingRelayCapabilities.delete(peerId);
|
|
929
|
+
this.outstandingRelayCapabilities.delete(peerId);
|
|
930
|
+
this.pairingMaterial.delete(peerId);
|
|
931
|
+
this.keys.set(peerId, pending.key);
|
|
932
|
+
this.relayPeerBindings.set(peerId, {
|
|
933
|
+
mobilePublicKey: pending.mobilePublicKey,
|
|
934
|
+
pairingTokenDigest: pending.pairingTokenDigest,
|
|
935
|
+
});
|
|
936
|
+
// The candidate key starts a distinct sequence generation. Never mix an
|
|
937
|
+
// old live key's high-water into it; pending-local authenticated progress
|
|
938
|
+
// becomes live only with the exact committed authority tuple.
|
|
939
|
+
this.receivedSequences.set(peerId, pending.localHighWater);
|
|
940
|
+
this.acceptedRelayMemberships.delete(peerId);
|
|
941
|
+
this._faultHooks.afterCommittedRelayKeyPublication?.({
|
|
942
|
+
sessionId: this.sessionId,
|
|
943
|
+
peerId,
|
|
944
|
+
principalId: pending.receipt.principalId,
|
|
945
|
+
sessionEpoch: pending.receipt.sessionEpoch,
|
|
946
|
+
membershipEpoch: pending.receipt.membershipEpoch,
|
|
947
|
+
});
|
|
948
|
+
this.acceptedRelayMemberships.set(peerId, pending.receipt);
|
|
949
|
+
this.pendingCommittedRelayAcceptances.delete(peerId);
|
|
950
|
+
this._faultHooks.afterCommittedRelayReceiptPublication?.({
|
|
951
|
+
sessionId: this.sessionId,
|
|
952
|
+
peerId,
|
|
953
|
+
principalId: pending.receipt.principalId,
|
|
954
|
+
sessionEpoch: pending.receipt.sessionEpoch,
|
|
955
|
+
membershipEpoch: pending.receipt.membershipEpoch,
|
|
956
|
+
});
|
|
957
|
+
return pending.receipt;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
decryptPendingCommittedRelayAcceptance(peerId, envelope) {
|
|
961
|
+
const pending = this.pendingCommittedRelayAcceptances.get(peerId);
|
|
962
|
+
if (!pending || envelope?.senderId !== peerId) {
|
|
963
|
+
throw new Error("Remote relay committed acceptance is unavailable");
|
|
964
|
+
}
|
|
965
|
+
const previous = pending.localHighWater;
|
|
966
|
+
const message = decryptEnvelopeWithKey(
|
|
967
|
+
this.sessionId,
|
|
968
|
+
pending.key,
|
|
969
|
+
envelope,
|
|
970
|
+
previous,
|
|
971
|
+
);
|
|
972
|
+
// Keep retry progress isolated from the live key generation until fresh
|
|
973
|
+
// authority readback and exact tuple validation permit final publication.
|
|
974
|
+
this.pendingCommittedRelayAcceptances.set(
|
|
975
|
+
peerId,
|
|
976
|
+
Object.freeze({ ...pending, localHighWater: envelope.sequence }),
|
|
977
|
+
);
|
|
978
|
+
return message;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
readPendingCommittedRelayAcceptance(
|
|
982
|
+
peerId,
|
|
983
|
+
{ mobilePublicKey, pairingTokenDigest, capabilities } = {},
|
|
984
|
+
) {
|
|
985
|
+
const receipt =
|
|
986
|
+
this.pendingCommittedRelayAcceptances.get(peerId)?.receipt || null;
|
|
987
|
+
const normalizedPublicKey = canonicalPublicKey(mobilePublicKey);
|
|
988
|
+
const normalizedCapabilities = normalizeRelayCapabilities(capabilities);
|
|
989
|
+
if (
|
|
990
|
+
!receipt ||
|
|
991
|
+
receipt.mobilePublicKey !== normalizedPublicKey ||
|
|
992
|
+
receipt.pairingTokenDigest !== pairingTokenDigest ||
|
|
993
|
+
!sameStringArray(receipt.capabilities, normalizedCapabilities)
|
|
994
|
+
) {
|
|
995
|
+
throw new Error(
|
|
996
|
+
"Remote relay committed acceptance is unavailable or mismatched",
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
return receipt;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
hasPendingCommittedRelayAcceptance(peerId) {
|
|
1003
|
+
return this.pendingCommittedRelayAcceptances.has(peerId);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
readPendingRelayMembershipRequest(
|
|
1007
|
+
peerId,
|
|
1008
|
+
{ mobilePublicKey, pairingTokenDigest, capabilities } = {},
|
|
1009
|
+
) {
|
|
1010
|
+
const pending = this.pendingCommittedRelayAcceptances.get(peerId) || null;
|
|
1011
|
+
const normalizedPublicKey = canonicalPublicKey(mobilePublicKey);
|
|
1012
|
+
const normalizedCapabilities = normalizeRelayCapabilities(capabilities);
|
|
1013
|
+
if (
|
|
1014
|
+
!pending ||
|
|
1015
|
+
pending.mobilePublicKey !== normalizedPublicKey ||
|
|
1016
|
+
pending.pairingTokenDigest !== pairingTokenDigest ||
|
|
1017
|
+
!sameStringArray(pending.expectedCapabilities, normalizedCapabilities)
|
|
1018
|
+
) {
|
|
1019
|
+
throw new Error(
|
|
1020
|
+
"Remote relay pending membership request is unavailable or mismatched",
|
|
199
1021
|
);
|
|
200
|
-
|
|
201
|
-
|
|
1022
|
+
}
|
|
1023
|
+
return Object.freeze({
|
|
1024
|
+
sessionId: this.sessionId,
|
|
1025
|
+
mobilePeerId: peerId,
|
|
1026
|
+
pairingTokenDigest: pending.pairingTokenDigest,
|
|
1027
|
+
expectedPrincipalId: pending.expectedPrincipalId,
|
|
1028
|
+
expectedSessionEpoch: pending.expectedSessionEpoch,
|
|
1029
|
+
expectedMembershipEpoch: pending.expectedMembershipEpoch,
|
|
1030
|
+
scopes: pending.expectedScopes,
|
|
1031
|
+
capabilities: pending.expectedCapabilities,
|
|
1032
|
+
phase: pending.phase,
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
clearRelayPeer(peerId) {
|
|
1037
|
+
const outstandingCapability = this.outstandingRelayCapabilities.get(peerId);
|
|
1038
|
+
if (outstandingCapability) {
|
|
1039
|
+
relayPossessionCapabilities.delete(outstandingCapability);
|
|
1040
|
+
}
|
|
1041
|
+
this.discardPendingRelayMembershipCommit(peerId);
|
|
1042
|
+
this.pendingRelayCapabilities.delete(peerId);
|
|
1043
|
+
this.outstandingRelayCapabilities.delete(peerId);
|
|
1044
|
+
this.pairingMaterial.delete(peerId);
|
|
1045
|
+
this.keys.delete(peerId);
|
|
1046
|
+
this.relayPeerBindings.delete(peerId);
|
|
1047
|
+
this.receivedSequences.delete(peerId);
|
|
1048
|
+
this.acceptedRelayMemberships.delete(peerId);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
takeRelayPossessionCapability(peerId) {
|
|
1052
|
+
const capability = this.pendingRelayCapabilities.get(peerId);
|
|
1053
|
+
this.pendingRelayCapabilities.delete(peerId);
|
|
1054
|
+
if (!capability) {
|
|
1055
|
+
throw new Error(
|
|
1056
|
+
"No authenticated Remote relay pairing transcript is available",
|
|
202
1057
|
);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
1058
|
+
}
|
|
1059
|
+
return capability;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Keep the exact, already-committed relay membership tuple in this crypto
|
|
1064
|
+
* process. It is deliberately not persisted: it only reconciles an ACK lost
|
|
1065
|
+
* while the same host process still owns the authenticated DH/AEAD context.
|
|
1066
|
+
*/
|
|
1067
|
+
rememberRelayMembershipAcceptance(
|
|
1068
|
+
peerId,
|
|
1069
|
+
{
|
|
1070
|
+
mobilePublicKey,
|
|
1071
|
+
pairingTokenDigest,
|
|
1072
|
+
principalId,
|
|
1073
|
+
sessionEpoch,
|
|
1074
|
+
membershipEpoch,
|
|
1075
|
+
scopes,
|
|
1076
|
+
capabilities,
|
|
1077
|
+
statement = null,
|
|
1078
|
+
} = {},
|
|
1079
|
+
) {
|
|
1080
|
+
const peer = this.relayPeerBindings.get(peerId);
|
|
1081
|
+
const receipt = createRelayMembershipAcceptance(this.sessionId, peerId, {
|
|
1082
|
+
mobilePublicKey,
|
|
1083
|
+
pairingTokenDigest,
|
|
1084
|
+
principalId,
|
|
1085
|
+
sessionEpoch,
|
|
1086
|
+
membershipEpoch,
|
|
1087
|
+
scopes,
|
|
1088
|
+
capabilities,
|
|
1089
|
+
statement,
|
|
1090
|
+
});
|
|
1091
|
+
if (
|
|
1092
|
+
!peer ||
|
|
1093
|
+
peer.mobilePublicKey !== receipt.mobilePublicKey ||
|
|
1094
|
+
peer.pairingTokenDigest !== receipt.pairingTokenDigest
|
|
1095
|
+
) {
|
|
1096
|
+
throw new Error("Remote relay membership acceptance binding is invalid");
|
|
1097
|
+
}
|
|
1098
|
+
this.acceptedRelayMemberships.set(peerId, receipt);
|
|
1099
|
+
return receipt;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
readRelayMembershipAcceptance(
|
|
1103
|
+
peerId,
|
|
1104
|
+
{ mobilePublicKey, pairingTokenDigest, capabilities } = {},
|
|
1105
|
+
) {
|
|
1106
|
+
const receipt = this.acceptedRelayMemberships.get(peerId);
|
|
1107
|
+
const normalizedPublicKey = canonicalPublicKey(mobilePublicKey);
|
|
1108
|
+
const normalizedCapabilities = normalizeRelayCapabilities(capabilities);
|
|
1109
|
+
if (
|
|
1110
|
+
!receipt ||
|
|
1111
|
+
receipt.mobilePublicKey !== normalizedPublicKey ||
|
|
1112
|
+
receipt.pairingTokenDigest !== pairingTokenDigest ||
|
|
1113
|
+
!sameStringArray(receipt.capabilities, normalizedCapabilities)
|
|
1114
|
+
) {
|
|
212
1115
|
throw new Error(
|
|
213
|
-
|
|
1116
|
+
"Remote relay membership acceptance is unavailable or mismatched",
|
|
214
1117
|
);
|
|
215
1118
|
}
|
|
1119
|
+
return receipt;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
hasRelayMembershipAcceptance(peerId) {
|
|
1123
|
+
return this.acceptedRelayMemberships.has(peerId);
|
|
216
1124
|
}
|
|
217
1125
|
}
|