chainlesschain 0.163.7 → 0.164.0
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 +5 -1
- package/scripts/macos-mcp-launcher-build.mjs +317 -0
- package/scripts/macos-mcp-launcher-postinstall.sh +54 -0
- package/scripts/macos-mcp-launcher-preinstall.sh +61 -0
- package/src/assets/web-panel/assets/{AIOps-axqQfwv8.js → AIOps-D2MEn7fI.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DV-Ro0By.js → ActionButton-lCW_4Gf0.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-Bt72WEVu.js → Analytics-Bs7i-nsS.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-BtepG55E.js → AppLayout-CK7B6S_j.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-B7KaJ7S_.js → Artifacts-C8ILjpyo.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-FNFaUg7A.js → Audit-Bb9cNaWc.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DFb6J2wQ.js → BackgroundAgents-sC9ooXIf.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CC7YBhyj.js → Backup-D-DrsplT.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-RkgTNVtj.js → BaseInput-uCNZpwSK.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-hHEmGZy-.js → Chat-CsDK1uE9.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CAP9YhHv.js → ChatBubbleRenderer-OqSDt50w.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-z5wXOQP1.js → Checkbox-JhTXM1-K.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-DVaDJnxy.js → Codegen-BbzZLnyD.js} +1 -1
- package/src/assets/web-panel/assets/{Col-CJmaRDR5.js → Col-D4VW4m4j.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DF29_2bI.js → Community-BXrYawxV.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DtkQkBzn.js → Compact-DCihGy1u.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-xoHDxp7I.js → Compliance-DlyZ-lk8.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BUm-R7ht.js → Cowork-DvGZrhbO.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-Cgl14ZkB.js → Cron-DUncZi8h.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BNcbmoqh.js → Crosschain-C_hLuUcm.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Bwja0Zo9.js → DID-b6YSUqxr.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Bk5XjEdJ.js → Dashboard-uvM15Cfb.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-Ci36Oitq.js → Dropdown-D1-Nd4qY.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer--U04ZN4q.js → EmailListRenderer-8ApvL6Zc.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-B2gNPR2s.js → FamilyGuardDashboard-LYJt8811.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-C4KghYn7.js → Federation-BbI3HGpv.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-QL7x_Q_0.js → FormItemContext-DSoEKuFu.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-F3Ryltwm.js → GenericCardRenderer-B0S9OWd5.js} +1 -1
- package/src/assets/web-panel/assets/{Git-2HassK5k.js → Git-DIs9Zhf3.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DoBIp7oe.js → Governance-jzZXrxcl.js} +1 -1
- package/src/assets/web-panel/assets/{Inference--W01QhZ-.js → Inference-JUPjf2ZI.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DmvwqGpT.js → KnowledgeGraph-DxnfJvlP.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BBJ4yAel.js → Logs-iXiz8BEE.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-BLCZMzhJ.js → MarkdownRenderer-GmB6DOr6.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-D5lNngSI.js → Marketplace-Y3Xch312.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-CkEVOMKH.js → McpTools-D7kGGbO7.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-B1Ow1xVE.js → Memory-Do_hVxzh.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-SgYbUfb6.js → MobileBridge-CtrR0Tas.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-BL0Czcoj.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-DcTXSSma.js → Mtc-qo5QCTGd.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-Dt3WG4oj.js → MtcAudit-cq5mnjYj.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-B9B38UZx.js → Multisig-Bq0MAQHw.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-B0S4GGvG.js → NLProgramming-DdWE6Vi2.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DZ5U58Ng.js → Notes-BxMBFcIp.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-PjL5ZW34.js → NotificationSettings-RTrAcbUe.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-DrAX2ESt.js → OrderTableRenderer-DJbpGqzT.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-Cv1GYs-c.js → Organization-SQzIPdqC.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Bbkp_PL3.js → Overflow-02yECh22.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BD8QUgoZ.js → P2P-BNg4pNCM.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-BO-2PeaB.js → PdhVaultBrowser-BxPJZsD3.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-D_OPVa02.js → Permissions-CLnDnPAz.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-mh-pEmaB.js → PersonalDataHub-C5m0AjGf.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-6JC2EDdb.js → Pipeline-VwOf5Dhv.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CUshzUAG.js → Privacy-CC0W2hZd.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Cs2Aalri.js → ProjectInit-Dz893Bob.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-1ak49tcj.js → ProjectSettings-CxQkvl0k.js} +2 -2
- package/src/assets/web-panel/assets/Projects-DDAjkUkT.js +1 -0
- package/src/assets/web-panel/assets/{Providers-CXXxvafH.js → Providers-DeB9IkxK.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-PTcDsF58.js → QrScannerModal-OCeoaswn.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CDwucrJw.js → QuickAsk-DenotaAj.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CJlLVRXR.js → Recommend-Cies_jjI.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-DlN-ETsO.js → RemoteSession-Dxzl5n2T.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-DI23LR6m.js → Reputation-CW0mvxgZ.js} +1 -1
- package/src/assets/web-panel/assets/{Row-Cl7UQMrM.js → Row-SEd32qZU.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-CexHNfh8.js → RssFeed-BSwnJ3Lm.js} +3 -3
- package/src/assets/web-panel/assets/{Search-BneBWvRN.js → Search-xe2m8bkC.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DX7Phgrp.js → Security-Mq2F9Nro.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BWXO8Mte.js → Services-BKGw2V3P.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DCyOjKPJ.js → Skeleton-DW95-Rxf.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CXqERsTB.js → Skills-CHgChCv_.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-bhbrbz8E.js → Sla-tKh6k3R7.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-3IPU0HBZ.js → SpeechSettings-D6UL3dA5.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-DBNTQLbM.js → SyncSettings-D0Fw8fVL.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-quP0bkN-.js → Tasks-20x1GB3D.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-BouWmgho.js → Templates-DJ-lts-a.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-qv0D9s7z.js → Tenant-BFdGpbs7.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-BxvbE8BB.js → Terminal-Xpx61vHL.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-D-IeLnJ2.js → TimelineRenderer-C6UIvyZQ.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CFMKKyVG.js → Tokens-Dellq08f.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-L8as3UuJ.js → Trigger-VKw8d0J-.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-MxmA4VZG.js → Trust-DNftdKT5.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-C0VvJYRi.js → UkeySign-DX9VAKJI.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-Bf5QZj8N.js → VideoEditing-B2h3jO8q.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BIJ9riRz.js → Wallet-CjGkof7f.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-B8p9Mhkw.js → WebAuthn-2Zfcu55P.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-BVsuaPen.js → WorkflowEditor-B9eZ8UxZ.js} +1 -1
- package/src/assets/web-panel/assets/{chat-BfkjoKpV.js → chat-CQNfD-YO.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DeAlsWK8.js → colors-D4usyFT2.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-Cs97bz5j.js → compact-item-s4TpjvjX.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BqbG60LH.js → createContext-BRCSvYxj.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DKJjSWRn.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-i_RY0b1X.js → hasIn-AdkUbkV-.js} +1 -1
- package/src/assets/web-panel/assets/{index-BOVkf1K3.js → index--00U6vXZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-B-3tEpTc.js → index-B2V-eKfK.js} +1 -1
- package/src/assets/web-panel/assets/{index-D2B4VPyP.js → index-B4JJCfQ2.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkBi5TmC.js → index-BIoG5yYf.js} +1 -1
- package/src/assets/web-panel/assets/{index-79V_zv1s.js → index-BMcimU-2.js} +1 -1
- package/src/assets/web-panel/assets/{index-U4f5izHz.js → index-BPhkFZ1I.js} +1 -1
- package/src/assets/web-panel/assets/{index-DiSkx4UB.js → index-BQ3DXBtD.js} +3 -3
- package/src/assets/web-panel/assets/{index-dIHr4B_u.js → index-Bad4VNvz.js} +1 -1
- package/src/assets/web-panel/assets/{index-CIG8fQ2_.js → index-BgiXR_-_.js} +1 -1
- package/src/assets/web-panel/assets/{index-DqBgBpgp.js → index-BtobShro.js} +1 -1
- package/src/assets/web-panel/assets/{index-ZxO_-E8a.js → index-Bw2MEnUk.js} +1 -1
- package/src/assets/web-panel/assets/{index-DUUrv141.js → index-C1mJtE6q.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bm7lLR4c.js → index-C484EJiS.js} +1 -1
- package/src/assets/web-panel/assets/{index-COdGA-8n.js → index-C5KAQFPQ.js} +1 -1
- package/src/assets/web-panel/assets/index-C61cnl3K.js +1 -0
- package/src/assets/web-panel/assets/{index-CED2PL_y.js → index-C7Y8Aluc.js} +1 -1
- package/src/assets/web-panel/assets/{index-YXgLng0K.js → index-CCsM_Ybx.js} +1 -1
- package/src/assets/web-panel/assets/{index-0-agYlWL.js → index-CJCFe2Qf.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5UkyA3c.js → index-CJVEOa4i.js} +1 -1
- package/src/assets/web-panel/assets/{index-B1CbOH1i.js → index-CJdQrZth.js} +1 -1
- package/src/assets/web-panel/assets/{index-s1opAL-M.js → index-CKC1t3Ic.js} +1 -1
- package/src/assets/web-panel/assets/{index-yObj1O4n.js → index-CQhAVU5t.js} +1 -1
- package/src/assets/web-panel/assets/{index-CgsqdnRV.js → index-CYdoQsa1.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dk7-v4hP.js → index-Cduu2Hhr.js} +1 -1
- package/src/assets/web-panel/assets/{index-B-JVTrpr.js → index-CkwiSLhU.js} +1 -1
- package/src/assets/web-panel/assets/{index-CeWoNHau.js → index-Cvyt-zhm.js} +1 -1
- package/src/assets/web-panel/assets/{index-CE2VtTlp.js → index-D2GjI2oe.js} +1 -1
- package/src/assets/web-panel/assets/index-D4XFHxBr.js +1 -0
- package/src/assets/web-panel/assets/{index-DmbfhJUP.js → index-DARsAyUp.js} +1 -1
- package/src/assets/web-panel/assets/{index-BESLFt4q.js → index-DVKfQ0Ux.js} +1 -1
- package/src/assets/web-panel/assets/{index-PT_5sBMA.js → index-DZT36ZYq.js} +1 -1
- package/src/assets/web-panel/assets/{index-gyFpvL7l.js → index-D_XBHopw.js} +1 -1
- package/src/assets/web-panel/assets/{index-BYUVuwKt.js → index-DdCB70SS.js} +1 -1
- package/src/assets/web-panel/assets/{index-BcUXAB2s.js → index-DzRjHxEv.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dk2qouAz.js → index-MMN9V6JF.js} +1 -1
- package/src/assets/web-panel/assets/{index-D9evtvg2.js → index-RfMr79MW.js} +1 -1
- package/src/assets/web-panel/assets/{index-BuLQobzW.js → index-_IRVsV9X.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cf3WCLmy.js → index-nXyk8GZ9.js} +1 -1
- package/src/assets/web-panel/assets/{index-CG3PRyAL.js → index-s_buFMEG.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-C9h9yKL3.js → initDefaultProps-Blmpk7HL.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BvmLsPdu.js → motion-DPBJMv1n.js} +1 -1
- package/src/assets/web-panel/assets/{move-CoL4ekU1.js → move-ButdzCM7.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-C4pMy6xV.js → mtc-parser-cyRC_QCe.js} +1 -1
- package/src/assets/web-panel/assets/{omit-DPuJfNxx.js → omit-1epSV7hz.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-D0KEeaK2.js → pickAttrs-DuPY8xJs.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BpkyINxt.js → placementArrow-6XZqarhe.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DQI_wOq8.js → responsiveObserve-B40pXBWM.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DYfdDiRg.js → slide-jbZ9Wu4n.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-B5dtvb0D.js → statusUtils-BKQQcVy4.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-CF310mSj.js → styleChecker-NiKfWjUT.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-D1Qp4BLp.js → useFlexGapSupport-bQUCvPeb.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-C7_iyvJK.js → useFs-CUGji7ph.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-DZUt_fIm.js → usePersonalDataHub-4veyN2N2.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-D3dsi0lZ.js → vnode-1-nLug-Z.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-WdbOlvdQ.js → zoom-dhCeRDMo.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +6 -6
- package/src/commands/automation.js +103 -1
- package/src/commands/cowork.js +174 -19
- package/src/commands/permissions.js +240 -16
- package/src/commands/plugin.js +1293 -56
- package/src/commands/session-location.js +276 -0
- package/src/commands/session.js +2 -0
- package/src/commands/team-merge-review.js +937 -0
- package/src/commands/team.js +2 -0
- package/src/data/changelog.json +27 -1
- package/src/gateways/ws/action-protocol.js +35 -10
- package/src/gateways/ws/remote-session-protocol.js +24 -1
- package/src/gateways/ws/ws-agent-handler.js +12 -0
- package/src/gateways/ws/ws-server.js +48 -12
- package/src/harness/jsonl-session-store.js +341 -24
- package/src/harness/mcp-client.js +8 -1
- package/src/harness/remote-session-audit.js +1 -0
- package/src/harness/remote-session-crypto.js +210 -2
- package/src/harness/remote-session-registry.js +159 -6
- package/src/harness/session-list-index.js +233 -14
- package/src/lib/agent-authority.js +6 -0
- package/src/lib/agent-schedule-store.js +139 -28
- package/src/lib/agent-team/team-merge-review-store.js +765 -0
- package/src/lib/agent-team/team-merge-review-transaction.js +1223 -0
- package/src/lib/agent-team/team-merge-review.js +1847 -0
- package/src/lib/automation-center-incidents.js +314 -0
- package/src/lib/automation-center-runtime.js +448 -0
- package/src/lib/automation-center.js +182 -38
- package/src/lib/automation-engine.js +41 -0
- package/src/lib/automation-execution-authority.js +241 -12
- package/src/lib/automation-execution-incident.js +737 -0
- package/src/lib/background-agent-keeper-client.js +248 -0
- package/src/lib/background-agent-keeper-protocol.js +147 -0
- package/src/lib/background-agent-supervisor.js +520 -40
- package/src/lib/background-interaction-resolver.js +12 -0
- package/src/lib/background-turn-bootstrap-protocol.js +126 -0
- package/src/lib/cowork-cron.js +85 -14
- package/src/lib/cowork-workflow.js +336 -16
- package/src/lib/dynamic-workflow-facade.js +648 -0
- package/src/lib/execution-location-contract.js +551 -0
- package/src/lib/execution-location-runtime.js +130 -0
- package/src/lib/ide-context.js +94 -6
- package/src/lib/mcp-stdio-package-materialization.js +166 -14
- package/src/lib/packer/pkg-config-generator.js +2 -0
- package/src/lib/permission-authority.js +77 -0
- package/src/lib/permission-side-effect-center.js +495 -0
- package/src/lib/plugin-runtime/install.js +514 -13
- package/src/lib/plugin-runtime/marketplace-artifact-readback.js +900 -0
- package/src/lib/plugin-runtime/marketplace-catalog.js +1181 -0
- package/src/lib/plugin-runtime/marketplace-impact.js +408 -0
- package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +1567 -0
- package/src/lib/plugin-security.js +60 -3
- package/src/lib/process-execution-broker/index.js +1095 -57
- package/src/lib/process-execution-broker/linux-bwrap-descriptor-launch.js +375 -0
- package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +90 -16
- package/src/lib/process-execution-broker/linux-generic-bwrap.js +159 -18
- package/src/lib/process-execution-broker/macos-mcp-launcher-contract.js +278 -0
- package/src/lib/process-execution-broker/macos-mcp-launcher-protocol.json +23 -0
- package/src/lib/process-execution-broker/macos-mcp-launcher.c +1497 -0
- package/src/lib/process-execution-broker/mcp-fd-entry-bootstrap.js +37 -0
- package/src/lib/process-execution-broker/platform-sandbox.js +2330 -279
- package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
- package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
- package/src/lib/process-execution-broker/windows-sandbox.cs +50 -4
- package/src/lib/public-ide-capability-manifest.js +3 -0
- package/src/lib/remote-approval-bridge.js +123 -17
- package/src/lib/remote-membership-authority.js +632 -0
- package/src/lib/remote-membership-coordinator.js +2409 -0
- package/src/lib/remote-membership-host-store.js +1750 -0
- package/src/lib/scheduler-kernel/agenda-adapter.js +17 -7
- package/src/lib/scheduler-kernel/automation-adapter.js +105 -11
- package/src/lib/scheduler-kernel/automation-event-adapter.js +87 -11
- package/src/lib/scheduler-kernel/contract.js +91 -0
- package/src/lib/scheduler-kernel/cowork-cron-adapter.js +12 -1
- package/src/lib/scheduler-kernel/loop-adapter.js +7 -1
- package/src/lib/scheduler-kernel/routine-adapter.js +7 -1
- package/src/lib/scheduler-kernel/runtime-control-capabilities.js +31 -0
- package/src/lib/scheduler-kernel/runtime.js +176 -3
- package/src/lib/scheduler-kernel/store.js +1253 -45
- package/src/lib/scoped-permission-store.js +369 -0
- package/src/lib/session-anti-rollback-anchor.js +262 -5
- package/src/lib/sub-agent-context.js +4 -0
- package/src/lib/workflow-definition-contract.js +149 -0
- package/src/repl/agent-repl.js +41 -7
- package/src/repl/chat-repl.js +2 -0
- package/src/repl/clipboard-image.js +735 -16
- package/src/repl/ide-status.js +1 -1
- package/src/repl/prompt-interactions.js +59 -6
- package/src/runtime/agent-core.js +93 -3
- package/src/runtime/headless-runner.js +30 -25
- package/src/runtime/headless-stream.js +32 -25
- package/src/runtime/mcp-config.js +81 -10
- package/src/workers/background-agent-keeper.js +498 -0
- package/src/workers/background-agent-worker.js +358 -13
- package/src/workers/background-turn-bootstrap.js +143 -0
- package/src/assets/web-panel/assets/MobileProjects-BWdOvQAj.js +0 -1
- package/src/assets/web-panel/assets/Projects-BV4oApsm.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DPty0mGN.js +0 -1
- package/src/assets/web-panel/assets/index-Bt7g0Vq_.js +0 -1
- package/src/assets/web-panel/assets/index-ChVT0p9u.js +0 -1
|
@@ -0,0 +1,1750 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side durable receipt store for server-signed Remote Membership leases.
|
|
3
|
+
*
|
|
4
|
+
* The coordinator and this store intentionally live on different hosts/state
|
|
5
|
+
* roots. A WebSocket frame is never sufficient: the host pins the Ed25519 key,
|
|
6
|
+
* verifies the statement, fsyncs an append-only receipt, and ACKs that exact
|
|
7
|
+
* receipt before an approval may be returned. A separate machine-security
|
|
8
|
+
* witness detects rollback of the ordinary host state file.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import {
|
|
14
|
+
createHash,
|
|
15
|
+
createPrivateKey,
|
|
16
|
+
createPublicKey,
|
|
17
|
+
verify,
|
|
18
|
+
} from "node:crypto";
|
|
19
|
+
import { getMachineSecurityAnchorDir, getStatePath } from "./paths.js";
|
|
20
|
+
import {
|
|
21
|
+
mutateSecurityStore,
|
|
22
|
+
readSecurityStore,
|
|
23
|
+
} from "./durable-security-store.js";
|
|
24
|
+
import { withFileLock } from "./with-file-lock.js";
|
|
25
|
+
import {
|
|
26
|
+
REMOTE_MEMBERSHIP_COORDINATOR_VERSION,
|
|
27
|
+
REMOTE_MEMBERSHIP_STATEMENT_SCHEMA,
|
|
28
|
+
_remoteMembershipCoordinatorInternals,
|
|
29
|
+
signRemoteMembershipAuthenticationChallenge,
|
|
30
|
+
} from "./remote-membership-coordinator.js";
|
|
31
|
+
|
|
32
|
+
export const REMOTE_MEMBERSHIP_HOST_STORE_VERSION =
|
|
33
|
+
"pinned-signed-receipt-high-water-v1";
|
|
34
|
+
export const REMOTE_MEMBERSHIP_HOST_UNAVAILABLE_CODE =
|
|
35
|
+
"CC_REMOTE_MEMBERSHIP_HOST_STATE_UNAVAILABLE";
|
|
36
|
+
export const REMOTE_MEMBERSHIP_HOST_ROLLBACK_CODE =
|
|
37
|
+
"CC_REMOTE_MEMBERSHIP_HOST_ROLLBACK";
|
|
38
|
+
export const REMOTE_MEMBERSHIP_HOST_TRUST_CODE =
|
|
39
|
+
"CC_REMOTE_MEMBERSHIP_HOST_TRUST_REJECTED";
|
|
40
|
+
|
|
41
|
+
const STORE_VERSION = 1;
|
|
42
|
+
const WITNESS_VERSION = 2;
|
|
43
|
+
const RECORD_SCHEMA = "chainlesschain.remote-membership-host-record/v1";
|
|
44
|
+
const STORE_LABEL = "Remote membership host receipts";
|
|
45
|
+
const WITNESS_LABEL = "Remote membership host rollback witness";
|
|
46
|
+
const DIGEST_RE = /^[0-9a-f]{64}$/;
|
|
47
|
+
const POSITIVE_RE = /^[1-9]\d*$/;
|
|
48
|
+
const NON_NEGATIVE_RE = /^(?:0|[1-9]\d*)$/;
|
|
49
|
+
const BASE64URL_RE = /^[A-Za-z0-9_-]+$/;
|
|
50
|
+
const VALID_STATEMENT_KINDS = new Set([
|
|
51
|
+
"session.snapshot",
|
|
52
|
+
"lease.created",
|
|
53
|
+
"lease.acked",
|
|
54
|
+
"lease.consumed",
|
|
55
|
+
"lease.cancelled",
|
|
56
|
+
]);
|
|
57
|
+
const STATEMENT_FIELDS = Object.freeze([
|
|
58
|
+
"algorithm",
|
|
59
|
+
"authorityVersion",
|
|
60
|
+
"coordinatorId",
|
|
61
|
+
"generation",
|
|
62
|
+
"issuedAtMs",
|
|
63
|
+
"keyId",
|
|
64
|
+
"kind",
|
|
65
|
+
"payload",
|
|
66
|
+
"schema",
|
|
67
|
+
"signature",
|
|
68
|
+
]);
|
|
69
|
+
const SNAPSHOT_FIELDS = Object.freeze([
|
|
70
|
+
"agentSessionId",
|
|
71
|
+
"authorityGeneration",
|
|
72
|
+
"createdAt",
|
|
73
|
+
"expiresAt",
|
|
74
|
+
"hostPrincipalId",
|
|
75
|
+
"leases",
|
|
76
|
+
"members",
|
|
77
|
+
"sessionEpoch",
|
|
78
|
+
"sessionId",
|
|
79
|
+
"status",
|
|
80
|
+
]);
|
|
81
|
+
const MEMBER_FIELDS = Object.freeze([
|
|
82
|
+
"credentialKeySha256",
|
|
83
|
+
"membershipEpoch",
|
|
84
|
+
"principalId",
|
|
85
|
+
"scopes",
|
|
86
|
+
"status",
|
|
87
|
+
]);
|
|
88
|
+
const LEASE_FIELDS = Object.freeze([
|
|
89
|
+
"ackedGeneration",
|
|
90
|
+
"binding",
|
|
91
|
+
"cancelReason",
|
|
92
|
+
"consumedGeneration",
|
|
93
|
+
"createdAt",
|
|
94
|
+
"createdGeneration",
|
|
95
|
+
"expiresAt",
|
|
96
|
+
"fingerprint",
|
|
97
|
+
"hostPrincipalId",
|
|
98
|
+
"hostReceiptDigest",
|
|
99
|
+
"leaseId",
|
|
100
|
+
"membershipEpoch",
|
|
101
|
+
"principalId",
|
|
102
|
+
"requestId",
|
|
103
|
+
"sessionEpoch",
|
|
104
|
+
"sessionId",
|
|
105
|
+
"status",
|
|
106
|
+
"terminalGeneration",
|
|
107
|
+
]);
|
|
108
|
+
const AUTHENTICATION_CHALLENGE_FIELDS = Object.freeze([
|
|
109
|
+
"authorityVersion",
|
|
110
|
+
"challengeId",
|
|
111
|
+
"connectionNonce",
|
|
112
|
+
"coordinatorId",
|
|
113
|
+
"credentialKeySha256",
|
|
114
|
+
"credentialPublicKey",
|
|
115
|
+
"credentialType",
|
|
116
|
+
"expiresAtMs",
|
|
117
|
+
"issuedAtMs",
|
|
118
|
+
"membershipEpoch",
|
|
119
|
+
"principalId",
|
|
120
|
+
"purpose",
|
|
121
|
+
"schema",
|
|
122
|
+
"scopes",
|
|
123
|
+
"serverInstanceId",
|
|
124
|
+
"serverNonce",
|
|
125
|
+
"sessionEpoch",
|
|
126
|
+
"sessionId",
|
|
127
|
+
]);
|
|
128
|
+
const COMMON_RECORD_FIELDS = Object.freeze([
|
|
129
|
+
"previousRecordHash",
|
|
130
|
+
"recordHash",
|
|
131
|
+
"recordedAtMs",
|
|
132
|
+
"revision",
|
|
133
|
+
"schema",
|
|
134
|
+
"type",
|
|
135
|
+
]);
|
|
136
|
+
const RECORD_FIELDS = Object.freeze({
|
|
137
|
+
"trust.pinned": Object.freeze([...COMMON_RECORD_FIELDS, "trust"]),
|
|
138
|
+
"session.bootstrap": Object.freeze([
|
|
139
|
+
...COMMON_RECORD_FIELDS,
|
|
140
|
+
"bootstrap",
|
|
141
|
+
"statement",
|
|
142
|
+
]),
|
|
143
|
+
"statement.adopted": Object.freeze([
|
|
144
|
+
...COMMON_RECORD_FIELDS,
|
|
145
|
+
"statement",
|
|
146
|
+
"statementDigest",
|
|
147
|
+
]),
|
|
148
|
+
});
|
|
149
|
+
const BOOTSTRAP_FIELDS = Object.freeze([
|
|
150
|
+
"agentSessionId",
|
|
151
|
+
"coordinatorId",
|
|
152
|
+
"hostCredentialPrivateKeyPkcs8",
|
|
153
|
+
"hostCredentialPublicKeySpki",
|
|
154
|
+
"hostPrincipalId",
|
|
155
|
+
"sessionId",
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
const { canonicalJson, sha256 } = _remoteMembershipCoordinatorInternals;
|
|
159
|
+
|
|
160
|
+
function hostError(code, message, cause = null, details = {}) {
|
|
161
|
+
const error = new Error(message, cause ? { cause } : undefined);
|
|
162
|
+
error.name = "RemoteMembershipHostStoreError";
|
|
163
|
+
error.code = code;
|
|
164
|
+
Object.assign(error, details);
|
|
165
|
+
return error;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function unavailable(cause, filePath) {
|
|
169
|
+
if (
|
|
170
|
+
cause?.code === REMOTE_MEMBERSHIP_HOST_ROLLBACK_CODE ||
|
|
171
|
+
cause?.code === REMOTE_MEMBERSHIP_HOST_TRUST_CODE ||
|
|
172
|
+
cause?.code === REMOTE_MEMBERSHIP_HOST_UNAVAILABLE_CODE
|
|
173
|
+
) {
|
|
174
|
+
return cause;
|
|
175
|
+
}
|
|
176
|
+
return hostError(
|
|
177
|
+
REMOTE_MEMBERSHIP_HOST_UNAVAILABLE_CODE,
|
|
178
|
+
"Remote membership host state is unavailable; remote execution is denied",
|
|
179
|
+
cause,
|
|
180
|
+
{ filePath, commitState: cause?.commitState || null },
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function rollback(message, details = {}) {
|
|
185
|
+
return hostError(
|
|
186
|
+
REMOTE_MEMBERSHIP_HOST_ROLLBACK_CODE,
|
|
187
|
+
`Remote membership host rollback detected: ${message}`,
|
|
188
|
+
null,
|
|
189
|
+
details,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function trustRejected(message, details = {}) {
|
|
194
|
+
return hostError(
|
|
195
|
+
REMOTE_MEMBERSHIP_HOST_TRUST_CODE,
|
|
196
|
+
`Remote membership coordinator trust rejected: ${message}`,
|
|
197
|
+
null,
|
|
198
|
+
details,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function requiredString(value, label, maximum = 4096) {
|
|
203
|
+
if (
|
|
204
|
+
typeof value !== "string" ||
|
|
205
|
+
value.length === 0 ||
|
|
206
|
+
value.length > maximum ||
|
|
207
|
+
value.includes("\0")
|
|
208
|
+
) {
|
|
209
|
+
throw new TypeError(`${label} must be a non-empty bounded string`);
|
|
210
|
+
}
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function hasExactKeys(value, expected) {
|
|
215
|
+
return (
|
|
216
|
+
value &&
|
|
217
|
+
typeof value === "object" &&
|
|
218
|
+
!Array.isArray(value) &&
|
|
219
|
+
canonicalJson(Object.keys(value).sort()) ===
|
|
220
|
+
canonicalJson([...expected].sort())
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function canonicalInteger(value, label, { zero = false } = {}) {
|
|
225
|
+
const pattern = zero ? NON_NEGATIVE_RE : POSITIVE_RE;
|
|
226
|
+
if (typeof value !== "string" || !pattern.test(value)) {
|
|
227
|
+
throw new TypeError(`${label} must be a canonical integer string`);
|
|
228
|
+
}
|
|
229
|
+
return BigInt(value);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function safeTimestamp(value, label) {
|
|
233
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
234
|
+
throw new TypeError(`${label} must be a non-negative safe timestamp`);
|
|
235
|
+
}
|
|
236
|
+
return value;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function decodeBase64Url(value, label, maximum = 8192) {
|
|
240
|
+
const encoded = requiredString(value, label, maximum);
|
|
241
|
+
if (!BASE64URL_RE.test(encoded)) {
|
|
242
|
+
throw new TypeError(`${label} is not canonical base64url`);
|
|
243
|
+
}
|
|
244
|
+
const decoded = Buffer.from(encoded, "base64url");
|
|
245
|
+
if (decoded.length === 0 || decoded.toString("base64url") !== encoded) {
|
|
246
|
+
throw new TypeError(`${label} is not canonical base64url`);
|
|
247
|
+
}
|
|
248
|
+
return decoded;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function recordHash(record) {
|
|
252
|
+
const core = { ...record };
|
|
253
|
+
delete core.recordHash;
|
|
254
|
+
return sha256(
|
|
255
|
+
Buffer.concat([
|
|
256
|
+
Buffer.from("chainlesschain.remote-membership-host-record.v1\0"),
|
|
257
|
+
Buffer.from(canonicalJson(core), "utf8"),
|
|
258
|
+
]),
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function statementDigest(statement) {
|
|
263
|
+
return sha256(
|
|
264
|
+
Buffer.concat([
|
|
265
|
+
Buffer.from("chainlesschain.remote-membership-statement.v1\0"),
|
|
266
|
+
Buffer.from(canonicalJson(statement), "utf8"),
|
|
267
|
+
]),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function validateTrustDescriptor(trust) {
|
|
272
|
+
if (
|
|
273
|
+
!trust ||
|
|
274
|
+
typeof trust !== "object" ||
|
|
275
|
+
Array.isArray(trust) ||
|
|
276
|
+
!hasExactKeys(trust, [
|
|
277
|
+
"algorithm",
|
|
278
|
+
"authorityVersion",
|
|
279
|
+
"coordinatorId",
|
|
280
|
+
"keyId",
|
|
281
|
+
"publicKeySha256",
|
|
282
|
+
"publicKeySpki",
|
|
283
|
+
]) ||
|
|
284
|
+
trust.authorityVersion !== REMOTE_MEMBERSHIP_COORDINATOR_VERSION ||
|
|
285
|
+
trust.algorithm !== "ed25519" ||
|
|
286
|
+
typeof trust.publicKeySpki !== "string" ||
|
|
287
|
+
!DIGEST_RE.test(String(trust.publicKeySha256)) ||
|
|
288
|
+
trust.coordinatorId !== `ed25519:${trust.publicKeySha256}` ||
|
|
289
|
+
trust.keyId !== trust.coordinatorId
|
|
290
|
+
) {
|
|
291
|
+
throw trustRejected("descriptor is malformed");
|
|
292
|
+
}
|
|
293
|
+
let publicKey;
|
|
294
|
+
let publicDer;
|
|
295
|
+
try {
|
|
296
|
+
publicDer = decodeBase64Url(
|
|
297
|
+
trust.publicKeySpki,
|
|
298
|
+
"trust.publicKeySpki",
|
|
299
|
+
4096,
|
|
300
|
+
);
|
|
301
|
+
publicKey = createPublicKey({
|
|
302
|
+
key: publicDer,
|
|
303
|
+
type: "spki",
|
|
304
|
+
format: "der",
|
|
305
|
+
});
|
|
306
|
+
} catch (cause) {
|
|
307
|
+
throw trustRejected("public key cannot be imported", {
|
|
308
|
+
causeCode: cause?.code || null,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
const actualDigest = createHash("sha256").update(publicDer).digest("hex");
|
|
312
|
+
if (
|
|
313
|
+
publicKey.asymmetricKeyType !== "ed25519" ||
|
|
314
|
+
actualDigest !== trust.publicKeySha256
|
|
315
|
+
) {
|
|
316
|
+
throw trustRejected("public key fingerprint does not match");
|
|
317
|
+
}
|
|
318
|
+
return Object.freeze({
|
|
319
|
+
authorityVersion: trust.authorityVersion,
|
|
320
|
+
algorithm: trust.algorithm,
|
|
321
|
+
coordinatorId: trust.coordinatorId,
|
|
322
|
+
keyId: trust.keyId,
|
|
323
|
+
publicKeySpki: trust.publicKeySpki,
|
|
324
|
+
publicKeySha256: trust.publicKeySha256,
|
|
325
|
+
publicKey,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function validateHostBootstrapCredential(bootstrap, label = "bootstrap") {
|
|
330
|
+
let publicDer;
|
|
331
|
+
try {
|
|
332
|
+
publicDer = decodeBase64Url(
|
|
333
|
+
bootstrap.hostCredentialPublicKeySpki,
|
|
334
|
+
`${label}.hostCredentialPublicKeySpki`,
|
|
335
|
+
4096,
|
|
336
|
+
);
|
|
337
|
+
const publicKey = createPublicKey({
|
|
338
|
+
key: publicDer,
|
|
339
|
+
type: "spki",
|
|
340
|
+
format: "der",
|
|
341
|
+
});
|
|
342
|
+
if (publicKey.asymmetricKeyType !== "ed25519") {
|
|
343
|
+
throw new TypeError("credential is not Ed25519");
|
|
344
|
+
}
|
|
345
|
+
if (bootstrap.hostCredentialPrivateKeyPkcs8 !== null) {
|
|
346
|
+
const privateKey = createPrivateKey({
|
|
347
|
+
key: decodeBase64Url(
|
|
348
|
+
bootstrap.hostCredentialPrivateKeyPkcs8,
|
|
349
|
+
`${label}.hostCredentialPrivateKeyPkcs8`,
|
|
350
|
+
4096,
|
|
351
|
+
),
|
|
352
|
+
type: "pkcs8",
|
|
353
|
+
format: "der",
|
|
354
|
+
});
|
|
355
|
+
if (
|
|
356
|
+
privateKey.asymmetricKeyType !== "ed25519" ||
|
|
357
|
+
!createPublicKey(privateKey)
|
|
358
|
+
.export({ type: "spki", format: "der" })
|
|
359
|
+
.equals(publicDer)
|
|
360
|
+
) {
|
|
361
|
+
throw new TypeError("private credential does not match public key");
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
} catch (cause) {
|
|
365
|
+
throw trustRejected(`${label} Ed25519 credential is invalid`, {
|
|
366
|
+
causeCode: cause?.code || null,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
const digest = createHash("sha256").update(publicDer).digest("hex");
|
|
370
|
+
if (bootstrap.hostPrincipalId !== `ed25519:${digest}`) {
|
|
371
|
+
throw trustRejected(`${label} host principal is not key-bound`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function validateMemberPayload(member, label) {
|
|
376
|
+
if (
|
|
377
|
+
!hasExactKeys(member, MEMBER_FIELDS) ||
|
|
378
|
+
!["active", "revoked"].includes(member.status) ||
|
|
379
|
+
!Array.isArray(member.scopes) ||
|
|
380
|
+
member.scopes.length === 0
|
|
381
|
+
) {
|
|
382
|
+
throw trustRejected(`${label} is malformed`);
|
|
383
|
+
}
|
|
384
|
+
requiredString(member.principalId, `${label}.principalId`);
|
|
385
|
+
canonicalInteger(member.membershipEpoch, `${label}.membershipEpoch`);
|
|
386
|
+
if (
|
|
387
|
+
!DIGEST_RE.test(String(member.credentialKeySha256)) ||
|
|
388
|
+
!new Set([
|
|
389
|
+
`ed25519:${member.credentialKeySha256}`,
|
|
390
|
+
`relay-x25519:${member.credentialKeySha256}`,
|
|
391
|
+
]).has(member.principalId)
|
|
392
|
+
) {
|
|
393
|
+
throw trustRejected(
|
|
394
|
+
`${label} principal is not bound to its credential key`,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
const scopes = [...new Set(member.scopes.map(String))].sort();
|
|
398
|
+
if (canonicalJson(scopes) !== canonicalJson(member.scopes)) {
|
|
399
|
+
throw trustRejected(`${label}.scopes are not canonical`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function validateLeasePayload(lease, statementKind, generation, label) {
|
|
404
|
+
if (
|
|
405
|
+
!hasExactKeys(lease, LEASE_FIELDS) ||
|
|
406
|
+
!["active", "acked", "consumed", "cancelled"].includes(lease.status)
|
|
407
|
+
) {
|
|
408
|
+
throw trustRejected(`${label} is malformed`);
|
|
409
|
+
}
|
|
410
|
+
for (const field of [
|
|
411
|
+
"leaseId",
|
|
412
|
+
"sessionId",
|
|
413
|
+
"principalId",
|
|
414
|
+
"hostPrincipalId",
|
|
415
|
+
"requestId",
|
|
416
|
+
"fingerprint",
|
|
417
|
+
"binding",
|
|
418
|
+
]) {
|
|
419
|
+
requiredString(lease[field], `${label}.${field}`);
|
|
420
|
+
}
|
|
421
|
+
for (const field of [
|
|
422
|
+
"sessionEpoch",
|
|
423
|
+
"membershipEpoch",
|
|
424
|
+
"createdGeneration",
|
|
425
|
+
]) {
|
|
426
|
+
canonicalInteger(lease[field], `${label}.${field}`);
|
|
427
|
+
}
|
|
428
|
+
for (const field of [
|
|
429
|
+
"ackedGeneration",
|
|
430
|
+
"consumedGeneration",
|
|
431
|
+
"terminalGeneration",
|
|
432
|
+
]) {
|
|
433
|
+
if (lease[field] !== null) {
|
|
434
|
+
canonicalInteger(lease[field], `${label}.${field}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
safeTimestamp(lease.createdAt, `${label}.createdAt`);
|
|
438
|
+
safeTimestamp(lease.expiresAt, `${label}.expiresAt`);
|
|
439
|
+
if (lease.expiresAt <= lease.createdAt) {
|
|
440
|
+
throw trustRejected(`${label} has an empty validity window`);
|
|
441
|
+
}
|
|
442
|
+
if (
|
|
443
|
+
lease.hostReceiptDigest !== null &&
|
|
444
|
+
!DIGEST_RE.test(String(lease.hostReceiptDigest))
|
|
445
|
+
) {
|
|
446
|
+
throw trustRejected(`${label}.hostReceiptDigest is invalid`);
|
|
447
|
+
}
|
|
448
|
+
if (lease.cancelReason !== null) {
|
|
449
|
+
requiredString(lease.cancelReason, `${label}.cancelReason`);
|
|
450
|
+
}
|
|
451
|
+
const createdGeneration = BigInt(lease.createdGeneration);
|
|
452
|
+
const ackedGeneration =
|
|
453
|
+
lease.ackedGeneration === null ? null : BigInt(lease.ackedGeneration);
|
|
454
|
+
const consumedGeneration =
|
|
455
|
+
lease.consumedGeneration === null ? null : BigInt(lease.consumedGeneration);
|
|
456
|
+
const terminalGeneration =
|
|
457
|
+
lease.terminalGeneration === null ? null : BigInt(lease.terminalGeneration);
|
|
458
|
+
const coherent = {
|
|
459
|
+
active:
|
|
460
|
+
ackedGeneration === null &&
|
|
461
|
+
consumedGeneration === null &&
|
|
462
|
+
terminalGeneration === null &&
|
|
463
|
+
lease.hostReceiptDigest === null &&
|
|
464
|
+
lease.cancelReason === null,
|
|
465
|
+
acked:
|
|
466
|
+
ackedGeneration !== null &&
|
|
467
|
+
ackedGeneration > createdGeneration &&
|
|
468
|
+
consumedGeneration === null &&
|
|
469
|
+
terminalGeneration === null &&
|
|
470
|
+
lease.hostReceiptDigest !== null &&
|
|
471
|
+
lease.cancelReason === null,
|
|
472
|
+
consumed:
|
|
473
|
+
ackedGeneration !== null &&
|
|
474
|
+
consumedGeneration !== null &&
|
|
475
|
+
terminalGeneration === consumedGeneration &&
|
|
476
|
+
ackedGeneration > createdGeneration &&
|
|
477
|
+
consumedGeneration > ackedGeneration &&
|
|
478
|
+
lease.hostReceiptDigest !== null &&
|
|
479
|
+
lease.cancelReason === null,
|
|
480
|
+
cancelled:
|
|
481
|
+
consumedGeneration === null &&
|
|
482
|
+
terminalGeneration !== null &&
|
|
483
|
+
terminalGeneration > (ackedGeneration ?? createdGeneration) &&
|
|
484
|
+
lease.cancelReason !== null &&
|
|
485
|
+
((ackedGeneration === null && lease.hostReceiptDigest === null) ||
|
|
486
|
+
(ackedGeneration !== null &&
|
|
487
|
+
ackedGeneration > createdGeneration &&
|
|
488
|
+
lease.hostReceiptDigest !== null)),
|
|
489
|
+
}[lease.status];
|
|
490
|
+
if (!coherent) {
|
|
491
|
+
throw trustRejected(`${label} has incoherent lease state fields`);
|
|
492
|
+
}
|
|
493
|
+
const expectedStatus = {
|
|
494
|
+
"lease.created": "active",
|
|
495
|
+
"lease.acked": "acked",
|
|
496
|
+
"lease.consumed": "consumed",
|
|
497
|
+
"lease.cancelled": "cancelled",
|
|
498
|
+
}[statementKind];
|
|
499
|
+
if (lease.status !== expectedStatus) {
|
|
500
|
+
throw trustRejected(`${label}.status does not match ${statementKind}`);
|
|
501
|
+
}
|
|
502
|
+
const generationField = {
|
|
503
|
+
"lease.created": "createdGeneration",
|
|
504
|
+
"lease.acked": "ackedGeneration",
|
|
505
|
+
"lease.consumed": "consumedGeneration",
|
|
506
|
+
"lease.cancelled": "terminalGeneration",
|
|
507
|
+
}[statementKind];
|
|
508
|
+
if (lease[generationField] !== generation) {
|
|
509
|
+
throw trustRejected(
|
|
510
|
+
`${label}.${generationField} is not statement generation`,
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function validateSnapshotPayload(snapshot, generation, label) {
|
|
516
|
+
if (
|
|
517
|
+
!hasExactKeys(snapshot, SNAPSHOT_FIELDS) ||
|
|
518
|
+
!["active", "closed"].includes(snapshot.status) ||
|
|
519
|
+
!Array.isArray(snapshot.members) ||
|
|
520
|
+
!Array.isArray(snapshot.leases) ||
|
|
521
|
+
snapshot.authorityGeneration !== generation
|
|
522
|
+
) {
|
|
523
|
+
throw trustRejected(`${label} is malformed`);
|
|
524
|
+
}
|
|
525
|
+
requiredString(snapshot.sessionId, `${label}.sessionId`);
|
|
526
|
+
requiredString(snapshot.agentSessionId, `${label}.agentSessionId`);
|
|
527
|
+
requiredString(snapshot.hostPrincipalId, `${label}.hostPrincipalId`);
|
|
528
|
+
canonicalInteger(snapshot.sessionEpoch, `${label}.sessionEpoch`);
|
|
529
|
+
safeTimestamp(snapshot.createdAt, `${label}.createdAt`);
|
|
530
|
+
safeTimestamp(snapshot.expiresAt, `${label}.expiresAt`);
|
|
531
|
+
if (snapshot.expiresAt <= snapshot.createdAt) {
|
|
532
|
+
throw trustRejected(`${label} has an empty validity window`);
|
|
533
|
+
}
|
|
534
|
+
const principals = new Set();
|
|
535
|
+
let previousPrincipal = null;
|
|
536
|
+
for (const [index, member] of snapshot.members.entries()) {
|
|
537
|
+
validateMemberPayload(member, `${label}.members[${index}]`);
|
|
538
|
+
if (principals.has(member.principalId)) {
|
|
539
|
+
throw trustRejected(`${label} repeats a member principal`);
|
|
540
|
+
}
|
|
541
|
+
if (
|
|
542
|
+
previousPrincipal !== null &&
|
|
543
|
+
previousPrincipal.localeCompare(member.principalId) >= 0
|
|
544
|
+
) {
|
|
545
|
+
throw trustRejected(`${label}.members are not canonically ordered`);
|
|
546
|
+
}
|
|
547
|
+
principals.add(member.principalId);
|
|
548
|
+
previousPrincipal = member.principalId;
|
|
549
|
+
}
|
|
550
|
+
const leases = new Set();
|
|
551
|
+
const requests = new Set();
|
|
552
|
+
let previousLeaseId = null;
|
|
553
|
+
for (const [index, lease] of snapshot.leases.entries()) {
|
|
554
|
+
if (!hasExactKeys(lease, LEASE_FIELDS)) {
|
|
555
|
+
throw trustRejected(`${label}.leases[${index}] is malformed`);
|
|
556
|
+
}
|
|
557
|
+
// Snapshot leases may be in any durable state. Validate by their state-
|
|
558
|
+
// corresponding statement kind while retaining the snapshot generation.
|
|
559
|
+
const kind = {
|
|
560
|
+
active: "lease.created",
|
|
561
|
+
acked: "lease.acked",
|
|
562
|
+
consumed: "lease.consumed",
|
|
563
|
+
cancelled: "lease.cancelled",
|
|
564
|
+
}[lease.status];
|
|
565
|
+
const stateGeneration = {
|
|
566
|
+
active: lease.createdGeneration,
|
|
567
|
+
acked: lease.ackedGeneration,
|
|
568
|
+
consumed: lease.consumedGeneration,
|
|
569
|
+
cancelled: lease.terminalGeneration,
|
|
570
|
+
}[lease.status];
|
|
571
|
+
validateLeasePayload(
|
|
572
|
+
lease,
|
|
573
|
+
kind,
|
|
574
|
+
stateGeneration,
|
|
575
|
+
`${label}.leases[${index}]`,
|
|
576
|
+
);
|
|
577
|
+
if (
|
|
578
|
+
lease.sessionId !== snapshot.sessionId ||
|
|
579
|
+
lease.hostPrincipalId !== snapshot.hostPrincipalId ||
|
|
580
|
+
leases.has(lease.leaseId)
|
|
581
|
+
) {
|
|
582
|
+
throw trustRejected(`${label} has a foreign or duplicate lease`);
|
|
583
|
+
}
|
|
584
|
+
if (requests.has(lease.requestId)) {
|
|
585
|
+
throw trustRejected(`${label} repeats an approval request`);
|
|
586
|
+
}
|
|
587
|
+
if (
|
|
588
|
+
previousLeaseId !== null &&
|
|
589
|
+
previousLeaseId.localeCompare(lease.leaseId) >= 0
|
|
590
|
+
) {
|
|
591
|
+
throw trustRejected(`${label}.leases are not canonically ordered`);
|
|
592
|
+
}
|
|
593
|
+
leases.add(lease.leaseId);
|
|
594
|
+
requests.add(lease.requestId);
|
|
595
|
+
previousLeaseId = lease.leaseId;
|
|
596
|
+
const principal = snapshot.members.find(
|
|
597
|
+
(member) => member.principalId === lease.principalId,
|
|
598
|
+
);
|
|
599
|
+
if (
|
|
600
|
+
!principal ||
|
|
601
|
+
BigInt(principal.membershipEpoch) < BigInt(lease.membershipEpoch) ||
|
|
602
|
+
BigInt(lease.sessionEpoch) > BigInt(snapshot.sessionEpoch) ||
|
|
603
|
+
lease.expiresAt > snapshot.expiresAt
|
|
604
|
+
) {
|
|
605
|
+
throw trustRejected(`${label} has a lease outside its membership tuple`);
|
|
606
|
+
}
|
|
607
|
+
const snapshotGeneration = BigInt(generation);
|
|
608
|
+
for (const leaseGeneration of [
|
|
609
|
+
lease.createdGeneration,
|
|
610
|
+
lease.ackedGeneration,
|
|
611
|
+
lease.consumedGeneration,
|
|
612
|
+
lease.terminalGeneration,
|
|
613
|
+
]) {
|
|
614
|
+
if (
|
|
615
|
+
leaseGeneration !== null &&
|
|
616
|
+
BigInt(leaseGeneration) > snapshotGeneration
|
|
617
|
+
) {
|
|
618
|
+
throw trustRejected(`${label} has a lease generation from the future`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const host = snapshot.members.find(
|
|
623
|
+
(member) => member.principalId === snapshot.hostPrincipalId,
|
|
624
|
+
);
|
|
625
|
+
if (!host || host.status !== "active") {
|
|
626
|
+
throw trustRejected(`${label} omits the active host principal`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function validateStatement(statement, trust) {
|
|
631
|
+
if (
|
|
632
|
+
!statement ||
|
|
633
|
+
typeof statement !== "object" ||
|
|
634
|
+
Array.isArray(statement) ||
|
|
635
|
+
!hasExactKeys(statement, STATEMENT_FIELDS) ||
|
|
636
|
+
statement.schema !== REMOTE_MEMBERSHIP_STATEMENT_SCHEMA ||
|
|
637
|
+
statement.authorityVersion !== REMOTE_MEMBERSHIP_COORDINATOR_VERSION ||
|
|
638
|
+
statement.algorithm !== "ed25519" ||
|
|
639
|
+
statement.coordinatorId !== trust.coordinatorId ||
|
|
640
|
+
statement.keyId !== trust.keyId ||
|
|
641
|
+
!POSITIVE_RE.test(String(statement.generation)) ||
|
|
642
|
+
!VALID_STATEMENT_KINDS.has(statement.kind) ||
|
|
643
|
+
!statement.payload ||
|
|
644
|
+
typeof statement.payload !== "object" ||
|
|
645
|
+
Array.isArray(statement.payload) ||
|
|
646
|
+
typeof statement.signature !== "string"
|
|
647
|
+
) {
|
|
648
|
+
throw trustRejected("signed statement is malformed");
|
|
649
|
+
}
|
|
650
|
+
safeTimestamp(statement.issuedAtMs, "statement.issuedAtMs");
|
|
651
|
+
const { signature, ...core } = statement;
|
|
652
|
+
let signatureBytes;
|
|
653
|
+
try {
|
|
654
|
+
signatureBytes = decodeBase64Url(signature, "statement.signature", 256);
|
|
655
|
+
} catch {
|
|
656
|
+
throw trustRejected("statement signature encoding is invalid");
|
|
657
|
+
}
|
|
658
|
+
if (
|
|
659
|
+
signatureBytes.length !== 64 ||
|
|
660
|
+
!verify(
|
|
661
|
+
null,
|
|
662
|
+
Buffer.from(canonicalJson(core), "utf8"),
|
|
663
|
+
trust.publicKey,
|
|
664
|
+
signatureBytes,
|
|
665
|
+
)
|
|
666
|
+
) {
|
|
667
|
+
throw trustRejected("statement signature is invalid");
|
|
668
|
+
}
|
|
669
|
+
if (statement.kind === "session.snapshot") {
|
|
670
|
+
validateSnapshotPayload(statement.payload, statement.generation, "payload");
|
|
671
|
+
} else {
|
|
672
|
+
validateLeasePayload(
|
|
673
|
+
statement.payload,
|
|
674
|
+
statement.kind,
|
|
675
|
+
statement.generation,
|
|
676
|
+
"payload",
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
return Object.freeze({
|
|
680
|
+
statement: structuredClone(statement),
|
|
681
|
+
generation: BigInt(statement.generation),
|
|
682
|
+
digest: statementDigest(statement),
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function validateBootstrapStatement(bootstrap, statement, trust, label) {
|
|
687
|
+
const validated = validateStatement(statement, trust);
|
|
688
|
+
const payload = validated.statement.payload;
|
|
689
|
+
const host =
|
|
690
|
+
validated.statement.kind === "session.snapshot" &&
|
|
691
|
+
Array.isArray(payload.members)
|
|
692
|
+
? payload.members.find(
|
|
693
|
+
(member) => member.principalId === bootstrap.hostPrincipalId,
|
|
694
|
+
)
|
|
695
|
+
: null;
|
|
696
|
+
const publicDigest = createHash("sha256")
|
|
697
|
+
.update(
|
|
698
|
+
decodeBase64Url(
|
|
699
|
+
bootstrap.hostCredentialPublicKeySpki,
|
|
700
|
+
`${label}.hostCredentialPublicKeySpki`,
|
|
701
|
+
4096,
|
|
702
|
+
),
|
|
703
|
+
)
|
|
704
|
+
.digest("hex");
|
|
705
|
+
if (
|
|
706
|
+
validated.statement.kind !== "session.snapshot" ||
|
|
707
|
+
payload.sessionId !== bootstrap.sessionId ||
|
|
708
|
+
payload.agentSessionId !== bootstrap.agentSessionId ||
|
|
709
|
+
payload.hostPrincipalId !== bootstrap.hostPrincipalId ||
|
|
710
|
+
!host ||
|
|
711
|
+
host.status !== "active" ||
|
|
712
|
+
host.credentialKeySha256 !== publicDigest
|
|
713
|
+
) {
|
|
714
|
+
throw trustRejected(`${label} is not authorized by its signed bootstrap`);
|
|
715
|
+
}
|
|
716
|
+
return validated;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function cloneLease(lease) {
|
|
720
|
+
return lease ? { ...lease } : null;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function replayHostStore(store, { missing = false } = {}) {
|
|
724
|
+
if (missing && Object.keys(store).length === 0) {
|
|
725
|
+
return {
|
|
726
|
+
revision: 0n,
|
|
727
|
+
headHash: null,
|
|
728
|
+
trust: null,
|
|
729
|
+
lastAuthorityGeneration: 0n,
|
|
730
|
+
lastStatementIssuedAtMs: 0,
|
|
731
|
+
bootstrap: null,
|
|
732
|
+
records: [],
|
|
733
|
+
statementDigests: new Set(),
|
|
734
|
+
statementReceipts: new Map(),
|
|
735
|
+
leases: new Map(),
|
|
736
|
+
leaseCreatedReceipts: new Map(),
|
|
737
|
+
sessionSnapshot: null,
|
|
738
|
+
lastRecordedAtMs: 0,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
if (
|
|
742
|
+
!hasExactKeys(store, [
|
|
743
|
+
"headHash",
|
|
744
|
+
"lastAuthorityGeneration",
|
|
745
|
+
"lastStatementIssuedAtMs",
|
|
746
|
+
"records",
|
|
747
|
+
"revision",
|
|
748
|
+
"version",
|
|
749
|
+
]) ||
|
|
750
|
+
store.version !== STORE_VERSION ||
|
|
751
|
+
!Array.isArray(store.records) ||
|
|
752
|
+
!NON_NEGATIVE_RE.test(String(store.revision)) ||
|
|
753
|
+
!NON_NEGATIVE_RE.test(String(store.lastAuthorityGeneration)) ||
|
|
754
|
+
!Number.isSafeInteger(store.lastStatementIssuedAtMs) ||
|
|
755
|
+
store.lastStatementIssuedAtMs < 0 ||
|
|
756
|
+
(store.headHash !== null && !DIGEST_RE.test(String(store.headHash)))
|
|
757
|
+
) {
|
|
758
|
+
throw new TypeError("Remote membership host store identity is invalid");
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
let trust = null;
|
|
762
|
+
let bootstrap = null;
|
|
763
|
+
let revision = 0n;
|
|
764
|
+
let headHash = null;
|
|
765
|
+
let lastAuthorityGeneration = 0n;
|
|
766
|
+
let lastStatementIssuedAtMs = 0;
|
|
767
|
+
let sessionSnapshot = null;
|
|
768
|
+
let lastRecordedAtMs = 0;
|
|
769
|
+
const statementDigests = new Set();
|
|
770
|
+
const statementReceipts = new Map();
|
|
771
|
+
const leases = new Map();
|
|
772
|
+
const leaseCreatedReceipts = new Map();
|
|
773
|
+
|
|
774
|
+
for (const [index, record] of store.records.entries()) {
|
|
775
|
+
const label = `records[${index}]`;
|
|
776
|
+
if (
|
|
777
|
+
!record ||
|
|
778
|
+
typeof record !== "object" ||
|
|
779
|
+
Array.isArray(record) ||
|
|
780
|
+
record.schema !== RECORD_SCHEMA ||
|
|
781
|
+
!hasExactKeys(record, RECORD_FIELDS[record.type] || []) ||
|
|
782
|
+
!POSITIVE_RE.test(String(record.revision)) ||
|
|
783
|
+
!DIGEST_RE.test(String(record.recordHash))
|
|
784
|
+
) {
|
|
785
|
+
throw new TypeError(`${label} is invalid`);
|
|
786
|
+
}
|
|
787
|
+
const currentRevision = BigInt(record.revision);
|
|
788
|
+
if (
|
|
789
|
+
currentRevision !== revision + 1n ||
|
|
790
|
+
record.previousRecordHash !== headHash ||
|
|
791
|
+
recordHash(record) !== record.recordHash
|
|
792
|
+
) {
|
|
793
|
+
throw new TypeError(`${label} breaks the receipt chain`);
|
|
794
|
+
}
|
|
795
|
+
safeTimestamp(record.recordedAtMs, `${label}.recordedAtMs`);
|
|
796
|
+
if (record.recordedAtMs < lastRecordedAtMs) {
|
|
797
|
+
throw new TypeError(`${label}.recordedAtMs moves host time backward`);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (record.type === "trust.pinned") {
|
|
801
|
+
const normalized = validateTrustDescriptor(record.trust);
|
|
802
|
+
if (trust && trust.publicKeySha256 !== normalized.publicKeySha256) {
|
|
803
|
+
throw new TypeError(`${label} rotates a pinned coordinator key`);
|
|
804
|
+
}
|
|
805
|
+
trust = normalized;
|
|
806
|
+
} else if (record.type === "session.bootstrap") {
|
|
807
|
+
if (!trust) throw new TypeError(`${label} precedes coordinator trust`);
|
|
808
|
+
const candidate = record.bootstrap;
|
|
809
|
+
if (
|
|
810
|
+
!candidate ||
|
|
811
|
+
!hasExactKeys(candidate, BOOTSTRAP_FIELDS) ||
|
|
812
|
+
candidate.coordinatorId !== trust.coordinatorId ||
|
|
813
|
+
typeof candidate.sessionId !== "string" ||
|
|
814
|
+
typeof candidate.agentSessionId !== "string" ||
|
|
815
|
+
typeof candidate.hostPrincipalId !== "string" ||
|
|
816
|
+
typeof candidate.hostCredentialPublicKeySpki !== "string" ||
|
|
817
|
+
(candidate.hostCredentialPrivateKeyPkcs8 !== null &&
|
|
818
|
+
typeof candidate.hostCredentialPrivateKeyPkcs8 !== "string")
|
|
819
|
+
) {
|
|
820
|
+
throw new TypeError(`${label} has an invalid bootstrap`);
|
|
821
|
+
}
|
|
822
|
+
validateHostBootstrapCredential(candidate, `${label}.bootstrap`);
|
|
823
|
+
validateBootstrapStatement(
|
|
824
|
+
candidate,
|
|
825
|
+
record.statement,
|
|
826
|
+
trust,
|
|
827
|
+
`${label}.statement`,
|
|
828
|
+
);
|
|
829
|
+
if (bootstrap && canonicalJson(bootstrap) !== canonicalJson(candidate)) {
|
|
830
|
+
throw new TypeError(`${label} changes an existing session bootstrap`);
|
|
831
|
+
}
|
|
832
|
+
bootstrap = { ...candidate };
|
|
833
|
+
} else if (record.type === "statement.adopted") {
|
|
834
|
+
if (!trust) throw new TypeError(`${label} precedes coordinator trust`);
|
|
835
|
+
if (!bootstrap) {
|
|
836
|
+
throw new TypeError(`${label} precedes the host session bootstrap`);
|
|
837
|
+
}
|
|
838
|
+
const validated = validateStatement(record.statement, trust);
|
|
839
|
+
if (
|
|
840
|
+
validated.digest !== record.statementDigest ||
|
|
841
|
+
validated.generation < lastAuthorityGeneration
|
|
842
|
+
) {
|
|
843
|
+
throw new TypeError(`${label} is stale or has the wrong digest`);
|
|
844
|
+
}
|
|
845
|
+
if (validated.statement.issuedAtMs < lastStatementIssuedAtMs) {
|
|
846
|
+
throw new TypeError(`${label} moves signed authority time backward`);
|
|
847
|
+
}
|
|
848
|
+
if (statementDigests.has(validated.digest)) {
|
|
849
|
+
throw new TypeError(`${label} repeats a signed statement`);
|
|
850
|
+
}
|
|
851
|
+
statementDigests.add(validated.digest);
|
|
852
|
+
statementReceipts.set(validated.digest, {
|
|
853
|
+
receiptRevision: record.revision,
|
|
854
|
+
receiptHash: record.recordHash,
|
|
855
|
+
});
|
|
856
|
+
lastAuthorityGeneration = validated.generation;
|
|
857
|
+
lastStatementIssuedAtMs = validated.statement.issuedAtMs;
|
|
858
|
+
const payload = validated.statement.payload;
|
|
859
|
+
if (bootstrap) {
|
|
860
|
+
if (
|
|
861
|
+
payload.sessionId !== bootstrap.sessionId ||
|
|
862
|
+
(validated.statement.kind === "session.snapshot" &&
|
|
863
|
+
(payload.agentSessionId !== bootstrap.agentSessionId ||
|
|
864
|
+
payload.hostPrincipalId !== bootstrap.hostPrincipalId)) ||
|
|
865
|
+
(validated.statement.kind !== "session.snapshot" &&
|
|
866
|
+
payload.hostPrincipalId !== bootstrap.hostPrincipalId)
|
|
867
|
+
) {
|
|
868
|
+
throw new TypeError(`${label} is not bound to the host bootstrap`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (validated.statement.kind === "session.snapshot") {
|
|
872
|
+
if (!Array.isArray(payload.members) || !Array.isArray(payload.leases)) {
|
|
873
|
+
throw new TypeError(`${label} has an invalid session snapshot`);
|
|
874
|
+
}
|
|
875
|
+
if (sessionSnapshot) {
|
|
876
|
+
if (
|
|
877
|
+
payload.sessionId !== sessionSnapshot.sessionId ||
|
|
878
|
+
payload.agentSessionId !== sessionSnapshot.agentSessionId ||
|
|
879
|
+
payload.hostPrincipalId !== sessionSnapshot.hostPrincipalId ||
|
|
880
|
+
payload.createdAt !== sessionSnapshot.createdAt ||
|
|
881
|
+
payload.expiresAt !== sessionSnapshot.expiresAt ||
|
|
882
|
+
BigInt(payload.sessionEpoch) < BigInt(sessionSnapshot.sessionEpoch)
|
|
883
|
+
) {
|
|
884
|
+
throw new TypeError(`${label} rolls back the session identity`);
|
|
885
|
+
}
|
|
886
|
+
const currentMembers = new Map(
|
|
887
|
+
payload.members.map((member) => [member.principalId, member]),
|
|
888
|
+
);
|
|
889
|
+
const previousPrincipals = new Set(
|
|
890
|
+
sessionSnapshot.members.map((member) => member.principalId),
|
|
891
|
+
);
|
|
892
|
+
for (const previous of sessionSnapshot.members) {
|
|
893
|
+
const current = currentMembers.get(previous.principalId);
|
|
894
|
+
if (
|
|
895
|
+
!current ||
|
|
896
|
+
BigInt(current.membershipEpoch) < BigInt(previous.membershipEpoch)
|
|
897
|
+
) {
|
|
898
|
+
throw new TypeError(`${label} rolls back a principal epoch`);
|
|
899
|
+
}
|
|
900
|
+
const priorEpoch = BigInt(previous.membershipEpoch);
|
|
901
|
+
const nextEpoch = BigInt(current.membershipEpoch);
|
|
902
|
+
if (
|
|
903
|
+
nextEpoch > priorEpoch + 1n ||
|
|
904
|
+
current.credentialKeySha256 !== previous.credentialKeySha256 ||
|
|
905
|
+
(nextEpoch === priorEpoch &&
|
|
906
|
+
canonicalJson(current) !== canonicalJson(previous)) ||
|
|
907
|
+
(nextEpoch === priorEpoch + 1n &&
|
|
908
|
+
current.status === previous.status)
|
|
909
|
+
) {
|
|
910
|
+
throw new TypeError(`${label} forks or skips a principal epoch`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
for (const current of payload.members) {
|
|
914
|
+
if (
|
|
915
|
+
!previousPrincipals.has(current.principalId) &&
|
|
916
|
+
current.membershipEpoch !== "1"
|
|
917
|
+
) {
|
|
918
|
+
throw new TypeError(
|
|
919
|
+
`${label} introduces a future principal epoch`,
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
const priorSessionEpoch = BigInt(sessionSnapshot.sessionEpoch);
|
|
924
|
+
const nextSessionEpoch = BigInt(payload.sessionEpoch);
|
|
925
|
+
if (
|
|
926
|
+
nextSessionEpoch > priorSessionEpoch + 1n ||
|
|
927
|
+
(nextSessionEpoch === priorSessionEpoch &&
|
|
928
|
+
payload.status !== sessionSnapshot.status) ||
|
|
929
|
+
(nextSessionEpoch === priorSessionEpoch + 1n &&
|
|
930
|
+
(sessionSnapshot.status !== "active" ||
|
|
931
|
+
payload.status !== "closed")) ||
|
|
932
|
+
sessionSnapshot.status === "closed"
|
|
933
|
+
) {
|
|
934
|
+
throw new TypeError(`${label} forks or reopens the session epoch`);
|
|
935
|
+
}
|
|
936
|
+
const currentLeases = new Map(
|
|
937
|
+
payload.leases.map((lease) => [lease.leaseId, lease]),
|
|
938
|
+
);
|
|
939
|
+
for (const previous of sessionSnapshot.leases) {
|
|
940
|
+
const current = currentLeases.get(previous.leaseId);
|
|
941
|
+
if (!current) {
|
|
942
|
+
throw new TypeError(`${label} omits a durable lease`);
|
|
943
|
+
}
|
|
944
|
+
for (const field of [
|
|
945
|
+
"sessionId",
|
|
946
|
+
"sessionEpoch",
|
|
947
|
+
"principalId",
|
|
948
|
+
"membershipEpoch",
|
|
949
|
+
"hostPrincipalId",
|
|
950
|
+
"requestId",
|
|
951
|
+
"fingerprint",
|
|
952
|
+
"binding",
|
|
953
|
+
"createdGeneration",
|
|
954
|
+
"createdAt",
|
|
955
|
+
"expiresAt",
|
|
956
|
+
]) {
|
|
957
|
+
if (current[field] !== previous[field]) {
|
|
958
|
+
throw new TypeError(`${label} changes lease.${field}`);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
const rank = { active: 0, acked: 1, consumed: 2, cancelled: 2 };
|
|
962
|
+
if (rank[current.status] < rank[previous.status]) {
|
|
963
|
+
throw new TypeError(`${label} rolls back a lease state`);
|
|
964
|
+
}
|
|
965
|
+
if (
|
|
966
|
+
rank[current.status] === rank[previous.status] &&
|
|
967
|
+
current.status !== previous.status
|
|
968
|
+
) {
|
|
969
|
+
throw new TypeError(`${label} forks a terminal lease state`);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
sessionSnapshot = structuredClone(payload);
|
|
974
|
+
leases.clear();
|
|
975
|
+
for (const lease of payload.leases) {
|
|
976
|
+
if (!lease || typeof lease.leaseId !== "string") {
|
|
977
|
+
throw new TypeError(`${label} has an invalid snapshot lease`);
|
|
978
|
+
}
|
|
979
|
+
leases.set(lease.leaseId, { ...lease });
|
|
980
|
+
}
|
|
981
|
+
} else {
|
|
982
|
+
if (!payload || typeof payload.leaseId !== "string") {
|
|
983
|
+
throw new TypeError(`${label} has an invalid lease payload`);
|
|
984
|
+
}
|
|
985
|
+
const prior = leases.get(payload.leaseId) || null;
|
|
986
|
+
if (validated.statement.kind === "lease.created") {
|
|
987
|
+
if (prior) throw new TypeError(`${label} recreates a lease`);
|
|
988
|
+
const principal = sessionSnapshot?.members.find(
|
|
989
|
+
(member) => member.principalId === payload.principalId,
|
|
990
|
+
);
|
|
991
|
+
if (
|
|
992
|
+
!sessionSnapshot ||
|
|
993
|
+
sessionSnapshot.status !== "active" ||
|
|
994
|
+
payload.sessionEpoch !== sessionSnapshot.sessionEpoch ||
|
|
995
|
+
!principal ||
|
|
996
|
+
principal.status !== "active" ||
|
|
997
|
+
principal.membershipEpoch !== payload.membershipEpoch ||
|
|
998
|
+
!principal.scopes.includes("approve") ||
|
|
999
|
+
payload.expiresAt > sessionSnapshot.expiresAt
|
|
1000
|
+
) {
|
|
1001
|
+
throw new TypeError(`${label} creates a lease outside membership`);
|
|
1002
|
+
}
|
|
1003
|
+
leaseCreatedReceipts.set(payload.leaseId, record.recordHash);
|
|
1004
|
+
} else if (!prior) {
|
|
1005
|
+
throw new TypeError(`${label} updates an unknown lease`);
|
|
1006
|
+
} else {
|
|
1007
|
+
const immutable = [
|
|
1008
|
+
"sessionId",
|
|
1009
|
+
"sessionEpoch",
|
|
1010
|
+
"principalId",
|
|
1011
|
+
"membershipEpoch",
|
|
1012
|
+
"hostPrincipalId",
|
|
1013
|
+
"requestId",
|
|
1014
|
+
"fingerprint",
|
|
1015
|
+
"binding",
|
|
1016
|
+
"createdGeneration",
|
|
1017
|
+
"createdAt",
|
|
1018
|
+
"expiresAt",
|
|
1019
|
+
];
|
|
1020
|
+
for (const field of immutable) {
|
|
1021
|
+
if (prior[field] !== payload[field]) {
|
|
1022
|
+
throw new TypeError(`${label} changes lease.${field}`);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
if (
|
|
1026
|
+
validated.statement.kind === "lease.acked" &&
|
|
1027
|
+
(prior.status !== "active" ||
|
|
1028
|
+
payload.status !== "acked" ||
|
|
1029
|
+
payload.hostReceiptDigest !==
|
|
1030
|
+
leaseCreatedReceipts.get(payload.leaseId))
|
|
1031
|
+
) {
|
|
1032
|
+
throw new TypeError(`${label} has an invalid ACK transition`);
|
|
1033
|
+
}
|
|
1034
|
+
if (
|
|
1035
|
+
validated.statement.kind === "lease.consumed" &&
|
|
1036
|
+
(prior.status !== "acked" || payload.status !== "consumed")
|
|
1037
|
+
) {
|
|
1038
|
+
throw new TypeError(`${label} has an invalid consume transition`);
|
|
1039
|
+
}
|
|
1040
|
+
if (
|
|
1041
|
+
validated.statement.kind === "lease.cancelled" &&
|
|
1042
|
+
!["active", "acked"].includes(prior.status)
|
|
1043
|
+
) {
|
|
1044
|
+
throw new TypeError(`${label} cancels a terminal lease`);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
leases.set(payload.leaseId, { ...payload });
|
|
1048
|
+
}
|
|
1049
|
+
} else {
|
|
1050
|
+
throw new TypeError(`${label}.type is unsupported`);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
revision = currentRevision;
|
|
1054
|
+
headHash = record.recordHash;
|
|
1055
|
+
lastRecordedAtMs = record.recordedAtMs;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (
|
|
1059
|
+
revision !== BigInt(store.revision) ||
|
|
1060
|
+
headHash !== store.headHash ||
|
|
1061
|
+
lastAuthorityGeneration !== BigInt(store.lastAuthorityGeneration) ||
|
|
1062
|
+
lastStatementIssuedAtMs !== store.lastStatementIssuedAtMs
|
|
1063
|
+
) {
|
|
1064
|
+
throw new TypeError("Remote membership host store head is inconsistent");
|
|
1065
|
+
}
|
|
1066
|
+
return {
|
|
1067
|
+
revision,
|
|
1068
|
+
headHash,
|
|
1069
|
+
trust,
|
|
1070
|
+
lastAuthorityGeneration,
|
|
1071
|
+
lastStatementIssuedAtMs,
|
|
1072
|
+
bootstrap,
|
|
1073
|
+
records: [...store.records],
|
|
1074
|
+
statementDigests,
|
|
1075
|
+
statementReceipts,
|
|
1076
|
+
leases,
|
|
1077
|
+
leaseCreatedReceipts,
|
|
1078
|
+
sessionSnapshot,
|
|
1079
|
+
lastRecordedAtMs,
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
export function getRemoteMembershipHostPaths(agentSessionId) {
|
|
1084
|
+
const id = requiredString(agentSessionId, "agentSessionId");
|
|
1085
|
+
const digest = sha256(Buffer.from(id, "utf8"));
|
|
1086
|
+
return Object.freeze({
|
|
1087
|
+
stateFile: path.join(
|
|
1088
|
+
getStatePath(),
|
|
1089
|
+
"remote-membership-host-v1",
|
|
1090
|
+
`${digest}.json`,
|
|
1091
|
+
),
|
|
1092
|
+
witnessFile: path.join(
|
|
1093
|
+
getMachineSecurityAnchorDir(),
|
|
1094
|
+
"remote-membership-host-v1",
|
|
1095
|
+
`${digest}.witness.json`,
|
|
1096
|
+
),
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
export class DurableRemoteMembershipHostStore {
|
|
1101
|
+
constructor({
|
|
1102
|
+
agentSessionId,
|
|
1103
|
+
stateFile,
|
|
1104
|
+
witnessFile,
|
|
1105
|
+
authorityLockFile = null,
|
|
1106
|
+
expectedPublicKeySha256 = null,
|
|
1107
|
+
now = () => Date.now(),
|
|
1108
|
+
lockOptions,
|
|
1109
|
+
lock = withFileLock,
|
|
1110
|
+
faultHooks = null,
|
|
1111
|
+
} = {}) {
|
|
1112
|
+
const defaults = getRemoteMembershipHostPaths(agentSessionId);
|
|
1113
|
+
this.stateFile = path.resolve(stateFile || defaults.stateFile);
|
|
1114
|
+
this.witnessFile = path.resolve(witnessFile || defaults.witnessFile);
|
|
1115
|
+
this.authorityLockFile = path.resolve(
|
|
1116
|
+
authorityLockFile || `${this.stateFile}.authority`,
|
|
1117
|
+
);
|
|
1118
|
+
if (
|
|
1119
|
+
expectedPublicKeySha256 !== null &&
|
|
1120
|
+
!DIGEST_RE.test(String(expectedPublicKeySha256))
|
|
1121
|
+
) {
|
|
1122
|
+
throw new TypeError("expectedPublicKeySha256 must be a SHA-256 digest");
|
|
1123
|
+
}
|
|
1124
|
+
this.expectedPublicKeySha256 = expectedPublicKeySha256;
|
|
1125
|
+
this._now = now;
|
|
1126
|
+
this._lockOptions = lockOptions;
|
|
1127
|
+
this._lock = lock;
|
|
1128
|
+
this._faultHooks = faultHooks || {};
|
|
1129
|
+
this._quarantined = false;
|
|
1130
|
+
fs.mkdirSync(path.dirname(this.authorityLockFile), {
|
|
1131
|
+
recursive: true,
|
|
1132
|
+
mode: 0o700,
|
|
1133
|
+
});
|
|
1134
|
+
this._observedRevision = 0n;
|
|
1135
|
+
this._observedAuthorityGeneration = 0n;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
_withAuthorityLock(task) {
|
|
1139
|
+
if (this._quarantined) {
|
|
1140
|
+
throw hostError(
|
|
1141
|
+
REMOTE_MEMBERSHIP_HOST_UNAVAILABLE_CODE,
|
|
1142
|
+
"Remote membership host state is quarantined after an unknown commit; restart and reconcile before reuse",
|
|
1143
|
+
null,
|
|
1144
|
+
{ filePath: this.stateFile, commitState: "unknown" },
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
return this._lock(this.authorityLockFile, task, {
|
|
1148
|
+
timeoutMs: 30_000,
|
|
1149
|
+
staleMs: 30_000,
|
|
1150
|
+
failIfUnavailable: true,
|
|
1151
|
+
...this._lockOptions,
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
_observe(snapshot) {
|
|
1156
|
+
if (snapshot.revision < this._observedRevision) {
|
|
1157
|
+
throw rollback("live receipt revision moved backward", {
|
|
1158
|
+
observedRevision: String(snapshot.revision),
|
|
1159
|
+
currentRevision: String(this._observedRevision),
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
if (snapshot.lastAuthorityGeneration < this._observedAuthorityGeneration) {
|
|
1163
|
+
throw rollback("live authority high-water moved backward", {
|
|
1164
|
+
observedGeneration: String(snapshot.lastAuthorityGeneration),
|
|
1165
|
+
currentGeneration: String(this._observedAuthorityGeneration),
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
this._observedRevision = snapshot.revision;
|
|
1169
|
+
this._observedAuthorityGeneration = snapshot.lastAuthorityGeneration;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
_validateWitness(witness) {
|
|
1173
|
+
if (
|
|
1174
|
+
!hasExactKeys(witness, [
|
|
1175
|
+
"headHash",
|
|
1176
|
+
"lastAuthorityGeneration",
|
|
1177
|
+
"pending",
|
|
1178
|
+
"revision",
|
|
1179
|
+
"version",
|
|
1180
|
+
]) ||
|
|
1181
|
+
witness.version !== WITNESS_VERSION ||
|
|
1182
|
+
!NON_NEGATIVE_RE.test(String(witness.revision)) ||
|
|
1183
|
+
!NON_NEGATIVE_RE.test(String(witness.lastAuthorityGeneration)) ||
|
|
1184
|
+
(witness.headHash !== null && !DIGEST_RE.test(String(witness.headHash)))
|
|
1185
|
+
) {
|
|
1186
|
+
throw new TypeError("Remote membership host witness is invalid");
|
|
1187
|
+
}
|
|
1188
|
+
if (witness.pending !== null) {
|
|
1189
|
+
if (
|
|
1190
|
+
!hasExactKeys(witness.pending, [
|
|
1191
|
+
"headHash",
|
|
1192
|
+
"lastAuthorityGeneration",
|
|
1193
|
+
"previousHeadHash",
|
|
1194
|
+
"previousLastAuthorityGeneration",
|
|
1195
|
+
"previousRevision",
|
|
1196
|
+
"revision",
|
|
1197
|
+
]) ||
|
|
1198
|
+
!POSITIVE_RE.test(String(witness.pending.revision)) ||
|
|
1199
|
+
!NON_NEGATIVE_RE.test(
|
|
1200
|
+
String(witness.pending.lastAuthorityGeneration),
|
|
1201
|
+
) ||
|
|
1202
|
+
!NON_NEGATIVE_RE.test(String(witness.pending.previousRevision)) ||
|
|
1203
|
+
!NON_NEGATIVE_RE.test(
|
|
1204
|
+
String(witness.pending.previousLastAuthorityGeneration),
|
|
1205
|
+
) ||
|
|
1206
|
+
!DIGEST_RE.test(String(witness.pending.headHash)) ||
|
|
1207
|
+
(witness.pending.previousHeadHash !== null &&
|
|
1208
|
+
!DIGEST_RE.test(String(witness.pending.previousHeadHash))) ||
|
|
1209
|
+
BigInt(witness.pending.revision) !== BigInt(witness.revision) + 1n ||
|
|
1210
|
+
witness.pending.previousRevision !== witness.revision ||
|
|
1211
|
+
witness.pending.previousHeadHash !== witness.headHash ||
|
|
1212
|
+
witness.pending.previousLastAuthorityGeneration !==
|
|
1213
|
+
witness.lastAuthorityGeneration
|
|
1214
|
+
) {
|
|
1215
|
+
throw new TypeError("Remote membership host pending commit is invalid");
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
return witness;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
_writeInitialWitness() {
|
|
1222
|
+
mutateSecurityStore(
|
|
1223
|
+
this.witnessFile,
|
|
1224
|
+
WITNESS_LABEL,
|
|
1225
|
+
(draft) => {
|
|
1226
|
+
if (Object.keys(draft).length !== 0) {
|
|
1227
|
+
this._validateWitness(draft);
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
draft.version = WITNESS_VERSION;
|
|
1231
|
+
draft.revision = "0";
|
|
1232
|
+
draft.headHash = null;
|
|
1233
|
+
draft.lastAuthorityGeneration = "0";
|
|
1234
|
+
draft.pending = null;
|
|
1235
|
+
},
|
|
1236
|
+
{ ...this._lockOptions, lock: this._lock },
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
_reconcileWitness(snapshot) {
|
|
1241
|
+
try {
|
|
1242
|
+
let witness = readSecurityStore(this.witnessFile, WITNESS_LABEL);
|
|
1243
|
+
if (Object.keys(witness).length === 0) {
|
|
1244
|
+
if (
|
|
1245
|
+
snapshot.revision !== 0n ||
|
|
1246
|
+
snapshot.headHash !== null ||
|
|
1247
|
+
snapshot.lastAuthorityGeneration !== 0n
|
|
1248
|
+
) {
|
|
1249
|
+
throw rollback("durable host state has no rollback witness", {
|
|
1250
|
+
observedRevision: String(snapshot.revision),
|
|
1251
|
+
filePath: this.stateFile,
|
|
1252
|
+
witnessFile: this.witnessFile,
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
this._writeInitialWitness();
|
|
1256
|
+
witness = readSecurityStore(this.witnessFile, WITNESS_LABEL);
|
|
1257
|
+
}
|
|
1258
|
+
this._validateWitness(witness);
|
|
1259
|
+
const committedMatches =
|
|
1260
|
+
BigInt(witness.revision) === snapshot.revision &&
|
|
1261
|
+
witness.headHash === snapshot.headHash &&
|
|
1262
|
+
BigInt(witness.lastAuthorityGeneration) ===
|
|
1263
|
+
snapshot.lastAuthorityGeneration;
|
|
1264
|
+
const pendingMatches =
|
|
1265
|
+
witness.pending !== null &&
|
|
1266
|
+
BigInt(witness.pending.revision) === snapshot.revision &&
|
|
1267
|
+
witness.pending.headHash === snapshot.headHash &&
|
|
1268
|
+
BigInt(witness.pending.lastAuthorityGeneration) ===
|
|
1269
|
+
snapshot.lastAuthorityGeneration;
|
|
1270
|
+
if (!committedMatches && !pendingMatches) {
|
|
1271
|
+
throw rollback(
|
|
1272
|
+
"host state and witness do not describe one recoverable commit",
|
|
1273
|
+
{
|
|
1274
|
+
witnessedRevision: witness.revision,
|
|
1275
|
+
pendingRevision: witness.pending?.revision || null,
|
|
1276
|
+
observedRevision: String(snapshot.revision),
|
|
1277
|
+
},
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
if (witness.pending !== null) {
|
|
1281
|
+
mutateSecurityStore(
|
|
1282
|
+
this.witnessFile,
|
|
1283
|
+
WITNESS_LABEL,
|
|
1284
|
+
(draft) => {
|
|
1285
|
+
this._validateWitness(draft);
|
|
1286
|
+
if (canonicalJson(draft) !== canonicalJson(witness)) {
|
|
1287
|
+
throw rollback("host witness changed during recovery");
|
|
1288
|
+
}
|
|
1289
|
+
if (pendingMatches) {
|
|
1290
|
+
draft.revision = witness.pending.revision;
|
|
1291
|
+
draft.headHash = witness.pending.headHash;
|
|
1292
|
+
draft.lastAuthorityGeneration =
|
|
1293
|
+
witness.pending.lastAuthorityGeneration;
|
|
1294
|
+
}
|
|
1295
|
+
draft.pending = null;
|
|
1296
|
+
},
|
|
1297
|
+
{ ...this._lockOptions, lock: this._lock },
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
} catch (cause) {
|
|
1301
|
+
throw unavailable(cause, this.witnessFile);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
_prepareWitness(previous, next) {
|
|
1306
|
+
try {
|
|
1307
|
+
mutateSecurityStore(
|
|
1308
|
+
this.witnessFile,
|
|
1309
|
+
WITNESS_LABEL,
|
|
1310
|
+
(draft) => {
|
|
1311
|
+
this._validateWitness(draft);
|
|
1312
|
+
if (
|
|
1313
|
+
draft.pending !== null ||
|
|
1314
|
+
BigInt(draft.revision) !== previous.revision ||
|
|
1315
|
+
draft.headHash !== previous.headHash ||
|
|
1316
|
+
BigInt(draft.lastAuthorityGeneration) !==
|
|
1317
|
+
previous.lastAuthorityGeneration ||
|
|
1318
|
+
next.revision !== previous.revision + 1n
|
|
1319
|
+
) {
|
|
1320
|
+
throw rollback("host witness cannot prepare the next commit");
|
|
1321
|
+
}
|
|
1322
|
+
draft.pending = {
|
|
1323
|
+
revision: String(next.revision),
|
|
1324
|
+
headHash: next.headHash,
|
|
1325
|
+
lastAuthorityGeneration: String(next.lastAuthorityGeneration),
|
|
1326
|
+
previousRevision: String(previous.revision),
|
|
1327
|
+
previousHeadHash: previous.headHash,
|
|
1328
|
+
previousLastAuthorityGeneration: String(
|
|
1329
|
+
previous.lastAuthorityGeneration,
|
|
1330
|
+
),
|
|
1331
|
+
};
|
|
1332
|
+
},
|
|
1333
|
+
{ ...this._lockOptions, lock: this._lock },
|
|
1334
|
+
);
|
|
1335
|
+
} catch (cause) {
|
|
1336
|
+
if (cause?.code === REMOTE_MEMBERSHIP_HOST_ROLLBACK_CODE) throw cause;
|
|
1337
|
+
throw unavailable(cause, this.witnessFile);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
_commitWitness(snapshot) {
|
|
1342
|
+
try {
|
|
1343
|
+
mutateSecurityStore(
|
|
1344
|
+
this.witnessFile,
|
|
1345
|
+
WITNESS_LABEL,
|
|
1346
|
+
(draft) => {
|
|
1347
|
+
this._validateWitness(draft);
|
|
1348
|
+
if (
|
|
1349
|
+
draft.pending === null ||
|
|
1350
|
+
BigInt(draft.pending.revision) !== snapshot.revision ||
|
|
1351
|
+
draft.pending.headHash !== snapshot.headHash ||
|
|
1352
|
+
BigInt(draft.pending.lastAuthorityGeneration) !==
|
|
1353
|
+
snapshot.lastAuthorityGeneration
|
|
1354
|
+
) {
|
|
1355
|
+
throw rollback("host witness lost its prepared commit");
|
|
1356
|
+
}
|
|
1357
|
+
draft.revision = draft.pending.revision;
|
|
1358
|
+
draft.headHash = draft.pending.headHash;
|
|
1359
|
+
draft.lastAuthorityGeneration = draft.pending.lastAuthorityGeneration;
|
|
1360
|
+
draft.pending = null;
|
|
1361
|
+
},
|
|
1362
|
+
{ ...this._lockOptions, lock: this._lock },
|
|
1363
|
+
);
|
|
1364
|
+
} catch (cause) {
|
|
1365
|
+
if (cause?.code === REMOTE_MEMBERSHIP_HOST_ROLLBACK_CODE) throw cause;
|
|
1366
|
+
throw unavailable(cause, this.witnessFile);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
_readLocked() {
|
|
1371
|
+
try {
|
|
1372
|
+
const existed = fs.existsSync(this.stateFile);
|
|
1373
|
+
const snapshot = replayHostStore(
|
|
1374
|
+
readSecurityStore(this.stateFile, STORE_LABEL),
|
|
1375
|
+
{ missing: !existed },
|
|
1376
|
+
);
|
|
1377
|
+
this._observe(snapshot);
|
|
1378
|
+
this._reconcileWitness(snapshot);
|
|
1379
|
+
return snapshot;
|
|
1380
|
+
} catch (cause) {
|
|
1381
|
+
throw unavailable(cause, this.stateFile);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
_read() {
|
|
1386
|
+
try {
|
|
1387
|
+
return this._withAuthorityLock(() => this._readLocked());
|
|
1388
|
+
} catch (cause) {
|
|
1389
|
+
throw unavailable(cause, this.stateFile);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
_appendLocked(buildRecord) {
|
|
1394
|
+
let prepared = false;
|
|
1395
|
+
try {
|
|
1396
|
+
const previous = this._readLocked();
|
|
1397
|
+
const recordedAtMs = safeTimestamp(this._now(), "host receipt clock");
|
|
1398
|
+
if (recordedAtMs < previous.lastRecordedAtMs) {
|
|
1399
|
+
throw rollback("host receipt clock moved backward", {
|
|
1400
|
+
observedAtMs: recordedAtMs,
|
|
1401
|
+
durableAtMs: previous.lastRecordedAtMs,
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
const revision = previous.revision + 1n;
|
|
1405
|
+
const record = {
|
|
1406
|
+
schema: RECORD_SCHEMA,
|
|
1407
|
+
revision: String(revision),
|
|
1408
|
+
previousRecordHash: previous.headHash,
|
|
1409
|
+
recordedAtMs,
|
|
1410
|
+
...buildRecord(previous),
|
|
1411
|
+
};
|
|
1412
|
+
record.recordHash = recordHash(record);
|
|
1413
|
+
const nextStore = {
|
|
1414
|
+
version: STORE_VERSION,
|
|
1415
|
+
revision: String(revision),
|
|
1416
|
+
headHash: record.recordHash,
|
|
1417
|
+
lastAuthorityGeneration: String(
|
|
1418
|
+
record.type === "statement.adopted"
|
|
1419
|
+
? BigInt(record.statement.generation)
|
|
1420
|
+
: previous.lastAuthorityGeneration,
|
|
1421
|
+
),
|
|
1422
|
+
lastStatementIssuedAtMs:
|
|
1423
|
+
record.type === "statement.adopted"
|
|
1424
|
+
? record.statement.issuedAtMs
|
|
1425
|
+
: previous.lastStatementIssuedAtMs,
|
|
1426
|
+
records: [...previous.records, record],
|
|
1427
|
+
};
|
|
1428
|
+
const verified = replayHostStore(nextStore);
|
|
1429
|
+
this._prepareWitness(previous, verified);
|
|
1430
|
+
prepared = true;
|
|
1431
|
+
const result = mutateSecurityStore(
|
|
1432
|
+
this.stateFile,
|
|
1433
|
+
STORE_LABEL,
|
|
1434
|
+
(draft) => {
|
|
1435
|
+
const current = replayHostStore(draft, {
|
|
1436
|
+
missing: !fs.existsSync(this.stateFile),
|
|
1437
|
+
});
|
|
1438
|
+
if (
|
|
1439
|
+
current.revision !== previous.revision ||
|
|
1440
|
+
current.headHash !== previous.headHash
|
|
1441
|
+
) {
|
|
1442
|
+
throw rollback("host state changed outside its authority lock");
|
|
1443
|
+
}
|
|
1444
|
+
for (const key of Object.keys(draft)) delete draft[key];
|
|
1445
|
+
Object.assign(draft, structuredClone(nextStore));
|
|
1446
|
+
return { snapshot: verified, record };
|
|
1447
|
+
},
|
|
1448
|
+
{ ...this._lockOptions, lock: this._lock },
|
|
1449
|
+
);
|
|
1450
|
+
this._faultHooks.afterStateCommit?.({
|
|
1451
|
+
record: structuredClone(result.record),
|
|
1452
|
+
stateFile: this.stateFile,
|
|
1453
|
+
witnessFile: this.witnessFile,
|
|
1454
|
+
});
|
|
1455
|
+
this._observe(result.snapshot);
|
|
1456
|
+
this._commitWitness(result.snapshot);
|
|
1457
|
+
prepared = false;
|
|
1458
|
+
return result;
|
|
1459
|
+
} catch (cause) {
|
|
1460
|
+
if (prepared) {
|
|
1461
|
+
this._quarantined = true;
|
|
1462
|
+
cause.commitState = "unknown";
|
|
1463
|
+
}
|
|
1464
|
+
throw unavailable(cause, this.stateFile);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
_append(buildRecord) {
|
|
1469
|
+
try {
|
|
1470
|
+
return this._withAuthorityLock(() => this._appendLocked(buildRecord));
|
|
1471
|
+
} catch (cause) {
|
|
1472
|
+
throw unavailable(cause, this.stateFile);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
pinTrust(trustDescriptor) {
|
|
1477
|
+
const trust = validateTrustDescriptor(trustDescriptor);
|
|
1478
|
+
if (
|
|
1479
|
+
this.expectedPublicKeySha256 &&
|
|
1480
|
+
trust.publicKeySha256 !== this.expectedPublicKeySha256
|
|
1481
|
+
) {
|
|
1482
|
+
throw trustRejected("key does not match the configured pin", {
|
|
1483
|
+
expectedPublicKeySha256: this.expectedPublicKeySha256,
|
|
1484
|
+
observedPublicKeySha256: trust.publicKeySha256,
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
const serializable = {
|
|
1488
|
+
authorityVersion: trust.authorityVersion,
|
|
1489
|
+
algorithm: trust.algorithm,
|
|
1490
|
+
coordinatorId: trust.coordinatorId,
|
|
1491
|
+
keyId: trust.keyId,
|
|
1492
|
+
publicKeySpki: trust.publicKeySpki,
|
|
1493
|
+
publicKeySha256: trust.publicKeySha256,
|
|
1494
|
+
};
|
|
1495
|
+
return this._withAuthorityLock(() => {
|
|
1496
|
+
const current = this._readLocked();
|
|
1497
|
+
if (current.trust) {
|
|
1498
|
+
if (current.trust.publicKeySha256 !== trust.publicKeySha256) {
|
|
1499
|
+
throw trustRejected("a different coordinator key is already pinned");
|
|
1500
|
+
}
|
|
1501
|
+
return Object.freeze({
|
|
1502
|
+
coordinatorId: current.trust.coordinatorId,
|
|
1503
|
+
publicKeySha256: current.trust.publicKeySha256,
|
|
1504
|
+
alreadyPinned: true,
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
this._appendLocked(() => ({
|
|
1508
|
+
type: "trust.pinned",
|
|
1509
|
+
trust: serializable,
|
|
1510
|
+
}));
|
|
1511
|
+
return Object.freeze({
|
|
1512
|
+
coordinatorId: trust.coordinatorId,
|
|
1513
|
+
publicKeySha256: trust.publicKeySha256,
|
|
1514
|
+
alreadyPinned: false,
|
|
1515
|
+
});
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
recordBootstrap({
|
|
1520
|
+
coordinatorId,
|
|
1521
|
+
sessionId,
|
|
1522
|
+
agentSessionId,
|
|
1523
|
+
hostPrincipalId,
|
|
1524
|
+
hostCredentialPublicKeySpki,
|
|
1525
|
+
hostCredentialPrivateKeyPkcs8 = null,
|
|
1526
|
+
statement,
|
|
1527
|
+
} = {}) {
|
|
1528
|
+
const bootstrap = {
|
|
1529
|
+
coordinatorId: requiredString(coordinatorId, "coordinatorId"),
|
|
1530
|
+
sessionId: requiredString(sessionId, "sessionId"),
|
|
1531
|
+
agentSessionId: requiredString(agentSessionId, "agentSessionId"),
|
|
1532
|
+
hostPrincipalId: requiredString(hostPrincipalId, "hostPrincipalId"),
|
|
1533
|
+
hostCredentialPublicKeySpki: requiredString(
|
|
1534
|
+
hostCredentialPublicKeySpki,
|
|
1535
|
+
"hostCredentialPublicKeySpki",
|
|
1536
|
+
),
|
|
1537
|
+
hostCredentialPrivateKeyPkcs8:
|
|
1538
|
+
hostCredentialPrivateKeyPkcs8 === null
|
|
1539
|
+
? null
|
|
1540
|
+
: requiredString(
|
|
1541
|
+
hostCredentialPrivateKeyPkcs8,
|
|
1542
|
+
"hostCredentialPrivateKeyPkcs8",
|
|
1543
|
+
),
|
|
1544
|
+
};
|
|
1545
|
+
validateHostBootstrapCredential(bootstrap);
|
|
1546
|
+
return this._withAuthorityLock(() => {
|
|
1547
|
+
const current = this._readLocked();
|
|
1548
|
+
if (!current.trust || current.trust.coordinatorId !== coordinatorId) {
|
|
1549
|
+
throw trustRejected("session bootstrap does not match pinned trust");
|
|
1550
|
+
}
|
|
1551
|
+
const validatedBootstrap = validateBootstrapStatement(
|
|
1552
|
+
bootstrap,
|
|
1553
|
+
statement,
|
|
1554
|
+
current.trust,
|
|
1555
|
+
"bootstrapStatement",
|
|
1556
|
+
);
|
|
1557
|
+
if (current.bootstrap) {
|
|
1558
|
+
if (canonicalJson(current.bootstrap) !== canonicalJson(bootstrap)) {
|
|
1559
|
+
throw new Error("Remote membership session bootstrap already exists");
|
|
1560
|
+
}
|
|
1561
|
+
return Object.freeze({ ...current.bootstrap });
|
|
1562
|
+
}
|
|
1563
|
+
this._appendLocked(() => ({
|
|
1564
|
+
type: "session.bootstrap",
|
|
1565
|
+
bootstrap: { ...bootstrap },
|
|
1566
|
+
statement: validatedBootstrap.statement,
|
|
1567
|
+
}));
|
|
1568
|
+
return Object.freeze({ ...bootstrap });
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
getBootstrap() {
|
|
1573
|
+
const bootstrap = this._read().bootstrap;
|
|
1574
|
+
return bootstrap ? Object.freeze({ ...bootstrap }) : null;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
signAuthenticationChallenge(challenge) {
|
|
1578
|
+
return this._withAuthorityLock(() => {
|
|
1579
|
+
const snapshot = this._readLocked();
|
|
1580
|
+
const bootstrap = snapshot.bootstrap;
|
|
1581
|
+
if (
|
|
1582
|
+
!bootstrap ||
|
|
1583
|
+
bootstrap.hostCredentialPrivateKeyPkcs8 === null ||
|
|
1584
|
+
!hasExactKeys(challenge, AUTHENTICATION_CHALLENGE_FIELDS) ||
|
|
1585
|
+
challenge.schema !==
|
|
1586
|
+
"chainlesschain.remote-membership-authentication-challenge/v1" ||
|
|
1587
|
+
challenge.authorityVersion !== REMOTE_MEMBERSHIP_COORDINATOR_VERSION ||
|
|
1588
|
+
challenge.coordinatorId !== bootstrap.coordinatorId ||
|
|
1589
|
+
challenge.sessionId !== bootstrap.sessionId ||
|
|
1590
|
+
challenge.principalId !== bootstrap.hostPrincipalId ||
|
|
1591
|
+
challenge.credentialType !== "ed25519" ||
|
|
1592
|
+
challenge.credentialPublicKey !==
|
|
1593
|
+
bootstrap.hostCredentialPublicKeySpki ||
|
|
1594
|
+
challenge.purpose !== "session.resume" ||
|
|
1595
|
+
challenge.scopes !== null
|
|
1596
|
+
) {
|
|
1597
|
+
throw trustRejected("authentication challenge is not host-bound");
|
|
1598
|
+
}
|
|
1599
|
+
requiredString(challenge.serverInstanceId, "challenge.serverInstanceId");
|
|
1600
|
+
requiredString(challenge.challengeId, "challenge.challengeId");
|
|
1601
|
+
requiredString(challenge.connectionNonce, "challenge.connectionNonce");
|
|
1602
|
+
requiredString(challenge.serverNonce, "challenge.serverNonce");
|
|
1603
|
+
canonicalInteger(challenge.sessionEpoch, "challenge.sessionEpoch");
|
|
1604
|
+
canonicalInteger(challenge.membershipEpoch, "challenge.membershipEpoch");
|
|
1605
|
+
safeTimestamp(challenge.issuedAtMs, "challenge.issuedAtMs");
|
|
1606
|
+
safeTimestamp(challenge.expiresAtMs, "challenge.expiresAtMs");
|
|
1607
|
+
if (
|
|
1608
|
+
challenge.expiresAtMs <= challenge.issuedAtMs ||
|
|
1609
|
+
challenge.expiresAtMs <= safeTimestamp(this._now(), "host clock") ||
|
|
1610
|
+
challenge.credentialKeySha256 !==
|
|
1611
|
+
bootstrap.hostPrincipalId.slice("ed25519:".length) ||
|
|
1612
|
+
(snapshot.sessionSnapshot &&
|
|
1613
|
+
(challenge.sessionEpoch !== snapshot.sessionSnapshot.sessionEpoch ||
|
|
1614
|
+
challenge.membershipEpoch !==
|
|
1615
|
+
snapshot.sessionSnapshot.members.find(
|
|
1616
|
+
(member) => member.principalId === bootstrap.hostPrincipalId,
|
|
1617
|
+
)?.membershipEpoch))
|
|
1618
|
+
) {
|
|
1619
|
+
throw trustRejected("authentication challenge is stale or malformed");
|
|
1620
|
+
}
|
|
1621
|
+
return signRemoteMembershipAuthenticationChallenge(
|
|
1622
|
+
challenge,
|
|
1623
|
+
bootstrap.hostCredentialPrivateKeyPkcs8,
|
|
1624
|
+
);
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
adopt(statement, { expectedKind = null, expectedSessionId = null } = {}) {
|
|
1629
|
+
return this._withAuthorityLock(() => {
|
|
1630
|
+
const current = this._readLocked();
|
|
1631
|
+
if (!current.trust) {
|
|
1632
|
+
throw trustRejected("no coordinator key is pinned");
|
|
1633
|
+
}
|
|
1634
|
+
if (!current.bootstrap) {
|
|
1635
|
+
throw trustRejected("no host session bootstrap is recorded");
|
|
1636
|
+
}
|
|
1637
|
+
const validated = validateStatement(statement, current.trust);
|
|
1638
|
+
if (expectedKind && validated.statement.kind !== expectedKind) {
|
|
1639
|
+
throw trustRejected("statement kind does not match the protocol step", {
|
|
1640
|
+
expectedKind,
|
|
1641
|
+
observedKind: validated.statement.kind,
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
const payloadSessionId = validated.statement.payload.sessionId;
|
|
1645
|
+
if (expectedSessionId && payloadSessionId !== expectedSessionId) {
|
|
1646
|
+
throw trustRejected("statement session does not match", {
|
|
1647
|
+
expectedSessionId,
|
|
1648
|
+
observedSessionId: payloadSessionId || null,
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
if (validated.generation < current.lastAuthorityGeneration) {
|
|
1652
|
+
throw rollback(
|
|
1653
|
+
"signed statement is older than the durable high-water",
|
|
1654
|
+
{
|
|
1655
|
+
statementGeneration: String(validated.generation),
|
|
1656
|
+
currentGeneration: String(current.lastAuthorityGeneration),
|
|
1657
|
+
},
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
if (current.statementDigests.has(validated.digest)) {
|
|
1661
|
+
const receipt = current.statementReceipts.get(validated.digest);
|
|
1662
|
+
return Object.freeze({
|
|
1663
|
+
adopted: false,
|
|
1664
|
+
replayed: true,
|
|
1665
|
+
statementDigest: validated.digest,
|
|
1666
|
+
authorityGeneration: String(validated.generation),
|
|
1667
|
+
receiptRevision: receipt.receiptRevision,
|
|
1668
|
+
receiptHash: receipt.receiptHash,
|
|
1669
|
+
payload: structuredClone(validated.statement.payload),
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
const result = this._appendLocked((snapshot) => {
|
|
1673
|
+
if (validated.generation < snapshot.lastAuthorityGeneration) {
|
|
1674
|
+
throw rollback("statement became stale before durable adoption");
|
|
1675
|
+
}
|
|
1676
|
+
if (snapshot.statementDigests.has(validated.digest)) {
|
|
1677
|
+
throw new Error("statement was adopted concurrently");
|
|
1678
|
+
}
|
|
1679
|
+
return {
|
|
1680
|
+
type: "statement.adopted",
|
|
1681
|
+
statementDigest: validated.digest,
|
|
1682
|
+
statement: validated.statement,
|
|
1683
|
+
};
|
|
1684
|
+
});
|
|
1685
|
+
return Object.freeze({
|
|
1686
|
+
adopted: true,
|
|
1687
|
+
replayed: false,
|
|
1688
|
+
statementDigest: validated.digest,
|
|
1689
|
+
authorityGeneration: String(validated.generation),
|
|
1690
|
+
receiptRevision: String(result.snapshot.revision),
|
|
1691
|
+
receiptHash: result.snapshot.headHash,
|
|
1692
|
+
payload: structuredClone(validated.statement.payload),
|
|
1693
|
+
});
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
getLease(leaseId) {
|
|
1698
|
+
const lease = this._read().leases.get(String(leaseId));
|
|
1699
|
+
return lease ? Object.freeze(cloneLease(lease)) : null;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
requireConsumableLease(leaseId) {
|
|
1703
|
+
return this._withAuthorityLock(() => {
|
|
1704
|
+
const snapshot = this._readLocked();
|
|
1705
|
+
const now = safeTimestamp(this._now(), "host execution clock");
|
|
1706
|
+
if (now < snapshot.lastRecordedAtMs) {
|
|
1707
|
+
throw rollback("host execution clock moved backward", {
|
|
1708
|
+
observedAtMs: now,
|
|
1709
|
+
durableAtMs: snapshot.lastRecordedAtMs,
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
const lease = snapshot.leases.get(String(leaseId));
|
|
1713
|
+
if (!lease) throw new Error("Remote execution lease is unknown");
|
|
1714
|
+
if (lease.status !== "acked") {
|
|
1715
|
+
throw new Error("Remote execution lease is not ACKed or is terminal");
|
|
1716
|
+
}
|
|
1717
|
+
if (lease.expiresAt <= now) {
|
|
1718
|
+
throw new Error("Remote execution lease expired");
|
|
1719
|
+
}
|
|
1720
|
+
return Object.freeze(cloneLease(lease));
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
inspect() {
|
|
1725
|
+
const snapshot = this._read();
|
|
1726
|
+
return Object.freeze({
|
|
1727
|
+
version: REMOTE_MEMBERSHIP_HOST_STORE_VERSION,
|
|
1728
|
+
revision: String(snapshot.revision),
|
|
1729
|
+
receiptHead: snapshot.headHash,
|
|
1730
|
+
lastAuthorityGeneration: String(snapshot.lastAuthorityGeneration),
|
|
1731
|
+
coordinatorId: snapshot.trust?.coordinatorId || null,
|
|
1732
|
+
publicKeySha256: snapshot.trust?.publicKeySha256 || null,
|
|
1733
|
+
bootstrap: snapshot.bootstrap
|
|
1734
|
+
? Object.freeze({ ...snapshot.bootstrap })
|
|
1735
|
+
: null,
|
|
1736
|
+
leases: Object.freeze(
|
|
1737
|
+
[...snapshot.leases.values()].map((lease) =>
|
|
1738
|
+
Object.freeze(cloneLease(lease)),
|
|
1739
|
+
),
|
|
1740
|
+
),
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
export const _remoteMembershipHostStoreInternals = Object.freeze({
|
|
1746
|
+
replayHostStore,
|
|
1747
|
+
statementDigest,
|
|
1748
|
+
validateStatement,
|
|
1749
|
+
validateTrustDescriptor,
|
|
1750
|
+
});
|