chainlesschain 0.162.198 → 0.162.200

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 (195) hide show
  1. package/bin/chainlesschain.js +14 -6
  2. package/package.json +4 -1
  3. package/scripts/command-lifecycle-report.mjs +94 -0
  4. package/src/assets/web-panel/assets/{AIOps-CSN-D2xb.js → AIOps-DcQ0bWKy.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-CBSMlGzv.js → ActionButton-GecwfFPt.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-3yr3Nt2t.js → Analytics-CD6iSt9v.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-1b-o3lMH.js → AppLayout-CDEtuEYf.js} +5 -5
  8. package/src/assets/web-panel/assets/{Artifacts-Dr8rJi17.js → Artifacts-DS3jAjeW.js} +1 -1
  9. package/src/assets/web-panel/assets/{Audit-2V689HC2.js → Audit-CcSJA048.js} +1 -1
  10. package/src/assets/web-panel/assets/{BackgroundAgents-DIxkNrD_.js → BackgroundAgents-DRo9Morb.js} +1 -1
  11. package/src/assets/web-panel/assets/{Backup-Pg6BcwrE.js → Backup-Vw5MrWft.js} +1 -1
  12. package/src/assets/web-panel/assets/{BaseInput-CLUpqyfH.js → BaseInput-DNHIu823.js} +1 -1
  13. package/src/assets/web-panel/assets/{Chat-qVS8rD4G.js → Chat-DvAsO8-b.js} +5 -5
  14. package/src/assets/web-panel/assets/{ChatBubbleRenderer-ti49X12Q.js → ChatBubbleRenderer-9aDuObay.js} +1 -1
  15. package/src/assets/web-panel/assets/{Checkbox-Bs9AaEay.js → Checkbox-CKCa7tvn.js} +1 -1
  16. package/src/assets/web-panel/assets/{Codegen-Dvc1mq8q.js → Codegen-CIFvZdaT.js} +1 -1
  17. package/src/assets/web-panel/assets/{Col-C4_biO7A.js → Col-C79yz7bK.js} +1 -1
  18. package/src/assets/web-panel/assets/{Community-al25XQUV.js → Community-DVvZskLJ.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compact-D-ZDB8M1.js → Compact-oaew61hV.js} +1 -1
  20. package/src/assets/web-panel/assets/{Compliance-6lIfDz1W.js → Compliance-DqA7wmRx.js} +1 -1
  21. package/src/assets/web-panel/assets/{Cowork-DdI48XMJ.js → Cowork-B-qGadxa.js} +2 -2
  22. package/src/assets/web-panel/assets/{Cron-DF5HZd2g.js → Cron-CUlCl4sS.js} +2 -2
  23. package/src/assets/web-panel/assets/{Crosschain-DuxeSvt3.js → Crosschain-CNOlr9DU.js} +1 -1
  24. package/src/assets/web-panel/assets/{DID-B9yHkUTd.js → DID-DJqFCT_3.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dashboard-Z5EuI3IM.js → Dashboard-CznGpphf.js} +2 -2
  26. package/src/assets/web-panel/assets/{Dropdown-vuRfO3dY.js → Dropdown-_cJw218E.js} +1 -1
  27. package/src/assets/web-panel/assets/{EmailListRenderer-QFLCc1uL.js → EmailListRenderer-Dr-1BZ9C.js} +1 -1
  28. package/src/assets/web-panel/assets/{FamilyGuardDashboard-BZ7EXkFJ.js → FamilyGuardDashboard-Dxf4xtSk.js} +1 -1
  29. package/src/assets/web-panel/assets/{Federation-DycIEg2s.js → Federation-DOiG3i_1.js} +1 -1
  30. package/src/assets/web-panel/assets/{FormItemContext-Ba0H_7yG.js → FormItemContext-BzbMcFB9.js} +1 -1
  31. package/src/assets/web-panel/assets/{GenericCardRenderer--JgSTK-C.js → GenericCardRenderer-VbBrYttP.js} +1 -1
  32. package/src/assets/web-panel/assets/{Git-DEvTbldE.js → Git-CLG8E6ED.js} +2 -2
  33. package/src/assets/web-panel/assets/{Governance-BJIljy18.js → Governance-LGJQfk_q.js} +1 -1
  34. package/src/assets/web-panel/assets/{Inference-CVyW3G3q.js → Inference-DlQKx9lF.js} +1 -1
  35. package/src/assets/web-panel/assets/{KnowledgeGraph-v8-borge.js → KnowledgeGraph-DU8vEouV.js} +1 -1
  36. package/src/assets/web-panel/assets/{Logs-BCdS7gGo.js → Logs-FZIQOdln.js} +2 -2
  37. package/src/assets/web-panel/assets/MarkdownRenderer-BJNWIrbw.js +1 -0
  38. package/src/assets/web-panel/assets/{Marketplace-AN6tGA2B.js → Marketplace-C_xd-zZI.js} +1 -1
  39. package/src/assets/web-panel/assets/{McpTools-DGViXASN.js → McpTools-q-YktphS.js} +5 -5
  40. package/src/assets/web-panel/assets/{Memory-Bpvmx9X4.js → Memory-Cq9OhDin.js} +2 -2
  41. package/src/assets/web-panel/assets/{MobileBridge-CTP-DOTC.js → MobileBridge-B03_R98T.js} +1 -1
  42. package/src/assets/web-panel/assets/{MobileProjects-C8qfktxn.js → MobileProjects-DZrk-qmF.js} +1 -1
  43. package/src/assets/web-panel/assets/{Mtc-ndYr0M1s.js → Mtc-BidCKW28.js} +2 -2
  44. package/src/assets/web-panel/assets/{MtcAudit-Ci0-YK7W.js → MtcAudit-r-iaMVM7.js} +2 -2
  45. package/src/assets/web-panel/assets/{Multisig-BehSTFW7.js → Multisig-CgTUFvff.js} +3 -3
  46. package/src/assets/web-panel/assets/{NLProgramming-B91_YBLv.js → NLProgramming-B7NvzfAV.js} +1 -1
  47. package/src/assets/web-panel/assets/{Notes-BQlJ0-gq.js → Notes-80CWNqJI.js} +3 -3
  48. package/src/assets/web-panel/assets/{NotificationSettings-CDgYMjYK.js → NotificationSettings-0H6xdiY3.js} +1 -1
  49. package/src/assets/web-panel/assets/{OrderTableRenderer-BJkSlKeH.js → OrderTableRenderer-CKP4AFWy.js} +1 -1
  50. package/src/assets/web-panel/assets/{Organization-B-b3cWCp.js → Organization-B8sxCV_h.js} +4 -4
  51. package/src/assets/web-panel/assets/{Overflow-CILUL4nL.js → Overflow-CW9JBBhQ.js} +1 -1
  52. package/src/assets/web-panel/assets/{P2P-BTCFBkF0.js → P2P-bdSQV7oZ.js} +2 -2
  53. package/src/assets/web-panel/assets/{PdhVaultBrowser-BAR6AUx5.js → PdhVaultBrowser-DQad42nW.js} +3 -3
  54. package/src/assets/web-panel/assets/{Permissions-RRf6HbWJ.js → Permissions-CueexxTR.js} +4 -4
  55. package/src/assets/web-panel/assets/{PersonalDataHub-BGTMiICa.js → PersonalDataHub-fqxflhR8.js} +6 -6
  56. package/src/assets/web-panel/assets/{Pipeline-945qY9YF.js → Pipeline-KFwwnIMC.js} +1 -1
  57. package/src/assets/web-panel/assets/{Privacy-DPZwI6lJ.js → Privacy-ClnD9buY.js} +1 -1
  58. package/src/assets/web-panel/assets/{ProjectInit-BPH2NRKc.js → ProjectInit-CZEz6sm3.js} +2 -2
  59. package/src/assets/web-panel/assets/{ProjectSettings-BZCRNCvb.js → ProjectSettings-B6NzpmOI.js} +2 -2
  60. package/src/assets/web-panel/assets/{Projects-BTI__tBt.js → Projects-DYyuwPCs.js} +1 -1
  61. package/src/assets/web-panel/assets/Providers-C3tz_jdh.js +1 -0
  62. package/src/assets/web-panel/assets/{QrScannerModal-N6rbqt1r.js → QrScannerModal-BzuOBbqB.js} +1 -1
  63. package/src/assets/web-panel/assets/{QuickAsk-DrwjPS7M.js → QuickAsk-CmNO9qSj.js} +1 -1
  64. package/src/assets/web-panel/assets/{Recommend-Cog9Wd99.js → Recommend-VJNj3_NF.js} +1 -1
  65. package/src/assets/web-panel/assets/{RemoteSession-DC9pynjM.js → RemoteSession-CrbdbSBF.js} +2 -2
  66. package/src/assets/web-panel/assets/{Reputation-D4th4gKS.js → Reputation-DTyMd45j.js} +1 -1
  67. package/src/assets/web-panel/assets/{Row-ogiWR6v9.js → Row-DHrQ-Icz.js} +1 -1
  68. package/src/assets/web-panel/assets/{RssFeed-uYlk-2Oj.js → RssFeed-DqWxY9c5.js} +3 -3
  69. package/src/assets/web-panel/assets/{Search-DYzzwvtM.js → Search-ctF1Ks3J.js} +1 -1
  70. package/src/assets/web-panel/assets/{Security-Da7YOyX6.js → Security-BrQz8uo5.js} +4 -4
  71. package/src/assets/web-panel/assets/{Services-CLI1VM1X.js → Services-D4scVFar.js} +2 -2
  72. package/src/assets/web-panel/assets/{Skeleton-DK77dzDC.js → Skeleton-oUScaOjv.js} +1 -1
  73. package/src/assets/web-panel/assets/{Skills-C_z7Lmhl.js → Skills-CL-y0eLt.js} +1 -1
  74. package/src/assets/web-panel/assets/{Sla-D6413Q8L.js → Sla-DQExhwXj.js} +1 -1
  75. package/src/assets/web-panel/assets/{SpeechSettings-C07ydbj2.js → SpeechSettings-iNp_I7kN.js} +1 -1
  76. package/src/assets/web-panel/assets/{SyncSettings-DGOm2W22.js → SyncSettings-CTDlMo0A.js} +2 -2
  77. package/src/assets/web-panel/assets/{Tasks-Cr_PVyLM.js → Tasks-4kb8l3sc.js} +1 -1
  78. package/src/assets/web-panel/assets/{Templates-BhL2mZ0t.js → Templates-BXcL9oXW.js} +1 -1
  79. package/src/assets/web-panel/assets/{Tenant-CPk5oyUr.js → Tenant-DLE8eS0D.js} +1 -1
  80. package/src/assets/web-panel/assets/{Terminal-ClhqUsqV.js → Terminal-DGpUZPPK.js} +2 -2
  81. package/src/assets/web-panel/assets/{TimelineRenderer-BCC7MV5a.js → TimelineRenderer-C1pWXGPC.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tokens-DD1Jt4oO.js → Tokens-BYfbfqhZ.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trigger-BCydFvN2.js → Trigger-K9YfLOCw.js} +1 -1
  84. package/src/assets/web-panel/assets/{Trust-DDHT1noa.js → Trust-Ctkq77Hy.js} +1 -1
  85. package/src/assets/web-panel/assets/{UkeySign-CWPZC7oI.js → UkeySign-C6_CSe9J.js} +1 -1
  86. package/src/assets/web-panel/assets/{VideoEditing-9Ea0nJR0.js → VideoEditing-Be3rpOWs.js} +1 -1
  87. package/src/assets/web-panel/assets/{Wallet-BjxhLxTf.js → Wallet-D0UIplxk.js} +4 -4
  88. package/src/assets/web-panel/assets/{WebAuthn-C6uCye67.js → WebAuthn-CF9bM9I3.js} +3 -3
  89. package/src/assets/web-panel/assets/{WorkflowEditor-d2g4reYo.js → WorkflowEditor-D0H1XVPY.js} +1 -1
  90. package/src/assets/web-panel/assets/{chat-frTc6ScX.js → chat-Cgzzbe0o.js} +1 -1
  91. package/src/assets/web-panel/assets/{colors-B5K6lG3v.js → colors-BwNxEVS0.js} +1 -1
  92. package/src/assets/web-panel/assets/{compact-item-Cdn46r8D.js → compact-item-D60Z4Hhs.js} +1 -1
  93. package/src/assets/web-panel/assets/{createContext-DU1jqVQi.js → createContext-MFk58XB0.js} +1 -1
  94. package/src/assets/web-panel/assets/devWarning-BJiw369i.js +1 -0
  95. package/src/assets/web-panel/assets/{hasIn-DdNH-aW_.js → hasIn-CRj2SvGO.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-DM5FmPZU.js → index--s45_1cb.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-D4l82Me1.js → index--tquQ2ZG.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-CQorP1Ng.js → index-1T1rhokn.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-CHmkkdfT.js → index-B-M82_rx.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CGiwfwB9.js → index-BE_el1Zs.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BwlIxAmX.js → index-BhRkR7IO.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Cj6P9Nst.js → index-Bo0y96-K.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-BoFak7le.js → index-CJgu5JUK.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-Dca_i7H8.js → index-CLWwdebO.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BVLL-mq_.js → index-CNAK5QG_.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-LaivuXyF.js → index-CQjJjPRD.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-t_OBzIP4.js → index-CTDbAfae.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-BcGwGIaR.js → index-CVI_9sRK.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-D23xGYjM.js → index-CY14oMQK.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-VDM2KKo9.js → index-CbiTJkZf.js} +1 -1
  111. package/src/assets/web-panel/assets/index-CiMouosn.js +1 -0
  112. package/src/assets/web-panel/assets/{index-32ajXmTb.js → index-CqWpxMqx.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-PjAS0sIZ.js → index-CvB8lYUR.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-7ehytj1K.js → index-D51XwtCY.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DtBCCkC4.js → index-D6HvK_1O.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-Be4OtL-N.js → index-D9jVIwLJ.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CUoNAmE8.js → index-DA4tE98u.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-cYaWV4Mq.js → index-DBwOtXw0.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-MM4Zz2OE.js → index-DKdNzTIb.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DIVyTmZ3.js → index-DRySdmvN.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CtRPi4YC.js → index-DWzyTev7.js} +1 -1
  122. package/src/assets/web-panel/assets/index-DXcKlwy7.js +1 -0
  123. package/src/assets/web-panel/assets/{index-kVO_MDm-.js → index-DYjs7jAP.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-D4miNkWv.js → index-DbROzMBp.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-yZRuZpwK.js → index-DhFtk5PR.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-BXKhhx9V.js → index-Di2AbMLg.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BC4eGkar.js → index-DvmocQsM.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-NmEXVjzt.js → index-Dw-o0zEF.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-DRzBBdQ0.js → index-DysPoP38.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-HjCvXHf5.js → index-LX9pJxm_.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-gg3QPC3i.js → index-OI_fXWx0.js} +3 -3
  132. package/src/assets/web-panel/assets/{index-mgU3Q-lN.js → index-eDTOX8Xj.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-D1qIC4GR.js → index-gWKDsTzF.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-DftZuyA7.js → index-q_GDUVAA.js} +1 -1
  135. package/src/assets/web-panel/assets/{initDefaultProps-DwLTRNdo.js → initDefaultProps-C61kZqEd.js} +1 -1
  136. package/src/assets/web-panel/assets/{motion-BchZDXnh.js → motion-DZnT5U1L.js} +1 -1
  137. package/src/assets/web-panel/assets/{move-e8M-zZFl.js → move-L8wzwbel.js} +1 -1
  138. package/src/assets/web-panel/assets/{mtc-parser-Ci40fQb3.js → mtc-parser-DLdYSejc.js} +1 -1
  139. package/src/assets/web-panel/assets/{omit-C7110_f2.js → omit-D53cbS59.js} +1 -1
  140. package/src/assets/web-panel/assets/{pickAttrs-skteMgnM.js → pickAttrs-Bbc5HPtu.js} +1 -1
  141. package/src/assets/web-panel/assets/{placementArrow-MGZ-lh2B.js → placementArrow-BkvmO860.js} +1 -1
  142. package/src/assets/web-panel/assets/{responsiveObserve-N4Sd7uy-.js → responsiveObserve-u2y1ryS1.js} +1 -1
  143. package/src/assets/web-panel/assets/{slide-C7HDGRaa.js → slide-BBqj7svU.js} +1 -1
  144. package/src/assets/web-panel/assets/{statusUtils-BhRexlgy.js → statusUtils-GN0mqu2d.js} +1 -1
  145. package/src/assets/web-panel/assets/{styleChecker-ChEoQkN2.js → styleChecker-BvwPKoBT.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFlexGapSupport-CDbG1s2Y.js → useFlexGapSupport-CCt8tnfY.js} +1 -1
  147. package/src/assets/web-panel/assets/{useFs-CUIx0tcJ.js → useFs-DXAxl2SV.js} +1 -1
  148. package/src/assets/web-panel/assets/{usePersonalDataHub-bkO7L7ec.js → usePersonalDataHub-DihlR2dj.js} +1 -1
  149. package/src/assets/web-panel/assets/{vnode-B_ZvABIG.js → vnode-D7BOX2gi.js} +1 -1
  150. package/src/assets/web-panel/assets/{zoom-D5shjpS8.js → zoom-CVbO-nL7.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/mcp.js +119 -0
  154. package/src/commands/session-show.js +8 -10
  155. package/src/commands/session.js +60 -52
  156. package/src/data/changelog.json +27 -1
  157. package/src/gateways/ws/session-protocol.js +14 -4
  158. package/src/gateways/ws/ws-agent-handler.js +31 -0
  159. package/src/harness/jsonl-session-store.js +1147 -130
  160. package/src/harness/mcp-client.js +44 -11
  161. package/src/harness/session-index.js +80 -80
  162. package/src/harness/session-list-index.js +147 -2
  163. package/src/lib/background-interaction-journal.js +2 -2
  164. package/src/lib/command-lifecycle-report.js +516 -0
  165. package/src/lib/mcp-stdio-environment.js +48 -0
  166. package/src/lib/mcp-stdio-executable-identity.js +552 -17
  167. package/src/lib/mcp-stdio-package-materialization.js +1424 -0
  168. package/src/lib/packer/native-update-state.js +979 -1
  169. package/src/lib/packer/pack-update-applier.js +1050 -125
  170. package/src/lib/packer/pkg-config-generator.js +1 -0
  171. package/src/lib/paths.js +53 -0
  172. package/src/lib/process-execution-broker/index.js +180 -37
  173. package/src/lib/process-execution-broker/platform-sandbox.js +291 -24
  174. package/src/lib/recent-session.js +17 -17
  175. package/src/lib/secure-file-identity.js +47 -0
  176. package/src/lib/secure-fs.js +21 -2
  177. package/src/lib/session-anti-rollback-anchor.js +766 -0
  178. package/src/lib/session-host-lease.js +492 -0
  179. package/src/lib/session-persistence-failure.js +109 -0
  180. package/src/lib/session-picker.js +26 -3
  181. package/src/lib/session-search.js +25 -11
  182. package/src/lib/side-effect-ledger-store.js +2 -2
  183. package/src/lib/skill-execution-authority.js +13 -9
  184. package/src/lib/turn-binding-store.js +2 -2
  185. package/src/repl/agent-repl.js +309 -39
  186. package/src/runtime/agent-core.js +88 -5
  187. package/src/runtime/cleanup-deadline.js +143 -0
  188. package/src/runtime/headless-runner.js +357 -139
  189. package/src/runtime/headless-stream.js +207 -28
  190. package/src/runtime/output-backpressure.js +53 -0
  191. package/src/assets/web-panel/assets/MarkdownRenderer-CU-JB2Ns.js +0 -1
  192. package/src/assets/web-panel/assets/Providers-DDCf_nIw.js +0 -1
  193. package/src/assets/web-panel/assets/devWarning-CP-jEUyw.js +0 -1
  194. package/src/assets/web-panel/assets/index-CzrCHrFy.js +0 -1
  195. package/src/assets/web-panel/assets/index-OJsGskZw.js +0 -1
@@ -0,0 +1,766 @@
1
+ /**
2
+ * Machine-local anti-rollback witness for canonical session generations.
3
+ *
4
+ * Session transcripts, metadata and tombstones all live below
5
+ * CHAINLESSCHAIN_HOME and can therefore be restored as one internally
6
+ * consistent but stale snapshot. This store deliberately lives in a separate
7
+ * OS user-state directory. Each session owns a small append-only witness log;
8
+ * a 256-way namespace index keeps exact/prefix discovery bounded at 10k+
9
+ * sessions without rewriting a global file for every transcript event.
10
+ *
11
+ * This is a cooperative same-user durability boundary, not tamper-proof
12
+ * hardware. Deleting both the configured home and this independent witness is
13
+ * outside its threat model.
14
+ */
15
+
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ import crypto from "node:crypto";
19
+ import * as cliPaths from "./paths.js";
20
+ import {
21
+ mutateSecurityStore,
22
+ readSecurityStore,
23
+ } from "./durable-security-store.js";
24
+ import { ensurePrivateDirectory } from "./secure-fs.js";
25
+ import {
26
+ isAffectedWindowsZeroDeviceStatRuntime,
27
+ samePathHandleStableFileIdentity,
28
+ withTrustedFileParentSync,
29
+ } from "./secure-file-identity.js";
30
+ import { withFileLock } from "./with-file-lock.js";
31
+ import { iterateFileLinesReverseSync } from "./file-lines.js";
32
+
33
+ export const SESSION_ANTI_ROLLBACK_DETECTED_CODE =
34
+ "CC_SESSION_ANTI_ROLLBACK_DETECTED";
35
+ export const SESSION_ANTI_ROLLBACK_UNAVAILABLE_CODE =
36
+ "CC_SESSION_ANTI_ROLLBACK_UNAVAILABLE";
37
+
38
+ const NAMESPACE_VERSION = 1;
39
+ const ANCHOR_SCHEMA = "chainlesschain.session-anti-rollback-anchor/v1";
40
+ const GENERATION_SCHEMA = "chainlesschain.session-generation-authority/v1";
41
+ const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
42
+ const GENERATION_ID_PATTERN = /^generation-[0-9a-f-]{32,36}$/;
43
+ const MAX_SESSION_ID_BYTES = 1024;
44
+ const LOCK_OPTIONS = Object.freeze({
45
+ timeoutMs: 30_000,
46
+ staleMs: 30_000,
47
+ failIfUnavailable: true,
48
+ });
49
+ const securedDirectories = new Map();
50
+ const testScopedDirectories = new Set();
51
+
52
+ function anchorError(code, message, cause = null, details = {}) {
53
+ const error = new Error(message, cause ? { cause } : undefined);
54
+ error.name = "SessionAntiRollbackAnchorError";
55
+ error.code = code;
56
+ Object.assign(error, details);
57
+ return error;
58
+ }
59
+
60
+ function unavailable(cause, filePath = null) {
61
+ if (cause?.code === SESSION_ANTI_ROLLBACK_DETECTED_CODE) return cause;
62
+ return anchorError(
63
+ SESSION_ANTI_ROLLBACK_UNAVAILABLE_CODE,
64
+ "Session anti-rollback witness is unavailable; canonical session access is denied",
65
+ cause,
66
+ filePath ? { filePath } : {},
67
+ );
68
+ }
69
+
70
+ function rollback(sessionId, message, current = null, candidate = null) {
71
+ return anchorError(
72
+ SESSION_ANTI_ROLLBACK_DETECTED_CODE,
73
+ `Session rollback detected for ${sessionId}: ${message}`,
74
+ null,
75
+ {
76
+ sessionId,
77
+ current: current ? structuredClone(current) : null,
78
+ candidate: candidate ? structuredClone(candidate) : null,
79
+ },
80
+ );
81
+ }
82
+
83
+ function sha256(value) {
84
+ return crypto.createHash("sha256").update(value, "utf8").digest("hex");
85
+ }
86
+
87
+ function canonicalSessionId(value) {
88
+ if (
89
+ typeof value !== "string" ||
90
+ value.length === 0 ||
91
+ value.includes("\0") ||
92
+ Buffer.byteLength(value, "utf8") > MAX_SESSION_ID_BYTES
93
+ ) {
94
+ throw new TypeError("session anti-rollback witness requires a bounded id");
95
+ }
96
+ return value;
97
+ }
98
+
99
+ function canonicalPathIdentity(value) {
100
+ const resolved = path.resolve(value);
101
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
102
+ }
103
+
104
+ function isWithin(parentPath, childPath) {
105
+ const relative = path.relative(parentPath, childPath);
106
+ return (
107
+ relative === "" ||
108
+ (!relative.startsWith("..") && !path.isAbsolute(relative))
109
+ );
110
+ }
111
+
112
+ export function getSessionAntiRollbackDirectory({
113
+ homeDir = cliPaths.getHomeDir(),
114
+ anchorBase = null,
115
+ } = {}) {
116
+ const canonicalHome = canonicalPathIdentity(homeDir);
117
+ // Legacy Vitest mocks intentionally expose only getHomeDir/getStatePath. The
118
+ // production module always exports getMachineSecurityAnchorDir, so a missing
119
+ // provider is an explicit injected test seam rather than an environment-
120
+ // variable downgrade a real CLI process can opt into.
121
+ const machineDirectoryProvider = Object.prototype.hasOwnProperty.call(
122
+ cliPaths,
123
+ "getMachineSecurityAnchorDir",
124
+ )
125
+ ? cliPaths.getMachineSecurityAnchorDir
126
+ : null;
127
+ const hasProductionProvider = typeof machineDirectoryProvider === "function";
128
+ const testBase = hasProductionProvider
129
+ ? null
130
+ : path.join(canonicalHome, ".test-machine-security-anchors");
131
+ const canonicalBase = canonicalPathIdentity(
132
+ anchorBase ||
133
+ testBase ||
134
+ (hasProductionProvider ? machineDirectoryProvider() : null),
135
+ );
136
+ if (hasProductionProvider && isWithin(canonicalHome, canonicalBase)) {
137
+ throw anchorError(
138
+ SESSION_ANTI_ROLLBACK_UNAVAILABLE_CODE,
139
+ "Session anti-rollback witness must be outside CHAINLESSCHAIN_HOME",
140
+ null,
141
+ { homeDir: canonicalHome, anchorBase: canonicalBase },
142
+ );
143
+ }
144
+ const directory = path.join(
145
+ canonicalBase,
146
+ "sessions-v1",
147
+ sha256(canonicalHome),
148
+ );
149
+ if (!hasProductionProvider) testScopedDirectories.add(directory);
150
+ return directory;
151
+ }
152
+
153
+ /**
154
+ * Register one explicit anti-rollback directory as a test-owned filesystem.
155
+ * This is an import-only test seam: production callers never invoke it, and
156
+ * it does not change path separation, record validation, locking, or CAS.
157
+ */
158
+ export function _registerTestScopedSessionAntiRollbackDirectory(options = {}) {
159
+ const directory = getSessionAntiRollbackDirectory(options);
160
+ testScopedDirectories.add(directory);
161
+ return directory;
162
+ }
163
+
164
+ function anchorLocation(sessionId, options = {}) {
165
+ const canonicalId = canonicalSessionId(sessionId);
166
+ const sessionDigest = sha256(canonicalId);
167
+ const directory = getSessionAntiRollbackDirectory(options);
168
+ const prefix = sessionDigest.slice(0, 2);
169
+ return Object.freeze({
170
+ directory,
171
+ namespaceDirectory: path.join(directory, "namespace"),
172
+ recordDirectory: path.join(directory, "records", prefix),
173
+ namespacePath: path.join(directory, "namespace", `${prefix}.json`),
174
+ recordPath: path.join(
175
+ directory,
176
+ "records",
177
+ prefix,
178
+ `${sessionDigest}.ndjson`,
179
+ ),
180
+ homeDigest: path.basename(directory),
181
+ sessionDigest,
182
+ sessionId: canonicalId,
183
+ });
184
+ }
185
+
186
+ function normalizeGeneration(value) {
187
+ if (value === null) return null;
188
+ if (
189
+ !value ||
190
+ typeof value !== "object" ||
191
+ Array.isArray(value) ||
192
+ value.schema !== GENERATION_SCHEMA ||
193
+ typeof value.sessionId !== "string" ||
194
+ value.sessionId.length === 0 ||
195
+ typeof value.generationId !== "string" ||
196
+ !GENERATION_ID_PATTERN.test(value.generationId) ||
197
+ !Number.isSafeInteger(value.ordinal) ||
198
+ value.ordinal < 1
199
+ ) {
200
+ throw new TypeError("session generation authority is invalid");
201
+ }
202
+ let predecessor = null;
203
+ if (value.predecessor !== null) {
204
+ const previous = value.predecessor;
205
+ if (
206
+ !previous ||
207
+ typeof previous !== "object" ||
208
+ Array.isArray(previous) ||
209
+ !["tombstone", "legacy-tombstone"].includes(previous.kind) ||
210
+ (previous.generationId !== null &&
211
+ (typeof previous.generationId !== "string" ||
212
+ !GENERATION_ID_PATTERN.test(previous.generationId))) ||
213
+ (previous.headHash !== null &&
214
+ (typeof previous.headHash !== "string" ||
215
+ !DIGEST_PATTERN.test(previous.headHash))) ||
216
+ !Number.isSafeInteger(previous.eventCount) ||
217
+ previous.eventCount < 0 ||
218
+ (previous.tombstonedAtMs !== null &&
219
+ (!Number.isSafeInteger(previous.tombstonedAtMs) ||
220
+ previous.tombstonedAtMs < 0))
221
+ ) {
222
+ throw new TypeError("session generation predecessor is invalid");
223
+ }
224
+ predecessor = Object.freeze({
225
+ kind: previous.kind,
226
+ generationId: previous.generationId,
227
+ headHash: previous.headHash,
228
+ eventCount: previous.eventCount,
229
+ tombstonedAtMs: previous.tombstonedAtMs,
230
+ });
231
+ }
232
+ return Object.freeze({
233
+ schema: GENERATION_SCHEMA,
234
+ sessionId: value.sessionId,
235
+ generationId: value.generationId,
236
+ ordinal: value.ordinal,
237
+ predecessor,
238
+ });
239
+ }
240
+
241
+ function canonicalCandidate(sessionId, value) {
242
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
243
+ throw new TypeError("session anti-rollback candidate must be an object");
244
+ }
245
+ if (!new Set(["live", "deleted"]).has(value.status)) {
246
+ throw new TypeError("session anti-rollback status is invalid");
247
+ }
248
+ if (
249
+ value.headHash !== null &&
250
+ (typeof value.headHash !== "string" || !DIGEST_PATTERN.test(value.headHash))
251
+ ) {
252
+ throw new TypeError("session anti-rollback head hash is invalid");
253
+ }
254
+ if (!Number.isSafeInteger(value.eventCount) || value.eventCount < 0) {
255
+ throw new TypeError("session anti-rollback event count is invalid");
256
+ }
257
+ const generation = normalizeGeneration(value.generation ?? null);
258
+ if (generation !== null && generation.sessionId !== sessionId) {
259
+ throw new TypeError("session anti-rollback generation id does not match");
260
+ }
261
+ const legacyDeletion =
262
+ value.status === "deleted" &&
263
+ generation === null &&
264
+ value.headHash === null;
265
+ if (
266
+ !legacyDeletion &&
267
+ (value.eventCount === 0) !== (value.headHash === null)
268
+ ) {
269
+ throw new TypeError("session anti-rollback head/count are inconsistent");
270
+ }
271
+ const deletedAtMs =
272
+ value.status === "deleted"
273
+ ? Math.max(0, Number(value.deletedAtMs) || 0)
274
+ : null;
275
+ if (
276
+ value.status === "deleted" &&
277
+ (!Number.isSafeInteger(deletedAtMs) || deletedAtMs < 0)
278
+ ) {
279
+ throw new TypeError("session deletion timestamp is invalid");
280
+ }
281
+ return Object.freeze({
282
+ status: value.status,
283
+ generation,
284
+ headHash: value.headHash,
285
+ eventCount: value.eventCount,
286
+ deletedAtMs,
287
+ });
288
+ }
289
+
290
+ function sameGeneration(left, right) {
291
+ return JSON.stringify(left) === JSON.stringify(right);
292
+ }
293
+
294
+ function recordCore(location, revision, previousRecordHash, candidate) {
295
+ return {
296
+ schema: ANCHOR_SCHEMA,
297
+ stateHomeDigest: location.homeDigest,
298
+ sessionDigest: location.sessionDigest,
299
+ sessionId: location.sessionId,
300
+ revision: String(revision),
301
+ previousRecordHash,
302
+ ...candidate,
303
+ };
304
+ }
305
+
306
+ function normalizeRecord(value, location) {
307
+ if (
308
+ !value ||
309
+ typeof value !== "object" ||
310
+ Array.isArray(value) ||
311
+ value.schema !== ANCHOR_SCHEMA ||
312
+ value.stateHomeDigest !== location.homeDigest ||
313
+ value.sessionDigest !== location.sessionDigest ||
314
+ value.sessionId !== location.sessionId ||
315
+ typeof value.revision !== "string" ||
316
+ !/^[1-9]\d*$/.test(value.revision) ||
317
+ (value.previousRecordHash !== null &&
318
+ (typeof value.previousRecordHash !== "string" ||
319
+ !DIGEST_PATTERN.test(value.previousRecordHash))) ||
320
+ typeof value.recordHash !== "string" ||
321
+ !DIGEST_PATTERN.test(value.recordHash)
322
+ ) {
323
+ throw new TypeError("session anti-rollback record identity is invalid");
324
+ }
325
+ const candidate = canonicalCandidate(location.sessionId, value);
326
+ const core = recordCore(
327
+ location,
328
+ value.revision,
329
+ value.previousRecordHash,
330
+ candidate,
331
+ );
332
+ if (sha256(JSON.stringify(core)) !== value.recordHash) {
333
+ throw new TypeError("session anti-rollback record digest is invalid");
334
+ }
335
+ return Object.freeze({ ...core, recordHash: value.recordHash });
336
+ }
337
+
338
+ function normalizeNamespace(value, homeDigest, { missing = false } = {}) {
339
+ if (missing && Object.keys(value).length === 0) {
340
+ return Object.freeze({ entries: Object.freeze({}) });
341
+ }
342
+ if (
343
+ value.version !== NAMESPACE_VERSION ||
344
+ value.stateHomeDigest !== homeDigest ||
345
+ !value.entries ||
346
+ typeof value.entries !== "object" ||
347
+ Array.isArray(value.entries)
348
+ ) {
349
+ throw new TypeError("session anti-rollback namespace bucket is invalid");
350
+ }
351
+ const entries = {};
352
+ for (const [digest, sessionId] of Object.entries(value.entries)) {
353
+ if (
354
+ !DIGEST_PATTERN.test(digest) ||
355
+ typeof sessionId !== "string" ||
356
+ sha256(sessionId) !== digest
357
+ ) {
358
+ throw new TypeError("session anti-rollback namespace entry is invalid");
359
+ }
360
+ entries[digest] = sessionId;
361
+ }
362
+ return Object.freeze({ entries: Object.freeze(entries) });
363
+ }
364
+
365
+ function prepareDirectory(directory) {
366
+ try {
367
+ const testScoped = [...testScopedDirectories].some((root) =>
368
+ isWithin(root, directory),
369
+ );
370
+ if (testScoped) {
371
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
372
+ const testDirectory = fs.lstatSync(directory);
373
+ if (testDirectory.isSymbolicLink() || !testDirectory.isDirectory()) {
374
+ throw new TypeError("session anti-rollback test path is unsafe");
375
+ }
376
+ securedDirectories.set(
377
+ directory,
378
+ `${testDirectory.dev}:${testDirectory.ino}`,
379
+ );
380
+ return;
381
+ }
382
+ let current = null;
383
+ try {
384
+ current = fs.lstatSync(directory);
385
+ } catch {
386
+ // The secure helper below creates a missing directory.
387
+ }
388
+ const identity = current ? `${current.dev}:${current.ino}` : null;
389
+ const unsafePosixMode =
390
+ process.platform !== "win32" &&
391
+ current !== null &&
392
+ (current.mode & 0o777) !== 0o700;
393
+ if (
394
+ current?.isDirectory() &&
395
+ !current.isSymbolicLink() &&
396
+ !unsafePosixMode &&
397
+ securedDirectories.get(directory) === identity
398
+ ) {
399
+ return;
400
+ }
401
+ ensurePrivateDirectory(directory, {
402
+ failIfUnavailable: true,
403
+ });
404
+ const secured = fs.lstatSync(directory);
405
+ securedDirectories.set(directory, `${secured.dev}:${secured.ino}`);
406
+ } catch (cause) {
407
+ throw unavailable(cause, directory);
408
+ }
409
+ }
410
+
411
+ function assertRegularRecordPath(filePath) {
412
+ if (!fs.existsSync(filePath)) return;
413
+ const stats = fs.lstatSync(filePath);
414
+ if (stats.isSymbolicLink() || !stats.isFile()) {
415
+ throw new TypeError("session anti-rollback record is not a regular file");
416
+ }
417
+ }
418
+
419
+ function readLastRecord(location) {
420
+ if (!fs.existsSync(location.recordPath)) return null;
421
+ try {
422
+ assertRegularRecordPath(location.recordPath);
423
+ const stats = fs.statSync(location.recordPath);
424
+ let endsWithNewline = false;
425
+ if (stats.size > 0) {
426
+ const descriptor = fs.openSync(location.recordPath, "r");
427
+ try {
428
+ const last = Buffer.allocUnsafe(1);
429
+ fs.readSync(descriptor, last, 0, 1, stats.size - 1);
430
+ endsWithNewline = last[0] === 0x0a;
431
+ } finally {
432
+ fs.closeSync(descriptor);
433
+ }
434
+ }
435
+ let first = true;
436
+ for (const item of iterateFileLinesReverseSync(location.recordPath)) {
437
+ if (first && !endsWithNewline && !item.terminated) {
438
+ first = false;
439
+ continue;
440
+ }
441
+ return normalizeRecord(JSON.parse(item.line), location);
442
+ }
443
+ return null;
444
+ } catch (cause) {
445
+ throw unavailable(cause, location.recordPath);
446
+ }
447
+ }
448
+
449
+ export function readSessionAntiRollbackAnchor(sessionId, options = {}) {
450
+ return readLastRecord(anchorLocation(sessionId, options));
451
+ }
452
+
453
+ function successorMatchesDeleted(current, candidate) {
454
+ const generation = candidate.generation;
455
+ if (!generation || generation.predecessor === null) return false;
456
+ const previous = generation.predecessor;
457
+ const currentGeneration = current.generation;
458
+ const expectedOrdinal = currentGeneration ? currentGeneration.ordinal + 1 : 2;
459
+ return (
460
+ generation.ordinal === expectedOrdinal &&
461
+ previous.kind === (currentGeneration ? "tombstone" : "legacy-tombstone") &&
462
+ previous.generationId === (currentGeneration?.generationId ?? null) &&
463
+ previous.headHash === current.headHash &&
464
+ previous.eventCount === current.eventCount &&
465
+ previous.tombstonedAtMs === current.deletedAtMs
466
+ );
467
+ }
468
+
469
+ function assertForwardTransition(sessionId, current, candidate, provePrefix) {
470
+ if (current === null) return;
471
+ if (current.status === "live" && candidate.status === "deleted") {
472
+ if (
473
+ !sameGeneration(current.generation, candidate.generation) ||
474
+ current.headHash !== candidate.headHash ||
475
+ current.eventCount !== candidate.eventCount
476
+ ) {
477
+ throw rollback(
478
+ sessionId,
479
+ "deletion does not preserve the anchored live generation",
480
+ current,
481
+ candidate,
482
+ );
483
+ }
484
+ return;
485
+ }
486
+ if (current.status === "deleted" && candidate.status === "live") {
487
+ if (!successorMatchesDeleted(current, candidate)) {
488
+ throw rollback(
489
+ sessionId,
490
+ "live generation is not the anchored tombstone successor",
491
+ current,
492
+ candidate,
493
+ );
494
+ }
495
+ return;
496
+ }
497
+ if (current.status !== candidate.status) {
498
+ throw rollback(
499
+ sessionId,
500
+ "session status moved backward",
501
+ current,
502
+ candidate,
503
+ );
504
+ }
505
+ if (!sameGeneration(current.generation, candidate.generation)) {
506
+ throw rollback(
507
+ sessionId,
508
+ "generation identity moved backward or forked",
509
+ current,
510
+ candidate,
511
+ );
512
+ }
513
+ if (candidate.status === "deleted") {
514
+ if (
515
+ current.headHash !== candidate.headHash ||
516
+ current.eventCount !== candidate.eventCount ||
517
+ candidate.deletedAtMs < current.deletedAtMs
518
+ ) {
519
+ throw rollback(
520
+ sessionId,
521
+ "tombstone witness moved backward",
522
+ current,
523
+ candidate,
524
+ );
525
+ }
526
+ return;
527
+ }
528
+ if (candidate.eventCount < current.eventCount) {
529
+ throw rollback(sessionId, "event count moved backward", current, candidate);
530
+ }
531
+ if (candidate.eventCount === current.eventCount) {
532
+ if (candidate.headHash !== current.headHash) {
533
+ throw rollback(
534
+ sessionId,
535
+ "equal event count has a different head",
536
+ current,
537
+ candidate,
538
+ );
539
+ }
540
+ return;
541
+ }
542
+ let prefixProven = false;
543
+ try {
544
+ prefixProven = provePrefix?.(current) === true;
545
+ } catch (cause) {
546
+ throw unavailable(cause);
547
+ }
548
+ if (!prefixProven) {
549
+ throw rollback(
550
+ sessionId,
551
+ "newer local head does not prove the external anchor as a prefix",
552
+ current,
553
+ candidate,
554
+ );
555
+ }
556
+ }
557
+
558
+ function truncatePartialTail(filePath) {
559
+ if (!fs.existsSync(filePath)) return;
560
+ let descriptor = null;
561
+ try {
562
+ let flags = fs.constants.O_RDWR;
563
+ if (typeof fs.constants.O_NOFOLLOW === "number") {
564
+ flags |= fs.constants.O_NOFOLLOW;
565
+ }
566
+ descriptor = fs.openSync(filePath, flags);
567
+ const size = fs.fstatSync(descriptor).size;
568
+ if (size === 0) return;
569
+ const last = Buffer.allocUnsafe(1);
570
+ fs.readSync(descriptor, last, 0, 1, size - 1);
571
+ if (last[0] === 0x0a) return;
572
+
573
+ const buffer = Buffer.allocUnsafe(4096);
574
+ let position = size;
575
+ let truncateAt = 0;
576
+ while (position > 0) {
577
+ const length = Math.min(buffer.length, position);
578
+ position -= length;
579
+ fs.readSync(descriptor, buffer, 0, length, position);
580
+ for (let index = length - 1; index >= 0; index -= 1) {
581
+ if (buffer[index] === 0x0a) {
582
+ truncateAt = position + index + 1;
583
+ position = 0;
584
+ break;
585
+ }
586
+ }
587
+ }
588
+ fs.ftruncateSync(descriptor, truncateAt);
589
+ fs.fsyncSync(descriptor);
590
+ } finally {
591
+ if (descriptor !== null) fs.closeSync(descriptor);
592
+ }
593
+ }
594
+
595
+ function ensureNamespaceEntry(location) {
596
+ prepareDirectory(location.namespaceDirectory);
597
+ mutateSecurityStore(
598
+ location.namespacePath,
599
+ "Session anti-rollback namespace",
600
+ (draft) => {
601
+ const namespace = normalizeNamespace(draft, location.homeDigest, {
602
+ missing: !fs.existsSync(location.namespacePath),
603
+ });
604
+ const existing = namespace.entries[location.sessionDigest];
605
+ if (existing !== undefined && existing !== location.sessionId) {
606
+ throw new TypeError("session anti-rollback namespace digest collision");
607
+ }
608
+ draft.version = NAMESPACE_VERSION;
609
+ draft.stateHomeDigest = location.homeDigest;
610
+ draft.entries = {
611
+ ...namespace.entries,
612
+ [location.sessionDigest]: location.sessionId,
613
+ };
614
+ },
615
+ LOCK_OPTIONS,
616
+ );
617
+ }
618
+
619
+ function appendRecord(location, record) {
620
+ if (isAffectedWindowsZeroDeviceStatRuntime()) {
621
+ return withTrustedFileParentSync(
622
+ fs,
623
+ location.recordPath,
624
+ ({ canonicalPath, parentDevice }) =>
625
+ appendRecordAtPath(canonicalPath, record, parentDevice),
626
+ );
627
+ }
628
+ return appendRecordAtPath(location.recordPath, record, null);
629
+ }
630
+
631
+ function appendRecordAtPath(recordPath, record, parentDevice) {
632
+ let descriptor = null;
633
+ try {
634
+ let flags = fs.constants.O_WRONLY | fs.constants.O_APPEND;
635
+ const createsRecord = !fs.existsSync(recordPath);
636
+ if (createsRecord) {
637
+ flags |= fs.constants.O_CREAT | fs.constants.O_EXCL;
638
+ }
639
+ if (typeof fs.constants.O_NOFOLLOW === "number") {
640
+ flags |= fs.constants.O_NOFOLLOW;
641
+ }
642
+ descriptor = fs.openSync(recordPath, flags, 0o600);
643
+ const opened = fs.fstatSync(descriptor, { bigint: true });
644
+ const payload = Buffer.from(`${JSON.stringify(record)}\n`, "utf8");
645
+ let offset = 0;
646
+ while (offset < payload.length) {
647
+ const written = fs.writeSync(
648
+ descriptor,
649
+ payload,
650
+ offset,
651
+ payload.length - offset,
652
+ );
653
+ if (!Number.isSafeInteger(written) || written <= 0) {
654
+ throw new Error(
655
+ "session anti-rollback append made no forward progress",
656
+ );
657
+ }
658
+ offset += written;
659
+ }
660
+ fs.fsyncSync(descriptor);
661
+ const published = fs.lstatSync(recordPath, { bigint: true });
662
+ const identityMatches =
663
+ parentDevice === null
664
+ ? String(opened.dev) === String(published.dev) &&
665
+ String(opened.ino) === String(published.ino)
666
+ : samePathHandleStableFileIdentity(published, opened, parentDevice);
667
+ if (published.isSymbolicLink() || !published.isFile() || !identityMatches) {
668
+ throw new Error("session anti-rollback record identity changed");
669
+ }
670
+ if (createsRecord && process.platform !== "win32") {
671
+ const directoryDescriptor = fs.openSync(path.dirname(recordPath), "r");
672
+ try {
673
+ fs.fsyncSync(directoryDescriptor);
674
+ } finally {
675
+ fs.closeSync(directoryDescriptor);
676
+ }
677
+ }
678
+ } finally {
679
+ if (descriptor !== null) fs.closeSync(descriptor);
680
+ }
681
+ }
682
+
683
+ export function publishSessionAntiRollbackAnchor(
684
+ sessionId,
685
+ value,
686
+ { provePrefix = null, ...pathOptions } = {},
687
+ ) {
688
+ const location = anchorLocation(sessionId, pathOptions);
689
+ const candidate = canonicalCandidate(location.sessionId, value);
690
+ prepareDirectory(location.recordDirectory);
691
+ try {
692
+ return withFileLock(
693
+ location.recordPath,
694
+ () => {
695
+ assertRegularRecordPath(location.recordPath);
696
+ truncatePartialTail(location.recordPath);
697
+ const current = readLastRecord(location);
698
+ assertForwardTransition(
699
+ location.sessionId,
700
+ current,
701
+ candidate,
702
+ provePrefix,
703
+ );
704
+ const unchanged =
705
+ current !== null &&
706
+ current.status === candidate.status &&
707
+ sameGeneration(current.generation, candidate.generation) &&
708
+ current.headHash === candidate.headHash &&
709
+ current.eventCount === candidate.eventCount &&
710
+ current.deletedAtMs === candidate.deletedAtMs;
711
+ if (unchanged) return current;
712
+ if (current === null) ensureNamespaceEntry(location);
713
+ const core = recordCore(
714
+ location,
715
+ BigInt(current?.revision || "0") + 1n,
716
+ current?.recordHash || null,
717
+ candidate,
718
+ );
719
+ const record = Object.freeze({
720
+ ...core,
721
+ recordHash: sha256(JSON.stringify(core)),
722
+ });
723
+ appendRecord(location, record);
724
+ return record;
725
+ },
726
+ LOCK_OPTIONS,
727
+ );
728
+ } catch (cause) {
729
+ throw unavailable(cause, location.recordPath);
730
+ }
731
+ }
732
+
733
+ export function listSessionAntiRollbackIds(options = {}) {
734
+ const directory = getSessionAntiRollbackDirectory(options);
735
+ const namespaceDirectory = path.join(directory, "namespace");
736
+ if (!fs.existsSync(namespaceDirectory)) return [];
737
+ try {
738
+ prepareDirectory(namespaceDirectory);
739
+ const ids = new Set();
740
+ for (const name of fs.readdirSync(namespaceDirectory)) {
741
+ if (!/^[0-9a-f]{2}\.json$/.test(name)) continue;
742
+ const filePath = path.join(namespaceDirectory, name);
743
+ const namespace = normalizeNamespace(
744
+ readSecurityStore(filePath, "Session anti-rollback namespace"),
745
+ path.basename(directory),
746
+ );
747
+ for (const sessionId of Object.values(namespace.entries)) {
748
+ ids.add(sessionId);
749
+ }
750
+ }
751
+ return [...ids].sort();
752
+ } catch (cause) {
753
+ throw unavailable(cause, namespaceDirectory);
754
+ }
755
+ }
756
+
757
+ export function sessionAntiRollbackPredecessorWitness(record) {
758
+ if (!record || record.status !== "deleted") return null;
759
+ return Object.freeze({
760
+ generation: record.generation,
761
+ last_hash: record.headHash,
762
+ event_count: record.eventCount,
763
+ deleted_at_ms: record.deletedAtMs,
764
+ deleted: true,
765
+ });
766
+ }