@noy-db/hub 0.2.0-pre.1 → 0.2.0-pre.3

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 (253) hide show
  1. package/dist/aggregate/index.cjs.map +1 -1
  2. package/dist/aggregate/index.js +2 -2
  3. package/dist/attestation/index.cjs +305 -0
  4. package/dist/attestation/index.cjs.map +1 -0
  5. package/dist/attestation/index.d.cts +52 -0
  6. package/dist/attestation/index.d.ts +52 -0
  7. package/dist/attestation/index.js +36 -0
  8. package/dist/attestation/index.js.map +1 -0
  9. package/dist/blobs/index.cjs.map +1 -1
  10. package/dist/blobs/index.d.cts +4 -3
  11. package/dist/blobs/index.d.ts +4 -3
  12. package/dist/blobs/index.js +10 -8
  13. package/dist/blobs/index.js.map +1 -1
  14. package/dist/bundle/index.cjs +17940 -129
  15. package/dist/bundle/index.cjs.map +1 -1
  16. package/dist/bundle/index.d.cts +172 -3
  17. package/dist/bundle/index.d.ts +172 -3
  18. package/dist/bundle/index.js +533 -5
  19. package/dist/bundle/index.js.map +1 -1
  20. package/dist/{chunk-CBAHB2BF.js → chunk-2EYC3WDT.js} +7 -70
  21. package/dist/chunk-2EYC3WDT.js.map +1 -0
  22. package/dist/{chunk-P7EQ2S5O.js → chunk-2XLVPKXG.js} +2 -2
  23. package/dist/chunk-4OQWR46B.js +79 -0
  24. package/dist/chunk-4OQWR46B.js.map +1 -0
  25. package/dist/{chunk-23TTQXVO.js → chunk-4UBOTYP5.js} +2 -2
  26. package/dist/chunk-4X2S7PBF.js +251 -0
  27. package/dist/chunk-4X2S7PBF.js.map +1 -0
  28. package/dist/{chunk-MKSA2V7A.js → chunk-5YHWBPOT.js} +2 -2
  29. package/dist/{chunk-DYBQG5PQ.js → chunk-6S3LLAQ5.js} +2 -2
  30. package/dist/{chunk-UA4RI7OT.js → chunk-74JEQFMT.js} +5 -5
  31. package/dist/chunk-75QDHSE4.js +59 -0
  32. package/dist/chunk-75QDHSE4.js.map +1 -0
  33. package/dist/chunk-A6SWRXUQ.js +118 -0
  34. package/dist/chunk-A6SWRXUQ.js.map +1 -0
  35. package/dist/{chunk-UZXLQCHP.js → chunk-BFI3RS42.js} +2 -2
  36. package/dist/{chunk-EGQYGYIU.js → chunk-EMEX37ZN.js} +2 -2
  37. package/dist/{chunk-PEULZC6M.js → chunk-EPK6A3WJ.js} +8 -1
  38. package/dist/chunk-EPK6A3WJ.js.map +1 -0
  39. package/dist/{chunk-VMIO4IXG.js → chunk-FBMXWVGP.js} +6 -229
  40. package/dist/chunk-FBMXWVGP.js.map +1 -0
  41. package/dist/{chunk-ZNOEIM6Y.js → chunk-FCDO7UAO.js} +2 -2
  42. package/dist/{chunk-5SCJ5UEF.js → chunk-FS7A4XNF.js} +3 -3
  43. package/dist/{chunk-YS3POABP.js → chunk-FXQYZNOW.js} +1 -1
  44. package/dist/chunk-FXQYZNOW.js.map +1 -0
  45. package/dist/{chunk-SIZWEV2Y.js → chunk-G7PAZ3TD.js} +4 -4
  46. package/dist/{chunk-SIZWEV2Y.js.map → chunk-G7PAZ3TD.js.map} +1 -1
  47. package/dist/{chunk-537VFZTR.js → chunk-GAUBWHAF.js} +4 -4
  48. package/dist/{chunk-FCXOFQAJ.js → chunk-GD3BGKAR.js} +2 -2
  49. package/dist/{chunk-DPMFBCV6.js → chunk-GDTCGIPX.js} +2 -2
  50. package/dist/{chunk-DPMFBCV6.js.map → chunk-GDTCGIPX.js.map} +1 -1
  51. package/dist/{chunk-6HPZY4ON.js → chunk-GVXBHCZ2.js} +8 -3
  52. package/dist/chunk-GVXBHCZ2.js.map +1 -0
  53. package/dist/{chunk-HB3Z2GCR.js → chunk-HGZ7DC5H.js} +2 -2
  54. package/dist/{chunk-MIQHZESA.js → chunk-IS5HWQO7.js} +5 -5
  55. package/dist/{chunk-MIQHZESA.js.map → chunk-IS5HWQO7.js.map} +1 -1
  56. package/dist/{chunk-5DWL3JBF.js → chunk-K5PVGKE4.js} +2 -2
  57. package/dist/{chunk-NIOHFJPJ.js → chunk-KMI2NBBF.js} +7 -119
  58. package/dist/chunk-KMI2NBBF.js.map +1 -0
  59. package/dist/{chunk-XGSOTWYX.js → chunk-KYKMKLJ6.js} +2 -2
  60. package/dist/chunk-LOL725S4.js +233 -0
  61. package/dist/chunk-LOL725S4.js.map +1 -0
  62. package/dist/{chunk-4TFSM22V.js → chunk-LS3JLEIB.js} +4 -4
  63. package/dist/{chunk-2AXFIYHT.js → chunk-NCO2JGKK.js} +1 -1
  64. package/dist/chunk-NCO2JGKK.js.map +1 -0
  65. package/dist/{chunk-Z72JH4KG.js → chunk-NGSPBLLE.js} +4 -34
  66. package/dist/chunk-NGSPBLLE.js.map +1 -0
  67. package/dist/{chunk-OMLIZL2P.js → chunk-NSLTPGEN.js} +2 -2
  68. package/dist/{chunk-7H6DOO3E.js → chunk-P6256WTJ.js} +211 -36
  69. package/dist/chunk-P6256WTJ.js.map +1 -0
  70. package/dist/{chunk-KESP7GOK.js → chunk-QAU5HM6Q.js} +3 -3
  71. package/dist/{chunk-34YSDCDP.js → chunk-SAVQ6E2O.js} +2 -2
  72. package/dist/chunk-T6HQMVML.js +9960 -0
  73. package/dist/chunk-T6HQMVML.js.map +1 -0
  74. package/dist/{chunk-PA6R5ZCI.js → chunk-TLFUDXVV.js} +4 -4
  75. package/dist/{chunk-WCA2NROQ.js → chunk-UOF74WQY.js} +2 -2
  76. package/dist/chunk-UVPGJXVO.js +83 -0
  77. package/dist/chunk-UVPGJXVO.js.map +1 -0
  78. package/dist/{chunk-DYECX3IX.js → chunk-WRLHNG6H.js} +2 -2
  79. package/dist/{chunk-ADQ5MQ54.js → chunk-YDLAFP36.js} +71 -1
  80. package/dist/chunk-YDLAFP36.js.map +1 -0
  81. package/dist/{chunk-I6MX32UC.js → chunk-YK72A4IT.js} +4 -4
  82. package/dist/chunk-YL2DR3HY.js +36 -0
  83. package/dist/chunk-YL2DR3HY.js.map +1 -0
  84. package/dist/{chunk-RD5LYKD6.js → chunk-ZC2AAE6J.js} +2 -2
  85. package/dist/chunk-ZUMGGHRB.js +57 -0
  86. package/dist/chunk-ZUMGGHRB.js.map +1 -0
  87. package/dist/consent/index.cjs.map +1 -1
  88. package/dist/consent/index.d.cts +4 -3
  89. package/dist/consent/index.d.ts +4 -3
  90. package/dist/consent/index.js +3 -3
  91. package/dist/{crypto-A7FRXYHC.js → crypto-H2Y3DDFW.js} +3 -3
  92. package/dist/{delegation-YBA4X4JN.js → delegation-QSC7G5QC.js} +5 -5
  93. package/dist/derivations/index.cjs.map +1 -1
  94. package/dist/derivations/index.d.cts +5 -4
  95. package/dist/derivations/index.d.ts +5 -4
  96. package/dist/derivations/index.js +4 -4
  97. package/dist/{dev-unlock-D9s-loPr.d.ts → dev-unlock-Cf2B7Kih.d.ts} +1 -1
  98. package/dist/{dev-unlock-DRwVSy2S.d.cts → dev-unlock-De3mjQWv.d.cts} +1 -1
  99. package/dist/executor-BZKFZVRC.js +8 -0
  100. package/dist/executor-GFZFDQXV.js +8 -0
  101. package/dist/executor-KT2IOZVP.js +11 -0
  102. package/dist/{fanout-sidecar-VJ52RIEY.js → fanout-sidecar-NRBWSLRK.js} +2 -2
  103. package/dist/guards/index.cjs +7 -0
  104. package/dist/guards/index.cjs.map +1 -1
  105. package/dist/guards/index.d.cts +5 -4
  106. package/dist/guards/index.d.ts +5 -4
  107. package/dist/guards/index.js +4 -4
  108. package/dist/{hash-DXXXusyk.d.ts → hash-gVn_uKhp.d.ts} +1 -1
  109. package/dist/{hash-DtRih9MQ.d.cts → hash-vBCB0-Ps.d.cts} +1 -1
  110. package/dist/history/index.cjs +2 -2
  111. package/dist/history/index.cjs.map +1 -1
  112. package/dist/history/index.d.cts +5 -4
  113. package/dist/history/index.d.ts +5 -4
  114. package/dist/history/index.js +6 -6
  115. package/dist/i18n/index.cjs.map +1 -1
  116. package/dist/i18n/index.d.cts +4 -3
  117. package/dist/i18n/index.d.ts +4 -3
  118. package/dist/i18n/index.js +14 -12
  119. package/dist/i18n/index.js.map +1 -1
  120. package/dist/{index-CNwA-B6-.d.ts → index-BF1B2HB9.d.ts} +53 -1
  121. package/dist/{index-CmVgTkqk.d.cts → index-DVkvrgpm.d.cts} +53 -1
  122. package/dist/index.cjs +1780 -64
  123. package/dist/index.cjs.map +1 -1
  124. package/dist/index.d.cts +34 -12
  125. package/dist/index.d.ts +34 -12
  126. package/dist/index.js +160 -8804
  127. package/dist/index.js.map +1 -1
  128. package/dist/indexing/index.cjs.map +1 -1
  129. package/dist/indexing/index.js +2 -2
  130. package/dist/issue-BAJ7ZB4S.js +12 -0
  131. package/dist/{ledger-3TXNP47J.js → ledger-WOEJUYTP.js} +6 -6
  132. package/dist/materialized-views/index.cjs.map +1 -1
  133. package/dist/materialized-views/index.d.cts +6 -5
  134. package/dist/materialized-views/index.d.ts +6 -5
  135. package/dist/materialized-views/index.js +6 -6
  136. package/dist/noydb-XNQSKXGO.js +34 -0
  137. package/dist/overlay-views/index.cjs.map +1 -1
  138. package/dist/overlay-views/index.d.cts +5 -4
  139. package/dist/overlay-views/index.d.ts +5 -4
  140. package/dist/overlay-views/index.js +6 -4
  141. package/dist/periods/index.cjs.map +1 -1
  142. package/dist/periods/index.d.cts +4 -3
  143. package/dist/periods/index.d.ts +4 -3
  144. package/dist/periods/index.js +6 -6
  145. package/dist/{public-envelope-PY6NKFLI.js → public-envelope-OHQ5UZFM.js} +4 -4
  146. package/dist/query/index.cjs.map +1 -1
  147. package/dist/query/index.d.cts +1 -1
  148. package/dist/query/index.d.ts +1 -1
  149. package/dist/query/index.js +3 -3
  150. package/dist/registry-2IEARCGT.js +7 -0
  151. package/dist/{registry-3L3N3PTG.js → registry-CDHASH73.js} +3 -3
  152. package/dist/registry-EMGLZGR6.js +8 -0
  153. package/dist/registry-NQALYR77.js +8 -0
  154. package/dist/registry-NQALYR77.js.map +1 -0
  155. package/dist/revoke-7JOVLZFD.js +17 -0
  156. package/dist/revoke-7JOVLZFD.js.map +1 -0
  157. package/dist/session/index.cjs.map +1 -1
  158. package/dist/session/index.d.cts +5 -4
  159. package/dist/session/index.d.ts +5 -4
  160. package/dist/session/index.js +3 -3
  161. package/dist/shadow/index.cjs.map +1 -1
  162. package/dist/shadow/index.d.cts +4 -3
  163. package/dist/shadow/index.d.ts +4 -3
  164. package/dist/shadow/index.js +2 -2
  165. package/dist/signer-M4K5HBLD.js +18 -0
  166. package/dist/signer-M4K5HBLD.js.map +1 -0
  167. package/dist/{stale-HSC5YO2O.js → stale-PAGCS4K5.js} +2 -2
  168. package/dist/stale-PAGCS4K5.js.map +1 -0
  169. package/dist/store/index.cjs.map +1 -1
  170. package/dist/store/index.d.cts +4 -3
  171. package/dist/store/index.d.ts +4 -3
  172. package/dist/store/index.js +2 -2
  173. package/dist/sync/index.cjs.map +1 -1
  174. package/dist/sync/index.d.cts +3 -2
  175. package/dist/sync/index.d.ts +3 -2
  176. package/dist/sync/index.js +4 -4
  177. package/dist/team/index.cjs.map +1 -1
  178. package/dist/team/index.d.cts +4 -3
  179. package/dist/team/index.d.ts +4 -3
  180. package/dist/team/index.js +13 -11
  181. package/dist/tx/index.cjs +81 -1
  182. package/dist/tx/index.cjs.map +1 -1
  183. package/dist/tx/index.d.cts +5 -4
  184. package/dist/tx/index.d.ts +5 -4
  185. package/dist/tx/index.js +56 -3
  186. package/dist/tx/index.js.map +1 -1
  187. package/dist/{types-C4lwMKKF.d.cts → types-CSLcfytP.d.cts} +644 -5
  188. package/dist/{types-DW9RGSSs.d.ts → types-D9eB0Rvh.d.ts} +644 -5
  189. package/dist/{index-4agOpzqd.d.ts → ulid-CG2YvAbg.d.cts} +51 -33
  190. package/dist/{index-hdFvZkBP.d.cts → ulid-CiM2OAeM.d.ts} +51 -33
  191. package/dist/util/index.cjs.map +1 -1
  192. package/dist/util/index.js +1 -1
  193. package/dist/{with-derivation-g-pGoMzL.d.ts → with-derivation-Bzpj6UTv.d.ts} +1 -1
  194. package/dist/{with-derivation-C8LDlV7t.d.cts → with-derivation-DWajFh4K.d.cts} +1 -1
  195. package/dist/{with-guard-jI1x9Z3k.d.cts → with-guard-DF_Ul3DT.d.cts} +1 -1
  196. package/dist/{with-guard-DWOCK4Ca.d.ts → with-guard-DR7U-l4v.d.ts} +1 -1
  197. package/dist/{with-materialized-view-DcTx4H3j.d.cts → with-materialized-view-_piodoIz.d.cts} +1 -1
  198. package/dist/{with-materialized-view-DaKR-N6J.d.ts → with-materialized-view-qtoJ3xKJ.d.ts} +1 -1
  199. package/dist/{with-overlayed-view-N7jYuNOS.d.ts → with-overlayed-view-DFaRfgMr.d.ts} +1 -1
  200. package/dist/{with-overlayed-view-D-6oWAgM.d.cts → with-overlayed-view-DwzCKxn2.d.cts} +1 -1
  201. package/package.json +15 -3
  202. package/dist/chunk-2AXFIYHT.js.map +0 -1
  203. package/dist/chunk-6HPZY4ON.js.map +0 -1
  204. package/dist/chunk-7H6DOO3E.js.map +0 -1
  205. package/dist/chunk-ADQ5MQ54.js.map +0 -1
  206. package/dist/chunk-CBAHB2BF.js.map +0 -1
  207. package/dist/chunk-NIOHFJPJ.js.map +0 -1
  208. package/dist/chunk-PEULZC6M.js.map +0 -1
  209. package/dist/chunk-VMIO4IXG.js.map +0 -1
  210. package/dist/chunk-YS3POABP.js.map +0 -1
  211. package/dist/chunk-Z72JH4KG.js.map +0 -1
  212. package/dist/executor-7E3VFGW7.js +0 -11
  213. package/dist/executor-CEWX2FQI.js +0 -8
  214. package/dist/executor-X4SQ3ZLC.js +0 -8
  215. package/dist/registry-O47PUPSY.js +0 -8
  216. package/dist/registry-RFGGMVNJ.js +0 -7
  217. package/dist/registry-WLLMODKN.js +0 -8
  218. /package/dist/{chunk-P7EQ2S5O.js.map → chunk-2XLVPKXG.js.map} +0 -0
  219. /package/dist/{chunk-23TTQXVO.js.map → chunk-4UBOTYP5.js.map} +0 -0
  220. /package/dist/{chunk-MKSA2V7A.js.map → chunk-5YHWBPOT.js.map} +0 -0
  221. /package/dist/{chunk-DYBQG5PQ.js.map → chunk-6S3LLAQ5.js.map} +0 -0
  222. /package/dist/{chunk-UA4RI7OT.js.map → chunk-74JEQFMT.js.map} +0 -0
  223. /package/dist/{chunk-UZXLQCHP.js.map → chunk-BFI3RS42.js.map} +0 -0
  224. /package/dist/{chunk-EGQYGYIU.js.map → chunk-EMEX37ZN.js.map} +0 -0
  225. /package/dist/{chunk-ZNOEIM6Y.js.map → chunk-FCDO7UAO.js.map} +0 -0
  226. /package/dist/{chunk-5SCJ5UEF.js.map → chunk-FS7A4XNF.js.map} +0 -0
  227. /package/dist/{chunk-537VFZTR.js.map → chunk-GAUBWHAF.js.map} +0 -0
  228. /package/dist/{chunk-FCXOFQAJ.js.map → chunk-GD3BGKAR.js.map} +0 -0
  229. /package/dist/{chunk-HB3Z2GCR.js.map → chunk-HGZ7DC5H.js.map} +0 -0
  230. /package/dist/{chunk-5DWL3JBF.js.map → chunk-K5PVGKE4.js.map} +0 -0
  231. /package/dist/{chunk-XGSOTWYX.js.map → chunk-KYKMKLJ6.js.map} +0 -0
  232. /package/dist/{chunk-4TFSM22V.js.map → chunk-LS3JLEIB.js.map} +0 -0
  233. /package/dist/{chunk-OMLIZL2P.js.map → chunk-NSLTPGEN.js.map} +0 -0
  234. /package/dist/{chunk-KESP7GOK.js.map → chunk-QAU5HM6Q.js.map} +0 -0
  235. /package/dist/{chunk-34YSDCDP.js.map → chunk-SAVQ6E2O.js.map} +0 -0
  236. /package/dist/{chunk-PA6R5ZCI.js.map → chunk-TLFUDXVV.js.map} +0 -0
  237. /package/dist/{chunk-WCA2NROQ.js.map → chunk-UOF74WQY.js.map} +0 -0
  238. /package/dist/{chunk-DYECX3IX.js.map → chunk-WRLHNG6H.js.map} +0 -0
  239. /package/dist/{chunk-I6MX32UC.js.map → chunk-YK72A4IT.js.map} +0 -0
  240. /package/dist/{chunk-RD5LYKD6.js.map → chunk-ZC2AAE6J.js.map} +0 -0
  241. /package/dist/{crypto-A7FRXYHC.js.map → crypto-H2Y3DDFW.js.map} +0 -0
  242. /package/dist/{delegation-YBA4X4JN.js.map → delegation-QSC7G5QC.js.map} +0 -0
  243. /package/dist/{executor-7E3VFGW7.js.map → executor-BZKFZVRC.js.map} +0 -0
  244. /package/dist/{executor-CEWX2FQI.js.map → executor-GFZFDQXV.js.map} +0 -0
  245. /package/dist/{executor-X4SQ3ZLC.js.map → executor-KT2IOZVP.js.map} +0 -0
  246. /package/dist/{fanout-sidecar-VJ52RIEY.js.map → fanout-sidecar-NRBWSLRK.js.map} +0 -0
  247. /package/dist/{ledger-3TXNP47J.js.map → issue-BAJ7ZB4S.js.map} +0 -0
  248. /package/dist/{public-envelope-PY6NKFLI.js.map → ledger-WOEJUYTP.js.map} +0 -0
  249. /package/dist/{registry-3L3N3PTG.js.map → noydb-XNQSKXGO.js.map} +0 -0
  250. /package/dist/{registry-O47PUPSY.js.map → public-envelope-OHQ5UZFM.js.map} +0 -0
  251. /package/dist/{registry-RFGGMVNJ.js.map → registry-2IEARCGT.js.map} +0 -0
  252. /package/dist/{registry-WLLMODKN.js.map → registry-CDHASH73.js.map} +0 -0
  253. /package/dist/{stale-HSC5YO2O.js.map → registry-EMGLZGR6.js.map} +0 -0
