@noy-db/hub 0.7.0-pre.13 → 0.7.0-pre.14

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 (233) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/README.md +4 -3
  3. package/dist/{api-UE2XQKDN.js → api-JPZT6BDD.js} +1 -2
  4. package/dist/as/index.js +3 -4
  5. package/dist/as/index.js.map +1 -1
  6. package/dist/at/index.js +0 -1
  7. package/dist/attestation/index.js +6 -7
  8. package/dist/attestation/index.js.map +1 -1
  9. package/dist/{backup-R6A4VZRX.js → backup-AKWIMYWU.js} +1 -2
  10. package/dist/{backup-R6A4VZRX.js.map → backup-AKWIMYWU.js.map} +1 -1
  11. package/dist/blobs/index.js +0 -1
  12. package/dist/blobs/index.js.map +1 -1
  13. package/dist/broker/index.js +3 -4
  14. package/dist/broker/index.js.map +1 -1
  15. package/dist/by/index.js +0 -1
  16. package/dist/cargo/index.js +10 -11
  17. package/dist/{chunk-5FUULQ6F.js → chunk-2HVILGVR.js} +6 -6
  18. package/dist/{chunk-DJ3LNNZQ.js → chunk-3LK2XZCW.js} +2 -2
  19. package/dist/{chunk-BWXGSSFX.js → chunk-4SZMZAYN.js} +79 -16
  20. package/dist/chunk-4SZMZAYN.js.map +1 -0
  21. package/dist/{chunk-PQKG5AHH.js → chunk-6X6SEMEE.js} +4 -2
  22. package/dist/{chunk-PQKG5AHH.js.map → chunk-6X6SEMEE.js.map} +1 -1
  23. package/dist/{chunk-E2MYJGSG.js → chunk-AMODH7AU.js} +2 -2
  24. package/dist/{chunk-B2ZSGYSF.js → chunk-B2KCRDJM.js} +3 -3
  25. package/dist/{chunk-TO3EQ7HF.js → chunk-BLN54W3L.js} +2 -2
  26. package/dist/{chunk-IHFXR2VQ.js → chunk-BOBFL5TW.js} +2 -2
  27. package/dist/{chunk-ZVCC36XQ.js → chunk-DRB6IHPS.js} +55 -55
  28. package/dist/{chunk-AJQY44CE.js → chunk-EKVNVJVV.js} +3 -20
  29. package/dist/chunk-EKVNVJVV.js.map +1 -0
  30. package/dist/{chunk-ZR2YMODS.js → chunk-EKZ6VZYF.js} +2 -2
  31. package/dist/{chunk-RTYJRFQO.js → chunk-EWPRPP3M.js} +1 -1
  32. package/dist/{chunk-VGICZPVE.js → chunk-FUJSIP4K.js} +2 -2
  33. package/dist/{chunk-PKJ43CMD.js → chunk-NLOLF6LD.js} +2 -2
  34. package/dist/{chunk-SXCYYMVS.js → chunk-NOSRK3ZZ.js} +5 -5
  35. package/dist/{chunk-COTPDJR5.js → chunk-OMB2ZKIT.js} +8 -8
  36. package/dist/{chunk-Y4MJQ2UG.js → chunk-QQ2YMQZU.js} +2 -2
  37. package/dist/{chunk-URHRIAV6.js → chunk-REDOOY2Q.js} +2 -2
  38. package/dist/chunk-REDOOY2Q.js.map +1 -0
  39. package/dist/{chunk-YCXICMOY.js → chunk-RJ7NN2UR.js} +2 -2
  40. package/dist/{chunk-PDD75NUZ.js → chunk-SMH6SRX7.js} +2 -2
  41. package/dist/{chunk-QHW35ZON.js → chunk-TGPST6FK.js} +2 -2
  42. package/dist/{chunk-BMELYFHU.js → chunk-V2PNPF6J.js} +3 -3
  43. package/dist/{chunk-BERJ4AWW.js → chunk-VGXZT72I.js} +2 -2
  44. package/dist/{chunk-2Z47NMFK.js → chunk-WZW37ZHL.js} +2 -2
  45. package/dist/classified/index.js +1 -2
  46. package/dist/{classified-marker-67HBIJMZ.js → classified-marker-OO65UYXN.js} +3 -5
  47. package/dist/{classified-marker-67HBIJMZ.js.map → classified-marker-OO65UYXN.js.map} +1 -1
  48. package/dist/{collection-facade-4ACKIDDY.js → collection-facade-VOWW732S.js} +3 -4
  49. package/dist/{computed-LSEZYRJS.js → computed-VPOKQK4N.js} +1 -2
  50. package/dist/consent/index.js +0 -1
  51. package/dist/consent/index.js.map +1 -1
  52. package/dist/cover/index.js +0 -1
  53. package/dist/crdt/index.js +0 -1
  54. package/dist/crdt/index.js.map +1 -1
  55. package/dist/custody/index.js +1 -2
  56. package/dist/{dead-filter-M3NZUWBZ.js → dead-filter-OMMVBAKZ.js} +2 -3
  57. package/dist/{dead-filter-M3NZUWBZ.js.map → dead-filter-OMMVBAKZ.js.map} +1 -1
  58. package/dist/debug/index.js +0 -1
  59. package/dist/debug/index.js.map +1 -1
  60. package/dist/{delegation-OLJUWTVA.js → delegation-VRYMQRCF.js} +1 -2
  61. package/dist/{delegation-OLJUWTVA.js.map → delegation-VRYMQRCF.js.map} +1 -1
  62. package/dist/derivations/index.js +3 -4
  63. package/dist/{derive-LY47HG7Q.js → derive-HHBRFS2U.js} +2 -3
  64. package/dist/directory/index.js +0 -1
  65. package/dist/{dispatch-MSTR5VBI.js → dispatch-DC3LQJ3S.js} +6 -7
  66. package/dist/{dispatch-MSTR5VBI.js.map → dispatch-DC3LQJ3S.js.map} +1 -1
  67. package/dist/{dispatch-PDP5TA73.js → dispatch-GGFBWQU7.js} +5 -7
  68. package/dist/{dispatch-PDP5TA73.js.map → dispatch-GGFBWQU7.js.map} +1 -1
  69. package/dist/{enclave-4YBQK77G.js → enclave-VM2YQONI.js} +1 -2
  70. package/dist/{executor-TOLPBHOC.js → executor-5NIMLSKZ.js} +1 -2
  71. package/dist/{executor-YDDC5XGY.js → executor-5XJNV5H5.js} +1 -2
  72. package/dist/{executor-MZJXBWNM.js → executor-AQVXV5GZ.js} +1 -2
  73. package/dist/{export-accessible-LGV6IAFX.js → export-accessible-2LOBM67T.js} +3 -4
  74. package/dist/{extract-partition-RYJZIKQZ.js → extract-partition-AZGSEMIF.js} +3 -4
  75. package/dist/{fanout-sidecar-TSFPXY5C.js → fanout-sidecar-HJIVUPTD.js} +1 -2
  76. package/dist/{fanout-sidecar-TSFPXY5C.js.map → fanout-sidecar-HJIVUPTD.js.map} +1 -1
  77. package/dist/{fence-watcher-ZVSMLKQD.js → fence-watcher-U2PW6JU5.js} +1 -3
  78. package/dist/{fence-watcher-ZVSMLKQD.js.map → fence-watcher-U2PW6JU5.js.map} +1 -1
  79. package/dist/{find-LC7P63MX.js → find-2ABFCZX4.js} +1 -2
  80. package/dist/forget/index.js +0 -1
  81. package/dist/forget/index.js.map +1 -1
  82. package/dist/guards/index.js +0 -1
  83. package/dist/history/index.js +0 -1
  84. package/dist/history/index.js.map +1 -1
  85. package/dist/i18n/index.js +0 -1
  86. package/dist/i18n/index.js.map +1 -1
  87. package/dist/index.js +26 -27
  88. package/dist/index.js.map +1 -1
  89. package/dist/indexing/index.js +0 -1
  90. package/dist/indexing/index.js.map +1 -1
  91. package/dist/introspection/index.js +2 -3
  92. package/dist/{issue-IOXM3RSJ.js → issue-RH4H733Z.js} +1 -2
  93. package/dist/{json-schema-I22JCYCG.js → json-schema-V3M5MUCM.js} +1 -3
  94. package/dist/{json-schema-I22JCYCG.js.map → json-schema-V3M5MUCM.js.map} +1 -1
  95. package/dist/kernel/collection-config.d.ts +34 -0
  96. package/dist/lazy/index.js +0 -1
  97. package/dist/{ledger-HXHIOD36.js → ledger-454SU3VE.js} +1 -2
  98. package/dist/{liberate-4APIV7FR.js → liberate-AAZSP2BT.js} +5 -6
  99. package/dist/{link-set-6RAHEKZN.js → link-set-67CAMUAL.js} +1 -2
  100. package/dist/{managed-secret-P3JQ7ZG4.js → managed-secret-NHRYG7CD.js} +1 -2
  101. package/dist/materialized-views/index.js +2 -3
  102. package/dist/{migrate-cek-25KNTS5U.js → migrate-cek-5ENH64AA.js} +1 -2
  103. package/dist/{migrate-cek-25KNTS5U.js.map → migrate-cek-5ENH64AA.js.map} +1 -1
  104. package/dist/money/index.js +0 -1
  105. package/dist/{noydb-2C6QQJ5S.js → noydb-Y53CQHZK.js} +11 -12
  106. package/dist/overlay-views/index.js +0 -1
  107. package/dist/periods/index.js +1 -2
  108. package/dist/pod/index.js +12 -13
  109. package/dist/{pod-handle-6II7KFBR.js → pod-handle-S7AMCJIF.js} +2 -3
  110. package/dist/{pod-handle-6II7KFBR.js.map → pod-handle-S7AMCJIF.js.map} +1 -1
  111. package/dist/policy/index.js +0 -1
  112. package/dist/portability/index.js +6 -7
  113. package/dist/portability/index.js.map +1 -1
  114. package/dist/{post-register-BPJ36A6D.js → post-register-HIYY4Y5S.js} +4 -5
  115. package/dist/{post-register-BPJ36A6D.js.map → post-register-HIYY4Y5S.js.map} +1 -1
  116. package/dist/{purge-scope-FGNNLWHD.js → purge-scope-4HEFPDOW.js} +1 -3
  117. package/dist/{purge-scope-FGNNLWHD.js.map → purge-scope-4HEFPDOW.js.map} +1 -1
  118. package/dist/query/index.js +0 -1
  119. package/dist/{read-only-facade-CHCKSEPD.js → read-only-facade-BIGO37U3.js} +1 -2
  120. package/dist/reduce/index.js +0 -1
  121. package/dist/reduce/index.js.map +1 -1
  122. package/dist/{register-AEOOGKNO.js → register-7OJWAWT5.js} +3 -4
  123. package/dist/{registry-SKSOUXNF.js → registry-23WF3TGH.js} +1 -2
  124. package/dist/{registry-SHMGHEK4.js → registry-4AYVUNXK.js} +1 -2
  125. package/dist/{registry-I6FP4677.js → registry-5MVJGPXW.js} +1 -2
  126. package/dist/{registry-D2YXKRYC.js → registry-HDINK5QW.js} +4 -5
  127. package/dist/{request-withdrawal-6HJT4ODF.js → request-withdrawal-P5EQATZ7.js} +5 -6
  128. package/dist/{reveal-DDQBDWPM.js → reveal-IMGQ2EE5.js} +1 -2
  129. package/dist/{reveal-DDQBDWPM.js.map → reveal-IMGQ2EE5.js.map} +1 -1
  130. package/dist/{revoke-U7U3FMXU.js → revoke-POHP4GPB.js} +1 -2
  131. package/dist/satellites/index.js +0 -1
  132. package/dist/schema-update/index.js +0 -1
  133. package/dist/sealed-record/index.js +3 -4
  134. package/dist/sealed-record/index.js.map +1 -1
  135. package/dist/search/index.js +1 -2
  136. package/dist/{seed-3RXT4VGN.js → seed-UYR6YG3V.js} +1 -2
  137. package/dist/{seed-3RXT4VGN.js.map → seed-UYR6YG3V.js.map} +1 -1
  138. package/dist/sequence/index.js +0 -1
  139. package/dist/session/index.js +0 -1
  140. package/dist/session/index.js.map +1 -1
  141. package/dist/shadow/index.js +0 -1
  142. package/dist/shadow/index.js.map +1 -1
  143. package/dist/share-link/index.js +0 -1
  144. package/dist/share-link/index.js.map +1 -1
  145. package/dist/{signer-7NK7TMWU.js → signer-5K3WDHOC.js} +1 -2
  146. package/dist/snapshots/index.js +1 -2
  147. package/dist/snapshots/index.js.map +1 -1
  148. package/dist/{stale-2CHKZTMK.js → stale-AU74OPUW.js} +2 -3
  149. package/dist/{storage-WP4M7IFO.js → storage-AVJA5BTW.js} +1 -2
  150. package/dist/store/index.js +0 -1
  151. package/dist/{store-coordination-provider-O2VUEMJH.js → store-coordination-provider-RX2CGWXV.js} +1 -2
  152. package/dist/{store-coordination-provider-O2VUEMJH.js.map → store-coordination-provider-RX2CGWXV.js.map} +1 -1
  153. package/dist/sync/index.js +0 -1
  154. package/dist/sync/index.js.map +1 -1
  155. package/dist/team/index.js +0 -1
  156. package/dist/tiers/index.js +1 -2
  157. package/dist/to/index.js +0 -1
  158. package/dist/to/index.js.map +1 -1
  159. package/dist/transactions/index.js +2 -3
  160. package/dist/transactions/index.js.map +1 -1
  161. package/dist/{ulid-VQW6ERQN.js → ulid-4MKXG325.js} +1 -2
  162. package/dist/util/index.js +0 -1
  163. package/dist/util/index.js.map +1 -1
  164. package/dist/{vault-diff-FE5J35NJ.js → vault-diff-C6WUJFNP.js} +1 -2
  165. package/dist/vault-head/index.js +0 -1
  166. package/dist/vault-head/index.js.map +1 -1
  167. package/dist/{verify-JBXCAGDL.js → verify-KOV2G5D6.js} +1 -2
  168. package/dist/{verify-JBXCAGDL.js.map → verify-KOV2G5D6.js.map} +1 -1
  169. package/dist/{walk-P3E6QSNC.js → walk-ERAJ5RHO.js} +3 -4
  170. package/dist/with-formula/derivations/registry.d.ts +6 -0
  171. package/dist/with-shape/introspection/describe.d.ts +2 -53
  172. package/dist/with-shape/introspection/field-meta.d.ts +86 -0
  173. package/dist/{withdraw-accessible-6Q7NE4EY.js → withdraw-accessible-42D32Z7G.js} +4 -5
  174. package/package.json +7 -3
  175. package/dist/chunk-AJQY44CE.js.map +0 -1
  176. package/dist/chunk-BWXGSSFX.js.map +0 -1
  177. package/dist/chunk-PZ5AY32C.js +0 -10
  178. package/dist/chunk-URHRIAV6.js.map +0 -1
  179. package/dist/withdraw-accessible-6Q7NE4EY.js.map +0 -1
  180. package/dist/zod-HAJM7LMS.js +0 -14763
  181. package/dist/zod-HAJM7LMS.js.map +0 -1
  182. /package/dist/{api-UE2XQKDN.js.map → api-JPZT6BDD.js.map} +0 -0
  183. /package/dist/{chunk-5FUULQ6F.js.map → chunk-2HVILGVR.js.map} +0 -0
  184. /package/dist/{chunk-DJ3LNNZQ.js.map → chunk-3LK2XZCW.js.map} +0 -0
  185. /package/dist/{chunk-E2MYJGSG.js.map → chunk-AMODH7AU.js.map} +0 -0
  186. /package/dist/{chunk-B2ZSGYSF.js.map → chunk-B2KCRDJM.js.map} +0 -0
  187. /package/dist/{chunk-TO3EQ7HF.js.map → chunk-BLN54W3L.js.map} +0 -0
  188. /package/dist/{chunk-IHFXR2VQ.js.map → chunk-BOBFL5TW.js.map} +0 -0
  189. /package/dist/{chunk-ZVCC36XQ.js.map → chunk-DRB6IHPS.js.map} +0 -0
  190. /package/dist/{chunk-ZR2YMODS.js.map → chunk-EKZ6VZYF.js.map} +0 -0
  191. /package/dist/{chunk-RTYJRFQO.js.map → chunk-EWPRPP3M.js.map} +0 -0
  192. /package/dist/{chunk-VGICZPVE.js.map → chunk-FUJSIP4K.js.map} +0 -0
  193. /package/dist/{chunk-PKJ43CMD.js.map → chunk-NLOLF6LD.js.map} +0 -0
  194. /package/dist/{chunk-SXCYYMVS.js.map → chunk-NOSRK3ZZ.js.map} +0 -0
  195. /package/dist/{chunk-COTPDJR5.js.map → chunk-OMB2ZKIT.js.map} +0 -0
  196. /package/dist/{chunk-Y4MJQ2UG.js.map → chunk-QQ2YMQZU.js.map} +0 -0
  197. /package/dist/{chunk-YCXICMOY.js.map → chunk-RJ7NN2UR.js.map} +0 -0
  198. /package/dist/{chunk-PDD75NUZ.js.map → chunk-SMH6SRX7.js.map} +0 -0
  199. /package/dist/{chunk-QHW35ZON.js.map → chunk-TGPST6FK.js.map} +0 -0
  200. /package/dist/{chunk-BMELYFHU.js.map → chunk-V2PNPF6J.js.map} +0 -0
  201. /package/dist/{chunk-BERJ4AWW.js.map → chunk-VGXZT72I.js.map} +0 -0
  202. /package/dist/{chunk-2Z47NMFK.js.map → chunk-WZW37ZHL.js.map} +0 -0
  203. /package/dist/{chunk-PZ5AY32C.js.map → collection-facade-VOWW732S.js.map} +0 -0
  204. /package/dist/{collection-facade-4ACKIDDY.js.map → computed-VPOKQK4N.js.map} +0 -0
  205. /package/dist/{computed-LSEZYRJS.js.map → derive-HHBRFS2U.js.map} +0 -0
  206. /package/dist/{derive-LY47HG7Q.js.map → enclave-VM2YQONI.js.map} +0 -0
  207. /package/dist/{enclave-4YBQK77G.js.map → executor-5NIMLSKZ.js.map} +0 -0
  208. /package/dist/{executor-MZJXBWNM.js.map → executor-5XJNV5H5.js.map} +0 -0
  209. /package/dist/{executor-TOLPBHOC.js.map → executor-AQVXV5GZ.js.map} +0 -0
  210. /package/dist/{executor-YDDC5XGY.js.map → export-accessible-2LOBM67T.js.map} +0 -0
  211. /package/dist/{export-accessible-LGV6IAFX.js.map → extract-partition-AZGSEMIF.js.map} +0 -0
  212. /package/dist/{extract-partition-RYJZIKQZ.js.map → find-2ABFCZX4.js.map} +0 -0
  213. /package/dist/{find-LC7P63MX.js.map → issue-RH4H733Z.js.map} +0 -0
  214. /package/dist/{issue-IOXM3RSJ.js.map → ledger-454SU3VE.js.map} +0 -0
  215. /package/dist/{ledger-HXHIOD36.js.map → liberate-AAZSP2BT.js.map} +0 -0
  216. /package/dist/{liberate-4APIV7FR.js.map → link-set-67CAMUAL.js.map} +0 -0
  217. /package/dist/{link-set-6RAHEKZN.js.map → managed-secret-NHRYG7CD.js.map} +0 -0
  218. /package/dist/{managed-secret-P3JQ7ZG4.js.map → noydb-Y53CQHZK.js.map} +0 -0
  219. /package/dist/{noydb-2C6QQJ5S.js.map → read-only-facade-BIGO37U3.js.map} +0 -0
  220. /package/dist/{read-only-facade-CHCKSEPD.js.map → register-7OJWAWT5.js.map} +0 -0
  221. /package/dist/{register-AEOOGKNO.js.map → registry-23WF3TGH.js.map} +0 -0
  222. /package/dist/{registry-D2YXKRYC.js.map → registry-4AYVUNXK.js.map} +0 -0
  223. /package/dist/{registry-I6FP4677.js.map → registry-5MVJGPXW.js.map} +0 -0
  224. /package/dist/{registry-SHMGHEK4.js.map → registry-HDINK5QW.js.map} +0 -0
  225. /package/dist/{registry-SKSOUXNF.js.map → request-withdrawal-P5EQATZ7.js.map} +0 -0
  226. /package/dist/{request-withdrawal-6HJT4ODF.js.map → revoke-POHP4GPB.js.map} +0 -0
  227. /package/dist/{revoke-U7U3FMXU.js.map → signer-5K3WDHOC.js.map} +0 -0
  228. /package/dist/{signer-7NK7TMWU.js.map → stale-AU74OPUW.js.map} +0 -0
  229. /package/dist/{stale-2CHKZTMK.js.map → storage-AVJA5BTW.js.map} +0 -0
  230. /package/dist/{storage-WP4M7IFO.js.map → ulid-4MKXG325.js.map} +0 -0
  231. /package/dist/{ulid-VQW6ERQN.js.map → vault-diff-C6WUJFNP.js.map} +0 -0
  232. /package/dist/{vault-diff-FE5J35NJ.js.map → walk-ERAJ5RHO.js.map} +0 -0
  233. /package/dist/{walk-P3E6QSNC.js.map → withdraw-accessible-42D32Z7G.js.map} +0 -0
