chainlesschain 0.163.8 → 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 +7 -4
- 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/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-CavM7W3k.js → AIOps-CYxFDrcm.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CUvZGabQ.js → ActionButton-CPBtdiU2.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DeBnWnbe.js → Analytics-Dayq7Dp7.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DbI5kxvd.js → AppLayout-6U7bPb2s.js} +4 -4
- package/src/assets/web-panel/assets/{Artifacts-D93bigKm.js → Artifacts-D2Iqg_3D.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CBNvfUZa.js → Audit-CdoOB7WV.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-CJLyh_YU.js → BackgroundAgents-DrnAVB8w.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-D_0yoehD.js → Backup-CesMi5cy.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-TJxmbObW.js → BaseInput-DaX9ptax.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CE0dto1q.js → Chat-OrwOPxDS.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CZ5-yYiD.js → ChatBubbleRenderer-CvPZFRxM.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-Ck1RVIVo.js → Checkbox-DiHbcysC.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-qpoyf6DK.js → Codegen-CbgCkaBJ.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DUyPWjIE.js → Col-Du5hgrh4.js} +1 -1
- package/src/assets/web-panel/assets/{Community-D_S1BemS.js → Community-DsPOBW-p.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BjxeK-Gc.js → Compact-BwHIFkNC.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-Dd3CEJsu.js → Compliance-BvrDQSgd.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-B4QG7-ad.js → Cowork-gwAE6arx.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-BtZnids5.js → Cron-DW-0l4uF.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-CHPfCBFD.js → Crosschain-j-NsLesB.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DT8DCEOK.js → DID-CibVEBOI.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-COQab5_U.js → Dashboard-W4z9vSfe.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BY1PsQuU.js → Dropdown-DAcrkKxG.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CsuAuaq3.js → EmailListRenderer-6GXah-89.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BqpinD-j.js → FamilyGuardDashboard-TKDCq8Ku.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-D0gkfQFv.js → Federation-BXBLlcNm.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BqgE-axK.js → FormItemContext-BoJJBkG3.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-D0hJ2KvC.js → GenericCardRenderer-okjsl46v.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CmMB0uPY.js → Git-Rvs7i6nn.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BnVEI0Dk.js → Governance-CHSUxXXS.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-YTAMihRP.js → Inference-V3BlfxsB.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-0ax7VHop.js → KnowledgeGraph-C_NlD4aR.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-B9TRcokL.js → Logs-Nn4LNxhA.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-SVAgNnkt.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-B6CSuD_H.js → Marketplace-DEmjFcg-.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-CjMXvOVU.js → McpTools-DlopXipI.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-CXRF71U3.js → Memory-BG3ynX60.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DQgNp_oS.js → MobileBridge-DeRzWOfT.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-DlPwcCbq.js → MobileProjects-Tyd3o2A0.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-6hDTRPe1.js → Mtc-BoPbOv1C.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-BlVHU1DS.js → MtcAudit-un-rTHp7.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-DP9pzEbV.js → Multisig-Ds0oveEB.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-Djorh8EV.js → NLProgramming-D09JI-yY.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-oLZ-JkUg.js → Notes-DTRhiKJn.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-Cg288VH3.js → NotificationSettings-BAnnNCim.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-Yr-1fSYi.js +1 -0
- package/src/assets/web-panel/assets/{Organization-CBdo-bHs.js → Organization-S9EAolpj.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-qZdnVumx.js → Overflow-CqcLKqJ8.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-D4kNfGNQ.js → P2P-Dw0F9EcU.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-DnlMemM7.js → PdhVaultBrowser-BDRwUevN.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-DHucHFBJ.js → Permissions-D3_c3nKB.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BovlOIPR.js → PersonalDataHub-CxdMV98x.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-Deru3CQY.js → Pipeline-CgjNQFLI.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-Bq5oTTbU.js → Privacy-BHghIIht.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BfzxVWAz.js → ProjectInit-De6pqaKM.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CKC6XBtH.js → ProjectSettings-Bg4ZkNYQ.js} +2 -2
- package/src/assets/web-panel/assets/Projects-CP8vPvzs.js +1 -0
- package/src/assets/web-panel/assets/{Providers-C_jx4TWX.js → Providers-CUnHbNf-.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BdIHeVGC.js → QrScannerModal-DA2f3Wts.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-BwZwAcPD.js → QuickAsk-C1FeHDqp.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BNTt0L-m.js → Recommend-B5guaUMN.js} +1 -1
- package/src/assets/web-panel/assets/RemoteSession-PDADSds3.js +5 -0
- package/src/assets/web-panel/assets/{Reputation-D6VpG32B.js → Reputation-eTlRhgvK.js} +1 -1
- package/src/assets/web-panel/assets/{Row-D_74OvQQ.js → Row-D7AqSGY0.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BTr0a-BQ.js → RssFeed-JiuLm4Ja.js} +3 -3
- package/src/assets/web-panel/assets/{Search-C3MCFoNU.js → Search-CwruahW3.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CqW9aJ6s.js → Security-CdhqaR0b.js} +4 -4
- package/src/assets/web-panel/assets/{Services-Cw5GNaHF.js → Services-CU0ZIlcT.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DZps5qeY.js → Skeleton-CtduPfAb.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-C6NQ1znT.js → Skills-CROXm6AY.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DFP8G2Sh.js → Sla-DiZiABFD.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-ESt6MB6v.js → SpeechSettings-B5pLcXUw.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BHnN98aM.js → SyncSettings-DBX6J1Oe.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-NiSCaOf-.js → Tasks-BLyRJd3-.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DsKFhW2C.js → Templates-BKa5qzmk.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CQ0E0ze5.js → Tenant-DnD3a4M6.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-D_rBu_by.js → Terminal-DEitQeTb.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-D3Vm4Po4.js → TimelineRenderer-CjHggTDq.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DNTxRH6q.js → Tokens-CsGT7sfl.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-Dgprz1ms.js → Trigger-yr4rFDrW.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-B2rUnVBD.js → Trust-CLvu1Sjp.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DEGQNc6_.js → UkeySign-B0m7yUP8.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-jlsAx3Eh.js → VideoEditing-C0VvP4lc.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-8jQih1ij.js → Wallet-kllBAG1M.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-XLeO6xIB.js → WebAuthn-C54UQNLx.js} +3 -3
- package/src/assets/web-panel/assets/{WorkflowEditor-De_dgDAG.js → WorkflowEditor-B7y8qSvU.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CI19R_Sk.js → chat-DRzTh8CJ.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DsYTOYN3.js → colors-BSMwSnCx.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CM3-t_Qr.js → compact-item-BTg602H5.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BjxlBRZv.js → createContext-B6EGsdvp.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-Dol8jLEP.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-RDaWX_4i.js → hasIn-DDKqJ8RD.js} +1 -1
- package/src/assets/web-panel/assets/{index-nAH87mQP.js → index-323Hhxxt.js} +1 -1
- package/src/assets/web-panel/assets/{index-DIUnNOjG.js → index-BICn3Qh1.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyIWssPC.js → index-BMuGd2--.js} +1 -1
- package/src/assets/web-panel/assets/{index-DX7tuWVJ.js → index-BRqt4fO-.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cd3euP_4.js → index-BVTAl5N2.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cpqm1qfU.js → index-BsR7Y2NG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DcPFB8qJ.js → index-Bx044qDj.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cd9OJhHG.js → index-C1GFfuqN.js} +1 -1
- package/src/assets/web-panel/assets/index-C3QX5tOl.js +1 -0
- package/src/assets/web-panel/assets/{index-B-Z8eFUJ.js → index-C66NpWy5.js} +1 -1
- package/src/assets/web-panel/assets/{index-BlT2VAEK.js → index-CDTvsX8t.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dsij_KsN.js → index-CGNklbP6.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4gkzxkw.js → index-CHfLYwcW.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6sm5-Xq.js → index-CNxg61YP.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0yx-jtQ.js → index-CQlHFN_g.js} +1 -1
- package/src/assets/web-panel/assets/{index-CjTrRt13.js → index-CUEU48o1.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7WBt_4T.js → index-CWGax2QW.js} +1 -1
- package/src/assets/web-panel/assets/{index-IeiL9RKc.js → index-C_JzIXUq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CnHH7bnD.js → index-Cf23wVMs.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKJr-TZp.js → index-CmAInUkX.js} +1 -1
- package/src/assets/web-panel/assets/{index-CiECJi-Q.js → index-CmAubjDi.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCnZyIMA.js → index-Co7oM1qr.js} +1 -1
- package/src/assets/web-panel/assets/{index-Drlnwr8F.js → index-CwPUZbMI.js} +1 -1
- package/src/assets/web-panel/assets/{index-9R6BxAr0.js → index-D1naNj4U.js} +1 -1
- package/src/assets/web-panel/assets/{index-BGqqJfL0.js → index-D5CsBShd.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bi46Xu4u.js → index-DBT3YeQ9.js} +1 -1
- package/src/assets/web-panel/assets/{index-DXXtGMUJ.js → index-DLdrHhML.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0PMlbMs.js → index-Dc77VR3H.js} +1 -1
- package/src/assets/web-panel/assets/{index-D0zcy54x.js → index-Dfv2lKu3.js} +1 -1
- package/src/assets/web-panel/assets/{index-BN6UPD2E.js → index-DiW0sDkz.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dxge-xWl.js → index-DpTHIRfZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CyOGN6a5.js → index-DqFXGzzC.js} +1 -1
- package/src/assets/web-panel/assets/{index-B9O9YyWE.js → index-XFe-1twF.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHNYPO0b.js → index-p2HYn89m.js} +3 -3
- package/src/assets/web-panel/assets/{index-CVQMeiwH.js → index-pqfux8vZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-pPL66J85.js → index-rjzk9z1Q.js} +1 -1
- package/src/assets/web-panel/assets/{index-DoQA0-52.js → index-th2ihDc0.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5REnKt8.js → index-u2YVld9W.js} +1 -1
- package/src/assets/web-panel/assets/index-xYybvEic.js +1 -0
- package/src/assets/web-panel/assets/{initDefaultProps-BTL88of4.js → initDefaultProps-C3Djh9fa.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BUB2pXwB.js → motion-BzqFmbSe.js} +1 -1
- package/src/assets/web-panel/assets/{move-egCkkOMm.js → move-DJo_FFaW.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DAWhZC8c.js → mtc-parser-_tlTiBvk.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CvOxe_6d.js → omit-CB2ZyxUg.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DZUSLZ5Z.js → pickAttrs-DZwIFD4u.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BERxGNMY.js → placementArrow-DxZY0dgk.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DZuDfZ9n.js → responsiveObserve-CzzGt7GL.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DO756sgI.js → slide-BaIK52yz.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BRQlIz28.js → statusUtils-3-FHu9_M.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DCSfdybl.js → styleChecker-BkRT68fK.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-LD8dWVve.js → useFlexGapSupport-DHYwKS-s.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BX9uW2LK.js → useFs-DVVUhV8p.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BPSlPuHU.js → usePersonalDataHub-CkrFKAzQ.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BmOILC9V.js → vnode-BuBLFz0c.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-1iKowKGg.js → zoom-kpm6LtXU.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +3 -3
- package/src/commands/cowork.js +418 -41
- 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/data/changelog.json +28 -2
- package/src/gateways/ws/action-protocol.js +316 -17
- package/src/gateways/ws/message-dispatcher.js +30 -0
- package/src/gateways/ws/remote-session-protocol.js +565 -35
- package/src/gateways/ws/ws-agent-handler.js +12 -0
- package/src/gateways/ws/ws-approval-gate.js +5 -0
- package/src/gateways/ws/ws-server.js +510 -50
- package/src/harness/jsonl-session-store.js +341 -24
- package/src/harness/mcp-client.js +8 -1
- package/src/harness/remote-session-audit.js +2 -0
- package/src/harness/remote-session-crypto.js +929 -21
- package/src/harness/remote-session-registry.js +945 -24
- package/src/harness/session-list-index.js +233 -14
- package/src/lib/agent-authority.js +6 -0
- package/src/lib/auto-mode-config.js +98 -3
- package/src/lib/background-agent-keeper-client.js +248 -0
- package/src/lib/background-agent-keeper-protocol.js +291 -0
- package/src/lib/background-agent-supervisor.js +1357 -85
- package/src/lib/background-interaction-resolver.js +12 -0
- package/src/lib/background-session-transport.js +23 -6
- package/src/lib/background-turn-bootstrap-protocol.js +190 -0
- package/src/lib/cowork-workflow.js +1188 -76
- package/src/lib/delivery-production-adapter.js +38 -2
- package/src/lib/dynamic-workflow-facade.js +1736 -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 +777 -34
- package/src/lib/remote-control.js +2 -0
- package/src/lib/remote-membership-authority.js +632 -0
- package/src/lib/remote-membership-coordinator.js +2973 -0
- package/src/lib/remote-membership-host-store.js +1799 -0
- package/src/lib/runtime-provenance-ledger.js +171 -46
- package/src/lib/scoped-permission-store.js +369 -0
- package/src/lib/session-anti-rollback-anchor.js +262 -5
- package/src/lib/shell-approval.js +169 -6
- package/src/lib/sub-agent-context.js +4 -0
- package/src/lib/with-file-lock.js +73 -38
- package/src/lib/workflow-definition-contract.js +149 -0
- package/src/repl/agent-repl.js +111 -36
- 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/repl/remote-approval.js +29 -7
- package/src/runtime/agent-core.js +1182 -155
- package/src/runtime/headless-runner.js +52 -30
- package/src/runtime/headless-stream.js +51 -28
- package/src/workers/background-agent-keeper.js +689 -0
- package/src/workers/background-agent-worker.js +365 -13
- package/src/workers/background-turn-bootstrap.js +170 -0
- package/src/assets/web-panel/assets/MarkdownRenderer-yySN5YNM.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-Bb1O3Ofr.js +0 -1
- package/src/assets/web-panel/assets/Projects-CMolGhae.js +0 -1
- package/src/assets/web-panel/assets/RemoteSession-Biyn2-Ro.js +0 -5
- package/src/assets/web-panel/assets/devWarning-S1ld2LwU.js +0 -1
- package/src/assets/web-panel/assets/index-Drz6yq0m.js +0 -1
- package/src/assets/web-panel/assets/index-uWiUs7S_.js +0 -1
|
@@ -7,6 +7,7 @@ export const REMOTE_SESSION_SCOPES = Object.freeze([
|
|
|
7
7
|
"approve",
|
|
8
8
|
"interrupt",
|
|
9
9
|
]);
|
|
10
|
+
export const REMOTE_APPROVAL_BINDING_CAPABILITY = "approval-binding-v1";
|
|
10
11
|
|
|
11
12
|
const DEFAULT_TOKEN_TTL_MS = 5 * 60 * 1000;
|
|
12
13
|
const DEFAULT_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
|
|
@@ -36,6 +37,25 @@ function normalizeScopes(scopes) {
|
|
|
36
37
|
return unique;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
function normalizeCapabilities(capabilities) {
|
|
41
|
+
if (capabilities == null) return [];
|
|
42
|
+
if (!Array.isArray(capabilities) || capabilities.length > 32) {
|
|
43
|
+
throw new TypeError("Remote session capabilities must be a bounded array");
|
|
44
|
+
}
|
|
45
|
+
const normalized = [...new Set(capabilities)];
|
|
46
|
+
for (const capability of normalized) {
|
|
47
|
+
if (
|
|
48
|
+
typeof capability !== "string" ||
|
|
49
|
+
capability.length === 0 ||
|
|
50
|
+
capability.length > 128 ||
|
|
51
|
+
capability.includes("\0")
|
|
52
|
+
) {
|
|
53
|
+
throw new TypeError("Remote session capability is invalid");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return normalized.sort();
|
|
57
|
+
}
|
|
58
|
+
|
|
39
59
|
/**
|
|
40
60
|
* Org-level constraints an administrator can impose on Remote Sessions. Lives
|
|
41
61
|
* next to the registry (its single enforcement point) so both the direct WS
|
|
@@ -49,6 +69,7 @@ export class RemoteSessionPolicy {
|
|
|
49
69
|
maxSessionTtlMs = null,
|
|
50
70
|
maxTokenTtlMs = null,
|
|
51
71
|
allowRelayPairing = true,
|
|
72
|
+
policyVersion = null,
|
|
52
73
|
} = {}) {
|
|
53
74
|
this.allowedScopes = allowedScopes ? [...new Set(allowedScopes)] : null;
|
|
54
75
|
if (this.allowedScopes) {
|
|
@@ -68,6 +89,24 @@ export class RemoteSessionPolicy {
|
|
|
68
89
|
this.maxTokenTtlMs =
|
|
69
90
|
maxTokenTtlMs == null ? null : Math.max(1, Math.floor(maxTokenTtlMs));
|
|
70
91
|
this.allowRelayPairing = allowRelayPairing !== false;
|
|
92
|
+
const authorityMaterial = JSON.stringify({
|
|
93
|
+
allowedScopes: this.allowedScopes ? [...this.allowedScopes].sort() : null,
|
|
94
|
+
allowRelayPairing: this.allowRelayPairing,
|
|
95
|
+
maxDevices: this.maxDevices,
|
|
96
|
+
});
|
|
97
|
+
this.policyVersion =
|
|
98
|
+
policyVersion == null
|
|
99
|
+
? `remote-session-policy:sha256:${createHash("sha256")
|
|
100
|
+
.update(authorityMaterial, "utf8")
|
|
101
|
+
.digest("hex")}`
|
|
102
|
+
: String(policyVersion);
|
|
103
|
+
if (
|
|
104
|
+
this.policyVersion.length === 0 ||
|
|
105
|
+
this.policyVersion.length > 512 ||
|
|
106
|
+
this.policyVersion.includes("\0")
|
|
107
|
+
) {
|
|
108
|
+
throw new TypeError("policyVersion must be a non-empty bounded string");
|
|
109
|
+
}
|
|
71
110
|
}
|
|
72
111
|
|
|
73
112
|
/**
|
|
@@ -119,9 +158,22 @@ export class RemoteSessionPolicy {
|
|
|
119
158
|
maxSessionTtlMs: this.maxSessionTtlMs,
|
|
120
159
|
maxTokenTtlMs: this.maxTokenTtlMs,
|
|
121
160
|
allowRelayPairing: this.allowRelayPairing,
|
|
161
|
+
policyVersion: this.policyVersion,
|
|
122
162
|
};
|
|
123
163
|
}
|
|
124
164
|
|
|
165
|
+
authorityDescriptor() {
|
|
166
|
+
return Object.freeze({
|
|
167
|
+
allowedScopes:
|
|
168
|
+
this.allowedScopes === null
|
|
169
|
+
? null
|
|
170
|
+
: Object.freeze([...this.allowedScopes].sort()),
|
|
171
|
+
maxDevices: this.maxDevices,
|
|
172
|
+
allowRelayPairing: this.allowRelayPairing,
|
|
173
|
+
policyVersion: this.policyVersion,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
125
177
|
static fromEnv(env = {}) {
|
|
126
178
|
const options = {};
|
|
127
179
|
const scopes = env.CHAINLESSCHAIN_REMOTE_SESSION_ALLOWED_SCOPES;
|
|
@@ -163,10 +215,44 @@ function publicSession(session) {
|
|
|
163
215
|
sessionId: session.sessionId,
|
|
164
216
|
agentSessionId: session.agentSessionId,
|
|
165
217
|
name: session.name,
|
|
166
|
-
hostClientId: session.hostClientId,
|
|
218
|
+
hostClientId: session.hostPrincipalId || session.hostClientId,
|
|
219
|
+
...(session.hostPrincipalId
|
|
220
|
+
? {
|
|
221
|
+
hostPrincipalId: session.hostPrincipalId,
|
|
222
|
+
hostConnected: Boolean(session.hostClientId),
|
|
223
|
+
}
|
|
224
|
+
: {}),
|
|
167
225
|
createdAt: session.createdAt,
|
|
168
226
|
expiresAt: session.expiresAt,
|
|
169
227
|
memberCount: session.members.size,
|
|
228
|
+
...(session.membershipAuthority
|
|
229
|
+
? {
|
|
230
|
+
membershipAuthority: session.membershipAuthority,
|
|
231
|
+
sessionEpoch: session.sessionEpoch,
|
|
232
|
+
}
|
|
233
|
+
: {}),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function transportBindingKey(sessionId, transportClientId) {
|
|
238
|
+
return JSON.stringify([String(sessionId), String(transportClientId)]);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function publicMember(member, session) {
|
|
242
|
+
return {
|
|
243
|
+
clientId: member.principalId || member.clientId,
|
|
244
|
+
...(member.principalId ? { principalId: member.principalId } : {}),
|
|
245
|
+
scopes: [...member.scopes],
|
|
246
|
+
joinedAt: member.joinedAt,
|
|
247
|
+
pushToken: member.pushToken || null,
|
|
248
|
+
pushProvider: member.pushToken ? member.pushProvider || null : null,
|
|
249
|
+
...(member.membershipEpoch
|
|
250
|
+
? { membershipEpoch: member.membershipEpoch }
|
|
251
|
+
: {}),
|
|
252
|
+
...(member.membershipQuarantined ? { membershipQuarantined: true } : {}),
|
|
253
|
+
...(session?.hostPrincipalId
|
|
254
|
+
? { connected: Boolean(member.transportClientId) }
|
|
255
|
+
: {}),
|
|
170
256
|
};
|
|
171
257
|
}
|
|
172
258
|
|
|
@@ -177,21 +263,276 @@ export class RemoteSessionRegistry {
|
|
|
177
263
|
this.tokenTtlMs = options.tokenTtlMs || DEFAULT_TOKEN_TTL_MS;
|
|
178
264
|
this.sessionTtlMs = options.sessionTtlMs || DEFAULT_SESSION_TTL_MS;
|
|
179
265
|
this.policy = options.policy || new RemoteSessionPolicy();
|
|
266
|
+
// A factory keeps ordinary/in-memory Remote Session tests and deployments
|
|
267
|
+
// from touching the machine security store. Client-hosted approval bridges
|
|
268
|
+
// explicitly require this authority and trigger its lazy construction.
|
|
269
|
+
this._membershipAuthority = options.membershipAuthority ?? null;
|
|
270
|
+
this._membershipCoordinator = options.membershipCoordinator ?? null;
|
|
180
271
|
this.sessions = new Map();
|
|
181
272
|
this.tokens = new Map();
|
|
273
|
+
this._transportBindings = new Map();
|
|
274
|
+
this._pendingAuthentication = new Map();
|
|
275
|
+
this._coordinatorHydrated = false;
|
|
276
|
+
this._coordinatorHydrationError = null;
|
|
277
|
+
if (
|
|
278
|
+
this._membershipCoordinator &&
|
|
279
|
+
typeof this._membershipCoordinator !== "function"
|
|
280
|
+
) {
|
|
281
|
+
try {
|
|
282
|
+
this._hydrateCoordinatorSessions(this._membershipCoordinator);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
// Construction remains side-effect safe/retryable. Every durable use
|
|
285
|
+
// retries the authoritative read and still fails closed if it remains
|
|
286
|
+
// unavailable.
|
|
287
|
+
this._coordinatorHydrated = false;
|
|
288
|
+
this._coordinatorHydrationError = error;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
_resolveMembershipCoordinator() {
|
|
294
|
+
if (typeof this._membershipCoordinator === "function") {
|
|
295
|
+
this._membershipCoordinator = this._membershipCoordinator();
|
|
296
|
+
}
|
|
297
|
+
if (
|
|
298
|
+
!this._membershipCoordinator ||
|
|
299
|
+
typeof this._membershipCoordinator.listSessionSnapshots !== "function" ||
|
|
300
|
+
typeof this._membershipCoordinator.getSessionSnapshot !== "function" ||
|
|
301
|
+
typeof this._membershipCoordinator.trustDescriptor !== "function"
|
|
302
|
+
) {
|
|
303
|
+
throw new Error("Durable Remote membership coordinator is unavailable");
|
|
304
|
+
}
|
|
305
|
+
return this._membershipCoordinator;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
_requireMembershipCoordinator() {
|
|
309
|
+
const coordinator = this._resolveMembershipCoordinator();
|
|
310
|
+
if (!this._coordinatorHydrated) {
|
|
311
|
+
this._hydrateCoordinatorSessions(coordinator);
|
|
312
|
+
}
|
|
313
|
+
return coordinator;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
_hydrateCoordinatorSessions(coordinator) {
|
|
317
|
+
try {
|
|
318
|
+
const snapshots = coordinator.listSessionSnapshots({ activeOnly: false });
|
|
319
|
+
const live = new Set();
|
|
320
|
+
for (const snapshot of snapshots) {
|
|
321
|
+
if (snapshot.expiresAt <= this.now() || snapshot.status !== "active") {
|
|
322
|
+
this._clearCoordinatorSession(snapshot.sessionId);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
live.add(snapshot.sessionId);
|
|
326
|
+
this._syncCoordinatorSnapshot(snapshot, coordinator);
|
|
327
|
+
}
|
|
328
|
+
for (const [sessionId, session] of this.sessions) {
|
|
329
|
+
if (session.hostPrincipalId && !live.has(sessionId)) {
|
|
330
|
+
this._clearCoordinatorSession(sessionId);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
this._coordinatorHydrated = true;
|
|
334
|
+
this._coordinatorHydrationError = null;
|
|
335
|
+
} catch (error) {
|
|
336
|
+
this._coordinatorHydrated = false;
|
|
337
|
+
this._coordinatorHydrationError = error;
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
_clearCoordinatorSession(sessionId) {
|
|
343
|
+
for (const [key, binding] of this._transportBindings) {
|
|
344
|
+
if (binding.sessionId === sessionId) this._transportBindings.delete(key);
|
|
345
|
+
}
|
|
346
|
+
for (const [challengeId, pending] of this._pendingAuthentication) {
|
|
347
|
+
if (pending.sessionId === sessionId) {
|
|
348
|
+
this._pendingAuthentication.delete(challengeId);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
this.sessions.delete(sessionId);
|
|
352
|
+
this.tokens.delete(sessionId);
|
|
182
353
|
}
|
|
183
354
|
|
|
184
|
-
|
|
355
|
+
_refreshCoordinatorSession(sessionId) {
|
|
356
|
+
const coordinator = this._requireMembershipCoordinator();
|
|
357
|
+
const current = coordinator.getSessionSnapshot(sessionId);
|
|
358
|
+
if (
|
|
359
|
+
!current ||
|
|
360
|
+
current.session.status !== "active" ||
|
|
361
|
+
current.session.expiresAt <= this.now()
|
|
362
|
+
) {
|
|
363
|
+
this._clearCoordinatorSession(sessionId);
|
|
364
|
+
return current?.session || null;
|
|
365
|
+
}
|
|
366
|
+
return this._syncCoordinatorSnapshot(current.session, coordinator);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
_syncCoordinatorSnapshot(snapshot, coordinator = null) {
|
|
370
|
+
const authority = coordinator || this._resolveMembershipCoordinator();
|
|
371
|
+
const previous = this.sessions.get(snapshot.sessionId) || null;
|
|
372
|
+
const priorMembers = previous?.members || new Map();
|
|
373
|
+
for (const [key, binding] of this._transportBindings) {
|
|
374
|
+
if (binding.sessionId === snapshot.sessionId) {
|
|
375
|
+
this._transportBindings.delete(key);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const members = new Map();
|
|
379
|
+
for (const durableMember of snapshot.members) {
|
|
380
|
+
const prior = priorMembers.get(durableMember.principalId) || null;
|
|
381
|
+
const sameActiveMembership =
|
|
382
|
+
prior?.status === "active" &&
|
|
383
|
+
durableMember.status === "active" &&
|
|
384
|
+
prior.membershipEpoch === durableMember.membershipEpoch;
|
|
385
|
+
const member = {
|
|
386
|
+
principalId: durableMember.principalId,
|
|
387
|
+
clientId: durableMember.principalId,
|
|
388
|
+
scopes: [...durableMember.scopes],
|
|
389
|
+
joinedAt: prior?.joinedAt || snapshot.createdAt,
|
|
390
|
+
pushToken: sameActiveMembership ? prior.pushToken || null : null,
|
|
391
|
+
pushProvider:
|
|
392
|
+
sameActiveMembership && prior.pushToken
|
|
393
|
+
? prior.pushProvider || null
|
|
394
|
+
: null,
|
|
395
|
+
membershipEpoch: durableMember.membershipEpoch,
|
|
396
|
+
status: durableMember.status,
|
|
397
|
+
transportClientId:
|
|
398
|
+
durableMember.status === "active"
|
|
399
|
+
? sameActiveMembership
|
|
400
|
+
? prior.transportClientId || null
|
|
401
|
+
: null
|
|
402
|
+
: null,
|
|
403
|
+
};
|
|
404
|
+
members.set(member.principalId, member);
|
|
405
|
+
if (member.transportClientId) {
|
|
406
|
+
this._transportBindings.set(
|
|
407
|
+
transportBindingKey(snapshot.sessionId, member.transportClientId),
|
|
408
|
+
{ sessionId: snapshot.sessionId, principalId: member.principalId },
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const host = members.get(snapshot.hostPrincipalId) || null;
|
|
413
|
+
const session = {
|
|
414
|
+
sessionId: snapshot.sessionId,
|
|
415
|
+
agentSessionId: snapshot.agentSessionId,
|
|
416
|
+
name: previous?.name || snapshot.agentSessionId,
|
|
417
|
+
hostPrincipalId: snapshot.hostPrincipalId,
|
|
418
|
+
hostClientId: host?.transportClientId || null,
|
|
419
|
+
createdAt: snapshot.createdAt,
|
|
420
|
+
expiresAt: snapshot.expiresAt,
|
|
421
|
+
status: snapshot.status,
|
|
422
|
+
members,
|
|
423
|
+
membershipAuthority: authority.trustDescriptor().authorityVersion,
|
|
424
|
+
sessionEpoch: snapshot.sessionEpoch,
|
|
425
|
+
authorityGeneration: snapshot.authorityGeneration,
|
|
426
|
+
leases: new Map(snapshot.leases.map((lease) => [lease.leaseId, lease])),
|
|
427
|
+
};
|
|
428
|
+
this.sessions.set(session.sessionId, session);
|
|
429
|
+
return session;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
_attachTransport(session, principalId, transportClientId) {
|
|
433
|
+
const member = session.members.get(principalId);
|
|
434
|
+
if (!member || member.status !== "active") {
|
|
435
|
+
throw new Error("Remote membership principal is not active");
|
|
436
|
+
}
|
|
437
|
+
const occupied = this._transportBindings.get(
|
|
438
|
+
transportBindingKey(session.sessionId, transportClientId),
|
|
439
|
+
);
|
|
440
|
+
if (occupied && occupied.principalId !== principalId) {
|
|
441
|
+
throw new Error("Remote transport is already bound to another principal");
|
|
442
|
+
}
|
|
443
|
+
if (member.transportClientId) {
|
|
444
|
+
this._transportBindings.delete(
|
|
445
|
+
transportBindingKey(session.sessionId, member.transportClientId),
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
member.transportClientId = transportClientId;
|
|
449
|
+
this._transportBindings.set(
|
|
450
|
+
transportBindingKey(session.sessionId, transportClientId),
|
|
451
|
+
{ sessionId: session.sessionId, principalId },
|
|
452
|
+
);
|
|
453
|
+
if (principalId === session.hostPrincipalId) {
|
|
454
|
+
session.hostClientId = transportClientId;
|
|
455
|
+
}
|
|
456
|
+
return member;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
_principalForTransport(session, transportClientId) {
|
|
460
|
+
if (!session.hostPrincipalId) return transportClientId;
|
|
461
|
+
const binding = this._transportBindings.get(
|
|
462
|
+
transportBindingKey(session.sessionId, transportClientId),
|
|
463
|
+
);
|
|
464
|
+
return binding?.principalId || null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
_requireMembershipAuthority() {
|
|
468
|
+
if (typeof this._membershipAuthority === "function") {
|
|
469
|
+
this._membershipAuthority = this._membershipAuthority();
|
|
470
|
+
}
|
|
471
|
+
if (!this._membershipAuthority) {
|
|
472
|
+
throw new Error(
|
|
473
|
+
"Durable Remote membership authority is required for client-hosted approvals",
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
return this._membershipAuthority;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
create({
|
|
480
|
+
hostClientId,
|
|
481
|
+
agentSessionId,
|
|
482
|
+
name,
|
|
483
|
+
scopes,
|
|
484
|
+
durableMembership = false,
|
|
485
|
+
hostCredentialPublicKeySpki = null,
|
|
486
|
+
} = {}) {
|
|
185
487
|
if (!hostClientId) throw new Error("hostClientId is required");
|
|
186
488
|
if (!agentSessionId) throw new Error("agentSessionId is required");
|
|
187
489
|
const now = this.now();
|
|
490
|
+
const expiresAt = now + this.policy.capSessionTtl(this.sessionTtlMs);
|
|
491
|
+
const sessionId = randomUUID();
|
|
492
|
+
const coordinator =
|
|
493
|
+
durableMembership && this._membershipCoordinator
|
|
494
|
+
? this._requireMembershipCoordinator()
|
|
495
|
+
: null;
|
|
496
|
+
const durable = coordinator
|
|
497
|
+
? coordinator.createSession({
|
|
498
|
+
sessionId,
|
|
499
|
+
agentSessionId,
|
|
500
|
+
scopes: REMOTE_SESSION_SCOPES,
|
|
501
|
+
expiresAt,
|
|
502
|
+
hostCredentialPublicKeySpki,
|
|
503
|
+
joinPolicy: this.policy.authorityDescriptor(),
|
|
504
|
+
})
|
|
505
|
+
: durableMembership
|
|
506
|
+
? this._requireMembershipAuthority().createSession({
|
|
507
|
+
sessionId,
|
|
508
|
+
agentSessionId,
|
|
509
|
+
hostPrincipalId: hostClientId,
|
|
510
|
+
scopes: REMOTE_SESSION_SCOPES,
|
|
511
|
+
expiresAt,
|
|
512
|
+
})
|
|
513
|
+
: null;
|
|
514
|
+
if (coordinator) {
|
|
515
|
+
const session = this._syncCoordinatorSnapshot(durable.statement.payload);
|
|
516
|
+
session.name = name || agentSessionId;
|
|
517
|
+
return {
|
|
518
|
+
session: publicSession(session),
|
|
519
|
+
pairing: this.issuePairingToken(session.sessionId, { scopes }),
|
|
520
|
+
bootstrap: Object.freeze({
|
|
521
|
+
trust: durable.trust,
|
|
522
|
+
statement: durable.statement,
|
|
523
|
+
hostPrincipalId: durable.hostPrincipalId,
|
|
524
|
+
membershipEpoch: durable.membershipEpoch,
|
|
525
|
+
hostCredentialPublicKeySpki: durable.hostCredentialPublicKeySpki,
|
|
526
|
+
}),
|
|
527
|
+
};
|
|
528
|
+
}
|
|
188
529
|
const session = {
|
|
189
|
-
sessionId
|
|
530
|
+
sessionId,
|
|
190
531
|
agentSessionId,
|
|
191
532
|
name: name || agentSessionId,
|
|
192
533
|
hostClientId,
|
|
193
534
|
createdAt: now,
|
|
194
|
-
expiresAt
|
|
535
|
+
expiresAt,
|
|
195
536
|
members: new Map([
|
|
196
537
|
[
|
|
197
538
|
hostClientId,
|
|
@@ -199,9 +540,16 @@ export class RemoteSessionRegistry {
|
|
|
199
540
|
clientId: hostClientId,
|
|
200
541
|
scopes: REMOTE_SESSION_SCOPES,
|
|
201
542
|
joinedAt: now,
|
|
543
|
+
...(durable ? { membershipEpoch: durable.membershipEpoch } : {}),
|
|
202
544
|
},
|
|
203
545
|
],
|
|
204
546
|
]),
|
|
547
|
+
...(durable
|
|
548
|
+
? {
|
|
549
|
+
membershipAuthority: durable.authorityVersion,
|
|
550
|
+
sessionEpoch: durable.sessionEpoch,
|
|
551
|
+
}
|
|
552
|
+
: {}),
|
|
205
553
|
};
|
|
206
554
|
this.sessions.set(session.sessionId, session);
|
|
207
555
|
return {
|
|
@@ -210,6 +558,312 @@ export class RemoteSessionRegistry {
|
|
|
210
558
|
};
|
|
211
559
|
}
|
|
212
560
|
|
|
561
|
+
pairingInvitation(sessionId, token) {
|
|
562
|
+
const session = this.requireSession(sessionId);
|
|
563
|
+
const pairing = this.tokens.get(sessionId);
|
|
564
|
+
if (!pairing || pairing.expiresAt <= this.now()) {
|
|
565
|
+
this.tokens.delete(sessionId);
|
|
566
|
+
throw new Error("Pairing token is missing or expired");
|
|
567
|
+
}
|
|
568
|
+
if (!tokensMatch(token, pairing.digest)) {
|
|
569
|
+
throw new Error("Invalid pairing token");
|
|
570
|
+
}
|
|
571
|
+
return Object.freeze({
|
|
572
|
+
session,
|
|
573
|
+
scopes: Object.freeze([...pairing.scopes]),
|
|
574
|
+
expiresAt: pairing.expiresAt,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
issueMemberJoinChallenge({
|
|
579
|
+
sessionId,
|
|
580
|
+
transportClientId,
|
|
581
|
+
token,
|
|
582
|
+
credentialPublicKey,
|
|
583
|
+
connectionNonce,
|
|
584
|
+
capabilities = null,
|
|
585
|
+
} = {}) {
|
|
586
|
+
const invitation = this.pairingInvitation(sessionId, token);
|
|
587
|
+
const session = invitation.session;
|
|
588
|
+
if (!session.hostPrincipalId) {
|
|
589
|
+
throw new Error("Durable direct membership challenge is unavailable");
|
|
590
|
+
}
|
|
591
|
+
const deviceCount = [...session.members.values()].filter(
|
|
592
|
+
(member) =>
|
|
593
|
+
member.principalId !== session.hostPrincipalId &&
|
|
594
|
+
member.status === "active",
|
|
595
|
+
).length;
|
|
596
|
+
this.policy.enforceJoin({ deviceCount, via: "direct" });
|
|
597
|
+
const challenge =
|
|
598
|
+
this._requireMembershipCoordinator().issueMemberJoinChallenge({
|
|
599
|
+
sessionId,
|
|
600
|
+
expectedSessionEpoch: session.sessionEpoch,
|
|
601
|
+
scopes: invitation.scopes,
|
|
602
|
+
credentialPublicKey,
|
|
603
|
+
connectionNonce,
|
|
604
|
+
ttlMs: Math.max(1, invitation.expiresAt - this.now()),
|
|
605
|
+
capabilities,
|
|
606
|
+
joinPolicy: this.policy.authorityDescriptor(),
|
|
607
|
+
});
|
|
608
|
+
this._pendingAuthentication.set(challenge.challengeId, {
|
|
609
|
+
purpose: "member.join",
|
|
610
|
+
sessionId,
|
|
611
|
+
transportClientId,
|
|
612
|
+
pairingDigest: Buffer.from(this.tokens.get(sessionId).digest),
|
|
613
|
+
pairingExpiresAt: invitation.expiresAt,
|
|
614
|
+
});
|
|
615
|
+
return challenge;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
completeMemberJoin({
|
|
619
|
+
challengeId,
|
|
620
|
+
transportClientId,
|
|
621
|
+
connectionNonce,
|
|
622
|
+
signature,
|
|
623
|
+
pushToken = null,
|
|
624
|
+
pushProvider = null,
|
|
625
|
+
} = {}) {
|
|
626
|
+
const pending = this._pendingAuthentication.get(challengeId);
|
|
627
|
+
this._pendingAuthentication.delete(challengeId);
|
|
628
|
+
if (
|
|
629
|
+
!pending ||
|
|
630
|
+
pending.purpose !== "member.join" ||
|
|
631
|
+
pending.transportClientId !== transportClientId ||
|
|
632
|
+
pending.pairingExpiresAt <= this.now()
|
|
633
|
+
) {
|
|
634
|
+
throw new Error("Remote membership join proof is stale");
|
|
635
|
+
}
|
|
636
|
+
const currentPairing = this.tokens.get(pending.sessionId);
|
|
637
|
+
if (
|
|
638
|
+
!currentPairing ||
|
|
639
|
+
currentPairing.digest.length !== pending.pairingDigest.length ||
|
|
640
|
+
!timingSafeEqual(currentPairing.digest, pending.pairingDigest)
|
|
641
|
+
) {
|
|
642
|
+
throw new Error("Remote membership pairing invitation changed");
|
|
643
|
+
}
|
|
644
|
+
const joined = this._requireMembershipCoordinator().joinMember({
|
|
645
|
+
challengeId,
|
|
646
|
+
connectionNonce,
|
|
647
|
+
signature,
|
|
648
|
+
});
|
|
649
|
+
this.tokens.delete(pending.sessionId);
|
|
650
|
+
const session = this._syncCoordinatorSnapshot(joined.statement.payload);
|
|
651
|
+
const member = this._attachTransport(
|
|
652
|
+
session,
|
|
653
|
+
joined.principalId,
|
|
654
|
+
transportClientId,
|
|
655
|
+
);
|
|
656
|
+
member.pushToken = pushToken || null;
|
|
657
|
+
member.pushProvider = pushToken ? pushProvider || null : null;
|
|
658
|
+
return {
|
|
659
|
+
session: publicSession(session),
|
|
660
|
+
member: publicMember(member, session),
|
|
661
|
+
nextConnectionNonce: joined.nextConnectionNonce,
|
|
662
|
+
statement: joined.statement,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
issueSessionResumeChallenge({
|
|
667
|
+
sessionId,
|
|
668
|
+
principalId,
|
|
669
|
+
transportClientId,
|
|
670
|
+
connectionNonce,
|
|
671
|
+
} = {}) {
|
|
672
|
+
const challenge =
|
|
673
|
+
this._requireMembershipCoordinator().issueSessionResumeChallenge({
|
|
674
|
+
sessionId,
|
|
675
|
+
principalId,
|
|
676
|
+
connectionNonce,
|
|
677
|
+
});
|
|
678
|
+
this._pendingAuthentication.set(challenge.challengeId, {
|
|
679
|
+
purpose: "session.resume",
|
|
680
|
+
sessionId,
|
|
681
|
+
principalId,
|
|
682
|
+
transportClientId,
|
|
683
|
+
});
|
|
684
|
+
return challenge;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
completeSessionResume({
|
|
688
|
+
challengeId,
|
|
689
|
+
transportClientId,
|
|
690
|
+
connectionNonce,
|
|
691
|
+
signature,
|
|
692
|
+
} = {}) {
|
|
693
|
+
const pending = this._pendingAuthentication.get(challengeId);
|
|
694
|
+
this._pendingAuthentication.delete(challengeId);
|
|
695
|
+
if (
|
|
696
|
+
!pending ||
|
|
697
|
+
pending.purpose !== "session.resume" ||
|
|
698
|
+
pending.transportClientId !== transportClientId
|
|
699
|
+
) {
|
|
700
|
+
throw new Error("Remote membership resume proof is stale");
|
|
701
|
+
}
|
|
702
|
+
const resumed = this._requireMembershipCoordinator().resumeSession({
|
|
703
|
+
challengeId,
|
|
704
|
+
connectionNonce,
|
|
705
|
+
signature,
|
|
706
|
+
});
|
|
707
|
+
if (
|
|
708
|
+
resumed.session.sessionId !== pending.sessionId ||
|
|
709
|
+
resumed.principalId !== pending.principalId
|
|
710
|
+
) {
|
|
711
|
+
throw new Error("Remote membership resume binding changed");
|
|
712
|
+
}
|
|
713
|
+
const session = this._syncCoordinatorSnapshot(resumed.session);
|
|
714
|
+
const member = this._attachTransport(
|
|
715
|
+
session,
|
|
716
|
+
resumed.principalId,
|
|
717
|
+
transportClientId,
|
|
718
|
+
);
|
|
719
|
+
return {
|
|
720
|
+
session: publicSession(session),
|
|
721
|
+
member: publicMember(member, session),
|
|
722
|
+
nextConnectionNonce: resumed.nextConnectionNonce,
|
|
723
|
+
statement: resumed.statement,
|
|
724
|
+
trust: resumed.trust,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
issueSessionReenableChallenge({
|
|
729
|
+
sessionId,
|
|
730
|
+
principalId,
|
|
731
|
+
transportClientId,
|
|
732
|
+
connectionNonce,
|
|
733
|
+
newHostCredentialPublicKeySpki = null,
|
|
734
|
+
} = {}) {
|
|
735
|
+
const coordinator = this._requireMembershipCoordinator();
|
|
736
|
+
const challenge = coordinator.issueSessionReenableChallenge({
|
|
737
|
+
sessionId,
|
|
738
|
+
principalId,
|
|
739
|
+
connectionNonce,
|
|
740
|
+
newHostCredentialPublicKeySpki,
|
|
741
|
+
scopes: REMOTE_SESSION_SCOPES,
|
|
742
|
+
expiresAt: this.now() + this.policy.capSessionTtl(this.sessionTtlMs),
|
|
743
|
+
joinPolicy: this.policy.authorityDescriptor(),
|
|
744
|
+
});
|
|
745
|
+
this._pendingAuthentication.set(challenge.challengeId, {
|
|
746
|
+
purpose: "session.reenable",
|
|
747
|
+
sessionId,
|
|
748
|
+
principalId,
|
|
749
|
+
nextPrincipalId: challenge.nextPrincipalId,
|
|
750
|
+
transportClientId,
|
|
751
|
+
});
|
|
752
|
+
return challenge;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
completeSessionReenable({
|
|
756
|
+
challengeId,
|
|
757
|
+
transportClientId,
|
|
758
|
+
connectionNonce,
|
|
759
|
+
signature,
|
|
760
|
+
} = {}) {
|
|
761
|
+
const pending = this._pendingAuthentication.get(challengeId);
|
|
762
|
+
this._pendingAuthentication.delete(challengeId);
|
|
763
|
+
if (
|
|
764
|
+
!pending ||
|
|
765
|
+
pending.purpose !== "session.reenable" ||
|
|
766
|
+
pending.transportClientId !== transportClientId
|
|
767
|
+
) {
|
|
768
|
+
throw new Error("Remote membership re-enable proof is stale");
|
|
769
|
+
}
|
|
770
|
+
const reenabled = this._requireMembershipCoordinator().reenableSession({
|
|
771
|
+
challengeId,
|
|
772
|
+
connectionNonce,
|
|
773
|
+
signature,
|
|
774
|
+
});
|
|
775
|
+
if (
|
|
776
|
+
reenabled.sessionId !== pending.sessionId ||
|
|
777
|
+
reenabled.hostPrincipalId !== pending.nextPrincipalId
|
|
778
|
+
) {
|
|
779
|
+
throw new Error("Remote membership re-enable binding changed");
|
|
780
|
+
}
|
|
781
|
+
const session = this._syncCoordinatorSnapshot(reenabled.statement.payload);
|
|
782
|
+
const member = this._attachTransport(
|
|
783
|
+
session,
|
|
784
|
+
reenabled.hostPrincipalId,
|
|
785
|
+
transportClientId,
|
|
786
|
+
);
|
|
787
|
+
return {
|
|
788
|
+
session: publicSession(session),
|
|
789
|
+
member: publicMember(member, session),
|
|
790
|
+
nextConnectionNonce: reenabled.nextConnectionNonce,
|
|
791
|
+
statement: reenabled.statement,
|
|
792
|
+
trust: reenabled.trust,
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
terminalSnapshot(sessionId) {
|
|
797
|
+
const current =
|
|
798
|
+
this._requireMembershipCoordinator().getSessionSnapshot(sessionId);
|
|
799
|
+
if (!current || current.session.status !== "closed") {
|
|
800
|
+
throw new Error("Remote membership session has no terminal tombstone");
|
|
801
|
+
}
|
|
802
|
+
this._clearCoordinatorSession(sessionId);
|
|
803
|
+
return current;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
joinRelayMember({
|
|
807
|
+
sessionId,
|
|
808
|
+
transportClientId,
|
|
809
|
+
token,
|
|
810
|
+
mobilePublicKey,
|
|
811
|
+
pairingTokenDigest,
|
|
812
|
+
possessionCapability,
|
|
813
|
+
capabilities = null,
|
|
814
|
+
pushToken = null,
|
|
815
|
+
pushProvider = null,
|
|
816
|
+
} = {}) {
|
|
817
|
+
const invitation = this.pairingInvitation(sessionId, token);
|
|
818
|
+
const session = invitation.session;
|
|
819
|
+
if (!session.hostPrincipalId) {
|
|
820
|
+
throw new Error("Durable relay membership is unavailable");
|
|
821
|
+
}
|
|
822
|
+
const deviceCount = [...session.members.values()].filter(
|
|
823
|
+
(member) =>
|
|
824
|
+
member.principalId !== session.hostPrincipalId &&
|
|
825
|
+
member.status === "active",
|
|
826
|
+
).length;
|
|
827
|
+
this.policy.enforceJoin({ deviceCount, via: "relay" });
|
|
828
|
+
const negotiatedCapabilities = normalizeCapabilities(capabilities);
|
|
829
|
+
const negotiatedScopes = invitation.scopes.filter(
|
|
830
|
+
(scope) =>
|
|
831
|
+
scope !== "approve" ||
|
|
832
|
+
negotiatedCapabilities.includes(REMOTE_APPROVAL_BINDING_CAPABILITY),
|
|
833
|
+
);
|
|
834
|
+
if (negotiatedScopes.length === 0) {
|
|
835
|
+
throw new Error(
|
|
836
|
+
"Remote relay capabilities do not authorize any requested scope",
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
const joined = this._requireMembershipCoordinator().joinRelayMember({
|
|
840
|
+
sessionId,
|
|
841
|
+
expectedSessionEpoch: session.sessionEpoch,
|
|
842
|
+
scopes: negotiatedScopes,
|
|
843
|
+
mobilePeerId: transportClientId,
|
|
844
|
+
mobilePublicKey,
|
|
845
|
+
pairingTokenDigest,
|
|
846
|
+
possessionCapability,
|
|
847
|
+
capabilities: negotiatedCapabilities,
|
|
848
|
+
joinPolicy: this.policy.authorityDescriptor(),
|
|
849
|
+
});
|
|
850
|
+
this.tokens.delete(sessionId);
|
|
851
|
+
const updated = this._syncCoordinatorSnapshot(joined.statement.payload);
|
|
852
|
+
const member = this._attachTransport(
|
|
853
|
+
updated,
|
|
854
|
+
joined.principalId,
|
|
855
|
+
transportClientId,
|
|
856
|
+
);
|
|
857
|
+
member.pushToken = pushToken || null;
|
|
858
|
+
member.pushProvider = pushToken ? pushProvider || null : null;
|
|
859
|
+
return {
|
|
860
|
+
session: publicSession(updated),
|
|
861
|
+
member: publicMember(member, updated),
|
|
862
|
+
statement: joined.statement,
|
|
863
|
+
capabilities: Object.freeze([...negotiatedCapabilities]),
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
213
867
|
issuePairingToken(sessionId, { scopes } = {}) {
|
|
214
868
|
const session = this.requireSession(sessionId);
|
|
215
869
|
const token = randomBytes(32).toString("base64url");
|
|
@@ -243,6 +897,11 @@ export class RemoteSessionRegistry {
|
|
|
243
897
|
} = {}) {
|
|
244
898
|
if (!clientId) throw new Error("clientId is required");
|
|
245
899
|
const session = this.requireSession(sessionId);
|
|
900
|
+
if (session.hostPrincipalId) {
|
|
901
|
+
throw new Error(
|
|
902
|
+
"Durable membership requires a possession-proof join protocol",
|
|
903
|
+
);
|
|
904
|
+
}
|
|
246
905
|
const pairing = this.tokens.get(sessionId);
|
|
247
906
|
if (!pairing || pairing.expiresAt <= this.now()) {
|
|
248
907
|
this.tokens.delete(sessionId);
|
|
@@ -251,6 +910,9 @@ export class RemoteSessionRegistry {
|
|
|
251
910
|
if (!tokensMatch(token, pairing.digest)) {
|
|
252
911
|
throw new Error("Invalid pairing token");
|
|
253
912
|
}
|
|
913
|
+
if (clientId === session.hostClientId) {
|
|
914
|
+
throw new Error("Cannot replace the host membership through pairing");
|
|
915
|
+
}
|
|
254
916
|
// Org policy is enforced only after the token proves the caller is invited,
|
|
255
917
|
// so a device-limit / relay-disabled message never leaks to unauthenticated
|
|
256
918
|
// probes. Existing (non-host) devices count against the limit.
|
|
@@ -258,8 +920,17 @@ export class RemoteSessionRegistry {
|
|
|
258
920
|
(member) => member.clientId !== session.hostClientId,
|
|
259
921
|
).length;
|
|
260
922
|
this.policy.enforceJoin({ deviceCount, via });
|
|
923
|
+
const durable = session.membershipAuthority
|
|
924
|
+
? this._requireMembershipAuthority().joinMember({
|
|
925
|
+
sessionId,
|
|
926
|
+
principalId: clientId,
|
|
927
|
+
scopes: pairing.scopes,
|
|
928
|
+
expectedSessionEpoch: session.sessionEpoch,
|
|
929
|
+
})
|
|
930
|
+
: null;
|
|
261
931
|
// Pairing credentials are deliberately one-time. The host must explicitly
|
|
262
|
-
// issue another token for each additional device.
|
|
932
|
+
// issue another token for each additional device. For durable sessions the
|
|
933
|
+
// authority commit happens first, so a persistence failure stays retryable.
|
|
263
934
|
this.tokens.delete(sessionId);
|
|
264
935
|
const member = {
|
|
265
936
|
clientId,
|
|
@@ -267,6 +938,7 @@ export class RemoteSessionRegistry {
|
|
|
267
938
|
joinedAt: this.now(),
|
|
268
939
|
pushToken: pushToken || null,
|
|
269
940
|
pushProvider: pushToken ? pushProvider || null : null,
|
|
941
|
+
...(durable ? { membershipEpoch: durable.membershipEpoch } : {}),
|
|
270
942
|
};
|
|
271
943
|
session.members.set(clientId, member);
|
|
272
944
|
return { session: publicSession(session), member: { ...member } };
|
|
@@ -279,8 +951,9 @@ export class RemoteSessionRegistry {
|
|
|
279
951
|
*/
|
|
280
952
|
registerPush(sessionId, clientId, { token = null, provider = null } = {}) {
|
|
281
953
|
const session = this.requireSession(sessionId);
|
|
282
|
-
const
|
|
283
|
-
|
|
954
|
+
const principalId = this._principalForTransport(session, clientId);
|
|
955
|
+
const member = session.members.get(principalId || clientId);
|
|
956
|
+
if (!member || (member.status && member.status !== "active")) {
|
|
284
957
|
throw new Error("Device is not paired with this remote session");
|
|
285
958
|
}
|
|
286
959
|
member.pushToken = token || null;
|
|
@@ -297,11 +970,25 @@ export class RemoteSessionRegistry {
|
|
|
297
970
|
const session = this.requireSession(sessionId);
|
|
298
971
|
const targets = [];
|
|
299
972
|
for (const member of session.members.values()) {
|
|
300
|
-
if (member.
|
|
301
|
-
if (
|
|
973
|
+
if (member.status && member.status !== "active") continue;
|
|
974
|
+
if (
|
|
975
|
+
(session.hostPrincipalId &&
|
|
976
|
+
member.principalId === session.hostPrincipalId) ||
|
|
977
|
+
(!session.hostPrincipalId && member.clientId === session.hostClientId)
|
|
978
|
+
)
|
|
979
|
+
continue;
|
|
980
|
+
if (
|
|
981
|
+
excludeClientId &&
|
|
982
|
+
(member.clientId === excludeClientId ||
|
|
983
|
+
member.transportClientId === excludeClientId)
|
|
984
|
+
)
|
|
985
|
+
continue;
|
|
302
986
|
if (!member.pushToken) continue;
|
|
303
987
|
targets.push({
|
|
304
988
|
clientId: member.clientId,
|
|
989
|
+
...(session.hostPrincipalId
|
|
990
|
+
? { transportClientId: member.transportClientId || null }
|
|
991
|
+
: {}),
|
|
305
992
|
pushToken: member.pushToken,
|
|
306
993
|
pushProvider: member.pushProvider,
|
|
307
994
|
});
|
|
@@ -311,18 +998,71 @@ export class RemoteSessionRegistry {
|
|
|
311
998
|
|
|
312
999
|
authorize(sessionId, clientId, scope) {
|
|
313
1000
|
const session = this.requireSession(sessionId);
|
|
314
|
-
|
|
1001
|
+
if (session.membershipQuarantined) {
|
|
1002
|
+
throw new Error(
|
|
1003
|
+
"Remote membership session is quarantined after an unknown durable transition",
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
const principalId = this._principalForTransport(session, clientId);
|
|
1007
|
+
const member = session.members.get(principalId || clientId);
|
|
315
1008
|
if (!member)
|
|
316
1009
|
throw new Error("Client is not paired with this remote session");
|
|
1010
|
+
if (member.status && member.status !== "active") {
|
|
1011
|
+
throw new Error("Remote membership principal is not active");
|
|
1012
|
+
}
|
|
1013
|
+
if (member.membershipQuarantined) {
|
|
1014
|
+
throw new Error(
|
|
1015
|
+
"Remote membership is quarantined after an unknown durable transition",
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
317
1018
|
if (!member.scopes.includes(scope)) {
|
|
318
1019
|
throw new Error(`Remote session scope required: ${scope}`);
|
|
319
1020
|
}
|
|
320
|
-
|
|
1021
|
+
let membership = null;
|
|
1022
|
+
if (session.hostPrincipalId) {
|
|
1023
|
+
const verdict = this._requireMembershipCoordinator().readMembership(
|
|
1024
|
+
{
|
|
1025
|
+
sessionId,
|
|
1026
|
+
principalId: member.principalId,
|
|
1027
|
+
sessionEpoch: session.sessionEpoch,
|
|
1028
|
+
membershipEpoch: member.membershipEpoch,
|
|
1029
|
+
},
|
|
1030
|
+
scope,
|
|
1031
|
+
);
|
|
1032
|
+
if (!verdict.ok) {
|
|
1033
|
+
throw new Error(
|
|
1034
|
+
`Remote membership coordinator denied: ${verdict.reason}`,
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
membership = verdict.binding;
|
|
1038
|
+
} else if (session.membershipAuthority) {
|
|
1039
|
+
const verdict = this._requireMembershipAuthority().readMembership(
|
|
1040
|
+
{
|
|
1041
|
+
sessionId,
|
|
1042
|
+
principalId: clientId,
|
|
1043
|
+
sessionEpoch: session.sessionEpoch,
|
|
1044
|
+
membershipEpoch: member.membershipEpoch,
|
|
1045
|
+
},
|
|
1046
|
+
scope,
|
|
1047
|
+
);
|
|
1048
|
+
if (!verdict.ok) {
|
|
1049
|
+
throw new Error(
|
|
1050
|
+
`Remote membership authority denied: ${verdict.reason}`,
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
membership = verdict.binding;
|
|
1054
|
+
}
|
|
1055
|
+
return { session, member, membership };
|
|
321
1056
|
}
|
|
322
1057
|
|
|
323
1058
|
members(sessionId) {
|
|
324
1059
|
const session = this.requireSession(sessionId);
|
|
325
|
-
return [...session.members.values()]
|
|
1060
|
+
return [...session.members.values()]
|
|
1061
|
+
.filter((member) => !member.status || member.status === "active")
|
|
1062
|
+
.map((member) => ({
|
|
1063
|
+
...publicMember(member, session),
|
|
1064
|
+
transportClientId: member.transportClientId || member.clientId,
|
|
1065
|
+
}));
|
|
326
1066
|
}
|
|
327
1067
|
|
|
328
1068
|
/**
|
|
@@ -331,18 +1071,33 @@ export class RemoteSessionRegistry {
|
|
|
331
1071
|
*/
|
|
332
1072
|
listDevices(sessionId, hostClientId) {
|
|
333
1073
|
const session = this.requireSession(sessionId);
|
|
334
|
-
|
|
1074
|
+
const hostPrincipal = hostClientId
|
|
1075
|
+
? this._principalForTransport(session, hostClientId)
|
|
1076
|
+
: null;
|
|
1077
|
+
if (
|
|
1078
|
+
hostClientId &&
|
|
1079
|
+
(session.hostPrincipalId
|
|
1080
|
+
? hostPrincipal !== session.hostPrincipalId
|
|
1081
|
+
: session.hostClientId !== hostClientId)
|
|
1082
|
+
) {
|
|
335
1083
|
throw new Error("Only the host can list paired devices");
|
|
336
1084
|
}
|
|
337
1085
|
return {
|
|
338
1086
|
session: publicSession(session),
|
|
339
1087
|
devices: [...session.members.values()].map((member) => ({
|
|
340
|
-
clientId: member.clientId,
|
|
1088
|
+
clientId: member.principalId || member.clientId,
|
|
1089
|
+
...(member.principalId ? { principalId: member.principalId } : {}),
|
|
341
1090
|
scopes: [...member.scopes],
|
|
342
1091
|
joinedAt: member.joinedAt,
|
|
343
|
-
isHost:
|
|
1092
|
+
isHost: session.hostPrincipalId
|
|
1093
|
+
? member.principalId === session.hostPrincipalId
|
|
1094
|
+
: member.clientId === session.hostClientId,
|
|
1095
|
+
...(session.hostPrincipalId
|
|
1096
|
+
? { connected: Boolean(member.transportClientId) }
|
|
1097
|
+
: {}),
|
|
344
1098
|
hasPush: Boolean(member.pushToken),
|
|
345
1099
|
pushProvider: member.pushProvider || null,
|
|
1100
|
+
...(member.status ? { status: member.status } : {}),
|
|
346
1101
|
})),
|
|
347
1102
|
};
|
|
348
1103
|
}
|
|
@@ -355,22 +1110,86 @@ export class RemoteSessionRegistry {
|
|
|
355
1110
|
revokeMember(sessionId, hostClientId, clientId) {
|
|
356
1111
|
if (!clientId) throw new Error("clientId is required");
|
|
357
1112
|
const session = this.requireSession(sessionId);
|
|
358
|
-
|
|
1113
|
+
const hostPrincipal = this._principalForTransport(session, hostClientId);
|
|
1114
|
+
if (
|
|
1115
|
+
session.hostPrincipalId
|
|
1116
|
+
? hostPrincipal !== session.hostPrincipalId
|
|
1117
|
+
: session.hostClientId !== hostClientId
|
|
1118
|
+
) {
|
|
359
1119
|
throw new Error("Only the host can revoke devices");
|
|
360
1120
|
}
|
|
361
|
-
if (clientId === session.hostClientId) {
|
|
1121
|
+
if (clientId === (session.hostPrincipalId || session.hostClientId)) {
|
|
362
1122
|
throw new Error("Cannot revoke the host device");
|
|
363
1123
|
}
|
|
364
1124
|
const member = session.members.get(clientId);
|
|
365
1125
|
if (!member)
|
|
366
1126
|
throw new Error("Device is not paired with this remote session");
|
|
367
|
-
|
|
368
|
-
|
|
1127
|
+
const notificationTransportId = member.transportClientId || member.clientId;
|
|
1128
|
+
let resultSession = session;
|
|
1129
|
+
let resultMember = member;
|
|
1130
|
+
let membershipStatement = null;
|
|
1131
|
+
if (session.hostPrincipalId) {
|
|
1132
|
+
try {
|
|
1133
|
+
const host = session.members.get(session.hostPrincipalId);
|
|
1134
|
+
const revoked = this._requireMembershipCoordinator().revokeMember({
|
|
1135
|
+
sessionId,
|
|
1136
|
+
principalId: clientId,
|
|
1137
|
+
hostPrincipalId: session.hostPrincipalId,
|
|
1138
|
+
expectedSessionEpoch: session.sessionEpoch,
|
|
1139
|
+
expectedMembershipEpoch: member.membershipEpoch,
|
|
1140
|
+
expectedHostMembershipEpoch: host.membershipEpoch,
|
|
1141
|
+
});
|
|
1142
|
+
resultSession = this._syncCoordinatorSnapshot(
|
|
1143
|
+
revoked.statement.payload,
|
|
1144
|
+
);
|
|
1145
|
+
resultMember = resultSession.members.get(clientId);
|
|
1146
|
+
membershipStatement = revoked.statement;
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
member.membershipQuarantined = true;
|
|
1149
|
+
throw error;
|
|
1150
|
+
}
|
|
1151
|
+
} else if (session.membershipAuthority) {
|
|
1152
|
+
try {
|
|
1153
|
+
this._requireMembershipAuthority().revokeMember({
|
|
1154
|
+
sessionId,
|
|
1155
|
+
principalId: clientId,
|
|
1156
|
+
expectedSessionEpoch: session.sessionEpoch,
|
|
1157
|
+
expectedMembershipEpoch: member.membershipEpoch,
|
|
1158
|
+
});
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
// The durable outcome may be unknown. Keep the record for diagnostics,
|
|
1161
|
+
// but locally quarantine it so no later frame can authorize.
|
|
1162
|
+
member.membershipQuarantined = true;
|
|
1163
|
+
throw error;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (!session.hostPrincipalId) session.members.delete(clientId);
|
|
1167
|
+
return {
|
|
1168
|
+
session: publicSession(resultSession),
|
|
1169
|
+
member: {
|
|
1170
|
+
...publicMember(resultMember, resultSession),
|
|
1171
|
+
transportClientId: notificationTransportId,
|
|
1172
|
+
},
|
|
1173
|
+
...(membershipStatement ? { statement: membershipStatement } : {}),
|
|
1174
|
+
};
|
|
369
1175
|
}
|
|
370
1176
|
|
|
371
1177
|
findHosted(agentSessionId, hostClientId) {
|
|
1178
|
+
if (this._membershipCoordinator) {
|
|
1179
|
+
// Enumeration cannot rely on the one-time startup hydration: another
|
|
1180
|
+
// server may have created, closed, or re-enabled a durable session since
|
|
1181
|
+
// this process started. Refresh the coordinator index before matching
|
|
1182
|
+
// local transport attachments.
|
|
1183
|
+
this._hydrateCoordinatorSessions(this._resolveMembershipCoordinator());
|
|
1184
|
+
}
|
|
372
1185
|
const matches = [];
|
|
373
|
-
for (const
|
|
1186
|
+
for (const sessionId of [...this.sessions.keys()]) {
|
|
1187
|
+
let session;
|
|
1188
|
+
try {
|
|
1189
|
+
session = this.requireSession(sessionId);
|
|
1190
|
+
} catch {
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
374
1193
|
if (
|
|
375
1194
|
session.agentSessionId === agentSessionId &&
|
|
376
1195
|
session.hostClientId === hostClientId &&
|
|
@@ -385,12 +1204,56 @@ export class RemoteSessionRegistry {
|
|
|
385
1204
|
removeClient(clientId) {
|
|
386
1205
|
const affected = [];
|
|
387
1206
|
for (const [sessionId, session] of this.sessions) {
|
|
388
|
-
|
|
1207
|
+
const principalId = this._principalForTransport(session, clientId);
|
|
1208
|
+
const member = session.members.get(principalId || clientId);
|
|
1209
|
+
if (!member) continue;
|
|
1210
|
+
if (session.hostPrincipalId) {
|
|
1211
|
+
this._transportBindings.delete(
|
|
1212
|
+
transportBindingKey(sessionId, clientId),
|
|
1213
|
+
);
|
|
1214
|
+
member.transportClientId = null;
|
|
1215
|
+
if (member.principalId === session.hostPrincipalId) {
|
|
1216
|
+
session.hostClientId = null;
|
|
1217
|
+
}
|
|
1218
|
+
affected.push({
|
|
1219
|
+
sessionId,
|
|
1220
|
+
closed: false,
|
|
1221
|
+
detached: true,
|
|
1222
|
+
principalId: member.principalId,
|
|
1223
|
+
});
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
389
1226
|
if (session.hostClientId === clientId) {
|
|
1227
|
+
if (session.membershipAuthority) {
|
|
1228
|
+
try {
|
|
1229
|
+
this._requireMembershipAuthority().closeSession({
|
|
1230
|
+
sessionId,
|
|
1231
|
+
hostPrincipalId: clientId,
|
|
1232
|
+
expectedSessionEpoch: session.sessionEpoch,
|
|
1233
|
+
});
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
session.membershipQuarantined = true;
|
|
1236
|
+
throw error;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
390
1239
|
this.sessions.delete(sessionId);
|
|
391
1240
|
this.tokens.delete(sessionId);
|
|
392
1241
|
affected.push({ sessionId, closed: true });
|
|
393
1242
|
} else {
|
|
1243
|
+
if (session.membershipAuthority) {
|
|
1244
|
+
try {
|
|
1245
|
+
this._requireMembershipAuthority().revokeMember({
|
|
1246
|
+
sessionId,
|
|
1247
|
+
principalId: clientId,
|
|
1248
|
+
expectedSessionEpoch: session.sessionEpoch,
|
|
1249
|
+
expectedMembershipEpoch: member.membershipEpoch,
|
|
1250
|
+
});
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
member.membershipQuarantined = true;
|
|
1253
|
+
throw error;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
session.members.delete(clientId);
|
|
394
1257
|
affected.push({ sessionId, closed: false });
|
|
395
1258
|
}
|
|
396
1259
|
}
|
|
@@ -399,18 +1262,76 @@ export class RemoteSessionRegistry {
|
|
|
399
1262
|
|
|
400
1263
|
close(sessionId, clientId) {
|
|
401
1264
|
const session = this.requireSession(sessionId);
|
|
402
|
-
|
|
1265
|
+
const hostPrincipal = this._principalForTransport(session, clientId);
|
|
1266
|
+
if (
|
|
1267
|
+
session.hostPrincipalId
|
|
1268
|
+
? hostPrincipal !== session.hostPrincipalId
|
|
1269
|
+
: session.hostClientId !== clientId
|
|
1270
|
+
) {
|
|
403
1271
|
throw new Error("Only the host can close a Remote Session");
|
|
404
1272
|
}
|
|
1273
|
+
let durableClose = null;
|
|
1274
|
+
if (session.hostPrincipalId) {
|
|
1275
|
+
try {
|
|
1276
|
+
const host = session.members.get(session.hostPrincipalId);
|
|
1277
|
+
durableClose = this._requireMembershipCoordinator().closeSession({
|
|
1278
|
+
sessionId,
|
|
1279
|
+
hostPrincipalId: session.hostPrincipalId,
|
|
1280
|
+
expectedSessionEpoch: session.sessionEpoch,
|
|
1281
|
+
expectedHostMembershipEpoch: host.membershipEpoch,
|
|
1282
|
+
});
|
|
1283
|
+
} catch (error) {
|
|
1284
|
+
session.membershipQuarantined = true;
|
|
1285
|
+
throw error;
|
|
1286
|
+
}
|
|
1287
|
+
} else if (session.membershipAuthority) {
|
|
1288
|
+
try {
|
|
1289
|
+
this._requireMembershipAuthority().closeSession({
|
|
1290
|
+
sessionId,
|
|
1291
|
+
hostPrincipalId: clientId,
|
|
1292
|
+
expectedSessionEpoch: session.sessionEpoch,
|
|
1293
|
+
});
|
|
1294
|
+
} catch (error) {
|
|
1295
|
+
session.membershipQuarantined = true;
|
|
1296
|
+
throw error;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
405
1299
|
this.sessions.delete(sessionId);
|
|
406
1300
|
this.tokens.delete(sessionId);
|
|
407
|
-
|
|
1301
|
+
const closedSession = publicSession(session);
|
|
1302
|
+
return durableClose
|
|
1303
|
+
? {
|
|
1304
|
+
session: closedSession,
|
|
1305
|
+
statement: durableClose.statement,
|
|
1306
|
+
terminal: durableClose.terminal === true,
|
|
1307
|
+
alreadyClosed: durableClose.alreadyClosed === true,
|
|
1308
|
+
}
|
|
1309
|
+
: closedSession;
|
|
408
1310
|
}
|
|
409
1311
|
|
|
410
1312
|
requireSession(sessionId) {
|
|
411
|
-
|
|
1313
|
+
let session = this.sessions.get(sessionId) || null;
|
|
1314
|
+
if ((session?.hostPrincipalId || !session) && this._membershipCoordinator) {
|
|
1315
|
+
const durable = this._refreshCoordinatorSession(sessionId);
|
|
1316
|
+
if (durable?.status && durable.status !== "active") {
|
|
1317
|
+
throw new Error("Remote session is durably closed");
|
|
1318
|
+
}
|
|
1319
|
+
session = this.sessions.get(sessionId) || null;
|
|
1320
|
+
}
|
|
412
1321
|
if (!session) throw new Error("Remote session not found");
|
|
413
1322
|
if (session.expiresAt <= this.now()) {
|
|
1323
|
+
if (session.membershipAuthority && !session.hostPrincipalId) {
|
|
1324
|
+
try {
|
|
1325
|
+
this._requireMembershipAuthority().closeSession({
|
|
1326
|
+
sessionId,
|
|
1327
|
+
hostPrincipalId: session.hostClientId,
|
|
1328
|
+
expectedSessionEpoch: session.sessionEpoch,
|
|
1329
|
+
});
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
session.membershipQuarantined = true;
|
|
1332
|
+
throw error;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
414
1335
|
this.sessions.delete(sessionId);
|
|
415
1336
|
this.tokens.delete(sessionId);
|
|
416
1337
|
throw new Error("Remote session expired");
|