@remnic/core 9.54.4 → 9.54.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 (648) hide show
  1. package/dist/access-admin-ops-surface.d.ts +24 -23
  2. package/dist/access-admin-ops-surface.js +55 -50
  3. package/dist/access-authorization-probe.d.ts +25 -22
  4. package/dist/access-authorization-probe.js +58 -51
  5. package/dist/access-boundary.d.ts +26 -23
  6. package/dist/access-boundary.js +59 -52
  7. package/dist/access-cli.js +115 -107
  8. package/dist/access-cli.js.map +1 -1
  9. package/dist/access-coding-context-resolution.d.ts +1 -1
  10. package/dist/access-extraction-force-flush.d.ts +19 -18
  11. package/dist/access-extraction-force-flush.js +55 -50
  12. package/dist/access-health-types.d.ts +1 -1
  13. package/dist/access-http-lcm-compaction.d.ts +22 -21
  14. package/dist/access-http-lifecycle-flush.d.ts +22 -21
  15. package/dist/access-http-offline-stream.d.ts +19 -18
  16. package/dist/access-http-query.js +55 -50
  17. package/dist/access-http.d.ts +55 -36
  18. package/dist/access-http.js +76 -65
  19. package/dist/access-identity-continuity-surface.d.ts +9 -9
  20. package/dist/access-identity-continuity-surface.js +55 -50
  21. package/dist/access-lcm-surface.d.ts +21 -20
  22. package/dist/access-lcm-surface.js +55 -50
  23. package/dist/access-mcp.d.ts +22 -21
  24. package/dist/access-mcp.js +63 -57
  25. package/dist/access-memory-search-fanout.d.ts +22 -2
  26. package/dist/access-memory-search-fanout.js +1 -1
  27. package/dist/{access-namespace-preflight-C7n4tgWC.d.ts → access-namespace-preflight-CCNn_6NE.d.ts} +1 -1
  28. package/dist/access-namespace-preflight.d.ts +3 -3
  29. package/dist/access-namespace-preflight.js +55 -50
  30. package/dist/access-observe-write-surface.d.ts +22 -21
  31. package/dist/access-observe-write-surface.js +55 -50
  32. package/dist/access-offline-manifest.d.ts +10 -9
  33. package/dist/access-offline-manifest.js +12 -10
  34. package/dist/access-operations-batch.js +57 -52
  35. package/dist/access-operations.d.ts +23 -22
  36. package/dist/access-operations.js +61 -55
  37. package/dist/access-recall-concurrency.d.ts +21 -20
  38. package/dist/access-recall-concurrency.js +55 -50
  39. package/dist/access-recall-response.d.ts +19 -18
  40. package/dist/access-recall-response.js +55 -50
  41. package/dist/access-recall-surface.d.ts +19 -18
  42. package/dist/access-recall-surface.js +55 -50
  43. package/dist/access-schema.d.ts +2 -2
  44. package/dist/access-schema.js +5 -4
  45. package/dist/{access-service-CXvbxLK-.d.ts → access-service-DkSdXK4S.d.ts} +1314 -83
  46. package/dist/access-service-helpers.d.ts +22 -21
  47. package/dist/access-service.d.ts +31 -30
  48. package/dist/access-service.js +55 -50
  49. package/dist/access-surface-catalog.d.ts +22 -21
  50. package/dist/access-surface-catalog.js +209 -2
  51. package/dist/access-surface-catalog.js.map +1 -1
  52. package/dist/access-wearables-meetings-surface.d.ts +3 -3
  53. package/dist/action-confidence.d.ts +1 -1
  54. package/dist/active-memory-bridge.d.ts +14 -1
  55. package/dist/active-memory-bridge.js +6 -1
  56. package/dist/active-recall.d.ts +1 -1
  57. package/dist/active-recall.js +4 -2
  58. package/dist/active-recall.js.map +1 -1
  59. package/dist/adapters/index.js +7 -7
  60. package/dist/adapters/registry.js +3 -3
  61. package/dist/admin-console/public/what-helps-me/app.js +1594 -0
  62. package/dist/admin-console/public/what-helps-me/index.html +339 -0
  63. package/dist/admin-console/public/what-helps-me/model.js +685 -0
  64. package/dist/admin-console/public/what-helps-me/what-helps-me.css +1296 -0
  65. package/dist/ambient-provenance.d.ts +1 -1
  66. package/dist/artifact-search.d.ts +1 -1
  67. package/dist/{auto-sync-TDHLZPVP.js → auto-sync-NR3GPCOA.js} +4 -4
  68. package/dist/behavior-learner.d.ts +1 -1
  69. package/dist/behavior-signals.d.ts +1 -1
  70. package/dist/bootstrap.d.ts +9 -9
  71. package/dist/briefing.d.ts +2 -2
  72. package/dist/briefing.js +33 -12
  73. package/dist/buffer-surprise-report.d.ts +1 -1
  74. package/dist/buffer-turn-helpers.d.ts +1 -1
  75. package/dist/buffer.d.ts +2 -2
  76. package/dist/buffer.js +3 -3
  77. package/dist/bulk-import/index.d.ts +3 -3
  78. package/dist/calibration.d.ts +1 -1
  79. package/dist/calibration.js +1 -1
  80. package/dist/capabilities.d.ts +1 -1
  81. package/dist/{catalog-BOOxl-I2.d.ts → catalog-DGFf8mD9.d.ts} +1 -1
  82. package/dist/causal-behavior.d.ts +1 -1
  83. package/dist/causal-consolidation.d.ts +1 -1
  84. package/dist/causal-consolidation.js +34 -13
  85. package/dist/causal-consolidation.js.map +1 -1
  86. package/dist/causal-trajectory-graph.d.ts +1 -1
  87. package/dist/{chunk-ZYLC7NFO.js → chunk-26ZXBTYN.js} +4 -4
  88. package/dist/chunk-26ZXBTYN.js.map +1 -0
  89. package/dist/{chunk-LLJMAULE.js → chunk-2FKEIPIT.js} +5 -5
  90. package/dist/{chunk-3CSX3ETE.js → chunk-2LWQSV6A.js} +23 -11
  91. package/dist/chunk-2LWQSV6A.js.map +1 -0
  92. package/dist/{chunk-JBPKEARU.js → chunk-2QSZNTDO.js} +7 -7
  93. package/dist/chunk-37DZZFFM.js +502 -0
  94. package/dist/chunk-37DZZFFM.js.map +1 -0
  95. package/dist/chunk-3HSRWJTC.js +9 -0
  96. package/dist/chunk-3HSRWJTC.js.map +1 -0
  97. package/dist/{chunk-XUAMCLY4.js → chunk-3TIZPSJC.js} +17 -13
  98. package/dist/chunk-3TIZPSJC.js.map +1 -0
  99. package/dist/{chunk-PDDTI7IC.js → chunk-43SEBY2M.js} +1 -1
  100. package/dist/{chunk-HWRD23DW.js → chunk-46SZBVDS.js} +3 -3
  101. package/dist/{chunk-6HPHEFXT.js → chunk-5FD7JMTX.js} +2 -2
  102. package/dist/{chunk-7BZOBA5J.js → chunk-5NB726GV.js} +70 -16
  103. package/dist/chunk-5NB726GV.js.map +1 -0
  104. package/dist/chunk-62OKGIJA.js +39 -0
  105. package/dist/chunk-62OKGIJA.js.map +1 -0
  106. package/dist/{chunk-IBKR2TFR.js → chunk-6CYHUWCO.js} +2 -2
  107. package/dist/{chunk-YN6ASZUG.js → chunk-6QPI2ITJ.js} +5 -5
  108. package/dist/{chunk-UU3G6GUP.js → chunk-76JJSXK7.js} +1 -1
  109. package/dist/chunk-76JJSXK7.js.map +1 -0
  110. package/dist/{chunk-PX2LSHYM.js → chunk-7BD6O7ZV.js} +189 -4
  111. package/dist/chunk-7BD6O7ZV.js.map +1 -0
  112. package/dist/chunk-7TBXNYLO.js +346 -0
  113. package/dist/chunk-7TBXNYLO.js.map +1 -0
  114. package/dist/{chunk-NH5IR2WM.js → chunk-BRR5EUTF.js} +6 -2
  115. package/dist/chunk-BRR5EUTF.js.map +1 -0
  116. package/dist/{chunk-NIS256NW.js → chunk-CIAI24TQ.js} +3 -3
  117. package/dist/{chunk-T2JHZWH6.js → chunk-D3LBIUWV.js} +11703 -7573
  118. package/dist/chunk-D3LBIUWV.js.map +1 -0
  119. package/dist/{chunk-7FGL6CGA.js → chunk-DVYRD4HY.js} +2 -2
  120. package/dist/{chunk-G7Z6EENO.js → chunk-EKTP7MNS.js} +4 -4
  121. package/dist/{chunk-S75M5ZRK.js → chunk-F2JRDPON.js} +16 -1
  122. package/dist/chunk-F2JRDPON.js.map +1 -0
  123. package/dist/{chunk-RZ4XGKOJ.js → chunk-FFRTNF3L.js} +8 -8
  124. package/dist/{chunk-VKXWK44Q.js → chunk-FONWBCVY.js} +3 -3
  125. package/dist/{chunk-DZD576ZU.js → chunk-FPLM2TKD.js} +2 -2
  126. package/dist/{chunk-ZPJ4QSGP.js → chunk-FTPONF6B.js} +2 -2
  127. package/dist/{chunk-LJS7KUPV.js → chunk-FWX7W5PM.js} +2 -2
  128. package/dist/{chunk-UTRO5UJU.js → chunk-G6DFSS6A.js} +5 -1
  129. package/dist/chunk-G6DFSS6A.js.map +1 -0
  130. package/dist/{chunk-6LAPLZOS.js → chunk-GJ776BC2.js} +8 -3
  131. package/dist/chunk-GJ776BC2.js.map +1 -0
  132. package/dist/{chunk-G4T7L557.js → chunk-GN3WRFJY.js} +6 -6
  133. package/dist/{chunk-QT2FRYI4.js → chunk-H7NAZ3UB.js} +2 -2
  134. package/dist/{chunk-APPFNGX6.js → chunk-HBUFQRIM.js} +2 -2
  135. package/dist/{chunk-UU7TRFNE.js → chunk-HGJ6NJ4E.js} +2 -2
  136. package/dist/{chunk-WB7JSQCP.js → chunk-HIEWDJ2O.js} +5 -2
  137. package/dist/{chunk-WB7JSQCP.js.map → chunk-HIEWDJ2O.js.map} +1 -1
  138. package/dist/{chunk-UES4BHA2.js → chunk-I3MSGFGD.js} +17 -6
  139. package/dist/chunk-I3MSGFGD.js.map +1 -0
  140. package/dist/{chunk-C4ZNGVK3.js → chunk-IKP5OYQQ.js} +2 -2
  141. package/dist/{chunk-KQAFEZQX.js → chunk-IPLYGWQF.js} +5 -5
  142. package/dist/{chunk-RDJRFDJY.js → chunk-IQ4NVWEF.js} +2 -2
  143. package/dist/{chunk-CGVHPCP2.js → chunk-JYG4OLWH.js} +2 -2
  144. package/dist/{chunk-WOPA3SGI.js → chunk-K26NWEOC.js} +8 -8
  145. package/dist/{chunk-K4QS7LXU.js → chunk-K2UGPTYY.js} +485 -44
  146. package/dist/chunk-K2UGPTYY.js.map +1 -0
  147. package/dist/{chunk-L6CQ7WIW.js → chunk-KFBOPMF6.js} +11 -11
  148. package/dist/chunk-KFBOPMF6.js.map +1 -0
  149. package/dist/{chunk-G7A5HLUS.js → chunk-KJH6W2I7.js} +2 -2
  150. package/dist/{chunk-IXEBUYO4.js → chunk-KM55SMLG.js} +11 -2
  151. package/dist/{chunk-IXEBUYO4.js.map → chunk-KM55SMLG.js.map} +1 -1
  152. package/dist/{chunk-HOIRB77A.js → chunk-L6Z5OAS4.js} +2 -2
  153. package/dist/chunk-LC2BDILZ.js +57 -0
  154. package/dist/chunk-LC2BDILZ.js.map +1 -0
  155. package/dist/{chunk-227UFSEQ.js → chunk-LEPOP46W.js} +4 -4
  156. package/dist/{chunk-6R4G7N4S.js → chunk-M4YHAODQ.js} +388 -748
  157. package/dist/chunk-M4YHAODQ.js.map +1 -0
  158. package/dist/{chunk-3JLPGJ43.js → chunk-MIH3CJPN.js} +5 -5
  159. package/dist/{chunk-QHFV6VNU.js → chunk-MK5VEWCR.js} +2 -2
  160. package/dist/chunk-NISZQW72.js +140 -0
  161. package/dist/chunk-NISZQW72.js.map +1 -0
  162. package/dist/{chunk-GUKBW4HL.js → chunk-NQMSEB5D.js} +6 -6
  163. package/dist/{chunk-G2LUMAUF.js → chunk-OCIP6YMD.js} +12 -5
  164. package/dist/chunk-OCIP6YMD.js.map +1 -0
  165. package/dist/{chunk-47P3RYZP.js → chunk-PG4QBPVG.js} +21 -11
  166. package/dist/chunk-PG4QBPVG.js.map +1 -0
  167. package/dist/{chunk-WRJVB3C2.js → chunk-PO7NXBG2.js} +2 -2
  168. package/dist/{chunk-QWGE2PUH.js → chunk-PT54WISF.js} +190 -56
  169. package/dist/chunk-PT54WISF.js.map +1 -0
  170. package/dist/{chunk-AZM4CCAC.js → chunk-QZLDRQ4I.js} +8 -8
  171. package/dist/{chunk-THPPCISA.js → chunk-RBII7637.js} +5 -5
  172. package/dist/{chunk-JDZPNCMA.js → chunk-RUNORG2P.js} +6 -3
  173. package/dist/chunk-RUNORG2P.js.map +1 -0
  174. package/dist/{chunk-JV5YUBIZ.js → chunk-RZQ6W2W4.js} +50 -44
  175. package/dist/chunk-RZQ6W2W4.js.map +1 -0
  176. package/dist/{chunk-RUMNY5DT.js → chunk-SMNYAMJD.js} +6 -2
  177. package/dist/chunk-SMNYAMJD.js.map +1 -0
  178. package/dist/{chunk-MKQBXQKU.js → chunk-TB6CBZ5V.js} +22 -15
  179. package/dist/chunk-TB6CBZ5V.js.map +1 -0
  180. package/dist/{chunk-VMMQDEN5.js → chunk-TO7ZTWJW.js} +2 -2
  181. package/dist/{chunk-K2TDK7GG.js → chunk-TWTUGCTY.js} +2 -2
  182. package/dist/{chunk-HLYR2TQL.js → chunk-UCTS3DDH.js} +2 -2
  183. package/dist/{chunk-JYIJNNN3.js → chunk-UG6VXIOK.js} +2 -2
  184. package/dist/{chunk-WM65LU67.js → chunk-UKYS3NU5.js} +2 -2
  185. package/dist/{chunk-WDBX6VER.js → chunk-UPLZNJKN.js} +5 -5
  186. package/dist/{chunk-C23BYTJV.js → chunk-UW5NPZJ2.js} +40 -12
  187. package/dist/chunk-UW5NPZJ2.js.map +1 -0
  188. package/dist/{chunk-2GFK22OZ.js → chunk-V6ERBR7U.js} +2 -2
  189. package/dist/{chunk-CYF6WVE7.js → chunk-W5ZKPI34.js} +139 -158
  190. package/dist/chunk-W5ZKPI34.js.map +1 -0
  191. package/dist/chunk-XX4HPFFM.js +370 -0
  192. package/dist/chunk-XX4HPFFM.js.map +1 -0
  193. package/dist/{chunk-ZMYDYNI4.js → chunk-XZHE2DEN.js} +4 -4
  194. package/dist/chunk-YXQEKCZM.js +223 -0
  195. package/dist/chunk-YXQEKCZM.js.map +1 -0
  196. package/dist/{chunk-GYPQL6XN.js → chunk-Z4N7SVVF.js} +35 -47
  197. package/dist/chunk-Z4N7SVVF.js.map +1 -0
  198. package/dist/{chunk-MAXVH44E.js → chunk-Z54EM5YZ.js} +7 -7
  199. package/dist/{chunk-YGLY6QFV.js → chunk-ZRZVLB7O.js} +2 -2
  200. package/dist/{chunk-4IIFL7GC.js → chunk-ZUKIUXHQ.js} +10 -10
  201. package/dist/{cli-DVUFIa5E.d.ts → cli-Bze8S0Qs.d.ts} +5 -5
  202. package/dist/cli.d.ts +22 -21
  203. package/dist/cli.js +90 -80
  204. package/dist/coding/pre-action-gate.d.ts +1 -1
  205. package/dist/compounding/engine.d.ts +2 -2
  206. package/dist/compounding/engine.js +33 -12
  207. package/dist/compounding/preference-consolidator.d.ts +1 -1
  208. package/dist/compression-optimizer.d.ts +1 -1
  209. package/dist/config.d.ts +1 -1
  210. package/dist/config.js +4 -2
  211. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  212. package/dist/connectors/codex-materialize-runner.js +33 -12
  213. package/dist/connectors/codex-materialize.d.ts +1 -1
  214. package/dist/connectors/index.d.ts +1 -1
  215. package/dist/connectors/index.js +32 -12
  216. package/dist/consolidation-provenance-check.d.ts +2 -2
  217. package/dist/consolidation-undo.d.ts +2 -2
  218. package/dist/contradiction/index.d.ts +2 -2
  219. package/dist/contradiction/index.js +3 -1
  220. package/dist/contradiction-scan-JDTBXRGW.js +20 -0
  221. package/dist/converge-config.d.ts +1 -1
  222. package/dist/convergence-refresh.d.ts +2 -2
  223. package/dist/convergence-refresh.js +35 -14
  224. package/dist/conversation-index/backend.d.ts +1 -1
  225. package/dist/conversation-index/backend.js +2 -2
  226. package/dist/conversation-index/chunker.d.ts +1 -1
  227. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  228. package/dist/conversation-index/indexer.d.ts +1 -1
  229. package/dist/conversation-index/search.d.ts +1 -1
  230. package/dist/corpus-watermark.d.ts +1 -1
  231. package/dist/corpus-watermark.js +25 -24
  232. package/dist/day-summary.d.ts +1 -1
  233. package/dist/delinearize.d.ts +1 -1
  234. package/dist/dependency-propagation-config.d.ts +1 -1
  235. package/dist/{dependency-propagation-delivery-XzF76C9n.d.ts → dependency-propagation-delivery-CzEfB31P.d.ts} +1 -1
  236. package/dist/direct-answer-wiring.d.ts +1 -1
  237. package/dist/direct-answer.d.ts +1 -1
  238. package/dist/embedding-fallback.d.ts +1 -1
  239. package/dist/enrichment/index.d.ts +1 -1
  240. package/dist/entity-retrieval.d.ts +2 -2
  241. package/dist/entity-retrieval.js +33 -12
  242. package/dist/entity-schema.d.ts +1 -1
  243. package/dist/explicit-capture.d.ts +9 -9
  244. package/dist/explicit-capture.js +7 -4
  245. package/dist/external-wiki-access.d.ts +22 -21
  246. package/dist/external-wiki-access.js +56 -51
  247. package/dist/external-wiki-collection-registration.d.ts +1 -1
  248. package/dist/external-wiki-collection.d.ts +1 -1
  249. package/dist/external-wiki-mcp-tools.d.ts +22 -21
  250. package/dist/extraction-error-classification.d.ts +1 -1
  251. package/dist/extraction-faithfulness.d.ts +1 -1
  252. package/dist/extraction-faithfulness.js +2 -2
  253. package/dist/extraction-judge-telemetry.d.ts +1 -1
  254. package/dist/extraction-judge-training.d.ts +1 -1
  255. package/dist/extraction-judge.d.ts +1 -1
  256. package/dist/extraction-judge.js +2 -2
  257. package/dist/extraction-liveness.d.ts +1 -1
  258. package/dist/extraction-normalization.d.ts +1 -1
  259. package/dist/extraction-prompt.d.ts +1 -1
  260. package/dist/extraction-source-grounding-rules.d.ts +1 -1
  261. package/dist/extraction-source-grounding.d.ts +1 -1
  262. package/dist/extraction.d.ts +1 -1
  263. package/dist/extraction.js +35 -19
  264. package/dist/fallback-llm.d.ts +14 -1
  265. package/dist/fallback-llm.js +1 -1
  266. package/dist/{first-start-migration-626C5554.js → first-start-migration-CRZCZAJN.js} +11 -3
  267. package/dist/first-start-migration-CRZCZAJN.js.map +1 -0
  268. package/dist/graph-dashboard-diff.d.ts +1 -1
  269. package/dist/graph-dashboard-key.d.ts +1 -1
  270. package/dist/graph-dashboard-parser.d.ts +1 -1
  271. package/dist/graph-edge-reinforcement.d.ts +1 -1
  272. package/dist/graph-path-reconstruction.d.ts +1 -1
  273. package/dist/graph-path-scoring.d.ts +1 -1
  274. package/dist/graph-snapshot.d.ts +1 -1
  275. package/dist/graph.d.ts +1 -1
  276. package/dist/harmonic-retrieval.js +1 -1
  277. package/dist/importance.d.ts +1 -1
  278. package/dist/importers/index.d.ts +1 -1
  279. package/dist/in-flight-reads.d.ts +1 -1
  280. package/dist/index.d.ts +314 -308
  281. package/dist/index.js +304 -133
  282. package/dist/inline-script.d.ts +3 -0
  283. package/dist/inline-script.js +8 -0
  284. package/dist/inline-script.js.map +1 -0
  285. package/dist/intent.d.ts +1 -1
  286. package/dist/lcm/engine.d.ts +1 -1
  287. package/dist/lcm/engine.js +3 -3
  288. package/dist/lcm/index.d.ts +1 -1
  289. package/dist/lcm/index.js +6 -6
  290. package/dist/lcm/tools.d.ts +1 -1
  291. package/dist/lifecycle.d.ts +1 -1
  292. package/dist/live-connectors-runner.d.ts +1 -1
  293. package/dist/local-llm.d.ts +2 -1
  294. package/dist/local-llm.js +2 -2
  295. package/dist/local-model-endpoint.d.ts +1 -1
  296. package/dist/maintenance/memory-governance.d.ts +1 -1
  297. package/dist/maintenance/memory-governance.js +33 -12
  298. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
  299. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +33 -12
  300. package/dist/maintenance/rebuild-memory-projection.d.ts +2 -2
  301. package/dist/maintenance/rebuild-memory-projection.js +34 -13
  302. package/dist/{maintenance-BJzNIKBE.d.ts → maintenance-C8aRWfhq.d.ts} +3 -3
  303. package/dist/mcp-memory-inspector-app.d.ts +22 -21
  304. package/dist/mcp-read-only-tools.js +1 -1
  305. package/dist/memory-action-policy.d.ts +1 -1
  306. package/dist/memory-cache.d.ts +1 -1
  307. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  308. package/dist/memory-projection-mutations.js +2 -2
  309. package/dist/memory-projection-store.d.ts +6 -2
  310. package/dist/memory-projection-store.js +1 -1
  311. package/dist/memory-provenance.d.ts +1 -1
  312. package/dist/memory-snapshot.d.ts +1 -1
  313. package/dist/memory-worth-outcomes.d.ts +2 -2
  314. package/dist/models-json.d.ts +1 -1
  315. package/dist/namespaces/migrate.d.ts +3 -3
  316. package/dist/namespaces/migrate.js +24 -23
  317. package/dist/namespaces/principal.d.ts +1 -1
  318. package/dist/namespaces/search.d.ts +1 -1
  319. package/dist/namespaces/search.js +8 -8
  320. package/dist/namespaces/storage.d.ts +3 -3
  321. package/dist/namespaces/storage.js +33 -12
  322. package/dist/native-knowledge.d.ts +1 -1
  323. package/dist/offline-sync-file-io.d.ts +20 -0
  324. package/dist/offline-sync-file-io.js +18 -0
  325. package/dist/offline-sync-file-io.js.map +1 -0
  326. package/dist/offline-sync-impression-drain.d.ts +1 -1
  327. package/dist/offline-sync-impression-drain.js +5 -5
  328. package/dist/offline-sync.d.ts +8 -6
  329. package/dist/offline-sync.js +2 -1
  330. package/dist/operator-doctor-corpus.d.ts +1 -1
  331. package/dist/operator-doctor-corpus.js +26 -25
  332. package/dist/operator-doctor-replica.d.ts +1 -1
  333. package/dist/operator-toolkit.d.ts +3 -3
  334. package/dist/operator-toolkit.js +39 -36
  335. package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
  336. package/dist/orchestration/maintenance.d.ts +4 -4
  337. package/dist/orchestration/maintenance.js +38 -18
  338. package/dist/{orchestrator-BiXgBJBQ.d.ts → orchestrator-D85CzDDY.d.ts} +11 -8
  339. package/dist/orchestrator.d.ts +9 -9
  340. package/dist/orchestrator.js +117 -109
  341. package/dist/patterns-cli.d.ts +1 -1
  342. package/dist/{pipeline-sz_dcuLg.d.ts → pipeline-DPcUifCO.d.ts} +1 -1
  343. package/dist/policy-runtime.d.ts +1 -1
  344. package/dist/proactive-contention.d.ts +1 -1
  345. package/dist/provenance.d.ts +1 -1
  346. package/dist/public-http-D-SyWNj6.d.ts +25 -0
  347. package/dist/{purge-533FZLS4.js → purge-GB7PZHT2.js} +2 -2
  348. package/dist/{qmd-eAWObK4M.d.ts → qmd-dFCHp1r0.d.ts} +1 -1
  349. package/dist/qmd-preflight.d.ts +2 -2
  350. package/dist/qmd-recall-cache.d.ts +1 -1
  351. package/dist/qmd.d.ts +2 -2
  352. package/dist/recall-concurrency-config.d.ts +1 -1
  353. package/dist/recall-disclosure-escalation.d.ts +1 -1
  354. package/dist/recall-explain-renderer.d.ts +1 -1
  355. package/dist/recall-explain-renderer.js +3 -3
  356. package/dist/recall-memory-map.d.ts +1 -1
  357. package/dist/recall-planner-llm.d.ts +1 -1
  358. package/dist/recall-planner-llm.js +1 -1
  359. package/dist/recall-state.d.ts +1 -1
  360. package/dist/recall-tag-filter.d.ts +1 -1
  361. package/dist/recall-timings.d.ts +1 -1
  362. package/dist/recall-xray-cli.d.ts +1 -1
  363. package/dist/recall-xray-cli.js +4 -4
  364. package/dist/recall-xray-renderer.d.ts +1 -1
  365. package/dist/recall-xray-renderer.js +3 -3
  366. package/dist/recall-xray.d.ts +1 -1
  367. package/dist/recall-xray.js +2 -2
  368. package/dist/reconcile/cursor.d.ts +2 -1
  369. package/dist/reconcile/manifest.d.ts +2 -1
  370. package/dist/reconcile/manifest.js +6 -5
  371. package/dist/reconcile/plan.d.ts +2 -1
  372. package/dist/reconcile/plan.js +3 -2
  373. package/dist/replay/normalizers/chatgpt.d.ts +1 -1
  374. package/dist/replay/normalizers/claude.d.ts +1 -1
  375. package/dist/replay/normalizers/openclaw.d.ts +1 -1
  376. package/dist/replay/normalizers/shared.d.ts +1 -1
  377. package/dist/replay/runner.d.ts +1 -1
  378. package/dist/replay/types.d.ts +1 -1
  379. package/dist/replica-divergence.d.ts +1 -1
  380. package/dist/replica-peers-config.d.ts +1 -1
  381. package/dist/resolve-auth-token.d.ts +1 -1
  382. package/dist/resume-bundles.js +5 -3
  383. package/dist/retrieval-agents.d.ts +2 -2
  384. package/dist/retrieval-tiers.d.ts +1 -1
  385. package/dist/routing/engine.d.ts +1 -1
  386. package/dist/routing/store.d.ts +1 -1
  387. package/dist/salvage-envelope.d.ts +1 -1
  388. package/dist/schemas.d.ts +38 -38
  389. package/dist/{scope-profiles-BBQsXuyj.d.ts → scope-profiles-DICK7GtN.d.ts} +1 -1
  390. package/dist/search/embed-helper.d.ts +1 -1
  391. package/dist/search/factory.d.ts +1 -1
  392. package/dist/search/factory.js +6 -6
  393. package/dist/search/index.d.ts +1 -1
  394. package/dist/search/index.js +12 -12
  395. package/dist/search/lancedb-backend.d.ts +1 -1
  396. package/dist/search/meilisearch-backend.d.ts +1 -1
  397. package/dist/search/noop-backend.d.ts +1 -1
  398. package/dist/search/orama-backend.d.ts +1 -1
  399. package/dist/search/port.d.ts +1 -1
  400. package/dist/search/remote-backend.d.ts +1 -1
  401. package/dist/{semantic-consolidation-HD8RkzVA.d.ts → semantic-consolidation-BP7NAX0N.d.ts} +1 -1
  402. package/dist/semantic-consolidation.d.ts +2 -2
  403. package/dist/semantic-consolidation.js +34 -13
  404. package/dist/semantic-rule-promotion.js +33 -12
  405. package/dist/semantic-rule-verifier.d.ts +1 -1
  406. package/dist/semantic-rule-verifier.js +33 -12
  407. package/dist/{service-DIrtHHEg.d.ts → service-DT-pJyda.d.ts} +660 -660
  408. package/dist/session-observer-bands.d.ts +1 -1
  409. package/dist/session-observer-state.d.ts +1 -1
  410. package/dist/shared-context/manager.d.ts +1 -1
  411. package/dist/signal.d.ts +1 -1
  412. package/dist/source-agent-qualifier.d.ts +1 -1
  413. package/dist/source-agent-qualifier.js +31 -15
  414. package/dist/{storage-DqCzXi56.d.ts → storage-qOnaJ1Lw.d.ts} +49 -13
  415. package/dist/storage.d.ts +2 -2
  416. package/dist/storage.js +33 -12
  417. package/dist/summarizer.d.ts +1 -1
  418. package/dist/summarizer.js +5 -5
  419. package/dist/summary-snapshot.d.ts +1 -1
  420. package/dist/support-passport/index.d.ts +518 -0
  421. package/dist/support-passport/index.js +357 -0
  422. package/dist/support-passport/index.js.map +1 -0
  423. package/dist/temporal-supersession.d.ts +2 -2
  424. package/dist/temporal-timeline-recall.d.ts +1 -1
  425. package/dist/temporal-validity.d.ts +1 -1
  426. package/dist/threading.d.ts +1 -1
  427. package/dist/tier-migration.d.ts +2 -2
  428. package/dist/tier-routing.d.ts +2 -2
  429. package/dist/tier-routing.js +7 -1
  430. package/dist/{tier-stats-2JCZYEF7.js → tier-stats-EXYSKSVN.js} +8 -2
  431. package/dist/{tier-stats-2JCZYEF7.js.map → tier-stats-EXYSKSVN.js.map} +1 -1
  432. package/dist/topics.d.ts +1 -1
  433. package/dist/transcript.d.ts +1 -1
  434. package/dist/transfer/autodetect.js +1 -1
  435. package/dist/transfer/backup.js +1 -1
  436. package/dist/transfer/capsule-export.js +2 -2
  437. package/dist/transfer/types.d.ts +36 -36
  438. package/dist/trust-score-stage.d.ts +1 -1
  439. package/dist/trust-score.d.ts +1 -1
  440. package/dist/{types-d_j4vC6N.d.ts → types-ed-SvS2t.d.ts} +8 -1
  441. package/dist/types.d.ts +1 -1
  442. package/dist/types.js +1 -1
  443. package/dist/utility-runtime.d.ts +2 -2
  444. package/dist/verified-recall.js +33 -12
  445. package/dist/write-envelope.d.ts +1 -1
  446. package/dist/write-rate-limiter.d.ts +11 -5
  447. package/dist/write-rate-limiter.js +1 -1
  448. package/package.json +12 -2
  449. package/scripts/build-with-heap.mjs +11 -1
  450. package/src/access-authorization-probe.ts +20 -2
  451. package/src/access-boundary.ts +32 -0
  452. package/src/access-http.test.ts +1971 -643
  453. package/src/access-http.ts +81 -74
  454. package/src/access-mcp.ts +3 -1
  455. package/src/access-memory-search-fanout.ts +24 -1
  456. package/src/access-offline-manifest.ts +5 -0
  457. package/src/access-operations.ts +2 -0
  458. package/src/access-service-namespace.test.ts +14 -0
  459. package/src/access-service-observe-scope.test.ts +2 -0
  460. package/src/access-service-offline-file-content.test.ts +211 -0
  461. package/src/access-service.ts +628 -884
  462. package/src/access-surface-catalog.test.ts +40 -10
  463. package/src/access-surface-catalog.ts +15 -0
  464. package/src/active-memory-bridge.test.ts +151 -0
  465. package/src/active-memory-bridge.ts +95 -21
  466. package/src/briefing.test.ts +38 -0
  467. package/src/briefing.ts +3 -3
  468. package/src/cli.ts +6 -1
  469. package/src/config.ts +18 -9
  470. package/src/connectors/codex-materialize-runner.ts +2 -0
  471. package/src/contradiction/contradiction-scan.ts +2 -0
  472. package/src/contradiction/contradiction.test.ts +42 -0
  473. package/src/correction/correction-access-wiring.ts +8 -2
  474. package/src/correction/correction-storage-integration.test.ts +7 -0
  475. package/src/explicit-capture.test.ts +24 -0
  476. package/src/explicit-capture.ts +2 -0
  477. package/src/fallback-llm.test.ts +54 -0
  478. package/src/fallback-llm.ts +54 -9
  479. package/src/index.ts +3 -0
  480. package/src/inline-script.ts +6 -0
  481. package/src/local-llm-headers-timeout.test.ts +42 -0
  482. package/src/local-llm.ts +8 -8
  483. package/src/maintenance/first-start-migration.ts +2 -1
  484. package/src/maintenance/memory-governance.test.ts +25 -0
  485. package/src/maintenance/memory-governance.ts +4 -1
  486. package/src/maintenance/pattern-reinforcement.test.ts +20 -0
  487. package/src/maintenance/pattern-reinforcement.ts +2 -1
  488. package/src/maintenance/rebuild-memory-projection.ts +8 -8
  489. package/src/mcp-read-only-tools.ts +3 -0
  490. package/src/memory-projection-store.ts +25 -27
  491. package/src/offline-sync-file-io.ts +68 -0
  492. package/src/offline-sync.ts +43 -57
  493. package/src/orchestration/consolidation-run.ts +5 -4
  494. package/src/orchestration/contradiction-linking-coordinator.ts +7 -0
  495. package/src/orchestration/contradiction-localization.integration.test.ts +115 -0
  496. package/src/orchestration/lifecycle-policy-coordinator.ts +2 -1
  497. package/src/orchestration/qmd-result-resolver.test.ts +215 -14
  498. package/src/orchestration/qmd-result-resolver.ts +142 -82
  499. package/src/orchestration/recall-search-pipeline.test.ts +41 -0
  500. package/src/orchestration/recall-search-pipeline.ts +12 -0
  501. package/src/orchestration/semantic-consolidation-coordinator.ts +2 -1
  502. package/src/orchestration/tier-migration-coordinator.ts +23 -56
  503. package/src/orchestration/workspace-ops.test.ts +37 -0
  504. package/src/orchestration/workspace-ops.ts +5 -29
  505. package/src/orchestrator.ts +7 -8
  506. package/src/relay/mission-access.test.ts +11 -11
  507. package/src/relay/mission-ui.test.ts +3 -1
  508. package/src/storage/supersession-audit.ts +7 -1
  509. package/src/storage/supersession-side-effects.ts +17 -5
  510. package/src/storage/tombstone-blocked-capture-sync.ts +31 -0
  511. package/src/storage-contract/failure-semantics.test.ts +102 -0
  512. package/src/storage.ts +18 -19
  513. package/src/support-passport/access-http-base.ts +143 -0
  514. package/src/support-passport/access-http.ts +172 -0
  515. package/src/support-passport/access-operations.ts +272 -0
  516. package/src/support-passport/access-service-base.test.ts +193 -0
  517. package/src/support-passport/access-service-base.ts +176 -0
  518. package/src/support-passport/access-surface.test.ts +283 -0
  519. package/src/support-passport/access-surface.ts +286 -0
  520. package/src/support-passport/card-projection.test.ts +137 -0
  521. package/src/support-passport/card-projection.ts +348 -0
  522. package/src/support-passport/card-replacement.ts +126 -0
  523. package/src/support-passport/card-service-commit-forwarding.test.ts +160 -0
  524. package/src/support-passport/card-service.test.ts +2964 -0
  525. package/src/support-passport/card-service.ts +1184 -0
  526. package/src/support-passport/card-state.ts +158 -0
  527. package/src/support-passport/config-model-routing.test.ts +37 -0
  528. package/src/support-passport/config.test.ts +34 -0
  529. package/src/support-passport/config.ts +41 -0
  530. package/src/support-passport/contracts.test.ts +156 -0
  531. package/src/support-passport/contracts.ts +149 -0
  532. package/src/support-passport/errors.ts +30 -0
  533. package/src/support-passport/generated-batch.test.ts +185 -0
  534. package/src/support-passport/generated-batch.ts +338 -0
  535. package/src/support-passport/grant-contracts.ts +183 -0
  536. package/src/support-passport/grant-generated-batch-scope.test.ts +201 -0
  537. package/src/support-passport/grant-service-cache.test.ts +148 -0
  538. package/src/support-passport/grant-service-cancellation.test.ts +83 -0
  539. package/src/support-passport/grant-service-marker-cache.test.ts +62 -0
  540. package/src/support-passport/grant-service.test.ts +3496 -0
  541. package/src/support-passport/grant-service.ts +513 -0
  542. package/src/support-passport/grant-store-commit-hook.test.ts +123 -0
  543. package/src/support-passport/grant-store-order.test.ts +60 -0
  544. package/src/support-passport/grant-store-path-safety.test.ts +30 -0
  545. package/src/support-passport/grant-store-recovery.test.ts +448 -0
  546. package/src/support-passport/grant-store.ts +906 -0
  547. package/src/support-passport/index.ts +17 -0
  548. package/src/support-passport/mcp-tools.test.ts +222 -0
  549. package/src/support-passport/mcp-tools.ts +187 -0
  550. package/src/support-passport/model-adapter.test.ts +840 -0
  551. package/src/support-passport/model-adapter.ts +602 -0
  552. package/src/support-passport/model-audit-pinned-write.test.ts +59 -0
  553. package/src/support-passport/model-audit.ts +152 -0
  554. package/src/support-passport/model-bridge.test.ts +456 -0
  555. package/src/support-passport/model-bridge.ts +439 -0
  556. package/src/support-passport/model-contracts.ts +124 -0
  557. package/src/support-passport/model-service.test.ts +1731 -0
  558. package/src/support-passport/model-service.ts +399 -0
  559. package/src/support-passport/offline-sync-guard.ts +230 -0
  560. package/src/support-passport/owner-http.test.ts +671 -0
  561. package/src/support-passport/owner-lock.ts +70 -0
  562. package/src/support-passport/private-file.test.ts +99 -0
  563. package/src/support-passport/private-file.ts +498 -0
  564. package/src/support-passport/public-http.test.ts +476 -0
  565. package/src/support-passport/public-http.ts +601 -0
  566. package/src/support-passport/ui.test.ts +181 -0
  567. package/src/tier-routing.ts +28 -17
  568. package/src/training-export/converter.test.ts +24 -0
  569. package/src/training-export/converter.ts +2 -0
  570. package/src/types.ts +8 -1
  571. package/src/wearables/service.test.ts +13 -0
  572. package/src/wearables/service.ts +2 -0
  573. package/src/write-rate-limiter.test.ts +30 -6
  574. package/src/write-rate-limiter.ts +29 -18
  575. package/dist/chunk-3CSX3ETE.js.map +0 -1
  576. package/dist/chunk-47P3RYZP.js.map +0 -1
  577. package/dist/chunk-6LAPLZOS.js.map +0 -1
  578. package/dist/chunk-6R4G7N4S.js.map +0 -1
  579. package/dist/chunk-7BZOBA5J.js.map +0 -1
  580. package/dist/chunk-C23BYTJV.js.map +0 -1
  581. package/dist/chunk-CYF6WVE7.js.map +0 -1
  582. package/dist/chunk-G2LUMAUF.js.map +0 -1
  583. package/dist/chunk-GYPQL6XN.js.map +0 -1
  584. package/dist/chunk-JDZPNCMA.js.map +0 -1
  585. package/dist/chunk-JV5YUBIZ.js.map +0 -1
  586. package/dist/chunk-K4QS7LXU.js.map +0 -1
  587. package/dist/chunk-L6CQ7WIW.js.map +0 -1
  588. package/dist/chunk-MKQBXQKU.js.map +0 -1
  589. package/dist/chunk-NH5IR2WM.js.map +0 -1
  590. package/dist/chunk-PX2LSHYM.js.map +0 -1
  591. package/dist/chunk-QWGE2PUH.js.map +0 -1
  592. package/dist/chunk-RUMNY5DT.js.map +0 -1
  593. package/dist/chunk-S75M5ZRK.js.map +0 -1
  594. package/dist/chunk-T2JHZWH6.js.map +0 -1
  595. package/dist/chunk-UES4BHA2.js.map +0 -1
  596. package/dist/chunk-UTRO5UJU.js.map +0 -1
  597. package/dist/chunk-UU3G6GUP.js.map +0 -1
  598. package/dist/chunk-XUAMCLY4.js.map +0 -1
  599. package/dist/chunk-ZYLC7NFO.js.map +0 -1
  600. package/dist/contradiction-scan-HYLIRMJ2.js +0 -14
  601. package/dist/first-start-migration-626C5554.js.map +0 -1
  602. /package/dist/{auto-sync-TDHLZPVP.js.map → auto-sync-NR3GPCOA.js.map} +0 -0
  603. /package/dist/{chunk-LLJMAULE.js.map → chunk-2FKEIPIT.js.map} +0 -0
  604. /package/dist/{chunk-JBPKEARU.js.map → chunk-2QSZNTDO.js.map} +0 -0
  605. /package/dist/{chunk-PDDTI7IC.js.map → chunk-43SEBY2M.js.map} +0 -0
  606. /package/dist/{chunk-HWRD23DW.js.map → chunk-46SZBVDS.js.map} +0 -0
  607. /package/dist/{chunk-6HPHEFXT.js.map → chunk-5FD7JMTX.js.map} +0 -0
  608. /package/dist/{chunk-IBKR2TFR.js.map → chunk-6CYHUWCO.js.map} +0 -0
  609. /package/dist/{chunk-YN6ASZUG.js.map → chunk-6QPI2ITJ.js.map} +0 -0
  610. /package/dist/{chunk-NIS256NW.js.map → chunk-CIAI24TQ.js.map} +0 -0
  611. /package/dist/{chunk-7FGL6CGA.js.map → chunk-DVYRD4HY.js.map} +0 -0
  612. /package/dist/{chunk-G7Z6EENO.js.map → chunk-EKTP7MNS.js.map} +0 -0
  613. /package/dist/{chunk-RZ4XGKOJ.js.map → chunk-FFRTNF3L.js.map} +0 -0
  614. /package/dist/{chunk-VKXWK44Q.js.map → chunk-FONWBCVY.js.map} +0 -0
  615. /package/dist/{chunk-DZD576ZU.js.map → chunk-FPLM2TKD.js.map} +0 -0
  616. /package/dist/{chunk-ZPJ4QSGP.js.map → chunk-FTPONF6B.js.map} +0 -0
  617. /package/dist/{chunk-LJS7KUPV.js.map → chunk-FWX7W5PM.js.map} +0 -0
  618. /package/dist/{chunk-G4T7L557.js.map → chunk-GN3WRFJY.js.map} +0 -0
  619. /package/dist/{chunk-QT2FRYI4.js.map → chunk-H7NAZ3UB.js.map} +0 -0
  620. /package/dist/{chunk-APPFNGX6.js.map → chunk-HBUFQRIM.js.map} +0 -0
  621. /package/dist/{chunk-UU7TRFNE.js.map → chunk-HGJ6NJ4E.js.map} +0 -0
  622. /package/dist/{chunk-C4ZNGVK3.js.map → chunk-IKP5OYQQ.js.map} +0 -0
  623. /package/dist/{chunk-KQAFEZQX.js.map → chunk-IPLYGWQF.js.map} +0 -0
  624. /package/dist/{chunk-RDJRFDJY.js.map → chunk-IQ4NVWEF.js.map} +0 -0
  625. /package/dist/{chunk-CGVHPCP2.js.map → chunk-JYG4OLWH.js.map} +0 -0
  626. /package/dist/{chunk-WOPA3SGI.js.map → chunk-K26NWEOC.js.map} +0 -0
  627. /package/dist/{chunk-G7A5HLUS.js.map → chunk-KJH6W2I7.js.map} +0 -0
  628. /package/dist/{chunk-HOIRB77A.js.map → chunk-L6Z5OAS4.js.map} +0 -0
  629. /package/dist/{chunk-227UFSEQ.js.map → chunk-LEPOP46W.js.map} +0 -0
  630. /package/dist/{chunk-3JLPGJ43.js.map → chunk-MIH3CJPN.js.map} +0 -0
  631. /package/dist/{chunk-QHFV6VNU.js.map → chunk-MK5VEWCR.js.map} +0 -0
  632. /package/dist/{chunk-GUKBW4HL.js.map → chunk-NQMSEB5D.js.map} +0 -0
  633. /package/dist/{chunk-WRJVB3C2.js.map → chunk-PO7NXBG2.js.map} +0 -0
  634. /package/dist/{chunk-AZM4CCAC.js.map → chunk-QZLDRQ4I.js.map} +0 -0
  635. /package/dist/{chunk-THPPCISA.js.map → chunk-RBII7637.js.map} +0 -0
  636. /package/dist/{chunk-VMMQDEN5.js.map → chunk-TO7ZTWJW.js.map} +0 -0
  637. /package/dist/{chunk-K2TDK7GG.js.map → chunk-TWTUGCTY.js.map} +0 -0
  638. /package/dist/{chunk-HLYR2TQL.js.map → chunk-UCTS3DDH.js.map} +0 -0
  639. /package/dist/{chunk-JYIJNNN3.js.map → chunk-UG6VXIOK.js.map} +0 -0
  640. /package/dist/{chunk-WM65LU67.js.map → chunk-UKYS3NU5.js.map} +0 -0
  641. /package/dist/{chunk-WDBX6VER.js.map → chunk-UPLZNJKN.js.map} +0 -0
  642. /package/dist/{chunk-2GFK22OZ.js.map → chunk-V6ERBR7U.js.map} +0 -0
  643. /package/dist/{chunk-ZMYDYNI4.js.map → chunk-XZHE2DEN.js.map} +0 -0
  644. /package/dist/{chunk-MAXVH44E.js.map → chunk-Z54EM5YZ.js.map} +0 -0
  645. /package/dist/{chunk-YGLY6QFV.js.map → chunk-ZRZVLB7O.js.map} +0 -0
  646. /package/dist/{chunk-4IIFL7GC.js.map → chunk-ZUKIUXHQ.js.map} +0 -0
  647. /package/dist/{contradiction-scan-HYLIRMJ2.js.map → contradiction-scan-JDTBXRGW.js.map} +0 -0
  648. /package/dist/{purge-533FZLS4.js.map → purge-GB7PZHT2.js.map} +0 -0
