@noy-db/hub 0.1.0-pre.9 → 0.2.0-pre.2

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 (288) hide show
  1. package/dist/aggregate/index.cjs +91 -36
  2. package/dist/aggregate/index.cjs.map +1 -1
  3. package/dist/aggregate/index.d.cts +2 -2
  4. package/dist/aggregate/index.d.ts +2 -2
  5. package/dist/aggregate/index.js +16 -9
  6. package/dist/aggregate/index.js.map +1 -1
  7. package/dist/attestation/index.cjs +305 -0
  8. package/dist/attestation/index.cjs.map +1 -0
  9. package/dist/attestation/index.d.cts +52 -0
  10. package/dist/attestation/index.d.ts +52 -0
  11. package/dist/attestation/index.js +36 -0
  12. package/dist/attestation/index.js.map +1 -0
  13. package/dist/blobs/index.cjs.map +1 -1
  14. package/dist/blobs/index.d.cts +7 -6
  15. package/dist/blobs/index.d.ts +7 -6
  16. package/dist/blobs/index.js +10 -8
  17. package/dist/blobs/index.js.map +1 -1
  18. package/dist/bundle/index.cjs +16923 -60
  19. package/dist/bundle/index.cjs.map +1 -1
  20. package/dist/bundle/index.d.cts +175 -6
  21. package/dist/bundle/index.d.ts +175 -6
  22. package/dist/bundle/index.js +543 -4
  23. package/dist/bundle/index.js.map +1 -1
  24. package/dist/{chunk-PTVMYYON.js → chunk-243PNUA6.js} +3 -3
  25. package/dist/{chunk-MR4424N3.js → chunk-2PAQNPE3.js} +2 -2
  26. package/dist/chunk-3QAKZ37R.js +83 -0
  27. package/dist/chunk-3QAKZ37R.js.map +1 -0
  28. package/dist/chunk-3S4BJX25.js +36 -0
  29. package/dist/chunk-3S4BJX25.js.map +1 -0
  30. package/dist/chunk-3XHOCQK4.js +118 -0
  31. package/dist/chunk-3XHOCQK4.js.map +1 -0
  32. package/dist/{chunk-AVVPZ4BC.js → chunk-3Y53S2SA.js} +4 -4
  33. package/dist/chunk-3Z2TPHC4.js +291 -0
  34. package/dist/chunk-3Z2TPHC4.js.map +1 -0
  35. package/dist/chunk-4HIL6AHQ.js +57 -0
  36. package/dist/chunk-4HIL6AHQ.js.map +1 -0
  37. package/dist/chunk-5ZGZ6HIZ.js +100 -0
  38. package/dist/chunk-5ZGZ6HIZ.js.map +1 -0
  39. package/dist/{chunk-ZFKD4QMV.js → chunk-7BRE6EUA.js} +3 -3
  40. package/dist/chunk-7BUTTVMR.js +34 -0
  41. package/dist/chunk-7BUTTVMR.js.map +1 -0
  42. package/dist/{chunk-VQBTTTUN.js → chunk-7Q5PLD5C.js} +4 -4
  43. package/dist/{chunk-VQBTTTUN.js.map → chunk-7Q5PLD5C.js.map} +1 -1
  44. package/dist/{chunk-QAVUREFT.js → chunk-7Z23ZFLV.js} +12 -6
  45. package/dist/chunk-7Z23ZFLV.js.map +1 -0
  46. package/dist/chunk-AHPFONIL.js +59 -0
  47. package/dist/chunk-AHPFONIL.js.map +1 -0
  48. package/dist/chunk-CXSCDO5T.js +51 -0
  49. package/dist/chunk-CXSCDO5T.js.map +1 -0
  50. package/dist/chunk-E535SAN4.js +8834 -0
  51. package/dist/chunk-E535SAN4.js.map +1 -0
  52. package/dist/chunk-EUYOGYGV.js +830 -0
  53. package/dist/chunk-EUYOGYGV.js.map +1 -0
  54. package/dist/chunk-FAQVNJD4.js +61 -0
  55. package/dist/chunk-FAQVNJD4.js.map +1 -0
  56. package/dist/{chunk-SCZXXXU4.js → chunk-G6FRSBKK.js} +7 -32
  57. package/dist/chunk-G6FRSBKK.js.map +1 -0
  58. package/dist/chunk-GIV6DWBG.js +79 -0
  59. package/dist/chunk-GIV6DWBG.js.map +1 -0
  60. package/dist/chunk-HXJXPZRE.js +73 -0
  61. package/dist/chunk-HXJXPZRE.js.map +1 -0
  62. package/dist/{chunk-GOUT6DND.js → chunk-J4KLMEUL.js} +173 -91
  63. package/dist/chunk-J4KLMEUL.js.map +1 -0
  64. package/dist/{chunk-2CSJGFCB.js → chunk-JYQTXEIO.js} +6 -229
  65. package/dist/chunk-JYQTXEIO.js.map +1 -0
  66. package/dist/{chunk-MDDTIZUO.js → chunk-LRAZDV5X.js} +7 -119
  67. package/dist/chunk-LRAZDV5X.js.map +1 -0
  68. package/dist/{chunk-M5INGEFC.js → chunk-MRIBLZL3.js} +3 -1
  69. package/dist/chunk-MRIBLZL3.js.map +1 -0
  70. package/dist/{chunk-USKYUS74.js → chunk-MUWOSVEP.js} +2 -2
  71. package/dist/{chunk-4PWAI7Q4.js → chunk-NWZ3I6R6.js} +5 -5
  72. package/dist/chunk-OVZDFEOR.js +124 -0
  73. package/dist/chunk-OVZDFEOR.js.map +1 -0
  74. package/dist/chunk-PEULZC6M.js +118 -0
  75. package/dist/chunk-PEULZC6M.js.map +1 -0
  76. package/dist/chunk-PFSNOPBQ.js +233 -0
  77. package/dist/chunk-PFSNOPBQ.js.map +1 -0
  78. package/dist/chunk-PLI5TV7N.js +53 -0
  79. package/dist/chunk-PLI5TV7N.js.map +1 -0
  80. package/dist/{chunk-WDM5XGGS.js → chunk-Q6W2CMEJ.js} +181 -11
  81. package/dist/chunk-Q6W2CMEJ.js.map +1 -0
  82. package/dist/{chunk-QGZRWRSL.js → chunk-QPEXPHJR.js} +4 -4
  83. package/dist/{chunk-R36SIKES.js → chunk-QXQRKXCU.js} +2 -2
  84. package/dist/chunk-RTZVQAJ7.js +82 -0
  85. package/dist/chunk-RTZVQAJ7.js.map +1 -0
  86. package/dist/chunk-TBKOGSYR.js +296 -0
  87. package/dist/chunk-TBKOGSYR.js.map +1 -0
  88. package/dist/chunk-UMLVJTYV.js +20 -0
  89. package/dist/chunk-UMLVJTYV.js.map +1 -0
  90. package/dist/chunk-UND4XIB6.js +251 -0
  91. package/dist/chunk-UND4XIB6.js.map +1 -0
  92. package/dist/chunk-VCGTOS2A.js +795 -0
  93. package/dist/chunk-VCGTOS2A.js.map +1 -0
  94. package/dist/chunk-VE6YVP32.js +19 -0
  95. package/dist/chunk-VE6YVP32.js.map +1 -0
  96. package/dist/{chunk-M62XNWRA.js → chunk-VK5EER6C.js} +2 -2
  97. package/dist/{chunk-NXFEYLVG.js → chunk-VPSUZLOJ.js} +4 -3
  98. package/dist/{chunk-NXFEYLVG.js.map → chunk-VPSUZLOJ.js.map} +1 -1
  99. package/dist/{chunk-TDR6T5CJ.js → chunk-VRBCTEKQ.js} +91 -132
  100. package/dist/chunk-VRBCTEKQ.js.map +1 -0
  101. package/dist/{chunk-ACLDOTNQ.js → chunk-W3XXT26A.js} +303 -3
  102. package/dist/chunk-W3XXT26A.js.map +1 -0
  103. package/dist/{chunk-CIMZBAZB.js → chunk-XG3PTSCD.js} +1 -1
  104. package/dist/chunk-XG3PTSCD.js.map +1 -0
  105. package/dist/chunk-Y2RKOPNC.js +145 -0
  106. package/dist/chunk-Y2RKOPNC.js.map +1 -0
  107. package/dist/{chunk-NPC4LFV5.js → chunk-YMYK7US4.js} +2 -2
  108. package/dist/{chunk-RKJ6OL7K.js → chunk-YS3POABP.js} +1 -1
  109. package/dist/chunk-YS3POABP.js.map +1 -0
  110. package/dist/chunk-YTXSFG3C.js +179 -0
  111. package/dist/chunk-YTXSFG3C.js.map +1 -0
  112. package/dist/consent/index.cjs.map +1 -1
  113. package/dist/consent/index.d.cts +7 -6
  114. package/dist/consent/index.d.ts +7 -6
  115. package/dist/consent/index.js +3 -3
  116. package/dist/{crypto-IVKU7YTT.js → crypto-5ZDIY3NG.js} +3 -3
  117. package/dist/{delegation-2DBS2EOH.js → delegation-QYXZW25W.js} +5 -4
  118. package/dist/derivations/index.cjs +351 -0
  119. package/dist/derivations/index.cjs.map +1 -0
  120. package/dist/derivations/index.d.cts +72 -0
  121. package/dist/derivations/index.d.ts +72 -0
  122. package/dist/derivations/index.js +27 -0
  123. package/dist/{dev-unlock-Da1B0TIK.d.cts → dev-unlock-DQCNDfFp.d.cts} +1 -1
  124. package/dist/{dev-unlock-BdPp68qn.d.ts → dev-unlock-utkybTKb.d.ts} +1 -1
  125. package/dist/executor-AS2IDHKZ.js +11 -0
  126. package/dist/executor-HLXFXNFM.js +8 -0
  127. package/dist/executor-HLXFXNFM.js.map +1 -0
  128. package/dist/executor-HN6YBHZ5.js +8 -0
  129. package/dist/executor-HN6YBHZ5.js.map +1 -0
  130. package/dist/fanout-sidecar-VJ52RIEY.js +51 -0
  131. package/dist/fanout-sidecar-VJ52RIEY.js.map +1 -0
  132. package/dist/guards/index.cjs +315 -0
  133. package/dist/guards/index.cjs.map +1 -0
  134. package/dist/guards/index.d.cts +31 -0
  135. package/dist/guards/index.d.ts +31 -0
  136. package/dist/guards/index.js +29 -0
  137. package/dist/guards/index.js.map +1 -0
  138. package/dist/{hash-lsoL3eEW.d.ts → hash-DcoYWfJ_.d.ts} +1 -1
  139. package/dist/{hash-BEfzPKwo.d.cts → hash-jDowCrK2.d.cts} +1 -1
  140. package/dist/history/index.cjs +8 -1
  141. package/dist/history/index.cjs.map +1 -1
  142. package/dist/history/index.d.cts +8 -7
  143. package/dist/history/index.d.ts +8 -7
  144. package/dist/history/index.js +6 -6
  145. package/dist/i18n/index.cjs +81 -0
  146. package/dist/i18n/index.cjs.map +1 -1
  147. package/dist/i18n/index.d.cts +7 -6
  148. package/dist/i18n/index.d.ts +7 -6
  149. package/dist/i18n/index.js +27 -12
  150. package/dist/i18n/index.js.map +1 -1
  151. package/dist/{index-6xNpPsxR.d.cts → index-BCKdioeh.d.ts} +331 -5
  152. package/dist/{index-DJTf9yxn.d.ts → index-BMjrzNZr.d.cts} +331 -5
  153. package/dist/index.cjs +6065 -959
  154. package/dist/index.cjs.map +1 -1
  155. package/dist/index.d.cts +208 -16
  156. package/dist/index.d.ts +208 -16
  157. package/dist/index.js +242 -7392
  158. package/dist/index.js.map +1 -1
  159. package/dist/indexing/index.cjs +2 -0
  160. package/dist/indexing/index.cjs.map +1 -1
  161. package/dist/indexing/index.d.cts +3 -3
  162. package/dist/indexing/index.d.ts +3 -3
  163. package/dist/indexing/index.js +4 -4
  164. package/dist/issue-ORP37MVW.js +12 -0
  165. package/dist/issue-ORP37MVW.js.map +1 -0
  166. package/dist/{lazy-builder-CZVLKh0Z.d.cts → lazy-builder-C-rPfWG0.d.cts} +1 -1
  167. package/dist/{lazy-builder-BwEoBQZ9.d.ts → lazy-builder-Rpd-V3jP.d.ts} +1 -1
  168. package/dist/{ledger-QZTTHQAQ.js → ledger-3IU5GMXA.js} +6 -6
  169. package/dist/ledger-3IU5GMXA.js.map +1 -0
  170. package/dist/materialized-views/index.cjs +837 -0
  171. package/dist/materialized-views/index.cjs.map +1 -0
  172. package/dist/materialized-views/index.d.cts +184 -0
  173. package/dist/materialized-views/index.d.ts +184 -0
  174. package/dist/materialized-views/index.js +45 -0
  175. package/dist/materialized-views/index.js.map +1 -0
  176. package/dist/noydb-5H3C24GG.js +34 -0
  177. package/dist/noydb-5H3C24GG.js.map +1 -0
  178. package/dist/overlay-views/index.cjs +359 -0
  179. package/dist/overlay-views/index.cjs.map +1 -0
  180. package/dist/overlay-views/index.d.cts +82 -0
  181. package/dist/overlay-views/index.d.ts +82 -0
  182. package/dist/overlay-views/index.js +25 -0
  183. package/dist/overlay-views/index.js.map +1 -0
  184. package/dist/periods/index.cjs +7 -1
  185. package/dist/periods/index.cjs.map +1 -1
  186. package/dist/periods/index.d.cts +7 -6
  187. package/dist/periods/index.d.ts +7 -6
  188. package/dist/periods/index.js +6 -6
  189. package/dist/{predicate-SBHmi6D0.d.cts → predicate-Dnu81tsS.d.cts} +25 -1
  190. package/dist/{predicate-SBHmi6D0.d.ts → predicate-Dnu81tsS.d.ts} +25 -1
  191. package/dist/{public-envelope-6JTACYJV.js → public-envelope-U3CMEOMV.js} +4 -4
  192. package/dist/public-envelope-U3CMEOMV.js.map +1 -0
  193. package/dist/query/index.cjs +302 -124
  194. package/dist/query/index.cjs.map +1 -1
  195. package/dist/query/index.d.cts +3 -3
  196. package/dist/query/index.d.ts +3 -3
  197. package/dist/query/index.js +26 -11
  198. package/dist/read-only-facade-ITU6L7BL.js +7 -0
  199. package/dist/read-only-facade-ITU6L7BL.js.map +1 -0
  200. package/dist/registry-3ALP62P6.js +10 -0
  201. package/dist/registry-3ALP62P6.js.map +1 -0
  202. package/dist/registry-7HE6VJGC.js +8 -0
  203. package/dist/registry-7HE6VJGC.js.map +1 -0
  204. package/dist/registry-PSIPG2QR.js +8 -0
  205. package/dist/registry-PSIPG2QR.js.map +1 -0
  206. package/dist/registry-RFGGMVNJ.js +7 -0
  207. package/dist/registry-RFGGMVNJ.js.map +1 -0
  208. package/dist/revoke-KY2GB4KP.js +17 -0
  209. package/dist/revoke-KY2GB4KP.js.map +1 -0
  210. package/dist/session/index.cjs +7 -1
  211. package/dist/session/index.cjs.map +1 -1
  212. package/dist/session/index.d.cts +8 -7
  213. package/dist/session/index.d.ts +8 -7
  214. package/dist/session/index.js +10 -3
  215. package/dist/session/index.js.map +1 -1
  216. package/dist/shadow/index.cjs.map +1 -1
  217. package/dist/shadow/index.d.cts +7 -6
  218. package/dist/shadow/index.d.ts +7 -6
  219. package/dist/shadow/index.js +2 -2
  220. package/dist/signer-GRI5TZKH.js +18 -0
  221. package/dist/signer-GRI5TZKH.js.map +1 -0
  222. package/dist/stale-OTOF3FH7.js +13 -0
  223. package/dist/stale-OTOF3FH7.js.map +1 -0
  224. package/dist/store/index.cjs +14 -0
  225. package/dist/store/index.cjs.map +1 -1
  226. package/dist/store/index.d.cts +7 -6
  227. package/dist/store/index.d.ts +7 -6
  228. package/dist/store/index.js +5 -2
  229. package/dist/{strategy-D-SrOLCl.d.cts → strategy-DSTrsZ8t.d.cts} +72 -19
  230. package/dist/{strategy-D-SrOLCl.d.ts → strategy-DSTrsZ8t.d.ts} +72 -19
  231. package/dist/sync/index.cjs.map +1 -1
  232. package/dist/sync/index.d.cts +6 -5
  233. package/dist/sync/index.d.ts +6 -5
  234. package/dist/sync/index.js +4 -4
  235. package/dist/team/index.cjs +1554 -2
  236. package/dist/team/index.cjs.map +1 -1
  237. package/dist/team/index.d.cts +7 -6
  238. package/dist/team/index.d.ts +7 -6
  239. package/dist/team/index.js +77 -8
  240. package/dist/tx/index.cjs +296 -44
  241. package/dist/tx/index.cjs.map +1 -1
  242. package/dist/tx/index.d.cts +7 -6
  243. package/dist/tx/index.d.ts +7 -6
  244. package/dist/tx/index.js +2 -2
  245. package/dist/{types-Bo7NSXJr.d.ts → types-BoFFiskX.d.ts} +2714 -321
  246. package/dist/{types-Bnb82f5R.d.cts → types-DJG8HG6F.d.cts} +2714 -321
  247. package/dist/{index-CywCC1qZ.d.cts → ulid-BmBgooGm.d.ts} +215 -26
  248. package/dist/{index-8QDuznDr.d.ts → ulid-C7ms9oli.d.cts} +215 -26
  249. package/dist/util/index.cjs.map +1 -1
  250. package/dist/util/index.js +1 -1
  251. package/dist/with-derivation-BKXXa8Vt.d.ts +13 -0
  252. package/dist/with-derivation-BjQ7q4NE.d.cts +13 -0
  253. package/dist/with-guard-C25yNjzd.d.ts +18 -0
  254. package/dist/with-guard-DQme5DKE.d.cts +18 -0
  255. package/dist/with-materialized-view-BbEPFIIJ.d.cts +27 -0
  256. package/dist/with-materialized-view-CqnRwI2S.d.ts +27 -0
  257. package/dist/with-overlayed-view-Ct1fSJt-.d.ts +13 -0
  258. package/dist/with-overlayed-view-bwlmmFjx.d.cts +13 -0
  259. package/package.json +65 -2
  260. package/dist/chunk-2CSJGFCB.js.map +0 -1
  261. package/dist/chunk-ACLDOTNQ.js.map +0 -1
  262. package/dist/chunk-BTDCBVJW.js +0 -160
  263. package/dist/chunk-BTDCBVJW.js.map +0 -1
  264. package/dist/chunk-CIMZBAZB.js.map +0 -1
  265. package/dist/chunk-EXHNQEV4.js +0 -392
  266. package/dist/chunk-EXHNQEV4.js.map +0 -1
  267. package/dist/chunk-GOUT6DND.js.map +0 -1
  268. package/dist/chunk-M5INGEFC.js.map +0 -1
  269. package/dist/chunk-MDDTIZUO.js.map +0 -1
  270. package/dist/chunk-QAVUREFT.js.map +0 -1
  271. package/dist/chunk-RKJ6OL7K.js.map +0 -1
  272. package/dist/chunk-SCZXXXU4.js.map +0 -1
  273. package/dist/chunk-TDR6T5CJ.js.map +0 -1
  274. package/dist/chunk-WDM5XGGS.js.map +0 -1
  275. /package/dist/{chunk-PTVMYYON.js.map → chunk-243PNUA6.js.map} +0 -0
  276. /package/dist/{chunk-MR4424N3.js.map → chunk-2PAQNPE3.js.map} +0 -0
  277. /package/dist/{chunk-AVVPZ4BC.js.map → chunk-3Y53S2SA.js.map} +0 -0
  278. /package/dist/{chunk-ZFKD4QMV.js.map → chunk-7BRE6EUA.js.map} +0 -0
  279. /package/dist/{chunk-USKYUS74.js.map → chunk-MUWOSVEP.js.map} +0 -0
  280. /package/dist/{chunk-4PWAI7Q4.js.map → chunk-NWZ3I6R6.js.map} +0 -0
  281. /package/dist/{chunk-QGZRWRSL.js.map → chunk-QPEXPHJR.js.map} +0 -0
  282. /package/dist/{chunk-R36SIKES.js.map → chunk-QXQRKXCU.js.map} +0 -0
  283. /package/dist/{chunk-M62XNWRA.js.map → chunk-VK5EER6C.js.map} +0 -0
  284. /package/dist/{chunk-NPC4LFV5.js.map → chunk-YMYK7US4.js.map} +0 -0
  285. /package/dist/{crypto-IVKU7YTT.js.map → crypto-5ZDIY3NG.js.map} +0 -0
  286. /package/dist/{delegation-2DBS2EOH.js.map → delegation-QYXZW25W.js.map} +0 -0
  287. /package/dist/{ledger-QZTTHQAQ.js.map → derivations/index.js.map} +0 -0
  288. /package/dist/{public-envelope-6JTACYJV.js.map → executor-AS2IDHKZ.js.map} +0 -0
@@ -1,8 +1,9 @@
1
- import { I as IndexStrategy, d as LazyQuery } from './lazy-builder-BwEoBQZ9.js';
2
- import { A as AggregateStrategy } from './strategy-D-SrOLCl.js';
1
+ import { I as IndexStrategy, d as LazyQuery } from './lazy-builder-Rpd-V3jP.js';
2
+ import { b as AggregateSpec, A as AggregateStrategy } from './strategy-DSTrsZ8t.js';
3
3
  import { C as CrdtStrategy, a as CrdtMode, b as CrdtState } from './strategy-BSxFXGzb.js';
4
- import { N as NoydbError, U as RefDescriptor, p as JoinableSource, Z as RefViolation, Q as Query, $ as ScanBuilder } from './index-DJTf9yxn.js';
5
- import { I as IndexDef, C as CollectionIndexes } from './predicate-SBHmi6D0.js';
4
+ import { N as NoydbError, Q as Query, ao as RefRegistry, al as RefDescriptor, a2 as JoinableSource, aq as RefViolation, ar as ScanBuilder } from './index-BCKdioeh.js';
5
+ import { F as FieldClause, I as IndexDef, C as CollectionIndexes } from './predicate-Dnu81tsS.js';
6
+ import { AttestationFieldSchema, RevocationList } from '@noy-db/attestation';
6
7
 
7
8
  /**
8
9
  * Standard Schema v1 integration.
@@ -785,12 +786,24 @@ interface LedgerEntry {
785
786
  readonly prevHash: string;
786
787
  /**
787
788
  * Which kind of mutation this entry records. only supports
788
- * data operations (`put`, `delete`). Access-control operations
789
- * (`grant`, `revoke`, `rotate`) will be added in a follow-up once
790
- * the keyring write path is instrumented — that's tracked in the
791
- * epic issue.
792
- */
793
- readonly op: 'put' | 'delete';
789
+ * data operations (`put`, `delete`, `amendment`). Access-control
790
+ * operations (`grant`, `revoke`, `rotate`) will be added in a
791
+ * follow-up once the keyring write path is instrumented — that's
792
+ * tracked in the epic issue.
793
+ *
794
+ * `'amendment'` is the multi-record audit entry written by the
795
+ * guards subsystem when an admin/owner uses `withTransactions(...)`
796
+ * to repair a constraint-violating state. See `amendment` field
797
+ * below for the structured payload.
798
+ *
799
+ * `'lifecycle'` records a non-data audit event (e.g. partition
800
+ * handover, #226) — `collection`/`id` are empty and the event detail
801
+ * lives in `reason` (e.g. `'partition-handed-over:<sealId>'`). Like
802
+ * `amendment`, it carries no data envelope, so `verifyBackupIntegrity`
803
+ * skips it in the data cross-check (it still participates in the
804
+ * tamper-evident chain).
805
+ */
806
+ readonly op: 'put' | 'delete' | 'amendment' | 'lifecycle';
794
807
  /** The collection the mutation targeted. */
795
808
  readonly collection: string;
796
809
  /** The record id the mutation targeted. */
@@ -814,6 +827,17 @@ interface LedgerEntry {
814
827
  * the file docstring.
815
828
  */
816
829
  readonly payloadHash: string;
830
+ /**
831
+ * Optional human-readable tag describing why this mutation happened
832
+ * (#1). Threaded through `collection.put(_, _, { reason })`. Common
833
+ * values include `'import:csv'`, `'import:json'`, `'import:xlsx'` from
834
+ * `as-*` ImportPlan.apply(), but consumers can use any string for
835
+ * domain-specific audit filtering. Auto-strip via `canonicalJson` —
836
+ * absent on the wire, never serialized as `null`.
837
+ *
838
+ * Audit consumers filter: `entries.filter(e => e.reason?.startsWith('import:'))`.
839
+ */
840
+ readonly reason?: string;
817
841
  /**
818
842
  * Optional hex-encoded sha256 of the encrypted JSON Patch delta
819
843
  * blob stored alongside this entry in `_ledger_deltas/`. Present
@@ -837,6 +861,24 @@ interface LedgerEntry {
837
861
  * entirely — never `{ deltaHash: undefined }`.
838
862
  */
839
863
  readonly deltaHash?: string;
864
+ /**
865
+ * Present only when `op === 'amendment'`. Records the human reason,
866
+ * the role of the actor, the (collection, id, vBefore, vAfter) tuple
867
+ * for every record touched, and which guard invariants passed.
868
+ *
869
+ * See docs/superpowers/specs/2026-05-18-guards-design.md.
870
+ */
871
+ readonly amendment?: {
872
+ readonly reason: string;
873
+ readonly role: 'admin' | 'owner';
874
+ readonly changes: ReadonlyArray<{
875
+ readonly collection: string;
876
+ readonly id: string;
877
+ readonly vBefore: number;
878
+ readonly vAfter: number;
879
+ }>;
880
+ readonly invariantsPassed: ReadonlyArray<string>;
881
+ };
840
882
  }
841
883
  /**
842
884
  * Canonical (sort-stable) JSON encoder.
@@ -1056,6 +1098,20 @@ interface AppendInput {
1056
1098
  * as the entry's `deltaHash` field.
1057
1099
  */
1058
1100
  delta?: JsonPatch;
1101
+ /**
1102
+ * Present only for `op === 'amendment'` — structured audit
1103
+ * payload for multi-record repair operations performed via
1104
+ * `withTransactions(...)`. Carried through verbatim to the
1105
+ * resulting ledger entry.
1106
+ */
1107
+ amendment?: LedgerEntry['amendment'];
1108
+ /**
1109
+ * Optional human-readable tag describing why this mutation happened
1110
+ * (#1). Threaded from `collection.put(_, _, { reason })`.
1111
+ * Carried verbatim onto the resulting ledger entry's `reason` field;
1112
+ * omitted from canonical JSON when undefined.
1113
+ */
1114
+ reason?: string;
1059
1115
  }
1060
1116
  /**
1061
1117
  * Result of `LedgerStore.verify()`. On success, `head` is the hash of
@@ -1949,8 +2005,9 @@ interface UnlockedKeyring {
1949
2005
  */
1950
2006
  readonly exportCapability?: ExportCapability;
1951
2007
  /**
1952
- * `@noy-db/as-*` import capability (issue ). Absent when the
1953
- * keyring was written before landed default-closed semantics
2008
+ * `@noy-db/as-*` import capability. Absent when the
2009
+ * keyring was written before the import-capability extension
2010
+ * landed — default-closed semantics
1954
2011
  * apply via `hasImportCapability` (no plaintext format granted, no
1955
2012
  * bundle import granted, regardless of role).
1956
2013
  */
@@ -1970,6 +2027,65 @@ interface UnlockedKeyring {
1970
2027
  */
1971
2028
  readonly policy?: VaultPolicyOnDisk;
1972
2029
  }
