chainlesschain 0.164.0 → 0.165.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/README.md +248 -2
  2. package/package.json +7 -4
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-D2MEn7fI.js → AIOps-lS1R0Cza.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-lCW_4Gf0.js → ActionButton-BmlgBPsj.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-Bs7i-nsS.js → Analytics-CNKczv1S.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-CK7B6S_j.js → AppLayout-BuZGJARQ.js} +5 -5
  8. package/src/assets/web-panel/assets/{Artifacts-C8ILjpyo.js → Artifacts-Dm73Sl-O.js} +1 -1
  9. package/src/assets/web-panel/assets/{Audit-Bb9cNaWc.js → Audit-BGMW7A5e.js} +1 -1
  10. package/src/assets/web-panel/assets/{BackgroundAgents-sC9ooXIf.js → BackgroundAgents-BQ6M54JR.js} +1 -1
  11. package/src/assets/web-panel/assets/{Backup-D-DrsplT.js → Backup-DBzGP0bR.js} +1 -1
  12. package/src/assets/web-panel/assets/{BaseInput-uCNZpwSK.js → BaseInput-D1mLCoOV.js} +1 -1
  13. package/src/assets/web-panel/assets/{Chat-CsDK1uE9.js → Chat-BUfcN7p8.js} +6 -6
  14. package/src/assets/web-panel/assets/{ChatBubbleRenderer-OqSDt50w.js → ChatBubbleRenderer-CY5m8ZHg.js} +1 -1
  15. package/src/assets/web-panel/assets/{Checkbox-JhTXM1-K.js → Checkbox-Du0Yg5Pi.js} +1 -1
  16. package/src/assets/web-panel/assets/{Codegen-BbzZLnyD.js → Codegen-B2RXziHW.js} +1 -1
  17. package/src/assets/web-panel/assets/{Col-D4VW4m4j.js → Col-DWhekOrR.js} +1 -1
  18. package/src/assets/web-panel/assets/{Community-BXrYawxV.js → Community-HQhrkon4.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compact-DCihGy1u.js → Compact-izHNb4uz.js} +1 -1
  20. package/src/assets/web-panel/assets/{Compliance-DlyZ-lk8.js → Compliance-BDwLy41-.js} +1 -1
  21. package/src/assets/web-panel/assets/{Cowork-DvGZrhbO.js → Cowork-DFJhLa7K.js} +3 -3
  22. package/src/assets/web-panel/assets/{Cron-DUncZi8h.js → Cron-DYY9RG7-.js} +2 -2
  23. package/src/assets/web-panel/assets/{Crosschain-C_hLuUcm.js → Crosschain-rr7tJTtv.js} +1 -1
  24. package/src/assets/web-panel/assets/{DID-b6YSUqxr.js → DID-D2sfHRLi.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dashboard-uvM15Cfb.js → Dashboard-q_eGdhha.js} +2 -2
  26. package/src/assets/web-panel/assets/{Dropdown-D1-Nd4qY.js → Dropdown-Cufrh2Bz.js} +1 -1
  27. package/src/assets/web-panel/assets/{EmailListRenderer-8ApvL6Zc.js → EmailListRenderer-B-kdv10f.js} +1 -1
  28. package/src/assets/web-panel/assets/{FamilyGuardDashboard-LYJt8811.js → FamilyGuardDashboard-D21XjgfG.js} +1 -1
  29. package/src/assets/web-panel/assets/{Federation-BbI3HGpv.js → Federation-CAwRTLZH.js} +1 -1
  30. package/src/assets/web-panel/assets/{FormItemContext-DSoEKuFu.js → FormItemContext-vx3daNDF.js} +1 -1
  31. package/src/assets/web-panel/assets/{GenericCardRenderer-B0S9OWd5.js → GenericCardRenderer-CRWY2wU4.js} +1 -1
  32. package/src/assets/web-panel/assets/{Git-DIs9Zhf3.js → Git-Q6SwauyD.js} +2 -2
  33. package/src/assets/web-panel/assets/{Governance-jzZXrxcl.js → Governance-BJ1q4IQZ.js} +1 -1
  34. package/src/assets/web-panel/assets/{Inference-JUPjf2ZI.js → Inference-CSWjROgD.js} +1 -1
  35. package/src/assets/web-panel/assets/{KnowledgeGraph-DxnfJvlP.js → KnowledgeGraph-fzuQsWuY.js} +1 -1
  36. package/src/assets/web-panel/assets/{Logs-iXiz8BEE.js → Logs-DVF8Mnns.js} +2 -2
  37. package/src/assets/web-panel/assets/MarkdownRenderer-BLif4QSg.js +1 -0
  38. package/src/assets/web-panel/assets/{Marketplace-Y3Xch312.js → Marketplace-BQP7LOA4.js} +1 -1
  39. package/src/assets/web-panel/assets/{McpTools-D7kGGbO7.js → McpTools-BNlJCmNc.js} +4 -4
  40. package/src/assets/web-panel/assets/{Memory-Do_hVxzh.js → Memory-DLLg5WQ4.js} +2 -2
  41. package/src/assets/web-panel/assets/{MobileBridge-CtrR0Tas.js → MobileBridge-CR2skNAF.js} +1 -1
  42. package/src/assets/web-panel/assets/MobileProjects-BxVavObl.js +1 -0
  43. package/src/assets/web-panel/assets/{Mtc-qo5QCTGd.js → Mtc-B17pgqMI.js} +4 -4
  44. package/src/assets/web-panel/assets/{MtcAudit-cq5mnjYj.js → MtcAudit-4UmV5cIL.js} +2 -2
  45. package/src/assets/web-panel/assets/{Multisig-Bq0MAQHw.js → Multisig-DoMEXEcU.js} +3 -3
  46. package/src/assets/web-panel/assets/{NLProgramming-DdWE6Vi2.js → NLProgramming-h9DMXFX4.js} +1 -1
  47. package/src/assets/web-panel/assets/{Notes-BxMBFcIp.js → Notes-Cihn-xX8.js} +3 -3
  48. package/src/assets/web-panel/assets/{NotificationSettings-RTrAcbUe.js → NotificationSettings-BE6ZGa3R.js} +1 -1
  49. package/src/assets/web-panel/assets/{OrderTableRenderer-DJbpGqzT.js → OrderTableRenderer-CnwojoSI.js} +1 -1
  50. package/src/assets/web-panel/assets/{Organization-SQzIPdqC.js → Organization-B2vpwmjb.js} +4 -4
  51. package/src/assets/web-panel/assets/{Overflow-02yECh22.js → Overflow-C4z28R0-.js} +1 -1
  52. package/src/assets/web-panel/assets/{P2P-BNg4pNCM.js → P2P-BVlFkwhW.js} +2 -2
  53. package/src/assets/web-panel/assets/{PdhVaultBrowser-BxPJZsD3.js → PdhVaultBrowser-BXbEdL5B.js} +3 -3
  54. package/src/assets/web-panel/assets/{Permissions-CLnDnPAz.js → Permissions-C28xs4lp.js} +4 -4
  55. package/src/assets/web-panel/assets/{PersonalDataHub-C5m0AjGf.js → PersonalDataHub-Dgl_rBjM.js} +4 -4
  56. package/src/assets/web-panel/assets/{Pipeline-VwOf5Dhv.js → Pipeline-3QUIhrKI.js} +1 -1
  57. package/src/assets/web-panel/assets/{Privacy-CC0W2hZd.js → Privacy-8Gh33OZ5.js} +1 -1
  58. package/src/assets/web-panel/assets/{ProjectInit-Dz893Bob.js → ProjectInit-BNmg4tZi.js} +2 -2
  59. package/src/assets/web-panel/assets/{ProjectSettings-CxQkvl0k.js → ProjectSettings-vgMfeCf9.js} +2 -2
  60. package/src/assets/web-panel/assets/Projects-BhtAVilc.js +1 -0
  61. package/src/assets/web-panel/assets/{Providers-DeB9IkxK.js → Providers-CXoOa8eX.js} +1 -1
  62. package/src/assets/web-panel/assets/{QrScannerModal-OCeoaswn.js → QrScannerModal-BhfTF0oS.js} +1 -1
  63. package/src/assets/web-panel/assets/{QuickAsk-DenotaAj.js → QuickAsk-C746HEAj.js} +1 -1
  64. package/src/assets/web-panel/assets/{Recommend-Cies_jjI.js → Recommend-BSej8Eo0.js} +1 -1
  65. package/src/assets/web-panel/assets/RemoteSession-f3gmnlG3.js +5 -0
  66. package/src/assets/web-panel/assets/{Reputation-CW0mvxgZ.js → Reputation-CyvybpkN.js} +1 -1
  67. package/src/assets/web-panel/assets/{Row-SEd32qZU.js → Row-DwV2PeYn.js} +1 -1
  68. package/src/assets/web-panel/assets/{RssFeed-BSwnJ3Lm.js → RssFeed-11GrzjZy.js} +3 -3
  69. package/src/assets/web-panel/assets/{Search-xe2m8bkC.js → Search-BjKXkrAY.js} +1 -1
  70. package/src/assets/web-panel/assets/{Security-Mq2F9Nro.js → Security-eWiFb9Yk.js} +4 -4
  71. package/src/assets/web-panel/assets/{Services-BKGw2V3P.js → Services-CdCsMAtt.js} +2 -2
  72. package/src/assets/web-panel/assets/{Skeleton-DW95-Rxf.js → Skeleton-4Q2UTKQ5.js} +1 -1
  73. package/src/assets/web-panel/assets/{Skills-CHgChCv_.js → Skills-v4rtYrRj.js} +1 -1
  74. package/src/assets/web-panel/assets/{Sla-tKh6k3R7.js → Sla-CRhsFvd_.js} +1 -1
  75. package/src/assets/web-panel/assets/SpeechSettings-DLjKsDVu.js +1 -0
  76. package/src/assets/web-panel/assets/{SyncSettings-D0Fw8fVL.js → SyncSettings-D16zonl7.js} +2 -2
  77. package/src/assets/web-panel/assets/{Tasks-20x1GB3D.js → Tasks-Xxozz0i_.js} +1 -1
  78. package/src/assets/web-panel/assets/{Templates-DJ-lts-a.js → Templates-CXJpPtCT.js} +1 -1
  79. package/src/assets/web-panel/assets/{Tenant-BFdGpbs7.js → Tenant-XRV0L1ZF.js} +1 -1
  80. package/src/assets/web-panel/assets/{Terminal-Xpx61vHL.js → Terminal-CYuO1i_w.js} +2 -2
  81. package/src/assets/web-panel/assets/{TimelineRenderer-C6UIvyZQ.js → TimelineRenderer-BXqitWsu.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tokens-Dellq08f.js → Tokens-BvPrDHIy.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trigger-VKw8d0J-.js → Trigger-DhCrSTf5.js} +1 -1
  84. package/src/assets/web-panel/assets/{Trust-DNftdKT5.js → Trust-DmsuZD6K.js} +1 -1
  85. package/src/assets/web-panel/assets/{UkeySign-DX9VAKJI.js → UkeySign-D_yrts5C.js} +1 -1
  86. package/src/assets/web-panel/assets/{VideoEditing-B2h3jO8q.js → VideoEditing-B01ihPrm.js} +1 -1
  87. package/src/assets/web-panel/assets/{Wallet-CjGkof7f.js → Wallet-DpUjKa-N.js} +4 -4
  88. package/src/assets/web-panel/assets/{WebAuthn-2Zfcu55P.js → WebAuthn-CL07yON6.js} +4 -4
  89. package/src/assets/web-panel/assets/{WorkflowEditor-B9eZ8UxZ.js → WorkflowEditor-CLMflnRr.js} +1 -1
  90. package/src/assets/web-panel/assets/{chat-CQNfD-YO.js → chat-DtUqe8h_.js} +1 -1
  91. package/src/assets/web-panel/assets/{colors-D4usyFT2.js → colors-DPKrgVfy.js} +1 -1
  92. package/src/assets/web-panel/assets/{compact-item-s4TpjvjX.js → compact-item-BTt2GtOm.js} +1 -1
  93. package/src/assets/web-panel/assets/{createContext-BRCSvYxj.js → createContext-D_VeUc1J.js} +1 -1
  94. package/src/assets/web-panel/assets/devWarning-Bj5nUW2v.js +1 -0
  95. package/src/assets/web-panel/assets/{hasIn-AdkUbkV-.js → hasIn-1kndiylj.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BIoG5yYf.js → index-2eyAXE3d.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-nXyk8GZ9.js → index-4bna2_SR.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-B2V-eKfK.js → index-8nz_fmlj.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-D_XBHopw.js → index-B9KvcS7R.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-s_buFMEG.js → index-BAXfRKfG.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DZT36ZYq.js → index-BHpBMWCh.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-BMcimU-2.js → index-BaZAbw9A.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DzRjHxEv.js → index-BepLZwgB.js} +1 -1
  104. package/src/assets/web-panel/assets/{index--00U6vXZ.js → index-Bhudc_Ez.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-C7Y8Aluc.js → index-BlDNVgBt.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BtobShro.js → index-By5n8up9.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-Cvyt-zhm.js → index-CHG-KcS4.js} +1 -1
  108. package/src/assets/web-panel/assets/index-CPcM-eqo.js +1 -0
  109. package/src/assets/web-panel/assets/{index-MMN9V6JF.js → index-CX81qSM4.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-Bad4VNvz.js → index-CauoBQkH.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-CCsM_Ybx.js → index-CcO04eDn.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-DARsAyUp.js → index-CtF7EN49.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-C5KAQFPQ.js → index-CzjkKUmR.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-RfMr79MW.js → index-D95OFht7.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DdCB70SS.js → index-DFIZ_1Sf.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CYdoQsa1.js → index-DG2MZQ3f.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BPhkFZ1I.js → index-DJ8vXGOa.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-_IRVsV9X.js → index-Dbbymxpj.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DVKfQ0Ux.js → index-DdkplL9R.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-CkwiSLhU.js → index-DfGl8CqG.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CJVEOa4i.js → index-DfKBRdSS.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-C1mJtE6q.js → index-DsJCDUJG.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BgiXR_-_.js → index-Dwqmtk65.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CJCFe2Qf.js → index-DxdTdBix.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-C484EJiS.js → index-FzdwcSy2.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-B4JJCfQ2.js → index-GsoUQ2M7.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CQhAVU5t.js → index-brCInrzq.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-CKC1t3Ic.js → index-dVc-CXY4.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BQ3DXBtD.js → index-fk2Rjacw.js} +3 -3
  130. package/src/assets/web-panel/assets/{index-Cduu2Hhr.js → index-l1BETrDb.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-Bw2MEnUk.js → index-m31Ka8hZ.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-D2GjI2oe.js → index-ocf06EVQ.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-CJdQrZth.js → index-sDoMHTNA.js} +1 -1
  134. package/src/assets/web-panel/assets/index-xeSyVoRe.js +1 -0
  135. package/src/assets/web-panel/assets/{initDefaultProps-Blmpk7HL.js → initDefaultProps-CvyYJd7z.js} +1 -1
  136. package/src/assets/web-panel/assets/{motion-DPBJMv1n.js → motion-DYZkhUdz.js} +1 -1
  137. package/src/assets/web-panel/assets/{move-ButdzCM7.js → move-slnJRVT1.js} +1 -1
  138. package/src/assets/web-panel/assets/{mtc-parser-cyRC_QCe.js → mtc-parser-DVc0CKmy.js} +1 -1
  139. package/src/assets/web-panel/assets/{omit-1epSV7hz.js → omit-BCXAtHCq.js} +1 -1
  140. package/src/assets/web-panel/assets/{pickAttrs-DuPY8xJs.js → pickAttrs-DBsemz_K.js} +1 -1
  141. package/src/assets/web-panel/assets/{placementArrow-6XZqarhe.js → placementArrow-BRtkJ-nv.js} +1 -1
  142. package/src/assets/web-panel/assets/{responsiveObserve-B40pXBWM.js → responsiveObserve-Cebq7EZn.js} +1 -1
  143. package/src/assets/web-panel/assets/{slide-jbZ9Wu4n.js → slide-DT7qvId2.js} +1 -1
  144. package/src/assets/web-panel/assets/{statusUtils-BKQQcVy4.js → statusUtils-pQiITLkG.js} +1 -1
  145. package/src/assets/web-panel/assets/{styleChecker-NiKfWjUT.js → styleChecker-c-19BVKQ.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFlexGapSupport-bQUCvPeb.js → useFlexGapSupport-DsS5-0bE.js} +1 -1
  147. package/src/assets/web-panel/assets/{useFs-CUGji7ph.js → useFs-CxGiMe5n.js} +1 -1
  148. package/src/assets/web-panel/assets/{usePersonalDataHub-4veyN2N2.js → usePersonalDataHub-D9btVID5.js} +1 -1
  149. package/src/assets/web-panel/assets/{vnode-1-nLug-Z.js → vnode-CUYFsFEN.js} +1 -1
  150. package/src/assets/web-panel/assets/{zoom-dhCeRDMo.js → zoom-CXmn_35j.js} +1 -1
  151. package/src/assets/web-panel/index.html +1 -1
  152. package/src/command-help-index.json +1 -1
  153. package/src/commands/cowork.js +559 -62
  154. package/src/commands/plugin.js +819 -64
  155. package/src/commands/session-location.js +318 -7
  156. package/src/constants.js +2 -2
  157. package/src/data/changelog.json +28 -2
  158. package/src/gateways/ws/action-protocol.js +293 -19
  159. package/src/gateways/ws/message-dispatcher.js +30 -0
  160. package/src/gateways/ws/remote-session-protocol.js +542 -35
  161. package/src/gateways/ws/ws-approval-gate.js +5 -0
  162. package/src/gateways/ws/ws-server.js +462 -38
  163. package/src/harness/jsonl-session-store.js +638 -2
  164. package/src/harness/prompt-compressor.js +4 -0
  165. package/src/harness/remote-session-audit.js +1 -0
  166. package/src/harness/remote-session-crypto.js +750 -50
  167. package/src/harness/remote-session-registry.js +795 -27
  168. package/src/lib/auto-mode-config.js +98 -3
  169. package/src/lib/background-agent-keeper-client.js +29 -0
  170. package/src/lib/background-agent-keeper-protocol.js +158 -10
  171. package/src/lib/background-agent-supervisor.js +870 -48
  172. package/src/lib/background-session-transport.js +23 -6
  173. package/src/lib/background-turn-bootstrap-protocol.js +64 -0
  174. package/src/lib/cowork-task-runner.js +70 -0
  175. package/src/lib/cowork-workflow.js +887 -62
  176. package/src/lib/delivery-production-adapter.js +38 -2
  177. package/src/lib/doctor-checkup.js +34 -10
  178. package/src/lib/dynamic-workflow-draft.js +535 -0
  179. package/src/lib/dynamic-workflow-facade.js +1105 -17
  180. package/src/lib/dynamic-workflow-runtime.js +2974 -0
  181. package/src/lib/execution-location-contract.js +111 -7
  182. package/src/lib/execution-location-runtime.js +3 -1
  183. package/src/lib/execution-location-target.js +910 -0
  184. package/src/lib/llm-providers.js +1 -0
  185. package/src/lib/mcp-call-ledger-store.js +35 -0
  186. package/src/lib/mcp-call-ledger.js +30 -0
  187. package/src/lib/mcp-stdio-package-materialization.js +1 -1
  188. package/src/lib/plugin-runtime/__tests__/remote-source.test.js +34 -16
  189. package/src/lib/plugin-runtime/install.js +4956 -657
  190. package/src/lib/plugin-runtime/manifest.js +3 -1
  191. package/src/lib/plugin-runtime/marketplace-artifact-readback.js +671 -31
  192. package/src/lib/plugin-runtime/marketplace-catalog.js +68 -2
  193. package/src/lib/plugin-runtime/marketplace-impact.js +208 -15
  194. package/src/lib/plugin-runtime/marketplace-network.js +317 -0
  195. package/src/lib/plugin-runtime/marketplace-pac-worker.js +15 -0
  196. package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +25 -2
  197. package/src/lib/plugin-runtime/marketplace-source-cache.js +359 -0
  198. package/src/lib/plugin-runtime/policy.js +21 -6
  199. package/src/lib/plugin-runtime/publisher-trust.js +332 -0
  200. package/src/lib/plugin-runtime/remote-source.js +323 -27
  201. package/src/lib/plugin-runtime/scopes.js +317 -41
  202. package/src/lib/plugin-runtime/transaction-journal.js +806 -0
  203. package/src/lib/public-ide-capability-manifest.js +1 -1
  204. package/src/lib/remote-approval-bridge.js +745 -108
  205. package/src/lib/remote-control.js +2 -0
  206. package/src/lib/remote-membership-coordinator.js +612 -46
  207. package/src/lib/remote-membership-host-store.js +59 -10
  208. package/src/lib/runtime-provenance-ledger.js +171 -46
  209. package/src/lib/shell-approval.js +169 -6
  210. package/src/lib/sub-agent-context.js +268 -2
  211. package/src/lib/task-model-selector.js +1 -1
  212. package/src/lib/with-file-lock.js +167 -42
  213. package/src/repl/agent-repl.js +70 -29
  214. package/src/repl/remote-approval.js +29 -7
  215. package/src/runtime/agent-core.js +1695 -194
  216. package/src/runtime/headless-runner.js +22 -5
  217. package/src/runtime/headless-stream.js +19 -3
  218. package/src/workers/background-agent-keeper.js +279 -58
  219. package/src/workers/background-agent-worker.js +7 -0
  220. package/src/workers/background-turn-bootstrap.js +29 -2
  221. package/src/assets/web-panel/assets/MarkdownRenderer-GmB6DOr6.js +0 -1
  222. package/src/assets/web-panel/assets/MobileProjects-BL0Czcoj.js +0 -1
  223. package/src/assets/web-panel/assets/Projects-DDAjkUkT.js +0 -1
  224. package/src/assets/web-panel/assets/RemoteSession-Dxzl5n2T.js +0 -5
  225. package/src/assets/web-panel/assets/SpeechSettings-D6UL3dA5.js +0 -1
  226. package/src/assets/web-panel/assets/devWarning-DKJjSWRn.js +0 -1
  227. package/src/assets/web-panel/assets/index-C61cnl3K.js +0 -1
  228. package/src/assets/web-panel/assets/index-D4XFHxBr.js +0 -1
