chainlesschain 0.164.0 → 0.165.2
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/README.md +248 -2
- package/package.json +7 -4
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-D2MEn7fI.js → AIOps-lS1R0Cza.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-lCW_4Gf0.js → ActionButton-BmlgBPsj.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-Bs7i-nsS.js → Analytics-CNKczv1S.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-CK7B6S_j.js → AppLayout-BuZGJARQ.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-C8ILjpyo.js → Artifacts-Dm73Sl-O.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-Bb9cNaWc.js → Audit-BGMW7A5e.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-sC9ooXIf.js → BackgroundAgents-BQ6M54JR.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-D-DrsplT.js → Backup-DBzGP0bR.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-uCNZpwSK.js → BaseInput-D1mLCoOV.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CsDK1uE9.js → Chat-BUfcN7p8.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-OqSDt50w.js → ChatBubbleRenderer-CY5m8ZHg.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-JhTXM1-K.js → Checkbox-Du0Yg5Pi.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BbzZLnyD.js → Codegen-B2RXziHW.js} +1 -1
- package/src/assets/web-panel/assets/{Col-D4VW4m4j.js → Col-DWhekOrR.js} +1 -1
- package/src/assets/web-panel/assets/{Community-BXrYawxV.js → Community-HQhrkon4.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DCihGy1u.js → Compact-izHNb4uz.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DlyZ-lk8.js → Compliance-BDwLy41-.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-DvGZrhbO.js → Cowork-DFJhLa7K.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DUncZi8h.js → Cron-DYY9RG7-.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-C_hLuUcm.js → Crosschain-rr7tJTtv.js} +1 -1
- package/src/assets/web-panel/assets/{DID-b6YSUqxr.js → DID-D2sfHRLi.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-uvM15Cfb.js → Dashboard-q_eGdhha.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-D1-Nd4qY.js → Dropdown-Cufrh2Bz.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-8ApvL6Zc.js → EmailListRenderer-B-kdv10f.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-LYJt8811.js → FamilyGuardDashboard-D21XjgfG.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-BbI3HGpv.js → Federation-CAwRTLZH.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DSoEKuFu.js → FormItemContext-vx3daNDF.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-B0S9OWd5.js → GenericCardRenderer-CRWY2wU4.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DIs9Zhf3.js → Git-Q6SwauyD.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-jzZXrxcl.js → Governance-BJ1q4IQZ.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-JUPjf2ZI.js → Inference-CSWjROgD.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DxnfJvlP.js → KnowledgeGraph-fzuQsWuY.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-iXiz8BEE.js → Logs-DVF8Mnns.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-BLif4QSg.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-Y3Xch312.js → Marketplace-BQP7LOA4.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-D7kGGbO7.js → McpTools-BNlJCmNc.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-Do_hVxzh.js → Memory-DLLg5WQ4.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CtrR0Tas.js → MobileBridge-CR2skNAF.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-BxVavObl.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-qo5QCTGd.js → Mtc-B17pgqMI.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-cq5mnjYj.js → MtcAudit-4UmV5cIL.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-Bq0MAQHw.js → Multisig-DoMEXEcU.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-DdWE6Vi2.js → NLProgramming-h9DMXFX4.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BxMBFcIp.js → Notes-Cihn-xX8.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-RTrAcbUe.js → NotificationSettings-BE6ZGa3R.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-DJbpGqzT.js → OrderTableRenderer-CnwojoSI.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-SQzIPdqC.js → Organization-B2vpwmjb.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-02yECh22.js → Overflow-C4z28R0-.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BNg4pNCM.js → P2P-BVlFkwhW.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-BxPJZsD3.js → PdhVaultBrowser-BXbEdL5B.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-CLnDnPAz.js → Permissions-C28xs4lp.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-C5m0AjGf.js → PersonalDataHub-Dgl_rBjM.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-VwOf5Dhv.js → Pipeline-3QUIhrKI.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CC0W2hZd.js → Privacy-8Gh33OZ5.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Dz893Bob.js → ProjectInit-BNmg4tZi.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CxQkvl0k.js → ProjectSettings-vgMfeCf9.js} +2 -2
- package/src/assets/web-panel/assets/Projects-BhtAVilc.js +1 -0
- package/src/assets/web-panel/assets/{Providers-DeB9IkxK.js → Providers-CXoOa8eX.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-OCeoaswn.js → QrScannerModal-BhfTF0oS.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-DenotaAj.js → QuickAsk-C746HEAj.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Cies_jjI.js → Recommend-BSej8Eo0.js} +1 -1
- package/src/assets/web-panel/assets/RemoteSession-f3gmnlG3.js +5 -0
- package/src/assets/web-panel/assets/{Reputation-CW0mvxgZ.js → Reputation-CyvybpkN.js} +1 -1
- package/src/assets/web-panel/assets/{Row-SEd32qZU.js → Row-DwV2PeYn.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BSwnJ3Lm.js → RssFeed-11GrzjZy.js} +3 -3
- package/src/assets/web-panel/assets/{Search-xe2m8bkC.js → Search-BjKXkrAY.js} +1 -1
- package/src/assets/web-panel/assets/{Security-Mq2F9Nro.js → Security-eWiFb9Yk.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BKGw2V3P.js → Services-CdCsMAtt.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DW95-Rxf.js → Skeleton-4Q2UTKQ5.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CHgChCv_.js → Skills-v4rtYrRj.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-tKh6k3R7.js → Sla-CRhsFvd_.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-DLjKsDVu.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-D0Fw8fVL.js → SyncSettings-D16zonl7.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-20x1GB3D.js → Tasks-Xxozz0i_.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DJ-lts-a.js → Templates-CXJpPtCT.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BFdGpbs7.js → Tenant-XRV0L1ZF.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-Xpx61vHL.js → Terminal-CYuO1i_w.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-C6UIvyZQ.js → TimelineRenderer-BXqitWsu.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-Dellq08f.js → Tokens-BvPrDHIy.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-VKw8d0J-.js → Trigger-DhCrSTf5.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DNftdKT5.js → Trust-DmsuZD6K.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DX9VAKJI.js → UkeySign-D_yrts5C.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B2h3jO8q.js → VideoEditing-B01ihPrm.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CjGkof7f.js → Wallet-DpUjKa-N.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-2Zfcu55P.js → WebAuthn-CL07yON6.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-B9eZ8UxZ.js → WorkflowEditor-CLMflnRr.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CQNfD-YO.js → chat-DtUqe8h_.js} +1 -1
- package/src/assets/web-panel/assets/{colors-D4usyFT2.js → colors-DPKrgVfy.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-s4TpjvjX.js → compact-item-BTt2GtOm.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BRCSvYxj.js → createContext-D_VeUc1J.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-Bj5nUW2v.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-AdkUbkV-.js → hasIn-1kndiylj.js} +1 -1
- package/src/assets/web-panel/assets/{index-BIoG5yYf.js → index-2eyAXE3d.js} +1 -1
- package/src/assets/web-panel/assets/{index-nXyk8GZ9.js → index-4bna2_SR.js} +1 -1
- package/src/assets/web-panel/assets/{index-B2V-eKfK.js → index-8nz_fmlj.js} +1 -1
- package/src/assets/web-panel/assets/{index-D_XBHopw.js → index-B9KvcS7R.js} +1 -1
- package/src/assets/web-panel/assets/{index-s_buFMEG.js → index-BAXfRKfG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZT36ZYq.js → index-BHpBMWCh.js} +1 -1
- package/src/assets/web-panel/assets/{index-BMcimU-2.js → index-BaZAbw9A.js} +1 -1
- package/src/assets/web-panel/assets/{index-DzRjHxEv.js → index-BepLZwgB.js} +1 -1
- package/src/assets/web-panel/assets/{index--00U6vXZ.js → index-Bhudc_Ez.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7Y8Aluc.js → index-BlDNVgBt.js} +1 -1
- package/src/assets/web-panel/assets/{index-BtobShro.js → index-By5n8up9.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cvyt-zhm.js → index-CHG-KcS4.js} +1 -1
- package/src/assets/web-panel/assets/index-CPcM-eqo.js +1 -0
- package/src/assets/web-panel/assets/{index-MMN9V6JF.js → index-CX81qSM4.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bad4VNvz.js → index-CauoBQkH.js} +1 -1
- package/src/assets/web-panel/assets/{index-CCsM_Ybx.js → index-CcO04eDn.js} +1 -1
- package/src/assets/web-panel/assets/{index-DARsAyUp.js → index-CtF7EN49.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5KAQFPQ.js → index-CzjkKUmR.js} +1 -1
- package/src/assets/web-panel/assets/{index-RfMr79MW.js → index-D95OFht7.js} +1 -1
- package/src/assets/web-panel/assets/{index-DdCB70SS.js → index-DFIZ_1Sf.js} +1 -1
- package/src/assets/web-panel/assets/{index-CYdoQsa1.js → index-DG2MZQ3f.js} +1 -1
- package/src/assets/web-panel/assets/{index-BPhkFZ1I.js → index-DJ8vXGOa.js} +1 -1
- package/src/assets/web-panel/assets/{index-_IRVsV9X.js → index-Dbbymxpj.js} +1 -1
- package/src/assets/web-panel/assets/{index-DVKfQ0Ux.js → index-DdkplL9R.js} +1 -1
- package/src/assets/web-panel/assets/{index-CkwiSLhU.js → index-DfGl8CqG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJVEOa4i.js → index-DfKBRdSS.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1mJtE6q.js → index-DsJCDUJG.js} +1 -1
- package/src/assets/web-panel/assets/{index-BgiXR_-_.js → index-Dwqmtk65.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJCFe2Qf.js → index-DxdTdBix.js} +1 -1
- package/src/assets/web-panel/assets/{index-C484EJiS.js → index-FzdwcSy2.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4JJCfQ2.js → index-GsoUQ2M7.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQhAVU5t.js → index-brCInrzq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CKC1t3Ic.js → index-dVc-CXY4.js} +1 -1
- package/src/assets/web-panel/assets/{index-BQ3DXBtD.js → index-fk2Rjacw.js} +3 -3
- package/src/assets/web-panel/assets/{index-Cduu2Hhr.js → index-l1BETrDb.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bw2MEnUk.js → index-m31Ka8hZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-D2GjI2oe.js → index-ocf06EVQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJdQrZth.js → index-sDoMHTNA.js} +1 -1
- package/src/assets/web-panel/assets/index-xeSyVoRe.js +1 -0
- package/src/assets/web-panel/assets/{initDefaultProps-Blmpk7HL.js → initDefaultProps-CvyYJd7z.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DPBJMv1n.js → motion-DYZkhUdz.js} +1 -1
- package/src/assets/web-panel/assets/{move-ButdzCM7.js → move-slnJRVT1.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-cyRC_QCe.js → mtc-parser-DVc0CKmy.js} +1 -1
- package/src/assets/web-panel/assets/{omit-1epSV7hz.js → omit-BCXAtHCq.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DuPY8xJs.js → pickAttrs-DBsemz_K.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-6XZqarhe.js → placementArrow-BRtkJ-nv.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-B40pXBWM.js → responsiveObserve-Cebq7EZn.js} +1 -1
- package/src/assets/web-panel/assets/{slide-jbZ9Wu4n.js → slide-DT7qvId2.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BKQQcVy4.js → statusUtils-pQiITLkG.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-NiKfWjUT.js → styleChecker-c-19BVKQ.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-bQUCvPeb.js → useFlexGapSupport-DsS5-0bE.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CUGji7ph.js → useFs-CxGiMe5n.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-4veyN2N2.js → usePersonalDataHub-D9btVID5.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-1-nLug-Z.js → vnode-CUYFsFEN.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-dhCeRDMo.js → zoom-CXmn_35j.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +1 -1
- package/src/commands/cowork.js +559 -62
- package/src/commands/plugin.js +819 -64
- package/src/commands/session-location.js +318 -7
- package/src/constants.js +2 -2
- package/src/data/changelog.json +28 -2
- package/src/gateways/ws/action-protocol.js +293 -19
- package/src/gateways/ws/message-dispatcher.js +30 -0
- package/src/gateways/ws/remote-session-protocol.js +542 -35
- package/src/gateways/ws/ws-approval-gate.js +5 -0
- package/src/gateways/ws/ws-server.js +462 -38
- package/src/harness/jsonl-session-store.js +638 -2
- package/src/harness/prompt-compressor.js +4 -0
- package/src/harness/remote-session-audit.js +1 -0
- package/src/harness/remote-session-crypto.js +750 -50
- package/src/harness/remote-session-registry.js +795 -27
- package/src/lib/auto-mode-config.js +98 -3
- package/src/lib/background-agent-keeper-client.js +29 -0
- package/src/lib/background-agent-keeper-protocol.js +158 -10
- package/src/lib/background-agent-supervisor.js +870 -48
- package/src/lib/background-session-transport.js +23 -6
- package/src/lib/background-turn-bootstrap-protocol.js +64 -0
- package/src/lib/cowork-task-runner.js +70 -0
- package/src/lib/cowork-workflow.js +887 -62
- package/src/lib/delivery-production-adapter.js +38 -2
- package/src/lib/doctor-checkup.js +34 -10
- package/src/lib/dynamic-workflow-draft.js +535 -0
- package/src/lib/dynamic-workflow-facade.js +1105 -17
- package/src/lib/dynamic-workflow-runtime.js +2974 -0
- package/src/lib/execution-location-contract.js +111 -7
- package/src/lib/execution-location-runtime.js +3 -1
- package/src/lib/execution-location-target.js +910 -0
- package/src/lib/llm-providers.js +1 -0
- package/src/lib/mcp-call-ledger-store.js +35 -0
- package/src/lib/mcp-call-ledger.js +30 -0
- package/src/lib/mcp-stdio-package-materialization.js +1 -1
- package/src/lib/plugin-runtime/__tests__/remote-source.test.js +34 -16
- package/src/lib/plugin-runtime/install.js +4956 -657
- package/src/lib/plugin-runtime/manifest.js +3 -1
- package/src/lib/plugin-runtime/marketplace-artifact-readback.js +671 -31
- package/src/lib/plugin-runtime/marketplace-catalog.js +68 -2
- package/src/lib/plugin-runtime/marketplace-impact.js +208 -15
- package/src/lib/plugin-runtime/marketplace-network.js +317 -0
- package/src/lib/plugin-runtime/marketplace-pac-worker.js +15 -0
- package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +25 -2
- package/src/lib/plugin-runtime/marketplace-source-cache.js +359 -0
- package/src/lib/plugin-runtime/policy.js +21 -6
- package/src/lib/plugin-runtime/publisher-trust.js +332 -0
- package/src/lib/plugin-runtime/remote-source.js +323 -27
- package/src/lib/plugin-runtime/scopes.js +317 -41
- package/src/lib/plugin-runtime/transaction-journal.js +806 -0
- package/src/lib/public-ide-capability-manifest.js +1 -1
- package/src/lib/remote-approval-bridge.js +745 -108
- package/src/lib/remote-control.js +2 -0
- package/src/lib/remote-membership-coordinator.js +612 -46
- package/src/lib/remote-membership-host-store.js +59 -10
- package/src/lib/runtime-provenance-ledger.js +171 -46
- package/src/lib/shell-approval.js +169 -6
- package/src/lib/sub-agent-context.js +268 -2
- package/src/lib/task-model-selector.js +1 -1
- package/src/lib/with-file-lock.js +167 -42
- package/src/repl/agent-repl.js +70 -29
- package/src/repl/remote-approval.js +29 -7
- package/src/runtime/agent-core.js +1695 -194
- package/src/runtime/headless-runner.js +22 -5
- package/src/runtime/headless-stream.js +19 -3
- package/src/workers/background-agent-keeper.js +279 -58
- package/src/workers/background-agent-worker.js +7 -0
- package/src/workers/background-turn-bootstrap.js +29 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-GmB6DOr6.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-BL0Czcoj.js +0 -1
- package/src/assets/web-panel/assets/Projects-DDAjkUkT.js +0 -1
- package/src/assets/web-panel/assets/RemoteSession-Dxzl5n2T.js +0 -5
- package/src/assets/web-panel/assets/SpeechSettings-D6UL3dA5.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DKJjSWRn.js +0 -1
- package/src/assets/web-panel/assets/index-C61cnl3K.js +0 -1
- package/src/assets/web-panel/assets/index-D4XFHxBr.js +0 -1
|
@@ -15,6 +15,7 @@ const PAIRING_SCHEME = "chainlesschain://remote-session/pair#";
|
|
|
15
15
|
const RELAY_POSSESSION_SCHEMA =
|
|
16
16
|
"chainlesschain.remote-session.relay-possession-capability/v1";
|
|
17
17
|
const relayPossessionCapabilities = new WeakMap();
|
|
18
|
+
const stagedRelayPairings = new WeakMap();
|
|
18
19
|
const RELAY_AUTHORITY_SCOPES = new Set([
|
|
19
20
|
"observe",
|
|
20
21
|
"prompt",
|
|
@@ -128,6 +129,144 @@ function normalizeRelayAuthorityScopes(scopes) {
|
|
|
128
129
|
return Object.freeze(normalized);
|
|
129
130
|
}
|
|
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
|
+
|
|
131
270
|
function aad(sessionId, senderId, sequence) {
|
|
132
271
|
return Buffer.from(
|
|
133
272
|
`${PROTOCOL}\n${sessionId}\n${senderId}\n${sequence}`,
|
|
@@ -221,6 +360,7 @@ export class RemoteSessionCryptoContext {
|
|
|
221
360
|
privateKey,
|
|
222
361
|
publicKey,
|
|
223
362
|
now = () => Date.now(),
|
|
363
|
+
faultHooks = null,
|
|
224
364
|
} = {}) {
|
|
225
365
|
if (!sessionId || !localPeerId)
|
|
226
366
|
throw new Error("sessionId and localPeerId are required");
|
|
@@ -235,8 +375,12 @@ export class RemoteSessionCryptoContext {
|
|
|
235
375
|
});
|
|
236
376
|
this.publicKey = publicKey || keyPair.publicKey;
|
|
237
377
|
this._now = now;
|
|
378
|
+
this._faultHooks = faultHooks || {};
|
|
238
379
|
this.keys = new Map();
|
|
239
380
|
this.pairingMaterial = new Map();
|
|
381
|
+
this.relayPeerBindings = new Map();
|
|
382
|
+
this.acceptedRelayMemberships = new Map();
|
|
383
|
+
this.pendingCommittedRelayAcceptances = new Map();
|
|
240
384
|
this.pendingRelayCapabilities = new Map();
|
|
241
385
|
this.outstandingRelayCapabilities = new Map();
|
|
242
386
|
this.sendSequence = 0;
|
|
@@ -298,6 +442,8 @@ export class RemoteSessionCryptoContext {
|
|
|
298
442
|
this.pendingRelayCapabilities.delete(peerId);
|
|
299
443
|
this.outstandingRelayCapabilities.delete(peerId);
|
|
300
444
|
const normalizedPublicKey = canonicalPublicKey(peerPublicKey);
|
|
445
|
+
this.acceptedRelayMemberships.delete(peerId);
|
|
446
|
+
this.discardPendingRelayMembershipCommit(peerId);
|
|
301
447
|
this.keys.set(
|
|
302
448
|
peerId,
|
|
303
449
|
deriveKey(
|
|
@@ -316,11 +462,25 @@ export class RemoteSessionCryptoContext {
|
|
|
316
462
|
serverInstanceId: durableAuthority?.serverInstanceId || null,
|
|
317
463
|
authorityVersion: durableAuthority?.authorityVersion || null,
|
|
318
464
|
});
|
|
465
|
+
this.relayPeerBindings.set(peerId, {
|
|
466
|
+
mobilePublicKey: normalizedPublicKey,
|
|
467
|
+
pairingTokenDigest: remoteSessionPairingTokenDigest(pairingToken),
|
|
468
|
+
});
|
|
319
469
|
}
|
|
320
470
|
|
|
321
471
|
encrypt(peerId, message) {
|
|
472
|
+
const revokesThisPeer =
|
|
473
|
+
message?.type === "session.revoked" &&
|
|
474
|
+
message.remoteSessionId === this.sessionId;
|
|
322
475
|
const key = this.keys.get(peerId);
|
|
323
|
-
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
|
+
}
|
|
324
484
|
const sequence = ++this.sendSequence;
|
|
325
485
|
const nonce = randomBytes(12);
|
|
326
486
|
const cipher = createCipheriv("aes-256-gcm", key, nonce);
|
|
@@ -329,7 +489,7 @@ export class RemoteSessionCryptoContext {
|
|
|
329
489
|
cipher.update(JSON.stringify(message), "utf8"),
|
|
330
490
|
cipher.final(),
|
|
331
491
|
]);
|
|
332
|
-
|
|
492
|
+
const envelope = {
|
|
333
493
|
v: 1,
|
|
334
494
|
sessionId: this.sessionId,
|
|
335
495
|
senderId: this.localPeerId,
|
|
@@ -338,6 +498,12 @@ export class RemoteSessionCryptoContext {
|
|
|
338
498
|
ciphertext: b64(ciphertext),
|
|
339
499
|
tag: b64(cipher.getAuthTag()),
|
|
340
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;
|
|
341
507
|
}
|
|
342
508
|
|
|
343
509
|
decrypt(envelope) {
|
|
@@ -356,60 +522,530 @@ export class RemoteSessionCryptoContext {
|
|
|
356
522
|
"Remote Session replay or out-of-order envelope rejected",
|
|
357
523
|
);
|
|
358
524
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
+
}
|
|
376
596
|
if (
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
message?.type === "pair.join" &&
|
|
384
|
-
typeof message.token === "string" &&
|
|
385
|
-
remoteSessionPairingTokenDigest(message.token) ===
|
|
386
|
-
pairing.pairingTokenDigest
|
|
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
|
|
387
603
|
) {
|
|
388
|
-
|
|
389
|
-
schema: RELAY_POSSESSION_SCHEMA,
|
|
390
|
-
sessionId: this.sessionId,
|
|
391
|
-
mobilePeerId: envelope.senderId,
|
|
392
|
-
mobilePublicKey: pairing.mobilePublicKey,
|
|
393
|
-
pairingTokenDigest: pairing.pairingTokenDigest,
|
|
394
|
-
coordinatorId: pairing.coordinatorId,
|
|
395
|
-
serverInstanceId: pairing.serverInstanceId,
|
|
396
|
-
authorityVersion: pairing.authorityVersion,
|
|
397
|
-
authorizedScopes: pairing.authorizedScopes,
|
|
398
|
-
pairingExpiresAtMs: pairing.expiresAtMs,
|
|
399
|
-
envelopeSequence: envelope.sequence,
|
|
400
|
-
envelopeTranscriptHash: relayEnvelopeTranscriptHash(envelope),
|
|
401
|
-
});
|
|
402
|
-
relayPossessionCapabilities.set(proof, { ...proof });
|
|
403
|
-
this.pendingRelayCapabilities.set(envelope.senderId, proof);
|
|
404
|
-
this.outstandingRelayCapabilities.set(envelope.senderId, proof);
|
|
405
|
-
this.pairingMaterial.delete(envelope.senderId);
|
|
604
|
+
throw new Error("Durable Remote relay authority domain is required");
|
|
406
605
|
}
|
|
407
|
-
|
|
408
|
-
|
|
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)) {
|
|
409
772
|
throw new Error(
|
|
410
|
-
|
|
773
|
+
"Remote relay peer already has a different pending membership commit",
|
|
411
774
|
);
|
|
412
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",
|
|
1021
|
+
);
|
|
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);
|
|
413
1049
|
}
|
|
414
1050
|
|
|
415
1051
|
takeRelayPossessionCapability(peerId) {
|
|
@@ -422,4 +1058,68 @@ export class RemoteSessionCryptoContext {
|
|
|
422
1058
|
}
|
|
423
1059
|
return capability;
|
|
424
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
|
+
) {
|
|
1115
|
+
throw new Error(
|
|
1116
|
+
"Remote relay membership acceptance is unavailable or mismatched",
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
return receipt;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
hasRelayMembershipAcceptance(peerId) {
|
|
1123
|
+
return this.acceptedRelayMemberships.has(peerId);
|
|
1124
|
+
}
|
|
425
1125
|
}
|