@@ -14,7 +14,6 @@ import "./chunk-LP7BEXCT.js";
14
14
  import "./chunk-SL3JAAKN.js";
15
15
  import "./chunk-N4CHCXDL.js";
16
16
  import "./chunk-7O3Y4PPD.js";
17
- import "./chunk-PZ5AY32C.js";
18
17
  export {
19
18
  ATTESTATIONS_COLLECTION,
20
19
  REVOKED_RECORD_ID,
@@ -22,4 +21,4 @@ export {
22
21
  loadOrCreateSigner,
23
22
  loadSigner
24
23
  };
25
- //# sourceMappingURL=signer-7NK7TMWU.js.map
24
+ //# sourceMappingURL=signer-5K3WDHOC.js.map
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  readPod,
6
6
  writePod
7
- } from "../chunk-PKJ43CMD.js";
7
+ } from "../chunk-NLOLF6LD.js";
8
8
  import "../chunk-MLO75SFJ.js";
9
9
  import "../chunk-CIGAQBJR.js";
10
10
  import "../chunk-4RUFYGWG.js";
@@ -17,7 +17,6 @@ import "../chunk-N4CHCXDL.js";
17
17
  import {
18
18
  SnapshotNotFoundError
19
19
  } from "../chunk-7O3Y4PPD.js";
20
- import "../chunk-PZ5AY32C.js";
21
20
 
22
21
  // src/with-fork/snapshots/engine.ts