@@ -15,6 +15,7 @@ const PAIRING_SCHEME = "chainlesschain://remote-session/pair#";
15
15
  const RELAY_POSSESSION_SCHEMA =
16
16
  "chainlesschain.remote-session.relay-possession-capability/v1";
17
17
  const relayPossessionCapabilities = new WeakMap();
18
+ const stagedRelayPairings = new WeakMap();
18
19
  const RELAY_AUTHORITY_SCOPES = new Set([
19
20
  "observe",
20
21
  "prompt",
@@ -128,6 +129,144 @@ function normalizeRelayAuthorityScopes(scopes) {
128
129
  return Object.freeze(normalized);
129
130
  }
130
131
 
132
+ function normalizeRelayCapabilities(capabilities) {
133
+ if (capabilities == null) return Object.freeze([]);
134
+ if (!Array.isArray(capabilities) || capabilities.length > 32) {
135
+ throw new Error("Durable Remote relay capabilities are invalid");
136
+ }
137
+ const normalized = [...new Set(capabilities)];
138
+ if (
139
+ normalized.some(
140
+ (capability) =>
141
+ typeof capability !== "string" ||
142
+ capability.length === 0 ||
143
+ capability.length > 128 ||
144
+ capability.includes("\0"),
145
+ )
146
+ ) {
147
+ throw new Error("Durable Remote relay capabilities are invalid");
148
+ }
149
+ return Object.freeze(normalized.sort());
150
+ }
151
+
152
+ function sameStringArray(left, right) {
153
+ return (
154
+ left.length === right.length &&
155
+ left.every((value, index) => value === right[index])
156
+ );
157
+ }
158
+
159
+ function relayCredentialDigest(publicKey) {
160
+ return createHash("sha256").update(unb64(publicKey)).digest("hex");
161
+ }
162
+
163
+ function samePendingRelayRequest(left, right) {
164
+ return (
165
+ left.peerId === right.peerId &&
166
+ left.key.equals(right.key) &&
167
+ left.mobilePublicKey === right.mobilePublicKey &&
168
+ left.pairingTokenDigest === right.pairingTokenDigest &&
169
+ left.initialEnvelopeSequence === right.initialEnvelopeSequence &&
170
+ left.initialEnvelopeTranscriptHash ===
171
+ right.initialEnvelopeTranscriptHash &&
172
+ left.expectedPrincipalId === right.expectedPrincipalId &&
173
+ left.expectedCredentialDigest === right.expectedCredentialDigest &&
174
+ left.expectedSessionEpoch === right.expectedSessionEpoch &&
175
+ left.expectedMembershipEpoch === right.expectedMembershipEpoch &&
176
+ left.baselineAuthorityGeneration === right.baselineAuthorityGeneration &&
177
+ left.baselineMembershipEpoch === right.baselineMembershipEpoch &&
178
+ left.baselineMembershipStatus === right.baselineMembershipStatus &&
179
+ sameStringArray(left.expectedScopes, right.expectedScopes) &&
180
+ sameStringArray(left.expectedCapabilities, right.expectedCapabilities) &&
181
+ sameStringArray(
182
+ left.authority.authorizedScopes,
183
+ right.authority.authorizedScopes,
184
+ ) &&
185
+ left.authority.expiresAtMs === right.authority.expiresAtMs &&
186
+ left.authority.coordinatorId === right.authority.coordinatorId &&
187
+ left.authority.serverInstanceId === right.authority.serverInstanceId &&
188
+ left.authority.authorityVersion === right.authority.authorityVersion
189
+ );
190
+ }
191
+
192
+ function createRelayMembershipAcceptance(
193
+ sessionId,
194
+ peerId,
195
+ {
196
+ mobilePublicKey,
197
+ pairingTokenDigest,
198
+ principalId,
199
+ sessionEpoch,
200
+ membershipEpoch,
201
+ scopes,
202
+ capabilities,
203
+ statement = null,
204
+ } = {},
205
+ ) {
206
+ const normalizedPublicKey = canonicalPublicKey(mobilePublicKey);
207
+ const normalizedScopes = normalizeRelayAuthorityScopes(scopes);
208
+ const normalizedCapabilities = normalizeRelayCapabilities(capabilities);
209
+ if (
210
+ typeof pairingTokenDigest !== "string" ||
211
+ !/^[0-9a-f]{64}$/.test(pairingTokenDigest) ||
212
+ typeof principalId !== "string" ||
213
+ principalId.length === 0 ||
214
+ typeof sessionEpoch !== "string" ||
215
+ !/^[1-9]\d*$/.test(sessionEpoch) ||
216
+ typeof membershipEpoch !== "string" ||
217
+ !/^[1-9]\d*$/.test(membershipEpoch)
218
+ ) {
219
+ throw new Error("Remote relay membership acceptance binding is invalid");
220
+ }
221
+ return Object.freeze({
222
+ sessionId,
223
+ mobilePeerId: peerId,
224
+ mobilePublicKey: normalizedPublicKey,
225
+ pairingTokenDigest,
226
+ principalId,
227
+ sessionEpoch,
228
+ membershipEpoch,
229
+ scopes: normalizedScopes,
230
+ capabilities: normalizedCapabilities,
231
+ statement,
232
+ });
233
+ }
234
+
235
+ function decryptEnvelopeWithKey(
236
+ sessionId,
237
+ key,
238
+ envelope,
239
+ previousSequence = 0,
240
+ ) {
241
+ if (envelope?.v !== 1 || envelope.sessionId !== sessionId) {
242
+ throw new Error("Invalid encrypted Remote Session envelope");
243
+ }
244
+ if (
245
+ !Number.isSafeInteger(envelope.sequence) ||
246
+ envelope.sequence <= previousSequence
247
+ ) {
248
+ throw new Error("Remote Session replay or out-of-order envelope rejected");
249
+ }
250
+ try {
251
+ const decipher = createDecipheriv(
252
+ "aes-256-gcm",
253
+ key,
254
+ unb64(envelope.nonce),
255
+ );
256
+ decipher.setAAD(aad(sessionId, envelope.senderId, envelope.sequence));
257
+ decipher.setAuthTag(unb64(envelope.tag));
258
+ const plaintext = Buffer.concat([
259
+ decipher.update(unb64(envelope.ciphertext)),
260
+ decipher.final(),
261
+ ]);
262
+ return JSON.parse(plaintext.toString("utf8"));
263
+ } catch (error) {
264
+ throw new Error(
265
+ `Remote Session envelope authentication failed: ${error.message}`,
266
+ );
267
+ }
268
+ }
269
+
131
270
  function aad(sessionId, senderId, sequence) {
132
271
  return Buffer.from(
133
272
  `${PROTOCOL}\n${sessionId}\n${senderId}\n${sequence}`,
@@ -221,6 +360,7 @@ export class RemoteSessionCryptoContext {
221
360
  privateKey,
222
361
  publicKey,
223
362
  now = () => Date.now(),
363
+ faultHooks = null,
224
364
  } = {}) {
225
365
  if (!sessionId || !localPeerId)
226
366
  throw new Error("sessionId and localPeerId are required");
@@ -235,8 +375,12 @@ export class RemoteSessionCryptoContext {
235
375
  });
236
376
  this.publicKey = publicKey || keyPair.publicKey;
237
377
  this._now = now;
378
+ this._faultHooks = faultHooks || {};
238
379
  this.keys = new Map();
239
380
  this.pairingMaterial = new Map();
381
+ this.relayPeerBindings = new Map();
382
+ this.acceptedRelayMemberships = new Map();
383
+ this.pendingCommittedRelayAcceptances = new Map();
240
384
  this.pendingRelayCapabilities = new Map();
241
385
  this.outstandingRelayCapabilities = new Map();
242
386
  this.sendSequence = 0;
@@ -298,6 +442,8 @@ export class RemoteSessionCryptoContext {
298
442
  this.pendingRelayCapabilities.delete(peerId);
299
443
  this.outstandingRelayCapabilities.delete(peerId);
300
444
  const normalizedPublicKey = canonicalPublicKey(peerPublicKey);
445
+ this.acceptedRelayMemberships.delete(peerId);
446
+ this.discardPendingRelayMembershipCommit(peerId);
301
447
  this.keys.set(
302
448
  peerId,
303
449
  deriveKey(
@@ -316,11 +462,25 @@ export class RemoteSessionCryptoContext {
316
462
  serverInstanceId: durableAuthority?.serverInstanceId || null,
317
463
  authorityVersion: durableAuthority?.authorityVersion || null,
318
464
  });
465
+ this.relayPeerBindings.set(peerId, {
466
+ mobilePublicKey: normalizedPublicKey,
467
+ pairingTokenDigest: remoteSessionPairingTokenDigest(pairingToken),
468
+ });
319
469
  }
320
470
 
321
471
  encrypt(peerId, message) {
472
+ const revokesThisPeer =
473
+ message?.type === "session.revoked" &&
474
+ message.remoteSessionId === this.sessionId;
322
475
  const key = this.keys.get(peerId);
323
- if (!key) throw new Error(`No encryption key for peer: ${peerId}`);
476
+ if (!key) {
477
+ // A durable join can be authoritative while its candidate key is still
478
+ // pending publication. A courtesy revoke cannot be encrypted in that
479
+ // window, but it must still retire the process-local recovery marker so
480
+ // a later, legitimately re-authorized generation is not blocked.
481
+ if (revokesThisPeer) this.clearRelayPeer(peerId);
482
+ throw new Error(`No encryption key for peer: ${peerId}`);
483
+ }
324
484
  const sequence = ++this.sendSequence;
325
485
  const nonce = randomBytes(12);
326
486
  const cipher = createCipheriv("aes-256-gcm", key, nonce);
@@ -329,7 +489,7 @@ export class RemoteSessionCryptoContext {
329
489
  cipher.update(JSON.stringify(message), "utf8"),
330
490
  cipher.final(),
331
491
  ]);
332
- return {
492
+ const envelope = {
333
493
  v: 1,
334
494
  sessionId: this.sessionId,
335
495
  senderId: this.localPeerId,
@@ -338,6 +498,12 @@ export class RemoteSessionCryptoContext {
338
498
  ciphertext: b64(ciphertext),
339
499
  tag: b64(cipher.getAuthTag()),
340
500
  };
501
+ if (revokesThisPeer) {
502
+ // The ciphertext is complete; remove every local generation for this
503
+ // peer before the courtesy revoke frame is physically sent.
504
+ this.clearRelayPeer(peerId);
505
+ }
506
+ return envelope;
341
507
  }
342
508
 
343
509
  decrypt(envelope) {
@@ -356,60 +522,530 @@ export class RemoteSessionCryptoContext {
356
522
  "Remote Session replay or out-of-order envelope rejected",
357
523
  );
358
524
  }
359
- try {
360
- const decipher = createDecipheriv(
361
- "aes-256-gcm",
362
- key,
363
- unb64(envelope.nonce),
364
- );
365
- decipher.setAAD(
366
- aad(this.sessionId, envelope.senderId, envelope.sequence),
367
- );
368
- decipher.setAuthTag(unb64(envelope.tag));
369
- const plaintext = Buffer.concat([
370
- decipher.update(unb64(envelope.ciphertext)),
371
- decipher.final(),
372
- ]);
373
- const message = JSON.parse(plaintext.toString("utf8"));
374
- this.receivedSequences.set(envelope.senderId, envelope.sequence);
375
- const pairing = this.pairingMaterial.get(envelope.senderId);
525
+ const message = decryptEnvelopeWithKey(
526
+ this.sessionId,
527
+ key,
528
+ envelope,
529
+ previous,
530
+ );
531
+ this.receivedSequences.set(envelope.senderId, envelope.sequence);
532
+ const pairing = this.pairingMaterial.get(envelope.senderId);
533
+ if (
534
+ pairing &&
535
+ pairing.authorizedScopes !== null &&
536
+ pairing.coordinatorId !== null &&
537
+ pairing.serverInstanceId !== null &&
538
+ pairing.authorityVersion !== null &&
539
+ pairing.expiresAtMs > this._now() &&
540
+ message?.type === "pair.join" &&
541
+ typeof message.token === "string" &&
542
+ remoteSessionPairingTokenDigest(message.token) ===
543
+ pairing.pairingTokenDigest
544
+ ) {
545
+ const proof = Object.freeze({
546
+ schema: RELAY_POSSESSION_SCHEMA,
547
+ sessionId: this.sessionId,
548
+ mobilePeerId: envelope.senderId,
549
+ mobilePublicKey: pairing.mobilePublicKey,
550
+ pairingTokenDigest: pairing.pairingTokenDigest,
551
+ coordinatorId: pairing.coordinatorId,
552
+ serverInstanceId: pairing.serverInstanceId,
553
+ authorityVersion: pairing.authorityVersion,
554
+ authorizedScopes: pairing.authorizedScopes,
555
+ pairingExpiresAtMs: pairing.expiresAtMs,
556
+ envelopeSequence: envelope.sequence,
557
+ envelopeTranscriptHash: relayEnvelopeTranscriptHash(envelope),
558
+ });
559
+ relayPossessionCapabilities.set(proof, { ...proof });
560
+ this.pendingRelayCapabilities.set(envelope.senderId, proof);
561
+ this.outstandingRelayCapabilities.set(envelope.senderId, proof);
562
+ this.pairingMaterial.delete(envelope.senderId);
563
+ }
564
+ return message;
565
+ }
566
+
567
+ /**
568
+ * Authenticate a prospective relay pairing without changing the live peer
569
+ * key, replay high-water mark, or an accepted-membership receipt. The caller
570
+ * commits the opaque stage only after the membership transition succeeds.
571
+ */
572
+ stageRelayPairing(
573
+ peerId,
574
+ peerPublicKey,
575
+ pairingToken,
576
+ envelope,
577
+ durableAuthority = null,
578
+ ) {
579
+ if (!peerId || !peerPublicKey || !pairingToken) {
580
+ throw new Error("Incomplete peer key material");
581
+ }
582
+ if (envelope?.senderId !== peerId) {
583
+ throw new Error("Remote Session relay peer binding is invalid");
584
+ }
585
+ const normalizedPublicKey = canonicalPublicKey(peerPublicKey);
586
+ const pairingTokenDigest = remoteSessionPairingTokenDigest(pairingToken);
587
+ let authority = null;
588
+ if (durableAuthority !== null) {
589
+ const normalizedExpiry = Number(durableAuthority?.expiresAtMs);
590
+ if (
591
+ !Number.isSafeInteger(normalizedExpiry) ||
592
+ normalizedExpiry <= this._now()
593
+ ) {
594
+ throw new Error("Durable Remote relay pairing authority is expired");
595
+ }
376
596
  if (
377
- pairing &&
378
- pairing.authorizedScopes !== null &&
379
- pairing.coordinatorId !== null &&
380
- pairing.serverInstanceId !== null &&
381
- pairing.authorityVersion !== null &&
382
- pairing.expiresAtMs > this._now() &&
383
- message?.type === "pair.join" &&
384
- typeof message.token === "string" &&
385
- remoteSessionPairingTokenDigest(message.token) ===
386
- pairing.pairingTokenDigest
597
+ typeof durableAuthority?.coordinatorId !== "string" ||
598
+ durableAuthority.coordinatorId.length === 0 ||
599
+ typeof durableAuthority?.serverInstanceId !== "string" ||
600
+ durableAuthority.serverInstanceId.length === 0 ||
601
+ typeof durableAuthority?.authorityVersion !== "string" ||
602
+ durableAuthority.authorityVersion.length === 0
387
603
  ) {
388
- const proof = Object.freeze({
389
- schema: RELAY_POSSESSION_SCHEMA,
390
- sessionId: this.sessionId,
391
- mobilePeerId: envelope.senderId,
392
- mobilePublicKey: pairing.mobilePublicKey,
393
- pairingTokenDigest: pairing.pairingTokenDigest,
394
- coordinatorId: pairing.coordinatorId,
395
- serverInstanceId: pairing.serverInstanceId,
396
- authorityVersion: pairing.authorityVersion,
397
- authorizedScopes: pairing.authorizedScopes,
398
- pairingExpiresAtMs: pairing.expiresAtMs,
399
- envelopeSequence: envelope.sequence,
400
- envelopeTranscriptHash: relayEnvelopeTranscriptHash(envelope),
401
- });
402
- relayPossessionCapabilities.set(proof, { ...proof });
403
- this.pendingRelayCapabilities.set(envelope.senderId, proof);
404
- this.outstandingRelayCapabilities.set(envelope.senderId, proof);
405
- this.pairingMaterial.delete(envelope.senderId);
604
+ throw new Error("Durable Remote relay authority domain is required");
406
605
  }
407
- return message;
408
- } catch (error) {
606
+ authority = Object.freeze({
607
+ authorizedScopes: normalizeRelayAuthorityScopes(
608
+ durableAuthority.authorizedScopes,
609
+ ),
610
+ expiresAtMs: normalizedExpiry,
611
+ coordinatorId: durableAuthority.coordinatorId,
612
+ serverInstanceId: durableAuthority.serverInstanceId,
613
+ authorityVersion: durableAuthority.authorityVersion,
614
+ });
615
+ }
616
+ const key = deriveKey(
617
+ this.privateKey,
618
+ importPublicKey(normalizedPublicKey),
619
+ this.sessionId,
620
+ pairingToken,
621
+ );
622
+ // A new key/token tuple starts a new encrypted sequence space. Nothing is
623
+ // published to the live maps until commitStagedRelayPairing().
624
+ const message = decryptEnvelopeWithKey(this.sessionId, key, envelope, 0);
625
+ if (
626
+ message?.type !== "pair.join" ||
627
+ message.remoteSessionId !== this.sessionId ||
628
+ message.token !== pairingToken
629
+ ) {
630
+ throw new Error("Invalid encrypted Remote Session pairing request");
631
+ }
632
+ let possessionCapability = null;
633
+ if (authority) {
634
+ const proof = Object.freeze({
635
+ schema: RELAY_POSSESSION_SCHEMA,
636
+ sessionId: this.sessionId,
637
+ mobilePeerId: peerId,
638
+ mobilePublicKey: normalizedPublicKey,
639
+ pairingTokenDigest,
640
+ coordinatorId: authority.coordinatorId,
641
+ serverInstanceId: authority.serverInstanceId,
642
+ authorityVersion: authority.authorityVersion,
643
+ authorizedScopes: authority.authorizedScopes,
644
+ pairingExpiresAtMs: authority.expiresAtMs,
645
+ envelopeSequence: envelope.sequence,
646
+ envelopeTranscriptHash: relayEnvelopeTranscriptHash(envelope),
647
+ });
648
+ relayPossessionCapabilities.set(proof, { ...proof });
649
+ possessionCapability = proof;
650
+ }
651
+ const stage = Object.freeze({});
652
+ stagedRelayPairings.set(stage, {
653
+ owner: this,
654
+ peerId,
655
+ key,
656
+ mobilePublicKey: normalizedPublicKey,
657
+ pairingTokenDigest,
658
+ envelopeSequence: envelope.sequence,
659
+ envelopeTranscriptHash: relayEnvelopeTranscriptHash(envelope),
660
+ authority,
661
+ possessionCapability,
662
+ });
663
+ return Object.freeze({
664
+ message,
665
+ stage,
666
+ possessionCapability,
667
+ });
668
+ }
669
+
670
+ commitStagedRelayPairing(stage) {
671
+ const candidate = stagedRelayPairings.get(stage);
672
+ stagedRelayPairings.delete(stage);
673
+ if (!candidate || candidate.owner !== this) {
674
+ throw new Error("Remote relay pairing stage is invalid or stale");
675
+ }
676
+ const outstandingCapability = this.outstandingRelayCapabilities.get(
677
+ candidate.peerId,
678
+ );
679
+ if (outstandingCapability) {
680
+ relayPossessionCapabilities.delete(outstandingCapability);
681
+ }
682
+ this.pendingRelayCapabilities.delete(candidate.peerId);
683
+ this.outstandingRelayCapabilities.delete(candidate.peerId);
684
+ this.pairingMaterial.delete(candidate.peerId);
685
+ this.keys.set(candidate.peerId, candidate.key);
686
+ this.relayPeerBindings.set(candidate.peerId, {
687
+ mobilePublicKey: candidate.mobilePublicKey,
688
+ pairingTokenDigest: candidate.pairingTokenDigest,
689
+ });
690
+ this.receivedSequences.set(candidate.peerId, candidate.envelopeSequence);
691
+ this.acceptedRelayMemberships.delete(candidate.peerId);
692
+ this.discardPendingRelayMembershipCommit(candidate.peerId);
693
+ }
694
+
695
+ /**
696
+ * Arm recovery before invoking the durable coordinator/registry. This moves
697
+ * the verified candidate out of the opaque stage and binds its exact request,
698
+ * authority instance, and pre-join authority generation without publishing
699
+ * a live peer key, replay high-water mark, or acceptance receipt.
700
+ */
701
+ armPendingRelayMembershipCommit(
702
+ stage,
703
+ { authoritySnapshot, scopes, capabilities } = {},
704
+ ) {
705
+ const candidate = stagedRelayPairings.get(stage);
706
+ const snapshot = authoritySnapshot;
707
+ if (
708
+ !candidate ||
709
+ candidate.owner !== this ||
710
+ !candidate.authority ||
711
+ snapshot?.sessionId !== this.sessionId ||
712
+ snapshot.status !== "active" ||
713
+ !/^[1-9]\d*$/.test(String(snapshot.sessionEpoch)) ||
714
+ !/^\d+$/.test(String(snapshot.authorityGeneration)) ||
715
+ !Array.isArray(snapshot.members)
716
+ ) {
717
+ throw new Error("Remote relay pairing stage is invalid or stale");
718
+ }
719
+ const expectedScopes = normalizeRelayAuthorityScopes(scopes);
720
+ const expectedCapabilities = normalizeRelayCapabilities(capabilities);
721
+ if (
722
+ expectedScopes.some(
723
+ (scope) => !candidate.authority.authorizedScopes.includes(scope),
724
+ )
725
+ ) {
726
+ throw new Error("Remote relay membership request exceeds its authority");
727
+ }
728
+ const expectedCredentialDigest = relayCredentialDigest(
729
+ candidate.mobilePublicKey,
730
+ );
731
+ const expectedPrincipalId = `relay-x25519:${expectedCredentialDigest}`;
732
+ const baselineMember = snapshot.members.find(
733
+ (member) => member.principalId === expectedPrincipalId,
734
+ );
735
+ if (baselineMember?.status === "active") {
736
+ throw new Error("Remote relay principal is already active");
737
+ }
738
+ if (
739
+ baselineMember &&
740
+ baselineMember.credentialKeySha256 !== expectedCredentialDigest
741
+ ) {
742
+ throw new Error("Remote relay authority credential binding changed");
743
+ }
744
+ const pending = Object.freeze({
745
+ phase: "prepared",
746
+ peerId: candidate.peerId,
747
+ key: candidate.key,
748
+ mobilePublicKey: candidate.mobilePublicKey,
749
+ pairingTokenDigest: candidate.pairingTokenDigest,
750
+ initialEnvelopeSequence: candidate.envelopeSequence,
751
+ initialEnvelopeTranscriptHash: candidate.envelopeTranscriptHash,
752
+ localHighWater: candidate.envelopeSequence,
753
+ authority: candidate.authority,
754
+ possessionCapability: candidate.possessionCapability,
755
+ expectedPrincipalId,
756
+ expectedCredentialDigest,
757
+ expectedSessionEpoch: String(snapshot.sessionEpoch),
758
+ expectedMembershipEpoch: baselineMember
759
+ ? String(BigInt(baselineMember.membershipEpoch) + 1n)
760
+ : "1",
761
+ expectedScopes,
762
+ expectedCapabilities,
763
+ baselineAuthorityGeneration: String(snapshot.authorityGeneration),
764
+ baselineMembershipEpoch: baselineMember?.membershipEpoch || null,
765
+ baselineMembershipStatus: baselineMember?.status || null,
766
+ receipt: null,
767
+ });
768
+ const existing = this.pendingCommittedRelayAcceptances.get(
769
+ candidate.peerId,
770
+ );
771
+ if (existing && !samePendingRelayRequest(existing, pending)) {
409
772
  throw new Error(
410
- `Remote Session envelope authentication failed: ${error.message}`,
773
+ "Remote relay peer already has a different pending membership commit",
411
774
  );
412
775
  }
776
+ if (!existing) {
777
+ this.pendingCommittedRelayAcceptances.set(candidate.peerId, pending);
778
+ } else if (candidate.possessionCapability) {
779
+ relayPossessionCapabilities.delete(candidate.possessionCapability);
780
+ }
781
+ stagedRelayPairings.delete(stage);
782
+ return existing || pending;
783
+ }
784
+
785
+ completePendingCommittedRelayAcceptance(peerId, acceptance) {
786
+ const pending = this.pendingCommittedRelayAcceptances.get(peerId);
787
+ if (!pending) {
788
+ throw new Error("Remote relay committed acceptance is unavailable");
789
+ }
790
+ // This boundary is deliberately after RemoteSessionRegistry.joinRelayMember
791
+ // has returned (including its local transport attachment). A coordinator
792
+ // commit that fails before registry attachment remains fail-closed because
793
+ // this three-file slice has no trusted public reattach operation.
794
+ this._faultHooks.afterRelayRegistryCommit?.({
795
+ sessionId: this.sessionId,
796
+ peerId,
797
+ expectedPrincipalId: pending.expectedPrincipalId,
798
+ expectedSessionEpoch: pending.expectedSessionEpoch,
799
+ expectedMembershipEpoch: pending.expectedMembershipEpoch,
800
+ });
801
+ const receipt = createRelayMembershipAcceptance(
802
+ this.sessionId,
803
+ peerId,
804
+ acceptance,
805
+ );
806
+ if (
807
+ receipt.mobilePublicKey !== pending.mobilePublicKey ||
808
+ receipt.pairingTokenDigest !== pending.pairingTokenDigest ||
809
+ receipt.principalId !== pending.expectedPrincipalId ||
810
+ receipt.sessionEpoch !== pending.expectedSessionEpoch ||
811
+ receipt.membershipEpoch !== pending.expectedMembershipEpoch ||
812
+ !sameStringArray(receipt.scopes, pending.expectedScopes) ||
813
+ !sameStringArray(receipt.capabilities, pending.expectedCapabilities)
814
+ ) {
815
+ throw new Error("Remote relay membership acceptance binding is invalid");
816
+ }
817
+ this.pendingCommittedRelayAcceptances.set(
818
+ peerId,
819
+ Object.freeze({ ...pending, phase: "committed", receipt }),
820
+ );
821
+ return receipt;
822
+ }
823
+
824
+ resolvePendingRelayMembershipCommit(
825
+ peerId,
826
+ authorityRead,
827
+ authorityDescriptor,
828
+ ) {
829
+ const pending = this.pendingCommittedRelayAcceptances.get(peerId);
830
+ const session = authorityRead?.session;
831
+ const statement = authorityRead?.statement;
832
+ if (
833
+ !pending ||
834
+ !session ||
835
+ session.sessionId !== this.sessionId ||
836
+ authorityDescriptor?.coordinatorId !== pending.authority.coordinatorId ||
837
+ authorityDescriptor?.serverInstanceId !==
838
+ pending.authority.serverInstanceId ||
839
+ authorityDescriptor?.authorityVersion !==
840
+ pending.authority.authorityVersion ||
841
+ statement?.coordinatorId !== pending.authority.coordinatorId ||
842
+ statement?.authorityVersion !== pending.authority.authorityVersion ||
843
+ statement?.generation !== String(session.authorityGeneration) ||
844
+ statement?.payload?.sessionId !== session.sessionId ||
845
+ statement?.payload?.sessionEpoch !== session.sessionEpoch
846
+ ) {
847
+ return Object.freeze({ status: "fenced", receipt: null });
848
+ }
849
+ const member = session.members?.find(
850
+ (candidate) => candidate.principalId === pending.expectedPrincipalId,
851
+ );
852
+ const generationAdvanced =
853
+ /^\d+$/.test(String(session.authorityGeneration)) &&
854
+ BigInt(session.authorityGeneration) >
855
+ BigInt(pending.baselineAuthorityGeneration);
856
+ if (
857
+ generationAdvanced &&
858
+ session.status === "active" &&
859
+ String(session.sessionEpoch) === pending.expectedSessionEpoch &&
860
+ member?.status === "active" &&
861
+ member.credentialKeySha256 === pending.expectedCredentialDigest &&
862
+ String(member.membershipEpoch) === pending.expectedMembershipEpoch &&
863
+ sameStringArray(
864
+ normalizeRelayAuthorityScopes(member.scopes),
865
+ pending.expectedScopes,
866
+ )
867
+ ) {
868
+ const receipt = createRelayMembershipAcceptance(this.sessionId, peerId, {
869
+ mobilePublicKey: pending.mobilePublicKey,
870
+ pairingTokenDigest: pending.pairingTokenDigest,
871
+ principalId: pending.expectedPrincipalId,
872
+ sessionEpoch: pending.expectedSessionEpoch,
873
+ membershipEpoch: pending.expectedMembershipEpoch,
874
+ scopes: pending.expectedScopes,
875
+ capabilities: pending.expectedCapabilities,
876
+ statement: authorityRead.statement || null,
877
+ });
878
+ this.pendingCommittedRelayAcceptances.set(
879
+ peerId,
880
+ Object.freeze({ ...pending, phase: "committed", receipt }),
881
+ );
882
+ return Object.freeze({ status: "committed", receipt });
883
+ }
884
+ const baselineMemberUnchanged = pending.baselineMembershipEpoch
885
+ ? member?.status === pending.baselineMembershipStatus &&
886
+ String(member.membershipEpoch) === pending.baselineMembershipEpoch &&
887
+ member.credentialKeySha256 === pending.expectedCredentialDigest
888
+ : !member;
889
+ if (
890
+ session.status === "active" &&
891
+ String(session.sessionEpoch) === pending.expectedSessionEpoch &&
892
+ baselineMemberUnchanged
893
+ ) {
894
+ return Object.freeze({ status: "not-committed", receipt: null });
895
+ }
896
+ return Object.freeze({ status: "fenced", receipt: null });
897
+ }
898
+
899
+ discardPendingRelayMembershipCommit(peerId) {
900
+ const pending = this.pendingCommittedRelayAcceptances.get(peerId);
901
+ if (!pending) return false;
902
+ if (pending.possessionCapability) {
903
+ relayPossessionCapabilities.delete(pending.possessionCapability);
904
+ }
905
+ this.pendingCommittedRelayAcceptances.delete(peerId);
906
+ return true;
907
+ }
908
+
909
+ /**
910
+ * Publish a previously armed durable outcome. Pending is deleted last, so a
911
+ * synchronous failure at either injected boundary can safely re-enter this
912
+ * method without lowering an already-observed replay high-water mark.
913
+ */
914
+ finalizeCommittedRelayAcceptance(peerId) {
915
+ const pending = this.pendingCommittedRelayAcceptances.get(peerId);
916
+ if (!pending) {
917
+ const accepted = this.acceptedRelayMemberships.get(peerId);
918
+ if (accepted) return accepted;
919
+ throw new Error("Remote relay committed acceptance is unavailable");
920
+ }
921
+ if (pending.phase !== "committed" || !pending.receipt) {
922
+ throw new Error("Remote relay committed acceptance is not authoritative");
923
+ }
924
+ const outstandingCapability = this.outstandingRelayCapabilities.get(peerId);
925
+ if (outstandingCapability) {
926
+ relayPossessionCapabilities.delete(outstandingCapability);
927
+ }
928
+ this.pendingRelayCapabilities.delete(peerId);
929
+ this.outstandingRelayCapabilities.delete(peerId);
930
+ this.pairingMaterial.delete(peerId);
931
+ this.keys.set(peerId, pending.key);
932
+ this.relayPeerBindings.set(peerId, {
933
+ mobilePublicKey: pending.mobilePublicKey,
934
+ pairingTokenDigest: pending.pairingTokenDigest,
935
+ });
936
+ // The candidate key starts a distinct sequence generation. Never mix an
937
+ // old live key's high-water into it; pending-local authenticated progress
938
+ // becomes live only with the exact committed authority tuple.
939
+ this.receivedSequences.set(peerId, pending.localHighWater);
940
+ this.acceptedRelayMemberships.delete(peerId);
941
+ this._faultHooks.afterCommittedRelayKeyPublication?.({
942
+ sessionId: this.sessionId,
943
+ peerId,
944
+ principalId: pending.receipt.principalId,
945
+ sessionEpoch: pending.receipt.sessionEpoch,
946
+ membershipEpoch: pending.receipt.membershipEpoch,
947
+ });
948
+ this.acceptedRelayMemberships.set(peerId, pending.receipt);
949
+ this.pendingCommittedRelayAcceptances.delete(peerId);
950
+ this._faultHooks.afterCommittedRelayReceiptPublication?.({
951
+ sessionId: this.sessionId,
952
+ peerId,
953
+ principalId: pending.receipt.principalId,
954
+ sessionEpoch: pending.receipt.sessionEpoch,
955
+ membershipEpoch: pending.receipt.membershipEpoch,
956
+ });
957
+ return pending.receipt;
958
+ }
959
+
960
+ decryptPendingCommittedRelayAcceptance(peerId, envelope) {
961
+ const pending = this.pendingCommittedRelayAcceptances.get(peerId);
962
+ if (!pending || envelope?.senderId !== peerId) {
963
+ throw new Error("Remote relay committed acceptance is unavailable");
964
+ }
965
+ const previous = pending.localHighWater;
966
+ const message = decryptEnvelopeWithKey(
967
+ this.sessionId,
968
+ pending.key,
969
+ envelope,
970
+ previous,
971
+ );
972
+ // Keep retry progress isolated from the live key generation until fresh
973
+ // authority readback and exact tuple validation permit final publication.
974
+ this.pendingCommittedRelayAcceptances.set(
975
+ peerId,
976
+ Object.freeze({ ...pending, localHighWater: envelope.sequence }),
977
+ );
978
+ return message;
979
+ }
980
+
981
+ readPendingCommittedRelayAcceptance(
982
+ peerId,
983
+ { mobilePublicKey, pairingTokenDigest, capabilities } = {},
984
+ ) {
985
+ const receipt =
986
+ this.pendingCommittedRelayAcceptances.get(peerId)?.receipt || null;
987
+ const normalizedPublicKey = canonicalPublicKey(mobilePublicKey);
988
+ const normalizedCapabilities = normalizeRelayCapabilities(capabilities);
989
+ if (
990
+ !receipt ||
991
+ receipt.mobilePublicKey !== normalizedPublicKey ||
992
+ receipt.pairingTokenDigest !== pairingTokenDigest ||
993
+ !sameStringArray(receipt.capabilities, normalizedCapabilities)
994
+ ) {
995
+ throw new Error(
996
+ "Remote relay committed acceptance is unavailable or mismatched",
997
+ );
998
+ }
999
+ return receipt;
1000
+ }
1001
+
1002
+ hasPendingCommittedRelayAcceptance(peerId) {
1003
+ return this.pendingCommittedRelayAcceptances.has(peerId);
1004
+ }
1005
+
1006
+ readPendingRelayMembershipRequest(
1007
+ peerId,
1008
+ { mobilePublicKey, pairingTokenDigest, capabilities } = {},
1009
+ ) {
1010
+ const pending = this.pendingCommittedRelayAcceptances.get(peerId) || null;
1011
+ const normalizedPublicKey = canonicalPublicKey(mobilePublicKey);
1012
+ const normalizedCapabilities = normalizeRelayCapabilities(capabilities);
1013
+ if (
1014
+ !pending ||
1015
+ pending.mobilePublicKey !== normalizedPublicKey ||
1016
+ pending.pairingTokenDigest !== pairingTokenDigest ||
1017
+ !sameStringArray(pending.expectedCapabilities, normalizedCapabilities)
1018
+ ) {
1019
+ throw new Error(
1020
+ "Remote relay pending membership request is unavailable or mismatched",
1021
+ );
1022
+ }
1023
+ return Object.freeze({
1024
+ sessionId: this.sessionId,
1025
+ mobilePeerId: peerId,
1026
+ pairingTokenDigest: pending.pairingTokenDigest,
1027
+ expectedPrincipalId: pending.expectedPrincipalId,
1028
+ expectedSessionEpoch: pending.expectedSessionEpoch,
1029
+ expectedMembershipEpoch: pending.expectedMembershipEpoch,
1030
+ scopes: pending.expectedScopes,
1031
+ capabilities: pending.expectedCapabilities,
1032
+ phase: pending.phase,
1033
+ });
1034
+ }
1035
+
1036
+ clearRelayPeer(peerId) {
1037
+ const outstandingCapability = this.outstandingRelayCapabilities.get(peerId);
1038
+ if (outstandingCapability) {
1039
+ relayPossessionCapabilities.delete(outstandingCapability);
1040
+ }
1041
+ this.discardPendingRelayMembershipCommit(peerId);
1042
+ this.pendingRelayCapabilities.delete(peerId);
1043
+ this.outstandingRelayCapabilities.delete(peerId);
1044
+ this.pairingMaterial.delete(peerId);
1045
+ this.keys.delete(peerId);
1046
+ this.relayPeerBindings.delete(peerId);
1047
+ this.receivedSequences.delete(peerId);
1048
+ this.acceptedRelayMemberships.delete(peerId);
413
1049
  }
414
1050
 
415
1051
  takeRelayPossessionCapability(peerId) {
@@ -422,4 +1058,68 @@ export class RemoteSessionCryptoContext {
422
1058
  }
423
1059
  return capability;
424
1060
  }
1061
+
1062
+ /**
1063
+ * Keep the exact, already-committed relay membership tuple in this crypto
1064
+ * process. It is deliberately not persisted: it only reconciles an ACK lost
1065
+ * while the same host process still owns the authenticated DH/AEAD context.
1066
+ */
1067
+ rememberRelayMembershipAcceptance(
1068
+ peerId,
1069
+ {
1070
+ mobilePublicKey,
1071
+ pairingTokenDigest,
1072
+ principalId,
1073
+ sessionEpoch,
1074
+ membershipEpoch,
1075
+ scopes,
1076
+ capabilities,
1077
+ statement = null,
1078
+ } = {},
1079
+ ) {
1080
+ const peer = this.relayPeerBindings.get(peerId);
1081
+ const receipt = createRelayMembershipAcceptance(this.sessionId, peerId, {
1082
+ mobilePublicKey,
1083
+ pairingTokenDigest,
1084
+ principalId,
1085
+ sessionEpoch,
1086
+ membershipEpoch,
1087
+ scopes,
1088
+ capabilities,
1089
+ statement,
1090
+ });
1091
+ if (
1092
+ !peer ||
1093
+ peer.mobilePublicKey !== receipt.mobilePublicKey ||
1094
+ peer.pairingTokenDigest !== receipt.pairingTokenDigest
1095
+ ) {
1096
+ throw new Error("Remote relay membership acceptance binding is invalid");
1097
+ }
1098
+ this.acceptedRelayMemberships.set(peerId, receipt);
1099
+ return receipt;
1100
+ }
1101
+
1102
+ readRelayMembershipAcceptance(
1103
+ peerId,
1104
+ { mobilePublicKey, pairingTokenDigest, capabilities } = {},
1105
+ ) {
1106
+ const receipt = this.acceptedRelayMemberships.get(peerId);
1107
+ const normalizedPublicKey = canonicalPublicKey(mobilePublicKey);
1108
+ const normalizedCapabilities = normalizeRelayCapabilities(capabilities);
1109
+ if (
1110
+ !receipt ||
1111
+ receipt.mobilePublicKey !== normalizedPublicKey ||
1112
+ receipt.pairingTokenDigest !== pairingTokenDigest ||
1113
+ !sameStringArray(receipt.capabilities, normalizedCapabilities)
1114
+ ) {
1115
+ throw new Error(
1116
+ "Remote relay membership acceptance is unavailable or mismatched",
1117
+ );
1118
+ }
1119
+ return receipt;
1120
+ }
1121
+
1122
+ hasRelayMembershipAcceptance(peerId) {
1123
+ return this.acceptedRelayMemberships.has(peerId);
1124
+ }
425
1125
  }