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
|
@@ -7,6 +7,7 @@ export const REMOTE_SESSION_SCOPES = Object.freeze([
|
|
|
7
7
|
"approve",
|
|
8
8
|
"interrupt",
|
|
9
9
|
]);
|
|
10
|
+
export const REMOTE_APPROVAL_BINDING_CAPABILITY = "approval-binding-v1";
|
|
10
11
|
|
|
11
12
|
const DEFAULT_TOKEN_TTL_MS = 5 * 60 * 1000;
|
|
12
13
|
const DEFAULT_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
|
|
@@ -36,6 +37,25 @@ function normalizeScopes(scopes) {
|
|
|
36
37
|
return unique;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
function normalizeCapabilities(capabilities) {
|
|
41
|
+
if (capabilities == null) return [];
|
|
42
|
+
if (!Array.isArray(capabilities) || capabilities.length > 32) {
|
|
43
|
+
throw new TypeError("Remote session capabilities must be a bounded array");
|
|
44
|
+
}
|
|
45
|
+
const normalized = [...new Set(capabilities)];
|
|
46
|
+
for (const capability of normalized) {
|
|
47
|
+
if (
|
|
48
|
+
typeof capability !== "string" ||
|
|
49
|
+
capability.length === 0 ||
|
|
50
|
+
capability.length > 128 ||
|
|
51
|
+
capability.includes("\0")
|
|
52
|
+
) {
|
|
53
|
+
throw new TypeError("Remote session capability is invalid");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return normalized.sort();
|
|
57
|
+
}
|
|
58
|
+
|
|
39
59
|
/**
|
|
40
60
|
* Org-level constraints an administrator can impose on Remote Sessions. Lives
|
|
41
61
|
* next to the registry (its single enforcement point) so both the direct WS
|
|
@@ -49,6 +69,7 @@ export class RemoteSessionPolicy {
|
|
|
49
69
|
maxSessionTtlMs = null,
|
|
50
70
|
maxTokenTtlMs = null,
|
|
51
71
|
allowRelayPairing = true,
|
|
72
|
+
policyVersion = null,
|
|
52
73
|
} = {}) {
|
|
53
74
|
this.allowedScopes = allowedScopes ? [...new Set(allowedScopes)] : null;
|
|
54
75
|
if (this.allowedScopes) {
|
|
@@ -68,6 +89,24 @@ export class RemoteSessionPolicy {
|
|
|
68
89
|
this.maxTokenTtlMs =
|
|
69
90
|
maxTokenTtlMs == null ? null : Math.max(1, Math.floor(maxTokenTtlMs));
|
|
70
91
|
this.allowRelayPairing = allowRelayPairing !== false;
|
|
92
|
+
const authorityMaterial = JSON.stringify({
|
|
93
|
+
allowedScopes: this.allowedScopes ? [...this.allowedScopes].sort() : null,
|
|
94
|
+
allowRelayPairing: this.allowRelayPairing,
|
|
95
|
+
maxDevices: this.maxDevices,
|
|
96
|
+
});
|
|
97
|
+
this.policyVersion =
|
|
98
|
+
policyVersion == null
|
|
99
|
+
? `remote-session-policy:sha256:${createHash("sha256")
|
|
100
|
+
.update(authorityMaterial, "utf8")
|
|
101
|
+
.digest("hex")}`
|
|
102
|
+
: String(policyVersion);
|
|
103
|
+
if (
|
|
104
|
+
this.policyVersion.length === 0 ||
|
|
105
|
+
this.policyVersion.length > 512 ||
|
|
106
|
+
this.policyVersion.includes("\0")
|
|
107
|
+
) {
|
|
108
|
+
throw new TypeError("policyVersion must be a non-empty bounded string");
|
|
109
|
+
}
|
|
71
110
|
}
|
|
72
111
|
|
|
73
112
|
/**
|
|
@@ -119,9 +158,22 @@ export class RemoteSessionPolicy {
|
|
|
119
158
|
maxSessionTtlMs: this.maxSessionTtlMs,
|
|
120
159
|
maxTokenTtlMs: this.maxTokenTtlMs,
|
|
121
160
|
allowRelayPairing: this.allowRelayPairing,
|
|
161
|
+
policyVersion: this.policyVersion,
|
|
122
162
|
};
|
|
123
163
|
}
|
|
124
164
|
|
|
165
|
+
authorityDescriptor() {
|
|
166
|
+
return Object.freeze({
|
|
167
|
+
allowedScopes:
|
|
168
|
+
this.allowedScopes === null
|
|
169
|
+
? null
|
|
170
|
+
: Object.freeze([...this.allowedScopes].sort()),
|
|
171
|
+
maxDevices: this.maxDevices,
|
|
172
|
+
allowRelayPairing: this.allowRelayPairing,
|
|
173
|
+
policyVersion: this.policyVersion,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
125
177
|
static fromEnv(env = {}) {
|
|
126
178
|
const options = {};
|
|
127
179
|
const scopes = env.CHAINLESSCHAIN_REMOTE_SESSION_ALLOWED_SCOPES;
|
|
@@ -163,7 +215,13 @@ function publicSession(session) {
|
|
|
163
215
|
sessionId: session.sessionId,
|
|
164
216
|
agentSessionId: session.agentSessionId,
|
|
165
217
|
name: session.name,
|
|
166
|
-
hostClientId: session.hostClientId,
|
|
218
|
+
hostClientId: session.hostPrincipalId || session.hostClientId,
|
|
219
|
+
...(session.hostPrincipalId
|
|
220
|
+
? {
|
|
221
|
+
hostPrincipalId: session.hostPrincipalId,
|
|
222
|
+
hostConnected: Boolean(session.hostClientId),
|
|
223
|
+
}
|
|
224
|
+
: {}),
|
|
167
225
|
createdAt: session.createdAt,
|
|
168
226
|
expiresAt: session.expiresAt,
|
|
169
227
|
memberCount: session.members.size,
|
|
@@ -176,6 +234,28 @@ function publicSession(session) {
|
|
|
176
234
|
};
|
|
177
235
|
}
|
|
178
236
|
|
|
237
|
+
function transportBindingKey(sessionId, transportClientId) {
|
|
238
|
+
return JSON.stringify([String(sessionId), String(transportClientId)]);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function publicMember(member, session) {
|
|
242
|
+
return {
|
|
243
|
+
clientId: member.principalId || member.clientId,
|
|
244
|
+
...(member.principalId ? { principalId: member.principalId } : {}),
|
|
245
|
+
scopes: [...member.scopes],
|
|
246
|
+
joinedAt: member.joinedAt,
|
|
247
|
+
pushToken: member.pushToken || null,
|
|
248
|
+
pushProvider: member.pushToken ? member.pushProvider || null : null,
|
|
249
|
+
...(member.membershipEpoch
|
|
250
|
+
? { membershipEpoch: member.membershipEpoch }
|
|
251
|
+
: {}),
|
|
252
|
+
...(member.membershipQuarantined ? { membershipQuarantined: true } : {}),
|
|
253
|
+
...(session?.hostPrincipalId
|
|
254
|
+
? { connected: Boolean(member.transportClientId) }
|
|
255
|
+
: {}),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
179
259
|
/** In-memory authorization state for one local CLI process. */
|
|
180
260
|
export class RemoteSessionRegistry {
|
|
181
261
|
constructor(options = {}) {
|
|
@@ -187,8 +267,201 @@ export class RemoteSessionRegistry {
|
|
|
187
267
|
// from touching the machine security store. Client-hosted approval bridges
|
|
188
268
|
// explicitly require this authority and trigger its lazy construction.
|
|
189
269
|
this._membershipAuthority = options.membershipAuthority ?? null;
|
|
270
|
+
this._membershipCoordinator = options.membershipCoordinator ?? null;
|
|
190
271
|
this.sessions = new Map();
|
|
191
272
|
this.tokens = new Map();
|
|
273
|
+
this._transportBindings = new Map();
|
|
274
|
+
this._pendingAuthentication = new Map();
|
|
275
|
+
this._coordinatorHydrated = false;
|
|
276
|
+
this._coordinatorHydrationError = null;
|
|
277
|
+
if (
|
|
278
|
+
this._membershipCoordinator &&
|
|
279
|
+
typeof this._membershipCoordinator !== "function"
|
|
280
|
+
) {
|
|
281
|
+
try {
|
|
282
|
+
this._hydrateCoordinatorSessions(this._membershipCoordinator);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
// Construction remains side-effect safe/retryable. Every durable use
|
|
285
|
+
// retries the authoritative read and still fails closed if it remains
|
|
286
|
+
// unavailable.
|
|
287
|
+
this._coordinatorHydrated = false;
|
|
288
|
+
this._coordinatorHydrationError = error;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
_resolveMembershipCoordinator() {
|
|
294
|
+
if (typeof this._membershipCoordinator === "function") {
|
|
295
|
+
this._membershipCoordinator = this._membershipCoordinator();
|
|
296
|
+
}
|
|
297
|
+
if (
|
|
298
|
+
!this._membershipCoordinator ||
|
|
299
|
+
typeof this._membershipCoordinator.listSessionSnapshots !== "function" ||
|
|
300
|
+
typeof this._membershipCoordinator.getSessionSnapshot !== "function" ||
|
|
301
|
+
typeof this._membershipCoordinator.trustDescriptor !== "function"
|
|
302
|
+
) {
|
|
303
|
+
throw new Error("Durable Remote membership coordinator is unavailable");
|
|
304
|
+
}
|
|
305
|
+
return this._membershipCoordinator;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
_requireMembershipCoordinator() {
|
|
309
|
+
const coordinator = this._resolveMembershipCoordinator();
|
|
310
|
+
if (!this._coordinatorHydrated) {
|
|
311
|
+
this._hydrateCoordinatorSessions(coordinator);
|
|
312
|
+
}
|
|
313
|
+
return coordinator;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
_hydrateCoordinatorSessions(coordinator) {
|
|
317
|
+
try {
|
|
318
|
+
const snapshots = coordinator.listSessionSnapshots({ activeOnly: false });
|
|
319
|
+
const live = new Set();
|
|
320
|
+
for (const snapshot of snapshots) {
|
|
321
|
+
if (snapshot.expiresAt <= this.now() || snapshot.status !== "active") {
|
|
322
|
+
this._clearCoordinatorSession(snapshot.sessionId);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
live.add(snapshot.sessionId);
|
|
326
|
+
this._syncCoordinatorSnapshot(snapshot, coordinator);
|
|
327
|
+
}
|
|
328
|
+
for (const [sessionId, session] of this.sessions) {
|
|
329
|
+
if (session.hostPrincipalId && !live.has(sessionId)) {
|
|
330
|
+
this._clearCoordinatorSession(sessionId);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
this._coordinatorHydrated = true;
|
|
334
|
+
this._coordinatorHydrationError = null;
|
|
335
|
+
} catch (error) {
|
|
336
|
+
this._coordinatorHydrated = false;
|
|
337
|
+
this._coordinatorHydrationError = error;
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
_clearCoordinatorSession(sessionId) {
|
|
343
|
+
for (const [key, binding] of this._transportBindings) {
|
|
344
|
+
if (binding.sessionId === sessionId) this._transportBindings.delete(key);
|
|
345
|
+
}
|
|
346
|
+
for (const [challengeId, pending] of this._pendingAuthentication) {
|
|
347
|
+
if (pending.sessionId === sessionId) {
|
|
348
|
+
this._pendingAuthentication.delete(challengeId);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
this.sessions.delete(sessionId);
|
|
352
|
+
this.tokens.delete(sessionId);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
_refreshCoordinatorSession(sessionId) {
|
|
356
|
+
const coordinator = this._requireMembershipCoordinator();
|
|
357
|
+
const current = coordinator.getSessionSnapshot(sessionId);
|
|
358
|
+
if (
|
|
359
|
+
!current ||
|
|
360
|
+
current.session.status !== "active" ||
|
|
361
|
+
current.session.expiresAt <= this.now()
|
|
362
|
+
) {
|
|
363
|
+
this._clearCoordinatorSession(sessionId);
|
|
364
|
+
return current?.session || null;
|
|
365
|
+
}
|
|
366
|
+
return this._syncCoordinatorSnapshot(current.session, coordinator);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
_syncCoordinatorSnapshot(snapshot, coordinator = null) {
|
|
370
|
+
const authority = coordinator || this._resolveMembershipCoordinator();
|
|
371
|
+
const previous = this.sessions.get(snapshot.sessionId) || null;
|
|
372
|
+
const priorMembers = previous?.members || new Map();
|
|
373
|
+
for (const [key, binding] of this._transportBindings) {
|
|
374
|
+
if (binding.sessionId === snapshot.sessionId) {
|
|
375
|
+
this._transportBindings.delete(key);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const members = new Map();
|
|
379
|
+
for (const durableMember of snapshot.members) {
|
|
380
|
+
const prior = priorMembers.get(durableMember.principalId) || null;
|
|
381
|
+
const sameActiveMembership =
|
|
382
|
+
prior?.status === "active" &&
|
|
383
|
+
durableMember.status === "active" &&
|
|
384
|
+
prior.membershipEpoch === durableMember.membershipEpoch;
|
|
385
|
+
const member = {
|
|
386
|
+
principalId: durableMember.principalId,
|
|
387
|
+
clientId: durableMember.principalId,
|
|
388
|
+
scopes: [...durableMember.scopes],
|
|
389
|
+
joinedAt: prior?.joinedAt || snapshot.createdAt,
|
|
390
|
+
pushToken: sameActiveMembership ? prior.pushToken || null : null,
|
|
391
|
+
pushProvider:
|
|
392
|
+
sameActiveMembership && prior.pushToken
|
|
393
|
+
? prior.pushProvider || null
|
|
394
|
+
: null,
|
|
395
|
+
membershipEpoch: durableMember.membershipEpoch,
|
|
396
|
+
status: durableMember.status,
|
|
397
|
+
transportClientId:
|
|
398
|
+
durableMember.status === "active"
|
|
399
|
+
? sameActiveMembership
|
|
400
|
+
? prior.transportClientId || null
|
|
401
|
+
: null
|
|
402
|
+
: null,
|
|
403
|
+
};
|
|
404
|
+
members.set(member.principalId, member);
|
|
405
|
+
if (member.transportClientId) {
|
|
406
|
+
this._transportBindings.set(
|
|
407
|
+
transportBindingKey(snapshot.sessionId, member.transportClientId),
|
|
408
|
+
{ sessionId: snapshot.sessionId, principalId: member.principalId },
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const host = members.get(snapshot.hostPrincipalId) || null;
|
|
413
|
+
const session = {
|
|
414
|
+
sessionId: snapshot.sessionId,
|
|
415
|
+
agentSessionId: snapshot.agentSessionId,
|
|
416
|
+
name: previous?.name || snapshot.agentSessionId,
|
|
417
|
+
hostPrincipalId: snapshot.hostPrincipalId,
|
|
418
|
+
hostClientId: host?.transportClientId || null,
|
|
419
|
+
createdAt: snapshot.createdAt,
|
|
420
|
+
expiresAt: snapshot.expiresAt,
|
|
421
|
+
status: snapshot.status,
|
|
422
|
+
members,
|
|
423
|
+
membershipAuthority: authority.trustDescriptor().authorityVersion,
|
|
424
|
+
sessionEpoch: snapshot.sessionEpoch,
|
|
425
|
+
authorityGeneration: snapshot.authorityGeneration,
|
|
426
|
+
leases: new Map(snapshot.leases.map((lease) => [lease.leaseId, lease])),
|
|
427
|
+
};
|
|
428
|
+
this.sessions.set(session.sessionId, session);
|
|
429
|
+
return session;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
_attachTransport(session, principalId, transportClientId) {
|
|
433
|
+
const member = session.members.get(principalId);
|
|
434
|
+
if (!member || member.status !== "active") {
|
|
435
|
+
throw new Error("Remote membership principal is not active");
|
|
436
|
+
}
|
|
437
|
+
const occupied = this._transportBindings.get(
|
|
438
|
+
transportBindingKey(session.sessionId, transportClientId),
|
|
439
|
+
);
|
|
440
|
+
if (occupied && occupied.principalId !== principalId) {
|
|
441
|
+
throw new Error("Remote transport is already bound to another principal");
|
|
442
|
+
}
|
|
443
|
+
if (member.transportClientId) {
|
|
444
|
+
this._transportBindings.delete(
|
|
445
|
+
transportBindingKey(session.sessionId, member.transportClientId),
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
member.transportClientId = transportClientId;
|
|
449
|
+
this._transportBindings.set(
|
|
450
|
+
transportBindingKey(session.sessionId, transportClientId),
|
|
451
|
+
{ sessionId: session.sessionId, principalId },
|
|
452
|
+
);
|
|
453
|
+
if (principalId === session.hostPrincipalId) {
|
|
454
|
+
session.hostClientId = transportClientId;
|
|
455
|
+
}
|
|
456
|
+
return member;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
_principalForTransport(session, transportClientId) {
|
|
460
|
+
if (!session.hostPrincipalId) return transportClientId;
|
|
461
|
+
const binding = this._transportBindings.get(
|
|
462
|
+
transportBindingKey(session.sessionId, transportClientId),
|
|
463
|
+
);
|
|
464
|
+
return binding?.principalId || null;
|
|
192
465
|
}
|
|
193
466
|
|
|
194
467
|
_requireMembershipAuthority() {
|
|
@@ -209,21 +482,50 @@ export class RemoteSessionRegistry {
|
|
|
209
482
|
name,
|
|
210
483
|
scopes,
|
|
211
484
|
durableMembership = false,
|
|
485
|
+
hostCredentialPublicKeySpki = null,
|
|
212
486
|
} = {}) {
|
|
213
487
|
if (!hostClientId) throw new Error("hostClientId is required");
|
|
214
488
|
if (!agentSessionId) throw new Error("agentSessionId is required");
|
|
215
489
|
const now = this.now();
|
|
216
490
|
const expiresAt = now + this.policy.capSessionTtl(this.sessionTtlMs);
|
|
217
491
|
const sessionId = randomUUID();
|
|
218
|
-
const
|
|
219
|
-
|
|
492
|
+
const coordinator =
|
|
493
|
+
durableMembership && this._membershipCoordinator
|
|
494
|
+
? this._requireMembershipCoordinator()
|
|
495
|
+
: null;
|
|
496
|
+
const durable = coordinator
|
|
497
|
+
? coordinator.createSession({
|
|
220
498
|
sessionId,
|
|
221
499
|
agentSessionId,
|
|
222
|
-
hostPrincipalId: hostClientId,
|
|
223
500
|
scopes: REMOTE_SESSION_SCOPES,
|
|
224
501
|
expiresAt,
|
|
502
|
+
hostCredentialPublicKeySpki,
|
|
503
|
+
joinPolicy: this.policy.authorityDescriptor(),
|
|
225
504
|
})
|
|
226
|
-
:
|
|
505
|
+
: durableMembership
|
|
506
|
+
? this._requireMembershipAuthority().createSession({
|
|
507
|
+
sessionId,
|
|
508
|
+
agentSessionId,
|
|
509
|
+
hostPrincipalId: hostClientId,
|
|
510
|
+
scopes: REMOTE_SESSION_SCOPES,
|
|
511
|
+
expiresAt,
|
|
512
|
+
})
|
|
513
|
+
: null;
|
|
514
|
+
if (coordinator) {
|
|
515
|
+
const session = this._syncCoordinatorSnapshot(durable.statement.payload);
|
|
516
|
+
session.name = name || agentSessionId;
|
|
517
|
+
return {
|
|
518
|
+
session: publicSession(session),
|
|
519
|
+
pairing: this.issuePairingToken(session.sessionId, { scopes }),
|
|
520
|
+
bootstrap: Object.freeze({
|
|
521
|
+
trust: durable.trust,
|
|
522
|
+
statement: durable.statement,
|
|
523
|
+
hostPrincipalId: durable.hostPrincipalId,
|
|
524
|
+
membershipEpoch: durable.membershipEpoch,
|
|
525
|
+
hostCredentialPublicKeySpki: durable.hostCredentialPublicKeySpki,
|
|
526
|
+
}),
|
|
527
|
+
};
|
|
528
|
+
}
|
|
227
529
|
const session = {
|
|
228
530
|
sessionId,
|
|
229
531
|
agentSessionId,
|
|
@@ -256,6 +558,312 @@ export class RemoteSessionRegistry {
|
|
|
256
558
|
};
|
|
257
559
|
}
|
|
258
560
|
|
|
561
|
+
pairingInvitation(sessionId, token) {
|
|
562
|
+
const session = this.requireSession(sessionId);
|
|
563
|
+
const pairing = this.tokens.get(sessionId);
|
|
564
|
+
if (!pairing || pairing.expiresAt <= this.now()) {
|
|
565
|
+
this.tokens.delete(sessionId);
|
|
566
|
+
throw new Error("Pairing token is missing or expired");
|
|
567
|
+
}
|
|
568
|
+
if (!tokensMatch(token, pairing.digest)) {
|
|
569
|
+
throw new Error("Invalid pairing token");
|
|
570
|
+
}
|
|
571
|
+
return Object.freeze({
|
|
572
|
+
session,
|
|
573
|
+
scopes: Object.freeze([...pairing.scopes]),
|
|
574
|
+
expiresAt: pairing.expiresAt,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
issueMemberJoinChallenge({
|
|
579
|
+
sessionId,
|
|
580
|
+
transportClientId,
|
|
581
|
+
token,
|
|
582
|
+
credentialPublicKey,
|
|
583
|
+
connectionNonce,
|
|
584
|
+
capabilities = null,
|
|
585
|
+
} = {}) {
|
|
586
|
+
const invitation = this.pairingInvitation(sessionId, token);
|
|
587
|
+
const session = invitation.session;
|
|
588
|
+
if (!session.hostPrincipalId) {
|
|
589
|
+
throw new Error("Durable direct membership challenge is unavailable");
|
|
590
|
+
}
|
|
591
|
+
const deviceCount = [...session.members.values()].filter(
|
|
592
|
+
(member) =>
|
|
593
|
+
member.principalId !== session.hostPrincipalId &&
|
|
594
|
+
member.status === "active",
|
|
595
|
+
).length;
|
|
596
|
+
this.policy.enforceJoin({ deviceCount, via: "direct" });
|
|
597
|
+
const challenge =
|
|
598
|
+
this._requireMembershipCoordinator().issueMemberJoinChallenge({
|
|
599
|
+
sessionId,
|
|
600
|
+
expectedSessionEpoch: session.sessionEpoch,
|
|
601
|
+
scopes: invitation.scopes,
|
|
602
|
+
credentialPublicKey,
|
|
603
|
+
connectionNonce,
|
|
604
|
+
ttlMs: Math.max(1, invitation.expiresAt - this.now()),
|
|
605
|
+
capabilities,
|
|
606
|
+
joinPolicy: this.policy.authorityDescriptor(),
|
|
607
|
+
});
|
|
608
|
+
this._pendingAuthentication.set(challenge.challengeId, {
|
|
609
|
+
purpose: "member.join",
|
|
610
|
+
sessionId,
|
|
611
|
+
transportClientId,
|
|
612
|
+
pairingDigest: Buffer.from(this.tokens.get(sessionId).digest),
|
|
613
|
+
pairingExpiresAt: invitation.expiresAt,
|
|
614
|
+
});
|
|
615
|
+
return challenge;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
completeMemberJoin({
|
|
619
|
+
challengeId,
|
|
620
|
+
transportClientId,
|
|
621
|
+
connectionNonce,
|
|
622
|
+
signature,
|
|
623
|
+
pushToken = null,
|
|
624
|
+
pushProvider = null,
|
|
625
|
+
} = {}) {
|
|
626
|
+
const pending = this._pendingAuthentication.get(challengeId);
|
|
627
|
+
this._pendingAuthentication.delete(challengeId);
|
|
628
|
+
if (
|
|
629
|
+
!pending ||
|
|
630
|
+
pending.purpose !== "member.join" ||
|
|
631
|
+
pending.transportClientId !== transportClientId ||
|
|
632
|
+
pending.pairingExpiresAt <= this.now()
|
|
633
|
+
) {
|
|
634
|
+
throw new Error("Remote membership join proof is stale");
|
|
635
|
+
}
|
|
636
|
+
const currentPairing = this.tokens.get(pending.sessionId);
|
|
637
|
+
if (
|
|
638
|
+
!currentPairing ||
|
|
639
|
+
currentPairing.digest.length !== pending.pairingDigest.length ||
|
|
640
|
+
!timingSafeEqual(currentPairing.digest, pending.pairingDigest)
|
|
641
|
+
) {
|
|
642
|
+
throw new Error("Remote membership pairing invitation changed");
|
|
643
|
+
}
|
|
644
|
+
const joined = this._requireMembershipCoordinator().joinMember({
|
|
645
|
+
challengeId,
|
|
646
|
+
connectionNonce,
|
|
647
|
+
signature,
|
|
648
|
+
});
|
|
649
|
+
this.tokens.delete(pending.sessionId);
|
|
650
|
+
const session = this._syncCoordinatorSnapshot(joined.statement.payload);
|
|
651
|
+
const member = this._attachTransport(
|
|
652
|
+
session,
|
|
653
|
+
joined.principalId,
|
|
654
|
+
transportClientId,
|
|
655
|
+
);
|
|
656
|
+
member.pushToken = pushToken || null;
|
|
657
|
+
member.pushProvider = pushToken ? pushProvider || null : null;
|
|
658
|
+
return {
|
|
659
|
+
session: publicSession(session),
|
|
660
|
+
member: publicMember(member, session),
|
|
661
|
+
nextConnectionNonce: joined.nextConnectionNonce,
|
|
662
|
+
statement: joined.statement,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
issueSessionResumeChallenge({
|
|
667
|
+
sessionId,
|
|
668
|
+
principalId,
|
|
669
|
+
transportClientId,
|
|
670
|
+
connectionNonce,
|
|
671
|
+
} = {}) {
|
|
672
|
+
const challenge =
|
|
673
|
+
this._requireMembershipCoordinator().issueSessionResumeChallenge({
|
|
674
|
+
sessionId,
|
|
675
|
+
principalId,
|
|
676
|
+
connectionNonce,
|
|
677
|
+
});
|
|
678
|
+
this._pendingAuthentication.set(challenge.challengeId, {
|
|
679
|
+
purpose: "session.resume",
|
|
680
|
+
sessionId,
|
|
681
|
+
principalId,
|
|
682
|
+
transportClientId,
|
|
683
|
+
});
|
|
684
|
+
return challenge;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
completeSessionResume({
|
|
688
|
+
challengeId,
|
|
689
|
+
transportClientId,
|
|
690
|
+
connectionNonce,
|
|
691
|
+
signature,
|
|
692
|
+
} = {}) {
|
|
693
|
+
const pending = this._pendingAuthentication.get(challengeId);
|
|
694
|
+
this._pendingAuthentication.delete(challengeId);
|
|
695
|
+
if (
|
|
696
|
+
!pending ||
|
|
697
|
+
pending.purpose !== "session.resume" ||
|
|
698
|
+
pending.transportClientId !== transportClientId
|
|
699
|
+
) {
|
|
700
|
+
throw new Error("Remote membership resume proof is stale");
|
|
701
|
+
}
|
|
702
|
+
const resumed = this._requireMembershipCoordinator().resumeSession({
|
|
703
|
+
challengeId,
|
|
704
|
+
connectionNonce,
|
|
705
|
+
signature,
|
|
706
|
+
});
|
|
707
|
+
if (
|
|
708
|
+
resumed.session.sessionId !== pending.sessionId ||
|
|
709
|
+
resumed.principalId !== pending.principalId
|
|
710
|
+
) {
|
|
711
|
+
throw new Error("Remote membership resume binding changed");
|
|
712
|
+
}
|
|
713
|
+
const session = this._syncCoordinatorSnapshot(resumed.session);
|
|
714
|
+
const member = this._attachTransport(
|
|
715
|
+
session,
|
|
716
|
+
resumed.principalId,
|
|
717
|
+
transportClientId,
|
|
718
|
+
);
|
|
719
|
+
return {
|
|
720
|
+
session: publicSession(session),
|
|
721
|
+
member: publicMember(member, session),
|
|
722
|
+
nextConnectionNonce: resumed.nextConnectionNonce,
|
|
723
|
+
statement: resumed.statement,
|
|
724
|
+
trust: resumed.trust,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
issueSessionReenableChallenge({
|
|
729
|
+
sessionId,
|
|
730
|
+
principalId,
|
|
731
|
+
transportClientId,
|
|
732
|
+
connectionNonce,
|
|
733
|
+
newHostCredentialPublicKeySpki = null,
|
|
734
|
+
} = {}) {
|
|
735
|
+
const coordinator = this._requireMembershipCoordinator();
|
|
736
|
+
const challenge = coordinator.issueSessionReenableChallenge({
|
|
737
|
+
sessionId,
|
|
738
|
+
principalId,
|
|
739
|
+
connectionNonce,
|
|
740
|
+
newHostCredentialPublicKeySpki,
|
|
741
|
+
scopes: REMOTE_SESSION_SCOPES,
|
|
742
|
+
expiresAt: this.now() + this.policy.capSessionTtl(this.sessionTtlMs),
|
|
743
|
+
joinPolicy: this.policy.authorityDescriptor(),
|
|
744
|
+
});
|
|
745
|
+
this._pendingAuthentication.set(challenge.challengeId, {
|
|
746
|
+
purpose: "session.reenable",
|
|
747
|
+
sessionId,
|
|
748
|
+
principalId,
|
|
749
|
+
nextPrincipalId: challenge.nextPrincipalId,
|
|
750
|
+
transportClientId,
|
|
751
|
+
});
|
|
752
|
+
return challenge;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
completeSessionReenable({
|
|
756
|
+
challengeId,
|
|
757
|
+
transportClientId,
|
|
758
|
+
connectionNonce,
|
|
759
|
+
signature,
|
|
760
|
+
} = {}) {
|
|
761
|
+
const pending = this._pendingAuthentication.get(challengeId);
|
|
762
|
+
this._pendingAuthentication.delete(challengeId);
|
|
763
|
+
if (
|
|
764
|
+
!pending ||
|
|
765
|
+
pending.purpose !== "session.reenable" ||
|
|
766
|
+
pending.transportClientId !== transportClientId
|
|
767
|
+
) {
|
|
768
|
+
throw new Error("Remote membership re-enable proof is stale");
|
|
769
|
+
}
|
|
770
|
+
const reenabled = this._requireMembershipCoordinator().reenableSession({
|
|
771
|
+
challengeId,
|
|
772
|
+
connectionNonce,
|
|
773
|
+
signature,
|
|
774
|
+
});
|
|
775
|
+
if (
|
|
776
|
+
reenabled.sessionId !== pending.sessionId ||
|
|
777
|
+
reenabled.hostPrincipalId !== pending.nextPrincipalId
|
|
778
|
+
) {
|
|
779
|
+
throw new Error("Remote membership re-enable binding changed");
|
|
780
|
+
}
|
|
781
|
+
const session = this._syncCoordinatorSnapshot(reenabled.statement.payload);
|
|
782
|
+
const member = this._attachTransport(
|
|
783
|
+
session,
|
|
784
|
+
reenabled.hostPrincipalId,
|
|
785
|
+
transportClientId,
|
|
786
|
+
);
|
|
787
|
+
return {
|
|
788
|
+
session: publicSession(session),
|
|
789
|
+
member: publicMember(member, session),
|
|
790
|
+
nextConnectionNonce: reenabled.nextConnectionNonce,
|
|
791
|
+
statement: reenabled.statement,
|
|
792
|
+
trust: reenabled.trust,
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
terminalSnapshot(sessionId) {
|
|
797
|
+
const current =
|
|
798
|
+
this._requireMembershipCoordinator().getSessionSnapshot(sessionId);
|
|
799
|
+
if (!current || current.session.status !== "closed") {
|
|
800
|
+
throw new Error("Remote membership session has no terminal tombstone");
|
|
801
|
+
}
|
|
802
|
+
this._clearCoordinatorSession(sessionId);
|
|
803
|
+
return current;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
joinRelayMember({
|
|
807
|
+
sessionId,
|
|
808
|
+
transportClientId,
|
|
809
|
+
token,
|
|
810
|
+
mobilePublicKey,
|
|
811
|
+
pairingTokenDigest,
|
|
812
|
+
possessionCapability,
|
|
813
|
+
capabilities = null,
|
|
814
|
+
pushToken = null,
|
|
815
|
+
pushProvider = null,
|
|
816
|
+
} = {}) {
|
|
817
|
+
const invitation = this.pairingInvitation(sessionId, token);
|
|
818
|
+
const session = invitation.session;
|
|
819
|
+
if (!session.hostPrincipalId) {
|
|
820
|
+
throw new Error("Durable relay membership is unavailable");
|
|
821
|
+
}
|
|
822
|
+
const deviceCount = [...session.members.values()].filter(
|
|
823
|
+
(member) =>
|
|
824
|
+
member.principalId !== session.hostPrincipalId &&
|
|
825
|
+
member.status === "active",
|
|
826
|
+
).length;
|
|
827
|
+
this.policy.enforceJoin({ deviceCount, via: "relay" });
|
|
828
|
+
const negotiatedCapabilities = normalizeCapabilities(capabilities);
|
|
829
|
+
const negotiatedScopes = invitation.scopes.filter(
|
|
830
|
+
(scope) =>
|
|
831
|
+
scope !== "approve" ||
|
|
832
|
+
negotiatedCapabilities.includes(REMOTE_APPROVAL_BINDING_CAPABILITY),
|
|
833
|
+
);
|
|
834
|
+
if (negotiatedScopes.length === 0) {
|
|
835
|
+
throw new Error(
|
|
836
|
+
"Remote relay capabilities do not authorize any requested scope",
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
const joined = this._requireMembershipCoordinator().joinRelayMember({
|
|
840
|
+
sessionId,
|
|
841
|
+
expectedSessionEpoch: session.sessionEpoch,
|
|
842
|
+
scopes: negotiatedScopes,
|
|
843
|
+
mobilePeerId: transportClientId,
|
|
844
|
+
mobilePublicKey,
|
|
845
|
+
pairingTokenDigest,
|
|
846
|
+
possessionCapability,
|
|
847
|
+
capabilities: negotiatedCapabilities,
|
|
848
|
+
joinPolicy: this.policy.authorityDescriptor(),
|
|
849
|
+
});
|
|
850
|
+
this.tokens.delete(sessionId);
|
|
851
|
+
const updated = this._syncCoordinatorSnapshot(joined.statement.payload);
|
|
852
|
+
const member = this._attachTransport(
|
|
853
|
+
updated,
|
|
854
|
+
joined.principalId,
|
|
855
|
+
transportClientId,
|
|
856
|
+
);
|
|
857
|
+
member.pushToken = pushToken || null;
|
|
858
|
+
member.pushProvider = pushToken ? pushProvider || null : null;
|
|
859
|
+
return {
|
|
860
|
+
session: publicSession(updated),
|
|
861
|
+
member: publicMember(member, updated),
|
|
862
|
+
statement: joined.statement,
|
|
863
|
+
capabilities: Object.freeze([...negotiatedCapabilities]),
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
259
867
|
issuePairingToken(sessionId, { scopes } = {}) {
|
|
260
868
|
const session = this.requireSession(sessionId);
|
|
261
869
|
const token = randomBytes(32).toString("base64url");
|
|
@@ -289,6 +897,11 @@ export class RemoteSessionRegistry {
|
|
|
289
897
|
} = {}) {
|
|
290
898
|
if (!clientId) throw new Error("clientId is required");
|
|
291
899
|
const session = this.requireSession(sessionId);
|
|
900
|
+
if (session.hostPrincipalId) {
|
|
901
|
+
throw new Error(
|
|
902
|
+
"Durable membership requires a possession-proof join protocol",
|
|
903
|
+
);
|
|
904
|
+
}
|
|
292
905
|
const pairing = this.tokens.get(sessionId);
|
|
293
906
|
if (!pairing || pairing.expiresAt <= this.now()) {
|
|
294
907
|
this.tokens.delete(sessionId);
|
|
@@ -338,8 +951,9 @@ export class RemoteSessionRegistry {
|
|
|
338
951
|
*/
|
|
339
952
|
registerPush(sessionId, clientId, { token = null, provider = null } = {}) {
|
|
340
953
|
const session = this.requireSession(sessionId);
|
|
341
|
-
const
|
|
342
|
-
|
|
954
|
+
const principalId = this._principalForTransport(session, clientId);
|
|
955
|
+
const member = session.members.get(principalId || clientId);
|
|
956
|
+
if (!member || (member.status && member.status !== "active")) {
|
|
343
957
|
throw new Error("Device is not paired with this remote session");
|
|
344
958
|
}
|
|
345
959
|
member.pushToken = token || null;
|
|
@@ -356,11 +970,25 @@ export class RemoteSessionRegistry {
|
|
|
356
970
|
const session = this.requireSession(sessionId);
|
|
357
971
|
const targets = [];
|
|
358
972
|
for (const member of session.members.values()) {
|
|
359
|
-
if (member.
|
|
360
|
-
if (
|
|
973
|
+
if (member.status && member.status !== "active") continue;
|
|
974
|
+
if (
|
|
975
|
+
(session.hostPrincipalId &&
|
|
976
|
+
member.principalId === session.hostPrincipalId) ||
|
|
977
|
+
(!session.hostPrincipalId && member.clientId === session.hostClientId)
|
|
978
|
+
)
|
|
979
|
+
continue;
|
|
980
|
+
if (
|
|
981
|
+
excludeClientId &&
|
|
982
|
+
(member.clientId === excludeClientId ||
|
|
983
|
+
member.transportClientId === excludeClientId)
|
|
984
|
+
)
|
|
985
|
+
continue;
|
|
361
986
|
if (!member.pushToken) continue;
|
|
362
987
|
targets.push({
|
|
363
988
|
clientId: member.clientId,
|
|
989
|
+
...(session.hostPrincipalId
|
|
990
|
+
? { transportClientId: member.transportClientId || null }
|
|
991
|
+
: {}),
|
|
364
992
|
pushToken: member.pushToken,
|
|
365
993
|
pushProvider: member.pushProvider,
|
|
366
994
|
});
|
|
@@ -375,9 +1003,13 @@ export class RemoteSessionRegistry {
|
|
|
375
1003
|
"Remote membership session is quarantined after an unknown durable transition",
|
|
376
1004
|
);
|
|
377
1005
|
}
|
|
378
|
-
const
|
|
1006
|
+
const principalId = this._principalForTransport(session, clientId);
|
|
1007
|
+
const member = session.members.get(principalId || clientId);
|
|
379
1008
|
if (!member)
|
|
380
1009
|
throw new Error("Client is not paired with this remote session");
|
|
1010
|
+
if (member.status && member.status !== "active") {
|
|
1011
|
+
throw new Error("Remote membership principal is not active");
|
|
1012
|
+
}
|
|
381
1013
|
if (member.membershipQuarantined) {
|
|
382
1014
|
throw new Error(
|
|
383
1015
|
"Remote membership is quarantined after an unknown durable transition",
|
|
@@ -387,7 +1019,23 @@ export class RemoteSessionRegistry {
|
|
|
387
1019
|
throw new Error(`Remote session scope required: ${scope}`);
|
|
388
1020
|
}
|
|
389
1021
|
let membership = null;
|
|
390
|
-
if (session.
|
|
1022
|
+
if (session.hostPrincipalId) {
|
|
1023
|
+
const verdict = this._requireMembershipCoordinator().readMembership(
|
|
1024
|
+
{
|
|
1025
|
+
sessionId,
|
|
1026
|
+
principalId: member.principalId,
|
|
1027
|
+
sessionEpoch: session.sessionEpoch,
|
|
1028
|
+
membershipEpoch: member.membershipEpoch,
|
|
1029
|
+
},
|
|
1030
|
+
scope,
|
|
1031
|
+
);
|
|
1032
|
+
if (!verdict.ok) {
|
|
1033
|
+
throw new Error(
|
|
1034
|
+
`Remote membership coordinator denied: ${verdict.reason}`,
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
membership = verdict.binding;
|
|
1038
|
+
} else if (session.membershipAuthority) {
|
|
391
1039
|
const verdict = this._requireMembershipAuthority().readMembership(
|
|
392
1040
|
{
|
|
393
1041
|
sessionId,
|
|
@@ -409,7 +1057,12 @@ export class RemoteSessionRegistry {
|
|
|
409
1057
|
|
|
410
1058
|
members(sessionId) {
|
|
411
1059
|
const session = this.requireSession(sessionId);
|
|
412
|
-
return [...session.members.values()]
|
|
1060
|
+
return [...session.members.values()]
|
|
1061
|
+
.filter((member) => !member.status || member.status === "active")
|
|
1062
|
+
.map((member) => ({
|
|
1063
|
+
...publicMember(member, session),
|
|
1064
|
+
transportClientId: member.transportClientId || member.clientId,
|
|
1065
|
+
}));
|
|
413
1066
|
}
|
|
414
1067
|
|
|
415
1068
|
/**
|
|
@@ -418,18 +1071,33 @@ export class RemoteSessionRegistry {
|
|
|
418
1071
|
*/
|
|
419
1072
|
listDevices(sessionId, hostClientId) {
|
|
420
1073
|
const session = this.requireSession(sessionId);
|
|
421
|
-
|
|
1074
|
+
const hostPrincipal = hostClientId
|
|
1075
|
+
? this._principalForTransport(session, hostClientId)
|
|
1076
|
+
: null;
|
|
1077
|
+
if (
|
|
1078
|
+
hostClientId &&
|
|
1079
|
+
(session.hostPrincipalId
|
|
1080
|
+
? hostPrincipal !== session.hostPrincipalId
|
|
1081
|
+
: session.hostClientId !== hostClientId)
|
|
1082
|
+
) {
|
|
422
1083
|
throw new Error("Only the host can list paired devices");
|
|
423
1084
|
}
|
|
424
1085
|
return {
|
|
425
1086
|
session: publicSession(session),
|
|
426
1087
|
devices: [...session.members.values()].map((member) => ({
|
|
427
|
-
clientId: member.clientId,
|
|
1088
|
+
clientId: member.principalId || member.clientId,
|
|
1089
|
+
...(member.principalId ? { principalId: member.principalId } : {}),
|
|
428
1090
|
scopes: [...member.scopes],
|
|
429
1091
|
joinedAt: member.joinedAt,
|
|
430
|
-
isHost:
|
|
1092
|
+
isHost: session.hostPrincipalId
|
|
1093
|
+
? member.principalId === session.hostPrincipalId
|
|
1094
|
+
: member.clientId === session.hostClientId,
|
|
1095
|
+
...(session.hostPrincipalId
|
|
1096
|
+
? { connected: Boolean(member.transportClientId) }
|
|
1097
|
+
: {}),
|
|
431
1098
|
hasPush: Boolean(member.pushToken),
|
|
432
1099
|
pushProvider: member.pushProvider || null,
|
|
1100
|
+
...(member.status ? { status: member.status } : {}),
|
|
433
1101
|
})),
|
|
434
1102
|
};
|
|
435
1103
|
}
|
|
@@ -442,16 +1110,45 @@ export class RemoteSessionRegistry {
|
|
|
442
1110
|
revokeMember(sessionId, hostClientId, clientId) {
|
|
443
1111
|
if (!clientId) throw new Error("clientId is required");
|
|
444
1112
|
const session = this.requireSession(sessionId);
|
|
445
|
-
|
|
1113
|
+
const hostPrincipal = this._principalForTransport(session, hostClientId);
|
|
1114
|
+
if (
|
|
1115
|
+
session.hostPrincipalId
|
|
1116
|
+
? hostPrincipal !== session.hostPrincipalId
|
|
1117
|
+
: session.hostClientId !== hostClientId
|
|
1118
|
+
) {
|
|
446
1119
|
throw new Error("Only the host can revoke devices");
|
|
447
1120
|
}
|
|
448
|
-
if (clientId === session.hostClientId) {
|
|
1121
|
+
if (clientId === (session.hostPrincipalId || session.hostClientId)) {
|
|
449
1122
|
throw new Error("Cannot revoke the host device");
|
|
450
1123
|
}
|
|
451
1124
|
const member = session.members.get(clientId);
|
|
452
1125
|
if (!member)
|
|
453
1126
|
throw new Error("Device is not paired with this remote session");
|
|
454
|
-
|
|
1127
|
+
const notificationTransportId = member.transportClientId || member.clientId;
|
|
1128
|
+
let resultSession = session;
|
|
1129
|
+
let resultMember = member;
|
|
1130
|
+
let membershipStatement = null;
|
|
1131
|
+
if (session.hostPrincipalId) {
|
|
1132
|
+
try {
|
|
1133
|
+
const host = session.members.get(session.hostPrincipalId);
|
|
1134
|
+
const revoked = this._requireMembershipCoordinator().revokeMember({
|
|
1135
|
+
sessionId,
|
|
1136
|
+
principalId: clientId,
|
|
1137
|
+
hostPrincipalId: session.hostPrincipalId,
|
|
1138
|
+
expectedSessionEpoch: session.sessionEpoch,
|
|
1139
|
+
expectedMembershipEpoch: member.membershipEpoch,
|
|
1140
|
+
expectedHostMembershipEpoch: host.membershipEpoch,
|
|
1141
|
+
});
|
|
1142
|
+
resultSession = this._syncCoordinatorSnapshot(
|
|
1143
|
+
revoked.statement.payload,
|
|
1144
|
+
);
|
|
1145
|
+
resultMember = resultSession.members.get(clientId);
|
|
1146
|
+
membershipStatement = revoked.statement;
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
member.membershipQuarantined = true;
|
|
1149
|
+
throw error;
|
|
1150
|
+
}
|
|
1151
|
+
} else if (session.membershipAuthority) {
|
|
455
1152
|
try {
|
|
456
1153
|
this._requireMembershipAuthority().revokeMember({
|
|
457
1154
|
sessionId,
|
|
@@ -466,13 +1163,33 @@ export class RemoteSessionRegistry {
|
|
|
466
1163
|
throw error;
|
|
467
1164
|
}
|
|
468
1165
|
}
|
|
469
|
-
session.members.delete(clientId);
|
|
470
|
-
return {
|
|
1166
|
+
if (!session.hostPrincipalId) session.members.delete(clientId);
|
|
1167
|
+
return {
|
|
1168
|
+
session: publicSession(resultSession),
|
|
1169
|
+
member: {
|
|
1170
|
+
...publicMember(resultMember, resultSession),
|
|
1171
|
+
transportClientId: notificationTransportId,
|
|
1172
|
+
},
|
|
1173
|
+
...(membershipStatement ? { statement: membershipStatement } : {}),
|
|
1174
|
+
};
|
|
471
1175
|
}
|
|
472
1176
|
|
|
473
1177
|
findHosted(agentSessionId, hostClientId) {
|
|
1178
|
+
if (this._membershipCoordinator) {
|
|
1179
|
+
// Enumeration cannot rely on the one-time startup hydration: another
|
|
1180
|
+
// server may have created, closed, or re-enabled a durable session since
|
|
1181
|
+
// this process started. Refresh the coordinator index before matching
|
|
1182
|
+
// local transport attachments.
|
|
1183
|
+
this._hydrateCoordinatorSessions(this._resolveMembershipCoordinator());
|
|
1184
|
+
}
|
|
474
1185
|
const matches = [];
|
|
475
|
-
for (const
|
|
1186
|
+
for (const sessionId of [...this.sessions.keys()]) {
|
|
1187
|
+
let session;
|
|
1188
|
+
try {
|
|
1189
|
+
session = this.requireSession(sessionId);
|
|
1190
|
+
} catch {
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
476
1193
|
if (
|
|
477
1194
|
session.agentSessionId === agentSessionId &&
|
|
478
1195
|
session.hostClientId === hostClientId &&
|
|
@@ -487,8 +1204,25 @@ export class RemoteSessionRegistry {
|
|
|
487
1204
|
removeClient(clientId) {
|
|
488
1205
|
const affected = [];
|
|
489
1206
|
for (const [sessionId, session] of this.sessions) {
|
|
490
|
-
const
|
|
1207
|
+
const principalId = this._principalForTransport(session, clientId);
|
|
1208
|
+
const member = session.members.get(principalId || clientId);
|
|
491
1209
|
if (!member) continue;
|
|
1210
|
+
if (session.hostPrincipalId) {
|
|
1211
|
+
this._transportBindings.delete(
|
|
1212
|
+
transportBindingKey(sessionId, clientId),
|
|
1213
|
+
);
|
|
1214
|
+
member.transportClientId = null;
|
|
1215
|
+
if (member.principalId === session.hostPrincipalId) {
|
|
1216
|
+
session.hostClientId = null;
|
|
1217
|
+
}
|
|
1218
|
+
affected.push({
|
|
1219
|
+
sessionId,
|
|
1220
|
+
closed: false,
|
|
1221
|
+
detached: true,
|
|
1222
|
+
principalId: member.principalId,
|
|
1223
|
+
});
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
492
1226
|
if (session.hostClientId === clientId) {
|
|
493
1227
|
if (session.membershipAuthority) {
|
|
494
1228
|
try {
|
|
@@ -528,10 +1262,29 @@ export class RemoteSessionRegistry {
|
|
|
528
1262
|
|
|
529
1263
|
close(sessionId, clientId) {
|
|
530
1264
|
const session = this.requireSession(sessionId);
|
|
531
|
-
|
|
1265
|
+
const hostPrincipal = this._principalForTransport(session, clientId);
|
|
1266
|
+
if (
|
|
1267
|
+
session.hostPrincipalId
|
|
1268
|
+
? hostPrincipal !== session.hostPrincipalId
|
|
1269
|
+
: session.hostClientId !== clientId
|
|
1270
|
+
) {
|
|
532
1271
|
throw new Error("Only the host can close a Remote Session");
|
|
533
1272
|
}
|
|
534
|
-
|
|
1273
|
+
let durableClose = null;
|
|
1274
|
+
if (session.hostPrincipalId) {
|
|
1275
|
+
try {
|
|
1276
|
+
const host = session.members.get(session.hostPrincipalId);
|
|
1277
|
+
durableClose = this._requireMembershipCoordinator().closeSession({
|
|
1278
|
+
sessionId,
|
|
1279
|
+
hostPrincipalId: session.hostPrincipalId,
|
|
1280
|
+
expectedSessionEpoch: session.sessionEpoch,
|
|
1281
|
+
expectedHostMembershipEpoch: host.membershipEpoch,
|
|
1282
|
+
});
|
|
1283
|
+
} catch (error) {
|
|
1284
|
+
session.membershipQuarantined = true;
|
|
1285
|
+
throw error;
|
|
1286
|
+
}
|
|
1287
|
+
} else if (session.membershipAuthority) {
|
|
535
1288
|
try {
|
|
536
1289
|
this._requireMembershipAuthority().closeSession({
|
|
537
1290
|
sessionId,
|
|
@@ -545,14 +1298,29 @@ export class RemoteSessionRegistry {
|
|
|
545
1298
|
}
|
|
546
1299
|
this.sessions.delete(sessionId);
|
|
547
1300
|
this.tokens.delete(sessionId);
|
|
548
|
-
|
|
1301
|
+
const closedSession = publicSession(session);
|
|
1302
|
+
return durableClose
|
|
1303
|
+
? {
|
|
1304
|
+
session: closedSession,
|
|
1305
|
+
statement: durableClose.statement,
|
|
1306
|
+
terminal: durableClose.terminal === true,
|
|
1307
|
+
alreadyClosed: durableClose.alreadyClosed === true,
|
|
1308
|
+
}
|
|
1309
|
+
: closedSession;
|
|
549
1310
|
}
|
|
550
1311
|
|
|
551
1312
|
requireSession(sessionId) {
|
|
552
|
-
|
|
1313
|
+
let session = this.sessions.get(sessionId) || null;
|
|
1314
|
+
if ((session?.hostPrincipalId || !session) && this._membershipCoordinator) {
|
|
1315
|
+
const durable = this._refreshCoordinatorSession(sessionId);
|
|
1316
|
+
if (durable?.status && durable.status !== "active") {
|
|
1317
|
+
throw new Error("Remote session is durably closed");
|
|
1318
|
+
}
|
|
1319
|
+
session = this.sessions.get(sessionId) || null;
|
|
1320
|
+
}
|
|
553
1321
|
if (!session) throw new Error("Remote session not found");
|
|
554
1322
|
if (session.expiresAt <= this.now()) {
|
|
555
|
-
if (session.membershipAuthority) {
|
|
1323
|
+
if (session.membershipAuthority && !session.hostPrincipalId) {
|
|
556
1324
|
try {
|
|
557
1325
|
this._requireMembershipAuthority().closeSession({
|
|
558
1326
|
sessionId,
|