chainlesschain 0.165.4 → 0.165.6

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 (214) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/{AIOps-C__nnraW.js → AIOps-vfoDb6HZ.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-CsKWHEZ-.js → ActionButton-Cm5f7p93.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-BhIjfjWO.js → Analytics-yRuvgWxa.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-BjuXFzWZ.js → AppLayout-BROsC4h1.js} +4 -4
  7. package/src/assets/web-panel/assets/{Artifacts-k6whDkZJ.js → Artifacts-CimHApb1.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit-C4FBMYRh.js → Audit-DI8kr8zI.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-C_vRrSXk.js → BackgroundAgents-DSLzKy-n.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-CngFZeVp.js → Backup-DkH_n2r1.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-B_H9EBRm.js → BaseInput-CKSrUoRA.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-DLEC5kt7.js → Chat-ClwfMXUS.js} +3 -3
  13. package/src/assets/web-panel/assets/ChatBubbleRenderer-CjMaqfL7.js +1 -0
  14. package/src/assets/web-panel/assets/{Checkbox-m3ClR4xh.js → Checkbox-BYTehcQG.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-CmLSFrgp.js → Codegen-B8AMcp6J.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-DFGQteHN.js → Col-BNSogPo3.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-EUUVORHE.js → Community-JxPvGb8O.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-2PSg0Yr7.js → Compact-DF7bFAgE.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-CcKur7gB.js → Compliance-Bvhqnwrq.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-DK_Fezfa.js → Cowork-BDS0CJvM.js} +3 -3
  21. package/src/assets/web-panel/assets/{Cron-Dbw89xl7.js → Cron-DqvRfxoQ.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-CH66h_Ec.js → Crosschain-CDcIvnqS.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-D1igRb17.js → DID-D7jaUxU3.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-DVAsrobk.js → Dashboard-B5A18wEV.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-Dl7W8AQs.js → Dropdown-_6IVkGag.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-D5tpaEQC.js → EmailListRenderer-C9PeiKCG.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-O_VQf8gq.js → FamilyGuardDashboard-DXjuEHrX.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-BTVcgS9n.js → Federation-B4IAR9zv.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-BRnkJAIz.js → FormItemContext-CxQeSNx1.js} +1 -1
  30. package/src/assets/web-panel/assets/{GenericCardRenderer-8OWSCkDj.js → GenericCardRenderer-B8GS-Y3N.js} +1 -1
  31. package/src/assets/web-panel/assets/{Git-WXLYTQj2.js → Git-BS8SouUX.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-BezaUhtc.js → Governance-Dq9aYb5Q.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-CuRay3Gu.js → Inference-BrCEe9AJ.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-DfWLDW1m.js → KnowledgeGraph-DybOpiY2.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-C55y2pPY.js → Logs-DBpzbRfx.js} +2 -2
  36. package/src/assets/web-panel/assets/MarkdownRenderer-CFyt8iUd.js +1 -0
  37. package/src/assets/web-panel/assets/{Marketplace-Dk08Uycg.js → Marketplace-Cc7CCSCu.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-C11fSB5V.js → McpTools-DFaIhOfu.js} +3 -3
  39. package/src/assets/web-panel/assets/{Memory-mr0eZc97.js → Memory-KCqlp9ol.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-C0iCSRbb.js → MobileBridge-Bys84ySN.js} +1 -1
  41. package/src/assets/web-panel/assets/{MobileProjects-CY-xMG8Z.js → MobileProjects-B-6qU-je.js} +1 -1
  42. package/src/assets/web-panel/assets/{Mtc-Bplqc_Wc.js → Mtc-BgY-fvCq.js} +4 -4
  43. package/src/assets/web-panel/assets/{MtcAudit-BlzwsoSM.js → MtcAudit-B8pCmqQJ.js} +4 -4
  44. package/src/assets/web-panel/assets/{Multisig-BBLmTty6.js → Multisig-BbOmYutA.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-DtT1uf1W.js → NLProgramming-BocLQz_M.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-DcqQAxTK.js → Notes-CSwbxEop.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-cb0kWoZe.js → NotificationSettings-CqFNSYG3.js} +1 -1
  48. package/src/assets/web-panel/assets/{OrderTableRenderer-kw9HZQNN.js → OrderTableRenderer-C6gMfAk7.js} +1 -1
  49. package/src/assets/web-panel/assets/{Organization-DlTlbkUN.js → Organization-QrDXVdMJ.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-pp1qs-oY.js → Overflow-Cxntotjt.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-BESLnyYz.js → P2P-CIdBOwNq.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-B-J1mGuf.js → PdhVaultBrowser-CT5aP8rD.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-B36PeT1_.js → Permissions-DH7OM8XU.js} +4 -4
  54. package/src/assets/web-panel/assets/{PersonalDataHub-CTK2ad-3.js → PersonalDataHub-CnJqbWce.js} +3 -3
  55. package/src/assets/web-panel/assets/{Pipeline-zJNGXsac.js → Pipeline-5C3HG1kH.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-b9fz0Qf0.js → Privacy-B4yIp7xK.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-DjZbZgiB.js → ProjectInit-BMvt-2M0.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-C-hHJXlD.js → ProjectSettings-CvSTEMRG.js} +2 -2
  59. package/src/assets/web-panel/assets/Projects-BKBJxEek.js +1 -0
  60. package/src/assets/web-panel/assets/Providers-BdwUqFWN.js +1 -0
  61. package/src/assets/web-panel/assets/{QrScannerModal-A0yoQ3-3.js → QrScannerModal-DtnCKlTm.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-DrFgFJNC.js → QuickAsk-sTbgwyNr.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-KC8hF1g4.js → Recommend-Bzr14gSb.js} +1 -1
  64. package/src/assets/web-panel/assets/{RemoteSession-DGTPIk6u.js → RemoteSession-C_TF5IaJ.js} +2 -2
  65. package/src/assets/web-panel/assets/{Reputation-QmbwM7Yk.js → Reputation-GCAM4H2K.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-BxjJylia.js → Row-Dir0r6OQ.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-B7Wda1J6.js → RssFeed-CpGnlecX.js} +2 -2
  68. package/src/assets/web-panel/assets/{Search-_plQ_YSA.js → Search-DMMamC6u.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-CD2YJZfx.js → Security-yqay5yG-.js} +4 -4
  70. package/src/assets/web-panel/assets/{Services-qQE7NmwE.js → Services-C5i2wh9G.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-DgQqkFf7.js → Skeleton-vUqk35yj.js} +1 -1
  72. package/src/assets/web-panel/assets/{Skills-C4C0oFFt.js → Skills-DzcSkhot.js} +1 -1
  73. package/src/assets/web-panel/assets/{Sla-Px2N2B5c.js → Sla-CB6qbYNr.js} +1 -1
  74. package/src/assets/web-panel/assets/{SpeechSettings-DN6C3lQd.js → SpeechSettings-BHUI7cox.js} +1 -1
  75. package/src/assets/web-panel/assets/{SyncSettings-jm5aH_m7.js → SyncSettings-D-RrsxTJ.js} +2 -2
  76. package/src/assets/web-panel/assets/{Tasks-DlkJcv67.js → Tasks-DFxdOJiq.js} +1 -1
  77. package/src/assets/web-panel/assets/{Templates-DyrTHUBs.js → Templates-SKAob_Aq.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant-mC434PtL.js → Tenant-BPfsfHQ-.js} +1 -1
  79. package/src/assets/web-panel/assets/{Terminal-o0_s3NgK.js → Terminal-8iw7FpUf.js} +2 -2
  80. package/src/assets/web-panel/assets/{TimelineRenderer-DAGlAXOw.js → TimelineRenderer-C4Gjj5Su.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tokens-BnulS_bs.js → Tokens-COYhN18e.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger-suQbVXYM.js → Trigger-B-JzXcf6.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-DbIJAPo5.js → Trust-BJ5bg0nI.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-CQNpbIHi.js → UkeySign-BG_ByRMf.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-D8aWCERk.js → VideoEditing-B3kOnSiU.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-CuLpYfZc.js → Wallet-CcoGFnK7.js} +2 -2
  87. package/src/assets/web-panel/assets/{WebAuthn-CsfG8ZMc.js → WebAuthn-DdT8vRC_.js} +4 -4
  88. package/src/assets/web-panel/assets/{WorkflowEditor-BYsEAZbd.js → WorkflowEditor-CZu3M9N8.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-FT4Um3-I.js → chat-DxJt3Qba.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-CKbS_Wlt.js → colors-DdYfwJaB.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-CFJtzuJM.js → compact-item-BG3SUzwj.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-B9kSRW6K.js → createContext-By51z2-1.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-DorRZcI9.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-DR-x0Xpi.js → hasIn-B0apZ3KJ.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-DjEP7WkH.js → index-3jWkIu5j.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-7jHr_A_k.js → index-B3rWuPMW.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-DcfxhKXR.js → index-B5rLiEo0.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-tqSEHPRT.js → index-BBPf3TpM.js} +1 -1
  99. package/src/assets/web-panel/assets/index-BETiL0a3.js +1 -0
  100. package/src/assets/web-panel/assets/{index-Cr2FLUsD.js → index-BIrJfqO4.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-Bt4fS2eL.js → index-BLIbGOLX.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-BwR7yc2h.js → index-BP94CrBV.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-BVPU43b1.js → index-BWF8_mkx.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DShSkww-.js → index-BYINoRLK.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-Bzz0C1BZ.js → index-BZ_0qm4g.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-KqDaCApw.js → index-BewKYrX9.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BW-t9n-q.js → index-BfWYsB9t.js} +1 -1
  108. package/src/assets/web-panel/assets/index-BhAVHuW8.js +1 -0
  109. package/src/assets/web-panel/assets/{index-B7osrXqy.js → index-Bissefox.js} +3 -3
  110. package/src/assets/web-panel/assets/{index-DW4BWjTk.js → index-BkvcEo4p.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DdkIhfi-.js → index-Bos5_aMV.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-DTxNy0ZU.js → index-C1C9i79A.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-QBSeNVO3.js → index-CAhHirVH.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-D4LfPvWA.js → index-CNIxXMmN.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-C_enk9I_.js → index-CW93tZDv.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-WE08gMJP.js → index-CWEuZWCK.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-r2VI1Ikk.js → index-Cp6e-hGw.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-Dap9c3eD.js → index-CpjZhgNd.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DWdqx7zI.js → index-CzmQElZy.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BGbPnC5x.js → index-DCn8IcxV.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-DiNUEpSQ.js → index-DE76D-kA.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-DtkFqPVA.js → index-DaIz0NAW.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CGaXUsIs.js → index-Da_WBdLX.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-B3ZX81mK.js → index-Dz49VOyQ.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-BK3TiKVq.js → index-Q2SCX-MI.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-CJmIQ9XR.js → index-RD6O-0C6.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CvrCCqJt.js → index-SQqOlR3d.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BI5hP_qy.js → index-ZFbps9po.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-DeSgw-WV.js → index-ZxtP8jG7.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-DMGmg497.js → index-bP_VlDb4.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BfFCQHL3.js → index-kf57LJ93.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-OSzjTzGM.js → index-oxRrmFO9.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-C2JvmYRc.js → index-p1_8SbI1.js} +1 -1
  134. package/src/assets/web-panel/assets/{initDefaultProps-Dd7n7VgI.js → initDefaultProps-BWJktcg8.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-BwxouRw1.js → motion-BEXBzUFV.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-DObN0PG9.js → move-BUKwn2sx.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-B3nOUQc7.js → mtc-parser-FTI7btqQ.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-o9WU2Nbl.js → omit-CsLtSPH3.js} +1 -1
  139. package/src/assets/web-panel/assets/parsers-YNLm7sQ0.js +4 -0
  140. package/src/assets/web-panel/assets/{pickAttrs-CL_vvfQ_.js → pickAttrs-DWZxsT_S.js} +1 -1
  141. package/src/assets/web-panel/assets/{placementArrow-CYrM2tyK.js → placementArrow-Bcfs43CX.js} +1 -1
  142. package/src/assets/web-panel/assets/{responsiveObserve-k_06cQ96.js → responsiveObserve-a9bcC1p-.js} +1 -1
  143. package/src/assets/web-panel/assets/{slide-CPKG0pxS.js → slide-BDTzgOXN.js} +1 -1
  144. package/src/assets/web-panel/assets/{statusUtils-BDDEeLTw.js → statusUtils-Iq1j_HUg.js} +1 -1
  145. package/src/assets/web-panel/assets/{styleChecker-Cmb7RTh7.js → styleChecker-DAZEYxeh.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFlexGapSupport-e2IedC4N.js → useFlexGapSupport-DVkjyDrh.js} +1 -1
  147. package/src/assets/web-panel/assets/{useFs-CE_gxVnm.js → useFs-wXTpNTHt.js} +1 -1
  148. package/src/assets/web-panel/assets/{usePersonalDataHub-BkMIIKMz.js → usePersonalDataHub-CtQM_aZr.js} +1 -1
  149. package/src/assets/web-panel/assets/{vnode-DW886XV1.js → vnode-hjEoG85v.js} +1 -1
  150. package/src/assets/web-panel/assets/{zoom-Dwo_adaf.js → zoom-BHVpyKg_.js} +1 -1
  151. package/src/assets/web-panel/index.html +1 -1
  152. package/src/command-help-index.json +3 -3
  153. package/src/commands/agent.js +14 -2
  154. package/src/commands/artifacts.js +235 -0
  155. package/src/commands/background-session.js +100 -10
  156. package/src/commands/cowork.js +86 -57
  157. package/src/commands/mcp.js +5 -0
  158. package/src/commands/plugin.js +145 -29
  159. package/src/commands/remote-control.js +295 -110
  160. package/src/commands/session-location.js +20 -1
  161. package/src/commands/session-message.js +171 -0
  162. package/src/commands/session.js +75 -69
  163. package/src/data/changelog.json +29 -3
  164. package/src/gateways/ws/message-dispatcher.js +57 -0
  165. package/src/harness/mcp-client.js +141 -10
  166. package/src/lib/artifact-access-ledger.js +1 -0
  167. package/src/lib/artifact-deletion-ledger.js +1 -0
  168. package/src/lib/artifact-recovery.js +970 -0
  169. package/src/lib/artifact-workbench-projection.js +171 -0
  170. package/src/lib/background-session-transport.js +153 -27
  171. package/src/lib/dynamic-workflow-runtime.js +408 -1
  172. package/src/lib/execution-location-target.js +4 -2
  173. package/src/lib/mcp-client.js +2 -0
  174. package/src/lib/mcp-headers-helper.js +47 -5
  175. package/src/lib/mcp-oauth.js +55 -6
  176. package/src/lib/mcp-stdio-executable-identity.js +16 -0
  177. package/src/lib/mcp-stdio-native-code-policy.js +74 -0
  178. package/src/lib/mcp-stdio-package-materialization.js +12 -3
  179. package/src/lib/mcp-tls.js +230 -0
  180. package/src/lib/output-styles.js +12 -0
  181. package/src/lib/plugin-runtime/install.js +614 -71
  182. package/src/lib/plugin-runtime/marketplace-archive-source.js +331 -0
  183. package/src/lib/plugin-runtime/marketplace-catalog.js +52 -6
  184. package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +1 -0
  185. package/src/lib/plugin-runtime/marketplace-source-adapter.js +163 -0
  186. package/src/lib/plugin-runtime/marketplace-source-cache.js +11 -1
  187. package/src/lib/plugin-runtime/remote-source.js +435 -55
  188. package/src/lib/plugin-security.js +393 -41
  189. package/src/lib/plugin-source-identity.js +850 -0
  190. package/src/lib/process-execution-broker/index.js +112 -0
  191. package/src/lib/process-execution-broker/platform-sandbox.js +57 -3
  192. package/src/lib/process-execution-broker/workspace-transaction.js +26 -0
  193. package/src/lib/remote-approval-bridge.js +136 -46
  194. package/src/lib/remote-control.js +217 -31
  195. package/src/lib/session-message-fabric.js +1105 -0
  196. package/src/lib/session-projection.js +171 -12
  197. package/src/lib/session-runtime-retention.js +155 -0
  198. package/src/lib/settings-loader.cjs +29 -0
  199. package/src/repl/agent-repl.js +51 -14
  200. package/src/repl/permission-prompt.js +31 -8
  201. package/src/repl/repl-keybindings.js +55 -0
  202. package/src/runtime/agent-core.js +63 -2
  203. package/src/runtime/headless-runner.js +47 -66
  204. package/src/runtime/headless-stream.js +18 -24
  205. package/src/runtime/mcp-config.js +2 -0
  206. package/src/runtime/policies/agent-policy.js +2 -0
  207. package/src/assets/web-panel/assets/ChatBubbleRenderer-xijcWkJe.js +0 -1
  208. package/src/assets/web-panel/assets/MarkdownRenderer-hDpWdiBc.js +0 -1
  209. package/src/assets/web-panel/assets/Projects-BC9KSDM6.js +0 -1
  210. package/src/assets/web-panel/assets/Providers-Dz-CAYYU.js +0 -1
  211. package/src/assets/web-panel/assets/devWarning-DJZ0gAMu.js +0 -1
  212. package/src/assets/web-panel/assets/index-Bizb4hkw.js +0 -1
  213. package/src/assets/web-panel/assets/index-OHSl-xCh.js +0 -1
  214. package/src/assets/web-panel/assets/parsers-UEvh_ShA.js +0 -4