@@ -0,0 +1,233 @@
1
+ import {
2
+ NOYDB_FORMAT_VERSION
3
+ } from "./chunk-FXQYZNOW.js";
4
+ import {
5
+ encrypt
6
+ } from "./chunk-UOF74WQY.js";
7
+
8
+ // src/blobs/export-blobs.ts
9
+ var ExportBlobsAbortedError = class extends Error {
10
+ constructor(reason) {
11
+ super(`exportBlobs aborted: ${reason}`);
12
+ this.name = "ExportBlobsAbortedError";
13
+ }
14
+ };
15
+ var EXPORT_AUDIT_COLLECTION = "_export_audit";
16
+ function createExportBlobsHandle(actor, listAccessibleCollections, getCollection, writeAudit, options) {
17
+ let aborted = false;
18
+ const abort = () => {
19
+ aborted = true;
20
+ };
21
+ if (options.signal) {
22
+ if (options.signal.aborted) aborted = true;
23
+ options.signal.addEventListener("abort", () => {
24
+ aborted = true;
25
+ });
26
+ }
27
+ function assertLive() {
28
+ if (aborted) throw new ExportBlobsAbortedError("aborted by caller");
29
+ }
30
+ const allowlist = options.collections ? new Set(options.collections) : null;
31
+ let auditPromise = null;
32
+ function writeAuditOnce() {
33
+ if (!auditPromise) {
34
+ auditPromise = writeAudit({
35
+ id: generateBatchId(),
36
+ mechanism: "exportBlobs",
37
+ actor,
38
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
39
+ collections: options.collections ?? null,
40
+ predicate: Boolean(options.where),
41
+ afterBlobId: options.afterBlobId ?? null
42
+ });
43
+ }
44
+ return auditPromise;
45
+ }
46
+ async function* generate() {
47
+ await writeAuditOnce();
48
+ assertLive();
49
+ const allCollections = await listAccessibleCollections();
50
+ const targets = allCollections.filter((name) => {
51
+ if (name.startsWith("_")) return false;
52
+ if (allowlist && !allowlist.has(name)) return false;
53
+ return true;
54
+ });
55
+ let resumeCursorHit = options.afterBlobId === void 0;
56
+ for (const collectionName of targets) {
57
+ if (aborted) return;
58
+ const coll = getCollection(collectionName);
59
+ const records = await coll.list().catch(() => []);
60
+ for (const record of records) {
61
+ if (aborted) return;
62
+ assertLive();
63
+ const idField = record.id;
64
+ if (typeof idField !== "string") continue;
65
+ if (options.where && !options.where(record, { collection: collectionName, id: idField })) continue;
66
+ const blobSet = coll.blob(idField);
67
+ const slots = await blobSet.list().catch(() => []);
68
+ for (const slot of slots) {
69
+ if (aborted) return;
70
+ if (!resumeCursorHit) {
71
+ if (slot.eTag === options.afterBlobId) {
72
+ resumeCursorHit = true;
73
+ }
74
+ continue;
75
+ }
76
+ const bytes = await blobSet.get(slot.name);
77
+ if (!bytes) continue;
78
+ const item = {
79
+ blobId: slot.eTag,
80
+ recordRef: { collection: collectionName, id: idField, slot: slot.name },
81
+ bytes,
82
+ meta: {
83
+ size: slot.size,
84
+ filename: slot.filename,
85
+ ...slot.mimeType !== void 0 && { mimeType: slot.mimeType },
86
+ ...slot.uploadedAt !== void 0 && { createdAt: slot.uploadedAt }
87
+ }
88
+ };
89
+ yield item;
90
+ }
91
+ }
92
+ }
93
+ }
94
+ const handle = {
95
+ abort,
96
+ get aborted() {
97
+ return aborted;
98
+ },
99
+ [Symbol.asyncIterator]: () => generate()
100
+ };
101
+ return handle;
102
+ }
103
+ function generateBatchId() {
104
+ const raw = globalThis.crypto.getRandomValues(new Uint8Array(16));
105
+ let s = "";
106
+ for (const b of raw) s += b.toString(16).padStart(2, "0");
107
+ return `batch-${Date.now().toString(36)}-${s.slice(0, 12)}`;
108
+ }
109
+
110
+ // src/blobs/blob-compaction.ts
111
+ var BLOB_EVICTION_AUDIT_COLLECTION = "_blob_eviction_audit";
112
+ async function runCompaction(ctx, options = {}) {
113
+ const now = options.now ?? /* @__PURE__ */ new Date();
114
+ const maxEvictions = options.maxEvictions ?? Infinity;
115
+ const dryRun = options.dryRun === true;
116
+ const allCollections = await ctx.listCollections();
117
+ const byCollection = {};
118
+ let evicted = 0;
119
+ let records = 0;
120
+ let auditEntries = 0;
121
+ let collectionsWithPolicy = 0;
122
+ outer: for (const collectionName of allCollections) {
123
+ if (collectionName.startsWith("_")) continue;
124
+ const config = ctx.getBlobFields(collectionName);
125
+ if (!config) continue;
126
+ const configuredSlots = Object.keys(config);
127
+ if (configuredSlots.length === 0) continue;
128
+ collectionsWithPolicy += 1;
129
+ byCollection[collectionName] = { records: 0, evicted: 0 };
130
+ const ids = await ctx.listRecords(collectionName);
131
+ for (const recordId of ids) {
132
+ if (evicted >= maxEvictions) break outer;
133
+ const record = await ctx.getRecord(collectionName, recordId).catch(() => null);
134
+ if (record === null) continue;
135
+ records += 1;
136
+ byCollection[collectionName].records += 1;
137
+ const slots = await ctx.listSlots(collectionName, recordId).catch(() => []);
138
+ for (const slot of slots) {
139
+ if (evicted >= maxEvictions) break outer;
140
+ const policy = config[slot.name];
141
+ if (!policy) continue;
142
+ const reason = evaluatePolicy(policy, record, slot, now);
143
+ if (!reason) continue;
144
+ if (!dryRun) {
145
+ await ctx.deleteSlot(collectionName, recordId, slot.name);
146
+ await writeAuditEntry(ctx, {
147
+ id: generateEvictionId(collectionName, recordId, slot.name),
148
+ collection: collectionName,
149
+ recordId,
150
+ slotName: slot.name,
151
+ blobHash: slot.eTag,
152
+ reason,
153
+ evictedAt: now.toISOString(),
154
+ actor: ctx.actor
155
+ });
156
+ auditEntries += 1;
157
+ }
158
+ evicted += 1;
159
+ byCollection[collectionName].evicted += 1;
160
+ }
161
+ }
162
+ }
163
+ return {
164
+ evicted,
165
+ records,
166
+ collections: collectionsWithPolicy,
167
+ auditEntries,
168
+ byCollection
169
+ };
170
+ }
171
+ function evaluatePolicy(policy, record, slot, now) {
172
+ let ttlTriggered = false;
173
+ let predicateTriggered = false;
174
+ if (policy.retainDays !== void 0 && policy.retainDays > 0) {
175
+ const uploadedAt = Date.parse(slot.uploadedAt);
176
+ if (Number.isFinite(uploadedAt)) {
177
+ const ageMs = now.getTime() - uploadedAt;
178
+ const limitMs = policy.retainDays * 864e5;
179
+ if (ageMs > limitMs) ttlTriggered = true;
180
+ }
181
+ }
182
+ if (policy.evictWhen) {
183
+ try {
184
+ if (policy.evictWhen(record)) predicateTriggered = true;
185
+ } catch {
186
+ }
187
+ }
188
+ if (ttlTriggered && predicateTriggered) return "both";
189
+ if (ttlTriggered) return "ttl";
190
+ if (predicateTriggered) return "predicate";
191
+ return null;
192
+ }
193
+ function generateEvictionId(collection, recordId, slotName) {
194
+ const rand = globalThis.crypto.getRandomValues(new Uint8Array(8));
195
+ let suffix = "";
196
+ for (const b of rand) suffix += b.toString(16).padStart(2, "0");
197
+ return `${collection}__${recordId}__${slotName}__${suffix}`;
198
+ }
199
+ async function writeAuditEntry(ctx, entry) {
200
+ const json = JSON.stringify(entry);
201
+ let envelope;
202
+ if (ctx.encrypted) {
203
+ const dek = await ctx.getDEK(BLOB_EVICTION_AUDIT_COLLECTION);
204
+ const { iv, data } = await encrypt(json, dek);
205
+ envelope = {
206
+ _noydb: NOYDB_FORMAT_VERSION,
207
+ _v: 1,
208
+ _ts: entry.evictedAt,
209
+ _iv: iv,
210
+ _data: data,
211
+ _by: entry.actor
212
+ };
213
+ } else {
214
+ envelope = {
215
+ _noydb: NOYDB_FORMAT_VERSION,
216
+ _v: 1,
217
+ _ts: entry.evictedAt,
218
+ _iv: "",
219
+ _data: json,
220
+ _by: entry.actor
221
+ };
222
+ }
223
+ await ctx.adapter.put(ctx.vault, BLOB_EVICTION_AUDIT_COLLECTION, entry.id, envelope);
224
+ }
225
+
226
+ export {
227
+ ExportBlobsAbortedError,
228
+ EXPORT_AUDIT_COLLECTION,
229
+ createExportBlobsHandle,
230
+ BLOB_EVICTION_AUDIT_COLLECTION,
231
+ runCompaction
232
+ };
233
+ //# sourceMappingURL=chunk-LOL725S4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/blobs/export-blobs.ts","../src/blobs/blob-compaction.ts"],"sourcesContent":["/**\n * `vault.exportBlobs()` — bulk blob extraction primitive.\n *\n * Async-iterable handle over every blob attached to records in a\n * vault, optionally filtered by collection allowlist and per-record\n * predicate. Emits tuples of `{ blobId, recordRef, bytes, meta }` so\n * the consumer can pipe into any sink (zip stream, S3 multipart, USB\n * copy, cold-storage tape) without pulling the whole export into\n * memory.\n *\n * ## Auth + audit\n *\n * - Capability check runs **once** at handle creation via\n * `Vault.assertCanExport('plaintext', 'blob')`. An operator whose\n * keyring lacks that bit fails before a single byte of ciphertext\n * is decrypted.\n * - Audit entry lands in `_export_audit` at handle creation: the\n * actor, start timestamp, target collections, predicate presence,\n * and batch mechanism. **No content hashes** — per the spec\n * non-correlation invariant.\n *\n * ## Abort + resume\n *\n * - `handle.abort()` flips the internal signal; the next iteration\n * boundary throws `AbortError`. Consumers already in `for await`\n * can catch and exit cleanly.\n * - Restart after a partial failure with `{ afterBlobId }` — the\n * iterator skips tuples up to (and including) that blob id before\n * yielding again. Combined with a blob-count ceiling it supports\n * idempotent batch re-runs.\n *\n * @module\n */\n\nimport type { Collection } from '../collection.js'\nimport type { SlotInfo } from '../types.js'\n\n// ─── Types ──────────────────────────────────────────────────────────────\n\nexport interface ExportBlobsOptions {\n /**\n * Collection allowlist. Omit to export blobs from every collection\n * the caller has read access to.\n */\n readonly collections?: readonly string[]\n /**\n * Per-record predicate. Called on the decrypted record BEFORE any\n * blob bytes are read for that record — returning false skips the\n * record and all its slots without touching their chunks.\n */\n readonly where?: (record: unknown, context: { collection: string; id: string }) => boolean\n /**\n * Resume after a specific blob id. The iterator skips tuples up to\n * and including this id, then yields. Format of the id is the same\n * as `ExportedBlob.blobId` (the HMAC-keyed eTag).\n */\n readonly afterBlobId?: string\n /**\n * External abort signal. When fired, the next iterator tick throws\n * `ExportBlobsAbortedError`. Honored alongside `handle.abort()`.\n */\n readonly signal?: AbortSignal\n}\n\nexport interface ExportedBlob {\n /** Opaque blob identifier — HMAC-keyed eTag, stable across vaults. */\n readonly blobId: string\n /** Where this blob came from in the vault. */\n readonly recordRef: {\n readonly collection: string\n readonly id: string\n readonly slot: string\n }\n /** Decrypted plaintext bytes. */\n readonly bytes: Uint8Array\n /** Best-effort metadata (from the blob slot record). */\n readonly meta: {\n readonly size: number\n /**\n * User-visible filename stored on the slot. Often equal to the\n * slot name; differs when the caller supplied an explicit\n * `filename` to `BlobSet.put()`.\n */\n readonly filename: string\n readonly mimeType?: string\n readonly createdAt?: string\n }\n}\n\nexport interface ExportBlobsHandle extends AsyncIterable<ExportedBlob> {\n /** Abort the export. Safe to call multiple times. */\n abort(): void\n /** True once `abort()` has fired or the external signal aborted. */\n readonly aborted: boolean\n}\n\nexport class ExportBlobsAbortedError extends Error {\n constructor(reason: string) {\n super(`exportBlobs aborted: ${reason}`)\n this.name = 'ExportBlobsAbortedError'\n }\n}\n\n// ─── Audit ──────────────────────────────────────────────────────────────\n\nexport const EXPORT_AUDIT_COLLECTION = '_export_audit'\n\nexport interface ExportBlobsAuditEntry {\n readonly id: string\n readonly mechanism: 'exportBlobs'\n readonly actor: string\n readonly startedAt: string\n readonly collections: readonly string[] | null\n readonly predicate: boolean\n readonly afterBlobId: string | null\n}\n\n// ─── Implementation ─────────────────────────────────────────────────────\n\n/**\n * Build the handle. Factored out of `Vault.exportBlobs` so the\n * implementation can be unit-tested without going through the\n * compartment lifecycle.\n */\nexport function createExportBlobsHandle(\n actor: string,\n listAccessibleCollections: () => Promise<string[]>,\n getCollection: <T>(name: string) => Collection<T>,\n writeAudit: (entry: ExportBlobsAuditEntry) => Promise<void>,\n options: ExportBlobsOptions,\n): ExportBlobsHandle {\n let aborted = false\n\n const abort = (): void => {\n aborted = true\n }\n\n if (options.signal) {\n if (options.signal.aborted) aborted = true\n options.signal.addEventListener('abort', () => { aborted = true })\n }\n\n function assertLive(): void {\n if (aborted) throw new ExportBlobsAbortedError('aborted by caller')\n }\n\n const allowlist = options.collections ? new Set(options.collections) : null\n\n // Write the audit entry BEFORE the first yield so a blocked\n // iteration still leaves an audit trail that the export started.\n let auditPromise: Promise<void> | null = null\n function writeAuditOnce(): Promise<void> {\n if (!auditPromise) {\n auditPromise = writeAudit({\n id: generateBatchId(),\n mechanism: 'exportBlobs',\n actor,\n startedAt: new Date().toISOString(),\n collections: options.collections ?? null,\n predicate: Boolean(options.where),\n afterBlobId: options.afterBlobId ?? null,\n })\n }\n return auditPromise\n }\n\n async function* generate(): AsyncGenerator<ExportedBlob> {\n await writeAuditOnce()\n assertLive()\n\n // Resolve target collections lazily — also keeps the call async.\n const allCollections = await listAccessibleCollections()\n const targets = allCollections.filter(name => {\n if (name.startsWith('_')) return false\n if (allowlist && !allowlist.has(name)) return false\n return true\n })\n\n let resumeCursorHit = options.afterBlobId === undefined\n\n for (const collectionName of targets) {\n if (aborted) return\n\n const coll = getCollection<Record<string, unknown>>(collectionName)\n const records = await coll.list().catch(() => [])\n for (const record of records) {\n if (aborted) return\n assertLive()\n\n const idField = (record as { id?: unknown }).id\n if (typeof idField !== 'string') continue\n\n if (options.where && !options.where(record, { collection: collectionName, id: idField })) continue\n\n const blobSet = coll.blob(idField)\n const slots = await blobSet.list().catch(() => [] as SlotInfo[])\n for (const slot of slots) {\n if (aborted) return\n\n if (!resumeCursorHit) {\n if (slot.eTag === options.afterBlobId) {\n resumeCursorHit = true\n }\n continue\n }\n\n const bytes = await blobSet.get(slot.name)\n if (!bytes) continue\n\n const item: ExportedBlob = {\n blobId: slot.eTag,\n recordRef: { collection: collectionName, id: idField, slot: slot.name },\n bytes,\n meta: {\n size: slot.size,\n filename: slot.filename,\n ...(slot.mimeType !== undefined && { mimeType: slot.mimeType }),\n ...(slot.uploadedAt !== undefined && { createdAt: slot.uploadedAt }),\n },\n }\n yield item\n }\n }\n }\n }\n\n const handle: ExportBlobsHandle = {\n abort,\n get aborted() { return aborted },\n [Symbol.asyncIterator]: () => generate(),\n }\n return handle\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────\n\nfunction generateBatchId(): string {\n // 16 bytes of crypto randomness, URL-safe base64, no padding.\n const raw = globalThis.crypto.getRandomValues(new Uint8Array(16))\n let s = ''\n for (const b of raw) s += b.toString(16).padStart(2, '0')\n return `batch-${Date.now().toString(36)}-${s.slice(0, 12)}`\n}\n","/**\n * Blob retention + compaction.\n *\n * Declarative per-collection / per-slot eviction policy. Two\n * triggers:\n *\n * - **`retainDays`** — age-based TTL. A slot uploaded more than N\n * days ago is evicted.\n * - **`evictWhen(record)`** — predicate over the **decrypted**\n * record. Lets consumers express \"the image is safe to drop once\n * the structured invoice has been reviewed and confirmed.\"\n *\n * Either trigger (or both) causes the slot to evict. Eviction removes\n * the slot entry from `_blob_slots_{collection}`, decrements the\n * blob's refCount (so unreferenced chunks can be GC'd by the next\n * sweep), and writes one entry to the `_blob_eviction_audit`\n * collection for tamper-evident record-keeping.\n *\n * The audit entry carries the eTag of the evicted blob (opaque HMAC\n * of plaintext under the vault's `_blob` DEK) — no plaintext leakage,\n * per the SPEC non-correlation invariant. Consumers reconstructing\n * \"what used to be attached\" can look up the audit entry by record\n * id.\n *\n * Compaction is **consumer-scheduled** — noy-db never runs a\n * background daemon. Call `vault.compact()` whenever your workflow\n * allows (cron, manual \"tidy\" button, cold-storage export prep, …).\n *\n * @module\n */\n\nimport type { NoydbStore, EncryptedEnvelope, SlotInfo } from '../types.js'\nimport { NOYDB_FORMAT_VERSION } from '../types.js'\nimport { encrypt } from '../crypto.js'\n\n// ─── Config types ───────────────────────────────────────────────────────\n\nexport interface BlobFieldPolicy<T = unknown> {\n /**\n * Age-based TTL in days. A slot whose `uploadedAt` is older than\n * `now - retainDays × 86400s` evicts on the next `vault.compact()`.\n * Omit to disable age-based eviction.\n */\n readonly retainDays?: number\n /**\n * Predicate evaluated against the decrypted record. When it returns\n * `true`, every matching slot on that record evicts. Omit to\n * disable predicate-based eviction.\n */\n readonly evictWhen?: (record: T) => boolean\n}\n\nexport type BlobFieldsConfig<T = unknown> = Record<string, BlobFieldPolicy<T>>\n\n// ─── Audit collection ──────────────────────────────────────────────────\n\nexport const BLOB_EVICTION_AUDIT_COLLECTION = '_blob_eviction_audit'\n\nexport interface BlobEvictionEntry {\n readonly id: string\n readonly collection: string\n readonly recordId: string\n readonly slotName: string\n readonly blobHash: string\n readonly reason: 'ttl' | 'predicate' | 'both'\n readonly evictedAt: string\n readonly actor: string\n}\n\n// ─── Compaction result ──────────────────────────────────────────────────\n\nexport interface CompactionResult {\n /** Number of blob slots evicted across all collections. */\n readonly evicted: number\n /** Number of records touched (iterated + policy checked). */\n readonly records: number\n /** Number of collections with `blobFields` configured. */\n readonly collections: number\n /** Number of audit entries written. Equal to `evicted`. */\n readonly auditEntries: number\n /** Per-collection breakdown for diagnostics. */\n readonly byCollection: Record<string, { records: number; evicted: number }>\n}\n\n// ─── Core ──────────────────────────────────────────────────────────────\n\nexport interface CompactRunOptions {\n /** Override \"now\" for deterministic testing. */\n readonly now?: Date\n /**\n * Stop after this many evictions. Useful for capped batches / cron\n * jobs that need to fit in a time window. `undefined` = unbounded.\n */\n readonly maxEvictions?: number\n /**\n * Dry-run — evaluate policies and return the counts, but do NOT\n * delete slots or write audit entries. Lets a consumer preview\n * what would happen.\n */\n readonly dryRun?: boolean\n}\n\nexport interface CompactionContext {\n readonly adapter: NoydbStore\n readonly vault: string\n readonly actor: string\n readonly encrypted: boolean\n readonly getDEK: (collection: string) => Promise<CryptoKey>\n /**\n * Resolve a collection's declared `blobFields` config. Returns an\n * empty map for collections without the config — the walk skips\n * those.\n */\n readonly getBlobFields: <T>(collection: string) => BlobFieldsConfig<T> | null\n /** List collection names in the vault. */\n readonly listCollections: () => Promise<string[]>\n /** List record ids in a collection. */\n readonly listRecords: (collection: string) => Promise<string[]>\n /** Decrypt and return the record. Null when absent. */\n readonly getRecord: <T>(collection: string, id: string) => Promise<T | null>\n /** Return the BlobSet-like handle for a record's slots. */\n readonly listSlots: (collection: string, id: string) => Promise<SlotInfo[]>\n /** Delete a slot and decrement its blob's refCount. */\n readonly deleteSlot: (collection: string, id: string, slotName: string) => Promise<void>\n}\n\nexport async function runCompaction(\n ctx: CompactionContext,\n options: CompactRunOptions = {},\n): Promise<CompactionResult> {\n const now = options.now ?? new Date()\n const maxEvictions = options.maxEvictions ?? Infinity\n const dryRun = options.dryRun === true\n\n const allCollections = await ctx.listCollections()\n const byCollection: Record<string, { records: number; evicted: number }> = {}\n let evicted = 0\n let records = 0\n let auditEntries = 0\n let collectionsWithPolicy = 0\n\n outer: for (const collectionName of allCollections) {\n if (collectionName.startsWith('_')) continue\n const config = ctx.getBlobFields(collectionName)\n if (!config) continue\n const configuredSlots = Object.keys(config)\n if (configuredSlots.length === 0) continue\n collectionsWithPolicy += 1\n byCollection[collectionName] = { records: 0, evicted: 0 }\n\n const ids = await ctx.listRecords(collectionName)\n for (const recordId of ids) {\n if (evicted >= maxEvictions) break outer\n\n const record = await ctx.getRecord(collectionName, recordId).catch(() => null)\n if (record === null) continue\n records += 1\n byCollection[collectionName].records += 1\n\n const slots = await ctx.listSlots(collectionName, recordId).catch(() => [])\n for (const slot of slots) {\n if (evicted >= maxEvictions) break outer\n const policy = config[slot.name]\n if (!policy) continue\n\n const reason = evaluatePolicy(policy, record, slot, now)\n if (!reason) continue\n\n if (!dryRun) {\n await ctx.deleteSlot(collectionName, recordId, slot.name)\n await writeAuditEntry(ctx, {\n id: generateEvictionId(collectionName, recordId, slot.name),\n collection: collectionName,\n recordId,\n slotName: slot.name,\n blobHash: slot.eTag,\n reason,\n evictedAt: now.toISOString(),\n actor: ctx.actor,\n })\n auditEntries += 1\n }\n evicted += 1\n byCollection[collectionName].evicted += 1\n }\n }\n }\n\n return {\n evicted,\n records,\n collections: collectionsWithPolicy,\n auditEntries,\n byCollection,\n }\n}\n\nfunction evaluatePolicy<T>(\n policy: BlobFieldPolicy<T>,\n record: T,\n slot: SlotInfo,\n now: Date,\n): 'ttl' | 'predicate' | 'both' | null {\n let ttlTriggered = false\n let predicateTriggered = false\n\n if (policy.retainDays !== undefined && policy.retainDays > 0) {\n const uploadedAt = Date.parse(slot.uploadedAt)\n if (Number.isFinite(uploadedAt)) {\n const ageMs = now.getTime() - uploadedAt\n const limitMs = policy.retainDays * 86_400_000\n if (ageMs > limitMs) ttlTriggered = true\n }\n }\n\n if (policy.evictWhen) {\n try {\n if (policy.evictWhen(record)) predicateTriggered = true\n } catch {\n // Predicate error → do NOT evict. Fail closed.\n }\n }\n\n if (ttlTriggered && predicateTriggered) return 'both'\n if (ttlTriggered) return 'ttl'\n if (predicateTriggered) return 'predicate'\n return null\n}\n\nfunction generateEvictionId(collection: string, recordId: string, slotName: string): string {\n const rand = globalThis.crypto.getRandomValues(new Uint8Array(8))\n let suffix = ''\n for (const b of rand) suffix += b.toString(16).padStart(2, '0')\n return `${collection}__${recordId}__${slotName}__${suffix}`\n}\n\nasync function writeAuditEntry(ctx: CompactionContext, entry: BlobEvictionEntry): Promise<void> {\n const json = JSON.stringify(entry)\n let envelope: EncryptedEnvelope\n if (ctx.encrypted) {\n const dek = await ctx.getDEK(BLOB_EVICTION_AUDIT_COLLECTION)\n const { iv, data } = await encrypt(json, dek)\n envelope = {\n _noydb: NOYDB_FORMAT_VERSION,\n _v: 1,\n _ts: entry.evictedAt,\n _iv: iv,\n _data: data,\n _by: entry.actor,\n }\n } else {\n envelope = {\n _noydb: NOYDB_FORMAT_VERSION,\n _v: 1,\n _ts: entry.evictedAt,\n _iv: '',\n _data: json,\n _by: entry.actor,\n }\n }\n await ctx.adapter.put(ctx.vault, BLOB_EVICTION_AUDIT_COLLECTION, entry.id, envelope)\n}\n"],"mappings":"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","names":[]}
@@ -3,17 +3,17 @@ import {
3
3
  } from "./chunk-2QR2PQTT.js";