2030
+ /** Load and unlock a user's keyring for a vault. */
2031
+ declare function loadKeyring(adapter: NoydbStore, vault: string, userId: string, passphrase: string): Promise<UnlockedKeyring>;
2032
+ /**
2033
+ * Create the initial owner keyring for a new vault.
2034
+ *
2035
+ * Pass `{ validate: true }` (or a `PassphrasePolicy`) to gate creation
2036
+ * on the phrase-format strength rules — `Noydb` threads this from
2037
+ * `NoydbOptions.validatePassphrase`. Direct callers (CLI, scripts,
2038
+ * test fixtures) opt in explicitly.
2039
+ */
2040
+ declare function createOwnerKeyring(adapter: NoydbStore, vault: string, userId: string, passphrase: string, passphraseOpts?: PassphrasePolicy & {
2041
+ validate?: boolean;
2042
+ allowWeakPassphrase?: boolean;
2043
+ }): Promise<UnlockedKeyring>;
2044
+ /** Grant access to a new user. Caller must have grant privilege. */
2045
+ declare function grant(adapter: NoydbStore, vault: string, callerKeyring: UnlockedKeyring, options: GrantOptions): Promise<void>;
2046
+ /** Revoke a user's access. Optionally rotate keys for affected collections. */
2047
+ declare function revoke(adapter: NoydbStore, vault: string, callerKeyring: UnlockedKeyring, options: RevokeOptions): Promise<void>;
2048
+ /**
2049
+ * Mutate `role`, `displayName`, and/or `permissions` on an existing
2050
+ * keyring. Pure plaintext-header rewrite — no DEK rewrap, no KEK
2051
+ * required, no authenticator slots touched. Tier-2 enrollments and
2052
+ * recovery codes survive the operation.
2053
+ *
2054
+ * Role-elevation guard: BOTH the old role AND the new role must
2055
+ * satisfy `canUpdateRole(callerRole, _)`. This blocks the two
2056
+ * privilege-escalation shapes:
2057
+ * - admin elevates someone (or themselves) to owner
2058
+ * - admin demotes an owner to a role they then control
2059
+ *
2060
+ * Owner is always allowed. Admin manages admin / operator / viewer /
2061
+ * client laterally.
2062
+ *
2063
+ * Identity preserved: same userId, same DEK wrappings. Last-write-wins
2064
+ * through the standard keyring put (same concurrency story as `grant`
2065
+ * and `revoke`).
2066
+ *
2067
+ * @throws `NoAccessError` when no keyring exists for the target.
2068
+ * @throws `PermissionDeniedError` when the role hierarchy rejects.
2069
+ * @throws `ValidationError` when the diff is empty (nothing to update).
2070
+ *
2071
+ * @see #54
2072
+ */
2073
+ declare function updateKeyringIdentity(adapter: NoydbStore, vault: string, callerKeyring: UnlockedKeyring, options: UpdateUserOptions): Promise<void>;
2074
+ /**
2075
+ * Change the user's passphrase. Re-wraps every DEK under the new KEK.
2076
+ *
2077
+ * Validates the new passphrase against the strength rules unless
2078
+ * `allowWeakPassphrase: true` is passed. Mirrors `rotatePassphrase`'s
2079
+ * default-on validation contract.
2080
+ *
2081
+ * `db.rotatePassphrase()` adds a `checkGate('rotate-passphrase')` step
2082
+ * on top of this primitive and additionally requires the OLD passphrase
2083
+ * for re-derivation; `changeSecret` reuses the cached unlocked KEK so
2084
+ * the OLD passphrase is not retyped.
2085
+ */
2086
+ declare function changeSecret(adapter: NoydbStore, vault: string, keyring: UnlockedKeyring, newPassphrase: string, passphraseOpts?: PassphrasePolicy & {
2087
+ allowWeakPassphrase?: boolean;
2088
+ }): Promise<UnlockedKeyring>;
1973
2089
  /**
1974
2090
  * Recipient slot in a re-keyed `.noydb` bundle. Each slot becomes its
1975
2091
  * own keyring file inside the bundle, sealed with its own passphrase.
@@ -2027,6 +2143,17 @@ interface BundleRecipient {
2027
2143
  declare function buildRecipientKeyringFile(callerKeyring: UnlockedKeyring, recipient: BundleRecipient): Promise<KeyringFile>;
2028
2144
  /** List all users with access to a vault. */
2029
2145
  declare function listUsers(adapter: NoydbStore, vault: string): Promise<UserInfo[]>;
2146
+ /**
2147
+ * Optional filter knobs for {@link listUsersWithEnvelopes}.
2148
+ *
2149
+ * - `includeHidden` — when true, principals with `_meta/visibility/<id>`
2150
+ * set to `{ hidden: true }` are returned alongside everyone else.
2151
+ * Requires `owner` or `admin` callerRole; lower roles get
2152
+ * {@link import('../errors.js').PermissionDeniedError}.
2153
+ */
2154
+ interface ListUsersOptions {
2155
+ readonly includeHidden?: boolean;
2156
+ }
2030
2157
  /**
2031
2158
  * Joined enumeration: every keyring + its `_users/<keyringId>`
2032
2159
  * envelope side by side. Convenience for admin UIs that want to
@@ -2036,6 +2163,27 @@ declare function listUsers(adapter: NoydbStore, vault: string): Promise<UserInfo
2036
2163
  * `userEnvelopeDek` is the vault's `_users` collection DEK
2037
2164
  * (`vault.getDEK('_users')`); used to decrypt every envelope.
2038
2165
  *
2166
+ * `callerRole` (#122) drives the directory-visibility checks:
2167
+ *
2168
+ * - When the vault's `_meta/directory` document has `enabled: false`,
2169
+ * only `owner` and `admin` callers may enumerate; anyone else gets
2170
+ * {@link import('../errors.js').DirectoryDisabledError}.
2171
+ * - Principals with `_meta/visibility/<id>` set to `{ hidden: true }`
2172
+ * are filtered out by default. `owner`/`admin` callers can pass
2173
+ * `{ includeHidden: true }` to see them; lower roles passing that
2174
+ * option get `PermissionDeniedError`.
2175
+ *
2176
+ * Honest caveat (#122): these filters are a UX hint, not a security
2177
+ * boundary. The keyring file is still listed at `_keyring/*` and the
2178
+ * envelope ciphertext at `_users/*`. A caller with direct store access
2179
+ * — or a caller that calls this function with `callerRole: 'owner'`
2180
+ * unconditionally — sees every principal. The protection is only as
2181
+ * strong as the role the calling layer passes in. The hub-level wrapper
2182
+ * on `Vault` sources `callerRole` from the unlocked keyring's `role`
2183
+ * field, which is signed-by-construction (it lives in the user's own
2184
+ * keyring file). See `docs/subsystems/user-envelope.md` →
2185
+ * "Directory visibility".
2186
+ *
2039
2187
  * Principals without a persisted envelope (legacy keyrings predating
2040
2188
  * the user-envelope feature) come back with `envelope: null`. The
2041
2189
  * caller chooses how to render — usually "fall back to keyring's
@@ -2044,10 +2192,14 @@ declare function listUsers(adapter: NoydbStore, vault: string): Promise<UserInfo
2044
2192
  * Order matches `listUsers()` (store-defined; sort if you need a
2045
2193
  * stable display order).
2046
2194
  */
