chainlesschain 0.164.0 → 0.165.1

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