@noy-db/hub 0.3.0-pre.9 → 0.3.0

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 (958) hide show
  1. package/dist/aggregate/index.js +5 -5
  2. package/dist/api-ZGS6LCOF.js +20 -0
  3. package/dist/as/index.js +11 -11
  4. package/dist/attestation/index.js +18 -18
  5. package/dist/backup-BJ57IKAO.js +242 -0
  6. package/dist/backup-BJ57IKAO.js.map +1 -0
  7. package/dist/blobs/index.js +13 -11
  8. package/dist/blobs/index.js.map +1 -1
  9. package/dist/broker/index.js +92 -0
  10. package/dist/broker/index.js.map +1 -0
  11. package/dist/bundle/index.js +25 -22
  12. package/dist/bundle/index.js.map +1 -1
  13. package/dist/by/index.js +2 -2
  14. package/dist/cargo/index.js +31 -32
  15. package/dist/chunk-2NVOWNI3.js +167 -0
  16. package/dist/chunk-2NVOWNI3.js.map +1 -0
  17. package/dist/chunk-3B5VVCNY.js +71 -0
  18. package/dist/chunk-3LQ2YKO4.js +230 -0
  19. package/dist/chunk-3QCRBHCY.js +54 -0
  20. package/dist/chunk-454JXE57.js +1697 -0
  21. package/dist/chunk-454JXE57.js.map +1 -0
  22. package/dist/chunk-4CTHCN4J.js +480 -0
  23. package/dist/chunk-4CTHCN4J.js.map +1 -0
  24. package/dist/chunk-4GPE63GU.js +111 -0
  25. package/dist/chunk-4GPE63GU.js.map +1 -0
  26. package/dist/chunk-4JBYV75E.js +150 -0
  27. package/dist/chunk-4NXKCLLQ.js +97 -0
  28. package/dist/chunk-4VNQOLML.js +29 -0
  29. package/dist/chunk-4VNQOLML.js.map +1 -0
  30. package/dist/chunk-5M5DVGV7.js +69 -0
  31. package/dist/chunk-5O3XF4DV.js +150 -0
  32. package/dist/chunk-5O3XF4DV.js.map +1 -0
  33. package/dist/chunk-5YOLBEQ6.js +30 -0
  34. package/dist/chunk-6CRIV7DE.js +129 -0
  35. package/dist/chunk-6JTIAGHD.js +621 -0
  36. package/dist/chunk-6JTIAGHD.js.map +1 -0
  37. package/dist/chunk-6QTNZJBS.js +255 -0
  38. package/dist/chunk-6T2SCQRU.js +86 -0
  39. package/dist/chunk-7DSTRQIJ.js +45 -0
  40. package/dist/chunk-AUSTCW2X.js +589 -0
  41. package/dist/chunk-B3HJYUOF.js +209 -0
  42. package/dist/chunk-BDVFPG6Y.js +164 -0
  43. package/dist/chunk-BIOUI4HL.js +1554 -0
  44. package/dist/chunk-BIOUI4HL.js.map +1 -0
  45. package/dist/chunk-CAF6WOK7.js +36 -0
  46. package/dist/chunk-CDUQTA3Z.js +381 -0
  47. package/dist/chunk-CDUQTA3Z.js.map +1 -0
  48. package/dist/chunk-CIE2BASH.js +404 -0
  49. package/dist/chunk-CRI4OUV4.js +42 -0
  50. package/dist/chunk-DLG74CNB.js +91 -0
  51. package/dist/chunk-E5ZQT5KX.js +30 -0
  52. package/dist/chunk-E5ZQT5KX.js.map +1 -0
  53. package/dist/chunk-EQO2UVP4.js +247 -0
  54. package/dist/chunk-EQO2UVP4.js.map +1 -0
  55. package/dist/chunk-ERJ7FU55.js +367 -0
  56. package/dist/chunk-EVVGJDRQ.js +205 -0
  57. package/dist/chunk-EVX47VBM.js +74 -0
  58. package/dist/chunk-EVX47VBM.js.map +1 -0
  59. package/dist/chunk-EXK7QRWF.js +59 -0
  60. package/dist/chunk-EYZ6IQ6X.js +202 -0
  61. package/dist/chunk-F64IRYG3.js +45 -0
  62. package/dist/chunk-FHS2HQSH.js +823 -0
  63. package/dist/chunk-FM3G3NOY.js +206 -0
  64. package/dist/chunk-FM3G3NOY.js.map +1 -0
  65. package/dist/chunk-FUG4SFN3.js +36 -0
  66. package/dist/chunk-FUG4SFN3.js.map +1 -0
  67. package/dist/chunk-GKEWXIV5.js +80 -0
  68. package/dist/chunk-GTGPO5KW.js +59 -0
  69. package/dist/chunk-HB3RFQSC.js +95 -0
  70. package/dist/chunk-HDMEGAQE.js +957 -0
  71. package/dist/chunk-HDMEGAQE.js.map +1 -0
  72. package/dist/chunk-HDQIIUPH.js +135 -0
  73. package/dist/chunk-HDQIIUPH.js.map +1 -0
  74. package/dist/chunk-HTJQYHZP.js +344 -0
  75. package/dist/chunk-HTJQYHZP.js.map +1 -0
  76. package/dist/chunk-I6GALP6E.js +99 -0
  77. package/dist/chunk-IGJCYUT5.js +99 -0
  78. package/dist/chunk-IGJCYUT5.js.map +1 -0
  79. package/dist/chunk-IL6POWCW.js +130 -0
  80. package/dist/chunk-IU6SNYX6.js +117 -0
  81. package/dist/chunk-J2ZX2XUV.js +778 -0
  82. package/dist/chunk-JIF24JPM.js +495 -0
  83. package/dist/chunk-JIF24JPM.js.map +1 -0
  84. package/dist/chunk-JK733I5V.js +23 -0
  85. package/dist/chunk-JK733I5V.js.map +1 -0
  86. package/dist/chunk-JKJOGPZN.js +29 -0
  87. package/dist/chunk-JNAI2HN2.js +32 -0
  88. package/dist/chunk-JNAI2HN2.js.map +1 -0
  89. package/dist/chunk-JZUV47FZ.js +41 -0
  90. package/dist/chunk-K5IMOGHB.js +30 -0
  91. package/dist/chunk-KPZS5QAQ.js +151 -0
  92. package/dist/chunk-KPZS5QAQ.js.map +1 -0
  93. package/dist/chunk-KSKBRKGJ.js +291 -0
  94. package/dist/chunk-KSKBRKGJ.js.map +1 -0
  95. package/dist/chunk-LASZKVMO.js +382 -0
  96. package/dist/chunk-LXEXBTKL.js +515 -0
  97. package/dist/chunk-LXEXBTKL.js.map +1 -0
  98. package/dist/chunk-M4CV6BRT.js +524 -0
  99. package/dist/chunk-M4CV6BRT.js.map +1 -0
  100. package/dist/chunk-MEFJPTZ4.js +371 -0
  101. package/dist/chunk-MEFJPTZ4.js.map +1 -0
  102. package/dist/chunk-MV43JSJF.js +265 -0
  103. package/dist/chunk-MV43JSJF.js.map +1 -0
  104. package/dist/chunk-NJ4EOKMB.js +392 -0
  105. package/dist/chunk-NJ4EOKMB.js.map +1 -0
  106. package/dist/chunk-NR5LT5RY.js +135 -0
  107. package/dist/chunk-NWSGI5C7.js +261 -0
  108. package/dist/chunk-OSDB5WW3.js +739 -0
  109. package/dist/chunk-OSDB5WW3.js.map +1 -0
  110. package/dist/chunk-PN2XMATY.js +37 -0
  111. package/dist/chunk-PN2XMATY.js.map +1 -0
  112. package/dist/chunk-Q5VNKNE5.js +70 -0
  113. package/dist/chunk-QLWMEXCT.js +21 -0
  114. package/dist/chunk-QRB7NAOJ.js +123 -0
  115. package/dist/chunk-R4ROFEYC.js +74 -0
  116. package/dist/chunk-RCVJZMFE.js +164 -0
  117. package/dist/chunk-RCVJZMFE.js.map +1 -0
  118. package/dist/chunk-RIX4V2TB.js +367 -0
  119. package/dist/chunk-RYCTDFUX.js +40 -0
  120. package/dist/chunk-RZ5SVOYR.js +155 -0
  121. package/dist/chunk-SLFHQTTW.js +99 -0
  122. package/dist/chunk-SNZ6URZZ.js +2782 -0
  123. package/dist/chunk-SNZ6URZZ.js.map +1 -0
  124. package/dist/chunk-T2FFWNPK.js +75 -0
  125. package/dist/chunk-UNFPPVNI.js +23 -0
  126. package/dist/chunk-VFUPHI2Z.js +333 -0
  127. package/dist/chunk-VFUPHI2Z.js.map +1 -0
  128. package/dist/chunk-VLFCJYTL.js +235 -0
  129. package/dist/chunk-VN3DBQAU.js +124 -0
  130. package/dist/chunk-VYYM3MEN.js +98 -0
  131. package/dist/chunk-VYYM3MEN.js.map +1 -0
  132. package/dist/chunk-VZRODIM3.js +49 -0
  133. package/dist/chunk-W22IKJE2.js +94 -0
  134. package/dist/chunk-X3YYQWOQ.js +155 -0
  135. package/dist/chunk-X7QBD67R.js +14405 -0
  136. package/dist/chunk-X7QBD67R.js.map +1 -0
  137. package/dist/chunk-XDYREZWE.js +1128 -0
  138. package/dist/chunk-XDYREZWE.js.map +1 -0
  139. package/dist/chunk-XWZVFISC.js +720 -0
  140. package/dist/chunk-XWZVFISC.js.map +1 -0
  141. package/dist/chunk-YFXZEAYK.js +85 -0
  142. package/dist/chunk-ZAJAVEXY.js +50 -0
  143. package/dist/chunk-ZICD77GM.js +34 -0
  144. package/dist/chunk-ZJ2AK4V7.js +38 -0
  145. package/dist/chunk-ZUODRVHY.js +14 -0
  146. package/dist/classified/index.js +5 -5
  147. package/dist/classified-marker-CZKA6ITE.js +28 -0
  148. package/dist/classified-marker-CZKA6ITE.js.map +1 -0
  149. package/dist/collection-facade-TLWIPI76.js +44 -0
  150. package/dist/computed-ZIQTYTDI.js +11 -0
  151. package/dist/consent/index.js +9 -9
  152. package/dist/crdt/index.js +1 -1
  153. package/dist/dead-filter-ESOBZZP5.js +28 -0
  154. package/dist/delegation-W7ZV2322.js +25 -0
  155. package/dist/derivations/index.js +12 -12
  156. package/dist/derive-H2BJTLUI.js +21 -0
  157. package/dist/enclave-AP3I5W7O.js +141 -0
  158. package/dist/executor-KGWTNEQD.js +9 -0
  159. package/dist/executor-OVEXR6RT.js +9 -0
  160. package/dist/executor-UNEH5ZYH.js +25 -0
  161. package/dist/export-accessible-PCMXSUZS.js +23 -0
  162. package/dist/extract-partition-SWHFSYJO.js +38 -0
  163. package/dist/fanout-sidecar-A765THMX.js +74 -0
  164. package/dist/find-N6345CNT.js +11 -0
  165. package/dist/forget/index.js +10 -10
  166. package/dist/forget/index.js.map +1 -1
  167. package/dist/guards/index.js +6 -6
  168. package/dist/history/index.js +14 -11
  169. package/dist/history/index.js.map +1 -1
  170. package/dist/i18n/index.js +21 -19
  171. package/dist/i18n/index.js.map +1 -1
  172. package/dist/index.d.ts +307 -1493
  173. package/dist/index.js +309 -190
  174. package/dist/index.js.map +1 -1
  175. package/dist/indexing/index.js +7 -7
  176. package/dist/indexing/index.js.map +1 -1
  177. package/dist/issue-5OKB6I2R.js +19 -0
  178. package/dist/kernel/best-effort-revert.d.ts +52 -0
  179. package/dist/kernel/cache/index.d.ts +10 -0
  180. package/dist/kernel/cache/lru.d.ts +97 -0
  181. package/dist/kernel/cache/policy.d.ts +32 -0
  182. package/dist/kernel/collection-config.d.ts +809 -0
  183. package/dist/kernel/collection.d.ts +1522 -0
  184. package/dist/kernel/constants.d.ts +18 -0
  185. package/dist/kernel/debug.d.ts +25 -0
  186. package/dist/kernel/enclave/broker/proof.d.ts +154 -0
  187. package/dist/kernel/enclave/classify/bidx.d.ts +77 -0
  188. package/dist/kernel/enclave/classify/compare.d.ts +10 -0
  189. package/dist/kernel/enclave/classify/digest.d.ts +8 -0
  190. package/dist/kernel/enclave/classify/find.d.ts +36 -0
  191. package/dist/kernel/enclave/classify/kofn.d.ts +7 -0
  192. package/dist/kernel/enclave/classify/normalize.d.ts +7 -0
  193. package/dist/kernel/enclave/classify/reveal.d.ts +10 -0
  194. package/dist/kernel/enclave/classify/vdig.d.ts +39 -0
  195. package/dist/kernel/enclave/classify/verify.d.ts +36 -0
  196. package/dist/kernel/enclave/classify/write.d.ts +8 -0
  197. package/dist/kernel/enclave/crypto.d.ts +263 -0
  198. package/dist/kernel/enclave/index.d.ts +79 -0
  199. package/dist/kernel/enclave/record-keys/deterministic.d.ts +55 -0
  200. package/dist/kernel/enclave/record-keys/envelope-body.d.ts +79 -0
  201. package/dist/kernel/enclave/record-keys/lifecycle.d.ts +107 -0
  202. package/dist/kernel/enclave/record-keys/record-codec.d.ts +328 -0
  203. package/dist/kernel/enclave/record-keys/sealed-slot.d.ts +14 -0
  204. package/dist/kernel/enclave/record-keys/sealed-slots.d.ts +123 -0
  205. package/dist/kernel/enclave/record-keys/sealing.d.ts +70 -0
  206. package/dist/kernel/enclave/record-keys/tombstone.d.ts +57 -0
  207. package/dist/kernel/env-check.d.ts +1 -0
  208. package/dist/kernel/errors.d.ts +1922 -0
  209. package/dist/kernel/events.d.ts +11 -0
  210. package/dist/kernel/lazy-count.d.ts +13 -0
  211. package/dist/kernel/memory-store.d.ts +17 -0
  212. package/dist/kernel/mutation.d.ts +14 -0
  213. package/dist/kernel/noydb.d.ts +826 -0
  214. package/dist/kernel/paths.d.ts +25 -0
  215. package/dist/kernel/query/builder.d.ts +520 -0
  216. package/dist/kernel/query/index.d.ts +31 -0
  217. package/dist/kernel/query/join.d.ts +205 -0
  218. package/dist/kernel/query/live.d.ts +111 -0
  219. package/dist/kernel/query/predicate.d.ts +139 -0
  220. package/dist/kernel/query/scan-builder.d.ts +334 -0
  221. package/dist/kernel/refs.d.ts +205 -0
  222. package/dist/kernel/schema.d.ts +134 -0
  223. package/dist/kernel/sync-policy.d.ts +188 -0
  224. package/dist/kernel/tier-visibility.d.ts +54 -0
  225. package/dist/kernel/types.d.ts +3239 -0
  226. package/dist/kernel/util/discriminant.d.ts +58 -0
  227. package/dist/kernel/util/index.d.ts +14 -0
  228. package/dist/kernel/util/sanitize-filename.d.ts +75 -0
  229. package/dist/kernel/validation.d.ts +130 -0
  230. package/dist/kernel/vault.d.ts +1740 -0
  231. package/dist/kernel/via/compose.d.ts +144 -0
  232. package/dist/kernel/via/dispatch.d.ts +195 -0
  233. package/dist/kernel/via/graph-wiring.d.ts +145 -0
  234. package/dist/kernel/via/graph.d.ts +166 -0
  235. package/dist/kernel/via/index.d.ts +200 -0
  236. package/dist/kernel/via/pipeline.d.ts +266 -0
  237. package/dist/kernel/via/reconcile.d.ts +153 -0
  238. package/dist/kernel/via/taint-binding.d.ts +66 -0
  239. package/dist/kernel/write-queue.d.ts +46 -0
  240. package/dist/ledger-GFBKVPV4.js +42 -0
  241. package/dist/legacy/bundle.d.ts +30 -0
  242. package/dist/legacy/kernel.d.ts +33 -0
  243. package/dist/liberate-3VDOT4KO.js +24 -0
  244. package/dist/link-set-Z7Q2ASFX.js +31 -0
  245. package/dist/materialized-views/index.js +19 -17
  246. package/dist/noydb-ZCJOYBDI.js +68 -0
  247. package/dist/on/index.js +9 -9
  248. package/dist/overlay-views/index.js +4 -4
  249. package/dist/periods/index.js +12 -12
  250. package/dist/pod/index.js +11 -11
  251. package/dist/policy-Z3RUAIY2.js +41 -0
  252. package/dist/port/as/index.d.ts +26 -0
  253. package/dist/port/at/index.d.ts +18 -0
  254. package/dist/port/by/default-provider.d.ts +9 -0
  255. package/dist/port/by/index.d.ts +17 -0
  256. package/dist/port/by/types.d.ts +86 -0
  257. package/dist/port/in/index.d.ts +21 -0
  258. package/dist/port/on/index.d.ts +27 -0
  259. package/dist/port/to/index.d.ts +19 -0
  260. package/dist/port/ui/index.d.ts +21 -0
  261. package/dist/port/with/blob-strategy.d.ts +98 -0
  262. package/dist/port/with/broker-strategy.d.ts +84 -0
  263. package/dist/port/with/capabilities.d.ts +31 -0
  264. package/dist/port/with/classified-marker.d.ts +9 -0
  265. package/dist/port/with/classified-strategy.d.ts +62 -0
  266. package/dist/port/with/computed-strategy.d.ts +19 -0
  267. package/dist/port/with/i18n-strategy.d.ts +175 -0
  268. package/dist/port/with/index.d.ts +18 -0
  269. package/dist/port/with/lazy-strategy.d.ts +48 -0
  270. package/dist/port/with/lookup-strategy.d.ts +129 -0
  271. package/dist/port/with/service-bus.d.ts +156 -0
  272. package/dist/port/with/team-strategy.d.ts +36 -0
  273. package/dist/port/with/write-hooks.d.ts +34 -0
  274. package/dist/portability/index.js +8 -8
  275. package/dist/post-register-JTH76BLY.js +60 -0
  276. package/dist/post-register-JTH76BLY.js.map +1 -0
  277. package/dist/public-envelope-B4KNWVUA.js +32 -0
  278. package/dist/purge-scope-FGNNLWHD.js +24 -0
  279. package/dist/purge-scope-FGNNLWHD.js.map +1 -0
  280. package/dist/query/index.js +7 -7
  281. package/dist/read-only-facade-CHCKSEPD.js +8 -0
  282. package/dist/register-VAHONCID.js +23 -0
  283. package/dist/registry-7MLVD3VO.js +19 -0
  284. package/dist/registry-EPKPD5SM.js +18 -0
  285. package/dist/registry-P5T7PS2U.js +9 -0
  286. package/dist/request-withdrawal-TI3A5WPS.js +31 -0
  287. package/dist/reveal-DIBHLUZX.js +38 -0
  288. package/dist/reveal-DIBHLUZX.js.map +1 -0
  289. package/dist/revoke-FDRD6GRW.js +24 -0
  290. package/dist/satellites/index.js +1 -1
  291. package/dist/sealed-record/index.js +12 -12
  292. package/dist/seed-Q6M2IZFL.js +231 -0
  293. package/dist/seed-Q6M2IZFL.js.map +1 -0
  294. package/dist/session/index.js +9 -9
  295. package/dist/shadow/index.js +2 -2
  296. package/dist/signer-MUWEIUI6.js +25 -0
  297. package/dist/snapshots/index.js +10 -10
  298. package/dist/stale-OSQ7J3AA.js +29 -0
  299. package/dist/storage-MVFCGEQE.js +23 -0
  300. package/dist/store-coordination-provider-64OEZFGS.js +142 -0
  301. package/dist/sync/index.js +9 -9
  302. package/dist/team/index.js +15 -15
  303. package/dist/tiers/index.js +14 -10
  304. package/dist/to/index.js +1 -2
  305. package/dist/tx/index.js +3 -3
  306. package/dist/util/index.js +1 -1
  307. package/dist/verify-ZASTRNPR.js +140 -0
  308. package/dist/verify-ZASTRNPR.js.map +1 -0
  309. package/dist/via/blob/active.d.ts +34 -0
  310. package/dist/via/blob/binding.d.ts +86 -0
  311. package/dist/via/blob/index.d.ts +34 -0
  312. package/dist/via/classified/active.d.ts +3 -0
  313. package/dist/via/classified/binding.d.ts +83 -0
  314. package/dist/via/classified/config-drift.d.ts +34 -0
  315. package/dist/via/classified/descriptor.d.ts +66 -0
  316. package/dist/via/classified/errors.d.ts +19 -0
  317. package/dist/via/classified/guards.d.ts +23 -0
  318. package/dist/via/classified/index.d.ts +11 -0
  319. package/dist/via/classified/presets.d.ts +65 -0
  320. package/dist/via/classified/resolve.d.ts +9 -0
  321. package/dist/via/classified/strategy.d.ts +11 -0
  322. package/dist/via/classified/validators.d.ts +3 -0
  323. package/dist/via/classified/write.d.ts +4 -0
  324. package/dist/via/computed/binding.d.ts +34 -0
  325. package/dist/via/computed/descriptor.d.ts +35 -0
  326. package/dist/via/i18n/active.d.ts +28 -0
  327. package/dist/via/i18n/binding.d.ts +36 -0
  328. package/dist/via/i18n/core.d.ts +267 -0
  329. package/dist/via/i18n/densify.d.ts +24 -0
  330. package/dist/via/i18n/dictionary.d.ts +200 -0
  331. package/dist/via/i18n/index.d.ts +21 -0
  332. package/dist/via/i18n/policy.d.ts +44 -0
  333. package/dist/via/i18n/script.d.ts +26 -0
  334. package/dist/via/i18n/strategy.d.ts +13 -0
  335. package/dist/via/lookup/active.d.ts +19 -0
  336. package/dist/via/lookup/binding.d.ts +99 -0
  337. package/dist/via/lookup/descriptor.d.ts +123 -0
  338. package/dist/via/lookup/handle.d.ts +220 -0
  339. package/dist/via/lookup/index.d.ts +17 -0
  340. package/dist/via/lookup/registry.d.ts +228 -0
  341. package/dist/via/lookup/snapshot.d.ts +67 -0
  342. package/dist/via/money/arith.d.ts +68 -0
  343. package/dist/via/money/binding.d.ts +21 -0
  344. package/dist/via/money/branded.d.ts +61 -0
  345. package/dist/via/money/descriptor.d.ts +73 -0
  346. package/dist/via/money/fixed-point.d.ts +51 -0
  347. package/dist/via/money/index.d.ts +15 -0
  348. package/dist/via/money/iso4217.d.ts +6 -0
  349. package/dist/via/money/money-reducer.d.ts +33 -0
  350. package/dist/via/money/normalize.d.ts +105 -0
  351. package/dist/via/money/paths.d.ts +61 -0
  352. package/dist/via/money/where.d.ts +127 -0
  353. package/dist/walk-SVNDIZ62.js +241 -0
  354. package/dist/with/index.js +11 -11
  355. package/dist/with-audit/attestation/active.d.ts +10 -0
  356. package/dist/with-audit/attestation/index.d.ts +16 -0
  357. package/dist/with-audit/attestation/issue.d.ts +29 -0
  358. package/dist/with-audit/attestation/revoke.d.ts +15 -0
  359. package/dist/with-audit/attestation/signer.d.ts +33 -0
  360. package/dist/with-audit/attestation/strategy.d.ts +44 -0
  361. package/dist/with-audit/attestation/vault-facade.d.ts +62 -0
  362. package/dist/with-audit/consent/active.d.ts +14 -0
  363. package/dist/with-audit/consent/consent.d.ts +114 -0
  364. package/dist/with-audit/consent/index.d.ts +14 -0
  365. package/dist/with-audit/consent/strategy.d.ts +36 -0
  366. package/dist/with-audit/forget/active.d.ts +31 -0
  367. package/dist/with-audit/forget/index.d.ts +28 -0
  368. package/dist/with-audit/forget/purge-scope.d.ts +54 -0
  369. package/dist/with-audit/forget/strategy.d.ts +199 -0
  370. package/dist/with-audit/forget/subject-index.d.ts +83 -0
  371. package/dist/with-audit/guards/executor.d.ts +26 -0
  372. package/dist/with-audit/guards/immutable-guard.d.ts +63 -0
  373. package/dist/with-audit/guards/index.d.ts +10 -0
  374. package/dist/with-audit/guards/read-only-facade.d.ts +31 -0
  375. package/dist/with-audit/guards/registry.d.ts +83 -0
  376. package/dist/with-audit/guards/transition-guard.d.ts +82 -0
  377. package/dist/with-audit/guards/types.d.ts +134 -0
  378. package/dist/with-audit/guards/with-guard.d.ts +15 -0
  379. package/dist/with-audit/periods/active.d.ts +11 -0
  380. package/dist/with-audit/periods/index.d.ts +18 -0
  381. package/dist/with-audit/periods/periods.d.ts +417 -0
  382. package/dist/with-audit/periods/strategy.d.ts +32 -0
  383. package/dist/with-audit/periods/vault-facade.d.ts +95 -0
  384. package/dist/with-audit/portability/active.d.ts +11 -0
  385. package/dist/with-audit/portability/export-accessible.d.ts +46 -0
  386. package/dist/with-audit/portability/index.d.ts +18 -0
  387. package/dist/with-audit/portability/request-withdrawal.d.ts +85 -0
  388. package/dist/with-audit/portability/strategy.d.ts +31 -0
  389. package/dist/with-audit/portability/withdraw-accessible.d.ts +82 -0
  390. package/dist/with-audit/sealed-record/active.d.ts +9 -0
  391. package/dist/with-audit/sealed-record/index.d.ts +55 -0
  392. package/dist/with-audit/sealed-record/strategy.d.ts +34 -0
  393. package/dist/with-audit/sealed-record/types.d.ts +27 -0
  394. package/dist/with-audit/tiers/active.d.ts +9 -0
  395. package/dist/with-audit/tiers/index.d.ts +394 -0
  396. package/dist/with-audit/tiers/strategy.d.ts +36 -0
  397. package/dist/with-cargo/active.d.ts +11 -0
  398. package/dist/with-cargo/adopt-partition.d.ts +81 -0
  399. package/dist/with-cargo/decrypt-partition.d.ts +19 -0
  400. package/dist/with-cargo/describe-extraction.d.ts +41 -0
  401. package/dist/with-cargo/extract-partition.d.ts +155 -0
  402. package/dist/with-cargo/index.d.ts +33 -0
  403. package/dist/with-cargo/strategy.d.ts +30 -0
  404. package/dist/with-cargo/vault-diff.d.ts +144 -0
  405. package/dist/with-cargo/walk-closure.d.ts +67 -0
  406. package/dist/with-commit/crdt/active.d.ts +25 -0
  407. package/dist/with-commit/crdt/crdt.d.ts +42 -0
  408. package/dist/with-commit/crdt/index.d.ts +16 -0
  409. package/dist/with-commit/crdt/strategy.d.ts +25 -0
  410. package/dist/with-commit/history/active.d.ts +40 -0
  411. package/dist/with-commit/history/diff.d.ts +23 -0
  412. package/dist/with-commit/history/history.d.ts +73 -0
  413. package/dist/with-commit/history/index.d.ts +30 -0
  414. package/dist/with-commit/history/ledger/constants.d.ts +37 -0
  415. package/dist/with-commit/history/ledger/entry.d.ts +195 -0
  416. package/dist/with-commit/history/ledger/hash.d.ts +30 -0
  417. package/dist/with-commit/history/ledger/index.d.ts +19 -0
  418. package/dist/with-commit/history/ledger/patch.d.ts +99 -0
  419. package/dist/with-commit/history/ledger/store.d.ts +356 -0
  420. package/dist/with-commit/history/strategy.d.ts +136 -0
  421. package/dist/with-commit/history/time-machine.d.ts +202 -0
  422. package/dist/with-commit/numbering/descriptor.d.ts +27 -0
  423. package/dist/with-commit/numbering/index.d.ts +69 -0
  424. package/dist/with-commit/sequence/active.d.ts +10 -0
  425. package/dist/with-commit/sequence/index.d.ts +150 -0
  426. package/dist/with-commit/sequence/strategy.d.ts +26 -0
  427. package/dist/with-commit/tx/active.d.ts +27 -0
  428. package/dist/with-commit/tx/dry-run.d.ts +29 -0
  429. package/dist/with-commit/tx/elevated-handle.d.ts +59 -0
  430. package/dist/with-commit/tx/index.d.ts +13 -0
  431. package/dist/with-commit/tx/invariants.d.ts +68 -0
  432. package/dist/with-commit/tx/strategy.d.ts +23 -0
  433. package/dist/with-commit/tx/transaction.d.ts +212 -0
  434. package/dist/with-fork/archive/engine.d.ts +71 -0
  435. package/dist/with-fork/archive/index.d.ts +23 -0
  436. package/dist/with-fork/shadow/active.d.ts +6 -0
  437. package/dist/with-fork/shadow/index.d.ts +14 -0
  438. package/dist/with-fork/shadow/strategy.d.ts +28 -0
  439. package/dist/with-fork/shadow/vault-frame.d.ts +93 -0
  440. package/dist/with-fork/snapshots/active.d.ts +20 -0
  441. package/dist/with-fork/snapshots/engine.d.ts +36 -0
  442. package/dist/with-fork/snapshots/index.d.ts +5 -0
  443. package/dist/with-fork/snapshots/noydb-facade.d.ts +64 -0
  444. package/dist/with-fork/snapshots/policy.d.ts +23 -0
  445. package/dist/with-fork/snapshots/scheduler.d.ts +34 -0
  446. package/dist/with-fork/snapshots/strategy.d.ts +48 -0
  447. package/dist/with-formula/computed/index.d.ts +48 -0
  448. package/dist/with-formula/computed/lazy.d.ts +13 -0
  449. package/dist/with-formula/derivations/executor.d.ts +61 -0
  450. package/dist/with-formula/derivations/fanout-sidecar.d.ts +75 -0
  451. package/dist/with-formula/derivations/index.d.ts +6 -0
  452. package/dist/with-formula/derivations/registry.d.ts +67 -0
  453. package/dist/with-formula/derivations/stale.d.ts +39 -0
  454. package/dist/with-formula/derivations/strategy-hash.d.ts +1 -0
  455. package/dist/with-formula/derivations/types.d.ts +248 -0
  456. package/dist/with-formula/derivations/with-derivation.d.ts +10 -0
  457. package/dist/with-formula/derivations/with-rollup.d.ts +34 -0
  458. package/dist/with-formula/materialized-views/dependency-analyzer.d.ts +58 -0
  459. package/dist/with-formula/materialized-views/executor.d.ts +80 -0
  460. package/dist/with-formula/materialized-views/index.d.ts +11 -0
  461. package/dist/with-formula/materialized-views/query-hash.d.ts +23 -0
  462. package/dist/with-formula/materialized-views/registry.d.ts +99 -0
  463. package/dist/with-formula/materialized-views/stale.d.ts +113 -0
  464. package/dist/with-formula/materialized-views/types.d.ts +318 -0
  465. package/dist/with-formula/materialized-views/with-materialized-view.d.ts +24 -0
  466. package/dist/with-formula/overlay-views/index.d.ts +5 -0
  467. package/dist/with-formula/overlay-views/registry.d.ts +42 -0
  468. package/dist/with-formula/overlay-views/types.d.ts +97 -0
  469. package/dist/with-formula/overlay-views/virtual-collection.d.ts +90 -0
  470. package/dist/with-formula/overlay-views/with-overlayed-view.d.ts +9 -0
  471. package/dist/with-lookup/aggregate/active.d.ts +33 -0
  472. package/dist/with-lookup/aggregate/aggregation.d.ts +166 -0
  473. package/dist/with-lookup/aggregate/canonical-key.d.ts +16 -0
  474. package/dist/with-lookup/aggregate/groupby.d.ts +280 -0
  475. package/dist/with-lookup/aggregate/index.d.ts +23 -0
  476. package/dist/with-lookup/aggregate/reducers.d.ts +257 -0
  477. package/dist/with-lookup/aggregate/strategy.d.ts +59 -0
  478. package/dist/with-lookup/embeddings/cosine.d.ts +2 -0
  479. package/dist/with-lookup/embeddings/descriptor.d.ts +8 -0
  480. package/dist/with-lookup/embeddings/index.d.ts +4 -0
  481. package/dist/with-lookup/embeddings/vec-id.d.ts +47 -0
  482. package/dist/with-lookup/embeddings/vector-set.d.ts +19 -0
  483. package/dist/with-lookup/indexing/active.d.ts +30 -0
  484. package/dist/with-lookup/indexing/collection-facade.d.ts +157 -0
  485. package/dist/with-lookup/indexing/eager-indexes.d.ts +122 -0
  486. package/dist/with-lookup/indexing/index.d.ts +27 -0
  487. package/dist/with-lookup/indexing/lazy-builder.d.ts +102 -0
  488. package/dist/with-lookup/indexing/persisted-indexes.d.ts +229 -0
  489. package/dist/with-lookup/indexing/strategy.d.ts +56 -0
  490. package/dist/with-lookup/indexing/unique-constraints.d.ts +74 -0
  491. package/dist/with-lookup/search/active.d.ts +10 -0
  492. package/dist/with-lookup/search/build-docs.d.ts +15 -0
  493. package/dist/with-lookup/search/collection-facade.d.ts +163 -0
  494. package/dist/with-lookup/search/fuse.d.ts +22 -0
  495. package/dist/with-lookup/search/index-store.d.ts +19 -0
  496. package/dist/with-lookup/search/index.d.ts +19 -0
  497. package/dist/with-lookup/search/inverted-index.d.ts +47 -0
  498. package/dist/with-lookup/search/persisted-index-store.d.ts +121 -0
  499. package/dist/with-lookup/search/retrieve-types.d.ts +29 -0
  500. package/dist/with-lookup/search/scan.d.ts +28 -0
  501. package/dist/with-lookup/search/segment.d.ts +15 -0
  502. package/dist/with-lookup/search/serialize.d.ts +4 -0
  503. package/dist/with-lookup/search/snippet.d.ts +5 -0
  504. package/dist/with-lookup/search/strategy.d.ts +41 -0
  505. package/dist/with-lookup/search/tokenize.d.ts +12 -0
  506. package/dist/with-party/auth-introspection/index.d.ts +44 -0
  507. package/dist/with-party/broker/active.d.ts +23 -0
  508. package/dist/with-party/broker/index.d.ts +28 -0
  509. package/dist/with-party/broker/seed.d.ts +77 -0
  510. package/dist/with-party/custody/active.d.ts +10 -0
  511. package/dist/with-party/custody/index.d.ts +69 -0
  512. package/dist/with-party/custody/liberate.d.ts +64 -0
  513. package/dist/with-party/custody/strategy.d.ts +48 -0
  514. package/dist/with-party/directory/index.d.ts +11 -0
  515. package/dist/with-party/directory/public-envelope/index.d.ts +12 -0
  516. package/dist/with-party/directory/public-envelope/schema.d.ts +24 -0
  517. package/dist/with-party/directory/public-envelope/storage.d.ts +59 -0
  518. package/dist/with-party/directory/public-envelope/types.d.ts +80 -0
  519. package/dist/with-party/directory/storage.d.ts +33 -0
  520. package/dist/with-party/directory/types.d.ts +43 -0
  521. package/dist/with-party/directory/user-envelope/api.d.ts +201 -0
  522. package/dist/with-party/directory/user-envelope/index.d.ts +12 -0
  523. package/dist/with-party/directory/user-envelope/storage.d.ts +54 -0
  524. package/dist/with-party/directory/visibility.d.ts +47 -0
  525. package/dist/with-party/policy/engine.d.ts +60 -0
  526. package/dist/with-party/policy/index.d.ts +15 -0
  527. package/dist/with-party/policy/noydb-facade.d.ts +39 -0
  528. package/dist/with-party/policy/presets.d.ts +38 -0
  529. package/dist/with-party/policy/storage.d.ts +31 -0
  530. package/dist/with-party/session/active.d.ts +34 -0
  531. package/dist/with-party/session/dev-unlock.d.ts +128 -0
  532. package/dist/with-party/session/index.d.ts +23 -0
  533. package/dist/with-party/session/session-policy.d.ts +92 -0
  534. package/dist/with-party/session/session.d.ts +131 -0
  535. package/dist/with-party/session/strategy.d.ts +42 -0
  536. package/dist/with-party/session/unlock-state.d.ts +47 -0
  537. package/dist/with-party/sync/index.d.ts +31 -0
  538. package/dist/with-party/tab-coordination.d.ts +73 -0
  539. package/dist/with-party/tab-write-relay.d.ts +39 -0
  540. package/dist/with-party/team/active.d.ts +13 -0
  541. package/dist/with-party/team/authenticators.d.ts +95 -0
  542. package/dist/with-party/team/deed.d.ts +97 -0
  543. package/dist/with-party/team/delegation.d.ts +88 -0
  544. package/dist/with-party/team/index.d.ts +38 -0
  545. package/dist/with-party/team/keyring.d.ts +318 -0
  546. package/dist/with-party/team/magic-link-grant.d.ts +152 -0
  547. package/dist/with-party/team/managed-passphrase.d.ts +280 -0
  548. package/dist/with-party/team/noydb-facade.d.ts +484 -0
  549. package/dist/with-party/team/peer-recover.d.ts +81 -0
  550. package/dist/with-party/team/presence.d.ts +81 -0
  551. package/dist/with-party/team/recovery.d.ts +191 -0
  552. package/dist/with-party/team/reserved-secret-collections.d.ts +41 -0
  553. package/dist/with-party/team/rotate-recover.d.ts +275 -0
  554. package/dist/with-party/team/shamir-recovery-provider.d.ts +15 -0
  555. package/dist/with-party/team/strategy.d.ts +9 -0
  556. package/dist/with-party/team/sync-active.d.ts +33 -0
  557. package/dist/with-party/team/sync-credentials.d.ts +113 -0
  558. package/dist/with-party/team/sync-strategy.d.ts +67 -0
  559. package/dist/with-party/team/sync-transaction.d.ts +39 -0
  560. package/dist/with-party/team/sync.d.ts +174 -0
  561. package/dist/with-party/team/tiers.d.ts +39 -0
  562. package/dist/with-party/team/wrapped-deks.d.ts +86 -0
  563. package/dist/with-pod/backup.d.ts +74 -0
  564. package/dist/with-pod/bundle.d.ts +414 -0
  565. package/dist/with-pod/format.d.ts +203 -0
  566. package/dist/with-pod/index.d.ts +20 -0
  567. package/dist/with-pod/pod-store.d.ts +54 -0
  568. package/dist/with-pod/ulid.d.ts +64 -0
  569. package/dist/with-shape/blobs/blob-compaction.d.ts +170 -0
  570. package/dist/with-shape/blobs/blob-intent.d.ts +153 -0
  571. package/dist/with-shape/blobs/blob-set.d.ts +1286 -0
  572. package/dist/with-shape/blobs/export-blobs.d.ts +110 -0
  573. package/dist/with-shape/blobs/import-external.d.ts +50 -0
  574. package/dist/with-shape/blobs/mime-magic.d.ts +48 -0
  575. package/dist/with-shape/blobs/object-projection.d.ts +70 -0
  576. package/dist/with-shape/introspection/describe.d.ts +237 -0
  577. package/dist/with-shape/introspection/field-meta.d.ts +61 -0
  578. package/dist/with-shape/introspection/fields.d.ts +14 -0
  579. package/dist/with-shape/introspection/index.d.ts +12 -0
  580. package/dist/with-shape/introspection/json-schema.d.ts +13 -0
  581. package/dist/with-shape/introspection/meta.d.ts +22 -0
  582. package/dist/with-shape/introspection/projection.d.ts +13 -0
  583. package/dist/with-shape/introspection/types.d.ts +154 -0
  584. package/dist/with-shape/introspection/walk.d.ts +50 -0
  585. package/dist/with-shape/links/lazy-handle.d.ts +36 -0
  586. package/dist/with-shape/links/link-set.d.ts +67 -0
  587. package/dist/with-shape/links/names.d.ts +65 -0
  588. package/dist/with-shape/links/vault-facade.d.ts +211 -0
  589. package/dist/with-shape/persisted-schemas/canonicalize.d.ts +11 -0
  590. package/dist/with-shape/persisted-schemas/derive.d.ts +29 -0
  591. package/dist/with-shape/persisted-schemas/index.d.ts +21 -0
  592. package/dist/with-shape/persisted-schemas/register.d.ts +66 -0
  593. package/dist/with-shape/persisted-schemas/storage.d.ts +61 -0
  594. package/dist/with-shape/persisted-schemas/types.d.ts +54 -0
  595. package/dist/with-shape/satellites/dead-filter.d.ts +21 -0
  596. package/dist/with-shape/satellites/declare.d.ts +60 -0
  597. package/dist/with-shape/satellites/existence.d.ts +5 -0
  598. package/dist/with-shape/satellites/fanout.d.ts +38 -0
  599. package/dist/with-shape/satellites/forget.d.ts +47 -0
  600. package/dist/with-shape/satellites/index.d.ts +3 -0
  601. package/dist/with-shape/satellites/joined.d.ts +19 -0
  602. package/dist/with-shape/satellites/marker.d.ts +10 -0
  603. package/dist/with-shape/satellites/migrate-cek.d.ts +26 -0
  604. package/dist/with-shape/satellites/post-register.d.ts +6 -0
  605. package/dist/with-shape/satellites/proxy.d.ts +37 -0
  606. package/dist/with-shape/satellites/raw-target.d.ts +14 -0
  607. package/dist/with-shape/satellites/registry.d.ts +19 -0
  608. package/dist/with-shape/satellites/types.d.ts +37 -0
  609. package/dist/with-shape/satellites/validate.d.ts +17 -0
  610. package/dist/with-shape/schema-update/client-registry.d.ts +35 -0
  611. package/dist/with-shape/schema-update/cutover.d.ts +5 -0
  612. package/dist/with-shape/schema-update/delta.d.ts +2 -0
  613. package/dist/with-shape/schema-update/dispatch.d.ts +3 -0
  614. package/dist/with-shape/schema-update/fence-controller.d.ts +62 -0
  615. package/dist/with-shape/schema-update/fence-watcher.d.ts +37 -0
  616. package/dist/with-shape/schema-update/fence.d.ts +15 -0
  617. package/dist/with-shape/schema-update/gate.d.ts +16 -0
  618. package/dist/with-shape/schema-update/index.d.ts +6 -0
  619. package/dist/with-shape/schema-update/store-coordination-provider.d.ts +29 -0
  620. package/dist/with-shape/schema-update/strategies.d.ts +12 -0
  621. package/dist/with-shape/schema-update/types.d.ts +53 -0
  622. package/dist/with-store/index.d.ts +15 -0
  623. package/dist/with-store/lazy/active.d.ts +12 -0
  624. package/dist/with-store/lazy/index.d.ts +14 -0
  625. package/dist/with-store/lazy/strategy.d.ts +9 -0
  626. package/dist/with-store/route-store.d.ts +259 -0
  627. package/dist/with-store/store-middleware.d.ts +189 -0
  628. package/dist/withdraw-accessible-WZIEVWSR.js +28 -0
  629. package/package.json +47 -55
  630. package/dist/adapter/index.d.ts +0 -4
  631. package/dist/adapter/index.js +0 -15
  632. package/dist/aggregate/index.d.ts +0 -41
  633. package/dist/api-HV4PDUWF.js +0 -20
  634. package/dist/as/index.d.ts +0 -6
  635. package/dist/at/index.d.ts +0 -4
  636. package/dist/attestation/index.d.ts +0 -19
  637. package/dist/backup-ODXO7OGA.js +0 -225
  638. package/dist/backup-ODXO7OGA.js.map +0 -1
  639. package/dist/blobs/index.d.ts +0 -43
  640. package/dist/bundle/index.d.ts +0 -121
  641. package/dist/bundle-DlSxo31R.d.ts +0 -548
  642. package/dist/by/index.d.ts +0 -1
  643. package/dist/cargo/index.d.ts +0 -8
  644. package/dist/chunk-2644KEGW.js +0 -247
  645. package/dist/chunk-2644KEGW.js.map +0 -1
  646. package/dist/chunk-2HVUKRNE.js +0 -21
  647. package/dist/chunk-2KOSJTMU.js +0 -69
  648. package/dist/chunk-2UQ4RMHV.js +0 -167
  649. package/dist/chunk-2UQ4RMHV.js.map +0 -1
  650. package/dist/chunk-2XE6LIXT.js +0 -515
  651. package/dist/chunk-2XE6LIXT.js.map +0 -1
  652. package/dist/chunk-34RRRIC3.js +0 -71
  653. package/dist/chunk-3C5CKXFI.js +0 -621
  654. package/dist/chunk-3C5CKXFI.js.map +0 -1
  655. package/dist/chunk-3I4KYWNP.js +0 -30
  656. package/dist/chunk-42KVY3OC.js +0 -86
  657. package/dist/chunk-4C4WBS4V.js +0 -13822
  658. package/dist/chunk-4C4WBS4V.js.map +0 -1
  659. package/dist/chunk-4G3IT7J7.js +0 -280
  660. package/dist/chunk-4G3IT7J7.js.map +0 -1
  661. package/dist/chunk-4RQ62VSR.js +0 -53
  662. package/dist/chunk-4RQ62VSR.js.map +0 -1
  663. package/dist/chunk-4ZSGSRBI.js +0 -1221
  664. package/dist/chunk-4ZSGSRBI.js.map +0 -1
  665. package/dist/chunk-5GAX2QOH.js +0 -132
  666. package/dist/chunk-5GAX2QOH.js.map +0 -1
  667. package/dist/chunk-5SAZMBW7.js +0 -202
  668. package/dist/chunk-5TZZUL5X.js +0 -30
  669. package/dist/chunk-6CU3FN74.js +0 -777
  670. package/dist/chunk-6G2SCQYS.js +0 -29
  671. package/dist/chunk-6G2SCQYS.js.map +0 -1
  672. package/dist/chunk-6MWUANRF.js +0 -29
  673. package/dist/chunk-6S7T6WC4.js +0 -111
  674. package/dist/chunk-6S7T6WC4.js.map +0 -1
  675. package/dist/chunk-6ULCYCMU.js +0 -74
  676. package/dist/chunk-7HAPHVRJ.js +0 -1695
  677. package/dist/chunk-7HAPHVRJ.js.map +0 -1
  678. package/dist/chunk-7IZQTCTR.js +0 -382
  679. package/dist/chunk-7ZANJ6JZ.js +0 -265
  680. package/dist/chunk-7ZANJ6JZ.js.map +0 -1
  681. package/dist/chunk-A2APWS32.js +0 -150
  682. package/dist/chunk-A2APWS32.js.map +0 -1
  683. package/dist/chunk-A4MAD6PO.js +0 -99
  684. package/dist/chunk-ANDBBA2P.js +0 -36
  685. package/dist/chunk-ANDBBA2P.js.map +0 -1
  686. package/dist/chunk-AUR7MRHA.js +0 -404
  687. package/dist/chunk-AYGZMQP5.js +0 -75
  688. package/dist/chunk-BGIZAFY7.js +0 -1
  689. package/dist/chunk-CJEMA2Z6.js +0 -230
  690. package/dist/chunk-CTSFKDGA.js +0 -40
  691. package/dist/chunk-CU62BBRB.js +0 -1465
  692. package/dist/chunk-CU62BBRB.js.map +0 -1
  693. package/dist/chunk-CWCCYH34.js +0 -23
  694. package/dist/chunk-DEYAO3N6.js +0 -589
  695. package/dist/chunk-DFIBOSOQ.js +0 -85
  696. package/dist/chunk-E5AHO65E.js +0 -150
  697. package/dist/chunk-FC3XK5JT.js +0 -99
  698. package/dist/chunk-FGOGS7CE.js +0 -662
  699. package/dist/chunk-FGOGS7CE.js.map +0 -1
  700. package/dist/chunk-GX2BYNCT.js +0 -117
  701. package/dist/chunk-GZ2MOPVX.js +0 -291
  702. package/dist/chunk-GZ2MOPVX.js.map +0 -1
  703. package/dist/chunk-H57P2SJB.js +0 -126
  704. package/dist/chunk-H57P2SJB.js.map +0 -1
  705. package/dist/chunk-HBTT7YQX.js +0 -94
  706. package/dist/chunk-HOCEGVGQ.js +0 -36
  707. package/dist/chunk-ICV32FPK.js +0 -50
  708. package/dist/chunk-IIP5WFVT.js +0 -462
  709. package/dist/chunk-IIP5WFVT.js.map +0 -1
  710. package/dist/chunk-IUKGIB2Q.js +0 -91
  711. package/dist/chunk-IWTYFS3D.js +0 -129
  712. package/dist/chunk-JC5R7P44.js +0 -355
  713. package/dist/chunk-JC5R7P44.js.map +0 -1
  714. package/dist/chunk-K2WCO3NX.js +0 -1
  715. package/dist/chunk-KBBWY46Y.js +0 -14
  716. package/dist/chunk-KBF5GSGX.js +0 -367
  717. package/dist/chunk-KCG3BHFG.js +0 -99
  718. package/dist/chunk-KCG3BHFG.js.map +0 -1
  719. package/dist/chunk-KNXN47MO.js +0 -155
  720. package/dist/chunk-KWLD2D75.js +0 -38
  721. package/dist/chunk-KXKLKJW4.js +0 -49
  722. package/dist/chunk-KYC4LIEU.js +0 -123
  723. package/dist/chunk-LEOXZY36.js +0 -130
  724. package/dist/chunk-M5I354EE.js +0 -235
  725. package/dist/chunk-MK2BSS44.js +0 -34
  726. package/dist/chunk-MSVY7ULZ.js +0 -70
  727. package/dist/chunk-NNWK3PSY.js +0 -135
  728. package/dist/chunk-NSZ6J5DF.js +0 -59
  729. package/dist/chunk-NUEFXLZU.js +0 -59
  730. package/dist/chunk-NYVQCZ54.js +0 -261
  731. package/dist/chunk-OQAODTYR.js +0 -45
  732. package/dist/chunk-OV4CMJGR.js +0 -164
  733. package/dist/chunk-PEDZFFKW.js +0 -41
  734. package/dist/chunk-PF3FQCCF.js +0 -164
  735. package/dist/chunk-PF3FQCCF.js.map +0 -1
  736. package/dist/chunk-Q2KVKTC2.js +0 -823
  737. package/dist/chunk-QJR3W3MF.js +0 -23
  738. package/dist/chunk-QJR3W3MF.js.map +0 -1
  739. package/dist/chunk-QTXQ6RNZ.js +0 -255
  740. package/dist/chunk-RJ5522XX.js +0 -453
  741. package/dist/chunk-RJ5522XX.js.map +0 -1
  742. package/dist/chunk-RMUXPHIH.js +0 -930
  743. package/dist/chunk-RMUXPHIH.js.map +0 -1
  744. package/dist/chunk-S6AQXDN4.js +0 -155
  745. package/dist/chunk-SSFC3IYA.js +0 -275
  746. package/dist/chunk-SSFC3IYA.js.map +0 -1
  747. package/dist/chunk-SZCHEDML.js +0 -209
  748. package/dist/chunk-TDPEJINP.js +0 -188
  749. package/dist/chunk-TDPEJINP.js.map +0 -1
  750. package/dist/chunk-TQ3REHVF.js +0 -95
  751. package/dist/chunk-TQ3REHVF.js.map +0 -1
  752. package/dist/chunk-UNJU345Q.js +0 -32
  753. package/dist/chunk-UNJU345Q.js.map +0 -1
  754. package/dist/chunk-V7RNIND6.js +0 -265
  755. package/dist/chunk-V7RNIND6.js.map +0 -1
  756. package/dist/chunk-VJ3NQK4K.js +0 -205
  757. package/dist/chunk-WNA6T3MA.js +0 -686
  758. package/dist/chunk-WNA6T3MA.js.map +0 -1
  759. package/dist/chunk-WVCQ75OK.js +0 -124
  760. package/dist/chunk-X66MXMR5.js +0 -42
  761. package/dist/chunk-XHD3L3JO.js +0 -930
  762. package/dist/chunk-XHD3L3JO.js.map +0 -1
  763. package/dist/chunk-XJK2WIYN.js +0 -430
  764. package/dist/chunk-XJK2WIYN.js.map +0 -1
  765. package/dist/chunk-XQ7UYPVN.js +0 -95
  766. package/dist/chunk-Y2OCKFZ3.js +0 -80
  767. package/dist/chunk-YBFOBRTO.js +0 -367
  768. package/dist/chunk-YLN33ADH.js +0 -97
  769. package/dist/chunk-Z5YFRMG5.js +0 -45
  770. package/dist/chunk-ZWQFOXBR.js +0 -54
  771. package/dist/classified/index.d.ts +0 -16
  772. package/dist/classified-marker-IO5EBW7Q.js +0 -24
  773. package/dist/classified-marker-IO5EBW7Q.js.map +0 -1
  774. package/dist/collection-facade-3RAXG2OU.js +0 -38
  775. package/dist/computed-R62HGCO4.js +0 -11
  776. package/dist/consent/index.d.ts +0 -22
  777. package/dist/crdt/index.d.ts +0 -33
  778. package/dist/dead-filter-VNA4M3GP.js +0 -28
  779. package/dist/decrypt-partition-IHtxEnTi.d.ts +0 -21
  780. package/dist/delegation-ZKII5UBI.js +0 -25
  781. package/dist/derivations/index.d.ts +0 -69
  782. package/dist/derive-5D2C3WG4.js +0 -21
  783. package/dist/describe/index.d.ts +0 -4
  784. package/dist/describe/index.js +0 -1
  785. package/dist/dev-unlock-DDTafVO6.d.ts +0 -263
  786. package/dist/discriminant-BN9REW3o.d.ts +0 -60
  787. package/dist/enclave-EUBEJGPG.js +0 -125
  788. package/dist/executor-IL7SRNUZ.js +0 -23
  789. package/dist/executor-OUA2Y6YC.js +0 -9
  790. package/dist/executor-WAEI6PDV.js +0 -9
  791. package/dist/export-accessible-O55RKAGC.js +0 -23
  792. package/dist/extract-partition-VBGCBA6Q.js +0 -36
  793. package/dist/fanout-sidecar-CI5BCMQM.js +0 -74
  794. package/dist/find-FISGUWSK.js +0 -11
  795. package/dist/forget/index.d.ts +0 -36
  796. package/dist/guards/index.d.ts +0 -34
  797. package/dist/hash-DBkZfIBW.d.ts +0 -72
  798. package/dist/history/index.d.ts +0 -60
  799. package/dist/i18n/index.d.ts +0 -56
  800. package/dist/in/index.d.ts +0 -4
  801. package/dist/index-B9QdHytu.d.ts +0 -123
  802. package/dist/index-CyclmyDr.d.ts +0 -122
  803. package/dist/index-DzhNI2vE.d.ts +0 -23260
  804. package/dist/indexing/index.d.ts +0 -38
  805. package/dist/issue-EQAWIYFQ.js +0 -19
  806. package/dist/kernel/index.d.ts +0 -31
  807. package/dist/kernel/index.js +0 -60
  808. package/dist/lazy/index.d.ts +0 -20
  809. package/dist/ledger-AJA2SR3C.js +0 -42
  810. package/dist/liberate-SEGR4JLY.js +0 -24
  811. package/dist/link-set-YBAC2DQT.js +0 -31
  812. package/dist/materialized-views/index.d.ts +0 -182
  813. package/dist/mime-magic-CbV1quyp.d.ts +0 -103
  814. package/dist/noydb-AMEOU456.js +0 -65
  815. package/dist/on/index.d.ts +0 -4
  816. package/dist/overlay-views/index.d.ts +0 -97
  817. package/dist/periods/index.d.ts +0 -19
  818. package/dist/pod/index.d.ts +0 -62
  819. package/dist/policy-SDGRWVIL.js +0 -41
  820. package/dist/portability/index.d.ts +0 -19
  821. package/dist/post-register-MROIUA5O.js +0 -55
  822. package/dist/post-register-MROIUA5O.js.map +0 -1
  823. package/dist/public-envelope-J5PLQOW2.js +0 -32
  824. package/dist/query/index.d.ts +0 -4
  825. package/dist/read-only-facade-UCYNRE56.js +0 -8
  826. package/dist/register-OVJKW7BS.js +0 -23
  827. package/dist/registry-5RJSK5AJ.js +0 -9
  828. package/dist/registry-DQRNTWRL.js +0 -18
  829. package/dist/registry-LTGLERS7.js +0 -19
  830. package/dist/request-withdrawal-27O77D2V.js +0 -31
  831. package/dist/reveal-LT3I2K4P.js +0 -38
  832. package/dist/reveal-LT3I2K4P.js.map +0 -1
  833. package/dist/revoke-4N45G7D2.js +0 -24
  834. package/dist/revoke-4N45G7D2.js.map +0 -1
  835. package/dist/satellites/index.d.ts +0 -4
  836. package/dist/sealed-record/index.d.ts +0 -68
  837. package/dist/session/index.d.ts +0 -43
  838. package/dist/shadow/index.d.ts +0 -14
  839. package/dist/signer-XJCOV7JD.js +0 -25
  840. package/dist/signer-XJCOV7JD.js.map +0 -1
  841. package/dist/snapshots/index.d.ts +0 -25
  842. package/dist/stale-IYYXEH3U.js +0 -14
  843. package/dist/stale-IYYXEH3U.js.map +0 -1
  844. package/dist/storage-Y44CAPE6.js +0 -23
  845. package/dist/storage-Y44CAPE6.js.map +0 -1
  846. package/dist/store-coordination-provider-WKGAE2WI.js +0 -142
  847. package/dist/subject-index-CyOgGMJL.d.ts +0 -380
  848. package/dist/sync/index.d.ts +0 -40
  849. package/dist/team/index.d.ts +0 -154
  850. package/dist/tiers/index.d.ts +0 -4
  851. package/dist/to/index.d.ts +0 -4
  852. package/dist/transition-guard-BOUkzQU4.d.ts +0 -165
  853. package/dist/tx/index.d.ts +0 -34
  854. package/dist/ui/index.d.ts +0 -4
  855. package/dist/ulid-DRH25k3y.d.ts +0 -66
  856. package/dist/util/index.d.ts +0 -79
  857. package/dist/validators-B3SalxlJ.d.ts +0 -101
  858. package/dist/vault-diff-Dt9NkLzw.d.ts +0 -147
  859. package/dist/verify-GH6CWVSW.js +0 -139
  860. package/dist/verify-GH6CWVSW.js.map +0 -1
  861. package/dist/walk-76UCFXYV.js +0 -241
  862. package/dist/with/index.d.ts +0 -37
  863. package/dist/with-materialized-view-BP33v1Z6.d.ts +0 -27
  864. package/dist/with-overlayed-view-DntFpOKN.d.ts +0 -12
  865. package/dist/with-rollup-ClCHO6-4.d.ts +0 -47
  866. package/dist/withdraw-accessible-6J2UH3IN.js +0 -28
  867. package/dist/withdraw-accessible-6J2UH3IN.js.map +0 -1
  868. /package/dist/{adapter/index.js.map → api-ZGS6LCOF.js.map} +0 -0
  869. /package/dist/{chunk-34RRRIC3.js.map → chunk-3B5VVCNY.js.map} +0 -0
  870. /package/dist/{chunk-CJEMA2Z6.js.map → chunk-3LQ2YKO4.js.map} +0 -0
  871. /package/dist/{chunk-ZWQFOXBR.js.map → chunk-3QCRBHCY.js.map} +0 -0
  872. /package/dist/{chunk-E5AHO65E.js.map → chunk-4JBYV75E.js.map} +0 -0
  873. /package/dist/{chunk-YLN33ADH.js.map → chunk-4NXKCLLQ.js.map} +0 -0
  874. /package/dist/{chunk-2KOSJTMU.js.map → chunk-5M5DVGV7.js.map} +0 -0
  875. /package/dist/{chunk-3I4KYWNP.js.map → chunk-5YOLBEQ6.js.map} +0 -0
  876. /package/dist/{chunk-IWTYFS3D.js.map → chunk-6CRIV7DE.js.map} +0 -0
  877. /package/dist/{chunk-QTXQ6RNZ.js.map → chunk-6QTNZJBS.js.map} +0 -0
  878. /package/dist/{chunk-42KVY3OC.js.map → chunk-6T2SCQRU.js.map} +0 -0
  879. /package/dist/{chunk-Z5YFRMG5.js.map → chunk-7DSTRQIJ.js.map} +0 -0
  880. /package/dist/{chunk-DEYAO3N6.js.map → chunk-AUSTCW2X.js.map} +0 -0
  881. /package/dist/{chunk-SZCHEDML.js.map → chunk-B3HJYUOF.js.map} +0 -0
  882. /package/dist/{chunk-OV4CMJGR.js.map → chunk-BDVFPG6Y.js.map} +0 -0
  883. /package/dist/{chunk-HOCEGVGQ.js.map → chunk-CAF6WOK7.js.map} +0 -0
  884. /package/dist/{chunk-AUR7MRHA.js.map → chunk-CIE2BASH.js.map} +0 -0
  885. /package/dist/{chunk-X66MXMR5.js.map → chunk-CRI4OUV4.js.map} +0 -0
  886. /package/dist/{chunk-IUKGIB2Q.js.map → chunk-DLG74CNB.js.map} +0 -0
  887. /package/dist/{chunk-YBFOBRTO.js.map → chunk-ERJ7FU55.js.map} +0 -0
  888. /package/dist/{chunk-VJ3NQK4K.js.map → chunk-EVVGJDRQ.js.map} +0 -0
  889. /package/dist/{chunk-NSZ6J5DF.js.map → chunk-EXK7QRWF.js.map} +0 -0
  890. /package/dist/{chunk-5SAZMBW7.js.map → chunk-EYZ6IQ6X.js.map} +0 -0
  891. /package/dist/{chunk-OQAODTYR.js.map → chunk-F64IRYG3.js.map} +0 -0
  892. /package/dist/{chunk-Q2KVKTC2.js.map → chunk-FHS2HQSH.js.map} +0 -0
  893. /package/dist/{chunk-Y2OCKFZ3.js.map → chunk-GKEWXIV5.js.map} +0 -0
  894. /package/dist/{chunk-NUEFXLZU.js.map → chunk-GTGPO5KW.js.map} +0 -0
  895. /package/dist/{chunk-XQ7UYPVN.js.map → chunk-HB3RFQSC.js.map} +0 -0
  896. /package/dist/{chunk-FC3XK5JT.js.map → chunk-I6GALP6E.js.map} +0 -0
  897. /package/dist/{chunk-LEOXZY36.js.map → chunk-IL6POWCW.js.map} +0 -0
  898. /package/dist/{chunk-GX2BYNCT.js.map → chunk-IU6SNYX6.js.map} +0 -0
  899. /package/dist/{chunk-6CU3FN74.js.map → chunk-J2ZX2XUV.js.map} +0 -0
  900. /package/dist/{chunk-6MWUANRF.js.map → chunk-JKJOGPZN.js.map} +0 -0
  901. /package/dist/{chunk-PEDZFFKW.js.map → chunk-JZUV47FZ.js.map} +0 -0
  902. /package/dist/{chunk-5TZZUL5X.js.map → chunk-K5IMOGHB.js.map} +0 -0
  903. /package/dist/{chunk-7IZQTCTR.js.map → chunk-LASZKVMO.js.map} +0 -0
  904. /package/dist/{chunk-NNWK3PSY.js.map → chunk-NR5LT5RY.js.map} +0 -0
  905. /package/dist/{chunk-NYVQCZ54.js.map → chunk-NWSGI5C7.js.map} +0 -0
  906. /package/dist/{chunk-MSVY7ULZ.js.map → chunk-Q5VNKNE5.js.map} +0 -0
  907. /package/dist/{chunk-2HVUKRNE.js.map → chunk-QLWMEXCT.js.map} +0 -0
  908. /package/dist/{chunk-KYC4LIEU.js.map → chunk-QRB7NAOJ.js.map} +0 -0
  909. /package/dist/{chunk-6ULCYCMU.js.map → chunk-R4ROFEYC.js.map} +0 -0
  910. /package/dist/{chunk-KBF5GSGX.js.map → chunk-RIX4V2TB.js.map} +0 -0
  911. /package/dist/{chunk-CTSFKDGA.js.map → chunk-RYCTDFUX.js.map} +0 -0
  912. /package/dist/{chunk-S6AQXDN4.js.map → chunk-RZ5SVOYR.js.map} +0 -0
  913. /package/dist/{chunk-A4MAD6PO.js.map → chunk-SLFHQTTW.js.map} +0 -0
  914. /package/dist/{chunk-AYGZMQP5.js.map → chunk-T2FFWNPK.js.map} +0 -0
  915. /package/dist/{chunk-CWCCYH34.js.map → chunk-UNFPPVNI.js.map} +0 -0
  916. /package/dist/{chunk-M5I354EE.js.map → chunk-VLFCJYTL.js.map} +0 -0
  917. /package/dist/{chunk-WVCQ75OK.js.map → chunk-VN3DBQAU.js.map} +0 -0
  918. /package/dist/{chunk-KXKLKJW4.js.map → chunk-VZRODIM3.js.map} +0 -0
  919. /package/dist/{chunk-HBTT7YQX.js.map → chunk-W22IKJE2.js.map} +0 -0
  920. /package/dist/{chunk-KNXN47MO.js.map → chunk-X3YYQWOQ.js.map} +0 -0
  921. /package/dist/{chunk-DFIBOSOQ.js.map → chunk-YFXZEAYK.js.map} +0 -0
  922. /package/dist/{chunk-ICV32FPK.js.map → chunk-ZAJAVEXY.js.map} +0 -0
  923. /package/dist/{chunk-MK2BSS44.js.map → chunk-ZICD77GM.js.map} +0 -0
  924. /package/dist/{chunk-KWLD2D75.js.map → chunk-ZJ2AK4V7.js.map} +0 -0
  925. /package/dist/{chunk-KBBWY46Y.js.map → chunk-ZUODRVHY.js.map} +0 -0
  926. /package/dist/{api-HV4PDUWF.js.map → collection-facade-TLWIPI76.js.map} +0 -0
  927. /package/dist/{chunk-BGIZAFY7.js.map → computed-ZIQTYTDI.js.map} +0 -0
  928. /package/dist/{dead-filter-VNA4M3GP.js.map → dead-filter-ESOBZZP5.js.map} +0 -0
  929. /package/dist/{chunk-K2WCO3NX.js.map → delegation-W7ZV2322.js.map} +0 -0
  930. /package/dist/{collection-facade-3RAXG2OU.js.map → derive-H2BJTLUI.js.map} +0 -0
  931. /package/dist/{computed-R62HGCO4.js.map → enclave-AP3I5W7O.js.map} +0 -0
  932. /package/dist/{delegation-ZKII5UBI.js.map → executor-KGWTNEQD.js.map} +0 -0
  933. /package/dist/{derive-5D2C3WG4.js.map → executor-OVEXR6RT.js.map} +0 -0
  934. /package/dist/{describe/index.js.map → executor-UNEH5ZYH.js.map} +0 -0
  935. /package/dist/{enclave-EUBEJGPG.js.map → export-accessible-PCMXSUZS.js.map} +0 -0
  936. /package/dist/{executor-IL7SRNUZ.js.map → extract-partition-SWHFSYJO.js.map} +0 -0
  937. /package/dist/{fanout-sidecar-CI5BCMQM.js.map → fanout-sidecar-A765THMX.js.map} +0 -0
  938. /package/dist/{executor-OUA2Y6YC.js.map → find-N6345CNT.js.map} +0 -0
  939. /package/dist/{executor-WAEI6PDV.js.map → issue-5OKB6I2R.js.map} +0 -0
  940. /package/dist/{export-accessible-O55RKAGC.js.map → ledger-GFBKVPV4.js.map} +0 -0
  941. /package/dist/{extract-partition-VBGCBA6Q.js.map → liberate-3VDOT4KO.js.map} +0 -0
  942. /package/dist/{find-FISGUWSK.js.map → link-set-Z7Q2ASFX.js.map} +0 -0
  943. /package/dist/{issue-EQAWIYFQ.js.map → noydb-ZCJOYBDI.js.map} +0 -0
  944. /package/dist/{kernel/index.js.map → policy-Z3RUAIY2.js.map} +0 -0
  945. /package/dist/{ledger-AJA2SR3C.js.map → public-envelope-B4KNWVUA.js.map} +0 -0
  946. /package/dist/{liberate-SEGR4JLY.js.map → read-only-facade-CHCKSEPD.js.map} +0 -0
  947. /package/dist/{link-set-YBAC2DQT.js.map → register-VAHONCID.js.map} +0 -0
  948. /package/dist/{noydb-AMEOU456.js.map → registry-7MLVD3VO.js.map} +0 -0
  949. /package/dist/{policy-SDGRWVIL.js.map → registry-EPKPD5SM.js.map} +0 -0
  950. /package/dist/{public-envelope-J5PLQOW2.js.map → registry-P5T7PS2U.js.map} +0 -0
  951. /package/dist/{read-only-facade-UCYNRE56.js.map → request-withdrawal-TI3A5WPS.js.map} +0 -0
  952. /package/dist/{register-OVJKW7BS.js.map → revoke-FDRD6GRW.js.map} +0 -0
  953. /package/dist/{registry-5RJSK5AJ.js.map → signer-MUWEIUI6.js.map} +0 -0
  954. /package/dist/{registry-DQRNTWRL.js.map → stale-OSQ7J3AA.js.map} +0 -0
  955. /package/dist/{registry-LTGLERS7.js.map → storage-MVFCGEQE.js.map} +0 -0
  956. /package/dist/{store-coordination-provider-WKGAE2WI.js.map → store-coordination-provider-64OEZFGS.js.map} +0 -0
  957. /package/dist/{walk-76UCFXYV.js.map → walk-SVNDIZ62.js.map} +0 -0
  958. /package/dist/{request-withdrawal-27O77D2V.js.map → withdraw-accessible-WZIEVWSR.js.map} +0 -0