23
22
  var SnapshotEngine = class {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/with-fork/snapshots/engine.ts","../../src/with-fork/snapshots/active.ts"],"sourcesContent":["import { writePod, readPod } from '../../with-pod/pod.js'\nimport { SnapshotNotFoundError } from '../../kernel/errors.js'\nimport type { NoydbPodStore } from '../../kernel/types.js'\nimport type { Vault } from '../../kernel/vault.js'\nimport type { SnapshotMeta, RetentionPolicy, SnapshotIndex } from './strategy.js'\n\nexport class SnapshotEngine {\n constructor(\n private readonly store: NoydbPodStore,\n private readonly retention: RetentionPolicy,\n ) {}\n\n private indexKey(vaultName: string): string {\n return `${vaultName}__index`\n }\n\n private snapKey(vaultName: string, n: number): string {\n return `${vaultName}__snap_${n.toString().padStart(6, '0')}`\n }\n\n private autoKey(vaultName: string): string {\n return `${vaultName}__auto`\n }\n\n private async readIndex(\n vaultName: string,\n ): Promise<{ index: SnapshotIndex; indexVersion: string | null }> {\n const result = await this.store.readBundle(this.indexKey(vaultName))\n if (!result) return { index: { snapshots: [], nextCounter: 1 }, indexVersion: null }\n const text = new TextDecoder().decode(result.bytes)\n return { index: JSON.parse(text) as SnapshotIndex, indexVersion: result.version }\n }\n\n private async writeIndex(\n vaultName: string,\n index: SnapshotIndex,\n expectedVersion: string | null,\n ): Promise<void> {\n const bytes = new TextEncoder().encode(JSON.stringify(index))\n await this.store.writeBundle(this.indexKey(vaultName), bytes, expectedVersion)\n }\n\n async snapshot(\n vault: Vault,\n by: string,\n opts?: { label?: string; note?: string },\n ): Promise<SnapshotMeta> {\n const bytes = await writePod(vault, {})\n const { index, indexVersion } = await this.readIndex(vault.name)\n const key = this.snapKey(vault.name, index.nextCounter)\n\n // Write blob first. If the subsequent index write fails, the next snapshot()\n // call will re-derive the same key (counter not persisted) and overwrite this blob.\n // This is an accepted v1 trade-off: failure window is narrow and requires a store\n // error between the two writes. A retry produces a fresh snapshot at the same key.\n await this.store.writeBundle(key, bytes, null)\n\n const meta: SnapshotMeta = {\n version: key,\n ...(opts?.label !== undefined ? { label: opts.label } : {}),\n ...(opts?.note !== undefined ? { note: opts.note } : {}),\n exportedAt: new Date().toISOString(),\n exportedBy: by,\n size: bytes.length,\n integrity: 'verified',\n }\n\n const newIndex: SnapshotIndex = {\n snapshots: [...index.snapshots, meta],\n nextCounter: index.nextCounter + 1,\n // Preserve the rolling auto slot — on-demand snapshots never touch it.\n ...(index.auto ? { auto: index.auto } : {}),\n }\n const toDelete = this.applyRetention(newIndex)\n await this.writeIndex(vault.name, newIndex, indexVersion)\n\n for (const k of toDelete) {\n await this.store.deleteBundle(k)\n }\n\n return meta\n }\n\n /**\n * Rolling auto-snapshot. Overwrites the single fixed `<vault>__auto` key and\n * stores its meta in `index.auto`, separate from the immutable `snapshots`\n * pool — retention never prunes it. Used by the cadence scheduler.\n */\n async autoSnapshot(\n vault: Vault,\n by: string,\n opts?: { label?: string; note?: string },\n ): Promise<SnapshotMeta> {\n const bytes = await writePod(vault, {})\n const { index, indexVersion } = await this.readIndex(vault.name)\n const key = this.autoKey(vault.name)\n\n // Unconditional overwrite of the rolling slot.\n await this.store.writeBundle(key, bytes, null)\n\n const meta: SnapshotMeta = {\n version: key,\n label: opts?.label ?? 'auto',\n ...(opts?.note !== undefined ? { note: opts.note } : {}),\n exportedAt: new Date().toISOString(),\n exportedBy: by,\n size: bytes.length,\n integrity: 'verified',\n auto: true,\n }\n\n index.auto = meta\n await this.writeIndex(vault.name, index, indexVersion)\n return meta\n }\n\n async listSnapshots(vaultId: string): Promise<SnapshotMeta[]> {\n const { index } = await this.readIndex(vaultId)\n const immutable = [...index.snapshots].reverse()\n return index.auto ? [index.auto, ...immutable] : immutable\n }\n\n async restoreSnapshot(vault: Vault, version: string): Promise<void> {\n if (!version.startsWith(`${vault.name}__`)) throw new SnapshotNotFoundError(version)\n const result = await this.store.readBundle(version)\n if (!result) throw new SnapshotNotFoundError(version)\n const { dumpJson } = await readPod(result.bytes)\n await vault.load(dumpJson)\n }\n\n /**\n * Applies the configured retention policy to `index`, mutating `index.snapshots`\n * in place and returning the blob keys that should be deleted from the store.\n * Called by `snapshot()` before the index is written.\n *\n * @internal — public for direct testing only\n */\n applyRetention(index: SnapshotIndex): string[] {\n const prune = this.retention.prune ?? true\n if (!prune) return []\n\n const toDelete: string[] = []\n let remaining = index.snapshots.slice()\n\n if (this.retention.keepLast !== undefined && remaining.length > this.retention.keepLast) {\n const excess = remaining.splice(0, remaining.length - this.retention.keepLast)\n toDelete.push(...excess.map(m => m.version))\n }\n\n if (this.retention.maxAgeDays !== undefined) {\n const cutoffMs = this.retention.maxAgeDays * 86_400_000\n const now = Date.now()\n const fresh = remaining.filter(m => now - new Date(m.exportedAt).getTime() <= cutoffMs)\n toDelete.push(...remaining.filter(m => !fresh.includes(m)).map(m => m.version))\n remaining = fresh\n }\n\n index.snapshots = remaining\n return toDelete\n }\n}\n","import { SnapshotEngine } from './engine.js'\nimport type { SnapshotsStrategy, RetentionPolicy } from './strategy.js'\nimport type { SnapshotPolicy } from './policy.js'\nimport type { NoydbPodStore } from '../../kernel/types.js'\nimport type { Vault } from '../../kernel/vault.js'\n\nexport interface WithSnapshotsOptions {\n /** Bundle store where snapshot blobs and the sidecar index are written. */\n store: NoydbPodStore\n /**\n * Declarative retention policy. Enforced eagerly after each on-demand `snapshot()`.\n * Defaults to no retention (all on-demand snapshots kept forever). Never affects\n * the rolling auto-snapshot.\n */\n retention?: RetentionPolicy\n /**\n * Automatic-snapshot cadence. Default `mode:'manual'` ⇒ no timers; snapshots\n * stay on-demand. Set `mode:'debounce'`/`'interval'` to enable auto-snapshots\n * to the rolling `<vault>__auto` key.\n */\n snapshotPolicy?: SnapshotPolicy\n}\n\nexport function withSnapshots(opts: WithSnapshotsOptions): SnapshotsStrategy {\n const engine = new SnapshotEngine(opts.store, opts.retention ?? {})\n return {\n snapshot(vault, by, snapOpts) {\n return engine.snapshot(vault as Vault, by, snapOpts)\n },\n listSnapshots(vaultId) {\n return engine.listSnapshots(vaultId)\n },\n restoreSnapshot(vault, version) {\n return engine.restoreSnapshot(vault as Vault, version)\n },\n autoSnapshot(vault, by, snapOpts) {\n return engine.autoSnapshot(vault as Vault, by, snapOpts)\n },\n ...(opts.snapshotPolicy ? { policy: opts.snapshotPolicy } : {}),\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/with-fork/snapshots/engine.ts","../../src/with-fork/snapshots/active.ts"],"sourcesContent":["import { writePod, readPod } from '../../with-pod/pod.js'\nimport { SnapshotNotFoundError } from '../../kernel/errors.js'\nimport type { NoydbPodStore } from '../../kernel/types.js'\nimport type { Vault } from '../../kernel/vault.js'\nimport type { SnapshotMeta, RetentionPolicy, SnapshotIndex } from './strategy.js'\n\nexport class SnapshotEngine {\n constructor(\n private readonly store: NoydbPodStore,\n private readonly retention: RetentionPolicy,\n ) {}\n\n private indexKey(vaultName: string): string {\n return `${vaultName}__index`\n }\n\n private snapKey(vaultName: string, n: number): string {\n return `${vaultName}__snap_${n.toString().padStart(6, '0')}`\n }\n\n private autoKey(vaultName: string): string {\n return `${vaultName}__auto`\n }\n\n private async readIndex(\n vaultName: string,\n ): Promise<{ index: SnapshotIndex; indexVersion: string | null }> {\n const result = await this.store.readBundle(this.indexKey(vaultName))\n if (!result) return { index: { snapshots: [], nextCounter: 1 }, indexVersion: null }\n const text = new TextDecoder().decode(result.bytes)\n return { index: JSON.parse(text) as SnapshotIndex, indexVersion: result.version }\n }\n\n private async writeIndex(\n vaultName: string,\n index: SnapshotIndex,\n expectedVersion: string | null,\n ): Promise<void> {\n const bytes = new TextEncoder().encode(JSON.stringify(index))\n await this.store.writeBundle(this.indexKey(vaultName), bytes, expectedVersion)\n }\n\n async snapshot(\n vault: Vault,\n by: string,\n opts?: { label?: string; note?: string },\n ): Promise<SnapshotMeta> {\n const bytes = await writePod(vault, {})\n const { index, indexVersion } = await this.readIndex(vault.name)\n const key = this.snapKey(vault.name, index.nextCounter)\n\n // Write blob first. If the subsequent index write fails, the next snapshot()\n // call will re-derive the same key (counter not persisted) and overwrite this blob.\n // This is an accepted v1 trade-off: failure window is narrow and requires a store\n // error between the two writes. A retry produces a fresh snapshot at the same key.\n await this.store.writeBundle(key, bytes, null)\n\n const meta: SnapshotMeta = {\n version: key,\n ...(opts?.label !== undefined ? { label: opts.label } : {}),\n ...(opts?.note !== undefined ? { note: opts.note } : {}),\n exportedAt: new Date().toISOString(),\n exportedBy: by,\n size: bytes.length,\n integrity: 'verified',\n }\n\n const newIndex: SnapshotIndex = {\n snapshots: [...index.snapshots, meta],\n nextCounter: index.nextCounter + 1,\n // Preserve the rolling auto slot — on-demand snapshots never touch it.\n ...(index.auto ? { auto: index.auto } : {}),\n }\n const toDelete = this.applyRetention(newIndex)\n await this.writeIndex(vault.name, newIndex, indexVersion)\n\n for (const k of toDelete) {\n await this.store.deleteBundle(k)\n }\n\n return meta\n }\n\n /**\n * Rolling auto-snapshot. Overwrites the single fixed `<vault>__auto` key and\n * stores its meta in `index.auto`, separate from the immutable `snapshots`\n * pool — retention never prunes it. Used by the cadence scheduler.\n */\n async autoSnapshot(\n vault: Vault,\n by: string,\n opts?: { label?: string; note?: string },\n ): Promise<SnapshotMeta> {\n const bytes = await writePod(vault, {})\n const { index, indexVersion } = await this.readIndex(vault.name)\n const key = this.autoKey(vault.name)\n\n // Unconditional overwrite of the rolling slot.\n await this.store.writeBundle(key, bytes, null)\n\n const meta: SnapshotMeta = {\n version: key,\n label: opts?.label ?? 'auto',\n ...(opts?.note !== undefined ? { note: opts.note } : {}),\n exportedAt: new Date().toISOString(),\n exportedBy: by,\n size: bytes.length,\n integrity: 'verified',\n auto: true,\n }\n\n index.auto = meta\n await this.writeIndex(vault.name, index, indexVersion)\n return meta\n }\n\n async listSnapshots(vaultId: string): Promise<SnapshotMeta[]> {\n const { index } = await this.readIndex(vaultId)\n const immutable = [...index.snapshots].reverse()\n return index.auto ? [index.auto, ...immutable] : immutable\n }\n\n async restoreSnapshot(vault: Vault, version: string): Promise<void> {\n if (!version.startsWith(`${vault.name}__`)) throw new SnapshotNotFoundError(version)\n const result = await this.store.readBundle(version)\n if (!result) throw new SnapshotNotFoundError(version)\n const { dumpJson } = await readPod(result.bytes)\n await vault.load(dumpJson)\n }\n\n /**\n * Applies the configured retention policy to `index`, mutating `index.snapshots`\n * in place and returning the blob keys that should be deleted from the store.\n * Called by `snapshot()` before the index is written.\n *\n * @internal — public for direct testing only\n */\n applyRetention(index: SnapshotIndex): string[] {\n const prune = this.retention.prune ?? true\n if (!prune) return []\n\n const toDelete: string[] = []\n let remaining = index.snapshots.slice()\n\n if (this.retention.keepLast !== undefined && remaining.length > this.retention.keepLast) {\n const excess = remaining.splice(0, remaining.length - this.retention.keepLast)\n toDelete.push(...excess.map(m => m.version))\n }\n\n if (this.retention.maxAgeDays !== undefined) {\n const cutoffMs = this.retention.maxAgeDays * 86_400_000\n const now = Date.now()\n const fresh = remaining.filter(m => now - new Date(m.exportedAt).getTime() <= cutoffMs)\n toDelete.push(...remaining.filter(m => !fresh.includes(m)).map(m => m.version))\n remaining = fresh\n }\n\n index.snapshots = remaining\n return toDelete\n }\n}\n","import { SnapshotEngine } from './engine.js'\nimport type { SnapshotsStrategy, RetentionPolicy } from './strategy.js'\nimport type { SnapshotPolicy } from './policy.js'\nimport type { NoydbPodStore } from '../../kernel/types.js'\nimport type { Vault } from '../../kernel/vault.js'\n\nexport interface WithSnapshotsOptions {\n /** Bundle store where snapshot blobs and the sidecar index are written. */\n store: NoydbPodStore\n /**\n * Declarative retention policy. Enforced eagerly after each on-demand `snapshot()`.\n * Defaults to no retention (all on-demand snapshots kept forever). Never affects\n * the rolling auto-snapshot.\n */\n retention?: RetentionPolicy\n /**\n * Automatic-snapshot cadence. Default `mode:'manual'` ⇒ no timers; snapshots\n * stay on-demand. Set `mode:'debounce'`/`'interval'` to enable auto-snapshots\n * to the rolling `<vault>__auto` key.\n */\n snapshotPolicy?: SnapshotPolicy\n}\n\nexport function withSnapshots(opts: WithSnapshotsOptions): SnapshotsStrategy {\n const engine = new SnapshotEngine(opts.store, opts.retention ?? {})\n return {\n snapshot(vault, by, snapOpts) {\n return engine.snapshot(vault as Vault, by, snapOpts)\n },\n listSnapshots(vaultId) {\n return engine.listSnapshots(vaultId)\n },\n restoreSnapshot(vault, version) {\n return engine.restoreSnapshot(vault as Vault, version)\n },\n autoSnapshot(vault, by, snapOpts) {\n return engine.autoSnapshot(vault as Vault, by, snapOpts)\n },\n ...(opts.snapshotPolicy ? { policy: opts.snapshotPolicy } : {}),\n }\n}\n"],"mappings":"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","names":[]}
@@ -6,7 +6,7 @@ import {
6
6
  isMVStale,
7
7
  markMVStale,
8
8
  resolveStaleMVOnRead
9
- } from "./chunk-E2MYJGSG.js";
9
+ } from "./chunk-AMODH7AU.js";
10
10
  import "./chunk-5D2ALSM5.js";
11
11
  import "./chunk-CIGAQBJR.js";
12
12
  import "./chunk-4RUFYGWG.js";
@@ -17,7 +17,6 @@ import "./chunk-LP7BEXCT.js";
17
17
  import "./chunk-SL3JAAKN.js";
18
18
  import "./chunk-N4CHCXDL.js";
19
19
  import "./chunk-7O3Y4PPD.js";
20
- import "./chunk-PZ5AY32C.js";
21
20
  export {
22
21
  MV_STALE_COLLECTION,
23
22
  clearMVStale,
@@ -27,4 +26,4 @@ export {
27
26
  markMVStale,
28
27
  resolveStaleMVOnRead
29
28
  };
30
- //# sourceMappingURL=stale-2CHKZTMK.js.map
29
+ //# sourceMappingURL=stale-AU74OPUW.js.map
@@ -13,11 +13,10 @@ import "./chunk-LP7BEXCT.js";
13
13
  import "./chunk-SL3JAAKN.js";
14
14
  import "./chunk-N4CHCXDL.js";
15
15
  import "./chunk-7O3Y4PPD.js";
16
- import "./chunk-PZ5AY32C.js";
17
16
  export {
18
17
  SCHEMAS_COLLECTION,
19
18
  loadPersistedSchema,
20
19
  loadPersistedSchemaEntry,
21
20
  savePersistedSchema
22
21
  };
23
- //# sourceMappingURL=storage-WP4M7IFO.js.map
22
+ //# sourceMappingURL=storage-AVJA5BTW.js.map
@@ -8,7 +8,6 @@ import {
8
8
  withRetry,
9
9
  wrapStore
10
10
  } from "../chunk-EIPVYUEP.js";
11
- import "../chunk-PZ5AY32C.js";
12
11
  export {
13
12
  routeStore,
14
13
  withCache,
@@ -13,7 +13,6 @@ import "./chunk-LP7BEXCT.js";
13
13
  import "./chunk-SL3JAAKN.js";
14
14
  import "./chunk-N4CHCXDL.js";
15
15
  import "./chunk-7O3Y4PPD.js";
16
- import "./chunk-PZ5AY32C.js";
17
16
 
18
17
  // src/with-shape/schema-update/client-registry.ts
19
18
  var META_COLLECTION = "_meta";
@@ -146,4 +145,4 @@ function unref(timer) {
146
145
  export {
147
146
  StoreMesh
148
147
  };
149
- //# sourceMappingURL=store-coordination-provider-O2VUEMJH.js.map
148
+ //# sourceMappingURL=store-coordination-provider-RX2CGWXV.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/with-shape/schema-update/client-registry.ts","../src/with-shape/schema-update/store-coordination-provider.ts"],"sourcesContent":["/**\n * Schema-cutover client registry. Each client keeps a\n * heartbeat doc at `_meta/schema-fence:client:<clientId>` carrying its\n * liveness (`lastSeen`) and the fence generation it has quiesced for\n * (`quiescedAtVersion`). Plaintext envelope, like the fence doc.\n */\nimport type { NoydbStore } from '../../kernel/types.js'\nimport { buildRecordEnvelope } from '../../kernel/enclave/index.js'\n\nconst META_COLLECTION = '_meta'\nconst CLIENT_PREFIX = 'schema-fence:client:'\n\nexport interface ClientDoc {\n readonly clientId: string\n readonly lastSeen: number\n readonly quiescedAtVersion: number | null\n /**\n * Session that owns this writer (one user's writers across vaults). Additive\n * and optional so legacy client docs (written without it) keep parsing;\n * readers default it.\n */\n readonly sessionId?: string\n}\n\nexport async function writeClientDoc(\n store: NoydbStore,\n vault: string,\n clientId: string,\n doc: { lastSeen: number; quiescedAtVersion: number | null; sessionId?: string },\n): Promise<void> {\n const recordId = `${CLIENT_PREFIX}${clientId}`\n const envelope = buildRecordEnvelope(\n { collection: META_COLLECTION, id: recordId, version: 1 },\n { iv: '', data: JSON.stringify({ clientId, ...doc }) },\n )\n await store.put(vault, META_COLLECTION, recordId, envelope)\n}\n\nexport async function listClientDocs(store: NoydbStore, vault: string): Promise<ClientDoc[]> {\n const ids = await store.list(vault, META_COLLECTION)\n const out: ClientDoc[] = []\n for (const id of ids) {\n if (!id.startsWith(CLIENT_PREFIX)) continue\n const env = await store.get(vault, META_COLLECTION, id)\n if (!env) continue\n try {\n const parsed = JSON.parse(env._data) as unknown\n if (isClientDoc(parsed)) out.push(parsed)\n } catch { /* skip corrupt */ }\n }\n return out\n}\n\n/**\n * True when every *active* client (lastSeen within staleMs of now) has\n * `quiescedAtVersion === generation`. Stale clients are ignored. An empty\n * active set is vacuously quiesced.\n */\nexport async function activeQuiesced(\n store: NoydbStore,\n vault: string,\n opts: { generation: number; now: number; staleMs: number; excludeClientId?: string },\n): Promise<boolean> {\n const docs = await listClientDocs(store, vault)\n const active = docs.filter(\n d => d.lastSeen >= opts.now - opts.staleMs && d.clientId !== opts.excludeClientId,\n )\n return active.every(d => d.quiescedAtVersion === opts.generation)\n}\n\nfunction isClientDoc(x: unknown): x is ClientDoc {\n if (x === null || typeof x !== 'object') return false\n const o = x as Record<string, unknown>\n return typeof o['clientId'] === 'string'\n && typeof o['lastSeen'] === 'number'\n && (o['quiescedAtVersion'] === null || typeof o['quiescedAtVersion'] === 'number')\n && (o['sessionId'] === undefined || typeof o['sessionId'] === 'string')\n}\n","/**\n * Store-backed default {@link NoydbMesh}.\n *\n * Maps the five coordination-port methods onto today's `_meta/schema-fence`\n * store ops so that, with no `by-*` provider injected, the fence service\n * reproduces its current store-polling behavior byte-for-byte. The `observe*`\n * methods are poll-emit (the legacy {@link FenceWatcher} model); `by-tabs` /\n * `by-peer` override them with real-time push.\n *\n * @module\n */\n\nimport type { NoydbStore } from '../../kernel/types.js'\nimport type { Unsubscribe } from '../../port/with/write-hooks.js'\nimport { loadFence, saveFence, type FenceDoc } from './fence.js'\nimport { writeClientDoc, listClientDocs, type ClientDoc } from './client-registry.js'\nimport type { NoydbMesh, WriterPresence } from '../../port/by/types.js'\n\n/**\n * Default poll cadence for the `observe*` fallbacks (ms). Matches the\n * store-poll granularity the fence-controller uses while waiting for quorum\n * (`delay(50)`); the same value the migration cutover falls back to when no\n * real-time provider is injected.\n */\nconst DEFAULT_POLL_INTERVAL_MS = 50\n\n/** Map a stored {@link ClientDoc} to the port's {@link WriterPresence}. */\nfunction toPresence(doc: ClientDoc): WriterPresence {\n return {\n writerId: doc.clientId,\n // Legacy docs written without a sessionId (and explicit empty) have no\n // session — fall back to the writerId so every presence is session-addressable.\n sessionId: doc.sessionId && doc.sessionId.length > 0 ? doc.sessionId : doc.clientId,\n lastSeen: doc.lastSeen,\n quiescedAtVersion: doc.quiescedAtVersion,\n }\n}\n\nfunction fenceEqual(a: FenceDoc, b: FenceDoc): boolean {\n return a.currentSchemaVersion === b.currentSchemaVersion && a.fenceState === b.fenceState\n}\n\nfunction presenceListEqual(a: readonly WriterPresence[], b: readonly WriterPresence[]): boolean {\n if (a.length !== b.length) return false\n const key = (w: WriterPresence) => `${w.writerId}\\u0000${w.sessionId}\\u0000${w.lastSeen}\\u0000${String(w.quiescedAtVersion)}`\n const bk = new Set(b.map(key))\n return a.every((w) => bk.has(key(w)))\n}\n\nexport class StoreMesh implements NoydbMesh {\n readonly #store: NoydbStore\n readonly #pollIntervalMs: number\n\n constructor(store: NoydbStore, opts?: { pollIntervalMs?: number }) {\n this.#store = store\n this.#pollIntervalMs = opts?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS\n }\n\n async setFence(vault: string, fence: FenceDoc): Promise<void> {\n // RE-READ AND SPREAD, never write the caller's doc whole (#1197).\n //\n // `saveFence` serialises what it is given and does a full `store.put` — no\n // merge. Callers legitimately construct a PARTIAL doc: `FenceController`\n // builds `{ currentSchemaVersion, fenceState }`, which is a valid `FenceDoc`\n // because `schemaHash` is optional. Writing that whole silently erased the\n // hash #946 added, so \"which schema is generation N is answerable from\n // schemaFenceState() alone\" held only until the first drain.\n //\n // The type cannot see the loss: for an optional field, \"absent\" and\n // \"deliberately cleared\" are the same value.\n //\n // Mirrors `persisted-schemas/register.ts:136`, which re-reads for the same\n // reason and documents it — a stale snapshot would roll back a concurrent\n // cutover's version. Re-reading NARROWS that window rather than closing it;\n // fence transitions are orchestrator-driven and serialised, so the residual\n // is the same one that path already accepts.\n const current = await loadFence(this.#store, vault)\n await saveFence(this.#store, vault, { ...current, ...fence })\n }\n\n async readFence(vault: string): Promise<FenceDoc> {\n return loadFence(this.#store, vault)\n }\n\n observeFence(vault: string, onChange: (f: FenceDoc) => void): Unsubscribe {\n let last: FenceDoc | null = null\n let busy = false\n const poll = async () => {\n if (busy) return\n busy = true\n try {\n const fence = await loadFence(this.#store, vault)\n if (last === null || !fenceEqual(last, fence)) {\n last = fence\n onChange(fence)\n }\n } catch {\n /* transient store error — retry next tick */\n } finally {\n busy = false\n }\n }\n void poll() // emit current state once immediately\n const timer = setInterval(() => void poll(), this.#pollIntervalMs)\n unref(timer)\n return () => clearInterval(timer)\n }\n\n async reportPresence(vault: string, p: WriterPresence): Promise<void> {\n await writeClientDoc(this.#store, vault, p.writerId, {\n lastSeen: p.lastSeen,\n quiescedAtVersion: p.quiescedAtVersion,\n sessionId: p.sessionId,\n })\n }\n\n observePresence(vault: string, onChange: (writers: readonly WriterPresence[]) => void): Unsubscribe {\n let last: readonly WriterPresence[] | null = null\n let busy = false\n const poll = async () => {\n if (busy) return\n busy = true\n try {\n const docs = await listClientDocs(this.#store, vault)\n const writers = docs.map(toPresence)\n if (last === null || !presenceListEqual(last, writers)) {\n last = writers\n onChange(writers)\n }\n } catch {\n /* transient store error — retry next tick */\n } finally {\n busy = false\n }\n }\n void poll() // emit current set once immediately\n const timer = setInterval(() => void poll(), this.#pollIntervalMs)\n unref(timer)\n return () => clearInterval(timer)\n }\n\n async reachableWriters(vault: string, o: { staleMs: number; now: number }): Promise<readonly WriterPresence[]> {\n const docs = await listClientDocs(this.#store, vault)\n return docs.map(toPresence).filter((w) => o.now - w.lastSeen <= o.staleMs)\n }\n}\n\n/** Best-effort `unref` so a poll timer never holds the process open (Node only). */\nfunction unref(timer: ReturnType<typeof setInterval>): void {\n const t = timer as unknown as { unref?: () => void }\n if (typeof t.unref === 'function') t.unref()\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/with-shape/schema-update/client-registry.ts","../src/with-shape/schema-update/store-coordination-provider.ts"],"sourcesContent":["/**\n * Schema-cutover client registry. Each client keeps a\n * heartbeat doc at `_meta/schema-fence:client:<clientId>` carrying its\n * liveness (`lastSeen`) and the fence generation it has quiesced for\n * (`quiescedAtVersion`). Plaintext envelope, like the fence doc.\n */\nimport type { NoydbStore } from '../../kernel/types.js'\nimport { buildRecordEnvelope } from '../../kernel/enclave/index.js'\n\nconst META_COLLECTION = '_meta'\nconst CLIENT_PREFIX = 'schema-fence:client:'\n\nexport interface ClientDoc {\n readonly clientId: string\n readonly lastSeen: number\n readonly quiescedAtVersion: number | null\n /**\n * Session that owns this writer (one user's writers across vaults). Additive\n * and optional so legacy client docs (written without it) keep parsing;\n * readers default it.\n */\n readonly sessionId?: string\n}\n\nexport async function writeClientDoc(\n store: NoydbStore,\n vault: string,\n clientId: string,\n doc: { lastSeen: number; quiescedAtVersion: number | null; sessionId?: string },\n): Promise<void> {\n const recordId = `${CLIENT_PREFIX}${clientId}`\n const envelope = buildRecordEnvelope(\n { collection: META_COLLECTION, id: recordId, version: 1 },\n { iv: '', data: JSON.stringify({ clientId, ...doc }) },\n )\n await store.put(vault, META_COLLECTION, recordId, envelope)\n}\n\nexport async function listClientDocs(store: NoydbStore, vault: string): Promise<ClientDoc[]> {\n const ids = await store.list(vault, META_COLLECTION)\n const out: ClientDoc[] = []\n for (const id of ids) {\n if (!id.startsWith(CLIENT_PREFIX)) continue\n const env = await store.get(vault, META_COLLECTION, id)\n if (!env) continue\n try {\n const parsed = JSON.parse(env._data) as unknown\n if (isClientDoc(parsed)) out.push(parsed)\n } catch { /* skip corrupt */ }\n }\n return out\n}\n\n/**\n * True when every *active* client (lastSeen within staleMs of now) has\n * `quiescedAtVersion === generation`. Stale clients are ignored. An empty\n * active set is vacuously quiesced.\n */\nexport async function activeQuiesced(\n store: NoydbStore,\n vault: string,\n opts: { generation: number; now: number; staleMs: number; excludeClientId?: string },\n): Promise<boolean> {\n const docs = await listClientDocs(store, vault)\n const active = docs.filter(\n d => d.lastSeen >= opts.now - opts.staleMs && d.clientId !== opts.excludeClientId,\n )\n return active.every(d => d.quiescedAtVersion === opts.generation)\n}\n\nfunction isClientDoc(x: unknown): x is ClientDoc {\n if (x === null || typeof x !== 'object') return false\n const o = x as Record<string, unknown>\n return typeof o['clientId'] === 'string'\n && typeof o['lastSeen'] === 'number'\n && (o['quiescedAtVersion'] === null || typeof o['quiescedAtVersion'] === 'number')\n && (o['sessionId'] === undefined || typeof o['sessionId'] === 'string')\n}\n","/**\n * Store-backed default {@link NoydbMesh}.\n *\n * Maps the five coordination-port methods onto today's `_meta/schema-fence`\n * store ops so that, with no `by-*` provider injected, the fence service\n * reproduces its current store-polling behavior byte-for-byte. The `observe*`\n * methods are poll-emit (the legacy {@link FenceWatcher} model); `by-tabs` /\n * `by-peer` override them with real-time push.\n *\n * @module\n */\n\nimport type { NoydbStore } from '../../kernel/types.js'\nimport type { Unsubscribe } from '../../port/with/write-hooks.js'\nimport { loadFence, saveFence, type FenceDoc } from './fence.js'\nimport { writeClientDoc, listClientDocs, type ClientDoc } from './client-registry.js'\nimport type { NoydbMesh, WriterPresence } from '../../port/by/types.js'\n\n/**\n * Default poll cadence for the `observe*` fallbacks (ms). Matches the\n * store-poll granularity the fence-controller uses while waiting for quorum\n * (`delay(50)`); the same value the migration cutover falls back to when no\n * real-time provider is injected.\n */\nconst DEFAULT_POLL_INTERVAL_MS = 50\n\n/** Map a stored {@link ClientDoc} to the port's {@link WriterPresence}. */\nfunction toPresence(doc: ClientDoc): WriterPresence {\n return {\n writerId: doc.clientId,\n // Legacy docs written without a sessionId (and explicit empty) have no\n // session — fall back to the writerId so every presence is session-addressable.\n sessionId: doc.sessionId && doc.sessionId.length > 0 ? doc.sessionId : doc.clientId,\n lastSeen: doc.lastSeen,\n quiescedAtVersion: doc.quiescedAtVersion,\n }\n}\n\nfunction fenceEqual(a: FenceDoc, b: FenceDoc): boolean {\n return a.currentSchemaVersion === b.currentSchemaVersion && a.fenceState === b.fenceState\n}\n\nfunction presenceListEqual(a: readonly WriterPresence[], b: readonly WriterPresence[]): boolean {\n if (a.length !== b.length) return false\n const key = (w: WriterPresence) => `${w.writerId}\\u0000${w.sessionId}\\u0000${w.lastSeen}\\u0000${String(w.quiescedAtVersion)}`\n const bk = new Set(b.map(key))\n return a.every((w) => bk.has(key(w)))\n}\n\nexport class StoreMesh implements NoydbMesh {\n readonly #store: NoydbStore\n readonly #pollIntervalMs: number\n\n constructor(store: NoydbStore, opts?: { pollIntervalMs?: number }) {\n this.#store = store\n this.#pollIntervalMs = opts?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS\n }\n\n async setFence(vault: string, fence: FenceDoc): Promise<void> {\n // RE-READ AND SPREAD, never write the caller's doc whole (#1197).\n //\n // `saveFence` serialises what it is given and does a full `store.put` — no\n // merge. Callers legitimately construct a PARTIAL doc: `FenceController`\n // builds `{ currentSchemaVersion, fenceState }`, which is a valid `FenceDoc`\n // because `schemaHash` is optional. Writing that whole silently erased the\n // hash #946 added, so \"which schema is generation N is answerable from\n // schemaFenceState() alone\" held only until the first drain.\n //\n // The type cannot see the loss: for an optional field, \"absent\" and\n // \"deliberately cleared\" are the same value.\n //\n // Mirrors `persisted-schemas/register.ts:136`, which re-reads for the same\n // reason and documents it — a stale snapshot would roll back a concurrent\n // cutover's version. Re-reading NARROWS that window rather than closing it;\n // fence transitions are orchestrator-driven and serialised, so the residual\n // is the same one that path already accepts.\n const current = await loadFence(this.#store, vault)\n await saveFence(this.#store, vault, { ...current, ...fence })\n }\n\n async readFence(vault: string): Promise<FenceDoc> {\n return loadFence(this.#store, vault)\n }\n\n observeFence(vault: string, onChange: (f: FenceDoc) => void): Unsubscribe {\n let last: FenceDoc | null = null\n let busy = false\n const poll = async () => {\n if (busy) return\n busy = true\n try {\n const fence = await loadFence(this.#store, vault)\n if (last === null || !fenceEqual(last, fence)) {\n last = fence\n onChange(fence)\n }\n } catch {\n /* transient store error — retry next tick */\n } finally {\n busy = false\n }\n }\n void poll() // emit current state once immediately\n const timer = setInterval(() => void poll(), this.#pollIntervalMs)\n unref(timer)\n return () => clearInterval(timer)\n }\n\n async reportPresence(vault: string, p: WriterPresence): Promise<void> {\n await writeClientDoc(this.#store, vault, p.writerId, {\n lastSeen: p.lastSeen,\n quiescedAtVersion: p.quiescedAtVersion,\n sessionId: p.sessionId,\n })\n }\n\n observePresence(vault: string, onChange: (writers: readonly WriterPresence[]) => void): Unsubscribe {\n let last: readonly WriterPresence[] | null = null\n let busy = false\n const poll = async () => {\n if (busy) return\n busy = true\n try {\n const docs = await listClientDocs(this.#store, vault)\n const writers = docs.map(toPresence)\n if (last === null || !presenceListEqual(last, writers)) {\n last = writers\n onChange(writers)\n }\n } catch {\n /* transient store error — retry next tick */\n } finally {\n busy = false\n }\n }\n void poll() // emit current set once immediately\n const timer = setInterval(() => void poll(), this.#pollIntervalMs)\n unref(timer)\n return () => clearInterval(timer)\n }\n\n async reachableWriters(vault: string, o: { staleMs: number; now: number }): Promise<readonly WriterPresence[]> {\n const docs = await listClientDocs(this.#store, vault)\n return docs.map(toPresence).filter((w) => o.now - w.lastSeen <= o.staleMs)\n }\n}\n\n/** Best-effort `unref` so a poll timer never holds the process open (Node only). */\nfunction unref(timer: ReturnType<typeof setInterval>): void {\n const t = timer as unknown as { unref?: () => void }\n if (typeof t.unref === 'function') t.unref()\n}\n"],"mappings":"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","names":[]}
@@ -16,7 +16,6 @@ import "../chunk-LP7BEXCT.js";
16
16
  import "../chunk-SL3JAAKN.js";
17
17
  import "../chunk-N4CHCXDL.js";
18
18
  import "../chunk-7O3Y4PPD.js";
19
- import "../chunk-PZ5AY32C.js";
20
19
 
21
20
  // src/with-sync/active.ts
22
21
  function withSync() {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/with-sync/active.ts"],"sourcesContent":["/**\n * Active sync strategy — `withSync()` returns the real implementation\n * that wires the `SyncEngine`, `SyncTransaction`, and `PresenceHandle`\n * constructors into the Noydb / Vault / Collection hot paths.\n *\n * Consumers opt in by:\n *\n * ```ts\n * import { createNoydb } from '@noy-db/hub'\n * import { withSync } from '@noy-db/hub/sync'\n *\n * const db = await createNoydb({\n * store: localStore,\n * sync: remoteStore,\n * user: ...,\n * syncStrategy: withSync(),\n * })\n * ```\n *\n * The factory delegates to the existing `sync.ts`,\n * `sync-transaction.ts`, and `presence.ts` modules. Splitting the\n * import chain through this file is what lets tsup tree-shake the\n * ~856 LOC of replication + presence machinery out of the default\n * bundle when no `withSync()` import is present.\n *\n * Keyring + grant/revoke/magic-link/delegation stay in the always-on\n * core (or tree-shake via direct named imports) — those are required\n * for any multi-user vault, even purely local ones.\n *\n * @public\n */\n\nimport type { SyncStrategy, BuildSyncEngineOptions } from './strategy.js'\nimport type { PresenceHandleOpts } from './presence.js'\nimport type { Vault } from '../kernel/vault.js'\nimport { SyncEngine } from './engine.js'\nimport { SyncTransaction } from './transaction.js'\nimport { PresenceHandle } from './presence.js'\n\nexport function withSync(): SyncStrategy {\n return {\n buildSyncEngine(opts: BuildSyncEngineOptions): SyncEngine {\n return new SyncEngine(opts)\n },\n buildSyncTransaction(vault: Vault, engine: SyncEngine): SyncTransaction {\n return new SyncTransaction(vault, engine)\n },\n buildPresence<P>(opts: PresenceHandleOpts): PresenceHandle<P> {\n return new PresenceHandle<P>(opts)\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAuCO,SAAS,WAAyB;AACvC,SAAO;AAAA,IACL,gBAAgB,MAA0C;AACxD,aAAO,IAAI,WAAW,IAAI;AAAA,IAC5B;AAAA,IACA,qBAAqB,OAAc,QAAqC;AACtE,aAAO,IAAI,gBAAgB,OAAO,MAAM;AAAA,IAC1C;AAAA,IACA,cAAiB,MAA6C;AAC5D,aAAO,IAAI,eAAkB,IAAI;AAAA,IACnC;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/with-sync/active.ts"],"sourcesContent":["/**\n * Active sync strategy — `withSync()` returns the real implementation\n * that wires the `SyncEngine`, `SyncTransaction`, and `PresenceHandle`\n * constructors into the Noydb / Vault / Collection hot paths.\n *\n * Consumers opt in by:\n *\n * ```ts\n * import { createNoydb } from '@noy-db/hub'\n * import { withSync } from '@noy-db/hub/sync'\n *\n * const db = await createNoydb({\n * store: localStore,\n * sync: remoteStore,\n * user: ...,\n * syncStrategy: withSync(),\n * })\n * ```\n *\n * The factory delegates to the existing `sync.ts`,\n * `sync-transaction.ts`, and `presence.ts` modules. Splitting the\n * import chain through this file is what lets tsup tree-shake the\n * ~856 LOC of replication + presence machinery out of the default\n * bundle when no `withSync()` import is present.\n *\n * Keyring + grant/revoke/magic-link/delegation stay in the always-on\n * core (or tree-shake via direct named imports) — those are required\n * for any multi-user vault, even purely local ones.\n *\n * @public\n */\n\nimport type { SyncStrategy, BuildSyncEngineOptions } from './strategy.js'\nimport type { PresenceHandleOpts } from './presence.js'\nimport type { Vault } from '../kernel/vault.js'\nimport { SyncEngine } from './engine.js'\nimport { SyncTransaction } from './transaction.js'\nimport { PresenceHandle } from './presence.js'\n\nexport function withSync(): SyncStrategy {\n return {\n buildSyncEngine(opts: BuildSyncEngineOptions): SyncEngine {\n return new SyncEngine(opts)\n },\n buildSyncTransaction(vault: Vault, engine: SyncEngine): SyncTransaction {\n return new SyncTransaction(vault, engine)\n },\n buildPresence<P>(opts: PresenceHandleOpts): PresenceHandle<P> {\n return new PresenceHandle<P>(opts)\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAuCO,SAAS,WAAyB;AACvC,SAAO;AAAA,IACL,gBAAgB,MAA0C;AACxD,aAAO,IAAI,WAAW,IAAI;AAAA,IAC5B;AAAA,IACA,qBAAqB,OAAc,QAAqC;AACtE,aAAO,IAAI,gBAAgB,OAAO,MAAM;AAAA,IAC1C;AAAA,IACA,cAAiB,MAA6C;AAC5D,aAAO,IAAI,eAAkB,IAAI;AAAA,IACnC;AAAA,EACF;AACF;","names":[]}
@@ -68,7 +68,6 @@ import "../chunk-N4CHCXDL.js";
68
68
  import {
69
69
  TeamNotEnabledError
70
70
  } from "../chunk-7O3Y4PPD.js";
71
- import "../chunk-PZ5AY32C.js";
72
71
  export {
73
72
  MemoryDeviceSeal,
74
73
  NO_TEAM,
@@ -11,7 +11,7 @@ import {
11
11
  putAtTier,
12
12
  syncDerivedOutputs,
13
13
  withTiers
14
- } from "../chunk-RTYJRFQO.js";
14
+ } from "../chunk-EWPRPP3M.js";
15
15
  import "../chunk-UGTBXF5U.js";
16
16
  import "../chunk-JMGDHKQF.js";
17
17
  import "../chunk-CIGAQBJR.js";
@@ -25,7 +25,6 @@ import "../chunk-N4CHCXDL.js";
25
25
  import {
26
26
  TiersNotEnabledError
27
27
  } from "../chunk-7O3Y4PPD.js";
28
- import "../chunk-PZ5AY32C.js";
29
28
  export {
30
29
  NO_TIERS,
31
30
  TiersNotEnabledError,
package/dist/to/index.js CHANGED
@@ -10,7 +10,6 @@ import {
10
10
  UnknownStoreKindError,
11
11
  isConflictError
12
12
  } from "../chunk-7O3Y4PPD.js";
13
- import "../chunk-PZ5AY32C.js";
14
13
 
15
14
  // src/port/to/locator.ts
16
15
  function isPodStore(store) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/port/to/locator.ts"],"sourcesContent":["/**\n * The Locator seam (#945 Task 1): a serializable, CREDENTIALLESS store\n * descriptor plus a factory registry that reconstructs a `NoydbStore` from\n * data.\n *\n * A `StoreDescriptor` is the pod-portable \"what store and where\" — kind,\n * broad topology class, and a kind-specific serializable address/options\n * bag. It is deliberately dumb data: no field on it is ever typed as a\n * function, a `StoreCredentialSource`, or a `StoreCredentials` value, so a\n * descriptor can be written into a pod / manifest / sync payload without an\n * audit for leaked secrets. Anything that must resolve device-side or\n * user-side — a short-lived credential, a device-local directory override,\n * a mounted drive handle — travels out-of-band via `resolve()`'s `opts`,\n * never through the descriptor itself.\n *\n * This module is pure TypeScript: no imports beyond the kernel's own types\n * and errors, no Node built-ins, no crypto. It adds zero runtime\n * dependencies to `@noy-db/hub/to`.\n */\nimport type { NoydbStore, NoydbPodStore, StoreCredentialSource } from '../../kernel/types.js'\nimport { DuplicateStoreKindError, UnknownStoreKindError } from '../../kernel/errors.js'\n\n/**\n * The broad topology bucket a store descriptor's kind falls into —\n * used by callers/UI to group or filter descriptors without knowing every\n * concrete `kind` string.\n */\nexport type StoreClass = 'local' | 'browser' | 'lan' | 'cloud'\n\n/**\n * A serializable, CREDENTIALLESS description of a store instance: enough\n * data to reconstruct a `NoydbStore` via a registered `StoreFactory`, and\n * safe to persist or transmit as-is.\n *\n * CREDENTIALLESS BY CONSTRUCTION: no field here may be typed as a function,\n * a `StoreCredentialSource`, or a `StoreCredentials` value. Credentials\n * NEVER ride the descriptor — they are supplied separately (as a\n * `StoreCredentialSource`) to `StoreLocator.resolve()` at resolve time.\n */\nexport interface StoreDescriptor {\n /** The registered factory key (e.g. `'file'`, `'aws-s3'`, `'webdav'`). */\n readonly kind: string\n /** The broad topology bucket this kind falls into. */\n readonly class: StoreClass\n /** Kind-specific serializable location (e.g. `{ dir }`, `{ bucket, region }`). */\n readonly address: unknown\n /** Kind-specific serializable tuning options. */\n readonly options?: unknown\n}\n\n/**\n * A kind-specific, device-local supplement to a `StoreDescriptor` — a\n * directory override, a mount point, a drive handle — that is resolved\n * device-side and never carried inside a pod alongside the descriptor.\n */\nexport type StoreBinding = unknown\n\n/**\n * Either store shape a factory may produce: the 6-method KV `NoydbStore`, or\n * the whole-vault `NoydbPodStore` implemented by `to-drive` / `to-icloud`.\n *\n * The two are DISJOINT, not sub/supertypes — a pod store has none of the six\n * KV methods — which is why moving between them needs a double cast and why\n * `isPodStore()` exists to narrow instead.\n */\nexport type AnyNoydbStore = NoydbStore | NoydbPodStore\n\n/**\n * Narrow an `AnyNoydbStore` to the pod shape. Discriminates on the\n * `kind: 'bundle'` tag `NoydbPodStore` carries for exactly this purpose; a\n * `NoydbStore` has no `kind` field at all.\n *\n * Use this at a `resolve()` boundary instead of casting — a store resolved\n * from a descriptor read out of a pod is only known to be one of the two\n * shapes at runtime.\n */\nexport function isPodStore(store: AnyNoydbStore): store is NoydbPodStore {\n return (store as NoydbPodStore).kind === 'bundle'\n}\n\n/**\n * Reconstructs a live store from a `StoreDescriptor`. Registered against a\n * `StoreLocator` under the descriptor's `kind`.\n *\n * The type parameter says WHICH shape this factory builds. It defaults to\n * `NoydbStore`, so a bare `StoreFactory` means exactly what it always has;\n * a pod-store factory declares `StoreFactory<NoydbPodStore>` and registers\n * without a cast (#988).\n */\nexport type StoreFactory<S extends AnyNoydbStore = NoydbStore> = (\n descriptor: StoreDescriptor,\n opts: { binding?: StoreBinding; credentials?: StoreCredentialSource },\n) => S | Promise<S>\n\n/** A registry of `StoreFactory`s, keyed by `StoreDescriptor.kind`. */\nexport interface StoreLocator {\n /**\n * Register a factory for `kind`. Throws if `kind` is already registered.\n *\n * Accepts a factory building EITHER store shape — `S` is inferred from the\n * factory's own return type, so a `NoydbPodStore` factory needs no cast.\n */\n register<S extends AnyNoydbStore>(kind: string, factory: StoreFactory<S>): void\n /**\n * Resolve `descriptor` to a live store via its kind's registered factory.\n *\n * Returns `NoydbStore`. If the `kind` was registered with a pod-store\n * factory, the resolved value is really a `NoydbPodStore`: this signature\n * is the seam's remaining unsoundness, kept because widening it to\n * `AnyNoydbStore` would break every existing caller. Where the descriptor's\n * kind is not statically known, prefer `resolveAny()` and `isPodStore()`.\n */\n resolve(\n descriptor: StoreDescriptor,\n opts?: { binding?: StoreBinding; credentials?: StoreCredentialSource },\n ): NoydbStore | Promise<NoydbStore>\n /**\n * As `resolve()`, but typed honestly: the registry is keyed by a runtime\n * `kind` string, so which of the two shapes comes back is not knowable\n * statically. Narrow the result with `isPodStore()`.\n */\n resolveAny(\n descriptor: StoreDescriptor,\n opts?: { binding?: StoreBinding; credentials?: StoreCredentialSource },\n ): AnyNoydbStore | Promise<AnyNoydbStore>\n}\n\n/**\n * Creates a fresh, empty `StoreLocator`.\n *\n * DUPLICATE REGISTRATION: `register()` throws if `kind` is already\n * registered, rather than silently overwriting it (last-wins). A\n * composition root that registers the same kind twice is almost always a\n * mistake — a copy-pasted setup block, two satellite packages fighting\n * over the same kind string — and last-wins would hide it behind whichever\n * registration happened to run last. This mirrors the\n * `DuplicateBehaviorNameError` philosophy (#947): fail loudly at the\n * mistake's source instead of silently at some unrelated `resolve()` call\n * later.\n */\nexport function createStoreLocator(): StoreLocator {\n const factories = new Map<string, StoreFactory<AnyNoydbStore>>()\n\n function lookup(kind: string): StoreFactory<AnyNoydbStore> {\n const factory = factories.get(kind)\n if (!factory) {\n throw new UnknownStoreKindError(kind, [...factories.keys()].sort())\n }\n return factory\n }\n\n return {\n register(kind, factory) {\n if (factories.has(kind)) {\n throw new DuplicateStoreKindError(kind)\n }\n factories.set(kind, factory)\n },\n resolve(descriptor, opts = {}) {\n // The one place the seam's `resolve(): NoydbStore` signature is paid\n // for. A pod-store `kind` really returns a `NoydbPodStore`; the registry\n // is keyed by a runtime string so nothing here can prove otherwise.\n // Concentrated in the module that owns the invariant rather than\n // duplicated as `as unknown as StoreFactory` in every pod-store package\n // (#988). `resolveAny()` is the honest signature over the same call.\n return lookup(descriptor.kind)(descriptor, opts) as NoydbStore | Promise<NoydbStore>\n },\n resolveAny(descriptor, opts = {}) {\n return lookup(descriptor.kind)(descriptor, opts)\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AA4EO,SAAS,WAAW,OAA8C;AACvE,SAAQ,MAAwB,SAAS;AAC3C;AA8DO,SAAS,qBAAmC;AACjD,QAAM,YAAY,oBAAI,IAAyC;AAE/D,WAAS,OAAO,MAA2C;AACzD,UAAM,UAAU,UAAU,IAAI,IAAI;AAClC,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,sBAAsB,MAAM,CAAC,GAAG,UAAU,KAAK,CAAC,EAAE,KAAK,CAAC;AAAA,IACpE;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,SAAS,MAAM,SAAS;AACtB,UAAI,UAAU,IAAI,IAAI,GAAG;AACvB,cAAM,IAAI,wBAAwB,IAAI;AAAA,MACxC;AACA,gBAAU,IAAI,MAAM,OAAO;AAAA,IAC7B;AAAA,IACA,QAAQ,YAAY,OAAO,CAAC,GAAG;AAO7B,aAAO,OAAO,WAAW,IAAI,EAAE,YAAY,IAAI;AAAA,IACjD;AAAA,IACA,WAAW,YAAY,OAAO,CAAC,GAAG;AAChC,aAAO,OAAO,WAAW,IAAI,EAAE,YAAY,IAAI;AAAA,IACjD;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/port/to/locator.ts"],"sourcesContent":["/**\n * The Locator seam (#945 Task 1): a serializable, CREDENTIALLESS store\n * descriptor plus a factory registry that reconstructs a `NoydbStore` from\n * data.\n *\n * A `StoreDescriptor` is the pod-portable \"what store and where\" — kind,\n * broad topology class, and a kind-specific serializable address/options\n * bag. It is deliberately dumb data: no field on it is ever typed as a\n * function, a `StoreCredentialSource`, or a `StoreCredentials` value, so a\n * descriptor can be written into a pod / manifest / sync payload without an\n * audit for leaked secrets. Anything that must resolve device-side or\n * user-side — a short-lived credential, a device-local directory override,\n * a mounted drive handle — travels out-of-band via `resolve()`'s `opts`,\n * never through the descriptor itself.\n *\n * This module is pure TypeScript: no imports beyond the kernel's own types\n * and errors, no Node built-ins, no crypto. It adds zero runtime\n * dependencies to `@noy-db/hub/to`.\n */\nimport type { NoydbStore, NoydbPodStore, StoreCredentialSource } from '../../kernel/types.js'\nimport { DuplicateStoreKindError, UnknownStoreKindError } from '../../kernel/errors.js'\n\n/**\n * The broad topology bucket a store descriptor's kind falls into —\n * used by callers/UI to group or filter descriptors without knowing every\n * concrete `kind` string.\n */\nexport type StoreClass = 'local' | 'browser' | 'lan' | 'cloud'\n\n/**\n * A serializable, CREDENTIALLESS description of a store instance: enough\n * data to reconstruct a `NoydbStore` via a registered `StoreFactory`, and\n * safe to persist or transmit as-is.\n *\n * CREDENTIALLESS BY CONSTRUCTION: no field here may be typed as a function,\n * a `StoreCredentialSource`, or a `StoreCredentials` value. Credentials\n * NEVER ride the descriptor — they are supplied separately (as a\n * `StoreCredentialSource`) to `StoreLocator.resolve()` at resolve time.\n */\nexport interface StoreDescriptor {\n /** The registered factory key (e.g. `'file'`, `'aws-s3'`, `'webdav'`). */\n readonly kind: string\n /** The broad topology bucket this kind falls into. */\n readonly class: StoreClass\n /** Kind-specific serializable location (e.g. `{ dir }`, `{ bucket, region }`). */\n readonly address: unknown\n /** Kind-specific serializable tuning options. */\n readonly options?: unknown\n}\n\n/**\n * A kind-specific, device-local supplement to a `StoreDescriptor` — a\n * directory override, a mount point, a drive handle — that is resolved\n * device-side and never carried inside a pod alongside the descriptor.\n */\nexport type StoreBinding = unknown\n\n/**\n * Either store shape a factory may produce: the 6-method KV `NoydbStore`, or\n * the whole-vault `NoydbPodStore` implemented by `to-drive` / `to-icloud`.\n *\n * The two are DISJOINT, not sub/supertypes — a pod store has none of the six\n * KV methods — which is why moving between them needs a double cast and why\n * `isPodStore()` exists to narrow instead.\n */\nexport type AnyNoydbStore = NoydbStore | NoydbPodStore\n\n/**\n * Narrow an `AnyNoydbStore` to the pod shape. Discriminates on the\n * `kind: 'bundle'` tag `NoydbPodStore` carries for exactly this purpose; a\n * `NoydbStore` has no `kind` field at all.\n *\n * Use this at a `resolve()` boundary instead of casting — a store resolved\n * from a descriptor read out of a pod is only known to be one of the two\n * shapes at runtime.\n */\nexport function isPodStore(store: AnyNoydbStore): store is NoydbPodStore {\n return (store as NoydbPodStore).kind === 'bundle'\n}\n\n/**\n * Reconstructs a live store from a `StoreDescriptor`. Registered against a\n * `StoreLocator` under the descriptor's `kind`.\n *\n * The type parameter says WHICH shape this factory builds. It defaults to\n * `NoydbStore`, so a bare `StoreFactory` means exactly what it always has;\n * a pod-store factory declares `StoreFactory<NoydbPodStore>` and registers\n * without a cast (#988).\n */\nexport type StoreFactory<S extends AnyNoydbStore = NoydbStore> = (\n descriptor: StoreDescriptor,\n opts: { binding?: StoreBinding; credentials?: StoreCredentialSource },\n) => S | Promise<S>\n\n/** A registry of `StoreFactory`s, keyed by `StoreDescriptor.kind`. */\nexport interface StoreLocator {\n /**\n * Register a factory for `kind`. Throws if `kind` is already registered.\n *\n * Accepts a factory building EITHER store shape — `S` is inferred from the\n * factory's own return type, so a `NoydbPodStore` factory needs no cast.\n */\n register<S extends AnyNoydbStore>(kind: string, factory: StoreFactory<S>): void\n /**\n * Resolve `descriptor` to a live store via its kind's registered factory.\n *\n * Returns `NoydbStore`. If the `kind` was registered with a pod-store\n * factory, the resolved value is really a `NoydbPodStore`: this signature\n * is the seam's remaining unsoundness, kept because widening it to\n * `AnyNoydbStore` would break every existing caller. Where the descriptor's\n * kind is not statically known, prefer `resolveAny()` and `isPodStore()`.\n */\n resolve(\n descriptor: StoreDescriptor,\n opts?: { binding?: StoreBinding; credentials?: StoreCredentialSource },\n ): NoydbStore | Promise<NoydbStore>\n /**\n * As `resolve()`, but typed honestly: the registry is keyed by a runtime\n * `kind` string, so which of the two shapes comes back is not knowable\n * statically. Narrow the result with `isPodStore()`.\n */\n resolveAny(\n descriptor: StoreDescriptor,\n opts?: { binding?: StoreBinding; credentials?: StoreCredentialSource },\n ): AnyNoydbStore | Promise<AnyNoydbStore>\n}\n\n/**\n * Creates a fresh, empty `StoreLocator`.\n *\n * DUPLICATE REGISTRATION: `register()` throws if `kind` is already\n * registered, rather than silently overwriting it (last-wins). A\n * composition root that registers the same kind twice is almost always a\n * mistake — a copy-pasted setup block, two satellite packages fighting\n * over the same kind string — and last-wins would hide it behind whichever\n * registration happened to run last. This mirrors the\n * `DuplicateBehaviorNameError` philosophy (#947): fail loudly at the\n * mistake's source instead of silently at some unrelated `resolve()` call\n * later.\n */\nexport function createStoreLocator(): StoreLocator {\n const factories = new Map<string, StoreFactory<AnyNoydbStore>>()\n\n function lookup(kind: string): StoreFactory<AnyNoydbStore> {\n const factory = factories.get(kind)\n if (!factory) {\n throw new UnknownStoreKindError(kind, [...factories.keys()].sort())\n }\n return factory\n }\n\n return {\n register(kind, factory) {\n if (factories.has(kind)) {\n throw new DuplicateStoreKindError(kind)\n }\n factories.set(kind, factory)\n },\n resolve(descriptor, opts = {}) {\n // The one place the seam's `resolve(): NoydbStore` signature is paid\n // for. A pod-store `kind` really returns a `NoydbPodStore`; the registry\n // is keyed by a runtime string so nothing here can prove otherwise.\n // Concentrated in the module that owns the invariant rather than\n // duplicated as `as unknown as StoreFactory` in every pod-store package\n // (#988). `resolveAny()` is the honest signature over the same call.\n return lookup(descriptor.kind)(descriptor, opts) as NoydbStore | Promise<NoydbStore>\n },\n resolveAny(descriptor, opts = {}) {\n return lookup(descriptor.kind)(descriptor, opts)\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AA4EO,SAAS,WAAW,OAA8C;AACvE,SAAQ,MAAwB,SAAS;AAC3C;AA8DO,SAAS,qBAAmC;AACjD,QAAM,YAAY,oBAAI,IAAyC;AAE/D,WAAS,OAAO,MAA2C;AACzD,UAAM,UAAU,UAAU,IAAI,IAAI;AAClC,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,sBAAsB,MAAM,CAAC,GAAG,UAAU,KAAK,CAAC,EAAE,KAAK,CAAC;AAAA,IACpE;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,SAAS,MAAM,SAAS;AACtB,UAAI,UAAU,IAAI,IAAI,GAAG;AACvB,cAAM,IAAI,wBAAwB,IAAI;AAAA,MACxC;AACA,gBAAU,IAAI,MAAM,OAAO;AAAA,IAC7B;AAAA,IACA,QAAQ,YAAY,OAAO,CAAC,GAAG;AAO7B,aAAO,OAAO,WAAW,IAAI,EAAE,YAAY,IAAI;AAAA,IACjD;AAAA,IACA,WAAW,YAAY,OAAO,CAAC,GAAG;AAChC,aAAO,OAAO,WAAW,IAAI,EAAE,YAAY,IAAI;AAAA,IACjD;AAAA,EACF;AACF;","names":[]}
@@ -3,16 +3,15 @@ import {
3
3
  TxContext,
4
4
  TxVault,
5
5
  runTransaction
6
- } from "../chunk-TO3EQ7HF.js";
6
+ } from "../chunk-BLN54W3L.js";
7
7
  import {
8
8
  lazy
9
9
  } from "../chunk-5D2ALSM5.js";
10
10
  import "../chunk-4AOERLTV.js";
11
11
  import "../chunk-7O3Y4PPD.js";
12
- import "../chunk-PZ5AY32C.js";
13
12
 
14
13
  // src/with-commit/tx/dry-run.ts
15
- var loadGuardExecutor = lazy(() => import("../executor-MZJXBWNM.js"));
14
+ var loadGuardExecutor = lazy(() => import("../executor-AQVXV5GZ.js"));
16
15
  var SEP = " ";
17
16
  var keyOf = (op) => `${op.vaultName}${SEP}${op.collectionName}${SEP}${op.id}`;
18
17
  async function runDryRun(db, fn) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/with-commit/tx/dry-run.ts","../../src/with-commit/tx/active.ts"],"sourcesContent":["/**\n * Dry-run transactions. Runs the tx body to STAGE ops, then builds\n * the directly-affected diff (before = current committed via collection.get,\n * after = staged record) and collects guard violations — without executing\n * phase 2. No adapter writes, no write-hooks, no commit. MV/derivation\n * cascade is NOT simulated (v2). Mirrors the guard loop in\n * `Collection._putInternal` — keep the two in sync.\n */\nimport type { Noydb } from '../../kernel/noydb.js'\nimport { TxContext, type StagedOp } from './transaction.js'\n\nimport { lazy } from '../../kernel/lazy.js'\n\n/** #846c — one memoized binding instead of a cast at the call site. */\nconst loadGuardExecutor = lazy(() => import('../../with-audit/guards/executor.js'))\n\nexport interface AffectedDocument {\n readonly vault: string\n readonly op: 'create' | 'update' | 'delete'\n readonly collection: string\n readonly docId: string\n readonly before: unknown // current committed record; null when creating\n readonly after: unknown // staged record; null when deleting\n}\n\nexport interface GuardViolation {\n readonly vault: string\n readonly collection: string\n readonly docId: string\n readonly message: string\n}\n\nexport interface DryRunResult {\n readonly affected: ReadonlyArray<AffectedDocument>\n readonly guardViolations: ReadonlyArray<GuardViolation>\n}\n\nconst SEP = ' '\nconst keyOf = (op: StagedOp) => `${op.vaultName}${SEP}${op.collectionName}${SEP}${op.id}`\n\nexport async function runDryRun(\n db: Noydb,\n fn: (tx: TxContext) => unknown,\n): Promise<DryRunResult> {\n const ctx = new TxContext(db)\n await fn(ctx) // stage ops (reads see staged writes via TxCollection); nothing committed\n\n // Dedup by (vault, collection, id) — last staged op wins.\n const lastOp = new Map<string, StagedOp>()\n for (const op of ctx._ops) lastOp.set(keyOf(op), op)\n\n const affected: AffectedDocument[] = []\n const guardViolations: GuardViolation[] = []\n\n for (const op of lastOp.values()) {\n const v = db.vault(op.vaultName)\n const coll = v.collection(op.collectionName)\n const before = await coll.get(op.id)\n\n if (op.type === 'delete') {\n affected.push({ vault: op.vaultName, op: 'delete', collection: op.collectionName, docId: op.id, before, after: null })\n continue\n }\n\n const after = op.record ?? null\n affected.push({\n vault: op.vaultName,\n op: before === null ? 'create' : 'update',\n collection: op.collectionName,\n docId: op.id,\n before,\n after,\n })\n\n // Guard violations — run the SAME checks Collection._putInternal does,\n // with the SAME ctx (existing + read-only facade + userId + role), but\n // collect the first thrown error instead of aborting.\n const registry = v._getGuardRegistry()\n if (!registry) continue\n const guards = registry.guardsFor(op.collectionName)\n if (guards.length === 0) continue\n const facade = v._getReadOnlyFacade()\n if (!facade) continue\n const gctx = { existing: before as Record<string, unknown> | null, vault: facade, userId: v.userId, role: v.role }\n try {\n await registry.runChecks(op.collectionName, after as Record<string, unknown>, gctx)\n const { GuardExecutor } = await loadGuardExecutor()\n for (const g of guards) {\n await GuardExecutor.checkFrozenFields(g, op.id, before as Record<string, unknown> | null, after as Record<string, unknown>)\n }\n } catch (err) {\n guardViolations.push({\n vault: op.vaultName,\n collection: op.collectionName,\n docId: op.id,\n message: err instanceof Error ? err.message : String(err),\n })\n }\n }\n\n return { affected, guardViolations }\n}\n","/**\n * Active transactions strategy. Only reachable via `@noy-db/hub/transactions`.\n */\n\nimport { runTransaction } from './transaction.js'\nimport { runDryRun } from './dry-run.js'\nimport type { TransactionsStrategy } from './strategy.js'\nimport type { TransactionInvariant } from './invariants.js'\n\n/**\n * Options for {@link withTransactions}. Currently only commit-time\n * changeset `invariants` (see {@link TransactionInvariant}).\n */\nexport interface WithTransactionsOptions {\n /**\n * Commit-time set-level invariants run over the changeset on every\n * commit (ordinary AND amendment). See {@link TransactionInvariant}.\n */\n invariants?: ReadonlyArray<TransactionInvariant>\n}\n\n/**\n * Build the default transactions strategy. Pass into\n * `createNoydb({ transactionsStrategy: withTransactions() })` to enable\n * `db.transaction(fn)` (and `db.transaction({ dryRun: true }, fn)`).\n *\n * Supply `{ invariants }` to register commit-time changeset invariants —\n * set-level constraints that fire after the writes commit and revert the\n * transaction on a throw. Unlike `amendment.invariant`, these run for\n * ordinary `db.transaction(fn)` calls (and amendments) with no role gate.\n */\nexport function withTransactions(opts?: WithTransactionsOptions): TransactionsStrategy {\n const invariants = opts?.invariants ?? []\n return {\n runTransaction: (db, fn, options) => runTransaction(db, fn, options, invariants),\n runDryRun,\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,IAAM,oBAAoB,KAAK,MAAM,OAAO,yBAAqC,CAAC;AAuBlF,IAAM,MAAM;AACZ,IAAM,QAAQ,CAAC,OAAiB,GAAG,GAAG,SAAS,GAAG,GAAG,GAAG,GAAG,cAAc,GAAG,GAAG,GAAG,GAAG,EAAE;AAEvF,eAAsB,UACpB,IACA,IACuB;AACvB,QAAM,MAAM,IAAI,UAAU,EAAE;AAC5B,QAAM,GAAG,GAAG;AAGZ,QAAM,SAAS,oBAAI,IAAsB;AACzC,aAAW,MAAM,IAAI,KAAM,QAAO,IAAI,MAAM,EAAE,GAAG,EAAE;AAEnD,QAAM,WAA+B,CAAC;AACtC,QAAM,kBAAoC,CAAC;AAE3C,aAAW,MAAM,OAAO,OAAO,GAAG;AAChC,UAAM,IAAI,GAAG,MAAM,GAAG,SAAS;AAC/B,UAAM,OAAO,EAAE,WAAW,GAAG,cAAc;AAC3C,UAAM,SAAS,MAAM,KAAK,IAAI,GAAG,EAAE;AAEnC,QAAI,GAAG,SAAS,UAAU;AACxB,eAAS,KAAK,EAAE,OAAO,GAAG,WAAW,IAAI,UAAU,YAAY,GAAG,gBAAgB,OAAO,GAAG,IAAI,QAAQ,OAAO,KAAK,CAAC;AACrH;AAAA,IACF;AAEA,UAAM,QAAQ,GAAG,UAAU;AAC3B,aAAS,KAAK;AAAA,MACZ,OAAO,GAAG;AAAA,MACV,IAAI,WAAW,OAAO,WAAW;AAAA,MACjC,YAAY,GAAG;AAAA,MACf,OAAO,GAAG;AAAA,MACV;AAAA,MACA;AAAA,IACF,CAAC;AAKD,UAAM,WAAW,EAAE,kBAAkB;AACrC,QAAI,CAAC,SAAU;AACf,UAAM,SAAS,SAAS,UAAU,GAAG,cAAc;AACnD,QAAI,OAAO,WAAW,EAAG;AACzB,UAAM,SAAS,EAAE,mBAAmB;AACpC,QAAI,CAAC,OAAQ;AACb,UAAM,OAAO,EAAE,UAAU,QAA0C,OAAO,QAAQ,QAAQ,EAAE,QAAQ,MAAM,EAAE,KAAK;AACjH,QAAI;AACF,YAAM,SAAS,UAAU,GAAG,gBAAgB,OAAkC,IAAI;AAClF,YAAM,EAAE,cAAc,IAAI,MAAM,kBAAkB;AAClD,iBAAW,KAAK,QAAQ;AACtB,cAAM,cAAc,kBAAkB,GAAG,GAAG,IAAI,QAA0C,KAAgC;AAAA,MAC5H;AAAA,IACF,SAAS,KAAK;AACZ,sBAAgB,KAAK;AAAA,QACnB,OAAO,GAAG;AAAA,QACV,YAAY,GAAG;AAAA,QACf,OAAO,GAAG;AAAA,QACV,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,MAC1D,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO,EAAE,UAAU,gBAAgB;AACrC;;;ACtEO,SAAS,iBAAiB,MAAsD;AACrF,QAAM,aAAa,MAAM,cAAc,CAAC;AACxC,SAAO;AAAA,IACL,gBAAgB,CAAC,IAAI,IAAI,YAAY,eAAe,IAAI,IAAI,SAAS,UAAU;AAAA,IAC/E;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/with-commit/tx/dry-run.ts","../../src/with-commit/tx/active.ts"],"sourcesContent":["/**\n * Dry-run transactions. Runs the tx body to STAGE ops, then builds\n * the directly-affected diff (before = current committed via collection.get,\n * after = staged record) and collects guard violations — without executing\n * phase 2. No adapter writes, no write-hooks, no commit. MV/derivation\n * cascade is NOT simulated (v2). Mirrors the guard loop in\n * `Collection._putInternal` — keep the two in sync.\n */\nimport type { Noydb } from '../../kernel/noydb.js'\nimport { TxContext, type StagedOp } from './transaction.js'\n\nimport { lazy } from '../../kernel/lazy.js'\n\n/** #846c — one memoized binding instead of a cast at the call site. */\nconst loadGuardExecutor = lazy(() => import('../../with-audit/guards/executor.js'))\n\nexport interface AffectedDocument {\n readonly vault: string\n readonly op: 'create' | 'update' | 'delete'\n readonly collection: string\n readonly docId: string\n readonly before: unknown // current committed record; null when creating\n readonly after: unknown // staged record; null when deleting\n}\n\nexport interface GuardViolation {\n readonly vault: string\n readonly collection: string\n readonly docId: string\n readonly message: string\n}\n\nexport interface DryRunResult {\n readonly affected: ReadonlyArray<AffectedDocument>\n readonly guardViolations: ReadonlyArray<GuardViolation>\n}\n\nconst SEP = ' '\nconst keyOf = (op: StagedOp) => `${op.vaultName}${SEP}${op.collectionName}${SEP}${op.id}`\n\nexport async function runDryRun(\n db: Noydb,\n fn: (tx: TxContext) => unknown,\n): Promise<DryRunResult> {\n const ctx = new TxContext(db)\n await fn(ctx) // stage ops (reads see staged writes via TxCollection); nothing committed\n\n // Dedup by (vault, collection, id) — last staged op wins.\n const lastOp = new Map<string, StagedOp>()\n for (const op of ctx._ops) lastOp.set(keyOf(op), op)\n\n const affected: AffectedDocument[] = []\n const guardViolations: GuardViolation[] = []\n\n for (const op of lastOp.values()) {\n const v = db.vault(op.vaultName)\n const coll = v.collection(op.collectionName)\n const before = await coll.get(op.id)\n\n if (op.type === 'delete') {\n affected.push({ vault: op.vaultName, op: 'delete', collection: op.collectionName, docId: op.id, before, after: null })\n continue\n }\n\n const after = op.record ?? null\n affected.push({\n vault: op.vaultName,\n op: before === null ? 'create' : 'update',\n collection: op.collectionName,\n docId: op.id,\n before,\n after,\n })\n\n // Guard violations — run the SAME checks Collection._putInternal does,\n // with the SAME ctx (existing + read-only facade + userId + role), but\n // collect the first thrown error instead of aborting.\n const registry = v._getGuardRegistry()\n if (!registry) continue\n const guards = registry.guardsFor(op.collectionName)\n if (guards.length === 0) continue\n const facade = v._getReadOnlyFacade()\n if (!facade) continue\n const gctx = { existing: before as Record<string, unknown> | null, vault: facade, userId: v.userId, role: v.role }\n try {\n await registry.runChecks(op.collectionName, after as Record<string, unknown>, gctx)\n const { GuardExecutor } = await loadGuardExecutor()\n for (const g of guards) {\n await GuardExecutor.checkFrozenFields(g, op.id, before as Record<string, unknown> | null, after as Record<string, unknown>)\n }\n } catch (err) {\n guardViolations.push({\n vault: op.vaultName,\n collection: op.collectionName,\n docId: op.id,\n message: err instanceof Error ? err.message : String(err),\n })\n }\n }\n\n return { affected, guardViolations }\n}\n","/**\n * Active transactions strategy. Only reachable via `@noy-db/hub/transactions`.\n */\n\nimport { runTransaction } from './transaction.js'\nimport { runDryRun } from './dry-run.js'\nimport type { TransactionsStrategy } from './strategy.js'\nimport type { TransactionInvariant } from './invariants.js'\n\n/**\n * Options for {@link withTransactions}. Currently only commit-time\n * changeset `invariants` (see {@link TransactionInvariant}).\n */\nexport interface WithTransactionsOptions {\n /**\n * Commit-time set-level invariants run over the changeset on every\n * commit (ordinary AND amendment). See {@link TransactionInvariant}.\n */\n invariants?: ReadonlyArray<TransactionInvariant>\n}\n\n/**\n * Build the default transactions strategy. Pass into\n * `createNoydb({ transactionsStrategy: withTransactions() })` to enable\n * `db.transaction(fn)` (and `db.transaction({ dryRun: true }, fn)`).\n *\n * Supply `{ invariants }` to register commit-time changeset invariants —\n * set-level constraints that fire after the writes commit and revert the\n * transaction on a throw. Unlike `amendment.invariant`, these run for\n * ordinary `db.transaction(fn)` calls (and amendments) with no role gate.\n */\nexport function withTransactions(opts?: WithTransactionsOptions): TransactionsStrategy {\n const invariants = opts?.invariants ?? []\n return {\n runTransaction: (db, fn, options) => runTransaction(db, fn, options, invariants),\n runDryRun,\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAcA,IAAM,oBAAoB,KAAK,MAAM,OAAO,yBAAqC,CAAC;AAuBlF,IAAM,MAAM;AACZ,IAAM,QAAQ,CAAC,OAAiB,GAAG,GAAG,SAAS,GAAG,GAAG,GAAG,GAAG,cAAc,GAAG,GAAG,GAAG,GAAG,EAAE;AAEvF,eAAsB,UACpB,IACA,IACuB;AACvB,QAAM,MAAM,IAAI,UAAU,EAAE;AAC5B,QAAM,GAAG,GAAG;AAGZ,QAAM,SAAS,oBAAI,IAAsB;AACzC,aAAW,MAAM,IAAI,KAAM,QAAO,IAAI,MAAM,EAAE,GAAG,EAAE;AAEnD,QAAM,WAA+B,CAAC;AACtC,QAAM,kBAAoC,CAAC;AAE3C,aAAW,MAAM,OAAO,OAAO,GAAG;AAChC,UAAM,IAAI,GAAG,MAAM,GAAG,SAAS;AAC/B,UAAM,OAAO,EAAE,WAAW,GAAG,cAAc;AAC3C,UAAM,SAAS,MAAM,KAAK,IAAI,GAAG,EAAE;AAEnC,QAAI,GAAG,SAAS,UAAU;AACxB,eAAS,KAAK,EAAE,OAAO,GAAG,WAAW,IAAI,UAAU,YAAY,GAAG,gBAAgB,OAAO,GAAG,IAAI,QAAQ,OAAO,KAAK,CAAC;AACrH;AAAA,IACF;AAEA,UAAM,QAAQ,GAAG,UAAU;AAC3B,aAAS,KAAK;AAAA,MACZ,OAAO,GAAG;AAAA,MACV,IAAI,WAAW,OAAO,WAAW;AAAA,MACjC,YAAY,GAAG;AAAA,MACf,OAAO,GAAG;AAAA,MACV;AAAA,MACA;AAAA,IACF,CAAC;AAKD,UAAM,WAAW,EAAE,kBAAkB;AACrC,QAAI,CAAC,SAAU;AACf,UAAM,SAAS,SAAS,UAAU,GAAG,cAAc;AACnD,QAAI,OAAO,WAAW,EAAG;AACzB,UAAM,SAAS,EAAE,mBAAmB;AACpC,QAAI,CAAC,OAAQ;AACb,UAAM,OAAO,EAAE,UAAU,QAA0C,OAAO,QAAQ,QAAQ,EAAE,QAAQ,MAAM,EAAE,KAAK;AACjH,QAAI;AACF,YAAM,SAAS,UAAU,GAAG,gBAAgB,OAAkC,IAAI;AAClF,YAAM,EAAE,cAAc,IAAI,MAAM,kBAAkB;AAClD,iBAAW,KAAK,QAAQ;AACtB,cAAM,cAAc,kBAAkB,GAAG,GAAG,IAAI,QAA0C,KAAgC;AAAA,MAC5H;AAAA,IACF,SAAS,KAAK;AACZ,sBAAgB,KAAK;AAAA,QACnB,OAAO,GAAG;AAAA,QACV,YAAY,GAAG;AAAA,QACf,OAAO,GAAG;AAAA,QACV,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,MAC1D,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO,EAAE,UAAU,gBAAgB;AACrC;;;ACtEO,SAAS,iBAAiB,MAAsD;AACrF,QAAM,aAAa,MAAM,cAAc,CAAC;AACxC,SAAO;AAAA,IACL,gBAAgB,CAAC,IAAI,IAAI,YAAY,eAAe,IAAI,IAAI,SAAS,UAAU;AAAA,IAC/E;AAAA,EACF;AACF;","names":[]}
@@ -2,9 +2,8 @@ import {
2
2
  generateULID,
3
3
  isULID
4
4
  } from "./chunk-4AOERLTV.js";
5
- import "./chunk-PZ5AY32C.js";
6
5
  export {
7
6
  generateULID,
8
7
  isULID
9
8
  };
10
- //# sourceMappingURL=ulid-VQW6ERQN.js.map
9
+ //# sourceMappingURL=ulid-4MKXG325.js.map
@@ -4,7 +4,6 @@ import {
4
4
  import {
5
5
  FilenameSanitizationError
6
6
  } from "../chunk-7O3Y4PPD.js";
7
- import "../chunk-PZ5AY32C.js";
8
7
 
9
8
  // src/kernel/util/sanitize-filename.ts
10
9
  var REPLACEMENT = "_";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/kernel/util/sanitize-filename.ts"],"sourcesContent":["/**\n * Target-profile aware filename sanitizer.\n *\n * Pure string in / string out. No filesystem access, no I/O. Use this\n * at the boundary where a user-supplied filename meets a storage\n * destination — local FS, ZIP archive, S3 key, URL path, SMB share —\n * to defuse the canonical 14-class footgun before it reaches that\n * destination.\n *\n * ## Threat model\n *\n * 1. Path injection — `..`, NUL bytes, `\\` on POSIX, absolute paths.\n * 2. Windows reserved names — `CON`, `PRN`, `AUX`, `NUL`,\n * `COM1-9`, `LPT1-9`, with or without an extension.\n * 3. Windows reserved chars — `< > : \" / \\ | ? *` plus ASCII 0-31.\n * 4. Trailing `.` and ` ` on Windows / SMB.\n * 5. Unicode normalization drift — same display, different bytes →\n * \"two files with the same name\" sync ghosts.\n * 6. Bidi override spoofing — U+202E reversing `harmless.exe.txt`\n * into `harmless.txt.exe`.\n * 7. URL `+` ambiguity in S3 presigned URLs.\n * 8. ZIP general-purpose-flag bit 11 (UTF-8 filename) optional in\n * pre-2006 readers.\n * 9-14. Length caps, leading/trailing whitespace + controls,\n * `.DS_Store`-style hidden noise, etc.\n *\n * ## Always-on transforms (every profile)\n *\n * - NFC normalize (`String.prototype.normalize('NFC')`).\n * - Reject NUL — hard fail, not strip. Silent strip enables a\n * classic truncation bypass (`safe.txt\\0.exe` → `safe.txt`).\n * - Strip bidi overrides (`U+202A..U+202E`, `U+2066..U+2069`).\n * - Trim leading/trailing whitespace and ASCII control chars.\n *\n * ## Non-goals (i18n boundary policy, )\n *\n * - No transliteration.\n * - No locale-aware slugging.\n * - No script-specific segmentation.\n *\n * @module\n */\n\nimport { FilenameSanitizationError } from '../errors.js'\n\n/**\n * One of seven storage destinations the sanitizer knows how to defang\n * for. Pick the most restrictive that covers your write site:\n * `'macos-smb'` is the safe default for \"I don't know where these\n * files end up but they're going to a real filesystem somewhere.\"\n */\nexport type FilenameProfile =\n | 'posix'\n | 'windows'\n | 'macos-smb'\n | 'zip'\n | 'url-path'\n | 's3-key'\n | 'opaque'\n\nexport interface SanitizeFilenameOptions {\n /** Target-destination profile. */\n readonly profile: FilenameProfile\n /**\n * Override the per-profile length cap. Useful when leaving headroom\n * for a collision suffix — e.g. `maxBytes: 240` on a `posix` write\n * site that wants 15 bytes of slack for `-1`, `-2`, …\n */\n readonly maxBytes?: number\n /**\n * Required when `profile === 'opaque'`. The opaque profile replaces\n * the entire input with `${opaqueId}.${ext}` (extension preserved\n * from the input when present).\n */\n readonly opaqueId?: string\n}\n\nconst REPLACEMENT = '_'\n\n// Bidi overrides: U+202A LRE, U+202B RLE, U+202C PDF, U+202D LRO,\n// U+202E RLO, U+2066 LRI, U+2067 RLI, U+2068 FSI, U+2069 PDI.\nconst BIDI_OVERRIDES = /[‪-‮⁦-⁩]/g\n\n// Reserved by Windows / NTFS / FAT / SMB.\nconst WINDOWS_RESERVED_CHARS = /[<>:\"/\\\\|?*]/g\n\nconst WINDOWS_RESERVED_NAMES = new Set([\n 'CON', 'PRN', 'AUX', 'NUL',\n 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',\n 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',\n])\n\n// macOS legacy hidden-noise files. Match the full name.\nconst MAC_HIDDEN_NOISE = /^(?:\\.DS_Store|\\.localized|\\.fseventsd|\\._.+)$/i\n\n// RFC 3986 unreserved set.\nconst URL_UNRESERVED = /[A-Za-z0-9\\-._~]/\n\nconst utf8 = new TextEncoder()\n\nfunction isControlCode(cp: number): boolean {\n return (cp >= 0 && cp <= 0x1f) || cp === 0x7f\n}\n\nfunction stripControlChars(s: string): string {\n let out = ''\n for (let i = 0; i < s.length; i++) {\n out += isControlCode(s.charCodeAt(i)) ? REPLACEMENT : s[i]\n }\n return out\n}\n\nfunction trimWhitespaceAndControls(s: string): string {\n let start = 0\n let end = s.length\n while (start < end) {\n const ch = s[start]!\n if (!isControlCode(s.charCodeAt(start)) && ch.trim() !== '') break\n start++\n }\n while (end > start) {\n const ch = s[end - 1]!\n if (!isControlCode(s.charCodeAt(end - 1)) && ch.trim() !== '') break\n end--\n }\n return s.slice(start, end)\n}\n\n/**\n * Sanitize a filename for a target-destination profile. Pure: same\n * input + options always returns the same output, no I/O.\n *\n * Returns the sanitized name. Throws {@link FilenameSanitizationError}\n * when the input cannot be made safe at all (NUL byte, empty after\n * normalization, missing `opaqueId` for the opaque profile,\n * `..` segment that would fall out of any reasonable target).\n */\nexport function sanitizeFilename(name: string, opts: SanitizeFilenameOptions): string {\n if (typeof name !== 'string') {\n throw new FilenameSanitizationError('input must be a string')\n }\n if (name.includes('\\0')) {\n throw new FilenameSanitizationError('NUL byte in filename')\n }\n\n let s = name.normalize('NFC').replace(BIDI_OVERRIDES, '')\n\n if (opts.profile === 'opaque') {\n return applyOpaque(s, opts)\n }\n\n s = trimWhitespaceAndControls(s)\n\n switch (opts.profile) {\n case 'posix': return cap(applyPosix(s), opts.maxBytes ?? 255, 'utf8')\n case 'windows': return cap(applyWindows(s), opts.maxBytes ?? 255, 'utf16')\n case 'macos-smb': return cap(applyMacosSmb(s), opts.maxBytes ?? 240, 'utf8')\n case 'zip': return cap(applyZip(s), opts.maxBytes ?? 255, 'utf8')\n case 'url-path': return cap(applyUrlPath(s), opts.maxBytes ?? 1024, 'bytes-pre-encode')\n case 's3-key': return cap(applyS3Key(s), opts.maxBytes ?? 1024, 'utf8')\n }\n}\n\n// ─── Profile implementations ────────────────────────────────────────────\n\nfunction applyPosix(s: string): string {\n // POSIX is permissive; only `/` and NUL are off-limits.\n const cleaned = s.replace(/\\//g, REPLACEMENT)\n return rejectDotSegments(cleaned)\n}\n\nfunction applyWindows(s: string): string {\n let cleaned = s.replace(WINDOWS_RESERVED_CHARS, REPLACEMENT)\n cleaned = stripControlChars(cleaned)\n // Trailing space/dot are stripped by Win32 path resolution.\n cleaned = cleaned.replace(/[. ]+$/g, '')\n cleaned = rejectDotSegments(cleaned)\n return avoidWindowsReservedName(cleaned)\n}\n\nfunction applyMacosSmb(s: string): string {\n let cleaned = applyWindows(s)\n if (MAC_HIDDEN_NOISE.test(cleaned)) {\n cleaned = REPLACEMENT + cleaned\n }\n return cleaned\n}\n\nfunction applyZip(s: string): string {\n let cleaned = s.replace(/^\\/+/, '')\n cleaned = rejectDotSegments(cleaned)\n return stripControlChars(cleaned)\n}\n\nfunction applyUrlPath(s: string): string {\n // RFC 3986 percent-encoding for path segment. Keep `unreserved`,\n // encode everything else. `+` is encoded as `%2B` because S3 and\n // legacy servers treat raw `+` as space ambiguously.\n let out = ''\n for (const ch of s) {\n if (URL_UNRESERVED.test(ch)) { out += ch; continue }\n const bytes = utf8.encode(ch)\n for (const b of bytes) {\n out += '%' + b.toString(16).toUpperCase().padStart(2, '0')\n }\n }\n return out\n}\n\nfunction applyS3Key(s: string): string {\n // Strip leading slashes BEFORE percent-encoding — once `/` is\n // encoded to `%2F` the leading-slash regex no longer matches.\n return applyUrlPath(s.replace(/^\\/+/, ''))\n}\n\nfunction applyOpaque(s: string, opts: SanitizeFilenameOptions): string {\n if (!opts.opaqueId) {\n throw new FilenameSanitizationError('opaque profile requires opaqueId')\n }\n // Preserve a \"safe-looking\" extension only — alphanumeric, ≤16 bytes.\n const dot = s.lastIndexOf('.')\n if (dot > 0 && dot < s.length - 1) {\n const ext = s.slice(dot + 1)\n if (/^[A-Za-z0-9]{1,16}$/.test(ext)) {\n return `${opts.opaqueId}.${ext.toLowerCase()}`\n }\n }\n return opts.opaqueId\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────\n\nfunction rejectDotSegments(s: string): string {\n if (s === '.' || s === '..' || s.split(/[/\\\\]/).some((seg) => seg === '..')) {\n throw new FilenameSanitizationError('path traversal segment in filename')\n }\n if (s.length === 0) {\n throw new FilenameSanitizationError('empty filename after sanitization')\n }\n return s\n}\n\nfunction avoidWindowsReservedName(s: string): string {\n const dot = s.indexOf('.')\n const base = dot === -1 ? s : s.slice(0, dot)\n if (WINDOWS_RESERVED_NAMES.has(base.toUpperCase())) {\n return REPLACEMENT + s\n }\n return s\n}\n\ntype LengthUnit = 'utf8' | 'utf16' | 'bytes-pre-encode'\n\nfunction cap(s: string, max: number, unit: LengthUnit): string {\n if (max <= 0) {\n throw new FilenameSanitizationError('maxBytes must be positive')\n }\n if (unit === 'utf16') {\n if (s.length <= max) return s\n return s.slice(0, max)\n }\n if (unit === 'bytes-pre-encode') {\n if (utf8.encode(s).byteLength <= max) return s\n return truncateToByteCap(s, max)\n }\n if (utf8.encode(s).byteLength <= max) return s\n return truncateToByteCap(s, max)\n}\n\nfunction truncateToByteCap(s: string, max: number): string {\n // Walk by code points so we never split a multi-byte sequence in\n // the middle. Whole-grapheme handling (ZWJ-joined emoji) is a\n // documented non-goal; we only protect against UTF-8 boundary\n // splits here.\n let out = ''\n let used = 0\n for (const cp of s) {\n const n = utf8.encode(cp).byteLength\n if (used + n > max) break\n out += cp\n used += n\n }\n if (out.length === 0) {\n throw new FilenameSanitizationError('maxBytes too small for a single code point')\n }\n return out\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/kernel/util/sanitize-filename.ts"],"sourcesContent":["/**\n * Target-profile aware filename sanitizer.\n *\n * Pure string in / string out. No filesystem access, no I/O. Use this\n * at the boundary where a user-supplied filename meets a storage\n * destination — local FS, ZIP archive, S3 key, URL path, SMB share —\n * to defuse the canonical 14-class footgun before it reaches that\n * destination.\n *\n * ## Threat model\n *\n * 1. Path injection — `..`, NUL bytes, `\\` on POSIX, absolute paths.\n * 2. Windows reserved names — `CON`, `PRN`, `AUX`, `NUL`,\n * `COM1-9`, `LPT1-9`, with or without an extension.\n * 3. Windows reserved chars — `< > : \" / \\ | ? *` plus ASCII 0-31.\n * 4. Trailing `.` and ` ` on Windows / SMB.\n * 5. Unicode normalization drift — same display, different bytes →\n * \"two files with the same name\" sync ghosts.\n * 6. Bidi override spoofing — U+202E reversing `harmless.exe.txt`\n * into `harmless.txt.exe`.\n * 7. URL `+` ambiguity in S3 presigned URLs.\n * 8. ZIP general-purpose-flag bit 11 (UTF-8 filename) optional in\n * pre-2006 readers.\n * 9-14. Length caps, leading/trailing whitespace + controls,\n * `.DS_Store`-style hidden noise, etc.\n *\n * ## Always-on transforms (every profile)\n *\n * - NFC normalize (`String.prototype.normalize('NFC')`).\n * - Reject NUL — hard fail, not strip. Silent strip enables a\n * classic truncation bypass (`safe.txt\\0.exe` → `safe.txt`).\n * - Strip bidi overrides (`U+202A..U+202E`, `U+2066..U+2069`).\n * - Trim leading/trailing whitespace and ASCII control chars.\n *\n * ## Non-goals (i18n boundary policy, )\n *\n * - No transliteration.\n * - No locale-aware slugging.\n * - No script-specific segmentation.\n *\n * @module\n */\n\nimport { FilenameSanitizationError } from '../errors.js'\n\n/**\n * One of seven storage destinations the sanitizer knows how to defang\n * for. Pick the most restrictive that covers your write site:\n * `'macos-smb'` is the safe default for \"I don't know where these\n * files end up but they're going to a real filesystem somewhere.\"\n */\nexport type FilenameProfile =\n | 'posix'\n | 'windows'\n | 'macos-smb'\n | 'zip'\n | 'url-path'\n | 's3-key'\n | 'opaque'\n\nexport interface SanitizeFilenameOptions {\n /** Target-destination profile. */\n readonly profile: FilenameProfile\n /**\n * Override the per-profile length cap. Useful when leaving headroom\n * for a collision suffix — e.g. `maxBytes: 240` on a `posix` write\n * site that wants 15 bytes of slack for `-1`, `-2`, …\n */\n readonly maxBytes?: number\n /**\n * Required when `profile === 'opaque'`. The opaque profile replaces\n * the entire input with `${opaqueId}.${ext}` (extension preserved\n * from the input when present).\n */\n readonly opaqueId?: string\n}\n\nconst REPLACEMENT = '_'\n\n// Bidi overrides: U+202A LRE, U+202B RLE, U+202C PDF, U+202D LRO,\n// U+202E RLO, U+2066 LRI, U+2067 RLI, U+2068 FSI, U+2069 PDI.\nconst BIDI_OVERRIDES = /[‪-‮⁦-⁩]/g\n\n// Reserved by Windows / NTFS / FAT / SMB.\nconst WINDOWS_RESERVED_CHARS = /[<>:\"/\\\\|?*]/g\n\nconst WINDOWS_RESERVED_NAMES = new Set([\n 'CON', 'PRN', 'AUX', 'NUL',\n 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',\n 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',\n])\n\n// macOS legacy hidden-noise files. Match the full name.\nconst MAC_HIDDEN_NOISE = /^(?:\\.DS_Store|\\.localized|\\.fseventsd|\\._.+)$/i\n\n// RFC 3986 unreserved set.\nconst URL_UNRESERVED = /[A-Za-z0-9\\-._~]/\n\nconst utf8 = new TextEncoder()\n\nfunction isControlCode(cp: number): boolean {\n return (cp >= 0 && cp <= 0x1f) || cp === 0x7f\n}\n\nfunction stripControlChars(s: string): string {\n let out = ''\n for (let i = 0; i < s.length; i++) {\n out += isControlCode(s.charCodeAt(i)) ? REPLACEMENT : s[i]\n }\n return out\n}\n\nfunction trimWhitespaceAndControls(s: string): string {\n let start = 0\n let end = s.length\n while (start < end) {\n const ch = s[start]!\n if (!isControlCode(s.charCodeAt(start)) && ch.trim() !== '') break\n start++\n }\n while (end > start) {\n const ch = s[end - 1]!\n if (!isControlCode(s.charCodeAt(end - 1)) && ch.trim() !== '') break\n end--\n }\n return s.slice(start, end)\n}\n\n/**\n * Sanitize a filename for a target-destination profile. Pure: same\n * input + options always returns the same output, no I/O.\n *\n * Returns the sanitized name. Throws {@link FilenameSanitizationError}\n * when the input cannot be made safe at all (NUL byte, empty after\n * normalization, missing `opaqueId` for the opaque profile,\n * `..` segment that would fall out of any reasonable target).\n */\nexport function sanitizeFilename(name: string, opts: SanitizeFilenameOptions): string {\n if (typeof name !== 'string') {\n throw new FilenameSanitizationError('input must be a string')\n }\n if (name.includes('\\0')) {\n throw new FilenameSanitizationError('NUL byte in filename')\n }\n\n let s = name.normalize('NFC').replace(BIDI_OVERRIDES, '')\n\n if (opts.profile === 'opaque') {\n return applyOpaque(s, opts)\n }\n\n s = trimWhitespaceAndControls(s)\n\n switch (opts.profile) {\n case 'posix': return cap(applyPosix(s), opts.maxBytes ?? 255, 'utf8')\n case 'windows': return cap(applyWindows(s), opts.maxBytes ?? 255, 'utf16')\n case 'macos-smb': return cap(applyMacosSmb(s), opts.maxBytes ?? 240, 'utf8')\n case 'zip': return cap(applyZip(s), opts.maxBytes ?? 255, 'utf8')\n case 'url-path': return cap(applyUrlPath(s), opts.maxBytes ?? 1024, 'bytes-pre-encode')\n case 's3-key': return cap(applyS3Key(s), opts.maxBytes ?? 1024, 'utf8')\n }\n}\n\n// ─── Profile implementations ────────────────────────────────────────────\n\nfunction applyPosix(s: string): string {\n // POSIX is permissive; only `/` and NUL are off-limits.\n const cleaned = s.replace(/\\//g, REPLACEMENT)\n return rejectDotSegments(cleaned)\n}\n\nfunction applyWindows(s: string): string {\n let cleaned = s.replace(WINDOWS_RESERVED_CHARS, REPLACEMENT)\n cleaned = stripControlChars(cleaned)\n // Trailing space/dot are stripped by Win32 path resolution.\n cleaned = cleaned.replace(/[. ]+$/g, '')\n cleaned = rejectDotSegments(cleaned)\n return avoidWindowsReservedName(cleaned)\n}\n\nfunction applyMacosSmb(s: string): string {\n let cleaned = applyWindows(s)\n if (MAC_HIDDEN_NOISE.test(cleaned)) {\n cleaned = REPLACEMENT + cleaned\n }\n return cleaned\n}\n\nfunction applyZip(s: string): string {\n let cleaned = s.replace(/^\\/+/, '')\n cleaned = rejectDotSegments(cleaned)\n return stripControlChars(cleaned)\n}\n\nfunction applyUrlPath(s: string): string {\n // RFC 3986 percent-encoding for path segment. Keep `unreserved`,\n // encode everything else. `+` is encoded as `%2B` because S3 and\n // legacy servers treat raw `+` as space ambiguously.\n let out = ''\n for (const ch of s) {\n if (URL_UNRESERVED.test(ch)) { out += ch; continue }\n const bytes = utf8.encode(ch)\n for (const b of bytes) {\n out += '%' + b.toString(16).toUpperCase().padStart(2, '0')\n }\n }\n return out\n}\n\nfunction applyS3Key(s: string): string {\n // Strip leading slashes BEFORE percent-encoding — once `/` is\n // encoded to `%2F` the leading-slash regex no longer matches.\n return applyUrlPath(s.replace(/^\\/+/, ''))\n}\n\nfunction applyOpaque(s: string, opts: SanitizeFilenameOptions): string {\n if (!opts.opaqueId) {\n throw new FilenameSanitizationError('opaque profile requires opaqueId')\n }\n // Preserve a \"safe-looking\" extension only — alphanumeric, ≤16 bytes.\n const dot = s.lastIndexOf('.')\n if (dot > 0 && dot < s.length - 1) {\n const ext = s.slice(dot + 1)\n if (/^[A-Za-z0-9]{1,16}$/.test(ext)) {\n return `${opts.opaqueId}.${ext.toLowerCase()}`\n }\n }\n return opts.opaqueId\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────\n\nfunction rejectDotSegments(s: string): string {\n if (s === '.' || s === '..' || s.split(/[/\\\\]/).some((seg) => seg === '..')) {\n throw new FilenameSanitizationError('path traversal segment in filename')\n }\n if (s.length === 0) {\n throw new FilenameSanitizationError('empty filename after sanitization')\n }\n return s\n}\n\nfunction avoidWindowsReservedName(s: string): string {\n const dot = s.indexOf('.')\n const base = dot === -1 ? s : s.slice(0, dot)\n if (WINDOWS_RESERVED_NAMES.has(base.toUpperCase())) {\n return REPLACEMENT + s\n }\n return s\n}\n\ntype LengthUnit = 'utf8' | 'utf16' | 'bytes-pre-encode'\n\nfunction cap(s: string, max: number, unit: LengthUnit): string {\n if (max <= 0) {\n throw new FilenameSanitizationError('maxBytes must be positive')\n }\n if (unit === 'utf16') {\n if (s.length <= max) return s\n return s.slice(0, max)\n }\n if (unit === 'bytes-pre-encode') {\n if (utf8.encode(s).byteLength <= max) return s\n return truncateToByteCap(s, max)\n }\n if (utf8.encode(s).byteLength <= max) return s\n return truncateToByteCap(s, max)\n}\n\nfunction truncateToByteCap(s: string, max: number): string {\n // Walk by code points so we never split a multi-byte sequence in\n // the middle. Whole-grapheme handling (ZWJ-joined emoji) is a\n // documented non-goal; we only protect against UTF-8 boundary\n // splits here.\n let out = ''\n let used = 0\n for (const cp of s) {\n const n = utf8.encode(cp).byteLength\n if (used + n > max) break\n out += cp\n used += n\n }\n if (out.length === 0) {\n throw new FilenameSanitizationError('maxBytes too small for a single code point')\n }\n return out\n}\n"],"mappings":"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","names":[]}
@@ -2,8 +2,7 @@ import {
2
2
  diffVault
3
3
  } from "./chunk-LFUID7OW.js";
4
4
  import "./chunk-SVLR4POP.js";
5
- import "./chunk-PZ5AY32C.js";
6
5
  export {
7
6
  diffVault
8
7
  };
9
- //# sourceMappingURL=vault-diff-FE5J35NJ.js.map
8
+ //# sourceMappingURL=vault-diff-C6WUJFNP.js.map
@@ -19,7 +19,6 @@ import {
19
19
  } from "../chunk-SL3JAAKN.js";
20
20
  import "../chunk-N4CHCXDL.js";
21
21
  import "../chunk-7O3Y4PPD.js";
22
- import "../chunk-PZ5AY32C.js";
23
22
 
24
23
  // src/with-commit/vault-head/head.ts
25
24
  function bucketIndex(collection, id, buckets) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/with-commit/vault-head/head.ts","../../src/with-commit/vault-head/active.ts","../../src/with-commit/vault-head/verify.ts"],"sourcesContent":["/**\n * The vault head's storage layer (#1044).\n *\n * A bucket is an ordinary encrypted record at `_head/<collection>::<n>`, so it\n * inherits #1041's identity binding for free: the store cannot relocate a\n * bucket, re-author it, or serve one bucket's bytes as another's. What it can\n * still do is serve an OLD bucket, which is why {@link readBucket} enforces\n * monotonicity against the caller's own record of what it last wrote.\n *\n * @packageDocumentation\n */\nimport { encrypt, buildRecordAad, openEnvelopeJson, buildRecordEnvelope, type EnclaveKey } from '../../kernel/enclave/index.js'\nimport type { NoydbStore } from '../../kernel/types.js'\nimport { VAULT_HEAD_COLLECTION } from './strategy.js'\n\n/** `{ recordId → version }` for one bucket. */\nexport type BucketBody = Record<string, number>\n\n/**\n * FNV-1a over `collection/id`. A hash, not a cipher: the bucket a record lands\n * in is not secret — the store already sees which bucket was written, and it\n * already saw the record's own address. Choosing something stronger would buy\n * nothing and cost every write.\n */\nexport function bucketIndex(collection: string, id: string, buckets: number): number {\n let h = 0x811c9dc5\n const s = `${collection}/${id}`\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i)\n h = Math.imul(h, 0x01000193) >>> 0\n }\n return h % buckets\n}\n\nexport function bucketId(collection: string, id: string, buckets: number): string {\n return `${collection}::${String(bucketIndex(collection, id, buckets))}`\n}\n\nexport async function readBucket(\n store: NoydbStore,\n vault: string,\n dek: EnclaveKey,\n bucket: string,\n): Promise<{ body: BucketBody; version: number }> {\n const env = await store.get(vault, VAULT_HEAD_COLLECTION, bucket)\n if (!env) return { body: {}, version: 0 }\n const json = await openEnvelopeJson({ collection: VAULT_HEAD_COLLECTION, id: bucket }, env, dek)\n return { body: JSON.parse(json) as BucketBody, version: env._v }\n}\n\nexport async function writeBucket(\n store: NoydbStore,\n vault: string,\n dek: EnclaveKey,\n bucket: string,\n body: BucketBody,\n expectedVersion: number,\n): Promise<void> {\n const identity = { collection: VAULT_HEAD_COLLECTION, id: bucket, version: expectedVersion + 1 }\n const json = JSON.stringify(body)\n const { iv, data } = await encrypt(json, dek, buildRecordAad(identity))\n const env = buildRecordEnvelope(identity, { iv, data })\n // CAS: two writers racing on one bucket must not silently lose an entry.\n // A lost head entry is a record the sweep stops expecting — a false clean.\n await store.put(vault, VAULT_HEAD_COLLECTION, bucket, env, expectedVersion || undefined)\n}\n","/**\n * The real {@link VaultHeadStrategy} (#1044).\n *\n * @packageDocumentation\n */\nimport type { NoydbStore } from '../../kernel/types.js'\nimport type { EnclaveKey } from '../../kernel/enclave/index.js'\nimport {\n DEFAULT_HEAD_BUCKETS,\n VAULT_HEAD_COLLECTION,\n type HeadEntry,\n type VaultHeadStrategy,\n type WithVaultHeadOptions,\n} from './strategy.js'\nimport { bucketId, readBucket, writeBucket } from './head.js'\n\nexport function withVaultHead(opts: WithVaultHeadOptions = {}): VaultHeadStrategy {\n const buckets = opts.buckets ?? DEFAULT_HEAD_BUCKETS\n\n const dekFor = (getDEK: (c: string) => Promise<EnclaveKey>) => getDEK(VAULT_HEAD_COLLECTION)\n\n return {\n enabled: true,\n bucketFor: (collection, id) => bucketId(collection, id, buckets),\n\n async note(store: NoydbStore, vault, getDEK, entry: HeadEntry) {\n const dek = await dekFor(getDEK)\n const bucket = bucketId(entry.collection, entry.id, buckets)\n const { body, version } = await readBucket(store, vault, dek, bucket)\n const key = `${entry.collection}/${entry.id}`\n // Monotonic: a head entry never moves BACKWARD. A write that would lower\n // it is either a stale retry or a rollback being laundered through the\n // client's own head, and neither should be recorded.\n if ((body[key] ?? -1) >= entry.version) return\n body[key] = entry.version\n await writeBucket(store, vault, dek, bucket, body, version)\n },\n\n async expected(store, vault, getDEK, collection, id) {\n const dek = await dekFor(getDEK)\n const { body } = await readBucket(store, vault, dek, bucketId(collection, id, buckets))\n return body[`${collection}/${id}`] ?? null\n },\n\n async knownIn(store, vault, getDEK, collection) {\n const dek = await dekFor(getDEK)\n const out = new Map<string, number>()\n const prefix = `${collection}/`\n // Sweep every bucket: a record's bucket is a function of its id, so the\n // set for one collection is spread across all of them.\n for (const bucket of await store.list(vault, VAULT_HEAD_COLLECTION)) {\n if (!bucket.startsWith(`${collection}::`)) continue\n const { body } = await readBucket(store, vault, dek, bucket)\n for (const [k, v] of Object.entries(body)) {\n if (k.startsWith(prefix)) out.set(k.slice(prefix.length), v)\n }\n }\n return out\n },\n }\n}\n","/**\n * The sweep — comparing what the head expects against what the store serves (#1044).\n *\n * This is the only place absence becomes visible. #1041 and #1042 can reject a\n * record that lies about itself; neither can notice one that never arrived, or\n * one served at an older version than the client wrote. Both are indistinguishable\n * from the truth without an independent record of what should be there.\n *\n * @packageDocumentation\n */\nimport type { NoydbStore } from '../../kernel/types.js'\nimport type { EnclaveKey } from '../../kernel/enclave/index.js'\nimport type { VaultHeadStrategy } from './strategy.js'\n\n/** One record the store is not serving faithfully. */\nexport interface HeadDiscrepancy {\n readonly collection: string\n readonly id: string\n /** What the client last committed. */\n readonly expected: number\n /**\n * What the store serves now — or `null` when it serves nothing.\n *\n * The two are different attacks wearing the same result. `null` is\n * **suppression**: the record is gone. A lower number is **rollback**: an\n * authentic older version served in place of the current one, which is the\n * case no amount of per-envelope authentication can see.\n */\n readonly actual: number | null\n readonly kind: 'withheld' | 'rolled-back'\n}\n\n/**\n * Why a sweep could not reach a conclusion.\n *\n * - `'no-expectations'` — the head holds nothing for this collection. A fresh\n * vault, a head switched on after the fact, or a **restore from a snapshot**:\n * `_head` is `_`-prefixed, so `loadAll` excludes it and a restored vault has\n * no head to compare against.\n * - `'store-cannot-cas'` — the store does not advertise `capabilities.casAtomic`,\n * so two writers racing on one bucket can silently lose an entry. A lost entry\n * is a record the sweep stops expecting, which is a false clean.\n */\nexport type HeadUnverifiableReason = 'no-expectations' | 'store-cannot-cas'\n\n/**\n * ## Three-way, deliberately (#1101)\n *\n * - `'verified'` — the head held expectations, every one was met, and the store\n * can serialize head writes.\n * - `'unverifiable'` — the sweep could not conclude. See `because`.\n * - `'tampered'` — at least one discrepancy. Positive evidence, so it wins over\n * any `unverifiable` reason present at the same time.\n *\n * **The middle value is the point, and collapsing it is wrong in a different\n * direction each way:** folded into \"clean\" it hides withholding, which is the\n * one thing this service exists to catch; folded into \"tampered\" it cries wolf\n * on a vault that is merely unexamined.\n *\n * That is not hypothetical. #1044's own first bug was a head that recorded\n * nothing and swept **perfectly clean** — the degraded state rendered\n * identically to a healthy one, which is why its test asserts `checked` before\n * anything else. A two-way verdict makes that state a supported outcome.\n */\nexport type HeadVerdict = 'verified' | 'unverifiable' | 'tampered'\n\nexport interface HeadVerifyResult {\n readonly verdict: HeadVerdict\n readonly checked: number\n readonly discrepancies: readonly HeadDiscrepancy[]\n /**\n * Non-empty only when `verdict === 'unverifiable'`.\n *\n * There is deliberately no `clean: boolean`. It could not distinguish \"no\n * expectations\" from \"all expectations met\" — both rendered `true` — and that\n * indistinguishability *was* the defect. Replacing it rather than keeping it\n * alongside forces every caller to face the three-way.\n */\n readonly because: readonly HeadUnverifiableReason[]\n}\n\n/**\n * Sweep one collection.\n *\n * **A record the store serves that the head does NOT know is not reported.**\n * That is not an oversight: the head can be enabled on an existing vault, and\n * every pre-existing record would otherwise read as an anomaly. The head's\n * claim is one-directional — *what I wrote must still be there* — and an\n * unknown record simply falls outside it.\n *\n * **A HIGHER version than expected is also not reported**, for the same reason:\n * another device legitimately advanced it. The head detects going backwards, not\n * moving forwards.\n */\nexport async function verifyVaultHead(\n head: VaultHeadStrategy,\n store: NoydbStore,\n vault: string,\n getDEK: (collection: string) => Promise<EnclaveKey>,\n collection: string,\n): Promise<HeadVerifyResult> {\n const known = await head.knownIn(store, vault, getDEK, collection)\n const discrepancies: HeadDiscrepancy[] = []\n\n const because: HeadUnverifiableReason[] = []\n if (known.size === 0) because.push('no-expectations')\n // Read the store's OWN declared capability rather than probing: a store that\n // cannot serialize head writes may have silently dropped an entry, and a\n // dropped entry is invisible by construction — there is nothing left to find.\n if (store.capabilities?.casAtomic !== true) because.push('store-cannot-cas')\n\n for (const [id, expected] of known) {\n const env = await store.get(vault, collection, id)\n if (env === null) {\n discrepancies.push({ collection, id, expected, actual: null, kind: 'withheld' })\n continue\n }\n if (env._v < expected) {\n discrepancies.push({ collection, id, expected, actual: env._v, kind: 'rolled-back' })\n }\n }\n\n // A discrepancy is positive evidence, so it outranks any reason the sweep was\n // also incomplete: a rolled-back record found by a head that may be missing\n // OTHER entries is still a rolled-back record.\n const verdict: HeadVerdict = discrepancies.length > 0\n ? 'tampered'\n : because.length > 0 ? 'unverifiable' : 'verified'\n\n return { verdict, checked: known.size, discrepancies, because }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAwBO,SAAS,YAAY,YAAoB,IAAY,SAAyB;AACnF,MAAI,IAAI;AACR,QAAM,IAAI,GAAG,UAAU,IAAI,EAAE;AAC7B,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AACjC,SAAK,EAAE,WAAW,CAAC;AACnB,QAAI,KAAK,KAAK,GAAG,QAAU,MAAM;AAAA,EACnC;AACA,SAAO,IAAI;AACb;AAEO,SAAS,SAAS,YAAoB,IAAY,SAAyB;AAChF,SAAO,GAAG,UAAU,KAAK,OAAO,YAAY,YAAY,IAAI,OAAO,CAAC,CAAC;AACvE;AAEA,eAAsB,WACpB,OACA,OACA,KACA,QACgD;AAChD,QAAM,MAAM,MAAM,MAAM,IAAI,OAAO,uBAAuB,MAAM;AAChE,MAAI,CAAC,IAAK,QAAO,EAAE,MAAM,CAAC,GAAG,SAAS,EAAE;AACxC,QAAM,OAAO,MAAM,iBAAiB,EAAE,YAAY,uBAAuB,IAAI,OAAO,GAAG,KAAK,GAAG;AAC/F,SAAO,EAAE,MAAM,KAAK,MAAM,IAAI,GAAiB,SAAS,IAAI,GAAG;AACjE;AAEA,eAAsB,YACpB,OACA,OACA,KACA,QACA,MACA,iBACe;AACf,QAAM,WAAW,EAAE,YAAY,uBAAuB,IAAI,QAAQ,SAAS,kBAAkB,EAAE;AAC/F,QAAM,OAAO,KAAK,UAAU,IAAI;AAChC,QAAM,EAAE,IAAI,KAAK,IAAI,MAAM,QAAQ,MAAM,KAAK,eAAe,QAAQ,CAAC;AACtE,QAAM,MAAM,oBAAoB,UAAU,EAAE,IAAI,KAAK,CAAC;AAGtD,QAAM,MAAM,IAAI,OAAO,uBAAuB,QAAQ,KAAK,mBAAmB,MAAS;AACzF;;;ACjDO,SAAS,cAAc,OAA6B,CAAC,GAAsB;AAChF,QAAM,UAAU,KAAK,WAAW;AAEhC,QAAM,SAAS,CAAC,WAA+C,OAAO,qBAAqB;AAE3F,SAAO;AAAA,IACL,SAAS;AAAA,IACT,WAAW,CAAC,YAAY,OAAO,SAAS,YAAY,IAAI,OAAO;AAAA,IAE/D,MAAM,KAAK,OAAmB,OAAO,QAAQ,OAAkB;AAC7D,YAAM,MAAM,MAAM,OAAO,MAAM;AAC/B,YAAM,SAAS,SAAS,MAAM,YAAY,MAAM,IAAI,OAAO;AAC3D,YAAM,EAAE,MAAM,QAAQ,IAAI,MAAM,WAAW,OAAO,OAAO,KAAK,MAAM;AACpE,YAAM,MAAM,GAAG,MAAM,UAAU,IAAI,MAAM,EAAE;AAI3C,WAAK,KAAK,GAAG,KAAK,OAAO,MAAM,QAAS;AACxC,WAAK,GAAG,IAAI,MAAM;AAClB,YAAM,YAAY,OAAO,OAAO,KAAK,QAAQ,MAAM,OAAO;AAAA,IAC5D;AAAA,IAEA,MAAM,SAAS,OAAO,OAAO,QAAQ,YAAY,IAAI;AACnD,YAAM,MAAM,MAAM,OAAO,MAAM;AAC/B,YAAM,EAAE,KAAK,IAAI,MAAM,WAAW,OAAO,OAAO,KAAK,SAAS,YAAY,IAAI,OAAO,CAAC;AACtF,aAAO,KAAK,GAAG,UAAU,IAAI,EAAE,EAAE,KAAK;AAAA,IACxC;AAAA,IAEA,MAAM,QAAQ,OAAO,OAAO,QAAQ,YAAY;AAC9C,YAAM,MAAM,MAAM,OAAO,MAAM;AAC/B,YAAM,MAAM,oBAAI,IAAoB;AACpC,YAAM,SAAS,GAAG,UAAU;AAG5B,iBAAW,UAAU,MAAM,MAAM,KAAK,OAAO,qBAAqB,GAAG;AACnE,YAAI,CAAC,OAAO,WAAW,GAAG,UAAU,IAAI,EAAG;AAC3C,cAAM,EAAE,KAAK,IAAI,MAAM,WAAW,OAAO,OAAO,KAAK,MAAM;AAC3D,mBAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,IAAI,GAAG;AACzC,cAAI,EAAE,WAAW,MAAM,EAAG,KAAI,IAAI,EAAE,MAAM,OAAO,MAAM,GAAG,CAAC;AAAA,QAC7D;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACF;;;ACkCA,eAAsB,gBACpB,MACA,OACA,OACA,QACA,YAC2B;AAC3B,QAAM,QAAQ,MAAM,KAAK,QAAQ,OAAO,OAAO,QAAQ,UAAU;AACjE,QAAM,gBAAmC,CAAC;AAE1C,QAAM,UAAoC,CAAC;AAC3C,MAAI,MAAM,SAAS,EAAG,SAAQ,KAAK,iBAAiB;AAIpD,MAAI,MAAM,cAAc,cAAc,KAAM,SAAQ,KAAK,kBAAkB;AAE3E,aAAW,CAAC,IAAI,QAAQ,KAAK,OAAO;AAClC,UAAM,MAAM,MAAM,MAAM,IAAI,OAAO,YAAY,EAAE;AACjD,QAAI,QAAQ,MAAM;AAChB,oBAAc,KAAK,EAAE,YAAY,IAAI,UAAU,QAAQ,MAAM,MAAM,WAAW,CAAC;AAC/E;AAAA,IACF;AACA,QAAI,IAAI,KAAK,UAAU;AACrB,oBAAc,KAAK,EAAE,YAAY,IAAI,UAAU,QAAQ,IAAI,IAAI,MAAM,cAAc,CAAC;AAAA,IACtF;AAAA,EACF;AAKA,QAAM,UAAuB,cAAc,SAAS,IAChD,aACA,QAAQ,SAAS,IAAI,iBAAiB;AAE1C,SAAO,EAAE,SAAS,SAAS,MAAM,MAAM,eAAe,QAAQ;AAChE;","names":[]}
1
+ {"version":3,"sources":["../../src/with-commit/vault-head/head.ts","../../src/with-commit/vault-head/active.ts","../../src/with-commit/vault-head/verify.ts"],"sourcesContent":["/**\n * The vault head's storage layer (#1044).\n *\n * A bucket is an ordinary encrypted record at `_head/<collection>::<n>`, so it\n * inherits #1041's identity binding for free: the store cannot relocate a\n * bucket, re-author it, or serve one bucket's bytes as another's. What it can\n * still do is serve an OLD bucket, which is why {@link readBucket} enforces\n * monotonicity against the caller's own record of what it last wrote.\n *\n * @packageDocumentation\n */\nimport { encrypt, buildRecordAad, openEnvelopeJson, buildRecordEnvelope, type EnclaveKey } from '../../kernel/enclave/index.js'\nimport type { NoydbStore } from '../../kernel/types.js'\nimport { VAULT_HEAD_COLLECTION } from './strategy.js'\n\n/** `{ recordId → version }` for one bucket. */\nexport type BucketBody = Record<string, number>\n\n/**\n * FNV-1a over `collection/id`. A hash, not a cipher: the bucket a record lands\n * in is not secret — the store already sees which bucket was written, and it\n * already saw the record's own address. Choosing something stronger would buy\n * nothing and cost every write.\n */\nexport function bucketIndex(collection: string, id: string, buckets: number): number {\n let h = 0x811c9dc5\n const s = `${collection}/${id}`\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i)\n h = Math.imul(h, 0x01000193) >>> 0\n }\n return h % buckets\n}\n\nexport function bucketId(collection: string, id: string, buckets: number): string {\n return `${collection}::${String(bucketIndex(collection, id, buckets))}`\n}\n\nexport async function readBucket(\n store: NoydbStore,\n vault: string,\n dek: EnclaveKey,\n bucket: string,\n): Promise<{ body: BucketBody; version: number }> {\n const env = await store.get(vault, VAULT_HEAD_COLLECTION, bucket)\n if (!env) return { body: {}, version: 0 }\n const json = await openEnvelopeJson({ collection: VAULT_HEAD_COLLECTION, id: bucket }, env, dek)\n return { body: JSON.parse(json) as BucketBody, version: env._v }\n}\n\nexport async function writeBucket(\n store: NoydbStore,\n vault: string,\n dek: EnclaveKey,\n bucket: string,\n body: BucketBody,\n expectedVersion: number,\n): Promise<void> {\n const identity = { collection: VAULT_HEAD_COLLECTION, id: bucket, version: expectedVersion + 1 }\n const json = JSON.stringify(body)\n const { iv, data } = await encrypt(json, dek, buildRecordAad(identity))\n const env = buildRecordEnvelope(identity, { iv, data })\n // CAS: two writers racing on one bucket must not silently lose an entry.\n // A lost head entry is a record the sweep stops expecting — a false clean.\n await store.put(vault, VAULT_HEAD_COLLECTION, bucket, env, expectedVersion || undefined)\n}\n","/**\n * The real {@link VaultHeadStrategy} (#1044).\n *\n * @packageDocumentation\n */\nimport type { NoydbStore } from '../../kernel/types.js'\nimport type { EnclaveKey } from '../../kernel/enclave/index.js'\nimport {\n DEFAULT_HEAD_BUCKETS,\n VAULT_HEAD_COLLECTION,\n type HeadEntry,\n type VaultHeadStrategy,\n type WithVaultHeadOptions,\n} from './strategy.js'\nimport { bucketId, readBucket, writeBucket } from './head.js'\n\nexport function withVaultHead(opts: WithVaultHeadOptions = {}): VaultHeadStrategy {\n const buckets = opts.buckets ?? DEFAULT_HEAD_BUCKETS\n\n const dekFor = (getDEK: (c: string) => Promise<EnclaveKey>) => getDEK(VAULT_HEAD_COLLECTION)\n\n return {\n enabled: true,\n bucketFor: (collection, id) => bucketId(collection, id, buckets),\n\n async note(store: NoydbStore, vault, getDEK, entry: HeadEntry) {\n const dek = await dekFor(getDEK)\n const bucket = bucketId(entry.collection, entry.id, buckets)\n const { body, version } = await readBucket(store, vault, dek, bucket)\n const key = `${entry.collection}/${entry.id}`\n // Monotonic: a head entry never moves BACKWARD. A write that would lower\n // it is either a stale retry or a rollback being laundered through the\n // client's own head, and neither should be recorded.\n if ((body[key] ?? -1) >= entry.version) return\n body[key] = entry.version\n await writeBucket(store, vault, dek, bucket, body, version)\n },\n\n async expected(store, vault, getDEK, collection, id) {\n const dek = await dekFor(getDEK)\n const { body } = await readBucket(store, vault, dek, bucketId(collection, id, buckets))\n return body[`${collection}/${id}`] ?? null\n },\n\n async knownIn(store, vault, getDEK, collection) {\n const dek = await dekFor(getDEK)\n const out = new Map<string, number>()\n const prefix = `${collection}/`\n // Sweep every bucket: a record's bucket is a function of its id, so the\n // set for one collection is spread across all of them.\n for (const bucket of await store.list(vault, VAULT_HEAD_COLLECTION)) {\n if (!bucket.startsWith(`${collection}::`)) continue\n const { body } = await readBucket(store, vault, dek, bucket)\n for (const [k, v] of Object.entries(body)) {\n if (k.startsWith(prefix)) out.set(k.slice(prefix.length), v)\n }\n }\n return out\n },\n }\n}\n","/**\n * The sweep — comparing what the head expects against what the store serves (#1044).\n *\n * This is the only place absence becomes visible. #1041 and #1042 can reject a\n * record that lies about itself; neither can notice one that never arrived, or\n * one served at an older version than the client wrote. Both are indistinguishable\n * from the truth without an independent record of what should be there.\n *\n * @packageDocumentation\n */\nimport type { NoydbStore } from '../../kernel/types.js'\nimport type { EnclaveKey } from '../../kernel/enclave/index.js'\nimport type { VaultHeadStrategy } from './strategy.js'\n\n/** One record the store is not serving faithfully. */\nexport interface HeadDiscrepancy {\n readonly collection: string\n readonly id: string\n /** What the client last committed. */\n readonly expected: number\n /**\n * What the store serves now — or `null` when it serves nothing.\n *\n * The two are different attacks wearing the same result. `null` is\n * **suppression**: the record is gone. A lower number is **rollback**: an\n * authentic older version served in place of the current one, which is the\n * case no amount of per-envelope authentication can see.\n */\n readonly actual: number | null\n readonly kind: 'withheld' | 'rolled-back'\n}\n\n/**\n * Why a sweep could not reach a conclusion.\n *\n * - `'no-expectations'` — the head holds nothing for this collection. A fresh\n * vault, a head switched on after the fact, or a **restore from a snapshot**:\n * `_head` is `_`-prefixed, so `loadAll` excludes it and a restored vault has\n * no head to compare against.\n * - `'store-cannot-cas'` — the store does not advertise `capabilities.casAtomic`,\n * so two writers racing on one bucket can silently lose an entry. A lost entry\n * is a record the sweep stops expecting, which is a false clean.\n */\nexport type HeadUnverifiableReason = 'no-expectations' | 'store-cannot-cas'\n\n/**\n * ## Three-way, deliberately (#1101)\n *\n * - `'verified'` — the head held expectations, every one was met, and the store\n * can serialize head writes.\n * - `'unverifiable'` — the sweep could not conclude. See `because`.\n * - `'tampered'` — at least one discrepancy. Positive evidence, so it wins over\n * any `unverifiable` reason present at the same time.\n *\n * **The middle value is the point, and collapsing it is wrong in a different\n * direction each way:** folded into \"clean\" it hides withholding, which is the\n * one thing this service exists to catch; folded into \"tampered\" it cries wolf\n * on a vault that is merely unexamined.\n *\n * That is not hypothetical. #1044's own first bug was a head that recorded\n * nothing and swept **perfectly clean** — the degraded state rendered\n * identically to a healthy one, which is why its test asserts `checked` before\n * anything else. A two-way verdict makes that state a supported outcome.\n */\nexport type HeadVerdict = 'verified' | 'unverifiable' | 'tampered'\n\nexport interface HeadVerifyResult {\n readonly verdict: HeadVerdict\n readonly checked: number\n readonly discrepancies: readonly HeadDiscrepancy[]\n /**\n * Non-empty only when `verdict === 'unverifiable'`.\n *\n * There is deliberately no `clean: boolean`. It could not distinguish \"no\n * expectations\" from \"all expectations met\" — both rendered `true` — and that\n * indistinguishability *was* the defect. Replacing it rather than keeping it\n * alongside forces every caller to face the three-way.\n */\n readonly because: readonly HeadUnverifiableReason[]\n}\n\n/**\n * Sweep one collection.\n *\n * **A record the store serves that the head does NOT know is not reported.**\n * That is not an oversight: the head can be enabled on an existing vault, and\n * every pre-existing record would otherwise read as an anomaly. The head's\n * claim is one-directional — *what I wrote must still be there* — and an\n * unknown record simply falls outside it.\n *\n * **A HIGHER version than expected is also not reported**, for the same reason:\n * another device legitimately advanced it. The head detects going backwards, not\n * moving forwards.\n */\nexport async function verifyVaultHead(\n head: VaultHeadStrategy,\n store: NoydbStore,\n vault: string,\n getDEK: (collection: string) => Promise<EnclaveKey>,\n collection: string,\n): Promise<HeadVerifyResult> {\n const known = await head.knownIn(store, vault, getDEK, collection)\n const discrepancies: HeadDiscrepancy[] = []\n\n const because: HeadUnverifiableReason[] = []\n if (known.size === 0) because.push('no-expectations')\n // Read the store's OWN declared capability rather than probing: a store that\n // cannot serialize head writes may have silently dropped an entry, and a\n // dropped entry is invisible by construction — there is nothing left to find.\n if (store.capabilities?.casAtomic !== true) because.push('store-cannot-cas')\n\n for (const [id, expected] of known) {\n const env = await store.get(vault, collection, id)\n if (env === null) {\n discrepancies.push({ collection, id, expected, actual: null, kind: 'withheld' })\n continue\n }\n if (env._v < expected) {\n discrepancies.push({ collection, id, expected, actual: env._v, kind: 'rolled-back' })\n }\n }\n\n // A discrepancy is positive evidence, so it outranks any reason the sweep was\n // also incomplete: a rolled-back record found by a head that may be missing\n // OTHER entries is still a rolled-back record.\n const verdict: HeadVerdict = discrepancies.length > 0\n ? 'tampered'\n : because.length > 0 ? 'unverifiable' : 'verified'\n\n return { verdict, checked: known.size, discrepancies, because }\n}\n"],"mappings":"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","names":[]}
@@ -19,7 +19,6 @@ import {
19
19
  ClassifiedConfigError,
20
20
  ClassifiedVerifyError
21
21
  } from "./chunk-7O3Y4PPD.js";
22
- import "./chunk-PZ5AY32C.js";
23
22
 
24
23
  // src/kernel/enclave/classify/verify.ts
25
24
  async function padOnce(iter = VDIG_ITERATIONS) {
@@ -137,4 +136,4 @@ export {
137
136
  verifyDigestField,
138
137
  verifyTextField
139
138
  };
140
- //# sourceMappingURL=verify-JBXCAGDL.js.map
139
+ //# sourceMappingURL=verify-KOV2G5D6.js.map