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
|
@@ -121,6 +121,11 @@ export async function createWsApprovalGate({
|
|
|
121
121
|
}),
|
|
122
122
|
hasConfirmer: () => gate.hasConfirmer(),
|
|
123
123
|
setConfirmer: (fn) => gate.setConfirmer(fn),
|
|
124
|
+
setAuthorizationConsumer: (fn) => gate.setAuthorizationConsumer?.(fn),
|
|
125
|
+
hasAuthorizationConsumer: () =>
|
|
126
|
+
gate.hasAuthorizationConsumer?.() === true,
|
|
127
|
+
consumeAuthorization: (authorization, ctx) =>
|
|
128
|
+
gate.consumeAuthorization(authorization, ctx),
|
|
124
129
|
getSessionPolicy: (sid) => inner.getSessionPolicy(sid || sessionId),
|
|
125
130
|
setSessionPolicy: (sid, policy) =>
|
|
126
131
|
inner.setSessionPolicy(sid || sessionId, policy),
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { EventEmitter } from "node:events";
|
|
14
|
-
import { createHash, timingSafeEqual } from "node:crypto";
|
|
14
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { dirname, join } from "node:path";
|
|
17
17
|
import { WebSocketServer } from "ws";
|
|
@@ -29,6 +29,7 @@ import { createWsMessageDispatcher } from "./message-dispatcher.js";
|
|
|
29
29
|
import {
|
|
30
30
|
RemoteSessionRegistry,
|
|
31
31
|
RemoteSessionPolicy,
|
|
32
|
+
REMOTE_APPROVAL_BINDING_CAPABILITY,
|
|
32
33
|
} from "../../harness/remote-session-registry.js";
|
|
33
34
|
import { RemoteSessionAuditLog } from "../../harness/remote-session-audit.js";
|
|
34
35
|
import { RemoteSessionAuditFileSink } from "../../harness/remote-session-audit-sink.js";
|
|
@@ -38,6 +39,8 @@ import {
|
|
|
38
39
|
} from "../../harness/remote-session-push.js";
|
|
39
40
|
import { createRemoteSessionPushSender } from "../../harness/remote-session-push-senders.js";
|
|
40
41
|
import { DurableRemoteMembershipAuthority } from "../../lib/remote-membership-authority.js";
|
|
42
|
+
import { DurableRemoteMembershipCoordinator } from "../../lib/remote-membership-coordinator.js";
|
|
43
|
+
import { remoteSessionPairingTokenDigest } from "../../harness/remote-session-crypto.js";
|
|
41
44
|
import { RemoteSessionRelay } from "../remote-session-relay.js";
|
|
42
45
|
import { handleRemoteSessionPublish } from "./remote-session-protocol.js";
|
|
43
46
|
import { handleTaskDetail, handleTaskHistory } from "./task-protocol.js";
|
|
@@ -271,11 +274,26 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
271
274
|
/** Org-policy constraints for Remote Sessions (scopes, device cap, TTL). */
|
|
272
275
|
this.remoteSessionPolicy =
|
|
273
276
|
options.remoteSessionPolicy || RemoteSessionPolicy.fromEnv(process.env);
|
|
277
|
+
/**
|
|
278
|
+
* Server-authoritative durable membership/lease coordinator. Constructed
|
|
279
|
+
* lazily so ordinary WS deployments that never enable Remote Approval do
|
|
280
|
+
* not create a machine-security root as a constructor side effect.
|
|
281
|
+
*/
|
|
282
|
+
this._remoteMembershipCoordinatorOption =
|
|
283
|
+
options.remoteMembershipCoordinator === null
|
|
284
|
+
? null
|
|
285
|
+
: options.remoteMembershipCoordinator || undefined;
|
|
286
|
+
this._remoteMembershipCoordinatorOptions =
|
|
287
|
+
options.remoteMembershipCoordinatorOptions || {};
|
|
274
288
|
/** Local authorization state for multi-device Remote Sessions. */
|
|
275
289
|
this.remoteSessions =
|
|
276
290
|
options.remoteSessionRegistry ||
|
|
277
291
|
new RemoteSessionRegistry({
|
|
278
292
|
policy: this.remoteSessionPolicy,
|
|
293
|
+
membershipCoordinator:
|
|
294
|
+
options.remoteMembershipCoordinator === null
|
|
295
|
+
? null
|
|
296
|
+
: () => this._requireRemoteMembershipCoordinator(),
|
|
279
297
|
membershipAuthority:
|
|
280
298
|
options.remoteMembershipAuthority === null
|
|
281
299
|
? null
|
|
@@ -343,6 +361,20 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
343
361
|
this._clientCounter = 0;
|
|
344
362
|
}
|
|
345
363
|
|
|
364
|
+
_requireRemoteMembershipCoordinator() {
|
|
365
|
+
if (typeof this._remoteMembershipCoordinatorOption === "function") {
|
|
366
|
+
this._remoteMembershipCoordinatorOption =
|
|
367
|
+
this._remoteMembershipCoordinatorOption();
|
|
368
|
+
}
|
|
369
|
+
if (!this._remoteMembershipCoordinatorOption) {
|
|
370
|
+
this._remoteMembershipCoordinatorOption =
|
|
371
|
+
new DurableRemoteMembershipCoordinator(
|
|
372
|
+
this._remoteMembershipCoordinatorOptions,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return this._remoteMembershipCoordinatorOption;
|
|
376
|
+
}
|
|
377
|
+
|
|
346
378
|
/** Start the WebSocket server */
|
|
347
379
|
start() {
|
|
348
380
|
return new Promise((resolve, reject) => {
|
|
@@ -465,6 +497,7 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
465
497
|
ip: clientIp,
|
|
466
498
|
alive: true,
|
|
467
499
|
authTimer: null,
|
|
500
|
+
membershipConnectionNonce: randomBytes(32).toString("base64url"),
|
|
468
501
|
};
|
|
469
502
|
this.clients.set(clientId, client);
|
|
470
503
|
|
|
@@ -1329,10 +1362,17 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
1329
1362
|
data.sessionId,
|
|
1330
1363
|
hostClientId,
|
|
1331
1364
|
)) {
|
|
1332
|
-
for (const
|
|
1365
|
+
for (const memberView of this.remoteSessions.members(
|
|
1366
|
+
remoteSession.sessionId,
|
|
1367
|
+
)) {
|
|
1368
|
+
const memberTransportId =
|
|
1369
|
+
memberView.transportClientId || memberView.clientId;
|
|
1370
|
+
if (remoteSession.hostPrincipalId && !memberView.transportClientId) {
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1333
1373
|
if (
|
|
1334
|
-
|
|
1335
|
-
!
|
|
1374
|
+
memberTransportId === hostClientId ||
|
|
1375
|
+
!memberView.scopes.includes("observe")
|
|
1336
1376
|
) {
|
|
1337
1377
|
continue;
|
|
1338
1378
|
}
|
|
@@ -1340,27 +1380,47 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
1340
1380
|
// Independent of the WS mirror below — a relay-paired mobile app may
|
|
1341
1381
|
// report a live socket while its process is suspended and will never
|
|
1342
1382
|
// read the mirrored event until the user taps the push.
|
|
1343
|
-
if (
|
|
1344
|
-
this._dispatchApprovalPush(remoteSession,
|
|
1383
|
+
if (memberView.pushToken && isApprovalRequestEvent(data.type)) {
|
|
1384
|
+
this._dispatchApprovalPush(remoteSession, memberView, data);
|
|
1345
1385
|
}
|
|
1346
|
-
const target = this.clients.get(
|
|
1386
|
+
const target = this.clients.get(memberTransportId);
|
|
1347
1387
|
if (!target && this.remoteSessionRelay) {
|
|
1348
1388
|
const crypto = this.remoteSessionCrypto.get(remoteSession.sessionId);
|
|
1349
1389
|
if (crypto) {
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1390
|
+
try {
|
|
1391
|
+
this.remoteSessions.authorize(
|
|
1392
|
+
remoteSession.sessionId,
|
|
1393
|
+
memberTransportId,
|
|
1394
|
+
"observe",
|
|
1395
|
+
);
|
|
1396
|
+
this.remoteSessionRelay.sendEncrypted(
|
|
1397
|
+
memberTransportId,
|
|
1398
|
+
crypto.encrypt(memberTransportId, data),
|
|
1399
|
+
);
|
|
1400
|
+
} catch {
|
|
1401
|
+
// Revoked/closed between enumeration and the physical send.
|
|
1402
|
+
}
|
|
1354
1403
|
}
|
|
1355
1404
|
continue;
|
|
1356
1405
|
}
|
|
1357
1406
|
if (!target) continue;
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1407
|
+
try {
|
|
1408
|
+
this.remoteSessions.authorize(
|
|
1409
|
+
remoteSession.sessionId,
|
|
1410
|
+
memberTransportId,
|
|
1411
|
+
"observe",
|
|
1412
|
+
);
|
|
1413
|
+
const currentTarget = this.clients.get(memberTransportId);
|
|
1414
|
+
if (!currentTarget || currentTarget.ws !== target.ws) continue;
|
|
1415
|
+
this._send(currentTarget.ws, {
|
|
1416
|
+
type: "remote-session-event",
|
|
1417
|
+
remoteSessionId: remoteSession.sessionId,
|
|
1418
|
+
agentSessionId: remoteSession.agentSessionId,
|
|
1419
|
+
event: data,
|
|
1420
|
+
});
|
|
1421
|
+
} catch {
|
|
1422
|
+
// Current durable membership is the send-time authority.
|
|
1423
|
+
}
|
|
1364
1424
|
}
|
|
1365
1425
|
}
|
|
1366
1426
|
}
|
|
@@ -1373,14 +1433,26 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
1373
1433
|
_dispatchApprovalPush(remoteSession, member, data) {
|
|
1374
1434
|
const dispatcher = this.remoteSessionPush;
|
|
1375
1435
|
if (!dispatcher || !dispatcher.enabled) return;
|
|
1436
|
+
let authorized;
|
|
1437
|
+
try {
|
|
1438
|
+
authorized = this.remoteSessions.authorize(
|
|
1439
|
+
remoteSession.sessionId,
|
|
1440
|
+
member.transportClientId || member.clientId,
|
|
1441
|
+
"observe",
|
|
1442
|
+
);
|
|
1443
|
+
} catch {
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
const currentMember = authorized.member;
|
|
1447
|
+
if (!currentMember.pushToken) return;
|
|
1376
1448
|
const requestId =
|
|
1377
1449
|
data.requestId || data.approvalId || data.id || `approval:${data.type}`;
|
|
1378
1450
|
dispatcher
|
|
1379
1451
|
.dispatch({
|
|
1380
|
-
token:
|
|
1381
|
-
provider:
|
|
1452
|
+
token: currentMember.pushToken,
|
|
1453
|
+
provider: currentMember.pushProvider,
|
|
1382
1454
|
sessionId: remoteSession.sessionId,
|
|
1383
|
-
clientId:
|
|
1455
|
+
clientId: currentMember.principalId || currentMember.clientId,
|
|
1384
1456
|
dedupeKey: requestId,
|
|
1385
1457
|
notification: {
|
|
1386
1458
|
title: "Approval requested",
|
|
@@ -1394,7 +1466,7 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
1394
1466
|
try {
|
|
1395
1467
|
this.remoteSessions.registerPush(
|
|
1396
1468
|
remoteSession.sessionId,
|
|
1397
|
-
|
|
1469
|
+
currentMember.principalId || currentMember.clientId,
|
|
1398
1470
|
{ token: null },
|
|
1399
1471
|
);
|
|
1400
1472
|
} catch {
|
|
@@ -1402,15 +1474,15 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
1402
1474
|
}
|
|
1403
1475
|
this.remoteSessionAudit?.record({
|
|
1404
1476
|
sessionId: remoteSession.sessionId,
|
|
1405
|
-
actor:
|
|
1477
|
+
actor: currentMember.principalId || currentMember.clientId,
|
|
1406
1478
|
action: "push.unregistered",
|
|
1407
|
-
detail: { provider:
|
|
1479
|
+
detail: { provider: currentMember.pushProvider || null },
|
|
1408
1480
|
});
|
|
1409
1481
|
return;
|
|
1410
1482
|
}
|
|
1411
1483
|
this.remoteSessionAudit?.record({
|
|
1412
1484
|
sessionId: remoteSession.sessionId,
|
|
1413
|
-
actor:
|
|
1485
|
+
actor: currentMember.principalId || currentMember.clientId,
|
|
1414
1486
|
action:
|
|
1415
1487
|
outcome.status === "sent"
|
|
1416
1488
|
? "push.sent"
|
|
@@ -1418,7 +1490,7 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
1418
1490
|
? "push.failed"
|
|
1419
1491
|
: "push.skipped",
|
|
1420
1492
|
detail: {
|
|
1421
|
-
provider:
|
|
1493
|
+
provider: currentMember.pushProvider || null,
|
|
1422
1494
|
reason: outcome.reason || null,
|
|
1423
1495
|
error: outcome.error || null,
|
|
1424
1496
|
},
|
|
@@ -1447,6 +1519,52 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
1447
1519
|
});
|
|
1448
1520
|
}
|
|
1449
1521
|
|
|
1522
|
+
_assertExactRemoteRelayMembership(sessionId, peerId, receipt) {
|
|
1523
|
+
if (
|
|
1524
|
+
receipt?.sessionId !== sessionId ||
|
|
1525
|
+
receipt.mobilePeerId !== peerId ||
|
|
1526
|
+
!Array.isArray(receipt.scopes) ||
|
|
1527
|
+
receipt.scopes.length === 0
|
|
1528
|
+
) {
|
|
1529
|
+
throw new Error("Remote relay membership reconciliation changed");
|
|
1530
|
+
}
|
|
1531
|
+
const authorized = this.remoteSessions.authorize(
|
|
1532
|
+
sessionId,
|
|
1533
|
+
peerId,
|
|
1534
|
+
receipt.scopes[0],
|
|
1535
|
+
);
|
|
1536
|
+
const expectedScopes = JSON.stringify([...receipt.scopes].sort());
|
|
1537
|
+
if (
|
|
1538
|
+
authorized.session.sessionId !== receipt.sessionId ||
|
|
1539
|
+
authorized.session.sessionEpoch !== receipt.sessionEpoch ||
|
|
1540
|
+
authorized.member.principalId !== receipt.principalId ||
|
|
1541
|
+
authorized.member.membershipEpoch !== receipt.membershipEpoch ||
|
|
1542
|
+
authorized.member.transportClientId !== peerId ||
|
|
1543
|
+
JSON.stringify([...authorized.member.scopes].sort()) !== expectedScopes ||
|
|
1544
|
+
authorized.membership?.sessionId !== receipt.sessionId ||
|
|
1545
|
+
authorized.membership?.sessionEpoch !== receipt.sessionEpoch ||
|
|
1546
|
+
authorized.membership?.principalId !== receipt.principalId ||
|
|
1547
|
+
authorized.membership?.membershipEpoch !== receipt.membershipEpoch ||
|
|
1548
|
+
JSON.stringify([...(authorized.membership?.scopes || [])].sort()) !==
|
|
1549
|
+
expectedScopes
|
|
1550
|
+
) {
|
|
1551
|
+
throw new Error("Remote relay membership reconciliation changed");
|
|
1552
|
+
}
|
|
1553
|
+
return authorized;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
_deleteRemoteSessionPairingSecretIfMatches(sessionId, tokenDigest) {
|
|
1557
|
+
const current = this.remoteSessionPairingSecrets.get(sessionId);
|
|
1558
|
+
if (
|
|
1559
|
+
typeof current === "string" &&
|
|
1560
|
+
remoteSessionPairingTokenDigest(current) === tokenDigest
|
|
1561
|
+
) {
|
|
1562
|
+
this.remoteSessionPairingSecrets.delete(sessionId);
|
|
1563
|
+
return true;
|
|
1564
|
+
}
|
|
1565
|
+
return false;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1450
1568
|
async _handleRemoteRelayMessage(message) {
|
|
1451
1569
|
if (
|
|
1452
1570
|
message?.type !== "message" ||
|
|
@@ -1457,21 +1575,312 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
1457
1575
|
const sessionId = payload.envelope?.sessionId;
|
|
1458
1576
|
const crypto = this.remoteSessionCrypto.get(sessionId);
|
|
1459
1577
|
const token = this.remoteSessionPairingSecrets.get(sessionId);
|
|
1460
|
-
if (!crypto
|
|
1578
|
+
if (!crypto)
|
|
1461
1579
|
throw new Error("Remote Session pairing is unavailable or expired");
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1580
|
+
if (
|
|
1581
|
+
typeof payload.mobilePeerId !== "string" ||
|
|
1582
|
+
payload.mobilePeerId !== message.from ||
|
|
1583
|
+
payload.envelope?.senderId !== payload.mobilePeerId
|
|
1584
|
+
) {
|
|
1585
|
+
throw new Error("Remote Session relay peer binding is invalid");
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// An already-authenticated receipt wins even if the host has meanwhile
|
|
1589
|
+
// issued a fresh token for another device. Trying the committed key first
|
|
1590
|
+
// lets an ACK-lost peer reconcile without letting the new invitation erase
|
|
1591
|
+
// its key/receipt. Authentication failure is side-effect free; only then
|
|
1592
|
+
// may this frame be considered as a prospective current-token pairing.
|
|
1593
|
+
let acceptedRetry = null;
|
|
1594
|
+
const hasPendingCommittedAcceptance =
|
|
1595
|
+
crypto.hasPendingCommittedRelayAcceptance(payload.mobilePeerId);
|
|
1596
|
+
if (crypto.hasRelayMembershipAcceptance(payload.mobilePeerId)) {
|
|
1597
|
+
try {
|
|
1598
|
+
acceptedRetry = crypto.decrypt(payload.envelope);
|
|
1599
|
+
} catch (error) {
|
|
1600
|
+
if (!token && !hasPendingCommittedAcceptance) throw error;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
if (acceptedRetry) {
|
|
1604
|
+
const join = acceptedRetry;
|
|
1605
|
+
if (
|
|
1606
|
+
join.type !== "pair.join" ||
|
|
1607
|
+
join.remoteSessionId !== sessionId ||
|
|
1608
|
+
typeof join.token !== "string"
|
|
1609
|
+
) {
|
|
1610
|
+
throw new Error("Invalid encrypted Remote Session pairing retry");
|
|
1611
|
+
}
|
|
1612
|
+
const receipt = crypto.readRelayMembershipAcceptance(
|
|
1613
|
+
payload.mobilePeerId,
|
|
1614
|
+
{
|
|
1615
|
+
mobilePublicKey: payload.mobilePublicKey,
|
|
1616
|
+
pairingTokenDigest: remoteSessionPairingTokenDigest(join.token),
|
|
1617
|
+
capabilities: join.capabilities,
|
|
1618
|
+
},
|
|
1619
|
+
);
|
|
1620
|
+
this._assertExactRemoteRelayMembership(
|
|
1621
|
+
sessionId,
|
|
1622
|
+
payload.mobilePeerId,
|
|
1623
|
+
receipt,
|
|
1624
|
+
);
|
|
1625
|
+
this._deleteRemoteSessionPairingSecretIfMatches(
|
|
1626
|
+
sessionId,
|
|
1627
|
+
receipt.pairingTokenDigest,
|
|
1628
|
+
);
|
|
1629
|
+
this.remoteSessionAudit?.record({
|
|
1630
|
+
sessionId,
|
|
1631
|
+
actor: payload.mobilePeerId,
|
|
1632
|
+
action: "device.join-reconciled",
|
|
1633
|
+
detail: {
|
|
1634
|
+
via: "relay",
|
|
1635
|
+
principalId: receipt.principalId,
|
|
1636
|
+
sessionEpoch: receipt.sessionEpoch,
|
|
1637
|
+
membershipEpoch: receipt.membershipEpoch,
|
|
1638
|
+
},
|
|
1639
|
+
});
|
|
1640
|
+
this.remoteSessionRelay.sendEncrypted(
|
|
1641
|
+
payload.mobilePeerId,
|
|
1642
|
+
crypto.encrypt(payload.mobilePeerId, {
|
|
1643
|
+
type: "pair.accepted",
|
|
1644
|
+
remoteSessionId: sessionId,
|
|
1645
|
+
principalId: receipt.principalId,
|
|
1646
|
+
sessionEpoch: receipt.sessionEpoch,
|
|
1647
|
+
membershipEpoch: receipt.membershipEpoch,
|
|
1648
|
+
scopes: [...receipt.scopes],
|
|
1649
|
+
capabilities: [...receipt.capabilities],
|
|
1650
|
+
reconciled: true,
|
|
1651
|
+
}),
|
|
1652
|
+
);
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
let pendingRetry = null;
|
|
1657
|
+
if (hasPendingCommittedAcceptance) {
|
|
1658
|
+
try {
|
|
1659
|
+
pendingRetry = crypto.decryptPendingCommittedRelayAcceptance(
|
|
1660
|
+
payload.mobilePeerId,
|
|
1661
|
+
payload.envelope,
|
|
1662
|
+
);
|
|
1663
|
+
} catch (error) {
|
|
1664
|
+
if (!token) throw error;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
if (pendingRetry) {
|
|
1668
|
+
const join = pendingRetry;
|
|
1669
|
+
if (
|
|
1670
|
+
join.type !== "pair.join" ||
|
|
1671
|
+
join.remoteSessionId !== sessionId ||
|
|
1672
|
+
typeof join.token !== "string"
|
|
1673
|
+
) {
|
|
1674
|
+
throw new Error("Invalid encrypted Remote Session pairing retry");
|
|
1675
|
+
}
|
|
1676
|
+
crypto.readPendingRelayMembershipRequest(payload.mobilePeerId, {
|
|
1677
|
+
mobilePublicKey: payload.mobilePublicKey,
|
|
1678
|
+
pairingTokenDigest: remoteSessionPairingTokenDigest(join.token),
|
|
1679
|
+
capabilities: join.capabilities,
|
|
1680
|
+
});
|
|
1681
|
+
const membershipCoordinator = this._requireRemoteMembershipCoordinator();
|
|
1682
|
+
const authorityRead = membershipCoordinator.getSessionSnapshot(sessionId);
|
|
1683
|
+
const resolution = crypto.resolvePendingRelayMembershipCommit(
|
|
1684
|
+
payload.mobilePeerId,
|
|
1685
|
+
authorityRead,
|
|
1686
|
+
membershipCoordinator.relayAuthorityDescriptor(),
|
|
1687
|
+
);
|
|
1688
|
+
if (resolution.status === "not-committed") {
|
|
1689
|
+
crypto.discardPendingRelayMembershipCommit(payload.mobilePeerId);
|
|
1690
|
+
} else if (resolution.status !== "committed") {
|
|
1691
|
+
crypto.clearRelayPeer(payload.mobilePeerId);
|
|
1692
|
+
throw new Error(
|
|
1693
|
+
"Remote relay committed membership is no longer active",
|
|
1694
|
+
);
|
|
1695
|
+
} else {
|
|
1696
|
+
const receipt = crypto.readPendingCommittedRelayAcceptance(
|
|
1697
|
+
payload.mobilePeerId,
|
|
1698
|
+
{
|
|
1699
|
+
mobilePublicKey: payload.mobilePublicKey,
|
|
1700
|
+
pairingTokenDigest: remoteSessionPairingTokenDigest(join.token),
|
|
1701
|
+
capabilities: join.capabilities,
|
|
1702
|
+
},
|
|
1703
|
+
);
|
|
1704
|
+
this._assertExactRemoteRelayMembership(
|
|
1705
|
+
sessionId,
|
|
1706
|
+
payload.mobilePeerId,
|
|
1707
|
+
receipt,
|
|
1708
|
+
);
|
|
1709
|
+
crypto.finalizeCommittedRelayAcceptance(payload.mobilePeerId);
|
|
1710
|
+
this._deleteRemoteSessionPairingSecretIfMatches(
|
|
1711
|
+
sessionId,
|
|
1712
|
+
receipt.pairingTokenDigest,
|
|
1713
|
+
);
|
|
1714
|
+
this.remoteSessionAudit?.record({
|
|
1715
|
+
sessionId,
|
|
1716
|
+
actor: payload.mobilePeerId,
|
|
1717
|
+
action: "device.join-reconciled",
|
|
1718
|
+
detail: {
|
|
1719
|
+
via: "relay",
|
|
1720
|
+
principalId: receipt.principalId,
|
|
1721
|
+
sessionEpoch: receipt.sessionEpoch,
|
|
1722
|
+
membershipEpoch: receipt.membershipEpoch,
|
|
1723
|
+
},
|
|
1724
|
+
});
|
|
1725
|
+
this.remoteSessionRelay.sendEncrypted(
|
|
1726
|
+
payload.mobilePeerId,
|
|
1727
|
+
crypto.encrypt(payload.mobilePeerId, {
|
|
1728
|
+
type: "pair.accepted",
|
|
1729
|
+
remoteSessionId: sessionId,
|
|
1730
|
+
principalId: receipt.principalId,
|
|
1731
|
+
sessionEpoch: receipt.sessionEpoch,
|
|
1732
|
+
membershipEpoch: receipt.membershipEpoch,
|
|
1733
|
+
scopes: [...receipt.scopes],
|
|
1734
|
+
capabilities: [...receipt.capabilities],
|
|
1735
|
+
reconciled: true,
|
|
1736
|
+
}),
|
|
1737
|
+
);
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
if (!token) {
|
|
1742
|
+
throw new Error("Remote Session pairing is unavailable or expired");
|
|
1743
|
+
}
|
|
1744
|
+
const invitation = this.remoteSessions.pairingInvitation(sessionId, token);
|
|
1745
|
+
const durableMembership = Boolean(invitation.session.hostPrincipalId);
|
|
1746
|
+
// A relay peer id is the transport identity for the life of its active
|
|
1747
|
+
// membership. If an existing receipt could not authenticate this frame,
|
|
1748
|
+
// do not let possession of a newer invitation replace that transport with
|
|
1749
|
+
// a different key/principal. In particular, joinRelayMember() commits the
|
|
1750
|
+
// durable coordinator transition before attaching the transport; detecting
|
|
1751
|
+
// the collision there would consume the token and leave an orphan member.
|
|
1752
|
+
// members() refreshes durable state and returns only active memberships, so
|
|
1753
|
+
// a revoked/inactive member whose binding was cleared does not block a
|
|
1754
|
+
// legitimate fresh join.
|
|
1755
|
+
const activeTransportMember = this.remoteSessions
|
|
1756
|
+
.members(sessionId)
|
|
1757
|
+
.find((member) => member.transportClientId === payload.mobilePeerId);
|
|
1758
|
+
if (activeTransportMember) {
|
|
1759
|
+
throw new Error(
|
|
1760
|
+
"Remote relay transport is already bound to an active membership",
|
|
1761
|
+
);
|
|
1762
|
+
}
|
|
1763
|
+
// Derive and authenticate against a candidate key without touching the
|
|
1764
|
+
// live key, replay high-water, or accepted receipt.
|
|
1765
|
+
const membershipCoordinator = durableMembership
|
|
1766
|
+
? this._requireRemoteMembershipCoordinator()
|
|
1767
|
+
: null;
|
|
1768
|
+
const relayAuthority = membershipCoordinator
|
|
1769
|
+
? membershipCoordinator.relayAuthorityDescriptor()
|
|
1770
|
+
: null;
|
|
1771
|
+
const stagedPairing = crypto.stageRelayPairing(
|
|
1772
|
+
payload.mobilePeerId,
|
|
1773
|
+
payload.mobilePublicKey,
|
|
1774
|
+
token,
|
|
1775
|
+
payload.envelope,
|
|
1776
|
+
durableMembership
|
|
1777
|
+
? {
|
|
1778
|
+
authorizedScopes: invitation.scopes,
|
|
1779
|
+
expiresAtMs: invitation.expiresAt,
|
|
1780
|
+
...relayAuthority,
|
|
1781
|
+
}
|
|
1782
|
+
: null,
|
|
1783
|
+
);
|
|
1784
|
+
const join = stagedPairing.message;
|
|
1785
|
+
if (
|
|
1786
|
+
join.type !== "pair.join" ||
|
|
1787
|
+
join.remoteSessionId !== sessionId ||
|
|
1788
|
+
join.token !== token
|
|
1789
|
+
) {
|
|
1465
1790
|
throw new Error("Invalid encrypted Remote Session pairing request");
|
|
1466
1791
|
}
|
|
1467
|
-
const
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1792
|
+
const relayPossessionCapability = durableMembership
|
|
1793
|
+
? stagedPairing.possessionCapability
|
|
1794
|
+
: null;
|
|
1795
|
+
let relayMember;
|
|
1796
|
+
let committedReceipt = null;
|
|
1797
|
+
if (durableMembership) {
|
|
1798
|
+
const expectedScopes = invitation.scopes.filter(
|
|
1799
|
+
(scope) =>
|
|
1800
|
+
scope !== "approve" ||
|
|
1801
|
+
(Array.isArray(join.capabilities) &&
|
|
1802
|
+
join.capabilities.includes(REMOTE_APPROVAL_BINDING_CAPABILITY)),
|
|
1803
|
+
);
|
|
1804
|
+
const authorityBefore =
|
|
1805
|
+
membershipCoordinator.getSessionSnapshot(sessionId);
|
|
1806
|
+
if (!authorityBefore) {
|
|
1807
|
+
throw new Error("Remote relay membership authority is unavailable");
|
|
1808
|
+
}
|
|
1809
|
+
// The prepared record exists before the durable call. If the coordinator
|
|
1810
|
+
// commits but the registry return path or the very next instruction
|
|
1811
|
+
// fails, a later authenticated retry can adjudicate against a fresh
|
|
1812
|
+
// authority snapshot without replaying the one-time join.
|
|
1813
|
+
crypto.armPendingRelayMembershipCommit(stagedPairing.stage, {
|
|
1814
|
+
authoritySnapshot: authorityBefore.session,
|
|
1815
|
+
scopes: expectedScopes,
|
|
1816
|
+
capabilities: join.capabilities,
|
|
1817
|
+
});
|
|
1818
|
+
try {
|
|
1819
|
+
relayMember = this.remoteSessions.joinRelayMember({
|
|
1820
|
+
sessionId,
|
|
1821
|
+
transportClientId: payload.mobilePeerId,
|
|
1822
|
+
token: join.token,
|
|
1823
|
+
// Mobile/web clients send a raw 32-byte X25519 key. The authenticated
|
|
1824
|
+
// relay transcript canonicalizes that key to SPKI; use the bound proof
|
|
1825
|
+
// value rather than reparsing the untrusted transport representation.
|
|
1826
|
+
mobilePublicKey: relayPossessionCapability.mobilePublicKey,
|
|
1827
|
+
pairingTokenDigest: remoteSessionPairingTokenDigest(join.token),
|
|
1828
|
+
possessionCapability: relayPossessionCapability,
|
|
1829
|
+
capabilities: join.capabilities,
|
|
1830
|
+
pushToken: join.pushToken,
|
|
1831
|
+
pushProvider: join.pushProvider,
|
|
1832
|
+
});
|
|
1833
|
+
committedReceipt = crypto.completePendingCommittedRelayAcceptance(
|
|
1834
|
+
payload.mobilePeerId,
|
|
1835
|
+
{
|
|
1836
|
+
mobilePublicKey: relayPossessionCapability.mobilePublicKey,
|
|
1837
|
+
pairingTokenDigest: remoteSessionPairingTokenDigest(join.token),
|
|
1838
|
+
principalId: relayMember.member.principalId,
|
|
1839
|
+
sessionEpoch: relayMember.session.sessionEpoch,
|
|
1840
|
+
membershipEpoch: relayMember.member.membershipEpoch,
|
|
1841
|
+
scopes: relayMember.member.scopes,
|
|
1842
|
+
capabilities: relayMember.capabilities,
|
|
1843
|
+
statement: relayMember.statement,
|
|
1844
|
+
},
|
|
1845
|
+
);
|
|
1846
|
+
} catch (error) {
|
|
1847
|
+
try {
|
|
1848
|
+
const freshAuthority =
|
|
1849
|
+
membershipCoordinator.getSessionSnapshot(sessionId);
|
|
1850
|
+
const resolution = crypto.resolvePendingRelayMembershipCommit(
|
|
1851
|
+
payload.mobilePeerId,
|
|
1852
|
+
freshAuthority,
|
|
1853
|
+
relayAuthority,
|
|
1854
|
+
);
|
|
1855
|
+
if (resolution.status === "not-committed") {
|
|
1856
|
+
crypto.discardPendingRelayMembershipCommit(payload.mobilePeerId);
|
|
1857
|
+
} else if (resolution.status === "fenced") {
|
|
1858
|
+
crypto.clearRelayPeer(payload.mobilePeerId);
|
|
1859
|
+
}
|
|
1860
|
+
} catch {
|
|
1861
|
+
// Keep the prepared record if the fresh authority read itself is
|
|
1862
|
+
// unavailable. Guessing non-commit here could erase the only
|
|
1863
|
+
// in-process evidence for an already-durable membership.
|
|
1864
|
+
}
|
|
1865
|
+
throw error;
|
|
1866
|
+
}
|
|
1867
|
+
this._assertExactRemoteRelayMembership(
|
|
1868
|
+
sessionId,
|
|
1869
|
+
payload.mobilePeerId,
|
|
1870
|
+
committedReceipt,
|
|
1871
|
+
);
|
|
1872
|
+
crypto.finalizeCommittedRelayAcceptance(payload.mobilePeerId);
|
|
1873
|
+
} else {
|
|
1874
|
+
relayMember = this.remoteSessions.join({
|
|
1875
|
+
sessionId,
|
|
1876
|
+
clientId: payload.mobilePeerId,
|
|
1877
|
+
token: join.token,
|
|
1878
|
+
via: "relay",
|
|
1879
|
+
pushToken: join.pushToken,
|
|
1880
|
+
pushProvider: join.pushProvider,
|
|
1881
|
+
});
|
|
1882
|
+
crypto.commitStagedRelayPairing(stagedPairing.stage);
|
|
1883
|
+
}
|
|
1475
1884
|
this.remoteSessionAudit?.record({
|
|
1476
1885
|
sessionId,
|
|
1477
1886
|
actor: payload.mobilePeerId,
|
|
@@ -1479,14 +1888,24 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
1479
1888
|
detail: {
|
|
1480
1889
|
via: "relay",
|
|
1481
1890
|
hasPush: relayMember?.member?.pushToken ? true : false,
|
|
1891
|
+
scopes: relayMember?.member?.scopes || null,
|
|
1892
|
+
capabilities: relayMember?.capabilities || [],
|
|
1482
1893
|
},
|
|
1483
1894
|
});
|
|
1484
|
-
this.
|
|
1895
|
+
this._deleteRemoteSessionPairingSecretIfMatches(
|
|
1896
|
+
sessionId,
|
|
1897
|
+
remoteSessionPairingTokenDigest(join.token),
|
|
1898
|
+
);
|
|
1485
1899
|
this.remoteSessionRelay.sendEncrypted(
|
|
1486
1900
|
payload.mobilePeerId,
|
|
1487
1901
|
crypto.encrypt(payload.mobilePeerId, {
|
|
1488
1902
|
type: "pair.accepted",
|
|
1489
1903
|
remoteSessionId: sessionId,
|
|
1904
|
+
principalId: relayMember?.member?.principalId || null,
|
|
1905
|
+
sessionEpoch: relayMember?.session?.sessionEpoch || null,
|
|
1906
|
+
membershipEpoch: relayMember?.member?.membershipEpoch || null,
|
|
1907
|
+
scopes: relayMember?.member?.scopes || null,
|
|
1908
|
+
capabilities: relayMember?.capabilities || [],
|
|
1490
1909
|
}),
|
|
1491
1910
|
);
|
|
1492
1911
|
}
|
|
@@ -1500,6 +1919,11 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
1500
1919
|
readyState: 1,
|
|
1501
1920
|
send: (raw) => {
|
|
1502
1921
|
const response = JSON.parse(raw);
|
|
1922
|
+
try {
|
|
1923
|
+
this.remoteSessions.authorize(envelope.sessionId, from, "observe");
|
|
1924
|
+
} catch {
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1503
1927
|
this.remoteSessionRelay.sendEncrypted(
|
|
1504
1928
|
from,
|
|
1505
1929
|
crypto.encrypt(from, response),
|