@@ -0,0 +1,2782 @@
1
+ import {
2
+ assertTierAccess,
3
+ dekKey
4
+ } from "./chunk-ZICD77GM.js";
5
+ import {
6
+ liveRecordIsElevated,
7
+ liveRecordTier
8
+ } from "./chunk-PN2XMATY.js";
9
+ import {
10
+ openEnvelopeJson,
11
+ writeEnvelopeBody
12
+ } from "./chunk-XDYREZWE.js";
13
+ import {
14
+ base64ToBuffer,
15
+ bufferToBase64,
16
+ decryptBytesWithAAD,
17
+ encrypt,
18
+ encryptBytesWithAAD,
19
+ generateDEK,
20
+ hmacSha256Hex,
21
+ sha256Hex,
22
+ unwrapCek,
23
+ wrapCek
24
+ } from "./chunk-CIE2BASH.js";
25
+ import {
26
+ NOYDB_FORMAT_VERSION
27
+ } from "./chunk-JNAI2HN2.js";
28
+ import {
29
+ BlobIntentPendingError,
30
+ ConflictError,
31
+ NotFoundError,
32
+ TamperedError,
33
+ TierNotGrantedError,
34
+ UnsupportedTierCompositionError,
35
+ ValidationError
36
+ } from "./chunk-BIOUI4HL.js";
37
+
38
+ // src/with-shape/blobs/mime-magic.ts
39
+ function hex(s) {
40
+ return new Uint8Array(s.split(" ").map((b) => parseInt(b, 16)));
41
+ }
42
+ var MAGIC_RULES = [
43
+ // ── Images ───────────────────────────────────────────────────────────
44
+ // #2 PNG — full 8-byte signature (RFC 2083)
45
+ { mime: "image/png", format: "PNG", bytes: hex("89 50 4E 47 0D 0A 1A 0A"), preCompressed: true },
46
+ // #1 JPEG — FF D8 FF (third byte is start of APP marker, always FF)
47
+ { mime: "image/jpeg", format: "JPEG", bytes: hex("FF D8 FF"), preCompressed: true },
48
+ // #7 WebP — RIFF compound: bytes 0-3 = RIFF, bytes 8-11 = WEBP
49
+ {
50
+ mime: "image/webp",
51
+ format: "WebP",
52
+ bytes: hex("52 49 46 46"),
53
+ secondaryBytes: hex("57 45 42 50"),
54
+ secondaryOffset: 8,
55
+ preCompressed: true
56
+ },
57
+ // #5 TIFF (little-endian) — II + version 42
58
+ { mime: "image/tiff", format: "TIFF", bytes: hex("49 49 2A 00") },
59
+ // #6 TIFF (big-endian) — MM + version 42
60
+ { mime: "image/tiff", format: "TIFF", bytes: hex("4D 4D 00 2A") },
61
+ // #3 GIF — GIF8 (covers GIF87a and GIF89a)
62
+ { mime: "image/gif", format: "GIF", bytes: hex("47 49 46 38"), preCompressed: true },
63
+ // #4 BMP — BM
64
+ { mime: "image/bmp", format: "BMP", bytes: hex("42 4D") },
65
+ // PSD — 8BPS
66
+ { mime: "image/vnd.adobe.photoshop", format: "PSD", bytes: hex("38 42 50 53") },
67
+ // #8 ICO — 00 00 01 00 (note: 00 00 02 00 is CUR cursor format)
68
+ { mime: "image/x-icon", format: "ICO", bytes: hex("00 00 01 00") },
69
+ // #9 HEIC — ISOBMFF: ftyp at offset 4, brand "heic" at offset 8
70
+ {
71
+ mime: "image/heic",
72
+ format: "HEIC",
73
+ bytes: hex("66 74 79 70"),
74
+ offset: 4,
75
+ secondaryBytes: hex("68 65 69 63"),
76
+ secondaryOffset: 8,
77
+ preCompressed: true
78
+ },
79
+ // ── Documents ────────────────────────────────────────────────────────
80
+ // PDF — %PDF
81
+ { mime: "application/pdf", format: "PDF", bytes: hex("25 50 44 46") },
82
+ // RTF — {\rtf
83
+ { mime: "application/rtf", format: "RTF", bytes: hex("7B 5C 72 74 66") },
84
+ // ── Archives & compression ───────────────────────────────────────────
85
+ // RAR v5 — 8-byte signature (test before RAR v4)
86
+ { mime: "application/vnd.rar", format: "RAR v5", bytes: hex("52 61 72 21 1A 07 01 00"), preCompressed: true },
87
+ // RAR v4 — 7-byte signature
88
+ { mime: "application/vnd.rar", format: "RAR v4", bytes: hex("52 61 72 21 1A 07 00"), preCompressed: true },
89
+ // 7-Zip — 6-byte signature
90
+ { mime: "application/x-7z-compressed", format: "7Z", bytes: hex("37 7A BC AF 27 1C"), preCompressed: true },
91
+ // XZ — 6-byte stream header
92
+ { mime: "application/x-xz", format: "XZ", bytes: hex("FD 37 7A 58 5A 00"), preCompressed: true },
93
+ // ZIP — PK\x03\x04 (local file header)
94
+ { mime: "application/zip", format: "ZIP", bytes: hex("50 4B 03 04"), preCompressed: true },
95
+ // GZIP — 1F 8B
96
+ { mime: "application/gzip", format: "GZIP", bytes: hex("1F 8B"), preCompressed: true },
97
+ // BZIP2 — BZh
98
+ { mime: "application/x-bzip2", format: "BZIP2", bytes: hex("42 5A 68"), preCompressed: true },
99
+ // LZIP — LZIP
100
+ { mime: "application/x-lzip", format: "LZIP", bytes: hex("4C 5A 49 50"), preCompressed: true },
101
+ // ── Audio ────────────────────────────────────────────────────────────
102
+ // WAV — RIFF compound: bytes 0-3 = RIFF, bytes 8-11 = WAVE
103
+ {
104
+ mime: "audio/wav",
105
+ format: "WAV",
106
+ bytes: hex("52 49 46 46"),
107
+ secondaryBytes: hex("57 41 56 45"),
108
+ secondaryOffset: 8
109
+ },
110
+ // AIFF — FORM compound: bytes 0-3 = FORM, bytes 8-11 = AIFF
111
+ {
112
+ mime: "audio/aiff",
113
+ format: "AIFF",
114
+ bytes: hex("46 4F 52 4D"),
115
+ secondaryBytes: hex("41 49 46 46"),
116
+ secondaryOffset: 8
117
+ },
118
+ // FLAC — fLaC
119
+ { mime: "audio/flac", format: "FLAC", bytes: hex("66 4C 61 43") },
120
+ // OGG — OggS (container — may hold Vorbis, Opus, Theora, etc.)
121
+ { mime: "application/ogg", format: "OGG", bytes: hex("4F 67 67 53") },
122
+ // MIDI — MThd
123
+ { mime: "audio/midi", format: "MIDI", bytes: hex("4D 54 68 64") },
124
+ // MP3 (ID3-tagged) — ID3
125
+ { mime: "audio/mpeg", format: "MP3", bytes: hex("49 44 33"), preCompressed: true },
126
+ // ── Video ────────────────────────────────────────────────────────────
127
+ // AVI — RIFF compound: bytes 0-3 = RIFF, bytes 8-11 = AVI\x20
128
+ {
129
+ mime: "video/x-msvideo",
130
+ format: "AVI",
131
+ bytes: hex("52 49 46 46"),
132
+ secondaryBytes: hex("41 56 49 20"),
133
+ secondaryOffset: 8,
134
+ preCompressed: true
135
+ },
136
+ // WMV/ASF — 8-byte ASF header GUID prefix
137
+ { mime: "video/x-ms-wmv", format: "WMV", bytes: hex("30 26 B2 75 8E 66 CF 11"), preCompressed: true },
138
+ // MKV/WebM — EBML header (Matroska container)
139
+ { mime: "video/x-matroska", format: "MKV", bytes: hex("1A 45 DF A3"), preCompressed: true },
140
+ // FLV — FLV
141
+ { mime: "video/x-flv", format: "FLV", bytes: hex("46 4C 56"), preCompressed: true },
142
+ // MOV — ISOBMFF: ftyp at offset 4, brand "qt " at offset 8
143
+ {
144
+ mime: "video/quicktime",
145
+ format: "MOV",
146
+ bytes: hex("66 74 79 70"),
147
+ offset: 4,
148
+ secondaryBytes: hex("71 74 20 20"),
149
+ secondaryOffset: 8,
150
+ preCompressed: true
151
+ },
152
+ // MP4 — ISOBMFF: ftyp at offset 4 (brands vary: isom, mp41, mp42, etc.)
153
+ // Tested AFTER MOV and HEIC so their specific brands match first.
154
+ { mime: "video/mp4", format: "MP4", bytes: hex("66 74 79 70"), offset: 4, preCompressed: true },
155
+ // ── Executables & binaries ───────────────────────────────────────────
156
+ // SQLite — "SQLite 3" (first 8 bytes of the 16-byte header)
157
+ { mime: "application/vnd.sqlite3", format: "SQLite", bytes: hex("53 51 4C 69 74 65 20 33") },
158
+ // WASM — \0asm
159
+ { mime: "application/wasm", format: "WASM", bytes: hex("00 61 73 6D") },
160
+ // ELF — \x7FELF
161
+ { mime: "application/x-elf", format: "ELF", bytes: hex("7F 45 4C 46") },
162
+ // PE (EXE/DLL) — MZ
163
+ { mime: "application/vnd.microsoft.portable-executable", format: "PE", bytes: hex("4D 5A") },
164
+ // Mach-O — all four single-arch variants
165
+ { mime: "application/x-mach-binary", format: "Mach-O 64 LE", bytes: hex("CF FA ED FE") },
166
+ { mime: "application/x-mach-binary", format: "Mach-O 64 BE", bytes: hex("FE ED FA CF") },
167
+ { mime: "application/x-mach-binary", format: "Mach-O 32 LE", bytes: hex("CE FA ED FE") },
168
+ { mime: "application/x-mach-binary", format: "Mach-O 32 BE", bytes: hex("FE ED FA CE") },
169
+ // Java Class — CA FE BA BE
170
+ // Note: collides with Mach-O Universal Binary. Disambiguated by checking
171
+ // bytes 4-7: Java class version is >= 0x002D (45), while fat binary
172
+ // arch count is a small number (typically 0x00000002).
173
+ // We place Java after Mach-O single-arch entries so the more common
174
+ // Mach-O variants match first. The CA FE BA BE collision between Java
175
+ // and Mach-O fat binary is resolved by the caller if needed.
176
+ { mime: "application/java-vm", format: "Java Class", bytes: hex("CA FE BA BE") },
177
+ // DEX — dex\n (Android Dalvik Executable)
178
+ { mime: "application/vnd.android.dex", format: "DEX", bytes: hex("64 65 78 0A") },
179
+ // ── Package formats ──────────────────────────────────────────────────
180
+ // DEB — !<arch> (ar archive; DEB-specific member follows)
181
+ { mime: "application/vnd.debian.binary-package", format: "DEB", bytes: hex("21 3C 61 72 63 68 3E") },
182
+ // RPM — ED AB EE DB
183
+ { mime: "application/x-rpm", format: "RPM", bytes: hex("ED AB EE DB") },
184
+ // CAB — MSCF
185
+ { mime: "application/vnd.ms-cab-compressed", format: "CAB", bytes: hex("4D 53 43 46"), preCompressed: true },
186
+ // ── Capture & Flash ──────────────────────────────────────────────────
187
+ // PCAP (little-endian) — D4 C3 B2 A1
188
+ { mime: "application/vnd.tcpdump.pcap", format: "PCAP", bytes: hex("D4 C3 B2 A1") },
189
+ // PCAP (big-endian) — A1 B2 C3 D4
190
+ { mime: "application/vnd.tcpdump.pcap", format: "PCAP BE", bytes: hex("A1 B2 C3 D4") },
191
+ // PCAPNG — Section Header Block
192
+ { mime: "application/x-pcapng", format: "PCAPNG", bytes: hex("0A 0D 0D 0A") },
193
+ // SWF — all three variants (uncompressed, zlib, LZMA)
194
+ { mime: "application/x-shockwave-flash", format: "SWF", bytes: hex("46 57 53") },
195
+ { mime: "application/x-shockwave-flash", format: "SWF zlib", bytes: hex("43 57 53"), preCompressed: true },
196
+ { mime: "application/x-shockwave-flash", format: "SWF LZMA", bytes: hex("5A 57 53"), preCompressed: true },
197
+ // ── Data formats ─────────────────────────────────────────────────────
198
+ // Parquet — PAR1 (no registered IANA MIME; using Apache's informal type)
199
+ { mime: "application/vnd.apache.parquet", format: "Parquet", bytes: hex("50 41 52 31") },
200
+ // Avro Object Container — Obj\x01
201
+ { mime: "application/avro", format: "Avro", bytes: hex("4F 62 6A 01") },
202
+ // NES ROM — NES\x1A (iNES header)
203
+ { mime: "application/x-nintendo-nes-rom", format: "NES ROM", bytes: hex("4E 45 53 1A") }
204
+ ];
205
+ function isMp3SyncWord(byte0, byte1) {
206
+ return byte0 === 255 && (byte1 & 224) === 224;
207
+ }
208
+ function detectMimeType(header) {
209
+ const result = detectMagic(header);
210
+ return result?.mime ?? "application/octet-stream";
211
+ }
212
+ function detectMagic(header) {
213
+ for (const rule of MAGIC_RULES) {
214
+ if (matchRule(header, rule)) {
215
+ return {
216
+ mime: rule.mime,
217
+ format: rule.format,
218
+ preCompressed: rule.preCompressed ?? false
219
+ };
220
+ }
221
+ }
222
+ if (header.length >= 2 && isMp3SyncWord(header[0], header[1])) {
223
+ return { mime: "audio/mpeg", format: "MP3", preCompressed: true };
224
+ }
225
+ return null;
226
+ }
227
+ function isPreCompressed(mimeType) {
228
+ return PRE_COMPRESSED_MIMES.has(mimeType);
229
+ }
230
+ function matchRule(header, rule) {
231
+ const offset = rule.offset ?? 0;
232
+ const end = offset + rule.bytes.length;
233
+ if (header.length < end) return false;
234
+ for (let i = 0; i < rule.bytes.length; i++) {
235
+ if (header[offset + i] !== rule.bytes[i]) return false;
236
+ }
237
+ if (rule.secondaryBytes && rule.secondaryOffset !== void 0) {
238
+ const sEnd = rule.secondaryOffset + rule.secondaryBytes.length;
239
+ if (header.length < sEnd) return false;
240
+ for (let i = 0; i < rule.secondaryBytes.length; i++) {
241
+ if (header[rule.secondaryOffset + i] !== rule.secondaryBytes[i]) return false;
242
+ }
243
+ }
244
+ return true;
245
+ }
246
+ var PRE_COMPRESSED_MIMES = new Set(
247
+ MAGIC_RULES.filter((r) => r.preCompressed).map((r) => r.mime)
248
+ );
249
+
250
+ // src/with-shape/blobs/blob-intent.ts
251
+ var BLOB_INTENT_COLLECTION = "_blob_intent";
252
+ function assertIntentKeyPartSafe(part, what) {
253
+ if (part.includes("::") || part.startsWith(":") || part.endsWith(":")) {
254
+ throw new ValidationError(
255
+ `Blob intent ${what} "${part}" must not contain '::' nor start/end with ':' (reserved as the _blob_intent marker-key separator; #753)`
256
+ );
257
+ }
258
+ }
259
+ function intentKey(collection, recordId) {
260
+ assertIntentKeyPartSafe(collection, "collection");
261
+ assertIntentKeyPartSafe(recordId, "record id");
262
+ return `${collection}::${recordId}`;
263
+ }
264
+ function parseIntentKey(key) {
265
+ const idx = key.indexOf("::");
266
+ if (idx === -1) return null;
267
+ return { collection: key.slice(0, idx), recordId: key.slice(idx + 2) };
268
+ }
269
+ async function encodeIntentEnvelope(intent, dek, newVersion = 1) {
270
+ const json = JSON.stringify(intent);
271
+ const body = await writeEnvelopeBody(json, dek);
272
+ return {
273
+ _noydb: NOYDB_FORMAT_VERSION,
274
+ _v: newVersion,
275
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
276
+ ...body
277
+ };
278
+ }
279
+ async function decodeIntentEnvelope(envelope, dek) {
280
+ const json = await openEnvelopeJson(envelope, dek);
281
+ return JSON.parse(json);
282
+ }
283
+ async function getIntent(adapter, vault, collection, recordId, getDEK) {
284
+ const envelope = await adapter.get(vault, BLOB_INTENT_COLLECTION, intentKey(collection, recordId));
285
+ if (!envelope) return null;
286
+ const dek = await getDEK(collection);
287
+ return decodeIntentEnvelope(envelope, dek);
288
+ }
289
+ async function createIntent(adapter, vault, collection, recordId, getDEK, intent) {
290
+ const key = intentKey(collection, recordId);
291
+ const dek = await getDEK(collection);
292
+ const envelope = await encodeIntentEnvelope(intent, dek);
293
+ try {
294
+ await adapter.put(vault, BLOB_INTENT_COLLECTION, key, envelope, 0);
295
+ } catch (err) {
296
+ if (err instanceof ConflictError) {
297
+ throw new BlobIntentPendingError(collection, recordId);
298
+ }
299
+ throw err;
300
+ }
301
+ }
302
+ async function deleteIntent(adapter, vault, collection, recordId) {
303
+ await adapter.delete(vault, BLOB_INTENT_COLLECTION, intentKey(collection, recordId));
304
+ }
305
+ var MAX_APPLIED_STAMP_RETRIES = 5;
306
+ async function recordAppliedStamp(adapter, vault, collection, recordId, getDEK, stamp) {
307
+ const key = intentKey(collection, recordId);
308
+ const dek = await getDEK(collection);
309
+ for (let attempt = 0; attempt < MAX_APPLIED_STAMP_RETRIES; attempt++) {
310
+ const envelope = await adapter.get(vault, BLOB_INTENT_COLLECTION, key);
311
+ if (!envelope) return;
312
+ const intent = await decodeIntentEnvelope(envelope, dek);
313
+ if (intent.op !== "rehome" || intent.appliedStamps?.includes(stamp)) return;
314
+ const updated = { ...intent, appliedStamps: [...intent.appliedStamps ?? [], stamp] };
315
+ const newEnvelope = await encodeIntentEnvelope(updated, dek, envelope._v + 1);
316
+ try {
317
+ await adapter.put(vault, BLOB_INTENT_COLLECTION, key, newEnvelope, envelope._v);
318
+ return;
319
+ } catch (err) {
320
+ if (err instanceof ConflictError && attempt < MAX_APPLIED_STAMP_RETRIES - 1) continue;
321
+ throw err;
322
+ }
323
+ }
324
+ }
325
+ async function sweepBlobIntents(adapter, vault, getDEK, resume, onResumeError) {
326
+ const keys = await adapter.list(vault, BLOB_INTENT_COLLECTION);
327
+ for (const key of keys) {
328
+ const parsed = parseIntentKey(key);
329
+ if (!parsed) continue;
330
+ try {
331
+ const envelope = await adapter.get(vault, BLOB_INTENT_COLLECTION, key);
332
+ if (!envelope) continue;
333
+ const dek = await getDEK(parsed.collection);
334
+ const intent = await decodeIntentEnvelope(envelope, dek);
335
+ await resume(parsed.collection, parsed.recordId, intent);
336
+ } catch (err) {
337
+ onResumeError?.(parsed.collection, parsed.recordId, err);
338
+ }
339
+ }
340
+ }
341
+
342
+ // src/with-shape/blobs/blob-set.ts
343
+ var BLOB_COLLECTION = "_blob";
344
+ var BLOB_INDEX_COLLECTION = "_blob_index";
345
+ var BLOB_CHUNKS_COLLECTION = "_blob_chunks";
346
+ var BLOB_SLOTS_PREFIX = "_blob_slots_";
347
+ var BLOB_VERSIONS_PREFIX = "_blob_versions_";
348
+ var DEFAULT_CHUNK_SIZE = 256 * 1024;
349
+ var MAX_CAS_RETRIES = 5;
350
+ var LAST_OPS_RING_SIZE = 8;
351
+ function appendStamp(lastOps, stamp) {
352
+ const next = [...lastOps ?? [], stamp];
353
+ return next.length > LAST_OPS_RING_SIZE ? next.slice(next.length - LAST_OPS_RING_SIZE) : next;
354
+ }
355
+ var EMPTY_STAMP_SET = /* @__PURE__ */ new Set();
356
+ function mintOpId() {
357
+ const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
358
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
359
+ }
360
+ async function compressBytes(data) {
361
+ if (typeof CompressionStream === "undefined") {
362
+ return { bytes: data, algorithm: "none" };
363
+ }
364
+ const piped = new Response(data).body.pipeThrough(new CompressionStream("gzip"));
365
+ const buf = await new Response(piped).arrayBuffer();
366
+ return { bytes: new Uint8Array(buf), algorithm: "gzip" };
367
+ }
368
+ async function decompressBytes(data) {
369
+ if (typeof DecompressionStream === "undefined") {
370
+ throw new Error(
371
+ "[noy-db] DecompressionStream not available \u2014 cannot decompress blob chunk"
372
+ );
373
+ }
374
+ const piped = new Response(data).body.pipeThrough(new DecompressionStream("gzip"));
375
+ const buf = await new Response(piped).arrayBuffer();
376
+ return new Uint8Array(buf);
377
+ }
378
+ function concatChunks(chunks) {
379
+ const total = chunks.reduce((s, c) => s + c.byteLength, 0);
380
+ const out = new Uint8Array(total);
381
+ let offset = 0;
382
+ for (const c of chunks) {
383
+ out.set(c, offset);
384
+ offset += c.byteLength;
385
+ }
386
+ return out;
387
+ }
388
+ function chunkAAD(eTag, chunkIndex, chunkCount) {
389
+ return new TextEncoder().encode(`${eTag}:${chunkIndex}:${chunkCount}`);
390
+ }
391
+ var BlobSet = class _BlobSet {
392
+ store;
393
+ vault;
394
+ collection;
395
+ recordId;
396
+ getDEK;
397
+ encrypted;
398
+ userId;
399
+ maxBlobBytes;
400
+ erasableBlobs;
401
+ tiersActive;
402
+ debugPlaintext;
403
+ objectStore;
404
+ blobFields;
405
+ keyring;
406
+ /**
407
+ * Set only on the cleared clone `atTier()` returns (#749) — never by
408
+ * `openSlot`/`Collection.blob()`. Its presence is what makes a `BlobSet`
409
+ * a cleared view: see `ownerRecordElevated()`/`ownerTier()`.
410
+ */
411
+ clearedTier;
412
+ constructor(opts) {
413
+ this.store = opts.store;
414
+ this.vault = opts.vault;
415
+ this.collection = opts.collection;
416
+ this.recordId = opts.recordId;
417
+ this.getDEK = opts.getDEK;
418
+ this.encrypted = opts.encrypted;
419
+ this.userId = opts.userId;
420
+ this.maxBlobBytes = opts.maxBlobBytes;
421
+ this.erasableBlobs = opts.erasableBlobs === true;
422
+ this.tiersActive = opts.tiersActive === true;
423
+ this.debugPlaintext = opts.debugPlaintext === true;
424
+ this.objectStore = opts.objectStore;
425
+ this.blobFields = opts.blobFields;
426
+ this.keyring = opts.keyring;
427
+ this.clearedTier = opts.clearedTier;
428
+ }
429
+ /**
430
+ * #724 I1 completion: a legacy (non-`perRecordKeys`) blob has no per-blob
431
+ * `_cek`, so `rehomeForTier` can never tier-isolate it on elevate/demote —
432
+ * it stays decryptable under the flat tier-0 `_blob` DEK at rest even
433
+ * after the owning record is elevated. The construction-time mandate
434
+ * (`resolveCollectionConfig`) only catches this when `blobFields` is
435
+ * DECLARED; a collection with an undeclared field (or no `blobFields` at
436
+ * all) constructs fine and reaches this write path unguarded. Refuse the
437
+ * write itself instead of broadening the construction mandate, which
438
+ * would over-refuse blobless tiered collections (`blobStrategy` is
439
+ * vault-wide, not per-collection). Called from every content-write entry
440
+ * (`put()`, `publish()`) — never from a read path, so a pre-existing
441
+ * legacy blob stays readable (back-compat).
442
+ */
443
+ assertBlobWritable() {
444
+ if (this.tiersActive && !this.erasableBlobs) {
445
+ throw new UnsupportedTierCompositionError(
446
+ "blobs",
447
+ `Collection "${this.collection}": writing a blob to a tiered collection requires perRecordKeys (legacy blobs have no per-record key and cannot be tier-isolated) (#724).`
448
+ );
449
+ }
450
+ }
451
+ /**
452
+ * #748: `adoptExternal()`/`setExternalMeta()` write an `external` slot
453
+ * reference directly — unlike `put()`, which only takes the external path
454
+ * when `blobFields[slotName].external` is declared (the construction-time
455
+ * tiers×blobFields mandate, `collection-config.ts`, then applies). Without
456
+ * this gate, either method can attach an external reference to a slot the
457
+ * collection never declared `external` — bypassing that mandate on a tiered
458
+ * collection. Mirrors `put()`'s own gate.
459
+ */
460
+ assertExternalDeclared(slotName) {
461
+ if (!this.blobFields?.[slotName]?.external) {
462
+ throw new ValidationError(
463
+ `Collection "${this.collection}": blob field "${slotName}" is not declared external (blobFields["${slotName}"].external must be true) \u2014 cannot attach an external reference to an undeclared slot (#748).`
464
+ );
465
+ }
466
+ }
467
+ /**
468
+ * Resolve the key the blob's CHUNKS are encrypted under.
469
+ *
470
+ * - `_cek` present (erasable blob) → unwrap the per-blob content CEK under
471
+ * `blobDEK`. Deleting the BlobObject (at `refCount → 0`) makes this
472
+ * key unrecoverable → the chunks are crypto-shredded.
473
+ * - `_cek` absent (legacy) → `blobDEK` encrypts chunks directly.
474
+ * - unencrypted vault → `null` (chunks stored as plaintext base64).
475
+ *
476
+ * `blobDEK` defaults to this record's own (tier-0) `_blob` DEK — every
477
+ * pre-#724-T3 call site. `rehomeForTier`'s isolate-fork path (#724 Arc 10
478
+ * Task 3) passes the `fromTier`-scoped DEK explicitly to read a shared
479
+ * blob's plaintext under the key it's CURRENTLY wrapped under, reusing
480
+ * this one `_cek` unwrap site rather than adding a new raw access.
481
+ */
482
+ async resolveChunkKey(blob, blobDEK) {
483
+ if (!this.encrypted) return null;
484
+ const dek = blobDEK ?? await this.getDEK(BLOB_COLLECTION);
485
+ return blob._cek !== void 0 ? await unwrapCek(blob._cek, dek) : dek;
486
+ }
487
+ /**
488
+ * #724 Arc 10 Task 1: the same "elevated ≡ invisible" clearance check
489
+ * `collection.get()` applies to the owning record (`liveRecordIsElevated`,
490
+ * #701/#707/#709/#712) — reused here, not reinvented, so a blob content
491
+ * read on an elevated record is refused exactly like a `get()` on it
492
+ * would be. Reads only the owning record's `_tier` envelope metadata;
493
+ * runs before any blob decrypt.
494
+ *
495
+ * #749: a cleared clone (`this.clearedTier !== undefined`, produced only by
496
+ * `atTier()`) short-circuits to `false` — its whole purpose is to see past
497
+ * this gate, having already paid the `assertTierAccess` cost `atTier()`
498
+ * charged to get here. No live envelope peek needed on that path.
499
+ */
500
+ async ownerRecordElevated() {
501
+ if (this.clearedTier !== void 0) return false;
502
+ return liveRecordIsElevated(this.store, this.vault, this.collection, this.recordId);
503
+ }
504
+ /**
505
+ * #724 Arc 10 Task 4: the tier this record's LIVE envelope currently
506
+ * carries — `loadSlots`/`saveSlots` default to resolving the slot map's
507
+ * collection DEK here, so the slot map (filenames/eTags/mimeTypes) is
508
+ * read/written wherever a prior `rehomeForTier` call physically re-keyed
509
+ * it to. `rehomeForTier` itself passes an EXPLICIT tier instead of
510
+ * relying on this default (see its own doc comment): by the time it
511
+ * runs, the owning record's live `_tier` has ALREADY moved to `toTier`
512
+ * (the collection-level `elevate`/`demote`/`putAtTier` writes the record
513
+ * before invoking `syncBlobs`), while the slot map is still physically at
514
+ * `fromTier` until `rehomeForTier`'s own move step lands — so this peek
515
+ * would resolve the wrong DEK mid-move.
516
+ *
517
+ * #749: a cleared clone reports its fixed `clearedTier` instead of
518
+ * re-peeking — a stable view for the caller's whole session with it. A
519
+ * concurrent demote/elevate on the underlying record is invisible to an
520
+ * already-cleared handle; call `atTier()` again to see it. This is not a
521
+ * silent-corruption risk: a WRITE through a stale clone after a concurrent
522
+ * move fails loud, not quiet — the slot map/index envelope has already
523
+ * been physically re-keyed to the record's new tier by `rehomeForTier`, so
524
+ * the stale clone's `clearedTier`-pinned DEK can no longer open it and the
525
+ * write throws an AEAD decrypt error rather than silently writing under
526
+ * the wrong key.
527
+ */
528
+ async ownerTier() {
529
+ if (this.clearedTier !== void 0) return this.clearedTier;
530
+ return liveRecordTier(this.store, this.vault, this.collection, this.recordId);
531
+ }
532
+ /**
533
+ * The sanctioned cleared-read path to an elevated record's blobs (#749).
534
+ *
535
+ * The law: `blob(id)` is the tier-0 surface — `get()`/`list()`/`blobInfo()`/
536
+ * etc. all hide an elevated record's blobs from EVERYONE, unconditionally
537
+ * (`ownerRecordElevated()`, #724 Task 1), with no cleared path of their own.
538
+ * `atTier()` is that cleared path, the `getAtTier()` analogue for blobs:
539
+ * live-peeks the owning record's tier, and for `tier > 0` runs
540
+ * `assertTierAccess` — the SAME gate `putAtTier`/`elevate`/`demote` run
541
+ * before ever touching a tier DEK — then returns a NEW `BlobSet` bound to
542
+ * that tier. Every subsequent call on the returned handle (`get()`,
543
+ * `list()`, `publish()`, …) sees through the gate as if the record were at
544
+ * tier 0.
545
+ *
546
+ * `assertTierAccess` — not a bare `getDEK` call — IS the authorization
547
+ * check: owner/admin/custodian bypass it (their on-demand tier-DEK mint is
548
+ * sanctioned), everyone else must already hold the tier DEK (via a prior
549
+ * grant or delegation) or it throws `TierNotGrantedError`. A bare `getDEK`
550
+ * would not gate anything here — it silently mints a fresh DEK for ANY
551
+ * caller when one is missing, which is exactly the "non-cleared caller
552
+ * creating tier key material inside the trust boundary" hazard
553
+ * `assertTierAccess`'s own doc comment (`with-party/team/tiers.ts`) warns
554
+ * against; running it BEFORE any `getDEK` call is what keeps an ungranted
555
+ * member's failed `atTier()` call from minting junk key material into
556
+ * their keyring as a side effect. No tier DEK is resolved here at all —
557
+ * every actual read still resolves its DEK lazily, same as today.
558
+ *
559
+ * `this.keyring` is undefined only on a construction path that never
560
+ * threads one through `openSlot` (`Collection.blob()` always does) — that
561
+ * can't prove clearance for anyone, so it throws the same
562
+ * `TierNotGrantedError` an ungranted caller would get.
563
+ */
564
+ async atTier() {
565
+ const tier = await liveRecordTier(this.store, this.vault, this.collection, this.recordId);
566
+ if (tier === 0) return this;
567
+ if (!this.keyring) throw new TierNotGrantedError(this.collection, tier);
568
+ assertTierAccess(this.keyring, this.collection, tier);
569
+ assertTierAccess(this.keyring, BLOB_COLLECTION, tier);
570
+ return new _BlobSet({
571
+ store: this.store,
572
+ vault: this.vault,
573
+ collection: this.collection,
574
+ recordId: this.recordId,
575
+ getDEK: this.getDEK,
576
+ encrypted: this.encrypted,
577
+ erasableBlobs: this.erasableBlobs,
578
+ tiersActive: this.tiersActive,
579
+ debugPlaintext: this.debugPlaintext,
580
+ keyring: this.keyring,
581
+ clearedTier: tier,
582
+ ...this.userId !== void 0 ? { userId: this.userId } : {},
583
+ ...this.maxBlobBytes !== void 0 ? { maxBlobBytes: this.maxBlobBytes } : {},
584
+ ...this.objectStore !== void 0 ? { objectStore: this.objectStore } : {},
585
+ ...this.blobFields !== void 0 ? { blobFields: this.blobFields } : {}
586
+ });
587
+ }
588
+ /** The internal collection that holds slot metadata for this collection's blobs. */
589
+ get slotsCollection() {
590
+ return `${BLOB_SLOTS_PREFIX}${this.collection}`;
591
+ }
592
+ /** The internal collection that holds published versions for this collection's blobs. */
593
+ get versionsCollection() {
594
+ return `${BLOB_VERSIONS_PREFIX}${this.collection}`;
595
+ }
596
+ // ─── Slot Metadata I/O (CAS-protected) ─────────────────────────────
597
+ /**
598
+ * @param tier #724 Arc 10 Task 4: explicit tier to resolve the slot map's
599
+ * collection DEK at — `getDEK(dekKey(this.collection, tier))`. Omitted →
600
+ * resolves via `ownerTier()` (the record's CURRENT live tier), correct
601
+ * for every call site except `rehomeForTier`'s own internal reads, which
602
+ * must pin `fromTier` explicitly (see `ownerTier()`'s doc comment).
603
+ */
604
+ async loadSlots(tier) {
605
+ const envelope = await this.store.get(this.vault, this.slotsCollection, this.recordId);
606
+ if (!envelope) return { slots: {}, version: 0 };
607
+ if (!this.encrypted) {
608
+ return {
609
+ slots: JSON.parse(envelope._data),
610
+ version: envelope._v
611
+ };
612
+ }
613
+ const dek = await this.getDEK(dekKey(this.collection, tier ?? await this.ownerTier()));
614
+ const json = await openEnvelopeJson(envelope, dek);
615
+ return {
616
+ slots: JSON.parse(json),
617
+ version: envelope._v
618
+ };
619
+ }
620
+ /**
621
+ * Resumable slot-map read for `runRehomeSteps` (#746 spec §7 §2d bullet
622
+ * 1): try `fromTier` (the map's pre-move physical location) first; on a
623
+ * decrypt failure (wrong key, not "missing" — mirrors `loadBlobObject`'s
624
+ * own TamperedError-only fallback trigger) retry `toTier` — the signal
625
+ * that a prior (crashed) run's own move step already landed. `atTier`
626
+ * reports which one opened it. A missing envelope reports `atTier:
627
+ * fromTier` (arbitrary — the `Object.keys(slots).length > 0` gate the
628
+ * caller applies makes the value moot for an empty map).
629
+ */
630
+ async loadSlotsTolerant(fromTier, toTier) {
631
+ const envelope = await this.store.get(this.vault, this.slotsCollection, this.recordId);
632
+ if (!envelope) return { slots: {}, version: 0, atTier: fromTier };
633
+ if (!this.encrypted) {
634
+ return { slots: JSON.parse(envelope._data), version: envelope._v, atTier: fromTier };
635
+ }
636
+ const fromDek = await this.getDEK(dekKey(this.collection, fromTier));
637
+ try {
638
+ const json2 = await openEnvelopeJson(envelope, fromDek);
639
+ return { slots: JSON.parse(json2), version: envelope._v, atTier: fromTier };
640
+ } catch (err) {
641
+ if (!(err instanceof TamperedError) || fromTier === toTier) throw err;
642
+ }
643
+ const toDek = await this.getDEK(dekKey(this.collection, toTier));
644
+ const json = await openEnvelopeJson(envelope, toDek);
645
+ return { slots: JSON.parse(json), version: envelope._v, atTier: toTier };
646
+ }
647
+ /** @param tier See {@link loadSlots}'s `tier` param — same resolution. */
648
+ async saveSlots(slots, currentVersion, tier) {
649
+ const json = JSON.stringify(slots);
650
+ const now = (/* @__PURE__ */ new Date()).toISOString();
651
+ let envelope;
652
+ if (this.encrypted) {
653
+ const dek = await this.getDEK(dekKey(this.collection, tier ?? await this.ownerTier()));
654
+ const { iv, data } = await encrypt(json, dek);
655
+ envelope = {
656
+ _noydb: NOYDB_FORMAT_VERSION,
657
+ _v: currentVersion + 1,
658
+ _ts: now,
659
+ _iv: iv,
660
+ _data: data
661
+ };
662
+ } else {
663
+ envelope = {
664
+ _noydb: NOYDB_FORMAT_VERSION,
665
+ _v: currentVersion + 1,
666
+ _ts: now,
667
+ _iv: "",
668
+ _data: json
669
+ };
670
+ }
671
+ await this.store.put(
672
+ this.vault,
673
+ this.slotsCollection,
674
+ this.recordId,
675
+ envelope,
676
+ currentVersion > 0 ? currentVersion : void 0
677
+ );
678
+ }
679
+ /**
680
+ * CAS retry loop for slot metadata updates. Re-reads slots on conflict
681
+ * and re-applies the mutation function.
682
+ *
683
+ * #724 re-review (High): when the mutation empties the slot map (the
684
+ * last slot was deleted), the `_blob_slots_{collection}/{recordId}` row
685
+ * is DELETED rather than saved as an empty-but-present envelope. An
686
+ * empty envelope left in place would still be tier-keyed at whatever
687
+ * tier it was last written at; `rehomeForTier`'s `slots exist` guard
688
+ * (`Object.keys(slots).length > 0`) would then skip re-keying it on a
689
+ * later elevate/demote, stranding it at the wrong tier's DEK —
690
+ * `TamperedError` on every later access. Deleting the row instead makes
691
+ * "no slots" and "no envelope" the SAME state, so a record that once
692
+ * had a blob and a record that never had one behave identically once
693
+ * empty.
694
+ *
695
+ * @param tier See {@link loadSlots}'s `tier` param — threaded through so
696
+ * `rehomeForTier`'s isolate-fork writes (via `putUnderDEK`) target the
697
+ * slot map's CURRENT (fromTier) location during the move.
698
+ */
699
+ async casUpdateSlots(mutate, tier) {
700
+ for (let attempt = 0; attempt < MAX_CAS_RETRIES; attempt++) {
701
+ const { slots, version } = await this.loadSlots(tier);
702
+ const updated = mutate(slots);
703
+ if (updated === null) return;
704
+ if (Object.keys(updated).length === 0) {
705
+ if (version > 0) await this.store.delete(this.vault, this.slotsCollection, this.recordId);
706
+ return;
707
+ }
708
+ try {
709
+ await this.saveSlots(updated, version, tier);
710
+ return;
711
+ } catch (err) {
712
+ if (err instanceof ConflictError && attempt < MAX_CAS_RETRIES - 1) continue;
713
+ throw err;
714
+ }
715
+ }
716
+ }
717
+ /**
718
+ * Clear a slot's `pendingRelease` breadcrumb once its release has landed
719
+ * (#746 spec §7 review, carried finding (b)) — a no-op CAS if the field is
720
+ * already gone (idempotent, safe to call again on resume) or no longer
721
+ * matches `expected` (a NEWER put() on this slot already moved past it).
722
+ */
723
+ async clearPendingRelease(slotName, expected, tier) {
724
+ await this.casUpdateSlots((slots) => {
725
+ const slot = slots[slotName];
726
+ if (!slot || slot.pendingRelease !== expected) return null;
727
+ const rest = { ...slot };
728
+ delete rest.pendingRelease;
729
+ slots[slotName] = rest;
730
+ return slots;
731
+ }, tier);
732
+ }
733
+ /**
734
+ * `runRehomeSteps`'s FIRST step (#746 spec §7 review, carried finding
735
+ * (b)) — reconcile any `pendingRelease` breadcrumb left on `slots` by a
736
+ * PRIOR (crashed) run of THIS SAME marker-governed rehome (`opId`
737
+ * identifies it). `putUnderDEK`'s slot-CAS (pointing the slot at its new
738
+ * eTag) and its old-eTag release are two separate writes; a crash between
739
+ * them strands the release — the slot map has already moved past the old
740
+ * eTag, so a plain re-derivation of "which eTags need releasing" from the
741
+ * CURRENT slot map can never find it again (a permanent leak, not merely
742
+ * a delayed one). Durably recording the old eTag ON the slot record
743
+ * itself (in the SAME CAS write that moves it) survives exactly the crash
744
+ * window that loses an in-memory breadcrumb, and this pass is what
745
+ * consumes it on the next attempt — completing the release (idempotent
746
+ * via the SAME row-scoped stamp `putUnderDEK` used) and clearing the
747
+ * field. C10: the release is NOT swallowed — a failure here must keep the
748
+ * marker alive (propagates), not silently strand it a second way.
749
+ */
750
+ async reconcilePendingReleases(slots, fromTier, opId) {
751
+ for (const [slotName, slot] of Object.entries(slots)) {
752
+ if (!slot.pendingRelease) continue;
753
+ const stamp = `${opId}:${slotName}`;
754
+ await this.releaseRef(slot.pendingRelease, 1, false, fromTier, stamp);
755
+ await this.clearPendingRelease(slotName, slot.pendingRelease, fromTier);
756
+ }
757
+ }
758
+ // ─── Blob Index I/O (versioned for CAS refCount) ──────────────────
759
+ /**
760
+ * #747: the `_blob_index` envelope follows the eTag's HOME tier — the
761
+ * same tier the `_cek` payload wrapped inside it has been scoped to since
762
+ * #724. That fix only tier-scoped the WRAPPED content CEK; the outer
763
+ * index envelope itself stayed flat, so a tier-0 DEK holder could still
764
+ * read an elevated record's blob metadata (size/mimeType/compression/
765
+ * chunkCount/refCount/createdAt) straight off `_blob_index` at rest.
766
+ *
767
+ * `tier` defaults to `ownerTier()`, mirroring `loadSlots`. Two-tier mode
768
+ * (#753 spec §7 C7 / §2d): try `tier`'s DEK first; on a decrypt failure
769
+ * (wrong key — AES-GCM auth fails, not "missing") retry under
770
+ * `alsoTryTier`'s DEK. `atTier` reports which key actually opened it, so a
771
+ * caller mutating the object (`casUpdateRefCount`) can write it back
772
+ * under that SAME key — never lifting a flat dedup/legacy object onto the
773
+ * owner's tier (or vice versa) as a side effect of a refCount change.
774
+ *
775
+ * `alsoTryTier` defaults to `0` when `tier` resolves `> 0` and no explicit
776
+ * value is given — this is #747's original tier-then-flat fallback,
777
+ * preserved byte-identical for every pre-#753 call site (none of which
778
+ * pass `alsoTryTier`). It covers the two legitimate classes that stay
779
+ * flat even for an elevated owner: a `dedup`-policy shared object (#741,
780
+ * left in place on purpose) and a legacy `_cek`-less object (chunks
781
+ * direct under the flat DEK, never tier-isolated — #724 I1). When `tier`
782
+ * resolves to `0` and no `alsoTryTier` is given, there is no fallback
783
+ * attempt at all (matches pre-#753 behavior exactly) — but a `tier === 0`
784
+ * caller MAY now pass an elevated `alsoTryTier` explicitly (the rehome
785
+ * resume path, §2d: "try `fromTier`, fall back to `toTier`", either of
786
+ * which may be 0). Passing `alsoTryTier === tier` is a no-op fallback (no
787
+ * second attempt — nothing new to try).
788
+ *
789
+ * No migration/compat path: the whole tiers×blobs arc is unpublished, so
790
+ * there is no previously-published elevated blob whose index envelope is
791
+ * stuck flat at rest — every elevated write from here on is born
792
+ * tier-keyed.
793
+ */
794
+ async loadBlobObject(eTag, tier, alsoTryTier) {
795
+ const envelope = await this.store.get(this.vault, BLOB_INDEX_COLLECTION, eTag);
796
+ if (!envelope) return null;
797
+ if (!this.encrypted) {
798
+ return { blob: JSON.parse(envelope._data), version: envelope._v, atTier: 0 };
799
+ }
800
+ const t = tier ?? await this.ownerTier();
801
+ const fallbackTier = alsoTryTier ?? (t > 0 ? 0 : void 0);
802
+ const primaryDek = await this.getDEK(dekKey(BLOB_COLLECTION, t));
803
+ try {
804
+ const json2 = await openEnvelopeJson(envelope, primaryDek);
805
+ return { blob: JSON.parse(json2), version: envelope._v, atTier: t };
806
+ } catch (err) {
807
+ if (!(err instanceof TamperedError)) throw err;
808
+ if (fallbackTier === void 0 || fallbackTier === t) throw err;
809
+ }
810
+ const fallbackDek = await this.getDEK(dekKey(BLOB_COLLECTION, fallbackTier));
811
+ const json = await openEnvelopeJson(envelope, fallbackDek);
812
+ return { blob: JSON.parse(json), version: envelope._v, atTier: fallbackTier };
813
+ }
814
+ /**
815
+ * `runRehomeSteps`'s per-eTag resume tolerance (#746 spec §7 §2d bullet 2)
816
+ * — `loadBlobObject(eTag, fromTier, toTier)`, `alsoTryTier`'s first real
817
+ * caller: `atTier === toTier` on return means this eTag's object already
818
+ * opens under the DESTINATION tier — either a prior (crashed) run's own
819
+ * re-`put()` already landed for it (skip: already moved), or it's a
820
+ * `dedup`-policy/legacy object that happens to sit flat at tier 0 and
821
+ * `toTier === 0` (skip either way — see the call site's doc comment).
822
+ *
823
+ * When `toTier !== 0`, that flat-at-0 legacy/dedup case is NOT covered by
824
+ * the two tiers already tried — `loadBlobObject`'s own default fallback
825
+ * (implicit `alsoTryTier: 0` when the primary tier is `> 0`) is exactly
826
+ * what closes it for a non-resumable caller, but passing `toTier`
827
+ * explicitly here overrides that default. So on a `TamperedError` from
828
+ * both `fromTier` and `toTier`, retry once more at flat `0` — the SAME
829
+ * fallback `loadBlobObject`'s own default already provides, just chained
830
+ * behind the resume signal instead of replacing it.
831
+ */
832
+ async loadBlobObjectResumable(eTag, fromTier, toTier) {
833
+ try {
834
+ return await this.loadBlobObject(eTag, fromTier, toTier);
835
+ } catch (err) {
836
+ if (!(err instanceof TamperedError) || toTier === 0) throw err;
837
+ return this.loadBlobObject(eTag, fromTier, 0);
838
+ }
839
+ }
840
+ /**
841
+ * @param tier See {@link loadBlobObject}'s `tier` param — same resolution.
842
+ * A caller mutating an EXISTING object must pass the tier that OPENED it
843
+ * (`loadBlobObject`'s `atTier`), never its own ask — see
844
+ * `casUpdateRefCount`.
845
+ */
846
+ async writeBlobObject(blob, expectedVersion, tier) {
847
+ const json = JSON.stringify(blob);
848
+ const now = (/* @__PURE__ */ new Date()).toISOString();
849
+ const newVersion = (expectedVersion ?? 0) + 1;
850
+ let envelope;
851
+ if (this.encrypted) {
852
+ const dek = await this.getDEK(dekKey(BLOB_COLLECTION, tier ?? await this.ownerTier()));
853
+ const { iv, data } = await encrypt(json, dek);
854
+ envelope = { _noydb: NOYDB_FORMAT_VERSION, _v: newVersion, _ts: now, _iv: iv, _data: data };
855
+ } else {
856
+ envelope = { _noydb: NOYDB_FORMAT_VERSION, _v: newVersion, _ts: now, _iv: "", _data: json };
857
+ }
858
+ await this.store.put(
859
+ this.vault,
860
+ BLOB_INDEX_COLLECTION,
861
+ blob.eTag,
862
+ envelope,
863
+ expectedVersion
864
+ );
865
+ }
866
+ /**
867
+ * CAS retry loop for refCount changes on a BlobObject.
868
+ *
869
+ * @param tier Resolves the READ side (see {@link loadBlobObject}). The
870
+ * WRITE-back always uses `atTier` — the tier that actually opened the
871
+ * object — never `tier` itself: a dedup-shared or legacy object left flat
872
+ * on purpose must stay flat even when asked for under an elevated
873
+ * owner's tier, or a refCount change would silently lift it there (#747).
874
+ */
875
+ async casUpdateRefCount(eTag, delta, tier) {
876
+ for (let attempt = 0; attempt < MAX_CAS_RETRIES; attempt++) {
877
+ const result = await this.loadBlobObject(eTag, tier);
878
+ if (!result) throw new NotFoundError(`BlobObject ${eTag} not found`);
879
+ const { blob, version, atTier } = result;
880
+ const updated = { ...blob, refCount: blob.refCount + delta };
881
+ try {
882
+ await this.writeBlobObject(updated, version, atTier);
883
+ return updated.refCount;
884
+ } catch (err) {
885
+ if (err instanceof ConflictError && attempt < MAX_CAS_RETRIES - 1) continue;
886
+ throw err;
887
+ }
888
+ }
889
+ throw new ConflictError(-1);
890
+ }
891
+ /**
892
+ * Stamp-aware CAS retry loop for refCount changes (#753 spec §7 C2/C4) —
893
+ * the journal primitive `casUpdateRefCount` itself is deliberately left
894
+ * untouched (every existing call site passes no stamp and must stay
895
+ * byte-identical). A distinct method rather than an optional param on
896
+ * `casUpdateRefCount` keeps the return shape type-clean: this one reports
897
+ * whether ITS delta was newly applied, not just the resulting count.
898
+ *
899
+ * C4: the membership check lives INSIDE the retry loop — every (re)read,
900
+ * including retries after a losing CAS attempt, checks `blob.lastOps` for
901
+ * `stamp` FIRST, before computing a delta. This is what makes it a true
902
+ * test-and-set rather than a check-then-act with a TOCTOU window: two
903
+ * concurrent callers racing the SAME stamp can each land on any attempt,
904
+ * but only the one that reads `lastOps` without the stamp present ever
905
+ * applies the delta — the other, whichever attempt it lands on, always
906
+ * observes the stamp (either already there, or freshly appended by the
907
+ * winner) and skips.
908
+ *
909
+ * On apply: the stamp is appended to the bounded ring (`appendStamp`,
910
+ * K=8) in the SAME `writeBlobObject` CAS write as the delta — no window
911
+ * between them (spec C2's atomicity requirement).
912
+ *
913
+ * @returns `{ applied: true, refCount }` when this call's delta was freshly
914
+ * applied (or `{ applied: false, refCount }` when `stamp` was already
915
+ * present — the delta from a PRIOR call with this exact stamp already
916
+ * landed; `refCount` reflects the object's current count, unchanged by
917
+ * this call).
918
+ * @param tier See {@link loadBlobObject}'s `tier` param — same resolution
919
+ * and the same atTier write-back rule as `casUpdateRefCount`.
920
+ */
921
+ async casUpdateRefCountStamped(eTag, delta, tier, stamp) {
922
+ for (let attempt = 0; attempt < MAX_CAS_RETRIES; attempt++) {
923
+ const result = await this.loadBlobObject(eTag, tier);
924
+ if (!result) throw new NotFoundError(`BlobObject ${eTag} not found`);
925
+ const { blob, version, atTier } = result;
926
+ if (blob.lastOps?.includes(stamp)) {
927
+ return { applied: false, refCount: blob.refCount };
928
+ }
929
+ const updated = {
930
+ ...blob,
931
+ refCount: blob.refCount + delta,
932
+ lastOps: appendStamp(blob.lastOps, stamp)
933
+ };
934
+ try {
935
+ await this.writeBlobObject(updated, version, atTier);
936
+ return { applied: true, refCount: updated.refCount };
937
+ } catch (err) {
938
+ if (err instanceof ConflictError && attempt < MAX_CAS_RETRIES - 1) continue;
939
+ throw err;
940
+ }
941
+ }
942
+ throw new ConflictError(-1);
943
+ }
944
+ /**
945
+ * Release `n` references to a blob and reclaim it at refCount 0.
946
+ *
947
+ * The single reclaim choke point for every reference-drop path — slot
948
+ * delete/overwrite, published-version delete, and `forget()` shred — so the
949
+ * refCount-0 policy is uniform:
950
+ * - **erasable blob** (`_cek` present) → delete the `BlobObject` (the SOLE
951
+ * copy of the wrapped content CEK → chunks permanently undecryptable) and
952
+ * reclaim the chunks. The crypto-shred is EAGER on every path: GDPR erasure
953
+ * must not wait on orphan retention.
954
+ * - **legacy blob** (no `_cek`) → reclaimed only when `reclaimLegacy` (the
955
+ * `forget()` erasure path); otherwise left for deferred GC so the existing
956
+ * `BlobLifecyclePolicy.orphanRetentionDays` semantics are preserved.
957
+ *
958
+ * @returns `'shredded'` (erasable, refCount 0, chunks dead) · `'retainedShared'`
959
+ * (erasable, still referenced) · `'residue'` (legacy — not a crypto-shred).
960
+ *
961
+ * @param tier See {@link loadBlobObject}'s `tier` param — forwarded to both
962
+ * the read and the refCount write-back (which resolves its own `atTier`
963
+ * from the read, per `casUpdateRefCount`'s doc comment).
964
+ *
965
+ * @param stamp #753 spec §7 C1 journal primitive: when present, this call
966
+ * is a (possibly-resumed) journaled release and the CAS goes through
967
+ * {@link casUpdateRefCountStamped} instead, under the two-armed resume
968
+ * rule:
969
+ * - `lastOps` already has `stamp` && `refCount > 0` → this stamp's
970
+ * decrement already landed and the object is still referenced elsewhere
971
+ * — skip re-decrementing, report the same outcome a fresh call would
972
+ * (`retainedShared`/`residue`).
973
+ * - `lastOps` already has `stamp` && `refCount <= 0` → this stamp's
974
+ * decrement already landed, but the crash window sat BETWEEN the
975
+ * decrement CAS and the index+chunk deletion below — COMPLETE that
976
+ * deletion now (idempotent: both `store.delete` calls are void on an
977
+ * already-absent key).
978
+ * - not yet stamped → apply via `casUpdateRefCountStamped`, then proceed
979
+ * exactly like the unstamped path below.
980
+ *
981
+ * A crash can also land AFTER the index row is deleted but BEFORE every
982
+ * chunk is — `loadBlobObject` then returns null (indistinguishable from
983
+ * "never existed") and the object's `chunkCount` is gone with it. A
984
+ * stamped caller who knows the true count (the journal marker's captured
985
+ * `holds`, once that plumbing lands) supplies it via `chunkCountHint` so
986
+ * this call can still finish the chunk deletion; without it, an
987
+ * already-index-gone eTag is reported `'shredded'` same as today (best
988
+ * effort — nothing left to key the cleanup off of).
989
+ */
990
+ async releaseRef(eTag, n, reclaimLegacy, tier, stamp, chunkCountHint) {
991
+ const loaded = await this.loadBlobObject(eTag, tier);
992
+ if (!loaded) {
993
+ if (stamp !== void 0 && chunkCountHint !== void 0) {
994
+ for (let i = 0; i < chunkCountHint; i++) {
995
+ await this.store.delete(this.vault, BLOB_CHUNKS_COLLECTION, `${eTag}_${i}`);
996
+ }
997
+ }
998
+ return "shredded";
999
+ }
1000
+ const erasable = loaded.blob._cek !== void 0;
1001
+ if (stamp !== void 0 && loaded.blob.lastOps?.includes(stamp)) {
1002
+ if (loaded.blob.refCount > 0) return erasable ? "retainedShared" : "residue";
1003
+ if (erasable || reclaimLegacy) {
1004
+ await this.store.delete(this.vault, BLOB_INDEX_COLLECTION, eTag);
1005
+ const chunkCount = chunkCountHint ?? loaded.blob.chunkCount;
1006
+ for (let i = 0; i < chunkCount; i++) {
1007
+ await this.store.delete(this.vault, BLOB_CHUNKS_COLLECTION, `${eTag}_${i}`);
1008
+ }
1009
+ }
1010
+ return erasable ? "shredded" : "residue";
1011
+ }
1012
+ const remaining = stamp !== void 0 ? (await this.casUpdateRefCountStamped(eTag, -n, tier, stamp)).refCount : await this.casUpdateRefCount(eTag, -n, tier);
1013
+ if (remaining > 0) return erasable ? "retainedShared" : "residue";
1014
+ if (erasable || reclaimLegacy) {
1015
+ await this.store.delete(this.vault, BLOB_INDEX_COLLECTION, eTag);
1016
+ for (let i = 0; i < loaded.blob.chunkCount; i++) {
1017
+ await this.store.delete(this.vault, BLOB_CHUNKS_COLLECTION, `${eTag}_${i}`);
1018
+ }
1019
+ }
1020
+ return erasable ? "shredded" : "residue";
1021
+ }
1022
+ /**
1023
+ * Crypto-shred this record's blob attachments — called by
1024
+ * `vault.forget()`.
1025
+ *
1026
+ * For each distinct eTag the record references (a record may attach the same
1027
+ * content under several slot names → several refCount holds): decrement the
1028
+ * blob's refCount by that many. When it reaches 0:
1029
+ * - **erasable blob** (`_cek` present) → delete the `BlobObject` (the SOLE
1030
+ * recoverable copy of the wrapped content CEK → chunks permanently
1031
+ * undecryptable) and reclaim the chunk bytes. This is the crypto-shred.
1032
+ * - **legacy blob** (no `_cek`) → chunks are under the shared `_blob` DEK and
1033
+ * stay decryptable until byte-deleted; we delete the orphaned chunks +
1034
+ * index but report it as residue, not a cryptographic erasure.
1035
+ * When refCount stays > 0 the content legitimately persists for its other
1036
+ * owner — reported as `retainedShared` (or `residue` if legacy).
1037
+ *
1038
+ * Finally drops the record's slot map, severing the subject's link.
1039
+ *
1040
+ * @param ownerTier #724 Arc 10 C3: `vault.forget()` writes the record's
1041
+ * tombstone (which drops `_tier`) BEFORE calling this method, so the
1042
+ * `ownerTier()` default — a fresh peek at the now-tombstoned envelope —
1043
+ * would resolve tier 0 and decrypt the tier-scoped slot map under the
1044
+ * wrong DEK (`TamperedError`, erasure aborted mid-shred). `forget()` reads
1045
+ * the LIVE record first and passes its pre-tombstone tier here instead.
1046
+ *
1047
+ * #724 re-review (Critical) defense-in-depth: an unreadable slot map must
1048
+ * not abort the whole erasure cascade — `casUpdateSlots` deleting the row
1049
+ * instead of leaving it empty-but-present (rather than a stray mis-keyed
1050
+ * envelope) is what makes an ABSENT row the normal "no blobs" case, and
1051
+ * `loadSlots` already returns `{ slots: {}, version: 0 }` cleanly for
1052
+ * that, never throwing. So the only way this catch fires is a row that IS
1053
+ * present failing to decrypt — genuine corruption/mis-key/tamper — and
1054
+ * that must never be swallowed as "no blobs to shred": it is pushed onto
1055
+ * `residue` so `forget()` surfaces it via `blobResidueCollections` rather
1056
+ * than silently reporting a clean erasure while un-shredded blobs remain.
1057
+ *
1058
+ * #750: published versions (`_blob_versions_{collection}/{recordId}::*`)
1059
+ * take an INDEPENDENT refCount hold on a `BlobObject`, separate from the
1060
+ * slot map (see `publish()`) — the slot loop above never sees them. Left
1061
+ * untouched, that hold (and the version-held content, if the slot was
1062
+ * later overwritten) survives `forget()` — a GDPR-erasure hole. So this
1063
+ * method also enumerates the record's version rows via
1064
+ * `collectVersionHolds`, folds each version's hold into the SAME `holds`
1065
+ * map (a shared eTag reports ONE outcome, not two), and deletes the
1066
+ * readable version rows once their holds are released.
1067
+ *
1068
+ * **#753 spec §7 (crash-idempotent shred):** the holds used for the
1069
+ * actual release loop below come from a `_blob_intent` MARKER, never
1070
+ * re-collected from rows on resume:
1071
+ * - `forget()` mints the marker PRE-tombstone (`mintShredIntent`,
1072
+ * C5) — the normal case, so a marker is ALREADY present by the time
1073
+ * this method runs and `intent.ownerTier` (captured live, before the
1074
+ * tombstone dropped `_tier`) is authoritative even when the caller's
1075
+ * own `ownerTier` argument is stale (a crash-retried `forget()`
1076
+ * re-reads the now-tombstoned record and can only pass `0`).
1077
+ * - Called WITHOUT a pre-existing marker (defensive/non-forget callers,
1078
+ * or the direct-call path this suite exercises) — one is minted here
1079
+ * from the live rows, exactly like `mintShredIntent` does, so the
1080
+ * crash matrix holds regardless of caller (spec §2c).
1081
+ * - A pending `op:'rehome'` marker is resumed to completion FIRST (#746
1082
+ * spec §7 Q1 — "supersession is resume-then-shred": a half-done rehome
1083
+ * can leave a row-unreferenced destination object that shred's
1084
+ * row-derived holds can never see; replacing the marker instead of
1085
+ * resuming it would leak that object past `forget()` permanently), via
1086
+ * {@link resolveShredIntent} — then a fresh SHRED marker is minted (or
1087
+ * discovered, if the resume's own completion raced with another
1088
+ * minter) exactly as the no-marker branch below does.
1089
+ *
1090
+ * **Whole-branch review (#753):** the marker is a crash-safety
1091
+ * ENHANCEMENT, not a precondition for erasure. If the no-marker branch's
1092
+ * own mint fails (transient store error, `getDEK` failure — possibly the
1093
+ * same failure that already sent `forget()`'s pre-tombstone mint to
1094
+ * residue), this degrades to {@link unmarkedShred} — a best-effort
1095
+ * UNMARKED shred, exactly the pre-#753 behavior — rather than aborting
1096
+ * and leaving the blobs un-shredded.
1097
+ *
1098
+ * **#746 review Critical 1:** that degradation covers ONLY a genuine
1099
+ * no-marker mint failure — nothing was in flight, so a live-row
1100
+ * best-effort re-collection is safe. A REHOME-resume failure is a
1101
+ * DIFFERENT failure mode: a marker WAS present and its holds are
1102
+ * ambiguous mid-move (C10's whole point). `resolveShredIntent` below
1103
+ * therefore never wraps `consumeRehomeIntent` in the mint's degradation
1104
+ * try/catch — a resume failure propagates all the way out of this
1105
+ * method uncaught, leaving the rehome marker alive (never `deleteIntent`d)
1106
+ * for a later resume, instead of silently falling through to
1107
+ * `unmarkedShred`, which would both clobber rows the ambiguous rehome
1108
+ * still needs and abandon the marker forever (the exact permanent-leak
1109
+ * shape the journal exists to prevent).
1110
+ *
1111
+ * `collectShredHolds` is re-run here (idempotent — a corrupt/unreadable
1112
+ * row stays corrupt/unreadable on re-read; an already-deleted row reads
1113
+ * back cleanly empty) purely to derive the slot-map/version-row DELETE
1114
+ * targets and residue notices — never to recompute the release counts,
1115
+ * which are fixed for the operation's lifetime in the marker (C1/C2).
1116
+ */
1117
+ async shredAllForRecord(ownerTier) {
1118
+ const tierArg = ownerTier ?? 0;
1119
+ const intent = await this.resolveShredIntent(tierArg);
1120
+ if (!intent) return this.unmarkedShred(tierArg);
1121
+ const tier = intent.ownerTier ?? tierArg;
1122
+ const collected = await this.collectShredHolds(tier);
1123
+ return this.consumeShredIntent(intent, tier, collected);
1124
+ }
1125
+ /**
1126
+ * Resolve a SHRED `BlobIntent` for this record, or `null` when the entry
1127
+ * mint itself failed and nothing was in flight (the caller degrades to
1128
+ * {@link unmarkedShred}) — `shredAllForRecord`'s entry logic (#746 spec §7
1129
+ * Q1), factored out so the two failure modes stay structurally separate
1130
+ * (#746 review Critical 1):
1131
+ *
1132
+ * - A pending REHOME marker — found up front OR discovered via a raced
1133
+ * `createIntent` inside `mintFreshShredIntent` — is resumed via
1134
+ * {@link consumeRehomeIntent} OUTSIDE the `try` below. Its failure
1135
+ * PROPAGATES to the caller uncaught: holds are ambiguous mid-move, so
1136
+ * this must surface, never silently degrade.
1137
+ * - Only `mintFreshShredIntent` itself (the create-a-fresh-SHRED-marker
1138
+ * step, reached with no rehome in flight) is wrapped — its failure is
1139
+ * the ONLY thing `null` reports, matching the whole-branch-review
1140
+ * degrade contract exactly.
1141
+ *
1142
+ * The loop re-attempts the mint after resuming a raced rehome discovery,
1143
+ * mirroring `resolveToShredIntent`'s prior recursive shape without ever
1144
+ * routing a `consumeRehomeIntent` call through the mint's own catch.
1145
+ */
1146
+ async resolveShredIntent(tierArg) {
1147
+ let intent = await getIntent(this.store, this.vault, this.collection, this.recordId, this.getDEK);
1148
+ let effectiveTier = tierArg;
1149
+ for (; ; ) {
1150
+ if (intent && intent.op === "rehome") {
1151
+ await this.consumeRehomeIntent(intent);
1152
+ effectiveTier = intent.toTier;
1153
+ intent = null;
1154
+ }
1155
+ if (intent) return intent;
1156
+ try {
1157
+ intent = await this.mintFreshShredIntent(effectiveTier);
1158
+ } catch {
1159
+ return null;
1160
+ }
1161
+ if (intent.op === "shred") return intent;
1162
+ }
1163
+ }
1164
+ /**
1165
+ * Best-effort UNMARKED shred (#753 whole-branch review) — reached only
1166
+ * when `shredAllForRecord`'s own entry-mint threw, so there is no marker
1167
+ * to journal against. Reproduces the pre-#753 `shredAllForRecord` body:
1168
+ * collect this record's holds from the live rows, release each unstamped
1169
+ * (no journal — a crash here reverts to the pre-#753 stranding-eTag
1170
+ * exposure, exactly like main before this arc), then unconditionally drop
1171
+ * the slot map / readable version rows. The mint failure itself is
1172
+ * reported as `_blob_intent` residue so `forget()` surfaces the
1173
+ * degraded crash-safety posture via `blobResidueCollections`, never
1174
+ * silently.
1175
+ */
1176
+ async unmarkedShred(tier) {
1177
+ const shredded = [];
1178
+ const retainedShared = [];
1179
+ const collected = await this.collectShredHolds(tier);
1180
+ const residue = [...collected.residue, `${this.collection}:${this.recordId}:_blob_intent`];
1181
+ for (const hold of collected.holds) {
1182
+ const outcome = await this.releaseRef(hold.eTag, hold.n, true, tier);
1183
+ if (outcome === "shredded") shredded.push(hold.eTag);
1184
+ else if (outcome === "retainedShared") retainedShared.push(hold.eTag);
1185
+ else residue.push(hold.eTag);
1186
+ }
1187
+ if (collected.slotsPresent) await this.store.delete(this.vault, this.slotsCollection, this.recordId);
1188
+ for (const key of collected.versionKeysToDelete) await this.store.delete(this.vault, this.versionsCollection, key);
1189
+ return { shredded, retainedShared, residue };
1190
+ }
1191
+ /**
1192
+ * Collect this record's CURRENT blob holds (slot map + published
1193
+ * versions) at `tier` — the shred journal's single hold-collection
1194
+ * routine (#753 spec §7), shared by `mintFreshShredIntent` (to seed a
1195
+ * fresh marker's authoritative `holds`) and `shredAllForRecord`/
1196
+ * `resolvePendingIntent` (to derive delete targets + residue on
1197
+ * consumption — see {@link shredAllForRecord}'s doc comment for why that
1198
+ * second use never feeds the release loop). Mirrors the pre-#753
1199
+ * inline collection logic byte-for-byte; `chunkCount` per hold (C5) is
1200
+ * best-effort — an unreadable index row just means the marker's hint is
1201
+ * absent, `releaseRef`'s own chunkCountHint fallback still applies.
1202
+ */
1203
+ async collectShredHolds(tier) {
1204
+ const residue = [];
1205
+ let slots = {};
1206
+ try {
1207
+ slots = (await this.loadSlots(tier)).slots;
1208
+ } catch {
1209
+ residue.push(`${this.collection}:${this.recordId}:_blob_slots`);
1210
+ }
1211
+ const holdsMap = /* @__PURE__ */ new Map();
1212
+ for (const name of Object.keys(slots)) {
1213
+ const eTag = slots[name].eTag;
1214
+ holdsMap.set(eTag, (holdsMap.get(eTag) ?? 0) + 1);
1215
+ }
1216
+ const versionKeysToDelete = await this.collectVersionHolds(tier, holdsMap, residue);
1217
+ const holds = [];
1218
+ for (const [eTag, n] of holdsMap) {
1219
+ let chunkCount = 0;
1220
+ try {
1221
+ chunkCount = (await this.loadBlobObject(eTag, tier))?.blob.chunkCount ?? 0;
1222
+ } catch {
1223
+ }
1224
+ holds.push({ eTag, n, chunkCount });
1225
+ }
1226
+ return { holds, slotsPresent: Object.keys(slots).length > 0, versionKeysToDelete, residue };
1227
+ }
1228
+ /**
1229
+ * Collect this record's current holds and CAS-create a fresh `_blob_intent`
1230
+ * SHRED marker from them (#753 spec §7 C8). Used by `mintShredIntent`
1231
+ * (forget()'s pre-tombstone step) and `shredAllForRecord`'s no-marker
1232
+ * (defensive/direct-call) branch.
1233
+ *
1234
+ * C4: a concurrent minter for this SAME record can win the create race —
1235
+ * `createIntent` throws `BlobIntentPendingError`, and the winner's marker
1236
+ * (whatever op it turns out to be) is returned instead; the caller decides
1237
+ * how to handle a raced rehome marker.
1238
+ */
1239
+ async mintFreshShredIntent(tier) {
1240
+ const collected = await this.collectShredHolds(tier);
1241
+ const candidate = { op: "shred", opId: mintOpId(), holds: collected.holds, ownerTier: tier };
1242
+ try {
1243
+ await createIntent(this.store, this.vault, this.collection, this.recordId, this.getDEK, candidate);
1244
+ return candidate;
1245
+ } catch (err) {
1246
+ if (!(err instanceof BlobIntentPendingError)) throw err;
1247
+ const raced = await getIntent(this.store, this.vault, this.collection, this.recordId, this.getDEK);
1248
+ if (raced) return raced;
1249
+ throw err;
1250
+ }
1251
+ }
1252
+ /**
1253
+ * Release every hold in `intent.holds` (stamped with `intent.opId`), then
1254
+ * delete the slot map / readable version rows / the marker itself — the
1255
+ * shred journal's consume step (#753 spec §7 §2c), shared by
1256
+ * `shredAllForRecord` and `resolvePendingIntent`.
1257
+ *
1258
+ * **C10 (no swallowed releases under a marker):** a release that THROWS
1259
+ * (as opposed to returning `'residue'`, a normal non-error outcome for a
1260
+ * legacy blob) is caught, reported as residue, and marks the whole
1261
+ * consumption incomplete — the slot map, version rows, and the marker
1262
+ * itself are then left IN PLACE (not deleted) so a later resume retries
1263
+ * every hold. Stamped holds that already landed skip re-decrementing
1264
+ * (C1/C4's test-and-set), so redoing the full loop on retry is safe.
1265
+ */
1266
+ async consumeShredIntent(intent, tier, collected) {
1267
+ const shredded = [];
1268
+ const retainedShared = [];
1269
+ const residue = [...collected.residue];
1270
+ let allApplied = true;
1271
+ for (const hold of intent.holds ?? []) {
1272
+ try {
1273
+ const outcome = await this.releaseRef(hold.eTag, hold.n, true, tier, intent.opId, hold.chunkCount);
1274
+ if (outcome === "shredded") shredded.push(hold.eTag);
1275
+ else if (outcome === "retainedShared") retainedShared.push(hold.eTag);
1276
+ else residue.push(hold.eTag);
1277
+ } catch {
1278
+ residue.push(hold.eTag);
1279
+ allApplied = false;
1280
+ }
1281
+ }
1282
+ if (allApplied) {
1283
+ if (collected.slotsPresent) await this.store.delete(this.vault, this.slotsCollection, this.recordId);
1284
+ for (const key of collected.versionKeysToDelete) await this.store.delete(this.vault, this.versionsCollection, key);
1285
+ await deleteIntent(this.store, this.vault, this.collection, this.recordId);
1286
+ }
1287
+ return { shredded, retainedShared, residue };
1288
+ }
1289
+ /**
1290
+ * Resume gate (#753/#746 spec §7 C6): every refCount/slot mutator calls
1291
+ * this FIRST. A pending SHRED marker means a previous `forget()`/
1292
+ * `shredAllForRecord()` crashed mid-flight — refCounts are ambiguous
1293
+ * until it's resumed, so no new blob write may proceed over them; this
1294
+ * resumes it to completion before the caller's own mutation runs. A
1295
+ * pending REHOME marker means a previous tier move crashed mid-flight —
1296
+ * resumed to completion the SAME way (§2d), via {@link consumeRehomeIntent}
1297
+ * using the marker's OWN captured `fromTier`/`toTier`/`policy`/`opId`,
1298
+ * never the caller's own ask (which may be a totally unrelated write).
1299
+ *
1300
+ * Never called from `shredAllForRecord`/`mintShredIntent`/
1301
+ * `mintFreshShredIntent`/`collectShredHolds`/`consumeShredIntent`/
1302
+ * `sweepPendingShredIntents`/`runRehomeSteps`/`consumeRehomeIntent`
1303
+ * themselves — those ARE the resume machinery; routing them back through
1304
+ * this gate would recurse.
1305
+ */
1306
+ async resolvePendingIntent() {
1307
+ const intent = await getIntent(this.store, this.vault, this.collection, this.recordId, this.getDEK);
1308
+ if (!intent) return;
1309
+ if (intent.op === "rehome") {
1310
+ await this.consumeRehomeIntent(intent);
1311
+ return;
1312
+ }
1313
+ const tier = intent.ownerTier ?? 0;
1314
+ const collected = await this.collectShredHolds(tier);
1315
+ await this.consumeShredIntent(intent, tier, collected);
1316
+ }
1317
+ /**
1318
+ * Resume a pending REHOME marker to completion: run {@link
1319
+ * runRehomeSteps} (never the public `rehomeForTier`/`syncTierMove` —
1320
+ * both would re-enter `resolvePendingIntent` on the SAME marker and
1321
+ * recurse) using the marker's own captured `fromTier`/`toTier`/`policy`/
1322
+ * `opId`, then delete it. Shared by `resolvePendingIntent`'s rehome
1323
+ * branch and `resolveToShredIntent` (#746 spec §7 Q1: forget() resumes a
1324
+ * pending rehome before shredding).
1325
+ */
1326
+ async consumeRehomeIntent(intent) {
1327
+ await this.runRehomeSteps(intent.fromTier, intent.toTier, intent.policy, intent.opId);
1328
+ await deleteIntent(this.store, this.vault, this.collection, this.recordId);
1329
+ }
1330
+ /**
1331
+ * Resume every stranded SHRED marker in THIS collection (#753 spec §7 —
1332
+ * `sweepBlobIntents`'s first real production caller, Task 3 item 5).
1333
+ * Scoped to `this.collection` rather than the whole vault: `forget()`
1334
+ * already iterates per-ref-collection, so calling this from
1335
+ * `mintShredIntent` once per ref opportunistically heals any OTHER
1336
+ * record's marker left behind by a previous crashed operation in the
1337
+ * same collection — "heals untouched ones" (spec C5's retry contract),
1338
+ * without a separate vault-open hook. A pending REHOME marker on a
1339
+ * sibling record is left alone here (out of THIS sweep's scope — it heals
1340
+ * on its OWN record's next write/tier-op via `resolvePendingIntent`,
1341
+ * §2d's "who resumes") rather than surfaced as a resume failure.
1342
+ * `sweepBlobIntents`'s own per-marker
1343
+ * isolation (blob-intent.ts) means one corrupt sibling never blocks
1344
+ * another record's healthy resume.
1345
+ */
1346
+ async sweepPendingShredIntents() {
1347
+ await sweepBlobIntents(this.store, this.vault, this.getDEK, async (collection, recordId, marker) => {
1348
+ if (collection !== this.collection || marker.op !== "shred") return;
1349
+ const target = new _BlobSet({
1350
+ store: this.store,
1351
+ vault: this.vault,
1352
+ collection,
1353
+ recordId,
1354
+ getDEK: this.getDEK,
1355
+ encrypted: this.encrypted
1356
+ });
1357
+ await target.resolvePendingIntent();
1358
+ });
1359
+ }
1360
+ /**
1361
+ * Mint the `_blob_intent` SHRED marker for this record — `vault.forget()`'s
1362
+ * PRE-tombstone step (#753 spec §7 C5). Captures this record's CURRENT
1363
+ * blob holds (slot map + published versions, at `ownerTier` — the LIVE
1364
+ * pre-tombstone tier) into a fresh marker before any destructive write
1365
+ * happens, so a crash between here and `shredAllForRecord`'s completion —
1366
+ * including one straddling the tombstone itself, which drops `_tier` and
1367
+ * would otherwise strand an elevated record's holds unrecoverably on
1368
+ * retry — leaves a resumable, tier-correct record of what must still be
1369
+ * released.
1370
+ *
1371
+ * First sweeps this collection for any stranded marker
1372
+ * (`sweepPendingShredIntents`) — this resumes a PRE-EXISTING marker for
1373
+ * THIS record (a previous forget() attempt crashed mid-shred) to
1374
+ * completion before minting fresh (C8: never overwrite a pending
1375
+ * marker — that would orphan its op-stamps), as a side effect of the
1376
+ * same collection-wide sweep.
1377
+ */
1378
+ async mintShredIntent(ownerTier) {
1379
+ await this.sweepPendingShredIntents();
1380
+ await this.mintFreshShredIntent(ownerTier);
1381
+ }
1382
+ /**
1383
+ * #750: enumerate this record's published-version rows (`{recordId}::*` in
1384
+ * `_blob_versions_{collection}` — the same raw prefix scan as
1385
+ * `rehomeVersionRecords`) and fold each version's independent refCount hold
1386
+ * into `holds`. Returns the READABLE version keys — safe to delete once
1387
+ * their holds are released. An unreadable row is pushed onto `residue` and
1388
+ * NOT returned: deleting it blind would orphan its refCount hold and strand
1389
+ * the content undecryptable-but-undeleted forever, so it stays in place for
1390
+ * out-of-band repair (mirrors the unreadable-slot-map posture above).
1391
+ */
1392
+ async collectVersionHolds(ownerTier, holds, residue) {
1393
+ const prefix = `${this.recordId}::`;
1394
+ const keys = (await this.store.list(this.vault, this.versionsCollection)).filter((k) => k.startsWith(prefix));
1395
+ if (keys.length === 0) return [];
1396
+ const readable = [];
1397
+ const dek = this.encrypted ? await this.getDEK(dekKey(this.collection, ownerTier ?? await this.ownerTier())) : null;
1398
+ for (const key of keys) {
1399
+ const envelope = await this.store.get(this.vault, this.versionsCollection, key);
1400
+ if (!envelope) continue;
1401
+ try {
1402
+ const record = this.encrypted ? JSON.parse(await openEnvelopeJson(envelope, dek)) : JSON.parse(envelope._data);
1403
+ holds.set(record.eTag, (holds.get(record.eTag) ?? 0) + 1);
1404
+ readable.push(key);
1405
+ } catch {
1406
+ residue.push(`${this.collection}:${this.recordId}:${key}`);
1407
+ }
1408
+ }
1409
+ return readable;
1410
+ }
1411
+ /**
1412
+ * Rehome this record's blobs after a tier move (#724 Arc 10 Task 2,
1413
+ * at-rest isolation) — called by `TiersContext.syncBlobs`. A blob's home
1414
+ * tier is its owning record's tier: the `_blob` DEK is tier-scoped via
1415
+ * `dekKey('_blob', tier)`, so moving a record's tier must move the
1416
+ * wrapping key of any blob it exclusively owns, or a tier-0-cleared
1417
+ * caller could still unwrap the content CEK off the store at rest even
1418
+ * though the runtime read gate (Task 1) hides it through the API.
1419
+ *
1420
+ * Every read/write of the slot map INSIDE this method pins `fromTier`
1421
+ * explicitly (never the `loadSlots()`/`saveSlots()` default) — by the
1422
+ * time this runs, the owning record's live `_tier` envelope has ALREADY
1423
+ * moved to `toTier` (the collection-level `elevate`/`demote`/`putAtTier`
1424
+ * writes the record before invoking `syncBlobs`), but the slot map itself
1425
+ * is still physically encrypted at `fromTier` until this method's own
1426
+ * move step (last) lands. `ownerTier()`'s default would resolve the
1427
+ * wrong DEK for every read until then.
1428
+ *
1429
+ * Enumerates the record's eTags via the slot map — mirrors
1430
+ * `shredAllForRecord`'s `loadSlots()` → `holds` enumeration above. For
1431
+ * each distinct eTag:
1432
+ * - **solo** (`refCount === 1`) and **shared `isolate`** (`refCount > 1`,
1433
+ * default policy) are UNIFIED (#724 Arc 10 correction, closes C1): both
1434
+ * re-`put()` the plaintext under `toBlobDEK` for every one of this
1435
+ * record's slots pointing at the eTag. That mints a fresh, tier-scoped
1436
+ * eTag/`_cek` (HMAC + wrap keyed by `toBlobDEK`) and, via `put()`'s own
1437
+ * existing old-eTag decrement, releases this record's hold on the old
1438
+ * object — a solo blob's old object drops to refCount 0 and is
1439
+ * crypto-shredded; a shared co-owner keeps its unchanged refCount. The
1440
+ * OLD in-place rewrap (`wrapCek(unwrapCek(_cek, fromDEK), toDEK)`, eTag
1441
+ * held stable) is UNSAFE — it leaves the eTag in the FROM tier's HMAC
1442
+ * namespace, so a later same-bytes put computed under that tier's DEK
1443
+ * dedup-*hits* the wrapped object (cross-tier corruption of an
1444
+ * uninvolved writer). Re-`put()`ing makes that structurally
1445
+ * impossible: different tiers hash to different eTags. Landing at
1446
+ * `toTier === 0` naturally REJOINS the tier-0 dedup pool if a co-owner
1447
+ * already holds the content there (`put()`'s own dedup check matches
1448
+ * on the recomputed tier-0-native eTag) — this is how `demote(→0)`
1449
+ * reverses an `elevate()` fork.
1450
+ * - **shared `dedup`** (#741, opt-in, only for `refCount > 1`): NO-OP.
1451
+ * The slot keeps pointing at the shared eTag. The Task-1 runtime read
1452
+ * gate still hides it from a tier-0 caller; the chunks remain
1453
+ * decryptable at rest under the flat `_blob` DEK (documented residue).
1454
+ * - **legacy** (`_cek` absent — chunks direct under the flat `_blob`
1455
+ * DEK): NO-OP. Tiered collections mandate `perRecordKeys`, so new
1456
+ * blob data is always erasable; a legacy blob reaching this method
1457
+ * would need a chunk re-encrypt (not attempted here).
1458
+ *
1459
+ * #724 Arc 10 Task 4 — slot-map metadata move: LAST step, after every
1460
+ * fork/rewrap/reconcile write above (which all operated on the slot map
1461
+ * via the explicit `fromTier`, its still-current physical location).
1462
+ * Re-reads (to pick up any fork/reconcile-produced eTag changes from the
1463
+ * loop) and re-encrypts the SAME `_blob_slots_{collection}/{recordId}`
1464
+ * row under the `toTier` collection DEK — filenames/sizes/mimeTypes/eTags
1465
+ * are no longer tier-0-readable at rest for an elevated record. No
1466
+ * separate delete: it's the same physical row, just re-keyed in place
1467
+ * (mirrors `history.ts`'s `rewrapHistory`).
1468
+ *
1469
+ * @param opId #753/#746 spec §7 C3: the governing `_blob_intent` rehome
1470
+ * marker's op-stamp identity, threaded through by `syncTierMove`. Omitted
1471
+ * (a direct call, e.g. tests) → byte-identical unstamped behavior. When
1472
+ * present, every DESTINATION refCount `+1` this call performs (the slot
1473
+ * loop's dedup-hit re-put below, and `rehomeVersionETag`'s own increment)
1474
+ * carries a ROW-SCOPED stamp — `${opId}:${slotName}` / `${opId}:${versionKey}`,
1475
+ * never the bare opId — so a resumed re-put's `+1` is idempotent per row
1476
+ * without collapsing N legitimate `+1`s onto one destination eTag into one.
1477
+ *
1478
+ * The direct-call entry point: resumes a pending SHRED marker first
1479
+ * (unchanged, #753 C6), or a pending REHOME marker for a DIFFERENT prior
1480
+ * op (#746 Q1 — resumed via its OWN stored fromTier/toTier/opId before
1481
+ * this call's requested move proceeds), then runs {@link runRehomeSteps}.
1482
+ * `syncTierMove` — the real tier-op seam (`syncBlobs`) — calls
1483
+ * `runRehomeSteps` directly instead, after minting/consuming its own
1484
+ * marker (calling back through here would re-enter `resolvePendingIntent`
1485
+ * on the marker `syncTierMove` itself just created).
1486
+ */
1487
+ async rehomeForTier(fromTier, toTier, policy, opId) {
1488
+ if (!this.encrypted || fromTier === toTier) return;
1489
+ await this.resolvePendingIntent();
1490
+ await this.runRehomeSteps(fromTier, toTier, policy, opId);
1491
+ }
1492
+ /**
1493
+ * The tier-op → rehome seam (#746 spec §7 §2d) — called by
1494
+ * `TiersContext.syncBlobs` (`collection.ts`), replacing the pre-#746
1495
+ * unstamped `rehomeForTier` call. Mints the governing `_blob_intent`
1496
+ * REHOME marker BEFORE the first write (CAS create-if-absent), resuming
1497
+ * any marker already pending for this record first — a stale rehome from
1498
+ * an earlier, different move, or a shred, per the same C6/Q1 resume-first
1499
+ * law `rehomeForTier`'s own entry follows. Deletes the marker as the LAST
1500
+ * step, once every phase of {@link runRehomeSteps} completes without
1501
+ * throwing (a failure — including a C10 unswallowed release — leaves the
1502
+ * marker in place for the next resume).
1503
+ */
1504
+ async syncTierMove(fromTier, toTier, policy) {
1505
+ if (!this.encrypted || fromTier === toTier) return;
1506
+ await this.resolvePendingIntent();
1507
+ if (await this.isBlobFree(fromTier)) return;
1508
+ const opId = mintOpId();
1509
+ const intent = { op: "rehome", opId, fromTier, toTier, policy };
1510
+ try {
1511
+ await createIntent(this.store, this.vault, this.collection, this.recordId, this.getDEK, intent);
1512
+ } catch (err) {
1513
+ if (!(err instanceof BlobIntentPendingError)) throw err;
1514
+ await this.resolvePendingIntent();
1515
+ return this.syncTierMove(fromTier, toTier, policy);
1516
+ }
1517
+ await this.runRehomeSteps(fromTier, toTier, policy, opId);
1518
+ await deleteIntent(this.store, this.vault, this.collection, this.recordId);
1519
+ }
1520
+ /**
1521
+ * #746 whole-branch review Hardening 1: `syncTierMove` runs on EVERY tier
1522
+ * move (`elevate`/`demote`/`putAtTier`) — including the common case of a
1523
+ * record that has never attached a blob. Minting-then-deleting a marker
1524
+ * for such a record is pure overhead (~4 extra adapter ops on every
1525
+ * bulk elevate/demote of blob-less records) protecting nothing: an empty
1526
+ * slot map AND no published versions means there is genuinely no content
1527
+ * this op could strand. Checked AFTER `resolvePendingIntent` (a pending
1528
+ * marker, however stale, is always resumed first regardless of current
1529
+ * blob state — it may reference content this check alone wouldn't see)
1530
+ * and BEFORE minting a new one.
1531
+ */
1532
+ async isBlobFree(fromTier) {
1533
+ const { slots } = await this.loadSlots(fromTier);
1534
+ if (Object.keys(slots).length > 0) return false;
1535
+ const prefix = `${this.recordId}::`;
1536
+ const versionKeys = await this.store.list(this.vault, this.versionsCollection);
1537
+ return !versionKeys.some((k) => k.startsWith(prefix));
1538
+ }
1539
+ /**
1540
+ * The resumable rehome executor (#746 spec §7 §2d) — `rehomeForTier`'s
1541
+ * pre-#746 body, factored out so it can be invoked WITHOUT re-entering
1542
+ * `resolvePendingIntent` (which would recurse: this method IS part of the
1543
+ * resume machinery, called from `resolvePendingIntent`'s own rehome
1544
+ * branch and from `syncTierMove` after its marker is already settled).
1545
+ *
1546
+ * Per-step resume tolerance:
1547
+ * - Slot map: {@link loadSlotsTolerant} tries `fromTier` then `toTier` —
1548
+ * if it opens at `toTier`, a prior run's move step already landed, so
1549
+ * the ENTIRE per-eTag loop (strictly sequenced before the move) is
1550
+ * also already done — skip both, but first reconstruct `rehomedETags`
1551
+ * (see below) so the version pass's same-eTag fast path still works
1552
+ * (carried finding (a)).
1553
+ * - Per-eTag: {@link loadBlobObjectResumable} — an eTag that only opens
1554
+ * under `toTier` means THIS slot's re-put already landed on a prior
1555
+ * run — skip re-processing it.
1556
+ * - Slot-CAS→release gap (carried finding (b)): {@link
1557
+ * reconcilePendingReleases} runs FIRST, completing any old-eTag
1558
+ * release a prior run's `putUnderDEK` left stranded (slot already
1559
+ * moved to the new eTag, release not yet applied) before either the
1560
+ * "already moved" or "still moving" branch below runs.
1561
+ * - Version pass: same per-key tolerance, in `rehomeVersionRecords`.
1562
+ * - Destination `+1`s (#746 whole-branch review, K=8 stamp-ring
1563
+ * blocker): `knownApplied` — this op's marker-backed
1564
+ * `appliedStamps`, read ONCE here — makes every row-scoped
1565
+ * increment idempotent INDEPENDENTLY of `BlobObject.lastOps`'s
1566
+ * bounded ring; see {@link applyStampedIncrement}'s doc comment.
1567
+ */
1568
+ async runRehomeSteps(fromTier, toTier, policy, opId) {
1569
+ const { slots, atTier: slotsAtTier } = await this.loadSlotsTolerant(fromTier, toTier);
1570
+ if (opId !== void 0) await this.reconcilePendingReleases(slots, fromTier, opId);
1571
+ const knownApplied = opId !== void 0 ? new Set((await getIntent(this.store, this.vault, this.collection, this.recordId, this.getDEK))?.appliedStamps ?? []) : EMPTY_STAMP_SET;
1572
+ const rehomedETags = /* @__PURE__ */ new Map();
1573
+ if (Object.keys(slots).length > 0) {
1574
+ if (slotsAtTier === toTier) {
1575
+ const toBlobDEK = await this.getDEK(dekKey(BLOB_COLLECTION, toTier));
1576
+ const fromBlobDEK = await this.getDEK(dekKey(BLOB_COLLECTION, fromTier));
1577
+ const doneETags = new Set(Object.values(slots).map((s) => s.eTag).filter((eTag) => eTag !== ""));
1578
+ for (const eTag of doneETags) {
1579
+ let loaded;
1580
+ try {
1581
+ loaded = await this.loadBlobObject(eTag, toTier);
1582
+ } catch (err) {
1583
+ if (err instanceof TamperedError) continue;
1584
+ throw err;
1585
+ }
1586
+ if (!loaded || loaded.atTier !== toTier || loaded.blob._cek === void 0) continue;
1587
+ const plaintext = await this.fetchAllChunks(loaded.blob, toBlobDEK);
1588
+ const oldETag = await hmacSha256Hex(fromBlobDEK, plaintext);
1589
+ if (oldETag !== eTag) rehomedETags.set(oldETag, eTag);
1590
+ }
1591
+ } else {
1592
+ const eTags = new Set(Object.values(slots).map((s) => s.eTag).filter((eTag) => eTag !== ""));
1593
+ if (eTags.size > 0) {
1594
+ const fromBlobDEK = await this.getDEK(dekKey(BLOB_COLLECTION, fromTier));
1595
+ const toBlobDEK = await this.getDEK(dekKey(BLOB_COLLECTION, toTier));
1596
+ for (const eTag of eTags) {
1597
+ const loaded = await this.loadBlobObjectResumable(eTag, fromTier, toTier);
1598
+ if (!loaded) continue;
1599
+ if (loaded.atTier === toTier) continue;
1600
+ const { blob } = loaded;
1601
+ if (blob._cek === void 0) continue;
1602
+ if (policy === "dedup" && blob.refCount > 1) continue;
1603
+ const plaintext = await this.fetchAllChunks(blob, fromBlobDEK);
1604
+ rehomedETags.set(eTag, await hmacSha256Hex(toBlobDEK, plaintext));
1605
+ for (const [slotName, slot] of Object.entries(slots)) {
1606
+ if (slot.eTag !== eTag) continue;
1607
+ await this.putUnderDEK(slotName, plaintext, toBlobDEK, {
1608
+ filename: slot.filename,
1609
+ ...blob.mimeType !== void 0 ? { mimeType: blob.mimeType } : {},
1610
+ compress: blob.compression === "gzip",
1611
+ ...slot.uploadedBy !== void 0 ? { uploadedBy: slot.uploadedBy } : {}
1612
+ }, fromTier, toTier, opId, knownApplied);
1613
+ }
1614
+ }
1615
+ }
1616
+ const { slots: finalSlots, version: finalVersion } = await this.loadSlots(fromTier);
1617
+ await this.saveSlots(finalSlots, finalVersion, toTier);
1618
+ }
1619
+ }
1620
+ await this.rehomeVersionRecords(fromTier, toTier, policy, rehomedETags, opId, knownApplied);
1621
+ }
1622
+ /**
1623
+ * Rehome this record's PUBLISHED VERSIONS after a tier move — the second
1624
+ * half of `rehomeForTier` (#724 Arc 10 Task 2, closes C4). Enumerates
1625
+ * every `_blob_versions_{collection}/{recordId}::*` row (mirrors
1626
+ * `listVersions`'s raw prefix scan, but across ALL slots on this record,
1627
+ * not one) and, for each:
1628
+ * - rehomes its held eTag via {@link rehomeVersionETag} (reusing the slot
1629
+ * loop's result when the version happens to hold the SAME eTag a slot
1630
+ * held — see that method's doc comment), and
1631
+ * - re-keys the version RECORD itself onto the `toTier` collection DEK,
1632
+ * regardless of whether the content moved — metadata protection is
1633
+ * orthogonal to the shared-content dedup policy (matches the slot-map
1634
+ * move, which always relocates even for a `dedup`-left-in-place blob).
1635
+ *
1636
+ * Per-key resume tolerance (#746 spec §7 §2d bullet 3): each key's
1637
+ * metadata is read via {@link loadVersionRecordAtKeyTolerant} (try
1638
+ * `fromTier` then `toTier`) — a key whose metadata already opens at
1639
+ * `toTier` had its `resolveRehomedVersionETag` call AND its
1640
+ * `writeVersionRecordAtKey` BOTH already land on a prior run (the
1641
+ * metadata write is sequenced strictly after the eTag rehome, which
1642
+ * itself completes the old-eTag release under C1's two-armed resume rule
1643
+ * — same order as the slot loop's own CAS-then-release), so it's skipped
1644
+ * entirely.
1645
+ *
1646
+ * Known residual gap (documented, not closed by this task — see the T2
1647
+ * report): if a version's OLD eTag is held ONLY by that version (never
1648
+ * shared with any slot), a crash strictly between
1649
+ * `resolveRehomedVersionETag`'s release (drops it to refCount 0,
1650
+ * crypto-shredding a solo-held erasable object) and THIS method's
1651
+ * metadata write would leave the version pointing at an eTag whose sole
1652
+ * copy is already gone. This mirrors the slot-side "carried finding (b)"
1653
+ * shape but on the version side; closing it durably (a `pendingRelease`-
1654
+ * style breadcrumb on `VersionRecord`, filtered from the public
1655
+ * `listVersions()` surface) is out of scope for this task.
1656
+ */
1657
+ async rehomeVersionRecords(fromTier, toTier, policy, rehomedETags, opId, knownApplied) {
1658
+ const prefix = `${this.recordId}::`;
1659
+ const allKeys = await this.store.list(this.vault, this.versionsCollection);
1660
+ const matchingKeys = allKeys.filter((k) => k.startsWith(prefix));
1661
+ if (matchingKeys.length === 0) return;
1662
+ const fromBlobDEK = await this.getDEK(dekKey(BLOB_COLLECTION, fromTier));
1663
+ const toBlobDEK = await this.getDEK(dekKey(BLOB_COLLECTION, toTier));
1664
+ for (const key of matchingKeys) {
1665
+ const loaded = await this.loadVersionRecordAtKeyTolerant(key, fromTier, toTier);
1666
+ if (!loaded) continue;
1667
+ if (loaded.atTier === toTier) continue;
1668
+ const stamp = opId !== void 0 ? `${opId}:${key}` : void 0;
1669
+ const newETag = await this.resolveRehomedVersionETag(loaded.record, fromTier, toTier, fromBlobDEK, toBlobDEK, policy, rehomedETags, stamp, knownApplied);
1670
+ const moved = newETag === loaded.record.eTag ? loaded.record : { ...loaded.record, eTag: newETag };
1671
+ await this.writeVersionRecordAtKey(key, moved, toTier);
1672
+ }
1673
+ }
1674
+ /**
1675
+ * Resumable per-key version-record metadata read (#746 spec §7 §2d bullet
1676
+ * 3) — mirrors {@link loadSlotsTolerant}: try `fromTier` first, fall back
1677
+ * to `toTier` on a decrypt failure. `atTier === toTier` on return is the
1678
+ * "already fully rehomed" signal `rehomeVersionRecords` skips on.
1679
+ */
1680
+ async loadVersionRecordAtKeyTolerant(key, fromTier, toTier) {
1681
+ const envelope = await this.store.get(this.vault, this.versionsCollection, key);
1682
+ if (!envelope) return null;
1683
+ if (!this.encrypted) return { record: JSON.parse(envelope._data), atTier: fromTier };
1684
+ const fromDek = await this.getDEK(dekKey(this.collection, fromTier));
1685
+ try {
1686
+ const json2 = await openEnvelopeJson(envelope, fromDek);
1687
+ return { record: JSON.parse(json2), atTier: fromTier };
1688
+ } catch (err) {
1689
+ if (!(err instanceof TamperedError) || fromTier === toTier) throw err;
1690
+ }
1691
+ const toDek = await this.getDEK(dekKey(this.collection, toTier));
1692
+ const json = await openEnvelopeJson(envelope, toDek);
1693
+ return { record: JSON.parse(json), atTier: toTier };
1694
+ }
1695
+ /**
1696
+ * Rehome ONE version's independently-held eTag — both the DESTINATION
1697
+ * `+1`/create AND the OLD eTag's release, in that order (mirrors the slot
1698
+ * loop's own CAS-then-release sequencing, and matches C1's two-armed
1699
+ * resume rule: a crash after the release CAS lands but before the index
1700
+ * row's delete completes is resumable — `already`/`loadBlobObject` still
1701
+ * find the row at `fromTier` on a re-run, since the CALLER's metadata
1702
+ * write hasn't happened yet). Returns the eTag the version should now
1703
+ * point at (unchanged if legacy/missing/left `dedup`-shared) — the caller
1704
+ * (`rehomeVersionRecords`) writes the version's metadata after this
1705
+ * returns.
1706
+ *
1707
+ * If the version happened to hold the SAME eTag a slot held (the common
1708
+ * case: publish right after put, no later overwrite), the slot loop in
1709
+ * `runRehomeSteps` already re-`put()` the content under `toBlobDEK` —
1710
+ * `rehomedETags` (content-addressed, so deterministic) tells us the
1711
+ * resulting destination eTag without a redundant fetch+re-encrypt; we
1712
+ * only need to move THIS hold's refCount onto it. Otherwise (the version
1713
+ * outlived its slot, or was never in the slot map) this re-`put()`s the
1714
+ * plaintext itself via `writeBlobContent` — the same content-write core
1715
+ * `put()`/the slot loop use, no new crypto — mirroring the slot case's
1716
+ * legacy/`dedup`-shared skip conditions.
1717
+ *
1718
+ * @param fromTier / @param toTier #747: same from/to split as the slot
1719
+ * loop above — the read of the version's CURRENTLY-held eTag pins
1720
+ * `fromTier`; the re-put and the refCount bump onto an already-rehomed
1721
+ * (slot-loop-produced) eTag pin `toTier`.
1722
+ *
1723
+ * @param stamp #753/#746 spec §7 C3: when present (a marker-governed
1724
+ * rehome), every destination `+1` this call performs — both the
1725
+ * `already`-rehomed fast path's explicit CAS and the `writeBlobContent`
1726
+ * re-put's own dedup-hit CAS — carries the row-scoped stamp
1727
+ * `${opId}:${versionKey}` the caller computed, never the bare opId (see
1728
+ * `rehomeForTier`'s `opId` doc comment).
1729
+ * @param knownApplied #746 whole-branch review (K=8 stamp-ring blocker):
1730
+ * this op's marker-backed confirmed-stamps set — see
1731
+ * {@link applyStampedIncrement}. Threaded through to the
1732
+ * `writeBlobContent` re-put's own dedup-hit CAS too.
1733
+ */
1734
+ async resolveRehomedVersionETag(record, fromTier, toTier, fromBlobDEK, toBlobDEK, policy, rehomedETags, stamp, knownApplied) {
1735
+ const already = rehomedETags.get(record.eTag);
1736
+ if (already !== void 0) {
1737
+ if (stamp !== void 0) {
1738
+ await this.applyStampedIncrement(already, toTier, stamp, knownApplied ?? EMPTY_STAMP_SET);
1739
+ } else {
1740
+ await this.casUpdateRefCount(already, 1, toTier);
1741
+ }
1742
+ await this.releaseOldETagAfterMove(record.eTag, fromTier, stamp);
1743
+ return already;
1744
+ }
1745
+ const loaded = await this.loadBlobObject(record.eTag, fromTier);
1746
+ if (!loaded || loaded.blob._cek === void 0) return record.eTag;
1747
+ if (policy === "dedup" && loaded.blob.refCount > 1) return record.eTag;
1748
+ const plaintext = await this.fetchAllChunks(loaded.blob, fromBlobDEK);
1749
+ const { eTag: newETag } = await this.writeBlobContent(plaintext, toBlobDEK, {
1750
+ filename: record.label,
1751
+ ...loaded.blob.mimeType !== void 0 ? { mimeType: loaded.blob.mimeType } : {},
1752
+ compress: loaded.blob.compression === "gzip"
1753
+ }, toTier, stamp, knownApplied);
1754
+ rehomedETags.set(record.eTag, newETag);
1755
+ if (newETag !== record.eTag) {
1756
+ await this.releaseOldETagAfterMove(record.eTag, fromTier, stamp);
1757
+ }
1758
+ return newETag;
1759
+ }
1760
+ /**
1761
+ * CAS retry loop for an arbitrary BlobObject mutation. Used only by
1762
+ * `migrate()`, which is legacy-only by definition — a legacy object is
1763
+ * always flat-keyed — so the tier is pinned to `0` explicitly rather than
1764
+ * left to `loadBlobObject`/`writeBlobObject`'s independent `ownerTier()`
1765
+ * defaults, which could diverge on an elevated owner and re-key the
1766
+ * envelope under the elevated tier DEK while chunks stay flat (#747 review).
1767
+ */
1768
+ async casUpdateBlobObject(eTag, mutate) {
1769
+ for (let attempt = 0; attempt < MAX_CAS_RETRIES; attempt++) {
1770
+ const result = await this.loadBlobObject(eTag, 0);
1771
+ if (!result) throw new NotFoundError(`BlobObject ${eTag} not found`);
1772
+ try {
1773
+ await this.writeBlobObject(mutate(result.blob), result.version, 0);
1774
+ return;
1775
+ } catch (err) {
1776
+ if (err instanceof ConflictError && attempt < MAX_CAS_RETRIES - 1) continue;
1777
+ throw err;
1778
+ }
1779
+ }
1780
+ throw new ConflictError(-1);
1781
+ }
1782
+ /**
1783
+ * Migrate this record's LEGACY blobs (no `_cek`, chunks under the shared
1784
+ * `_blob` DEK) to per-blob content CEKs so they become crypto-shreddable.
1785
+ * Returns the eTags migrated vs. already-erasable.
1786
+ *
1787
+ * **Explicit maintenance pass** (mirrors the record-CEK migration posture):
1788
+ * re-encrypts the existing compressed chunks IN PLACE under a fresh content
1789
+ * CEK — preserving the eTag, chunkCount, chunkSize, and compression — then
1790
+ * flips the `_cek` discriminant. Crash-safe + idempotent via `_cekPending`:
1791
+ * 1. persist the wrapped content CEK in `_cekPending` (readers ignore it →
1792
+ * the blob stays readable under the `_blob` DEK; the key survives a crash);
1793
+ * 2. re-encrypt each chunk under the content CEK (a resume reads an
1794
+ * already-migrated chunk under the content CEK, else under the `_blob` DEK);
1795
+ * 3. promote `_cekPending` → `_cek` (atomic flip). Reads now use the CEK.
1796
+ * A re-run after a crash resumes from whichever phase was reached.
1797
+ *
1798
+ * Dedup-safe: migrating a shared blob (refCount > 1) re-keys it for every
1799
+ * referencer at once; a non-erasable collection still reads it (it unwraps
1800
+ * `_cek` under the `_blob` DEK it holds).
1801
+ */
1802
+ async migrate() {
1803
+ const migrated = [];
1804
+ const alreadyErasable = [];
1805
+ if (!this.encrypted) return { migrated, alreadyErasable };
1806
+ await this.resolvePendingIntent();
1807
+ const blobDEK = await this.getDEK(BLOB_COLLECTION);
1808
+ const ownerTier = await this.ownerTier();
1809
+ const { slots } = await this.loadSlots(ownerTier);
1810
+ const eTags = new Set(Object.values(slots).map((s) => s.eTag));
1811
+ for (const eTag of eTags) {
1812
+ const loaded = await this.loadBlobObject(eTag, ownerTier, 0);
1813
+ if (!loaded) continue;
1814
+ const blob = loaded.blob;
1815
+ if (loaded.atTier > 0 || blob._cek !== void 0) {
1816
+ alreadyErasable.push(eTag);
1817
+ continue;
1818
+ }
1819
+ let contentCek;
1820
+ if (blob._cekPending !== void 0) {
1821
+ contentCek = await unwrapCek(blob._cekPending, blobDEK);
1822
+ } else {
1823
+ contentCek = await generateDEK();
1824
+ const wrapped = await wrapCek(contentCek, blobDEK);
1825
+ await this.casUpdateBlobObject(eTag, (b) => ({ ...b, _cekPending: wrapped }));
1826
+ }
1827
+ for (let i = 0; i < blob.chunkCount; i++) {
1828
+ let raw;
1829
+ try {
1830
+ raw = await this.readChunk(eTag, i, blob.chunkCount, blobDEK);
1831
+ } catch {
1832
+ raw = await this.readChunk(eTag, i, blob.chunkCount, contentCek);
1833
+ }
1834
+ if (!raw) {
1835
+ throw new NotFoundError(
1836
+ `Blob chunk ${i}/${blob.chunkCount} missing for eTag "${eTag}" during migration`
1837
+ );
1838
+ }
1839
+ await this.writeChunk(eTag, i, blob.chunkCount, raw, contentCek);
1840
+ }
1841
+ await this.casUpdateBlobObject(eTag, (b) => {
1842
+ const { _cekPending, ...rest } = b;
1843
+ return _cekPending === void 0 ? b : { ...rest, _cek: _cekPending };
1844
+ });
1845
+ migrated.push(eTag);
1846
+ }
1847
+ return { migrated, alreadyErasable };
1848
+ }
1849
+ // ─── Chunk I/O (with AAD binding) ─────────────────────────────────
1850
+ async writeChunk(eTag, index, chunkCount, chunk, dek) {
1851
+ const id = `${eTag}_${index}`;
1852
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1853
+ let envelope;
1854
+ if (dek) {
1855
+ const aad = chunkAAD(eTag, index, chunkCount);
1856
+ const { iv, data } = await encryptBytesWithAAD(chunk, dek, aad);
1857
+ envelope = { _noydb: NOYDB_FORMAT_VERSION, _v: 1, _ts: now, _iv: iv, _data: data };
1858
+ } else {
1859
+ envelope = {
1860
+ _noydb: NOYDB_FORMAT_VERSION,
1861
+ _v: 1,
1862
+ _ts: now,
1863
+ _iv: "",
1864
+ _data: bufferToBase64(chunk)
1865
+ };
1866
+ }
1867
+ await this.store.put(this.vault, BLOB_CHUNKS_COLLECTION, id, envelope);
1868
+ }
1869
+ async readChunk(eTag, index, chunkCount, dek) {
1870
+ const envelope = await this.store.get(this.vault, BLOB_CHUNKS_COLLECTION, `${eTag}_${index}`);
1871
+ if (!envelope) return null;
1872
+ if (dek) {
1873
+ const aad = chunkAAD(eTag, index, chunkCount);
1874
+ return await decryptBytesWithAAD(envelope._iv, envelope._data, dek, aad);
1875
+ }
1876
+ return base64ToBuffer(envelope._data);
1877
+ }
1878
+ // ─── Version record I/O ───────────────────────────────────────────
1879
+ /** #752: '::' is the version-key separator (`versionKey`) — a recordId/slotName/label
1880
+ * containing it makes the `{recordId}::` prefix scans (listVersions / rehomeVersionRecords /
1881
+ * collectVersionHolds) match ACROSS records, which escalated from mis-read to destructive
1882
+ * with #750's shred path. Also refused: a part that starts or ends with ':' — otherwise a
1883
+ * boundary colon re-segments the key (recordId `"a:"` + slotName `"slot"` yields
1884
+ * `"a:::slot::label"`, which starts with `"a::"` and prefix-matches record `"a"`'s rows).
1885
+ * With both rules, every '::' in a stored key is exactly a component separator — the grammar
1886
+ * is unambiguous by construction. Refused at the write surface only: legacy '::' data stays
1887
+ * readable/sheddable. */
1888
+ assertKeyPartSafe(part, what) {
1889
+ if (part.includes("::") || part.startsWith(":") || part.endsWith(":")) {
1890
+ throw new ValidationError(
1891
+ `Blob ${what} "${part}" must not contain '::' nor start/end with ':' (reserved as the blob version-key separator; #752)`
1892
+ );
1893
+ }
1894
+ }
1895
+ /** #752: `recordId`/`slotName`/`label` must not contain '::' — see `assertKeyPartSafe`. */
1896
+ versionKey(slotName, label) {
1897
+ return `${this.recordId}::${slotName}::${label}`;
1898
+ }
1899
+ /**
1900
+ * @param tier #724 Arc 10 Task 2: explicit tier to resolve the version
1901
+ * record's collection DEK at — `getDEK(dekKey(this.collection, tier))`,
1902
+ * mirroring `loadSlots`'s `tier` param. Omitted → resolves via
1903
+ * `ownerTier()` (the record's CURRENT live tier); `rehomeForTier`'s own
1904
+ * reads pin `fromTier`/`toTier` explicitly instead (see `ownerTier()`'s
1905
+ * doc comment for why the default would be wrong mid-move).
1906
+ */
1907
+ async loadVersionRecord(slotName, label, tier) {
1908
+ const key = this.versionKey(slotName, label);
1909
+ const envelope = await this.store.get(this.vault, this.versionsCollection, key);
1910
+ if (!envelope) return null;
1911
+ if (!this.encrypted) {
1912
+ return JSON.parse(envelope._data);
1913
+ }
1914
+ const dek = await this.getDEK(dekKey(this.collection, tier ?? await this.ownerTier()));
1915
+ const json = await openEnvelopeJson(envelope, dek);
1916
+ return JSON.parse(json);
1917
+ }
1918
+ /** @param tier See {@link loadVersionRecord}'s `tier` param — same resolution. */
1919
+ async writeVersionRecord(slotName, record, tier) {
1920
+ await this.writeVersionRecordAtKey(
1921
+ this.versionKey(slotName, record.label),
1922
+ record,
1923
+ tier ?? await this.ownerTier()
1924
+ );
1925
+ }
1926
+ /**
1927
+ * `writeVersionRecord`'s body, addressed by an explicit raw store key
1928
+ * instead of `slotName`+`label` — used by `rehomeVersionRecords` (#724
1929
+ * Arc 10 Task 2), which already has the key from its raw `store.list()`
1930
+ * scan across ALL of this record's version slots.
1931
+ */
1932
+ async writeVersionRecordAtKey(key, record, tier) {
1933
+ const json = JSON.stringify(record);
1934
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1935
+ let envelope;
1936
+ if (this.encrypted) {
1937
+ const dek = await this.getDEK(dekKey(this.collection, tier));
1938
+ const { iv, data } = await encrypt(json, dek);
1939
+ envelope = { _noydb: NOYDB_FORMAT_VERSION, _v: 1, _ts: now, _iv: iv, _data: data };
1940
+ } else {
1941
+ envelope = { _noydb: NOYDB_FORMAT_VERSION, _v: 1, _ts: now, _iv: "", _data: json };
1942
+ }
1943
+ await this.store.put(this.vault, this.versionsCollection, key, envelope);
1944
+ }
1945
+ async deleteVersionRecord(slotName, label) {
1946
+ const key = this.versionKey(slotName, label);
1947
+ await this.store.delete(this.vault, this.versionsCollection, key);
1948
+ }
1949
+ // ─── Effective chunk size ─────────────────────────────────────────
1950
+ effectiveChunkSize(opts) {
1951
+ if (opts?.chunkSize) return opts.chunkSize;
1952
+ if (this.maxBlobBytes) return this.maxBlobBytes;
1953
+ return DEFAULT_CHUNK_SIZE;
1954
+ }
1955
+ // ─── Fetch all chunks for a blob ──────────────────────────────────
1956
+ /**
1957
+ * #747/#749 whole-branch review I1: when `loadBlobObject`'s tier-scoped open
1958
+ * fell through to the flat retry while a CLEARED higher-tier view (#749)
1959
+ * was reading (`this.clearedTier > 0`, `result.atTier === 0`), the
1960
+ * `_blob_index` row that "won" the read is not necessarily the one this
1961
+ * handle's `assertTierAccess` grant vouches for. Chunk AAD
1962
+ * (`{eTag}:{index}:{count}`) is attacker-computable, and a legacy
1963
+ * (`_cek`-less) object has no wrapped content CEK to unwrap under a key
1964
+ * the attacker doesn't hold — so ANY flat `_blob` DEK holder with store
1965
+ * write access can plant a `_cek`-less forgery at `_blob_index/{eTag}`
1966
+ * (this elevated blob's own address) whose chunks are their own flat
1967
+ * bytes, and it will decrypt cleanly under the flat DEK the fallback
1968
+ * already tries.
1969
+ *
1970
+ * `verifyFlatETag`, when set, is the eTag the CALLER originally asked
1971
+ * for (the slot/version's `.eTag` — never `blob.eTag`, which is
1972
+ * attacker-controlled content pulled from the same forged row and would
1973
+ * make the check tautological). After assembling the plaintext, we
1974
+ * recompute the SAME content address every write path mints
1975
+ * (`hmacSha256Hex(flatBlobDEK, plaintext)` — `writeBlobContent`'s Step 1,
1976
+ * unconditional on `_cek`) and compare it to that requested eTag. A
1977
+ * forged row can produce valid ciphertext under the flat DEK, but can't
1978
+ * produce plaintext that re-hashes to an address it doesn't control.
1979
+ *
1980
+ * Both legitimate flat-fallback classes — a `dedup`-policy shared object
1981
+ * (#741) and a legacy `_cek`-less object (#724 I1) — were minted this
1982
+ * same way, so an honest read's recomputed hash always matches and this
1983
+ * is a no-op for them.
1984
+ */
1985
+ async fetchAllChunks(blob, blobDEK, verifyFlatETag) {
1986
+ const chunkKey = await this.resolveChunkKey(blob, blobDEK);
1987
+ const chunks = [];
1988
+ for (let i = 0; i < blob.chunkCount; i++) {
1989
+ const chunk = await this.readChunk(blob.eTag, i, blob.chunkCount, chunkKey);
1990
+ if (!chunk) {
1991
+ throw new NotFoundError(
1992
+ `Blob chunk ${i}/${blob.chunkCount} missing for eTag "${blob.eTag}" on record "${this.recordId}"`
1993
+ );
1994
+ }
1995
+ chunks.push(chunk);
1996
+ }
1997
+ const assembled = concatChunks(chunks);
1998
+ const plaintext = blob.compression === "gzip" ? await decompressBytes(assembled) : assembled;
1999
+ if (verifyFlatETag !== void 0 && blobDEK) {
2000
+ const recomputed = await hmacSha256Hex(blobDEK, plaintext);
2001
+ if (recomputed !== verifyFlatETag) {
2002
+ throw new TamperedError(
2003
+ `Blob content for eTag "${verifyFlatETag}" failed content-address verification after a flat-tier fallback open \u2014 the _blob_index/_blob_chunks rows do not match the requested content address (#747/#749 review I1)`
2004
+ );
2005
+ }
2006
+ }
2007
+ return plaintext;
2008
+ }
2009
+ /**
2010
+ * #747/#749 review I1: the eTag to pass as `fetchAllChunks`'s `verifyFlatETag` — set
2011
+ * only when `loadBlobObject` fell through to the flat retry
2012
+ * (`resolvedAtTier === 0`) on a CLEARED higher-tier view
2013
+ * (`this.clearedTier > 0`). Every ordinary (non-cleared) call reaching
2014
+ * a content-fetch site already has `ownerTier() === 0` (elevated records
2015
+ * are hidden by `ownerRecordElevated()` before this point), so
2016
+ * `loadBlobObject` never even attempts the tier branch for them — no
2017
+ * fallback occurred, nothing to verify.
2018
+ */
2019
+ flatFallbackVerifyETag(requestedETag, resolvedAtTier) {
2020
+ if (this.clearedTier === void 0 || this.clearedTier <= 0) return void 0;
2021
+ return resolvedAtTier === 0 ? requestedETag : void 0;
2022
+ }
2023
+ // ─── Public API: Slot management ──────────────────────────────────
2024
+ /**
2025
+ * Upload bytes and attach them to this record under `slotName`.
2026
+ *
2027
+ * 1. Computes `eTag = HMAC-SHA-256(blobDEK, plaintext)` for keyed content-addressing.
2028
+ * 2. Auto-detects MIME type from magic bytes if not provided.
2029
+ * 3. If a blob with this eTag already exists, skips chunk upload (deduplication)
2030
+ * and CAS-increments refCount.
2031
+ * 4. Otherwise: compresses → splits into chunks → encrypts each chunk with
2032
+ * AAD binding → writes `_blob_chunks` → writes `BlobObject` to `_blob_index`.
2033
+ * 5. CAS-updates the slot metadata in `_blob_slots_{collection}`.
2034
+ * If overwriting an existing slot, decrements the old eTag's refCount.
2035
+ */
2036
+ async put(slotName, data, opts) {
2037
+ this.assertKeyPartSafe(this.recordId, "record id");
2038
+ this.assertKeyPartSafe(slotName, "slot name");
2039
+ await this.resolvePendingIntent();
2040
+ if (this.objectStore && this.blobFields?.[slotName]?.external) {
2041
+ const policy = this.blobFields[slotName];
2042
+ let contentType = opts?.mimeType;
2043
+ if (!contentType) {
2044
+ const detected = detectMagic(data.subarray(0, 16));
2045
+ contentType = detected?.mime ?? "application/octet-stream";
2046
+ }
2047
+ const key = `${this.collection}/${this.recordId}/${slotName}`;
2048
+ const isPublic = policy.public === true;
2049
+ const backlink = await this.buildBacklink(slotName, policy.backlink ?? "opaque-token");
2050
+ await this.objectStore.putObject(key, data, {
2051
+ contentType,
2052
+ public: isPublic,
2053
+ ...backlink.userMeta ? { userMeta: backlink.userMeta } : {}
2054
+ });
2055
+ const uploaderUserId = opts?.uploadedBy ?? this.userId;
2056
+ let oldETag;
2057
+ await this.casUpdateSlots((slots) => {
2058
+ oldETag = slots[slotName]?.eTag || void 0;
2059
+ const prevMeta = slots[slotName]?.external?.meta;
2060
+ slots[slotName] = {
2061
+ eTag: "",
2062
+ external: {
2063
+ key,
2064
+ contentType,
2065
+ ...isPublic ? { public: true } : {},
2066
+ ...backlink.token ? { backlink: backlink.token } : {},
2067
+ ...prevMeta ? { meta: prevMeta } : {}
2068
+ },
2069
+ filename: opts?.filename ?? slotName,
2070
+ size: data.byteLength,
2071
+ mimeType: contentType,
2072
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
2073
+ ...uploaderUserId !== void 0 ? { uploadedBy: uploaderUserId } : {}
2074
+ };
2075
+ return slots;
2076
+ });
2077
+ if (oldETag) {
2078
+ await this.releaseRef(oldETag, 1, false).catch(() => {
2079
+ });
2080
+ }
2081
+ return;
2082
+ }
2083
+ this.assertBlobWritable();
2084
+ const blobDEK = this.encrypted ? await this.getDEK(dekKey(BLOB_COLLECTION, await this.ownerTier())) : null;
2085
+ return this.putUnderDEK(slotName, data, blobDEK, opts);
2086
+ }
2087
+ /**
2088
+ * The slot-attachment core of `put()` (Step 7 — CAS the slot metadata),
2089
+ * parameterized by which `_blob` DEK to hash the eTag and wrap the
2090
+ * content CEK under. Steps 1-6 (hash/dedup/compress/chunk/index-write)
2091
+ * are `writeBlobContent` (#724 Arc 10 Task 2 extraction, so `rehomeForTier`
2092
+ * can reuse them for a version-held eTag without touching the slot map).
2093
+ * `put()` always passes this record's own OWNER-TIER blob DEK (#724 Arc
2094
+ * 10 correction) — `dekKey(BLOB_COLLECTION, 0)` for a tier-0 record.
2095
+ *
2096
+ * `rehomeForTier` is the other caller: on every tier move it re-`put()`s
2097
+ * each owned blob's plaintext under the `toTier`-scoped DEK — solo and
2098
+ * shared-isolate alike — so the moved copy gets a private, tier-scoped
2099
+ * eTag and `_cek` wrap from the moment it's written. The move never
2100
+ * routes through public `put()` under the wrong key, and this method
2101
+ * adds no new raw `_cek`/`_iv`/`_data` access (identical bodies, just
2102
+ * parameterized).
2103
+ *
2104
+ * @param slotsTier #724 Arc 10 Task 4: pins the slot-map CAS update
2105
+ * (Step 7) to a specific tier instead of the caller's `ownerTier()`
2106
+ * default — `rehomeForTier` passes `fromTier`, since mid-move the slot
2107
+ * map is still physically there (see `ownerTier()`'s doc comment). The
2108
+ * old eTag being replaced lives wherever the slot map itself is
2109
+ * physically keyed, so its release (#747, below) reuses this SAME value
2110
+ * rather than a separate param.
2111
+ * @param contentTier #747: pins the `_blob_index` read/write inside
2112
+ * `writeBlobContent` to a specific tier — `rehomeForTier` passes `toTier`
2113
+ * (matching `blobDEK`, already resolved at `toTier`), since mid-move
2114
+ * `ownerTier()` already reads `toTier` too but this is more direct.
2115
+ * Omitted → `ownerTier()`, correct for `put()`'s ordinary (non-rehome) call.
2116
+ * @param opId #753/#746 spec §7 C3: `rehomeForTier`'s governing marker
2117
+ * op-stamp identity (omitted → `undefined`, `put()`'s ordinary call and
2118
+ * every pre-existing caller, byte-identical unstamped behavior). When
2119
+ * present, the ROW-SCOPED stamp `${opId}:${slotName}` — never the bare
2120
+ * opId — governs BOTH this call's destination `+1` (via
2121
+ * `writeBlobContent`'s dedup-hit CAS) and its old-eTag release below: two
2122
+ * different `BlobObject`s (destination vs. source), so the shared stamp
2123
+ * string collides with neither, and a resumed re-put of THIS slot can
2124
+ * neither double-apply the `+1` nor double-release the old object. Two
2125
+ * DIFFERENT slots on this record legitimately re-putting the SAME
2126
+ * destination eTag each get their OWN stamp (their own `slotName`) — the
2127
+ * row scope is what lets N slots' `+1`s all land on one destination
2128
+ * object instead of the first silently absorbing the rest.
2129
+ */
2130
+ async putUnderDEK(slotName, data, blobDEK, opts, slotsTier, contentTier, opId, knownApplied) {
2131
+ const rowStamp = opId !== void 0 ? `${opId}:${slotName}` : void 0;
2132
+ const { eTag, mimeType } = await this.writeBlobContent(data, blobDEK, opts, contentTier, rowStamp, knownApplied);
2133
+ const uploaderUserId = opts?.uploadedBy ?? this.userId;
2134
+ let deferredOldETag;
2135
+ await this.casUpdateSlots((slots) => {
2136
+ const oldETag = slots[slotName]?.eTag;
2137
+ const pending = oldETag && oldETag !== eTag ? oldETag : void 0;
2138
+ slots[slotName] = {
2139
+ eTag,
2140
+ filename: opts?.filename ?? slotName,
2141
+ size: data.byteLength,
2142
+ ...mimeType !== void 0 ? { mimeType } : {},
2143
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
2144
+ ...uploaderUserId !== void 0 ? { uploadedBy: uploaderUserId } : {},
2145
+ // #746 spec §7 review, carried finding (b): durably record the OLD
2146
+ // eTag ON the slot row itself, in the SAME CAS write that moves the
2147
+ // slot to `eTag` — see `SlotRecord.pendingRelease`'s doc comment.
2148
+ // Gated to marker-governed (stamped) calls only: ordinary put()
2149
+ // (`rowStamp` undefined) keeps today's in-memory best-effort
2150
+ // posture, zero footprint.
2151
+ ...pending !== void 0 && rowStamp !== void 0 ? { pendingRelease: pending } : {}
2152
+ };
2153
+ deferredOldETag = pending;
2154
+ return slots;
2155
+ }, slotsTier);
2156
+ if (deferredOldETag) {
2157
+ await this.releaseOldETagAfterMove(deferredOldETag, slotsTier, rowStamp);
2158
+ if (rowStamp !== void 0) await this.clearPendingRelease(slotName, deferredOldETag, slotsTier);
2159
+ }
2160
+ }
2161
+ /**
2162
+ * C10 (#753/#746 spec §7): under a marker-governed (`stamp` present)
2163
+ * rehome, a failed old-eTag release must not be silently swallowed — it
2164
+ * is the crypto-shred of the FROM-tier object, and swallowing it during a
2165
+ * documented-exactly-once op is the bug C10 closes. Propagating keeps the
2166
+ * governing `_blob_intent` marker alive (never deleted) so a later resume
2167
+ * retries it, rather than completing the op over a residue that never
2168
+ * surfaces. Unstamped (ordinary `put()`/`delete()`) calls keep the
2169
+ * pre-#753 best-effort posture unchanged — a missed decrement there is
2170
+ * reconciled by a later pass, same as always.
2171
+ */
2172
+ async releaseOldETagAfterMove(eTag, tier, stamp) {
2173
+ if (stamp !== void 0) {
2174
+ await this.releaseRef(eTag, 1, false, tier, stamp);
2175
+ return;
2176
+ }
2177
+ await this.releaseRef(eTag, 1, false, tier).catch(() => {
2178
+ });
2179
+ }
2180
+ /**
2181
+ * Durably confirm a rehome row-stamp's `+1` in the governing marker —
2182
+ * #746 whole-branch review (K=8 stamp-ring blocker). No-op when there is
2183
+ * no marker to record against (a direct `rehomeForTier(..., opId)` call
2184
+ * with no real `_blob_intent` row, e.g. this suite's Task-1 tests —
2185
+ * byte-identical ring-only behavior for that path, unchanged). NOT
2186
+ * swallowed on failure (mirrors C10): a failed confirmation write must
2187
+ * keep the governing marker's op from completing silently over an
2188
+ * unconfirmed increment, not be dropped.
2189
+ */
2190
+ async recordAppliedRehomeStamp(stamp) {
2191
+ await recordAppliedStamp(this.store, this.vault, this.collection, this.recordId, this.getDEK, stamp);
2192
+ }
2193
+ /**
2194
+ * Apply ONE destination `+1` under a row-scoped rehome stamp, made
2195
+ * idempotent INDEPENDENTLY of `BlobObject.lastOps`'s bounded ring (#746
2196
+ * whole-branch review — see `BlobIntent.appliedStamps`'s doc comment for
2197
+ * the full hazard). `knownApplied` — this op's OWN marker-backed,
2198
+ * unbounded record of confirmed stamps, read ONCE at the top of
2199
+ * `runRehomeSteps` — is consulted FIRST: a hit means this exact row's
2200
+ * `+1` was already confirmed on a PRIOR run, so the CAS is skipped
2201
+ * entirely regardless of whether the shared destination's ring has since
2202
+ * evicted it (another 8+ rows — from THIS record's own fan-out, or from
2203
+ * unrelated CONCURRENT rehomes converging on the same destination —
2204
+ * could have pushed it out). A miss falls through to the existing
2205
+ * ring-based stamped CAS (correct and sufficient for the overwhelming
2206
+ * majority of resumes — same-session or shortly-after), and on success
2207
+ * (freshly applied OR the ring itself already had it) records the stamp
2208
+ * into the marker so THIS row is never re-examined again by a later
2209
+ * resume, no matter how much unrelated activity lands on the destination
2210
+ * in between.
2211
+ */
2212
+ async applyStampedIncrement(eTag, tier, stamp, knownApplied) {
2213
+ if (knownApplied.has(stamp)) return;
2214
+ await this.casUpdateRefCountStamped(eTag, 1, tier, stamp);
2215
+ await this.recordAppliedRehomeStamp(stamp);
2216
+ }
2217
+ /**
2218
+ * The chunk/CEK/dedup core of `put()` (former Steps 1-6 of `putUnderDEK`,
2219
+ * extracted #724 Arc 10 Task 2 so `rehomeVersionETag` can write a
2220
+ * version-held blob's content under a target tier's DEK WITHOUT touching
2221
+ * the slot map — `putUnderDEK`'s remaining Step 7 is slot-specific).
2222
+ * Content-addressed and dedup-aware exactly like `put()`: hashing the same
2223
+ * plaintext under the same `blobDEK` twice always lands on the same eTag.
2224
+ *
2225
+ * @param tier #747: the index-envelope tier to read/write at (see
2226
+ * {@link loadBlobObject}'s `tier` param) — `putUnderDEK`'s `contentTier`,
2227
+ * matching whichever tier `blobDEK` itself was resolved at. Omitted →
2228
+ * `ownerTier()`, correct for `put()`'s ordinary (non-rehome) call.
2229
+ * @param incrementStamp #753/#746 spec §7 C3: when present, a dedup hit at
2230
+ * Step 3 applies its `+1` via {@link casUpdateRefCountStamped} under this
2231
+ * stamp instead of the plain {@link casUpdateRefCount} — the row-scoped
2232
+ * identity `putUnderDEK`/`rehomeVersionETag` compute so a resumed re-put's
2233
+ * destination increment is idempotent per row. Omitted (every call site
2234
+ * before this arc) → byte-identical unstamped behavior.
2235
+ *
2236
+ * ALSO seeds `lastOps: [incrementStamp]` on the fresh-object create path
2237
+ * (Step 6, review finding on #746 C3): a solo blob has no pre-existing
2238
+ * destination object for a resumed re-put to dedup-hit against a stamp on
2239
+ * — the FIRST attempt's create is itself the only write, and a crash
2240
+ * after it lands but before the slot/version CAS leaves the object
2241
+ * present at refCount 1 with NO stamp. An unseeded resume then
2242
+ * re-executes Step 3, finds that same object, and — finding no matching
2243
+ * stamp — applies a SECOND, spurious `+1` (1 → 2), the exact over-count
2244
+ * hazard this stamping arc exists to close. Seeding the ring at create
2245
+ * time closes that: the resumed re-put's Step 3 dedup-hit against THIS
2246
+ * object finds its own stamp already present and skips, matching the
2247
+ * dedup-hit branch's behavior exactly.
2248
+ */
2249
+ async writeBlobContent(data, blobDEK, opts, tier, incrementStamp, knownApplied) {
2250
+ const eTag = blobDEK ? await hmacSha256Hex(blobDEK, data) : await plainSha256Hex(data);
2251
+ let mimeType = opts?.mimeType;
2252
+ if (!mimeType) {
2253
+ const detected = detectMagic(data.subarray(0, 16));
2254
+ if (detected) mimeType = detected.mime;
2255
+ }
2256
+ let shouldCompress;
2257
+ if (opts?.compress !== void 0) {
2258
+ shouldCompress = opts.compress;
2259
+ } else if (mimeType && isPreCompressed(mimeType)) {
2260
+ shouldCompress = false;
2261
+ } else {
2262
+ shouldCompress = true;
2263
+ }
2264
+ if (this.debugPlaintext) shouldCompress = false;
2265
+ const existingBlob = await this.loadBlobObject(eTag, tier);
2266
+ if (existingBlob) {
2267
+ if (incrementStamp !== void 0) {
2268
+ await this.applyStampedIncrement(eTag, tier, incrementStamp, knownApplied ?? EMPTY_STAMP_SET);
2269
+ } else {
2270
+ await this.casUpdateRefCount(eTag, 1, tier);
2271
+ }
2272
+ return { eTag, mimeType };
2273
+ }
2274
+ const { bytes: compressed, algorithm } = shouldCompress ? await compressBytes(data) : { bytes: data, algorithm: "none" };
2275
+ const chunkSize = this.debugPlaintext ? Math.max(compressed.byteLength, 1) : this.effectiveChunkSize(opts);
2276
+ const chunkCount = Math.max(1, Math.ceil(compressed.byteLength / chunkSize));
2277
+ let chunkKey = blobDEK;
2278
+ let wrappedCek;
2279
+ if (blobDEK && this.erasableBlobs) {
2280
+ const contentCek = await generateDEK();
2281
+ wrappedCek = await wrapCek(contentCek, blobDEK);
2282
+ chunkKey = contentCek;
2283
+ }
2284
+ for (let i = 0; i < chunkCount; i++) {
2285
+ const start = i * chunkSize;
2286
+ await this.writeChunk(
2287
+ eTag,
2288
+ i,
2289
+ chunkCount,
2290
+ compressed.subarray(start, start + chunkSize),
2291
+ chunkKey
2292
+ );
2293
+ }
2294
+ await this.writeBlobObject({
2295
+ eTag,
2296
+ size: data.byteLength,
2297
+ compressedSize: compressed.byteLength,
2298
+ compression: algorithm,
2299
+ chunkSize,
2300
+ chunkCount,
2301
+ ...mimeType !== void 0 ? { mimeType } : {},
2302
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2303
+ refCount: 1,
2304
+ ...wrappedCek !== void 0 ? { _cek: wrappedCek } : {},
2305
+ ...incrementStamp !== void 0 ? { lastOps: appendStamp(void 0, incrementStamp) } : {}
2306
+ }, void 0, tier);
2307
+ if (incrementStamp !== void 0) await this.recordAppliedRehomeStamp(incrementStamp);
2308
+ return { eTag, mimeType };
2309
+ }
2310
+ /**
2311
+ * Fetch all bytes for the named slot.
2312
+ * Returns `null` if the slot does not exist.
2313
+ * Throws `NotFoundError` if the index entry exists but a chunk is missing.
2314
+ */
2315
+ async get(slotName) {
2316
+ if (await this.ownerRecordElevated()) return null;
2317
+ const { slots } = await this.loadSlots();
2318
+ const slot = slots[slotName];
2319
+ if (!slot) return null;
2320
+ if (slot.external) {
2321
+ if (!this.objectStore) {
2322
+ throw new NotFoundError(`Blob slot "${slotName}" is external but no objectStore is configured`);
2323
+ }
2324
+ return this.objectStore.getObject(slot.external.key);
2325
+ }
2326
+ const result = await this.loadBlobObject(slot.eTag);
2327
+ if (!result) return null;
2328
+ const blobDEK = this.encrypted ? await this.getDEK(dekKey(BLOB_COLLECTION, result.atTier)) : void 0;
2329
+ return this.fetchAllChunks(result.blob, blobDEK, this.flatFallbackVerifyETag(slot.eTag, result.atTier));
2330
+ }
2331
+ /**
2332
+ * A URL to fetch an `external` slot's object directly — presigned
2333
+ * (time-limited) or public, per the projection. Returns `null` if the slot
2334
+ * does not exist. Throws for a non-external slot (use `get()`/`response()`).
2335
+ */
2336
+ async url(slotName, opts) {
2337
+ if (await this.ownerRecordElevated()) return null;
2338
+ const { slots } = await this.loadSlots();
2339
+ const slot = slots[slotName];
2340
+ if (!slot) return null;
2341
+ if (!slot.external) {
2342
+ throw new NotFoundError(`Blob slot "${slotName}" is not external \u2014 url() is only for external fields`);
2343
+ }
2344
+ if (!this.objectStore) {
2345
+ throw new NotFoundError(`Blob slot "${slotName}" is external but no objectStore is configured`);
2346
+ }
2347
+ return this.objectStore.objectUrl(slot.external.key, opts);
2348
+ }
2349
+ /**
2350
+ * Build the backlink stamped onto an external object's metadata — the
2351
+ * self-describing "secondary store" reference back to this record. See
2352
+ * {@link BlobFieldPolicy.backlink}. Returns the `userMeta` to attach and, for
2353
+ * `opaque-token`, the `token` to record on the slot.
2354
+ */
2355
+ async buildBacklink(slotName, mode) {
2356
+ if (mode === "none") return {};
2357
+ const ref = { vault: this.vault, collection: this.collection, record: this.recordId, field: slotName };
2358
+ if (mode === "plain") {
2359
+ return {
2360
+ userMeta: {
2361
+ "noydb-vault": ref.vault,
2362
+ "noydb-collection": ref.collection,
2363
+ "noydb-record": ref.record,
2364
+ "noydb-field": ref.field
2365
+ }
2366
+ };
2367
+ }
2368
+ if (mode === "encrypted" && this.encrypted) {
2369
+ const dek = await this.getDEK(BLOB_COLLECTION);
2370
+ const { iv, data } = await encrypt(JSON.stringify(ref), dek);
2371
+ return { userMeta: { "noydb-backlink-enc": `${iv}.${data}` } };
2372
+ }
2373
+ const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
2374
+ const token = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
2375
+ return { userMeta: { "noydb-backlink": token }, token };
2376
+ }
2377
+ /**
2378
+ * Adopt an EXISTING object in the projection into this slot **without
2379
+ * re-uploading** — used by import/bootstrap to anchor objects already in the
2380
+ * bucket. Writes the external slot record (the catalog entry).
2381
+ */
2382
+ async adoptExternal(slotName, ref) {
2383
+ this.assertKeyPartSafe(this.recordId, "record id");
2384
+ this.assertKeyPartSafe(slotName, "slot name");
2385
+ this.assertExternalDeclared(slotName);
2386
+ await this.resolvePendingIntent();
2387
+ const uploaderUserId = this.userId;
2388
+ await this.casUpdateSlots((slots) => {
2389
+ slots[slotName] = {
2390
+ eTag: "",
2391
+ external: {
2392
+ key: ref.key,
2393
+ ...ref.contentType ? { contentType: ref.contentType } : {},
2394
+ ...ref.public ? { public: true } : {},
2395
+ ...ref.backlink ? { backlink: ref.backlink } : {},
2396
+ ...ref.meta ? { meta: ref.meta } : {}
2397
+ },
2398
+ filename: slotName,
2399
+ size: ref.size ?? 0,
2400
+ ...ref.contentType ? { mimeType: ref.contentType } : {},
2401
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
2402
+ ...uploaderUserId !== void 0 ? { uploadedBy: uploaderUserId } : {}
2403
+ };
2404
+ return slots;
2405
+ });
2406
+ }
2407
+ /**
2408
+ * Merge derived metadata (video `duration`, image `width`/`height`, arbitrary
2409
+ * metatags) into an external slot's secondary metadata store. Typically called
2410
+ * from an AWS-side processing callback (MediaConvert / ffprobe / Rekognition)
2411
+ * once it has probed the object. No-op for a missing or non-external slot.
2412
+ */
2413
+ async setExternalMeta(slotName, meta) {
2414
+ this.assertExternalDeclared(slotName);
2415
+ await this.resolvePendingIntent();
2416
+ await this.casUpdateSlots((slots) => {
2417
+ const slot = slots[slotName];
2418
+ if (!slot?.external) return null;
2419
+ slots[slotName] = {
2420
+ ...slot,
2421
+ external: { ...slot.external, meta: { ...slot.external.meta, ...meta } }
2422
+ };
2423
+ return slots;
2424
+ });
2425
+ }
2426
+ /**
2427
+ * Read an external slot's synced derived metadata (the secondary store).
2428
+ * Returns `null` for a missing or non-external slot, `{}` if none synced yet.
2429
+ */
2430
+ async externalMeta(slotName) {
2431
+ if (await this.ownerRecordElevated()) return null;
2432
+ const { slots } = await this.loadSlots();
2433
+ const slot = slots[slotName];
2434
+ if (!slot?.external) return null;
2435
+ return slot.external.meta ?? {};
2436
+ }
2437
+ /**
2438
+ * List all slot entries for this record.
2439
+ * Returns metadata only — no chunk data is loaded.
2440
+ */
2441
+ async list() {
2442
+ if (await this.ownerRecordElevated()) return [];
2443
+ const { slots } = await this.loadSlots();
2444
+ return Object.entries(slots).map(([name, slot]) => {
2445
+ const pub = { ...slot };
2446
+ delete pub.pendingRelease;
2447
+ return { name, ...pub };
2448
+ });
2449
+ }
2450
+ /**
2451
+ * Delete the named slot from this record.
2452
+ * Decrements refCount on the blob. Chunks are GC'd by `vault.blobGC()`.
2453
+ */
2454
+ async delete(slotName) {
2455
+ await this.resolvePendingIntent();
2456
+ let eTagToRelease;
2457
+ let externalKeyToDelete;
2458
+ await this.casUpdateSlots((slots) => {
2459
+ if (!(slotName in slots)) return null;
2460
+ const slot = slots[slotName];
2461
+ if (slot.external) externalKeyToDelete = slot.external.key;
2462
+ else eTagToRelease = slot.eTag;
2463
+ delete slots[slotName];
2464
+ return slots;
2465
+ });
2466
+ if (externalKeyToDelete && this.objectStore) {
2467
+ await this.objectStore.deleteObject(externalKeyToDelete).catch(() => {
2468
+ });
2469
+ }
2470
+ if (eTagToRelease) {
2471
+ await this.releaseRef(eTagToRelease, 1, false).catch(() => {
2472
+ });
2473
+ }
2474
+ }
2475
+ /**
2476
+ * Return a native `Response` whose body streams the decrypted,
2477
+ * decompressed blob bytes with full HTTP metadata headers.
2478
+ *
2479
+ * Note: implementation is buffered — all chunks are loaded into
2480
+ * memory before being enqueued. True streaming deferred to.
2481
+ *
2482
+ * Returns `null` if the slot does not exist.
2483
+ */
2484
+ async response(slotName, opts) {
2485
+ if (await this.ownerRecordElevated()) return null;
2486
+ const { slots } = await this.loadSlots();
2487
+ const slot = slots[slotName];
2488
+ if (!slot) return null;
2489
+ const result = await this.loadBlobObject(slot.eTag);
2490
+ if (!result) return null;
2491
+ const blobDEK = this.encrypted ? await this.getDEK(dekKey(BLOB_COLLECTION, result.atTier)) : void 0;
2492
+ return this.buildResponse(slot, result.blob, opts, blobDEK, this.flatFallbackVerifyETag(slot.eTag, result.atTier));
2493
+ }
2494
+ /**
2495
+ * Decrypt the slot and wrap the bytes in a browser ObjectURL ready
2496
+ * to feed into `<img src>`, `<a href>`, etc. The caller MUST call
2497
+ * `revoke()` when the URL is no longer needed — otherwise the URL
2498
+ * (and the underlying decrypted Blob) are pinned for the lifetime
2499
+ * of the document, which leaks memory in long-lived pages.
2500
+ *
2501
+ * Returns `null` when the slot does not exist.
2502
+ *
2503
+ * Throws when `URL.createObjectURL` is unavailable in the host
2504
+ * environment (Node without DOM, restricted workers). Framework
2505
+ * adapters — `useBlobURL` in `@noy-db/in-vue`, etc. — guard against
2506
+ * this for SSR contexts and stay at `null` instead of propagating.
2507
+ */
2508
+ async objectURL(slotName, opts) {
2509
+ if (typeof URL === "undefined" || typeof URL.createObjectURL !== "function") {
2510
+ throw new Error(
2511
+ "BlobSet.objectURL: URL.createObjectURL is unavailable in this environment. Call this from the browser, or use BlobSet.get() and create the URL yourself."
2512
+ );
2513
+ }
2514
+ const bytes = await this.get(slotName);
2515
+ if (!bytes) return null;
2516
+ const { slots } = await this.loadSlots();
2517
+ const slot = slots[slotName];
2518
+ const type = opts?.mimeType ?? slot?.mimeType ?? "application/octet-stream";
2519
+ const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
2520
+ const blob = new Blob([buffer], { type });
2521
+ const url = URL.createObjectURL(blob);
2522
+ let revoked = false;
2523
+ const revoke = () => {
2524
+ if (revoked) return;
2525
+ revoked = true;
2526
+ URL.revokeObjectURL(url);
2527
+ };
2528
+ return { url, revoke };
2529
+ }
2530
+ // ─── Public API: Published versions (UC-3 amendment versioning) ───
2531
+ /**
2532
+ * Publish the current slot content as a named version snapshot.
2533
+ *
2534
+ * The published version holds an independent refCount reference to
2535
+ * the blob. Even if the slot is later overwritten or deleted, the
2536
+ * published version keeps the blob data alive.
2537
+ *
2538
+ * Publishing with an existing label overwrites it — if the eTags differ,
2539
+ * refCounts are adjusted accordingly.
2540
+ *
2541
+ * #724 Arc 10 Task 2 (C4): the version record is written under the owning
2542
+ * record's CURRENT tier — a version published on a tier-N record is keyed
2543
+ * at tier N, mirroring `put()`'s owner-tier resolution (Task 1). The
2544
+ * refCount hold it takes lands on `slot.eTag`, which is already
2545
+ * tier-scoped (born there by `put()`, or moved there by a prior
2546
+ * `rehomeForTier`) — no separate tier-scoping needed for the content side.
2547
+ */
2548
+ async publish(slotName, label) {
2549
+ this.assertKeyPartSafe(this.recordId, "record id");
2550
+ this.assertKeyPartSafe(slotName, "slot name");
2551
+ this.assertKeyPartSafe(label, "version label");
2552
+ this.assertBlobWritable();
2553
+ await this.resolvePendingIntent();
2554
+ const { slots } = await this.loadSlots();
2555
+ const slot = slots[slotName];
2556
+ if (!slot) throw new NotFoundError(`Slot "${slotName}" not found on record "${this.recordId}"`);
2557
+ const tier = await this.ownerTier();
2558
+ const existing = await this.loadVersionRecord(slotName, label, tier);
2559
+ if (existing && existing.eTag === slot.eTag) return;
2560
+ const record = {
2561
+ label,
2562
+ eTag: slot.eTag,
2563
+ publishedAt: (/* @__PURE__ */ new Date()).toISOString(),
2564
+ ...this.userId !== void 0 ? { publishedBy: this.userId } : {}
2565
+ };
2566
+ await this.writeVersionRecord(slotName, record, tier);
2567
+ await this.casUpdateRefCount(slot.eTag, 1);
2568
+ if (existing && existing.eTag !== slot.eTag) {
2569
+ await this.releaseRef(existing.eTag, 1, false).catch(() => {
2570
+ });
2571
+ }
2572
+ }
2573
+ /**
2574
+ * Fetch bytes for a published version.
2575
+ * Returns `null` if the version does not exist.
2576
+ */
2577
+ async getVersion(slotName, label) {
2578
+ if (await this.ownerRecordElevated()) return null;
2579
+ const record = await this.loadVersionRecord(slotName, label);
2580
+ if (!record) return null;
2581
+ const result = await this.loadBlobObject(record.eTag);
2582
+ if (!result) return null;
2583
+ const blobDEK = this.encrypted ? await this.getDEK(dekKey(BLOB_COLLECTION, result.atTier)) : void 0;
2584
+ return this.fetchAllChunks(result.blob, blobDEK, this.flatFallbackVerifyETag(record.eTag, result.atTier));
2585
+ }
2586
+ /**
2587
+ * List all published versions for a slot.
2588
+ */
2589
+ async listVersions(slotName) {
2590
+ if (await this.ownerRecordElevated()) return [];
2591
+ const prefix = `${this.recordId}::${slotName}::`;
2592
+ const allKeys = await this.store.list(this.vault, this.versionsCollection);
2593
+ const matchingKeys = allKeys.filter((k) => k.startsWith(prefix));
2594
+ const versions = [];
2595
+ for (const key of matchingKeys) {
2596
+ const envelope = await this.store.get(this.vault, this.versionsCollection, key);
2597
+ if (!envelope) continue;
2598
+ if (!this.encrypted) {
2599
+ versions.push(JSON.parse(envelope._data));
2600
+ } else {
2601
+ const dek = await this.getDEK(this.collection);
2602
+ const json = await openEnvelopeJson(envelope, dek);
2603
+ versions.push(JSON.parse(json));
2604
+ }
2605
+ }
2606
+ return versions;
2607
+ }
2608
+ /**
2609
+ * Delete a published version. Decrements refCount on its blob.
2610
+ */
2611
+ async deleteVersion(slotName, label) {
2612
+ await this.resolvePendingIntent();
2613
+ const record = await this.loadVersionRecord(slotName, label);
2614
+ if (!record) return;
2615
+ await this.deleteVersionRecord(slotName, label);
2616
+ await this.releaseRef(record.eTag, 1, false).catch(() => {
2617
+ });
2618
+ }
2619
+ /**
2620
+ * Return a `Response` for a published version — same as `response()`
2621
+ * but reads from the version record's eTag instead of the current slot.
2622
+ */
2623
+ async responseVersion(slotName, label, opts) {
2624
+ if (await this.ownerRecordElevated()) return null;
2625
+ const record = await this.loadVersionRecord(slotName, label);
2626
+ if (!record) return null;
2627
+ const result = await this.loadBlobObject(record.eTag);
2628
+ if (!result) return null;
2629
+ const slotLike = {
2630
+ eTag: record.eTag,
2631
+ filename: opts?.filename ?? `${slotName}-${label}`,
2632
+ size: result.blob.size,
2633
+ ...result.blob.mimeType !== void 0 ? { mimeType: result.blob.mimeType } : {},
2634
+ uploadedAt: record.publishedAt,
2635
+ ...record.publishedBy !== void 0 ? { uploadedBy: record.publishedBy } : {}
2636
+ };
2637
+ const blobDEK = this.encrypted ? await this.getDEK(dekKey(BLOB_COLLECTION, result.atTier)) : void 0;
2638
+ return this.buildResponse(slotLike, result.blob, opts, blobDEK, this.flatFallbackVerifyETag(record.eTag, result.atTier));
2639
+ }
2640
+ // ─── Diagnostics ──────────────────────────────────────────────────
2641
+ /**
2642
+ * Return the `BlobObject` metadata for the named slot.
2643
+ * Returns `null` if the slot or blob does not exist.
2644
+ */
2645
+ async blobInfo(slotName) {
2646
+ if (await this.ownerRecordElevated()) return null;
2647
+ const { slots } = await this.loadSlots();
2648
+ const slot = slots[slotName];
2649
+ if (!slot) return null;
2650
+ const result = await this.loadBlobObject(slot.eTag);
2651
+ return result?.blob ?? null;
2652
+ }
2653
+ // ─── Presigned URL (E5) ────────────────────────────────────────────
2654
+ /**
2655
+ * Generate a presigned URL for direct client download of the blob's
2656
+ * ciphertext. Only works when the blob store supports `presignUrl`.
2657
+ *
2658
+ * **Important:** The URL returns encrypted data. The caller must
2659
+ * decrypt client-side using `decryptResponse()` or a service worker.
2660
+ *
2661
+ * Returns `null` if the slot doesn't exist or the store doesn't support presigning.
2662
+ */
2663
+ async presignedUrl(slotName, expiresInSeconds = 3600) {
2664
+ if (await this.ownerRecordElevated()) return null;
2665
+ const { slots } = await this.loadSlots();
2666
+ const slot = slots[slotName];
2667
+ if (!slot) return null;
2668
+ const result = await this.loadBlobObject(slot.eTag);
2669
+ if (!result) return null;
2670
+ if (result.blob.chunkCount !== 1) return null;
2671
+ if (!this.store.presignUrl) return null;
2672
+ const chunkId = `${slot.eTag}_0`;
2673
+ return this.store.presignUrl(this.vault, "_blob_chunks", chunkId, expiresInSeconds);
2674
+ }
2675
+ /**
2676
+ * Decrypt a ciphertext Response (e.g. from a presigned URL fetch)
2677
+ * back into a plaintext Response with correct headers.
2678
+ *
2679
+ * Usage with service worker or client-side fetch:
2680
+ * ```ts
2681
+ * const url = await blobs.presignedUrl('invoice.pdf')
2682
+ * const cipherResponse = await fetch(url)
2683
+ * const plainResponse = await blobs.decryptResponse('invoice.pdf', cipherResponse)
2684
+ * ```
2685
+ */
2686
+ async decryptResponse(slotName, cipherResponse) {
2687
+ if (await this.ownerRecordElevated()) return null;
2688
+ const { slots } = await this.loadSlots();
2689
+ const slot = slots[slotName];
2690
+ if (!slot) return null;
2691
+ const result = await this.loadBlobObject(slot.eTag);
2692
+ if (!result) return null;
2693
+ if (result.blob.chunkCount !== 1) {
2694
+ throw new ValidationError(
2695
+ `decryptResponse() does not support multi-chunk blobs (slot "${slotName}" has ${result.blob.chunkCount} chunks) \u2014 presignedUrl() only mints URLs for single-chunk blobs (#757)`
2696
+ );
2697
+ }
2698
+ const blobDEK = this.encrypted ? await this.getDEK(dekKey(BLOB_COLLECTION, result.atTier)) : null;
2699
+ if (!blobDEK) {
2700
+ return this.buildResponse(slot, result.blob, { inline: true });
2701
+ }
2702
+ const text = await cipherResponse.text();
2703
+ const envelope = JSON.parse(text);
2704
+ const chunkKey = await this.resolveChunkKey(result.blob, blobDEK);
2705
+ if (!chunkKey) {
2706
+ return this.buildResponse(slot, result.blob, { inline: true });
2707
+ }
2708
+ const aad = chunkAAD(slot.eTag, 0, result.blob.chunkCount);
2709
+ const decrypted = await decryptBytesWithAAD(envelope._iv, envelope._data, chunkKey, aad);
2710
+ const plaintext = result.blob.compression === "gzip" ? await decompressBytes(decrypted) : decrypted;
2711
+ const recomputed = await hmacSha256Hex(blobDEK, plaintext);
2712
+ if (recomputed !== slot.eTag) {
2713
+ throw new TamperedError(
2714
+ `Blob content for eTag "${slot.eTag}" failed content-address verification in decryptResponse() \u2014 the caller-provided ciphertext does not match the requested content address (#757)`
2715
+ );
2716
+ }
2717
+ const body = new ReadableStream({
2718
+ start(controller) {
2719
+ controller.enqueue(plaintext);
2720
+ controller.close();
2721
+ }
2722
+ });
2723
+ const filename = slot.filename;
2724
+ return new Response(body, {
2725
+ headers: {
2726
+ "Content-Type": slot.mimeType ?? "application/octet-stream",
2727
+ "Content-Length": String(slot.size),
2728
+ "ETag": `"${slot.eTag}"`,
2729
+ "Content-Disposition": `inline; filename="${filename}"`,
2730
+ "Last-Modified": new Date(slot.uploadedAt).toUTCString()
2731
+ }
2732
+ });
2733
+ }
2734
+ // ─── Internal: build Response from slot + blob ────────────────────
2735
+ async buildResponse(slot, blob, opts, blobDEK, verifyFlatETag) {
2736
+ const fetchAllChunks = this.fetchAllChunks.bind(this);
2737
+ const body = new ReadableStream({
2738
+ async start(controller) {
2739
+ try {
2740
+ const output = await fetchAllChunks(blob, blobDEK, verifyFlatETag);
2741
+ controller.enqueue(output);
2742
+ controller.close();
2743
+ } catch (err) {
2744
+ controller.error(err);
2745
+ }
2746
+ }
2747
+ });
2748
+ const filename = opts?.filename ?? slot.filename;
2749
+ const disposition = opts?.inline ? `inline; filename="${filename}"` : `attachment; filename="${filename}"`;
2750
+ return new Response(body, {
2751
+ headers: {
2752
+ "Content-Type": slot.mimeType ?? "application/octet-stream",
2753
+ "Content-Length": String(slot.size),
2754
+ "ETag": `"${slot.eTag}"`,
2755
+ "Content-Disposition": disposition,
2756
+ "Last-Modified": new Date(slot.uploadedAt).toUTCString()
2757
+ }
2758
+ });
2759
+ }
2760
+ };
2761
+ async function plainSha256Hex(data) {
2762
+ return sha256Hex(data);
2763
+ }
2764
+
2765
+ export {
2766
+ detectMimeType,
2767
+ detectMagic,
2768
+ isPreCompressed,
2769
+ BLOB_INTENT_COLLECTION,
2770
+ getIntent,
2771
+ createIntent,
2772
+ deleteIntent,
2773
+ sweepBlobIntents,
2774
+ BLOB_COLLECTION,
2775
+ BLOB_INDEX_COLLECTION,
2776
+ BLOB_CHUNKS_COLLECTION,
2777
+ BLOB_SLOTS_PREFIX,
2778
+ BLOB_VERSIONS_PREFIX,
2779
+ DEFAULT_CHUNK_SIZE,
2780
+ BlobSet
2781
+ };
2782
+ //# sourceMappingURL=chunk-SNZ6URZZ.js.map