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
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
* with a pinned Ed25519 key; WebSocket delivery is transport only and never an
|
|
9
9
|
* authority by itself.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* Production integration: the Remote Session registry and WebSocket protocol
|
|
12
|
+
* use this coordinator, the client-hosted approval bridge durably adopts and
|
|
13
|
+
* ACKs its signed leases, and Agent shell dispatch consumes the exact one-shot
|
|
14
|
+
* lease online immediately before the side effect. Side-effect paths without
|
|
15
|
+
* that consume boundary reject Remote approval instead of falling back to a
|
|
16
|
+
* cached decision. This provides cross-host revoke fencing under one online,
|
|
17
|
+
* strongly consistent coordinator; it does not claim quorum availability or
|
|
18
|
+
* undo an external effect after a successful consume has linearized dispatch.
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
21
|
import fs from "node:fs";
|
|
@@ -44,6 +46,8 @@ export const REMOTE_MEMBERSHIP_COORDINATOR_UNAVAILABLE_CODE =
|
|
|
44
46
|
"CC_REMOTE_MEMBERSHIP_COORDINATOR_UNAVAILABLE";
|
|
45
47
|
export const REMOTE_MEMBERSHIP_COORDINATOR_ROLLBACK_CODE =
|
|
46
48
|
"CC_REMOTE_MEMBERSHIP_COORDINATOR_ROLLBACK";
|
|
49
|
+
export const REMOTE_MEMBERSHIP_NOT_ACTIVE_CODE =
|
|
50
|
+
"CC_REMOTE_MEMBERSHIP_NOT_ACTIVE";
|
|
47
51
|
|
|
48
52
|
const STORE_VERSION = 1;
|
|
49
53
|
const KEY_VERSION = 1;
|
|
@@ -62,6 +66,7 @@ const NON_NEGATIVE_RE = /^(?:0|[1-9]\d*)$/;
|
|
|
62
66
|
const VALID_SCOPES = new Set(["observe", "prompt", "approve", "interrupt"]);
|
|
63
67
|
const VALID_TYPES = new Set([
|
|
64
68
|
"session.created",
|
|
69
|
+
"session.reenabled",
|
|
65
70
|
"member.joined",
|
|
66
71
|
"member.revoked",
|
|
67
72
|
"session.closed",
|
|
@@ -75,6 +80,19 @@ const MAX_LEASE_TTL_MS = 60_000;
|
|
|
75
80
|
const DEFAULT_CHALLENGE_TTL_MS = 30_000;
|
|
76
81
|
const MAX_CHALLENGE_TTL_MS = 60_000;
|
|
77
82
|
const BASE64URL_RE = /^[A-Za-z0-9_-]+$/;
|
|
83
|
+
export const REMOTE_APPROVAL_BINDING_CAPABILITY = "approval-binding-v1";
|
|
84
|
+
const DEFAULT_JOIN_POLICY_VERSION = "remote-session-policy:unrestricted-v1";
|
|
85
|
+
const JOIN_POLICY_EVENT_FIELDS = Object.freeze([
|
|
86
|
+
"policyAllowedScopes",
|
|
87
|
+
"policyAllowRelayPairing",
|
|
88
|
+
"policyMaxDevices",
|
|
89
|
+
"policyVersion",
|
|
90
|
+
]);
|
|
91
|
+
const MEMBER_JOIN_AUTHORITY_FIELDS = Object.freeze([
|
|
92
|
+
"capabilities",
|
|
93
|
+
"joinVia",
|
|
94
|
+
...JOIN_POLICY_EVENT_FIELDS,
|
|
95
|
+
]);
|
|
78
96
|
|
|
79
97
|
const COMMON_EVENT_FIELDS = Object.freeze([
|
|
80
98
|
"eventHash",
|
|
@@ -97,6 +115,22 @@ const EVENT_FIELDS = Object.freeze({
|
|
|
97
115
|
"hostCredentialType",
|
|
98
116
|
"hostPrincipalId",
|
|
99
117
|
"membershipEpoch",
|
|
118
|
+
...JOIN_POLICY_EVENT_FIELDS,
|
|
119
|
+
"scopes",
|
|
120
|
+
"sessionEpoch",
|
|
121
|
+
]),
|
|
122
|
+
"session.reenabled": Object.freeze([
|
|
123
|
+
...COMMON_EVENT_FIELDS,
|
|
124
|
+
"expectedHostMembershipEpoch",
|
|
125
|
+
"expectedSessionEpoch",
|
|
126
|
+
"expiresAt",
|
|
127
|
+
"hostPrincipalId",
|
|
128
|
+
"membershipEpoch",
|
|
129
|
+
"nextHostCredentialDigest",
|
|
130
|
+
"nextHostCredentialPublicKey",
|
|
131
|
+
"nextHostCredentialType",
|
|
132
|
+
"nextHostPrincipalId",
|
|
133
|
+
...JOIN_POLICY_EVENT_FIELDS,
|
|
100
134
|
"scopes",
|
|
101
135
|
"sessionEpoch",
|
|
102
136
|
]),
|
|
@@ -109,6 +143,7 @@ const EVENT_FIELDS = Object.freeze({
|
|
|
109
143
|
"membershipEpoch",
|
|
110
144
|
"previousMembershipEpoch",
|
|
111
145
|
"principalId",
|
|
146
|
+
...MEMBER_JOIN_AUTHORITY_FIELDS,
|
|
112
147
|
"scopes",
|
|
113
148
|
]),
|
|
114
149
|
"member.revoked": Object.freeze([
|
|
@@ -172,6 +207,19 @@ const EVENT_FIELDS = Object.freeze({
|
|
|
172
207
|
]),
|
|
173
208
|
});
|
|
174
209
|
|
|
210
|
+
const LEGACY_EVENT_FIELDS = Object.freeze({
|
|
211
|
+
"session.created": Object.freeze(
|
|
212
|
+
EVENT_FIELDS["session.created"].filter(
|
|
213
|
+
(field) => !JOIN_POLICY_EVENT_FIELDS.includes(field),
|
|
214
|
+
),
|
|
215
|
+
),
|
|
216
|
+
"member.joined": Object.freeze(
|
|
217
|
+
EVENT_FIELDS["member.joined"].filter(
|
|
218
|
+
(field) => !MEMBER_JOIN_AUTHORITY_FIELDS.includes(field),
|
|
219
|
+
),
|
|
220
|
+
),
|
|
221
|
+
});
|
|
222
|
+
|
|
175
223
|
function coordinatorError(code, message, cause = null, details = {}) {
|
|
176
224
|
const error = new Error(message, cause ? { cause } : undefined);
|
|
177
225
|
error.name = "RemoteMembershipCoordinatorError";
|
|
@@ -183,7 +231,8 @@ function coordinatorError(code, message, cause = null, details = {}) {
|
|
|
183
231
|
function unavailable(cause, filePath) {
|
|
184
232
|
if (
|
|
185
233
|
cause?.code === REMOTE_MEMBERSHIP_COORDINATOR_UNAVAILABLE_CODE ||
|
|
186
|
-
cause?.code === REMOTE_MEMBERSHIP_COORDINATOR_ROLLBACK_CODE
|
|
234
|
+
cause?.code === REMOTE_MEMBERSHIP_COORDINATOR_ROLLBACK_CODE ||
|
|
235
|
+
cause?.code === REMOTE_MEMBERSHIP_NOT_ACTIVE_CODE
|
|
187
236
|
) {
|
|
188
237
|
return cause;
|
|
189
238
|
}
|
|
@@ -244,6 +293,141 @@ function normalizeScopes(scopes) {
|
|
|
244
293
|
return normalized;
|
|
245
294
|
}
|
|
246
295
|
|
|
296
|
+
function normalizeCapabilities(capabilities) {
|
|
297
|
+
if (capabilities == null) return [];
|
|
298
|
+
if (!Array.isArray(capabilities) || capabilities.length > 32) {
|
|
299
|
+
throw new TypeError("membership capabilities must be a bounded array");
|
|
300
|
+
}
|
|
301
|
+
return [
|
|
302
|
+
...new Set(
|
|
303
|
+
capabilities.map((capability) => {
|
|
304
|
+
if (typeof capability !== "string") {
|
|
305
|
+
throw new TypeError("membership capability must be a string");
|
|
306
|
+
}
|
|
307
|
+
return requiredString(capability, "membership capability", 128);
|
|
308
|
+
}),
|
|
309
|
+
),
|
|
310
|
+
].sort();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function normalizeJoinPolicy(policy = null) {
|
|
314
|
+
const value = policy || {};
|
|
315
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
316
|
+
throw new TypeError("membership join policy must be an object");
|
|
317
|
+
}
|
|
318
|
+
const allowedScopes =
|
|
319
|
+
value.allowedScopes == null ? null : normalizeScopes(value.allowedScopes);
|
|
320
|
+
const maxDevices = value.maxDevices == null ? null : value.maxDevices;
|
|
321
|
+
if (
|
|
322
|
+
maxDevices !== null &&
|
|
323
|
+
(!Number.isSafeInteger(maxDevices) ||
|
|
324
|
+
maxDevices < 0 ||
|
|
325
|
+
maxDevices > 1_000_000)
|
|
326
|
+
) {
|
|
327
|
+
throw new TypeError(
|
|
328
|
+
"membership policy maxDevices must be a bounded integer",
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
if (
|
|
332
|
+
value.allowRelayPairing != null &&
|
|
333
|
+
typeof value.allowRelayPairing !== "boolean"
|
|
334
|
+
) {
|
|
335
|
+
throw new TypeError("membership policy allowRelayPairing must be boolean");
|
|
336
|
+
}
|
|
337
|
+
return Object.freeze({
|
|
338
|
+
policyVersion:
|
|
339
|
+
value.policyVersion == null
|
|
340
|
+
? DEFAULT_JOIN_POLICY_VERSION
|
|
341
|
+
: requiredString(value.policyVersion, "policyVersion", 512),
|
|
342
|
+
allowedScopes:
|
|
343
|
+
allowedScopes === null ? null : Object.freeze([...allowedScopes]),
|
|
344
|
+
maxDevices,
|
|
345
|
+
allowRelayPairing: value.allowRelayPairing !== false,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function joinPolicyEventFields(policy) {
|
|
350
|
+
const normalized = normalizeJoinPolicy(policy);
|
|
351
|
+
return {
|
|
352
|
+
policyVersion: normalized.policyVersion,
|
|
353
|
+
policyAllowedScopes:
|
|
354
|
+
normalized.allowedScopes === null ? null : [...normalized.allowedScopes],
|
|
355
|
+
policyMaxDevices: normalized.maxDevices,
|
|
356
|
+
policyAllowRelayPairing: normalized.allowRelayPairing,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function joinPolicyFromEvent(event, label) {
|
|
361
|
+
const present = JOIN_POLICY_EVENT_FIELDS.filter((field) =>
|
|
362
|
+
Object.prototype.hasOwnProperty.call(event, field),
|
|
363
|
+
);
|
|
364
|
+
if (present.length === 0) return null;
|
|
365
|
+
if (present.length !== JOIN_POLICY_EVENT_FIELDS.length) {
|
|
366
|
+
throw new TypeError(`${label} has an incomplete membership join policy`);
|
|
367
|
+
}
|
|
368
|
+
return normalizeJoinPolicy({
|
|
369
|
+
policyVersion: event.policyVersion,
|
|
370
|
+
allowedScopes: event.policyAllowedScopes,
|
|
371
|
+
maxDevices: event.policyMaxDevices,
|
|
372
|
+
allowRelayPairing: event.policyAllowRelayPairing,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function joinPoliciesMatch(left, right) {
|
|
377
|
+
return (
|
|
378
|
+
canonicalJson(normalizeJoinPolicy(left)) ===
|
|
379
|
+
canonicalJson(normalizeJoinPolicy(right))
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function enforceJoinAuthority(session, { scopes, via, capabilities, policy }) {
|
|
384
|
+
const normalizedPolicy = normalizeJoinPolicy(policy);
|
|
385
|
+
if (
|
|
386
|
+
session.joinPolicy &&
|
|
387
|
+
!joinPoliciesMatch(session.joinPolicy, normalizedPolicy)
|
|
388
|
+
) {
|
|
389
|
+
throw new Error("Remote membership policy version or authority changed");
|
|
390
|
+
}
|
|
391
|
+
if (via !== "direct" && via !== "relay") {
|
|
392
|
+
throw new TypeError("membership join transport is unsupported");
|
|
393
|
+
}
|
|
394
|
+
if (via === "relay" && !normalizedPolicy.allowRelayPairing) {
|
|
395
|
+
throw new Error("Relay pairing is disabled by membership policy");
|
|
396
|
+
}
|
|
397
|
+
const normalizedCapabilities = normalizeCapabilities(capabilities);
|
|
398
|
+
let grantedScopes = normalizeScopes(scopes);
|
|
399
|
+
if (normalizedPolicy.allowedScopes !== null) {
|
|
400
|
+
grantedScopes = grantedScopes.filter((scope) =>
|
|
401
|
+
normalizedPolicy.allowedScopes.includes(scope),
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
if (!normalizedCapabilities.includes(REMOTE_APPROVAL_BINDING_CAPABILITY)) {
|
|
405
|
+
grantedScopes = grantedScopes.filter((scope) => scope !== "approve");
|
|
406
|
+
}
|
|
407
|
+
if (grantedScopes.length === 0) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
"Remote membership scopes are denied by policy or capability",
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
const deviceCount = [...session.members.values()].filter(
|
|
413
|
+
(member) =>
|
|
414
|
+
member.principalId !== session.hostPrincipalId &&
|
|
415
|
+
member.status === "active",
|
|
416
|
+
).length;
|
|
417
|
+
if (
|
|
418
|
+
normalizedPolicy.maxDevices !== null &&
|
|
419
|
+
deviceCount >= normalizedPolicy.maxDevices
|
|
420
|
+
) {
|
|
421
|
+
throw new Error("Remote membership device limit reached");
|
|
422
|
+
}
|
|
423
|
+
return Object.freeze({
|
|
424
|
+
policy: normalizedPolicy,
|
|
425
|
+
scopes: Object.freeze([...grantedScopes]),
|
|
426
|
+
capabilities: Object.freeze([...normalizedCapabilities]),
|
|
427
|
+
via,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
247
431
|
function canonicalJson(value) {
|
|
248
432
|
if (
|
|
249
433
|
value === null ||
|
|
@@ -469,7 +653,13 @@ function replayStore(store, coordinatorId, { missing = false } = {}) {
|
|
|
469
653
|
if (event.schema !== EVENT_SCHEMA || !VALID_TYPES.has(event.type)) {
|
|
470
654
|
throw new TypeError(`${label} has an unsupported schema or type`);
|
|
471
655
|
}
|
|
472
|
-
if (
|
|
656
|
+
if (
|
|
657
|
+
!hasExactKeys(event, EVENT_FIELDS[event.type]) &&
|
|
658
|
+
!(
|
|
659
|
+
LEGACY_EVENT_FIELDS[event.type] &&
|
|
660
|
+
hasExactKeys(event, LEGACY_EVENT_FIELDS[event.type])
|
|
661
|
+
)
|
|
662
|
+
) {
|
|
473
663
|
throw new TypeError(
|
|
474
664
|
`${label} has non-canonical fields for ${event.type}`,
|
|
475
665
|
);
|
|
@@ -554,6 +744,7 @@ function replayStore(store, coordinatorId, { missing = false } = {}) {
|
|
|
554
744
|
createdAt: event.occurredAtMs,
|
|
555
745
|
expiresAt,
|
|
556
746
|
status: "active",
|
|
747
|
+
joinPolicy: joinPolicyFromEvent(event, label),
|
|
557
748
|
members: new Map([
|
|
558
749
|
[
|
|
559
750
|
hostPrincipalId,
|
|
@@ -565,6 +756,7 @@ function replayStore(store, coordinatorId, { missing = false } = {}) {
|
|
|
565
756
|
credentialType: hostCredential.type,
|
|
566
757
|
credentialPublicKey: hostCredential.publicKey,
|
|
567
758
|
credentialDigest: hostCredential.digest,
|
|
759
|
+
capabilities: [],
|
|
568
760
|
},
|
|
569
761
|
],
|
|
570
762
|
]),
|
|
@@ -584,12 +776,83 @@ function replayStore(store, coordinatorId, { missing = false } = {}) {
|
|
|
584
776
|
}
|
|
585
777
|
if (
|
|
586
778
|
event.type !== "session.closed" &&
|
|
779
|
+
event.type !== "session.reenabled" &&
|
|
587
780
|
event.occurredAtMs >= session.expiresAt
|
|
588
781
|
) {
|
|
589
782
|
throw new TypeError(`${label} occurs after session expiry`);
|
|
590
783
|
}
|
|
591
784
|
|
|
592
|
-
if (event.type === "
|
|
785
|
+
if (event.type === "session.reenabled") {
|
|
786
|
+
if (session.status !== "closed") {
|
|
787
|
+
throw new TypeError(`${label} re-enables an active session`);
|
|
788
|
+
}
|
|
789
|
+
const currentHost = session.members.get(session.hostPrincipalId);
|
|
790
|
+
if (
|
|
791
|
+
event.hostPrincipalId !== session.hostPrincipalId ||
|
|
792
|
+
!currentHost ||
|
|
793
|
+
event.expectedHostMembershipEpoch !== currentHost.membershipEpoch
|
|
794
|
+
) {
|
|
795
|
+
throw new TypeError(
|
|
796
|
+
`${label} was not re-enabled by the current host`,
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
const nextSessionEpoch = String(
|
|
800
|
+
canonicalEpoch(event.sessionEpoch, `${label}.sessionEpoch`),
|
|
801
|
+
);
|
|
802
|
+
if (nextSessionEpoch !== String(BigInt(session.sessionEpoch) + 1n)) {
|
|
803
|
+
throw new TypeError(`${label} does not advance the session epoch`);
|
|
804
|
+
}
|
|
805
|
+
const nextHost = normalizePrincipalCredential(
|
|
806
|
+
{
|
|
807
|
+
type: event.nextHostCredentialType,
|
|
808
|
+
publicKey: event.nextHostCredentialPublicKey,
|
|
809
|
+
},
|
|
810
|
+
`${label}.nextHostCredential`,
|
|
811
|
+
);
|
|
812
|
+
if (
|
|
813
|
+
nextHost.digest !== event.nextHostCredentialDigest ||
|
|
814
|
+
nextHost.principalId !== event.nextHostPrincipalId
|
|
815
|
+
) {
|
|
816
|
+
throw new TypeError(`${label}.nextHostPrincipalId is not key-bound`);
|
|
817
|
+
}
|
|
818
|
+
const priorNextHost = session.members.get(nextHost.principalId) || null;
|
|
819
|
+
const nextMembershipEpoch = String(
|
|
820
|
+
canonicalEpoch(event.membershipEpoch, `${label}.membershipEpoch`),
|
|
821
|
+
);
|
|
822
|
+
const expectedMembershipEpoch = priorNextHost
|
|
823
|
+
? String(BigInt(priorNextHost.membershipEpoch) + 1n)
|
|
824
|
+
: "1";
|
|
825
|
+
if (nextMembershipEpoch !== expectedMembershipEpoch) {
|
|
826
|
+
throw new TypeError(`${label} does not advance the new host epoch`);
|
|
827
|
+
}
|
|
828
|
+
const expiresAt = safeTimestamp(event.expiresAt, `${label}.expiresAt`);
|
|
829
|
+
if (expiresAt <= event.occurredAtMs) {
|
|
830
|
+
throw new TypeError(`${label}.expiresAt must be in the future`);
|
|
831
|
+
}
|
|
832
|
+
for (const member of session.members.values()) {
|
|
833
|
+
if (member.status === "active") {
|
|
834
|
+
member.status = "revoked";
|
|
835
|
+
member.membershipEpoch = String(
|
|
836
|
+
BigInt(member.membershipEpoch) + 1n,
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
session.members.set(nextHost.principalId, {
|
|
841
|
+
principalId: nextHost.principalId,
|
|
842
|
+
membershipEpoch: nextMembershipEpoch,
|
|
843
|
+
status: "active",
|
|
844
|
+
scopes: normalizeScopes(event.scopes),
|
|
845
|
+
credentialType: nextHost.type,
|
|
846
|
+
credentialPublicKey: nextHost.publicKey,
|
|
847
|
+
credentialDigest: nextHost.digest,
|
|
848
|
+
capabilities: [],
|
|
849
|
+
});
|
|
850
|
+
session.hostPrincipalId = nextHost.principalId;
|
|
851
|
+
session.sessionEpoch = nextSessionEpoch;
|
|
852
|
+
session.expiresAt = expiresAt;
|
|
853
|
+
session.status = "active";
|
|
854
|
+
session.joinPolicy = joinPolicyFromEvent(event, label);
|
|
855
|
+
} else if (event.type === "member.joined") {
|
|
593
856
|
if (session.status !== "active") {
|
|
594
857
|
throw new TypeError(`${label} joins a closed session`);
|
|
595
858
|
}
|
|
@@ -638,14 +901,56 @@ function replayStore(store, coordinatorId, { missing = false } = {}) {
|
|
|
638
901
|
if (membershipEpoch !== expected) {
|
|
639
902
|
throw new TypeError(`${label} does not advance the principal epoch`);
|
|
640
903
|
}
|
|
904
|
+
const eventPolicy = joinPolicyFromEvent(event, label);
|
|
905
|
+
const joinVia = Object.prototype.hasOwnProperty.call(event, "joinVia")
|
|
906
|
+
? requiredString(event.joinVia, `${label}.joinVia`, 16)
|
|
907
|
+
: "direct";
|
|
908
|
+
const capabilities = Object.prototype.hasOwnProperty.call(
|
|
909
|
+
event,
|
|
910
|
+
"capabilities",
|
|
911
|
+
)
|
|
912
|
+
? normalizeCapabilities(event.capabilities)
|
|
913
|
+
: [];
|
|
914
|
+
const declaredScopes = normalizeScopes(event.scopes);
|
|
915
|
+
// Pre-capability stores could persist `approve` without proving that the
|
|
916
|
+
// client understood the exact approval tuple. Preserve non-approval
|
|
917
|
+
// membership on upgrade, but never grandfather that authority through
|
|
918
|
+
// replay. A legacy approve-only member remains counted as active with no
|
|
919
|
+
// usable scopes rather than silently freeing the device-cap slot.
|
|
920
|
+
const replayedScopes = eventPolicy
|
|
921
|
+
? declaredScopes
|
|
922
|
+
: declaredScopes.filter((scope) => scope !== "approve");
|
|
923
|
+
if (eventPolicy) {
|
|
924
|
+
if (
|
|
925
|
+
session.joinPolicy &&
|
|
926
|
+
!joinPoliciesMatch(session.joinPolicy, eventPolicy)
|
|
927
|
+
) {
|
|
928
|
+
throw new TypeError(`${label} changes the session join policy`);
|
|
929
|
+
}
|
|
930
|
+
session.joinPolicy = eventPolicy;
|
|
931
|
+
const authority = enforceJoinAuthority(session, {
|
|
932
|
+
scopes: event.scopes,
|
|
933
|
+
via: joinVia,
|
|
934
|
+
capabilities,
|
|
935
|
+
policy: eventPolicy,
|
|
936
|
+
});
|
|
937
|
+
if (
|
|
938
|
+
canonicalJson(authority.scopes) !== canonicalJson(declaredScopes)
|
|
939
|
+
) {
|
|
940
|
+
throw new TypeError(
|
|
941
|
+
`${label} grants scopes outside join authority`,
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
641
945
|
session.members.set(principalId, {
|
|
642
946
|
principalId,
|
|
643
947
|
membershipEpoch,
|
|
644
948
|
status: "active",
|
|
645
|
-
scopes:
|
|
949
|
+
scopes: replayedScopes,
|
|
646
950
|
credentialType: credential.type,
|
|
647
951
|
credentialPublicKey: credential.publicKey,
|
|
648
952
|
credentialDigest: credential.digest,
|
|
953
|
+
capabilities,
|
|
649
954
|
});
|
|
650
955
|
} else if (event.type === "member.revoked") {
|
|
651
956
|
if (session.status !== "active") {
|
|
@@ -1461,6 +1766,11 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1461
1766
|
member,
|
|
1462
1767
|
credential,
|
|
1463
1768
|
scopes = null,
|
|
1769
|
+
capabilities = null,
|
|
1770
|
+
joinPolicy = null,
|
|
1771
|
+
joinVia = null,
|
|
1772
|
+
nextCredential = null,
|
|
1773
|
+
nextSessionExpiresAt = null,
|
|
1464
1774
|
connectionNonce,
|
|
1465
1775
|
now,
|
|
1466
1776
|
ttlMs = null,
|
|
@@ -1506,6 +1816,20 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1506
1816
|
connectionNonce: requiredString(connectionNonce, "connectionNonce", 4096),
|
|
1507
1817
|
serverNonce: requiredString(this._createSecret(), "serverNonce", 4096),
|
|
1508
1818
|
scopes: scopes === null ? null : Object.freeze([...scopes]),
|
|
1819
|
+
capabilities:
|
|
1820
|
+
capabilities === null
|
|
1821
|
+
? null
|
|
1822
|
+
: Object.freeze([...normalizeCapabilities(capabilities)]),
|
|
1823
|
+
joinPolicy:
|
|
1824
|
+
joinPolicy === null
|
|
1825
|
+
? null
|
|
1826
|
+
: Object.freeze({ ...normalizeJoinPolicy(joinPolicy) }),
|
|
1827
|
+
joinVia,
|
|
1828
|
+
nextCredentialType: nextCredential?.type || null,
|
|
1829
|
+
nextCredentialPublicKey: nextCredential?.publicKey || null,
|
|
1830
|
+
nextCredentialKeySha256: nextCredential?.digest || null,
|
|
1831
|
+
nextPrincipalId: nextCredential?.principalId || null,
|
|
1832
|
+
nextSessionExpiresAt,
|
|
1509
1833
|
issuedAtMs: now,
|
|
1510
1834
|
expiresAtMs,
|
|
1511
1835
|
});
|
|
@@ -1520,12 +1844,16 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1520
1844
|
credentialPublicKey,
|
|
1521
1845
|
connectionNonce,
|
|
1522
1846
|
ttlMs = null,
|
|
1847
|
+
capabilities = null,
|
|
1848
|
+
joinPolicy = null,
|
|
1523
1849
|
} = {}) {
|
|
1524
1850
|
const normalizedSessionId = requiredString(sessionId, "sessionId");
|
|
1525
1851
|
const sessionEpoch = String(
|
|
1526
1852
|
canonicalEpoch(expectedSessionEpoch, "expectedSessionEpoch"),
|
|
1527
1853
|
);
|
|
1528
1854
|
const normalizedScopes = normalizeScopes(scopes);
|
|
1855
|
+
const normalizedCapabilities = normalizeCapabilities(capabilities);
|
|
1856
|
+
const normalizedJoinPolicy = normalizeJoinPolicy(joinPolicy);
|
|
1529
1857
|
const credential = normalizePrincipalCredential(
|
|
1530
1858
|
{ type: "ed25519", publicKey: credentialPublicKey },
|
|
1531
1859
|
"credential",
|
|
@@ -1544,6 +1872,12 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1544
1872
|
if (prior?.status === "active") {
|
|
1545
1873
|
throw new Error("Remote membership principal is already active");
|
|
1546
1874
|
}
|
|
1875
|
+
enforceJoinAuthority(session, {
|
|
1876
|
+
scopes: normalizedScopes,
|
|
1877
|
+
via: "direct",
|
|
1878
|
+
capabilities: normalizedCapabilities,
|
|
1879
|
+
policy: normalizedJoinPolicy,
|
|
1880
|
+
});
|
|
1547
1881
|
return this._newAuthenticationChallenge({
|
|
1548
1882
|
purpose: "member.join",
|
|
1549
1883
|
session,
|
|
@@ -1554,6 +1888,9 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1554
1888
|
},
|
|
1555
1889
|
credential,
|
|
1556
1890
|
scopes: normalizedScopes,
|
|
1891
|
+
capabilities: normalizedCapabilities,
|
|
1892
|
+
joinPolicy: normalizedJoinPolicy,
|
|
1893
|
+
joinVia: "direct",
|
|
1557
1894
|
connectionNonce,
|
|
1558
1895
|
now,
|
|
1559
1896
|
ttlMs,
|
|
@@ -1580,7 +1917,10 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1580
1917
|
member.status !== "active" ||
|
|
1581
1918
|
member.credentialType !== "ed25519"
|
|
1582
1919
|
) {
|
|
1583
|
-
throw
|
|
1920
|
+
throw coordinatorError(
|
|
1921
|
+
REMOTE_MEMBERSHIP_NOT_ACTIVE_CODE,
|
|
1922
|
+
"Remote membership is not active for resume",
|
|
1923
|
+
);
|
|
1584
1924
|
}
|
|
1585
1925
|
return this._newAuthenticationChallenge({
|
|
1586
1926
|
purpose: "session.resume",
|
|
@@ -1600,6 +1940,65 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1600
1940
|
});
|
|
1601
1941
|
}
|
|
1602
1942
|
|
|
1943
|
+
issueSessionReenableChallenge({
|
|
1944
|
+
sessionId,
|
|
1945
|
+
principalId,
|
|
1946
|
+
connectionNonce,
|
|
1947
|
+
newHostCredentialPublicKeySpki = null,
|
|
1948
|
+
scopes,
|
|
1949
|
+
expiresAt,
|
|
1950
|
+
joinPolicy = null,
|
|
1951
|
+
ttlMs = null,
|
|
1952
|
+
} = {}) {
|
|
1953
|
+
const normalizedSessionId = requiredString(sessionId, "sessionId");
|
|
1954
|
+
const normalizedPrincipal = requiredString(principalId, "principalId");
|
|
1955
|
+
const normalizedScopes = normalizeScopes(scopes);
|
|
1956
|
+
const normalizedExpiresAt = safeTimestamp(expiresAt, "expiresAt");
|
|
1957
|
+
const normalizedJoinPolicy = normalizeJoinPolicy(joinPolicy);
|
|
1958
|
+
return this._withRead((snapshot, now) => {
|
|
1959
|
+
const session = snapshot.sessions.get(normalizedSessionId);
|
|
1960
|
+
const member = session?.members.get(normalizedPrincipal);
|
|
1961
|
+
if (
|
|
1962
|
+
!session ||
|
|
1963
|
+
session.status !== "closed" ||
|
|
1964
|
+
session.hostPrincipalId !== normalizedPrincipal ||
|
|
1965
|
+
!member ||
|
|
1966
|
+
member.status !== "active" ||
|
|
1967
|
+
member.credentialType !== "ed25519" ||
|
|
1968
|
+
normalizedExpiresAt <= now
|
|
1969
|
+
) {
|
|
1970
|
+
throw new Error("Remote membership session re-enable challenge denied");
|
|
1971
|
+
}
|
|
1972
|
+
const nextCredential = normalizePrincipalCredential(
|
|
1973
|
+
{
|
|
1974
|
+
type: "ed25519",
|
|
1975
|
+
publicKey:
|
|
1976
|
+
newHostCredentialPublicKeySpki || member.credentialPublicKey,
|
|
1977
|
+
},
|
|
1978
|
+
"nextHostCredential",
|
|
1979
|
+
);
|
|
1980
|
+
return this._newAuthenticationChallenge({
|
|
1981
|
+
purpose: "session.reenable",
|
|
1982
|
+
session,
|
|
1983
|
+
member,
|
|
1984
|
+
credential: normalizePrincipalCredential(
|
|
1985
|
+
{
|
|
1986
|
+
type: member.credentialType,
|
|
1987
|
+
publicKey: member.credentialPublicKey,
|
|
1988
|
+
},
|
|
1989
|
+
"memberCredential",
|
|
1990
|
+
),
|
|
1991
|
+
scopes: normalizedScopes,
|
|
1992
|
+
joinPolicy: normalizedJoinPolicy,
|
|
1993
|
+
nextCredential,
|
|
1994
|
+
nextSessionExpiresAt: normalizedExpiresAt,
|
|
1995
|
+
connectionNonce,
|
|
1996
|
+
now,
|
|
1997
|
+
ttlMs,
|
|
1998
|
+
});
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
|
|
1603
2002
|
_consumeAuthenticationChallenge(
|
|
1604
2003
|
snapshot,
|
|
1605
2004
|
now,
|
|
@@ -1622,10 +2021,11 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1622
2021
|
throw new Error("Remote membership authentication challenge denied");
|
|
1623
2022
|
}
|
|
1624
2023
|
const session = snapshot.sessions.get(challenge.sessionId);
|
|
2024
|
+
const reenable = expectedPurpose === "session.reenable";
|
|
1625
2025
|
if (
|
|
1626
2026
|
!session ||
|
|
1627
|
-
session.status !== "active" ||
|
|
1628
|
-
session.expiresAt <= now ||
|
|
2027
|
+
(reenable ? session.status !== "closed" : session.status !== "active") ||
|
|
2028
|
+
(!reenable && session.expiresAt <= now) ||
|
|
1629
2029
|
session.sessionEpoch !== challenge.sessionEpoch
|
|
1630
2030
|
) {
|
|
1631
2031
|
throw new Error("Remote membership authentication session is stale");
|
|
@@ -1643,7 +2043,8 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1643
2043
|
!current ||
|
|
1644
2044
|
current.status !== "active" ||
|
|
1645
2045
|
current.membershipEpoch !== challenge.membershipEpoch ||
|
|
1646
|
-
current.credentialDigest !== challenge.credentialKeySha256
|
|
2046
|
+
current.credentialDigest !== challenge.credentialKeySha256 ||
|
|
2047
|
+
(reenable && session.hostPrincipalId !== challenge.principalId)
|
|
1647
2048
|
) {
|
|
1648
2049
|
throw new Error("Remote membership resume challenge is stale");
|
|
1649
2050
|
}
|
|
@@ -1685,11 +2086,13 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1685
2086
|
expiresAt,
|
|
1686
2087
|
hostPrincipalId = null,
|
|
1687
2088
|
hostCredentialPublicKeySpki,
|
|
2089
|
+
joinPolicy = null,
|
|
1688
2090
|
} = {}) {
|
|
1689
2091
|
const normalizedSessionId = requiredString(sessionId, "sessionId");
|
|
1690
2092
|
const normalizedAgent = requiredString(agentSessionId, "agentSessionId");
|
|
1691
2093
|
const normalizedScopes = normalizeScopes(scopes);
|
|
1692
2094
|
const normalizedExpiresAt = safeTimestamp(expiresAt, "expiresAt");
|
|
2095
|
+
const normalizedJoinPolicy = normalizeJoinPolicy(joinPolicy);
|
|
1693
2096
|
const hostCredential = normalizePrincipalCredential(
|
|
1694
2097
|
{
|
|
1695
2098
|
type: "ed25519",
|
|
@@ -1722,6 +2125,7 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1722
2125
|
hostCredentialDigest: hostCredential.digest,
|
|
1723
2126
|
sessionEpoch: "1",
|
|
1724
2127
|
membershipEpoch: "1",
|
|
2128
|
+
...joinPolicyEventFields(normalizedJoinPolicy),
|
|
1725
2129
|
scopes: normalizedScopes,
|
|
1726
2130
|
expiresAt: normalizedExpiresAt,
|
|
1727
2131
|
};
|
|
@@ -1774,6 +2178,73 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1774
2178
|
}
|
|
1775
2179
|
}
|
|
1776
2180
|
|
|
2181
|
+
reenableSession({ challengeId, connectionNonce, signature } = {}) {
|
|
2182
|
+
const result = this._mutate((snapshot, _generation, now) => {
|
|
2183
|
+
const authenticated = this._consumeAuthenticationChallenge(
|
|
2184
|
+
snapshot,
|
|
2185
|
+
now,
|
|
2186
|
+
{ challengeId, connectionNonce, signature },
|
|
2187
|
+
"session.reenable",
|
|
2188
|
+
);
|
|
2189
|
+
const { challenge, session } = authenticated;
|
|
2190
|
+
const nextCredential = normalizePrincipalCredential(
|
|
2191
|
+
{
|
|
2192
|
+
type: challenge.nextCredentialType,
|
|
2193
|
+
publicKey: challenge.nextCredentialPublicKey,
|
|
2194
|
+
},
|
|
2195
|
+
"nextHostCredential",
|
|
2196
|
+
);
|
|
2197
|
+
if (
|
|
2198
|
+
nextCredential.digest !== challenge.nextCredentialKeySha256 ||
|
|
2199
|
+
nextCredential.principalId !== challenge.nextPrincipalId ||
|
|
2200
|
+
challenge.nextSessionExpiresAt <= now
|
|
2201
|
+
) {
|
|
2202
|
+
throw new Error("Remote membership session re-enable binding changed");
|
|
2203
|
+
}
|
|
2204
|
+
const priorNextHost =
|
|
2205
|
+
session.members.get(nextCredential.principalId) || null;
|
|
2206
|
+
return {
|
|
2207
|
+
type: "session.reenabled",
|
|
2208
|
+
sessionId: session.sessionId,
|
|
2209
|
+
expectedSessionEpoch: session.sessionEpoch,
|
|
2210
|
+
hostPrincipalId: session.hostPrincipalId,
|
|
2211
|
+
expectedHostMembershipEpoch: challenge.membershipEpoch,
|
|
2212
|
+
nextHostPrincipalId: nextCredential.principalId,
|
|
2213
|
+
nextHostCredentialType: nextCredential.type,
|
|
2214
|
+
nextHostCredentialPublicKey: nextCredential.publicKey,
|
|
2215
|
+
nextHostCredentialDigest: nextCredential.digest,
|
|
2216
|
+
sessionEpoch: String(BigInt(session.sessionEpoch) + 1n),
|
|
2217
|
+
membershipEpoch: priorNextHost
|
|
2218
|
+
? String(BigInt(priorNextHost.membershipEpoch) + 1n)
|
|
2219
|
+
: "1",
|
|
2220
|
+
scopes: [...challenge.scopes],
|
|
2221
|
+
expiresAt: challenge.nextSessionExpiresAt,
|
|
2222
|
+
...joinPolicyEventFields(challenge.joinPolicy),
|
|
2223
|
+
};
|
|
2224
|
+
});
|
|
2225
|
+
const session = result.snapshot.sessions.get(result.event.sessionId);
|
|
2226
|
+
const host = session.members.get(session.hostPrincipalId);
|
|
2227
|
+
return Object.freeze({
|
|
2228
|
+
authorityVersion: REMOTE_MEMBERSHIP_COORDINATOR_VERSION,
|
|
2229
|
+
trust: this.trustDescriptor(),
|
|
2230
|
+
sessionId: session.sessionId,
|
|
2231
|
+
sessionEpoch: session.sessionEpoch,
|
|
2232
|
+
hostPrincipalId: session.hostPrincipalId,
|
|
2233
|
+
membershipEpoch: host.membershipEpoch,
|
|
2234
|
+
hostCredentialPublicKeySpki: host.credentialPublicKey,
|
|
2235
|
+
nextConnectionNonce: requiredString(
|
|
2236
|
+
this._createSecret(),
|
|
2237
|
+
"nextConnectionNonce",
|
|
2238
|
+
4096,
|
|
2239
|
+
),
|
|
2240
|
+
statement: this._snapshotStatement(
|
|
2241
|
+
session,
|
|
2242
|
+
result.snapshot.generation,
|
|
2243
|
+
result.event.occurredAtMs,
|
|
2244
|
+
),
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
|
|
1777
2248
|
joinMember({ challengeId, connectionNonce, signature } = {}) {
|
|
1778
2249
|
const result = this._mutate((snapshot, _generation, now) => {
|
|
1779
2250
|
const authenticated = this._consumeAuthenticationChallenge(
|
|
@@ -1786,6 +2257,12 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1786
2257
|
if (session.hostPrincipalId === challenge.principalId) {
|
|
1787
2258
|
throw new Error("Cannot replace the host membership");
|
|
1788
2259
|
}
|
|
2260
|
+
const authority = enforceJoinAuthority(session, {
|
|
2261
|
+
scopes: challenge.scopes,
|
|
2262
|
+
via: challenge.joinVia,
|
|
2263
|
+
capabilities: challenge.capabilities,
|
|
2264
|
+
policy: challenge.joinPolicy,
|
|
2265
|
+
});
|
|
1789
2266
|
return {
|
|
1790
2267
|
type: "member.joined",
|
|
1791
2268
|
sessionId: session.sessionId,
|
|
@@ -1796,7 +2273,10 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1796
2273
|
credentialType: credential.type,
|
|
1797
2274
|
credentialPublicKey: credential.publicKey,
|
|
1798
2275
|
credentialDigest: credential.digest,
|
|
1799
|
-
|
|
2276
|
+
...joinPolicyEventFields(authority.policy),
|
|
2277
|
+
joinVia: authority.via,
|
|
2278
|
+
capabilities: [...authority.capabilities],
|
|
2279
|
+
scopes: [...authority.scopes],
|
|
1800
2280
|
};
|
|
1801
2281
|
});
|
|
1802
2282
|
const session = result.snapshot.sessions.get(result.event.sessionId);
|
|
@@ -1808,6 +2288,8 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1808
2288
|
credentialPublicKey: member.credentialPublicKey,
|
|
1809
2289
|
sessionEpoch: session.sessionEpoch,
|
|
1810
2290
|
membershipEpoch: member.membershipEpoch,
|
|
2291
|
+
scopes: Object.freeze([...member.scopes]),
|
|
2292
|
+
capabilities: Object.freeze([...(member.capabilities || [])]),
|
|
1811
2293
|
nextConnectionNonce: requiredString(
|
|
1812
2294
|
this._createSecret(),
|
|
1813
2295
|
"nextConnectionNonce",
|
|
@@ -1829,12 +2311,16 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1829
2311
|
mobilePublicKey,
|
|
1830
2312
|
pairingTokenDigest,
|
|
1831
2313
|
possessionCapability,
|
|
2314
|
+
capabilities = null,
|
|
2315
|
+
joinPolicy = null,
|
|
1832
2316
|
} = {}) {
|
|
1833
2317
|
const normalizedSessionId = requiredString(sessionId, "sessionId");
|
|
1834
2318
|
const sessionEpoch = String(
|
|
1835
2319
|
canonicalEpoch(expectedSessionEpoch, "expectedSessionEpoch"),
|
|
1836
2320
|
);
|
|
1837
2321
|
const normalizedScopes = normalizeScopes(scopes);
|
|
2322
|
+
const normalizedCapabilities = normalizeCapabilities(capabilities);
|
|
2323
|
+
const normalizedJoinPolicy = normalizeJoinPolicy(joinPolicy);
|
|
1838
2324
|
const normalizedPeer = requiredString(mobilePeerId, "mobilePeerId");
|
|
1839
2325
|
if (!DIGEST_RE.test(String(pairingTokenDigest))) {
|
|
1840
2326
|
throw new TypeError("pairingTokenDigest must be a SHA-256 digest");
|
|
@@ -1880,6 +2366,12 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1880
2366
|
"Remote relay transcript proof is malformed, expired, or over-scoped",
|
|
1881
2367
|
);
|
|
1882
2368
|
}
|
|
2369
|
+
const authority = enforceJoinAuthority(session, {
|
|
2370
|
+
scopes: normalizedScopes,
|
|
2371
|
+
via: "relay",
|
|
2372
|
+
capabilities: normalizedCapabilities,
|
|
2373
|
+
policy: normalizedJoinPolicy,
|
|
2374
|
+
});
|
|
1883
2375
|
return {
|
|
1884
2376
|
type: "member.joined",
|
|
1885
2377
|
sessionId: normalizedSessionId,
|
|
@@ -1892,7 +2384,10 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1892
2384
|
credentialType: credential.type,
|
|
1893
2385
|
credentialPublicKey: credential.publicKey,
|
|
1894
2386
|
credentialDigest: credential.digest,
|
|
1895
|
-
|
|
2387
|
+
...joinPolicyEventFields(authority.policy),
|
|
2388
|
+
joinVia: authority.via,
|
|
2389
|
+
capabilities: [...authority.capabilities],
|
|
2390
|
+
scopes: [...authority.scopes],
|
|
1896
2391
|
};
|
|
1897
2392
|
});
|
|
1898
2393
|
const session = result.snapshot.sessions.get(normalizedSessionId);
|
|
@@ -1904,6 +2399,8 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1904
2399
|
credentialPublicKey: member.credentialPublicKey,
|
|
1905
2400
|
sessionEpoch: session.sessionEpoch,
|
|
1906
2401
|
membershipEpoch: member.membershipEpoch,
|
|
2402
|
+
scopes: Object.freeze([...member.scopes]),
|
|
2403
|
+
capabilities: Object.freeze([...(member.capabilities || [])]),
|
|
1907
2404
|
statement: this._snapshotStatement(
|
|
1908
2405
|
session,
|
|
1909
2406
|
result.snapshot.generation,
|
|
@@ -2036,41 +2533,81 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
2036
2533
|
"expectedHostMembershipEpoch",
|
|
2037
2534
|
),
|
|
2038
2535
|
);
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2536
|
+
let result;
|
|
2537
|
+
try {
|
|
2538
|
+
result = this._mutate((snapshot) => {
|
|
2539
|
+
const session = snapshot.sessions.get(normalizedSessionId);
|
|
2540
|
+
const host = session?.members.get(normalizedHost);
|
|
2541
|
+
if (
|
|
2542
|
+
!session ||
|
|
2543
|
+
session.status !== "active" ||
|
|
2544
|
+
session.hostPrincipalId !== normalizedHost ||
|
|
2545
|
+
session.sessionEpoch !== sessionEpoch ||
|
|
2546
|
+
!host ||
|
|
2547
|
+
host.status !== "active" ||
|
|
2548
|
+
host.membershipEpoch !== hostMembershipEpoch
|
|
2549
|
+
) {
|
|
2550
|
+
throw new Error("Remote membership session close denied");
|
|
2551
|
+
}
|
|
2552
|
+
const cancelledLeaseIds = [...session.leases.values()]
|
|
2553
|
+
.filter(
|
|
2554
|
+
(lease) => lease.status === "active" || lease.status === "acked",
|
|
2555
|
+
)
|
|
2556
|
+
.map((lease) => lease.leaseId)
|
|
2557
|
+
.sort();
|
|
2558
|
+
return {
|
|
2559
|
+
type: "session.closed",
|
|
2560
|
+
sessionId: normalizedSessionId,
|
|
2561
|
+
expectedSessionEpoch: sessionEpoch,
|
|
2562
|
+
hostPrincipalId: normalizedHost,
|
|
2563
|
+
expectedHostMembershipEpoch: hostMembershipEpoch,
|
|
2564
|
+
sessionEpoch: String(BigInt(sessionEpoch) + 1n),
|
|
2565
|
+
cancelledLeaseIds,
|
|
2566
|
+
};
|
|
2567
|
+
});
|
|
2568
|
+
} catch (cause) {
|
|
2569
|
+
if (cause?.commitState === "unknown") throw cause;
|
|
2570
|
+
const terminal = this._withRead((snapshot, now) => {
|
|
2571
|
+
const session = snapshot.sessions.get(normalizedSessionId);
|
|
2572
|
+
const host = session?.members.get(normalizedHost);
|
|
2573
|
+
if (
|
|
2574
|
+
!session ||
|
|
2575
|
+
session.status !== "closed" ||
|
|
2576
|
+
session.hostPrincipalId !== normalizedHost ||
|
|
2577
|
+
session.sessionEpoch !== String(BigInt(sessionEpoch) + 1n) ||
|
|
2578
|
+
!host ||
|
|
2579
|
+
host.membershipEpoch !== hostMembershipEpoch
|
|
2580
|
+
) {
|
|
2581
|
+
return null;
|
|
2582
|
+
}
|
|
2583
|
+
return Object.freeze({
|
|
2584
|
+
authorityVersion: REMOTE_MEMBERSHIP_COORDINATOR_VERSION,
|
|
2585
|
+
sessionEpoch: session.sessionEpoch,
|
|
2586
|
+
cancelledLeaseIds: Object.freeze(
|
|
2587
|
+
[...session.leases.values()]
|
|
2588
|
+
.filter(
|
|
2589
|
+
(lease) =>
|
|
2590
|
+
lease.cancelReason === "session-closed" &&
|
|
2591
|
+
lease.terminalGeneration === String(snapshot.generation),
|
|
2592
|
+
)
|
|
2593
|
+
.map((lease) => lease.leaseId)
|
|
2594
|
+
.sort(),
|
|
2595
|
+
),
|
|
2596
|
+
terminal: true,
|
|
2597
|
+
alreadyClosed: true,
|
|
2598
|
+
statement: this._snapshotStatement(session, snapshot.generation, now),
|
|
2599
|
+
});
|
|
2600
|
+
});
|
|
2601
|
+
if (terminal) return terminal;
|
|
2602
|
+
throw cause;
|
|
2603
|
+
}
|
|
2069
2604
|
const session = result.snapshot.sessions.get(normalizedSessionId);
|
|
2070
2605
|
return Object.freeze({
|
|
2071
2606
|
authorityVersion: REMOTE_MEMBERSHIP_COORDINATOR_VERSION,
|
|
2072
2607
|
sessionEpoch: session.sessionEpoch,
|
|
2073
2608
|
cancelledLeaseIds: Object.freeze([...result.event.cancelledLeaseIds]),
|
|
2609
|
+
terminal: true,
|
|
2610
|
+
alreadyClosed: false,
|
|
2074
2611
|
statement: this._snapshotStatement(
|
|
2075
2612
|
session,
|
|
2076
2613
|
result.snapshot.generation,
|
|
@@ -2397,6 +2934,35 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
2397
2934
|
});
|
|
2398
2935
|
});
|
|
2399
2936
|
}
|
|
2937
|
+
|
|
2938
|
+
getSessionSnapshot(sessionId) {
|
|
2939
|
+
const normalizedSessionId = requiredString(sessionId, "sessionId");
|
|
2940
|
+
return this._withRead((snapshot, now) => {
|
|
2941
|
+
const session = snapshot.sessions.get(normalizedSessionId);
|
|
2942
|
+
if (!session) return null;
|
|
2943
|
+
return Object.freeze({
|
|
2944
|
+
session: publicSessionSnapshot(session, snapshot.generation),
|
|
2945
|
+
statement: this._snapshotStatement(session, snapshot.generation, now),
|
|
2946
|
+
});
|
|
2947
|
+
});
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
/**
|
|
2951
|
+
* Enumerate coordinator-owned session snapshots for server-process recovery.
|
|
2952
|
+
* The caller receives immutable public projections only; live transport
|
|
2953
|
+
* attachment remains a registry concern and is deliberately not persisted
|
|
2954
|
+
* here. Reading also exercises the normal clock/witness rollback checks.
|
|
2955
|
+
*/
|
|
2956
|
+
listSessionSnapshots({ activeOnly = true } = {}) {
|
|
2957
|
+
return this._withRead((snapshot) =>
|
|
2958
|
+
Object.freeze(
|
|
2959
|
+
[...snapshot.sessions.values()]
|
|
2960
|
+
.filter((session) => !activeOnly || session.status === "active")
|
|
2961
|
+
.map((session) => publicSessionSnapshot(session, snapshot.generation))
|
|
2962
|
+
.sort((left, right) => left.sessionId.localeCompare(right.sessionId)),
|
|
2963
|
+
),
|
|
2964
|
+
);
|
|
2965
|
+
}
|
|
2400
2966
|
}
|
|
2401
2967
|
|
|
2402
2968
|
export const _remoteMembershipCoordinatorInternals = Object.freeze({
|