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

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 +169 -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-PQKG5AHH.js → chunk-6X6SEMEE.js} +4 -2
  20. package/dist/{chunk-PQKG5AHH.js.map → chunk-6X6SEMEE.js.map} +1 -1
  21. package/dist/{chunk-E2MYJGSG.js → chunk-AMODH7AU.js} +2 -2
  22. package/dist/{chunk-B2ZSGYSF.js → chunk-B2KCRDJM.js} +3 -3
  23. package/dist/{chunk-TO3EQ7HF.js → chunk-BLN54W3L.js} +2 -2
  24. package/dist/{chunk-IHFXR2VQ.js → chunk-BOBFL5TW.js} +2 -2
  25. package/dist/{chunk-ZR2YMODS.js → chunk-EKZ6VZYF.js} +2 -2
  26. package/dist/{chunk-RTYJRFQO.js → chunk-EWPRPP3M.js} +1 -1
  27. package/dist/{chunk-VGICZPVE.js → chunk-FUJSIP4K.js} +2 -2
  28. package/dist/{chunk-BWXGSSFX.js → chunk-NJXX55QX.js} +79 -16
  29. package/dist/chunk-NJXX55QX.js.map +1 -0
  30. package/dist/{chunk-PKJ43CMD.js → chunk-NLOLF6LD.js} +2 -2
  31. package/dist/{chunk-COTPDJR5.js → chunk-OMB2ZKIT.js} +8 -8
  32. package/dist/{chunk-BMELYFHU.js → chunk-P3KOF624.js} +3 -3
  33. package/dist/{chunk-Y4MJQ2UG.js → chunk-QQ2YMQZU.js} +2 -2
  34. package/dist/{chunk-URHRIAV6.js → chunk-REDOOY2Q.js} +2 -2
  35. package/dist/chunk-REDOOY2Q.js.map +1 -0
  36. package/dist/{chunk-YCXICMOY.js → chunk-RJ7NN2UR.js} +2 -2
  37. package/dist/{chunk-PDD75NUZ.js → chunk-SMH6SRX7.js} +2 -2
  38. package/dist/{chunk-QHW35ZON.js → chunk-TGPST6FK.js} +2 -2
  39. package/dist/{chunk-BERJ4AWW.js → chunk-VGXZT72I.js} +2 -2
  40. package/dist/{chunk-ZVCC36XQ.js → chunk-WL36WA3O.js} +55 -55
  41. package/dist/{chunk-2Z47NMFK.js → chunk-WZW37ZHL.js} +2 -2
  42. package/dist/{chunk-SXCYYMVS.js → chunk-YINEDONN.js} +5 -5
  43. package/dist/{chunk-AJQY44CE.js → chunk-ZXLZ62R5.js} +3 -20
  44. package/dist/chunk-ZXLZ62R5.js.map +1 -0
  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-F5CABCU4.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-BOHIKFWG.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-ZR2YMODS.js.map → chunk-EKZ6VZYF.js.map} +0 -0
  190. /package/dist/{chunk-RTYJRFQO.js.map → chunk-EWPRPP3M.js.map} +0 -0
  191. /package/dist/{chunk-VGICZPVE.js.map → chunk-FUJSIP4K.js.map} +0 -0
  192. /package/dist/{chunk-PKJ43CMD.js.map → chunk-NLOLF6LD.js.map} +0 -0
  193. /package/dist/{chunk-COTPDJR5.js.map → chunk-OMB2ZKIT.js.map} +0 -0
  194. /package/dist/{chunk-BMELYFHU.js.map → chunk-P3KOF624.js.map} +0 -0
  195. /package/dist/{chunk-Y4MJQ2UG.js.map → chunk-QQ2YMQZU.js.map} +0 -0
  196. /package/dist/{chunk-YCXICMOY.js.map → chunk-RJ7NN2UR.js.map} +0 -0
  197. /package/dist/{chunk-PDD75NUZ.js.map → chunk-SMH6SRX7.js.map} +0 -0
  198. /package/dist/{chunk-QHW35ZON.js.map → chunk-TGPST6FK.js.map} +0 -0
  199. /package/dist/{chunk-BERJ4AWW.js.map → chunk-VGXZT72I.js.map} +0 -0
  200. /package/dist/{chunk-ZVCC36XQ.js.map → chunk-WL36WA3O.js.map} +0 -0
  201. /package/dist/{chunk-2Z47NMFK.js.map → chunk-WZW37ZHL.js.map} +0 -0
  202. /package/dist/{chunk-SXCYYMVS.js.map → chunk-YINEDONN.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-F5CABCU4.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-BOHIKFWG.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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,174 @@
1
1
  # Changelog — hub
2
2
 
3
+ ## 0.7.0-pre.15
4
+
5
+ ### Patch Changes
6
+
7
+ - CORRECTION to `0.7.0-pre.14`'s description of #1269 — the fix is unchanged; what
8
+ it covers was described wrongly, twice.
9
+
10
+ The guard reads **declarative `spec.groupBy` and nothing else**. So:
11
+
12
+ - **caught** — a spec declaring `groupBy: [...]` as a top-level field, whether
13
+ `unionSources` or query-form;
14
+ - **silent** — `.groupBy(...)` chained INSIDE a `query: (db) => …` callback,
15
+ which is a runtime call on the query builder and never appears in the spec;
16
+ - **irrelevant** — `spec.sources`. The guard never reads it, so declaring it
17
+ cannot bring a shape into coverage.
18
+
19
+ The published `pre.14` entry said the uncovered case was "a single-query MV that
20
+ constructs its own source before the dependency is recorded". That framing is
21
+ wrong in a way that misleads: it points at dependency ORDERING and invites a
22
+ reader to fix coverage by declaring `sources`, which does nothing. Ordering
23
+ decides which OTHER guard fires first — a late-attach reconcile can loudly
24
+ refuse a virtual field on an already-constructed collection — not whether this
25
+ one fires.
26
+
27
+ Measured by a consumer on the published `pre.14` and re-derived from the code
28
+ here. No behaviour change: a `.groupBy()` chained in a callback was silent
29
+ before this correction and is silent after it. Closing that residue needs the
30
+ built query plan inspected, or a compute-time refusal, and is not attempted
31
+ here.
32
+
33
+ - CORRECTION to `0.7.0-pre.14`: the optional `zod` peer now reads
34
+ `^3.0.0 || ^4.0.0`, not `^4.0.0`.
35
+
36
+ `pre.14` un-vendored zod and declared it as an optional peer — correctly — but
37
+ declared a range NARROWER THAN WHAT HUB SUPPORTS. Under npm that made
38
+ `@noy-db/hub@0.7.0-pre.14` + `zod@3` uninstallable by name.
39
+
40
+ Measured, with controls in both directions:
41
+
42
+ ```
43
+ npm i @noy-db/hub@0.7.0-pre.14 zod@3.25.76 -> exit 1, ERESOLVE
44
+ npm i @noy-db/hub@0.7.0-pre.14 zod@4.4.3 -> exit 0 (control: major, not range shape)
45
+ npm i @noy-db/hub@0.7.0-pre.13 zod@3.25.76 -> exit 0 (control: the DECLARATION changed,
46
+ not the support)
47
+ ```
48
+
49
+ Zod 3 has always worked and still does — through the `zod-to-json-schema`
50
+ optional peer, with the v4-native `toJSONSchema` loader falling back when it is
51
+ absent. The two releases immediately before this one were substantially Zod 3
52
+ hardening (the `ZodEffects` unwrap, `z.preprocess` on both majors), so the
53
+ investment and the declaration pointed in opposite directions.
54
+
55
+ **Why no gate caught it:** while zod was vendored, no resolver ever saw a range,
56
+ so none was ever exercised. Every in-repo check stays green under either
57
+ declaration — a peer FORM deciding installability, which is the same class as
58
+ this family's exact-peer incident. There is now a test asserting the range
59
+ admits a real version of each major, and refusing a range that ends in a
60
+ dangling `||`.
61
+
62
+ pnpm only warns, so a pnpm workspace saw nothing; npm refuses.
63
+
64
+ ## 0.7.0-pre.14
65
+
66
+ ### Patch Changes
67
+
68
+ - The bundle-size gate now needs a growth to exceed BOTH its percentage tolerance
69
+ and an absolute byte allowance before failing (#1268). No runtime change — this
70
+ is the CI gate only.
71
+
72
+ A percentage on a small baseline measures the wrong thing. The `floor` scenario
73
+ is ~500 gzipped bytes, so 5% is ~25 bytes — narrower than a single
74
+ registration-time guard. The gate had fired twice on necessary validation
75
+ (#1249, #1266) and never once on the thing it exists to catch.
76
+
77
+ The two are separated by orders of magnitude: a subsystem leaking into a bundle
78
+ is kilobytes (measured elsewhere in the family: forcing an SDK inline moved a
79
+ package from 14,069 to 1,081,539 bytes), while a guard is tens of bytes. A
80
+ 192-byte allowance sits far above the latter and far below the former.
81
+
82
+ Verified in both directions rather than assumed: a simulated leak (+406 bytes)
83
+ still FAILS, and a 26-byte guard at +5.2% now PASSES. The numbers still ratchet
84
+ and nothing was re-baselined.
85
+
86
+ - A materialized view whose `groupBy` names a VIRTUAL computed field is now
87
+ refused at collection registration (#1269).
88
+
89
+ A virtual field is evaluated on read and never stored, so the MV pipeline read
90
+ the stored row, found nothing, and bucketed every row under an `undefined` key —
91
+ a well-formed aggregate carrying a wrong NUMBER rather than an error, on the one
92
+ path where nobody re-checks the arithmetic. `query().where()` already refuses the
93
+ same field with `FieldNotQueryableError`, so the two halves of what reads as one
94
+ query layer disagreed.
95
+
96
+ Same principle as the executor's existing object-valued-group-key refusal
97
+ ("refuse, don't bucket wrong") and the `triggerBy` virtual-target refusal
98
+ (#1266), applied at the earliest point that can see both the MV and the
99
+ collection's field modes.
100
+
101
+ **Known residue, stated rather than left to be discovered:** a single-query MV of
102
+ the shape `query: (db) => db.collection(name)…` constructs its own source from
103
+ inside `MaterializedViewRegistry.register()`, before the dependency is recorded,
104
+ so that shape is not caught by this guard. `unionSources`, an aggregate with
105
+ explicit `sources`, and any source built by an earlier `vault.collection()` call
106
+ are caught. The same ordering is already documented for the neighbouring
107
+ tiers+crdt guard.
108
+
109
+ - Two guards now refuse where the information to refuse already exists (design
110
+ pass: _where a check refuses vs where it quietly answers_).
111
+
112
+ **`fieldMeta` typo keys are refused at `vault.collection()`, not only at
113
+ `describe()`.** A collection nobody describes was never checked at all — and
114
+ `fieldMeta` is what carries `sensitivity`, so the unchecked case was the one
115
+ with a data-classification inventory hanging off it.
116
+
117
+ ⚠️ **BREAKING for code that catches around `describe()`.** For a validator whose
118
+ fields read synchronously, `FieldMetaUnknownFieldError` now arrives from
119
+ `vault.collection()` instead of from `describe()`. This is not only "the throw
120
+ comes earlier" — **a `try` that wraps only the `describe()` call no longer
121
+ catches it at all**, so the error escapes the handler written for it. A pilot
122
+ consumer's guard suite had exactly this shape: it constructed the collection
123
+ outside the `try`, and on upgrade those tests would have started ERRORING rather
124
+ than failing with a clear message, reading as "the new release broke our tests"
125
+ instead of "the guard moved". If you assert on this error, catch around the
126
+ `vault.collection()` call — or around both, and report which surface rejected,
127
+ which turns the relocation into a changed label rather than a break.
128
+
129
+ The describe-time check is KEPT, not moved, because there are three tiers of
130
+ knowability rather than two: a name declared in the config itself needs no
131
+ schema; a validator whose fields read synchronously is checkable at
132
+ registration; fields that exist only after async derivation are checkable only
133
+ at `describe()`. Hoisting alone would move the check earlier for some
134
+ collections and remove it for others. Unchanged: no readable validator means no
135
+ check, because a TS-generic collection names real fields that appear in no
136
+ runtime config.
137
+
138
+ **`withMaterializedView` distinguishes an absent `rowKey` from a wrong-typed
139
+ one.** Passing a field NAME — the shape every neighbouring option takes —
140
+ reported `rowKey is required`, which names absence. It now says it must be a
141
+ function, names the type it got, and shows the fix. Two states that warrant
142
+ different responses no longer render identically.
143
+
144
+ - `zod` is now an OPTIONAL peer dependency instead of being vendored into the
145
+ bundle (#1227).
146
+
147
+ Hub shipped `zod@4.4.3` inside `dist/` — 548 KB, at a build-frozen version,
148
+ declared in no manifest field. `npm ls zod` in a consumer tree showed nothing
149
+ while that copy was reachable, so SBOM and audit tooling missed it and a zod
150
+ advisory could not be remediated by a consumer bumping zod.
151
+
152
+ **This was never a deliberate vendoring.** The loader's own comment said "this
153
+ is a dynamic import so it does not add a static zod dependency to hub" — but
154
+ tsup externalises DECLARED dependencies and bundles everything else, static or
155
+ dynamic alike, and zod was a devDependency only. The comment described an
156
+ intent the build silently defeated; it now records the actual mechanism.
157
+
158
+ **Declaring it is also the correctness fix, not only a size one.** Zod's
159
+ `toJSONSchema` reads a schema's internals, and the schema is built by the
160
+ CONSUMER's zod. A vendored copy meant hub inspected one zod's objects with a
161
+ different zod's reader, with version skew that nothing detected. Now there is
162
+ one zod — theirs.
163
+
164
+ Nothing to install: the peer is optional, exactly like `zod-to-json-schema`,
165
+ and the converter is still loaded lazily. A caller with a Zod v4 schema
166
+ necessarily already has zod; a caller without one never reaches the path.
167
+
168
+ Measured, both sides rebuilt: tarball 3.3 MB → 3.1 MB packed, 12.0 MB → 10.6 MB
169
+ unpacked. The README claim is updated in the same change to say what is now
170
+ true.
171
+
3
172
  ## 0.7.0-pre.13
4
173
 
5
174
  ### Patch Changes
package/README.md CHANGED
@@ -77,9 +77,10 @@ const all = await invoices.list()
77
77
  - **Offline-first sync** — push/pull with optimistic concurrency on encrypted envelopes
78
78
  - **Audit history** — full-copy snapshots with `history()`, `diff()`, `revert()`, `pruneHistory()`
79
79
  - **No third-party runtime dependencies to install** — the only runtime dependency is
80
- `@noy-db/attestation`, on the same version line. (`zod` is vendored into the bundle for the
81
- optional persisted-schema converter and loaded lazily; it never executes unless you use that
82
- path, and it does not appear in your lockfile.)
80
+ `@noy-db/attestation`, on the same version line. `zod` (v3 or v4) and `zod-to-json-schema` are
81
+ OPTIONAL peers used by the persisted-schema converter: nothing is installed on your behalf, nothing is
82
+ vendored into the bundle, and if you do use Zod schemas the converter reads YOUR copy rather
83
+ than a second one frozen at hub's build time.
83
84
 
84
85
  ## Cross-vault queries
85
86
 
@@ -12,9 +12,8 @@ import "./chunk-LP7BEXCT.js";
12
12
  import "./chunk-SL3JAAKN.js";
13
13
  import "./chunk-N4CHCXDL.js";
14
14
  import "./chunk-7O3Y4PPD.js";
15
- import "./chunk-PZ5AY32C.js";
16
15
  export {
17
16
  UserApi,
18
17
  createUserApi
19
18
  };
20
- //# sourceMappingURL=api-UE2XQKDN.js.map
19
+ //# sourceMappingURL=api-JPZT6BDD.js.map
package/dist/as/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import "../chunk-UBSFPGVJ.js";
2
- import "../chunk-VGICZPVE.js";
2
+ import "../chunk-FUJSIP4K.js";
3
3
  import {
4
4
  applyListProjection
5
5
  } from "../chunk-VG2ZSFYD.js";
6
6
  import "../chunk-RXLYKVTF.js";
7
7
  import "../chunk-VYYM3MEN.js";
8
- import "../chunk-URHRIAV6.js";
8
+ import "../chunk-REDOOY2Q.js";
9
9
  import "../chunk-Y2NBBT6K.js";
10
10
  import "../chunk-WWNYSV7S.js";
11
11
  import "../chunk-CIGAQBJR.js";
@@ -19,7 +19,6 @@ import "../chunk-N4CHCXDL.js";
19
19
  import {
20
20
  ValidationError
21
21
  } from "../chunk-7O3Y4PPD.js";
22
- import "../chunk-PZ5AY32C.js";
23
22
 
24
23
  // src/port/as/active.ts
25
24
  function redactChunk(chunk, ctx, redact) {
@@ -75,7 +74,7 @@ function contextFor(vault) {
75
74
  // export fails instead.
76
75
  describe: (collection) => vault.collection(collection).describe(),
77
76
  plan: async (records, policy, formatId, idKey) => {
78
- const { diffVault } = await import("../vault-diff-FE5J35NJ.js");
77
+ const { diffVault } = await import("../vault-diff-C6WUJFNP.js");
79
78
  const plan = await diffVault(vault, records, idKey ? { idKey } : {});
80
79
  return {
81
80
  plan,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/port/as/active.ts"],"sourcesContent":["/**\n * `withFormats()` — the service that consumes the `as-*` port.\n *\n * Everything with a correctness or security consequence lives here rather\n * than in each format: the capability gate, the vault read, and redaction.\n * A {@link NoydbFormat} receives records and returns bytes.\n *\n * That is the whole point of the inversion (ADR 0004). The conformance kit it\n * replaces could only check implementors who ran it; a format that cannot\n * reach a vault is gated whether its author cooperated or not.\n *\n * @module\n */\nimport type { NoydbFormat, ImportPlan } from './types.js'\nimport type {\n FormatsStrategy,\n FormatsContext,\n FormatExportOptions,\n FormatImportOptions,\n} from '../with/formats-strategy.js'\nimport type { ExportChunk } from '../../kernel/types.js'\nimport type { Vault } from '../../kernel/vault.js'\nimport type { ImportPolicy } from './types.js'\nimport { applyListProjection } from '../../with-shape/introspection/index.js'\nimport type { CollectionDescription, ListProjectionOptions } from '../../with-shape/introspection/index.js'\nimport { ValidationError } from '../../kernel/errors.js'\n\n/** Apply the redaction projection hub owns, so a format never asks for it. */\nfunction redactChunk(\n chunk: ExportChunk,\n ctx: FormatsContext,\n redact: FormatExportOptions['redact'],\n): ExportChunk {\n if (redact === undefined) return chunk\n const described = ctx.describe(chunk.collection) as CollectionDescription | undefined\n if (!described) {\n throw new Error(\n `vault.export: redaction was requested but '${chunk.collection}' has no description to redact against. ` +\n 'Open the collection with its classifiedFields / fieldMeta options first.',\n )\n }\n const opts: ListProjectionOptions | undefined =\n redact === true ? undefined : { sensitivity: redact.sensitivity as never }\n const records = chunk.records.map((r) =>\n applyListProjection(described, r as Record<string, unknown>, opts),\n )\n return { ...chunk, records: records as typeof chunk.records }\n}\n\n/**\n * Derive the narrow, format-facing capability from the vault.\n *\n * Lives here rather than on `Vault` because `kernel-surface` said so, and it\n * was right: this is service capability, not spine. The spine keeps two\n * delegating methods and nothing else.\n */\nfunction contextFor(vault: Vault): FormatsContext {\n return {\n // The empty-id guards are NOT defensive padding. The\n // plaintext tier is gated per-format, so a missing id means the calling\n // binding is broken — and without this it would reach the gate, miss the\n // allowlist, and surface as `ExportCapabilityError`: \"the owner did not\n // grant this\", which sends the reader to the keyring instead of to the\n // format. A loud TypeError names the real fault.\n assertCanExport: (tier, format) => {\n if (tier === 'bundle') return vault.assertCanExport('bundle')\n if (!format) throw new TypeError(\"assertCanExport('plaintext') requires a non-empty format id\")\n vault.assertCanExport('plaintext', format)\n },\n assertCanImport: (tier, format) => {\n if (tier === 'bundle') return vault.assertCanImport('bundle')\n if (!format) throw new TypeError(\"assertCanImport('plaintext') requires a non-empty format id\")\n vault.assertCanImport('plaintext', format)\n },\n // ⚠️ Filtered HERE, not passed to exportStream — `ExportStreamOptions`\n // has no `collections` field, so the option I first passed was silently\n // dropped and hub read every collection. Caught by an as-json test\n // expecting ['invoices'] and getting ['invoices','payments'].\n //\n // Post-read filtering is correct but not free: hub still DECRYPTS the\n // collections the caller excluded. Pushing the filter down into\n // exportStream is the real fix and is a hub change beyond this port.\n chunks: async function* (collections) {\n const wanted = collections ? new Set(collections) : null\n for await (const chunk of vault.exportStream({ granularity: 'collection' })) {\n if (wanted && !wanted.has(chunk.collection)) continue\n yield chunk\n }\n },\n // NOT swallowed. A caller who asked for redaction and silently got\n // unredacted output is the exact failure this port exists to remove — a\n // security duty that fails quiet. If the description cannot be read, the\n // export fails instead.\n describe: (collection) => vault.collection(collection).describe() as never,\n plan: async (records, policy: ImportPolicy, formatId: string, idKey?: string) => {\n const { diffVault } = await import('../../with-cargo/vault-diff.js')\n const plan = await diffVault(vault, records as never, idKey ? { idKey } : {})\n return {\n plan,\n policy,\n // The apply walk every as-* package had independently: added always,\n // modified unless insert-only, deleted only on replace — in one\n // transaction so a partial failure rolls back. Owned once, here.\n apply: async () => {\n await vault.noydb.transaction((tx) => {\n const txVault = tx.vault(vault.name)\n const reason = `import:${formatId}`\n for (const e of plan.added) {\n txVault.collection(e.collection).put(e.id, e.record as never, { reason })\n }\n if (policy !== 'insert-only') {\n for (const e of plan.modified) {\n txVault.collection(e.collection).put(e.id, e.record as never, { reason })\n }\n }\n if (policy === 'replace') {\n for (const e of plan.deleted) txVault.collection(e.collection).delete(e.id)\n }\n })\n },\n }\n },\n }\n}\n\n/**\n * Enable `vault.export()` / `vault.import()`.\n *\n * ```ts\n * const db = await createNoydb({ store, user, secret, formatsStrategy: withFormats() })\n * const csv = await vault.export(asCsv(), { collections: ['invoices'] })\n * ```\n */\nexport function withFormats(): FormatsStrategy {\n return {\n enabled: true,\n\n async exportWith<Out extends string | Uint8Array>(\n vault: Vault,\n format: NoydbFormat<Out>,\n options: FormatExportOptions = {},\n ): Promise<Out> {\n const ctx = contextFor(vault)\n // FIRST, and before a single record is read. The format is not in scope\n // yet and could not reach the vault if it were.\n if (format.tier === 'bundle') ctx.assertCanExport('bundle')\n else ctx.assertCanExport('plaintext', format.id)\n\n const chunks: ExportChunk[] = []\n for await (const chunk of ctx.chunks(options.collections)) {\n chunks.push(redactChunk(chunk, ctx, options.redact))\n }\n return format.encode(chunks)\n },\n\n async importWith<Out extends string | Uint8Array>(\n vault: Vault,\n format: NoydbFormat<Out>,\n input: Out,\n options: FormatImportOptions = {},\n ): Promise<ImportPlan> {\n const ctx = contextFor(vault)\n // Mirrors the export side: the import gate is two-tier as well, and a\n // bundle format importing under the plaintext tier would be a silent\n // downgrade in the other direction.\n if (format.tier === 'bundle') ctx.assertCanImport('bundle')\n else ctx.assertCanImport('plaintext', format.id)\n if (!format.decode) {\n throw new ValidationError(\n `vault.import: the '${format.id}' format is export-only — it declares no decode(). ` +\n 'Pick a format that round-trips, or write the records directly.',\n )\n }\n const decoded = await format.decode(input)\n const candidate: Record<string, readonly unknown[]> = {}\n for (const chunk of decoded) {\n const target = chunk.collection || options.collection\n if (!target) {\n throw new ValidationError(\n `vault.import: the '${format.id}' input names no collection and none was supplied. ` +\n 'Pass { collection } for formats whose payload is collection-less.',\n )\n }\n candidate[target] = [...(candidate[target] ?? []), ...chunk.records]\n }\n return ctx.plan(candidate, options.policy ?? 'merge', format.id, options.idKey)\n },\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/port/as/active.ts"],"sourcesContent":["/**\n * `withFormats()` — the service that consumes the `as-*` port.\n *\n * Everything with a correctness or security consequence lives here rather\n * than in each format: the capability gate, the vault read, and redaction.\n * A {@link NoydbFormat} receives records and returns bytes.\n *\n * That is the whole point of the inversion (ADR 0004). The conformance kit it\n * replaces could only check implementors who ran it; a format that cannot\n * reach a vault is gated whether its author cooperated or not.\n *\n * @module\n */\nimport type { NoydbFormat, ImportPlan } from './types.js'\nimport type {\n FormatsStrategy,\n FormatsContext,\n FormatExportOptions,\n FormatImportOptions,\n} from '../with/formats-strategy.js'\nimport type { ExportChunk } from '../../kernel/types.js'\nimport type { Vault } from '../../kernel/vault.js'\nimport type { ImportPolicy } from './types.js'\nimport { applyListProjection } from '../../with-shape/introspection/index.js'\nimport type { CollectionDescription, ListProjectionOptions } from '../../with-shape/introspection/index.js'\nimport { ValidationError } from '../../kernel/errors.js'\n\n/** Apply the redaction projection hub owns, so a format never asks for it. */\nfunction redactChunk(\n chunk: ExportChunk,\n ctx: FormatsContext,\n redact: FormatExportOptions['redact'],\n): ExportChunk {\n if (redact === undefined) return chunk\n const described = ctx.describe(chunk.collection) as CollectionDescription | undefined\n if (!described) {\n throw new Error(\n `vault.export: redaction was requested but '${chunk.collection}' has no description to redact against. ` +\n 'Open the collection with its classifiedFields / fieldMeta options first.',\n )\n }\n const opts: ListProjectionOptions | undefined =\n redact === true ? undefined : { sensitivity: redact.sensitivity as never }\n const records = chunk.records.map((r) =>\n applyListProjection(described, r as Record<string, unknown>, opts),\n )\n return { ...chunk, records: records as typeof chunk.records }\n}\n\n/**\n * Derive the narrow, format-facing capability from the vault.\n *\n * Lives here rather than on `Vault` because `kernel-surface` said so, and it\n * was right: this is service capability, not spine. The spine keeps two\n * delegating methods and nothing else.\n */\nfunction contextFor(vault: Vault): FormatsContext {\n return {\n // The empty-id guards are NOT defensive padding. The\n // plaintext tier is gated per-format, so a missing id means the calling\n // binding is broken — and without this it would reach the gate, miss the\n // allowlist, and surface as `ExportCapabilityError`: \"the owner did not\n // grant this\", which sends the reader to the keyring instead of to the\n // format. A loud TypeError names the real fault.\n assertCanExport: (tier, format) => {\n if (tier === 'bundle') return vault.assertCanExport('bundle')\n if (!format) throw new TypeError(\"assertCanExport('plaintext') requires a non-empty format id\")\n vault.assertCanExport('plaintext', format)\n },\n assertCanImport: (tier, format) => {\n if (tier === 'bundle') return vault.assertCanImport('bundle')\n if (!format) throw new TypeError(\"assertCanImport('plaintext') requires a non-empty format id\")\n vault.assertCanImport('plaintext', format)\n },\n // ⚠️ Filtered HERE, not passed to exportStream — `ExportStreamOptions`\n // has no `collections` field, so the option I first passed was silently\n // dropped and hub read every collection. Caught by an as-json test\n // expecting ['invoices'] and getting ['invoices','payments'].\n //\n // Post-read filtering is correct but not free: hub still DECRYPTS the\n // collections the caller excluded. Pushing the filter down into\n // exportStream is the real fix and is a hub change beyond this port.\n chunks: async function* (collections) {\n const wanted = collections ? new Set(collections) : null\n for await (const chunk of vault.exportStream({ granularity: 'collection' })) {\n if (wanted && !wanted.has(chunk.collection)) continue\n yield chunk\n }\n },\n // NOT swallowed. A caller who asked for redaction and silently got\n // unredacted output is the exact failure this port exists to remove — a\n // security duty that fails quiet. If the description cannot be read, the\n // export fails instead.\n describe: (collection) => vault.collection(collection).describe() as never,\n plan: async (records, policy: ImportPolicy, formatId: string, idKey?: string) => {\n const { diffVault } = await import('../../with-cargo/vault-diff.js')\n const plan = await diffVault(vault, records as never, idKey ? { idKey } : {})\n return {\n plan,\n policy,\n // The apply walk every as-* package had independently: added always,\n // modified unless insert-only, deleted only on replace — in one\n // transaction so a partial failure rolls back. Owned once, here.\n apply: async () => {\n await vault.noydb.transaction((tx) => {\n const txVault = tx.vault(vault.name)\n const reason = `import:${formatId}`\n for (const e of plan.added) {\n txVault.collection(e.collection).put(e.id, e.record as never, { reason })\n }\n if (policy !== 'insert-only') {\n for (const e of plan.modified) {\n txVault.collection(e.collection).put(e.id, e.record as never, { reason })\n }\n }\n if (policy === 'replace') {\n for (const e of plan.deleted) txVault.collection(e.collection).delete(e.id)\n }\n })\n },\n }\n },\n }\n}\n\n/**\n * Enable `vault.export()` / `vault.import()`.\n *\n * ```ts\n * const db = await createNoydb({ store, user, secret, formatsStrategy: withFormats() })\n * const csv = await vault.export(asCsv(), { collections: ['invoices'] })\n * ```\n */\nexport function withFormats(): FormatsStrategy {\n return {\n enabled: true,\n\n async exportWith<Out extends string | Uint8Array>(\n vault: Vault,\n format: NoydbFormat<Out>,\n options: FormatExportOptions = {},\n ): Promise<Out> {\n const ctx = contextFor(vault)\n // FIRST, and before a single record is read. The format is not in scope\n // yet and could not reach the vault if it were.\n if (format.tier === 'bundle') ctx.assertCanExport('bundle')\n else ctx.assertCanExport('plaintext', format.id)\n\n const chunks: ExportChunk[] = []\n for await (const chunk of ctx.chunks(options.collections)) {\n chunks.push(redactChunk(chunk, ctx, options.redact))\n }\n return format.encode(chunks)\n },\n\n async importWith<Out extends string | Uint8Array>(\n vault: Vault,\n format: NoydbFormat<Out>,\n input: Out,\n options: FormatImportOptions = {},\n ): Promise<ImportPlan> {\n const ctx = contextFor(vault)\n // Mirrors the export side: the import gate is two-tier as well, and a\n // bundle format importing under the plaintext tier would be a silent\n // downgrade in the other direction.\n if (format.tier === 'bundle') ctx.assertCanImport('bundle')\n else ctx.assertCanImport('plaintext', format.id)\n if (!format.decode) {\n throw new ValidationError(\n `vault.import: the '${format.id}' format is export-only — it declares no decode(). ` +\n 'Pick a format that round-trips, or write the records directly.',\n )\n }\n const decoded = await format.decode(input)\n const candidate: Record<string, readonly unknown[]> = {}\n for (const chunk of decoded) {\n const target = chunk.collection || options.collection\n if (!target) {\n throw new ValidationError(\n `vault.import: the '${format.id}' input names no collection and none was supplied. ` +\n 'Pass { collection } for formats whose payload is collection-less.',\n )\n }\n candidate[target] = [...(candidate[target] ?? []), ...chunk.records]\n }\n return ctx.plan(candidate, options.policy ?? 'merge', format.id, options.idKey)\n },\n }\n}\n"],"mappings":"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","names":[]}
package/dist/at/index.js CHANGED
@@ -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
  export {
18
17
  MemoryRecipientSealer,
19
18
  MemorySealer,
@@ -26,17 +26,16 @@ import {
26
26
  AttestationError,
27
27
  AttestationNotEnabledError
28
28
  } from "../chunk-7O3Y4PPD.js";
29
- import "../chunk-PZ5AY32C.js";
30
29
 
31
30
  // src/with-audit/attestation/active.ts
32
31
  function withAttestation() {
33
32
  return {
34
33
  async issueAttestation(ctx, args) {
35
- const { issueAttestationCore: issueAttestationCore2 } = await import("../issue-IOXM3RSJ.js");
34
+ const { issueAttestationCore: issueAttestationCore2 } = await import("../issue-RH4H733Z.js");
36
35
  return issueAttestationCore2(ctx, args);
37
36
  },
38
37
  async getDocumentSigningPublicKey(deps) {
39
- const { loadSigner, loadOrCreateSigner } = await import("../signer-7NK7TMWU.js");
38
+ const { loadSigner, loadOrCreateSigner } = await import("../signer-5K3WDHOC.js");
40
39
  const existing = await loadSigner(deps.adapter, deps.vault, deps.getDEK);
41
40
  if (existing) return { keyId: existing.keyId, publicKeyB64: existing.publicKeyB64 };
42
41
  if (deps.role !== "owner") {
@@ -46,19 +45,19 @@ function withAttestation() {
46
45
  return { keyId: signer.keyId, publicKeyB64: signer.publicKeyB64 };
47
46
  },
48
47
  async revokeAttestation(ctx, docId) {
49
- const { revokeDocCore: revokeDocCore2 } = await import("../revoke-U7U3FMXU.js");
48
+ const { revokeDocCore: revokeDocCore2 } = await import("../revoke-POHP4GPB.js");
50
49
  return revokeDocCore2(ctx, docId);
51
50
  },
52
51
  async unrevokeAttestation(ctx, docId) {
53
- const { unrevokeDocCore: unrevokeDocCore2 } = await import("../revoke-U7U3FMXU.js");
52
+ const { unrevokeDocCore: unrevokeDocCore2 } = await import("../revoke-POHP4GPB.js");
54
53
  return unrevokeDocCore2(ctx, docId);
55
54
  },
56
55
  async getRevokedDocIds(ctx) {
57
- const { getRevokedDocIdsCore: getRevokedDocIdsCore2 } = await import("../revoke-U7U3FMXU.js");
56
+ const { getRevokedDocIdsCore: getRevokedDocIdsCore2 } = await import("../revoke-POHP4GPB.js");
58
57
  return getRevokedDocIdsCore2(ctx);
59
58
  },
60
59
  async publishRevocationList(ctx) {
61
- const { publishRevocationListCore: publishRevocationListCore2 } = await import("../revoke-U7U3FMXU.js");
60
+ const { publishRevocationListCore: publishRevocationListCore2 } = await import("../revoke-POHP4GPB.js");
62
61
  return publishRevocationListCore2(ctx);
63
62
  }
64
63
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/with-audit/attestation/active.ts","../../src/with-audit/attestation/index.ts"],"sourcesContent":["import { AttestationError } from '../../kernel/errors.js'\nimport type { AttestationStrategy } from './strategy.js'\n\n/**\n * Enable the attestation capability.\n * Pass to `createNoydb({ attestationStrategy: withAttestation() })` to make the\n * vault's `issueAttestation` / `getDocumentSigningPublicKey` / `revokeAttestation`\n * / `unrevokeAttestation` / `getRevokedDocIds` / `publishRevocationList` methods\n * live. The issue/revoke/signer engines are dynamically imported here, so they\n * stay out of the floor bundle until opted in.\n */\nexport function withAttestation(): AttestationStrategy {\n return {\n async issueAttestation(ctx, args) {\n const { issueAttestationCore } = await import('./issue.js')\n return issueAttestationCore(ctx, args)\n },\n async getDocumentSigningPublicKey(deps) {\n const { loadSigner, loadOrCreateSigner } = await import('./signer.js')\n // Reading an existing public key is open to any role that holds the\n // _attestations DEK — the public key is not secret. But MINTING the\n // signer is the firm's identity operation (same rule as issueAttestation):\n // a non-owner read must not silently create it.\n const existing = await loadSigner(deps.adapter, deps.vault, deps.getDEK)\n if (existing) return { keyId: existing.keyId, publicKeyB64: existing.publicKeyB64 }\n if (deps.role !== 'owner') {\n throw new AttestationError(`getDocumentSigningPublicKey: no document-signing key exists yet; only the 'owner' may mint it. Caller is '${deps.role}'. Have the owner issue an attestation (or call this) first.`)\n }\n const signer = await loadOrCreateSigner(deps.adapter, deps.vault, deps.getDEK)\n return { keyId: signer.keyId, publicKeyB64: signer.publicKeyB64 }\n },\n async revokeAttestation(ctx, docId) {\n const { revokeDocCore } = await import('./revoke.js')\n return revokeDocCore(ctx, docId)\n },\n async unrevokeAttestation(ctx, docId) {\n const { unrevokeDocCore } = await import('./revoke.js')\n return unrevokeDocCore(ctx, docId)\n },\n async getRevokedDocIds(ctx) {\n const { getRevokedDocIdsCore } = await import('./revoke.js')\n return getRevokedDocIdsCore(ctx)\n },\n async publishRevocationList(ctx) {\n const { publishRevocationListCore } = await import('./revoke.js')\n return publishRevocationListCore(ctx)\n },\n }\n}\n","/**\n * @category capability\n * Document attestation — issue side. Mint a signed, per-field commitment\n * for a record and emit a QR credential verifiable offline via\n * `@noy-db/attestation`. See design-history/2026-05-29-attestation-core-and-issue-design.md.\n */\nexport { withAttestation } from './active.js'\nexport { NO_ATTESTATION, type AttestationStrategy } from './strategy.js'\nexport { AttestationNotEnabledError } from '../../kernel/errors.js'\nexport { issueAttestationCore } from './issue.js'\nexport type { IssueContext, IssueArgs, IssueResult } from './issue.js'\nexport { ATTESTATIONS_COLLECTION } from './signer.js'\nexport { revokeDocCore, unrevokeDocCore, getRevokedDocIdsCore, publishRevocationListCore } from './revoke.js'\nexport type { RevokeContext } from './revoke.js'\nexport { AttestationError } from '../../kernel/errors.js'\n// Re-export the pure verifier surface so consumers can verify from one import:\nexport { verifyAttestation, decodeQr, verifyRevocationList, isRevoked, signRevocationList, type QrPayload, type AttestationFieldSchema, type VerifyResult, type VerifyInput, type RevocationList } from '@noy-db/attestation'\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAWO,SAAS,kBAAuC;AACrD,SAAO;AAAA,IACL,MAAM,iBAAiB,KAAK,MAAM;AAChC,YAAM,EAAE,sBAAAA,sBAAqB,IAAI,MAAM,OAAO,sBAAY;AAC1D,aAAOA,sBAAqB,KAAK,IAAI;AAAA,IACvC;AAAA,IACA,MAAM,4BAA4B,MAAM;AACtC,YAAM,EAAE,YAAY,mBAAmB,IAAI,MAAM,OAAO,uBAAa;AAKrE,YAAM,WAAW,MAAM,WAAW,KAAK,SAAS,KAAK,OAAO,KAAK,MAAM;AACvE,UAAI,SAAU,QAAO,EAAE,OAAO,SAAS,OAAO,cAAc,SAAS,aAAa;AAClF,UAAI,KAAK,SAAS,SAAS;AACzB,cAAM,IAAI,iBAAiB,6GAA6G,KAAK,IAAI,8DAA8D;AAAA,MACjN;AACA,YAAM,SAAS,MAAM,mBAAmB,KAAK,SAAS,KAAK,OAAO,KAAK,MAAM;AAC7E,aAAO,EAAE,OAAO,OAAO,OAAO,cAAc,OAAO,aAAa;AAAA,IAClE;AAAA,IACA,MAAM,kBAAkB,KAAK,OAAO;AAClC,YAAM,EAAE,eAAAC,eAAc,IAAI,MAAM,OAAO,uBAAa;AACpD,aAAOA,eAAc,KAAK,KAAK;AAAA,IACjC;AAAA,IACA,MAAM,oBAAoB,KAAK,OAAO;AACpC,YAAM,EAAE,iBAAAC,iBAAgB,IAAI,MAAM,OAAO,uBAAa;AACtD,aAAOA,iBAAgB,KAAK,KAAK;AAAA,IACnC;AAAA,IACA,MAAM,iBAAiB,KAAK;AAC1B,YAAM,EAAE,sBAAAC,sBAAqB,IAAI,MAAM,OAAO,uBAAa;AAC3D,aAAOA,sBAAqB,GAAG;AAAA,IACjC;AAAA,IACA,MAAM,sBAAsB,KAAK;AAC/B,YAAM,EAAE,2BAAAC,2BAA0B,IAAI,MAAM,OAAO,uBAAa;AAChE,aAAOA,2BAA0B,GAAG;AAAA,IACtC;AAAA,EACF;AACF;;;AChCA,SAAS,mBAAmB,UAAU,sBAAsB,WAAW,0BAAiI;","names":["issueAttestationCore","revokeDocCore","unrevokeDocCore","getRevokedDocIdsCore","publishRevocationListCore"]}
1
+ {"version":3,"sources":["../../src/with-audit/attestation/active.ts","../../src/with-audit/attestation/index.ts"],"sourcesContent":["import { AttestationError } from '../../kernel/errors.js'\nimport type { AttestationStrategy } from './strategy.js'\n\n/**\n * Enable the attestation capability.\n * Pass to `createNoydb({ attestationStrategy: withAttestation() })` to make the\n * vault's `issueAttestation` / `getDocumentSigningPublicKey` / `revokeAttestation`\n * / `unrevokeAttestation` / `getRevokedDocIds` / `publishRevocationList` methods\n * live. The issue/revoke/signer engines are dynamically imported here, so they\n * stay out of the floor bundle until opted in.\n */\nexport function withAttestation(): AttestationStrategy {\n return {\n async issueAttestation(ctx, args) {\n const { issueAttestationCore } = await import('./issue.js')\n return issueAttestationCore(ctx, args)\n },\n async getDocumentSigningPublicKey(deps) {\n const { loadSigner, loadOrCreateSigner } = await import('./signer.js')\n // Reading an existing public key is open to any role that holds the\n // _attestations DEK — the public key is not secret. But MINTING the\n // signer is the firm's identity operation (same rule as issueAttestation):\n // a non-owner read must not silently create it.\n const existing = await loadSigner(deps.adapter, deps.vault, deps.getDEK)\n if (existing) return { keyId: existing.keyId, publicKeyB64: existing.publicKeyB64 }\n if (deps.role !== 'owner') {\n throw new AttestationError(`getDocumentSigningPublicKey: no document-signing key exists yet; only the 'owner' may mint it. Caller is '${deps.role}'. Have the owner issue an attestation (or call this) first.`)\n }\n const signer = await loadOrCreateSigner(deps.adapter, deps.vault, deps.getDEK)\n return { keyId: signer.keyId, publicKeyB64: signer.publicKeyB64 }\n },\n async revokeAttestation(ctx, docId) {\n const { revokeDocCore } = await import('./revoke.js')\n return revokeDocCore(ctx, docId)\n },\n async unrevokeAttestation(ctx, docId) {\n const { unrevokeDocCore } = await import('./revoke.js')\n return unrevokeDocCore(ctx, docId)\n },\n async getRevokedDocIds(ctx) {\n const { getRevokedDocIdsCore } = await import('./revoke.js')\n return getRevokedDocIdsCore(ctx)\n },\n async publishRevocationList(ctx) {\n const { publishRevocationListCore } = await import('./revoke.js')\n return publishRevocationListCore(ctx)\n },\n }\n}\n","/**\n * @category capability\n * Document attestation — issue side. Mint a signed, per-field commitment\n * for a record and emit a QR credential verifiable offline via\n * `@noy-db/attestation`. See design-history/2026-05-29-attestation-core-and-issue-design.md.\n */\nexport { withAttestation } from './active.js'\nexport { NO_ATTESTATION, type AttestationStrategy } from './strategy.js'\nexport { AttestationNotEnabledError } from '../../kernel/errors.js'\nexport { issueAttestationCore } from './issue.js'\nexport type { IssueContext, IssueArgs, IssueResult } from './issue.js'\nexport { ATTESTATIONS_COLLECTION } from './signer.js'\nexport { revokeDocCore, unrevokeDocCore, getRevokedDocIdsCore, publishRevocationListCore } from './revoke.js'\nexport type { RevokeContext } from './revoke.js'\nexport { AttestationError } from '../../kernel/errors.js'\n// Re-export the pure verifier surface so consumers can verify from one import:\nexport { verifyAttestation, decodeQr, verifyRevocationList, isRevoked, signRevocationList, type QrPayload, type AttestationFieldSchema, type VerifyResult, type VerifyInput, type RevocationList } from '@noy-db/attestation'\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAWO,SAAS,kBAAuC;AACrD,SAAO;AAAA,IACL,MAAM,iBAAiB,KAAK,MAAM;AAChC,YAAM,EAAE,sBAAAA,sBAAqB,IAAI,MAAM,OAAO,sBAAY;AAC1D,aAAOA,sBAAqB,KAAK,IAAI;AAAA,IACvC;AAAA,IACA,MAAM,4BAA4B,MAAM;AACtC,YAAM,EAAE,YAAY,mBAAmB,IAAI,MAAM,OAAO,uBAAa;AAKrE,YAAM,WAAW,MAAM,WAAW,KAAK,SAAS,KAAK,OAAO,KAAK,MAAM;AACvE,UAAI,SAAU,QAAO,EAAE,OAAO,SAAS,OAAO,cAAc,SAAS,aAAa;AAClF,UAAI,KAAK,SAAS,SAAS;AACzB,cAAM,IAAI,iBAAiB,6GAA6G,KAAK,IAAI,8DAA8D;AAAA,MACjN;AACA,YAAM,SAAS,MAAM,mBAAmB,KAAK,SAAS,KAAK,OAAO,KAAK,MAAM;AAC7E,aAAO,EAAE,OAAO,OAAO,OAAO,cAAc,OAAO,aAAa;AAAA,IAClE;AAAA,IACA,MAAM,kBAAkB,KAAK,OAAO;AAClC,YAAM,EAAE,eAAAC,eAAc,IAAI,MAAM,OAAO,uBAAa;AACpD,aAAOA,eAAc,KAAK,KAAK;AAAA,IACjC;AAAA,IACA,MAAM,oBAAoB,KAAK,OAAO;AACpC,YAAM,EAAE,iBAAAC,iBAAgB,IAAI,MAAM,OAAO,uBAAa;AACtD,aAAOA,iBAAgB,KAAK,KAAK;AAAA,IACnC;AAAA,IACA,MAAM,iBAAiB,KAAK;AAC1B,YAAM,EAAE,sBAAAC,sBAAqB,IAAI,MAAM,OAAO,uBAAa;AAC3D,aAAOA,sBAAqB,GAAG;AAAA,IACjC;AAAA,IACA,MAAM,sBAAsB,KAAK;AAC/B,YAAM,EAAE,2BAAAC,2BAA0B,IAAI,MAAM,OAAO,uBAAa;AAChE,aAAOA,2BAA0B,GAAG;AAAA,IACtC;AAAA,EACF;AACF;;;AChCA,SAAS,mBAAmB,UAAU,sBAAsB,WAAW,0BAAiI;","names":["issueAttestationCore","revokeDocCore","unrevokeDocCore","getRevokedDocIdsCore","publishRevocationListCore"]}
@@ -34,7 +34,6 @@ import {
34
34
  BackupCorruptedError,
35
35
  BackupLedgerError
36
36
  } from "./chunk-7O3Y4PPD.js";
37
- import "./chunk-PZ5AY32C.js";
38
37
 
39
38
  // src/with-pod/backup.ts
40
39
  async function dumpVault(ctx) {
@@ -262,4 +261,4 @@ export {
262
261
  loadVault,
263
262
  verifyBackupIntegrity
264
263
  };
265
- //# sourceMappingURL=backup-R6A4VZRX.js.map
264
+ //# sourceMappingURL=backup-AKWIMYWU.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/with-pod/backup.ts"],"sourcesContent":["/**\n * Vault backup / restore / integrity.\n *\n * Holds `dump()` (verifiable encrypted JSON backup, including the blob\n * collections so blob \"covers\" travel in the bundle), `load()` (restore +\n * post-load integrity gate), `verifyBackupIntegrity()` (chain + data-envelope\n * cross-check), and `exportJSON()` (plaintext per-collection export, built on\n * the vault's `exportStream`). Behaviour is byte-identical to the inline\n * `Vault` methods these functions replaced — every dependency the moving code\n * touched on `this.*` arrives via {@link BackupContext}. `Vault` keeps the\n * public methods as thin delegators.\n *\n * Internal — reached through `vault.dump()` / `vault.load()` / etc.\n */\nimport { NOYDB_BACKUP_VERSION } from '../kernel/types.js'\nimport { BackupLedgerError, BackupCorruptedError } from '../kernel/errors.js'\nimport { LEDGER_COLLECTION, LEDGER_DELTAS_COLLECTION } from '../with-commit/history/ledger/constants.js'\nimport { SCHEMAS_COLLECTION } from '../with-shape/persisted-schemas/storage.js'\nimport { SEQUENCE_COLLECTION } from '../with-commit/sequence/index.js'\nimport { MANIFEST_COLLECTION } from '../with-shape/manifest/reserved-collections.js'\n// #1025 — imported from the dependency-light `window.ts`, not `periods.ts`: that\n// module exists precisely so a consumer can name the period collections without\n// dragging the ledger hash-chain machinery (`sha256Hex`/`canonicalJson`) into a\n// bundle that opted out of the periods service. Same reason the sync engine's\n// period-scoped pull imports it.\nimport {\n PERIODS_COLLECTION,\n PERIOD_FREEZES_COLLECTION,\n PERIOD_ARCHIVES_COLLECTION,\n PERIOD_TARGET_PURGES_COLLECTION,\n PERIOD_REOPENS_COLLECTION,\n} from '../with-audit/periods/window.js'\nimport type {\n NoydbStore,\n EncryptedEnvelope,\n VaultBackup,\n VaultSnapshot,\n ExportStreamOptions,\n ExportChunk,\n} from '../kernel/types.js'\nimport type { LedgerStore } from '../with-commit/history/ledger/store.js'\nimport { buildRecordEnvelope } from '../kernel/enclave/index.js'\n\n/** Everything the moving backup methods touched on the vault's `this.*`. */\nexport interface BackupContext {\n /** The ciphertext store. */\n readonly adapter: NoydbStore\n /** Vault namespace name. */\n readonly vault: string\n /** The invoking keyring's user id (read fresh per call). */\n userId(): string\n /** The vault's ledger store, or null when history is off. */\n getLedgerOrNull(): LedgerStore | null\n /** Recompute an envelope's `payloadHash` (bound `historyStrategy.envelopePayloadHash`). */\n envelopePayloadHash(envelope: EncryptedEnvelope): Promise<string>\n /**\n * Refresh the in-memory keyring from the freshly-loaded keyring file and\n * rebuild the DEK resolver. No-op when the vault has no `reloadKeyring`\n * callback (plaintext vaults / test constructions).\n */\n reloadKeyringAndRebuildDEK(): Promise<void>\n /** Clear the vault's collection cache (post-load). */\n clearCollectionCache(): void\n /** Reset the ledger store so the next `ledger()` rebuilds its head cache. */\n resetLedgerStore(): void\n /** The vault's decrypt+ACL export stream (used by `exportJSON`). */\n exportStream(opts: ExportStreamOptions): AsyncIterableIterator<ExportChunk>\n}\n\n/** Result of {@link verifyBackupIntegrity}. */\nexport type VerifyBackupResult =\n | { readonly ok: true; readonly head: string; readonly length: number }\n | {\n readonly ok: false\n readonly kind: 'chain'\n readonly divergedAt: number\n readonly message: string\n }\n | {\n readonly ok: false\n readonly kind: 'data'\n readonly collection: string\n readonly id: string\n readonly message: string\n }\n\n/**\n * Dump vault as a verifiable encrypted JSON backup string.\n *\n * backups embed the current ledger head and the full `_ledger` +\n * `_ledger_deltas` internal collections so the receiver can run\n * `verifyBackupIntegrity()` after `load()` and detect any tampering between\n * dump and restore. Backups produced without a ledger skip the integrity check\n * with a warning — both modes round-trip cleanly.\n */\nexport async function dumpVault(ctx: BackupContext): Promise<string> {\n const snapshot = await ctx.adapter.loadAll(ctx.vault)\n\n // Load keyrings (separate path because loadAll filters them out\n // along with all other underscore-prefixed internal collections).\n const keyringIds = await ctx.adapter.list(ctx.vault, '_keyring')\n const keyrings: Record<string, unknown> = {}\n for (const keyringId of keyringIds) {\n const envelope = await ctx.adapter.get(ctx.vault, '_keyring', keyringId)\n if (envelope) {\n keyrings[keyringId] = JSON.parse(envelope._data)\n }\n }\n\n // Load the ledger entries + deltas so the receiver can replay\n // the chain after restore. Without this, `load()` would have an\n // empty ledger and `verifyBackupIntegrity()` would have nothing\n // to compare against.\n //\n // Also enumerate the blob collections so blob content (\"covers\")\n // travels in the bundle (the blob DEK already travels in `_keyring`).\n // Literals are inlined (not imported from blobs/blob-set.ts) to keep\n // the blob runtime out of this kernel hot path — they mirror\n // BLOB_INDEX/CHUNKS/EVICTION_AUDIT_COLLECTION and SLOTS/VERSIONS_PREFIX.\n // The collect-loop skips empty ids, so this no-ops without blobs.\n const internalSnapshot: VaultSnapshot = {}\n const internalNames = [\n LEDGER_COLLECTION, LEDGER_DELTAS_COLLECTION, SCHEMAS_COLLECTION, SEQUENCE_COLLECTION,\n MANIFEST_COLLECTION, // #941: the pod's manifest-set record(s) travel in the bundle too\n // #1025: the accounting-period close state + its companions. The bundle is\n // the backup/restore path, so without these a restore discarded the\n // hash-chained evidence that a month was ever closed — the artifact\n // `closePeriod` exists to produce — and dropped the write gate with it, so\n // the restored vault silently accepted back-dated writes into a sealed\n // month. The companions matter as much as the close: a bounded reopen\n // window (`reopenPeriod({ until })`) is state a restore must not lose.\n PERIODS_COLLECTION,\n PERIOD_REOPENS_COLLECTION,\n PERIOD_FREEZES_COLLECTION,\n PERIOD_ARCHIVES_COLLECTION,\n PERIOD_TARGET_PURGES_COLLECTION,\n '_history', // full-snapshot version history — so history()/getVersion()/diff() survive the bundle\n '_blob_index', '_blob_chunks', '_blob_eviction_audit',\n // #753 spec §7 Q3: `_blob_intent` markers travel too — restoring mid-op\n // state (a blob row without its marker) would reproduce the ambiguity\n // the journal exists to prevent. Note: `_mv_stale` does NOT travel\n // today (a pre-existing, separate gap) — observed here, tracked on\n // #761, not fixed in this pass.\n '_blob_intent',\n // #769: `_blob_slots_<c>` envelopes travel verbatim (raw ciphertext,\n // same DEK, same vault) — including any `SlotRecord.pendingRelease`\n // resume breadcrumb they carry. This is INTENTIONALLY the opposite of\n // `extract-partition`'s `reKeyBlobs`, which STRIPS `pendingRelease`:\n // a full-vault backup restores into the SAME vault it was taken from,\n // so a breadcrumb taken mid-rehome (together with its `_blob_intent`\n // marker, carried above) is resumable on restore — carry-and-resume is\n // correct here. A partition extraction crosses vaults: the breadcrumb's\n // old eTag may not exist in the destination, so it must be stripped\n // there instead. Asymmetry: backup = same-vault-resumable = carry;\n // partition = cross-vault = strip-breadcrumb-carry-marker.\n ...Object.keys(snapshot).flatMap((c) => [`_blob_slots_${c}`, `_blob_versions_${c}`]),\n ]\n for (const internalName of internalNames) {\n const ids = await ctx.adapter.list(ctx.vault, internalName)\n if (ids.length === 0) continue\n const records: Record<string, EncryptedEnvelope> = {}\n for (const id of ids) {\n const envelope = await ctx.adapter.get(ctx.vault, internalName, id)\n if (envelope) records[id] = envelope\n }\n internalSnapshot[internalName] = records\n }\n\n // Embed the ledger head if there's a chain. An empty ledger\n // (fresh vault) leaves `ledgerHead` undefined, which\n // load() treats the same as a legacy backup (no integrity\n // check, console warning). If history is not opted in,\n // `getLedgerOrNull` returns null and we skip embedding entirely\n // — the backup is still valid, just without the integrity head.\n const ledgerForHead = ctx.getLedgerOrNull()\n const head = ledgerForHead ? await ledgerForHead.head() : null\n const backup: VaultBackup = {\n _noydb_backup: NOYDB_BACKUP_VERSION,\n _compartment: ctx.vault,\n _exported_at: new Date().toISOString(),\n _exported_by: ctx.userId(),\n keyrings: keyrings as VaultBackup['keyrings'],\n collections: snapshot,\n ...(Object.keys(internalSnapshot).length > 0\n ? { _internal: internalSnapshot }\n : {}),\n ...(head\n ? {\n ledgerHead: {\n hash: head.hash,\n index: head.entry.index,\n ts: head.entry.ts,\n },\n }\n : {}),\n }\n\n return JSON.stringify(backup)\n}\n\n/**\n * Restore a vault from a verifiable backup. After loading, runs\n * `verifyBackupIntegrity()` to confirm the hash chain, the embedded head, and\n * every data envelope's payload hash. Legacy backups (no `ledgerHead`) load\n * with a console warning and skip the integrity check.\n */\nexport async function loadVault(ctx: BackupContext, backupJson: string): Promise<void> {\n const backup = JSON.parse(backupJson) as VaultBackup\n\n // 1. Restore data collections.\n await ctx.adapter.saveAll(ctx.vault, backup.collections)\n\n // 2. Restore keyrings.\n for (const [userId, keyringFile] of Object.entries(backup.keyrings)) {\n const envelope = buildRecordEnvelope({ collection: '_keyring', id: userId, version: 1 },\n { iv: '', data: JSON.stringify(keyringFile) })\n await ctx.adapter.put(ctx.vault, '_keyring', userId, envelope)\n }\n\n // 3. Restore internal collections (`_ledger`, `_ledger_deltas`).\n // Required so verifyBackupIntegrity has the chain to walk.\n if (backup._internal) {\n for (const [internalName, records] of Object.entries(backup._internal)) {\n for (const [id, envelope] of Object.entries(records)) {\n await ctx.adapter.put(ctx.vault, internalName, id, envelope)\n }\n }\n }\n\n // 4. Refresh the in-memory keyring from the freshly-loaded\n // keyring file. Without this, the Vault's getDEK\n // closure still holds the OLD session's DEKs, and every\n // decrypt of a loaded ledger entry / data envelope fails\n // with TamperedError because the DEK doesn't match the\n // ciphertext that was encrypted with the SOURCE user's DEK.\n // Skipped for plaintext vaults and for tests that\n // construct Vault without a reloadKeyring callback.\n await ctx.reloadKeyringAndRebuildDEK()\n\n // 5. Clear collection cache + reset the ledger store so the\n // next ledger() call rebuilds its head cache from the\n // freshly-loaded entries.\n ctx.clearCollectionCache()\n ctx.resetLedgerStore()\n\n // 5. Run the verification gate. Legacy backups (no ledgerHead)\n // skip this with a one-line warning so existing consumers can\n // still read them while migrating.\n if (!backup.ledgerHead) {\n console.warn(\n `[noy-db] Loaded a legacy backup with no ledgerHead — ` +\n `verifiable-backup integrity check skipped. ` +\n `Re-export with a ledger-aware build to get tamper detection.`,\n )\n return\n }\n\n const result = await verifyBackupIntegrity(ctx)\n if (!result.ok) {\n // Surface the most specific error class we can. The result\n // shape carries enough info for callers to inspect.\n if (result.kind === 'data') {\n throw new BackupCorruptedError(\n result.collection,\n result.id,\n result.message,\n )\n }\n throw new BackupLedgerError(result.message, result.divergedAt)\n }\n\n // 6. Cross-check: the freshly-verified head must match the\n // value embedded at dump time. A mismatch means someone\n // truncated or extended the chain after dump.\n if (result.head !== backup.ledgerHead.hash) {\n throw new BackupLedgerError(\n `Backup ledger head mismatch: embedded \"${backup.ledgerHead.hash}\" ` +\n `but reconstructed \"${result.head}\".`,\n )\n }\n}\n\n/**\n * End-to-end backup integrity check: `ledger.verify()` (hash chain) plus a data\n * envelope cross-check (every current record's `_data` hash must match the\n * latest `put` entry's `payloadHash`). Returns a discriminated union so callers\n * can distinguish chain vs data failures.\n */\nexport async function verifyBackupIntegrity(ctx: BackupContext): Promise<VerifyBackupResult> {\n // Step 1: chain verification. Without the history strategy there\n // is no ledger; an unaudited backup verifies trivially as `ok`\n // because there's nothing to diverge from.\n const ledgerForVerify = ctx.getLedgerOrNull()\n if (!ledgerForVerify) {\n return { ok: true, head: '', length: 0 }\n }\n const chainResult = await ledgerForVerify.verify()\n if (!chainResult.ok) {\n return {\n ok: false,\n kind: 'chain',\n divergedAt: chainResult.divergedAt,\n message:\n `Ledger chain diverged at index ${chainResult.divergedAt}: ` +\n `expected prevHash \"${chainResult.expected}\" but found \"${chainResult.actual}\".`,\n }\n }\n\n // Step 2: data envelope cross-check. Walk every entry in the\n // ledger and, for the LATEST `put` per (collection, id), recompute\n // the data envelope's payloadHash and compare. Earlier puts of the\n // same id are skipped because the data collection only holds the\n // current version — historical envelopes live in the deltas\n // collection (which is itself protected by the chain).\n // Reuse the ledger we already resolved in step 1.\n const allEntries = await ledgerForVerify.loadAllEntries()\n\n // Find the latest non-delete entry per (collection, id). Walk\n // the entries in reverse so we hit the latest first; mark each\n // (collection, id) as seen and skip subsequent entries.\n const seen = new Set<string>()\n const latest = new Map<\n string,\n { collection: string; id: string; expectedHash: string }\n >()\n for (let i = allEntries.length - 1; i >= 0; i--) {\n const entry = allEntries[i]\n if (!entry) continue\n // Amendment entries are multi-record audit entries whose\n // `collection` and `id` are empty strings — building a `\"/\"`\n // key here would mark that synthetic slot as seen and falsely\n // trip the data check on a record that never existed. Skip\n // them BEFORE the key/seen bookkeeping so they neither\n // tombstone real entries nor enter the latest map.\n if (entry.op === 'amendment' || entry.op === 'lifecycle') continue\n const key = `${entry.collection}/${entry.id}`\n if (seen.has(key)) continue\n seen.add(key)\n // For deletes the data collection should NOT have the record,\n // so we skip — there's nothing to cross-check. Marking the key\n // as seen above ensures any earlier `put` of the same id is\n // also skipped (the record was subsequently deleted).\n if (entry.op === 'delete') continue\n latest.set(key, {\n collection: entry.collection,\n id: entry.id,\n expectedHash: entry.payloadHash,\n })\n }\n\n for (const { collection, id, expectedHash } of latest.values()) {\n const envelope = await ctx.adapter.get(ctx.vault, collection, id)\n if (!envelope) {\n return {\n ok: false,\n kind: 'data',\n collection,\n id,\n message:\n `Ledger expects data record \"${collection}/${id}\" to exist, ` +\n `but the adapter has no envelope for it.`,\n }\n }\n const actualHash = await ctx.envelopePayloadHash(envelope)\n if (actualHash !== expectedHash) {\n return {\n ok: false,\n kind: 'data',\n collection,\n id,\n message:\n `Data envelope \"${collection}/${id}\" has been tampered with: ` +\n `expected payloadHash \"${expectedHash}\", got \"${actualHash}\".`,\n }\n }\n }\n\n return {\n ok: true,\n head: chainResult.head,\n length: chainResult.length,\n }\n}\n\n/**\n * Plaintext per-collection export, built on the vault's `exportStream`. Forces\n * collection granularity (record-by-record output makes no sense in a single\n * string) and merges dictionary snapshots across collections.\n */\nexport async function exportVaultJSON(ctx: BackupContext, opts: ExportStreamOptions = {}): Promise<string> {\n // Force per-collection granularity regardless of caller setting:\n // record-by-record output doesn't make sense in a single string.\n const collections: Record<\n string,\n {\n schema: null\n refs: Record<string, { target: string; mode: 'strict' | 'warn' | 'cascade' }>\n records: unknown[]\n }\n > = {}\n let ledgerHead: ExportChunk['ledgerHead'] | undefined\n // Merged dictionary snapshot across all collections.\n // Only populated when `resolveLabels` is not set.\n const allDictionaries: Record<\n string, // collection name\n Record<string, Record<string, Record<string, string>>>\n > = {}\n\n for await (const chunk of ctx.exportStream({\n granularity: 'collection',\n withLedgerHead: opts.withLedgerHead === true,\n // Thread the export locale so records are read at the\n // `export` layer (i18nText collapsed + dictKey/staticDict labels resolved).\n ...(opts.resolveLabels !== undefined ? { resolveLabels: opts.resolveLabels } : {}),\n })) {\n collections[chunk.collection] = {\n schema: null, // Standard Schema validators are not JSON-serializable\n refs: chunk.refs,\n records: chunk.records,\n }\n if (chunk.ledgerHead) ledgerHead = chunk.ledgerHead\n // Collect dictionary snapshots unless resolveLabels is set\n if (!opts.resolveLabels && chunk.dictionaries) {\n allDictionaries[chunk.collection] = chunk.dictionaries\n }\n }\n\n const hasDictionaries = Object.keys(allDictionaries).length > 0\n return JSON.stringify({\n _noydb_export: 1,\n _compartment: ctx.vault,\n _exported_at: new Date().toISOString(),\n _exported_by: ctx.userId(),\n collections,\n ...(hasDictionaries ? { _dictionaries: allDictionaries } : {}),\n ...(ledgerHead ? { ledgerHead } : {}),\n })\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/with-pod/backup.ts"],"sourcesContent":["/**\n * Vault backup / restore / integrity.\n *\n * Holds `dump()` (verifiable encrypted JSON backup, including the blob\n * collections so blob \"covers\" travel in the bundle), `load()` (restore +\n * post-load integrity gate), `verifyBackupIntegrity()` (chain + data-envelope\n * cross-check), and `exportJSON()` (plaintext per-collection export, built on\n * the vault's `exportStream`). Behaviour is byte-identical to the inline\n * `Vault` methods these functions replaced — every dependency the moving code\n * touched on `this.*` arrives via {@link BackupContext}. `Vault` keeps the\n * public methods as thin delegators.\n *\n * Internal — reached through `vault.dump()` / `vault.load()` / etc.\n */\nimport { NOYDB_BACKUP_VERSION } from '../kernel/types.js'\nimport { BackupLedgerError, BackupCorruptedError } from '../kernel/errors.js'\nimport { LEDGER_COLLECTION, LEDGER_DELTAS_COLLECTION } from '../with-commit/history/ledger/constants.js'\nimport { SCHEMAS_COLLECTION } from '../with-shape/persisted-schemas/storage.js'\nimport { SEQUENCE_COLLECTION } from '../with-commit/sequence/index.js'\nimport { MANIFEST_COLLECTION } from '../with-shape/manifest/reserved-collections.js'\n// #1025 — imported from the dependency-light `window.ts`, not `periods.ts`: that\n// module exists precisely so a consumer can name the period collections without\n// dragging the ledger hash-chain machinery (`sha256Hex`/`canonicalJson`) into a\n// bundle that opted out of the periods service. Same reason the sync engine's\n// period-scoped pull imports it.\nimport {\n PERIODS_COLLECTION,\n PERIOD_FREEZES_COLLECTION,\n PERIOD_ARCHIVES_COLLECTION,\n PERIOD_TARGET_PURGES_COLLECTION,\n PERIOD_REOPENS_COLLECTION,\n} from '../with-audit/periods/window.js'\nimport type {\n NoydbStore,\n EncryptedEnvelope,\n VaultBackup,\n VaultSnapshot,\n ExportStreamOptions,\n ExportChunk,\n} from '../kernel/types.js'\nimport type { LedgerStore } from '../with-commit/history/ledger/store.js'\nimport { buildRecordEnvelope } from '../kernel/enclave/index.js'\n\n/** Everything the moving backup methods touched on the vault's `this.*`. */\nexport interface BackupContext {\n /** The ciphertext store. */\n readonly adapter: NoydbStore\n /** Vault namespace name. */\n readonly vault: string\n /** The invoking keyring's user id (read fresh per call). */\n userId(): string\n /** The vault's ledger store, or null when history is off. */\n getLedgerOrNull(): LedgerStore | null\n /** Recompute an envelope's `payloadHash` (bound `historyStrategy.envelopePayloadHash`). */\n envelopePayloadHash(envelope: EncryptedEnvelope): Promise<string>\n /**\n * Refresh the in-memory keyring from the freshly-loaded keyring file and\n * rebuild the DEK resolver. No-op when the vault has no `reloadKeyring`\n * callback (plaintext vaults / test constructions).\n */\n reloadKeyringAndRebuildDEK(): Promise<void>\n /** Clear the vault's collection cache (post-load). */\n clearCollectionCache(): void\n /** Reset the ledger store so the next `ledger()` rebuilds its head cache. */\n resetLedgerStore(): void\n /** The vault's decrypt+ACL export stream (used by `exportJSON`). */\n exportStream(opts: ExportStreamOptions): AsyncIterableIterator<ExportChunk>\n}\n\n/** Result of {@link verifyBackupIntegrity}. */\nexport type VerifyBackupResult =\n | { readonly ok: true; readonly head: string; readonly length: number }\n | {\n readonly ok: false\n readonly kind: 'chain'\n readonly divergedAt: number\n readonly message: string\n }\n | {\n readonly ok: false\n readonly kind: 'data'\n readonly collection: string\n readonly id: string\n readonly message: string\n }\n\n/**\n * Dump vault as a verifiable encrypted JSON backup string.\n *\n * backups embed the current ledger head and the full `_ledger` +\n * `_ledger_deltas` internal collections so the receiver can run\n * `verifyBackupIntegrity()` after `load()` and detect any tampering between\n * dump and restore. Backups produced without a ledger skip the integrity check\n * with a warning — both modes round-trip cleanly.\n */\nexport async function dumpVault(ctx: BackupContext): Promise<string> {\n const snapshot = await ctx.adapter.loadAll(ctx.vault)\n\n // Load keyrings (separate path because loadAll filters them out\n // along with all other underscore-prefixed internal collections).\n const keyringIds = await ctx.adapter.list(ctx.vault, '_keyring')\n const keyrings: Record<string, unknown> = {}\n for (const keyringId of keyringIds) {\n const envelope = await ctx.adapter.get(ctx.vault, '_keyring', keyringId)\n if (envelope) {\n keyrings[keyringId] = JSON.parse(envelope._data)\n }\n }\n\n // Load the ledger entries + deltas so the receiver can replay\n // the chain after restore. Without this, `load()` would have an\n // empty ledger and `verifyBackupIntegrity()` would have nothing\n // to compare against.\n //\n // Also enumerate the blob collections so blob content (\"covers\")\n // travels in the bundle (the blob DEK already travels in `_keyring`).\n // Literals are inlined (not imported from blobs/blob-set.ts) to keep\n // the blob runtime out of this kernel hot path — they mirror\n // BLOB_INDEX/CHUNKS/EVICTION_AUDIT_COLLECTION and SLOTS/VERSIONS_PREFIX.\n // The collect-loop skips empty ids, so this no-ops without blobs.\n const internalSnapshot: VaultSnapshot = {}\n const internalNames = [\n LEDGER_COLLECTION, LEDGER_DELTAS_COLLECTION, SCHEMAS_COLLECTION, SEQUENCE_COLLECTION,\n MANIFEST_COLLECTION, // #941: the pod's manifest-set record(s) travel in the bundle too\n // #1025: the accounting-period close state + its companions. The bundle is\n // the backup/restore path, so without these a restore discarded the\n // hash-chained evidence that a month was ever closed — the artifact\n // `closePeriod` exists to produce — and dropped the write gate with it, so\n // the restored vault silently accepted back-dated writes into a sealed\n // month. The companions matter as much as the close: a bounded reopen\n // window (`reopenPeriod({ until })`) is state a restore must not lose.\n PERIODS_COLLECTION,\n PERIOD_REOPENS_COLLECTION,\n PERIOD_FREEZES_COLLECTION,\n PERIOD_ARCHIVES_COLLECTION,\n PERIOD_TARGET_PURGES_COLLECTION,\n '_history', // full-snapshot version history — so history()/getVersion()/diff() survive the bundle\n '_blob_index', '_blob_chunks', '_blob_eviction_audit',\n // #753 spec §7 Q3: `_blob_intent` markers travel too — restoring mid-op\n // state (a blob row without its marker) would reproduce the ambiguity\n // the journal exists to prevent. Note: `_mv_stale` does NOT travel\n // today (a pre-existing, separate gap) — observed here, tracked on\n // #761, not fixed in this pass.\n '_blob_intent',\n // #769: `_blob_slots_<c>` envelopes travel verbatim (raw ciphertext,\n // same DEK, same vault) — including any `SlotRecord.pendingRelease`\n // resume breadcrumb they carry. This is INTENTIONALLY the opposite of\n // `extract-partition`'s `reKeyBlobs`, which STRIPS `pendingRelease`:\n // a full-vault backup restores into the SAME vault it was taken from,\n // so a breadcrumb taken mid-rehome (together with its `_blob_intent`\n // marker, carried above) is resumable on restore — carry-and-resume is\n // correct here. A partition extraction crosses vaults: the breadcrumb's\n // old eTag may not exist in the destination, so it must be stripped\n // there instead. Asymmetry: backup = same-vault-resumable = carry;\n // partition = cross-vault = strip-breadcrumb-carry-marker.\n ...Object.keys(snapshot).flatMap((c) => [`_blob_slots_${c}`, `_blob_versions_${c}`]),\n ]\n for (const internalName of internalNames) {\n const ids = await ctx.adapter.list(ctx.vault, internalName)\n if (ids.length === 0) continue\n const records: Record<string, EncryptedEnvelope> = {}\n for (const id of ids) {\n const envelope = await ctx.adapter.get(ctx.vault, internalName, id)\n if (envelope) records[id] = envelope\n }\n internalSnapshot[internalName] = records\n }\n\n // Embed the ledger head if there's a chain. An empty ledger\n // (fresh vault) leaves `ledgerHead` undefined, which\n // load() treats the same as a legacy backup (no integrity\n // check, console warning). If history is not opted in,\n // `getLedgerOrNull` returns null and we skip embedding entirely\n // — the backup is still valid, just without the integrity head.\n const ledgerForHead = ctx.getLedgerOrNull()\n const head = ledgerForHead ? await ledgerForHead.head() : null\n const backup: VaultBackup = {\n _noydb_backup: NOYDB_BACKUP_VERSION,\n _compartment: ctx.vault,\n _exported_at: new Date().toISOString(),\n _exported_by: ctx.userId(),\n keyrings: keyrings as VaultBackup['keyrings'],\n collections: snapshot,\n ...(Object.keys(internalSnapshot).length > 0\n ? { _internal: internalSnapshot }\n : {}),\n ...(head\n ? {\n ledgerHead: {\n hash: head.hash,\n index: head.entry.index,\n ts: head.entry.ts,\n },\n }\n : {}),\n }\n\n return JSON.stringify(backup)\n}\n\n/**\n * Restore a vault from a verifiable backup. After loading, runs\n * `verifyBackupIntegrity()` to confirm the hash chain, the embedded head, and\n * every data envelope's payload hash. Legacy backups (no `ledgerHead`) load\n * with a console warning and skip the integrity check.\n */\nexport async function loadVault(ctx: BackupContext, backupJson: string): Promise<void> {\n const backup = JSON.parse(backupJson) as VaultBackup\n\n // 1. Restore data collections.\n await ctx.adapter.saveAll(ctx.vault, backup.collections)\n\n // 2. Restore keyrings.\n for (const [userId, keyringFile] of Object.entries(backup.keyrings)) {\n const envelope = buildRecordEnvelope({ collection: '_keyring', id: userId, version: 1 },\n { iv: '', data: JSON.stringify(keyringFile) })\n await ctx.adapter.put(ctx.vault, '_keyring', userId, envelope)\n }\n\n // 3. Restore internal collections (`_ledger`, `_ledger_deltas`).\n // Required so verifyBackupIntegrity has the chain to walk.\n if (backup._internal) {\n for (const [internalName, records] of Object.entries(backup._internal)) {\n for (const [id, envelope] of Object.entries(records)) {\n await ctx.adapter.put(ctx.vault, internalName, id, envelope)\n }\n }\n }\n\n // 4. Refresh the in-memory keyring from the freshly-loaded\n // keyring file. Without this, the Vault's getDEK\n // closure still holds the OLD session's DEKs, and every\n // decrypt of a loaded ledger entry / data envelope fails\n // with TamperedError because the DEK doesn't match the\n // ciphertext that was encrypted with the SOURCE user's DEK.\n // Skipped for plaintext vaults and for tests that\n // construct Vault without a reloadKeyring callback.\n await ctx.reloadKeyringAndRebuildDEK()\n\n // 5. Clear collection cache + reset the ledger store so the\n // next ledger() call rebuilds its head cache from the\n // freshly-loaded entries.\n ctx.clearCollectionCache()\n ctx.resetLedgerStore()\n\n // 5. Run the verification gate. Legacy backups (no ledgerHead)\n // skip this with a one-line warning so existing consumers can\n // still read them while migrating.\n if (!backup.ledgerHead) {\n console.warn(\n `[noy-db] Loaded a legacy backup with no ledgerHead — ` +\n `verifiable-backup integrity check skipped. ` +\n `Re-export with a ledger-aware build to get tamper detection.`,\n )\n return\n }\n\n const result = await verifyBackupIntegrity(ctx)\n if (!result.ok) {\n // Surface the most specific error class we can. The result\n // shape carries enough info for callers to inspect.\n if (result.kind === 'data') {\n throw new BackupCorruptedError(\n result.collection,\n result.id,\n result.message,\n )\n }\n throw new BackupLedgerError(result.message, result.divergedAt)\n }\n\n // 6. Cross-check: the freshly-verified head must match the\n // value embedded at dump time. A mismatch means someone\n // truncated or extended the chain after dump.\n if (result.head !== backup.ledgerHead.hash) {\n throw new BackupLedgerError(\n `Backup ledger head mismatch: embedded \"${backup.ledgerHead.hash}\" ` +\n `but reconstructed \"${result.head}\".`,\n )\n }\n}\n\n/**\n * End-to-end backup integrity check: `ledger.verify()` (hash chain) plus a data\n * envelope cross-check (every current record's `_data` hash must match the\n * latest `put` entry's `payloadHash`). Returns a discriminated union so callers\n * can distinguish chain vs data failures.\n */\nexport async function verifyBackupIntegrity(ctx: BackupContext): Promise<VerifyBackupResult> {\n // Step 1: chain verification. Without the history strategy there\n // is no ledger; an unaudited backup verifies trivially as `ok`\n // because there's nothing to diverge from.\n const ledgerForVerify = ctx.getLedgerOrNull()\n if (!ledgerForVerify) {\n return { ok: true, head: '', length: 0 }\n }\n const chainResult = await ledgerForVerify.verify()\n if (!chainResult.ok) {\n return {\n ok: false,\n kind: 'chain',\n divergedAt: chainResult.divergedAt,\n message:\n `Ledger chain diverged at index ${chainResult.divergedAt}: ` +\n `expected prevHash \"${chainResult.expected}\" but found \"${chainResult.actual}\".`,\n }\n }\n\n // Step 2: data envelope cross-check. Walk every entry in the\n // ledger and, for the LATEST `put` per (collection, id), recompute\n // the data envelope's payloadHash and compare. Earlier puts of the\n // same id are skipped because the data collection only holds the\n // current version — historical envelopes live in the deltas\n // collection (which is itself protected by the chain).\n // Reuse the ledger we already resolved in step 1.\n const allEntries = await ledgerForVerify.loadAllEntries()\n\n // Find the latest non-delete entry per (collection, id). Walk\n // the entries in reverse so we hit the latest first; mark each\n // (collection, id) as seen and skip subsequent entries.\n const seen = new Set<string>()\n const latest = new Map<\n string,\n { collection: string; id: string; expectedHash: string }\n >()\n for (let i = allEntries.length - 1; i >= 0; i--) {\n const entry = allEntries[i]\n if (!entry) continue\n // Amendment entries are multi-record audit entries whose\n // `collection` and `id` are empty strings — building a `\"/\"`\n // key here would mark that synthetic slot as seen and falsely\n // trip the data check on a record that never existed. Skip\n // them BEFORE the key/seen bookkeeping so they neither\n // tombstone real entries nor enter the latest map.\n if (entry.op === 'amendment' || entry.op === 'lifecycle') continue\n const key = `${entry.collection}/${entry.id}`\n if (seen.has(key)) continue\n seen.add(key)\n // For deletes the data collection should NOT have the record,\n // so we skip — there's nothing to cross-check. Marking the key\n // as seen above ensures any earlier `put` of the same id is\n // also skipped (the record was subsequently deleted).\n if (entry.op === 'delete') continue\n latest.set(key, {\n collection: entry.collection,\n id: entry.id,\n expectedHash: entry.payloadHash,\n })\n }\n\n for (const { collection, id, expectedHash } of latest.values()) {\n const envelope = await ctx.adapter.get(ctx.vault, collection, id)\n if (!envelope) {\n return {\n ok: false,\n kind: 'data',\n collection,\n id,\n message:\n `Ledger expects data record \"${collection}/${id}\" to exist, ` +\n `but the adapter has no envelope for it.`,\n }\n }\n const actualHash = await ctx.envelopePayloadHash(envelope)\n if (actualHash !== expectedHash) {\n return {\n ok: false,\n kind: 'data',\n collection,\n id,\n message:\n `Data envelope \"${collection}/${id}\" has been tampered with: ` +\n `expected payloadHash \"${expectedHash}\", got \"${actualHash}\".`,\n }\n }\n }\n\n return {\n ok: true,\n head: chainResult.head,\n length: chainResult.length,\n }\n}\n\n/**\n * Plaintext per-collection export, built on the vault's `exportStream`. Forces\n * collection granularity (record-by-record output makes no sense in a single\n * string) and merges dictionary snapshots across collections.\n */\nexport async function exportVaultJSON(ctx: BackupContext, opts: ExportStreamOptions = {}): Promise<string> {\n // Force per-collection granularity regardless of caller setting:\n // record-by-record output doesn't make sense in a single string.\n const collections: Record<\n string,\n {\n schema: null\n refs: Record<string, { target: string; mode: 'strict' | 'warn' | 'cascade' }>\n records: unknown[]\n }\n > = {}\n let ledgerHead: ExportChunk['ledgerHead'] | undefined\n // Merged dictionary snapshot across all collections.\n // Only populated when `resolveLabels` is not set.\n const allDictionaries: Record<\n string, // collection name\n Record<string, Record<string, Record<string, string>>>\n > = {}\n\n for await (const chunk of ctx.exportStream({\n granularity: 'collection',\n withLedgerHead: opts.withLedgerHead === true,\n // Thread the export locale so records are read at the\n // `export` layer (i18nText collapsed + dictKey/staticDict labels resolved).\n ...(opts.resolveLabels !== undefined ? { resolveLabels: opts.resolveLabels } : {}),\n })) {\n collections[chunk.collection] = {\n schema: null, // Standard Schema validators are not JSON-serializable\n refs: chunk.refs,\n records: chunk.records,\n }\n if (chunk.ledgerHead) ledgerHead = chunk.ledgerHead\n // Collect dictionary snapshots unless resolveLabels is set\n if (!opts.resolveLabels && chunk.dictionaries) {\n allDictionaries[chunk.collection] = chunk.dictionaries\n }\n }\n\n const hasDictionaries = Object.keys(allDictionaries).length > 0\n return JSON.stringify({\n _noydb_export: 1,\n _compartment: ctx.vault,\n _exported_at: new Date().toISOString(),\n _exported_by: ctx.userId(),\n collections,\n ...(hasDictionaries ? { _dictionaries: allDictionaries } : {}),\n ...(ledgerHead ? { ledgerHead } : {}),\n })\n}\n"],"mappings":"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","names":[]}
@@ -40,7 +40,6 @@ import "../chunk-N4CHCXDL.js";
40
40
  import {
41
41
  BlobOfflineError
42
42
  } from "../chunk-7O3Y4PPD.js";
43
- import "../chunk-PZ5AY32C.js";
44
43
 
45
44
  // src/via/blob/active.ts
46
45
  function withBlobs(options = {}) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/via/blob/active.ts"],"sourcesContent":["/**\n * Active blob strategy factory. Calling `blobs()` returns a\n * `BlobsStrategy` whose `openSlot` constructs a real `BlobSet` bound\n * to the caller's record. The returned strategy is passed into\n * `createNoydb({ blobsStrategy: blobs() })` to light up the\n * `collection.blob(id)` path.\n *\n * This module is only reachable through the `@noy-db/hub/blobs`\n * subpath — a consumer that never imports the subpath ships none of\n * this (ESM tree-shaking + hub's `\"sideEffects\": false`).\n */\n\nimport { BlobSet } from '../../with-shape/blobs/blob-set.js'\nimport type { BlobsStrategy } from '../../port/with/blob-strategy.js'\nimport { createBlobPinCache } from '../../with-shape/blobs/blob-pinning.js'\nimport type { BlobPinStore } from '../../with-shape/blobs/blob-pinning.js'\n\n/** Options for {@link withBlobs} (#808). */\nexport interface WithBlobsOptions {\n /**\n * Device-local backend for the offline-pin registry + external side-cache\n * (`collection.blob(id).pin()`, the `cacheBudget` LRU stamps). Default:\n * in-memory — pins then last one app session. Supply a durable,\n * DEVICE-LOCAL store (IndexedDB-backed on the web; it must never sync) to\n * keep pins across restarts. See `BlobPinStore`'s doc for the posture.\n */\n readonly pinStore?: BlobPinStore\n}\n\n/**\n * Build a default `BlobsStrategy` ready to pass into `createNoydb`.\n *\n * Named `withBlobs` (plugin-pattern canonical) rather than `blobs` to\n * avoid shadowing the very common local idiom\n * `const blobs = invoices.blob(id)` in user code.\n *\n * One `withBlobs()` instance owns one device-local pin registry + KPI\n * counter set (#808) — treat it as \"this device's\" blob service.\n *\n * @example\n * ```ts\n * import { createNoydb } from '@noy-db/hub'\n * import { withBlobs } from '@noy-db/hub/blobs'\n *\n * const db = await createNoydb({\n * store, user, secret,\n * blobsStrategy: withBlobs(),\n * })\n *\n * // Now live — delegates to BlobSet.\n * const vault = await db.openVault('acme')\n * await vault.collection('invoices').blob('inv-1').put('receipt.pdf', bytes)\n * ```\n */\nexport function withBlobs(options: WithBlobsOptions = {}): BlobsStrategy {\n const pinCache = createBlobPinCache(options.pinStore)\n return {\n openSlot(args) {\n return new BlobSet({ ...args, pinCache })\n },\n cacheStats() {\n return { ...pinCache.stats }\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDO,SAAS,UAAU,UAA4B,CAAC,GAAkB;AACvE,QAAM,WAAW,mBAAmB,QAAQ,QAAQ;AACpD,SAAO;AAAA,IACL,SAAS,MAAM;AACb,aAAO,IAAI,QAAQ,EAAE,GAAG,MAAM,SAAS,CAAC;AAAA,IAC1C;AAAA,IACA,aAAa;AACX,aAAO,EAAE,GAAG,SAAS,MAAM;AAAA,IAC7B;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/via/blob/active.ts"],"sourcesContent":["/**\n * Active blob strategy factory. Calling `blobs()` returns a\n * `BlobsStrategy` whose `openSlot` constructs a real `BlobSet` bound\n * to the caller's record. The returned strategy is passed into\n * `createNoydb({ blobsStrategy: blobs() })` to light up the\n * `collection.blob(id)` path.\n *\n * This module is only reachable through the `@noy-db/hub/blobs`\n * subpath — a consumer that never imports the subpath ships none of\n * this (ESM tree-shaking + hub's `\"sideEffects\": false`).\n */\n\nimport { BlobSet } from '../../with-shape/blobs/blob-set.js'\nimport type { BlobsStrategy } from '../../port/with/blob-strategy.js'\nimport { createBlobPinCache } from '../../with-shape/blobs/blob-pinning.js'\nimport type { BlobPinStore } from '../../with-shape/blobs/blob-pinning.js'\n\n/** Options for {@link withBlobs} (#808). */\nexport interface WithBlobsOptions {\n /**\n * Device-local backend for the offline-pin registry + external side-cache\n * (`collection.blob(id).pin()`, the `cacheBudget` LRU stamps). Default:\n * in-memory — pins then last one app session. Supply a durable,\n * DEVICE-LOCAL store (IndexedDB-backed on the web; it must never sync) to\n * keep pins across restarts. See `BlobPinStore`'s doc for the posture.\n */\n readonly pinStore?: BlobPinStore\n}\n\n/**\n * Build a default `BlobsStrategy` ready to pass into `createNoydb`.\n *\n * Named `withBlobs` (plugin-pattern canonical) rather than `blobs` to\n * avoid shadowing the very common local idiom\n * `const blobs = invoices.blob(id)` in user code.\n *\n * One `withBlobs()` instance owns one device-local pin registry + KPI\n * counter set (#808) — treat it as \"this device's\" blob service.\n *\n * @example\n * ```ts\n * import { createNoydb } from '@noy-db/hub'\n * import { withBlobs } from '@noy-db/hub/blobs'\n *\n * const db = await createNoydb({\n * store, user, secret,\n * blobsStrategy: withBlobs(),\n * })\n *\n * // Now live — delegates to BlobSet.\n * const vault = await db.openVault('acme')\n * await vault.collection('invoices').blob('inv-1').put('receipt.pdf', bytes)\n * ```\n */\nexport function withBlobs(options: WithBlobsOptions = {}): BlobsStrategy {\n const pinCache = createBlobPinCache(options.pinStore)\n return {\n openSlot(args) {\n return new BlobSet({ ...args, pinCache })\n },\n cacheStats() {\n return { ...pinCache.stats }\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDO,SAAS,UAAU,UAA4B,CAAC,GAAkB;AACvE,QAAM,WAAW,mBAAmB,QAAQ,QAAQ;AACpD,SAAO;AAAA,IACL,SAAS,MAAM;AACb,aAAO,IAAI,QAAQ,EAAE,GAAG,MAAM,SAAS,CAAC;AAAA,IAC1C;AAAA,IACA,aAAa;AACX,aAAO,EAAE,GAAG,SAAS,MAAM;AAAA,IAC7B;AAAA,EACF;AACF;","names":[]}
@@ -17,7 +17,6 @@ import {
17
17
  BrokerNotEnabledError,
18
18
  BrokerProofError
19
19
  } from "../chunk-7O3Y4PPD.js";
20
- import "../chunk-PZ5AY32C.js";
21
20
 
22
21
  // src/with-party/broker/active.ts
23
22
  var DEFAULT_SKEW_MS = 6e4;
@@ -30,7 +29,7 @@ function withBroker(config) {
30
29
  const cache = /* @__PURE__ */ new Map();
31
30
  return {
32
31
  async enroll(ctx) {
33
- const { enrollSeed } = await import("../seed-3RXT4VGN.js");
32
+ const { enrollSeed } = await import("../seed-UYR6YG3V.js");
34
33
  return enrollSeed({ ...ctx, config });
35
34
  },
36
35
  async rotate(ctx) {
@@ -39,7 +38,7 @@ function withBroker(config) {
39
38
  const inFlight = scoped.map(([, e]) => e.inFlight).filter((p) => p !== void 0);
40
39
  await Promise.allSettled(inFlight);
41
40
  for (const [key] of scoped) cache.delete(key);
42
- const { rotateSeed } = await import("../seed-3RXT4VGN.js");
41
+ const { rotateSeed } = await import("../seed-UYR6YG3V.js");
43
42
  return rotateSeed({ ...ctx, config });
44
43
  },
45
44
  credentialSource(ctx, profile) {
@@ -56,7 +55,7 @@ function withBroker(config) {
56
55
  const skewMs = config.skewMs ?? DEFAULT_SKEW_MS;
57
56
  const capturedEntry = entry;
58
57
  const inFlight = (async () => {
59
- const { mintStoreCredentials } = await import("../seed-3RXT4VGN.js");
58
+ const { mintStoreCredentials } = await import("../seed-UYR6YG3V.js");
60
59
  const creds = await mintStoreCredentials({ ...ctx, config }, profile);
61
60
  capturedEntry.cached = { creds, validUntil: cacheFloor(creds, skewMs) };
62
61
  return creds;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/with-party/broker/active.ts","../../src/with-party/broker/index.ts"],"sourcesContent":["/**\n * Enable the credential-broker capability (#479, spec §5).\n *\n * Pass to `createNoydb({ brokerStrategy: withBroker(config) })` to make\n * `vault.broker()` live. Each method dynamic-imports the seed lifecycle\n * engine (`./seed.js`) — mirroring `via/classified/active.ts`'s per-method\n * dynamic-import seam — so `with-party/broker/seed.ts` (the heavy module:\n * the CAS enrol, quiesce-then-swap rotate, and the challenge/response round\n * trip) is reachable only through the `@noy-db/hub/broker` subpath and\n * never enters the single-user floor bundle.\n *\n * The single-flight, per-vault-per-profile refresh cache (H-6/I6) lives\n * HERE, in this factory's closure — shared across every `credentialSource(profile)`\n * call for this one broker instance (in turn shared across every vault of a\n * `createNoydb()` instance — `vault.broker()` passes the SAME `BrokerStrategy`\n * for all of them), so `rotate()` can quiesce it (await any in-flight\n * round-trip, then drop it) before the seed swap. The cache key is\n * `${vault}\\0${profile}` — keyed by vault FIRST, or two vaults sharing this\n * one strategy would collide and hand vault-B a cached credential minted\n * (and STS-scoped) for vault-A.\n */\nimport type { BrokerStrategy, BrokerConfig, BrokerCtx } from '../../port/with/broker-strategy.js'\nimport type { StoreCredentials, StoreCredentialSource } from '../../kernel/types.js'\n\nconst DEFAULT_SKEW_MS = 60_000\n/** Floor under the skew-adjusted boundary so a sub-skew credential TTL\n * doesn't collapse to \"already expired\" and thrash re-proving every call (I6b). */\nconst MIN_CACHE_MS = 1_000\n\ninterface CacheEntry {\n cached?: { creds: StoreCredentials; validUntil: number } | undefined\n inFlight?: Promise<StoreCredentials> | undefined\n}\n\nfunction cacheFloor(creds: StoreCredentials, skewMs: number): number {\n const expiresAtMs = creds.expiresAt ? Date.parse(creds.expiresAt) : Date.now() + skewMs * 2\n return Math.max(expiresAtMs - skewMs, Date.now() + MIN_CACHE_MS)\n}\n\nexport function withBroker(config: BrokerConfig): BrokerStrategy {\n const cache = new Map<string, CacheEntry>()\n\n return {\n async enroll(ctx) {\n const { enrollSeed } = await import('./seed.js')\n return enrollSeed({ ...ctx, config })\n },\n async rotate(ctx) {\n // Quiesce (I5): let any in-flight round-trip finish (it may still be\n // proving under the pre-rotation seed) before the local record is\n // overwritten, then drop the cache — scoped to THIS vault's entries\n // only (`${ctx.vault}\\0`-prefixed keys). A global cache.clear() here\n // would blow away every OTHER vault's cache on this shared strategy\n // too — same root cause as the cross-vault read collision this fixes.\n const vaultPrefix = `${ctx.vault}\\0`\n const scoped = [...cache.entries()].filter(([key]) => key.startsWith(vaultPrefix))\n const inFlight = scoped.map(([, e]) => e.inFlight).filter((p): p is Promise<StoreCredentials> => p !== undefined)\n await Promise.allSettled(inFlight)\n for (const [key] of scoped) cache.delete(key)\n const { rotateSeed } = await import('./seed.js')\n return rotateSeed({ ...ctx, config })\n },\n credentialSource(ctx: BrokerCtx, profile?: string): StoreCredentialSource {\n const key = `${ctx.vault}\\0${profile ?? ''}`\n return async () => {\n let entry = cache.get(key)\n if (!entry) {\n entry = {}\n cache.set(key, entry)\n }\n const now = Date.now()\n if (entry.cached && now < entry.cached.validUntil) return entry.cached.creds\n if (entry.inFlight) return entry.inFlight\n\n const skewMs = config.skewMs ?? DEFAULT_SKEW_MS\n const capturedEntry = entry\n const inFlight = (async () => {\n const { mintStoreCredentials } = await import('./seed.js')\n const creds = await mintStoreCredentials({ ...ctx, config }, profile)\n capturedEntry.cached = { creds, validUntil: cacheFloor(creds, skewMs) }\n return creds\n })()\n entry.inFlight = inFlight\n try {\n return await inFlight\n } finally {\n // Clear-on-settle (I6a): a rejected in-flight promise must never\n // stay pinned in the cache, or every subsequent call would wedge\n // on the same rejection instead of retrying.\n capturedEntry.inFlight = undefined\n }\n }\n },\n }\n}\n","/**\n * `@noy-db/hub/broker` — subpath export for the credential-broker capability\n * (#479, credential-broker spec). Apps that never call `vault.broker()` can\n * exclude this subpath entirely: the floor default `NO_BROKER` throws, and\n * this module's real engine (`with-party/broker/{seed,active}.ts`) is only\n * ever reached through `withBroker()`'s dynamic imports, so it never enters\n * a floor bundle.\n *\n * Also re-exports the host-side helpers (`issueChallenge`, `verifyBrokerProof`)\n * from the enclave barrel, so a Lambda/Worker implementing the reference\n * broker host can import just this one subpath and get everything it\n * needs, `crypto.subtle`-only.\n */\n\n// ─── Capability opt-in seam ──────────────────────────────────\nexport { withBroker } from './active.js'\nexport {\n NO_BROKER,\n type BrokerStrategy,\n type BrokerCtx,\n type BrokerConfig,\n type CredentialBrokerHandle,\n} from '../../port/with/broker-strategy.js'\nexport { BrokerNotEnabledError, BrokerEnrolmentError, BrokerProofError } from '../../kernel/errors.js'\n\n// ─── Host helpers (verify side — for a reference broker host) ───\nimport { issueChallenge as issueChallengeRaw, verifyBrokerProof } from '../../kernel/enclave/index.js'\nimport type { IssuedChallenge, VerifyBrokerProofArgs } from '../../kernel/enclave/index.js'\n\n/**\n * Floor — a challenge TTL must survive network latency + clock drift\n * between host and client. The enclave's `issueChallenge` is deliberately\n * unclamped (a mechanical primitive, not a policy); this floor is host\n * POLICY, so it lives here in the party layer, not\n * `kernel/enclave/broker/proof.ts` (carried forward from Task 2's review,\n * I6b).\n */\nconst MIN_CHALLENGE_TTL_MS = 10_000\n/** Ceiling — spec §2 step 2: \"TTL ≤ 60 s\". */\nconst MAX_CHALLENGE_TTL_MS = 60_000\n\n/**\n * {@link issueChallengeRaw} (the enclave's mechanical primitive) with a\n * `[10s, 60s]` TTL clamp applied, so a reference host built on this barrel\n * gets a safe challenge TTL without having to know the bounds itself.\n */\nexport function issueChallenge(opts?: { ttlMs?: number }): IssuedChallenge {\n const requested = opts?.ttlMs ?? MAX_CHALLENGE_TTL_MS\n const clamped = Math.min(Math.max(requested, MIN_CHALLENGE_TTL_MS), MAX_CHALLENGE_TTL_MS)\n return issueChallengeRaw({ ttlMs: clamped })\n}\nexport { verifyBrokerProof }\nexport type { VerifyBrokerProofArgs }\n\n// #837 — issueChallenge's return type must be nameable from this entry.\nexport type { IssuedChallenge } from '../../kernel/enclave/index.js'\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAwBA,IAAM,kBAAkB;AAGxB,IAAM,eAAe;AAOrB,SAAS,WAAW,OAAyB,QAAwB;AACnE,QAAM,cAAc,MAAM,YAAY,KAAK,MAAM,MAAM,SAAS,IAAI,KAAK,IAAI,IAAI,SAAS;AAC1F,SAAO,KAAK,IAAI,cAAc,QAAQ,KAAK,IAAI,IAAI,YAAY;AACjE;AAEO,SAAS,WAAW,QAAsC;AAC/D,QAAM,QAAQ,oBAAI,IAAwB;AAE1C,SAAO;AAAA,IACL,MAAM,OAAO,KAAK;AAChB,YAAM,EAAE,WAAW,IAAI,MAAM,OAAO,qBAAW;AAC/C,aAAO,WAAW,EAAE,GAAG,KAAK,OAAO,CAAC;AAAA,IACtC;AAAA,IACA,MAAM,OAAO,KAAK;AAOhB,YAAM,cAAc,GAAG,IAAI,KAAK;AAChC,YAAM,SAAS,CAAC,GAAG,MAAM,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC,GAAG,MAAM,IAAI,WAAW,WAAW,CAAC;AACjF,YAAM,WAAW,OAAO,IAAI,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,MAAsC,MAAM,MAAS;AAChH,YAAM,QAAQ,WAAW,QAAQ;AACjC,iBAAW,CAAC,GAAG,KAAK,OAAQ,OAAM,OAAO,GAAG;AAC5C,YAAM,EAAE,WAAW,IAAI,MAAM,OAAO,qBAAW;AAC/C,aAAO,WAAW,EAAE,GAAG,KAAK,OAAO,CAAC;AAAA,IACtC;AAAA,IACA,iBAAiB,KAAgB,SAAyC;AACxE,YAAM,MAAM,GAAG,IAAI,KAAK,KAAK,WAAW,EAAE;AAC1C,aAAO,YAAY;AACjB,YAAI,QAAQ,MAAM,IAAI,GAAG;AACzB,YAAI,CAAC,OAAO;AACV,kBAAQ,CAAC;AACT,gBAAM,IAAI,KAAK,KAAK;AAAA,QACtB;AACA,cAAM,MAAM,KAAK,IAAI;AACrB,YAAI,MAAM,UAAU,MAAM,MAAM,OAAO,WAAY,QAAO,MAAM,OAAO;AACvE,YAAI,MAAM,SAAU,QAAO,MAAM;AAEjC,cAAM,SAAS,OAAO,UAAU;AAChC,cAAM,gBAAgB;AACtB,cAAM,YAAY,YAAY;AAC5B,gBAAM,EAAE,qBAAqB,IAAI,MAAM,OAAO,qBAAW;AACzD,gBAAM,QAAQ,MAAM,qBAAqB,EAAE,GAAG,KAAK,OAAO,GAAG,OAAO;AACpE,wBAAc,SAAS,EAAE,OAAO,YAAY,WAAW,OAAO,MAAM,EAAE;AACtE,iBAAO;AAAA,QACT,GAAG;AACH,cAAM,WAAW;AACjB,YAAI;AACF,iBAAO,MAAM;AAAA,QACf,UAAE;AAIA,wBAAc,WAAW;AAAA,QAC3B;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;;ACzDA,IAAM,uBAAuB;AAE7B,IAAM,uBAAuB;AAOtB,SAASA,gBAAe,MAA4C;AACzE,QAAM,YAAY,MAAM,SAAS;AACjC,QAAM,UAAU,KAAK,IAAI,KAAK,IAAI,WAAW,oBAAoB,GAAG,oBAAoB;AACxF,SAAO,eAAkB,EAAE,OAAO,QAAQ,CAAC;AAC7C;","names":["issueChallenge"]}
1
+ {"version":3,"sources":["../../src/with-party/broker/active.ts","../../src/with-party/broker/index.ts"],"sourcesContent":["/**\n * Enable the credential-broker capability (#479, spec §5).\n *\n * Pass to `createNoydb({ brokerStrategy: withBroker(config) })` to make\n * `vault.broker()` live. Each method dynamic-imports the seed lifecycle\n * engine (`./seed.js`) — mirroring `via/classified/active.ts`'s per-method\n * dynamic-import seam — so `with-party/broker/seed.ts` (the heavy module:\n * the CAS enrol, quiesce-then-swap rotate, and the challenge/response round\n * trip) is reachable only through the `@noy-db/hub/broker` subpath and\n * never enters the single-user floor bundle.\n *\n * The single-flight, per-vault-per-profile refresh cache (H-6/I6) lives\n * HERE, in this factory's closure — shared across every `credentialSource(profile)`\n * call for this one broker instance (in turn shared across every vault of a\n * `createNoydb()` instance — `vault.broker()` passes the SAME `BrokerStrategy`\n * for all of them), so `rotate()` can quiesce it (await any in-flight\n * round-trip, then drop it) before the seed swap. The cache key is\n * `${vault}\\0${profile}` — keyed by vault FIRST, or two vaults sharing this\n * one strategy would collide and hand vault-B a cached credential minted\n * (and STS-scoped) for vault-A.\n */\nimport type { BrokerStrategy, BrokerConfig, BrokerCtx } from '../../port/with/broker-strategy.js'\nimport type { StoreCredentials, StoreCredentialSource } from '../../kernel/types.js'\n\nconst DEFAULT_SKEW_MS = 60_000\n/** Floor under the skew-adjusted boundary so a sub-skew credential TTL\n * doesn't collapse to \"already expired\" and thrash re-proving every call (I6b). */\nconst MIN_CACHE_MS = 1_000\n\ninterface CacheEntry {\n cached?: { creds: StoreCredentials; validUntil: number } | undefined\n inFlight?: Promise<StoreCredentials> | undefined\n}\n\nfunction cacheFloor(creds: StoreCredentials, skewMs: number): number {\n const expiresAtMs = creds.expiresAt ? Date.parse(creds.expiresAt) : Date.now() + skewMs * 2\n return Math.max(expiresAtMs - skewMs, Date.now() + MIN_CACHE_MS)\n}\n\nexport function withBroker(config: BrokerConfig): BrokerStrategy {\n const cache = new Map<string, CacheEntry>()\n\n return {\n async enroll(ctx) {\n const { enrollSeed } = await import('./seed.js')\n return enrollSeed({ ...ctx, config })\n },\n async rotate(ctx) {\n // Quiesce (I5): let any in-flight round-trip finish (it may still be\n // proving under the pre-rotation seed) before the local record is\n // overwritten, then drop the cache — scoped to THIS vault's entries\n // only (`${ctx.vault}\\0`-prefixed keys). A global cache.clear() here\n // would blow away every OTHER vault's cache on this shared strategy\n // too — same root cause as the cross-vault read collision this fixes.\n const vaultPrefix = `${ctx.vault}\\0`\n const scoped = [...cache.entries()].filter(([key]) => key.startsWith(vaultPrefix))\n const inFlight = scoped.map(([, e]) => e.inFlight).filter((p): p is Promise<StoreCredentials> => p !== undefined)\n await Promise.allSettled(inFlight)\n for (const [key] of scoped) cache.delete(key)\n const { rotateSeed } = await import('./seed.js')\n return rotateSeed({ ...ctx, config })\n },\n credentialSource(ctx: BrokerCtx, profile?: string): StoreCredentialSource {\n const key = `${ctx.vault}\\0${profile ?? ''}`\n return async () => {\n let entry = cache.get(key)\n if (!entry) {\n entry = {}\n cache.set(key, entry)\n }\n const now = Date.now()\n if (entry.cached && now < entry.cached.validUntil) return entry.cached.creds\n if (entry.inFlight) return entry.inFlight\n\n const skewMs = config.skewMs ?? DEFAULT_SKEW_MS\n const capturedEntry = entry\n const inFlight = (async () => {\n const { mintStoreCredentials } = await import('./seed.js')\n const creds = await mintStoreCredentials({ ...ctx, config }, profile)\n capturedEntry.cached = { creds, validUntil: cacheFloor(creds, skewMs) }\n return creds\n })()\n entry.inFlight = inFlight\n try {\n return await inFlight\n } finally {\n // Clear-on-settle (I6a): a rejected in-flight promise must never\n // stay pinned in the cache, or every subsequent call would wedge\n // on the same rejection instead of retrying.\n capturedEntry.inFlight = undefined\n }\n }\n },\n }\n}\n","/**\n * `@noy-db/hub/broker` — subpath export for the credential-broker capability\n * (#479, credential-broker spec). Apps that never call `vault.broker()` can\n * exclude this subpath entirely: the floor default `NO_BROKER` throws, and\n * this module's real engine (`with-party/broker/{seed,active}.ts`) is only\n * ever reached through `withBroker()`'s dynamic imports, so it never enters\n * a floor bundle.\n *\n * Also re-exports the host-side helpers (`issueChallenge`, `verifyBrokerProof`)\n * from the enclave barrel, so a Lambda/Worker implementing the reference\n * broker host can import just this one subpath and get everything it\n * needs, `crypto.subtle`-only.\n */\n\n// ─── Capability opt-in seam ──────────────────────────────────\nexport { withBroker } from './active.js'\nexport {\n NO_BROKER,\n type BrokerStrategy,\n type BrokerCtx,\n type BrokerConfig,\n type CredentialBrokerHandle,\n} from '../../port/with/broker-strategy.js'\nexport { BrokerNotEnabledError, BrokerEnrolmentError, BrokerProofError } from '../../kernel/errors.js'\n\n// ─── Host helpers (verify side — for a reference broker host) ───\nimport { issueChallenge as issueChallengeRaw, verifyBrokerProof } from '../../kernel/enclave/index.js'\nimport type { IssuedChallenge, VerifyBrokerProofArgs } from '../../kernel/enclave/index.js'\n\n/**\n * Floor — a challenge TTL must survive network latency + clock drift\n * between host and client. The enclave's `issueChallenge` is deliberately\n * unclamped (a mechanical primitive, not a policy); this floor is host\n * POLICY, so it lives here in the party layer, not\n * `kernel/enclave/broker/proof.ts` (carried forward from Task 2's review,\n * I6b).\n */\nconst MIN_CHALLENGE_TTL_MS = 10_000\n/** Ceiling — spec §2 step 2: \"TTL ≤ 60 s\". */\nconst MAX_CHALLENGE_TTL_MS = 60_000\n\n/**\n * {@link issueChallengeRaw} (the enclave's mechanical primitive) with a\n * `[10s, 60s]` TTL clamp applied, so a reference host built on this barrel\n * gets a safe challenge TTL without having to know the bounds itself.\n */\nexport function issueChallenge(opts?: { ttlMs?: number }): IssuedChallenge {\n const requested = opts?.ttlMs ?? MAX_CHALLENGE_TTL_MS\n const clamped = Math.min(Math.max(requested, MIN_CHALLENGE_TTL_MS), MAX_CHALLENGE_TTL_MS)\n return issueChallengeRaw({ ttlMs: clamped })\n}\nexport { verifyBrokerProof }\nexport type { VerifyBrokerProofArgs }\n\n// #837 — issueChallenge's return type must be nameable from this entry.\nexport type { IssuedChallenge } from '../../kernel/enclave/index.js'\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAwBA,IAAM,kBAAkB;AAGxB,IAAM,eAAe;AAOrB,SAAS,WAAW,OAAyB,QAAwB;AACnE,QAAM,cAAc,MAAM,YAAY,KAAK,MAAM,MAAM,SAAS,IAAI,KAAK,IAAI,IAAI,SAAS;AAC1F,SAAO,KAAK,IAAI,cAAc,QAAQ,KAAK,IAAI,IAAI,YAAY;AACjE;AAEO,SAAS,WAAW,QAAsC;AAC/D,QAAM,QAAQ,oBAAI,IAAwB;AAE1C,SAAO;AAAA,IACL,MAAM,OAAO,KAAK;AAChB,YAAM,EAAE,WAAW,IAAI,MAAM,OAAO,qBAAW;AAC/C,aAAO,WAAW,EAAE,GAAG,KAAK,OAAO,CAAC;AAAA,IACtC;AAAA,IACA,MAAM,OAAO,KAAK;AAOhB,YAAM,cAAc,GAAG,IAAI,KAAK;AAChC,YAAM,SAAS,CAAC,GAAG,MAAM,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC,GAAG,MAAM,IAAI,WAAW,WAAW,CAAC;AACjF,YAAM,WAAW,OAAO,IAAI,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,MAAsC,MAAM,MAAS;AAChH,YAAM,QAAQ,WAAW,QAAQ;AACjC,iBAAW,CAAC,GAAG,KAAK,OAAQ,OAAM,OAAO,GAAG;AAC5C,YAAM,EAAE,WAAW,IAAI,MAAM,OAAO,qBAAW;AAC/C,aAAO,WAAW,EAAE,GAAG,KAAK,OAAO,CAAC;AAAA,IACtC;AAAA,IACA,iBAAiB,KAAgB,SAAyC;AACxE,YAAM,MAAM,GAAG,IAAI,KAAK,KAAK,WAAW,EAAE;AAC1C,aAAO,YAAY;AACjB,YAAI,QAAQ,MAAM,IAAI,GAAG;AACzB,YAAI,CAAC,OAAO;AACV,kBAAQ,CAAC;AACT,gBAAM,IAAI,KAAK,KAAK;AAAA,QACtB;AACA,cAAM,MAAM,KAAK,IAAI;AACrB,YAAI,MAAM,UAAU,MAAM,MAAM,OAAO,WAAY,QAAO,MAAM,OAAO;AACvE,YAAI,MAAM,SAAU,QAAO,MAAM;AAEjC,cAAM,SAAS,OAAO,UAAU;AAChC,cAAM,gBAAgB;AACtB,cAAM,YAAY,YAAY;AAC5B,gBAAM,EAAE,qBAAqB,IAAI,MAAM,OAAO,qBAAW;AACzD,gBAAM,QAAQ,MAAM,qBAAqB,EAAE,GAAG,KAAK,OAAO,GAAG,OAAO;AACpE,wBAAc,SAAS,EAAE,OAAO,YAAY,WAAW,OAAO,MAAM,EAAE;AACtE,iBAAO;AAAA,QACT,GAAG;AACH,cAAM,WAAW;AACjB,YAAI;AACF,iBAAO,MAAM;AAAA,QACf,UAAE;AAIA,wBAAc,WAAW;AAAA,QAC3B;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;;ACzDA,IAAM,uBAAuB;AAE7B,IAAM,uBAAuB;AAOtB,SAASA,gBAAe,MAA4C;AACzE,QAAM,YAAY,MAAM,SAAS;AACjC,QAAM,UAAU,KAAK,IAAI,KAAK,IAAI,WAAW,oBAAoB,GAAG,oBAAoB;AACxF,SAAO,eAAkB,EAAE,OAAO,QAAQ,CAAC;AAC7C;","names":["issueChallenge"]}