2047
- declare function listUsersWithEnvelopes<T = unknown>(adapter: NoydbStore, vault: string, userEnvelopeDek: CryptoKey): Promise<Array<{
2195
+ declare function listUsersWithEnvelopes<T = unknown>(adapter: NoydbStore, vault: string, userEnvelopeDek: CryptoKey, callerRole: Role, options?: ListUsersOptions): Promise<Array<{
2048
2196
  user: UserInfo;
2049
2197
  envelope: UserEnvelope<T> | null;
2050
2198
  }>>;
2199
+ /** Ensure a DEK exists for a collection. Generates one if new. */
2200
+ declare function ensureCollectionDEK(adapter: NoydbStore, vault: string, keyring: UnlockedKeyring): Promise<(collectionName: string) => Promise<CryptoKey>>;
2201
+ /** Persist a keyring file to the adapter. */
2202
+ declare function persistKeyring(adapter: NoydbStore, vault: string, keyring: UnlockedKeyring): Promise<void>;
2051
2203
  /**
2052
2204
  * Check whether a keyring is authorised for a given `@noy-db/as-*`
2053
2205
  * export tier.
@@ -2946,6 +3098,283 @@ declare class SyncEngine {
2946
3098
  private persistMeta;
2947
3099
  }
2948
3100
 
3101
+ /**
3102
+ * **Wrap-DEKs primitive (#44)** — a single canonical shape for the
3103
+ * pattern of "serialize a DEK set, encrypt it under a credential-derived
3104
+ * AES-GCM key." Used by:
3105
+ *
3106
+ * - **tier-0** — paper recovery entries (`_meta/recovery-paper`),
3107
+ * credential = the printed code.
3108
+ * - **tier-2** — password authenticator slots (`KeyringFile.authenticators`,
3109
+ * `wrapKind: 'deks'`), credential = the user's password.
3110
+ *
3111
+ * **Not** used by `@noy-db/on-pin` — tier-3 wraps the DEK set under
3112
+ * the same conceptual pattern but at **100,000 PBKDF2 iterations**
3113
+ * (vs the 600,000 here), because the protection window for a PIN
3114
+ * slot is short (idle-timeout-bounded, typically 15 min) and 600k
3115
+ * iterations would make every PIN-resume noticeably slow. The wire
3116
+ * formats are deliberately incompatible. See `@noy-db/on-pin`'s
3117
+ * `PIN_PBKDF2_ITERATIONS` and the threat-model rationale in its
3118
+ * module docstring.
3119
+ *
3120
+ * Before #44, the same crypto lived in two places: `mintPaperRecoveryEntry`
3121
+ * (in `team/recovery.ts`) and `enrollPasswordAuthenticator` (in
3122
+ * `@noy-db/on-password`). Both functions did identical work — PBKDF2
3123
+ * the credential, AES-GCM-encrypt the JSON-serialized DEK set — but
3124
+ * their implementations had drifted apart enough that fixing a bug
3125
+ * in one wouldn't fix the other.
3126
+ *
3127
+ * This module owns the canonical implementation. Consumers compose:
3128
+ *
3129
+ * - `mintPaperRecoveryEntry` is now a thin wrapper that calls
3130
+ * `mintWrappedDeksBlob` and adds `{ codeId, enrolledAt }`.
3131
+ * - `enrollPasswordAuthenticator` calls `mintWrappedDeksBlob` and
3132
+ * wraps the result in the slot envelope.
3133
+ *
3134
+ * @module
3135
+ */
3136
+ /**
3137
+ * The wrap-DEKs primitive — a serialized + AES-GCM-encrypted DEK set
3138
+ * keyed under a credential-derived key.
3139
+ *
3140
+ * All three fields are base64-encoded so the blob is JSON-safe and
3141
+ * round-trips through `_meta/*` envelopes (which carry plaintext
3142
+ * JSON in `_data`).
3143
+ *
3144
+ * Composition: `PaperRecoveryEntry extends WrappedDeksBlob` plus
3145
+ * `{ codeId, enrolledAt }`. `KeyringAuthenticatorWrappingDEKs`
3146
+ * carries the same three fields with `salt` stored in `meta` for
3147
+ * slot-format back-compat (#44 defers moving it to top-level).
3148
+ */
3149
+ interface WrappedDeksBlob {
3150
+ /** Base64 PBKDF2 salt for the credential-derived wrapping key. */
3151
+ readonly salt: string;
3152
+ /** Base64 AES-GCM IV used for the `wrappedDeks` ciphertext. */
3153
+ readonly iv: string;
3154
+ /** Base64 AES-GCM ciphertext of `{ deks: { collection: base64rawDek } }`. */
3155
+ readonly wrappedDeks: string;
3156
+ }
3157
+ /**
3158
+ * Mint a fresh `WrappedDeksBlob` from a DEK set + a string credential.
3159
+ *
3160
+ * Generates a random salt + IV, derives a 256-bit AES-GCM key via
3161
+ * PBKDF2-SHA256(credential, salt, 600K), serializes the DEK set as
3162
+ * `{ deks: { coll: rawBase64 } }`, and AES-GCM-encrypts.
3163
+ *
3164
+ * The `credential` is the user-typed string (recovery code, password,
3165
+ * PIN). Caller normalization rules apply (e.g. paper
3166
+ * recovery uppercase-strips the code before reaching this function).
3167
+ *
3168
+ * @param deks - DEK set to wrap. Each DEK must be exportable via
3169
+ * `subtle.exportKey('raw', dek)` (the hub mints DEKs
3170
+ * this way; consumers feeding non-extractable keys
3171
+ * will get `InvalidAccessError` from WebCrypto).
3172
+ * @param credential - String input the consumer minted (paper code,
3173
+ * password, PIN). Treated as opaque bytes by PBKDF2.
3174
+ */
3175
+ declare function mintWrappedDeksBlob(deks: Map<string, CryptoKey>, credential: string): Promise<WrappedDeksBlob>;
3176
+ /**
3177
+ * Reverse of {@link mintWrappedDeksBlob}. Re-derives the wrapping key
3178
+ * from the credential + stored salt, AES-GCM-decrypts the wrapped DEK
3179
+ * set, and re-imports each DEK as an extractable AES-GCM CryptoKey.
3180
+ *
3181
+ * Throws (AES-GCM auth tag failure) when the credential doesn't
3182
+ * match the blob. Callers iterating over multiple blobs (e.g. paper
3183
+ * recovery's "try every entry until one matches") should catch.
3184
+ */
3185
+ declare function unwrapDeksFromBlob(blob: WrappedDeksBlob, credential: string): Promise<Map<string, CryptoKey>>;
3186
+
3187
+ /**
3188
+ * String-level Shamir provider injected into hub for `profile: 'shamir'`
3189
+ * recovery. Keeps hub free of any `@noy-db/on-shamir` import — hub never
3190
+ * sees `RawShare` or the share codecs. Implemented by
3191
+ * `shamirRecoveryProvider()` from `@noy-db/on-shamir`.
3192
+ */
3193
+ interface ShamirRecoveryProvider {
3194
+ /** Split `secret` into `n` base32 share strings; any `k` recombine it. */
3195
+ splitToShares(secret: Uint8Array, k: number, n: number): string[];
3196
+ /**
3197
+ * Recombine `k`+ share strings into the secret. MUST throw on malformed,
3198
+ * truncated, insufficient, or mismatched shares.
3199
+ */
3200
+ combineShares(shares: readonly string[]): Uint8Array;
3201
+ }
3202
+
3203
+ /**
3204
+ * Recovery profile persistence + dispatch — issue #10.
3205
+ *
3206
+ * v0.1.0-pre.5 wires the **paper** profile end-to-end through
3207
+ * `@noy-db/on-recovery`. The other three profiles (Shamir,
3208
+ * multi-channel, admin-mediated) ship the API surface and throw
3209
+ * {@link RecoveryProfileNotImplementedError} during use; per-profile
3210
+ * dispatch lands in follow-up issues.
3211
+ *
3212
+ * Storage layout:
3213
+ *
3214
+ * ```
3215
+ * _meta/recovery-paper — JSON { entries: RecoveryCodeEntry[] } produced by `on-recovery`.
3216
+ * _meta/recovery-shamir — reserved
3217
+ * _meta/recovery-multi — reserved
3218
+ * _meta/recovery-admin — reserved
3219
+ * ```
3220
+ *
3221
+ * Like `_meta/policy` and `_meta/handle`, the documents are plain JSON
3222
+ * with empty `_iv` — the recovery-code wrapping is what protects the
3223
+ * KEK; the entries themselves are inert without the user's code.
3224
+ *
3225
+ * @module
3226
+ */
3227
+
3228
+ /**
3229
+ * One paper recovery code as persisted in `_meta/recovery-paper`.
3230
+ *
3231
+ * The hub's KEK is intentionally non-extractable (see `crypto.ts`),
3232
+ * so the recovery entry can't AES-KW-wrap the KEK directly. Instead
3233
+ * we wrap a serialized DEK set: the entry holds the AES-GCM
3234
+ * ciphertext of `{ deks: { collection: rawDekBase64 } }`. Recovery
3235
+ * deserializes the DEK set, then mints a fresh KEK from the new
3236
+ * passphrase and rewraps the DEKs under it.
3237
+ *
3238
+ * This is the same pattern `@noy-db/on-pin` uses for tier-3 quick
3239
+ * resume — the cryptographic guarantee is identical (AES-GCM with a
3240
+ * PBKDF2-derived key), and it sidesteps the non-extractable-KEK
3241
+ * constraint cleanly.
3242
+ *
3243
+ * Type-level composition (#44): `PaperRecoveryEntry extends
3244
+ * WrappedDeksBlob` — the three crypto fields (`salt`, `iv`,
3245
+ * `wrappedDeks`) come from the shared primitive; `codeId` and
3246
+ * `enrolledAt` are paper-recovery's own metadata. Wire format
3247
+ * unchanged.
3248
+ */
3249
+ interface PaperRecoveryEntry extends WrappedDeksBlob {
3250
+ readonly codeId: string;
3251
+ readonly enrolledAt: string;
3252
+ }
3253
+ interface PaperRecoveryDoc {
3254
+ readonly _noydb_recovery: 1;
3255
+ readonly profile: 'paper';
3256
+ readonly entries: ReadonlyArray<PaperRecoveryEntry>;
3257
+ }
3258
+ /** Read the paper-recovery entries. Returns empty array when absent. */
3259
+ declare function loadPaperRecoveryEntries(store: NoydbStore, vault: string): Promise<ReadonlyArray<PaperRecoveryEntry>>;
3260
+ /** Replace the paper-recovery entries (used after burn-on-recovery). */
3261
+ declare function savePaperRecoveryEntries(store: NoydbStore, vault: string, entries: ReadonlyArray<PaperRecoveryEntry>): Promise<void>;
3262
+ /** Drop a single paper-recovery entry (burn-on-use). */
3263
+ declare function burnPaperRecoveryEntry(store: NoydbStore, vault: string, codeId: string): Promise<void>;
3264
+ /** Whether at least one recovery profile has any enrolled entries. */
3265
+ declare function hasRecoveryEnrolled(store: NoydbStore, vault: string): Promise<boolean>;
3266
+ /**
3267
+ * One Shamir-recovery entry as persisted in `_meta/recovery-shamir`.
3268
+ *
3269
+ * Like {@link PaperRecoveryEntry}, the entry composes
3270
+ * {@link WrappedDeksBlob} (DEKs wrapped under a fresh ephemeral
3271
+ * recovery secret) with profile-specific metadata. Unlike paper, the
3272
+ * "credential" was never visible to the user — it was 32 random
3273
+ * bytes split into N Shamir shares at enrollment. The shares ARE
3274
+ * the credential; the user holds them, the hub never sees them
3275
+ * again after `enrollRecovery` returns.
3276
+ *
3277
+ * Per the spec §5: the recovery secret is base64-encoded and
3278
+ * passed as the `credential` arg to
3279
+ * {@link mintWrappedDeksBlob} / {@link unwrapDeksFromBlob}. The
3280
+ * PBKDF2 round over high-entropy input is harmless overhead — it
3281
+ * keeps the shared primitive unchanged while letting Shamir reuse
3282
+ * the same wrapping pipeline as paper.
3283
+ */
3284
+ interface ShamirRecoveryEntry extends WrappedDeksBlob {
3285
+ /** Stable id for this entry. Allows multiple Shamir splits to coexist. */
3286
+ readonly entryId: string;
3287
+ /** Threshold — minimum shares to reconstruct. */
3288
+ readonly k: number;
3289
+ /** Total shares minted at enrollment. */
3290
+ readonly n: number;
3291
+ /** x-coordinates of the n minted shares. Informational. Omitted as of 0.2
3292
+ * (string-level provider doesn't expose share x-coords); kept optional so
3293
+ * pre-0.2 entries still read. */
3294
+ readonly xCoords?: ReadonlyArray<number>;
3295
+ /** ISO timestamp. */
3296
+ readonly enrolledAt: string;
3297
+ /** Optional caller-supplied label (e.g., "2-of-3 board escrow"). */
3298
+ readonly label?: string;
3299
+ }
3300
+ interface ShamirRecoveryDoc {
3301
+ readonly _noydb_recovery: 1;
3302
+ readonly profile: 'shamir';
3303
+ readonly entries: ReadonlyArray<ShamirRecoveryEntry>;
3304
+ }
3305
+ /** Read the Shamir-recovery entries. Returns empty array when absent. */
3306
+ declare function loadShamirRecoveryEntries(store: NoydbStore, vault: string): Promise<ReadonlyArray<ShamirRecoveryEntry>>;
3307
+ /** Replace the Shamir-recovery entries (used by enrollment and rotation). */
3308
+ declare function saveShamirRecoveryEntries(store: NoydbStore, vault: string, entries: ReadonlyArray<ShamirRecoveryEntry>): Promise<void>;
3309
+ /**
3310
+ * Mint a fresh Shamir recovery entry from a DEK set.
3311
+ *
3312
+ * 1. Generates a 32-byte recovery secret.
3313
+ * 2. Wraps the DEK set under that secret via
3314
+ * {@link mintWrappedDeksBlob} (the recovery secret is base64-
3315
+ * encoded as the credential string — PBKDF2 over high-entropy
3316
+ * input is harmless overhead).
3317
+ * 3. Splits the recovery secret via Shamir into `n` shares with
3318
+ * threshold `k`.
3319
+ * 4. Zeros the in-memory recovery secret after wrapping + splitting.
3320
+ *
3321
+ * Returns:
3322
+ * - `entry` — the {@link ShamirRecoveryEntry} to persist.
3323
+ * - `shareStrings` — the `n` Base32-encoded share strings to
3324
+ * return to the caller. The HUB MUST NOT PERSIST THESE; once
3325
+ * returned they are the user's responsibility.
3326
+ *
3327
+ * @param deks - DEK set to wrap.
3328
+ * @param entryId - Stable id for this entry (caller-supplied or
3329
+ * hub-generated).
3330
+ * @param k - Threshold (>= 2).
3331
+ * @param n - Total shares (k <= n <= 255).
3332
+ * @param label - Optional caller label.
3333
+ */
3334
+ declare function mintShamirRecoveryEntry(provider: ShamirRecoveryProvider, deks: Map<string, CryptoKey>, entryId: string, k: number, n: number, label?: string): Promise<{
3335
+ entry: ShamirRecoveryEntry;
3336
+ shareStrings: string[];
3337
+ }>;
3338
+ /**
3339
+ * Decrypt a Shamir recovery entry to recover the raw DEK set.
3340
+ *
3341
+ * Combines K or more `shares`, reconstructs the recovery secret,
3342
+ * unwraps the DEKs via {@link unwrapDeksFromBlob}.
3343
+ *
3344
+ * Throws (AES-GCM auth-tag mismatch) when the shares don't combine
3345
+ * to the secret originally used to mint the entry — typically
3346
+ * because they came from a different enrollment or were tampered
3347
+ * with. Callers iterating multiple entries should catch.
3348
+ */
3349
+ declare function unwrapDeksFromShamirEntry(provider: ShamirRecoveryProvider, entry: ShamirRecoveryEntry, shareStrings: readonly string[]): Promise<Map<string, CryptoKey>>;
3350
+ /**
3351
+ * Generate one paper-recovery entry from an unlocked DEK set.
3352
+ *
3353
+ * Returns the serializable entry (persisted via
3354
+ * {@link savePaperRecoveryEntries}). The recovery flow unwraps the
3355
+ * DEK set, then mints a fresh KEK from the user's new passphrase.
3356
+ *
3357
+ * Thin wrapper over {@link mintWrappedDeksBlob} (#44) — the crypto
3358
+ * lives in the shared primitive; this function just adds paper-
3359
+ * recovery's own metadata (`codeId`, `enrolledAt`).
3360
+ *
3361
+ * @param deks Map of collection-name → DEK (extractable).
3362
+ * @param code The plaintext recovery code (caller-supplied;
3363
+ * pair this with `@noy-db/on-recovery`'s code
3364
+ * generator/parser if available).
3365
+ * @param codeId Stable id used by `burnPaperRecoveryEntry`.
3366
+ */
3367
+ declare function mintPaperRecoveryEntry(deks: Map<string, CryptoKey>, code: string, codeId: string): Promise<PaperRecoveryEntry>;
3368
+ /**
3369
+ * Decrypt a recovery entry to recover the raw DEK set. Used by the
3370
+ * `recoverPassphrase` flow after the user's code has been parsed.
3371
+ *
3372
+ * Thin wrapper over {@link unwrapDeksFromBlob} (#44).
3373
+ *
3374
+ * @throws when the code does not match the entry (AES-GCM auth tag fail).
3375
+ */
3376
+ declare function unwrapDeksFromPaperEntry(entry: PaperRecoveryEntry, code: string): Promise<Map<string, CryptoKey>>;
3377
+
2949
3378
  /**
2950
3379
  * Tier-2 authenticator slot management — issue #11.
2951
3380
  *
@@ -3010,6 +3439,26 @@ declare function enrollAuthenticator(store: NoydbStore, vault: string, keyring:
3010
3439
  interface UpdateAuthenticatorOptions {
3011
3440
  readonly meta?: Record<string, unknown>;
3012
3441
  }
3442
+ /**
3443
+ * Mutate a tier-2 authenticator slot's `meta` blob (slot rename,
3444
+ * label changes). The slot's `id`, `method`, and wrap material
3445
+ * (`wrapped_kek` for wrap-KEK; `wrapped_deks` + `iv` for wrap-DEKs)
3446
+ * are immutable through this entry point — the anti-slot-swap guard
3447
+ * is structural, not gate-driven, so even if the policy gate is
3448
+ * weakened a future caller cannot use this path to swap one slot's
3449
+ * crypto for another's.
3450
+ *
3451
+ * `meta` patch semantics:
3452
+ * - Top-level merge — absent keys preserved, present keys overwrite
3453
+ * - `null` value — delete that meta key
3454
+ * - Non-object values (string, number, boolean, array) — replace verbatim
3455
+ *
3456
+ * @throws `NoAccessError` when no slot with the given id exists.
3457
+ * @throws `ValidationError` when no patch field is provided.
3458
+ *
3459
+ * @see #55
3460
+ */
3461
+ declare function updateAuthenticator(store: NoydbStore, vault: string, keyring: UnlockedKeyring, slotId: string, options: UpdateAuthenticatorOptions): Promise<UnlockedKeyring>;
3013
3462
  /**
3014
3463
  * Drop a slot by id. No-op if the slot doesn't exist (idempotent —
3015
3464
  * removing a non-existent slot is a recoverable retry, not an error).
@@ -3044,8 +3493,8 @@ declare function findAuthenticator(keyring: UnlockedKeyring, slotId: string): Ke
3044
3493
  /**
3045
3494
  * Context handed to a {@link SlotRewrapCeremony} when `rotatePassphrase`
3046
3495
  * preserves a tier-2 slot. The ceremony's job is to re-derive its
3047
- * method-specific wrapping material (PRF assertion, PBKDF2 of a
3048
- * daily-password, etc.) and wrap the freshly rewrapped DEK set under
3496
+ * method-specific wrapping material (PRF assertion, PBKDF2 of the
3497
+ * password, etc.) and wrap the freshly rewrapped DEK set under
3049
3498
  * the new wrapping key.
3050
3499
  *
3051
3500
  * Two surfaces are exposed:
@@ -3123,7 +3572,15 @@ interface RotatePassphraseInput {
3123
3572
  * slot's id or method (anti-slot-swap guard).
3124
3573
  */
3125
3574
  declare function rotatePassphrase(store: NoydbStore, vault: string, userId: string, input: RotatePassphraseInput): Promise<UnlockedKeyring>;
3126
- /** Caller payload for {@link recoverPassphrase}. */
3575
+ /**
3576
+ * Caller payload for {@link recoverPassphrase}.
3577
+ *
3578
+ * As of #196 slice 1, `paper` and `shamir` are wired end-to-end.
3579
+ * The remaining two profiles (`multi-channel`, `admin-mediated`)
3580
+ * stay outside the union and throw
3581
+ * {@link RecoveryProfileNotImplementedError} at the runtime guard
3582
+ * when bypassed via `as unknown as RecoveryProof`.
3583
+ */
3127
3584
  type RecoveryProof = {
3128
3585
  readonly profile: 'paper';
3129
3586
  readonly payload: {
@@ -3132,19 +3589,12 @@ type RecoveryProof = {
3132
3589
  } | {
3133
3590
  readonly profile: 'shamir';
3134
3591
  readonly payload: {
3592
+ /** Optional disambiguator when multiple Shamir entries are enrolled.
3593
+ * When omitted, hub tries each entry until one combines. */
3594
+ readonly entryId?: string;
3595
+ /** K or more opaque share strings, as returned by `ShamirRecoveryProvider.splitToShares`. */
3135
3596
  readonly shares: ReadonlyArray<string>;
3136
3597
  };
3137
- } | {
3138
- readonly profile: 'multi-channel';
3139
- readonly payload: {
3140
- readonly proofs: ReadonlyArray<unknown>;
3141
- };
3142
- } | {
3143
- readonly profile: 'admin-mediated';
3144
- readonly payload: {
3145
- readonly token: string;
3146
- readonly factor?: unknown;
3147
- };
3148
3598
  };
3149
3599
  interface RecoverPassphraseInput {
3150
3600
  readonly newPassphrase: string;
@@ -3205,20 +3655,97 @@ interface RecoverPassphraseResult {
3205
3655
  readonly newCodes: readonly string[];
3206
3656
  }
3207
3657
  /**
3208
- * Reset the user's passphrase using a recovery proof. v0.1.0-pre.5
3209
- * supports the `'paper'` profile via `@noy-db/on-recovery` entries
3210
- * persisted in `_meta/recovery-paper`. The other three profiles throw
3211
- * {@link RecoveryProfileNotImplementedError}.
3212
- *
3213
- * On success, the used recovery entry is burned (deleted from the
3214
- * stored set).
3658
+ * Input for {@link Noydb.rotateRecovery} (#121) deliberate
3659
+ * recovery-credential regeneration when the user knows their
3660
+ * passphrase but wants a fresh sheet (paper) or fresh shares
3661
+ * (shamir). Symmetric to {@link RotatePassphraseInput}.
3215
3662
  */
3216
- declare function recoverPassphrase(store: NoydbStore, vault: string, userId: string, input: RecoverPassphraseInput): Promise<UnlockedKeyring>;
3217
-
3663
+ type RotateRecoveryOptions = {
3664
+ readonly profile: 'paper';
3665
+ /** How many fresh codes to mint. Default: existing sheet size. */
3666
+ readonly count?: number;
3667
+ /** Optional code generator — see {@link RecoverPassphraseInput.codeGenerator}. */
3668
+ readonly codeGenerator?: () => string;
3669
+ } | {
3670
+ readonly profile: 'shamir';
3671
+ /** New threshold. */
3672
+ readonly k: number;
3673
+ /** New total share count. */
3674
+ readonly n: number;
3675
+ /** Disambiguator when multiple Shamir entries exist; required if there are 2+. */
3676
+ readonly entryId?: string;
3677
+ /** Optional updated label. */
3678
+ readonly label?: string;
3679
+ };
3218
3680
  /**
3219
- * Atomic peer-recovery primitive issues #33 + #34.
3681
+ * Result of {@link Noydb.rotateRecovery}. Shape varies by profile:
3220
3682
  *
3221
- * `recoverUser` is a SEPARATE operation from `revoke + grant`. It
3683
+ * - `paper` `{ newCodes: string[] }` (and `entryId === 'paper-batch'`)
3684
+ * - `shamir` → `{ newShares: string[], entryId }`
3685
+ *
3686
+ * `newCodes` is populated for paper rotations; `newShares` for
3687
+ * Shamir rotations. Both are show-once — the hub does not
3688
+ * retain them.
3689
+ */
3690
+ interface RotateRecoveryResult {
3691
+ readonly newCodes?: readonly string[];
3692
+ readonly newShares?: readonly string[];
3693
+ readonly entryId?: string;
3694
+ }
3695
+ /**
3696
+ * Result of {@link Noydb.enrollRecovery}. Shape varies by profile:
3697
+ *
3698
+ * - `paper` → `{ entryId: 'paper-batch' }` (caller minted the
3699
+ * entries; this is a sentinel since paper enrollments are batch-shaped).
3700
+ * - `shamir` → `{ entryId, shares: string[] }` — shares are
3701
+ * show-once; the hub does not retain them.
3702
+ */
3703
+ interface EnrollRecoveryResult {
3704
+ readonly entryId: string;
3705
+ readonly shares?: readonly string[];
3706
+ }
3707
+ /**
3708
+ * Input shape for {@link Noydb.enrollRecovery} and
3709
+ * {@link Noydb.openVaultAndEnrollRecovery} (#195). Discriminated
3710
+ * union over recovery profiles.
3711
+ *
3712
+ * - `paper`: caller pre-mints entries (typically via
3713
+ * `mintPaperRecoveryEntry` or `@noy-db/on-recovery`'s
3714
+ * `generateRecoveryCodeSet`) and passes them in. The hub stores
3715
+ * them and surfaces an opaque batch id.
3716
+ * - `shamir`: hub mints the recovery secret + the shares at
3717
+ * enrollment time. The shares are returned in
3718
+ * {@link EnrollRecoveryResult.shares} (show-once); the hub never
3719
+ * retains them.
3720
+ *
3721
+ * Multi-channel and admin-mediated will be added when the respective
3722
+ * dispatch slices ship.
3723
+ */
3724
+ type RecoveryEnrollmentInput = {
3725
+ readonly profile: 'paper';
3726
+ readonly entries: ReadonlyArray<PaperRecoveryEntry>;
3727
+ } | {
3728
+ readonly profile: 'shamir';
3729
+ readonly k: number;
3730
+ readonly n: number;
3731
+ readonly label?: string;
3732
+ readonly entryId?: string;
3733
+ };
3734
+ /**
3735
+ * Reset the user's passphrase using a recovery proof. v0.1.0-pre.5
3736
+ * supports the `'paper'` profile via `@noy-db/on-recovery` entries
3737
+ * persisted in `_meta/recovery-paper`. The other three profiles throw
3738
+ * {@link RecoveryProfileNotImplementedError}.
3739
+ *
3740
+ * On success, the used recovery entry is burned (deleted from the
3741
+ * stored set).
3742
+ */
3743
+ declare function recoverPassphrase(provider: ShamirRecoveryProvider | undefined, store: NoydbStore, vault: string, userId: string, input: RecoverPassphraseInput): Promise<UnlockedKeyring>;
3744
+
3745
+ /**
3746
+ * Atomic peer-recovery primitive — issues #33 + #34.
3747
+ *
3748
+ * `recoverUser` is a SEPARATE operation from `revoke + grant`. It
3222
3749
  * exists because peer-recovery has different semantics than account
3223
3750
  * removal-then-reissue:
3224
3751
  *
@@ -3295,183 +3822,6 @@ interface RecoverUserOptions {
3295
3822
  */
3296
3823
  declare function recoverUser(store: NoydbStore, vault: string, callerKeyring: UnlockedKeyring, options: RecoverUserOptions): Promise<void>;
3297
3824
 
3298
- /**
3299
- * **Wrap-DEKs primitive (#44)** — a single canonical shape for the
3300
- * pattern of "serialize a DEK set, encrypt it under a credential-derived
3301
- * AES-GCM key." Used by:
3302
- *
3303
- * - **tier-0** — paper recovery entries (`_meta/recovery-paper`),
3304
- * credential = the printed code.
3305
- * - **tier-2** — password authenticator slots (`KeyringFile.authenticators`,
3306
- * `wrapKind: 'deks'`), credential = the daily password.
3307
- *
3308
- * **Not** used by `@noy-db/on-pin` — tier-3 wraps the DEK set under
3309
- * the same conceptual pattern but at **100,000 PBKDF2 iterations**
3310
- * (vs the 600,000 here), because the protection window for a PIN
3311
- * slot is short (idle-timeout-bounded, typically 15 min) and 600k
3312
- * iterations would make every PIN-resume noticeably slow. The wire
3313
- * formats are deliberately incompatible. See `@noy-db/on-pin`'s
3314
- * `PIN_PBKDF2_ITERATIONS` and the threat-model rationale in its
3315
- * module docstring.
3316
- *
3317
- * Before #44, the same crypto lived in two places: `mintPaperRecoveryEntry`
3318
- * (in `team/recovery.ts`) and `enrollPasswordAuthenticator` (in
3319
- * `@noy-db/on-password`). Both functions did identical work — PBKDF2
3320
- * the credential, AES-GCM-encrypt the JSON-serialized DEK set — but
3321
- * their implementations had drifted apart enough that fixing a bug
3322
- * in one wouldn't fix the other.
3323
- *
3324
- * This module owns the canonical implementation. Consumers compose:
3325
- *
3326
- * - `mintPaperRecoveryEntry` is now a thin wrapper that calls
3327
- * `mintWrappedDeksBlob` and adds `{ codeId, enrolledAt }`.
3328
- * - `enrollPasswordAuthenticator` calls `mintWrappedDeksBlob` and
3329
- * wraps the result in the slot envelope.
3330
- *
3331
- * @module
3332
- */
3333
- /**
3334
- * The wrap-DEKs primitive — a serialized + AES-GCM-encrypted DEK set
3335
- * keyed under a credential-derived key.
3336
- *
3337
- * All three fields are base64-encoded so the blob is JSON-safe and
3338
- * round-trips through `_meta/*` envelopes (which carry plaintext
3339
- * JSON in `_data`).
3340
- *
3341
- * Composition: `PaperRecoveryEntry extends WrappedDeksBlob` plus
3342
- * `{ codeId, enrolledAt }`. `KeyringAuthenticatorWrappingDEKs`
3343
- * carries the same three fields with `salt` stored in `meta` for
3344
- * slot-format back-compat (#44 defers moving it to top-level).
3345
- */
3346
- interface WrappedDeksBlob {
3347
- /** Base64 PBKDF2 salt for the credential-derived wrapping key. */
3348
- readonly salt: string;
3349
- /** Base64 AES-GCM IV used for the `wrappedDeks` ciphertext. */
3350
- readonly iv: string;
3351
- /** Base64 AES-GCM ciphertext of `{ deks: { collection: base64rawDek } }`. */
3352
- readonly wrappedDeks: string;
3353
- }
3354
- /**
3355
- * Mint a fresh `WrappedDeksBlob` from a DEK set + a string credential.
3356
- *
3357
- * Generates a random salt + IV, derives a 256-bit AES-GCM key via
3358
- * PBKDF2-SHA256(credential, salt, 600K), serializes the DEK set as
3359
- * `{ deks: { coll: rawBase64 } }`, and AES-GCM-encrypts.
3360
- *
3361
- * The `credential` is the user-typed string (recovery code, daily
3362
- * password, PIN). Caller normalization rules apply (e.g. paper
3363
- * recovery uppercase-strips the code before reaching this function).
3364
- *
3365
- * @param deks - DEK set to wrap. Each DEK must be exportable via
3366
- * `subtle.exportKey('raw', dek)` (the hub mints DEKs
3367
- * this way; consumers feeding non-extractable keys
3368
- * will get `InvalidAccessError` from WebCrypto).
3369
- * @param credential - String input the consumer minted (paper code,
3370
- * password, PIN). Treated as opaque bytes by PBKDF2.
3371
- */
3372
- declare function mintWrappedDeksBlob(deks: Map<string, CryptoKey>, credential: string): Promise<WrappedDeksBlob>;
3373
- /**
3374
- * Reverse of {@link mintWrappedDeksBlob}. Re-derives the wrapping key
3375
- * from the credential + stored salt, AES-GCM-decrypts the wrapped DEK
3376
- * set, and re-imports each DEK as an extractable AES-GCM CryptoKey.
3377
- *
3378
- * Throws (AES-GCM auth tag failure) when the credential doesn't
3379
- * match the blob. Callers iterating over multiple blobs (e.g. paper
3380
- * recovery's "try every entry until one matches") should catch.
3381
- */
3382
- declare function unwrapDeksFromBlob(blob: WrappedDeksBlob, credential: string): Promise<Map<string, CryptoKey>>;
3383
-
3384
- /**
3385
- * Recovery profile persistence + dispatch — issue #10.
3386
- *
3387
- * v0.1.0-pre.5 wires the **paper** profile end-to-end through
3388
- * `@noy-db/on-recovery`. The other three profiles (Shamir,
3389
- * multi-channel, admin-mediated) ship the API surface and throw
3390
- * {@link RecoveryProfileNotImplementedError} during use; per-profile
3391
- * dispatch lands in follow-up issues.
3392
- *
3393
- * Storage layout:
3394
- *
3395
- * ```
3396
- * _meta/recovery-paper — JSON { entries: RecoveryCodeEntry[] } produced by `on-recovery`.
3397
- * _meta/recovery-shamir — reserved
3398
- * _meta/recovery-multi — reserved
3399
- * _meta/recovery-admin — reserved
3400
- * ```
3401
- *
3402
- * Like `_meta/policy` and `_meta/handle`, the documents are plain JSON
3403
- * with empty `_iv` — the recovery-code wrapping is what protects the
3404
- * KEK; the entries themselves are inert without the user's code.
3405
- *
3406
- * @module
3407
- */
3408
-
3409
- /**
3410
- * One paper recovery code as persisted in `_meta/recovery-paper`.
3411
- *
3412
- * The hub's KEK is intentionally non-extractable (see `crypto.ts`),
3413
- * so the recovery entry can't AES-KW-wrap the KEK directly. Instead
3414
- * we wrap a serialized DEK set: the entry holds the AES-GCM
3415
- * ciphertext of `{ deks: { collection: rawDekBase64 } }`. Recovery
3416
- * deserializes the DEK set, then mints a fresh KEK from the new
3417
- * passphrase and rewraps the DEKs under it.
3418
- *
3419
- * This is the same pattern `@noy-db/on-pin` uses for tier-3 quick
3420
- * resume — the cryptographic guarantee is identical (AES-GCM with a
3421
- * PBKDF2-derived key), and it sidesteps the non-extractable-KEK
3422
- * constraint cleanly.
3423
- *
3424
- * Type-level composition (#44): `PaperRecoveryEntry extends
3425
- * WrappedDeksBlob` — the three crypto fields (`salt`, `iv`,
3426
- * `wrappedDeks`) come from the shared primitive; `codeId` and
3427
- * `enrolledAt` are paper-recovery's own metadata. Wire format
3428
- * unchanged.
3429
- */
3430
- interface PaperRecoveryEntry extends WrappedDeksBlob {
3431
- readonly codeId: string;
3432
- readonly enrolledAt: string;
3433
- }
3434
- interface PaperRecoveryDoc {
3435
- readonly _noydb_recovery: 1;
3436
- readonly profile: 'paper';
3437
- readonly entries: ReadonlyArray<PaperRecoveryEntry>;
3438
- }
3439
- /** Read the paper-recovery entries. Returns empty array when absent. */
3440
- declare function loadPaperRecoveryEntries(store: NoydbStore, vault: string): Promise<ReadonlyArray<PaperRecoveryEntry>>;
3441
- /** Replace the paper-recovery entries (used after burn-on-recovery). */
3442
- declare function savePaperRecoveryEntries(store: NoydbStore, vault: string, entries: ReadonlyArray<PaperRecoveryEntry>): Promise<void>;
3443
- /** Drop a single paper-recovery entry (burn-on-use). */
3444
- declare function burnPaperRecoveryEntry(store: NoydbStore, vault: string, codeId: string): Promise<void>;
3445
- /** Whether at least one recovery profile has any enrolled entries. */
3446
- declare function hasRecoveryEnrolled(store: NoydbStore, vault: string): Promise<boolean>;
3447
- /**
3448
- * Generate one paper-recovery entry from an unlocked DEK set.
3449
- *
3450
- * Returns the serializable entry (persisted via
3451
- * {@link savePaperRecoveryEntries}). The recovery flow unwraps the
3452
- * DEK set, then mints a fresh KEK from the user's new passphrase.
3453
- *
3454
- * Thin wrapper over {@link mintWrappedDeksBlob} (#44) — the crypto
3455
- * lives in the shared primitive; this function just adds paper-
3456
- * recovery's own metadata (`codeId`, `enrolledAt`).
3457
- *
3458
- * @param deks Map of collection-name → DEK (extractable).
3459
- * @param code The plaintext recovery code (caller-supplied;
3460
- * pair this with `@noy-db/on-recovery`'s code
3461
- * generator/parser if available).
3462
- * @param codeId Stable id used by `burnPaperRecoveryEntry`.
3463
- */
3464
- declare function mintPaperRecoveryEntry(deks: Map<string, CryptoKey>, code: string, codeId: string): Promise<PaperRecoveryEntry>;
3465
- /**
3466
- * Decrypt a recovery entry to recover the raw DEK set. Used by the
3467
- * `recoverPassphrase` flow after the user's code has been parsed.
3468
- *
3469
- * Thin wrapper over {@link unwrapDeksFromBlob} (#44).
3470
- *
3471
- * @throws when the code does not match the entry (AES-GCM auth tag fail).
3472
- */
3473
- declare function unwrapDeksFromPaperEntry(entry: PaperRecoveryEntry, code: string): Promise<Map<string, CryptoKey>>;
3474
-
3475
3825
  /**
3476
3826
  * Public envelope — owner-curated plaintext metadata, readable
3477
3827
  * before vault unlock or bundle decryption.
@@ -3692,6 +4042,33 @@ interface StagedOp {
3692
4042
  id: string;
3693
4043
  record?: unknown;
3694
4044
  expectedVersion?: number;
4045
+ /**
4046
+ * Optional human-readable tag forwarded to the resulting ledger
4047
+ * entry's `reason` field (#1). Set by callers via
4048
+ * `tx.vault(v).collection(c).put(id, record, { reason })`.
4049
+ */
4050
+ reason?: string;
4051
+ }
4052
+ /**
4053
+ * One executed op (main staged op or recursive side-effect like a
4054
+ * derivation output) paired with the envelope captured before the write.
4055
+ * `revertExecuted` walks this array in reverse on rollback.
4056
+ * @internal
4057
+ */
4058
+ interface ExecutedOp {
4059
+ op: StagedOp;
4060
+ priorEnvelope: EncryptedEnvelope | null;
4061
+ }
4062
+ /**
4063
+ * Options accepted by `db.transaction({ amendment, reason }, fn)`.
4064
+ * Only the amendment variant uses these — a plain `db.transaction(fn)`
4065
+ * never sees this shape.
4066
+ */
4067
+ interface AmendmentTxOptions {
4068
+ /** Opt into amendment mode. Required to be `true`. */
4069
+ readonly amendment: true;
4070
+ /** Human-readable rationale recorded in the ledger entry. Required. */
4071
+ readonly reason: string;
3695
4072
  }
3696
4073
  /**
3697
4074
  * Transaction handle passed to the user's body. Use
@@ -3701,10 +4078,27 @@ interface StagedOp {
3701
4078
  declare class TxContext {
3702
4079
  /** @internal */
3703
4080
  readonly _ops: StagedOp[];
4081
+ /**
4082
+ * @internal — write log built up in Phase 2. Each entry records the
4083
+ * envelope captured BEFORE the write so a mid-batch failure can
4084
+ * restore prior state via `revertExecuted`. Side-effect writes (e.g.
4085
+ * recursive derivation outputs fired inside `Collection.put`) are
4086
+ * appended here in execution order so they roll back alongside the
4087
+ * main staged ops (#133).
4088
+ */
4089
+ readonly _executed: ExecutedOp[];
3704
4090
  /** @internal */
3705
4091
  readonly _db: Noydb;
4092
+ /**
4093
+ * @internal — true when this TxContext was opened in amendment
4094
+ * mode. Toggles the lazy-`beginAmendment` + role-check path on first
4095
+ * `tx.vault(name)` and unlocks the post-Phase-2 invariant + audit run.
4096
+ */
4097
+ readonly _amendment: boolean;
4098
+ /** @internal — vaults that have already had `beginAmendment` called. */
4099
+ readonly _amendmentVaults: Map<string, Vault>;
3706
4100
  /** @internal */
3707
- constructor(db: Noydb);
4101
+ constructor(db: Noydb, amendment?: boolean);
3708
4102
  /** Scope subsequent `collection()` calls to the named vault. */
3709
4103
  vault(name: string): TxVault;
3710
4104
  }
@@ -3743,6 +4137,7 @@ declare class TxCollection<T> {
3743
4137
  */
3744
4138
  put(id: string, record: T, options?: {
3745
4139
  expectedVersion?: number;
4140
+ reason?: string;
3746
4141
  }): void;
3747
4142
  /**
3748
4143
  * Stage a delete. Does not write until the transaction body returns.
@@ -3754,14 +4149,16 @@ declare class TxCollection<T> {
3754
4149
  }): void;
3755
4150
  }
3756
4151
  /**
3757
- * Commit plan: pre-flight check + execution + revert plan. Returned
3758
- * from `runTransaction`.
4152
+ * Commit plan: pre-flight check + execution + revert plan.
3759
4153
  *
3760
- * @internal — exposed only for the `Collection.putMany({atomic:true})`
3761
- * wire-up so the bulk path can share the executor without creating
3762
- * an outer TxContext.
4154
+ * @internal — driven by `withTransactions()` (via `tx/active.ts`) for
4155
+ * user-facing `db.transaction(...)` calls and by the `amendment` path
4156
+ * in `noydb.ts`. `Collection.putManyAtomic` runs its own Phase 2 loop
4157
+ * but shares the `_activeTxContext` mechanism (and the `revertExecuted`
4158
+ * helper) so nested side-effect derivation writes get registered for
4159
+ * revert alongside the bulk-put source ops (#133).
3763
4160
  */
3764
- declare function runTransaction<T>(db: Noydb, fn: (tx: TxContext) => Promise<T> | T): Promise<T>;
4161
+ declare function runTransaction<T>(db: Noydb, fn: (tx: TxContext) => Promise<T> | T, options?: AmendmentTxOptions): Promise<T>;
3765
4162
 
3766
4163
  /**
3767
4164
  * Policy gate DSL types — issue #9.
@@ -3788,7 +4185,7 @@ declare function runTransaction<T>(db: Noydb, fn: (tx: TxContext) => Promise<T>
3788
4185
  * | `shamir` | k-of-n threshold share (`@noy-db/on-shamir`) | yes |
3789
4186
  * | `webauthn-roaming` | hardware key (YubiKey, SoloKey, Titan) | yes (key portable) |
3790
4187
  * | `webauthn-platform` | platform passkey (Touch ID, Face ID, Hello) | no (device-bound) |
3791
- * | `password` | tier-2 daily password (`@noy-db/on-password`) | no |
4188
+ * | `password` | tier-2 password (`@noy-db/on-password`) | no |
3792
4189
  * | `pin` | tier-3 quick-resume PIN (`@noy-db/on-pin`) | no |
3793
4190
  *
3794
4191
  * Off-device kinds (TOTP, email-OTP, recovery, shamir, roaming WebAuthn)
@@ -3845,6 +4242,16 @@ interface GatePolicy {
3845
4242
  * configured policy as "no gate" (no-op).
3846
4243
  */
3847
4244
  type BuiltInGateName = 'rotate-passphrase' | 'recover-passphrase' | 'enroll-authenticator' | 'remove-authenticator'
4245
+ /**
4246
+ * Authorize a deliberate paper-recovery-code regeneration —
4247
+ * `db.rotateRecovery` (#121). Symmetric to `rotate-passphrase` for
4248
+ * the case where the user remembers their passphrase but wants a
4249
+ * fresh sheet (lost the printout, suspect compromise of the off-site
4250
+ * copy). PERSONAL allows tier-1; STRICT requires an off-device
4251
+ * factor so a stolen unlocked laptop cannot silently mint a new
4252
+ * sheet for an attacker.
4253
+ */
4254
+ | 'rotate-recovery'
3848
4255
  /**
3849
4256
  * Authorize a meta-only mutation on an existing authenticator slot —
3850
4257
  * `db.updateAuthenticator` (#55). The slot's wrap material, id, and
@@ -3897,6 +4304,25 @@ interface FactorProof {
3897
4304
  /** Method-specific payload. The engine treats it as opaque — verification is delegated. */
3898
4305
  readonly payload?: unknown;
3899
4306
  }
4307
+ /**
4308
+ * Bundle of factor proofs + session-context flags passed to a gated
4309
+ * Noydb method. Used as the optional last parameter of every method
4310
+ * that runs through `checkGate`: `db.grant`, `db.revoke`, `db.updateUser`,
4311
+ * `db.enrollAuthenticator`, `db.removeAuthenticator`, `db.updateAuthenticator`,
4312
+ * `db.enrollWebAuthn`, `db.rotatePassphrase`, `db.recoverPassphrase`,
4313
+ * `db.recoverUser`, `db.enrollUnlock`, `db.describeUserAuth`,
4314
+ * `db.describeAllUsersAuth`.
4315
+ *
4316
+ * Pre-#89 this type was inlined at every call site as
4317
+ * `{ factors?: ReadonlyArray<FactorProof>; sharedDevice?: boolean }`
4318
+ * and parameter names alternated between `factors` and `presented`.
4319
+ * Now exported so consumers can name their helpers and so the param
4320
+ * name converges to `factors` everywhere.
4321
+ */
4322
+ interface FactorProofBundle {
4323
+ readonly factors?: ReadonlyArray<FactorProof>;
4324
+ readonly sharedDevice?: boolean;
4325
+ }
3900
4326
  /** Active session tier — what the engine compares against `gate.minTier`. */
3901
4327
  type ActiveTier = 1 | 2 | 3;
3902
4328
 
@@ -3918,6 +4344,14 @@ declare class Noydb {
3918
4344
  * `_meta/policy` load; replaced by `db.updatePolicy()`.
3919
4345
  */
3920
4346
  private readonly policyCache;
4347
+ /**
4348
+ * One-shot bypass for the managed-mode strong-recovery check (#195).
4349
+ * Set true by {@link openVaultAndEnrollRecovery} for the duration of
4350
+ * the bootstrap window so the keyring can be created before the
4351
+ * strong recovery is enrolled. Always cleared (try/finally).
4352
+ * @internal
4353
+ */
4354
+ private _skipNextManagedRecoveryCheck;
3921
4355
  /** Per-vault tier-3 (PIN / quick-resume) state — issue #11. */
3922
4356
  private readonly quickUnlock;
3923
4357
  /**
@@ -3933,6 +4367,17 @@ declare class Noydb {
3933
4367
  private readonly txStrategy;
3934
4368
  private readonly sessionStrategy;
3935
4369
  private readonly syncStrategy;
4370
+ /**
4371
+ * Currently-running multi-record transaction, set by
4372
+ * `runTransaction` at the start of Phase 2 (commit) and cleared in
4373
+ * the same function's `finally` block. Side-effect writes triggered
4374
+ * during a staged op's `Collection.put` (today: eager derivation
4375
+ * outputs) register their pre-write envelope on `_executed` here so
4376
+ * a mid-batch failure rolls them back alongside the main staged ops
4377
+ * (#133). `null` outside of Phase 2.
4378
+ * @internal
4379
+ */
4380
+ private _activeTxContext;
3936
4381
  /**
3937
4382
  * In-process translation cache. Key is `"${field}\x00${collection}\x00${from}\x00${to}\x00${text}"`.
3938
4383
  * Cleared on `close()` alongside the KEK and DEKs.
@@ -3972,10 +4417,27 @@ declare class Noydb {
3972
4417
  }): Promise<Vault>;
3973
4418
  /** Synchronous vault access (must call openVault first, or auto-opens). */
3974
4419
  vault(name: string): Vault;
3975
- /** Grant access to a user for a vault. */
3976
- grant(vault: string, options: GrantOptions): Promise<void>;
3977
- /** Revoke a user's access to a vault. */
3978
- revoke(vault: string, options: RevokeOptions): Promise<void>;
4420
+ /**
4421
+ * Grant access to a user for a vault.
4422
+ *
4423
+ * Gated by `enroll-user`. `STRICT_POLICY` requires a TOTP / email-OTP
4424
+ * factor proof so the operator affirmatively re-asserts identity at
4425
+ * the moment of grant; `PERSONAL_POLICY` accepts a tier-1 unlock alone.
4426
+ *
4427
+ * The legacy `requireReAuthFor: ['grant']` session-policy check still
4428
+ * fires on top — both are independent opt-ins.
4429
+ */
4430
+ grant(vault: string, options: GrantOptions, factors?: FactorProofBundle): Promise<void>;
4431
+ /**
4432
+ * Revoke a user's access to a vault.
4433
+ *
4434
+ * Gated by `revoke-user`. `STRICT_POLICY` requires a TOTP / email-OTP
4435
+ * factor proof; `PERSONAL_POLICY` accepts a tier-1 unlock alone.
4436
+ *
4437
+ * The legacy `requireReAuthFor: ['revoke']` session-policy check still
4438
+ * fires on top — both are independent opt-ins.
4439
+ */
4440
+ revoke(vault: string, options: RevokeOptions, factors?: FactorProofBundle): Promise<void>;
3979
4441
  /**
3980
4442
  * Mutate post-grant identity fields on an existing keyring — `role`,
3981
4443
  * `displayName`, and/or `permissions`. Pure plaintext-header rewrite:
@@ -4018,10 +4480,7 @@ declare class Noydb {
4018
4480
  *
4019
4481
  * @see #54
4020
4482
  */
4021
- updateUser(vault: string, options: UpdateUserOptions, factors?: {
4022
- factors?: ReadonlyArray<FactorProof>;
4023
- sharedDevice?: boolean;
4024
- }): Promise<void>;
4483
+ updateUser(vault: string, options: UpdateUserOptions, factors?: FactorProofBundle): Promise<void>;
4025
4484
  /**
4026
4485
  * Rotate the DEKs for the given collections in a vault.
4027
4486
  *
@@ -4154,8 +4613,17 @@ declare class Noydb {
4154
4613
  * ```
4155
4614
  */
4156
4615
  queryAcross<T>(vaultIds: string[], fn: (vault: Vault) => Promise<T>, options?: QueryAcrossOptions): Promise<QueryAcrossResult<T>[]>;
4157
- /** Change the current user's passphrase for a vault. */
4158
- changeSecret(vault: string, newPassphrase: string): Promise<void>;
4616
+ /**
4617
+ * Change the current user's passphrase for a vault.
4618
+ *
4619
+ * Validates the new passphrase against the strength rules. Pass
4620
+ * `{ allowWeakPassphrase: true }` to skip — typically only useful for
4621
+ * fixtures and migrations. Pass a `PassphrasePolicy` to override the
4622
+ * default rules (e.g. consumer-tunable `pattern` / `customValidator`).
4623
+ */
4624
+ changeSecret(vault: string, newPassphrase: string, options?: PassphrasePolicy & {
4625
+ allowWeakPassphrase?: boolean;
4626
+ }): Promise<void>;
4159
4627
  /** Push local changes to remote for a vault. */
4160
4628
  push(vault: string, options?: PushOptions): Promise<PushResult>;
4161
4629
  /** Pull remote changes to local for a vault. */
@@ -4187,6 +4655,16 @@ declare class Noydb {
4187
4655
  * which batches push/pull across sync peers.
4188
4656
  */
4189
4657
  transaction<T>(fn: (tx: TxContext) => Promise<T> | T): Promise<T>;
4658
+ /**
4659
+ * Open an amendment-mode transaction. Requires `admin` or `owner`
4660
+ * role on every vault touched by the body; throws
4661
+ * `AmendmentForbiddenError` on first non-privileged `tx.vault(name)`
4662
+ * call. Guard `check` callbacks are SKIPPED inside an amendment —
4663
+ * the staged change-set is fed to each guard's `amendment.invariant`
4664
+ * after the body returns, and the multi-record summary is appended
4665
+ * to the vault's ledger as `op: 'amendment'`.
4666
+ */
4667
+ transaction<T>(options: AmendmentTxOptions, fn: (tx: TxContext) => Promise<T> | T): Promise<T>;
4190
4668
  /**
4191
4669
  * Create a sync transaction for the given vault.
4192
4670
  * The vault must already be open via `openVault()`.
@@ -4202,13 +4680,57 @@ declare class Noydb {
4202
4680
  * @internal
4203
4681
  */
4204
4682
  get _store(): NoydbStore;
4205
- /** Get sync status for a vault. */
4206
- syncStatus(vault: string): SyncStatus;
4207
- private getSyncEngine;
4208
- on<K extends keyof NoydbEventMap>(event: K, handler: (data: NoydbEventMap[K]) => void): void;
4209
- off<K extends keyof NoydbEventMap>(event: K, handler: (data: NoydbEventMap[K]) => void): void;
4210
4683
  /**
4211
- * Soft-lock a single vault: clear its in-memory keyring, DEKs, vault
4684
+ * Currently-running multi-record transaction, or `null` outside
4685
+ * Phase 2. `Collection.dispatchDerivations` consults this so a
4686
+ * recursive derived-output write inside `Collection.put` can register
4687
+ * its envelope onto `ctx._executed` and roll back with the main
4688
+ * staged ops on mid-batch failure (#133).
4689
+ *
4690
+ * @internal
4691
+ */
4692
+ get _activeTxContextOrNull(): TxContext | null;
4693
+ /**
4694
+ * Called by `runTransaction` at Phase 2 start, and by
4695
+ * `Collection.putManyAtomic` (via `derivationSource.setActiveTxContext`)
4696
+ * for its own Phase 2 loop. Nested or concurrent (non-nested)
4697
+ * transactions on the same Noydb instance are NOT supported —
4698
+ * overwriting an active context means another transaction is still
4699
+ * running and its `_executed` list would be cross-contaminated by
4700
+ * the nested writes. We tolerate the overwrite (best-effort, no
4701
+ * throw) to keep the rare interleaving from breaking consumers who
4702
+ * currently get lucky with timing, but applications should ensure
4703
+ * their multi-record commits are serialised on a single Noydb.
4704
+ *
4705
+ * @internal
4706
+ */
4707
+ _setActiveTxContext(ctx: TxContext): void;
4708
+ /**
4709
+ * Factory for a transient `TxContext` bound to this Noydb. Used by
4710
+ * `Collection.putManyAtomic` (via `derivationSource.createTxContext`)
4711
+ * to publish an active context for the duration of its bulk-atomic
4712
+ * Phase 2 loop, so recursive derivation-output writes register on
4713
+ * `ctx._executed` and roll back together with the source ops (#133).
4714
+ *
4715
+ * @internal
4716
+ */
4717
+ _createTxContext(): TxContext;
4718
+ /**
4719
+ * Called by `runTransaction` in its `finally`. Only clears when the
4720
+ * passed ctx matches the active one — a defensive no-op if some
4721
+ * other code path already cleared it.
4722
+ *
4723
+ * @internal
4724
+ */
4725
+ _clearActiveTxContext(ctx: TxContext): void;
4726
+ /** Get sync status for a vault. */
4727
+ syncStatus(vault: string): SyncStatus;
4728
+ private requireShamirProvider;
4729
+ private getSyncEngine;
4730
+ on<K extends keyof NoydbEventMap>(event: K, handler: (data: NoydbEventMap[K]) => void): void;
4731
+ off<K extends keyof NoydbEventMap>(event: K, handler: (data: NoydbEventMap[K]) => void): void;
4732
+ /**
4733
+ * Soft-lock a single vault: clear its in-memory keyring, DEKs, vault
4212
4734
  * instance, sync engine, policy enforcer, and active-tier entry —
4213
4735
  * WITHOUT destroying the `Noydb` instance.
4214
4736
  *
@@ -4259,6 +4781,27 @@ declare class Noydb {
4259
4781
  * change is fundamentally a privilege-management action).
4260
4782
  */
4261
4783
  updatePolicy(vault: string, override: Partial<VaultPolicy>): Promise<VaultPolicy>;
4784
+ /**
4785
+ * Read the current vault-level user-directory toggle (#122). Returns
4786
+ * the default-on shape (`{ enabled: true }`) when no `_meta/directory`
4787
+ * document has been persisted yet.
4788
+ *
4789
+ * No role gate — anyone who can open the vault can read the toggle.
4790
+ */
4791
+ getDirectoryEnabled(vault: string): Promise<boolean>;
4792
+ /**
4793
+ * Toggle the vault's user-directory listing on or off (#122).
4794
+ * Owner-only. When disabled, `listUsersWithEnvelopes()` throws
4795
+ * {@link import('./errors.js').DirectoryDisabledError} for callers
4796
+ * whose role is neither `owner` nor `admin`.
4797
+ *
4798
+ * Honest caveat: this is a UX flag, not a privacy guarantee. The
4799
+ * keyring file at `_keyring/<userId>` and the envelope ciphertext at
4800
+ * `_users/<keyringId>` remain observable to anyone with direct store
4801
+ * read access — only the hub-level enumeration is gated. See
4802
+ * `docs/subsystems/user-envelope.md` → "Directory visibility".
4803
+ */
4804
+ setDirectoryEnabled(vault: string, enabled: boolean): Promise<void>;
4262
4805
  /**
4263
4806
  * Evaluate a policy gate against the active session tier and the
4264
4807
  * presented factor proofs. Throws {@link PolicyDeniedError} on
@@ -4269,22 +4812,33 @@ declare class Noydb {
4269
4812
  * or app-defined (`app:*`).
4270
4813
  * @param presented Caller-supplied factor proofs.
4271
4814
  */
4272
- checkGate(vault: string, gate: GateName, presented?: {
4273
- factors?: ReadonlyArray<FactorProof>;
4274
- sharedDevice?: boolean;
4275
- }): Promise<void>;
4815
+ checkGate(vault: string, gate: GateName, factors?: FactorProofBundle): Promise<void>;
4276
4816
  /** Read or persist the vault policy at `_meta/policy` on first open. */
4277
4817
  private bootstrapPolicy;
4278
4818
  /**
4279
- * Throw {@link RecoveryNotEnrolledError} when the developer
4280
- * explicitly opts into strict mandatory-recovery enforcement
4281
- * (`createNoydb({ requireRecovery: true })`) and no recovery
4282
- * entries are persisted.
4819
+ * Throw {@link RecoveryNotEnrolledError} or
4820
+ * {@link ManagedRecoveryNotEnrolledError} when recovery enrollment
4821
+ * is missing.
4822
+ *
4823
+ * Two enforcement modes:
4824
+ *
4825
+ * 1. **Managed-mode mandatory strong-recovery (#195).** When
4826
+ * `passphraseMode === 'managed'`, the vault MUST have at least
4827
+ * one **strong** recovery profile (Shamir today). Paper alone is
4828
+ * rejected because under managed mode the user has no memorized
4829
+ * passphrase, so losing the paper sheet = losing every record.
4830
+ * This check is unconditional — independent of `requireRecovery`
4831
+ * and the `recover-passphrase` gate.
4283
4832
  *
4284
- * The default behavior is lenient — `recover-passphrase` is enabled
4285
- * in `PERSONAL_POLICY` but the hub does not block vault open on
4286
- * missing enrollment. v1.0 will flip the default to strict; for now,
4287
- * apps that want the spec-mandated check turn it on per-vault.
4833
+ * 2. **Opt-in strict mandatory-recovery.** When
4834
+ * `requireRecovery: true` is set on createNoydb (and the gate is
4835
+ * not explicitly disabled), require ANY recovery profile (paper
4836
+ * or shamir). This is the v0.x default-off behavior; v1.0 may
4837
+ * flip it default-on.
4838
+ *
4839
+ * The managed-mode check fires from {@link bootstrapPolicy} unless
4840
+ * the `skipManagedCheck` flag is set (used by
4841
+ * {@link openVaultAndEnrollRecovery} to allow atomic create-and-enroll).
4288
4842
  */
4289
4843
  private assertRecoveryEnrolled;
4290
4844
  /**
@@ -4305,19 +4859,13 @@ declare class Noydb {
4305
4859
  * Gated by `enroll-authenticator`; `presented` carries any factor
4306
4860
  * proofs the active policy demands.
4307
4861
  */
4308
- enrollAuthenticator(vault: string, options: EnrollAuthenticatorOptions, presented?: {
4309
- factors?: ReadonlyArray<FactorProof>;
4310
- sharedDevice?: boolean;
4311
- }): Promise<void>;
4862
+ enrollAuthenticator(vault: string, options: EnrollAuthenticatorOptions, factors?: FactorProofBundle): Promise<void>;
4312
4863
  /**
4313
4864
  * Remove a tier-2 authenticator slot. Idempotent — removing a
4314
4865
  * non-existent slot is a successful no-op. Gated by
4315
4866
  * `remove-authenticator`.
4316
4867
  */
4317
- removeAuthenticator(vault: string, slotId: string, presented?: {
4318
- factors?: ReadonlyArray<FactorProof>;
4319
- sharedDevice?: boolean;
4320
- }): Promise<void>;
4868
+ removeAuthenticator(vault: string, slotId: string, factors?: FactorProofBundle): Promise<void>;
4321
4869
  /** Read the slot list for a vault. Internal — `describeAuthConfig` (#13) consumes this. */
4322
4870
  listAuthenticators(vault: string): Promise<ReadonlyArray<KeyringAuthenticator>>;
4323
4871
  /**
@@ -4348,10 +4896,7 @@ declare class Noydb {
4348
4896
  *
4349
4897
  * @see #55
4350
4898
  */
4351
- updateAuthenticator(vault: string, slotId: string, options: UpdateAuthenticatorOptions, presented?: {
4352
- factors?: ReadonlyArray<FactorProof>;
4353
- sharedDevice?: boolean;
4354
- }): Promise<void>;
4899
+ updateAuthenticator(vault: string, slotId: string, options: UpdateAuthenticatorOptions, factors?: FactorProofBundle): Promise<void>;
4355
4900
  /**
4356
4901
  * Native WebAuthn enrollment using the **real** internal keyring (#16).
4357
4902
  *
@@ -4399,10 +4944,7 @@ declare class Noydb {
4399
4944
  *
4400
4945
  * @see #16
4401
4946
  */
4402
- enrollWebAuthn(vault: string, ceremony: (keyring: UnlockedKeyring) => Promise<EnrollAuthenticatorOptions>, presented?: {
4403
- factors?: ReadonlyArray<FactorProof>;
4404
- sharedDevice?: boolean;
4405
- }): Promise<{
4947
+ enrollWebAuthn(vault: string, ceremony: (keyring: UnlockedKeyring) => Promise<EnrollAuthenticatorOptions>, factors?: FactorProofBundle): Promise<{
4406
4948
  credentialId: string;
4407
4949
  }>;
4408
4950
  /**
@@ -4463,15 +5005,9 @@ declare class Noydb {
4463
5005
  * disabled). Sanitization is allowlist-based — never renders cred
4464
5006
  * ids, password hashes, secrets, or any field outside the allowlist.
4465
5007
  */
4466
- describeUserAuth(vault: string, userId: string, factors?: {
4467
- factors?: ReadonlyArray<FactorProof>;
4468
- sharedDevice?: boolean;
4469
- }): Promise<string>;
5008
+ describeUserAuth(vault: string, userId: string, factors?: FactorProofBundle): Promise<string>;
4470
5009
  /** Bulk variant for owner dashboards. Gated by `view-user-auth`. */
4471
- describeAllUsersAuth(vault: string, factors?: {
4472
- factors?: ReadonlyArray<FactorProof>;
4473
- sharedDevice?: boolean;
4474
- }): Promise<Array<{
5010
+ describeAllUsersAuth(vault: string, factors?: FactorProofBundle): Promise<Array<{
4475
5011
  userId: string;
4476
5012
  description: string;
4477
5013
  }>>;
@@ -4489,10 +5025,7 @@ declare class Noydb {
4489
5025
  * @throws `PolicyDeniedError` when the gate denies (missing factor, …).
4490
5026
  * @throws `InvalidKeyError` when `oldPassphrase` is wrong.
4491
5027
  */
4492
- rotatePassphrase(vault: string, input: RotatePassphraseInput, factors?: {
4493
- factors?: ReadonlyArray<FactorProof>;
4494
- sharedDevice?: boolean;
4495
- }): Promise<void>;
5028
+ rotatePassphrase(vault: string, input: RotatePassphraseInput, factors?: FactorProofBundle): Promise<void>;
4496
5029
  /**
4497
5030
  * Reset the passphrase using a recovery proof (user forgot the old).
4498
5031
  * v0.1.0-pre.5 supports the `'paper'` profile end-to-end; the
@@ -4500,10 +5033,124 @@ declare class Noydb {
4500
5033
  *
4501
5034
  * Burns the used recovery entry on success.
4502
5035
  */
4503
- recoverPassphrase(vault: string, input: RecoverPassphraseInput, factors?: {
4504
- factors?: ReadonlyArray<FactorProof>;
4505
- sharedDevice?: boolean;
4506
- }): Promise<RecoverPassphraseResult>;
5036
+ recoverPassphrase(vault: string, input: RecoverPassphraseInput, factors?: FactorProofBundle): Promise<RecoverPassphraseResult>;
5037
+ /**
5038
+ * Deliberate paper-recovery-code regeneration (#121). User knows their
5039
+ * passphrase but wants a fresh sheet — they lost the printout or
5040
+ * suspect compromise of the off-site copy.
5041
+ *
5042
+ * Symmetric to {@link rotatePassphrase} for the recovery profile:
5043
+ * gated, audit-trackable, ergonomic. Replaces (not appends) the
5044
+ * paper sheet under `_meta/recovery-paper` in a single envelope `put`.
5045
+ *
5046
+ * Gated by the `rotate-recovery` policy gate:
5047
+ * - PERSONAL_POLICY: `{ minTier: 1 }` — knowing the passphrase
5048
+ * suffices, matching the pre-#121 low-level flow's bar.
5049
+ * - STRICT_POLICY: `{ minTier: 1, factors: [{ anyOf: ['totp',
5050
+ * 'email-otp', 'webauthn-roaming'] }] }` — rotation is an
5051
+ * off-site-trust event; require an off-device factor so a
5052
+ * stolen unlocked laptop cannot silently mint a sheet for the
5053
+ * attacker.
5054
+ *
5055
+ * Defaults `count` to the existing sheet size so consumers aren't
5056
+ * surprised by a different code count. Explicit `count` overrides.
5057
+ *
5058
+ * @throws {@link RecoveryProfileNotImplementedError} when `profile`
5059
+ * is anything other than `'paper'` (v1 dispatch limit).
5060
+ * @throws {@link PolicyDeniedError} when the gate denies (missing
5061
+ * factor, tier mismatch, ...).
5062
+ * @throws on missing paper sheet — "nothing to rotate" surfaces as
5063
+ * an error rather than silently minting an entire new sheet.
5064
+ *
5065
+ * @example Default count + show-once UI
5066
+ * ```ts
5067
+ * const { newCodes } = await db.rotateRecovery('acme', { profile: 'paper' })
5068
+ * showCodesToUser(newCodes)
5069
+ * ```
5070
+ *
5071
+ * @example STRICT-policy site with TOTP factor proof
5072
+ * ```ts
5073
+ * await db.rotateRecovery(
5074
+ * 'acme',
5075
+ * { profile: 'paper', count: 10 },
5076
+ * { factors: [{ kind: 'totp', proof: '123456' }] },
5077
+ * )
5078
+ * ```
5079
+ */
5080
+ rotateRecovery(vault: string, options: RotateRecoveryOptions, factors?: FactorProofBundle): Promise<RotateRecoveryResult>;
5081
+ private rotateRecoveryPaper;
5082
+ private rotateRecoveryShamir;
5083
+ /**
5084
+ * **Atomic create-and-enroll for managed-mode vaults (#195).**
5085
+ *
5086
+ * Bootstraps a managed-mode vault and enrolls strong recovery in
5087
+ * a single ceremony. Under `passphraseMode: 'managed'`, every
5088
+ * `openVault` call requires a strong recovery profile (Shamir
5089
+ * today) to be enrolled — otherwise it throws
5090
+ * {@link ManagedRecoveryNotEnrolledError}. This method bypasses
5091
+ * the check temporarily so the keyring can be created, enrolls
5092
+ * the supplied recovery profile(s), then returns the vault.
5093
+ *
5094
+ * For Shamir enrollments, the show-once share strings come back
5095
+ * in `recoveryEnrollments[i].shares`. The hub never retains them
5096
+ * — the caller MUST display them to the user (once) before any
5097
+ * subsequent operation.
5098
+ *
5099
+ * Paper alone is NOT a strong profile under managed mode; passing
5100
+ * `{ profile: 'paper', ... }` without an accompanying shamir entry
5101
+ * is rejected at validation time.
5102
+ *
5103
+ * ```ts
5104
+ * const db = await createNoydb({
5105
+ * store, user: 'alice',
5106
+ * passphraseMode: 'managed',
5107
+ * sealingKey: macosKeychainSealingProvider({ ... }),
5108
+ * })
5109
+ *
5110
+ * const { vault, recoveryEnrollments } = await db.openVaultAndEnrollRecovery('acme', {
5111
+ * recovery: [{ profile: 'shamir', k: 2, n: 3 }],
5112
+ * })
5113
+ * for (const r of recoveryEnrollments) {
5114
+ * if (r.shares) showSharesToUser(r.shares) // ONCE
5115
+ * }
5116
+ * ```
5117
+ *
5118
+ * @throws ValidationError if recovery is empty, or contains no
5119
+ * strong profile under managed mode.
5120
+ */
5121
+ openVaultAndEnrollRecovery(vault: string, opts: {
5122
+ readonly recovery: ReadonlyArray<RecoveryEnrollmentInput>;
5123
+ readonly locale?: string;
5124
+ }): Promise<{
5125
+ readonly vault: Vault;
5126
+ readonly recoveryEnrollments: ReadonlyArray<EnrollRecoveryResult>;
5127
+ }>;
5128
+ /**
5129
+ * **Recovery flow under managed-passphrase mode (#195).**
5130
+ *
5131
+ * Replaces the sealed passphrase of a managed-mode vault with a
5132
+ * fresh 256-bit random, sealed under the configured
5133
+ * `SealingKeyProvider`. The user never sees the new passphrase.
5134
+ *
5135
+ * Internally:
5136
+ * 1. Verify the recovery proof (Shamir today) and unwrap the
5137
+ * DEK set.
5138
+ * 2. Mint a fresh 256-bit random as the new effective passphrase.
5139
+ * 3. Rewrap the DEK set under a fresh KEK derived from the new
5140
+ * passphrase (via the existing `recoverPassphrase` path).
5141
+ * 4. Seal the random bytes under the provider and overwrite
5142
+ * `_meta/sealed-passphrase`.
5143
+ * 5. Drop the keyring cache so the next operation re-derives.
5144
+ *
5145
+ * The vault's strong-recovery enrollment is preserved across
5146
+ * recovery (Shamir entries are not burned on use — see #196).
5147
+ *
5148
+ * @throws ValidationError if the Noydb instance is not in managed mode.
5149
+ */
5150
+ recoverManagedPassphrase(vault: string, options: {
5151
+ readonly recoveryProof: RecoveryProof;
5152
+ readonly passphrasePolicy?: PassphrasePolicy;
5153
+ }): Promise<void>;
4507
5154
  /**
4508
5155
  * Atomic peer-recovery — re-wraps an EXISTING user's keyring under
4509
5156
  * a fresh temp passphrase in a single store write. Closes #34's
@@ -4547,10 +5194,7 @@ declare class Noydb {
4547
5194
  *
4548
5195
  * @see #33 #34 — the issues this method closes.
4549
5196
  */
4550
- recoverUser(vault: string, options: RecoverUserOptions, factors?: {
4551
- factors?: ReadonlyArray<FactorProof>;
4552
- sharedDevice?: boolean;
4553
- }): Promise<void>;
5197
+ recoverUser(vault: string, options: RecoverUserOptions, factors?: FactorProofBundle): Promise<void>;
4554
5198
  /**
4555
5199
  * Persist a recovery enrollment. v0.1.0-pre.5 accepts the `'paper'`
4556
5200
  * profile.
@@ -4582,13 +5226,11 @@ declare class Noydb {
4582
5226
  * await db.enrollRecovery('acme', { profile: 'paper', entries })
4583
5227
  * ```
4584
5228
  */
4585
- enrollRecovery(vault: string, enrollment: {
4586
- profile: 'paper';
4587
- entries: ReadonlyArray<PaperRecoveryEntry>;
4588
- }): Promise<void>;
4589
- /** Read the persisted paper-recovery entries. Used by `describeAuthConfig` (#13). */
5229
+ enrollRecovery(vault: string, enrollment: RecoveryEnrollmentInput): Promise<EnrollRecoveryResult>;
5230
+ /** Read the persisted recovery entries (paper + Shamir). Used by `describeAuthConfig` (#13). */
4590
5231
  listRecoveryEntries(vault: string): Promise<{
4591
5232
  paper: ReadonlyArray<PaperRecoveryEntry>;
5233
+ shamir: ReadonlyArray<ShamirRecoveryEntry>;
4592
5234
  }>;
4593
5235
  /**
4594
5236
  * Register a tier-3 quick-unlock state for the vault. The state is
@@ -4599,10 +5241,7 @@ declare class Noydb {
4599
5241
  * Gated by `rotate-unlock` (the same gate covers "set" and "rotate"
4600
5242
  * because tier-3 is a single-slot rolling secret).
4601
5243
  */
4602
- enrollUnlock(vault: string, state: QuickUnlockState, presented?: {
4603
- factors?: ReadonlyArray<FactorProof>;
4604
- sharedDevice?: boolean;
4605
- }): Promise<void>;
5244
+ enrollUnlock(vault: string, state: QuickUnlockState, factors?: FactorProofBundle): Promise<void>;
4606
5245
  /**
4607
5246
  * Resume a session via the registered tier-3 state. The verifier is
4608
5247
  * `@noy-db/on-pin/resumePin` (or compatible). On success, mark the
@@ -4618,8 +5257,17 @@ declare class Noydb {
4618
5257
  /**
4619
5258
  * Public accessor for the unlocked keyring of a vault — issue #28.
4620
5259
  *
4621
- * Returns the cached `UnlockedKeyring` (already in memory after
4622
- * `createNoydb` + first vault touch); loads it on demand if absent.
5260
+ * Returns a **defensive shallow copy** so consumers can read the DEK
5261
+ * map and authenticator list without the risk of mutating the hub's
5262
+ * internal cache (#88). Internal hub code paths use a live reference
5263
+ * via `getKeyringInternal`; ceremonies and external consumers always
5264
+ * get a snapshot.
5265
+ *
5266
+ * The CryptoKey values inside `deks` are not cloned — Web Crypto
5267
+ * keys are opaque handles, and a shared handle is intentional
5268
+ * (encrypt / decrypt go through the same key the cache holds).
5269
+ * Only the container Map / authenticator array is fresh.
5270
+ *
4623
5271
  * Used by `@noy-db/on-*` ceremonies that need the live DEK set
4624
5272
  * (paper recovery via {@link mintPaperRecoveryEntry}, tier-3 PIN
4625
5273
  * enrolment via on-pin's `enrollPin`, custom on-* ceremonies that
@@ -4634,11 +5282,19 @@ declare class Noydb {
4634
5282
  * ```ts
4635
5283
  * const keyring = await db.getKeyring('acme')
4636
5284
  * // keyring.deks: Map<collection, CryptoKey>
4637
- * // keyring.kek: CryptoKey (non-extractable; null for tier-3 sessions)
5285
+ * // keyring.kek: CryptoKey | null (null for tier-3 / wrap-DEKs sessions)
4638
5286
  * // keyring.role / .permissions / .authenticators
4639
5287
  * ```
4640
5288
  */
4641
5289
  getKeyring(vault: string): Promise<UnlockedKeyring>;
5290
+ /**
5291
+ * Live-reference variant used by the hub's own code paths. Internal
5292
+ * mutations on `deks` (e.g. {@link ensureCollectionDEK} adding a
5293
+ * collection key) need to land on the cached keyring so subsequent
5294
+ * accesses see them. Not exposed publicly — callers outside hub
5295
+ * should use {@link getKeyring}, which returns a defensive copy.
5296
+ */
5297
+ private getKeyringInternal;
4642
5298
  }
4643
5299
  /** Create a new NOYDB instance. */
4644
5300
  declare function createNoydb(options: NoydbOptions): Promise<Noydb>;
@@ -4722,13 +5378,766 @@ declare function issueDelegation(store: NoydbStore, vault: string, grantor: Unlo
4722
5378
  * list of merged delegations so the caller can register per-access
4723
5379
  * audit context.
4724
5380
  */
4725
- declare function loadActiveDelegations(store: NoydbStore, vault: string, user: UnlockedKeyring, delegationsDek: CryptoKey, now?: Date): Promise<DelegationToken[]>;
5381
+ declare function loadActiveDelegations(store: NoydbStore, vault: string, user: UnlockedKeyring, delegationsDek: CryptoKey, now?: Date): Promise<DelegationToken[]>;
5382
+ /**
5383
+ * Revoke a delegation by id — the caller resolves the envelope and
5384
+ * issues a `delete`. Provided as a stable helper so the naming is
5385
+ * symmetric to `issueDelegation`.
5386
+ */
5387
+ declare function revokeDelegation(store: NoydbStore, vault: string, id: string): Promise<void>;
5388
+
5389
+ /**
5390
+ * Minimum read surface exposed to guard `check` functions. Intentionally
5391
+ * narrow — guards can read other collections but never write.
5392
+ *
5393
+ * `query()` returns the same chainable builder used elsewhere. `Query<T>`
5394
+ * has no write terminals (no `.update()` / `.delete()`) so exposing it
5395
+ * here preserves the read-only contract while letting guards aggregate
5396
+ * with `.where().aggregate()` / `.groupBy()` / `.join()` instead of
5397
+ * decrypting every sibling row via `.list()`.
5398
+ */
5399
+ interface ReadOnlyVaultFacade$1 {
5400
+ collection<T = unknown>(name: string): {
5401
+ get(id: string): Promise<T | null>;
5402
+ list(): Promise<T[]>;
5403
+ query(): Query<T>;
5404
+ };
5405
+ }
5406
+ /**
5407
+ * Runtime context passed to `check` and `invariant` callbacks.
5408
+ * `existing` is the currently-persisted record (null for inserts).
5409
+ */
5410
+ interface GuardContext<T> {
5411
+ existing: T | null;
5412
+ vault: ReadOnlyVaultFacade$1;
5413
+ userId: string;
5414
+ role: Role;
5415
+ }
5416
+ /**
5417
+ * One {before, after} pair handed to an `invariant` function. `before`
5418
+ * is null for inserts; `after` reflects the proposed post-commit record.
5419
+ */
5420
+ interface GuardChange<T> {
5421
+ before: T | null;
5422
+ after: T;
5423
+ }
5424
+ /** @internal — output of {@link withGuard}. */
5425
+ interface GuardStrategyHandle<T extends Record<string, unknown>> {
5426
+ readonly __noydb_strategy: 'guard';
5427
+ readonly spec: GuardStrategy<T>;
5428
+ }
5429
+ /**
5430
+ * Existential erasure of `GuardStrategyHandle<T>` — used as the
5431
+ * element type of `ReadonlyArray<>` fields where the per-handle T
5432
+ * differs (e.g. `guardStrategies: [invoiceGuard, disbursementGuard]`).
5433
+ *
5434
+ * Background: `GuardStrategyHandle<T>` is INVARIANT in T because T
5435
+ * appears in callback positions on the spec (`check(incoming: T, ctx)`,
5436
+ * `invariant(changes: ReadonlyArray<GuardChange<T>>, ctx)`). So
5437
+ * `Handle<Invoice>` is not assignable to `Handle<Record<string, unknown>>`.
5438
+ * A bounded existential ("there exists some T satisfying the constraint
5439
+ * such that this is a Handle<T>") is the right shape; TypeScript has
5440
+ * no first-class existentials, so we fake it with a structurally narrow
5441
+ * interface that ERASES T from both the discriminant and the spec.
5442
+ *
5443
+ * Consumers continue to construct typed handles via `withGuard<T>(...)`
5444
+ * which returns `GuardStrategyHandle<T>`. Both `Handle<Invoice>` and
5445
+ * `Handle<Disbursement>` structurally assign to `GuardStrategyHandleAny`,
5446
+ * so an array of them is `GuardStrategyHandleAny[]`.
5447
+ *
5448
+ * Internal code that needs T re-narrows via the runtime discriminant
5449
+ * (`__noydb_strategy === 'guard'`) plus per-handle type information
5450
+ * carried by the registry.
5451
+ *
5452
+ * NOT exported from the public barrel — keeping this internal
5453
+ * discourages consumers from constructing it directly. Used only as
5454
+ * the array-element type on `Vault` / `NoydbOptions.guardStrategies`.
5455
+ *
5456
+ * @internal
5457
+ */
5458
+ interface GuardStrategyHandleAny {
5459
+ readonly __noydb_strategy: 'guard';
5460
+ readonly spec: GuardStrategy<any>;
5461
+ }
5462
+ /** Public registration shape. See `withGuard()`. */
5463
+ interface GuardStrategy<T extends Record<string, unknown>> {
5464
+ collection: string;
5465
+ /**
5466
+ * Fires on `Collection.put` (insert + update). The `incoming` argument
5467
+ * is the record being written. Throw to cancel the put.
5468
+ *
5469
+ * Does NOT fire on `Collection.delete` — use {@link onDelete} for
5470
+ * delete-time validation. Skipped during an amendment transaction
5471
+ * (`db.transaction({ amendment: true })`) — admin/owner override.
5472
+ */
5473
+ check?: (incoming: T, ctx: GuardContext<T>) => Promise<void> | void;
5474
+ /**
5475
+ * Fires on user-initiated `Collection.delete` before the adapter
5476
+ * delete and before the ledger append. The `existing` argument is
5477
+ * the currently-persisted record. Throw to cancel the delete — no
5478
+ * partial state, no tombstone ledger entry.
5479
+ *
5480
+ * Skipped during an amendment transaction (admin/owner override) —
5481
+ * amendments are the unlock primitive. To make a delete TRULY
5482
+ * unconditional (e.g. legal-document immutability rules), pair
5483
+ * `onDelete` with an `amendment.invariant` that re-throws on any
5484
+ * `before !== null && after === null` change:
5485
+ *
5486
+ * ```ts
5487
+ * withGuard<Receipt>({
5488
+ * collection: 'receipts',
5489
+ * onDelete: () => { throw new RecordLockedError(...) },
5490
+ * amendment: {
5491
+ * roles: ['admin', 'owner'],
5492
+ * invariant: (changes) => {
5493
+ * for (const c of changes) {
5494
+ * if (c.before !== null && c.after === null) {
5495
+ * throw new RecordLockedError(...) // wrapped as InvariantError
5496
+ * }
5497
+ * }
5498
+ * },
5499
+ * },
5500
+ * })
5501
+ * ```
5502
+ *
5503
+ * Also skipped on system-internal deletes (derivation tombstones from
5504
+ * #144, MV refresh from Dim 14 v2) — those use `_internalDelete`
5505
+ * which bypasses every user-facing delete hook. Housekeeping ops are
5506
+ * NOT user-initiated and should not trip user invariants.
5507
+ *
5508
+ * Delete of an absent record is a no-op and does not consult any
5509
+ * guard, matching the idempotent-delete contract.
5510
+ */
5511
+ onDelete?: (existing: T, ctx: GuardContext<T>) => Promise<void> | void;
5512
+ frozenFields?: {
5513
+ when: (existing: T) => boolean;
5514
+ fields: ReadonlyArray<keyof T>;
5515
+ };
5516
+ amendment?: {
5517
+ roles: ReadonlyArray<'admin' | 'owner'>;
5518
+ invariant: (changes: ReadonlyArray<GuardChange<T>>, ctx: GuardContext<T>) => Promise<void> | void;
5519
+ };
5520
+ }
5521
+
5522
+ /**
5523
+ * Runtime context handed to `derive(source, ctx)`. Mirrors `GuardContext`'s
5524
+ * narrow shape: read-only vault access, no write capability, no
5525
+ * transaction handle. Determinism is the consumer's responsibility — the
5526
+ * strategy hash includes `derive.toString()`, so the source string fixes
5527
+ * the function's inputs; whatever sibling reads `derive` performs must
5528
+ * yield the same outputs for the same source.
5529
+ */
5530
+ interface DerivationContext {
5531
+ vault: ReadOnlyVaultFacade$1;
5532
+ }
5533
+ /**
5534
+ * Metadata that travels inside the `_data` payload of a derived record.
5535
+ * Lives in encrypted payload, not in the unencrypted envelope — the
5536
+ * storage backend cannot infer the derivation graph from listing.
5537
+ */
5538
+ interface DerivedFromMeta {
5539
+ /** Source collection name. */
5540
+ readonly source: string;
5541
+ /** Source record id. */
5542
+ readonly sourceId: string;
5543
+ /** `_v` of the source at derivation time. */
5544
+ readonly sourceVersion: number;
5545
+ /** ISO timestamp when this output was derived. */
5546
+ readonly derivedAt: string;
5547
+ /**
5548
+ * SHA-256 of (source + outputs map keys + derive function source).
5549
+ * Changes when the strategy changes → forces `vault.deriveAll` to
5550
+ * recompute on next visit.
5551
+ */
5552
+ readonly strategyHash: string;
5553
+ }
5554
+ /** Record-shape output — one source row produces (optionally) one output row at the source's id. */
5555
+ interface RecordOutputSpec {
5556
+ shape: 'record';
5557
+ collection: string;
5558
+ /**
5559
+ * When `true`, the `derive` function may return `null` (or
5560
+ * `undefined`) for this output key. The executor interprets that as
5561
+ * "no output for this invocation": a previously-emitted output at
5562
+ * the same id is deleted (mirroring the empty-group / empty-aggregate
5563
+ * semantics flagged in #142); a never-emitted output is a silent
5564
+ * no-op. When `false` (default), returning `null` throws
5565
+ * `DerivationOutputShapeError` — same as v1.
5566
+ */
5567
+ optional?: boolean;
5568
+ }
5569
+ /**
5570
+ * Array-shape output (#200) — one source row produces a variable-length
5571
+ * list of output rows, each with its own id (from the `key` extractor).
5572
+ *
5573
+ * On every source-row change, the dispatcher diffs the previously
5574
+ * emitted key set against the new one: removed keys are deleted via
5575
+ * `_internalDelete`, new and unchanged keys are upserted via
5576
+ * `Collection.put`. Strict-mode rollback is preserved via the existing
5577
+ * `_executed` tracking.
5578
+ *
5579
+ * Storage of the per-source-row key set lives at
5580
+ * `_meta/derivations-fanout/<source>/<sourceId>/<outputKey>` as a
5581
+ * plain JSON sidecar — keeps dispatch cost O(1) per source row.
5582
+ *
5583
+ * **Slice 1 limitation**: only `lifecycle: 'eager'` is supported.
5584
+ * Registering an array-shape output with `lifecycle: 'lazy'` throws
5585
+ * at `withDerivation` construction time.
5586
+ */
5587
+ interface ArrayOutputSpec {
5588
+ shape: 'array';
5589
+ collection: string;
5590
+ /**
5591
+ * Stable identity extractor for each derived row. Called on every
5592
+ * row returned by `derive`. The string MUST be unique within a
5593
+ * single invocation — duplicate keys throw
5594
+ * `DerivationOutputShapeError`.
5595
+ *
5596
+ * Type is intentionally `(out: Record<string, unknown>) => string`
5597
+ * (not generic) because OutputSpec is type-erased at the registry
5598
+ * level. Strategy-level inference still produces typed `out`
5599
+ * through the strategy's `outputs` map.
5600
+ */
5601
+ key: (output: Record<string, unknown>) => string;
5602
+ /**
5603
+ * Cap on derived rows per source-row invocation. Defaults to 64.
5604
+ * Raise for carry-forward cases (e.g. monthly expansion of
5605
+ * multi-year contracts). Exceeding the cap throws
5606
+ * `DerivationCapExceededError` BEFORE any writes — partial fanout
5607
+ * is never persisted.
5608
+ */
5609
+ maxFanout?: number;
5610
+ }
5611
+ /** Discriminated union — record + array. */
5612
+ type OutputSpec = RecordOutputSpec | ArrayOutputSpec;
5613
+ /**
5614
+ * Registration shape passed to `withDerivation()`.
5615
+ *
5616
+ * @typeParam TSource - the source record type
5617
+ * @typeParam TOutputs - map of output-key → output record type
5618
+ */
5619
+ interface DerivationStrategy<TSource extends Record<string, unknown>, TOutputs extends Record<string, Record<string, unknown>>> {
5620
+ /** Source collection name. */
5621
+ source: string;
5622
+ /** v1: only deterministic derivations supported. */
5623
+ deterministic: true;
5624
+ /**
5625
+ * Output declarations keyed by name. The `derive` function's return
5626
+ * value must have the same keys.
5627
+ */
5628
+ outputs: {
5629
+ [K in keyof TOutputs]: OutputSpec;
5630
+ };
5631
+ /**
5632
+ * Pure function from source to outputs. Runs on plaintext, after DEK
5633
+ * unwrap. Returns a map of named outputs. Each output is encrypted +
5634
+ * stored via the existing `Collection.put` pipeline.
5635
+ *
5636
+ * `ctx.vault` is the same `ReadOnlyVaultFacade` guards see — fetch
5637
+ * sibling records via `ctx.vault.collection<T>(name).get(id)` /
5638
+ * `.list()` / `.query()`. The vault accessor is read-only; there is
5639
+ * no path to a writer from `ctx`.
5640
+ */
5641
+ derive: (source: TSource, ctx: DerivationContext) => Promise<TOutputs> | TOutputs;
5642
+ /**
5643
+ * `'eager'` runs `derive` synchronously inside the source-write
5644
+ * transaction. `'lazy'` marks outputs stale on source-change and
5645
+ * derives on first read.
5646
+ */
5647
+ lifecycle: 'eager' | 'lazy' | {
5648
+ mode: 'eager' | 'lazy';
5649
+ maxDepth?: number;
5650
+ };
5651
+ /**
5652
+ * `true` = any output failure rolls back the source write (only with
5653
+ * `withTransactions`). `false` = isolate per-output failure, log,
5654
+ * continue. Default `false`.
5655
+ */
5656
+ strict?: boolean;
5657
+ }
5658
+ /** Returned by `withDerivation()` and consumed by `createNoydb`. */
5659
+ interface DerivationStrategyHandle {
5660
+ readonly __noydb_strategy: 'derivation';
5661
+ readonly spec: DerivationStrategy<any, any>;
5662
+ }
5663
+
5664
+ interface RegisteredStrategy {
5665
+ spec: DerivationStrategy<any, any>;
5666
+ strategyHash: string;
5667
+ }
5668
+ /**
5669
+ * Vault-internal registry of derivation strategies. Owned by `Vault`;
5670
+ * not exported.
5671
+ *
5672
+ * @internal
5673
+ */
5674
+ declare class DerivationRegistry {
5675
+ private readonly _bySource;
5676
+ private readonly _byOutput;
5677
+ register(spec: DerivationStrategy<any, any>): Promise<void>;
5678
+ strategiesForSource(source: string): ReadonlyArray<RegisteredStrategy>;
5679
+ strategiesProducingOutput(collection: string): ReadonlyArray<RegisteredStrategy>;
5680
+ /**
5681
+ * Cycle detection over the source → output → … graph. Call after all
5682
+ * `register()` calls complete (i.e. at vault open). Throws
5683
+ * `DerivationCycleError` on the first cycle found.
5684
+ */
5685
+ validate(): void;
5686
+ }
5687
+
5688
+ /**
5689
+ * Minimal vault-shaped accessor passed to the MV `query()` callback.
5690
+ * Defined as a structural interface so the strategy types don't have
5691
+ * to import the full `Vault` class (avoids a circular import). The
5692
+ * Vault implements this shape natively.
5693
+ */
5694
+ interface MVQueryContext {
5695
+ collection<T extends Record<string, unknown>>(name: string): Collection<T>;
5696
+ }
5697
+ /**
5698
+ * Metadata that travels inside the `_data` payload of a materialized
5699
+ * row. Lives in encrypted payload, not in the unencrypted envelope —
5700
+ * the storage backend cannot infer the MV graph from listing.
5701
+ *
5702
+ * Extends the `_derivedFrom` precedent from v1: same encryption shape,
5703
+ * same "metadata-inside-data" location.
5704
+ */
5705
+ interface MaterializedFromMeta {
5706
+ /** Stable identity for the MV that emitted this row. */
5707
+ readonly mvName: string;
5708
+ /**
5709
+ * SHA-256 of (mvName + canonical query plan + dependency-set).
5710
+ * Changes when the query structure changes → forces refresh on
5711
+ * next visit (parallels v1's `strategyHash`).
5712
+ */
5713
+ readonly queryHash: string;
5714
+ /**
5715
+ * Map from source collection name → `_v` of the source row(s) that
5716
+ * contributed to this MV row at materialization time. For aggregates
5717
+ * over many rows, this is `max(_v)` per source collection — coarse
5718
+ * but sufficient for stale detection.
5719
+ */
5720
+ readonly sourceVersions: Record<string, number>;
5721
+ /** ISO timestamp when this row was materialized. */
5722
+ readonly materializedAt: string;
5723
+ }
5724
+ /** Output routing for an MV. Optional — when omitted, writes to a collection named after `name`. */
5725
+ interface MaterializedViewOutput {
5726
+ /** Output collection name. Defaults to `name`. */
5727
+ collection?: string;
5728
+ /**
5729
+ * For same-collection-as-source MVs — see § Same-collection partition
5730
+ * discriminator in the v2 spec. The cycle detector resolves the
5731
+ * same-collection edge IFF the query has a where-clause that
5732
+ * provably excludes `partition.value` (supports `==` against a
5733
+ * different value, `!=` against the value, and `in` lists that
5734
+ * don't contain it). Naïve same-collection MVs without a disjoint
5735
+ * clause throw `MaterializedViewCycleError` at vault open.
5736
+ */
5737
+ partition?: {
5738
+ field: string;
5739
+ value: unknown;
5740
+ };
5741
+ }
5742
+ /**
5743
+ * One arm of a UNION materialized view. Reads rows from `collection`,
5744
+ * then maps each into the MV's row shape via `map`.
5745
+ *
5746
+ * The per-source `map` is the schema-unification boundary — sibling
5747
+ * collections can have different schemas, and `map` is where they
5748
+ * meet the MV's row type. The hub does NOT compare schemas across
5749
+ * arms; consumer responsibility is that every arm's `map` returns
5750
+ * the same shape (the strategy's `TRow` type parameter enforces this
5751
+ * at compile time).
5752
+ */
5753
+ interface UnionSource<TRow extends Record<string, unknown>> {
5754
+ /** Source collection name. Must exist in the vault. */
5755
+ readonly collection: string;
5756
+ /**
5757
+ * Pure function from a source row to the unified MV row shape.
5758
+ * Called once per source row at materialization time. Each arm's
5759
+ * mapped output is concatenated into a single stream before
5760
+ * `groupBy` + `aggregate` run.
5761
+ */
5762
+ readonly map: (sourceRow: Record<string, unknown>) => TRow;
5763
+ }
5764
+ /**
5765
+ * Registration shape passed to `withMaterializedView()`.
5766
+ *
5767
+ * @typeParam TRow - the materialized row type (the query's result row)
5768
+ */
5769
+ interface MaterializedViewStrategy<TRow extends Record<string, unknown>> {
5770
+ /**
5771
+ * Stable identity for this view. Used as the output collection name
5772
+ * unless `output.collection` overrides. Must be unique within the vault.
5773
+ */
5774
+ name: string;
5775
+ /**
5776
+ * Declared query (single-source mode). Called at registration time
5777
+ * with a vault-shaped accessor so the closure can compose collections
5778
+ * without pre-existing in-scope references; called again at each
5779
+ * refresh.
5780
+ *
5781
+ * Built via the same `Query<T>` chainable builder used elsewhere —
5782
+ * `.where()`, `.join()`, `.groupBy()`, `.aggregate()`. The
5783
+ * dependency analyzer walks the returned plan to determine source
5784
+ * collections.
5785
+ *
5786
+ * Mutually exclusive with {@link unionSources}: a strategy must
5787
+ * declare exactly one of `query` (single-source) or `unionSources`
5788
+ * (multi-source UNION). Registration throws
5789
+ * `MaterializedViewConfigError` if both are set or neither is set.
5790
+ */
5791
+ query?: (db: MVQueryContext) => Query<TRow>;
5792
+ /**
5793
+ * UNION-form sources (#165): an explicit list of sibling collections
5794
+ * that contribute rows to a single MV. Each arm's `map` projects a
5795
+ * source row into the MV's unified row shape; the mapped streams are
5796
+ * concatenated, then {@link groupBy} + {@link aggregate} run on the
5797
+ * combined output.
5798
+ *
5799
+ * Mutually exclusive with {@link query}. Registration throws
5800
+ * `MaterializedViewConfigError` if both are set, if `unionSources`
5801
+ * has fewer than 2 arms, or if two arms name the same `collection`.
5802
+ *
5803
+ * UNION mode replaces the dependency-analyzer path: the source
5804
+ * collections come directly from `unionSources[].collection`, and
5805
+ * {@link sources} is ignored.
5806
+ */
5807
+ unionSources?: ReadonlyArray<UnionSource<TRow>>;
5808
+ /**
5809
+ * Group-key field(s) for UNION mode (#165). Applied to the
5810
+ * concatenated mapped-row stream from {@link unionSources} before
5811
+ * {@link aggregate} runs. Accepts a single field name or a tuple of
5812
+ * field names for multi-key grouping (same shape as
5813
+ * `Query.groupBy(...fields)`).
5814
+ *
5815
+ * UNION-mode only. Ignored if {@link query} is set — single-source
5816
+ * grouping is expressed inside the `Query<T>` returned from `query()`
5817
+ * via `.groupBy(...).aggregate(...)`.
5818
+ */
5819
+ groupBy?: string | ReadonlyArray<string>;
5820
+ /**
5821
+ * Aggregation spec for UNION mode (#165). Applied per-group after
5822
+ * {@link groupBy} buckets the concatenated mapped-row stream from
5823
+ * {@link unionSources}. Same shape as the `AggregateSpec` passed to
5824
+ * `Query.aggregate()`.
5825
+ *
5826
+ * UNION-mode only. Ignored if {@link query} is set.
5827
+ */
5828
+ aggregate?: AggregateSpec;
5829
+ /**
5830
+ * Pure function from a materialized row → stable id used in the
5831
+ * output collection. Required — explicit always beats default-with-pitfalls
5832
+ * (see niwat-review of #149 round 1 for the slash-collision rationale).
5833
+ */
5834
+ rowKey: (row: TRow) => string;
5835
+ /**
5836
+ * Explicit source collections (#152). Required when `query()` returns
5837
+ * an `Aggregation` or `GroupedAggregation` rather than a `Query<T>`
5838
+ * — the dependency analyzer can't introspect through `groupBy().aggregate()`
5839
+ * back to the source. Optional for plain `Query<T>` results — the
5840
+ * analyzer extracts dependencies automatically from the query plan.
5841
+ *
5842
+ * When set, takes precedence over auto-analysis.
5843
+ */
5844
+ sources?: ReadonlyArray<string>;
5845
+ /**
5846
+ * Declared deterministic predicates (#153). Each entry pairs a
5847
+ * consumer-stable `hash` with a function. The `query()` callback's
5848
+ * Query<T> can invoke them via `.wherePredicate(name, ctx?)`. The
5849
+ * predicate's `hash` + a canonical-JSON hash of `ctx` both fold
5850
+ * into `queryHash` — bumping either forces refresh on next visit.
5851
+ *
5852
+ * Consumer responsibility: bump `hash` when the function's semantics
5853
+ * change. Failing to bump after a non-equivalent change leaves
5854
+ * stale rows around until the next explicit refresh.
5855
+ */
5856
+ predicates?: {
5857
+ [name: string]: {
5858
+ hash: string;
5859
+ fn: (row: TRow, ctx?: unknown) => boolean;
5860
+ };
5861
+ };
5862
+ /**
5863
+ * Refresh policy.
5864
+ *
5865
+ * - `'eager'` — re-materialize synchronously inside the source-write
5866
+ * transaction (composes with `withTransactions` for strict-mode
5867
+ * rollback).
5868
+ * - `'lazy'` — mark stale on source-change; materialize on first
5869
+ * read of the MV.
5870
+ * - `'manual'` — only materializes when `vault.refreshView(name)` is
5871
+ * called. Useful for very expensive MVs or time-dependent queries
5872
+ * whose `ctx` changes externally.
5873
+ */
5874
+ refresh: 'eager' | 'lazy' | 'manual';
5875
+ /** Output routing. Optional; defaults to writing the collection named after `name`. */
5876
+ output?: MaterializedViewOutput;
5877
+ /**
5878
+ * What to do when a re-materialization produces zero rows for a key
5879
+ * that previously had rows.
5880
+ *
5881
+ * - `'delete'` (default) — tombstone the prior MV row via
5882
+ * `Collection._internalDelete` (system housekeeping bypasses user
5883
+ * `onDelete` guards on the output collection — see PR #148's
5884
+ * composition fix).
5885
+ * - `'keep'` — leave the prior MV row in place. Useful when zero
5886
+ * is a meaningful state.
5887
+ */
5888
+ onEmpty?: 'delete' | 'keep';
5889
+ /**
5890
+ * `true` re-throws on any row-write failure → composes with
5891
+ * `withTransactions` to roll back the source-write atomically via
5892
+ * `revertExecuted` (#133). Default `false` (failed rows are
5893
+ * isolated; other rows commit).
5894
+ */
5895
+ strict?: boolean;
5896
+ /**
5897
+ * Row-count ceiling for the materialized output. Throws
5898
+ * `MaterializedViewTooLargeError` before any writes when exceeded
5899
+ * — keeps the rollback clean. Default `100_000`; override per-MV
5900
+ * when the domain warrants it.
5901
+ */
5902
+ maxRows?: number;
5903
+ }
5904
+ /** Returned by `withMaterializedView()` and consumed by `createNoydb`. */
5905
+ interface MaterializedViewStrategyHandle {
5906
+ readonly __noydb_strategy: 'materialized-view';
5907
+ readonly spec: MaterializedViewStrategy<any>;
5908
+ }
5909
+
5910
+ /**
5911
+ * One registered MV strategy alongside its derived metadata. Stored
5912
+ * type-erased on `TRow` so the registry can hold heterogeneous MVs.
5913
+ */
5914
+ interface RegisteredMV {
5915
+ readonly spec: MaterializedViewStrategy<any>;
5916
+ /** Output collection name (`spec.output?.collection ?? spec.name`). */
5917
+ readonly outputCollection: string;
5918
+ /** Set of source collections; populated at registration via the analyzer. */
5919
+ readonly dependencies: ReadonlySet<string>;
5920
+ /** Canonical `queryHash` — `_materializedFrom.queryHash` for every emitted row. */
5921
+ readonly queryHash: string;
5922
+ /**
5923
+ * Top-level FieldClauses on the partition field, captured at
5924
+ * registration time. Used by the cycle detector to resolve
5925
+ * same-collection-as-source edges via the partition-discriminator
5926
+ * check (#152). Empty when `spec.output?.partition` is undefined.
5927
+ */
5928
+ readonly partitionClauses: readonly FieldClause[];
5929
+ }
5930
+ /**
5931
+ * Vault-internal registry of MV strategies. Owned by `Vault`; not
5932
+ * exported. Parallel to v1's `DerivationRegistry`; the two graphs share
5933
+ * a single cycle-detection pass at vault open (see `validate`).
5934
+ *
5935
+ * @internal
5936
+ */
5937
+ declare class MaterializedViewRegistry {
5938
+ /** Keyed by `spec.name`. */
5939
+ private readonly _byName;
5940
+ /** Keyed by dependency source-collection → MVs that depend on it. */
5941
+ private readonly _bySource;
5942
+ /**
5943
+ * Register an MV. Invokes `spec.query()` once at registration time to
5944
+ * read the plan + join context; the resulting `Query<T>` is discarded
5945
+ * after dependency extraction. `vault.collection(...)` must therefore
5946
+ * be functional by the time this runs — typically wired from
5947
+ * `Vault._initMaterializedViews` after collection bootstrap.
5948
+ *
5949
+ * Throws `MaterializedViewSourceUnknownError` if the analyzer
5950
+ * surfaces a dependency the vault doesn't know about (when a
5951
+ * `knownCollections` checker is supplied).
5952
+ */
5953
+ register(spec: MaterializedViewStrategy<any>, db: MVQueryContext, options?: {
5954
+ knownCollections?: (name: string) => boolean;
5955
+ }): Promise<void>;
5956
+ /** All MVs that depend on `source`, in registration order. */
5957
+ mvsForSource(source: string): ReadonlyArray<RegisteredMV>;
5958
+ /** Single MV by name, or `undefined`. */
5959
+ byName(name: string): RegisteredMV | undefined;
5960
+ /** Iterate over every registered MV. */
5961
+ all(): ReadonlyArray<RegisteredMV>;
5962
+ /**
5963
+ * Cycle detection over the combined derivation + MV graph. Edges:
5964
+ * - Derivation: derivation.source → output.collection (each output)
5965
+ * - MV: every dep in MV.dependencies → MV.outputCollection
5966
+ *
5967
+ * Throws `MaterializedViewCycleError` if the cycle's terminal node
5968
+ * is an MV output collection; otherwise (a pure-derivation cycle)
5969
+ * the caller's `DerivationRegistry.validate()` will surface
5970
+ * `DerivationCycleError` separately at vault open.
5971
+ *
5972
+ * Call AFTER all `register()` calls complete.
5973
+ */
5974
+ validate(derivationRegistry?: DerivationRegistry | null): void;
5975
+ }
5976
+
5977
+ /**
5978
+ * Read-shadow overlay primitive (#154, MV v2 spec § Composition with
5979
+ * operator-editable lifecycle). Binds an MV's read-only base output
5980
+ * to a separate user-writable overlay collection; reads merge via a
5981
+ * single shadow predicate, writes route to the overlay.
5982
+ *
5983
+ * v2 ships the read-shadow variant only — arbitrary `mergePolicy`
5984
+ * callbacks are deferred to v3.
5985
+ */
5986
+ interface OverlayedViewStrategy {
5987
+ /**
5988
+ * Virtual collection name. `vault.collection(name)` returns a
5989
+ * proxy that merges `base` and `overlay` per the shadow rule.
5990
+ * Writes to the proxy route to the `overlay` collection. The name
5991
+ * must be unique within the vault — collisions with MV outputs or
5992
+ * concrete source collections throw `OverlayNameCollisionError` at
5993
+ * vault open.
5994
+ */
5995
+ name: string;
5996
+ /**
5997
+ * The collection providing the default rows. Typically an MV's
5998
+ * output collection. Must be a CONCRETE collection (a real source
5999
+ * or an MV output) — not itself another overlay's virtual name.
6000
+ * Multi-overlay stacking is a v3 non-goal; the constraint is
6001
+ * enforced at vault open via `OverlayBaseIsVirtualError`.
6002
+ */
6003
+ base: string;
6004
+ /**
6005
+ * User-writable collection that carries overrides. Must be a real,
6006
+ * vault-known collection that is NOT an MV-output collection. The
6007
+ * overlay's `withGuard` / `withDerivation` registrations apply to
6008
+ * direct writes; the virtual layer's `put(record)` also flows
6009
+ * through the overlay's normal write pipeline.
6010
+ */
6011
+ overlay: string;
6012
+ /**
6013
+ * Single-field shadow predicate. When `overlay[shadowField] ===
6014
+ * shadowValue` for a given id, virtual-collection reads of that id
6015
+ * return the overlay row; otherwise reads return the base row.
6016
+ *
6017
+ * Niwat's canonical example: `dataStatus === 'override'` flips a
6018
+ * row into operator-controlled mode.
6019
+ *
6020
+ * No callback merge, no priority lattice, no field-level merge —
6021
+ * v2 stays explicitly narrow.
6022
+ */
6023
+ shadowField: string;
6024
+ shadowValue: unknown;
6025
+ }
6026
+ /** Returned by `withOverlayedView()` and consumed by `createNoydb`. */
6027
+ interface OverlayedViewStrategyHandle {
6028
+ readonly __noydb_strategy: 'overlayed-view';
6029
+ readonly spec: OverlayedViewStrategy;
6030
+ }
6031
+
6032
+ /**
6033
+ * Vault-internal registry of overlay strategies. Resolves the base
6034
+ * MV's `rowKey` lazily so virtual-collection writes can derive ids
6035
+ * from the row.
6036
+ *
6037
+ * @internal
6038
+ */
6039
+ declare class OverlayedViewRegistry {
6040
+ private readonly _byName;
6041
+ /**
6042
+ * Register an overlay. Validates name uniqueness, base concreteness,
6043
+ * and overlay availability AGAINST the MV registry — overlays
6044
+ * declared without the MV registry context skip cross-registry
6045
+ * checks but still validate self-consistency.
6046
+ */
6047
+ register(spec: OverlayedViewStrategy, options: {
6048
+ isOverlayName?: (name: string) => boolean;
6049
+ isMVOutput?: (name: string) => boolean;
6050
+ isKnownCollection?: (name: string) => boolean;
6051
+ }): void;
6052
+ byName(name: string): OverlayedViewStrategy | undefined;
6053
+ /** All overlay virtual names. */
6054
+ names(): ReadonlySet<string>;
6055
+ isOverlay(name: string): boolean;
6056
+ /**
6057
+ * Resolve the `rowKey` function for an overlay's base MV. Returns
6058
+ * `undefined` if the base isn't an MV (raw source collection) or
6059
+ * if the MV registry isn't supplied. Used by the virtual-collection
6060
+ * proxy to derive ids from `put(record)` calls.
6061
+ */
6062
+ resolveBaseRowKey(name: string, mvRegistry: MaterializedViewRegistry | null): ((row: Record<string, unknown>) => string) | undefined;
6063
+ }
6064
+
6065
+ /**
6066
+ * Vault-internal singleton that holds the guard graph and dispatches
6067
+ * per-collection guard execution. Owned by `Vault`; not exported.
6068
+ *
6069
+ * @internal
6070
+ */
6071
+ type AnyGuard = GuardStrategy<Record<string, unknown>>;
6072
+ type AnyChange = GuardChange<Record<string, unknown>>;
6073
+ declare class GuardRegistry {
6074
+ private readonly _byCollection;
6075
+ private _amendmentChanges;
6076
+ private _amendmentMeta;
6077
+ /** Register a guard. Multiple guards per collection are allowed. */
6078
+ register<T extends Record<string, unknown>>(spec: GuardStrategy<T>): void;
6079
+ /** All guards registered against `collection` in registration order. */
6080
+ guardsFor(collection: string): ReadonlyArray<AnyGuard>;
6081
+ /**
6082
+ * Run every guard's `check` for this collection. First throw wins —
6083
+ * remaining guards are not invoked. Guards without a `check` skip.
6084
+ */
6085
+ runChecks<T>(collection: string, incoming: T, ctx: GuardContext<T>): Promise<void>;
6086
+ /**
6087
+ * Run every guard's `onDelete` for this collection. First throw wins —
6088
+ * remaining guards are not invoked. Guards without an `onDelete` skip.
6089
+ * Mirrors {@link runChecks} but for the delete path.
6090
+ */
6091
+ runOnDelete<T>(collection: string, existing: T, ctx: GuardContext<T>): Promise<void>;
6092
+ /** True if any guard for `collection` declares an `amendment` block. */
6093
+ hasAmendment(collection: string): boolean;
6094
+ /** Open a new amendment change-collection window. */
6095
+ beginAmendment(): void;
6096
+ /** True iff we're currently inside an amendment transaction. */
6097
+ isAmendmentActive(): boolean;
6098
+ /**
6099
+ * Record a {before, after} pair for the active amendment. `vBefore`
6100
+ * and `vAfter` are stored in a parallel meta structure so the public
6101
+ * {@link GuardChange} shape handed to invariant callbacks stays
6102
+ * `{ before, after }` only — the audit ledger reads version metadata
6103
+ * via {@link consumeMeta}.
6104
+ */
6105
+ collectChange<T>(collection: string, id: string, before: T | null, after: T, vBefore?: number, vAfter?: number): void;
6106
+ /**
6107
+ * Drain the change-set and close the amendment window. The caller
6108
+ * (transaction commit) feeds these to each affected guard's invariant.
6109
+ */
6110
+ consumeChanges(): ReadonlyMap<string, ReadonlyArray<AnyChange>>;
6111
+ /**
6112
+ * Drain the parallel id/version metadata captured during the
6113
+ * amendment. Returned as a flat list with `collection` denormalised
6114
+ * so the audit ledger can emit one `{ collection, id, vBefore,
6115
+ * vAfter }` tuple per record. Must be called AFTER
6116
+ * {@link consumeChanges} (or independently) — calling it closes the
6117
+ * meta window in the same way.
6118
+ */
6119
+ consumeMeta(): ReadonlyArray<{
6120
+ collection: string;
6121
+ id: string;
6122
+ vBefore: number;
6123
+ vAfter: number;
6124
+ }>;
6125
+ }
6126
+
4726
6127
  /**
4727
- * Revoke a delegation by id the caller resolves the envelope and
4728
- * issues a `delete`. Provided as a stable helper so the naming is
4729
- * symmetric to `issueDelegation`.
6128
+ * Minimal read-only wrapper over a `Vault`. Used as `ctx.vault` inside
6129
+ * guard callbacks so they can fetch related records without acquiring
6130
+ * any write capability.
4730
6131
  */
4731
- declare function revokeDelegation(store: NoydbStore, vault: string, id: string): Promise<void>;
6132
+ declare class ReadOnlyVaultFacade implements ReadOnlyVaultFacade$1 {
6133
+ private readonly _vault;
6134
+ constructor(vault: Vault);
6135
+ collection<T = unknown>(name: string): {
6136
+ get(id: string): Promise<T | null>;
6137
+ list(): Promise<T[]>;
6138
+ query(): Query<T>;
6139
+ };
6140
+ }
4732
6141
 
4733
6142
  /**
4734
6143
  * `vault.exportBlobs()` — bulk blob extraction primitive.
@@ -5104,6 +6513,50 @@ declare function magicLinkGrantRecordId(token: string, index: number): string;
5104
6513
  */
5105
6514
  declare function isMagicLinkGrantExpired(payload: MagicLinkGrantPayload, now?: Date): boolean;
5106
6515
 
6516
+ /**
6517
+ * Type surface for the user-list visibility subsystem (#122).
6518
+ *
6519
+ * Two complementary flags:
6520
+ * - {@link DirectoryConfig} — vault-level "is the directory listing
6521
+ * enabled at all?" toggle. Owner-only mutation.
6522
+ * - {@link UserVisibility} — per-user "hide me from teammate listings"
6523
+ * opt-out. Self-mutation via `vault.user.setMyVisibility`.
6524
+ *
6525
+ * Both flags live in the existing `_meta` collection as plaintext-bypass
6526
+ * sidecars (`_iv: ''`). Neither is a security boundary — the keyring
6527
+ * file is still observable at `_keyring/*` and the envelope ciphertext
6528
+ * is still at `_users/*` to anyone with direct store read access. The
6529
+ * flags exist to keep admin-UI listings tidy, not to hide principals
6530
+ * from a determined attacker.
6531
+ *
6532
+ * @see docs/subsystems/user-envelope.md → Directory visibility
6533
+ *
6534
+ * @module
6535
+ */
6536
+ /**
6537
+ * Vault-level directory toggle. Persisted at `_meta/directory`.
6538
+ *
6539
+ * - `enabled: true` (default when no document exists) — every authenticated
6540
+ * caller can enumerate users via `listUsersWithEnvelopes`.
6541
+ * - `enabled: false` — only `owner` and `admin` callers can enumerate;
6542
+ * anyone else gets {@link import('../errors.js').DirectoryDisabledError}.
6543
+ */
6544
+ interface DirectoryConfig {
6545
+ readonly enabled: boolean;
6546
+ }
6547
+ /**
6548
+ * Per-user visibility flag. Persisted at `_meta/visibility/<keyringId>`.
6549
+ *
6550
+ * - `hidden: false` (default when no document exists) — the user shows up
6551
+ * in `listUsersWithEnvelopes` like any other principal.
6552
+ * - `hidden: true` — the user is filtered out of the default listing.
6553
+ * `owner`/`admin` callers can still see them by passing
6554
+ * `{ includeHidden: true }`.
6555
+ */
6556
+ interface UserVisibility {
6557
+ readonly hidden: boolean;
6558
+ }
6559
+
5107
6560
  /**
5108
6561
  * Public `vault.user.*` API surface.
5109
6562
  *
@@ -5238,6 +6691,30 @@ declare class UserApi {
5238
6691
  * Gated by `edit-own-profile`. See `updateMe` for `presented` usage.
5239
6692
  */
5240
6693
  setMe<T = unknown>(payload: T, presented?: UserEnvelopePresented): Promise<UserEnvelope<T>>;
6694
+ /**
6695
+ * Read the current user's visibility flag from
6696
+ * `_meta/visibility/<keyringId>`. Returns `{ hidden: false }` when no
6697
+ * document has been persisted (the default-visible case).
6698
+ */
6699
+ getMyVisibility(): Promise<UserVisibility>;
6700
+ /**
6701
+ * Update the current user's visibility in the team directory.
6702
+ *
6703
+ * - `hidden: true` — opt out of the default `listUsersWithEnvelopes`
6704
+ * listing. `owner`/`admin` callers can still see the user by passing
6705
+ * `{ includeHidden: true }`.
6706
+ * - `hidden: false` — opt back in.
6707
+ *
6708
+ * Own-only by construction: the keyringId argument doesn't exist on
6709
+ * this method, so no caller can hide or unhide another principal.
6710
+ *
6711
+ * Honest caveat: this is a UX flag, not a privacy guarantee. The
6712
+ * envelope ciphertext at `_users/<keyringId>` and the keyring file at
6713
+ * `_keyring/<userId>` are both still observable to anyone with direct
6714
+ * store read access. See `docs/subsystems/user-envelope.md` →
6715
+ * "Directory visibility".
6716
+ */
6717
+ setMyVisibility(visibility: UserVisibility): Promise<void>;
5241
6718
  /**
5242
6719
  * Read another principal's envelope by their keyringId. Returns null
5243
6720
  * if the principal exists but has no envelope yet, or if the
@@ -5297,6 +6774,159 @@ declare class UserApi {
5297
6774
  private fireChange;
5298
6775
  }
5299
6776
 
6777
+ /**
6778
+ * Persisted-schema envelope shape.
6779
+ *
6780
+ * Stored encrypted under `_schemas/<collection>` with the same DEK as the
6781
+ * collection's records. Auditors who can unlock the collection's data can
6782
+ * also read its schema; nothing more.
6783
+ *
6784
+ * @see docs/superpowers/specs/2026-05-22-schema-dump-design.md
6785
+ *
6786
+ * @module
6787
+ */
6788
+ /** Family of Standard Schema v1 validator the persisted snapshot was derived from. */
6789
+ type PersistedSchemaKind = 'Zod' | 'Valibot' | 'ArkType' | 'Effect' | 'Unknown';
6790
+ /**
6791
+ * Plaintext payload encrypted into the `_data` field of the
6792
+ * `_schemas/<collection>` envelope. The wrapper `EncryptedEnvelope` adds
6793
+ * `_noydb`, `_v`, `_ts`, `_iv`, `_data` per the standard noy-db record
6794
+ * format.
6795
+ */
6796
+ interface PersistedSchemaEnvelope {
6797
+ readonly _noydb_schema: 1;
6798
+ /** Detected validator family. */
6799
+ readonly kind: PersistedSchemaKind;
6800
+ /**
6801
+ * JSON Schema (Draft 2020-12) derived from the validator. Null when
6802
+ * derivation isn't yet supported for `kind`; in that case `reason` is
6803
+ * populated.
6804
+ */
6805
+ readonly jsonSchema: object | null;
6806
+ /** SHA-256 (hex) of the canonicalised JSON Schema, or null when unavailable. */
6807
+ readonly hash: string | null;
6808
+ /** Human-readable reason when `jsonSchema` is null. */
6809
+ readonly reason?: string;
6810
+ /** ISO-8601 timestamp of the most recent derivation write. */
6811
+ readonly derivedAt: string;
6812
+ }
6813
+
6814
+ /**
6815
+ * Types for {@link VaultSchemaSnapshot} — the structured object returned
6816
+ * by `vault.dumpSchema()`. Consumed by the upcoming `noydb describe`
6817
+ * CLI to emit human-readable YAML/JSON audit output.
6818
+ *
6819
+ * @see docs/superpowers/specs/2026-05-22-schema-dump-design.md
6820
+ *
6821
+ * @module
6822
+ */
6823
+
6824
+ /** Where the field-level info in the snapshot came from. */
6825
+ type FieldSource = 'persisted' | 'live-validator' | 'sampled' | 'unknown';
6826
+ interface FieldDescriptor {
6827
+ /** Inferred type tag: 'string' | 'number' | 'boolean' | 'enum' | 'object' | 'array' | 'null' | 'opaque'. */
6828
+ readonly type: string;
6829
+ /** Where this field info was sourced from. */
6830
+ readonly source: FieldSource;
6831
+ /** Optional constraints — minLength, maxLength, enum values, gt, etc. */
6832
+ readonly constraints?: Record<string, unknown>;
6833
+ /** True when the schema marks this field optional. */
6834
+ readonly optional?: boolean;
6835
+ /** Foreign-key target as `<collection>.<field>` when declared. */
6836
+ readonly references?: string;
6837
+ }
6838
+ interface CollectionStats {
6839
+ readonly records: number;
6840
+ readonly bytes: number;
6841
+ readonly bytesAvg: number;
6842
+ readonly bytesMin: number;
6843
+ readonly bytesMax: number;
6844
+ /** ISO-8601 from min(_ts) across envelopes. Empty string when no records. */
6845
+ readonly oldest: string;
6846
+ /** ISO-8601 from max(_ts) across envelopes. Empty string when no records. */
6847
+ readonly newest: string;
6848
+ }
6849
+ interface CollectionDescriptor {
6850
+ readonly fields: Record<string, FieldDescriptor>;
6851
+ readonly indexes: ReadonlyArray<{
6852
+ readonly fields: ReadonlyArray<string>;
6853
+ readonly unique?: boolean;
6854
+ }>;
6855
+ readonly refs: Record<string, {
6856
+ readonly target: string;
6857
+ readonly mode: 'strict' | 'warn' | 'cascade';
6858
+ }>;
6859
+ readonly validator?: {
6860
+ readonly kind: PersistedSchemaKind;
6861
+ readonly source: 'persisted' | 'live-validator';
6862
+ };
6863
+ readonly stats?: CollectionStats;
6864
+ }
6865
+ interface MaterializedViewDescriptor {
6866
+ readonly sources: ReadonlyArray<string>;
6867
+ readonly groupBy?: ReadonlyArray<string>;
6868
+ readonly aggregate?: Record<string, string>;
6869
+ readonly refresh: string;
6870
+ readonly stats?: CollectionStats;
6871
+ }
6872
+ interface OverlayViewDescriptor {
6873
+ readonly base: string;
6874
+ readonly overlay: string;
6875
+ }
6876
+ interface DerivationDescriptor {
6877
+ readonly source: string;
6878
+ readonly outputs: ReadonlyArray<string>;
6879
+ }
6880
+ interface InternalCollectionStats {
6881
+ readonly records: number;
6882
+ readonly bytes: number;
6883
+ }
6884
+ interface VaultSchemaSnapshot {
6885
+ readonly _noydb_snapshot: 1;
6886
+ readonly vault: string;
6887
+ readonly emittedAt: string;
6888
+ readonly subsystems: Record<string, boolean>;
6889
+ readonly aclRoles?: ReadonlyArray<string>;
6890
+ readonly collections: Record<string, CollectionDescriptor>;
6891
+ readonly materializedViews: Record<string, MaterializedViewDescriptor>;
6892
+ readonly overlayViews: Record<string, OverlayViewDescriptor>;
6893
+ readonly derivations: Record<string, DerivationDescriptor>;
6894
+ /** Only present when `dumpSchema({ withStats: true })` was called. */
6895
+ readonly internal?: Record<string, InternalCollectionStats>;
6896
+ }
6897
+ interface DumpSchemaOptions {
6898
+ /** When true, walk every collection's envelopes to compute counters. Default `false`. */
6899
+ readonly withStats?: boolean;
6900
+ /** Sample N records per collection lacking a persisted/live schema. Default 50. `0` disables sampling. */
6901
+ readonly sampleSize?: number;
6902
+ }
6903
+
6904
+ /**
6905
+ * Orchestrate the structural walk of a Vault, producing a
6906
+ * {@link VaultSchemaSnapshot}. Called from `Vault.dumpSchema()`.
6907
+ *
6908
+ * @module
6909
+ */
6910
+
6911
+ /**
6912
+ * The minimal slice of Vault internal state the walker needs.
6913
+ * Exposed via `vault._introspectState()` to keep the public Vault
6914
+ * surface narrow.
6915
+ *
6916
+ * @internal
6917
+ */
6918
+ interface VaultIntrospectState {
6919
+ readonly name: string;
6920
+ readonly adapter: NoydbStore;
6921
+ readonly collectionCache: Map<string, Collection<unknown>>;
6922
+ readonly refRegistry: RefRegistry;
6923
+ readonly getDEK: (collectionName: string) => Promise<CryptoKey>;
6924
+ readonly subsystems: Record<string, boolean>;
6925
+ readonly mvRegistry: unknown;
6926
+ readonly overlayRegistry: unknown;
6927
+ readonly derivationRegistry: unknown;
6928
+ }
6929
+
5300
6930
  /** A vault (tenant namespace) containing collections. */
5301
6931
  declare class Vault {
5302
6932
  private readonly adapter;
@@ -5339,6 +6969,40 @@ declare class Vault {
5339
6969
  private readonly historyStrategy;
5340
6970
  private readonly i18nStrategy;
5341
6971
  private readonly syncStrategy;
6972
+ /**
6973
+ * Per-vault guard registry. `null` until `_initGuards()` runs; stays
6974
+ * `null` for vaults that never register any guard strategy. The
6975
+ * runtime class is dynamic-imported on demand so consumers that
6976
+ * never use guards don't pull `GuardRegistry`/`GuardExecutor` into
6977
+ * their bundle (#130).
6978
+ */
6979
+ private guardRegistry;
6980
+ /**
6981
+ * Per-vault derivation registry. Same lazy-load contract as
6982
+ * `guardRegistry` — `null` until `_initDerivations()` runs with at
6983
+ * least one strategy handle. See #130 for the bundle motivation.
6984
+ */
6985
+ private derivationRegistry;
6986
+ /**
6987
+ * Per-vault materialized-view registry (#143/#150). Same lazy-load
6988
+ * contract as `derivationRegistry` — `null` until
6989
+ * `_initMaterializedViews()` runs with at least one MV handle.
6990
+ */
6991
+ private materializedViewRegistry;
6992
+ /**
6993
+ * Per-vault overlay registry (#154). Same lazy-load contract as
6994
+ * `materializedViewRegistry` — `null` until `_initOverlayedViews()`
6995
+ * runs with at least one handle.
6996
+ */
6997
+ private overlayedViewRegistry;
6998
+ /**
6999
+ * Cached read-only facade handed to guard callbacks via `ctx.vault`,
7000
+ * and to derivation callbacks via `derive(source, ctx)`. Allocated
7001
+ * eagerly inside `_initGuards()` and/or `_initDerivations()` so read
7002
+ * accessors stay synchronous (callers in `tx/transaction.ts` rely on
7003
+ * that). Stays `null` for vaults with neither subsystem configured.
7004
+ */
7005
+ private readOnlyFacade;
5342
7006
  private getDEK;
5343
7007
  /**
5344
7008
  * Per-principal user envelope API.
@@ -5373,6 +7037,12 @@ declare class Vault {
5373
7037
  * `vault.compact()`. Indexed by collection name.
5374
7038
  */
5375
7039
  private readonly blobFieldsRegistry;
7040
+ /**
7041
+ * Per-collection attestation field-schema (issue side). Populated on
7042
+ * `collection({ attestation })` and read by `issueAttestation()`.
7043
+ * Indexed by collection name.
7044
+ */
7045
+ private readonly attestationRegistry;
5376
7046
  /**
5377
7047
  * Per-vault ledger store. Lazy-initialized on first
5378
7048
  * `collection()` call (which passes it through to the Collection)
@@ -5386,6 +7056,16 @@ declare class Vault {
5386
7056
  * docstring.
5387
7057
  */
5388
7058
  private ledgerStore;
7059
+ /**
7060
+ * Background writes for persisted-schema envelopes (#schema-dump v0
7061
+ * slice 1). One promise per `collection({ persistJsonSchema: true })`
7062
+ * registration that actually fired a derive call. Fire-and-forget
7063
+ * from the collection factory; tests await
7064
+ * {@link _drainPendingSchemaWrites} before asserting on storage.
7065
+ * Production code does not need to drain — the writes are
7066
+ * idempotent fingerprints, not correctness invariants.
7067
+ */
7068
+ private _pendingSchemaWrites;
5389
7069
  /**
5390
7070
  * Per-vault foreign-key reference registry. Collections
5391
7071
  * register their `refs` option here on construction; the
@@ -5497,6 +7177,7 @@ declare class Vault {
5497
7177
  historyStrategy?: HistoryStrategy | undefined;
5498
7178
  i18nStrategy?: I18nStrategy | undefined;
5499
7179
  syncStrategy?: SyncStrategy | undefined;
7180
+ guardStrategies?: ReadonlyArray<GuardStrategyHandleAny> | undefined;
5500
7181
  });
5501
7182
  /**
5502
7183
  * Construct (or reconstruct) the lazy DEK resolver. Captures the
@@ -5569,7 +7250,28 @@ declare class Vault {
5569
7250
  tiers?: readonly number[];
5570
7251
  /** — how lower-tier reads see above-tier records. */
5571
7252
  tierMode?: TierMode;
7253
+ /**
7254
+ * Opt-in persisted JSON Schema. When `true` AND a Zod `schema` is
7255
+ * provided, hub derives a JSON Schema via `zod-to-json-schema`
7256
+ * (optional peer-dep) and writes an encrypted snapshot to
7257
+ * `_schemas/<collectionName>`. Re-runs on every open; hash-skip
7258
+ * avoids write churn when the schema is unchanged.
7259
+ *
7260
+ * Default: `false`. Non-Zod Standard Schema validators receive a
7261
+ * stub envelope flagging the kind without a JSON Schema body.
7262
+ *
7263
+ * @see docs/superpowers/specs/2026-05-22-schema-dump-design.md
7264
+ */
7265
+ persistJsonSchema?: boolean;
7266
+ /** — declare the per-field schema for document attestation (issue side). */
7267
+ attestation?: AttestationFieldSchema;
5572
7268
  }): Collection<T>;
7269
+ /**
7270
+ * Await all background persisted-schema writes triggered by
7271
+ * `collection({ persistJsonSchema: true })` calls on this vault.
7272
+ * Used in tests; production code does not need to call this.
7273
+ */
7274
+ _drainPendingSchemaWrites(): Promise<void>;
5573
7275
  /**
5574
7276
  * Validate i18nText fields on a `put()`. Called by Collection just
5575
7277
  * before the adapter write, after schema validation. Throws
@@ -5745,6 +7447,22 @@ declare class Vault {
5745
7447
  */
5746
7448
  compact(options?: CompactRunOptions): Promise<CompactionResult>;
5747
7449
  exportBlobs(options?: ExportBlobsOptions): ExportBlobsHandle;
7450
+ issueAttestation(collectionName: string, id: string): Promise<{
7451
+ docId: string;
7452
+ qr: string;
7453
+ keyId: string;
7454
+ publicKeyB64: string;
7455
+ }>;
7456
+ getDocumentSigningPublicKey(): Promise<{
7457
+ keyId: string;
7458
+ publicKeyB64: string;
7459
+ }>;
7460
+ private makeIssueContext;
7461
+ revokeAttestation(docId: string): Promise<void>;
7462
+ unrevokeAttestation(docId: string): Promise<void>;
7463
+ getRevokedDocIds(): Promise<string[]>;
7464
+ publishRevocationList(): Promise<RevocationList>;
7465
+ private makeRevokeContext;
5748
7466
  private writeExportAudit;
5749
7467
  /**
5750
7468
  * Read-only accessor for the invoking keyring's export capability,
@@ -5859,6 +7577,124 @@ declare class Vault {
5859
7577
  * throws on null; this one stays silent so the off-path no-ops.
5860
7578
  */
5861
7579
  private getLedgerOrNull;
7580
+ /**
7581
+ * @internal — called by `Noydb.openVault` after construction.
7582
+ * Dynamic-imports `GuardRegistry` + `ReadOnlyVaultFacade` and seeds
7583
+ * the registry with the supplied strategy handles. No-op when the
7584
+ * handles array is empty — keeps the guard subsystem out of the
7585
+ * floor bundle for consumers that don't use guards (#130).
7586
+ *
7587
+ * The read-only facade is eagerly instantiated here so the sync
7588
+ * accessor `_getReadOnlyFacade()` (called from the tx amendment
7589
+ * runner) stays synchronous.
7590
+ */
7591
+ _initGuards(handles: ReadonlyArray<GuardStrategyHandleAny>): Promise<void>;
7592
+ /**
7593
+ * @internal — Collection.put calls into this. Returns `null` for
7594
+ * vaults that never registered any guard strategy. Callers MUST
7595
+ * gate on null (the existing `if (this.guardSource)` branches in
7596
+ * `Collection` already do this transitively).
7597
+ */
7598
+ _getGuardRegistry(): GuardRegistry | null;
7599
+ /**
7600
+ * @internal — called by `Noydb.openVault` after construction.
7601
+ * Dynamic-imports `DerivationRegistry` and registers the supplied
7602
+ * derivation strategies (async because `strategyHash` computation
7603
+ * goes through `crypto.subtle.digest`). No-op when the handles
7604
+ * array is empty — keeps the derivation subsystem out of the floor
7605
+ * bundle for consumers that don't use derivations (#130). Throws
7606
+ * `DerivationCycleError` if a cycle is detected after registration.
7607
+ */
7608
+ _initDerivations(handles: ReadonlyArray<DerivationStrategyHandle>): Promise<void>;
7609
+ /**
7610
+ * @internal — consumed by `Collection.put` at write-time. Returns
7611
+ * `null` for vaults that never registered any derivation strategy.
7612
+ */
7613
+ _getDerivationRegistry(): DerivationRegistry | null;
7614
+ /**
7615
+ * @internal — called by `Noydb.openVault` after collections are
7616
+ * wired. Dynamic-imports `MaterializedViewRegistry`, registers each
7617
+ * MV spec (which invokes its `query()` once for dependency
7618
+ * analysis), then runs the unified cycle detection across the MV +
7619
+ * derivation graphs. No-op when the handles array is empty — keeps
7620
+ * the MV subsystem out of the floor bundle (mirrors v1 #130).
7621
+ * Throws `MaterializedViewCycleError` if a cycle is detected.
7622
+ */
7623
+ _initMaterializedViews(handles: ReadonlyArray<MaterializedViewStrategyHandle>): Promise<void>;
7624
+ /**
7625
+ * @internal — consumed by `Collection.put` at write-time. Returns
7626
+ * `null` for vaults that never registered any MV strategy.
7627
+ */
7628
+ _getMaterializedViewRegistry(): MaterializedViewRegistry | null;
7629
+ /**
7630
+ * @internal — called by `Noydb.openVault` after MVs are wired.
7631
+ * Dynamic-imports `OverlayedViewRegistry`, registers each spec,
7632
+ * validates against the MV registry for name/base/overlay collisions.
7633
+ * Throws on validation failure.
7634
+ */
7635
+ _initOverlayedViews(handles: ReadonlyArray<OverlayedViewStrategyHandle>): Promise<void>;
7636
+ /**
7637
+ * @internal — consumed by `Vault.collection()`. Returns `null` for
7638
+ * vaults with no overlays registered.
7639
+ */
7640
+ _getOverlayedViewRegistry(): OverlayedViewRegistry | null;
7641
+ /**
7642
+ * Manual re-materialize for a single registered MV (#151). Useful
7643
+ * for `refresh: 'manual'` MVs (whose consumer drives refreshes
7644
+ * externally), for stale-bit recovery on vault re-open, and as the
7645
+ * explicit bulk-recompute escape hatch after a strategy change.
7646
+ *
7647
+ * Returns `{ written, deleted, failed }`. `deleted` is always 0 in
7648
+ * foundation + this sub-issue — tombstoning lands in #152.
7649
+ *
7650
+ * Throws if `name` is not a registered MV.
7651
+ */
7652
+ refreshView(name: string): Promise<{
7653
+ written: number;
7654
+ deleted: number;
7655
+ failed: number;
7656
+ }>;
7657
+ /**
7658
+ * Re-derive every record in the named source collection. Useful
7659
+ * after a strategy change to bring previously-derived records
7660
+ * up-to-date.
7661
+ *
7662
+ * Sequential in v1; parallelisation deferred to v2.
7663
+ */
7664
+ deriveAll(sourceCollection: string): Promise<{
7665
+ derived: number;
7666
+ failed: number;
7667
+ }>;
7668
+ /**
7669
+ * @internal — exposed for `runTransaction({ amendment: true })` so
7670
+ * the amendment invariant runner can pass the SAME read-only vault
7671
+ * facade that the per-record `Collection.put` guard hook uses
7672
+ * (`guardSource.readOnlyVault()` above). Eagerly instantiated by
7673
+ * `_initGuards()` so this accessor stays synchronous; returns
7674
+ * `null` for vaults that never registered any guard (amendments
7675
+ * require at least one guard, so the caller should never see null).
7676
+ */
7677
+ _getReadOnlyFacade(): ReadOnlyVaultFacade | null;
7678
+ /**
7679
+ * Internal lazy-allocator for the read-only facade. Used by the
7680
+ * per-collection `guardSource.readOnlyVault` callback when guards
7681
+ * ARE configured but `_initGuards()` raced with the first guard
7682
+ * invocation (theoretically impossible — `Noydb.openVault` awaits
7683
+ * `_initGuards` before returning — but we keep the defensive lazy
7684
+ * path so the closure's contract stays "always returns a facade").
7685
+ */
7686
+ private _ensureReadOnlyFacade;
7687
+ /**
7688
+ * @internal — exposed for `runTransaction({ amendment: true })`
7689
+ * to append the structured `op: 'amendment'` audit entry without
7690
+ * dragging this private accessor onto the public surface or
7691
+ * forcing the tx executor to depend on the history-strategy
7692
+ * shape directly. Returns `null` when no history strategy is
7693
+ * configured, in which case the amendment commits silently
7694
+ * (the records still write through; only the multi-record
7695
+ * audit summary is skipped).
7696
+ */
7697
+ _getLedgerOrNull(): LedgerStore | null;
5862
7698
  /**
5863
7699
  * Return a read-only view of this vault as it existed at
5864
7700
  * `timestamp`. Time-machine queries are reconstructed from the
@@ -6062,6 +7898,34 @@ declare class Vault {
6062
7898
  private _decryptPeriodRecord;
6063
7899
  /** List all collection names in this vault. */
6064
7900
  collections(): Promise<string[]>;
7901
+ /**
7902
+ * Emit a structured introspection snapshot of this vault — vault name,
7903
+ * subsystem opt-in matrix, collections + their fields, materialized
7904
+ * views, overlay views, derivations. With `withStats: true`, walks
7905
+ * every collection's envelopes to compute record counts, byte totals,
7906
+ * and oldest/newest timestamps.
7907
+ *
7908
+ * Consumed by the `noydb describe` CLI to produce human-readable
7909
+ * audit YAML/JSON from a `.noydb` bundle.
7910
+ *
7911
+ * Field provenance:
7912
+ * - `persisted`: read from `_schemas/<col>` envelope (Route B opt-in)
7913
+ * - `live-validator`: derived in-process from a Zod schema attached
7914
+ * to the live `Collection`
7915
+ * - `sampled`: inferred from decrypted records (deferred to a follow-up)
7916
+ * - `unknown`: no schema info available
7917
+ *
7918
+ * @see docs/superpowers/specs/2026-05-22-schema-dump-design.md
7919
+ */
7920
+ dumpSchema(opts?: DumpSchemaOptions): Promise<VaultSchemaSnapshot>;
7921
+ /**
7922
+ * Internal accessor for {@link dumpVaultSchema}. Exposes the structural
7923
+ * state the walker needs (collection cache, registries, ref registry,
7924
+ * adapter) without widening the public Vault surface.
7925
+ *
7926
+ * @internal
7927
+ */
7928
+ _introspectState(): VaultIntrospectState;
6065
7929
  /**
6066
7930
  * Return the stable opaque bundle handle for this vault,
6067
7931
  * generating and persisting a fresh ULID on first call.
@@ -6729,6 +8593,34 @@ declare class Collection<T> {
6729
8593
  * adapter on first use.
6730
8594
  */
6731
8595
  private readonly periodGuard;
8596
+ /**
8597
+ * Optional back-reference to the owning vault's guard registry + a
8598
+ * read-only vault facade. When present, `Collection.put` and
8599
+ * `Collection.delete` consult the registry for guards declared
8600
+ * against this collection and run their `check` + `frozenFields`
8601
+ * before the adapter write. Absent in unit tests that construct
8602
+ * a Collection directly; production code always sets it via
8603
+ * `Vault.collection()`.
8604
+ *
8605
+ * Typed structurally rather than as `Vault` to avoid a circular
8606
+ * import (mirrors the `refEnforcer` / `joinResolver` pattern).
8607
+ */
8608
+ private readonly guardSource;
8609
+ /**
8610
+ * Vault-internal hook for derivation dispatch. When set,
8611
+ * `Collection.put` consults the registry after the source-write
8612
+ * commits and writes derived outputs through `getCollection(name).put`.
8613
+ * Same structural-interface pattern as `guardSource` to avoid a
8614
+ * circular Vault import.
8615
+ */
8616
+ private readonly derivationSource;
8617
+ /**
8618
+ * Vault-internal hook for materialized-view dispatch (#143/#150).
8619
+ * Parallel to `derivationSource` — when set, `Collection.put` fires
8620
+ * `MaterializedViewRegistry.onSourceWrite` after the source-write
8621
+ * commits + after `dispatchDerivations` has run.
8622
+ */
8623
+ private readonly materializedViewSource;
6732
8624
  /**
6733
8625
  * Optional back-reference to the owning compartment's ref
6734
8626
  * enforcer. When present, `Collection.put` calls
@@ -6982,6 +8874,63 @@ declare class Collection<T> {
6982
8874
  ts: string | null;
6983
8875
  record: Record<string, unknown> | null;
6984
8876
  } | null, incoming: Record<string, unknown> | null) => Promise<void>;
8877
+ /**
8878
+ * Optional back-reference to the owning vault's guard registry +
8879
+ * read-only facade. When present, put/delete consult registered
8880
+ * guards for this collection. Same structural-interface pattern
8881
+ * as `refEnforcer` to avoid a circular Vault import.
8882
+ */
8883
+ guardSource?: {
8884
+ registry(): GuardRegistry;
8885
+ readOnlyVault(): ReadOnlyVaultFacade$1;
8886
+ } | undefined;
8887
+ /**
8888
+ * Optional back-reference to the owning vault's derivation
8889
+ * registry + collection accessor. When present, successful
8890
+ * `put()` dispatches registered derivation strategies for the
8891
+ * source collection. Same structural-interface pattern as
8892
+ * `guardSource` to avoid a circular Vault import.
8893
+ */
8894
+ derivationSource?: {
8895
+ registry(): DerivationRegistry;
8896
+ getCollection(name: string): Collection<Record<string, unknown>>;
8897
+ /**
8898
+ * Read-only vault facade handed to `derive(source, ctx)` so a
8899
+ * derivation can fetch sibling records (#147). Same shape and
8900
+ * instance the guards subsystem uses for `check(incoming, ctx)`.
8901
+ */
8902
+ getReadOnlyFacade(): ReadOnlyVaultFacade$1;
8903
+ /**
8904
+ * Read access to the owning Noydb's currently-active multi-record
8905
+ * transaction context, or `null` when no transaction is running.
8906
+ * `dispatchDerivations` consults this so a recursive derived-output
8907
+ * write can register its pre-write envelope onto `ctx._executed`
8908
+ * and roll back alongside the source op on mid-batch failure (#133).
8909
+ */
8910
+ getActiveTxContext(): TxContext | null;
8911
+ /**
8912
+ * Construct a transient TxContext bound to the owning Noydb. Used
8913
+ * by `Collection.putManyAtomic` to publish an active context for
8914
+ * its Phase 2 loop (#133).
8915
+ */
8916
+ createTxContext(): TxContext;
8917
+ /** Publish a TxContext for the duration of a bulk-atomic loop. */
8918
+ setActiveTxContext(ctx: TxContext): void;
8919
+ /** Drop a previously-published TxContext. */
8920
+ clearActiveTxContext(ctx: TxContext): void;
8921
+ } | undefined;
8922
+ /**
8923
+ * Vault-internal hook for materialized-view dispatch (#143/#150).
8924
+ * Parallel to `derivationSource`. When set, `Collection.put` fires
8925
+ * registered MV `onSourceWrite` after the standard derivation
8926
+ * dispatch.
8927
+ */
8928
+ materializedViewSource?: {
8929
+ registry(): MaterializedViewRegistry;
8930
+ getCollection(name: string): Collection<any>;
8931
+ getActiveTxContext(): TxContext | null;
8932
+ getQueryContext(): MVQueryContext;
8933
+ } | undefined;
6985
8934
  });
6986
8935
  /**
6987
8936
  * Return the Standard Schema validator attached to this collection,
@@ -7032,10 +8981,109 @@ declare class Collection<T> {
7032
8981
  staleMs?: number;
7033
8982
  pollIntervalMs?: number;
7034
8983
  }): PresenceHandle<P>;
7035
- /** Create or update a record. */
7036
- put(id: string, record: T): Promise<void>;
8984
+ /**
8985
+ * Create or update a record.
8986
+ *
8987
+ * @param id Record identifier.
8988
+ * @param record The record body (validated by the collection's schema
8989
+ * if one was attached at `vault.collection(...)` time).
8990
+ * @param options Optional metadata for audit + import workflows.
8991
+ * `reason` is stamped onto the resulting ledger entry
8992
+ * (see #1) so audit consumers can filter via
8993
+ * `entries.filter(e => e.reason?.startsWith('import:'))`.
8994
+ */
8995
+ put(id: string, record: T, options?: {
8996
+ readonly reason?: string;
8997
+ }): Promise<void>;
8998
+ /**
8999
+ * Fire registered MV strategies whose dependency set includes this
9000
+ * collection. Eager-mode MVs re-materialize inline via
9001
+ * `MaterializedViewExecutor.refresh`; lazy / manual modes are
9002
+ * no-ops in the foundation (subtask #150) — wired in #151.
9003
+ *
9004
+ * Skips entirely when the record being written is itself an
9005
+ * MV-emitted row (carries `_materializedFrom`) — defensive guard
9006
+ * against missed cycle detection.
9007
+ *
9008
+ * @internal
9009
+ */
9010
+ private dispatchMaterializedViews;
9011
+ /**
9012
+ * Fire registered derivation strategies for this source collection.
9013
+ * Eager mode runs `derive` inline and writes each output via the
9014
+ * sibling `Collection.put`; lazy mode marks dependent outputs stale
9015
+ * (D11 stub today). Errors in non-strict mode are logged and
9016
+ * skipped; strict mode propagates the first failing output's error.
9017
+ *
9018
+ * Skips entirely when the record being written is itself a derived
9019
+ * output (carries `_derivedFrom`) — defensive guard against missed
9020
+ * cycle detection.
9021
+ */
9022
+ private dispatchDerivations;
7037
9023
  /** Delete a record by ID. */
7038
9024
  delete(id: string): Promise<void>;
9025
+ /**
9026
+ * @internal — system-internal delete that bypasses user-facing
9027
+ * delete hooks (`onDelete`, accounting-period guard, FK ref
9028
+ * enforcer). Used by derivation tombstones (#144) and MV refresh
9029
+ * (Dim 14 v2) — system housekeeping shouldn't trip user invariants
9030
+ * registered against the output collection. The ledger entry and
9031
+ * history snapshot still fire so backup integrity and time-travel
9032
+ * reconstruction stay consistent.
9033
+ *
9034
+ * Returns silently for delete-of-absent (idempotent contract — both
9035
+ * paths honour this: the `txCtx === null` path also reads the prior
9036
+ * envelope and short-circuits before the ledger/event side-effects).
9037
+ *
9038
+ * When a `txCtx` is supplied, the prior envelope is captured and
9039
+ * pushed onto `txCtx._executed` BEFORE the delete fires — mirrors
9040
+ * the #133 rollback hardening for puts. Callers outside a
9041
+ * multi-record transaction pass `null` and skip the tracking.
9042
+ *
9043
+ * Amendment composition: if `_internalDelete` runs while a vault's
9044
+ * `GuardRegistry` has an amendment window open, the `{before, after:
9045
+ * null}` change pair is pushed onto the amendment change-set the
9046
+ * same way a user-initiated delete would. The `onDelete` user-hook
9047
+ * is still skipped (housekeeping must not trip user invariants in
9048
+ * normal mode), but the amendment's invariant DOES see the change
9049
+ * — so a `RCT-CANCEL-001`-style invariant pairing can reject a
9050
+ * derivation-driven tombstone fired during an admin amendment.
9051
+ *
9052
+ * Constraint to surface to consumers: output collections of
9053
+ * derivations with `optional: true` outputs should not be the
9054
+ * targets of `strict` or `cascade` inbound foreign-key refs —
9055
+ * `_internalDelete` bypasses the ref enforcer by design (the
9056
+ * `onDelete` bypass primitive). Treat the housekeeping path as
9057
+ * "system can tombstone its own emissions regardless of FK shape."
9058
+ *
9059
+ * Permission handling is unchanged: the caller must still hold
9060
+ * write permission on the collection (derivations run under the
9061
+ * user's keyring).
9062
+ */
9063
+ _internalDelete(id: string, txCtx?: TxContext | null): Promise<void>;
9064
+ private _doDelete;
9065
+ /**
9066
+ * Cascade deletes of array-shape derived rows when a source row is
9067
+ * deleted (#200). Reads each registered strategy's fanout sidecar
9068
+ * for this source id, deletes every listed derived row, then
9069
+ * deletes the sidecar itself.
9070
+ *
9071
+ * Record-shape derivations are skipped — see _doDelete's comment
9072
+ * for why the asymmetry is correct.
9073
+ *
9074
+ * @internal
9075
+ */
9076
+ private dispatchArrayDerivationsOnDelete;
9077
+ /**
9078
+ * Mirror of {@link dispatchMaterializedViews} for the delete path
9079
+ * (#181). No record content is available (it's gone), so the
9080
+ * `_materializedFrom` skip used by the put-side dispatch doesn't
9081
+ * apply here — instead, the recursion guard is the `internal` gate
9082
+ * at the `_doDelete` call site above.
9083
+ *
9084
+ * @internal
9085
+ */
9086
+ private dispatchMaterializedViewsOnDelete;
7039
9087
  /**
7040
9088
  * List all records in the collection.
7041
9089
  *
@@ -7110,6 +9158,15 @@ declare class Collection<T> {
7110
9158
  * the filtered records directly (the API). Prefer the chainable
7111
9159
  * form for new code.
7112
9160
  *
9161
+ * **Lazy-MV gap (#157):** `query()` is synchronous and does NOT
9162
+ * trigger lazy materialized-view resolve-on-read. If this
9163
+ * collection is a lazy MV's output and the MV is currently stale,
9164
+ * `query().toArray()` returns the pre-stale snapshot. To force a
9165
+ * fresh read on a lazy MV, either call `list()` (which DOES
9166
+ * trigger resolve) or `vault.refreshView(mvName)` before querying.
9167
+ * The proper fix — extending `QuerySource` with an async prepare
9168
+ * hook — is a separate PR.
9169
+ *
7113
9170
  * @example
7114
9171
  * ```ts
7115
9172
  * // New chainable API:
@@ -7262,6 +9319,11 @@ declare class Collection<T> {
7262
9319
  * .aggregate({ total: sum('amount'), n: count() })
7263
9320
  * ```
7264
9321
  *
9322
+ * **Lazy-MV gap (#157):** `scan()` is synchronous-build and does
9323
+ * NOT trigger lazy materialized-view resolve-on-read. For lazy
9324
+ * MVs, call `list()` (which DOES resolve) or `vault.refreshView(name)`
9325
+ * before scanning. Same shape as the `query()` limitation.
9326
+ *
7265
9327
  * Returns a `ScanBuilder<T>` instead of the raw async iterator
7266
9328
  * that previous versions used. The builder implements
7267
9329
  * `AsyncIterable<T>`, so every existing `for await … of` call
@@ -7280,6 +9342,22 @@ declare class Collection<T> {
7280
9342
  /** Decrypt a page of envelopes returned by `adapter.listPage`. */
7281
9343
  private decryptPage;
7282
9344
  /** Load all records from adapter into memory cache. */
9345
+ /**
9346
+ * @internal — refresh the in-memory cache entry for a single id by
9347
+ * re-reading from the adapter. Used by the transaction executor's
9348
+ * Phase-3 revert path: that path writes the prior envelope directly
9349
+ * via the raw store (to avoid re-firing Collection-level side
9350
+ * effects), which would otherwise leave this Collection's eager
9351
+ * cache holding the rolled-back value. After revert, the executor
9352
+ * calls this hook so subsequent `get` / `query` reads see the
9353
+ * actual on-disk state.
9354
+ *
9355
+ * Lazy mode: drops the LRU entry; the next `get` repopulates from
9356
+ * the adapter. Eager mode: re-reads the envelope and either sets
9357
+ * the cache entry (record still present) or deletes it (record was
9358
+ * gone before the tx and the revert deleted it again).
9359
+ */
9360
+ _invalidateCacheEntry(id: string): Promise<void>;
7283
9361
  private ensureHydrated;
7284
9362
  /** Hydrate from a pre-loaded snapshot (used by Vault). */
7285
9363
  hydrateFromSnapshot(records: Record<string, EncryptedEnvelope>): Promise<void>;
@@ -7704,7 +9782,7 @@ interface ShadowStrategy {
7704
9782
  * @internal
7705
9783
  */
7706
9784
  interface TxStrategy {
7707
- runTransaction<T>(db: Noydb, fn: (tx: TxContext) => Promise<T> | T): Promise<T>;
9785
+ runTransaction<T>(db: Noydb, fn: (tx: TxContext) => Promise<T> | T, options?: AmendmentTxOptions): Promise<T>;
7708
9786
  }
7709
9787
 
7710
9788
  /**
@@ -7834,6 +9912,244 @@ interface SessionStrategy {
7834
9912
  revokeAllSessions(): void;
7835
9913
  }
7836
9914
 
9915
+ /**
9916
+ * Managed-passphrase mode — issue #14, rubber-hose-resistant vaults.
9917
+ *
9918
+ * A vault mode where the passphrase is machine-generated and never
9919
+ * exposed to the user, sealed under a developer-provided
9920
+ * {@link SealingKeyProvider} (macOS Keychain, Windows Credential
9921
+ * Manager, libsecret, AWS KMS, …). The user has no secret to give
9922
+ * up to coercion — they can't reveal what they don't know.
9923
+ *
9924
+ * ## Components in this file
9925
+ *
9926
+ * - {@link SealingKeyProvider} — the interface concrete providers
9927
+ * implement. Provider implementations live OUTSIDE hub (per-
9928
+ * platform packages).
9929
+ * - {@link MemorySealingKeyProvider} — in-memory test provider; uses
9930
+ * a deterministic per-instance "key" so two providers with
9931
+ * different ids cannot unseal each other's outputs.
9932
+ * - {@link RecipientHint} — public material a sender uses to seal
9933
+ * plaintext for a specific recipient; published by
9934
+ * {@link RecipientSealer.publishRecipientHint} and transported
9935
+ * out-of-band to the sender before bundle writes.
9936
+ * - {@link RecipientSealer} — interface for asymmetric/granted
9937
+ * providers that support recipient-target sealing (RSA-OAEP,
9938
+ * cloud-KMS asymmetric, etc.); distinct from self-only
9939
+ * {@link SealingKeyProvider} (macOS Keychain, WebAuthn-PRF).
9940
+ * - {@link MemoryRecipientSealer} — in-process reference
9941
+ * implementation of both `RecipientSealer` and
9942
+ * `SealingKeyProvider` using real WebCrypto RSA-OAEP + AES-GCM;
9943
+ * safe for tests and same-process sender/recipient scenarios.
9944
+ * - {@link loadSealedPassphrase} / {@link saveSealedPassphrase} —
9945
+ * plaintext envelope storage at `_meta/sealed-passphrase`.
9946
+ * Mirrors the `_meta/handle` and `_meta/public-envelope` AES-
9947
+ * GCM-bypassed patterns. The sealing layer (provider's job)
9948
+ * is the security boundary; hub doesn't have a key to encrypt
9949
+ * with at this layer — that's the whole point of the design.
9950
+ * - {@link resolveManagedSecret} — orchestrates the "generate +
9951
+ * seal + persist on first open; unseal on reopen" flow.
9952
+ * Returns the plaintext passphrase string that the rest of the
9953
+ * `createNoydb` keyring path consumes.
9954
+ *
9955
+ * Slice 1 of #14. Deferred to follow-ups:
9956
+ * - Block `rotate-passphrase` policy gate under managed mode.
9957
+ * - Mandatory strong-recovery enforcement (depends on #10).
9958
+ * - Recovery flow under managed mode (generates fresh sealed phrase).
9959
+ *
9960
+ * @see docs/subsystems/session-tiers.md → Managed-passphrase mode
9961
+ *
9962
+ * @module
9963
+ */
9964
+
9965
+ /**
9966
+ * The contract concrete providers (per-platform key stores) implement
9967
+ * to seal and unseal a hub-generated random passphrase. The plaintext
9968
+ * passphrase NEVER leaves hub-controlled memory in unsealed form —
9969
+ * the provider receives the bytes, returns opaque sealed bytes, and
9970
+ * later reverses the operation. Hub treats the sealed bytes as
9971
+ * fully opaque.
9972
+ *
9973
+ * Implementations live OUTSIDE `@noy-db/hub` (separate packages
9974
+ * per the issue's "Concrete providers (live outside hub)" note):
9975
+ *
9976
+ * | Platform | Package (TBD) | Backing |
9977
+ * |---|---|---|
9978
+ * | macOS | `@noy-db/seal-macos-keychain` | Security.framework |
9979
+ * | Windows | `@noy-db/seal-wincred` | Credential Manager |
9980
+ * | Linux | `@noy-db/seal-libsecret` | libsecret / secret-service |
9981
+ * | Cloud / server | `@noy-db/seal-aws-kms` | AWS KMS Decrypt |
9982
+ */
9983
+ interface SealingKeyProvider {
9984
+ /**
9985
+ * Non-sensitive identifier disclosed in the persisted envelope.
9986
+ * Surfaced to consumers via `loadSealedPassphrase().providerId` so
9987
+ * a vault opened with the wrong provider class can detect the
9988
+ * mismatch and surface a clear error. NOT secret — fine to log.
9989
+ *
9990
+ * Suggested format: `<family>:<scope>` — e.g. `macos-keychain:com.acme.app`,
9991
+ * `aws-kms:arn:aws:kms:us-east-1:123:key/abc`. The hub never
9992
+ * parses this; it's purely audit metadata.
9993
+ */
9994
+ readonly id: string;
9995
+ /** Seal raw passphrase bytes. Output bytes are opaque to hub. */
9996
+ seal(passphrase: Uint8Array): Promise<Uint8Array>;
9997
+ /**
9998
+ * Reverse {@link seal}. MUST throw on tamper, wrong-provider, or
9999
+ * any other failure — hub treats a thrown error as "this provider
10000
+ * cannot unlock this vault" and surfaces it to the caller.
10001
+ */
10002
+ unseal(sealed: Uint8Array): Promise<Uint8Array>;
10003
+ }
10004
+ /**
10005
+ * In-memory test provider. NOT secure — uses a deterministic
10006
+ * per-instance "key" (16-byte SHA-256 of `id`) XOR'd over the
10007
+ * passphrase plus a 4-byte provider-id fingerprint prefix. The XOR is
10008
+ * sufficient to make different `id` values produce mutually-unsealable
10009
+ * outputs (the contract tests for that), but offers ZERO real
10010
+ * confidentiality — never use outside tests.
10011
+ *
10012
+ * Replace with a real platform provider in production.
10013
+ */
10014
+ declare class MemorySealingKeyProvider implements SealingKeyProvider {
10015
+ readonly id: string;
10016
+ private readonly fingerprint;
10017
+ private readonly keyBytes;
10018
+ constructor(opts: {
10019
+ id: string;
10020
+ });
10021
+ seal(passphrase: Uint8Array): Promise<Uint8Array>;
10022
+ unseal(sealed: Uint8Array): Promise<Uint8Array>;
10023
+ }
10024
+ /**
10025
+ * Public material a sender uses to seal-for-this-recipient. Published by
10026
+ * a recipient's RecipientSealer; transported to the sender out-of-band
10027
+ * (email, S3, in-app message). The sender obtains the hint, supplies it
10028
+ * to writeNoydbBundle's sealedCredentials.perUser[userId].hint, and the
10029
+ * hub seals each user's credential against it. Per foundation §11.4.
10030
+ */
10031
+ type RecipientHint = {
10032
+ readonly v: 1;
10033
+ /** Recipient's provider id; matches the SealedAutoUnlockEntry.pid they'll unseal under. */
10034
+ readonly pid: string;
10035
+ /** Algorithm the sender uses to produce the seal. Slice 1 ships RSA-OAEP-SHA256 only. */
10036
+ readonly alg: 'rsa-oaep-sha256';
10037
+ /** Public material — alg-specific. For 'rsa-oaep-sha256': { publicKeyPem: string }. */
10038
+ readonly material: Readonly<Record<string, unknown>>;
10039
+ };
10040
+ /**
10041
+ * Handover-capable provider. Implemented additionally by asymmetric/granted
10042
+ * providers (cloud-KMS asymmetric, Azure RSA Key Vault, AWS KMS with grant).
10043
+ * Self-only providers (macOS Keychain, env-var, WebAuthn-PRF) do NOT
10044
+ * implement this — the §11.2 capability matrix lives in the type system.
10045
+ *
10046
+ * Per foundation §11.4. A function that requires recipient-target sealing
10047
+ * takes `RecipientSealer`, not `SealingKeyProvider` — the compiler rejects
10048
+ * passing a self-only provider at the spec site.
10049
+ */
10050
+ interface RecipientSealer {
10051
+ readonly id: string;
10052
+ /** Produce hint material a sender uses to seal-for-this-recipient. */
10053
+ publishRecipientHint(): Promise<RecipientHint>;
10054
+ /**
10055
+ * Seal plaintext for the recipient described by `hint`. Returns opaque
10056
+ * bytes — same contract as `SealingKeyProvider.seal()`. The bundle
10057
+ * layer base64-encodes the bytes into `SealedAutoUnlockEntry.sealed`
10058
+ * without inspecting them.
10059
+ */
10060
+ sealForRecipient(plaintext: Uint8Array, hint: RecipientHint): Promise<Uint8Array>;
10061
+ }
10062
+ /**
10063
+ * Reference implementation of `RecipientSealer` + `SealingKeyProvider`.
10064
+ * Uses WebCrypto RSA-OAEP-SHA256 (2048-bit) to wrap a fresh 32-byte
10065
+ * AES-GCM CEK, AES-GCM-encrypts plaintext under it, and packs the
10066
+ * result into a self-describing TLV:
10067
+ *
10068
+ * byte 0 : version (0x01)
10069
+ * bytes 1..256 : RSA-OAEP-wrapped CEK (fixed 256 bytes at RSA-2048)
10070
+ * bytes 257..268: AES-GCM IV (12 bytes)
10071
+ * bytes 269.. : AES-GCM ciphertext ‖ 16-byte tag
10072
+ *
10073
+ * Implements BOTH interfaces. `seal(plaintext)` (self-target) is just
10074
+ * `sealForRecipient(plaintext, this own hint)` — same TLV. Convenient
10075
+ * for tests where one provider plays both ends. Real cloud providers
10076
+ * (`at-aws-kms`, etc.) will pick their own internal layouts; the only
10077
+ * contract is round-trip identity.
10078
+ *
10079
+ * SAFE for production within its scope — the cryptography is real
10080
+ * (RSA-OAEP + AES-GCM via WebCrypto), but the keypair lives in-process
10081
+ * and is regenerated on every construction. Not suitable as a managed
10082
+ * keychain; use it for tests and for shipping bundles where the
10083
+ * recipient instance lives in the same process as the sender (rare).
10084
+ */
10085
+ declare class MemoryRecipientSealer implements SealingKeyProvider, RecipientSealer {
10086
+ readonly id: string;
10087
+ private readonly keypair;
10088
+ constructor(opts: {
10089
+ id: string;
10090
+ });
10091
+ publishRecipientHint(): Promise<RecipientHint>;
10092
+ sealForRecipient(plaintext: Uint8Array, hint: RecipientHint): Promise<Uint8Array>;
10093
+ seal(plaintext: Uint8Array): Promise<Uint8Array>;
10094
+ unseal(bytes: Uint8Array): Promise<Uint8Array>;
10095
+ }
10096
+ /** Reserved id for the managed-passphrase envelope under `_meta`. */
10097
+ declare const SEALED_PASSPHRASE_RECORD_ID: "sealed-passphrase";
10098
+ /** Plaintext payload stored inside the `_meta/sealed-passphrase` envelope. */
10099
+ interface SealedPassphrase {
10100
+ readonly _noydb_sealed: 1;
10101
+ readonly providerId: string;
10102
+ /** Sealed bytes. Base64-encoded on the wire; decoded on load. */
10103
+ readonly sealed: Uint8Array;
10104
+ }
10105
+ /**
10106
+ * Wire-format envelope persisted at `_meta/sealed-passphrase` for
10107
+ * managed-mode vaults. The provider produces raw sealed bytes via
10108
+ * {@link SealingKeyProvider.seal}; this wrapper carries the dispatch
10109
+ * metadata hub needs to pick the right provider on the unseal path.
10110
+ *
10111
+ * Stability boundary: once shipped, the wire format only grows by
10112
+ * adding optional fields. See the at-* sealing dimension foundation
10113
+ * doc, §11.9.1.
10114
+ *
10115
+ * v1 shape (this release): `{ v: 1, _noydb_sealed: 1, pid, payload }`.
10116
+ *
10117
+ * Legacy shape (pre.14, pre.15): `{ _noydb_sealed: 1, providerId, sealed }`
10118
+ * — accepted on read for backwards compatibility; never produced on
10119
+ * write going forward.
10120
+ */
10121
+ interface SealedEnvelope {
10122
+ /** Envelope schema version. v1 is the shape shipped in pre.16. */
10123
+ readonly v: 1;
10124
+ /** Magic marker for forensics + legacy-shape detection. */
10125
+ readonly _noydb_sealed: 1;
10126
+ /** Matches the producing provider's `.id`. Dispatch key on unseal. */
10127
+ readonly pid: string;
10128
+ /** Sealed bytes from the provider, base64-encoded on the wire. */
10129
+ readonly payload: string;
10130
+ }
10131
+ /**
10132
+ * Parse a `_meta/sealed-passphrase` `_data` JSON string into the
10133
+ * in-memory {@link SealedPassphrase} representation. Accepts both:
10134
+ *
10135
+ * 1. v1 wire format `{ v: 1, _noydb_sealed: 1, pid, payload }` —
10136
+ * the shape produced from pre.16 onward.
10137
+ * 2. Legacy wire format `{ _noydb_sealed: 1, providerId, sealed }` —
10138
+ * the shape produced in pre.14/pre.15. Read-only; never written
10139
+ * going forward.
10140
+ *
10141
+ * Returns `undefined` for any input that doesn't match either shape,
10142
+ * so callers can fall back to "no managed-mode envelope present."
10143
+ *
10144
+ * @internal — exported only for the migration safety-net test suite.
10145
+ */
10146
+ declare function parseSealedEnvelope(raw: unknown): SealedPassphrase | undefined;
10147
+ declare function saveSealedPassphrase(store: NoydbStore, vault: string, payload: {
10148
+ readonly providerId: string;
10149
+ readonly sealed: Uint8Array;
10150
+ }): Promise<void>;
10151
+ declare function loadSealedPassphrase(store: NoydbStore, vault: string): Promise<SealedPassphrase | undefined>;
10152
+
7837
10153
  /**
7838
10154
  * Core types — the {@link NoydbStore} interface, envelope format, roles, and
7839
10155
  * all configuration shapes consumed by {@link createNoydb}.
@@ -7892,7 +10208,7 @@ type Permission = 'rw' | 'ro';
7892
10208
  * `'*'` is the wildcard collection matching all collections in the vault.
7893
10209
  */
7894
10210
  type Permissions = Record<string, Permission>;
7895
- /** The encrypted wrapper stored by adapters. Adapters only ever see this. */
10211
+ /** The encrypted wrapper stored by stores. Stores only ever see this. */
7896
10212
  interface EncryptedEnvelope {
7897
10213
  readonly _noydb: typeof NOYDB_FORMAT_VERSION;
7898
10214
  readonly _v: number;
@@ -7995,8 +10311,8 @@ interface ListPageResult {
7995
10311
  }
7996
10312
  interface NoydbStore {
7997
10313
  /**
7998
- * Optional human-readable adapter name (e.g. 'memory', 'file', 'dynamo').
7999
- * Used in diagnostic messages and the listPage fallback warning. Adapters
10314
+ * Optional human-readable store name (e.g. 'memory', 'file', 'dynamo').
10315
+ * Used in diagnostic messages and the listPage fallback warning. Stores
8000
10316
  * are encouraged to set this so logs are clearer about which backend is
8001
10317
  * involved when something goes wrong.
8002
10318
  */
@@ -8017,22 +10333,22 @@ interface NoydbStore {
8017
10333
  ping?(): Promise<boolean>;
8018
10334
  /**
8019
10335
  * Optional: list record IDs in a collection that have `_ts` after `since`.
8020
- * Used by partial sync (`pull({ modifiedSince })`). Adapters that omit this
10336
+ * Used by partial sync (`pull({ modifiedSince })`). Stores that omit this
8021
10337
  * fall back to a full `loadAll` + client-side timestamp filter.
8022
10338
  */
8023
10339
  listSince?(vault: string, collection: string, since: string): Promise<string[]>;
8024
10340
  /**
8025
- * Optional pagination extension. Adapters that implement `listPage` get
8026
- * the streaming `Collection.scan()` fast path; adapters that don't are
10341
+ * Optional pagination extension. Stores that implement `listPage` get
10342
+ * the streaming `Collection.scan()` fast path; stores that don't are
8027
10343
  * silently fallen back to a full `loadAll()` + slice (with a one-time
8028
10344
  * console.warn).
8029
10345
  *
8030
- * `cursor` is opaque to the core — each adapter encodes its own paging
10346
+ * `cursor` is opaque to the core — each store encodes its own paging
8031
10347
  * state (DynamoDB: base64 LastEvaluatedKey JSON; S3: ContinuationToken;
8032
10348
  * memory/file/browser: numeric offset of a sorted id list). Pass
8033
10349
  * `undefined` to start from the beginning.
8034
10350
  *
8035
- * `limit` is a soft upper bound on `items.length`. Adapters MAY return
10351
+ * `limit` is a soft upper bound on `items.length`. Stores MAY return
8036
10352
  * fewer items even when more exist (e.g. if the underlying store has
8037
10353
  * its own page size cap), and MUST signal "no more pages" by returning
8038
10354
  * `nextCursor: null`.
@@ -8245,7 +10561,7 @@ type RecoveryEnrollment = {
8245
10561
  * metadata only.
8246
10562
  */
8247
10563
  interface KeyringAuthenticatorBase {
8248
- /** Caller-chosen identifier — e.g. `'webauthn-yubikey-blue'`, `'oidc-google'`, `'password-daily'`. */
10564
+ /** Caller-chosen identifier — e.g. `'webauthn-yubikey-blue'`, `'oidc-google'`, `'password'`. */
8249
10565
  readonly id: string;
8250
10566
  /** Method family — selects which `@noy-db/on-*` package handles unlock. */
8251
10567
  readonly method: 'webauthn' | 'oidc' | 'password';
@@ -8330,6 +10646,21 @@ interface KeyringFile {
8330
10646
  readonly salt: string;
8331
10647
  readonly created_at: string;
8332
10648
  readonly granted_by: string;
10649
+ /**
10650
+ * Passphrase canary — base64 AES-KW-wrapped form of a known constant
10651
+ * 256-bit value, wrapped under the keyring's KEK (#113).
10652
+ *
10653
+ * Optional: pre-#113 keyrings load with no canary and fall back to
10654
+ * the multi-DEK corruption heuristic from #82. Keyrings written after
10655
+ * #113 carry one and let `loadKeyring` distinguish wrong-passphrase
10656
+ * from corruption even when ALL DEKs (including a single-DEK keyring's
10657
+ * sole DEK) are corrupted.
10658
+ *
10659
+ * AES-KW is deterministic — every write site mints fresh on each
10660
+ * persist; same KEK + same constant input always produces the same
10661
+ * ciphertext, so this round-trips without state.
10662
+ */
10663
+ readonly canary?: string;
8333
10664
  /**
8334
10665
  * Tier-2 authenticator slots (multi-slot keyring extension).
8335
10666
  * Optional / append-only: keyring files written before the
@@ -8573,7 +10904,7 @@ interface PullOptions {
8573
10904
  collections?: string[];
8574
10905
  /**
8575
10906
  * Only pull records with `_ts` strictly after this ISO timestamp.
8576
- * Adapters that implement `listSince` use it directly; others fall back
10907
+ * Stores that implement `listSince` use it directly; others fall back
8577
10908
  * to a full scan with client-side filtering.
8578
10909
  */
8579
10910
  modifiedSince?: string;
@@ -8737,6 +11068,11 @@ interface GrantOptions {
8737
11068
  * keyring put (same concurrency story as `db.grant` / `db.revoke`).
8738
11069
  *
8739
11070
  * Top-level fields are partial-merge: absent fields are not modified.
11071
+ * `null` on `displayName` clears the field (stored as the empty string;
11072
+ * UI consumers typically render the empty case by falling back to the
11073
+ * user id). `undefined` / absent leaves the field untouched. Mirrors
11074
+ * the `null`-as-clear convention `UserApi.updateMe` uses (#57).
11075
+ *
8740
11076
  * `permissions`, however, is a **full replacement** at the map level —
8741
11077
  * passing `{ invoices: 'rw' }` REPLACES the entire permissions map,
8742
11078
  * silently dropping any other entries. To partially update, read the
@@ -8755,7 +11091,7 @@ interface GrantOptions {
8755
11091
  interface UpdateUserOptions {
8756
11092
  readonly userId: string;
8757
11093
  readonly role?: Role;
8758
- readonly displayName?: string;
11094
+ readonly displayName?: string | null;
8759
11095
  readonly permissions?: Permissions;
8760
11096
  }
8761
11097
  interface RevokeOptions {
@@ -8798,8 +11134,8 @@ interface AccessibleVault {
8798
11134
  */
8799
11135
  interface ListAccessibleVaultsOptions {
8800
11136
  /**
8801
- * Minimum role the caller must hold to include a compartment in the
8802
- * result. Compartments where the caller's role is strictly *below*
11137
+ * Minimum role the caller must hold to include a vault in the
11138
+ * result. Vaults where the caller's role is strictly *below*
8803
11139
  * this threshold are silently excluded. Defaults to `'client'`,
8804
11140
  * which means "every vault I can unwrap is returned." Set to
8805
11141
  * `'admin'` for "vaults where I can grant/revoke," or
@@ -9326,6 +11662,37 @@ interface NoydbOptions {
9326
11662
  * @internal
9327
11663
  */
9328
11664
  readonly syncStrategy?: SyncStrategy;
11665
+ /**
11666
+ * Optional guard strategies — collection-level write guards. Each
11667
+ * handle is the output of `withGuard()` from `@noy-db/hub/guards`.
11668
+ * Multiple guards per collection are allowed; they are dispatched
11669
+ * in registration order on `collection.put()`.
11670
+ */
11671
+ readonly guardStrategies?: ReadonlyArray<GuardStrategyHandleAny>;
11672
+ /**
11673
+ * Optional derivation strategies — source-to-output projections that
11674
+ * fire on `collection.put()`. Each handle is the output of
11675
+ * `withDerivation()` from `@noy-db/hub/derivations`. The vault
11676
+ * validates the derivation graph for cycles on `openVault`; a cyclic
11677
+ * graph throws `DerivationCycleError`.
11678
+ */
11679
+ readonly derivationStrategies?: ReadonlyArray<DerivationStrategyHandle>;
11680
+ /**
11681
+ * Optional materialized-view strategies (#143, foundation in #150).
11682
+ * Each handle returned by `withMaterializedView()` from
11683
+ * `@noy-db/hub/materialized-views`. The vault runs unified cycle
11684
+ * detection across the MV + derivation graphs at `openVault`; a
11685
+ * cyclic graph throws `MaterializedViewCycleError`.
11686
+ */
11687
+ readonly materializedViewStrategies?: ReadonlyArray<MaterializedViewStrategyHandle>;
11688
+ /**
11689
+ * Optional overlay strategies (#154). Each handle returned by
11690
+ * `withOverlayedView()` from `@noy-db/hub/overlay-views`. The vault
11691
+ * validates name uniqueness + base concreteness + overlay
11692
+ * availability at `openVault`; a clash throws one of the
11693
+ * `Overlay*Error` family.
11694
+ */
11695
+ readonly overlayedViewStrategies?: ReadonlyArray<OverlayedViewStrategyHandle>;
9329
11696
  /** Optional remote store(s) for sync. Accepts a single store, a SyncTarget, or an array. */
9330
11697
  readonly sync?: NoydbStore | SyncTarget | SyncTarget[];
9331
11698
  /** User identifier. */
@@ -9371,6 +11738,32 @@ interface NoydbOptions {
9371
11738
  * subsequent sessions.
9372
11739
  */
9373
11740
  readonly getKeyring?: (vault: string) => Promise<UnlockedKeyring>;
11741
+ /**
11742
+ * Passphrase mode (#14). Default `'standard'`.
11743
+ *
11744
+ * - `'standard'` — the legacy flow. `secret` supplies the
11745
+ * plaintext passphrase, the user knows it, and the policy gate
11746
+ * `rotate-passphrase` is enabled.
11747
+ * - `'managed'` — rubber-hose-resistant mode. Hub generates a
11748
+ * 256-bit random passphrase at first open and seals it under
11749
+ * the provided `sealingKey`. The user never sees or types the
11750
+ * passphrase, defeating the $5-wrench attack. Mutually
11751
+ * exclusive with `secret` and `getKeyring`.
11752
+ *
11753
+ * @see docs/subsystems/session-tiers.md → Managed-passphrase mode
11754
+ */
11755
+ readonly passphraseMode?: 'standard' | 'managed';
11756
+ /**
11757
+ * Provider that seals/unseals the auto-generated managed-mode
11758
+ * passphrase. Required when `passphraseMode === 'managed'`; ignored
11759
+ * otherwise. Implementations live in per-platform packages
11760
+ * (`@noy-db/seal-macos-keychain`, `@noy-db/seal-wincred`,
11761
+ * `@noy-db/seal-libsecret`, `@noy-db/seal-aws-kms`, …).
11762
+ */
11763
+ readonly sealingKey?: SealingKeyProvider;
11764
+ /** Required to use `profile: 'shamir'` recovery. Pass
11765
+ * `shamirRecoveryProvider()` from `@noy-db/on-shamir`. */
11766
+ readonly shamirRecovery?: ShamirRecoveryProvider;
9374
11767
  /** Auth method. Default: 'passphrase'. */
9375
11768
  readonly auth?: 'passphrase' | 'biometric';
9376
11769
  /** Enable encryption. Default: true. */
@@ -9574,4 +11967,4 @@ interface DeleteManyResult {
9574
11967
  }>;
9575
11968
  }
9576
11969
 
9577
- export { type ConsentAuditEntry as $, type BlobObject as A, type BlobStrategy as B, type BlobPutOptions as C, DICT_COLLECTION_PREFIX as D, type BlobResponseOptions as E, BlobSet as F, type BlobStrategyOpenArgs as G, type CompactRunOptions as H, type I18nStrategy as I, type CompactionContext as J, type CompactionResult as K, DEFAULT_CHUNK_SIZE as L, EXPORT_AUDIT_COLLECTION as M, ExportBlobsAbortedError as N, type ExportBlobsAuditEntry as O, PolicyEnforcer as P, type ExportBlobsHandle as Q, type ExportBlobsOptions as R, type SessionStrategy as S, type ExportedBlob as T, type SlotInfo as U, type SlotRecord as V, type VersionRecord as W, createExportBlobsHandle as X, runCompaction as Y, type ConsentStrategy as Z, CONSENT_AUDIT_COLLECTION as _, type DictEntry as a, type BuiltInGateName as a$, type ConsentAuditFilter as a0, type ConsentContext as a1, type ConsentOp as a2, loadConsentEntries as a3, writeConsentEntry as a4, type PeriodsStrategy as a5, type CarryForwardContext as a6, type ClosePeriodOptions as a7, type OpenPeriodOptions as a8, PERIODS_COLLECTION as a9, type DiffEntry as aA, type JsonPatch as aB, type JsonPatchOp as aC, type LedgerEntry as aD, LedgerStore as aE, type VaultEngine as aF, VaultInstant as aG, type VerifyResult as aH, applyPatch as aI, canonicalJson as aJ, computePatch as aK, diff as aL, formatDiff as aM, hashEntry as aN, paddedIndex as aO, parseIndex as aP, sha256Hex as aQ, type UserEnvelope as aR, type PublicEnvelope as aS, type GateName as aT, type GatePolicy as aU, type VaultPolicy as aV, type ActiveTier as aW, type FactorProof as aX, Vault as aY, type AccessibleVault as aZ, BUNDLE_STORE_POLICY as a_, type PeriodRecord as aa, type ReadOnlyCollection as ab, appendPeriodLedgerEntry as ac, assertTsWritable as ad, chainAnchor as ae, loadPeriods as af, validatePeriodName as ag, type ShadowStrategy as ah, CollectionFrame as ai, VaultFrame as aj, type TxStrategy as ak, TxCollection as al, TxContext as am, TxVault as an, runTransaction as ao, type SyncStrategy as ap, type Role as aq, type UnlockedKeyring as ar, type HistoryStrategy as as, type NoydbStore as at, type HistoryOptions as au, type EncryptedEnvelope as av, type PruneOptions as aw, type AppendInput as ax, type ChangeType as ay, CollectionInstant as az, type DictKeyDescriptor as b, type Permission as b$, type BundleRecipient as b0, type CacheOptions as b1, type CacheStats as b2, type ChangeEvent as b3, Collection as b4, type CollectionChangeEvent as b5, type CollectionConflictResolver as b6, type Conflict as b7, type ConflictPolicy as b8, type ConflictStrategy as b9, type KeyringAuthenticator as bA, type KeyringFile as bB, type ListAccessibleVaultsOptions as bC, type ListPageResult as bD, type LiveUserEnvelope as bE, type LocaleReadOptions as bF, Lru as bG, type LruOptions as bH, type LruStats as bI, MAGIC_LINK_CONTENT_INFO_PREFIX as bJ, MAGIC_LINK_GRANTS_COLLECTION as bK, MAGIC_LINK_KEK_INFO_PREFIX as bL, type MagicLinkGrantPayload as bM, type MagicLinkGrantRecord as bN, NOYDB_BACKUP_VERSION as bO, NOYDB_FORMAT_VERSION as bP, NOYDB_KEYRING_VERSION as bQ, NOYDB_SYNC_VERSION as bR, Noydb as bS, type NoydbBundleStore as bT, type NoydbEventMap as bU, type NoydbOptions as bV, PUBLIC_ENVELOPE_FIELDS as bW, type PaperRecoveryDoc as bX, type PaperRecoveryEntry as bY, type PassphrasePolicy as bZ, type PassphraseValidationResult as b_, type CrossTierAccessEvent as ba, DEFAULT_PUBLIC_ENVELOPE_SCHEMA as bb, DELEGATIONS_COLLECTION as bc, type DeepPartial as bd, type DeepPartialOrNull as be, type DelegationToken as bf, type DeleteManyResult as bg, type DirtyEntry as bh, ELEVATION_AUDIT_COLLECTION as bi, ElevatedHandle as bj, type EnrollAuthenticatorOptions as bk, type ExportCapability as bl, type ExportChunk as bm, type ExportFormat as bn, type ExportStreamOptions as bo, type FactorKind as bp, type FactorRequirement as bq, type GhostRecord as br, type GrantOptions as bs, type HistoryConfig as bt, type HistoryEntry as bu, INDEXED_STORE_POLICY as bv, type ImportCapability as bw, type InferOutput as bx, type IssueDelegationOptions as by, type IssueMagicLinkGrantOptions as bz, DictionaryHandle as c, type UserInfo as c$, type Permissions as c0, type PlaintextTranslatorContext as c1, type PlaintextTranslatorFn as c2, PresenceHandle as c3, type PresencePeer as c4, type PublicEnvelopeField as c5, type PublicEnvelopeSchema as c6, type PublicEnvelopeText as c7, type PullMode as c8, type PullOptions as c9, type StandardSchemaV1Issue as cA, type StandardSchemaV1SyncResult as cB, type StoreAuth as cC, type StoreAuthKind as cD, type StoreCapabilities as cE, SyncEngine as cF, type SyncMetadata as cG, type SyncPolicy as cH, SyncScheduler as cI, type SyncSchedulerStatus as cJ, type SyncStatus as cK, type SyncTarget as cL, type SyncTargetRole as cM, SyncTransaction as cN, type SyncTransactionResult as cO, type TierMode as cP, type TranslatorAuditEntry as cQ, type TxOp as cR, USER_ENVELOPE_COLLECTION as cS, USER_ENVELOPE_MAX_BYTES as cT, type Unsubscribe as cU, type UpdateAuthenticatorOptions as cV, type UpdateUserOptions as cW, UserApi as cX, type UserEnvelopeCheckGate as cY, UserEnvelopeOversizedError as cZ, type UserEnvelopePresented as c_, type PullPolicy as ca, type PullResult as cb, type PushMode as cc, type PushOptions as cd, type PushPolicy as ce, type PushResult as cf, type PutManyItemOptions as cg, type PutManyOptions as ch, type PutManyResult as ci, type QueryAcrossOptions as cj, type QueryAcrossResult as ck, type QuickUnlockState as cl, QuickUnlockStore as cm, type ReAuthOperation as cn, type RecoverPassphraseInput as co, type RecoverPassphraseResult as cp, type RecoverUserOptions as cq, type RecoveryProof as cr, type ResolvedPublicEnvelopeSchema as cs, type RevokeOptions as ct, type RotatePassphraseInput as cu, type SessionPolicy as cv, type SetPublicEnvelopeInput as cw, type SlotRewrapCeremony as cx, type SlotRewrapContext as cy, type StandardSchemaV1 as cz, type DictionaryOptions as d, type VaultBackup as d0, type VaultPolicyOnDisk as d1, type VaultSnapshot as d2, type WarningRules as d3, WeakPassphraseError as d4, type WeakPassphraseReason as d5, type WrappedDeksBlob as d6, assertStrongPassphrase as d7, buildRecipientKeyringFile as d8, burnPaperRecoveryEntry as d9, recoverUser as dA, removeAuthenticator as dB, resolveSchema as dC, revokeDelegation as dD, revokeMagicLinkGrant as dE, savePaperRecoveryEntries as dF, unwrapDeksFromBlob as dG, unwrapDeksFromPaperEntry as dH, unwrapMagicLinkGrant as dI, validatePassphrase as dJ, validatePublicEnvelopeInput as dK, validateSchemaInput as dL, validateSchemaOutput as dM, writeMagicLinkGrant as dN, createNoydb as da, createStore as db, deriveMagicLinkContentKey as dc, enrollAuthenticator as dd, estimateEntropy as de, evaluateExportCapability as df, evaluateImportCapability as dg, findAuthenticator as dh, hasExportCapability as di, hasImportCapability as dj, hasRecoveryEnrolled as dk, isMagicLinkGrantExpired as dl, isPublicEnvelope as dm, issueDelegation as dn, recoverPassphrase as dp, rotatePassphrase as dq, listMagicLinkGrants as dr, listUsers as ds, listUsersWithEnvelopes as dt, loadActiveDelegations as du, loadPaperRecoveryEntries as dv, magicLinkGrantRecordId as dw, mintPaperRecoveryEntry as dx, mintWrappedDeksBlob as dy, readMagicLinkGrantRecord as dz, type I18nTextDescriptor as e, type I18nTextOptions as f, applyI18nLocale as g, dictCollectionName as h, dictKey as i, i18nText as j, isDictCollectionName as k, isDictKeyDescriptor as l, isI18nTextDescriptor as m, createEnforcer as n, validateSessionPolicy as o, BLOB_CHUNKS_COLLECTION as p, BLOB_COLLECTION as q, resolveI18nText as r, BLOB_EVICTION_AUDIT_COLLECTION as s, BLOB_INDEX_COLLECTION as t, BLOB_SLOTS_PREFIX as u, validateI18nTextValue as v, BLOB_VERSIONS_PREFIX as w, type BlobEvictionEntry as x, type BlobFieldPolicy as y, type BlobFieldsConfig as z };
11970
+ export { type ConsentAuditEntry as $, type BlobObject as A, type BlobStrategy as B, type BlobPutOptions as C, DICT_COLLECTION_PREFIX as D, type BlobResponseOptions as E, BlobSet as F, type BlobStrategyOpenArgs as G, type CompactRunOptions as H, type I18nStrategy as I, type CompactionContext as J, type CompactionResult as K, DEFAULT_CHUNK_SIZE as L, EXPORT_AUDIT_COLLECTION as M, ExportBlobsAbortedError as N, type ExportBlobsAuditEntry as O, PolicyEnforcer as P, type ExportBlobsHandle as Q, type ExportBlobsOptions as R, type SessionStrategy as S, type ExportedBlob as T, type SlotInfo as U, type SlotRecord as V, type VersionRecord as W, createExportBlobsHandle as X, runCompaction as Y, type ConsentStrategy as Z, CONSENT_AUDIT_COLLECTION as _, type DictEntry as a, VaultInstant as a$, type ConsentAuditFilter as a0, type ConsentContext as a1, type ConsentOp as a2, loadConsentEntries as a3, writeConsentEntry as a4, type PeriodsStrategy as a5, type CarryForwardContext as a6, type ClosePeriodOptions as a7, type OpenPeriodOptions as a8, PERIODS_COLLECTION as a9, type DerivationStrategyHandle as aA, type DerivedFromMeta as aB, type OutputSpec as aC, type RecordOutputSpec as aD, type MaterializedViewStrategy as aE, type MaterializedViewStrategyHandle as aF, type OverlayedViewStrategy as aG, Collection as aH, OverlayedViewRegistry as aI, type OverlayedViewStrategyHandle as aJ, type SyncStrategy as aK, type Role as aL, type UnlockedKeyring as aM, type HistoryStrategy as aN, type NoydbStore as aO, type HistoryOptions as aP, type EncryptedEnvelope as aQ, type PruneOptions as aR, type AppendInput as aS, type ChangeType as aT, CollectionInstant as aU, type DiffEntry as aV, type JsonPatch as aW, type JsonPatchOp as aX, type LedgerEntry as aY, LedgerStore as aZ, type VaultEngine as a_, type PeriodRecord as aa, type ReadOnlyCollection as ab, appendPeriodLedgerEntry as ac, assertTsWritable as ad, chainAnchor as ae, loadPeriods as af, validatePeriodName as ag, type GuardStrategy as ah, type GuardChange as ai, type GuardContext as aj, GuardRegistry as ak, type GuardStrategyHandle as al, ReadOnlyVaultFacade as am, type ShadowStrategy as an, CollectionFrame as ao, VaultFrame as ap, type TxStrategy as aq, type AmendmentTxOptions as ar, TxCollection as as, TxContext as at, TxVault as au, runTransaction as av, type DerivationStrategy as aw, type DerivationContext as ax, type ArrayOutputSpec as ay, DerivationRegistry as az, type DictKeyDescriptor as b, type ExportChunk as b$, type VerifyResult as b0, applyPatch as b1, canonicalJson as b2, computePatch as b3, diff as b4, formatDiff as b5, hashEntry as b6, paddedIndex as b7, parseIndex as b8, sha256Hex as b9, type CacheOptions as bA, type CacheStats as bB, type ChangeEvent as bC, type CollectionChangeEvent as bD, type CollectionConflictResolver as bE, type CollectionDescriptor as bF, type CollectionStats as bG, type Conflict as bH, type ConflictPolicy as bI, type ConflictStrategy as bJ, type CrossTierAccessEvent as bK, DEFAULT_PUBLIC_ENVELOPE_SCHEMA as bL, DELEGATIONS_COLLECTION as bM, type DeepPartial as bN, type DeepPartialOrNull as bO, type DelegationToken as bP, type DeleteManyResult as bQ, type DerivationDescriptor as bR, type DirtyEntry as bS, type DumpSchemaOptions as bT, ELEVATION_AUDIT_COLLECTION as bU, ElevatedHandle as bV, type EnrollAuthenticatorOptions as bW, type EnrollAuthenticatorWrappingDEKsOptions as bX, type EnrollAuthenticatorWrappingKEKOptions as bY, type EnrollRecoveryResult as bZ, type ExportCapability as b_, type PublicEnvelope as ba, type SealingKeyProvider as bb, type BundleRecipient as bc, type RecipientSealer as bd, type RecipientHint as be, Vault as bf, type RecoveryEnrollmentInput as bg, type ShamirRecoveryProvider as bh, type MVQueryContext as bi, type RegisteredMV as bj, MaterializedViewRegistry as bk, type MaterializedFromMeta as bl, type MaterializedViewOutput as bm, type UnionSource as bn, type UserEnvelope as bo, type GateName as bp, type GatePolicy as bq, type VaultPolicy as br, type ActiveTier as bs, type FactorProof as bt, type PersistedSchemaEnvelope as bu, type DirectoryConfig as bv, type UserVisibility as bw, type AccessibleVault as bx, BUNDLE_STORE_POLICY as by, type BuiltInGateName as bz, DictionaryHandle as c, type PullPolicy as c$, type ExportFormat as c0, type ExportStreamOptions as c1, type FactorKind as c2, type FactorProofBundle as c3, type FactorRequirement as c4, type FieldDescriptor as c5, type FieldSource as c6, type GhostRecord as c7, type GrantOptions as c8, type HistoryConfig as c9, MemorySealingKeyProvider as cA, NOYDB_BACKUP_VERSION as cB, NOYDB_FORMAT_VERSION as cC, NOYDB_KEYRING_VERSION as cD, NOYDB_SYNC_VERSION as cE, Noydb as cF, type NoydbBundleStore as cG, type NoydbEventMap as cH, type NoydbOptions as cI, type OverlayViewDescriptor as cJ, PUBLIC_ENVELOPE_FIELDS as cK, type PaperRecoveryDoc as cL, type PaperRecoveryEntry as cM, type PassphrasePolicy as cN, type PassphraseValidationResult as cO, type Permission as cP, type Permissions as cQ, type PersistedSchemaKind as cR, type PlaintextTranslatorContext as cS, type PlaintextTranslatorFn as cT, PresenceHandle as cU, type PresencePeer as cV, type PublicEnvelopeField as cW, type PublicEnvelopeSchema as cX, type PublicEnvelopeText as cY, type PullMode as cZ, type PullOptions as c_, type HistoryEntry as ca, INDEXED_STORE_POLICY as cb, type ImportCapability as cc, type InferOutput as cd, type InternalCollectionStats as ce, type IssueDelegationOptions as cf, type IssueMagicLinkGrantOptions as cg, type KeyringAuthenticator as ch, type KeyringAuthenticatorWrappingDEKs as ci, type KeyringAuthenticatorWrappingKEK as cj, type KeyringFile as ck, type ListAccessibleVaultsOptions as cl, type ListPageResult as cm, type ListUsersOptions as cn, type LiveUserEnvelope as co, type LocaleReadOptions as cp, Lru as cq, type LruOptions as cr, type LruStats as cs, MAGIC_LINK_CONTENT_INFO_PREFIX as ct, MAGIC_LINK_GRANTS_COLLECTION as cu, MAGIC_LINK_KEK_INFO_PREFIX as cv, type MagicLinkGrantPayload as cw, type MagicLinkGrantRecord as cx, type MaterializedViewDescriptor as cy, MemoryRecipientSealer as cz, type DictionaryOptions as d, type VaultSchemaSnapshot as d$, type PullResult as d0, type PushMode as d1, type PushOptions as d2, type PushPolicy as d3, type PushResult as d4, type PutManyItemOptions as d5, type PutManyOptions as d6, type PutManyResult as d7, type QueryAcrossOptions as d8, type QueryAcrossResult as d9, type StoreAuthKind as dA, type StoreCapabilities as dB, SyncEngine as dC, type SyncMetadata as dD, type SyncPolicy as dE, SyncScheduler as dF, type SyncSchedulerStatus as dG, type SyncStatus as dH, type SyncTarget as dI, type SyncTargetRole as dJ, SyncTransaction as dK, type SyncTransactionResult as dL, type TierMode as dM, type TranslatorAuditEntry as dN, type TxOp as dO, USER_ENVELOPE_COLLECTION as dP, USER_ENVELOPE_MAX_BYTES as dQ, type Unsubscribe as dR, type UpdateAuthenticatorOptions as dS, type UpdateUserOptions as dT, UserApi as dU, type UserEnvelopeCheckGate as dV, UserEnvelopeOversizedError as dW, type UserEnvelopePresented as dX, type UserInfo as dY, type VaultBackup as dZ, type VaultPolicyOnDisk as d_, type QuickUnlockState as da, QuickUnlockStore as db, type ReAuthOperation as dc, type RecoverPassphraseInput as dd, type RecoverPassphraseResult as de, type RecoverUserOptions as df, type RecoveryProof as dg, type ResolvedPublicEnvelopeSchema as dh, type RevokeOptions as di, type RotatePassphraseInput as dj, type RotateRecoveryOptions as dk, type RotateRecoveryResult as dl, SEALED_PASSPHRASE_RECORD_ID as dm, type SealedEnvelope as dn, type SealedPassphrase as dp, type SessionPolicy as dq, type SetPublicEnvelopeInput as dr, type ShamirRecoveryDoc as ds, type ShamirRecoveryEntry as dt, type SlotRewrapCeremony as du, type SlotRewrapContext as dv, type StandardSchemaV1 as dw, type StandardSchemaV1Issue as dx, type StandardSchemaV1SyncResult as dy, type StoreAuth as dz, type I18nTextDescriptor as e, type VaultSnapshot as e0, type WarningRules as e1, WeakPassphraseError as e2, type WeakPassphraseReason as e3, type WrappedDeksBlob as e4, assertStrongPassphrase as e5, buildRecipientKeyringFile as e6, burnPaperRecoveryEntry as e7, createNoydb as e8, createStore as e9, readMagicLinkGrantRecord as eA, recoverUser as eB, removeAuthenticator as eC, resolveSchema as eD, revokeDelegation as eE, revokeMagicLinkGrant as eF, savePaperRecoveryEntries as eG, saveSealedPassphrase as eH, saveShamirRecoveryEntries as eI, unwrapDeksFromBlob as eJ, unwrapDeksFromPaperEntry as eK, unwrapDeksFromShamirEntry as eL, unwrapMagicLinkGrant as eM, validatePassphrase as eN, validatePublicEnvelopeInput as eO, validateSchemaInput as eP, validateSchemaOutput as eQ, writeMagicLinkGrant as eR, changeSecret as eS, createOwnerKeyring as eT, ensureCollectionDEK as eU, grant as eV, loadKeyring as eW, persistKeyring as eX, revoke as eY, updateAuthenticator as eZ, updateKeyringIdentity as e_, deriveMagicLinkContentKey as ea, enrollAuthenticator as eb, estimateEntropy as ec, evaluateExportCapability as ed, evaluateImportCapability as ee, findAuthenticator as ef, hasExportCapability as eg, hasImportCapability as eh, hasRecoveryEnrolled as ei, isMagicLinkGrantExpired as ej, isPublicEnvelope as ek, issueDelegation as el, recoverPassphrase as em, rotatePassphrase as en, listMagicLinkGrants as eo, listUsers as ep, listUsersWithEnvelopes as eq, loadActiveDelegations as er, loadPaperRecoveryEntries as es, loadSealedPassphrase as et, loadShamirRecoveryEntries as eu, magicLinkGrantRecordId as ev, mintPaperRecoveryEntry as ew, mintShamirRecoveryEntry as ex, mintWrappedDeksBlob as ey, parseSealedEnvelope as ez, type I18nTextOptions as f, applyI18nLocale as g, dictCollectionName as h, dictKey as i, i18nText as j, isDictCollectionName as k, isDictKeyDescriptor as l, isI18nTextDescriptor as m, createEnforcer as n, validateSessionPolicy as o, BLOB_CHUNKS_COLLECTION as p, BLOB_COLLECTION as q, resolveI18nText as r, BLOB_EVICTION_AUDIT_COLLECTION as s, BLOB_INDEX_COLLECTION as t, BLOB_SLOTS_PREFIX as u, validateI18nTextValue as v, BLOB_VERSIONS_PREFIX as w, type BlobEvictionEntry as x, type BlobFieldPolicy as y, type BlobFieldsConfig as z };