4
4
  import {
5
5
  NOYDB_SYNC_VERSION
6
- } from "./chunk-YS3POABP.js";
6
+ } from "./chunk-FXQYZNOW.js";
7
7
  import {
8
8
  bufferToBase64,
9
9
  decrypt,
10
10
  derivePresenceKey,
11
11
  encrypt,
12
12
  generateIV
13
- } from "./chunk-WCA2NROQ.js";
13
+ } from "./chunk-UOF74WQY.js";
14
14
  import {
15
15
  ConflictError
16
- } from "./chunk-ADQ5MQ54.js";
16
+ } from "./chunk-YDLAFP36.js";
17
17
 
18
18
  // src/team/presence.ts
19
19
  var PresenceHandle = class {
@@ -719,4 +719,4 @@ export {
719
719
  SyncEngine,
720
720
  SyncTransaction
721
721
  };
722
- //# sourceMappingURL=chunk-4TFSM22V.js.map
722
+ //# sourceMappingURL=chunk-LS3JLEIB.js.map
@@ -69,4 +69,4 @@ export {
69
69
  parseIndex,
70
70
  envelopePayloadHash
71
71
  };
72
- //# sourceMappingURL=chunk-2AXFIYHT.js.map
72
+ //# sourceMappingURL=chunk-NCO2JGKK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/history/ledger/entry.ts","../src/history/ledger/hash.ts"],"sourcesContent":["/**\n * Ledger entry shape + canonical JSON + sha256 helpers.\n *\n * This file holds the PURE primitives used by the hash-chained ledger:\n * the entry type, the deterministic (sort-stable) JSON encoder, and\n * the sha256 hasher that produces `prevHash` and `ledger.head()`.\n *\n * Everything here is validator-free and side-effect free — the only\n * runtime dep is Web Crypto's `subtle.digest` for the sha256 call,\n * which we already use for every other hashing operation in the core.\n *\n * The hash chain property works like this:\n *\n * hash(entry[i]) = sha256(canonicalJSON(entry[i]))\n * entry[i+1].prevHash = hash(entry[i])\n *\n * Any modification to `entry[i]` (field values, field order, whitespace)\n * produces a different `hash(entry[i])`, which means `entry[i+1]`'s\n * stored `prevHash` no longer matches the recomputed hash, which means\n * `verify()` returns `{ ok: false, divergedAt: i + 1 }`. The chain is\n * append-only and tamper-evident without external anchoring.\n */\n\n/**\n * A single ledger entry in its plaintext form — what gets serialized,\n * hashed, and then encrypted with the ledger DEK before being written\n * to the `_ledger/` adapter collection.\n *\n * ## Why hash the ciphertext, not the plaintext?\n *\n * `payloadHash` is the sha256 of the record's ENCRYPTED envelope bytes,\n * not its plaintext. This matters:\n *\n * 1. **Zero-knowledge preserved.** A user (or a third party) can\n * verify the ledger against the stored envelopes without any\n * decryption keys. The adapter layer already holds only\n * ciphertext, so hashing the ciphertext keeps the ledger at the\n * same privacy level as the adapter.\n *\n * 2. **Determinism.** Plaintext → ciphertext is randomized by the\n * fresh per-write IV, so `hash(plaintext)` would need extra\n * normalization. `hash(ciphertext)` is already deterministic and\n * unique per write.\n *\n * 3. **Detection property.** If an attacker modifies even one byte of\n * the stored ciphertext (trying to flip a record), the hash\n * changes, the ledger's recorded `payloadHash` no longer matches,\n * and a data-integrity check fails. We don't do that check in\n * `verify()` today, but the\n * hook is there for a future `verifyIntegrity()` follow-up.\n *\n * Fields marked `op`, `collection`, `id`, `version`, `ts`, `actor` are\n * plaintext METADATA about the operation — NOT the record itself. The\n * entry is still encrypted at rest via the ledger DEK, but adapters\n * could theoretically infer operation patterns from the sizes and\n * timestamps. This is an accepted trade-off for the tamper-evidence\n * property; full ORAM-level privacy is out of scope for noy-db.\n */\nexport interface LedgerEntry {\n /**\n * Zero-based sequential position of this entry in the chain. The\n * canonical adapter key is this number zero-padded to 10 digits\n * (`\"0000000001\"`) so lexicographic ordering matches numeric order.\n */\n readonly index: number\n\n /**\n * Hex-encoded sha256 of the canonical JSON of the PREVIOUS entry.\n * The genesis entry (index 0) has `prevHash === ''` — the first\n * entry in a fresh vault has nothing to point back to.\n */\n readonly prevHash: string\n\n /**\n * Which kind of mutation this entry records. only supports\n * data operations (`put`, `delete`, `amendment`). Access-control\n * operations (`grant`, `revoke`, `rotate`) will be added in a\n * follow-up once the keyring write path is instrumented — that's\n * tracked in the epic issue.\n *\n * `'amendment'` is the multi-record audit entry written by the\n * guards subsystem when an admin/owner uses `withTransactions(...)`\n * to repair a constraint-violating state. See `amendment` field\n * below for the structured payload.\n *\n * `'lifecycle'` records a non-data audit event (e.g. partition\n * handover, #226) — `collection`/`id` are empty and the event detail\n * lives in `reason` (e.g. `'partition-handed-over:<sealId>'`). Like\n * `amendment`, it carries no data envelope, so `verifyBackupIntegrity`\n * skips it in the data cross-check (it still participates in the\n * tamper-evident chain).\n */\n readonly op: 'put' | 'delete' | 'amendment' | 'lifecycle' | 'migration'\n\n /** The collection the mutation targeted. */\n readonly collection: string\n\n /** The record id the mutation targeted. */\n readonly id: string\n\n /**\n * The record version AFTER this mutation. For `put` this is the\n * newly assigned version; for `delete` this is the version that\n * was deleted (the last version visible to reads).\n */\n readonly version: number\n\n /** ISO timestamp of the mutation. */\n readonly ts: string\n\n /** User id of the actor who performed the mutation. */\n readonly actor: string\n\n /**\n * Hex-encoded sha256 of the encrypted envelope's `_data` field.\n * For `put`, this is the hash of the new ciphertext. For `delete`,\n * it's the hash of the last visible ciphertext at deletion time,\n * or the empty string if nothing was there to delete. Hashing the\n * ciphertext (not the plaintext) preserves zero-knowledge — see\n * the file docstring.\n */\n readonly payloadHash: string\n\n /**\n * Optional human-readable tag describing why this mutation happened\n * (#1). Threaded through `collection.put(_, _, { reason })`. Common\n * values include `'import:csv'`, `'import:json'`, `'import:xlsx'` from\n * `as-*` ImportPlan.apply(), but consumers can use any string for\n * domain-specific audit filtering. Auto-strip via `canonicalJson` —\n * absent on the wire, never serialized as `null`.\n *\n * Audit consumers filter: `entries.filter(e => e.reason?.startsWith('import:'))`.\n */\n readonly reason?: string\n\n /**\n * Optional hex-encoded sha256 of the encrypted JSON Patch delta\n * blob stored alongside this entry in `_ledger_deltas/`. Present\n * only for `put` operations that had a previous version — the\n * genesis put of a new record, and every `delete`, leave this\n * field undefined.\n *\n * The delta payload itself lives in a sibling internal collection\n * (`_ledger_deltas/<paddedIndex>`) and is encrypted with the\n * ledger DEK. Callers use `ledger.loadDelta(index)` to decrypt and\n * deserialize it when reconstructing a historical version.\n *\n * Why optional instead of always-present: the first put of a\n * record has no previous version to diff against, so storing an\n * empty patch would be noise. For deletes there's no \"next\" state\n * to describe with a delta. Both cases set this field to undefined.\n *\n * Note: the canonical-JSON hasher treats `undefined` as invalid\n * (it's one of the guard rails), so on the wire this field is\n * either `{ deltaHash: '<hex>' }` or absent from the JSON\n * entirely — never `{ deltaHash: undefined }`.\n */\n readonly deltaHash?: string\n\n /**\n * Present only when `op === 'amendment'`. Records the human reason,\n * the role of the actor, the (collection, id, vBefore, vAfter) tuple\n * for every record touched, and which guard invariants passed.\n *\n * See docs/superpowers/specs/2026-05-18-guards-design.md.\n */\n readonly amendment?: {\n readonly reason: string\n readonly role: 'admin' | 'owner'\n readonly changes: ReadonlyArray<{\n readonly collection: string\n readonly id: string\n readonly vBefore: number\n readonly vAfter: number\n }>\n readonly invariantsPassed: ReadonlyArray<string>\n }\n}\n\n/**\n * Canonical (sort-stable) JSON encoder.\n *\n * This function is the load-bearing primitive of the hash chain:\n * `sha256(canonicalJSON(entry))` must produce the same hex string\n * every time, on every machine, for the same logical entry — otherwise\n * `verify()` would return `{ ok: false }` on cross-platform reads.\n *\n * JavaScript's `JSON.stringify` is almost canonical, but NOT quite:\n * it preserves the insertion order of object keys, which means\n * `{a:1,b:2}` and `{b:2,a:1}` serialize differently. We fix this by\n * recursively walking objects and sorting their keys before\n * concatenation.\n *\n * Arrays keep their original order (reordering them would change\n * semantics). Numbers, strings, booleans, and `null` use the default\n * JSON encoding. `undefined` and functions are rejected — ledger\n * entries are plain data, and silently dropping `undefined` would\n * break the \"same input → same hash\" property if a caller forgot to\n * omit a field.\n *\n * Performance: one pass per nesting level; O(n log n) for key sorting\n * at each object. Entries are small (< 1 KB) so this is negligible\n * compared to the sha256 call.\n */\nexport function canonicalJson(value: unknown): string {\n if (value === null) return 'null'\n if (typeof value === 'boolean') return value ? 'true' : 'false'\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) {\n throw new Error(\n `canonicalJson: refusing to encode non-finite number ${String(value)}`,\n )\n }\n return JSON.stringify(value)\n }\n if (typeof value === 'string') return JSON.stringify(value)\n if (typeof value === 'bigint') {\n throw new Error('canonicalJson: BigInt is not JSON-serializable')\n }\n if (typeof value === 'undefined' || typeof value === 'function') {\n throw new Error(\n `canonicalJson: refusing to encode ${typeof value} — include all fields explicitly`,\n )\n }\n if (Array.isArray(value)) {\n return '[' + value.map((v) => canonicalJson(v)).join(',') + ']'\n }\n if (typeof value === 'object') {\n const obj = value as Record<string, unknown>\n const keys = Object.keys(obj).sort()\n const parts: string[] = []\n for (const key of keys) {\n parts.push(JSON.stringify(key) + ':' + canonicalJson(obj[key]))\n }\n return '{' + parts.join(',') + '}'\n }\n throw new Error(`canonicalJson: unexpected value type: ${typeof value}`)\n}\n\n/**\n * Compute a hex-encoded sha256 of a string via Web Crypto's subtle API.\n *\n * We use hex (not base64) for hashes because hex is case-insensitive,\n * fixed-length (64 chars), and easier to compare visually in debug\n * output. Base64 would save a few bytes in storage but every encrypted\n * ledger entry is already much larger than the hash itself.\n */\nexport async function sha256Hex(input: string): Promise<string> {\n const bytes = new TextEncoder().encode(input)\n const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes)\n return bytesToHex(new Uint8Array(digest))\n}\n\n/**\n * Compute the canonical hash of a ledger entry. Short wrapper around\n * `canonicalJson` + `sha256Hex`; callers use this instead of composing\n * the two functions every time, so any future change to the hashing\n * pipeline (e.g., adding a domain-separation prefix) lives in one place.\n */\nexport async function hashEntry(entry: LedgerEntry): Promise<string> {\n return sha256Hex(canonicalJson(entry))\n}\n\n/** Convert a Uint8Array to a lowercase hex string. */\nfunction bytesToHex(bytes: Uint8Array): string {\n const hex = new Array<string>(bytes.length)\n for (let i = 0; i < bytes.length; i++) {\n // Non-null assertion: indexing a Uint8Array within bounds always\n // returns a number, but the compiler's noUncheckedIndexedAccess\n // flag widens it to `number | undefined`. Safe here by construction.\n hex[i] = (bytes[i] ?? 0).toString(16).padStart(2, '0')\n }\n return hex.join('')\n}\n\n/**\n * Pad an index to the canonical 10-digit form used as the adapter key.\n * Ten digits is enough for ~10 billion ledger entries per vault\n * — far beyond any realistic use case, but cheap enough that the extra\n * digits don't hurt storage.\n */\nexport function paddedIndex(index: number): string {\n return String(index).padStart(10, '0')\n}\n\n/** Parse a padded adapter key back into a number. Returns NaN on malformed input. */\nexport function parseIndex(key: string): number {\n return Number.parseInt(key, 10)\n}\n","/**\n * Envelope payload hash — pinned in its own leaf module so consumers\n * (DictionaryHandle, the active history strategy) can import it\n * without dragging in the `LedgerStore` class.\n *\n * see `constants.ts` for the broader rationale.\n *\n * @internal\n */\n\nimport type { EncryptedEnvelope } from '../../types.js'\nimport { sha256Hex } from './entry.js'\n\n/**\n * Compute the `payloadHash` value for an encrypted envelope. Used by\n * `LedgerStore.append` for both put (hash the new envelope) and\n * delete (hash the previous envelope) paths, and by\n * `DictionaryHandle` so its ledger entries match the same contract.\n *\n * Returns the empty string when there is no envelope (delete of a\n * never-existed record). The empty string tolerated by the ledger\n * entry's `payloadHash` field as the canonical \"nothing here\" value.\n */\nexport async function envelopePayloadHash(\n envelope: EncryptedEnvelope | null,\n): Promise<string> {\n if (!envelope) return ''\n // `_data` is a base64 string for encrypted envelopes and the raw\n // JSON for plaintext ones. Both are strings, so a single sha256Hex\n // call works for both modes — the hash value differs between\n // encrypted/plaintext compartments because the bytes on disk\n // differ.\n return sha256Hex(envelope._data)\n}\n"],"mappings":";AA4MO,SAAS,cAAc,OAAwB;AACpD,MAAI,UAAU,KAAM,QAAO;AAC3B,MAAI,OAAO,UAAU,UAAW,QAAO,QAAQ,SAAS;AACxD,MAAI,OAAO,UAAU,UAAU;AAC7B,QAAI,CAAC,OAAO,SAAS,KAAK,GAAG;AAC3B,YAAM,IAAI;AAAA,QACR,uDAAuD,OAAO,KAAK,CAAC;AAAA,MACtE;AAAA,IACF;AACA,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B;AACA,MAAI,OAAO,UAAU,SAAU,QAAO,KAAK,UAAU,KAAK;AAC1D,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,IAAI,MAAM,gDAAgD;AAAA,EAClE;AACA,MAAI,OAAO,UAAU,eAAe,OAAO,UAAU,YAAY;AAC/D,UAAM,IAAI;AAAA,MACR,qCAAqC,OAAO,KAAK;AAAA,IACnD;AAAA,EACF;AACA,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,MAAM,IAAI,CAAC,MAAM,cAAc,CAAC,CAAC,EAAE,KAAK,GAAG,IAAI;AAAA,EAC9D;AACA,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,MAAM;AACZ,UAAM,OAAO,OAAO,KAAK,GAAG,EAAE,KAAK;AACnC,UAAM,QAAkB,CAAC;AACzB,eAAW,OAAO,MAAM;AACtB,YAAM,KAAK,KAAK,UAAU,GAAG,IAAI,MAAM,cAAc,IAAI,GAAG,CAAC,CAAC;AAAA,IAChE;AACA,WAAO,MAAM,MAAM,KAAK,GAAG,IAAI;AAAA,EACjC;AACA,QAAM,IAAI,MAAM,yCAAyC,OAAO,KAAK,EAAE;AACzE;AAUA,eAAsB,UAAU,OAAgC;AAC9D,QAAM,QAAQ,IAAI,YAAY,EAAE,OAAO,KAAK;AAC5C,QAAM,SAAS,MAAM,WAAW,OAAO,OAAO,OAAO,WAAW,KAAK;AACrE,SAAO,WAAW,IAAI,WAAW,MAAM,CAAC;AAC1C;AAQA,eAAsB,UAAU,OAAqC;AACnE,SAAO,UAAU,cAAc,KAAK,CAAC;AACvC;AAGA,SAAS,WAAW,OAA2B;AAC7C,QAAM,MAAM,IAAI,MAAc,MAAM,MAAM;AAC1C,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AAIrC,QAAI,CAAC,KAAK,MAAM,CAAC,KAAK,GAAG,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAAA,EACvD;AACA,SAAO,IAAI,KAAK,EAAE;AACpB;AAQO,SAAS,YAAY,OAAuB;AACjD,SAAO,OAAO,KAAK,EAAE,SAAS,IAAI,GAAG;AACvC;AAGO,SAAS,WAAW,KAAqB;AAC9C,SAAO,OAAO,SAAS,KAAK,EAAE;AAChC;;;ACzQA,eAAsB,oBACpB,UACiB;AACjB,MAAI,CAAC,SAAU,QAAO;AAMtB,SAAO,UAAU,SAAS,KAAK;AACjC;","names":[]}
@@ -1,35 +1,6 @@
1
1
  import {
2
- OverlayIdMismatchError,
3
- ValidationError
4
- } from "./chunk-ADQ5MQ54.js";
5
-
6
- // src/overlay-views/with-overlayed-view.ts
7
- function withOverlayedView(spec) {
8
- if (!spec.name || spec.name.length === 0) {
9
- throw new ValidationError("withOverlayedView: name is required");
10
- }
11
- if (!spec.base || spec.base.length === 0) {
12
- throw new ValidationError("withOverlayedView: base is required");
13
- }
14
- if (!spec.overlay || spec.overlay.length === 0) {
15
- throw new ValidationError("withOverlayedView: overlay is required");
16
- }
17
- if (spec.base === spec.overlay) {
18
- throw new ValidationError("withOverlayedView: base and overlay must be different collections");
19
- }
20
- if (spec.base === spec.name || spec.overlay === spec.name) {
21
- throw new ValidationError(
22
- "withOverlayedView: virtual name must differ from both base and overlay collection names"
23
- );
24
- }
25
- if (!spec.shadowField || spec.shadowField.length === 0) {
26
- throw new ValidationError("withOverlayedView: shadowField is required");
27
- }
28
- return {
29
- __noydb_strategy: "overlayed-view",
30
- spec
31
- };
32
- }
2
+ OverlayIdMismatchError
3
+ } from "./chunk-YDLAFP36.js";
33
4
 