@@ -0,0 +1,1105 @@
1
+ /**
2
+ * Durable, CLI-owned cross-session message fabric.
3
+ *
4
+ * The fabric deliberately owns only addressing, admission, receipts and inbox
5
+ * durability. Transports (same-host processes, Remote Control, A2A or a future
6
+ * relay) may all call the same API, so none of them can invent a broader
7
+ * inbound policy. Every mutation is a strict cross-process locked, atomic JSON
8
+ * transaction and every endpoint registration has a fresh epoch; a reused
9
+ * name can therefore never inherit an earlier session's inbox.
10
+ */
11
+
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+ import { createHash, randomUUID } from "node:crypto";
15
+ import { ensureDir, getHomeDir } from "./paths.js";
16
+ import { withFileLock } from "./with-file-lock.js";
17
+
18
+ export const SESSION_MESSAGE_FABRIC_SCHEMA =
19
+ "chainlesschain.session-message-fabric/v1";
20
+ export const SESSION_MESSAGE_FABRIC_PROJECTION_SCHEMA =
21
+ "chainlesschain.session-message-fabric-projection/v1";
22
+
23
+ export const SESSION_MESSAGE_POLICIES = Object.freeze([
24
+ "accept",
25
+ "hold",
26
+ "refuse",
27
+ ]);
28
+
29
+ export const SESSION_MESSAGE_RECEIPT_STATUSES = Object.freeze([
30
+ "delivered",
31
+ "held",
32
+ "refused",
33
+ "full",
34
+ "expired",
35
+ ]);
36
+
37
+ export const SESSION_MESSAGE_FABRIC_LIMITS = Object.freeze({
38
+ maxMessageBytes: 256 * 1024,
39
+ maxPendingPerRecipient: 100,
40
+ maxReceiptHistory: 5_000,
41
+ defaultTtlMs: 24 * 60 * 60 * 1000,
42
+ maxTtlMs: 7 * 24 * 60 * 60 * 1000,
43
+ });
44
+
45
+ export const SESSION_MESSAGE_FABRIC_ERROR_CODES = Object.freeze({
46
+ INVALID_ARGUMENT: "SESSION_MESSAGE_INVALID_ARGUMENT",
47
+ ENDPOINT_NOT_FOUND: "SESSION_MESSAGE_ENDPOINT_NOT_FOUND",
48
+ NAME_CONFLICT: "SESSION_MESSAGE_NAME_CONFLICT",
49
+ MESSAGE_TOO_LARGE: "SESSION_MESSAGE_TOO_LARGE",
50
+ MESSAGE_ID_CONFLICT: "SESSION_MESSAGE_ID_CONFLICT",
51
+ STALE_REVISION: "SESSION_MESSAGE_STALE_REVISION",
52
+ STATE_CORRUPT: "SESSION_MESSAGE_STATE_CORRUPT",
53
+ STATE_UNAVAILABLE: "SESSION_MESSAGE_STATE_UNAVAILABLE",
54
+ });
55
+
56
+ const PENDING_STATUSES = new Set(["held", "delivered"]);
57
+ const RECEIPT_STATUSES = new Set(SESSION_MESSAGE_RECEIPT_STATUSES);
58
+ const POLICIES = new Set(SESSION_MESSAGE_POLICIES);
59
+ const NAME_PATTERN = /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/u;
60
+ const ID_PATTERN = /^[^\u0000-\u001f\u007f]{1,256}$/u;
61
+ const MESSAGE_ID_PATTERN = /^[a-zA-Z0-9._:-]{1,128}$/u;
62
+
63
+ function fabricError(code, message, details = {}, cause = undefined) {
64
+ const error = new Error(message, cause ? { cause } : undefined);
65
+ error.name = "SessionMessageFabricError";
66
+ error.code = code;
67
+ Object.assign(error, details);
68
+ return error;
69
+ }
70
+
71
+ function stableValue(value) {
72
+ if (Array.isArray(value)) return value.map(stableValue);
73
+ if (!value || typeof value !== "object") return value;
74
+ return Object.fromEntries(
75
+ Object.keys(value)
76
+ .sort()
77
+ .map((key) => [key, stableValue(value[key])]),
78
+ );
79
+ }
80
+
81
+ function digestValue(value) {
82
+ return `sha256:${createHash("sha256")
83
+ .update(JSON.stringify(stableValue(value)))
84
+ .digest("hex")}`;
85
+ }
86
+
87
+ function clone(value) {
88
+ return JSON.parse(JSON.stringify(value));
89
+ }
90
+
91
+ function canonicalName(value) {
92
+ const name = String(value || "")
93
+ .trim()
94
+ .replace(/^@/u, "")
95
+ .toLowerCase();
96
+ if (!NAME_PATTERN.test(name)) {
97
+ throw fabricError(
98
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.INVALID_ARGUMENT,
99
+ "Session message name must be 1-64 lowercase ASCII letters, digits, '.', '_' or '-'",
100
+ );
101
+ }
102
+ return name;
103
+ }
104
+
105
+ function boundedId(value, label) {
106
+ const id = String(value || "").trim();
107
+ if (!ID_PATTERN.test(id)) {
108
+ throw fabricError(
109
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.INVALID_ARGUMENT,
110
+ `${label} must be a non-empty bounded string without control characters`,
111
+ );
112
+ }
113
+ return id;
114
+ }
115
+
116
+ function messageId(value, create = randomUUID) {
117
+ const id = value == null ? create() : String(value).trim();
118
+ if (!MESSAGE_ID_PATTERN.test(id)) {
119
+ throw fabricError(
120
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.INVALID_ARGUMENT,
121
+ "messageId must contain 1-128 safe identifier characters",
122
+ );
123
+ }
124
+ return id;
125
+ }
126
+
127
+ function policyValue(value) {
128
+ const policy = String(value || "accept")
129
+ .trim()
130
+ .toLowerCase();
131
+ if (!POLICIES.has(policy)) {
132
+ throw fabricError(
133
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.INVALID_ARGUMENT,
134
+ "Inbound policy must be accept, hold or refuse",
135
+ );
136
+ }
137
+ return policy;
138
+ }
139
+
140
+ function positiveInteger(value, fallback, maximum = Number.MAX_SAFE_INTEGER) {
141
+ const number = Number(value);
142
+ if (!Number.isSafeInteger(number) || number <= 0) return fallback;
143
+ return Math.min(number, maximum);
144
+ }
145
+
146
+ function emptyState(now) {
147
+ const state = {
148
+ schema: SESSION_MESSAGE_FABRIC_SCHEMA,
149
+ version: 1,
150
+ revision: 0,
151
+ updatedAt: now,
152
+ endpoints: [],
153
+ channels: [],
154
+ messages: [],
155
+ receipts: [],
156
+ };
157
+ return { ...state, digest: digestValue(state) };
158
+ }
159
+
160
+ function stateWithoutDigest(state) {
161
+ const { digest: _digest, ...rest } = state;
162
+ return rest;
163
+ }
164
+
165
+ function authorityId(machineId, sessionId) {
166
+ return `sha256:${createHash("sha256")
167
+ .update(`${machineId}\0${sessionId}`)
168
+ .digest("hex")}`;
169
+ }
170
+
171
+ function channelId(senderAuthority, recipientAuthority, recipientEpoch) {
172
+ return digestValue({ senderAuthority, recipientAuthority, recipientEpoch });
173
+ }
174
+
175
+ function endpointAddress(endpoint) {
176
+ return `cc-session://${encodeURIComponent(endpoint.machineId)}/@${endpoint.name}?epoch=${encodeURIComponent(endpoint.epoch)}`;
177
+ }
178
+
179
+ function parseAddress(value) {
180
+ const raw = String(value || "").trim();
181
+ if (!raw.startsWith("cc-session://")) return null;
182
+ let parsed;
183
+ try {
184
+ parsed = new URL(raw);
185
+ } catch (cause) {
186
+ throw fabricError(
187
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.INVALID_ARGUMENT,
188
+ "Session message address is malformed",
189
+ {},
190
+ cause,
191
+ );
192
+ }
193
+ const name = decodeURIComponent(parsed.pathname.replace(/^\/@?/u, ""));
194
+ const machineId = decodeURIComponent(parsed.hostname);
195
+ const epoch = parsed.searchParams.get("epoch");
196
+ if (!machineId || !epoch) {
197
+ throw fabricError(
198
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.INVALID_ARGUMENT,
199
+ "Session message address must bind machine and endpoint epoch",
200
+ );
201
+ }
202
+ return {
203
+ name: canonicalName(name),
204
+ machineId: boundedId(machineId, "address machineId"),
205
+ epoch: boundedId(epoch, "address epoch"),
206
+ };
207
+ }
208
+
209
+ function jsonBody(body) {
210
+ let serialized;
211
+ try {
212
+ serialized = JSON.stringify(body);
213
+ } catch (cause) {
214
+ throw fabricError(
215
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.INVALID_ARGUMENT,
216
+ "Session message body must be JSON-serializable",
217
+ {},
218
+ cause,
219
+ );
220
+ }
221
+ if (serialized === undefined) {
222
+ throw fabricError(
223
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.INVALID_ARGUMENT,
224
+ "Session message body must be JSON-serializable",
225
+ );
226
+ }
227
+ return { value: JSON.parse(serialized), serialized };
228
+ }
229
+
230
+ function receiptFor(state, senderAuthority, id) {
231
+ return state.receipts.find(
232
+ (receipt) =>
233
+ receipt.senderAuthority === senderAuthority && receipt.messageId === id,
234
+ );
235
+ }
236
+
237
+ function endpointFor(state, selector, { activeOnly = true } = {}) {
238
+ const raw = String(selector || "").trim();
239
+ const address = parseAddress(raw);
240
+ const name = raw.startsWith("@") ? canonicalName(raw) : null;
241
+ return state.endpoints.find((endpoint) => {
242
+ if (activeOnly && endpoint.active !== true) return false;
243
+ if (address) {
244
+ return (
245
+ endpoint.name === address.name &&
246
+ endpoint.machineId === address.machineId &&
247
+ endpoint.epoch === address.epoch
248
+ );
249
+ }
250
+ return (
251
+ endpoint.authorityId === raw ||
252
+ endpoint.sessionId === raw ||
253
+ (name != null && endpoint.name === name) ||
254
+ endpoint.name === raw.toLowerCase()
255
+ );
256
+ });
257
+ }
258
+
259
+ function requireEndpoint(state, selector) {
260
+ const endpoint = endpointFor(state, selector);
261
+ if (!endpoint) {
262
+ throw fabricError(
263
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.ENDPOINT_NOT_FOUND,
264
+ `Session message endpoint was not found: ${String(selector || "")}`,
265
+ );
266
+ }
267
+ return endpoint;
268
+ }
269
+
270
+ function channelFor(state, sender, recipient) {
271
+ const id = channelId(
272
+ sender.authorityId,
273
+ recipient.authorityId,
274
+ recipient.epoch,
275
+ );
276
+ let channel = state.channels.find((entry) => entry.id === id);
277
+ if (!channel) {
278
+ channel = {
279
+ id,
280
+ senderAuthority: sender.authorityId,
281
+ recipientAuthority: recipient.authorityId,
282
+ recipientEpoch: recipient.epoch,
283
+ nextExpected: 1,
284
+ };
285
+ state.channels.push(channel);
286
+ }
287
+ return channel;
288
+ }
289
+
290
+ function pendingForRecipient(state, recipient) {
291
+ return state.messages.filter(
292
+ (message) =>
293
+ message.recipientAuthority === recipient.authorityId &&
294
+ message.recipientEpoch === recipient.epoch &&
295
+ PENDING_STATUSES.has(message.status) &&
296
+ message.acknowledgedAt == null,
297
+ );
298
+ }
299
+
300
+ function updateReceipt(state, message, status, reason, now) {
301
+ let receipt = receiptFor(state, message.senderAuthority, message.messageId);
302
+ if (!receipt) {
303
+ receipt = {
304
+ messageId: message.messageId,
305
+ senderAuthority: message.senderAuthority,
306
+ recipientAuthority: message.recipientAuthority,
307
+ recipientEpoch: message.recipientEpoch,
308
+ sequence: message.sequence,
309
+ payloadDigest: message.payloadDigest,
310
+ status,
311
+ reason,
312
+ createdAt: now,
313
+ updatedAt: now,
314
+ idleNotifiedAt: null,
315
+ readAt: null,
316
+ };
317
+ state.receipts.push(receipt);
318
+ } else {
319
+ receipt.status = status;
320
+ receipt.reason = reason;
321
+ receipt.updatedAt = now;
322
+ }
323
+ message.status = status;
324
+ message.reason = reason;
325
+ message.updatedAt = now;
326
+ return receipt;
327
+ }
328
+
329
+ function trimReceiptHistory(state, limit) {
330
+ if (state.receipts.length <= limit) return false;
331
+ const protectedIds = new Set(
332
+ state.messages.map(
333
+ (message) => `${message.senderAuthority}\0${message.messageId}`,
334
+ ),
335
+ );
336
+ const removable = state.receipts
337
+ .filter(
338
+ (receipt) =>
339
+ !protectedIds.has(`${receipt.senderAuthority}\0${receipt.messageId}`),
340
+ )
341
+ .sort((left, right) => left.updatedAt - right.updatedAt);
342
+ let changed = false;
343
+ while (state.receipts.length > limit && removable.length > 0) {
344
+ const target = removable.shift();
345
+ const index = state.receipts.indexOf(target);
346
+ if (index >= 0) {
347
+ state.receipts.splice(index, 1);
348
+ changed = true;
349
+ }
350
+ }
351
+ return changed;
352
+ }
353
+
354
+ function compactSettledMessages(state) {
355
+ const before = state.messages.length;
356
+ state.messages = state.messages.filter((message) => {
357
+ if (!PENDING_STATUSES.has(message.status)) return false;
358
+ if (message.acknowledgedAt == null) return true;
359
+ if (message.notifyWhenIdle !== true) return false;
360
+ const receipt = receiptFor(
361
+ state,
362
+ message.senderAuthority,
363
+ message.messageId,
364
+ );
365
+ return receipt?.idleNotifiedAt == null;
366
+ });
367
+ return state.messages.length !== before;
368
+ }
369
+
370
+ function applyExpectedMessage(state, channel, message, recipient, now) {
371
+ if (recipient.policy === "hold") {
372
+ updateReceipt(state, message, "held", "policy_hold", now);
373
+ return false;
374
+ }
375
+ if (recipient.policy === "refuse") {
376
+ updateReceipt(state, message, "refused", "policy_refuse", now);
377
+ } else {
378
+ const receipt = updateReceipt(state, message, "delivered", null, now);
379
+ message.deliveredAt = now;
380
+ if (message.notifyWhenIdle && recipient.idle === true) {
381
+ receipt.idleNotifiedAt ??= now;
382
+ }
383
+ }
384
+ channel.nextExpected += 1;
385
+ return true;
386
+ }
387
+
388
+ function promoteChannel(state, channel, now) {
389
+ const recipient = state.endpoints.find(
390
+ (entry) =>
391
+ entry.authorityId === channel.recipientAuthority &&
392
+ entry.epoch === channel.recipientEpoch &&
393
+ entry.active === true,
394
+ );
395
+ if (!recipient) return false;
396
+ let changed = false;
397
+ for (;;) {
398
+ const message = state.messages.find(
399
+ (entry) =>
400
+ entry.channelId === channel.id &&
401
+ entry.sequence === channel.nextExpected &&
402
+ entry.status === "held",
403
+ );
404
+ if (!message) break;
405
+ if (message.expiresAt <= now) {
406
+ updateReceipt(state, message, "expired", "ttl_expired", now);
407
+ channel.nextExpected += 1;
408
+ changed = true;
409
+ continue;
410
+ }
411
+ if (!applyExpectedMessage(state, channel, message, recipient, now)) break;
412
+ changed = true;
413
+ }
414
+ return changed;
415
+ }
416
+
417
+ function sweepExpired(state, now) {
418
+ let changed = false;
419
+ for (const message of state.messages) {
420
+ if (
421
+ PENDING_STATUSES.has(message.status) &&
422
+ message.acknowledgedAt == null &&
423
+ message.expiresAt <= now
424
+ ) {
425
+ updateReceipt(state, message, "expired", "ttl_expired", now);
426
+ const channel = state.channels.find(
427
+ (entry) => entry.id === message.channelId,
428
+ );
429
+ if (channel && channel.nextExpected === message.sequence) {
430
+ channel.nextExpected += 1;
431
+ }
432
+ changed = true;
433
+ }
434
+ }
435
+ for (const channel of state.channels) {
436
+ changed = promoteChannel(state, channel, now) || changed;
437
+ }
438
+ return changed;
439
+ }
440
+
441
+ function validateState(state) {
442
+ const fail = (message) => {
443
+ throw fabricError(
444
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.STATE_CORRUPT,
445
+ `Session message state is corrupt: ${message}`,
446
+ );
447
+ };
448
+ if (
449
+ !state ||
450
+ state.schema !== SESSION_MESSAGE_FABRIC_SCHEMA ||
451
+ state.version !== 1 ||
452
+ !Number.isSafeInteger(state.revision) ||
453
+ state.revision < 0 ||
454
+ !Array.isArray(state.endpoints) ||
455
+ !Array.isArray(state.channels) ||
456
+ !Array.isArray(state.messages) ||
457
+ !Array.isArray(state.receipts) ||
458
+ typeof state.digest !== "string"
459
+ ) {
460
+ fail("invalid envelope");
461
+ }
462
+ if (digestValue(stateWithoutDigest(state)) !== state.digest) {
463
+ fail("digest mismatch");
464
+ }
465
+ const authorityIds = new Set();
466
+ const activeNames = new Set();
467
+ for (const endpoint of state.endpoints) {
468
+ if (
469
+ typeof endpoint?.authorityId !== "string" ||
470
+ authorityIds.has(endpoint.authorityId) ||
471
+ !NAME_PATTERN.test(endpoint.name) ||
472
+ !POLICIES.has(endpoint.policy) ||
473
+ typeof endpoint.epoch !== "string" ||
474
+ typeof endpoint.active !== "boolean" ||
475
+ typeof endpoint.online !== "boolean" ||
476
+ typeof endpoint.idle !== "boolean"
477
+ ) {
478
+ fail("invalid endpoint");
479
+ }
480
+ authorityIds.add(endpoint.authorityId);
481
+ if (endpoint.active) {
482
+ if (activeNames.has(endpoint.name)) fail("duplicate active name");
483
+ activeNames.add(endpoint.name);
484
+ }
485
+ }
486
+ const channelIds = new Set();
487
+ for (const channel of state.channels) {
488
+ if (
489
+ typeof channel?.id !== "string" ||
490
+ channelIds.has(channel.id) ||
491
+ !Number.isSafeInteger(channel.nextExpected) ||
492
+ channel.nextExpected < 1
493
+ ) {
494
+ fail("invalid channel");
495
+ }
496
+ channelIds.add(channel.id);
497
+ }
498
+ const messageKeys = new Set();
499
+ for (const message of state.messages) {
500
+ const key = `${message?.senderAuthority}\0${message?.messageId}`;
501
+ if (
502
+ messageKeys.has(key) ||
503
+ !MESSAGE_ID_PATTERN.test(message?.messageId || "") ||
504
+ !Number.isSafeInteger(message?.sequence) ||
505
+ message.sequence < 1 ||
506
+ !RECEIPT_STATUSES.has(message?.status) ||
507
+ !channelIds.has(message?.channelId) ||
508
+ typeof message?.payloadDigest !== "string"
509
+ ) {
510
+ fail("invalid message");
511
+ }
512
+ messageKeys.add(key);
513
+ }
514
+ for (const receipt of state.receipts) {
515
+ if (
516
+ !MESSAGE_ID_PATTERN.test(receipt?.messageId || "") ||
517
+ !RECEIPT_STATUSES.has(receipt?.status) ||
518
+ typeof receipt?.payloadDigest !== "string"
519
+ ) {
520
+ fail("invalid receipt");
521
+ }
522
+ }
523
+ return state;
524
+ }
525
+
526
+ export function defaultSessionMessageFabricPath() {
527
+ return path.join(getHomeDir(), "session-message-fabric", "state.json");
528
+ }
529
+
530
+ export class SessionMessageFabric {
531
+ constructor({
532
+ statePath = defaultSessionMessageFabricPath(),
533
+ now = () => Date.now(),
534
+ createId = randomUUID,
535
+ lock = withFileLock,
536
+ lockTimeoutMs = 10_000,
537
+ maxMessageBytes = SESSION_MESSAGE_FABRIC_LIMITS.maxMessageBytes,
538
+ maxPendingPerRecipient = SESSION_MESSAGE_FABRIC_LIMITS.maxPendingPerRecipient,
539
+ maxReceiptHistory = SESSION_MESSAGE_FABRIC_LIMITS.maxReceiptHistory,
540
+ defaultTtlMs = SESSION_MESSAGE_FABRIC_LIMITS.defaultTtlMs,
541
+ maxTtlMs = SESSION_MESSAGE_FABRIC_LIMITS.maxTtlMs,
542
+ } = {}) {
543
+ this.statePath = path.resolve(statePath);
544
+ this._secureDefaultDirectory =
545
+ this.statePath === path.resolve(defaultSessionMessageFabricPath());
546
+ this._now = now;
547
+ this._createId = createId;
548
+ this._lock = lock;
549
+ this._lockTimeoutMs = lockTimeoutMs;
550
+ this._limits = {
551
+ maxMessageBytes: positiveInteger(
552
+ maxMessageBytes,
553
+ SESSION_MESSAGE_FABRIC_LIMITS.maxMessageBytes,
554
+ ),
555
+ maxPendingPerRecipient: positiveInteger(
556
+ maxPendingPerRecipient,
557
+ SESSION_MESSAGE_FABRIC_LIMITS.maxPendingPerRecipient,
558
+ ),
559
+ maxReceiptHistory: positiveInteger(
560
+ maxReceiptHistory,
561
+ SESSION_MESSAGE_FABRIC_LIMITS.maxReceiptHistory,
562
+ ),
563
+ defaultTtlMs: positiveInteger(
564
+ defaultTtlMs,
565
+ SESSION_MESSAGE_FABRIC_LIMITS.defaultTtlMs,
566
+ ),
567
+ maxTtlMs: positiveInteger(
568
+ maxTtlMs,
569
+ SESSION_MESSAGE_FABRIC_LIMITS.maxTtlMs,
570
+ ),
571
+ };
572
+ }
573
+
574
+ _ensureDirectory() {
575
+ const directory = path.dirname(this.statePath);
576
+ if (this._secureDefaultDirectory) {
577
+ ensureDir(directory);
578
+ return;
579
+ }
580
+ fs.mkdirSync(directory, {
581
+ recursive: true,
582
+ mode: 0o700,
583
+ });
584
+ }
585
+
586
+ _readState() {
587
+ try {
588
+ return validateState(JSON.parse(fs.readFileSync(this.statePath, "utf8")));
589
+ } catch (cause) {
590
+ if (cause?.code === "ENOENT") return emptyState(this._now());
591
+ if (cause?.code === SESSION_MESSAGE_FABRIC_ERROR_CODES.STATE_CORRUPT) {
592
+ throw cause;
593
+ }
594
+ throw fabricError(
595
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.STATE_CORRUPT,
596
+ "Could not read session message state",
597
+ {},
598
+ cause,
599
+ );
600
+ }
601
+ }
602
+
603
+ _writeState(state) {
604
+ const directory = path.dirname(this.statePath);
605
+ const temporary = path.join(
606
+ directory,
607
+ `.${path.basename(this.statePath)}.${process.pid}.${this._createId()}.tmp`,
608
+ );
609
+ let descriptor = null;
610
+ try {
611
+ descriptor = fs.openSync(temporary, "wx", 0o600);
612
+ fs.writeFileSync(
613
+ descriptor,
614
+ `${JSON.stringify(state, null, 2)}\n`,
615
+ "utf8",
616
+ );
617
+ fs.fsyncSync(descriptor);
618
+ fs.closeSync(descriptor);
619
+ descriptor = null;
620
+ fs.renameSync(temporary, this.statePath);
621
+ if (process.platform !== "win32") {
622
+ const directoryDescriptor = fs.openSync(directory, "r");
623
+ try {
624
+ fs.fsyncSync(directoryDescriptor);
625
+ } finally {
626
+ fs.closeSync(directoryDescriptor);
627
+ }
628
+ }
629
+ } catch (cause) {
630
+ if (descriptor != null) {
631
+ try {
632
+ fs.closeSync(descriptor);
633
+ } catch {
634
+ // Preserve the original write failure.
635
+ }
636
+ }
637
+ try {
638
+ fs.unlinkSync(temporary);
639
+ } catch {
640
+ // The rename may already have committed, or no temporary was created.
641
+ }
642
+ throw fabricError(
643
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.STATE_UNAVAILABLE,
644
+ "Could not atomically persist session message state",
645
+ {},
646
+ cause,
647
+ );
648
+ }
649
+ }
650
+
651
+ _transaction(mutator, { expectedRevision = null } = {}) {
652
+ this._ensureDirectory();
653
+ try {
654
+ return this._lock(
655
+ this.statePath,
656
+ () => {
657
+ const state = this._readState();
658
+ if (
659
+ expectedRevision != null &&
660
+ Number(expectedRevision) !== state.revision
661
+ ) {
662
+ throw fabricError(
663
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.STALE_REVISION,
664
+ "Session message fabric revision changed",
665
+ { currentRevision: state.revision },
666
+ );
667
+ }
668
+ const now = this._now();
669
+ const swept = sweepExpired(state, now);
670
+ const outcome = mutator(state, now) || { value: undefined };
671
+ const compacted = compactSettledMessages(state);
672
+ const trimmed = trimReceiptHistory(
673
+ state,
674
+ this._limits.maxReceiptHistory,
675
+ );
676
+ if (swept || compacted || trimmed || outcome.changed === true) {
677
+ state.revision += 1;
678
+ state.updatedAt = now;
679
+ state.digest = digestValue(stateWithoutDigest(state));
680
+ validateState(state);
681
+ this._writeState(state);
682
+ }
683
+ return outcome.value;
684
+ },
685
+ {
686
+ failIfUnavailable: true,
687
+ timeoutMs: this._lockTimeoutMs,
688
+ retryMs: 5,
689
+ maxRetryMs: 50,
690
+ retryJitterMs: 10,
691
+ yieldAfterReleaseMs: 1,
692
+ },
693
+ );
694
+ } catch (cause) {
695
+ if (cause?.code?.startsWith?.("SESSION_MESSAGE_")) throw cause;
696
+ throw fabricError(
697
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.STATE_UNAVAILABLE,
698
+ "Session message state transaction failed",
699
+ {},
700
+ cause,
701
+ );
702
+ }
703
+ }
704
+
705
+ register({
706
+ sessionId,
707
+ name,
708
+ machineId = "local",
709
+ policy = "accept",
710
+ idle = true,
711
+ } = {}) {
712
+ const safeSessionId = boundedId(sessionId, "sessionId");
713
+ const safeMachineId = boundedId(machineId, "machineId").toLowerCase();
714
+ const safeName = canonicalName(name || safeSessionId);
715
+ const safePolicy = policyValue(policy);
716
+ const id = authorityId(safeMachineId, safeSessionId);
717
+ return this._transaction((state, now) => {
718
+ const conflict = state.endpoints.find(
719
+ (endpoint) =>
720
+ endpoint.active &&
721
+ endpoint.name === safeName &&
722
+ endpoint.authorityId !== id,
723
+ );
724
+ if (conflict) {
725
+ throw fabricError(
726
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.NAME_CONFLICT,
727
+ `Session message name is already active: @${safeName}`,
728
+ );
729
+ }
730
+ let endpoint = state.endpoints.find((entry) => entry.authorityId === id);
731
+ if (endpoint) {
732
+ if (!endpoint.active) {
733
+ endpoint.epoch = this._createId();
734
+ endpoint.registeredAt = now;
735
+ }
736
+ endpoint.sessionId = safeSessionId;
737
+ endpoint.machineId = safeMachineId;
738
+ endpoint.name = safeName;
739
+ endpoint.policy = safePolicy;
740
+ endpoint.idle = idle === true;
741
+ endpoint.online = true;
742
+ endpoint.active = true;
743
+ endpoint.updatedAt = now;
744
+ } else {
745
+ endpoint = {
746
+ authorityId: id,
747
+ sessionId: safeSessionId,
748
+ machineId: safeMachineId,
749
+ name: safeName,
750
+ epoch: this._createId(),
751
+ policy: safePolicy,
752
+ idle: idle === true,
753
+ online: true,
754
+ active: true,
755
+ registeredAt: now,
756
+ updatedAt: now,
757
+ };
758
+ state.endpoints.push(endpoint);
759
+ }
760
+ return {
761
+ changed: true,
762
+ value: { ...clone(endpoint), address: endpointAddress(endpoint) },
763
+ };
764
+ });
765
+ }
766
+
767
+ unregister(selector) {
768
+ return this._transaction((state, now) => {
769
+ const endpoint = requireEndpoint(state, selector);
770
+ endpoint.active = false;
771
+ endpoint.online = false;
772
+ endpoint.idle = false;
773
+ endpoint.updatedAt = now;
774
+ for (const message of pendingForRecipient(state, endpoint)) {
775
+ updateReceipt(state, message, "refused", "recipient_unregistered", now);
776
+ }
777
+ return { changed: true, value: true };
778
+ });
779
+ }
780
+
781
+ reconnect(selector) {
782
+ return this._transaction((state, now) => {
783
+ const endpoint = requireEndpoint(state, selector);
784
+ endpoint.online = true;
785
+ endpoint.updatedAt = now;
786
+ return {
787
+ changed: true,
788
+ value: { ...clone(endpoint), address: endpointAddress(endpoint) },
789
+ };
790
+ });
791
+ }
792
+
793
+ disconnect(selector) {
794
+ return this._transaction((state, now) => {
795
+ const endpoint = requireEndpoint(state, selector);
796
+ endpoint.online = false;
797
+ endpoint.updatedAt = now;
798
+ return { changed: true, value: true };
799
+ });
800
+ }
801
+
802
+ setPolicy(selector, policy, options = {}) {
803
+ const safePolicy = policyValue(policy);
804
+ return this._transaction((state, now) => {
805
+ const endpoint = requireEndpoint(state, selector);
806
+ endpoint.policy = safePolicy;
807
+ endpoint.updatedAt = now;
808
+ for (const channel of state.channels.filter(
809
+ (entry) =>
810
+ entry.recipientAuthority === endpoint.authorityId &&
811
+ entry.recipientEpoch === endpoint.epoch,
812
+ )) {
813
+ promoteChannel(state, channel, now);
814
+ }
815
+ return {
816
+ changed: true,
817
+ value: { ...clone(endpoint), address: endpointAddress(endpoint) },
818
+ };
819
+ }, options);
820
+ }
821
+
822
+ setIdle(selector, idle, options = {}) {
823
+ return this._transaction((state, now) => {
824
+ const endpoint = requireEndpoint(state, selector);
825
+ const becameIdle = endpoint.idle !== true && idle === true;
826
+ endpoint.idle = idle === true;
827
+ endpoint.updatedAt = now;
828
+ let notifications = 0;
829
+ if (becameIdle) {
830
+ for (const message of state.messages) {
831
+ if (
832
+ message.recipientAuthority !== endpoint.authorityId ||
833
+ message.recipientEpoch !== endpoint.epoch ||
834
+ message.notifyWhenIdle !== true ||
835
+ message.status !== "delivered"
836
+ ) {
837
+ continue;
838
+ }
839
+ const receipt = receiptFor(
840
+ state,
841
+ message.senderAuthority,
842
+ message.messageId,
843
+ );
844
+ if (receipt && receipt.idleNotifiedAt == null) {
845
+ receipt.idleNotifiedAt = now;
846
+ receipt.updatedAt = now;
847
+ notifications += 1;
848
+ }
849
+ }
850
+ }
851
+ return {
852
+ changed: true,
853
+ value: { idle: endpoint.idle, notifications },
854
+ };
855
+ }, options);
856
+ }
857
+
858
+ send({
859
+ from,
860
+ to,
861
+ body,
862
+ subject = null,
863
+ messageId: requestedMessageId = null,
864
+ sequence = null,
865
+ ttlMs = this._limits.defaultTtlMs,
866
+ notifyWhenIdle = false,
867
+ } = {}) {
868
+ const id = messageId(requestedMessageId, this._createId);
869
+ const cleanSubject = subject == null ? null : String(subject).slice(0, 512);
870
+ const cleanBody = jsonBody(body);
871
+ const messageBytes = Buffer.byteLength(
872
+ JSON.stringify({ subject: cleanSubject, body: cleanBody.value }),
873
+ "utf8",
874
+ );
875
+ if (messageBytes > this._limits.maxMessageBytes) {
876
+ throw fabricError(
877
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.MESSAGE_TOO_LARGE,
878
+ `Session message is ${messageBytes} bytes; limit is ${this._limits.maxMessageBytes}`,
879
+ {
880
+ messageBytes,
881
+ maxMessageBytes: this._limits.maxMessageBytes,
882
+ },
883
+ );
884
+ }
885
+ const safeTtlMs = positiveInteger(
886
+ ttlMs,
887
+ this._limits.defaultTtlMs,
888
+ this._limits.maxTtlMs,
889
+ );
890
+ const requestedSequence =
891
+ sequence == null ? null : positiveInteger(sequence, null);
892
+ if (sequence != null && requestedSequence == null) {
893
+ throw fabricError(
894
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.INVALID_ARGUMENT,
895
+ "Session message sequence must be a positive integer",
896
+ );
897
+ }
898
+ return this._transaction((state, now) => {
899
+ const sender = requireEndpoint(state, from);
900
+ const recipient = endpointFor(state, to);
901
+ const payloadDigest = digestValue({
902
+ body: cleanBody.value,
903
+ subject: cleanSubject,
904
+ to: String(to),
905
+ });
906
+ const duplicate = receiptFor(state, sender.authorityId, id);
907
+ if (duplicate) {
908
+ if (duplicate.payloadDigest !== payloadDigest) {
909
+ throw fabricError(
910
+ SESSION_MESSAGE_FABRIC_ERROR_CODES.MESSAGE_ID_CONFLICT,
911
+ "messageId was already used with a different payload",
912
+ );
913
+ }
914
+ return { value: clone(duplicate) };
915
+ }
916
+ if (!recipient) {
917
+ const receipt = {
918
+ messageId: id,
919
+ senderAuthority: sender.authorityId,
920
+ recipientAuthority: null,
921
+ recipientEpoch: null,
922
+ sequence: requestedSequence,
923
+ payloadDigest,
924
+ status: "refused",
925
+ reason: "unknown_recipient",
926
+ createdAt: now,
927
+ updatedAt: now,
928
+ idleNotifiedAt: null,
929
+ readAt: null,
930
+ };
931
+ state.receipts.push(receipt);
932
+ return { changed: true, value: clone(receipt) };
933
+ }
934
+ const channel = channelFor(state, sender, recipient);
935
+ const pendingSequences = state.messages
936
+ .filter(
937
+ (message) =>
938
+ message.channelId === channel.id &&
939
+ PENDING_STATUSES.has(message.status),
940
+ )
941
+ .map((message) => message.sequence);
942
+ const safeSequence =
943
+ requestedSequence ??
944
+ Math.max(
945
+ channel.nextExpected,
946
+ ...pendingSequences.map((value) => value + 1),
947
+ );
948
+ const message = {
949
+ messageId: id,
950
+ channelId: channel.id,
951
+ senderAuthority: sender.authorityId,
952
+ senderName: sender.name,
953
+ recipientAuthority: recipient.authorityId,
954
+ recipientName: recipient.name,
955
+ recipientEpoch: recipient.epoch,
956
+ sequence: safeSequence,
957
+ subject: cleanSubject,
958
+ body: cleanBody.value,
959
+ messageBytes,
960
+ payloadDigest,
961
+ status: "held",
962
+ reason: "out_of_order",
963
+ notifyWhenIdle: notifyWhenIdle === true,
964
+ createdAt: now,
965
+ updatedAt: now,
966
+ deliveredAt: null,
967
+ acknowledgedAt: null,
968
+ expiresAt: now + safeTtlMs,
969
+ };
970
+ if (
971
+ safeSequence < channel.nextExpected ||
972
+ state.messages.some(
973
+ (entry) =>
974
+ entry.channelId === channel.id &&
975
+ entry.sequence === safeSequence &&
976
+ PENDING_STATUSES.has(entry.status),
977
+ )
978
+ ) {
979
+ updateReceipt(
980
+ state,
981
+ message,
982
+ "refused",
983
+ safeSequence < channel.nextExpected
984
+ ? "stale_sequence"
985
+ : "duplicate_sequence",
986
+ now,
987
+ );
988
+ return {
989
+ changed: true,
990
+ value: clone(receiptFor(state, sender.authorityId, id)),
991
+ };
992
+ }
993
+ if (
994
+ recipient.policy !== "refuse" &&
995
+ pendingForRecipient(state, recipient).length >=
996
+ this._limits.maxPendingPerRecipient
997
+ ) {
998
+ updateReceipt(state, message, "full", "queue_capacity", now);
999
+ return {
1000
+ changed: true,
1001
+ value: clone(receiptFor(state, sender.authorityId, id)),
1002
+ };
1003
+ }
1004
+ state.messages.push(message);
1005
+ updateReceipt(state, message, "held", "out_of_order", now);
1006
+ if (safeSequence === channel.nextExpected) {
1007
+ applyExpectedMessage(state, channel, message, recipient, now);
1008
+ promoteChannel(state, channel, now);
1009
+ }
1010
+ return {
1011
+ changed: true,
1012
+ value: clone(receiptFor(state, sender.authorityId, id)),
1013
+ };
1014
+ });
1015
+ }
1016
+
1017
+ inbox(selector, { acknowledge = false } = {}) {
1018
+ return this._transaction((state, now) => {
1019
+ const endpoint = requireEndpoint(state, selector);
1020
+ const messages = pendingForRecipient(state, endpoint)
1021
+ .filter((message) => message.status === "delivered")
1022
+ .sort(
1023
+ (left, right) =>
1024
+ (left.senderAuthority === right.senderAuthority
1025
+ ? left.sequence - right.sequence
1026
+ : left.createdAt - right.createdAt) ||
1027
+ left.senderAuthority.localeCompare(right.senderAuthority),
1028
+ );
1029
+ if (acknowledge) {
1030
+ for (const message of messages) {
1031
+ message.acknowledgedAt = now;
1032
+ const receipt = receiptFor(
1033
+ state,
1034
+ message.senderAuthority,
1035
+ message.messageId,
1036
+ );
1037
+ if (receipt) {
1038
+ receipt.readAt = now;
1039
+ receipt.updatedAt = now;
1040
+ }
1041
+ }
1042
+ }
1043
+ return {
1044
+ changed: acknowledge && messages.length > 0,
1045
+ value: clone(messages),
1046
+ };
1047
+ });
1048
+ }
1049
+
1050
+ receipts(selector) {
1051
+ return this._transaction((state) => {
1052
+ const endpoint = requireEndpoint(state, selector);
1053
+ return {
1054
+ value: clone(
1055
+ state.receipts
1056
+ .filter(
1057
+ (receipt) => receipt.senderAuthority === endpoint.authorityId,
1058
+ )
1059
+ .sort(
1060
+ (left, right) =>
1061
+ left.createdAt - right.createdAt ||
1062
+ left.messageId.localeCompare(right.messageId),
1063
+ ),
1064
+ ),
1065
+ };
1066
+ });
1067
+ }
1068
+
1069
+ projection() {
1070
+ return this._transaction((state) => {
1071
+ const endpoints = state.endpoints
1072
+ .filter((endpoint) => endpoint.active)
1073
+ .map((endpoint) => {
1074
+ const pending = pendingForRecipient(state, endpoint);
1075
+ return {
1076
+ authorityId: endpoint.authorityId,
1077
+ sessionId: endpoint.sessionId,
1078
+ machineId: endpoint.machineId,
1079
+ name: endpoint.name,
1080
+ address: endpointAddress(endpoint),
1081
+ epoch: endpoint.epoch,
1082
+ policy: endpoint.policy,
1083
+ online: endpoint.online,
1084
+ idle: endpoint.idle,
1085
+ unread: pending.filter((message) => message.status === "delivered")
1086
+ .length,
1087
+ held: pending.filter((message) => message.status === "held").length,
1088
+ updatedAt: endpoint.updatedAt,
1089
+ };
1090
+ })
1091
+ .sort((left, right) => left.name.localeCompare(right.name));
1092
+ return {
1093
+ value: {
1094
+ schema: SESSION_MESSAGE_FABRIC_PROJECTION_SCHEMA,
1095
+ version: 1,
1096
+ authority: "cli",
1097
+ revision: state.revision,
1098
+ digest: state.digest,
1099
+ limits: clone(this._limits),
1100
+ endpoints,
1101
+ },
1102
+ };
1103
+ });
1104
+ }
1105
+ }