@@ -0,0 +1,906 @@
1
+ import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
2
+ import { type FileHandle, lstat, open, readdir } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import { log } from "../logger.js";
6
+ import { expandTildePath } from "../utils/path.js";
7
+ import { type HeldFileLockController, serializeMutations, withHeldFileLock } from "../utils/serialize-mutations.js";
8
+ import { computeSupportPassportOwnerKey } from "./card-projection.js";
9
+ import { SupportPassportNamespaceSchema } from "./contracts.js";
10
+ import { SupportPassportError } from "./errors.js";
11
+ import {
12
+ SupportPassportCreateGrantInputSchema,
13
+ type SupportPassportGrantCardRef,
14
+ type SupportPassportGrantState,
15
+ SupportPassportGrantStateSchema,
16
+ } from "./grant-contracts.js";
17
+ import { computeSupportPassportOwnerLockKey } from "./owner-lock.js";
18
+ import {
19
+ ensurePrivateDirectoryNoFollow,
20
+ ensurePrivateDirectoryTreeNoFollow,
21
+ readPrivateFileNoFollow,
22
+ removePrivateFilesNoFollow,
23
+ withPrivateDirectoryNoFollow,
24
+ writePrivateFileAtomicallyNoFollow,
25
+ } from "./private-file.js";
26
+
27
+ const GRANT_LOCK_STALE_MS = 30_000;
28
+ const GRANT_LOCK_WAIT_MS = 5_000;
29
+ const GRANT_LOCK_HEARTBEAT_MS = 10_000;
30
+ const SAFE_GRANT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
31
+ const UUID_INPUT = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
32
+ const SAFE_OWNER_INDEX_HASH = /^[0-9a-f]{64}$/;
33
+ const UNSUPPORTED_DIRECTORY_SYNC_ERRORS = new Set(["EINVAL", "ENOSYS", "ENOTSUP", "EOPNOTSUPP"]);
34
+ const MAX_OWNER_GRANT_HISTORY = 100;
35
+ const MAX_OWNER_INDEX_RECOVERY_ATTEMPTS = 3;
36
+
37
+ class OwnerIndexLockLostError extends Error {
38
+ constructor(cause: unknown) {
39
+ super("support passport owner index lock was lost", { cause });
40
+ this.name = "OwnerIndexLockLostError";
41
+ }
42
+ }
43
+
44
+ class MalformedGrantStateError extends Error {
45
+ constructor(message = "support passport grant state is invalid", cause?: unknown) {
46
+ super(message, { cause });
47
+ this.name = "MalformedGrantStateError";
48
+ }
49
+ }
50
+
51
+ export interface SupportPassportGrantStoreOptions {
52
+ memoryDir: string;
53
+ now?: () => Date;
54
+ makeSecret?: () => Buffer;
55
+ makeGrantId?: () => string;
56
+ withHeldFileLock?: typeof withHeldFileLock;
57
+ syncDirectory?: typeof syncDirectoryForDurability;
58
+ }
59
+
60
+ export interface CreateStoredGrantInput {
61
+ namespace: string;
62
+ principal: string;
63
+ cards: SupportPassportGrantCardRef[];
64
+ expiresAt: string;
65
+ requestedAt?: Date;
66
+ }
67
+
68
+ export interface SupportPassportGrantMutationHooks {
69
+ beforeCommit?: () => Promise<void>;
70
+ onCommitted?: () => void | Promise<void>;
71
+ }
72
+
73
+ type SupportPassportGrantCreateHooks = SupportPassportGrantMutationHooks | (() => Promise<void>);
74
+ type SupportPassportGrantRevokeHooks = SupportPassportGrantMutationHooks | (() => Promise<void>);
75
+
76
+ function sha256(domain: string, value: string): string {
77
+ return createHash("sha256").update(domain).update("\0").update(value).digest("hex");
78
+ }
79
+
80
+ function hashesMatch(left: string, right: string): boolean {
81
+ const leftBytes = Buffer.from(left, "hex");
82
+ const rightBytes = Buffer.from(right, "hex");
83
+ return leftBytes.length === 32 && rightBytes.length === 32 && timingSafeEqual(leftBytes, rightBytes);
84
+ }
85
+
86
+ function sameGrantState(left: SupportPassportGrantState, right: SupportPassportGrantState): boolean {
87
+ return JSON.stringify(left) === JSON.stringify(right);
88
+ }
89
+
90
+ function newestGrantFirst(left: SupportPassportGrantState, right: SupportPassportGrantState): number {
91
+ return Date.parse(right.createdAt) - Date.parse(left.createdAt) || left.grantId.localeCompare(right.grantId);
92
+ }
93
+
94
+ function grantNotFound(): SupportPassportError {
95
+ return new SupportPassportError("grant_not_found", "The share link was not found.", 404);
96
+ }
97
+
98
+ function warnCommitNotificationFailure(error: unknown): void {
99
+ log.warn(`support passport commit notification failed: ${error instanceof Error ? error.message : String(error)}`);
100
+ }
101
+
102
+ export function notifySupportPassportCommitted(callback: (() => void | Promise<void>) | undefined): void {
103
+ try {
104
+ const completion = callback?.();
105
+ if (completion) void Promise.resolve(completion).catch(warnCommitNotificationFailure);
106
+ } catch (error) {
107
+ warnCommitNotificationFailure(error);
108
+ }
109
+ }
110
+
111
+ function normalizeGrantId(grantId: unknown): string {
112
+ if (typeof grantId !== "string" || !UUID_INPUT.test(grantId)) throw grantNotFound();
113
+ return grantId.toLowerCase();
114
+ }
115
+
116
+ function normalizeNamespace(namespace: unknown): string {
117
+ const parsed = SupportPassportNamespaceSchema.safeParse(namespace);
118
+ if (!parsed.success) {
119
+ throw new SupportPassportError("invalid_input", "The share link request is invalid.", 400);
120
+ }
121
+ return parsed.data;
122
+ }
123
+
124
+ function normalizePrincipal(principal: unknown): string {
125
+ const normalized = typeof principal === "string" ? principal.trim() : "";
126
+ if (normalized.length < 1 || normalized.length > 512) {
127
+ throw new SupportPassportError("invalid_input", "The share link request is invalid.", 400);
128
+ }
129
+ return normalized;
130
+ }
131
+
132
+ export async function syncDirectoryForDurability(
133
+ directory: string,
134
+ openDirectory: (directory: string) => Promise<Pick<FileHandle, "sync" | "close">> = async (target) =>
135
+ await open(target, "r")
136
+ ): Promise<void> {
137
+ let handle: Pick<FileHandle, "sync" | "close"> | undefined;
138
+ try {
139
+ handle = await openDirectory(directory);
140
+ await handle.sync();
141
+ } catch (error) {
142
+ const code = (error as NodeJS.ErrnoException).code;
143
+ if (!UNSUPPORTED_DIRECTORY_SYNC_ERRORS.has(code ?? "")) throw error;
144
+ } finally {
145
+ await handle?.close().catch(() => undefined);
146
+ }
147
+ }
148
+
149
+ export class SupportPassportGrantStore {
150
+ private memoryDir: string;
151
+ private grantsDir: string;
152
+ private ownerIndexesDir: string;
153
+ private memoryRootReady?: Promise<void>;
154
+ private safeDirectoriesReady?: Promise<void>;
155
+ private readonly now: () => Date;
156
+ private readonly makeSecret: () => Buffer;
157
+ private readonly makeGrantId: () => string;
158
+ private readonly runWithHeldFileLock: typeof withHeldFileLock;
159
+ private readonly syncDirectory: typeof syncDirectoryForDurability;
160
+
161
+ constructor(options: SupportPassportGrantStoreOptions) {
162
+ this.memoryDir = path.resolve(expandTildePath(options.memoryDir));
163
+ this.grantsDir = path.join(this.memoryDir, "state", "support-passport", "grants");
164
+ this.ownerIndexesDir = path.join(this.grantsDir, "owners");
165
+ this.now = options.now ?? (() => new Date());
166
+ this.makeSecret = options.makeSecret ?? (() => randomBytes(32));
167
+ this.makeGrantId = options.makeGrantId ?? randomUUID;
168
+ this.runWithHeldFileLock = options.withHeldFileLock ?? withHeldFileLock;
169
+ this.syncDirectory = options.syncDirectory ?? syncDirectoryForDurability;
170
+ }
171
+
172
+ async create(
173
+ input: CreateStoredGrantInput,
174
+ hooks: SupportPassportGrantCreateHooks = {}
175
+ ): Promise<{ state: SupportPassportGrantState; secret: string }> {
176
+ const mutationHooks = typeof hooks === "function" ? { beforeCommit: hooks } : hooks;
177
+ const namespace = normalizeNamespace(input.namespace);
178
+ const parsed = SupportPassportCreateGrantInputSchema.safeParse({
179
+ principal: input.principal,
180
+ cards: input.cards,
181
+ expiresAt: input.expiresAt,
182
+ });
183
+ if (!parsed.success) {
184
+ throw new SupportPassportError("invalid_input", "The share link request is invalid.", 400);
185
+ }
186
+ const expiresAt = parsed.data.expiresAt;
187
+ if (expiresAt === undefined) {
188
+ throw new SupportPassportError("invalid_input", "The share link request is invalid.", 400);
189
+ }
190
+ const requestedAt = input.requestedAt ?? this.now();
191
+ const durationMs = Date.parse(expiresAt) - requestedAt.getTime();
192
+ if (
193
+ !Number.isFinite(requestedAt.getTime()) ||
194
+ !Number.isFinite(durationMs) ||
195
+ durationMs < 300_000 ||
196
+ durationMs > 604_800_000
197
+ ) {
198
+ throw new SupportPassportError("invalid_input", "The share link request is invalid.", 400);
199
+ }
200
+ const principalHash = computeSupportPassportOwnerKey(parsed.data.principal);
201
+ const ownerHash = this.ownerHash(namespace, principalHash);
202
+ let committed: { state: SupportPassportGrantState; secret: string } | undefined;
203
+ try {
204
+ let finalized: { state: SupportPassportGrantState; secret: string };
205
+ try {
206
+ finalized = await this.withOwnerIndexLock(ownerHash, async (lock) => {
207
+ await this.requireAvailableOwnerGrantSlot(namespace, principalHash);
208
+ committed = await this.withMutationLock(async (mutationLock) => {
209
+ const secretBytes = this.makeSecret();
210
+ if (!Buffer.isBuffer(secretBytes) || secretBytes.length !== 32) {
211
+ throw new Error("SupportPassportGrantStore.makeSecret must return 32 bytes");
212
+ }
213
+ const rawGrantId = this.makeGrantId();
214
+ if (!UUID_INPUT.test(rawGrantId)) {
215
+ throw new Error("SupportPassportGrantStore.makeGrantId must return a UUID");
216
+ }
217
+ const grantId = rawGrantId.toLowerCase();
218
+ const secret = secretBytes.toString("base64url");
219
+ const createdAt = this.now();
220
+ const createdAtMs = createdAt.getTime();
221
+ const expiresAtMs = Date.parse(expiresAt);
222
+ if (
223
+ !Number.isFinite(createdAtMs) ||
224
+ requestedAt.getTime() > createdAtMs ||
225
+ expiresAtMs <= createdAtMs ||
226
+ expiresAtMs - createdAtMs > 604_800_000
227
+ ) {
228
+ throw new SupportPassportError("invalid_input", "The share link request is invalid.", 400);
229
+ }
230
+ const state = SupportPassportGrantStateSchema.parse({
231
+ schemaVersion: 1,
232
+ stateVersion: 1,
233
+ grantId,
234
+ namespace,
235
+ principalHash,
236
+ ownerLockKey: computeSupportPassportOwnerLockKey(namespace, parsed.data.principal),
237
+ secretHash: sha256("support-passport-secret:v1", secret),
238
+ cards: parsed.data.cards,
239
+ createdAt: createdAt.toISOString(),
240
+ expiresAt,
241
+ });
242
+ await this.requireMutationLock(mutationLock);
243
+ await mutationHooks.beforeCommit?.();
244
+ await this.requireMutationLock(mutationLock);
245
+ try {
246
+ await this.writeState(state, true);
247
+ } catch (error) {
248
+ const persisted = await this.readState(state.grantId).catch(() => undefined);
249
+ if (!persisted || !sameGrantState(persisted, state)) throw error;
250
+ try {
251
+ await this.syncDirectory(this.grantsDir);
252
+ } catch {
253
+ await this.removeGrantStates([state.grantId]).catch(() => undefined);
254
+ throw error;
255
+ }
256
+ }
257
+ try {
258
+ await this.writeOwnerMembership(state);
259
+ } catch (error) {
260
+ await this.removeStoredGrant(state).catch(() => undefined);
261
+ throw error;
262
+ }
263
+ const persisted = { state, secret };
264
+ committed = persisted;
265
+ return persisted;
266
+ });
267
+ try {
268
+ await this.addToOwnerIndex(committed.state, lock);
269
+ } catch (error) {
270
+ if (error instanceof OwnerIndexLockLostError) throw error;
271
+ const indexed = await this.readOwnerIndexByHash(ownerHash).catch(() => undefined);
272
+ if (!indexed?.includes(committed.state.grantId)) throw error;
273
+ try {
274
+ await this.syncDirectory(this.ownerIndexesDir);
275
+ } catch {
276
+ throw error;
277
+ }
278
+ await this.requireOwnerIndexLock(lock);
279
+ }
280
+ return committed;
281
+ });
282
+ } catch (error) {
283
+ if (!(error instanceof OwnerIndexLockLostError) || !committed) throw error;
284
+ finalized = await this.reconcileCreateAfterOwnerIndexLockLoss(committed);
285
+ }
286
+ notifySupportPassportCommitted(mutationHooks.onCommitted);
287
+ return finalized;
288
+ } catch (error) {
289
+ if (committed) {
290
+ await this.withGrantLock(committed.state.grantId, async (lock) => {
291
+ await this.requireMutationLock(lock);
292
+ await this.removeStoredGrant(committed!.state);
293
+ }).catch(() => undefined);
294
+ }
295
+ throw error;
296
+ }
297
+ }
298
+
299
+ async authenticate(grantId: string, secret: string): Promise<SupportPassportGrantState> {
300
+ const normalizedGrantId = normalizeGrantId(grantId);
301
+ if (typeof secret !== "string" || secret.length > 512) throw grantNotFound();
302
+ let state: SupportPassportGrantState;
303
+ try {
304
+ state = await this.readState(normalizedGrantId);
305
+ } catch (error) {
306
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") throw grantNotFound();
307
+ throw error;
308
+ }
309
+ const candidate = sha256("support-passport-secret:v1", secret);
310
+ if (!hashesMatch(candidate, state.secretHash)) throw grantNotFound();
311
+ if (state.revokedAt) {
312
+ throw new SupportPassportError("grant_gone", "The share link is no longer active.", 410);
313
+ }
314
+ if (Date.parse(state.expiresAt) <= this.now().getTime()) {
315
+ throw new SupportPassportError("grant_expired", "The share link has expired.", 410);
316
+ }
317
+ return state;
318
+ }
319
+
320
+ async withAuthenticatedGrant<T>(
321
+ grantId: string,
322
+ secret: string,
323
+ task: (state: SupportPassportGrantState) => Promise<T>,
324
+ beforeReturn?: (state: SupportPassportGrantState) => Promise<void>
325
+ ): Promise<T> {
326
+ return await this.withGrantLock(grantId, async (lock) => {
327
+ const state = await this.authenticate(grantId, secret);
328
+ const result = await task(state);
329
+ await this.requireMutationLock(lock);
330
+ const finalState = await this.authenticate(grantId, secret);
331
+ await this.requireMutationLock(lock);
332
+ if (finalState.stateVersion !== state.stateVersion) {
333
+ throw new SupportPassportError("grant_stale", "The shared support guide has changed.", 410);
334
+ }
335
+ this.requireActiveState(finalState);
336
+ await beforeReturn?.(finalState);
337
+ await this.requireMutationLock(lock);
338
+ this.requireActiveState(finalState);
339
+ return result;
340
+ });
341
+ }
342
+
343
+ async listForOwner(namespace: string, principal: string): Promise<SupportPassportGrantState[]> {
344
+ const normalizedNamespace = normalizeNamespace(namespace);
345
+ const principalHash = computeSupportPassportOwnerKey(normalizePrincipal(principal));
346
+ const activeCutoff = this.now().getTime();
347
+ const states = this.listedOwnerStates(
348
+ await this.readOwnerMembershipStates(normalizedNamespace, principalHash),
349
+ activeCutoff
350
+ );
351
+ return states.sort((a, b) => {
352
+ const aActive = !a.revokedAt && Date.parse(a.expiresAt) > activeCutoff;
353
+ const bActive = !b.revokedAt && Date.parse(b.expiresAt) > activeCutoff;
354
+ if (aActive !== bActive) return aActive ? -1 : 1;
355
+ return newestGrantFirst(a, b);
356
+ });
357
+ }
358
+
359
+ async revoke(
360
+ input: {
361
+ grantId: string;
362
+ namespace: string;
363
+ principal: string;
364
+ expectedStateVersion?: number;
365
+ },
366
+ hooks: SupportPassportGrantRevokeHooks = {}
367
+ ): Promise<SupportPassportGrantState> {
368
+ const mutationHooks = typeof hooks === "function" ? { beforeCommit: hooks } : hooks;
369
+ const normalizedInput = { ...input, grantId: normalizeGrantId(input.grantId) };
370
+ const namespace = normalizeNamespace(input.namespace);
371
+ return await this.withGrantLock(normalizedInput.grantId, async (lock) => {
372
+ let state: SupportPassportGrantState;
373
+ try {
374
+ state = await this.readState(normalizedInput.grantId);
375
+ } catch (error) {
376
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") throw grantNotFound();
377
+ throw error;
378
+ }
379
+ const principalHash = computeSupportPassportOwnerKey(normalizePrincipal(input.principal));
380
+ if (state.namespace !== namespace || !hashesMatch(state.principalHash, principalHash)) throw grantNotFound();
381
+ if (state.revokedAt) {
382
+ return state;
383
+ }
384
+ if (
385
+ normalizedInput.expectedStateVersion !== undefined &&
386
+ normalizedInput.expectedStateVersion !== state.stateVersion
387
+ ) {
388
+ throw new SupportPassportError("state_conflict", "The share link changed after it was loaded.", 409);
389
+ }
390
+ const revoked = SupportPassportGrantStateSchema.parse({
391
+ ...state,
392
+ stateVersion: state.stateVersion + 1,
393
+ revokedAt: this.now().toISOString(),
394
+ });
395
+ await this.requireMutationLock(lock);
396
+ await mutationHooks.beforeCommit?.();
397
+ await this.requireMutationLock(lock);
398
+ try {
399
+ await this.writeState(revoked);
400
+ } catch (error) {
401
+ const persisted = await this.readState(revoked.grantId).catch(() => undefined);
402
+ if (!persisted || !sameGrantState(persisted, revoked)) throw error;
403
+ await this.syncDirectory(this.grantsDir);
404
+ }
405
+ notifySupportPassportCommitted(mutationHooks.onCommitted);
406
+ return revoked;
407
+ });
408
+ }
409
+
410
+ private filePath(grantId: string): string {
411
+ const normalizedGrantId = normalizeGrantId(grantId);
412
+ return path.join(this.grantsDir, `${normalizedGrantId}.json`);
413
+ }
414
+
415
+ private ownerIndexPath(ownerHash: string): string {
416
+ if (!SAFE_OWNER_INDEX_HASH.test(ownerHash)) throw new Error("support passport owner index hash is invalid");
417
+ return path.join(this.ownerIndexesDir, `${ownerHash}.json`);
418
+ }
419
+
420
+ private ownerHash(namespace: string, principalHash: string): string {
421
+ return sha256("support-passport-owner-index:v1", `${namespace}\0${principalHash}`);
422
+ }
423
+
424
+ private async addToOwnerIndex(state: SupportPassportGrantState, lock: HeldFileLockController): Promise<void> {
425
+ const ownerHash = this.ownerHash(state.namespace, state.principalHash);
426
+ const ownerStates = (await this.readOwnerMembershipStates(state.namespace, state.principalHash))
427
+ .filter((item) => item.grantId !== state.grantId)
428
+ .sort(newestGrantFirst);
429
+ const current = ownerStates.map((item) => item.grantId);
430
+ let retained = current;
431
+ const indexedStates: SupportPassportGrantState[] = [...ownerStates];
432
+ if (current.length >= MAX_OWNER_GRANT_HISTORY) {
433
+ const expiryCutoff = this.now().getTime();
434
+ const active = indexedStates.filter((item) => !item.revokedAt && Date.parse(item.expiresAt) > expiryCutoff);
435
+ if (active.length >= MAX_OWNER_GRANT_HISTORY) {
436
+ throw new SupportPassportError(
437
+ "invalid_input",
438
+ "A support passport can contain at most 100 active share links.",
439
+ 400
440
+ );
441
+ }
442
+ const inactive = indexedStates
443
+ .filter((item) => item.revokedAt || Date.parse(item.expiresAt) <= expiryCutoff)
444
+ .sort(newestGrantFirst);
445
+ retained = [
446
+ ...active.map((item) => item.grantId),
447
+ ...inactive.slice(0, MAX_OWNER_GRANT_HISTORY - active.length - 1).map((item) => item.grantId),
448
+ ];
449
+ }
450
+ const grantIds = [...retained, state.grantId];
451
+ const retainedIds = new Set(grantIds);
452
+ const indexedStateById = new Map(indexedStates.map((indexedState) => [indexedState.grantId, indexedState]));
453
+ const evictedGrantIds = current.filter((grantId) => !retainedIds.has(grantId));
454
+ await this.writeOwnerIndexWhileLocked(ownerHash, grantIds, lock);
455
+ try {
456
+ for (const grantId of evictedGrantIds) {
457
+ await this.withGrantLock(grantId, async (grantLock) => {
458
+ await this.requireMutationLock(grantLock);
459
+ const evictedState = indexedStateById.get(grantId);
460
+ if (evictedState) await this.removeStoredGrant(evictedState);
461
+ else await this.removeGrantStates([grantId]);
462
+ });
463
+ }
464
+ } catch (error) {
465
+ log.warn(
466
+ `support passport could not remove inactive grant state: ${error instanceof Error ? error.message : String(error)}`
467
+ );
468
+ }
469
+ }
470
+
471
+ private async removeGrantStates(grantIds: string[]): Promise<void> {
472
+ if (grantIds.length === 0) return;
473
+ for (const grantId of grantIds) {
474
+ if (!SAFE_GRANT_ID.test(grantId)) throw grantNotFound();
475
+ }
476
+ await removePrivateFilesNoFollow(
477
+ this.grantsDir,
478
+ grantIds.map((grantId) => `${grantId}.json`),
479
+ "support passport grant files must be regular files in a stable directory",
480
+ path.parse(this.memoryDir).root
481
+ );
482
+ }
483
+
484
+ private ownerMembershipDirectory(ownerHash: string): string {
485
+ if (!SAFE_OWNER_INDEX_HASH.test(ownerHash)) throw new Error("support passport owner index hash is invalid");
486
+ return path.join(this.ownerIndexesDir, ownerHash);
487
+ }
488
+
489
+ private async ensureOwnerMembershipDirectory(ownerHash: string): Promise<string> {
490
+ const directory = this.ownerMembershipDirectory(ownerHash);
491
+ await ensurePrivateDirectoryTreeNoFollow(
492
+ directory,
493
+ "support passport owner membership must remain inside the memory directory"
494
+ );
495
+ return directory;
496
+ }
497
+
498
+ private async writeOwnerMembership(state: SupportPassportGrantState): Promise<void> {
499
+ const ownerHash = this.ownerHash(state.namespace, state.principalHash);
500
+ const directory = await this.ensureOwnerMembershipDirectory(ownerHash);
501
+ await writePrivateFileAtomicallyNoFollow(
502
+ directory,
503
+ path.join(directory, `${state.grantId}.json`),
504
+ `${JSON.stringify({ schemaVersion: 1, grantId: state.grantId })}\n`,
505
+ "support passport owner membership must be regular files in a stable directory",
506
+ path.parse(this.memoryDir).root
507
+ );
508
+ }
509
+
510
+ private async removeOwnerMembership(state: SupportPassportGrantState): Promise<void> {
511
+ const ownerHash = this.ownerHash(state.namespace, state.principalHash);
512
+ const directory = await this.ensureOwnerMembershipDirectory(ownerHash);
513
+ await removePrivateFilesNoFollow(
514
+ directory,
515
+ [`${state.grantId}.json`],
516
+ "support passport owner membership must be regular files in a stable directory",
517
+ path.parse(this.memoryDir).root
518
+ );
519
+ }
520
+
521
+ private async removeStoredGrant(state: SupportPassportGrantState): Promise<void> {
522
+ await this.removeGrantStates([state.grantId]);
523
+ await this.removeOwnerMembership(state);
524
+ }
525
+
526
+ private async writeOwnerIndexWhileLocked(
527
+ ownerHash: string,
528
+ grantIds: string[],
529
+ lock: HeldFileLockController
530
+ ): Promise<void> {
531
+ await this.requireOwnerIndexLock(lock);
532
+ await this.writeOwnerIndex(ownerHash, grantIds);
533
+ await this.requireOwnerIndexLock(lock);
534
+ }
535
+
536
+ private async reconcileCreateAfterOwnerIndexLockLoss(committed: {
537
+ state: SupportPassportGrantState;
538
+ secret: string;
539
+ }): Promise<{ state: SupportPassportGrantState; secret: string }> {
540
+ const ownerHash = this.ownerHash(committed.state.namespace, committed.state.principalHash);
541
+ for (let attempt = 1; attempt <= MAX_OWNER_INDEX_RECOVERY_ATTEMPTS; attempt += 1) {
542
+ try {
543
+ return await this.withOwnerIndexLock(
544
+ ownerHash,
545
+ async (lock) => await this.reconcileCommittedGrant(committed, ownerHash, lock)
546
+ );
547
+ } catch (error) {
548
+ if (!(error instanceof OwnerIndexLockLostError)) throw error;
549
+ }
550
+ }
551
+ throw new SupportPassportError("storage_conflict", "The share link store changed during the request.", 409);
552
+ }
553
+
554
+ private async reconcileCommittedGrant(
555
+ committed: { state: SupportPassportGrantState; secret: string },
556
+ ownerHash: string,
557
+ lock: HeldFileLockController
558
+ ): Promise<{ state: SupportPassportGrantState; secret: string }> {
559
+ const persisted = await this.readState(committed.state.grantId).catch(() => undefined);
560
+ if (!persisted || !sameGrantState(persisted, committed.state)) {
561
+ throw new SupportPassportError("storage_conflict", "The share link store changed during the request.", 409);
562
+ }
563
+ const ownerStates = await this.readOwnerMembershipStates(committed.state.namespace, committed.state.principalHash);
564
+ const committedIndex = ownerStates.findIndex((state) => state.grantId === committed.state.grantId);
565
+ if (committedIndex === -1) ownerStates.push(committed.state);
566
+ else ownerStates[committedIndex] = committed.state;
567
+ const activeCutoff = this.now().getTime();
568
+ const active = ownerStates.filter((state) => !state.revokedAt && Date.parse(state.expiresAt) > activeCutoff);
569
+ if (active.length > MAX_OWNER_GRANT_HISTORY) {
570
+ await this.withGrantLock(committed.state.grantId, async (grantLock) => {
571
+ await this.requireMutationLock(grantLock);
572
+ await this.removeStoredGrant(committed.state);
573
+ });
574
+ const committedIndex = ownerStates.findIndex((state) => state.grantId === committed.state.grantId);
575
+ if (committedIndex !== -1) ownerStates.splice(committedIndex, 1);
576
+ }
577
+ const retained = this.retainedOwnerStates(ownerStates, activeCutoff);
578
+ await this.writeOwnerIndexWhileLocked(
579
+ ownerHash,
580
+ retained.map((state) => state.grantId),
581
+ lock
582
+ );
583
+ const retainedIds = new Set(retained.map((state) => state.grantId));
584
+ for (const state of ownerStates) {
585
+ if (retainedIds.has(state.grantId)) continue;
586
+ try {
587
+ await this.withGrantLock(state.grantId, async (grantLock) => {
588
+ await this.requireMutationLock(grantLock);
589
+ await this.removeStoredGrant(state);
590
+ });
591
+ } catch (error) {
592
+ log.warn(
593
+ `support passport could not remove inactive grant state: ${error instanceof Error ? error.message : String(error)}`
594
+ );
595
+ }
596
+ }
597
+ if (active.length > MAX_OWNER_GRANT_HISTORY) {
598
+ throw new SupportPassportError("storage_conflict", "The share link store changed during the request.", 409);
599
+ }
600
+ return committed;
601
+ }
602
+
603
+ private retainedOwnerStates(states: SupportPassportGrantState[], activeCutoff: number): SupportPassportGrantState[] {
604
+ const active = states.filter((state) => !state.revokedAt && Date.parse(state.expiresAt) > activeCutoff);
605
+ if (active.length > MAX_OWNER_GRANT_HISTORY) {
606
+ throw new SupportPassportError("storage_conflict", "The share link store contains too many active links.", 409);
607
+ }
608
+ const inactive = states
609
+ .filter((state) => state.revokedAt || Date.parse(state.expiresAt) <= activeCutoff)
610
+ .sort(newestGrantFirst);
611
+ return [...active, ...inactive.slice(0, MAX_OWNER_GRANT_HISTORY - active.length)];
612
+ }
613
+
614
+ private listedOwnerStates(states: SupportPassportGrantState[], activeCutoff: number): SupportPassportGrantState[] {
615
+ const active = states.filter((state) => !state.revokedAt && Date.parse(state.expiresAt) > activeCutoff);
616
+ const inactive = states
617
+ .filter((state) => state.revokedAt || Date.parse(state.expiresAt) <= activeCutoff)
618
+ .sort(newestGrantFirst);
619
+ return [...active, ...inactive.slice(0, Math.max(0, MAX_OWNER_GRANT_HISTORY - active.length))];
620
+ }
621
+
622
+ private async requireAvailableOwnerGrantSlot(namespace: string, principalHash: string): Promise<void> {
623
+ const activeCutoff = this.now().getTime();
624
+ const active = (await this.readOwnerMembershipStates(namespace, principalHash)).filter(
625
+ (state) => !state.revokedAt && Date.parse(state.expiresAt) > activeCutoff
626
+ );
627
+ if (active.length >= MAX_OWNER_GRANT_HISTORY) {
628
+ throw new SupportPassportError(
629
+ "invalid_input",
630
+ "A support passport can contain at most 100 active share links.",
631
+ 400
632
+ );
633
+ }
634
+ }
635
+
636
+ private async readOwnerMembershipStates(
637
+ namespace: string,
638
+ principalHash: string
639
+ ): Promise<SupportPassportGrantState[]> {
640
+ await this.ensureSafeDirectories();
641
+ const ownerHash = this.ownerHash(namespace, principalHash);
642
+ const directory = await this.ensureOwnerMembershipDirectory(ownerHash);
643
+ const grantIds = await withPrivateDirectoryNoFollow(
644
+ path.parse(this.memoryDir).root,
645
+ directory,
646
+ "support passport owner membership must be regular files in a stable directory",
647
+ async (pinnedDirectory) => {
648
+ const entries = await readdir(pinnedDirectory, { withFileTypes: true });
649
+ return entries
650
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
651
+ .map((entry) => entry.name.slice(0, -5))
652
+ .filter((grantId) => SAFE_GRANT_ID.test(grantId));
653
+ }
654
+ );
655
+ const ownerStates: SupportPassportGrantState[] = [];
656
+ for (const grantId of grantIds) {
657
+ try {
658
+ const state = await this.readState(grantId);
659
+ if (state.namespace === namespace && hashesMatch(state.principalHash, principalHash)) {
660
+ ownerStates.push(state);
661
+ }
662
+ } catch (error) {
663
+ if ((error as NodeJS.ErrnoException).code === "ENOENT" || error instanceof MalformedGrantStateError) continue;
664
+ throw error;
665
+ }
666
+ }
667
+ return ownerStates;
668
+ }
669
+
670
+ private async readOwnerIndexByHash(ownerHash: string): Promise<string[]> {
671
+ await this.ensureSafeDirectories();
672
+ const filePath = this.ownerIndexPath(ownerHash);
673
+ let content: string;
674
+ try {
675
+ content = await readPrivateFileNoFollow(
676
+ this.ownerIndexesDir,
677
+ filePath,
678
+ "support passport owner indexes must be regular files in a stable directory",
679
+ path.parse(this.memoryDir).root
680
+ );
681
+ } catch (error) {
682
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
683
+ throw error;
684
+ }
685
+ const parsed: unknown = JSON.parse(content);
686
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
687
+ throw new Error("support passport owner index is invalid");
688
+ }
689
+ const record = parsed as Record<string, unknown>;
690
+ const grantIds = record.grantIds;
691
+ if (record.schemaVersion !== 1 || record.ownerHash !== ownerHash || !Array.isArray(grantIds)) {
692
+ throw new Error("support passport owner index is invalid");
693
+ }
694
+ if (grantIds.some((grantId) => typeof grantId !== "string" || !SAFE_GRANT_ID.test(grantId))) {
695
+ throw new Error("support passport owner index is invalid");
696
+ }
697
+ const uniqueGrantIds = new Set(grantIds as string[]);
698
+ if (uniqueGrantIds.size !== grantIds.length || uniqueGrantIds.size > MAX_OWNER_GRANT_HISTORY) {
699
+ throw new Error("support passport owner index is invalid");
700
+ }
701
+ return [...uniqueGrantIds];
702
+ }
703
+
704
+ private async writeOwnerIndex(ownerHash: string, grantIds: string[]): Promise<void> {
705
+ const filePath = this.ownerIndexPath(ownerHash);
706
+ try {
707
+ const metadata = await lstat(filePath);
708
+ if (metadata.isSymbolicLink() || !metadata.isFile()) {
709
+ throw new Error("support passport owner indexes must be regular files");
710
+ }
711
+ } catch (error) {
712
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
713
+ }
714
+ await writePrivateFileAtomicallyNoFollow(
715
+ this.ownerIndexesDir,
716
+ filePath,
717
+ `${JSON.stringify({ schemaVersion: 1, ownerHash, grantIds }, null, 2)}\n`,
718
+ "support passport owner indexes must be regular files in a stable directory",
719
+ path.parse(this.memoryDir).root
720
+ );
721
+ }
722
+
723
+ private async readState(grantId: string): Promise<SupportPassportGrantState> {
724
+ await this.ensureSafeDirectories();
725
+ const filePath = this.filePath(grantId);
726
+ const content = await readPrivateFileNoFollow(
727
+ this.grantsDir,
728
+ filePath,
729
+ "support passport grant files must be regular files in a stable directory",
730
+ path.parse(this.memoryDir).root
731
+ );
732
+ try {
733
+ const state = SupportPassportGrantStateSchema.parse(JSON.parse(content));
734
+ if (state.grantId !== grantId) {
735
+ throw new MalformedGrantStateError("support passport grant ID must match its file name");
736
+ }
737
+ return state;
738
+ } catch (error) {
739
+ if (error instanceof MalformedGrantStateError) throw error;
740
+ if (error instanceof SyntaxError || (error as Error).name === "ZodError") {
741
+ throw new MalformedGrantStateError(undefined, error);
742
+ }
743
+ throw error;
744
+ }
745
+ }
746
+
747
+ private async writeState(state: SupportPassportGrantState, requireAbsent = false): Promise<void> {
748
+ const filePath = this.filePath(state.grantId);
749
+ try {
750
+ const metadata = await lstat(filePath);
751
+ if (metadata.isSymbolicLink() || !metadata.isFile())
752
+ throw new Error("support passport grant files must be regular files");
753
+ if (requireAbsent) {
754
+ throw new SupportPassportError("storage_conflict", "A new share link could not be allocated.", 409);
755
+ }
756
+ } catch (error) {
757
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
758
+ }
759
+ await writePrivateFileAtomicallyNoFollow(
760
+ this.grantsDir,
761
+ filePath,
762
+ `${JSON.stringify(state, null, 2)}\n`,
763
+ "support passport grant files must be regular files in a stable directory",
764
+ path.parse(this.memoryDir).root
765
+ );
766
+ }
767
+
768
+ private async ensureSafeDirectories(): Promise<void> {
769
+ if (!this.safeDirectoriesReady) {
770
+ this.safeDirectoriesReady = (async () => {
771
+ await this.ensureMemoryRoot();
772
+ await ensurePrivateDirectoryNoFollow(
773
+ this.memoryDir,
774
+ this.ownerIndexesDir,
775
+ "support passport grant directories must remain inside the memory directory",
776
+ undefined,
777
+ true
778
+ );
779
+ })();
780
+ }
781
+ const currentAttempt = this.safeDirectoriesReady;
782
+ try {
783
+ await currentAttempt;
784
+ } catch (error) {
785
+ if (this.safeDirectoriesReady === currentAttempt) this.safeDirectoriesReady = undefined;
786
+ throw error;
787
+ }
788
+ }
789
+
790
+ private async ensureMemoryRoot(): Promise<void> {
791
+ const configuredMemoryDir = this.memoryDir;
792
+ if (!this.memoryRootReady) {
793
+ this.memoryRootReady = (async () => {
794
+ await ensurePrivateDirectoryTreeNoFollow(
795
+ configuredMemoryDir,
796
+ "support passport memory directory must be a stable directory",
797
+ undefined
798
+ );
799
+ this.memoryDir = configuredMemoryDir;
800
+ this.grantsDir = path.join(configuredMemoryDir, "state", "support-passport", "grants");
801
+ this.ownerIndexesDir = path.join(this.grantsDir, "owners");
802
+ })();
803
+ }
804
+ const currentAttempt = this.memoryRootReady;
805
+ try {
806
+ await currentAttempt;
807
+ } catch (error) {
808
+ if (this.memoryRootReady === currentAttempt) this.memoryRootReady = undefined;
809
+ throw error;
810
+ }
811
+ }
812
+
813
+ private async withMutationLock<T>(task: (lock: HeldFileLockController) => Promise<T>): Promise<T> {
814
+ await this.ensureSafeDirectories();
815
+ return await withPrivateDirectoryNoFollow(
816
+ path.parse(this.memoryDir).root,
817
+ this.grantsDir,
818
+ "support passport grant lock directory must remain inside the memory directory",
819
+ async (pinnedDirectory) =>
820
+ await this.withExclusiveLock(
821
+ `support-passport-grants:${this.grantsDir}`,
822
+ path.join(pinnedDirectory, ".grants.lock"),
823
+ task
824
+ )
825
+ );
826
+ }
827
+
828
+ private async withGrantLock<T>(grantId: string, task: (lock: HeldFileLockController) => Promise<T>): Promise<T> {
829
+ const normalizedGrantId = normalizeGrantId(grantId);
830
+ await this.ensureSafeDirectories();
831
+ return await withPrivateDirectoryNoFollow(
832
+ path.parse(this.memoryDir).root,
833
+ this.grantsDir,
834
+ "support passport grant lock directory must remain inside the memory directory",
835
+ async (pinnedDirectory) =>
836
+ await this.withExclusiveLock(
837
+ `support-passport-grant:${this.grantsDir}:${normalizedGrantId}`,
838
+ path.join(pinnedDirectory, `.${normalizedGrantId}.lock`),
839
+ task
840
+ )
841
+ );
842
+ }
843
+
844
+ private async withOwnerIndexLock<T>(
845
+ ownerHash: string,
846
+ task: (lock: HeldFileLockController) => Promise<T>
847
+ ): Promise<T> {
848
+ await this.ensureSafeDirectories();
849
+ return await withPrivateDirectoryNoFollow(
850
+ path.parse(this.memoryDir).root,
851
+ this.ownerIndexesDir,
852
+ "support passport owner lock directory must remain inside the memory directory",
853
+ async (pinnedDirectory) =>
854
+ await this.withExclusiveLock(
855
+ `support-passport-owner:${this.ownerIndexesDir}:${ownerHash}`,
856
+ path.join(pinnedDirectory, `.${ownerHash}.lock`),
857
+ task
858
+ )
859
+ );
860
+ }
861
+
862
+ private async withExclusiveLock<T>(
863
+ serializationKey: string,
864
+ lockPath: string,
865
+ task: (lock: HeldFileLockController) => Promise<T>
866
+ ): Promise<T> {
867
+ return await serializeMutations(serializationKey, () =>
868
+ this.runWithHeldFileLock(
869
+ lockPath,
870
+ {
871
+ staleMs: GRANT_LOCK_STALE_MS,
872
+ maxWaitMs: GRANT_LOCK_WAIT_MS,
873
+ heartbeatMs: GRANT_LOCK_HEARTBEAT_MS,
874
+ },
875
+ async (acquired, lock) => {
876
+ if (!acquired) {
877
+ throw new SupportPassportError("storage_conflict", "The share link store is busy. Try again.", 409);
878
+ }
879
+ return await task(lock);
880
+ }
881
+ )
882
+ );
883
+ }
884
+
885
+ private async requireMutationLock(lock: HeldFileLockController): Promise<void> {
886
+ if (await lock.refresh()) return;
887
+ throw new SupportPassportError("storage_conflict", "The share link store changed during the request.", 409);
888
+ }
889
+
890
+ private async requireOwnerIndexLock(lock: HeldFileLockController): Promise<void> {
891
+ try {
892
+ await this.requireMutationLock(lock);
893
+ } catch (error) {
894
+ throw new OwnerIndexLockLostError(error);
895
+ }
896
+ }
897
+
898
+ private requireActiveState(state: SupportPassportGrantState): void {
899
+ if (state.revokedAt) {
900
+ throw new SupportPassportError("grant_gone", "The share link is no longer active.", 410);
901
+ }
902
+ if (Date.parse(state.expiresAt) <= this.now().getTime()) {
903
+ throw new SupportPassportError("grant_expired", "The share link has expired.", 410);
904
+ }
905
+ }
906
+ }