34
5
  // src/overlay-views/virtual-collection.ts
35
6
  var OverlayedCollection = class {
@@ -203,7 +174,6 @@ var OverlayedCollection = class {
203
174
  };
204
175
 
205
176
  export {
206
- OverlayedCollection,
207
- withOverlayedView
177
+ OverlayedCollection
208
178
  };
209
- //# sourceMappingURL=chunk-Z72JH4KG.js.map
179
+ //# sourceMappingURL=chunk-NGSPBLLE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/overlay-views/virtual-collection.ts"],"sourcesContent":["import { OverlayIdMismatchError } from '../errors.js'\nimport type { Collection } from '../collection.js'\nimport type { OverlayedViewStrategy } from './types.js'\n\n/**\n * Virtual-collection proxy returned by `vault.collection(overlayName)`\n * when `overlayName` is a registered `withOverlayedView` (#154).\n *\n * Implements the core `Collection<T>`-shaped read/write surface with\n * merge-on-read semantics:\n * - `get(id)`: overlay row wins iff `overlay[shadowField] === shadowValue`\n * - `list()` / `.query()`: union of ids, per-id merge applied\n * - `put(record)` / `put(id, record)`: routes to overlay; id derived\n * via the base MV's `rowKey` (validated on the two-arg form)\n * - `delete(id)`: removes the overlay row only; base stays\n *\n * Reactive APIs (`live`, `subscribe`, `query().live()`) are out of\n * scope for #154 and surface as \"not yet implemented\" — wired in a\n * future sub-issue.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport class OverlayedCollection<T extends Record<string, unknown> = any> {\n constructor(\n private readonly spec: OverlayedViewStrategy,\n private readonly baseCollection: Collection<T>,\n private readonly overlayCollection: Collection<T>,\n private readonly baseRowKey: ((row: Record<string, unknown>) => string) | undefined,\n ) {}\n\n /**\n * Convenience accessors for advanced callers that need to bypass the\n * virtual layer (bulk imports, direct overlay queries). Mirrors the\n * spec's \"direct writes to the underlying overlay collection skip\n * the validation\" escape hatch.\n */\n readonly overlay = {\n rowKey: (row: Record<string, unknown>): string => {\n if (!this.baseRowKey) {\n throw new Error(\n `Overlay \"${this.spec.name}\": base \"${this.spec.base}\" is not an MV — ` +\n `cannot auto-derive id from the row. Use \\`put(id, record)\\` instead.`,\n )\n }\n return this.baseRowKey(row)\n },\n }\n\n /** Get the merged row by id. */\n async get(id: string): Promise<T | null> {\n const overlayRow = await this.overlayCollection.get(id)\n if (overlayRow !== null && this.shadowPredicateApplies(overlayRow)) {\n return overlayRow\n }\n const baseRow = await this.baseCollection.get(id)\n if (baseRow !== null) return baseRow\n // No base row — but if an overlay row exists with the shadow\n // predicate true, we returned it above. If overlay exists but\n // predicate is false, return null (overlay exists but doesn't\n // qualify, and there's no base to fall back to) — per spec\n // operations table row \"overlay exists, predicate false, no base\".\n return null\n }\n\n /** List union of base + overlay ids, applying the merge per row. */\n async list(): Promise<T[]> {\n const baseRows = await this.baseCollection.list()\n const overlayRows = await this.overlayCollection.list()\n // Build id → merged row, base-first then overlay applies shadow rule.\n const merged = new Map<string, T>()\n const idOf = (row: T): string => {\n // Best-effort: use baseRowKey if available, else assume the row\n // has a `.id` field (common pattern). The spec requires every\n // base MV to declare `rowKey`, so the first branch is the\n // canonical path.\n if (this.baseRowKey) return this.baseRowKey(row as Record<string, unknown>)\n const idField = (row as Record<string, unknown>).id\n return typeof idField === 'string' ? idField : ''\n }\n for (const row of baseRows) {\n const id = idOf(row)\n if (id) merged.set(id, row)\n }\n for (const row of overlayRows) {\n const id = idOf(row)\n if (!id) continue\n if (this.shadowPredicateApplies(row)) {\n merged.set(id, row) // overlay shadow wins\n } else if (!merged.has(id)) {\n // Overlay-only + predicate false + no base → don't surface\n // (matches spec operations table)\n continue\n }\n // else: overlay exists but predicate is false and base is\n // present → keep the base row already in `merged`\n }\n return [...merged.values()]\n }\n\n /**\n * Write to the overlay. Two forms:\n * - `put(record)`: id is derived via the base MV's `rowKey(record)`.\n * Throws if the base isn't an MV.\n * - `put(id, record)`: validates `id === rowKey(record)`; throws\n * `OverlayIdMismatchError` on mismatch.\n */\n async put(idOrRecord: string | T, maybeRecord?: T): Promise<void> {\n let id: string\n let record: T\n if (maybeRecord === undefined) {\n // Single-arg form: put(record). Derive id via base rowKey.\n record = idOrRecord as T\n if (!this.baseRowKey) {\n throw new Error(\n `Overlay \"${this.spec.name}\".put(record): base \"${this.spec.base}\" is not an MV. ` +\n `Use put(id, record) explicitly.`,\n )\n }\n id = this.baseRowKey(record as Record<string, unknown>)\n } else {\n // Two-arg form: put(id, record). Validate against rowKey.\n id = idOrRecord as string\n record = maybeRecord\n if (this.baseRowKey) {\n const expected = this.baseRowKey(record as Record<string, unknown>)\n if (id !== expected) {\n throw new OverlayIdMismatchError(id, expected)\n }\n }\n }\n await this.overlayCollection.put(id, record)\n }\n\n /**\n * Remove the overlay row only. Idempotent (no-op on absent).\n * The base row is untouched — if a base row exists for `id`,\n * subsequent reads return it.\n */\n async delete(id: string): Promise<void> {\n await this.overlayCollection.delete(id)\n }\n\n /** True when `overlay[shadowField] === shadowValue`. */\n private shadowPredicateApplies(row: T): boolean {\n return (row as Record<string, unknown>)[this.spec.shadowField] === this.spec.shadowValue\n }\n\n // ─── Throw-stubs for the unimplemented Collection<T> surface ───────\n //\n // `Vault.collection(name)` widens the return type to `Collection<T>`\n // for the overlay intercept, but `OverlayedCollection` doesn't\n // implement the full surface. These stubs catch the common\n // reactive / chainable APIs with a clear \"not yet implemented\"\n // error pointing at the relevant issue — so consumers don't hit a\n // cryptic `undefined is not a function` runtime crash.\n //\n // Closes niwat-review of PR #160.\n\n /** @throws — chainable Query<T> over a virtual collection is deferred. */\n query(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".query() is not yet implemented for overlay views (#154). ` +\n `Use \\`list()\\` + filter for now, or read from the underlying \\`${this.spec.base}\\` / \\`${this.spec.overlay}\\` collections directly. ` +\n `Reactive APIs land in a future MV sub-issue.`,\n )\n }\n\n /** @throws — change-stream subscription over a virtual collection is deferred. */\n subscribe(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".subscribe() is not yet implemented for overlay views (#154). ` +\n `Subscribe to the underlying \\`${this.spec.base}\\` / \\`${this.spec.overlay}\\` collections individually for now. ` +\n `Merged change-stream lands in a future MV sub-issue.`,\n )\n }\n\n /** @throws — live query over a virtual collection is deferred. */\n live(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".live() is not yet implemented for overlay views (#154). ` +\n `Reactive APIs land in a future MV sub-issue.`,\n )\n }\n\n /** @throws — async iteration over a virtual collection is deferred. */\n scan(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".scan() is not yet implemented for overlay views (#154). ` +\n `Use \\`list()\\` for now (no row-count ceiling at niwat scale), or scan the underlying collections directly.`,\n )\n }\n\n /** @throws — lazy-mode query is not applicable to virtual collections. */\n lazyQuery(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".lazyQuery() is not supported. ` +\n `Virtual collections always materialize through base + overlay reads — lazy-mode indexed lookups don't apply.`,\n )\n }\n\n /** @throws — bulk-atomic put is deferred to a future MV sub-issue. */\n putManyAtomic(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".putManyAtomic() is not yet implemented for overlay views (#154). ` +\n `Use sequential \\`.put(record)\\` calls for now, or write to \\`${this.spec.overlay}\\` directly.`,\n )\n }\n\n /** @throws — bulk delete is deferred to a future MV sub-issue. */\n deleteMany(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".deleteMany() is not yet implemented for overlay views (#154). ` +\n `Use sequential \\`.delete(id)\\` calls for now, or operate on \\`${this.spec.overlay}\\` directly.`,\n )\n }\n\n /** @throws — `.first()` over a virtual collection is deferred. */\n first(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".first() is not yet implemented for overlay views (#154). ` +\n `Use \\`(await list())[0]\\` for now.`,\n )\n }\n\n /** @throws — `.count()` over a virtual collection is deferred. */\n count(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".count() is not yet implemented for overlay views (#154). ` +\n `Use \\`(await list()).length\\` for now.`,\n )\n }\n}\n"],"mappings":"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","names":[]}
@@ -2,7 +2,7 @@ import {
2
2
  OverlayBaseIsVirtualError,
3
3
  OverlayCollectionUnavailableError,
4
4
  OverlayNameCollisionError
5
- } from "./chunk-ADQ5MQ54.js";
5
+ } from "./chunk-YDLAFP36.js";
6
6
 
7
7
  // src/overlay-views/registry.ts
8
8
  var OverlayedViewRegistry = class {
@@ -58,4 +58,4 @@ var OverlayedViewRegistry = class {
58
58
  export {
59
59
  OverlayedViewRegistry
60
60
  };
61
- //# sourceMappingURL=chunk-OMLIZL2P.js.map
61
+ //# sourceMappingURL=chunk-NSLTPGEN.js.map