chainlesschain 0.164.0 → 0.165.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-D2MEn7fI.js → AIOps-CYxFDrcm.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-lCW_4Gf0.js → ActionButton-CPBtdiU2.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-Bs7i-nsS.js → Analytics-Dayq7Dp7.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-CK7B6S_j.js → AppLayout-6U7bPb2s.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-C8ILjpyo.js → Artifacts-D2Iqg_3D.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-Bb9cNaWc.js → Audit-CdoOB7WV.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-sC9ooXIf.js → BackgroundAgents-DrnAVB8w.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-D-DrsplT.js → Backup-CesMi5cy.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-uCNZpwSK.js → BaseInput-DaX9ptax.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CsDK1uE9.js → Chat-OrwOPxDS.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-OqSDt50w.js → ChatBubbleRenderer-CvPZFRxM.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-JhTXM1-K.js → Checkbox-DiHbcysC.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BbzZLnyD.js → Codegen-CbgCkaBJ.js} +1 -1
- package/src/assets/web-panel/assets/{Col-D4VW4m4j.js → Col-Du5hgrh4.js} +1 -1
- package/src/assets/web-panel/assets/{Community-BXrYawxV.js → Community-DsPOBW-p.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DCihGy1u.js → Compact-BwHIFkNC.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DlyZ-lk8.js → Compliance-BvrDQSgd.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-DvGZrhbO.js → Cowork-gwAE6arx.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-DUncZi8h.js → Cron-DW-0l4uF.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-C_hLuUcm.js → Crosschain-j-NsLesB.js} +1 -1
- package/src/assets/web-panel/assets/{DID-b6YSUqxr.js → DID-CibVEBOI.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-uvM15Cfb.js → Dashboard-W4z9vSfe.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-D1-Nd4qY.js → Dropdown-DAcrkKxG.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-8ApvL6Zc.js → EmailListRenderer-6GXah-89.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-LYJt8811.js → FamilyGuardDashboard-TKDCq8Ku.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-BbI3HGpv.js → Federation-BXBLlcNm.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DSoEKuFu.js → FormItemContext-BoJJBkG3.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-B0S9OWd5.js → GenericCardRenderer-okjsl46v.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DIs9Zhf3.js → Git-Rvs7i6nn.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-jzZXrxcl.js → Governance-CHSUxXXS.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-JUPjf2ZI.js → Inference-V3BlfxsB.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DxnfJvlP.js → KnowledgeGraph-C_NlD4aR.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-iXiz8BEE.js → Logs-Nn4LNxhA.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-GmB6DOr6.js → MarkdownRenderer-SVAgNnkt.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-Y3Xch312.js → Marketplace-DEmjFcg-.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-D7kGGbO7.js → McpTools-DlopXipI.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-Do_hVxzh.js → Memory-BG3ynX60.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CtrR0Tas.js → MobileBridge-DeRzWOfT.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-Tyd3o2A0.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-qo5QCTGd.js → Mtc-BoPbOv1C.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-cq5mnjYj.js → MtcAudit-un-rTHp7.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-Bq0MAQHw.js → Multisig-Ds0oveEB.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-DdWE6Vi2.js → NLProgramming-D09JI-yY.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BxMBFcIp.js → Notes-DTRhiKJn.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-RTrAcbUe.js → NotificationSettings-BAnnNCim.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-DJbpGqzT.js → OrderTableRenderer-Yr-1fSYi.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-SQzIPdqC.js → Organization-S9EAolpj.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-02yECh22.js → Overflow-CqcLKqJ8.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BNg4pNCM.js → P2P-Dw0F9EcU.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-BxPJZsD3.js → PdhVaultBrowser-BDRwUevN.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-CLnDnPAz.js → Permissions-D3_c3nKB.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-C5m0AjGf.js → PersonalDataHub-CxdMV98x.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-VwOf5Dhv.js → Pipeline-CgjNQFLI.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CC0W2hZd.js → Privacy-BHghIIht.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Dz893Bob.js → ProjectInit-De6pqaKM.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CxQkvl0k.js → ProjectSettings-Bg4ZkNYQ.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-DDAjkUkT.js → Projects-CP8vPvzs.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-DeB9IkxK.js → Providers-CUnHbNf-.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-OCeoaswn.js → QrScannerModal-DA2f3Wts.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-DenotaAj.js → QuickAsk-C1FeHDqp.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Cies_jjI.js → Recommend-B5guaUMN.js} +1 -1
- package/src/assets/web-panel/assets/RemoteSession-PDADSds3.js +5 -0
- package/src/assets/web-panel/assets/{Reputation-CW0mvxgZ.js → Reputation-eTlRhgvK.js} +1 -1
- package/src/assets/web-panel/assets/{Row-SEd32qZU.js → Row-D7AqSGY0.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BSwnJ3Lm.js → RssFeed-JiuLm4Ja.js} +3 -3
- package/src/assets/web-panel/assets/{Search-xe2m8bkC.js → Search-CwruahW3.js} +1 -1
- package/src/assets/web-panel/assets/{Security-Mq2F9Nro.js → Security-CdhqaR0b.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BKGw2V3P.js → Services-CU0ZIlcT.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DW95-Rxf.js → Skeleton-CtduPfAb.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CHgChCv_.js → Skills-CROXm6AY.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-tKh6k3R7.js → Sla-DiZiABFD.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-D6UL3dA5.js → SpeechSettings-B5pLcXUw.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-D0Fw8fVL.js → SyncSettings-DBX6J1Oe.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-20x1GB3D.js → Tasks-BLyRJd3-.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DJ-lts-a.js → Templates-BKa5qzmk.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BFdGpbs7.js → Tenant-DnD3a4M6.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-Xpx61vHL.js → Terminal-DEitQeTb.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-C6UIvyZQ.js → TimelineRenderer-CjHggTDq.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-Dellq08f.js → Tokens-CsGT7sfl.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-VKw8d0J-.js → Trigger-yr4rFDrW.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DNftdKT5.js → Trust-CLvu1Sjp.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DX9VAKJI.js → UkeySign-B0m7yUP8.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B2h3jO8q.js → VideoEditing-C0VvP4lc.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CjGkof7f.js → Wallet-kllBAG1M.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-2Zfcu55P.js → WebAuthn-C54UQNLx.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-B9eZ8UxZ.js → WorkflowEditor-B7y8qSvU.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CQNfD-YO.js → chat-DRzTh8CJ.js} +1 -1
- package/src/assets/web-panel/assets/{colors-D4usyFT2.js → colors-BSMwSnCx.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-s4TpjvjX.js → compact-item-BTg602H5.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BRCSvYxj.js → createContext-B6EGsdvp.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-Dol8jLEP.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-AdkUbkV-.js → hasIn-DDKqJ8RD.js} +1 -1
- package/src/assets/web-panel/assets/{index-CKC1t3Ic.js → index-323Hhxxt.js} +1 -1
- package/src/assets/web-panel/assets/{index-CkwiSLhU.js → index-BICn3Qh1.js} +1 -1
- package/src/assets/web-panel/assets/{index-s_buFMEG.js → index-BMuGd2--.js} +1 -1
- package/src/assets/web-panel/assets/{index-MMN9V6JF.js → index-BRqt4fO-.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZT36ZYq.js → index-BVTAl5N2.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bad4VNvz.js → index-BsR7Y2NG.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1mJtE6q.js → index-Bx044qDj.js} +1 -1
- package/src/assets/web-panel/assets/{index-CYdoQsa1.js → index-C1GFfuqN.js} +1 -1
- package/src/assets/web-panel/assets/index-C3QX5tOl.js +1 -0
- package/src/assets/web-panel/assets/{index-CJCFe2Qf.js → index-C66NpWy5.js} +1 -1
- package/src/assets/web-panel/assets/{index-D2GjI2oe.js → index-CDTvsX8t.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7Y8Aluc.js → index-CGNklbP6.js} +1 -1
- package/src/assets/web-panel/assets/{index-DdCB70SS.js → index-CHfLYwcW.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bw2MEnUk.js → index-CNxg61YP.js} +1 -1
- package/src/assets/web-panel/assets/{index-DARsAyUp.js → index-CQlHFN_g.js} +1 -1
- package/src/assets/web-panel/assets/{index-BIoG5yYf.js → index-CUEU48o1.js} +1 -1
- package/src/assets/web-panel/assets/{index-DzRjHxEv.js → index-CWGax2QW.js} +1 -1
- package/src/assets/web-panel/assets/{index-CCsM_Ybx.js → index-C_JzIXUq.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4JJCfQ2.js → index-Cf23wVMs.js} +1 -1
- package/src/assets/web-panel/assets/{index-C484EJiS.js → index-CmAInUkX.js} +1 -1
- package/src/assets/web-panel/assets/{index-B2V-eKfK.js → index-CmAubjDi.js} +1 -1
- package/src/assets/web-panel/assets/{index-D_XBHopw.js → index-Co7oM1qr.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5KAQFPQ.js → index-CwPUZbMI.js} +1 -1
- package/src/assets/web-panel/assets/{index-BPhkFZ1I.js → index-D1naNj4U.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQhAVU5t.js → index-D5CsBShd.js} +1 -1
- package/src/assets/web-panel/assets/{index-DVKfQ0Ux.js → index-DBT3YeQ9.js} +1 -1
- package/src/assets/web-panel/assets/{index-_IRVsV9X.js → index-DLdrHhML.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJdQrZth.js → index-Dc77VR3H.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cduu2Hhr.js → index-Dfv2lKu3.js} +1 -1
- package/src/assets/web-panel/assets/{index--00U6vXZ.js → index-DiW0sDkz.js} +1 -1
- package/src/assets/web-panel/assets/{index-BMcimU-2.js → index-DpTHIRfZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJVEOa4i.js → index-DqFXGzzC.js} +1 -1
- package/src/assets/web-panel/assets/{index-BgiXR_-_.js → index-XFe-1twF.js} +1 -1
- package/src/assets/web-panel/assets/{index-BQ3DXBtD.js → index-p2HYn89m.js} +3 -3
- package/src/assets/web-panel/assets/{index-Cvyt-zhm.js → index-pqfux8vZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-nXyk8GZ9.js → index-rjzk9z1Q.js} +1 -1
- package/src/assets/web-panel/assets/{index-RfMr79MW.js → index-th2ihDc0.js} +1 -1
- package/src/assets/web-panel/assets/{index-BtobShro.js → index-u2YVld9W.js} +1 -1
- package/src/assets/web-panel/assets/index-xYybvEic.js +1 -0
- package/src/assets/web-panel/assets/{initDefaultProps-Blmpk7HL.js → initDefaultProps-C3Djh9fa.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DPBJMv1n.js → motion-BzqFmbSe.js} +1 -1
- package/src/assets/web-panel/assets/{move-ButdzCM7.js → move-DJo_FFaW.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-cyRC_QCe.js → mtc-parser-_tlTiBvk.js} +1 -1
- package/src/assets/web-panel/assets/{omit-1epSV7hz.js → omit-CB2ZyxUg.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DuPY8xJs.js → pickAttrs-DZwIFD4u.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-6XZqarhe.js → placementArrow-DxZY0dgk.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-B40pXBWM.js → responsiveObserve-CzzGt7GL.js} +1 -1
- package/src/assets/web-panel/assets/{slide-jbZ9Wu4n.js → slide-BaIK52yz.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BKQQcVy4.js → statusUtils-3-FHu9_M.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-NiKfWjUT.js → styleChecker-BkRT68fK.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-bQUCvPeb.js → useFlexGapSupport-DHYwKS-s.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CUGji7ph.js → useFs-DVVUhV8p.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-4veyN2N2.js → usePersonalDataHub-CkrFKAzQ.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-1-nLug-Z.js → vnode-BuBLFz0c.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-dhCeRDMo.js → zoom-kpm6LtXU.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/cowork.js +283 -61
- package/src/data/changelog.json +15 -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/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-protocol.js +154 -10
- package/src/lib/background-agent-supervisor.js +840 -48
- package/src/lib/background-session-transport.js +23 -6
- package/src/lib/background-turn-bootstrap-protocol.js +64 -0
- package/src/lib/cowork-workflow.js +853 -61
- package/src/lib/delivery-production-adapter.js +38 -2
- package/src/lib/dynamic-workflow-facade.js +1105 -17
- package/src/lib/mcp-stdio-package-materialization.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 +604 -40
- 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/with-file-lock.js +73 -38
- package/src/repl/agent-repl.js +70 -29
- package/src/repl/remote-approval.js +29 -7
- package/src/runtime/agent-core.js +1099 -152
- package/src/runtime/headless-runner.js +22 -5
- package/src/runtime/headless-stream.js +19 -3
- package/src/workers/background-agent-keeper.js +249 -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/MobileProjects-BL0Czcoj.js +0 -1
- package/src/assets/web-panel/assets/RemoteSession-Dxzl5n2T.js +0 -5
- 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
|
@@ -44,6 +44,8 @@ export const REMOTE_MEMBERSHIP_COORDINATOR_UNAVAILABLE_CODE =
|
|
|
44
44
|
"CC_REMOTE_MEMBERSHIP_COORDINATOR_UNAVAILABLE";
|
|
45
45
|
export const REMOTE_MEMBERSHIP_COORDINATOR_ROLLBACK_CODE =
|
|
46
46
|
"CC_REMOTE_MEMBERSHIP_COORDINATOR_ROLLBACK";
|
|
47
|
+
export const REMOTE_MEMBERSHIP_NOT_ACTIVE_CODE =
|
|
48
|
+
"CC_REMOTE_MEMBERSHIP_NOT_ACTIVE";
|
|
47
49
|
|
|
48
50
|
const STORE_VERSION = 1;
|
|
49
51
|
const KEY_VERSION = 1;
|
|
@@ -62,6 +64,7 @@ const NON_NEGATIVE_RE = /^(?:0|[1-9]\d*)$/;
|
|
|
62
64
|
const VALID_SCOPES = new Set(["observe", "prompt", "approve", "interrupt"]);
|
|
63
65
|
const VALID_TYPES = new Set([
|
|
64
66
|
"session.created",
|
|
67
|
+
"session.reenabled",
|
|
65
68
|
"member.joined",
|
|
66
69
|
"member.revoked",
|
|
67
70
|
"session.closed",
|
|
@@ -75,6 +78,19 @@ const MAX_LEASE_TTL_MS = 60_000;
|
|
|
75
78
|
const DEFAULT_CHALLENGE_TTL_MS = 30_000;
|
|
76
79
|
const MAX_CHALLENGE_TTL_MS = 60_000;
|
|
77
80
|
const BASE64URL_RE = /^[A-Za-z0-9_-]+$/;
|
|
81
|
+
export const REMOTE_APPROVAL_BINDING_CAPABILITY = "approval-binding-v1";
|
|
82
|
+
const DEFAULT_JOIN_POLICY_VERSION = "remote-session-policy:unrestricted-v1";
|
|
83
|
+
const JOIN_POLICY_EVENT_FIELDS = Object.freeze([
|
|
84
|
+
"policyAllowedScopes",
|
|
85
|
+
"policyAllowRelayPairing",
|
|
86
|
+
"policyMaxDevices",
|
|
87
|
+
"policyVersion",
|
|
88
|
+
]);
|
|
89
|
+
const MEMBER_JOIN_AUTHORITY_FIELDS = Object.freeze([
|
|
90
|
+
"capabilities",
|
|
91
|
+
"joinVia",
|
|
92
|
+
...JOIN_POLICY_EVENT_FIELDS,
|
|
93
|
+
]);
|
|
78
94
|
|
|
79
95
|
const COMMON_EVENT_FIELDS = Object.freeze([
|
|
80
96
|
"eventHash",
|
|
@@ -97,6 +113,22 @@ const EVENT_FIELDS = Object.freeze({
|
|
|
97
113
|
"hostCredentialType",
|
|
98
114
|
"hostPrincipalId",
|
|
99
115
|
"membershipEpoch",
|
|
116
|
+
...JOIN_POLICY_EVENT_FIELDS,
|
|
117
|
+
"scopes",
|
|
118
|
+
"sessionEpoch",
|
|
119
|
+
]),
|
|
120
|
+
"session.reenabled": Object.freeze([
|
|
121
|
+
...COMMON_EVENT_FIELDS,
|
|
122
|
+
"expectedHostMembershipEpoch",
|
|
123
|
+
"expectedSessionEpoch",
|
|
124
|
+
"expiresAt",
|
|
125
|
+
"hostPrincipalId",
|
|
126
|
+
"membershipEpoch",
|
|
127
|
+
"nextHostCredentialDigest",
|
|
128
|
+
"nextHostCredentialPublicKey",
|
|
129
|
+
"nextHostCredentialType",
|
|
130
|
+
"nextHostPrincipalId",
|
|
131
|
+
...JOIN_POLICY_EVENT_FIELDS,
|
|
100
132
|
"scopes",
|
|
101
133
|
"sessionEpoch",
|
|
102
134
|
]),
|
|
@@ -109,6 +141,7 @@ const EVENT_FIELDS = Object.freeze({
|
|
|
109
141
|
"membershipEpoch",
|
|
110
142
|
"previousMembershipEpoch",
|
|
111
143
|
"principalId",
|
|
144
|
+
...MEMBER_JOIN_AUTHORITY_FIELDS,
|
|
112
145
|
"scopes",
|
|
113
146
|
]),
|
|
114
147
|
"member.revoked": Object.freeze([
|
|
@@ -172,6 +205,19 @@ const EVENT_FIELDS = Object.freeze({
|
|
|
172
205
|
]),
|
|
173
206
|
});
|
|
174
207
|
|
|
208
|
+
const LEGACY_EVENT_FIELDS = Object.freeze({
|
|
209
|
+
"session.created": Object.freeze(
|
|
210
|
+
EVENT_FIELDS["session.created"].filter(
|
|
211
|
+
(field) => !JOIN_POLICY_EVENT_FIELDS.includes(field),
|
|
212
|
+
),
|
|
213
|
+
),
|
|
214
|
+
"member.joined": Object.freeze(
|
|
215
|
+
EVENT_FIELDS["member.joined"].filter(
|
|
216
|
+
(field) => !MEMBER_JOIN_AUTHORITY_FIELDS.includes(field),
|
|
217
|
+
),
|
|
218
|
+
),
|
|
219
|
+
});
|
|
220
|
+
|
|
175
221
|
function coordinatorError(code, message, cause = null, details = {}) {
|
|
176
222
|
const error = new Error(message, cause ? { cause } : undefined);
|
|
177
223
|
error.name = "RemoteMembershipCoordinatorError";
|
|
@@ -183,7 +229,8 @@ function coordinatorError(code, message, cause = null, details = {}) {
|
|
|
183
229
|
function unavailable(cause, filePath) {
|
|
184
230
|
if (
|
|
185
231
|
cause?.code === REMOTE_MEMBERSHIP_COORDINATOR_UNAVAILABLE_CODE ||
|
|
186
|
-
cause?.code === REMOTE_MEMBERSHIP_COORDINATOR_ROLLBACK_CODE
|
|
232
|
+
cause?.code === REMOTE_MEMBERSHIP_COORDINATOR_ROLLBACK_CODE ||
|
|
233
|
+
cause?.code === REMOTE_MEMBERSHIP_NOT_ACTIVE_CODE
|
|
187
234
|
) {
|
|
188
235
|
return cause;
|
|
189
236
|
}
|
|
@@ -244,6 +291,141 @@ function normalizeScopes(scopes) {
|
|
|
244
291
|
return normalized;
|
|
245
292
|
}
|
|
246
293
|
|
|
294
|
+
function normalizeCapabilities(capabilities) {
|
|
295
|
+
if (capabilities == null) return [];
|
|
296
|
+
if (!Array.isArray(capabilities) || capabilities.length > 32) {
|
|
297
|
+
throw new TypeError("membership capabilities must be a bounded array");
|
|
298
|
+
}
|
|
299
|
+
return [
|
|
300
|
+
...new Set(
|
|
301
|
+
capabilities.map((capability) => {
|
|
302
|
+
if (typeof capability !== "string") {
|
|
303
|
+
throw new TypeError("membership capability must be a string");
|
|
304
|
+
}
|
|
305
|
+
return requiredString(capability, "membership capability", 128);
|
|
306
|
+
}),
|
|
307
|
+
),
|
|
308
|
+
].sort();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function normalizeJoinPolicy(policy = null) {
|
|
312
|
+
const value = policy || {};
|
|
313
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
314
|
+
throw new TypeError("membership join policy must be an object");
|
|
315
|
+
}
|
|
316
|
+
const allowedScopes =
|
|
317
|
+
value.allowedScopes == null ? null : normalizeScopes(value.allowedScopes);
|
|
318
|
+
const maxDevices = value.maxDevices == null ? null : value.maxDevices;
|
|
319
|
+
if (
|
|
320
|
+
maxDevices !== null &&
|
|
321
|
+
(!Number.isSafeInteger(maxDevices) ||
|
|
322
|
+
maxDevices < 0 ||
|
|
323
|
+
maxDevices > 1_000_000)
|
|
324
|
+
) {
|
|
325
|
+
throw new TypeError(
|
|
326
|
+
"membership policy maxDevices must be a bounded integer",
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
if (
|
|
330
|
+
value.allowRelayPairing != null &&
|
|
331
|
+
typeof value.allowRelayPairing !== "boolean"
|
|
332
|
+
) {
|
|
333
|
+
throw new TypeError("membership policy allowRelayPairing must be boolean");
|
|
334
|
+
}
|
|
335
|
+
return Object.freeze({
|
|
336
|
+
policyVersion:
|
|
337
|
+
value.policyVersion == null
|
|
338
|
+
? DEFAULT_JOIN_POLICY_VERSION
|
|
339
|
+
: requiredString(value.policyVersion, "policyVersion", 512),
|
|
340
|
+
allowedScopes:
|
|
341
|
+
allowedScopes === null ? null : Object.freeze([...allowedScopes]),
|
|
342
|
+
maxDevices,
|
|
343
|
+
allowRelayPairing: value.allowRelayPairing !== false,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function joinPolicyEventFields(policy) {
|
|
348
|
+
const normalized = normalizeJoinPolicy(policy);
|
|
349
|
+
return {
|
|
350
|
+
policyVersion: normalized.policyVersion,
|
|
351
|
+
policyAllowedScopes:
|
|
352
|
+
normalized.allowedScopes === null ? null : [...normalized.allowedScopes],
|
|
353
|
+
policyMaxDevices: normalized.maxDevices,
|
|
354
|
+
policyAllowRelayPairing: normalized.allowRelayPairing,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function joinPolicyFromEvent(event, label) {
|
|
359
|
+
const present = JOIN_POLICY_EVENT_FIELDS.filter((field) =>
|
|
360
|
+
Object.prototype.hasOwnProperty.call(event, field),
|
|
361
|
+
);
|
|
362
|
+
if (present.length === 0) return null;
|
|
363
|
+
if (present.length !== JOIN_POLICY_EVENT_FIELDS.length) {
|
|
364
|
+
throw new TypeError(`${label} has an incomplete membership join policy`);
|
|
365
|
+
}
|
|
366
|
+
return normalizeJoinPolicy({
|
|
367
|
+
policyVersion: event.policyVersion,
|
|
368
|
+
allowedScopes: event.policyAllowedScopes,
|
|
369
|
+
maxDevices: event.policyMaxDevices,
|
|
370
|
+
allowRelayPairing: event.policyAllowRelayPairing,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function joinPoliciesMatch(left, right) {
|
|
375
|
+
return (
|
|
376
|
+
canonicalJson(normalizeJoinPolicy(left)) ===
|
|
377
|
+
canonicalJson(normalizeJoinPolicy(right))
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function enforceJoinAuthority(session, { scopes, via, capabilities, policy }) {
|
|
382
|
+
const normalizedPolicy = normalizeJoinPolicy(policy);
|
|
383
|
+
if (
|
|
384
|
+
session.joinPolicy &&
|
|
385
|
+
!joinPoliciesMatch(session.joinPolicy, normalizedPolicy)
|
|
386
|
+
) {
|
|
387
|
+
throw new Error("Remote membership policy version or authority changed");
|
|
388
|
+
}
|
|
389
|
+
if (via !== "direct" && via !== "relay") {
|
|
390
|
+
throw new TypeError("membership join transport is unsupported");
|
|
391
|
+
}
|
|
392
|
+
if (via === "relay" && !normalizedPolicy.allowRelayPairing) {
|
|
393
|
+
throw new Error("Relay pairing is disabled by membership policy");
|
|
394
|
+
}
|
|
395
|
+
const normalizedCapabilities = normalizeCapabilities(capabilities);
|
|
396
|
+
let grantedScopes = normalizeScopes(scopes);
|
|
397
|
+
if (normalizedPolicy.allowedScopes !== null) {
|
|
398
|
+
grantedScopes = grantedScopes.filter((scope) =>
|
|
399
|
+
normalizedPolicy.allowedScopes.includes(scope),
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
if (!normalizedCapabilities.includes(REMOTE_APPROVAL_BINDING_CAPABILITY)) {
|
|
403
|
+
grantedScopes = grantedScopes.filter((scope) => scope !== "approve");
|
|
404
|
+
}
|
|
405
|
+
if (grantedScopes.length === 0) {
|
|
406
|
+
throw new Error(
|
|
407
|
+
"Remote membership scopes are denied by policy or capability",
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
const deviceCount = [...session.members.values()].filter(
|
|
411
|
+
(member) =>
|
|
412
|
+
member.principalId !== session.hostPrincipalId &&
|
|
413
|
+
member.status === "active",
|
|
414
|
+
).length;
|
|
415
|
+
if (
|
|
416
|
+
normalizedPolicy.maxDevices !== null &&
|
|
417
|
+
deviceCount >= normalizedPolicy.maxDevices
|
|
418
|
+
) {
|
|
419
|
+
throw new Error("Remote membership device limit reached");
|
|
420
|
+
}
|
|
421
|
+
return Object.freeze({
|
|
422
|
+
policy: normalizedPolicy,
|
|
423
|
+
scopes: Object.freeze([...grantedScopes]),
|
|
424
|
+
capabilities: Object.freeze([...normalizedCapabilities]),
|
|
425
|
+
via,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
247
429
|
function canonicalJson(value) {
|
|
248
430
|
if (
|
|
249
431
|
value === null ||
|
|
@@ -469,7 +651,13 @@ function replayStore(store, coordinatorId, { missing = false } = {}) {
|
|
|
469
651
|
if (event.schema !== EVENT_SCHEMA || !VALID_TYPES.has(event.type)) {
|
|
470
652
|
throw new TypeError(`${label} has an unsupported schema or type`);
|
|
471
653
|
}
|
|
472
|
-
if (
|
|
654
|
+
if (
|
|
655
|
+
!hasExactKeys(event, EVENT_FIELDS[event.type]) &&
|
|
656
|
+
!(
|
|
657
|
+
LEGACY_EVENT_FIELDS[event.type] &&
|
|
658
|
+
hasExactKeys(event, LEGACY_EVENT_FIELDS[event.type])
|
|
659
|
+
)
|
|
660
|
+
) {
|
|
473
661
|
throw new TypeError(
|
|
474
662
|
`${label} has non-canonical fields for ${event.type}`,
|
|
475
663
|
);
|
|
@@ -554,6 +742,7 @@ function replayStore(store, coordinatorId, { missing = false } = {}) {
|
|
|
554
742
|
createdAt: event.occurredAtMs,
|
|
555
743
|
expiresAt,
|
|
556
744
|
status: "active",
|
|
745
|
+
joinPolicy: joinPolicyFromEvent(event, label),
|
|
557
746
|
members: new Map([
|
|
558
747
|
[
|
|
559
748
|
hostPrincipalId,
|
|
@@ -565,6 +754,7 @@ function replayStore(store, coordinatorId, { missing = false } = {}) {
|
|
|
565
754
|
credentialType: hostCredential.type,
|
|
566
755
|
credentialPublicKey: hostCredential.publicKey,
|
|
567
756
|
credentialDigest: hostCredential.digest,
|
|
757
|
+
capabilities: [],
|
|
568
758
|
},
|
|
569
759
|
],
|
|
570
760
|
]),
|
|
@@ -584,12 +774,83 @@ function replayStore(store, coordinatorId, { missing = false } = {}) {
|
|
|
584
774
|
}
|
|
585
775
|
if (
|
|
586
776
|
event.type !== "session.closed" &&
|
|
777
|
+
event.type !== "session.reenabled" &&
|
|
587
778
|
event.occurredAtMs >= session.expiresAt
|
|
588
779
|
) {
|
|
589
780
|
throw new TypeError(`${label} occurs after session expiry`);
|
|
590
781
|
}
|
|
591
782
|
|
|
592
|
-
if (event.type === "
|
|
783
|
+
if (event.type === "session.reenabled") {
|
|
784
|
+
if (session.status !== "closed") {
|
|
785
|
+
throw new TypeError(`${label} re-enables an active session`);
|
|
786
|
+
}
|
|
787
|
+
const currentHost = session.members.get(session.hostPrincipalId);
|
|
788
|
+
if (
|
|
789
|
+
event.hostPrincipalId !== session.hostPrincipalId ||
|
|
790
|
+
!currentHost ||
|
|
791
|
+
event.expectedHostMembershipEpoch !== currentHost.membershipEpoch
|
|
792
|
+
) {
|
|
793
|
+
throw new TypeError(
|
|
794
|
+
`${label} was not re-enabled by the current host`,
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
const nextSessionEpoch = String(
|
|
798
|
+
canonicalEpoch(event.sessionEpoch, `${label}.sessionEpoch`),
|
|
799
|
+
);
|
|
800
|
+
if (nextSessionEpoch !== String(BigInt(session.sessionEpoch) + 1n)) {
|
|
801
|
+
throw new TypeError(`${label} does not advance the session epoch`);
|
|
802
|
+
}
|
|
803
|
+
const nextHost = normalizePrincipalCredential(
|
|
804
|
+
{
|
|
805
|
+
type: event.nextHostCredentialType,
|
|
806
|
+
publicKey: event.nextHostCredentialPublicKey,
|
|
807
|
+
},
|
|
808
|
+
`${label}.nextHostCredential`,
|
|
809
|
+
);
|
|
810
|
+
if (
|
|
811
|
+
nextHost.digest !== event.nextHostCredentialDigest ||
|
|
812
|
+
nextHost.principalId !== event.nextHostPrincipalId
|
|
813
|
+
) {
|
|
814
|
+
throw new TypeError(`${label}.nextHostPrincipalId is not key-bound`);
|
|
815
|
+
}
|
|
816
|
+
const priorNextHost = session.members.get(nextHost.principalId) || null;
|
|
817
|
+
const nextMembershipEpoch = String(
|
|
818
|
+
canonicalEpoch(event.membershipEpoch, `${label}.membershipEpoch`),
|
|
819
|
+
);
|
|
820
|
+
const expectedMembershipEpoch = priorNextHost
|
|
821
|
+
? String(BigInt(priorNextHost.membershipEpoch) + 1n)
|
|
822
|
+
: "1";
|
|
823
|
+
if (nextMembershipEpoch !== expectedMembershipEpoch) {
|
|
824
|
+
throw new TypeError(`${label} does not advance the new host epoch`);
|
|
825
|
+
}
|
|
826
|
+
const expiresAt = safeTimestamp(event.expiresAt, `${label}.expiresAt`);
|
|
827
|
+
if (expiresAt <= event.occurredAtMs) {
|
|
828
|
+
throw new TypeError(`${label}.expiresAt must be in the future`);
|
|
829
|
+
}
|
|
830
|
+
for (const member of session.members.values()) {
|
|
831
|
+
if (member.status === "active") {
|
|
832
|
+
member.status = "revoked";
|
|
833
|
+
member.membershipEpoch = String(
|
|
834
|
+
BigInt(member.membershipEpoch) + 1n,
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
session.members.set(nextHost.principalId, {
|
|
839
|
+
principalId: nextHost.principalId,
|
|
840
|
+
membershipEpoch: nextMembershipEpoch,
|
|
841
|
+
status: "active",
|
|
842
|
+
scopes: normalizeScopes(event.scopes),
|
|
843
|
+
credentialType: nextHost.type,
|
|
844
|
+
credentialPublicKey: nextHost.publicKey,
|
|
845
|
+
credentialDigest: nextHost.digest,
|
|
846
|
+
capabilities: [],
|
|
847
|
+
});
|
|
848
|
+
session.hostPrincipalId = nextHost.principalId;
|
|
849
|
+
session.sessionEpoch = nextSessionEpoch;
|
|
850
|
+
session.expiresAt = expiresAt;
|
|
851
|
+
session.status = "active";
|
|
852
|
+
session.joinPolicy = joinPolicyFromEvent(event, label);
|
|
853
|
+
} else if (event.type === "member.joined") {
|
|
593
854
|
if (session.status !== "active") {
|
|
594
855
|
throw new TypeError(`${label} joins a closed session`);
|
|
595
856
|
}
|
|
@@ -638,14 +899,56 @@ function replayStore(store, coordinatorId, { missing = false } = {}) {
|
|
|
638
899
|
if (membershipEpoch !== expected) {
|
|
639
900
|
throw new TypeError(`${label} does not advance the principal epoch`);
|
|
640
901
|
}
|
|
902
|
+
const eventPolicy = joinPolicyFromEvent(event, label);
|
|
903
|
+
const joinVia = Object.prototype.hasOwnProperty.call(event, "joinVia")
|
|
904
|
+
? requiredString(event.joinVia, `${label}.joinVia`, 16)
|
|
905
|
+
: "direct";
|
|
906
|
+
const capabilities = Object.prototype.hasOwnProperty.call(
|
|
907
|
+
event,
|
|
908
|
+
"capabilities",
|
|
909
|
+
)
|
|
910
|
+
? normalizeCapabilities(event.capabilities)
|
|
911
|
+
: [];
|
|
912
|
+
const declaredScopes = normalizeScopes(event.scopes);
|
|
913
|
+
// Pre-capability stores could persist `approve` without proving that the
|
|
914
|
+
// client understood the exact approval tuple. Preserve non-approval
|
|
915
|
+
// membership on upgrade, but never grandfather that authority through
|
|
916
|
+
// replay. A legacy approve-only member remains counted as active with no
|
|
917
|
+
// usable scopes rather than silently freeing the device-cap slot.
|
|
918
|
+
const replayedScopes = eventPolicy
|
|
919
|
+
? declaredScopes
|
|
920
|
+
: declaredScopes.filter((scope) => scope !== "approve");
|
|
921
|
+
if (eventPolicy) {
|
|
922
|
+
if (
|
|
923
|
+
session.joinPolicy &&
|
|
924
|
+
!joinPoliciesMatch(session.joinPolicy, eventPolicy)
|
|
925
|
+
) {
|
|
926
|
+
throw new TypeError(`${label} changes the session join policy`);
|
|
927
|
+
}
|
|
928
|
+
session.joinPolicy = eventPolicy;
|
|
929
|
+
const authority = enforceJoinAuthority(session, {
|
|
930
|
+
scopes: event.scopes,
|
|
931
|
+
via: joinVia,
|
|
932
|
+
capabilities,
|
|
933
|
+
policy: eventPolicy,
|
|
934
|
+
});
|
|
935
|
+
if (
|
|
936
|
+
canonicalJson(authority.scopes) !== canonicalJson(declaredScopes)
|
|
937
|
+
) {
|
|
938
|
+
throw new TypeError(
|
|
939
|
+
`${label} grants scopes outside join authority`,
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
641
943
|
session.members.set(principalId, {
|
|
642
944
|
principalId,
|
|
643
945
|
membershipEpoch,
|
|
644
946
|
status: "active",
|
|
645
|
-
scopes:
|
|
947
|
+
scopes: replayedScopes,
|
|
646
948
|
credentialType: credential.type,
|
|
647
949
|
credentialPublicKey: credential.publicKey,
|
|
648
950
|
credentialDigest: credential.digest,
|
|
951
|
+
capabilities,
|
|
649
952
|
});
|
|
650
953
|
} else if (event.type === "member.revoked") {
|
|
651
954
|
if (session.status !== "active") {
|
|
@@ -1461,6 +1764,11 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1461
1764
|
member,
|
|
1462
1765
|
credential,
|
|
1463
1766
|
scopes = null,
|
|
1767
|
+
capabilities = null,
|
|
1768
|
+
joinPolicy = null,
|
|
1769
|
+
joinVia = null,
|
|
1770
|
+
nextCredential = null,
|
|
1771
|
+
nextSessionExpiresAt = null,
|
|
1464
1772
|
connectionNonce,
|
|
1465
1773
|
now,
|
|
1466
1774
|
ttlMs = null,
|
|
@@ -1506,6 +1814,20 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1506
1814
|
connectionNonce: requiredString(connectionNonce, "connectionNonce", 4096),
|
|
1507
1815
|
serverNonce: requiredString(this._createSecret(), "serverNonce", 4096),
|
|
1508
1816
|
scopes: scopes === null ? null : Object.freeze([...scopes]),
|
|
1817
|
+
capabilities:
|
|
1818
|
+
capabilities === null
|
|
1819
|
+
? null
|
|
1820
|
+
: Object.freeze([...normalizeCapabilities(capabilities)]),
|
|
1821
|
+
joinPolicy:
|
|
1822
|
+
joinPolicy === null
|
|
1823
|
+
? null
|
|
1824
|
+
: Object.freeze({ ...normalizeJoinPolicy(joinPolicy) }),
|
|
1825
|
+
joinVia,
|
|
1826
|
+
nextCredentialType: nextCredential?.type || null,
|
|
1827
|
+
nextCredentialPublicKey: nextCredential?.publicKey || null,
|
|
1828
|
+
nextCredentialKeySha256: nextCredential?.digest || null,
|
|
1829
|
+
nextPrincipalId: nextCredential?.principalId || null,
|
|
1830
|
+
nextSessionExpiresAt,
|
|
1509
1831
|
issuedAtMs: now,
|
|
1510
1832
|
expiresAtMs,
|
|
1511
1833
|
});
|
|
@@ -1520,12 +1842,16 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1520
1842
|
credentialPublicKey,
|
|
1521
1843
|
connectionNonce,
|
|
1522
1844
|
ttlMs = null,
|
|
1845
|
+
capabilities = null,
|
|
1846
|
+
joinPolicy = null,
|
|
1523
1847
|
} = {}) {
|
|
1524
1848
|
const normalizedSessionId = requiredString(sessionId, "sessionId");
|
|
1525
1849
|
const sessionEpoch = String(
|
|
1526
1850
|
canonicalEpoch(expectedSessionEpoch, "expectedSessionEpoch"),
|
|
1527
1851
|
);
|
|
1528
1852
|
const normalizedScopes = normalizeScopes(scopes);
|
|
1853
|
+
const normalizedCapabilities = normalizeCapabilities(capabilities);
|
|
1854
|
+
const normalizedJoinPolicy = normalizeJoinPolicy(joinPolicy);
|
|
1529
1855
|
const credential = normalizePrincipalCredential(
|
|
1530
1856
|
{ type: "ed25519", publicKey: credentialPublicKey },
|
|
1531
1857
|
"credential",
|
|
@@ -1544,6 +1870,12 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1544
1870
|
if (prior?.status === "active") {
|
|
1545
1871
|
throw new Error("Remote membership principal is already active");
|
|
1546
1872
|
}
|
|
1873
|
+
enforceJoinAuthority(session, {
|
|
1874
|
+
scopes: normalizedScopes,
|
|
1875
|
+
via: "direct",
|
|
1876
|
+
capabilities: normalizedCapabilities,
|
|
1877
|
+
policy: normalizedJoinPolicy,
|
|
1878
|
+
});
|
|
1547
1879
|
return this._newAuthenticationChallenge({
|
|
1548
1880
|
purpose: "member.join",
|
|
1549
1881
|
session,
|
|
@@ -1554,6 +1886,9 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1554
1886
|
},
|
|
1555
1887
|
credential,
|
|
1556
1888
|
scopes: normalizedScopes,
|
|
1889
|
+
capabilities: normalizedCapabilities,
|
|
1890
|
+
joinPolicy: normalizedJoinPolicy,
|
|
1891
|
+
joinVia: "direct",
|
|
1557
1892
|
connectionNonce,
|
|
1558
1893
|
now,
|
|
1559
1894
|
ttlMs,
|
|
@@ -1580,7 +1915,10 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1580
1915
|
member.status !== "active" ||
|
|
1581
1916
|
member.credentialType !== "ed25519"
|
|
1582
1917
|
) {
|
|
1583
|
-
throw
|
|
1918
|
+
throw coordinatorError(
|
|
1919
|
+
REMOTE_MEMBERSHIP_NOT_ACTIVE_CODE,
|
|
1920
|
+
"Remote membership is not active for resume",
|
|
1921
|
+
);
|
|
1584
1922
|
}
|
|
1585
1923
|
return this._newAuthenticationChallenge({
|
|
1586
1924
|
purpose: "session.resume",
|
|
@@ -1600,6 +1938,65 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1600
1938
|
});
|
|
1601
1939
|
}
|
|
1602
1940
|
|
|
1941
|
+
issueSessionReenableChallenge({
|
|
1942
|
+
sessionId,
|
|
1943
|
+
principalId,
|
|
1944
|
+
connectionNonce,
|
|
1945
|
+
newHostCredentialPublicKeySpki = null,
|
|
1946
|
+
scopes,
|
|
1947
|
+
expiresAt,
|
|
1948
|
+
joinPolicy = null,
|
|
1949
|
+
ttlMs = null,
|
|
1950
|
+
} = {}) {
|
|
1951
|
+
const normalizedSessionId = requiredString(sessionId, "sessionId");
|
|
1952
|
+
const normalizedPrincipal = requiredString(principalId, "principalId");
|
|
1953
|
+
const normalizedScopes = normalizeScopes(scopes);
|
|
1954
|
+
const normalizedExpiresAt = safeTimestamp(expiresAt, "expiresAt");
|
|
1955
|
+
const normalizedJoinPolicy = normalizeJoinPolicy(joinPolicy);
|
|
1956
|
+
return this._withRead((snapshot, now) => {
|
|
1957
|
+
const session = snapshot.sessions.get(normalizedSessionId);
|
|
1958
|
+
const member = session?.members.get(normalizedPrincipal);
|
|
1959
|
+
if (
|
|
1960
|
+
!session ||
|
|
1961
|
+
session.status !== "closed" ||
|
|
1962
|
+
session.hostPrincipalId !== normalizedPrincipal ||
|
|
1963
|
+
!member ||
|
|
1964
|
+
member.status !== "active" ||
|
|
1965
|
+
member.credentialType !== "ed25519" ||
|
|
1966
|
+
normalizedExpiresAt <= now
|
|
1967
|
+
) {
|
|
1968
|
+
throw new Error("Remote membership session re-enable challenge denied");
|
|
1969
|
+
}
|
|
1970
|
+
const nextCredential = normalizePrincipalCredential(
|
|
1971
|
+
{
|
|
1972
|
+
type: "ed25519",
|
|
1973
|
+
publicKey:
|
|
1974
|
+
newHostCredentialPublicKeySpki || member.credentialPublicKey,
|
|
1975
|
+
},
|
|
1976
|
+
"nextHostCredential",
|
|
1977
|
+
);
|
|
1978
|
+
return this._newAuthenticationChallenge({
|
|
1979
|
+
purpose: "session.reenable",
|
|
1980
|
+
session,
|
|
1981
|
+
member,
|
|
1982
|
+
credential: normalizePrincipalCredential(
|
|
1983
|
+
{
|
|
1984
|
+
type: member.credentialType,
|
|
1985
|
+
publicKey: member.credentialPublicKey,
|
|
1986
|
+
},
|
|
1987
|
+
"memberCredential",
|
|
1988
|
+
),
|
|
1989
|
+
scopes: normalizedScopes,
|
|
1990
|
+
joinPolicy: normalizedJoinPolicy,
|
|
1991
|
+
nextCredential,
|
|
1992
|
+
nextSessionExpiresAt: normalizedExpiresAt,
|
|
1993
|
+
connectionNonce,
|
|
1994
|
+
now,
|
|
1995
|
+
ttlMs,
|
|
1996
|
+
});
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
|
|
1603
2000
|
_consumeAuthenticationChallenge(
|
|
1604
2001
|
snapshot,
|
|
1605
2002
|
now,
|
|
@@ -1622,10 +2019,11 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1622
2019
|
throw new Error("Remote membership authentication challenge denied");
|
|
1623
2020
|
}
|
|
1624
2021
|
const session = snapshot.sessions.get(challenge.sessionId);
|
|
2022
|
+
const reenable = expectedPurpose === "session.reenable";
|
|
1625
2023
|
if (
|
|
1626
2024
|
!session ||
|
|
1627
|
-
session.status !== "active" ||
|
|
1628
|
-
session.expiresAt <= now ||
|
|
2025
|
+
(reenable ? session.status !== "closed" : session.status !== "active") ||
|
|
2026
|
+
(!reenable && session.expiresAt <= now) ||
|
|
1629
2027
|
session.sessionEpoch !== challenge.sessionEpoch
|
|
1630
2028
|
) {
|
|
1631
2029
|
throw new Error("Remote membership authentication session is stale");
|
|
@@ -1643,7 +2041,8 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1643
2041
|
!current ||
|
|
1644
2042
|
current.status !== "active" ||
|
|
1645
2043
|
current.membershipEpoch !== challenge.membershipEpoch ||
|
|
1646
|
-
current.credentialDigest !== challenge.credentialKeySha256
|
|
2044
|
+
current.credentialDigest !== challenge.credentialKeySha256 ||
|
|
2045
|
+
(reenable && session.hostPrincipalId !== challenge.principalId)
|
|
1647
2046
|
) {
|
|
1648
2047
|
throw new Error("Remote membership resume challenge is stale");
|
|
1649
2048
|
}
|
|
@@ -1685,11 +2084,13 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1685
2084
|
expiresAt,
|
|
1686
2085
|
hostPrincipalId = null,
|
|
1687
2086
|
hostCredentialPublicKeySpki,
|
|
2087
|
+
joinPolicy = null,
|
|
1688
2088
|
} = {}) {
|
|
1689
2089
|
const normalizedSessionId = requiredString(sessionId, "sessionId");
|
|
1690
2090
|
const normalizedAgent = requiredString(agentSessionId, "agentSessionId");
|
|
1691
2091
|
const normalizedScopes = normalizeScopes(scopes);
|
|
1692
2092
|
const normalizedExpiresAt = safeTimestamp(expiresAt, "expiresAt");
|
|
2093
|
+
const normalizedJoinPolicy = normalizeJoinPolicy(joinPolicy);
|
|
1693
2094
|
const hostCredential = normalizePrincipalCredential(
|
|
1694
2095
|
{
|
|
1695
2096
|
type: "ed25519",
|
|
@@ -1722,6 +2123,7 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1722
2123
|
hostCredentialDigest: hostCredential.digest,
|
|
1723
2124
|
sessionEpoch: "1",
|
|
1724
2125
|
membershipEpoch: "1",
|
|
2126
|
+
...joinPolicyEventFields(normalizedJoinPolicy),
|
|
1725
2127
|
scopes: normalizedScopes,
|
|
1726
2128
|
expiresAt: normalizedExpiresAt,
|
|
1727
2129
|
};
|
|
@@ -1774,6 +2176,73 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1774
2176
|
}
|
|
1775
2177
|
}
|
|
1776
2178
|
|
|
2179
|
+
reenableSession({ challengeId, connectionNonce, signature } = {}) {
|
|
2180
|
+
const result = this._mutate((snapshot, _generation, now) => {
|
|
2181
|
+
const authenticated = this._consumeAuthenticationChallenge(
|
|
2182
|
+
snapshot,
|
|
2183
|
+
now,
|
|
2184
|
+
{ challengeId, connectionNonce, signature },
|
|
2185
|
+
"session.reenable",
|
|
2186
|
+
);
|
|
2187
|
+
const { challenge, session } = authenticated;
|
|
2188
|
+
const nextCredential = normalizePrincipalCredential(
|
|
2189
|
+
{
|
|
2190
|
+
type: challenge.nextCredentialType,
|
|
2191
|
+
publicKey: challenge.nextCredentialPublicKey,
|
|
2192
|
+
},
|
|
2193
|
+
"nextHostCredential",
|
|
2194
|
+
);
|
|
2195
|
+
if (
|
|
2196
|
+
nextCredential.digest !== challenge.nextCredentialKeySha256 ||
|
|
2197
|
+
nextCredential.principalId !== challenge.nextPrincipalId ||
|
|
2198
|
+
challenge.nextSessionExpiresAt <= now
|
|
2199
|
+
) {
|
|
2200
|
+
throw new Error("Remote membership session re-enable binding changed");
|
|
2201
|
+
}
|
|
2202
|
+
const priorNextHost =
|
|
2203
|
+
session.members.get(nextCredential.principalId) || null;
|
|
2204
|
+
return {
|
|
2205
|
+
type: "session.reenabled",
|
|
2206
|
+
sessionId: session.sessionId,
|
|
2207
|
+
expectedSessionEpoch: session.sessionEpoch,
|
|
2208
|
+
hostPrincipalId: session.hostPrincipalId,
|
|
2209
|
+
expectedHostMembershipEpoch: challenge.membershipEpoch,
|
|
2210
|
+
nextHostPrincipalId: nextCredential.principalId,
|
|
2211
|
+
nextHostCredentialType: nextCredential.type,
|
|
2212
|
+
nextHostCredentialPublicKey: nextCredential.publicKey,
|
|
2213
|
+
nextHostCredentialDigest: nextCredential.digest,
|
|
2214
|
+
sessionEpoch: String(BigInt(session.sessionEpoch) + 1n),
|
|
2215
|
+
membershipEpoch: priorNextHost
|
|
2216
|
+
? String(BigInt(priorNextHost.membershipEpoch) + 1n)
|
|
2217
|
+
: "1",
|
|
2218
|
+
scopes: [...challenge.scopes],
|
|
2219
|
+
expiresAt: challenge.nextSessionExpiresAt,
|
|
2220
|
+
...joinPolicyEventFields(challenge.joinPolicy),
|
|
2221
|
+
};
|
|
2222
|
+
});
|
|
2223
|
+
const session = result.snapshot.sessions.get(result.event.sessionId);
|
|
2224
|
+
const host = session.members.get(session.hostPrincipalId);
|
|
2225
|
+
return Object.freeze({
|
|
2226
|
+
authorityVersion: REMOTE_MEMBERSHIP_COORDINATOR_VERSION,
|
|
2227
|
+
trust: this.trustDescriptor(),
|
|
2228
|
+
sessionId: session.sessionId,
|
|
2229
|
+
sessionEpoch: session.sessionEpoch,
|
|
2230
|
+
hostPrincipalId: session.hostPrincipalId,
|
|
2231
|
+
membershipEpoch: host.membershipEpoch,
|
|
2232
|
+
hostCredentialPublicKeySpki: host.credentialPublicKey,
|
|
2233
|
+
nextConnectionNonce: requiredString(
|
|
2234
|
+
this._createSecret(),
|
|
2235
|
+
"nextConnectionNonce",
|
|
2236
|
+
4096,
|
|
2237
|
+
),
|
|
2238
|
+
statement: this._snapshotStatement(
|
|
2239
|
+
session,
|
|
2240
|
+
result.snapshot.generation,
|
|
2241
|
+
result.event.occurredAtMs,
|
|
2242
|
+
),
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
|
|
1777
2246
|
joinMember({ challengeId, connectionNonce, signature } = {}) {
|
|
1778
2247
|
const result = this._mutate((snapshot, _generation, now) => {
|
|
1779
2248
|
const authenticated = this._consumeAuthenticationChallenge(
|
|
@@ -1786,6 +2255,12 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1786
2255
|
if (session.hostPrincipalId === challenge.principalId) {
|
|
1787
2256
|
throw new Error("Cannot replace the host membership");
|
|
1788
2257
|
}
|
|
2258
|
+
const authority = enforceJoinAuthority(session, {
|
|
2259
|
+
scopes: challenge.scopes,
|
|
2260
|
+
via: challenge.joinVia,
|
|
2261
|
+
capabilities: challenge.capabilities,
|
|
2262
|
+
policy: challenge.joinPolicy,
|
|
2263
|
+
});
|
|
1789
2264
|
return {
|
|
1790
2265
|
type: "member.joined",
|
|
1791
2266
|
sessionId: session.sessionId,
|
|
@@ -1796,7 +2271,10 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1796
2271
|
credentialType: credential.type,
|
|
1797
2272
|
credentialPublicKey: credential.publicKey,
|
|
1798
2273
|
credentialDigest: credential.digest,
|
|
1799
|
-
|
|
2274
|
+
...joinPolicyEventFields(authority.policy),
|
|
2275
|
+
joinVia: authority.via,
|
|
2276
|
+
capabilities: [...authority.capabilities],
|
|
2277
|
+
scopes: [...authority.scopes],
|
|
1800
2278
|
};
|
|
1801
2279
|
});
|
|
1802
2280
|
const session = result.snapshot.sessions.get(result.event.sessionId);
|
|
@@ -1808,6 +2286,8 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1808
2286
|
credentialPublicKey: member.credentialPublicKey,
|
|
1809
2287
|
sessionEpoch: session.sessionEpoch,
|
|
1810
2288
|
membershipEpoch: member.membershipEpoch,
|
|
2289
|
+
scopes: Object.freeze([...member.scopes]),
|
|
2290
|
+
capabilities: Object.freeze([...(member.capabilities || [])]),
|
|
1811
2291
|
nextConnectionNonce: requiredString(
|
|
1812
2292
|
this._createSecret(),
|
|
1813
2293
|
"nextConnectionNonce",
|
|
@@ -1829,12 +2309,16 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1829
2309
|
mobilePublicKey,
|
|
1830
2310
|
pairingTokenDigest,
|
|
1831
2311
|
possessionCapability,
|
|
2312
|
+
capabilities = null,
|
|
2313
|
+
joinPolicy = null,
|
|
1832
2314
|
} = {}) {
|
|
1833
2315
|
const normalizedSessionId = requiredString(sessionId, "sessionId");
|
|
1834
2316
|
const sessionEpoch = String(
|
|
1835
2317
|
canonicalEpoch(expectedSessionEpoch, "expectedSessionEpoch"),
|
|
1836
2318
|
);
|
|
1837
2319
|
const normalizedScopes = normalizeScopes(scopes);
|
|
2320
|
+
const normalizedCapabilities = normalizeCapabilities(capabilities);
|
|
2321
|
+
const normalizedJoinPolicy = normalizeJoinPolicy(joinPolicy);
|
|
1838
2322
|
const normalizedPeer = requiredString(mobilePeerId, "mobilePeerId");
|
|
1839
2323
|
if (!DIGEST_RE.test(String(pairingTokenDigest))) {
|
|
1840
2324
|
throw new TypeError("pairingTokenDigest must be a SHA-256 digest");
|
|
@@ -1880,6 +2364,12 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1880
2364
|
"Remote relay transcript proof is malformed, expired, or over-scoped",
|
|
1881
2365
|
);
|
|
1882
2366
|
}
|
|
2367
|
+
const authority = enforceJoinAuthority(session, {
|
|
2368
|
+
scopes: normalizedScopes,
|
|
2369
|
+
via: "relay",
|
|
2370
|
+
capabilities: normalizedCapabilities,
|
|
2371
|
+
policy: normalizedJoinPolicy,
|
|
2372
|
+
});
|
|
1883
2373
|
return {
|
|
1884
2374
|
type: "member.joined",
|
|
1885
2375
|
sessionId: normalizedSessionId,
|
|
@@ -1892,7 +2382,10 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1892
2382
|
credentialType: credential.type,
|
|
1893
2383
|
credentialPublicKey: credential.publicKey,
|
|
1894
2384
|
credentialDigest: credential.digest,
|
|
1895
|
-
|
|
2385
|
+
...joinPolicyEventFields(authority.policy),
|
|
2386
|
+
joinVia: authority.via,
|
|
2387
|
+
capabilities: [...authority.capabilities],
|
|
2388
|
+
scopes: [...authority.scopes],
|
|
1896
2389
|
};
|
|
1897
2390
|
});
|
|
1898
2391
|
const session = result.snapshot.sessions.get(normalizedSessionId);
|
|
@@ -1904,6 +2397,8 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
1904
2397
|
credentialPublicKey: member.credentialPublicKey,
|
|
1905
2398
|
sessionEpoch: session.sessionEpoch,
|
|
1906
2399
|
membershipEpoch: member.membershipEpoch,
|
|
2400
|
+
scopes: Object.freeze([...member.scopes]),
|
|
2401
|
+
capabilities: Object.freeze([...(member.capabilities || [])]),
|
|
1907
2402
|
statement: this._snapshotStatement(
|
|
1908
2403
|
session,
|
|
1909
2404
|
result.snapshot.generation,
|
|
@@ -2036,41 +2531,81 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
2036
2531
|
"expectedHostMembershipEpoch",
|
|
2037
2532
|
),
|
|
2038
2533
|
);
|
|
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
|
-
|
|
2534
|
+
let result;
|
|
2535
|
+
try {
|
|
2536
|
+
result = this._mutate((snapshot) => {
|
|
2537
|
+
const session = snapshot.sessions.get(normalizedSessionId);
|
|
2538
|
+
const host = session?.members.get(normalizedHost);
|
|
2539
|
+
if (
|
|
2540
|
+
!session ||
|
|
2541
|
+
session.status !== "active" ||
|
|
2542
|
+
session.hostPrincipalId !== normalizedHost ||
|
|
2543
|
+
session.sessionEpoch !== sessionEpoch ||
|
|
2544
|
+
!host ||
|
|
2545
|
+
host.status !== "active" ||
|
|
2546
|
+
host.membershipEpoch !== hostMembershipEpoch
|
|
2547
|
+
) {
|
|
2548
|
+
throw new Error("Remote membership session close denied");
|
|
2549
|
+
}
|
|
2550
|
+
const cancelledLeaseIds = [...session.leases.values()]
|
|
2551
|
+
.filter(
|
|
2552
|
+
(lease) => lease.status === "active" || lease.status === "acked",
|
|
2553
|
+
)
|
|
2554
|
+
.map((lease) => lease.leaseId)
|
|
2555
|
+
.sort();
|
|
2556
|
+
return {
|
|
2557
|
+
type: "session.closed",
|
|
2558
|
+
sessionId: normalizedSessionId,
|
|
2559
|
+
expectedSessionEpoch: sessionEpoch,
|
|
2560
|
+
hostPrincipalId: normalizedHost,
|
|
2561
|
+
expectedHostMembershipEpoch: hostMembershipEpoch,
|
|
2562
|
+
sessionEpoch: String(BigInt(sessionEpoch) + 1n),
|
|
2563
|
+
cancelledLeaseIds,
|
|
2564
|
+
};
|
|
2565
|
+
});
|
|
2566
|
+
} catch (cause) {
|
|
2567
|
+
if (cause?.commitState === "unknown") throw cause;
|
|
2568
|
+
const terminal = this._withRead((snapshot, now) => {
|
|
2569
|
+
const session = snapshot.sessions.get(normalizedSessionId);
|
|
2570
|
+
const host = session?.members.get(normalizedHost);
|
|
2571
|
+
if (
|
|
2572
|
+
!session ||
|
|
2573
|
+
session.status !== "closed" ||
|
|
2574
|
+
session.hostPrincipalId !== normalizedHost ||
|
|
2575
|
+
session.sessionEpoch !== String(BigInt(sessionEpoch) + 1n) ||
|
|
2576
|
+
!host ||
|
|
2577
|
+
host.membershipEpoch !== hostMembershipEpoch
|
|
2578
|
+
) {
|
|
2579
|
+
return null;
|
|
2580
|
+
}
|
|
2581
|
+
return Object.freeze({
|
|
2582
|
+
authorityVersion: REMOTE_MEMBERSHIP_COORDINATOR_VERSION,
|
|
2583
|
+
sessionEpoch: session.sessionEpoch,
|
|
2584
|
+
cancelledLeaseIds: Object.freeze(
|
|
2585
|
+
[...session.leases.values()]
|
|
2586
|
+
.filter(
|
|
2587
|
+
(lease) =>
|
|
2588
|
+
lease.cancelReason === "session-closed" &&
|
|
2589
|
+
lease.terminalGeneration === String(snapshot.generation),
|
|
2590
|
+
)
|
|
2591
|
+
.map((lease) => lease.leaseId)
|
|
2592
|
+
.sort(),
|
|
2593
|
+
),
|
|
2594
|
+
terminal: true,
|
|
2595
|
+
alreadyClosed: true,
|
|
2596
|
+
statement: this._snapshotStatement(session, snapshot.generation, now),
|
|
2597
|
+
});
|
|
2598
|
+
});
|
|
2599
|
+
if (terminal) return terminal;
|
|
2600
|
+
throw cause;
|
|
2601
|
+
}
|
|
2069
2602
|
const session = result.snapshot.sessions.get(normalizedSessionId);
|
|
2070
2603
|
return Object.freeze({
|
|
2071
2604
|
authorityVersion: REMOTE_MEMBERSHIP_COORDINATOR_VERSION,
|
|
2072
2605
|
sessionEpoch: session.sessionEpoch,
|
|
2073
2606
|
cancelledLeaseIds: Object.freeze([...result.event.cancelledLeaseIds]),
|
|
2607
|
+
terminal: true,
|
|
2608
|
+
alreadyClosed: false,
|
|
2074
2609
|
statement: this._snapshotStatement(
|
|
2075
2610
|
session,
|
|
2076
2611
|
result.snapshot.generation,
|
|
@@ -2397,6 +2932,35 @@ export class DurableRemoteMembershipCoordinator {
|
|
|
2397
2932
|
});
|
|
2398
2933
|
});
|
|
2399
2934
|
}
|
|
2935
|
+
|
|
2936
|
+
getSessionSnapshot(sessionId) {
|
|
2937
|
+
const normalizedSessionId = requiredString(sessionId, "sessionId");
|
|
2938
|
+
return this._withRead((snapshot, now) => {
|
|
2939
|
+
const session = snapshot.sessions.get(normalizedSessionId);
|
|
2940
|
+
if (!session) return null;
|
|
2941
|
+
return Object.freeze({
|
|
2942
|
+
session: publicSessionSnapshot(session, snapshot.generation),
|
|
2943
|
+
statement: this._snapshotStatement(session, snapshot.generation, now),
|
|
2944
|
+
});
|
|
2945
|
+
});
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
/**
|
|
2949
|
+
* Enumerate coordinator-owned session snapshots for server-process recovery.
|
|
2950
|
+
* The caller receives immutable public projections only; live transport
|
|
2951
|
+
* attachment remains a registry concern and is deliberately not persisted
|
|
2952
|
+
* here. Reading also exercises the normal clock/witness rollback checks.
|
|
2953
|
+
*/
|
|
2954
|
+
listSessionSnapshots({ activeOnly = true } = {}) {
|
|
2955
|
+
return this._withRead((snapshot) =>
|
|
2956
|
+
Object.freeze(
|
|
2957
|
+
[...snapshot.sessions.values()]
|
|
2958
|
+
.filter((session) => !activeOnly || session.status === "active")
|
|
2959
|
+
.map((session) => publicSessionSnapshot(session, snapshot.generation))
|
|
2960
|
+
.sort((left, right) => left.sessionId.localeCompare(right.sessionId)),
|
|
2961
|
+
),
|
|
2962
|
+
);
|
|
2963
|
+
}
|
|
2400
2964
|
}
|
|
2401
2965
|
|
|
2402
2966
|
export const _remoteMembershipCoordinatorInternals = Object.freeze({
|