@noy-db/hub 0.1.0-pre.8 → 0.2.0-pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/dist/aggregate/index.cjs +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/blobs/index.cjs.map +1 -1
  8. package/dist/blobs/index.d.cts +6 -6
  9. package/dist/blobs/index.d.ts +6 -6
  10. package/dist/blobs/index.js +4 -4
  11. package/dist/bundle/index.cjs +298 -7
  12. package/dist/bundle/index.cjs.map +1 -1
  13. package/dist/bundle/index.d.cts +6 -6
  14. package/dist/bundle/index.d.ts +6 -6
  15. package/dist/bundle/index.js +15 -4
  16. package/dist/{chunk-GOUT6DND.js → chunk-23TTQXVO.js} +173 -91
  17. package/dist/chunk-23TTQXVO.js.map +1 -0
  18. package/dist/{chunk-CIMZBAZB.js → chunk-2AXFIYHT.js} +1 -1
  19. package/dist/chunk-2AXFIYHT.js.map +1 -0
  20. package/dist/chunk-34YSDCDP.js +73 -0
  21. package/dist/chunk-34YSDCDP.js.map +1 -0
  22. package/dist/{chunk-HC7Z5EQZ.js → chunk-4TFSM22V.js} +4 -4
  23. package/dist/{chunk-7XBQS42M.js → chunk-537VFZTR.js} +4 -4
  24. package/dist/{chunk-M62XNWRA.js → chunk-5DWL3JBF.js} +2 -2
  25. package/dist/{chunk-RSPLI376.js → chunk-5SCJ5UEF.js} +3 -3
  26. package/dist/chunk-5ZGZ6HIZ.js +100 -0
  27. package/dist/chunk-5ZGZ6HIZ.js.map +1 -0
  28. package/dist/chunk-6HPZY4ON.js +291 -0
  29. package/dist/chunk-6HPZY4ON.js.map +1 -0
  30. package/dist/{chunk-WN6UK7PM.js → chunk-7H6DOO3E.js} +239 -11
  31. package/dist/chunk-7H6DOO3E.js.map +1 -0
  32. package/dist/{chunk-ACLDOTNQ.js → chunk-ADQ5MQ54.js} +275 -3
  33. package/dist/chunk-ADQ5MQ54.js.map +1 -0
  34. package/dist/chunk-CBAHB2BF.js +893 -0
  35. package/dist/chunk-CBAHB2BF.js.map +1 -0
  36. package/dist/chunk-DPMFBCV6.js +296 -0
  37. package/dist/chunk-DPMFBCV6.js.map +1 -0
  38. package/dist/chunk-DYBQG5PQ.js +34 -0
  39. package/dist/chunk-DYBQG5PQ.js.map +1 -0
  40. package/dist/{chunk-ZFKD4QMV.js → chunk-DYECX3IX.js} +3 -3
  41. package/dist/chunk-EGQYGYIU.js +51 -0
  42. package/dist/chunk-EGQYGYIU.js.map +1 -0
  43. package/dist/chunk-FCXOFQAJ.js +79 -0
  44. package/dist/chunk-FCXOFQAJ.js.map +1 -0
  45. package/dist/chunk-HB3Z2GCR.js +124 -0
  46. package/dist/chunk-HB3Z2GCR.js.map +1 -0
  47. package/dist/{chunk-SCZXXXU4.js → chunk-I6MX32UC.js} +7 -32
  48. package/dist/chunk-I6MX32UC.js.map +1 -0
  49. package/dist/{chunk-VQBTTTUN.js → chunk-KESP7GOK.js} +4 -4
  50. package/dist/{chunk-VQBTTTUN.js.map → chunk-KESP7GOK.js.map} +1 -1
  51. package/dist/{chunk-NXFEYLVG.js → chunk-MIQHZESA.js} +4 -3
  52. package/dist/{chunk-NXFEYLVG.js.map → chunk-MIQHZESA.js.map} +1 -1
  53. package/dist/chunk-MKSA2V7A.js +19 -0
  54. package/dist/chunk-MKSA2V7A.js.map +1 -0
  55. package/dist/{chunk-M5INGEFC.js → chunk-MRIBLZL3.js} +3 -1
  56. package/dist/chunk-MRIBLZL3.js.map +1 -0
  57. package/dist/{chunk-2WGMYBYS.js → chunk-NIOHFJPJ.js} +6 -6
  58. package/dist/chunk-OMLIZL2P.js +61 -0
  59. package/dist/chunk-OMLIZL2P.js.map +1 -0
  60. package/dist/{chunk-USKYUS74.js → chunk-P7EQ2S5O.js} +2 -2
  61. package/dist/{chunk-YVFTBQHL.js → chunk-PA6R5ZCI.js} +217 -10
  62. package/dist/chunk-PA6R5ZCI.js.map +1 -0
  63. package/dist/chunk-PEULZC6M.js +118 -0
  64. package/dist/chunk-PEULZC6M.js.map +1 -0
  65. package/dist/chunk-RD5LYKD6.js +82 -0
  66. package/dist/chunk-RD5LYKD6.js.map +1 -0
  67. package/dist/chunk-SIZWEV2Y.js +145 -0
  68. package/dist/chunk-SIZWEV2Y.js.map +1 -0
  69. package/dist/{chunk-Y4CMTMUW.js → chunk-UA4RI7OT.js} +12 -6
  70. package/dist/chunk-UA4RI7OT.js.map +1 -0
  71. package/dist/chunk-UMLVJTYV.js +20 -0
  72. package/dist/chunk-UMLVJTYV.js.map +1 -0
  73. package/dist/chunk-UZXLQCHP.js +53 -0
  74. package/dist/chunk-UZXLQCHP.js.map +1 -0
  75. package/dist/{chunk-R2ZTGEVP.js → chunk-VMIO4IXG.js} +5 -5
  76. package/dist/{chunk-MR4424N3.js → chunk-WCA2NROQ.js} +2 -2
  77. package/dist/{chunk-TDR6T5CJ.js → chunk-XGSOTWYX.js} +91 -132
  78. package/dist/chunk-XGSOTWYX.js.map +1 -0
  79. package/dist/{chunk-NPC4LFV5.js → chunk-YMYK7US4.js} +2 -2
  80. package/dist/{chunk-PJK6IOBC.js → chunk-YS3POABP.js} +1 -1
  81. package/dist/chunk-YS3POABP.js.map +1 -0
  82. package/dist/chunk-Z72JH4KG.js +209 -0
  83. package/dist/chunk-Z72JH4KG.js.map +1 -0
  84. package/dist/{chunk-R36SIKES.js → chunk-ZNOEIM6Y.js} +2 -2
  85. package/dist/consent/index.cjs.map +1 -1
  86. package/dist/consent/index.d.cts +6 -6
  87. package/dist/consent/index.d.ts +6 -6
  88. package/dist/consent/index.js +3 -3
  89. package/dist/{crypto-IVKU7YTT.js → crypto-A7FRXYHC.js} +3 -3
  90. package/dist/{delegation-2DBS2EOH.js → delegation-YBA4X4JN.js} +5 -4
  91. package/dist/derivations/index.cjs +351 -0
  92. package/dist/derivations/index.cjs.map +1 -0
  93. package/dist/derivations/index.d.cts +71 -0
  94. package/dist/derivations/index.d.ts +71 -0
  95. package/dist/derivations/index.js +27 -0
  96. package/dist/{dev-unlock-BygpnIWe.d.ts → dev-unlock-D9s-loPr.d.ts} +1 -1
  97. package/dist/{dev-unlock-BZKx666y.d.cts → dev-unlock-DRwVSy2S.d.cts} +1 -1
  98. package/dist/executor-7E3VFGW7.js +11 -0
  99. package/dist/executor-CEWX2FQI.js +8 -0
  100. package/dist/executor-CEWX2FQI.js.map +1 -0
  101. package/dist/executor-X4SQ3ZLC.js +8 -0
  102. package/dist/executor-X4SQ3ZLC.js.map +1 -0
  103. package/dist/fanout-sidecar-VJ52RIEY.js +51 -0
  104. package/dist/fanout-sidecar-VJ52RIEY.js.map +1 -0
  105. package/dist/guards/index.cjs +315 -0
  106. package/dist/guards/index.cjs.map +1 -0
  107. package/dist/guards/index.d.cts +30 -0
  108. package/dist/guards/index.d.ts +30 -0
  109. package/dist/guards/index.js +29 -0
  110. package/dist/guards/index.js.map +1 -0
  111. package/dist/{hash-B0eU2Qv9.d.ts → hash-DXXXusyk.d.ts} +1 -1
  112. package/dist/{hash-CIyfmKsg.d.cts → hash-DtRih9MQ.d.cts} +1 -1
  113. package/dist/history/index.cjs +8 -1
  114. package/dist/history/index.cjs.map +1 -1
  115. package/dist/history/index.d.cts +7 -7
  116. package/dist/history/index.d.ts +7 -7
  117. package/dist/history/index.js +6 -6
  118. package/dist/i18n/index.cjs +81 -0
  119. package/dist/i18n/index.cjs.map +1 -1
  120. package/dist/i18n/index.d.cts +6 -6
  121. package/dist/i18n/index.d.ts +6 -6
  122. package/dist/i18n/index.js +19 -6
  123. package/dist/i18n/index.js.map +1 -1
  124. package/dist/{index-Dp4tKCjX.d.ts → index-4agOpzqd.d.ts} +174 -3
  125. package/dist/{index-6xNpPsxR.d.cts → index-CNwA-B6-.d.ts} +303 -5
  126. package/dist/{index-DJTf9yxn.d.ts → index-CmVgTkqk.d.cts} +303 -5
  127. package/dist/{index-DsVbTDZI.d.cts → index-hdFvZkBP.d.cts} +174 -3
  128. package/dist/index.cjs +5929 -1089
  129. package/dist/index.cjs.map +1 -1
  130. package/dist/index.d.cts +207 -16
  131. package/dist/index.d.ts +207 -16
  132. package/dist/index.js +2402 -672
  133. package/dist/index.js.map +1 -1
  134. package/dist/indexing/index.cjs +2 -0
  135. package/dist/indexing/index.cjs.map +1 -1
  136. package/dist/indexing/index.d.cts +3 -3
  137. package/dist/indexing/index.d.ts +3 -3
  138. package/dist/indexing/index.js +4 -4
  139. package/dist/{lazy-builder-CZVLKh0Z.d.cts → lazy-builder-C-rPfWG0.d.cts} +1 -1
  140. package/dist/{lazy-builder-BwEoBQZ9.d.ts → lazy-builder-Rpd-V3jP.d.ts} +1 -1
  141. package/dist/{ledger-UQIMMKO5.js → ledger-3TXNP47J.js} +6 -6
  142. package/dist/ledger-3TXNP47J.js.map +1 -0
  143. package/dist/materialized-views/index.cjs +837 -0
  144. package/dist/materialized-views/index.cjs.map +1 -0
  145. package/dist/materialized-views/index.d.cts +183 -0
  146. package/dist/materialized-views/index.d.ts +183 -0
  147. package/dist/materialized-views/index.js +45 -0
  148. package/dist/materialized-views/index.js.map +1 -0
  149. package/dist/overlay-views/index.cjs +359 -0
  150. package/dist/overlay-views/index.cjs.map +1 -0
  151. package/dist/overlay-views/index.d.cts +81 -0
  152. package/dist/overlay-views/index.d.ts +81 -0
  153. package/dist/overlay-views/index.js +23 -0
  154. package/dist/overlay-views/index.js.map +1 -0
  155. package/dist/periods/index.cjs +7 -1
  156. package/dist/periods/index.cjs.map +1 -1
  157. package/dist/periods/index.d.cts +6 -6
  158. package/dist/periods/index.d.ts +6 -6
  159. package/dist/periods/index.js +6 -6
  160. package/dist/{predicate-SBHmi6D0.d.cts → predicate-Dnu81tsS.d.cts} +25 -1
  161. package/dist/{predicate-SBHmi6D0.d.ts → predicate-Dnu81tsS.d.ts} +25 -1
  162. package/dist/{public-envelope-3QTQADDW.js → public-envelope-PY6NKFLI.js} +4 -4
  163. package/dist/public-envelope-PY6NKFLI.js.map +1 -0
  164. package/dist/query/index.cjs +302 -124
  165. package/dist/query/index.cjs.map +1 -1
  166. package/dist/query/index.d.cts +3 -3
  167. package/dist/query/index.d.ts +3 -3
  168. package/dist/query/index.js +26 -11
  169. package/dist/read-only-facade-ITU6L7BL.js +7 -0
  170. package/dist/read-only-facade-ITU6L7BL.js.map +1 -0
  171. package/dist/registry-3L3N3PTG.js +10 -0
  172. package/dist/registry-3L3N3PTG.js.map +1 -0
  173. package/dist/registry-O47PUPSY.js +8 -0
  174. package/dist/registry-O47PUPSY.js.map +1 -0
  175. package/dist/registry-RFGGMVNJ.js +7 -0
  176. package/dist/registry-RFGGMVNJ.js.map +1 -0
  177. package/dist/registry-WLLMODKN.js +8 -0
  178. package/dist/registry-WLLMODKN.js.map +1 -0
  179. package/dist/session/index.cjs +7 -1
  180. package/dist/session/index.cjs.map +1 -1
  181. package/dist/session/index.d.cts +7 -7
  182. package/dist/session/index.d.ts +7 -7
  183. package/dist/session/index.js +10 -3
  184. package/dist/session/index.js.map +1 -1
  185. package/dist/shadow/index.cjs.map +1 -1
  186. package/dist/shadow/index.d.cts +6 -6
  187. package/dist/shadow/index.d.ts +6 -6
  188. package/dist/shadow/index.js +2 -2
  189. package/dist/stale-HSC5YO2O.js +13 -0
  190. package/dist/stale-HSC5YO2O.js.map +1 -0
  191. package/dist/store/index.cjs +14 -0
  192. package/dist/store/index.cjs.map +1 -1
  193. package/dist/store/index.d.cts +6 -6
  194. package/dist/store/index.d.ts +6 -6
  195. package/dist/store/index.js +5 -2
  196. package/dist/{strategy-D-SrOLCl.d.cts → strategy-DSTrsZ8t.d.cts} +72 -19
  197. package/dist/{strategy-D-SrOLCl.d.ts → strategy-DSTrsZ8t.d.ts} +72 -19
  198. package/dist/sync/index.cjs.map +1 -1
  199. package/dist/sync/index.d.cts +5 -5
  200. package/dist/sync/index.d.ts +5 -5
  201. package/dist/sync/index.js +4 -4
  202. package/dist/team/index.cjs +1554 -2
  203. package/dist/team/index.cjs.map +1 -1
  204. package/dist/team/index.d.cts +6 -6
  205. package/dist/team/index.d.ts +6 -6
  206. package/dist/team/index.js +76 -9
  207. package/dist/tx/index.cjs +296 -44
  208. package/dist/tx/index.cjs.map +1 -1
  209. package/dist/tx/index.d.cts +6 -6
  210. package/dist/tx/index.d.ts +6 -6
  211. package/dist/tx/index.js +2 -2
  212. package/dist/{types-DD9eKKNc.d.ts → types-C4lwMKKF.d.cts} +2771 -322
  213. package/dist/{types-arFMsCtn.d.cts → types-DW9RGSSs.d.ts} +2771 -322
  214. package/dist/util/index.cjs.map +1 -1
  215. package/dist/util/index.js +1 -1
  216. package/dist/with-derivation-C8LDlV7t.d.cts +13 -0
  217. package/dist/with-derivation-g-pGoMzL.d.ts +13 -0
  218. package/dist/with-guard-DWOCK4Ca.d.ts +18 -0
  219. package/dist/with-guard-jI1x9Z3k.d.cts +18 -0
  220. package/dist/with-materialized-view-DaKR-N6J.d.ts +27 -0
  221. package/dist/with-materialized-view-DcTx4H3j.d.cts +27 -0
  222. package/dist/with-overlayed-view-D-6oWAgM.d.cts +13 -0
  223. package/dist/with-overlayed-view-N7jYuNOS.d.ts +13 -0
  224. package/package.json +53 -2
  225. package/dist/chunk-ACLDOTNQ.js.map +0 -1
  226. package/dist/chunk-BTDCBVJW.js +0 -160
  227. package/dist/chunk-BTDCBVJW.js.map +0 -1
  228. package/dist/chunk-CIMZBAZB.js.map +0 -1
  229. package/dist/chunk-GOUT6DND.js.map +0 -1
  230. package/dist/chunk-M5INGEFC.js.map +0 -1
  231. package/dist/chunk-PJK6IOBC.js.map +0 -1
  232. package/dist/chunk-SCZXXXU4.js.map +0 -1
  233. package/dist/chunk-TDR6T5CJ.js.map +0 -1
  234. package/dist/chunk-TOQK4KAN.js +0 -79
  235. package/dist/chunk-TOQK4KAN.js.map +0 -1
  236. package/dist/chunk-WN6UK7PM.js.map +0 -1
  237. package/dist/chunk-Y4CMTMUW.js.map +0 -1
  238. package/dist/chunk-YVFTBQHL.js.map +0 -1
  239. /package/dist/{chunk-HC7Z5EQZ.js.map → chunk-4TFSM22V.js.map} +0 -0
  240. /package/dist/{chunk-7XBQS42M.js.map → chunk-537VFZTR.js.map} +0 -0
  241. /package/dist/{chunk-M62XNWRA.js.map → chunk-5DWL3JBF.js.map} +0 -0
  242. /package/dist/{chunk-RSPLI376.js.map → chunk-5SCJ5UEF.js.map} +0 -0
  243. /package/dist/{chunk-ZFKD4QMV.js.map → chunk-DYECX3IX.js.map} +0 -0
  244. /package/dist/{chunk-2WGMYBYS.js.map → chunk-NIOHFJPJ.js.map} +0 -0
  245. /package/dist/{chunk-USKYUS74.js.map → chunk-P7EQ2S5O.js.map} +0 -0
  246. /package/dist/{chunk-R2ZTGEVP.js.map → chunk-VMIO4IXG.js.map} +0 -0
  247. /package/dist/{chunk-MR4424N3.js.map → chunk-WCA2NROQ.js.map} +0 -0
  248. /package/dist/{chunk-NPC4LFV5.js.map → chunk-YMYK7US4.js.map} +0 -0
  249. /package/dist/{chunk-R36SIKES.js.map → chunk-ZNOEIM6Y.js.map} +0 -0
  250. /package/dist/{crypto-IVKU7YTT.js.map → crypto-A7FRXYHC.js.map} +0 -0
  251. /package/dist/{delegation-2DBS2EOH.js.map → delegation-YBA4X4JN.js.map} +0 -0
  252. /package/dist/{ledger-UQIMMKO5.js.map → derivations/index.js.map} +0 -0
  253. /package/dist/{public-envelope-3QTQADDW.js.map → executor-7E3VFGW7.js.map} +0 -0
@@ -1,8 +1,8 @@
1
- import { I as IndexStrategy, d as LazyQuery } from './lazy-builder-BwEoBQZ9.js';
2
- import { A as AggregateStrategy } from './strategy-D-SrOLCl.js';
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';
1
+ import { I as IndexStrategy, d as LazyQuery } from './lazy-builder-C-rPfWG0.cjs';
2
+ import { b as AggregateSpec, A as AggregateStrategy } from './strategy-DSTrsZ8t.cjs';
3
+ import { C as CrdtStrategy, a as CrdtMode, b as CrdtState } from './strategy-BSxFXGzb.cjs';
4
+ import { N as NoydbError, Q as Query, ak as RefRegistry, ah as RefDescriptor, _ as JoinableSource, am as RefViolation, an as ScanBuilder } from './index-CmVgTkqk.cjs';
5
+ import { F as FieldClause, I as IndexDef, C as CollectionIndexes } from './predicate-Dnu81tsS.cjs';
6
6
 
7
7
  /**
8
8
  * Standard Schema v1 integration.
@@ -785,12 +785,17 @@ interface LedgerEntry {
785
785
  readonly prevHash: string;
786
786
  /**
787
787
  * 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.
788
+ * data operations (`put`, `delete`, `amendment`). Access-control
789
+ * operations (`grant`, `revoke`, `rotate`) will be added in a
790
+ * follow-up once the keyring write path is instrumented — that's
791
+ * tracked in the epic issue.
792
+ *
793
+ * `'amendment'` is the multi-record audit entry written by the
794
+ * guards subsystem when an admin/owner uses `withTransactions(...)`
795
+ * to repair a constraint-violating state. See `amendment` field
796
+ * below for the structured payload.
792
797
  */
793
- readonly op: 'put' | 'delete';
798
+ readonly op: 'put' | 'delete' | 'amendment';
794
799
  /** The collection the mutation targeted. */
795
800
  readonly collection: string;
796
801
  /** The record id the mutation targeted. */
@@ -814,6 +819,17 @@ interface LedgerEntry {
814
819
  * the file docstring.
815
820
  */
816
821
  readonly payloadHash: string;
822
+ /**
823
+ * Optional human-readable tag describing why this mutation happened
824
+ * (#1). Threaded through `collection.put(_, _, { reason })`. Common
825
+ * values include `'import:csv'`, `'import:json'`, `'import:xlsx'` from
826
+ * `as-*` ImportPlan.apply(), but consumers can use any string for
827
+ * domain-specific audit filtering. Auto-strip via `canonicalJson` —
828
+ * absent on the wire, never serialized as `null`.
829
+ *
830
+ * Audit consumers filter: `entries.filter(e => e.reason?.startsWith('import:'))`.
831
+ */
832
+ readonly reason?: string;
817
833
  /**
818
834
  * Optional hex-encoded sha256 of the encrypted JSON Patch delta
819
835
  * blob stored alongside this entry in `_ledger_deltas/`. Present
@@ -837,6 +853,24 @@ interface LedgerEntry {
837
853
  * entirely — never `{ deltaHash: undefined }`.
838
854
  */
839
855
  readonly deltaHash?: string;
856
+ /**
857
+ * Present only when `op === 'amendment'`. Records the human reason,
858
+ * the role of the actor, the (collection, id, vBefore, vAfter) tuple
859
+ * for every record touched, and which guard invariants passed.
860
+ *
861
+ * See docs/superpowers/specs/2026-05-18-guards-design.md.
862
+ */
863
+ readonly amendment?: {
864
+ readonly reason: string;
865
+ readonly role: 'admin' | 'owner';
866
+ readonly changes: ReadonlyArray<{
867
+ readonly collection: string;
868
+ readonly id: string;
869
+ readonly vBefore: number;
870
+ readonly vAfter: number;
871
+ }>;
872
+ readonly invariantsPassed: ReadonlyArray<string>;
873
+ };
840
874
  }
841
875
  /**
842
876
  * Canonical (sort-stable) JSON encoder.
@@ -1056,6 +1090,20 @@ interface AppendInput {
1056
1090
  * as the entry's `deltaHash` field.
1057
1091
  */
1058
1092
  delta?: JsonPatch;
1093
+ /**
1094
+ * Present only for `op === 'amendment'` — structured audit
1095
+ * payload for multi-record repair operations performed via
1096
+ * `withTransactions(...)`. Carried through verbatim to the
1097
+ * resulting ledger entry.
1098
+ */
1099
+ amendment?: LedgerEntry['amendment'];
1100
+ /**
1101
+ * Optional human-readable tag describing why this mutation happened
1102
+ * (#1). Threaded from `collection.put(_, _, { reason })`.
1103
+ * Carried verbatim onto the resulting ledger entry's `reason` field;
1104
+ * omitted from canonical JSON when undefined.
1105
+ */
1106
+ reason?: string;
1059
1107
  }
1060
1108
  /**
1061
1109
  * Result of `LedgerStore.verify()`. On success, `head` is the hash of
@@ -1949,8 +1997,9 @@ interface UnlockedKeyring {
1949
1997
  */
1950
1998
  readonly exportCapability?: ExportCapability;
1951
1999
  /**
1952
- * `@noy-db/as-*` import capability (issue ). Absent when the
1953
- * keyring was written before landed default-closed semantics
2000
+ * `@noy-db/as-*` import capability. Absent when the
2001
+ * keyring was written before the import-capability extension
2002
+ * landed — default-closed semantics
1954
2003
  * apply via `hasImportCapability` (no plaintext format granted, no
1955
2004
  * bundle import granted, regardless of role).
1956
2005
  */
@@ -1970,6 +2019,65 @@ interface UnlockedKeyring {
1970
2019
  */
1971
2020
  readonly policy?: VaultPolicyOnDisk;
1972
2021
  }
2022
+ /** Load and unlock a user's keyring for a vault. */
2023
+ declare function loadKeyring(adapter: NoydbStore, vault: string, userId: string, passphrase: string): Promise<UnlockedKeyring>;
2024
+ /**
2025
+ * Create the initial owner keyring for a new vault.
2026
+ *
2027
+ * Pass `{ validate: true }` (or a `PassphrasePolicy`) to gate creation
2028
+ * on the phrase-format strength rules — `Noydb` threads this from
2029
+ * `NoydbOptions.validatePassphrase`. Direct callers (CLI, scripts,
2030
+ * test fixtures) opt in explicitly.
2031
+ */
2032
+ declare function createOwnerKeyring(adapter: NoydbStore, vault: string, userId: string, passphrase: string, passphraseOpts?: PassphrasePolicy & {
2033
+ validate?: boolean;
2034
+ allowWeakPassphrase?: boolean;
2035
+ }): Promise<UnlockedKeyring>;
2036
+ /** Grant access to a new user. Caller must have grant privilege. */
2037
+ declare function grant(adapter: NoydbStore, vault: string, callerKeyring: UnlockedKeyring, options: GrantOptions): Promise<void>;
2038
+ /** Revoke a user's access. Optionally rotate keys for affected collections. */
2039
+ declare function revoke(adapter: NoydbStore, vault: string, callerKeyring: UnlockedKeyring, options: RevokeOptions): Promise<void>;
2040
+ /**
2041
+ * Mutate `role`, `displayName`, and/or `permissions` on an existing
2042
+ * keyring. Pure plaintext-header rewrite — no DEK rewrap, no KEK
2043
+ * required, no authenticator slots touched. Tier-2 enrollments and
2044
+ * recovery codes survive the operation.
2045
+ *
2046
+ * Role-elevation guard: BOTH the old role AND the new role must
2047
+ * satisfy `canUpdateRole(callerRole, _)`. This blocks the two
2048
+ * privilege-escalation shapes:
2049
+ * - admin elevates someone (or themselves) to owner
2050
+ * - admin demotes an owner to a role they then control
2051
+ *
2052
+ * Owner is always allowed. Admin manages admin / operator / viewer /
2053
+ * client laterally.
2054
+ *
2055
+ * Identity preserved: same userId, same DEK wrappings. Last-write-wins
2056
+ * through the standard keyring put (same concurrency story as `grant`
2057
+ * and `revoke`).
2058
+ *
2059
+ * @throws `NoAccessError` when no keyring exists for the target.
2060
+ * @throws `PermissionDeniedError` when the role hierarchy rejects.
2061
+ * @throws `ValidationError` when the diff is empty (nothing to update).
2062
+ *
2063
+ * @see #54
2064
+ */
2065
+ declare function updateKeyringIdentity(adapter: NoydbStore, vault: string, callerKeyring: UnlockedKeyring, options: UpdateUserOptions): Promise<void>;
2066
+ /**
2067
+ * Change the user's passphrase. Re-wraps every DEK under the new KEK.
2068
+ *
2069
+ * Validates the new passphrase against the strength rules unless
2070
+ * `allowWeakPassphrase: true` is passed. Mirrors `rotatePassphrase`'s
2071
+ * default-on validation contract.
2072
+ *
2073
+ * `db.rotatePassphrase()` adds a `checkGate('rotate-passphrase')` step
2074
+ * on top of this primitive and additionally requires the OLD passphrase
2075
+ * for re-derivation; `changeSecret` reuses the cached unlocked KEK so
2076
+ * the OLD passphrase is not retyped.
2077
+ */
2078
+ declare function changeSecret(adapter: NoydbStore, vault: string, keyring: UnlockedKeyring, newPassphrase: string, passphraseOpts?: PassphrasePolicy & {
2079
+ allowWeakPassphrase?: boolean;
2080
+ }): Promise<UnlockedKeyring>;
1973
2081
  /**
1974
2082
  * Recipient slot in a re-keyed `.noydb` bundle. Each slot becomes its
1975
2083
  * own keyring file inside the bundle, sealed with its own passphrase.
@@ -2027,6 +2135,17 @@ interface BundleRecipient {
2027
2135
  declare function buildRecipientKeyringFile(callerKeyring: UnlockedKeyring, recipient: BundleRecipient): Promise<KeyringFile>;
2028
2136
  /** List all users with access to a vault. */
2029
2137
  declare function listUsers(adapter: NoydbStore, vault: string): Promise<UserInfo[]>;
2138
+ /**
2139
+ * Optional filter knobs for {@link listUsersWithEnvelopes}.
2140
+ *
2141
+ * - `includeHidden` — when true, principals with `_meta/visibility/<id>`
2142
+ * set to `{ hidden: true }` are returned alongside everyone else.
2143
+ * Requires `owner` or `admin` callerRole; lower roles get
2144
+ * {@link import('../errors.js').PermissionDeniedError}.
2145
+ */
2146
+ interface ListUsersOptions {
2147
+ readonly includeHidden?: boolean;
2148
+ }
2030
2149
  /**
2031
2150
  * Joined enumeration: every keyring + its `_users/<keyringId>`
2032
2151
  * envelope side by side. Convenience for admin UIs that want to
@@ -2036,6 +2155,27 @@ declare function listUsers(adapter: NoydbStore, vault: string): Promise<UserInfo
2036
2155
  * `userEnvelopeDek` is the vault's `_users` collection DEK
2037
2156
  * (`vault.getDEK('_users')`); used to decrypt every envelope.
2038
2157
  *
2158
+ * `callerRole` (#122) drives the directory-visibility checks:
2159
+ *
2160
+ * - When the vault's `_meta/directory` document has `enabled: false`,
2161
+ * only `owner` and `admin` callers may enumerate; anyone else gets
2162
+ * {@link import('../errors.js').DirectoryDisabledError}.
2163
+ * - Principals with `_meta/visibility/<id>` set to `{ hidden: true }`
2164
+ * are filtered out by default. `owner`/`admin` callers can pass
2165
+ * `{ includeHidden: true }` to see them; lower roles passing that
2166
+ * option get `PermissionDeniedError`.
2167
+ *
2168
+ * Honest caveat (#122): these filters are a UX hint, not a security
2169
+ * boundary. The keyring file is still listed at `_keyring/*` and the
2170
+ * envelope ciphertext at `_users/*`. A caller with direct store access
2171
+ * — or a caller that calls this function with `callerRole: 'owner'`
2172
+ * unconditionally — sees every principal. The protection is only as
2173
+ * strong as the role the calling layer passes in. The hub-level wrapper
2174
+ * on `Vault` sources `callerRole` from the unlocked keyring's `role`
2175
+ * field, which is signed-by-construction (it lives in the user's own
2176
+ * keyring file). See `docs/subsystems/user-envelope.md` →
2177
+ * "Directory visibility".
2178
+ *
2039
2179
  * Principals without a persisted envelope (legacy keyrings predating
2040
2180
  * the user-envelope feature) come back with `envelope: null`. The
2041
2181
  * caller chooses how to render — usually "fall back to keyring's
@@ -2044,10 +2184,14 @@ declare function listUsers(adapter: NoydbStore, vault: string): Promise<UserInfo
2044
2184
  * Order matches `listUsers()` (store-defined; sort if you need a
2045
2185
  * stable display order).
2046
2186
  */
2047
- declare function listUsersWithEnvelopes<T = unknown>(adapter: NoydbStore, vault: string, userEnvelopeDek: CryptoKey): Promise<Array<{
2187
+ declare function listUsersWithEnvelopes<T = unknown>(adapter: NoydbStore, vault: string, userEnvelopeDek: CryptoKey, callerRole: Role, options?: ListUsersOptions): Promise<Array<{
2048
2188
  user: UserInfo;
2049
2189
  envelope: UserEnvelope<T> | null;
2050
2190
  }>>;
2191
+ /** Ensure a DEK exists for a collection. Generates one if new. */
2192
+ declare function ensureCollectionDEK(adapter: NoydbStore, vault: string, keyring: UnlockedKeyring): Promise<(collectionName: string) => Promise<CryptoKey>>;
2193
+ /** Persist a keyring file to the adapter. */
2194
+ declare function persistKeyring(adapter: NoydbStore, vault: string, keyring: UnlockedKeyring): Promise<void>;
2051
2195
  /**
2052
2196
  * Check whether a keyring is authorised for a given `@noy-db/as-*`
2053
2197
  * export tier.
@@ -2946,6 +3090,283 @@ declare class SyncEngine {
2946
3090
  private persistMeta;
2947
3091
  }
2948
3092
 
3093
+ /**
3094
+ * **Wrap-DEKs primitive (#44)** — a single canonical shape for the
3095
+ * pattern of "serialize a DEK set, encrypt it under a credential-derived
3096
+ * AES-GCM key." Used by:
3097
+ *
3098
+ * - **tier-0** — paper recovery entries (`_meta/recovery-paper`),
3099
+ * credential = the printed code.
3100
+ * - **tier-2** — password authenticator slots (`KeyringFile.authenticators`,
3101
+ * `wrapKind: 'deks'`), credential = the user's password.
3102
+ *
3103
+ * **Not** used by `@noy-db/on-pin` — tier-3 wraps the DEK set under
3104
+ * the same conceptual pattern but at **100,000 PBKDF2 iterations**
3105
+ * (vs the 600,000 here), because the protection window for a PIN
3106
+ * slot is short (idle-timeout-bounded, typically 15 min) and 600k
3107
+ * iterations would make every PIN-resume noticeably slow. The wire
3108
+ * formats are deliberately incompatible. See `@noy-db/on-pin`'s
3109
+ * `PIN_PBKDF2_ITERATIONS` and the threat-model rationale in its
3110
+ * module docstring.
3111
+ *
3112
+ * Before #44, the same crypto lived in two places: `mintPaperRecoveryEntry`
3113
+ * (in `team/recovery.ts`) and `enrollPasswordAuthenticator` (in
3114
+ * `@noy-db/on-password`). Both functions did identical work — PBKDF2
3115
+ * the credential, AES-GCM-encrypt the JSON-serialized DEK set — but
3116
+ * their implementations had drifted apart enough that fixing a bug
3117
+ * in one wouldn't fix the other.
3118
+ *
3119
+ * This module owns the canonical implementation. Consumers compose:
3120
+ *
3121
+ * - `mintPaperRecoveryEntry` is now a thin wrapper that calls
3122
+ * `mintWrappedDeksBlob` and adds `{ codeId, enrolledAt }`.
3123
+ * - `enrollPasswordAuthenticator` calls `mintWrappedDeksBlob` and
3124
+ * wraps the result in the slot envelope.
3125
+ *
3126
+ * @module
3127
+ */
3128
+ /**
3129
+ * The wrap-DEKs primitive — a serialized + AES-GCM-encrypted DEK set
3130
+ * keyed under a credential-derived key.
3131
+ *
3132
+ * All three fields are base64-encoded so the blob is JSON-safe and
3133
+ * round-trips through `_meta/*` envelopes (which carry plaintext
3134
+ * JSON in `_data`).
3135
+ *
3136
+ * Composition: `PaperRecoveryEntry extends WrappedDeksBlob` plus
3137
+ * `{ codeId, enrolledAt }`. `KeyringAuthenticatorWrappingDEKs`
3138
+ * carries the same three fields with `salt` stored in `meta` for
3139
+ * slot-format back-compat (#44 defers moving it to top-level).
3140
+ */
3141
+ interface WrappedDeksBlob {
3142
+ /** Base64 PBKDF2 salt for the credential-derived wrapping key. */
3143
+ readonly salt: string;
3144
+ /** Base64 AES-GCM IV used for the `wrappedDeks` ciphertext. */
3145
+ readonly iv: string;
3146
+ /** Base64 AES-GCM ciphertext of `{ deks: { collection: base64rawDek } }`. */
3147
+ readonly wrappedDeks: string;
3148
+ }
3149
+ /**
3150
+ * Mint a fresh `WrappedDeksBlob` from a DEK set + a string credential.
3151
+ *
3152
+ * Generates a random salt + IV, derives a 256-bit AES-GCM key via
3153
+ * PBKDF2-SHA256(credential, salt, 600K), serializes the DEK set as
3154
+ * `{ deks: { coll: rawBase64 } }`, and AES-GCM-encrypts.
3155
+ *
3156
+ * The `credential` is the user-typed string (recovery code, password,
3157
+ * PIN). Caller normalization rules apply (e.g. paper
3158
+ * recovery uppercase-strips the code before reaching this function).
3159
+ *
3160
+ * @param deks - DEK set to wrap. Each DEK must be exportable via
3161
+ * `subtle.exportKey('raw', dek)` (the hub mints DEKs
3162
+ * this way; consumers feeding non-extractable keys
3163
+ * will get `InvalidAccessError` from WebCrypto).
3164
+ * @param credential - String input the consumer minted (paper code,
3165
+ * password, PIN). Treated as opaque bytes by PBKDF2.
3166
+ */
3167
+ declare function mintWrappedDeksBlob(deks: Map<string, CryptoKey>, credential: string): Promise<WrappedDeksBlob>;
3168
+ /**
3169
+ * Reverse of {@link mintWrappedDeksBlob}. Re-derives the wrapping key
3170
+ * from the credential + stored salt, AES-GCM-decrypts the wrapped DEK
3171
+ * set, and re-imports each DEK as an extractable AES-GCM CryptoKey.
3172
+ *
3173
+ * Throws (AES-GCM auth tag failure) when the credential doesn't
3174
+ * match the blob. Callers iterating over multiple blobs (e.g. paper
3175
+ * recovery's "try every entry until one matches") should catch.
3176
+ */
3177
+ declare function unwrapDeksFromBlob(blob: WrappedDeksBlob, credential: string): Promise<Map<string, CryptoKey>>;
3178
+
3179
+ /**
3180
+ * String-level Shamir provider injected into hub for `profile: 'shamir'`
3181
+ * recovery. Keeps hub free of any `@noy-db/on-shamir` import — hub never
3182
+ * sees `RawShare` or the share codecs. Implemented by
3183
+ * `shamirRecoveryProvider()` from `@noy-db/on-shamir`.
3184
+ */
3185
+ interface ShamirRecoveryProvider {
3186
+ /** Split `secret` into `n` base32 share strings; any `k` recombine it. */
3187
+ splitToShares(secret: Uint8Array, k: number, n: number): string[];
3188
+ /**
3189
+ * Recombine `k`+ share strings into the secret. MUST throw on malformed,
3190
+ * truncated, insufficient, or mismatched shares.
3191
+ */
3192
+ combineShares(shares: readonly string[]): Uint8Array;
3193
+ }
3194
+
3195
+ /**
3196
+ * Recovery profile persistence + dispatch — issue #10.
3197
+ *
3198
+ * v0.1.0-pre.5 wires the **paper** profile end-to-end through
3199
+ * `@noy-db/on-recovery`. The other three profiles (Shamir,
3200
+ * multi-channel, admin-mediated) ship the API surface and throw
3201
+ * {@link RecoveryProfileNotImplementedError} during use; per-profile
3202
+ * dispatch lands in follow-up issues.
3203
+ *
3204
+ * Storage layout:
3205
+ *
3206
+ * ```
3207
+ * _meta/recovery-paper — JSON { entries: RecoveryCodeEntry[] } produced by `on-recovery`.
3208
+ * _meta/recovery-shamir — reserved
3209
+ * _meta/recovery-multi — reserved
3210
+ * _meta/recovery-admin — reserved
3211
+ * ```
3212
+ *
3213
+ * Like `_meta/policy` and `_meta/handle`, the documents are plain JSON
3214
+ * with empty `_iv` — the recovery-code wrapping is what protects the
3215
+ * KEK; the entries themselves are inert without the user's code.
3216
+ *
3217
+ * @module
3218
+ */
3219
+
3220
+ /**
3221
+ * One paper recovery code as persisted in `_meta/recovery-paper`.
3222
+ *
3223
+ * The hub's KEK is intentionally non-extractable (see `crypto.ts`),
3224
+ * so the recovery entry can't AES-KW-wrap the KEK directly. Instead
3225
+ * we wrap a serialized DEK set: the entry holds the AES-GCM
3226
+ * ciphertext of `{ deks: { collection: rawDekBase64 } }`. Recovery
3227
+ * deserializes the DEK set, then mints a fresh KEK from the new
3228
+ * passphrase and rewraps the DEKs under it.
3229
+ *
3230
+ * This is the same pattern `@noy-db/on-pin` uses for tier-3 quick
3231
+ * resume — the cryptographic guarantee is identical (AES-GCM with a
3232
+ * PBKDF2-derived key), and it sidesteps the non-extractable-KEK
3233
+ * constraint cleanly.
3234
+ *
3235
+ * Type-level composition (#44): `PaperRecoveryEntry extends
3236
+ * WrappedDeksBlob` — the three crypto fields (`salt`, `iv`,
3237
+ * `wrappedDeks`) come from the shared primitive; `codeId` and
3238
+ * `enrolledAt` are paper-recovery's own metadata. Wire format
3239
+ * unchanged.
3240
+ */
3241
+ interface PaperRecoveryEntry extends WrappedDeksBlob {
3242
+ readonly codeId: string;
3243
+ readonly enrolledAt: string;
3244
+ }
3245
+ interface PaperRecoveryDoc {
3246
+ readonly _noydb_recovery: 1;
3247
+ readonly profile: 'paper';
3248
+ readonly entries: ReadonlyArray<PaperRecoveryEntry>;
3249
+ }
3250
+ /** Read the paper-recovery entries. Returns empty array when absent. */
3251
+ declare function loadPaperRecoveryEntries(store: NoydbStore, vault: string): Promise<ReadonlyArray<PaperRecoveryEntry>>;
3252
+ /** Replace the paper-recovery entries (used after burn-on-recovery). */
3253
+ declare function savePaperRecoveryEntries(store: NoydbStore, vault: string, entries: ReadonlyArray<PaperRecoveryEntry>): Promise<void>;
3254
+ /** Drop a single paper-recovery entry (burn-on-use). */
3255
+ declare function burnPaperRecoveryEntry(store: NoydbStore, vault: string, codeId: string): Promise<void>;
3256
+ /** Whether at least one recovery profile has any enrolled entries. */
3257
+ declare function hasRecoveryEnrolled(store: NoydbStore, vault: string): Promise<boolean>;
3258
+ /**
3259
+ * One Shamir-recovery entry as persisted in `_meta/recovery-shamir`.
3260
+ *
3261
+ * Like {@link PaperRecoveryEntry}, the entry composes
3262
+ * {@link WrappedDeksBlob} (DEKs wrapped under a fresh ephemeral
3263
+ * recovery secret) with profile-specific metadata. Unlike paper, the
3264
+ * "credential" was never visible to the user — it was 32 random
3265
+ * bytes split into N Shamir shares at enrollment. The shares ARE
3266
+ * the credential; the user holds them, the hub never sees them
3267
+ * again after `enrollRecovery` returns.
3268
+ *
3269
+ * Per the spec §5: the recovery secret is base64-encoded and
3270
+ * passed as the `credential` arg to
3271
+ * {@link mintWrappedDeksBlob} / {@link unwrapDeksFromBlob}. The
3272
+ * PBKDF2 round over high-entropy input is harmless overhead — it
3273
+ * keeps the shared primitive unchanged while letting Shamir reuse
3274
+ * the same wrapping pipeline as paper.
3275
+ */
3276
+ interface ShamirRecoveryEntry extends WrappedDeksBlob {
3277
+ /** Stable id for this entry. Allows multiple Shamir splits to coexist. */
3278
+ readonly entryId: string;
3279
+ /** Threshold — minimum shares to reconstruct. */
3280
+ readonly k: number;
3281
+ /** Total shares minted at enrollment. */
3282
+ readonly n: number;
3283
+ /** x-coordinates of the n minted shares. Informational. Omitted as of 0.2
3284
+ * (string-level provider doesn't expose share x-coords); kept optional so
3285
+ * pre-0.2 entries still read. */
3286
+ readonly xCoords?: ReadonlyArray<number>;
3287
+ /** ISO timestamp. */
3288
+ readonly enrolledAt: string;
3289
+ /** Optional caller-supplied label (e.g., "2-of-3 board escrow"). */
3290
+ readonly label?: string;
3291
+ }
3292
+ interface ShamirRecoveryDoc {
3293
+ readonly _noydb_recovery: 1;
3294
+ readonly profile: 'shamir';
3295
+ readonly entries: ReadonlyArray<ShamirRecoveryEntry>;
3296
+ }
3297
+ /** Read the Shamir-recovery entries. Returns empty array when absent. */
3298
+ declare function loadShamirRecoveryEntries(store: NoydbStore, vault: string): Promise<ReadonlyArray<ShamirRecoveryEntry>>;
3299
+ /** Replace the Shamir-recovery entries (used by enrollment and rotation). */
3300
+ declare function saveShamirRecoveryEntries(store: NoydbStore, vault: string, entries: ReadonlyArray<ShamirRecoveryEntry>): Promise<void>;
3301
+ /**
3302
+ * Mint a fresh Shamir recovery entry from a DEK set.
3303
+ *
3304
+ * 1. Generates a 32-byte recovery secret.
3305
+ * 2. Wraps the DEK set under that secret via
3306
+ * {@link mintWrappedDeksBlob} (the recovery secret is base64-
3307
+ * encoded as the credential string — PBKDF2 over high-entropy
3308
+ * input is harmless overhead).
3309
+ * 3. Splits the recovery secret via Shamir into `n` shares with
3310
+ * threshold `k`.
3311
+ * 4. Zeros the in-memory recovery secret after wrapping + splitting.
3312
+ *
3313
+ * Returns:
3314
+ * - `entry` — the {@link ShamirRecoveryEntry} to persist.
3315
+ * - `shareStrings` — the `n` Base32-encoded share strings to
3316
+ * return to the caller. The HUB MUST NOT PERSIST THESE; once
3317
+ * returned they are the user's responsibility.
3318
+ *
3319
+ * @param deks - DEK set to wrap.
3320
+ * @param entryId - Stable id for this entry (caller-supplied or
3321
+ * hub-generated).
3322
+ * @param k - Threshold (>= 2).
3323
+ * @param n - Total shares (k <= n <= 255).
3324
+ * @param label - Optional caller label.
3325
+ */
3326
+ declare function mintShamirRecoveryEntry(provider: ShamirRecoveryProvider, deks: Map<string, CryptoKey>, entryId: string, k: number, n: number, label?: string): Promise<{
3327
+ entry: ShamirRecoveryEntry;
3328
+ shareStrings: string[];
3329
+ }>;
3330
+ /**
3331
+ * Decrypt a Shamir recovery entry to recover the raw DEK set.
3332
+ *
3333
+ * Combines K or more `shares`, reconstructs the recovery secret,
3334
+ * unwraps the DEKs via {@link unwrapDeksFromBlob}.
3335
+ *
3336
+ * Throws (AES-GCM auth-tag mismatch) when the shares don't combine
3337
+ * to the secret originally used to mint the entry — typically
3338
+ * because they came from a different enrollment or were tampered
3339
+ * with. Callers iterating multiple entries should catch.
3340
+ */
3341
+ declare function unwrapDeksFromShamirEntry(provider: ShamirRecoveryProvider, entry: ShamirRecoveryEntry, shareStrings: readonly string[]): Promise<Map<string, CryptoKey>>;
3342
+ /**
3343
+ * Generate one paper-recovery entry from an unlocked DEK set.
3344
+ *
3345
+ * Returns the serializable entry (persisted via
3346
+ * {@link savePaperRecoveryEntries}). The recovery flow unwraps the
3347
+ * DEK set, then mints a fresh KEK from the user's new passphrase.
3348
+ *
3349
+ * Thin wrapper over {@link mintWrappedDeksBlob} (#44) — the crypto
3350
+ * lives in the shared primitive; this function just adds paper-
3351
+ * recovery's own metadata (`codeId`, `enrolledAt`).
3352
+ *
3353
+ * @param deks Map of collection-name → DEK (extractable).
3354
+ * @param code The plaintext recovery code (caller-supplied;
3355
+ * pair this with `@noy-db/on-recovery`'s code
3356
+ * generator/parser if available).
3357
+ * @param codeId Stable id used by `burnPaperRecoveryEntry`.
3358
+ */
3359
+ declare function mintPaperRecoveryEntry(deks: Map<string, CryptoKey>, code: string, codeId: string): Promise<PaperRecoveryEntry>;
3360
+ /**
3361
+ * Decrypt a recovery entry to recover the raw DEK set. Used by the
3362
+ * `recoverPassphrase` flow after the user's code has been parsed.
3363
+ *
3364
+ * Thin wrapper over {@link unwrapDeksFromBlob} (#44).
3365
+ *
3366
+ * @throws when the code does not match the entry (AES-GCM auth tag fail).
3367
+ */
3368
+ declare function unwrapDeksFromPaperEntry(entry: PaperRecoveryEntry, code: string): Promise<Map<string, CryptoKey>>;
3369
+
2949
3370
  /**
2950
3371
  * Tier-2 authenticator slot management — issue #11.
2951
3372
  *
@@ -2996,6 +3417,40 @@ type EnrollAuthenticatorOptions = EnrollAuthenticatorWrappingKEKOptions | Enroll
2996
3417
  * input. The variant is preserved verbatim into `KeyringAuthenticator`.
2997
3418
  */
2998
3419
  declare function enrollAuthenticator(store: NoydbStore, vault: string, keyring: UnlockedKeyring, options: EnrollAuthenticatorOptions): Promise<UnlockedKeyring>;
3420
+ /**
3421
+ * Caller payload for {@link updateAuthenticator} (#55). Mutates only
3422
+ * `meta` — the slot's id, method, and wrap material are immutable
3423
+ * through this primitive, preserving the anti-slot-swap guard.
3424
+ *
3425
+ * `meta` is **merged** at the top level: keys absent from the patch
3426
+ * are preserved, keys present overwrite. To clear a meta key, pass
3427
+ * `null` for that key explicitly. (Same semantics as #57's
3428
+ * `UserApi.updateMe`, scoped to this top-level merge — no recursion
3429
+ * into nested meta values.)
3430
+ */
3431
+ interface UpdateAuthenticatorOptions {
3432
+ readonly meta?: Record<string, unknown>;
3433
+ }
3434
+ /**
3435
+ * Mutate a tier-2 authenticator slot's `meta` blob (slot rename,
3436
+ * label changes). The slot's `id`, `method`, and wrap material
3437
+ * (`wrapped_kek` for wrap-KEK; `wrapped_deks` + `iv` for wrap-DEKs)
3438
+ * are immutable through this entry point — the anti-slot-swap guard
3439
+ * is structural, not gate-driven, so even if the policy gate is
3440
+ * weakened a future caller cannot use this path to swap one slot's
3441
+ * crypto for another's.
3442
+ *
3443
+ * `meta` patch semantics:
3444
+ * - Top-level merge — absent keys preserved, present keys overwrite
3445
+ * - `null` value — delete that meta key
3446
+ * - Non-object values (string, number, boolean, array) — replace verbatim
3447
+ *
3448
+ * @throws `NoAccessError` when no slot with the given id exists.
3449
+ * @throws `ValidationError` when no patch field is provided.
3450
+ *
3451
+ * @see #55
3452
+ */
3453
+ declare function updateAuthenticator(store: NoydbStore, vault: string, keyring: UnlockedKeyring, slotId: string, options: UpdateAuthenticatorOptions): Promise<UnlockedKeyring>;
2999
3454
  /**
3000
3455
  * Drop a slot by id. No-op if the slot doesn't exist (idempotent —
3001
3456
  * removing a non-existent slot is a recoverable retry, not an error).
@@ -3030,8 +3485,8 @@ declare function findAuthenticator(keyring: UnlockedKeyring, slotId: string): Ke
3030
3485
  /**
3031
3486
  * Context handed to a {@link SlotRewrapCeremony} when `rotatePassphrase`
3032
3487
  * preserves a tier-2 slot. The ceremony's job is to re-derive its
3033
- * method-specific wrapping material (PRF assertion, PBKDF2 of a
3034
- * daily-password, etc.) and wrap the freshly rewrapped DEK set under
3488
+ * method-specific wrapping material (PRF assertion, PBKDF2 of the
3489
+ * password, etc.) and wrap the freshly rewrapped DEK set under
3035
3490
  * the new wrapping key.
3036
3491
  *
3037
3492
  * Two surfaces are exposed:
@@ -3109,7 +3564,15 @@ interface RotatePassphraseInput {
3109
3564
  * slot's id or method (anti-slot-swap guard).
3110
3565
  */
3111
3566
  declare function rotatePassphrase(store: NoydbStore, vault: string, userId: string, input: RotatePassphraseInput): Promise<UnlockedKeyring>;
3112
- /** Caller payload for {@link recoverPassphrase}. */
3567
+ /**
3568
+ * Caller payload for {@link recoverPassphrase}.
3569
+ *
3570
+ * As of #196 slice 1, `paper` and `shamir` are wired end-to-end.
3571
+ * The remaining two profiles (`multi-channel`, `admin-mediated`)
3572
+ * stay outside the union and throw
3573
+ * {@link RecoveryProfileNotImplementedError} at the runtime guard
3574
+ * when bypassed via `as unknown as RecoveryProof`.
3575
+ */
3113
3576
  type RecoveryProof = {
3114
3577
  readonly profile: 'paper';
3115
3578
  readonly payload: {
@@ -3118,19 +3581,12 @@ type RecoveryProof = {
3118
3581
  } | {
3119
3582
  readonly profile: 'shamir';
3120
3583
  readonly payload: {
3584
+ /** Optional disambiguator when multiple Shamir entries are enrolled.
3585
+ * When omitted, hub tries each entry until one combines. */
3586
+ readonly entryId?: string;
3587
+ /** K or more opaque share strings, as returned by `ShamirRecoveryProvider.splitToShares`. */
3121
3588
  readonly shares: ReadonlyArray<string>;
3122
3589
  };
3123
- } | {
3124
- readonly profile: 'multi-channel';
3125
- readonly payload: {
3126
- readonly proofs: ReadonlyArray<unknown>;
3127
- };
3128
- } | {
3129
- readonly profile: 'admin-mediated';
3130
- readonly payload: {
3131
- readonly token: string;
3132
- readonly factor?: unknown;
3133
- };
3134
3590
  };
3135
3591
  interface RecoverPassphraseInput {
3136
3592
  readonly newPassphrase: string;
@@ -3191,19 +3647,96 @@ interface RecoverPassphraseResult {
3191
3647
  readonly newCodes: readonly string[];
3192
3648
  }
3193
3649
  /**
3194
- * Reset the user's passphrase using a recovery proof. v0.1.0-pre.5
3195
- * supports the `'paper'` profile via `@noy-db/on-recovery` entries
3196
- * persisted in `_meta/recovery-paper`. The other three profiles throw
3197
- * {@link RecoveryProfileNotImplementedError}.
3198
- *
3199
- * On success, the used recovery entry is burned (deleted from the
3200
- * stored set).
3650
+ * Input for {@link Noydb.rotateRecovery} (#121) deliberate
3651
+ * recovery-credential regeneration when the user knows their
3652
+ * passphrase but wants a fresh sheet (paper) or fresh shares
3653
+ * (shamir). Symmetric to {@link RotatePassphraseInput}.
3201
3654
  */
3202
- declare function recoverPassphrase(store: NoydbStore, vault: string, userId: string, input: RecoverPassphraseInput): Promise<UnlockedKeyring>;
3203
-
3204
- /**
3205
- * Atomic peer-recovery primitive — issues #33 + #34.
3206
- *
3655
+ type RotateRecoveryOptions = {
3656
+ readonly profile: 'paper';
3657
+ /** How many fresh codes to mint. Default: existing sheet size. */
3658
+ readonly count?: number;
3659
+ /** Optional code generator — see {@link RecoverPassphraseInput.codeGenerator}. */
3660
+ readonly codeGenerator?: () => string;
3661
+ } | {
3662
+ readonly profile: 'shamir';
3663
+ /** New threshold. */
3664
+ readonly k: number;
3665
+ /** New total share count. */
3666
+ readonly n: number;
3667
+ /** Disambiguator when multiple Shamir entries exist; required if there are 2+. */
3668
+ readonly entryId?: string;
3669
+ /** Optional updated label. */
3670
+ readonly label?: string;
3671
+ };
3672
+ /**
3673
+ * Result of {@link Noydb.rotateRecovery}. Shape varies by profile:
3674
+ *
3675
+ * - `paper` → `{ newCodes: string[] }` (and `entryId === 'paper-batch'`)
3676
+ * - `shamir` → `{ newShares: string[], entryId }`
3677
+ *
3678
+ * `newCodes` is populated for paper rotations; `newShares` for
3679
+ * Shamir rotations. Both are show-once — the hub does not
3680
+ * retain them.
3681
+ */
3682
+ interface RotateRecoveryResult {
3683
+ readonly newCodes?: readonly string[];
3684
+ readonly newShares?: readonly string[];
3685
+ readonly entryId?: string;
3686
+ }
3687
+ /**
3688
+ * Result of {@link Noydb.enrollRecovery}. Shape varies by profile:
3689
+ *
3690
+ * - `paper` → `{ entryId: 'paper-batch' }` (caller minted the
3691
+ * entries; this is a sentinel since paper enrollments are batch-shaped).
3692
+ * - `shamir` → `{ entryId, shares: string[] }` — shares are
3693
+ * show-once; the hub does not retain them.
3694
+ */
3695
+ interface EnrollRecoveryResult {
3696
+ readonly entryId: string;
3697
+ readonly shares?: readonly string[];
3698
+ }
3699
+ /**
3700
+ * Input shape for {@link Noydb.enrollRecovery} and
3701
+ * {@link Noydb.openVaultAndEnrollRecovery} (#195). Discriminated
3702
+ * union over recovery profiles.
3703
+ *
3704
+ * - `paper`: caller pre-mints entries (typically via
3705
+ * `mintPaperRecoveryEntry` or `@noy-db/on-recovery`'s
3706
+ * `generateRecoveryCodeSet`) and passes them in. The hub stores
3707
+ * them and surfaces an opaque batch id.
3708
+ * - `shamir`: hub mints the recovery secret + the shares at
3709
+ * enrollment time. The shares are returned in
3710
+ * {@link EnrollRecoveryResult.shares} (show-once); the hub never
3711
+ * retains them.
3712
+ *
3713
+ * Multi-channel and admin-mediated will be added when the respective
3714
+ * dispatch slices ship.
3715
+ */
3716
+ type RecoveryEnrollmentInput = {
3717
+ readonly profile: 'paper';
3718
+ readonly entries: ReadonlyArray<PaperRecoveryEntry>;
3719
+ } | {
3720
+ readonly profile: 'shamir';
3721
+ readonly k: number;
3722
+ readonly n: number;
3723
+ readonly label?: string;
3724
+ readonly entryId?: string;
3725
+ };
3726
+ /**
3727
+ * Reset the user's passphrase using a recovery proof. v0.1.0-pre.5
3728
+ * supports the `'paper'` profile via `@noy-db/on-recovery` entries
3729
+ * persisted in `_meta/recovery-paper`. The other three profiles throw
3730
+ * {@link RecoveryProfileNotImplementedError}.
3731
+ *
3732
+ * On success, the used recovery entry is burned (deleted from the
3733
+ * stored set).
3734
+ */
3735
+ declare function recoverPassphrase(provider: ShamirRecoveryProvider | undefined, store: NoydbStore, vault: string, userId: string, input: RecoverPassphraseInput): Promise<UnlockedKeyring>;
3736
+
3737
+ /**
3738
+ * Atomic peer-recovery primitive — issues #33 + #34.
3739
+ *
3207
3740
  * `recoverUser` is a SEPARATE operation from `revoke + grant`. It
3208
3741
  * exists because peer-recovery has different semantics than account
3209
3742
  * removal-then-reissue:
@@ -3281,183 +3814,6 @@ interface RecoverUserOptions {
3281
3814
  */
3282
3815
  declare function recoverUser(store: NoydbStore, vault: string, callerKeyring: UnlockedKeyring, options: RecoverUserOptions): Promise<void>;
3283
3816
 
3284
- /**
3285
- * **Wrap-DEKs primitive (#44)** — a single canonical shape for the
3286
- * pattern of "serialize a DEK set, encrypt it under a credential-derived
3287
- * AES-GCM key." Used by:
3288
- *
3289
- * - **tier-0** — paper recovery entries (`_meta/recovery-paper`),
3290
- * credential = the printed code.
3291
- * - **tier-2** — password authenticator slots (`KeyringFile.authenticators`,
3292
- * `wrapKind: 'deks'`), credential = the daily password.
3293
- *
3294
- * **Not** used by `@noy-db/on-pin` — tier-3 wraps the DEK set under
3295
- * the same conceptual pattern but at **100,000 PBKDF2 iterations**
3296
- * (vs the 600,000 here), because the protection window for a PIN
3297
- * slot is short (idle-timeout-bounded, typically 15 min) and 600k
3298
- * iterations would make every PIN-resume noticeably slow. The wire
3299
- * formats are deliberately incompatible. See `@noy-db/on-pin`'s
3300
- * `PIN_PBKDF2_ITERATIONS` and the threat-model rationale in its
3301
- * module docstring.
3302
- *
3303
- * Before #44, the same crypto lived in two places: `mintPaperRecoveryEntry`
3304
- * (in `team/recovery.ts`) and `enrollPasswordAuthenticator` (in
3305
- * `@noy-db/on-password`). Both functions did identical work — PBKDF2
3306
- * the credential, AES-GCM-encrypt the JSON-serialized DEK set — but
3307
- * their implementations had drifted apart enough that fixing a bug
3308
- * in one wouldn't fix the other.
3309
- *
3310
- * This module owns the canonical implementation. Consumers compose:
3311
- *
3312
- * - `mintPaperRecoveryEntry` is now a thin wrapper that calls
3313
- * `mintWrappedDeksBlob` and adds `{ codeId, enrolledAt }`.
3314
- * - `enrollPasswordAuthenticator` calls `mintWrappedDeksBlob` and
3315
- * wraps the result in the slot envelope.
3316
- *
3317
- * @module
3318
- */
3319
- /**
3320
- * The wrap-DEKs primitive — a serialized + AES-GCM-encrypted DEK set
3321
- * keyed under a credential-derived key.
3322
- *
3323
- * All three fields are base64-encoded so the blob is JSON-safe and
3324
- * round-trips through `_meta/*` envelopes (which carry plaintext
3325
- * JSON in `_data`).
3326
- *
3327
- * Composition: `PaperRecoveryEntry extends WrappedDeksBlob` plus
3328
- * `{ codeId, enrolledAt }`. `KeyringAuthenticatorWrappingDEKs`
3329
- * carries the same three fields with `salt` stored in `meta` for
3330
- * slot-format back-compat (#44 defers moving it to top-level).
3331
- */
3332
- interface WrappedDeksBlob {
3333
- /** Base64 PBKDF2 salt for the credential-derived wrapping key. */
3334
- readonly salt: string;
3335
- /** Base64 AES-GCM IV used for the `wrappedDeks` ciphertext. */
3336
- readonly iv: string;
3337
- /** Base64 AES-GCM ciphertext of `{ deks: { collection: base64rawDek } }`. */
3338
- readonly wrappedDeks: string;
3339
- }
3340
- /**
3341
- * Mint a fresh `WrappedDeksBlob` from a DEK set + a string credential.
3342
- *
3343
- * Generates a random salt + IV, derives a 256-bit AES-GCM key via
3344
- * PBKDF2-SHA256(credential, salt, 600K), serializes the DEK set as
3345
- * `{ deks: { coll: rawBase64 } }`, and AES-GCM-encrypts.
3346
- *
3347
- * The `credential` is the user-typed string (recovery code, daily
3348
- * password, PIN). Caller normalization rules apply (e.g. paper
3349
- * recovery uppercase-strips the code before reaching this function).
3350
- *
3351
- * @param deks - DEK set to wrap. Each DEK must be exportable via
3352
- * `subtle.exportKey('raw', dek)` (the hub mints DEKs
3353
- * this way; consumers feeding non-extractable keys
3354
- * will get `InvalidAccessError` from WebCrypto).
3355
- * @param credential - String input the consumer minted (paper code,
3356
- * password, PIN). Treated as opaque bytes by PBKDF2.
3357
- */
3358
- declare function mintWrappedDeksBlob(deks: Map<string, CryptoKey>, credential: string): Promise<WrappedDeksBlob>;
3359
- /**
3360
- * Reverse of {@link mintWrappedDeksBlob}. Re-derives the wrapping key
3361
- * from the credential + stored salt, AES-GCM-decrypts the wrapped DEK
3362
- * set, and re-imports each DEK as an extractable AES-GCM CryptoKey.
3363
- *
3364
- * Throws (AES-GCM auth tag failure) when the credential doesn't
3365
- * match the blob. Callers iterating over multiple blobs (e.g. paper
3366
- * recovery's "try every entry until one matches") should catch.
3367
- */
3368
- declare function unwrapDeksFromBlob(blob: WrappedDeksBlob, credential: string): Promise<Map<string, CryptoKey>>;
3369
-
3370
- /**
3371
- * Recovery profile persistence + dispatch — issue #10.
3372
- *
3373
- * v0.1.0-pre.5 wires the **paper** profile end-to-end through
3374
- * `@noy-db/on-recovery`. The other three profiles (Shamir,
3375
- * multi-channel, admin-mediated) ship the API surface and throw
3376
- * {@link RecoveryProfileNotImplementedError} during use; per-profile
3377
- * dispatch lands in follow-up issues.
3378
- *
3379
- * Storage layout:
3380
- *
3381
- * ```
3382
- * _meta/recovery-paper — JSON { entries: RecoveryCodeEntry[] } produced by `on-recovery`.
3383
- * _meta/recovery-shamir — reserved
3384
- * _meta/recovery-multi — reserved
3385
- * _meta/recovery-admin — reserved
3386
- * ```
3387
- *
3388
- * Like `_meta/policy` and `_meta/handle`, the documents are plain JSON
3389
- * with empty `_iv` — the recovery-code wrapping is what protects the
3390
- * KEK; the entries themselves are inert without the user's code.
3391
- *
3392
- * @module
3393
- */
3394
-
3395
- /**
3396
- * One paper recovery code as persisted in `_meta/recovery-paper`.
3397
- *
3398
- * The hub's KEK is intentionally non-extractable (see `crypto.ts`),
3399
- * so the recovery entry can't AES-KW-wrap the KEK directly. Instead
3400
- * we wrap a serialized DEK set: the entry holds the AES-GCM
3401
- * ciphertext of `{ deks: { collection: rawDekBase64 } }`. Recovery
3402
- * deserializes the DEK set, then mints a fresh KEK from the new
3403
- * passphrase and rewraps the DEKs under it.
3404
- *
3405
- * This is the same pattern `@noy-db/on-pin` uses for tier-3 quick
3406
- * resume — the cryptographic guarantee is identical (AES-GCM with a
3407
- * PBKDF2-derived key), and it sidesteps the non-extractable-KEK
3408
- * constraint cleanly.
3409
- *
3410
- * Type-level composition (#44): `PaperRecoveryEntry extends
3411
- * WrappedDeksBlob` — the three crypto fields (`salt`, `iv`,
3412
- * `wrappedDeks`) come from the shared primitive; `codeId` and
3413
- * `enrolledAt` are paper-recovery's own metadata. Wire format
3414
- * unchanged.
3415
- */
3416
- interface PaperRecoveryEntry extends WrappedDeksBlob {
3417
- readonly codeId: string;
3418
- readonly enrolledAt: string;
3419
- }
3420
- interface PaperRecoveryDoc {
3421
- readonly _noydb_recovery: 1;
3422
- readonly profile: 'paper';
3423
- readonly entries: ReadonlyArray<PaperRecoveryEntry>;
3424
- }
3425
- /** Read the paper-recovery entries. Returns empty array when absent. */
3426
- declare function loadPaperRecoveryEntries(store: NoydbStore, vault: string): Promise<ReadonlyArray<PaperRecoveryEntry>>;
3427
- /** Replace the paper-recovery entries (used after burn-on-recovery). */
3428
- declare function savePaperRecoveryEntries(store: NoydbStore, vault: string, entries: ReadonlyArray<PaperRecoveryEntry>): Promise<void>;
3429
- /** Drop a single paper-recovery entry (burn-on-use). */
3430
- declare function burnPaperRecoveryEntry(store: NoydbStore, vault: string, codeId: string): Promise<void>;
3431
- /** Whether at least one recovery profile has any enrolled entries. */
3432
- declare function hasRecoveryEnrolled(store: NoydbStore, vault: string): Promise<boolean>;
3433
- /**
3434
- * Generate one paper-recovery entry from an unlocked DEK set.
3435
- *
3436
- * Returns the serializable entry (persisted via
3437
- * {@link savePaperRecoveryEntries}). The recovery flow unwraps the
3438
- * DEK set, then mints a fresh KEK from the user's new passphrase.
3439
- *
3440
- * Thin wrapper over {@link mintWrappedDeksBlob} (#44) — the crypto
3441
- * lives in the shared primitive; this function just adds paper-
3442
- * recovery's own metadata (`codeId`, `enrolledAt`).
3443
- *
3444
- * @param deks Map of collection-name → DEK (extractable).
3445
- * @param code The plaintext recovery code (caller-supplied;
3446
- * pair this with `@noy-db/on-recovery`'s code
3447
- * generator/parser if available).
3448
- * @param codeId Stable id used by `burnPaperRecoveryEntry`.
3449
- */
3450
- declare function mintPaperRecoveryEntry(deks: Map<string, CryptoKey>, code: string, codeId: string): Promise<PaperRecoveryEntry>;
3451
- /**
3452
- * Decrypt a recovery entry to recover the raw DEK set. Used by the
3453
- * `recoverPassphrase` flow after the user's code has been parsed.
3454
- *
3455
- * Thin wrapper over {@link unwrapDeksFromBlob} (#44).
3456
- *
3457
- * @throws when the code does not match the entry (AES-GCM auth tag fail).
3458
- */
3459
- declare function unwrapDeksFromPaperEntry(entry: PaperRecoveryEntry, code: string): Promise<Map<string, CryptoKey>>;
3460
-
3461
3817
  /**
3462
3818
  * Public envelope — owner-curated plaintext metadata, readable
3463
3819
  * before vault unlock or bundle decryption.
@@ -3678,6 +4034,33 @@ interface StagedOp {
3678
4034
  id: string;
3679
4035
  record?: unknown;
3680
4036
  expectedVersion?: number;
4037
+ /**
4038
+ * Optional human-readable tag forwarded to the resulting ledger
4039
+ * entry's `reason` field (#1). Set by callers via
4040
+ * `tx.vault(v).collection(c).put(id, record, { reason })`.
4041
+ */
4042
+ reason?: string;
4043
+ }
4044
+ /**
4045
+ * One executed op (main staged op or recursive side-effect like a
4046
+ * derivation output) paired with the envelope captured before the write.
4047
+ * `revertExecuted` walks this array in reverse on rollback.
4048
+ * @internal
4049
+ */
4050
+ interface ExecutedOp {
4051
+ op: StagedOp;
4052
+ priorEnvelope: EncryptedEnvelope | null;
4053
+ }
4054
+ /**
4055
+ * Options accepted by `db.transaction({ amendment, reason }, fn)`.
4056
+ * Only the amendment variant uses these — a plain `db.transaction(fn)`
4057
+ * never sees this shape.
4058
+ */
4059
+ interface AmendmentTxOptions {
4060
+ /** Opt into amendment mode. Required to be `true`. */
4061
+ readonly amendment: true;
4062
+ /** Human-readable rationale recorded in the ledger entry. Required. */
4063
+ readonly reason: string;
3681
4064
  }
3682
4065
  /**
3683
4066
  * Transaction handle passed to the user's body. Use
@@ -3687,10 +4070,27 @@ interface StagedOp {
3687
4070
  declare class TxContext {
3688
4071
  /** @internal */
3689
4072
  readonly _ops: StagedOp[];
4073
+ /**
4074
+ * @internal — write log built up in Phase 2. Each entry records the
4075
+ * envelope captured BEFORE the write so a mid-batch failure can
4076
+ * restore prior state via `revertExecuted`. Side-effect writes (e.g.
4077
+ * recursive derivation outputs fired inside `Collection.put`) are
4078
+ * appended here in execution order so they roll back alongside the
4079
+ * main staged ops (#133).
4080
+ */
4081
+ readonly _executed: ExecutedOp[];
3690
4082
  /** @internal */
3691
4083
  readonly _db: Noydb;
4084
+ /**
4085
+ * @internal — true when this TxContext was opened in amendment
4086
+ * mode. Toggles the lazy-`beginAmendment` + role-check path on first
4087
+ * `tx.vault(name)` and unlocks the post-Phase-2 invariant + audit run.
4088
+ */
4089
+ readonly _amendment: boolean;
4090
+ /** @internal — vaults that have already had `beginAmendment` called. */
4091
+ readonly _amendmentVaults: Map<string, Vault>;
3692
4092
  /** @internal */
3693
- constructor(db: Noydb);
4093
+ constructor(db: Noydb, amendment?: boolean);
3694
4094
  /** Scope subsequent `collection()` calls to the named vault. */
3695
4095
  vault(name: string): TxVault;
3696
4096
  }
@@ -3729,6 +4129,7 @@ declare class TxCollection<T> {
3729
4129
  */
3730
4130
  put(id: string, record: T, options?: {
3731
4131
  expectedVersion?: number;
4132
+ reason?: string;
3732
4133
  }): void;
3733
4134
  /**
3734
4135
  * Stage a delete. Does not write until the transaction body returns.
@@ -3740,14 +4141,16 @@ declare class TxCollection<T> {
3740
4141
  }): void;
3741
4142
  }
3742
4143
  /**
3743
- * Commit plan: pre-flight check + execution + revert plan. Returned
3744
- * from `runTransaction`.
4144
+ * Commit plan: pre-flight check + execution + revert plan.
3745
4145
  *
3746
- * @internal — exposed only for the `Collection.putMany({atomic:true})`
3747
- * wire-up so the bulk path can share the executor without creating
3748
- * an outer TxContext.
4146
+ * @internal — driven by `withTransactions()` (via `tx/active.ts`) for
4147
+ * user-facing `db.transaction(...)` calls and by the `amendment` path
4148
+ * in `noydb.ts`. `Collection.putManyAtomic` runs its own Phase 2 loop
4149
+ * but shares the `_activeTxContext` mechanism (and the `revertExecuted`
4150
+ * helper) so nested side-effect derivation writes get registered for
4151
+ * revert alongside the bulk-put source ops (#133).
3749
4152
  */
3750
- declare function runTransaction<T>(db: Noydb, fn: (tx: TxContext) => Promise<T> | T): Promise<T>;
4153
+ declare function runTransaction<T>(db: Noydb, fn: (tx: TxContext) => Promise<T> | T, options?: AmendmentTxOptions): Promise<T>;
3751
4154
 
3752
4155
  /**
3753
4156
  * Policy gate DSL types — issue #9.
@@ -3774,7 +4177,7 @@ declare function runTransaction<T>(db: Noydb, fn: (tx: TxContext) => Promise<T>
3774
4177
  * | `shamir` | k-of-n threshold share (`@noy-db/on-shamir`) | yes |
3775
4178
  * | `webauthn-roaming` | hardware key (YubiKey, SoloKey, Titan) | yes (key portable) |
3776
4179
  * | `webauthn-platform` | platform passkey (Touch ID, Face ID, Hello) | no (device-bound) |
3777
- * | `password` | tier-2 daily password (`@noy-db/on-password`) | no |
4180
+ * | `password` | tier-2 password (`@noy-db/on-password`) | no |
3778
4181
  * | `pin` | tier-3 quick-resume PIN (`@noy-db/on-pin`) | no |
3779
4182
  *
3780
4183
  * Off-device kinds (TOTP, email-OTP, recovery, shamir, roaming WebAuthn)
@@ -3830,7 +4233,26 @@ interface GatePolicy {
3830
4233
  * and use the same engine; the engine treats unknown names with no
3831
4234
  * configured policy as "no gate" (no-op).
3832
4235
  */
3833
- type BuiltInGateName = 'rotate-passphrase' | 'recover-passphrase' | 'enroll-authenticator' | 'remove-authenticator' | 'rotate-unlock' | 'enroll-user' | 'revoke-user' | 'export-bundle' | 'export-plaintext' | 'view-user-auth'
4236
+ type BuiltInGateName = 'rotate-passphrase' | 'recover-passphrase' | 'enroll-authenticator' | 'remove-authenticator'
4237
+ /**
4238
+ * Authorize a deliberate paper-recovery-code regeneration —
4239
+ * `db.rotateRecovery` (#121). Symmetric to `rotate-passphrase` for
4240
+ * the case where the user remembers their passphrase but wants a
4241
+ * fresh sheet (lost the printout, suspect compromise of the off-site
4242
+ * copy). PERSONAL allows tier-1; STRICT requires an off-device
4243
+ * factor so a stolen unlocked laptop cannot silently mint a new
4244
+ * sheet for an attacker.
4245
+ */
4246
+ | 'rotate-recovery'
4247
+ /**
4248
+ * Authorize a meta-only mutation on an existing authenticator slot —
4249
+ * `db.updateAuthenticator` (#55). The slot's wrap material, id, and
4250
+ * method are immutable through this gate; only the `meta` blob
4251
+ * (nicknames, method-specific labels) can change. Anti-slot-swap
4252
+ * guard is preserved structurally regardless of this gate's
4253
+ * settings.
4254
+ */
4255
+ | 'update-authenticator' | 'rotate-unlock' | 'enroll-user' | 'revoke-user' | 'export-bundle' | 'export-plaintext' | 'view-user-auth'
3834
4256
  /** Authorize a write to one's own user envelope (#22). */
3835
4257
  | 'edit-own-profile'
3836
4258
  /** Authorize reading other principals' user envelopes (#22). */
@@ -3844,7 +4266,16 @@ type BuiltInGateName = 'rotate-passphrase' | 'recover-passphrase' | 'enroll-auth
3844
4266
  * factor-proof default in `STRICT_POLICY` so the issuer must
3845
4267
  * affirmatively prove identity at the moment of recovery.
3846
4268
  */
3847
- | 'peer-recover-user';
4269
+ | 'peer-recover-user'
4270
+ /**
4271
+ * Authorize a post-grant identity mutation — `db.updateUser` (#54).
4272
+ * Covers `role`, `displayName`, `permissions` changes on an existing
4273
+ * keyring. Pure plaintext-header rewrite — no DEKs touched, no KEK
4274
+ * required. The role-elevation guard inside the implementation
4275
+ * mirrors `db.grant`'s hierarchy (admin cannot promote to owner)
4276
+ * regardless of this gate's settings.
4277
+ */
4278
+ | 'update-user';
3848
4279
  /** Either a built-in gate name or an `app:*` custom gate. */
3849
4280
  type GateName = BuiltInGateName | `app:${string}`;
3850
4281
  /**
@@ -3865,6 +4296,25 @@ interface FactorProof {
3865
4296
  /** Method-specific payload. The engine treats it as opaque — verification is delegated. */
3866
4297
  readonly payload?: unknown;
3867
4298
  }
4299
+ /**
4300
+ * Bundle of factor proofs + session-context flags passed to a gated
4301
+ * Noydb method. Used as the optional last parameter of every method
4302
+ * that runs through `checkGate`: `db.grant`, `db.revoke`, `db.updateUser`,
4303
+ * `db.enrollAuthenticator`, `db.removeAuthenticator`, `db.updateAuthenticator`,
4304
+ * `db.enrollWebAuthn`, `db.rotatePassphrase`, `db.recoverPassphrase`,
4305
+ * `db.recoverUser`, `db.enrollUnlock`, `db.describeUserAuth`,
4306
+ * `db.describeAllUsersAuth`.
4307
+ *
4308
+ * Pre-#89 this type was inlined at every call site as
4309
+ * `{ factors?: ReadonlyArray<FactorProof>; sharedDevice?: boolean }`
4310
+ * and parameter names alternated between `factors` and `presented`.
4311
+ * Now exported so consumers can name their helpers and so the param
4312
+ * name converges to `factors` everywhere.
4313
+ */
4314
+ interface FactorProofBundle {
4315
+ readonly factors?: ReadonlyArray<FactorProof>;
4316
+ readonly sharedDevice?: boolean;
4317
+ }
3868
4318
  /** Active session tier — what the engine compares against `gate.minTier`. */
3869
4319
  type ActiveTier = 1 | 2 | 3;
3870
4320
 
@@ -3886,6 +4336,14 @@ declare class Noydb {
3886
4336
  * `_meta/policy` load; replaced by `db.updatePolicy()`.
3887
4337
  */
3888
4338
  private readonly policyCache;
4339
+ /**
4340
+ * One-shot bypass for the managed-mode strong-recovery check (#195).
4341
+ * Set true by {@link openVaultAndEnrollRecovery} for the duration of
4342
+ * the bootstrap window so the keyring can be created before the
4343
+ * strong recovery is enrolled. Always cleared (try/finally).
4344
+ * @internal
4345
+ */
4346
+ private _skipNextManagedRecoveryCheck;
3889
4347
  /** Per-vault tier-3 (PIN / quick-resume) state — issue #11. */
3890
4348
  private readonly quickUnlock;
3891
4349
  /**
@@ -3901,6 +4359,17 @@ declare class Noydb {
3901
4359
  private readonly txStrategy;
3902
4360
  private readonly sessionStrategy;
3903
4361
  private readonly syncStrategy;
4362
+ /**
4363
+ * Currently-running multi-record transaction, set by
4364
+ * `runTransaction` at the start of Phase 2 (commit) and cleared in
4365
+ * the same function's `finally` block. Side-effect writes triggered
4366
+ * during a staged op's `Collection.put` (today: eager derivation
4367
+ * outputs) register their pre-write envelope on `_executed` here so
4368
+ * a mid-batch failure rolls them back alongside the main staged ops
4369
+ * (#133). `null` outside of Phase 2.
4370
+ * @internal
4371
+ */
4372
+ private _activeTxContext;
3904
4373
  /**
3905
4374
  * In-process translation cache. Key is `"${field}\x00${collection}\x00${from}\x00${to}\x00${text}"`.
3906
4375
  * Cleared on `close()` alongside the KEK and DEKs.
@@ -3940,21 +4409,81 @@ declare class Noydb {
3940
4409
  }): Promise<Vault>;
3941
4410
  /** Synchronous vault access (must call openVault first, or auto-opens). */
3942
4411
  vault(name: string): Vault;
3943
- /** Grant access to a user for a vault. */
3944
- grant(vault: string, options: GrantOptions): Promise<void>;
3945
- /** Revoke a user's access to a vault. */
3946
- revoke(vault: string, options: RevokeOptions): Promise<void>;
3947
4412
  /**
3948
- * Rotate the DEKs for the given collections in a vault.
4413
+ * Grant access to a user for a vault.
3949
4414
  *
3950
- * Generates fresh DEKs, re-encrypts every record in each collection,
3951
- * and re-wraps the new DEKs into every remaining user's keyring. The
3952
- * old DEKs become unreachable useful as a defense-in-depth measure
3953
- * after a suspected key leak, or as the scheduled half of a
3954
- * key-rotation policy.
4415
+ * Gated by `enroll-user`. `STRICT_POLICY` requires a TOTP / email-OTP
4416
+ * factor proof so the operator affirmatively re-asserts identity at
4417
+ * the moment of grant; `PERSONAL_POLICY` accepts a tier-1 unlock alone.
3955
4418
  *
3956
- * Unlike `revoke({ rotateKeys: true })`, this call does NOT remove
3957
- * any usersevery current member keeps access, but with fresh
4419
+ * The legacy `requireReAuthFor: ['grant']` session-policy check still
4420
+ * fires on top both are independent opt-ins.
4421
+ */
4422
+ grant(vault: string, options: GrantOptions, factors?: FactorProofBundle): Promise<void>;
4423
+ /**
4424
+ * Revoke a user's access to a vault.
4425
+ *
4426
+ * Gated by `revoke-user`. `STRICT_POLICY` requires a TOTP / email-OTP
4427
+ * factor proof; `PERSONAL_POLICY` accepts a tier-1 unlock alone.
4428
+ *
4429
+ * The legacy `requireReAuthFor: ['revoke']` session-policy check still
4430
+ * fires on top — both are independent opt-ins.
4431
+ */
4432
+ revoke(vault: string, options: RevokeOptions, factors?: FactorProofBundle): Promise<void>;
4433
+ /**
4434
+ * Mutate post-grant identity fields on an existing keyring — `role`,
4435
+ * `displayName`, and/or `permissions`. Pure plaintext-header rewrite:
4436
+ * no DEK rewrap, no KEK required, no authenticator slots touched.
4437
+ * Tier-2 enrollments and recovery codes survive.
4438
+ *
4439
+ * Different from `db.revoke + db.grant`:
4440
+ *
4441
+ * - Same `userId`, same DEK wrappings, same `granted_by`, same
4442
+ * `_users/<keyringId>` envelope. Only the specified header
4443
+ * fields move. Last-write-wins via the standard keyring put.
4444
+ * - No cascade on role demotion (admins demoted to operator keep
4445
+ * the keyrings they previously granted; the cascade rules are
4446
+ * a `db.revoke` concern, not `db.updateUser`).
4447
+ * - Tier-2 slots NOT dropped — the wrapping is unaffected.
4448
+ *
4449
+ * Role-elevation guard: BOTH the old and new role must satisfy
4450
+ * `db.grant`'s hierarchy. Owner can do anything; admin manages
4451
+ * admin/operator/viewer/client laterally; admin cannot promote to
4452
+ * owner OR demote from owner. The guard runs regardless of the
4453
+ * `update-user` policy gate's settings — gates can only be more
4454
+ * permissive than the structural floor, never less.
4455
+ *
4456
+ * Gated by `update-user`. `STRICT_POLICY` requires a TOTP/email-OTP
4457
+ * factor proof so the operator affirmatively re-asserts identity at
4458
+ * the moment of mutation; `PERSONAL_POLICY` accepts a tier-1 unlock
4459
+ * alone.
4460
+ *
4461
+ * ```ts
4462
+ * await db.updateUser('acme', {
4463
+ * userId: 'bob',
4464
+ * role: 'operator', // promote
4465
+ * permissions: { invoices: 'rw' },
4466
+ * }, { factors: [{ kind: 'totp' }] })
4467
+ * ```
4468
+ *
4469
+ * @throws `NoAccessError` when no keyring exists for the target.
4470
+ * @throws `PermissionDeniedError` when the role hierarchy rejects.
4471
+ * @throws `ValidationError` when no field is provided.
4472
+ *
4473
+ * @see #54
4474
+ */
4475
+ updateUser(vault: string, options: UpdateUserOptions, factors?: FactorProofBundle): Promise<void>;
4476
+ /**
4477
+ * Rotate the DEKs for the given collections in a vault.
4478
+ *
4479
+ * Generates fresh DEKs, re-encrypts every record in each collection,
4480
+ * and re-wraps the new DEKs into every remaining user's keyring. The
4481
+ * old DEKs become unreachable — useful as a defense-in-depth measure
4482
+ * after a suspected key leak, or as the scheduled half of a
4483
+ * key-rotation policy.
4484
+ *
4485
+ * Unlike `revoke({ rotateKeys: true })`, this call does NOT remove
4486
+ * any users — every current member keeps access, but with fresh
3958
4487
  * keys. This is the "just rotate" path; the "revoke and rotate"
3959
4488
  * path still lives in `revoke()`.
3960
4489
  *
@@ -4076,8 +4605,17 @@ declare class Noydb {
4076
4605
  * ```
4077
4606
  */
4078
4607
  queryAcross<T>(vaultIds: string[], fn: (vault: Vault) => Promise<T>, options?: QueryAcrossOptions): Promise<QueryAcrossResult<T>[]>;
4079
- /** Change the current user's passphrase for a vault. */
4080
- changeSecret(vault: string, newPassphrase: string): Promise<void>;
4608
+ /**
4609
+ * Change the current user's passphrase for a vault.
4610
+ *
4611
+ * Validates the new passphrase against the strength rules. Pass
4612
+ * `{ allowWeakPassphrase: true }` to skip — typically only useful for
4613
+ * fixtures and migrations. Pass a `PassphrasePolicy` to override the
4614
+ * default rules (e.g. consumer-tunable `pattern` / `customValidator`).
4615
+ */
4616
+ changeSecret(vault: string, newPassphrase: string, options?: PassphrasePolicy & {
4617
+ allowWeakPassphrase?: boolean;
4618
+ }): Promise<void>;
4081
4619
  /** Push local changes to remote for a vault. */
4082
4620
  push(vault: string, options?: PushOptions): Promise<PushResult>;
4083
4621
  /** Pull remote changes to local for a vault. */
@@ -4109,6 +4647,16 @@ declare class Noydb {
4109
4647
  * which batches push/pull across sync peers.
4110
4648
  */
4111
4649
  transaction<T>(fn: (tx: TxContext) => Promise<T> | T): Promise<T>;
4650
+ /**
4651
+ * Open an amendment-mode transaction. Requires `admin` or `owner`
4652
+ * role on every vault touched by the body; throws
4653
+ * `AmendmentForbiddenError` on first non-privileged `tx.vault(name)`
4654
+ * call. Guard `check` callbacks are SKIPPED inside an amendment —
4655
+ * the staged change-set is fed to each guard's `amendment.invariant`
4656
+ * after the body returns, and the multi-record summary is appended
4657
+ * to the vault's ledger as `op: 'amendment'`.
4658
+ */
4659
+ transaction<T>(options: AmendmentTxOptions, fn: (tx: TxContext) => Promise<T> | T): Promise<T>;
4112
4660
  /**
4113
4661
  * Create a sync transaction for the given vault.
4114
4662
  * The vault must already be open via `openVault()`.
@@ -4124,8 +4672,52 @@ declare class Noydb {
4124
4672
  * @internal
4125
4673
  */
4126
4674
  get _store(): NoydbStore;
4675
+ /**
4676
+ * Currently-running multi-record transaction, or `null` outside
4677
+ * Phase 2. `Collection.dispatchDerivations` consults this so a
4678
+ * recursive derived-output write inside `Collection.put` can register
4679
+ * its envelope onto `ctx._executed` and roll back with the main
4680
+ * staged ops on mid-batch failure (#133).
4681
+ *
4682
+ * @internal
4683
+ */
4684
+ get _activeTxContextOrNull(): TxContext | null;
4685
+ /**
4686
+ * Called by `runTransaction` at Phase 2 start, and by
4687
+ * `Collection.putManyAtomic` (via `derivationSource.setActiveTxContext`)
4688
+ * for its own Phase 2 loop. Nested or concurrent (non-nested)
4689
+ * transactions on the same Noydb instance are NOT supported —
4690
+ * overwriting an active context means another transaction is still
4691
+ * running and its `_executed` list would be cross-contaminated by
4692
+ * the nested writes. We tolerate the overwrite (best-effort, no
4693
+ * throw) to keep the rare interleaving from breaking consumers who
4694
+ * currently get lucky with timing, but applications should ensure
4695
+ * their multi-record commits are serialised on a single Noydb.
4696
+ *
4697
+ * @internal
4698
+ */
4699
+ _setActiveTxContext(ctx: TxContext): void;
4700
+ /**
4701
+ * Factory for a transient `TxContext` bound to this Noydb. Used by
4702
+ * `Collection.putManyAtomic` (via `derivationSource.createTxContext`)
4703
+ * to publish an active context for the duration of its bulk-atomic
4704
+ * Phase 2 loop, so recursive derivation-output writes register on
4705
+ * `ctx._executed` and roll back together with the source ops (#133).
4706
+ *
4707
+ * @internal
4708
+ */
4709
+ _createTxContext(): TxContext;
4710
+ /**
4711
+ * Called by `runTransaction` in its `finally`. Only clears when the
4712
+ * passed ctx matches the active one — a defensive no-op if some
4713
+ * other code path already cleared it.
4714
+ *
4715
+ * @internal
4716
+ */
4717
+ _clearActiveTxContext(ctx: TxContext): void;
4127
4718
  /** Get sync status for a vault. */
4128
4719
  syncStatus(vault: string): SyncStatus;
4720
+ private requireShamirProvider;
4129
4721
  private getSyncEngine;
4130
4722
  on<K extends keyof NoydbEventMap>(event: K, handler: (data: NoydbEventMap[K]) => void): void;
4131
4723
  off<K extends keyof NoydbEventMap>(event: K, handler: (data: NoydbEventMap[K]) => void): void;
@@ -4181,6 +4773,27 @@ declare class Noydb {
4181
4773
  * change is fundamentally a privilege-management action).
4182
4774
  */
4183
4775
  updatePolicy(vault: string, override: Partial<VaultPolicy>): Promise<VaultPolicy>;
4776
+ /**
4777
+ * Read the current vault-level user-directory toggle (#122). Returns
4778
+ * the default-on shape (`{ enabled: true }`) when no `_meta/directory`
4779
+ * document has been persisted yet.
4780
+ *
4781
+ * No role gate — anyone who can open the vault can read the toggle.
4782
+ */
4783
+ getDirectoryEnabled(vault: string): Promise<boolean>;
4784
+ /**
4785
+ * Toggle the vault's user-directory listing on or off (#122).
4786
+ * Owner-only. When disabled, `listUsersWithEnvelopes()` throws
4787
+ * {@link import('./errors.js').DirectoryDisabledError} for callers
4788
+ * whose role is neither `owner` nor `admin`.
4789
+ *
4790
+ * Honest caveat: this is a UX flag, not a privacy guarantee. The
4791
+ * keyring file at `_keyring/<userId>` and the envelope ciphertext at
4792
+ * `_users/<keyringId>` remain observable to anyone with direct store
4793
+ * read access — only the hub-level enumeration is gated. See
4794
+ * `docs/subsystems/user-envelope.md` → "Directory visibility".
4795
+ */
4796
+ setDirectoryEnabled(vault: string, enabled: boolean): Promise<void>;
4184
4797
  /**
4185
4798
  * Evaluate a policy gate against the active session tier and the
4186
4799
  * presented factor proofs. Throws {@link PolicyDeniedError} on
@@ -4191,22 +4804,33 @@ declare class Noydb {
4191
4804
  * or app-defined (`app:*`).
4192
4805
  * @param presented Caller-supplied factor proofs.
4193
4806
  */
4194
- checkGate(vault: string, gate: GateName, presented?: {
4195
- factors?: ReadonlyArray<FactorProof>;
4196
- sharedDevice?: boolean;
4197
- }): Promise<void>;
4807
+ checkGate(vault: string, gate: GateName, factors?: FactorProofBundle): Promise<void>;
4198
4808
  /** Read or persist the vault policy at `_meta/policy` on first open. */
4199
4809
  private bootstrapPolicy;
4200
4810
  /**
4201
- * Throw {@link RecoveryNotEnrolledError} when the developer
4202
- * explicitly opts into strict mandatory-recovery enforcement
4203
- * (`createNoydb({ requireRecovery: true })`) and no recovery
4204
- * entries are persisted.
4811
+ * Throw {@link RecoveryNotEnrolledError} or
4812
+ * {@link ManagedRecoveryNotEnrolledError} when recovery enrollment
4813
+ * is missing.
4814
+ *
4815
+ * Two enforcement modes:
4205
4816
  *
4206
- * The default behavior is lenient — `recover-passphrase` is enabled
4207
- * in `PERSONAL_POLICY` but the hub does not block vault open on
4208
- * missing enrollment. v1.0 will flip the default to strict; for now,
4209
- * apps that want the spec-mandated check turn it on per-vault.
4817
+ * 1. **Managed-mode mandatory strong-recovery (#195).** When
4818
+ * `passphraseMode === 'managed'`, the vault MUST have at least
4819
+ * one **strong** recovery profile (Shamir today). Paper alone is
4820
+ * rejected because under managed mode the user has no memorized
4821
+ * passphrase, so losing the paper sheet = losing every record.
4822
+ * This check is unconditional — independent of `requireRecovery`
4823
+ * and the `recover-passphrase` gate.
4824
+ *
4825
+ * 2. **Opt-in strict mandatory-recovery.** When
4826
+ * `requireRecovery: true` is set on createNoydb (and the gate is
4827
+ * not explicitly disabled), require ANY recovery profile (paper
4828
+ * or shamir). This is the v0.x default-off behavior; v1.0 may
4829
+ * flip it default-on.
4830
+ *
4831
+ * The managed-mode check fires from {@link bootstrapPolicy} unless
4832
+ * the `skipManagedCheck` flag is set (used by
4833
+ * {@link openVaultAndEnrollRecovery} to allow atomic create-and-enroll).
4210
4834
  */
4211
4835
  private assertRecoveryEnrolled;
4212
4836
  /**
@@ -4227,21 +4851,44 @@ declare class Noydb {
4227
4851
  * Gated by `enroll-authenticator`; `presented` carries any factor
4228
4852
  * proofs the active policy demands.
4229
4853
  */
4230
- enrollAuthenticator(vault: string, options: EnrollAuthenticatorOptions, presented?: {
4231
- factors?: ReadonlyArray<FactorProof>;
4232
- sharedDevice?: boolean;
4233
- }): Promise<void>;
4854
+ enrollAuthenticator(vault: string, options: EnrollAuthenticatorOptions, factors?: FactorProofBundle): Promise<void>;
4234
4855
  /**
4235
4856
  * Remove a tier-2 authenticator slot. Idempotent — removing a
4236
4857
  * non-existent slot is a successful no-op. Gated by
4237
4858
  * `remove-authenticator`.
4238
4859
  */
4239
- removeAuthenticator(vault: string, slotId: string, presented?: {
4240
- factors?: ReadonlyArray<FactorProof>;
4241
- sharedDevice?: boolean;
4242
- }): Promise<void>;
4860
+ removeAuthenticator(vault: string, slotId: string, factors?: FactorProofBundle): Promise<void>;
4243
4861
  /** Read the slot list for a vault. Internal — `describeAuthConfig` (#13) consumes this. */
4244
4862
  listAuthenticators(vault: string): Promise<ReadonlyArray<KeyringAuthenticator>>;
4863
+ /**
4864
+ * Mutate the `meta` blob on an existing authenticator slot — slot
4865
+ * rename, label change, attachment of UI hints. The slot's `id`,
4866
+ * `method`, and wrap material (`wrapped_kek` / `wrapped_deks` + `iv`)
4867
+ * are immutable through this method. Anti-slot-swap is structural,
4868
+ * not gate-driven.
4869
+ *
4870
+ * `meta` patch semantics (#57-aligned):
4871
+ * - Top-level merge — absent keys preserved
4872
+ * - `null` value — delete that meta key
4873
+ * - Other values — replace verbatim
4874
+ *
4875
+ * Use case: per-slot nickname for "iPhone Touch ID" vs "MacBook
4876
+ * Touch ID" disambiguation in admin UIs. The slot id (auto-derived
4877
+ * from credentialId prefix) is not human-friendly; `meta.nickname`
4878
+ * is.
4879
+ *
4880
+ * Gated by `update-authenticator`. PERSONAL_POLICY: tier-1 unlock
4881
+ * alone (matches enroll/remove). STRICT_POLICY: tier-1 +
4882
+ * TOTP/email-OTP factor proof — a malicious rename on a shared
4883
+ * workstation could mislead the user about which device a slot
4884
+ * corresponds to, so STRICT requires fresh factor binding.
4885
+ *
4886
+ * @throws `NoAccessError` when no slot with the given id exists.
4887
+ * @throws `ValidationError` when no patch field is provided.
4888
+ *
4889
+ * @see #55
4890
+ */
4891
+ updateAuthenticator(vault: string, slotId: string, options: UpdateAuthenticatorOptions, factors?: FactorProofBundle): Promise<void>;
4245
4892
  /**
4246
4893
  * Native WebAuthn enrollment using the **real** internal keyring (#16).
4247
4894
  *
@@ -4289,10 +4936,7 @@ declare class Noydb {
4289
4936
  *
4290
4937
  * @see #16
4291
4938
  */
4292
- enrollWebAuthn(vault: string, ceremony: (keyring: UnlockedKeyring) => Promise<EnrollAuthenticatorOptions>, presented?: {
4293
- factors?: ReadonlyArray<FactorProof>;
4294
- sharedDevice?: boolean;
4295
- }): Promise<{
4939
+ enrollWebAuthn(vault: string, ceremony: (keyring: UnlockedKeyring) => Promise<EnrollAuthenticatorOptions>, factors?: FactorProofBundle): Promise<{
4296
4940
  credentialId: string;
4297
4941
  }>;
4298
4942
  /**
@@ -4353,15 +4997,9 @@ declare class Noydb {
4353
4997
  * disabled). Sanitization is allowlist-based — never renders cred
4354
4998
  * ids, password hashes, secrets, or any field outside the allowlist.
4355
4999
  */
4356
- describeUserAuth(vault: string, userId: string, factors?: {
4357
- factors?: ReadonlyArray<FactorProof>;
4358
- sharedDevice?: boolean;
4359
- }): Promise<string>;
5000
+ describeUserAuth(vault: string, userId: string, factors?: FactorProofBundle): Promise<string>;
4360
5001
  /** Bulk variant for owner dashboards. Gated by `view-user-auth`. */
4361
- describeAllUsersAuth(vault: string, factors?: {
4362
- factors?: ReadonlyArray<FactorProof>;
4363
- sharedDevice?: boolean;
4364
- }): Promise<Array<{
5002
+ describeAllUsersAuth(vault: string, factors?: FactorProofBundle): Promise<Array<{
4365
5003
  userId: string;
4366
5004
  description: string;
4367
5005
  }>>;
@@ -4379,10 +5017,7 @@ declare class Noydb {
4379
5017
  * @throws `PolicyDeniedError` when the gate denies (missing factor, …).
4380
5018
  * @throws `InvalidKeyError` when `oldPassphrase` is wrong.
4381
5019
  */
4382
- rotatePassphrase(vault: string, input: RotatePassphraseInput, factors?: {
4383
- factors?: ReadonlyArray<FactorProof>;
4384
- sharedDevice?: boolean;
4385
- }): Promise<void>;
5020
+ rotatePassphrase(vault: string, input: RotatePassphraseInput, factors?: FactorProofBundle): Promise<void>;
4386
5021
  /**
4387
5022
  * Reset the passphrase using a recovery proof (user forgot the old).
4388
5023
  * v0.1.0-pre.5 supports the `'paper'` profile end-to-end; the
@@ -4390,10 +5025,124 @@ declare class Noydb {
4390
5025
  *
4391
5026
  * Burns the used recovery entry on success.
4392
5027
  */
4393
- recoverPassphrase(vault: string, input: RecoverPassphraseInput, factors?: {
4394
- factors?: ReadonlyArray<FactorProof>;
4395
- sharedDevice?: boolean;
4396
- }): Promise<RecoverPassphraseResult>;
5028
+ recoverPassphrase(vault: string, input: RecoverPassphraseInput, factors?: FactorProofBundle): Promise<RecoverPassphraseResult>;
5029
+ /**
5030
+ * Deliberate paper-recovery-code regeneration (#121). User knows their
5031
+ * passphrase but wants a fresh sheet — they lost the printout or
5032
+ * suspect compromise of the off-site copy.
5033
+ *
5034
+ * Symmetric to {@link rotatePassphrase} for the recovery profile:
5035
+ * gated, audit-trackable, ergonomic. Replaces (not appends) the
5036
+ * paper sheet under `_meta/recovery-paper` in a single envelope `put`.
5037
+ *
5038
+ * Gated by the `rotate-recovery` policy gate:
5039
+ * - PERSONAL_POLICY: `{ minTier: 1 }` — knowing the passphrase
5040
+ * suffices, matching the pre-#121 low-level flow's bar.
5041
+ * - STRICT_POLICY: `{ minTier: 1, factors: [{ anyOf: ['totp',
5042
+ * 'email-otp', 'webauthn-roaming'] }] }` — rotation is an
5043
+ * off-site-trust event; require an off-device factor so a
5044
+ * stolen unlocked laptop cannot silently mint a sheet for the
5045
+ * attacker.
5046
+ *
5047
+ * Defaults `count` to the existing sheet size so consumers aren't
5048
+ * surprised by a different code count. Explicit `count` overrides.
5049
+ *
5050
+ * @throws {@link RecoveryProfileNotImplementedError} when `profile`
5051
+ * is anything other than `'paper'` (v1 dispatch limit).
5052
+ * @throws {@link PolicyDeniedError} when the gate denies (missing
5053
+ * factor, tier mismatch, ...).
5054
+ * @throws on missing paper sheet — "nothing to rotate" surfaces as
5055
+ * an error rather than silently minting an entire new sheet.
5056
+ *
5057
+ * @example Default count + show-once UI
5058
+ * ```ts
5059
+ * const { newCodes } = await db.rotateRecovery('acme', { profile: 'paper' })
5060
+ * showCodesToUser(newCodes)
5061
+ * ```
5062
+ *
5063
+ * @example STRICT-policy site with TOTP factor proof
5064
+ * ```ts
5065
+ * await db.rotateRecovery(
5066
+ * 'acme',
5067
+ * { profile: 'paper', count: 10 },
5068
+ * { factors: [{ kind: 'totp', proof: '123456' }] },
5069
+ * )
5070
+ * ```
5071
+ */
5072
+ rotateRecovery(vault: string, options: RotateRecoveryOptions, factors?: FactorProofBundle): Promise<RotateRecoveryResult>;
5073
+ private rotateRecoveryPaper;
5074
+ private rotateRecoveryShamir;
5075
+ /**
5076
+ * **Atomic create-and-enroll for managed-mode vaults (#195).**
5077
+ *
5078
+ * Bootstraps a managed-mode vault and enrolls strong recovery in
5079
+ * a single ceremony. Under `passphraseMode: 'managed'`, every
5080
+ * `openVault` call requires a strong recovery profile (Shamir
5081
+ * today) to be enrolled — otherwise it throws
5082
+ * {@link ManagedRecoveryNotEnrolledError}. This method bypasses
5083
+ * the check temporarily so the keyring can be created, enrolls
5084
+ * the supplied recovery profile(s), then returns the vault.
5085
+ *
5086
+ * For Shamir enrollments, the show-once share strings come back
5087
+ * in `recoveryEnrollments[i].shares`. The hub never retains them
5088
+ * — the caller MUST display them to the user (once) before any
5089
+ * subsequent operation.
5090
+ *
5091
+ * Paper alone is NOT a strong profile under managed mode; passing
5092
+ * `{ profile: 'paper', ... }` without an accompanying shamir entry
5093
+ * is rejected at validation time.
5094
+ *
5095
+ * ```ts
5096
+ * const db = await createNoydb({
5097
+ * store, user: 'alice',
5098
+ * passphraseMode: 'managed',
5099
+ * sealingKey: macosKeychainSealingProvider({ ... }),
5100
+ * })
5101
+ *
5102
+ * const { vault, recoveryEnrollments } = await db.openVaultAndEnrollRecovery('acme', {
5103
+ * recovery: [{ profile: 'shamir', k: 2, n: 3 }],
5104
+ * })
5105
+ * for (const r of recoveryEnrollments) {
5106
+ * if (r.shares) showSharesToUser(r.shares) // ONCE
5107
+ * }
5108
+ * ```
5109
+ *
5110
+ * @throws ValidationError if recovery is empty, or contains no
5111
+ * strong profile under managed mode.
5112
+ */
5113
+ openVaultAndEnrollRecovery(vault: string, opts: {
5114
+ readonly recovery: ReadonlyArray<RecoveryEnrollmentInput>;
5115
+ readonly locale?: string;
5116
+ }): Promise<{
5117
+ readonly vault: Vault;
5118
+ readonly recoveryEnrollments: ReadonlyArray<EnrollRecoveryResult>;
5119
+ }>;
5120
+ /**
5121
+ * **Recovery flow under managed-passphrase mode (#195).**
5122
+ *
5123
+ * Replaces the sealed passphrase of a managed-mode vault with a
5124
+ * fresh 256-bit random, sealed under the configured
5125
+ * `SealingKeyProvider`. The user never sees the new passphrase.
5126
+ *
5127
+ * Internally:
5128
+ * 1. Verify the recovery proof (Shamir today) and unwrap the
5129
+ * DEK set.
5130
+ * 2. Mint a fresh 256-bit random as the new effective passphrase.
5131
+ * 3. Rewrap the DEK set under a fresh KEK derived from the new
5132
+ * passphrase (via the existing `recoverPassphrase` path).
5133
+ * 4. Seal the random bytes under the provider and overwrite
5134
+ * `_meta/sealed-passphrase`.
5135
+ * 5. Drop the keyring cache so the next operation re-derives.
5136
+ *
5137
+ * The vault's strong-recovery enrollment is preserved across
5138
+ * recovery (Shamir entries are not burned on use — see #196).
5139
+ *
5140
+ * @throws ValidationError if the Noydb instance is not in managed mode.
5141
+ */
5142
+ recoverManagedPassphrase(vault: string, options: {
5143
+ readonly recoveryProof: RecoveryProof;
5144
+ readonly passphrasePolicy?: PassphrasePolicy;
5145
+ }): Promise<void>;
4397
5146
  /**
4398
5147
  * Atomic peer-recovery — re-wraps an EXISTING user's keyring under
4399
5148
  * a fresh temp passphrase in a single store write. Closes #34's
@@ -4437,10 +5186,7 @@ declare class Noydb {
4437
5186
  *
4438
5187
  * @see #33 #34 — the issues this method closes.
4439
5188
  */
4440
- recoverUser(vault: string, options: RecoverUserOptions, factors?: {
4441
- factors?: ReadonlyArray<FactorProof>;
4442
- sharedDevice?: boolean;
4443
- }): Promise<void>;
5189
+ recoverUser(vault: string, options: RecoverUserOptions, factors?: FactorProofBundle): Promise<void>;
4444
5190
  /**
4445
5191
  * Persist a recovery enrollment. v0.1.0-pre.5 accepts the `'paper'`
4446
5192
  * profile.
@@ -4472,13 +5218,11 @@ declare class Noydb {
4472
5218
  * await db.enrollRecovery('acme', { profile: 'paper', entries })
4473
5219
  * ```
4474
5220
  */
4475
- enrollRecovery(vault: string, enrollment: {
4476
- profile: 'paper';
4477
- entries: ReadonlyArray<PaperRecoveryEntry>;
4478
- }): Promise<void>;
4479
- /** Read the persisted paper-recovery entries. Used by `describeAuthConfig` (#13). */
5221
+ enrollRecovery(vault: string, enrollment: RecoveryEnrollmentInput): Promise<EnrollRecoveryResult>;
5222
+ /** Read the persisted recovery entries (paper + Shamir). Used by `describeAuthConfig` (#13). */
4480
5223
  listRecoveryEntries(vault: string): Promise<{
4481
5224
  paper: ReadonlyArray<PaperRecoveryEntry>;
5225
+ shamir: ReadonlyArray<ShamirRecoveryEntry>;
4482
5226
  }>;
4483
5227
  /**
4484
5228
  * Register a tier-3 quick-unlock state for the vault. The state is
@@ -4489,10 +5233,7 @@ declare class Noydb {
4489
5233
  * Gated by `rotate-unlock` (the same gate covers "set" and "rotate"
4490
5234
  * because tier-3 is a single-slot rolling secret).
4491
5235
  */
4492
- enrollUnlock(vault: string, state: QuickUnlockState, presented?: {
4493
- factors?: ReadonlyArray<FactorProof>;
4494
- sharedDevice?: boolean;
4495
- }): Promise<void>;
5236
+ enrollUnlock(vault: string, state: QuickUnlockState, factors?: FactorProofBundle): Promise<void>;
4496
5237
  /**
4497
5238
  * Resume a session via the registered tier-3 state. The verifier is
4498
5239
  * `@noy-db/on-pin/resumePin` (or compatible). On success, mark the
@@ -4508,8 +5249,17 @@ declare class Noydb {
4508
5249
  /**
4509
5250
  * Public accessor for the unlocked keyring of a vault — issue #28.
4510
5251
  *
4511
- * Returns the cached `UnlockedKeyring` (already in memory after
4512
- * `createNoydb` + first vault touch); loads it on demand if absent.
5252
+ * Returns a **defensive shallow copy** so consumers can read the DEK
5253
+ * map and authenticator list without the risk of mutating the hub's
5254
+ * internal cache (#88). Internal hub code paths use a live reference
5255
+ * via `getKeyringInternal`; ceremonies and external consumers always
5256
+ * get a snapshot.
5257
+ *
5258
+ * The CryptoKey values inside `deks` are not cloned — Web Crypto
5259
+ * keys are opaque handles, and a shared handle is intentional
5260
+ * (encrypt / decrypt go through the same key the cache holds).
5261
+ * Only the container Map / authenticator array is fresh.
5262
+ *
4513
5263
  * Used by `@noy-db/on-*` ceremonies that need the live DEK set
4514
5264
  * (paper recovery via {@link mintPaperRecoveryEntry}, tier-3 PIN
4515
5265
  * enrolment via on-pin's `enrollPin`, custom on-* ceremonies that
@@ -4524,11 +5274,19 @@ declare class Noydb {
4524
5274
  * ```ts
4525
5275
  * const keyring = await db.getKeyring('acme')
4526
5276
  * // keyring.deks: Map<collection, CryptoKey>
4527
- * // keyring.kek: CryptoKey (non-extractable; null for tier-3 sessions)
5277
+ * // keyring.kek: CryptoKey | null (null for tier-3 / wrap-DEKs sessions)
4528
5278
  * // keyring.role / .permissions / .authenticators
4529
5279
  * ```
4530
5280
  */
4531
5281
  getKeyring(vault: string): Promise<UnlockedKeyring>;
5282
+ /**
5283
+ * Live-reference variant used by the hub's own code paths. Internal
5284
+ * mutations on `deks` (e.g. {@link ensureCollectionDEK} adding a
5285
+ * collection key) need to land on the cached keyring so subsequent
5286
+ * accesses see them. Not exposed publicly — callers outside hub
5287
+ * should use {@link getKeyring}, which returns a defensive copy.
5288
+ */
5289
+ private getKeyringInternal;
4532
5290
  }
4533
5291
  /** Create a new NOYDB instance. */
4534
5292
  declare function createNoydb(options: NoydbOptions): Promise<Noydb>;
@@ -4607,18 +5365,771 @@ interface IssueDelegationOptions {
4607
5365
  */
4608
5366
  declare function issueDelegation(store: NoydbStore, vault: string, grantor: UnlockedKeyring, targetKek: CryptoKey | null, delegationsDek: CryptoKey, opts: IssueDelegationOptions): Promise<DelegationToken>;
4609
5367
  /**
4610
- * Enumerate every live (non-expired) delegation addressed to `toUser`
4611
- * and merge the unwrapped tier DEKs into their keyring. Returns the
4612
- * list of merged delegations so the caller can register per-access
4613
- * audit context.
5368
+ * Enumerate every live (non-expired) delegation addressed to `toUser`
5369
+ * and merge the unwrapped tier DEKs into their keyring. Returns the
5370
+ * list of merged delegations so the caller can register per-access
5371
+ * audit context.
5372
+ */
5373
+ declare function loadActiveDelegations(store: NoydbStore, vault: string, user: UnlockedKeyring, delegationsDek: CryptoKey, now?: Date): Promise<DelegationToken[]>;
5374
+ /**
5375
+ * Revoke a delegation by id — the caller resolves the envelope and
5376
+ * issues a `delete`. Provided as a stable helper so the naming is
5377
+ * symmetric to `issueDelegation`.
5378
+ */
5379
+ declare function revokeDelegation(store: NoydbStore, vault: string, id: string): Promise<void>;
5380
+
5381
+ /**
5382
+ * Minimum read surface exposed to guard `check` functions. Intentionally
5383
+ * narrow — guards can read other collections but never write.
5384
+ *
5385
+ * `query()` returns the same chainable builder used elsewhere. `Query<T>`
5386
+ * has no write terminals (no `.update()` / `.delete()`) so exposing it
5387
+ * here preserves the read-only contract while letting guards aggregate
5388
+ * with `.where().aggregate()` / `.groupBy()` / `.join()` instead of
5389
+ * decrypting every sibling row via `.list()`.
5390
+ */
5391
+ interface ReadOnlyVaultFacade$1 {
5392
+ collection<T = unknown>(name: string): {
5393
+ get(id: string): Promise<T | null>;
5394
+ list(): Promise<T[]>;
5395
+ query(): Query<T>;
5396
+ };
5397
+ }
5398
+ /**
5399
+ * Runtime context passed to `check` and `invariant` callbacks.
5400
+ * `existing` is the currently-persisted record (null for inserts).
5401
+ */
5402
+ interface GuardContext<T> {
5403
+ existing: T | null;
5404
+ vault: ReadOnlyVaultFacade$1;
5405
+ userId: string;
5406
+ role: Role;
5407
+ }
5408
+ /**
5409
+ * One {before, after} pair handed to an `invariant` function. `before`
5410
+ * is null for inserts; `after` reflects the proposed post-commit record.
5411
+ */
5412
+ interface GuardChange<T> {
5413
+ before: T | null;
5414
+ after: T;
5415
+ }
5416
+ /** @internal — output of {@link withGuard}. */
5417
+ interface GuardStrategyHandle<T extends Record<string, unknown>> {
5418
+ readonly __noydb_strategy: 'guard';
5419
+ readonly spec: GuardStrategy<T>;
5420
+ }
5421
+ /**
5422
+ * Existential erasure of `GuardStrategyHandle<T>` — used as the
5423
+ * element type of `ReadonlyArray<>` fields where the per-handle T
5424
+ * differs (e.g. `guardStrategies: [invoiceGuard, disbursementGuard]`).
5425
+ *
5426
+ * Background: `GuardStrategyHandle<T>` is INVARIANT in T because T
5427
+ * appears in callback positions on the spec (`check(incoming: T, ctx)`,
5428
+ * `invariant(changes: ReadonlyArray<GuardChange<T>>, ctx)`). So
5429
+ * `Handle<Invoice>` is not assignable to `Handle<Record<string, unknown>>`.
5430
+ * A bounded existential ("there exists some T satisfying the constraint
5431
+ * such that this is a Handle<T>") is the right shape; TypeScript has
5432
+ * no first-class existentials, so we fake it with a structurally narrow
5433
+ * interface that ERASES T from both the discriminant and the spec.
5434
+ *
5435
+ * Consumers continue to construct typed handles via `withGuard<T>(...)`
5436
+ * which returns `GuardStrategyHandle<T>`. Both `Handle<Invoice>` and
5437
+ * `Handle<Disbursement>` structurally assign to `GuardStrategyHandleAny`,
5438
+ * so an array of them is `GuardStrategyHandleAny[]`.
5439
+ *
5440
+ * Internal code that needs T re-narrows via the runtime discriminant
5441
+ * (`__noydb_strategy === 'guard'`) plus per-handle type information
5442
+ * carried by the registry.
5443
+ *
5444
+ * NOT exported from the public barrel — keeping this internal
5445
+ * discourages consumers from constructing it directly. Used only as
5446
+ * the array-element type on `Vault` / `NoydbOptions.guardStrategies`.
5447
+ *
5448
+ * @internal
5449
+ */
5450
+ interface GuardStrategyHandleAny {
5451
+ readonly __noydb_strategy: 'guard';
5452
+ readonly spec: GuardStrategy<any>;
5453
+ }
5454
+ /** Public registration shape. See `withGuard()`. */
5455
+ interface GuardStrategy<T extends Record<string, unknown>> {
5456
+ collection: string;
5457
+ /**
5458
+ * Fires on `Collection.put` (insert + update). The `incoming` argument
5459
+ * is the record being written. Throw to cancel the put.
5460
+ *
5461
+ * Does NOT fire on `Collection.delete` — use {@link onDelete} for
5462
+ * delete-time validation. Skipped during an amendment transaction
5463
+ * (`db.transaction({ amendment: true })`) — admin/owner override.
5464
+ */
5465
+ check?: (incoming: T, ctx: GuardContext<T>) => Promise<void> | void;
5466
+ /**
5467
+ * Fires on user-initiated `Collection.delete` before the adapter
5468
+ * delete and before the ledger append. The `existing` argument is
5469
+ * the currently-persisted record. Throw to cancel the delete — no
5470
+ * partial state, no tombstone ledger entry.
5471
+ *
5472
+ * Skipped during an amendment transaction (admin/owner override) —
5473
+ * amendments are the unlock primitive. To make a delete TRULY
5474
+ * unconditional (e.g. legal-document immutability rules), pair
5475
+ * `onDelete` with an `amendment.invariant` that re-throws on any
5476
+ * `before !== null && after === null` change:
5477
+ *
5478
+ * ```ts
5479
+ * withGuard<Receipt>({
5480
+ * collection: 'receipts',
5481
+ * onDelete: () => { throw new RecordLockedError(...) },
5482
+ * amendment: {
5483
+ * roles: ['admin', 'owner'],
5484
+ * invariant: (changes) => {
5485
+ * for (const c of changes) {
5486
+ * if (c.before !== null && c.after === null) {
5487
+ * throw new RecordLockedError(...) // wrapped as InvariantError
5488
+ * }
5489
+ * }
5490
+ * },
5491
+ * },
5492
+ * })
5493
+ * ```
5494
+ *
5495
+ * Also skipped on system-internal deletes (derivation tombstones from
5496
+ * #144, MV refresh from Dim 14 v2) — those use `_internalDelete`
5497
+ * which bypasses every user-facing delete hook. Housekeeping ops are
5498
+ * NOT user-initiated and should not trip user invariants.
5499
+ *
5500
+ * Delete of an absent record is a no-op and does not consult any
5501
+ * guard, matching the idempotent-delete contract.
5502
+ */
5503
+ onDelete?: (existing: T, ctx: GuardContext<T>) => Promise<void> | void;
5504
+ frozenFields?: {
5505
+ when: (existing: T) => boolean;
5506
+ fields: ReadonlyArray<keyof T>;
5507
+ };
5508
+ amendment?: {
5509
+ roles: ReadonlyArray<'admin' | 'owner'>;
5510
+ invariant: (changes: ReadonlyArray<GuardChange<T>>, ctx: GuardContext<T>) => Promise<void> | void;
5511
+ };
5512
+ }
5513
+
5514
+ /**
5515
+ * Runtime context handed to `derive(source, ctx)`. Mirrors `GuardContext`'s
5516
+ * narrow shape: read-only vault access, no write capability, no
5517
+ * transaction handle. Determinism is the consumer's responsibility — the
5518
+ * strategy hash includes `derive.toString()`, so the source string fixes
5519
+ * the function's inputs; whatever sibling reads `derive` performs must
5520
+ * yield the same outputs for the same source.
5521
+ */
5522
+ interface DerivationContext {
5523
+ vault: ReadOnlyVaultFacade$1;
5524
+ }
5525
+ /**
5526
+ * Metadata that travels inside the `_data` payload of a derived record.
5527
+ * Lives in encrypted payload, not in the unencrypted envelope — the
5528
+ * storage backend cannot infer the derivation graph from listing.
5529
+ */
5530
+ interface DerivedFromMeta {
5531
+ /** Source collection name. */
5532
+ readonly source: string;
5533
+ /** Source record id. */
5534
+ readonly sourceId: string;
5535
+ /** `_v` of the source at derivation time. */
5536
+ readonly sourceVersion: number;
5537
+ /** ISO timestamp when this output was derived. */
5538
+ readonly derivedAt: string;
5539
+ /**
5540
+ * SHA-256 of (source + outputs map keys + derive function source).
5541
+ * Changes when the strategy changes → forces `vault.deriveAll` to
5542
+ * recompute on next visit.
5543
+ */
5544
+ readonly strategyHash: string;
5545
+ }
5546
+ /** Record-shape output — one source row produces (optionally) one output row at the source's id. */
5547
+ interface RecordOutputSpec {
5548
+ shape: 'record';
5549
+ collection: string;
5550
+ /**
5551
+ * When `true`, the `derive` function may return `null` (or
5552
+ * `undefined`) for this output key. The executor interprets that as
5553
+ * "no output for this invocation": a previously-emitted output at
5554
+ * the same id is deleted (mirroring the empty-group / empty-aggregate
5555
+ * semantics flagged in #142); a never-emitted output is a silent
5556
+ * no-op. When `false` (default), returning `null` throws
5557
+ * `DerivationOutputShapeError` — same as v1.
5558
+ */
5559
+ optional?: boolean;
5560
+ }
5561
+ /**
5562
+ * Array-shape output (#200) — one source row produces a variable-length
5563
+ * list of output rows, each with its own id (from the `key` extractor).
5564
+ *
5565
+ * On every source-row change, the dispatcher diffs the previously
5566
+ * emitted key set against the new one: removed keys are deleted via
5567
+ * `_internalDelete`, new and unchanged keys are upserted via
5568
+ * `Collection.put`. Strict-mode rollback is preserved via the existing
5569
+ * `_executed` tracking.
5570
+ *
5571
+ * Storage of the per-source-row key set lives at
5572
+ * `_meta/derivations-fanout/<source>/<sourceId>/<outputKey>` as a
5573
+ * plain JSON sidecar — keeps dispatch cost O(1) per source row.
5574
+ *
5575
+ * **Slice 1 limitation**: only `lifecycle: 'eager'` is supported.
5576
+ * Registering an array-shape output with `lifecycle: 'lazy'` throws
5577
+ * at `withDerivation` construction time.
5578
+ */
5579
+ interface ArrayOutputSpec {
5580
+ shape: 'array';
5581
+ collection: string;
5582
+ /**
5583
+ * Stable identity extractor for each derived row. Called on every
5584
+ * row returned by `derive`. The string MUST be unique within a
5585
+ * single invocation — duplicate keys throw
5586
+ * `DerivationOutputShapeError`.
5587
+ *
5588
+ * Type is intentionally `(out: Record<string, unknown>) => string`
5589
+ * (not generic) because OutputSpec is type-erased at the registry
5590
+ * level. Strategy-level inference still produces typed `out`
5591
+ * through the strategy's `outputs` map.
5592
+ */
5593
+ key: (output: Record<string, unknown>) => string;
5594
+ /**
5595
+ * Cap on derived rows per source-row invocation. Defaults to 64.
5596
+ * Raise for carry-forward cases (e.g. monthly expansion of
5597
+ * multi-year contracts). Exceeding the cap throws
5598
+ * `DerivationCapExceededError` BEFORE any writes — partial fanout
5599
+ * is never persisted.
5600
+ */
5601
+ maxFanout?: number;
5602
+ }
5603
+ /** Discriminated union — record + array. */
5604
+ type OutputSpec = RecordOutputSpec | ArrayOutputSpec;
5605
+ /**
5606
+ * Registration shape passed to `withDerivation()`.
5607
+ *
5608
+ * @typeParam TSource - the source record type
5609
+ * @typeParam TOutputs - map of output-key → output record type
5610
+ */
5611
+ interface DerivationStrategy<TSource extends Record<string, unknown>, TOutputs extends Record<string, Record<string, unknown>>> {
5612
+ /** Source collection name. */
5613
+ source: string;
5614
+ /** v1: only deterministic derivations supported. */
5615
+ deterministic: true;
5616
+ /**
5617
+ * Output declarations keyed by name. The `derive` function's return
5618
+ * value must have the same keys.
5619
+ */
5620
+ outputs: {
5621
+ [K in keyof TOutputs]: OutputSpec;
5622
+ };
5623
+ /**
5624
+ * Pure function from source to outputs. Runs on plaintext, after DEK
5625
+ * unwrap. Returns a map of named outputs. Each output is encrypted +
5626
+ * stored via the existing `Collection.put` pipeline.
5627
+ *
5628
+ * `ctx.vault` is the same `ReadOnlyVaultFacade` guards see — fetch
5629
+ * sibling records via `ctx.vault.collection<T>(name).get(id)` /
5630
+ * `.list()` / `.query()`. The vault accessor is read-only; there is
5631
+ * no path to a writer from `ctx`.
5632
+ */
5633
+ derive: (source: TSource, ctx: DerivationContext) => Promise<TOutputs> | TOutputs;
5634
+ /**
5635
+ * `'eager'` runs `derive` synchronously inside the source-write
5636
+ * transaction. `'lazy'` marks outputs stale on source-change and
5637
+ * derives on first read.
5638
+ */
5639
+ lifecycle: 'eager' | 'lazy' | {
5640
+ mode: 'eager' | 'lazy';
5641
+ maxDepth?: number;
5642
+ };
5643
+ /**
5644
+ * `true` = any output failure rolls back the source write (only with
5645
+ * `withTransactions`). `false` = isolate per-output failure, log,
5646
+ * continue. Default `false`.
5647
+ */
5648
+ strict?: boolean;
5649
+ }
5650
+ /** Returned by `withDerivation()` and consumed by `createNoydb`. */
5651
+ interface DerivationStrategyHandle {
5652
+ readonly __noydb_strategy: 'derivation';
5653
+ readonly spec: DerivationStrategy<any, any>;
5654
+ }
5655
+
5656
+ interface RegisteredStrategy {
5657
+ spec: DerivationStrategy<any, any>;
5658
+ strategyHash: string;
5659
+ }
5660
+ /**
5661
+ * Vault-internal registry of derivation strategies. Owned by `Vault`;
5662
+ * not exported.
5663
+ *
5664
+ * @internal
5665
+ */
5666
+ declare class DerivationRegistry {
5667
+ private readonly _bySource;
5668
+ private readonly _byOutput;
5669
+ register(spec: DerivationStrategy<any, any>): Promise<void>;
5670
+ strategiesForSource(source: string): ReadonlyArray<RegisteredStrategy>;
5671
+ strategiesProducingOutput(collection: string): ReadonlyArray<RegisteredStrategy>;
5672
+ /**
5673
+ * Cycle detection over the source → output → … graph. Call after all
5674
+ * `register()` calls complete (i.e. at vault open). Throws
5675
+ * `DerivationCycleError` on the first cycle found.
5676
+ */
5677
+ validate(): void;
5678
+ }
5679
+
5680
+ /**
5681
+ * Minimal vault-shaped accessor passed to the MV `query()` callback.
5682
+ * Defined as a structural interface so the strategy types don't have
5683
+ * to import the full `Vault` class (avoids a circular import). The
5684
+ * Vault implements this shape natively.
5685
+ */
5686
+ interface MVQueryContext {
5687
+ collection<T extends Record<string, unknown>>(name: string): Collection<T>;
5688
+ }
5689
+ /**
5690
+ * Metadata that travels inside the `_data` payload of a materialized
5691
+ * row. Lives in encrypted payload, not in the unencrypted envelope —
5692
+ * the storage backend cannot infer the MV graph from listing.
5693
+ *
5694
+ * Extends the `_derivedFrom` precedent from v1: same encryption shape,
5695
+ * same "metadata-inside-data" location.
5696
+ */
5697
+ interface MaterializedFromMeta {
5698
+ /** Stable identity for the MV that emitted this row. */
5699
+ readonly mvName: string;
5700
+ /**
5701
+ * SHA-256 of (mvName + canonical query plan + dependency-set).
5702
+ * Changes when the query structure changes → forces refresh on
5703
+ * next visit (parallels v1's `strategyHash`).
5704
+ */
5705
+ readonly queryHash: string;
5706
+ /**
5707
+ * Map from source collection name → `_v` of the source row(s) that
5708
+ * contributed to this MV row at materialization time. For aggregates
5709
+ * over many rows, this is `max(_v)` per source collection — coarse
5710
+ * but sufficient for stale detection.
5711
+ */
5712
+ readonly sourceVersions: Record<string, number>;
5713
+ /** ISO timestamp when this row was materialized. */
5714
+ readonly materializedAt: string;
5715
+ }
5716
+ /** Output routing for an MV. Optional — when omitted, writes to a collection named after `name`. */
5717
+ interface MaterializedViewOutput {
5718
+ /** Output collection name. Defaults to `name`. */
5719
+ collection?: string;
5720
+ /**
5721
+ * For same-collection-as-source MVs — see § Same-collection partition
5722
+ * discriminator in the v2 spec. The cycle detector resolves the
5723
+ * same-collection edge IFF the query has a where-clause that
5724
+ * provably excludes `partition.value` (supports `==` against a
5725
+ * different value, `!=` against the value, and `in` lists that
5726
+ * don't contain it). Naïve same-collection MVs without a disjoint
5727
+ * clause throw `MaterializedViewCycleError` at vault open.
5728
+ */
5729
+ partition?: {
5730
+ field: string;
5731
+ value: unknown;
5732
+ };
5733
+ }
5734
+ /**
5735
+ * One arm of a UNION materialized view. Reads rows from `collection`,
5736
+ * then maps each into the MV's row shape via `map`.
5737
+ *
5738
+ * The per-source `map` is the schema-unification boundary — sibling
5739
+ * collections can have different schemas, and `map` is where they
5740
+ * meet the MV's row type. The hub does NOT compare schemas across
5741
+ * arms; consumer responsibility is that every arm's `map` returns
5742
+ * the same shape (the strategy's `TRow` type parameter enforces this
5743
+ * at compile time).
5744
+ */
5745
+ interface UnionSource<TRow extends Record<string, unknown>> {
5746
+ /** Source collection name. Must exist in the vault. */
5747
+ readonly collection: string;
5748
+ /**
5749
+ * Pure function from a source row to the unified MV row shape.
5750
+ * Called once per source row at materialization time. Each arm's
5751
+ * mapped output is concatenated into a single stream before
5752
+ * `groupBy` + `aggregate` run.
5753
+ */
5754
+ readonly map: (sourceRow: Record<string, unknown>) => TRow;
5755
+ }
5756
+ /**
5757
+ * Registration shape passed to `withMaterializedView()`.
5758
+ *
5759
+ * @typeParam TRow - the materialized row type (the query's result row)
5760
+ */
5761
+ interface MaterializedViewStrategy<TRow extends Record<string, unknown>> {
5762
+ /**
5763
+ * Stable identity for this view. Used as the output collection name
5764
+ * unless `output.collection` overrides. Must be unique within the vault.
5765
+ */
5766
+ name: string;
5767
+ /**
5768
+ * Declared query (single-source mode). Called at registration time
5769
+ * with a vault-shaped accessor so the closure can compose collections
5770
+ * without pre-existing in-scope references; called again at each
5771
+ * refresh.
5772
+ *
5773
+ * Built via the same `Query<T>` chainable builder used elsewhere —
5774
+ * `.where()`, `.join()`, `.groupBy()`, `.aggregate()`. The
5775
+ * dependency analyzer walks the returned plan to determine source
5776
+ * collections.
5777
+ *
5778
+ * Mutually exclusive with {@link unionSources}: a strategy must
5779
+ * declare exactly one of `query` (single-source) or `unionSources`
5780
+ * (multi-source UNION). Registration throws
5781
+ * `MaterializedViewConfigError` if both are set or neither is set.
5782
+ */
5783
+ query?: (db: MVQueryContext) => Query<TRow>;
5784
+ /**
5785
+ * UNION-form sources (#165): an explicit list of sibling collections
5786
+ * that contribute rows to a single MV. Each arm's `map` projects a
5787
+ * source row into the MV's unified row shape; the mapped streams are
5788
+ * concatenated, then {@link groupBy} + {@link aggregate} run on the
5789
+ * combined output.
5790
+ *
5791
+ * Mutually exclusive with {@link query}. Registration throws
5792
+ * `MaterializedViewConfigError` if both are set, if `unionSources`
5793
+ * has fewer than 2 arms, or if two arms name the same `collection`.
5794
+ *
5795
+ * UNION mode replaces the dependency-analyzer path: the source
5796
+ * collections come directly from `unionSources[].collection`, and
5797
+ * {@link sources} is ignored.
5798
+ */
5799
+ unionSources?: ReadonlyArray<UnionSource<TRow>>;
5800
+ /**
5801
+ * Group-key field(s) for UNION mode (#165). Applied to the
5802
+ * concatenated mapped-row stream from {@link unionSources} before
5803
+ * {@link aggregate} runs. Accepts a single field name or a tuple of
5804
+ * field names for multi-key grouping (same shape as
5805
+ * `Query.groupBy(...fields)`).
5806
+ *
5807
+ * UNION-mode only. Ignored if {@link query} is set — single-source
5808
+ * grouping is expressed inside the `Query<T>` returned from `query()`
5809
+ * via `.groupBy(...).aggregate(...)`.
5810
+ */
5811
+ groupBy?: string | ReadonlyArray<string>;
5812
+ /**
5813
+ * Aggregation spec for UNION mode (#165). Applied per-group after
5814
+ * {@link groupBy} buckets the concatenated mapped-row stream from
5815
+ * {@link unionSources}. Same shape as the `AggregateSpec` passed to
5816
+ * `Query.aggregate()`.
5817
+ *
5818
+ * UNION-mode only. Ignored if {@link query} is set.
5819
+ */
5820
+ aggregate?: AggregateSpec;
5821
+ /**
5822
+ * Pure function from a materialized row → stable id used in the
5823
+ * output collection. Required — explicit always beats default-with-pitfalls
5824
+ * (see niwat-review of #149 round 1 for the slash-collision rationale).
5825
+ */
5826
+ rowKey: (row: TRow) => string;
5827
+ /**
5828
+ * Explicit source collections (#152). Required when `query()` returns
5829
+ * an `Aggregation` or `GroupedAggregation` rather than a `Query<T>`
5830
+ * — the dependency analyzer can't introspect through `groupBy().aggregate()`
5831
+ * back to the source. Optional for plain `Query<T>` results — the
5832
+ * analyzer extracts dependencies automatically from the query plan.
5833
+ *
5834
+ * When set, takes precedence over auto-analysis.
5835
+ */
5836
+ sources?: ReadonlyArray<string>;
5837
+ /**
5838
+ * Declared deterministic predicates (#153). Each entry pairs a
5839
+ * consumer-stable `hash` with a function. The `query()` callback's
5840
+ * Query<T> can invoke them via `.wherePredicate(name, ctx?)`. The
5841
+ * predicate's `hash` + a canonical-JSON hash of `ctx` both fold
5842
+ * into `queryHash` — bumping either forces refresh on next visit.
5843
+ *
5844
+ * Consumer responsibility: bump `hash` when the function's semantics
5845
+ * change. Failing to bump after a non-equivalent change leaves
5846
+ * stale rows around until the next explicit refresh.
5847
+ */
5848
+ predicates?: {
5849
+ [name: string]: {
5850
+ hash: string;
5851
+ fn: (row: TRow, ctx?: unknown) => boolean;
5852
+ };
5853
+ };
5854
+ /**
5855
+ * Refresh policy.
5856
+ *
5857
+ * - `'eager'` — re-materialize synchronously inside the source-write
5858
+ * transaction (composes with `withTransactions` for strict-mode
5859
+ * rollback).
5860
+ * - `'lazy'` — mark stale on source-change; materialize on first
5861
+ * read of the MV.
5862
+ * - `'manual'` — only materializes when `vault.refreshView(name)` is
5863
+ * called. Useful for very expensive MVs or time-dependent queries
5864
+ * whose `ctx` changes externally.
5865
+ */
5866
+ refresh: 'eager' | 'lazy' | 'manual';
5867
+ /** Output routing. Optional; defaults to writing the collection named after `name`. */
5868
+ output?: MaterializedViewOutput;
5869
+ /**
5870
+ * What to do when a re-materialization produces zero rows for a key
5871
+ * that previously had rows.
5872
+ *
5873
+ * - `'delete'` (default) — tombstone the prior MV row via
5874
+ * `Collection._internalDelete` (system housekeeping bypasses user
5875
+ * `onDelete` guards on the output collection — see PR #148's
5876
+ * composition fix).
5877
+ * - `'keep'` — leave the prior MV row in place. Useful when zero
5878
+ * is a meaningful state.
5879
+ */
5880
+ onEmpty?: 'delete' | 'keep';
5881
+ /**
5882
+ * `true` re-throws on any row-write failure → composes with
5883
+ * `withTransactions` to roll back the source-write atomically via
5884
+ * `revertExecuted` (#133). Default `false` (failed rows are
5885
+ * isolated; other rows commit).
5886
+ */
5887
+ strict?: boolean;
5888
+ /**
5889
+ * Row-count ceiling for the materialized output. Throws
5890
+ * `MaterializedViewTooLargeError` before any writes when exceeded
5891
+ * — keeps the rollback clean. Default `100_000`; override per-MV
5892
+ * when the domain warrants it.
5893
+ */
5894
+ maxRows?: number;
5895
+ }
5896
+ /** Returned by `withMaterializedView()` and consumed by `createNoydb`. */
5897
+ interface MaterializedViewStrategyHandle {
5898
+ readonly __noydb_strategy: 'materialized-view';
5899
+ readonly spec: MaterializedViewStrategy<any>;
5900
+ }
5901
+
5902
+ /**
5903
+ * One registered MV strategy alongside its derived metadata. Stored
5904
+ * type-erased on `TRow` so the registry can hold heterogeneous MVs.
5905
+ */
5906
+ interface RegisteredMV {
5907
+ readonly spec: MaterializedViewStrategy<any>;
5908
+ /** Output collection name (`spec.output?.collection ?? spec.name`). */
5909
+ readonly outputCollection: string;
5910
+ /** Set of source collections; populated at registration via the analyzer. */
5911
+ readonly dependencies: ReadonlySet<string>;
5912
+ /** Canonical `queryHash` — `_materializedFrom.queryHash` for every emitted row. */
5913
+ readonly queryHash: string;
5914
+ /**
5915
+ * Top-level FieldClauses on the partition field, captured at
5916
+ * registration time. Used by the cycle detector to resolve
5917
+ * same-collection-as-source edges via the partition-discriminator
5918
+ * check (#152). Empty when `spec.output?.partition` is undefined.
5919
+ */
5920
+ readonly partitionClauses: readonly FieldClause[];
5921
+ }
5922
+ /**
5923
+ * Vault-internal registry of MV strategies. Owned by `Vault`; not
5924
+ * exported. Parallel to v1's `DerivationRegistry`; the two graphs share
5925
+ * a single cycle-detection pass at vault open (see `validate`).
5926
+ *
5927
+ * @internal
5928
+ */
5929
+ declare class MaterializedViewRegistry {
5930
+ /** Keyed by `spec.name`. */
5931
+ private readonly _byName;
5932
+ /** Keyed by dependency source-collection → MVs that depend on it. */
5933
+ private readonly _bySource;
5934
+ /**
5935
+ * Register an MV. Invokes `spec.query()` once at registration time to
5936
+ * read the plan + join context; the resulting `Query<T>` is discarded
5937
+ * after dependency extraction. `vault.collection(...)` must therefore
5938
+ * be functional by the time this runs — typically wired from
5939
+ * `Vault._initMaterializedViews` after collection bootstrap.
5940
+ *
5941
+ * Throws `MaterializedViewSourceUnknownError` if the analyzer
5942
+ * surfaces a dependency the vault doesn't know about (when a
5943
+ * `knownCollections` checker is supplied).
5944
+ */
5945
+ register(spec: MaterializedViewStrategy<any>, db: MVQueryContext, options?: {
5946
+ knownCollections?: (name: string) => boolean;
5947
+ }): Promise<void>;
5948
+ /** All MVs that depend on `source`, in registration order. */
5949
+ mvsForSource(source: string): ReadonlyArray<RegisteredMV>;
5950
+ /** Single MV by name, or `undefined`. */
5951
+ byName(name: string): RegisteredMV | undefined;
5952
+ /** Iterate over every registered MV. */
5953
+ all(): ReadonlyArray<RegisteredMV>;
5954
+ /**
5955
+ * Cycle detection over the combined derivation + MV graph. Edges:
5956
+ * - Derivation: derivation.source → output.collection (each output)
5957
+ * - MV: every dep in MV.dependencies → MV.outputCollection
5958
+ *
5959
+ * Throws `MaterializedViewCycleError` if the cycle's terminal node
5960
+ * is an MV output collection; otherwise (a pure-derivation cycle)
5961
+ * the caller's `DerivationRegistry.validate()` will surface
5962
+ * `DerivationCycleError` separately at vault open.
5963
+ *
5964
+ * Call AFTER all `register()` calls complete.
5965
+ */
5966
+ validate(derivationRegistry?: DerivationRegistry | null): void;
5967
+ }
5968
+
5969
+ /**
5970
+ * Read-shadow overlay primitive (#154, MV v2 spec § Composition with
5971
+ * operator-editable lifecycle). Binds an MV's read-only base output
5972
+ * to a separate user-writable overlay collection; reads merge via a
5973
+ * single shadow predicate, writes route to the overlay.
5974
+ *
5975
+ * v2 ships the read-shadow variant only — arbitrary `mergePolicy`
5976
+ * callbacks are deferred to v3.
5977
+ */
5978
+ interface OverlayedViewStrategy {
5979
+ /**
5980
+ * Virtual collection name. `vault.collection(name)` returns a
5981
+ * proxy that merges `base` and `overlay` per the shadow rule.
5982
+ * Writes to the proxy route to the `overlay` collection. The name
5983
+ * must be unique within the vault — collisions with MV outputs or
5984
+ * concrete source collections throw `OverlayNameCollisionError` at
5985
+ * vault open.
5986
+ */
5987
+ name: string;
5988
+ /**
5989
+ * The collection providing the default rows. Typically an MV's
5990
+ * output collection. Must be a CONCRETE collection (a real source
5991
+ * or an MV output) — not itself another overlay's virtual name.
5992
+ * Multi-overlay stacking is a v3 non-goal; the constraint is
5993
+ * enforced at vault open via `OverlayBaseIsVirtualError`.
5994
+ */
5995
+ base: string;
5996
+ /**
5997
+ * User-writable collection that carries overrides. Must be a real,
5998
+ * vault-known collection that is NOT an MV-output collection. The
5999
+ * overlay's `withGuard` / `withDerivation` registrations apply to
6000
+ * direct writes; the virtual layer's `put(record)` also flows
6001
+ * through the overlay's normal write pipeline.
6002
+ */
6003
+ overlay: string;
6004
+ /**
6005
+ * Single-field shadow predicate. When `overlay[shadowField] ===
6006
+ * shadowValue` for a given id, virtual-collection reads of that id
6007
+ * return the overlay row; otherwise reads return the base row.
6008
+ *
6009
+ * Niwat's canonical example: `dataStatus === 'override'` flips a
6010
+ * row into operator-controlled mode.
6011
+ *
6012
+ * No callback merge, no priority lattice, no field-level merge —
6013
+ * v2 stays explicitly narrow.
6014
+ */
6015
+ shadowField: string;
6016
+ shadowValue: unknown;
6017
+ }
6018
+ /** Returned by `withOverlayedView()` and consumed by `createNoydb`. */
6019
+ interface OverlayedViewStrategyHandle {
6020
+ readonly __noydb_strategy: 'overlayed-view';
6021
+ readonly spec: OverlayedViewStrategy;
6022
+ }
6023
+
6024
+ /**
6025
+ * Vault-internal registry of overlay strategies. Resolves the base
6026
+ * MV's `rowKey` lazily so virtual-collection writes can derive ids
6027
+ * from the row.
6028
+ *
6029
+ * @internal
6030
+ */
6031
+ declare class OverlayedViewRegistry {
6032
+ private readonly _byName;
6033
+ /**
6034
+ * Register an overlay. Validates name uniqueness, base concreteness,
6035
+ * and overlay availability AGAINST the MV registry — overlays
6036
+ * declared without the MV registry context skip cross-registry
6037
+ * checks but still validate self-consistency.
6038
+ */
6039
+ register(spec: OverlayedViewStrategy, options: {
6040
+ isOverlayName?: (name: string) => boolean;
6041
+ isMVOutput?: (name: string) => boolean;
6042
+ isKnownCollection?: (name: string) => boolean;
6043
+ }): void;
6044
+ byName(name: string): OverlayedViewStrategy | undefined;
6045
+ /** All overlay virtual names. */
6046
+ names(): ReadonlySet<string>;
6047
+ isOverlay(name: string): boolean;
6048
+ /**
6049
+ * Resolve the `rowKey` function for an overlay's base MV. Returns
6050
+ * `undefined` if the base isn't an MV (raw source collection) or
6051
+ * if the MV registry isn't supplied. Used by the virtual-collection
6052
+ * proxy to derive ids from `put(record)` calls.
6053
+ */
6054
+ resolveBaseRowKey(name: string, mvRegistry: MaterializedViewRegistry | null): ((row: Record<string, unknown>) => string) | undefined;
6055
+ }
6056
+
6057
+ /**
6058
+ * Vault-internal singleton that holds the guard graph and dispatches
6059
+ * per-collection guard execution. Owned by `Vault`; not exported.
6060
+ *
6061
+ * @internal
4614
6062
  */
4615
- declare function loadActiveDelegations(store: NoydbStore, vault: string, user: UnlockedKeyring, delegationsDek: CryptoKey, now?: Date): Promise<DelegationToken[]>;
6063
+ type AnyGuard = GuardStrategy<Record<string, unknown>>;
6064
+ type AnyChange = GuardChange<Record<string, unknown>>;
6065
+ declare class GuardRegistry {
6066
+ private readonly _byCollection;
6067
+ private _amendmentChanges;
6068
+ private _amendmentMeta;
6069
+ /** Register a guard. Multiple guards per collection are allowed. */
6070
+ register<T extends Record<string, unknown>>(spec: GuardStrategy<T>): void;
6071
+ /** All guards registered against `collection` in registration order. */
6072
+ guardsFor(collection: string): ReadonlyArray<AnyGuard>;
6073
+ /**
6074
+ * Run every guard's `check` for this collection. First throw wins —
6075
+ * remaining guards are not invoked. Guards without a `check` skip.
6076
+ */
6077
+ runChecks<T>(collection: string, incoming: T, ctx: GuardContext<T>): Promise<void>;
6078
+ /**
6079
+ * Run every guard's `onDelete` for this collection. First throw wins —
6080
+ * remaining guards are not invoked. Guards without an `onDelete` skip.
6081
+ * Mirrors {@link runChecks} but for the delete path.
6082
+ */
6083
+ runOnDelete<T>(collection: string, existing: T, ctx: GuardContext<T>): Promise<void>;
6084
+ /** True if any guard for `collection` declares an `amendment` block. */
6085
+ hasAmendment(collection: string): boolean;
6086
+ /** Open a new amendment change-collection window. */
6087
+ beginAmendment(): void;
6088
+ /** True iff we're currently inside an amendment transaction. */
6089
+ isAmendmentActive(): boolean;
6090
+ /**
6091
+ * Record a {before, after} pair for the active amendment. `vBefore`
6092
+ * and `vAfter` are stored in a parallel meta structure so the public
6093
+ * {@link GuardChange} shape handed to invariant callbacks stays
6094
+ * `{ before, after }` only — the audit ledger reads version metadata
6095
+ * via {@link consumeMeta}.
6096
+ */
6097
+ collectChange<T>(collection: string, id: string, before: T | null, after: T, vBefore?: number, vAfter?: number): void;
6098
+ /**
6099
+ * Drain the change-set and close the amendment window. The caller
6100
+ * (transaction commit) feeds these to each affected guard's invariant.
6101
+ */
6102
+ consumeChanges(): ReadonlyMap<string, ReadonlyArray<AnyChange>>;
6103
+ /**
6104
+ * Drain the parallel id/version metadata captured during the
6105
+ * amendment. Returned as a flat list with `collection` denormalised
6106
+ * so the audit ledger can emit one `{ collection, id, vBefore,
6107
+ * vAfter }` tuple per record. Must be called AFTER
6108
+ * {@link consumeChanges} (or independently) — calling it closes the
6109
+ * meta window in the same way.
6110
+ */
6111
+ consumeMeta(): ReadonlyArray<{
6112
+ collection: string;
6113
+ id: string;
6114
+ vBefore: number;
6115
+ vAfter: number;
6116
+ }>;
6117
+ }
6118
+
4616
6119
  /**
4617
- * Revoke a delegation by id the caller resolves the envelope and
4618
- * issues a `delete`. Provided as a stable helper so the naming is
4619
- * symmetric to `issueDelegation`.
6120
+ * Minimal read-only wrapper over a `Vault`. Used as `ctx.vault` inside
6121
+ * guard callbacks so they can fetch related records without acquiring
6122
+ * any write capability.
4620
6123
  */
4621
- declare function revokeDelegation(store: NoydbStore, vault: string, id: string): Promise<void>;
6124
+ declare class ReadOnlyVaultFacade implements ReadOnlyVaultFacade$1 {
6125
+ private readonly _vault;
6126
+ constructor(vault: Vault);
6127
+ collection<T = unknown>(name: string): {
6128
+ get(id: string): Promise<T | null>;
6129
+ list(): Promise<T[]>;
6130
+ query(): Query<T>;
6131
+ };
6132
+ }
4622
6133
 
4623
6134
  /**
4624
6135
  * `vault.exportBlobs()` — bulk blob extraction primitive.
@@ -4994,6 +6505,50 @@ declare function magicLinkGrantRecordId(token: string, index: number): string;
4994
6505
  */
4995
6506
  declare function isMagicLinkGrantExpired(payload: MagicLinkGrantPayload, now?: Date): boolean;
4996
6507
 
6508
+ /**
6509
+ * Type surface for the user-list visibility subsystem (#122).
6510
+ *
6511
+ * Two complementary flags:
6512
+ * - {@link DirectoryConfig} — vault-level "is the directory listing
6513
+ * enabled at all?" toggle. Owner-only mutation.
6514
+ * - {@link UserVisibility} — per-user "hide me from teammate listings"
6515
+ * opt-out. Self-mutation via `vault.user.setMyVisibility`.
6516
+ *
6517
+ * Both flags live in the existing `_meta` collection as plaintext-bypass
6518
+ * sidecars (`_iv: ''`). Neither is a security boundary — the keyring
6519
+ * file is still observable at `_keyring/*` and the envelope ciphertext
6520
+ * is still at `_users/*` to anyone with direct store read access. The
6521
+ * flags exist to keep admin-UI listings tidy, not to hide principals
6522
+ * from a determined attacker.
6523
+ *
6524
+ * @see docs/subsystems/user-envelope.md → Directory visibility
6525
+ *
6526
+ * @module
6527
+ */
6528
+ /**
6529
+ * Vault-level directory toggle. Persisted at `_meta/directory`.
6530
+ *
6531
+ * - `enabled: true` (default when no document exists) — every authenticated
6532
+ * caller can enumerate users via `listUsersWithEnvelopes`.
6533
+ * - `enabled: false` — only `owner` and `admin` callers can enumerate;
6534
+ * anyone else gets {@link import('../errors.js').DirectoryDisabledError}.
6535
+ */
6536
+ interface DirectoryConfig {
6537
+ readonly enabled: boolean;
6538
+ }
6539
+ /**
6540
+ * Per-user visibility flag. Persisted at `_meta/visibility/<keyringId>`.
6541
+ *
6542
+ * - `hidden: false` (default when no document exists) — the user shows up
6543
+ * in `listUsersWithEnvelopes` like any other principal.
6544
+ * - `hidden: true` — the user is filtered out of the default listing.
6545
+ * `owner`/`admin` callers can still see them by passing
6546
+ * `{ includeHidden: true }`.
6547
+ */
6548
+ interface UserVisibility {
6549
+ readonly hidden: boolean;
6550
+ }
6551
+
4997
6552
  /**
4998
6553
  * Public `vault.user.*` API surface.
4999
6554
  *
@@ -5020,6 +6575,24 @@ declare function isMagicLinkGrantExpired(payload: MagicLinkGrantPayload, now?: D
5020
6575
  type DeepPartial<T> = T extends object ? {
5021
6576
  [P in keyof T]?: DeepPartial<T[P]>;
5022
6577
  } : T;
6578
+ /**
6579
+ * Recursive partial with `null` allowed at every level — used by
6580
+ * `updateMe` (#57) to express deletion intent in addition to merge.
6581
+ *
6582
+ * Semantics inside `updateMe`:
6583
+ * - `undefined` (or absent key) — skip; source value preserved
6584
+ * - `null` — delete the key from the resulting envelope
6585
+ * - any other value — overwrite (deep-merge for plain objects,
6586
+ * replace for primitives / arrays)
6587
+ *
6588
+ * Matches lodash `_.merge` behavior on `null` and Firestore's
6589
+ * `FieldValue.delete()` semantics. Loosened from `DeepPartial<T>` per
6590
+ * #57; consumers wanting the original "merge-only" surface can keep
6591
+ * importing `DeepPartial` and avoid passing `null`.
6592
+ */
6593
+ type DeepPartialOrNull<T> = T extends object ? {
6594
+ [P in keyof T]?: DeepPartialOrNull<T[P]> | null;
6595
+ } : T;
5023
6596
  /** Cancel a previously-registered subscription. */
5024
6597
  type Unsubscribe = () => void;
5025
6598
  /**
@@ -5086,11 +6659,22 @@ declare class UserApi {
5086
6659
  * the envelope on first call. Optimistic-concurrency safe — a stale
5087
6660
  * `_v` (parallel writer on another device) throws `ConflictError`.
5088
6661
  *
6662
+ * Patch semantics (#57):
6663
+ * - `undefined` (or omitted key) — skip; existing value preserved
6664
+ * - `null` — delete the field from the merged result
6665
+ * - any other value — overwrite (deep-merge for plain objects,
6666
+ * replace for primitives / arrays)
6667
+ *
6668
+ * To clear a field, pass `null` rather than `undefined`. Callers
6669
+ * with shape `T = string | null` where `null` is a meaningful value
6670
+ * should use `setMe` for that specific field instead — `null` here
6671
+ * always means delete.
6672
+ *
5089
6673
  * Gated by the `edit-own-profile` policy gate (default `minTier: 3`).
5090
6674
  * Pass `presented` to satisfy tightened policies that require a
5091
6675
  * factor proof (e.g. STRICT_POLICY's TOTP requirement).
5092
6676
  */
5093
- updateMe<T extends object = Record<string, unknown>>(patch: DeepPartial<T>, presented?: UserEnvelopePresented): Promise<UserEnvelope<T>>;
6677
+ updateMe<T extends object = Record<string, unknown>>(patch: DeepPartialOrNull<T>, presented?: UserEnvelopePresented): Promise<UserEnvelope<T>>;
5094
6678
  /**
5095
6679
  * Replace the writer's own envelope with `payload`. Use sparingly —
5096
6680
  * `updateMe` is the canonical mutation. No `expectedVersion` check;
@@ -5099,6 +6683,30 @@ declare class UserApi {
5099
6683
  * Gated by `edit-own-profile`. See `updateMe` for `presented` usage.
5100
6684
  */
5101
6685
  setMe<T = unknown>(payload: T, presented?: UserEnvelopePresented): Promise<UserEnvelope<T>>;
6686
+ /**
6687
+ * Read the current user's visibility flag from
6688
+ * `_meta/visibility/<keyringId>`. Returns `{ hidden: false }` when no
6689
+ * document has been persisted (the default-visible case).
6690
+ */
6691
+ getMyVisibility(): Promise<UserVisibility>;
6692
+ /**
6693
+ * Update the current user's visibility in the team directory.
6694
+ *
6695
+ * - `hidden: true` — opt out of the default `listUsersWithEnvelopes`
6696
+ * listing. `owner`/`admin` callers can still see the user by passing
6697
+ * `{ includeHidden: true }`.
6698
+ * - `hidden: false` — opt back in.
6699
+ *
6700
+ * Own-only by construction: the keyringId argument doesn't exist on
6701
+ * this method, so no caller can hide or unhide another principal.
6702
+ *
6703
+ * Honest caveat: this is a UX flag, not a privacy guarantee. The
6704
+ * envelope ciphertext at `_users/<keyringId>` and the keyring file at
6705
+ * `_keyring/<userId>` are both still observable to anyone with direct
6706
+ * store read access. See `docs/subsystems/user-envelope.md` →
6707
+ * "Directory visibility".
6708
+ */
6709
+ setMyVisibility(visibility: UserVisibility): Promise<void>;
5102
6710
  /**
5103
6711
  * Read another principal's envelope by their keyringId. Returns null
5104
6712
  * if the principal exists but has no envelope yet, or if the
@@ -5158,6 +6766,159 @@ declare class UserApi {
5158
6766
  private fireChange;
5159
6767
  }
5160
6768
 
6769
+ /**
6770
+ * Persisted-schema envelope shape.
6771
+ *
6772
+ * Stored encrypted under `_schemas/<collection>` with the same DEK as the
6773
+ * collection's records. Auditors who can unlock the collection's data can
6774
+ * also read its schema; nothing more.
6775
+ *
6776
+ * @see docs/superpowers/specs/2026-05-22-schema-dump-design.md
6777
+ *
6778
+ * @module
6779
+ */
6780
+ /** Family of Standard Schema v1 validator the persisted snapshot was derived from. */
6781
+ type PersistedSchemaKind = 'Zod' | 'Valibot' | 'ArkType' | 'Effect' | 'Unknown';
6782
+ /**
6783
+ * Plaintext payload encrypted into the `_data` field of the
6784
+ * `_schemas/<collection>` envelope. The wrapper `EncryptedEnvelope` adds
6785
+ * `_noydb`, `_v`, `_ts`, `_iv`, `_data` per the standard noy-db record
6786
+ * format.
6787
+ */
6788
+ interface PersistedSchemaEnvelope {
6789
+ readonly _noydb_schema: 1;
6790
+ /** Detected validator family. */
6791
+ readonly kind: PersistedSchemaKind;
6792
+ /**
6793
+ * JSON Schema (Draft 2020-12) derived from the validator. Null when
6794
+ * derivation isn't yet supported for `kind`; in that case `reason` is
6795
+ * populated.
6796
+ */
6797
+ readonly jsonSchema: object | null;
6798
+ /** SHA-256 (hex) of the canonicalised JSON Schema, or null when unavailable. */
6799
+ readonly hash: string | null;
6800
+ /** Human-readable reason when `jsonSchema` is null. */
6801
+ readonly reason?: string;
6802
+ /** ISO-8601 timestamp of the most recent derivation write. */
6803
+ readonly derivedAt: string;
6804
+ }
6805
+
6806
+ /**
6807
+ * Types for {@link VaultSchemaSnapshot} — the structured object returned
6808
+ * by `vault.dumpSchema()`. Consumed by the upcoming `noydb describe`
6809
+ * CLI to emit human-readable YAML/JSON audit output.
6810
+ *
6811
+ * @see docs/superpowers/specs/2026-05-22-schema-dump-design.md
6812
+ *
6813
+ * @module
6814
+ */
6815
+
6816
+ /** Where the field-level info in the snapshot came from. */
6817
+ type FieldSource = 'persisted' | 'live-validator' | 'sampled' | 'unknown';
6818
+ interface FieldDescriptor {
6819
+ /** Inferred type tag: 'string' | 'number' | 'boolean' | 'enum' | 'object' | 'array' | 'null' | 'opaque'. */
6820
+ readonly type: string;
6821
+ /** Where this field info was sourced from. */
6822
+ readonly source: FieldSource;
6823
+ /** Optional constraints — minLength, maxLength, enum values, gt, etc. */
6824
+ readonly constraints?: Record<string, unknown>;
6825
+ /** True when the schema marks this field optional. */
6826
+ readonly optional?: boolean;
6827
+ /** Foreign-key target as `<collection>.<field>` when declared. */
6828
+ readonly references?: string;
6829
+ }
6830
+ interface CollectionStats {
6831
+ readonly records: number;
6832
+ readonly bytes: number;
6833
+ readonly bytesAvg: number;
6834
+ readonly bytesMin: number;
6835
+ readonly bytesMax: number;
6836
+ /** ISO-8601 from min(_ts) across envelopes. Empty string when no records. */
6837
+ readonly oldest: string;
6838
+ /** ISO-8601 from max(_ts) across envelopes. Empty string when no records. */
6839
+ readonly newest: string;
6840
+ }
6841
+ interface CollectionDescriptor {
6842
+ readonly fields: Record<string, FieldDescriptor>;
6843
+ readonly indexes: ReadonlyArray<{
6844
+ readonly fields: ReadonlyArray<string>;
6845
+ readonly unique?: boolean;
6846
+ }>;
6847
+ readonly refs: Record<string, {
6848
+ readonly target: string;
6849
+ readonly mode: 'strict' | 'warn' | 'cascade';
6850
+ }>;
6851
+ readonly validator?: {
6852
+ readonly kind: PersistedSchemaKind;
6853
+ readonly source: 'persisted' | 'live-validator';
6854
+ };
6855
+ readonly stats?: CollectionStats;
6856
+ }
6857
+ interface MaterializedViewDescriptor {
6858
+ readonly sources: ReadonlyArray<string>;
6859
+ readonly groupBy?: ReadonlyArray<string>;
6860
+ readonly aggregate?: Record<string, string>;
6861
+ readonly refresh: string;
6862
+ readonly stats?: CollectionStats;
6863
+ }
6864
+ interface OverlayViewDescriptor {
6865
+ readonly base: string;
6866
+ readonly overlay: string;
6867
+ }
6868
+ interface DerivationDescriptor {
6869
+ readonly source: string;
6870
+ readonly outputs: ReadonlyArray<string>;
6871
+ }
6872
+ interface InternalCollectionStats {
6873
+ readonly records: number;
6874
+ readonly bytes: number;
6875
+ }
6876
+ interface VaultSchemaSnapshot {
6877
+ readonly _noydb_snapshot: 1;
6878
+ readonly vault: string;
6879
+ readonly emittedAt: string;
6880
+ readonly subsystems: Record<string, boolean>;
6881
+ readonly aclRoles?: ReadonlyArray<string>;
6882
+ readonly collections: Record<string, CollectionDescriptor>;
6883
+ readonly materializedViews: Record<string, MaterializedViewDescriptor>;
6884
+ readonly overlayViews: Record<string, OverlayViewDescriptor>;
6885
+ readonly derivations: Record<string, DerivationDescriptor>;
6886
+ /** Only present when `dumpSchema({ withStats: true })` was called. */
6887
+ readonly internal?: Record<string, InternalCollectionStats>;
6888
+ }
6889
+ interface DumpSchemaOptions {
6890
+ /** When true, walk every collection's envelopes to compute counters. Default `false`. */
6891
+ readonly withStats?: boolean;
6892
+ /** Sample N records per collection lacking a persisted/live schema. Default 50. `0` disables sampling. */
6893
+ readonly sampleSize?: number;
6894
+ }
6895
+
6896
+ /**
6897
+ * Orchestrate the structural walk of a Vault, producing a
6898
+ * {@link VaultSchemaSnapshot}. Called from `Vault.dumpSchema()`.
6899
+ *
6900
+ * @module
6901
+ */
6902
+
6903
+ /**
6904
+ * The minimal slice of Vault internal state the walker needs.
6905
+ * Exposed via `vault._introspectState()` to keep the public Vault
6906
+ * surface narrow.
6907
+ *
6908
+ * @internal
6909
+ */
6910
+ interface VaultIntrospectState {
6911
+ readonly name: string;
6912
+ readonly adapter: NoydbStore;
6913
+ readonly collectionCache: Map<string, Collection<unknown>>;
6914
+ readonly refRegistry: RefRegistry;
6915
+ readonly getDEK: (collectionName: string) => Promise<CryptoKey>;
6916
+ readonly subsystems: Record<string, boolean>;
6917
+ readonly mvRegistry: unknown;
6918
+ readonly overlayRegistry: unknown;
6919
+ readonly derivationRegistry: unknown;
6920
+ }
6921
+
5161
6922
  /** A vault (tenant namespace) containing collections. */
5162
6923
  declare class Vault {
5163
6924
  private readonly adapter;
@@ -5200,6 +6961,40 @@ declare class Vault {
5200
6961
  private readonly historyStrategy;
5201
6962
  private readonly i18nStrategy;
5202
6963
  private readonly syncStrategy;
6964
+ /**
6965
+ * Per-vault guard registry. `null` until `_initGuards()` runs; stays
6966
+ * `null` for vaults that never register any guard strategy. The
6967
+ * runtime class is dynamic-imported on demand so consumers that
6968
+ * never use guards don't pull `GuardRegistry`/`GuardExecutor` into
6969
+ * their bundle (#130).
6970
+ */
6971
+ private guardRegistry;
6972
+ /**
6973
+ * Per-vault derivation registry. Same lazy-load contract as
6974
+ * `guardRegistry` — `null` until `_initDerivations()` runs with at
6975
+ * least one strategy handle. See #130 for the bundle motivation.
6976
+ */
6977
+ private derivationRegistry;
6978
+ /**
6979
+ * Per-vault materialized-view registry (#143/#150). Same lazy-load
6980
+ * contract as `derivationRegistry` — `null` until
6981
+ * `_initMaterializedViews()` runs with at least one MV handle.
6982
+ */
6983
+ private materializedViewRegistry;
6984
+ /**
6985
+ * Per-vault overlay registry (#154). Same lazy-load contract as
6986
+ * `materializedViewRegistry` — `null` until `_initOverlayedViews()`
6987
+ * runs with at least one handle.
6988
+ */
6989
+ private overlayedViewRegistry;
6990
+ /**
6991
+ * Cached read-only facade handed to guard callbacks via `ctx.vault`,
6992
+ * and to derivation callbacks via `derive(source, ctx)`. Allocated
6993
+ * eagerly inside `_initGuards()` and/or `_initDerivations()` so read
6994
+ * accessors stay synchronous (callers in `tx/transaction.ts` rely on
6995
+ * that). Stays `null` for vaults with neither subsystem configured.
6996
+ */
6997
+ private readOnlyFacade;
5203
6998
  private getDEK;
5204
6999
  /**
5205
7000
  * Per-principal user envelope API.
@@ -5247,6 +7042,16 @@ declare class Vault {
5247
7042
  * docstring.
5248
7043
  */
5249
7044
  private ledgerStore;
7045
+ /**
7046
+ * Background writes for persisted-schema envelopes (#schema-dump v0
7047
+ * slice 1). One promise per `collection({ persistJsonSchema: true })`
7048
+ * registration that actually fired a derive call. Fire-and-forget
7049
+ * from the collection factory; tests await
7050
+ * {@link _drainPendingSchemaWrites} before asserting on storage.
7051
+ * Production code does not need to drain — the writes are
7052
+ * idempotent fingerprints, not correctness invariants.
7053
+ */
7054
+ private _pendingSchemaWrites;
5250
7055
  /**
5251
7056
  * Per-vault foreign-key reference registry. Collections
5252
7057
  * register their `refs` option here on construction; the
@@ -5358,6 +7163,7 @@ declare class Vault {
5358
7163
  historyStrategy?: HistoryStrategy | undefined;
5359
7164
  i18nStrategy?: I18nStrategy | undefined;
5360
7165
  syncStrategy?: SyncStrategy | undefined;
7166
+ guardStrategies?: ReadonlyArray<GuardStrategyHandleAny> | undefined;
5361
7167
  });
5362
7168
  /**
5363
7169
  * Construct (or reconstruct) the lazy DEK resolver. Captures the
@@ -5430,7 +7236,26 @@ declare class Vault {
5430
7236
  tiers?: readonly number[];
5431
7237
  /** — how lower-tier reads see above-tier records. */
5432
7238
  tierMode?: TierMode;
7239
+ /**
7240
+ * Opt-in persisted JSON Schema. When `true` AND a Zod `schema` is
7241
+ * provided, hub derives a JSON Schema via `zod-to-json-schema`
7242
+ * (optional peer-dep) and writes an encrypted snapshot to
7243
+ * `_schemas/<collectionName>`. Re-runs on every open; hash-skip
7244
+ * avoids write churn when the schema is unchanged.
7245
+ *
7246
+ * Default: `false`. Non-Zod Standard Schema validators receive a
7247
+ * stub envelope flagging the kind without a JSON Schema body.
7248
+ *
7249
+ * @see docs/superpowers/specs/2026-05-22-schema-dump-design.md
7250
+ */
7251
+ persistJsonSchema?: boolean;
5433
7252
  }): Collection<T>;
7253
+ /**
7254
+ * Await all background persisted-schema writes triggered by
7255
+ * `collection({ persistJsonSchema: true })` calls on this vault.
7256
+ * Used in tests; production code does not need to call this.
7257
+ */
7258
+ _drainPendingSchemaWrites(): Promise<void>;
5434
7259
  /**
5435
7260
  * Validate i18nText fields on a `put()`. Called by Collection just
5436
7261
  * before the adapter write, after schema validation. Throws
@@ -5720,6 +7545,124 @@ declare class Vault {
5720
7545
  * throws on null; this one stays silent so the off-path no-ops.
5721
7546
  */
5722
7547
  private getLedgerOrNull;
7548
+ /**
7549
+ * @internal — called by `Noydb.openVault` after construction.
7550
+ * Dynamic-imports `GuardRegistry` + `ReadOnlyVaultFacade` and seeds
7551
+ * the registry with the supplied strategy handles. No-op when the
7552
+ * handles array is empty — keeps the guard subsystem out of the
7553
+ * floor bundle for consumers that don't use guards (#130).
7554
+ *
7555
+ * The read-only facade is eagerly instantiated here so the sync
7556
+ * accessor `_getReadOnlyFacade()` (called from the tx amendment
7557
+ * runner) stays synchronous.
7558
+ */
7559
+ _initGuards(handles: ReadonlyArray<GuardStrategyHandleAny>): Promise<void>;
7560
+ /**
7561
+ * @internal — Collection.put calls into this. Returns `null` for
7562
+ * vaults that never registered any guard strategy. Callers MUST
7563
+ * gate on null (the existing `if (this.guardSource)` branches in
7564
+ * `Collection` already do this transitively).
7565
+ */
7566
+ _getGuardRegistry(): GuardRegistry | null;
7567
+ /**
7568
+ * @internal — called by `Noydb.openVault` after construction.
7569
+ * Dynamic-imports `DerivationRegistry` and registers the supplied
7570
+ * derivation strategies (async because `strategyHash` computation
7571
+ * goes through `crypto.subtle.digest`). No-op when the handles
7572
+ * array is empty — keeps the derivation subsystem out of the floor
7573
+ * bundle for consumers that don't use derivations (#130). Throws
7574
+ * `DerivationCycleError` if a cycle is detected after registration.
7575
+ */
7576
+ _initDerivations(handles: ReadonlyArray<DerivationStrategyHandle>): Promise<void>;
7577
+ /**
7578
+ * @internal — consumed by `Collection.put` at write-time. Returns
7579
+ * `null` for vaults that never registered any derivation strategy.
7580
+ */
7581
+ _getDerivationRegistry(): DerivationRegistry | null;
7582
+ /**
7583
+ * @internal — called by `Noydb.openVault` after collections are
7584
+ * wired. Dynamic-imports `MaterializedViewRegistry`, registers each
7585
+ * MV spec (which invokes its `query()` once for dependency
7586
+ * analysis), then runs the unified cycle detection across the MV +
7587
+ * derivation graphs. No-op when the handles array is empty — keeps
7588
+ * the MV subsystem out of the floor bundle (mirrors v1 #130).
7589
+ * Throws `MaterializedViewCycleError` if a cycle is detected.
7590
+ */
7591
+ _initMaterializedViews(handles: ReadonlyArray<MaterializedViewStrategyHandle>): Promise<void>;
7592
+ /**
7593
+ * @internal — consumed by `Collection.put` at write-time. Returns
7594
+ * `null` for vaults that never registered any MV strategy.
7595
+ */
7596
+ _getMaterializedViewRegistry(): MaterializedViewRegistry | null;
7597
+ /**
7598
+ * @internal — called by `Noydb.openVault` after MVs are wired.
7599
+ * Dynamic-imports `OverlayedViewRegistry`, registers each spec,
7600
+ * validates against the MV registry for name/base/overlay collisions.
7601
+ * Throws on validation failure.
7602
+ */
7603
+ _initOverlayedViews(handles: ReadonlyArray<OverlayedViewStrategyHandle>): Promise<void>;
7604
+ /**
7605
+ * @internal — consumed by `Vault.collection()`. Returns `null` for
7606
+ * vaults with no overlays registered.
7607
+ */
7608
+ _getOverlayedViewRegistry(): OverlayedViewRegistry | null;
7609
+ /**
7610
+ * Manual re-materialize for a single registered MV (#151). Useful
7611
+ * for `refresh: 'manual'` MVs (whose consumer drives refreshes
7612
+ * externally), for stale-bit recovery on vault re-open, and as the
7613
+ * explicit bulk-recompute escape hatch after a strategy change.
7614
+ *
7615
+ * Returns `{ written, deleted, failed }`. `deleted` is always 0 in
7616
+ * foundation + this sub-issue — tombstoning lands in #152.
7617
+ *
7618
+ * Throws if `name` is not a registered MV.
7619
+ */
7620
+ refreshView(name: string): Promise<{
7621
+ written: number;
7622
+ deleted: number;
7623
+ failed: number;
7624
+ }>;
7625
+ /**
7626
+ * Re-derive every record in the named source collection. Useful
7627
+ * after a strategy change to bring previously-derived records
7628
+ * up-to-date.
7629
+ *
7630
+ * Sequential in v1; parallelisation deferred to v2.
7631
+ */
7632
+ deriveAll(sourceCollection: string): Promise<{
7633
+ derived: number;
7634
+ failed: number;
7635
+ }>;
7636
+ /**
7637
+ * @internal — exposed for `runTransaction({ amendment: true })` so
7638
+ * the amendment invariant runner can pass the SAME read-only vault
7639
+ * facade that the per-record `Collection.put` guard hook uses
7640
+ * (`guardSource.readOnlyVault()` above). Eagerly instantiated by
7641
+ * `_initGuards()` so this accessor stays synchronous; returns
7642
+ * `null` for vaults that never registered any guard (amendments
7643
+ * require at least one guard, so the caller should never see null).
7644
+ */
7645
+ _getReadOnlyFacade(): ReadOnlyVaultFacade | null;
7646
+ /**
7647
+ * Internal lazy-allocator for the read-only facade. Used by the
7648
+ * per-collection `guardSource.readOnlyVault` callback when guards
7649
+ * ARE configured but `_initGuards()` raced with the first guard
7650
+ * invocation (theoretically impossible — `Noydb.openVault` awaits
7651
+ * `_initGuards` before returning — but we keep the defensive lazy
7652
+ * path so the closure's contract stays "always returns a facade").
7653
+ */
7654
+ private _ensureReadOnlyFacade;
7655
+ /**
7656
+ * @internal — exposed for `runTransaction({ amendment: true })`
7657
+ * to append the structured `op: 'amendment'` audit entry without
7658
+ * dragging this private accessor onto the public surface or
7659
+ * forcing the tx executor to depend on the history-strategy
7660
+ * shape directly. Returns `null` when no history strategy is
7661
+ * configured, in which case the amendment commits silently
7662
+ * (the records still write through; only the multi-record
7663
+ * audit summary is skipped).
7664
+ */
7665
+ _getLedgerOrNull(): LedgerStore | null;
5723
7666
  /**
5724
7667
  * Return a read-only view of this vault as it existed at
5725
7668
  * `timestamp`. Time-machine queries are reconstructed from the
@@ -5923,6 +7866,34 @@ declare class Vault {
5923
7866
  private _decryptPeriodRecord;
5924
7867
  /** List all collection names in this vault. */
5925
7868
  collections(): Promise<string[]>;
7869
+ /**
7870
+ * Emit a structured introspection snapshot of this vault — vault name,
7871
+ * subsystem opt-in matrix, collections + their fields, materialized
7872
+ * views, overlay views, derivations. With `withStats: true`, walks
7873
+ * every collection's envelopes to compute record counts, byte totals,
7874
+ * and oldest/newest timestamps.
7875
+ *
7876
+ * Consumed by the `noydb describe` CLI to produce human-readable
7877
+ * audit YAML/JSON from a `.noydb` bundle.
7878
+ *
7879
+ * Field provenance:
7880
+ * - `persisted`: read from `_schemas/<col>` envelope (Route B opt-in)
7881
+ * - `live-validator`: derived in-process from a Zod schema attached
7882
+ * to the live `Collection`
7883
+ * - `sampled`: inferred from decrypted records (deferred to a follow-up)
7884
+ * - `unknown`: no schema info available
7885
+ *
7886
+ * @see docs/superpowers/specs/2026-05-22-schema-dump-design.md
7887
+ */
7888
+ dumpSchema(opts?: DumpSchemaOptions): Promise<VaultSchemaSnapshot>;
7889
+ /**
7890
+ * Internal accessor for {@link dumpVaultSchema}. Exposes the structural
7891
+ * state the walker needs (collection cache, registries, ref registry,
7892
+ * adapter) without widening the public Vault surface.
7893
+ *
7894
+ * @internal
7895
+ */
7896
+ _introspectState(): VaultIntrospectState;
5926
7897
  /**
5927
7898
  * Return the stable opaque bundle handle for this vault,
5928
7899
  * generating and persisting a fresh ULID on first call.
@@ -6590,6 +8561,34 @@ declare class Collection<T> {
6590
8561
  * adapter on first use.
6591
8562
  */
6592
8563
  private readonly periodGuard;
8564
+ /**
8565
+ * Optional back-reference to the owning vault's guard registry + a
8566
+ * read-only vault facade. When present, `Collection.put` and
8567
+ * `Collection.delete` consult the registry for guards declared
8568
+ * against this collection and run their `check` + `frozenFields`
8569
+ * before the adapter write. Absent in unit tests that construct
8570
+ * a Collection directly; production code always sets it via
8571
+ * `Vault.collection()`.
8572
+ *
8573
+ * Typed structurally rather than as `Vault` to avoid a circular
8574
+ * import (mirrors the `refEnforcer` / `joinResolver` pattern).
8575
+ */
8576
+ private readonly guardSource;
8577
+ /**
8578
+ * Vault-internal hook for derivation dispatch. When set,
8579
+ * `Collection.put` consults the registry after the source-write
8580
+ * commits and writes derived outputs through `getCollection(name).put`.
8581
+ * Same structural-interface pattern as `guardSource` to avoid a
8582
+ * circular Vault import.
8583
+ */
8584
+ private readonly derivationSource;
8585
+ /**
8586
+ * Vault-internal hook for materialized-view dispatch (#143/#150).
8587
+ * Parallel to `derivationSource` — when set, `Collection.put` fires
8588
+ * `MaterializedViewRegistry.onSourceWrite` after the source-write
8589
+ * commits + after `dispatchDerivations` has run.
8590
+ */
8591
+ private readonly materializedViewSource;
6593
8592
  /**
6594
8593
  * Optional back-reference to the owning compartment's ref
6595
8594
  * enforcer. When present, `Collection.put` calls
@@ -6843,6 +8842,63 @@ declare class Collection<T> {
6843
8842
  ts: string | null;
6844
8843
  record: Record<string, unknown> | null;
6845
8844
  } | null, incoming: Record<string, unknown> | null) => Promise<void>;
8845
+ /**
8846
+ * Optional back-reference to the owning vault's guard registry +
8847
+ * read-only facade. When present, put/delete consult registered
8848
+ * guards for this collection. Same structural-interface pattern
8849
+ * as `refEnforcer` to avoid a circular Vault import.
8850
+ */
8851
+ guardSource?: {
8852
+ registry(): GuardRegistry;
8853
+ readOnlyVault(): ReadOnlyVaultFacade$1;
8854
+ } | undefined;
8855
+ /**
8856
+ * Optional back-reference to the owning vault's derivation
8857
+ * registry + collection accessor. When present, successful
8858
+ * `put()` dispatches registered derivation strategies for the
8859
+ * source collection. Same structural-interface pattern as
8860
+ * `guardSource` to avoid a circular Vault import.
8861
+ */
8862
+ derivationSource?: {
8863
+ registry(): DerivationRegistry;
8864
+ getCollection(name: string): Collection<Record<string, unknown>>;
8865
+ /**
8866
+ * Read-only vault facade handed to `derive(source, ctx)` so a
8867
+ * derivation can fetch sibling records (#147). Same shape and
8868
+ * instance the guards subsystem uses for `check(incoming, ctx)`.
8869
+ */
8870
+ getReadOnlyFacade(): ReadOnlyVaultFacade$1;
8871
+ /**
8872
+ * Read access to the owning Noydb's currently-active multi-record
8873
+ * transaction context, or `null` when no transaction is running.
8874
+ * `dispatchDerivations` consults this so a recursive derived-output
8875
+ * write can register its pre-write envelope onto `ctx._executed`
8876
+ * and roll back alongside the source op on mid-batch failure (#133).
8877
+ */
8878
+ getActiveTxContext(): TxContext | null;
8879
+ /**
8880
+ * Construct a transient TxContext bound to the owning Noydb. Used
8881
+ * by `Collection.putManyAtomic` to publish an active context for
8882
+ * its Phase 2 loop (#133).
8883
+ */
8884
+ createTxContext(): TxContext;
8885
+ /** Publish a TxContext for the duration of a bulk-atomic loop. */
8886
+ setActiveTxContext(ctx: TxContext): void;
8887
+ /** Drop a previously-published TxContext. */
8888
+ clearActiveTxContext(ctx: TxContext): void;
8889
+ } | undefined;
8890
+ /**
8891
+ * Vault-internal hook for materialized-view dispatch (#143/#150).
8892
+ * Parallel to `derivationSource`. When set, `Collection.put` fires
8893
+ * registered MV `onSourceWrite` after the standard derivation
8894
+ * dispatch.
8895
+ */
8896
+ materializedViewSource?: {
8897
+ registry(): MaterializedViewRegistry;
8898
+ getCollection(name: string): Collection<any>;
8899
+ getActiveTxContext(): TxContext | null;
8900
+ getQueryContext(): MVQueryContext;
8901
+ } | undefined;
6846
8902
  });
6847
8903
  /**
6848
8904
  * Return the Standard Schema validator attached to this collection,
@@ -6893,10 +8949,109 @@ declare class Collection<T> {
6893
8949
  staleMs?: number;
6894
8950
  pollIntervalMs?: number;
6895
8951
  }): PresenceHandle<P>;
6896
- /** Create or update a record. */
6897
- put(id: string, record: T): Promise<void>;
8952
+ /**
8953
+ * Create or update a record.
8954
+ *
8955
+ * @param id Record identifier.
8956
+ * @param record The record body (validated by the collection's schema
8957
+ * if one was attached at `vault.collection(...)` time).
8958
+ * @param options Optional metadata for audit + import workflows.
8959
+ * `reason` is stamped onto the resulting ledger entry
8960
+ * (see #1) so audit consumers can filter via
8961
+ * `entries.filter(e => e.reason?.startsWith('import:'))`.
8962
+ */
8963
+ put(id: string, record: T, options?: {
8964
+ readonly reason?: string;
8965
+ }): Promise<void>;
8966
+ /**
8967
+ * Fire registered MV strategies whose dependency set includes this
8968
+ * collection. Eager-mode MVs re-materialize inline via
8969
+ * `MaterializedViewExecutor.refresh`; lazy / manual modes are
8970
+ * no-ops in the foundation (subtask #150) — wired in #151.
8971
+ *
8972
+ * Skips entirely when the record being written is itself an
8973
+ * MV-emitted row (carries `_materializedFrom`) — defensive guard
8974
+ * against missed cycle detection.
8975
+ *
8976
+ * @internal
8977
+ */
8978
+ private dispatchMaterializedViews;
8979
+ /**
8980
+ * Fire registered derivation strategies for this source collection.
8981
+ * Eager mode runs `derive` inline and writes each output via the
8982
+ * sibling `Collection.put`; lazy mode marks dependent outputs stale
8983
+ * (D11 stub today). Errors in non-strict mode are logged and
8984
+ * skipped; strict mode propagates the first failing output's error.
8985
+ *
8986
+ * Skips entirely when the record being written is itself a derived
8987
+ * output (carries `_derivedFrom`) — defensive guard against missed
8988
+ * cycle detection.
8989
+ */
8990
+ private dispatchDerivations;
6898
8991
  /** Delete a record by ID. */
6899
8992
  delete(id: string): Promise<void>;
8993
+ /**
8994
+ * @internal — system-internal delete that bypasses user-facing
8995
+ * delete hooks (`onDelete`, accounting-period guard, FK ref
8996
+ * enforcer). Used by derivation tombstones (#144) and MV refresh
8997
+ * (Dim 14 v2) — system housekeeping shouldn't trip user invariants
8998
+ * registered against the output collection. The ledger entry and
8999
+ * history snapshot still fire so backup integrity and time-travel
9000
+ * reconstruction stay consistent.
9001
+ *
9002
+ * Returns silently for delete-of-absent (idempotent contract — both
9003
+ * paths honour this: the `txCtx === null` path also reads the prior
9004
+ * envelope and short-circuits before the ledger/event side-effects).
9005
+ *
9006
+ * When a `txCtx` is supplied, the prior envelope is captured and
9007
+ * pushed onto `txCtx._executed` BEFORE the delete fires — mirrors
9008
+ * the #133 rollback hardening for puts. Callers outside a
9009
+ * multi-record transaction pass `null` and skip the tracking.
9010
+ *
9011
+ * Amendment composition: if `_internalDelete` runs while a vault's
9012
+ * `GuardRegistry` has an amendment window open, the `{before, after:
9013
+ * null}` change pair is pushed onto the amendment change-set the
9014
+ * same way a user-initiated delete would. The `onDelete` user-hook
9015
+ * is still skipped (housekeeping must not trip user invariants in
9016
+ * normal mode), but the amendment's invariant DOES see the change
9017
+ * — so a `RCT-CANCEL-001`-style invariant pairing can reject a
9018
+ * derivation-driven tombstone fired during an admin amendment.
9019
+ *
9020
+ * Constraint to surface to consumers: output collections of
9021
+ * derivations with `optional: true` outputs should not be the
9022
+ * targets of `strict` or `cascade` inbound foreign-key refs —
9023
+ * `_internalDelete` bypasses the ref enforcer by design (the
9024
+ * `onDelete` bypass primitive). Treat the housekeeping path as
9025
+ * "system can tombstone its own emissions regardless of FK shape."
9026
+ *
9027
+ * Permission handling is unchanged: the caller must still hold
9028
+ * write permission on the collection (derivations run under the
9029
+ * user's keyring).
9030
+ */
9031
+ _internalDelete(id: string, txCtx?: TxContext | null): Promise<void>;
9032
+ private _doDelete;
9033
+ /**
9034
+ * Cascade deletes of array-shape derived rows when a source row is
9035
+ * deleted (#200). Reads each registered strategy's fanout sidecar
9036
+ * for this source id, deletes every listed derived row, then
9037
+ * deletes the sidecar itself.
9038
+ *
9039
+ * Record-shape derivations are skipped — see _doDelete's comment
9040
+ * for why the asymmetry is correct.
9041
+ *
9042
+ * @internal
9043
+ */
9044
+ private dispatchArrayDerivationsOnDelete;
9045
+ /**
9046
+ * Mirror of {@link dispatchMaterializedViews} for the delete path
9047
+ * (#181). No record content is available (it's gone), so the
9048
+ * `_materializedFrom` skip used by the put-side dispatch doesn't
9049
+ * apply here — instead, the recursion guard is the `internal` gate
9050
+ * at the `_doDelete` call site above.
9051
+ *
9052
+ * @internal
9053
+ */
9054
+ private dispatchMaterializedViewsOnDelete;
6900
9055
  /**
6901
9056
  * List all records in the collection.
6902
9057
  *
@@ -6971,6 +9126,15 @@ declare class Collection<T> {
6971
9126
  * the filtered records directly (the API). Prefer the chainable
6972
9127
  * form for new code.
6973
9128
  *
9129
+ * **Lazy-MV gap (#157):** `query()` is synchronous and does NOT
9130
+ * trigger lazy materialized-view resolve-on-read. If this
9131
+ * collection is a lazy MV's output and the MV is currently stale,
9132
+ * `query().toArray()` returns the pre-stale snapshot. To force a
9133
+ * fresh read on a lazy MV, either call `list()` (which DOES
9134
+ * trigger resolve) or `vault.refreshView(mvName)` before querying.
9135
+ * The proper fix — extending `QuerySource` with an async prepare
9136
+ * hook — is a separate PR.
9137
+ *
6974
9138
  * @example
6975
9139
  * ```ts
6976
9140
  * // New chainable API:
@@ -7123,6 +9287,11 @@ declare class Collection<T> {
7123
9287
  * .aggregate({ total: sum('amount'), n: count() })
7124
9288
  * ```
7125
9289
  *
9290
+ * **Lazy-MV gap (#157):** `scan()` is synchronous-build and does
9291
+ * NOT trigger lazy materialized-view resolve-on-read. For lazy
9292
+ * MVs, call `list()` (which DOES resolve) or `vault.refreshView(name)`
9293
+ * before scanning. Same shape as the `query()` limitation.
9294
+ *
7126
9295
  * Returns a `ScanBuilder<T>` instead of the raw async iterator
7127
9296
  * that previous versions used. The builder implements
7128
9297
  * `AsyncIterable<T>`, so every existing `for await … of` call
@@ -7141,6 +9310,22 @@ declare class Collection<T> {
7141
9310
  /** Decrypt a page of envelopes returned by `adapter.listPage`. */
7142
9311
  private decryptPage;
7143
9312
  /** Load all records from adapter into memory cache. */
9313
+ /**
9314
+ * @internal — refresh the in-memory cache entry for a single id by
9315
+ * re-reading from the adapter. Used by the transaction executor's
9316
+ * Phase-3 revert path: that path writes the prior envelope directly
9317
+ * via the raw store (to avoid re-firing Collection-level side
9318
+ * effects), which would otherwise leave this Collection's eager
9319
+ * cache holding the rolled-back value. After revert, the executor
9320
+ * calls this hook so subsequent `get` / `query` reads see the
9321
+ * actual on-disk state.
9322
+ *
9323
+ * Lazy mode: drops the LRU entry; the next `get` repopulates from
9324
+ * the adapter. Eager mode: re-reads the envelope and either sets
9325
+ * the cache entry (record still present) or deletes it (record was
9326
+ * gone before the tx and the revert deleted it again).
9327
+ */
9328
+ _invalidateCacheEntry(id: string): Promise<void>;
7144
9329
  private ensureHydrated;
7145
9330
  /** Hydrate from a pre-loaded snapshot (used by Vault). */
7146
9331
  hydrateFromSnapshot(records: Record<string, EncryptedEnvelope>): Promise<void>;
@@ -7565,7 +9750,7 @@ interface ShadowStrategy {
7565
9750
  * @internal
7566
9751
  */
7567
9752
  interface TxStrategy {
7568
- runTransaction<T>(db: Noydb, fn: (tx: TxContext) => Promise<T> | T): Promise<T>;
9753
+ runTransaction<T>(db: Noydb, fn: (tx: TxContext) => Promise<T> | T, options?: AmendmentTxOptions): Promise<T>;
7569
9754
  }
7570
9755
 
7571
9756
  /**
@@ -7695,6 +9880,160 @@ interface SessionStrategy {
7695
9880
  revokeAllSessions(): void;
7696
9881
  }
7697
9882
 
9883
+ /**
9884
+ * Managed-passphrase mode — issue #14, rubber-hose-resistant vaults.
9885
+ *
9886
+ * A vault mode where the passphrase is machine-generated and never
9887
+ * exposed to the user, sealed under a developer-provided
9888
+ * {@link SealingKeyProvider} (macOS Keychain, Windows Credential
9889
+ * Manager, libsecret, AWS KMS, …). The user has no secret to give
9890
+ * up to coercion — they can't reveal what they don't know.
9891
+ *
9892
+ * ## Components in this file
9893
+ *
9894
+ * - {@link SealingKeyProvider} — the interface concrete providers
9895
+ * implement. Provider implementations live OUTSIDE hub (per-
9896
+ * platform packages).
9897
+ * - {@link MemorySealingKeyProvider} — in-memory test provider; uses
9898
+ * a deterministic per-instance "key" so two providers with
9899
+ * different ids cannot unseal each other's outputs.
9900
+ * - {@link loadSealedPassphrase} / {@link saveSealedPassphrase} —
9901
+ * plaintext envelope storage at `_meta/sealed-passphrase`.
9902
+ * Mirrors the `_meta/handle` and `_meta/public-envelope` AES-
9903
+ * GCM-bypassed patterns. The sealing layer (provider's job)
9904
+ * is the security boundary; hub doesn't have a key to encrypt
9905
+ * with at this layer — that's the whole point of the design.
9906
+ * - {@link resolveManagedSecret} — orchestrates the "generate +
9907
+ * seal + persist on first open; unseal on reopen" flow.
9908
+ * Returns the plaintext passphrase string that the rest of the
9909
+ * `createNoydb` keyring path consumes.
9910
+ *
9911
+ * Slice 1 of #14. Deferred to follow-ups:
9912
+ * - Block `rotate-passphrase` policy gate under managed mode.
9913
+ * - Mandatory strong-recovery enforcement (depends on #10).
9914
+ * - Recovery flow under managed mode (generates fresh sealed phrase).
9915
+ *
9916
+ * @see docs/subsystems/session-tiers.md → Managed-passphrase mode
9917
+ *
9918
+ * @module
9919
+ */
9920
+
9921
+ /**
9922
+ * The contract concrete providers (per-platform key stores) implement
9923
+ * to seal and unseal a hub-generated random passphrase. The plaintext
9924
+ * passphrase NEVER leaves hub-controlled memory in unsealed form —
9925
+ * the provider receives the bytes, returns opaque sealed bytes, and
9926
+ * later reverses the operation. Hub treats the sealed bytes as
9927
+ * fully opaque.
9928
+ *
9929
+ * Implementations live OUTSIDE `@noy-db/hub` (separate packages
9930
+ * per the issue's "Concrete providers (live outside hub)" note):
9931
+ *
9932
+ * | Platform | Package (TBD) | Backing |
9933
+ * |---|---|---|
9934
+ * | macOS | `@noy-db/seal-macos-keychain` | Security.framework |
9935
+ * | Windows | `@noy-db/seal-wincred` | Credential Manager |
9936
+ * | Linux | `@noy-db/seal-libsecret` | libsecret / secret-service |
9937
+ * | Cloud / server | `@noy-db/seal-aws-kms` | AWS KMS Decrypt |
9938
+ */
9939
+ interface SealingKeyProvider {
9940
+ /**
9941
+ * Non-sensitive identifier disclosed in the persisted envelope.
9942
+ * Surfaced to consumers via `loadSealedPassphrase().providerId` so
9943
+ * a vault opened with the wrong provider class can detect the
9944
+ * mismatch and surface a clear error. NOT secret — fine to log.
9945
+ *
9946
+ * Suggested format: `<family>:<scope>` — e.g. `macos-keychain:com.acme.app`,
9947
+ * `aws-kms:arn:aws:kms:us-east-1:123:key/abc`. The hub never
9948
+ * parses this; it's purely audit metadata.
9949
+ */
9950
+ readonly id: string;
9951
+ /** Seal raw passphrase bytes. Output bytes are opaque to hub. */
9952
+ seal(passphrase: Uint8Array): Promise<Uint8Array>;
9953
+ /**
9954
+ * Reverse {@link seal}. MUST throw on tamper, wrong-provider, or
9955
+ * any other failure — hub treats a thrown error as "this provider
9956
+ * cannot unlock this vault" and surfaces it to the caller.
9957
+ */
9958
+ unseal(sealed: Uint8Array): Promise<Uint8Array>;
9959
+ }
9960
+ /**
9961
+ * In-memory test provider. NOT secure — uses a deterministic
9962
+ * per-instance "key" (16-byte SHA-256 of `id`) XOR'd over the
9963
+ * passphrase plus a 4-byte provider-id fingerprint prefix. The XOR is
9964
+ * sufficient to make different `id` values produce mutually-unsealable
9965
+ * outputs (the contract tests for that), but offers ZERO real
9966
+ * confidentiality — never use outside tests.
9967
+ *
9968
+ * Replace with a real platform provider in production.
9969
+ */
9970
+ declare class MemorySealingKeyProvider implements SealingKeyProvider {
9971
+ readonly id: string;
9972
+ private readonly fingerprint;
9973
+ private readonly keyBytes;
9974
+ constructor(opts: {
9975
+ id: string;
9976
+ });
9977
+ seal(passphrase: Uint8Array): Promise<Uint8Array>;
9978
+ unseal(sealed: Uint8Array): Promise<Uint8Array>;
9979
+ }
9980
+ /** Reserved id for the managed-passphrase envelope under `_meta`. */
9981
+ declare const SEALED_PASSPHRASE_RECORD_ID: "sealed-passphrase";
9982
+ /** Plaintext payload stored inside the `_meta/sealed-passphrase` envelope. */
9983
+ interface SealedPassphrase {
9984
+ readonly _noydb_sealed: 1;
9985
+ readonly providerId: string;
9986
+ /** Sealed bytes. Base64-encoded on the wire; decoded on load. */
9987
+ readonly sealed: Uint8Array;
9988
+ }
9989
+ /**
9990
+ * Wire-format envelope persisted at `_meta/sealed-passphrase` for
9991
+ * managed-mode vaults. The provider produces raw sealed bytes via
9992
+ * {@link SealingKeyProvider.seal}; this wrapper carries the dispatch
9993
+ * metadata hub needs to pick the right provider on the unseal path.
9994
+ *
9995
+ * Stability boundary: once shipped, the wire format only grows by
9996
+ * adding optional fields. See the at-* sealing dimension foundation
9997
+ * doc, §11.9.1.
9998
+ *
9999
+ * v1 shape (this release): `{ v: 1, _noydb_sealed: 1, pid, payload }`.
10000
+ *
10001
+ * Legacy shape (pre.14, pre.15): `{ _noydb_sealed: 1, providerId, sealed }`
10002
+ * — accepted on read for backwards compatibility; never produced on
10003
+ * write going forward.
10004
+ */
10005
+ interface SealedEnvelope {
10006
+ /** Envelope schema version. v1 is the shape shipped in pre.16. */
10007
+ readonly v: 1;
10008
+ /** Magic marker for forensics + legacy-shape detection. */
10009
+ readonly _noydb_sealed: 1;
10010
+ /** Matches the producing provider's `.id`. Dispatch key on unseal. */
10011
+ readonly pid: string;
10012
+ /** Sealed bytes from the provider, base64-encoded on the wire. */
10013
+ readonly payload: string;
10014
+ }
10015
+ /**
10016
+ * Parse a `_meta/sealed-passphrase` `_data` JSON string into the
10017
+ * in-memory {@link SealedPassphrase} representation. Accepts both:
10018
+ *
10019
+ * 1. v1 wire format `{ v: 1, _noydb_sealed: 1, pid, payload }` —
10020
+ * the shape produced from pre.16 onward.
10021
+ * 2. Legacy wire format `{ _noydb_sealed: 1, providerId, sealed }` —
10022
+ * the shape produced in pre.14/pre.15. Read-only; never written
10023
+ * going forward.
10024
+ *
10025
+ * Returns `undefined` for any input that doesn't match either shape,
10026
+ * so callers can fall back to "no managed-mode envelope present."
10027
+ *
10028
+ * @internal — exported only for the migration safety-net test suite.
10029
+ */
10030
+ declare function parseSealedEnvelope(raw: unknown): SealedPassphrase | undefined;
10031
+ declare function saveSealedPassphrase(store: NoydbStore, vault: string, payload: {
10032
+ readonly providerId: string;
10033
+ readonly sealed: Uint8Array;
10034
+ }): Promise<void>;
10035
+ declare function loadSealedPassphrase(store: NoydbStore, vault: string): Promise<SealedPassphrase | undefined>;
10036
+
7698
10037
  /**
7699
10038
  * Core types — the {@link NoydbStore} interface, envelope format, roles, and
7700
10039
  * all configuration shapes consumed by {@link createNoydb}.
@@ -7753,7 +10092,7 @@ type Permission = 'rw' | 'ro';
7753
10092
  * `'*'` is the wildcard collection matching all collections in the vault.
7754
10093
  */
7755
10094
  type Permissions = Record<string, Permission>;
7756
- /** The encrypted wrapper stored by adapters. Adapters only ever see this. */
10095
+ /** The encrypted wrapper stored by stores. Stores only ever see this. */
7757
10096
  interface EncryptedEnvelope {
7758
10097
  readonly _noydb: typeof NOYDB_FORMAT_VERSION;
7759
10098
  readonly _v: number;
@@ -7856,8 +10195,8 @@ interface ListPageResult {
7856
10195
  }
7857
10196
  interface NoydbStore {
7858
10197
  /**
7859
- * Optional human-readable adapter name (e.g. 'memory', 'file', 'dynamo').
7860
- * Used in diagnostic messages and the listPage fallback warning. Adapters
10198
+ * Optional human-readable store name (e.g. 'memory', 'file', 'dynamo').
10199
+ * Used in diagnostic messages and the listPage fallback warning. Stores
7861
10200
  * are encouraged to set this so logs are clearer about which backend is
7862
10201
  * involved when something goes wrong.
7863
10202
  */
@@ -7878,22 +10217,22 @@ interface NoydbStore {
7878
10217
  ping?(): Promise<boolean>;
7879
10218
  /**
7880
10219
  * Optional: list record IDs in a collection that have `_ts` after `since`.
7881
- * Used by partial sync (`pull({ modifiedSince })`). Adapters that omit this
10220
+ * Used by partial sync (`pull({ modifiedSince })`). Stores that omit this
7882
10221
  * fall back to a full `loadAll` + client-side timestamp filter.
7883
10222
  */
7884
10223
  listSince?(vault: string, collection: string, since: string): Promise<string[]>;
7885
10224
  /**
7886
- * Optional pagination extension. Adapters that implement `listPage` get
7887
- * the streaming `Collection.scan()` fast path; adapters that don't are
10225
+ * Optional pagination extension. Stores that implement `listPage` get
10226
+ * the streaming `Collection.scan()` fast path; stores that don't are
7888
10227
  * silently fallen back to a full `loadAll()` + slice (with a one-time
7889
10228
  * console.warn).
7890
10229
  *
7891
- * `cursor` is opaque to the core — each adapter encodes its own paging
10230
+ * `cursor` is opaque to the core — each store encodes its own paging
7892
10231
  * state (DynamoDB: base64 LastEvaluatedKey JSON; S3: ContinuationToken;
7893
10232
  * memory/file/browser: numeric offset of a sorted id list). Pass
7894
10233
  * `undefined` to start from the beginning.
7895
10234
  *
7896
- * `limit` is a soft upper bound on `items.length`. Adapters MAY return
10235
+ * `limit` is a soft upper bound on `items.length`. Stores MAY return
7897
10236
  * fewer items even when more exist (e.g. if the underlying store has
7898
10237
  * its own page size cap), and MUST signal "no more pages" by returning
7899
10238
  * `nextCursor: null`.
@@ -8106,7 +10445,7 @@ type RecoveryEnrollment = {
8106
10445
  * metadata only.
8107
10446
  */
8108
10447
  interface KeyringAuthenticatorBase {
8109
- /** Caller-chosen identifier — e.g. `'webauthn-yubikey-blue'`, `'oidc-google'`, `'password-daily'`. */
10448
+ /** Caller-chosen identifier — e.g. `'webauthn-yubikey-blue'`, `'oidc-google'`, `'password'`. */
8110
10449
  readonly id: string;
8111
10450
  /** Method family — selects which `@noy-db/on-*` package handles unlock. */
8112
10451
  readonly method: 'webauthn' | 'oidc' | 'password';
@@ -8191,6 +10530,21 @@ interface KeyringFile {
8191
10530
  readonly salt: string;
8192
10531
  readonly created_at: string;
8193
10532
  readonly granted_by: string;
10533
+ /**
10534
+ * Passphrase canary — base64 AES-KW-wrapped form of a known constant
10535
+ * 256-bit value, wrapped under the keyring's KEK (#113).
10536
+ *
10537
+ * Optional: pre-#113 keyrings load with no canary and fall back to
10538
+ * the multi-DEK corruption heuristic from #82. Keyrings written after
10539
+ * #113 carry one and let `loadKeyring` distinguish wrong-passphrase
10540
+ * from corruption even when ALL DEKs (including a single-DEK keyring's
10541
+ * sole DEK) are corrupted.
10542
+ *
10543
+ * AES-KW is deterministic — every write site mints fresh on each
10544
+ * persist; same KEK + same constant input always produces the same
10545
+ * ciphertext, so this round-trips without state.
10546
+ */
10547
+ readonly canary?: string;
8194
10548
  /**
8195
10549
  * Tier-2 authenticator slots (multi-slot keyring extension).
8196
10550
  * Optional / append-only: keyring files written before the
@@ -8434,7 +10788,7 @@ interface PullOptions {
8434
10788
  collections?: string[];
8435
10789
  /**
8436
10790
  * Only pull records with `_ts` strictly after this ISO timestamp.
8437
- * Adapters that implement `listSince` use it directly; others fall back
10791
+ * Stores that implement `listSince` use it directly; others fall back
8438
10792
  * to a full scan with client-side filtering.
8439
10793
  */
8440
10794
  modifiedSince?: string;
@@ -8586,6 +10940,44 @@ interface GrantOptions {
8586
10940
  */
8587
10941
  readonly initialProfile?: unknown;
8588
10942
  }
10943
+ /**
10944
+ * Caller payload for `db.updateUser` (#54). Mutate one or more
10945
+ * identity fields on an existing keyring without rotating any keys.
10946
+ *
10947
+ * `role`, `displayName`, and `permissions` live in the plaintext header
10948
+ * of `_keyring/<userId>` (the sync engine reads them without keys).
10949
+ * Mutating them is a JSON header swap — no DEK rewrap, no KEK
10950
+ * required, no authenticator slots touched. Tier-2 slots and recovery
10951
+ * enrollments survive unchanged. Last-write-wins through the existing
10952
+ * keyring put (same concurrency story as `db.grant` / `db.revoke`).
10953
+ *
10954
+ * Top-level fields are partial-merge: absent fields are not modified.
10955
+ * `null` on `displayName` clears the field (stored as the empty string;
10956
+ * UI consumers typically render the empty case by falling back to the
10957
+ * user id). `undefined` / absent leaves the field untouched. Mirrors
10958
+ * the `null`-as-clear convention `UserApi.updateMe` uses (#57).
10959
+ *
10960
+ * `permissions`, however, is a **full replacement** at the map level —
10961
+ * passing `{ invoices: 'rw' }` REPLACES the entire permissions map,
10962
+ * silently dropping any other entries. To partially update, read the
10963
+ * current keyring and merge: `permissions: { ...current, invoices: 'rw' }`.
10964
+ * To clear all permissions, pass `permissions: {}` explicitly.
10965
+ *
10966
+ * Role-elevation guard: the same hierarchy as `db.grant`. Admins can
10967
+ * change `admin` / `operator` / `viewer` / `client` to and from each
10968
+ * other; admins cannot promote to or demote from `owner`. Owners can
10969
+ * do anything. Non-admin callers (operator/viewer/client) cannot call
10970
+ * `db.updateUser` at all — for self-displayName changes, use
10971
+ * `vault.user.updateMe` (the user-envelope API).
10972
+ *
10973
+ * @see #54
10974
+ */
10975
+ interface UpdateUserOptions {
10976
+ readonly userId: string;
10977
+ readonly role?: Role;
10978
+ readonly displayName?: string | null;
10979
+ readonly permissions?: Permissions;
10980
+ }
8589
10981
  interface RevokeOptions {
8590
10982
  readonly userId: string;
8591
10983
  readonly rotateKeys?: boolean;
@@ -8626,8 +11018,8 @@ interface AccessibleVault {
8626
11018
  */
8627
11019
  interface ListAccessibleVaultsOptions {
8628
11020
  /**
8629
- * Minimum role the caller must hold to include a compartment in the
8630
- * result. Compartments where the caller's role is strictly *below*
11021
+ * Minimum role the caller must hold to include a vault in the
11022
+ * result. Vaults where the caller's role is strictly *below*
8631
11023
  * this threshold are silently excluded. Defaults to `'client'`,
8632
11024
  * which means "every vault I can unwrap is returned." Set to
8633
11025
  * `'admin'` for "vaults where I can grant/revoke," or
@@ -9154,6 +11546,37 @@ interface NoydbOptions {
9154
11546
  * @internal
9155
11547
  */
9156
11548
  readonly syncStrategy?: SyncStrategy;
11549
+ /**
11550
+ * Optional guard strategies — collection-level write guards. Each
11551
+ * handle is the output of `withGuard()` from `@noy-db/hub/guards`.
11552
+ * Multiple guards per collection are allowed; they are dispatched
11553
+ * in registration order on `collection.put()`.
11554
+ */
11555
+ readonly guardStrategies?: ReadonlyArray<GuardStrategyHandleAny>;
11556
+ /**
11557
+ * Optional derivation strategies — source-to-output projections that
11558
+ * fire on `collection.put()`. Each handle is the output of
11559
+ * `withDerivation()` from `@noy-db/hub/derivations`. The vault
11560
+ * validates the derivation graph for cycles on `openVault`; a cyclic
11561
+ * graph throws `DerivationCycleError`.
11562
+ */
11563
+ readonly derivationStrategies?: ReadonlyArray<DerivationStrategyHandle>;
11564
+ /**
11565
+ * Optional materialized-view strategies (#143, foundation in #150).
11566
+ * Each handle returned by `withMaterializedView()` from
11567
+ * `@noy-db/hub/materialized-views`. The vault runs unified cycle
11568
+ * detection across the MV + derivation graphs at `openVault`; a
11569
+ * cyclic graph throws `MaterializedViewCycleError`.
11570
+ */
11571
+ readonly materializedViewStrategies?: ReadonlyArray<MaterializedViewStrategyHandle>;
11572
+ /**
11573
+ * Optional overlay strategies (#154). Each handle returned by
11574
+ * `withOverlayedView()` from `@noy-db/hub/overlay-views`. The vault
11575
+ * validates name uniqueness + base concreteness + overlay
11576
+ * availability at `openVault`; a clash throws one of the
11577
+ * `Overlay*Error` family.
11578
+ */
11579
+ readonly overlayedViewStrategies?: ReadonlyArray<OverlayedViewStrategyHandle>;
9157
11580
  /** Optional remote store(s) for sync. Accepts a single store, a SyncTarget, or an array. */
9158
11581
  readonly sync?: NoydbStore | SyncTarget | SyncTarget[];
9159
11582
  /** User identifier. */
@@ -9199,6 +11622,32 @@ interface NoydbOptions {
9199
11622
  * subsequent sessions.
9200
11623
  */
9201
11624
  readonly getKeyring?: (vault: string) => Promise<UnlockedKeyring>;
11625
+ /**
11626
+ * Passphrase mode (#14). Default `'standard'`.
11627
+ *
11628
+ * - `'standard'` — the legacy flow. `secret` supplies the
11629
+ * plaintext passphrase, the user knows it, and the policy gate
11630
+ * `rotate-passphrase` is enabled.
11631
+ * - `'managed'` — rubber-hose-resistant mode. Hub generates a
11632
+ * 256-bit random passphrase at first open and seals it under
11633
+ * the provided `sealingKey`. The user never sees or types the
11634
+ * passphrase, defeating the $5-wrench attack. Mutually
11635
+ * exclusive with `secret` and `getKeyring`.
11636
+ *
11637
+ * @see docs/subsystems/session-tiers.md → Managed-passphrase mode
11638
+ */
11639
+ readonly passphraseMode?: 'standard' | 'managed';
11640
+ /**
11641
+ * Provider that seals/unseals the auto-generated managed-mode
11642
+ * passphrase. Required when `passphraseMode === 'managed'`; ignored
11643
+ * otherwise. Implementations live in per-platform packages
11644
+ * (`@noy-db/seal-macos-keychain`, `@noy-db/seal-wincred`,
11645
+ * `@noy-db/seal-libsecret`, `@noy-db/seal-aws-kms`, …).
11646
+ */
11647
+ readonly sealingKey?: SealingKeyProvider;
11648
+ /** Required to use `profile: 'shamir'` recovery. Pass
11649
+ * `shamirRecoveryProvider()` from `@noy-db/on-shamir`. */
11650
+ readonly shamirRecovery?: ShamirRecoveryProvider;
9202
11651
  /** Auth method. Default: 'passphrase'. */
9203
11652
  readonly auth?: 'passphrase' | 'biometric';
9204
11653
  /** Enable encryption. Default: true. */
@@ -9402,4 +11851,4 @@ interface DeleteManyResult {
9402
11851
  }>;
9403
11852
  }
9404
11853
 
9405
- 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 Permissions 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 KeyringFile as bA, type ListAccessibleVaultsOptions as bB, type ListPageResult as bC, type LiveUserEnvelope as bD, type LocaleReadOptions as bE, Lru as bF, type LruOptions as bG, type LruStats as bH, MAGIC_LINK_CONTENT_INFO_PREFIX as bI, MAGIC_LINK_GRANTS_COLLECTION as bJ, MAGIC_LINK_KEK_INFO_PREFIX as bK, type MagicLinkGrantPayload as bL, type MagicLinkGrantRecord as bM, NOYDB_BACKUP_VERSION as bN, NOYDB_FORMAT_VERSION as bO, NOYDB_KEYRING_VERSION as bP, NOYDB_SYNC_VERSION as bQ, Noydb as bR, type NoydbBundleStore as bS, type NoydbEventMap as bT, type NoydbOptions as bU, PUBLIC_ENVELOPE_FIELDS as bV, type PaperRecoveryDoc as bW, type PaperRecoveryEntry as bX, type PassphrasePolicy as bY, type PassphraseValidationResult as bZ, type Permission as b_, type CrossTierAccessEvent as ba, DEFAULT_PUBLIC_ENVELOPE_SCHEMA as bb, DELEGATIONS_COLLECTION as bc, type DeepPartial as bd, type DelegationToken as be, type DeleteManyResult as bf, type DirtyEntry as bg, ELEVATION_AUDIT_COLLECTION as bh, ElevatedHandle as bi, type EnrollAuthenticatorOptions as bj, type ExportCapability as bk, type ExportChunk as bl, type ExportFormat as bm, type ExportStreamOptions as bn, type FactorKind as bo, type FactorRequirement as bp, type GhostRecord as bq, type GrantOptions as br, type HistoryConfig as bs, type HistoryEntry as bt, INDEXED_STORE_POLICY as bu, type ImportCapability as bv, type InferOutput as bw, type IssueDelegationOptions as bx, type IssueMagicLinkGrantOptions as by, type KeyringAuthenticator as bz, DictionaryHandle as c, WeakPassphraseError as c$, type PlaintextTranslatorContext as c0, type PlaintextTranslatorFn as c1, PresenceHandle as c2, type PresencePeer as c3, type PublicEnvelopeField as c4, type PublicEnvelopeSchema as c5, type PublicEnvelopeText as c6, type PullMode as c7, type PullOptions as c8, type PullPolicy as c9, type StoreAuthKind as cA, type StoreCapabilities as cB, SyncEngine as cC, type SyncMetadata as cD, type SyncPolicy as cE, SyncScheduler as cF, type SyncSchedulerStatus as cG, type SyncStatus as cH, type SyncTarget as cI, type SyncTargetRole as cJ, SyncTransaction as cK, type SyncTransactionResult as cL, type TierMode as cM, type TranslatorAuditEntry as cN, type TxOp as cO, USER_ENVELOPE_COLLECTION as cP, USER_ENVELOPE_MAX_BYTES as cQ, type Unsubscribe as cR, UserApi as cS, type UserEnvelopeCheckGate as cT, UserEnvelopeOversizedError as cU, type UserEnvelopePresented as cV, type UserInfo as cW, type VaultBackup as cX, type VaultPolicyOnDisk as cY, type VaultSnapshot as cZ, type WarningRules as c_, type PullResult as ca, type PushMode as cb, type PushOptions as cc, type PushPolicy as cd, type PushResult as ce, type PutManyItemOptions as cf, type PutManyOptions as cg, type PutManyResult as ch, type QueryAcrossOptions as ci, type QueryAcrossResult as cj, type QuickUnlockState as ck, QuickUnlockStore as cl, type ReAuthOperation as cm, type RecoverPassphraseInput as cn, type RecoverPassphraseResult as co, type RecoverUserOptions as cp, type RecoveryProof as cq, type ResolvedPublicEnvelopeSchema as cr, type RevokeOptions as cs, type RotatePassphraseInput as ct, type SessionPolicy as cu, type SetPublicEnvelopeInput as cv, type StandardSchemaV1 as cw, type StandardSchemaV1Issue as cx, type StandardSchemaV1SyncResult as cy, type StoreAuth as cz, type DictionaryOptions as d, type WeakPassphraseReason as d0, type WrappedDeksBlob as d1, assertStrongPassphrase as d2, buildRecipientKeyringFile as d3, burnPaperRecoveryEntry as d4, createNoydb as d5, createStore as d6, deriveMagicLinkContentKey as d7, enrollAuthenticator as d8, estimateEntropy as d9, savePaperRecoveryEntries as dA, unwrapDeksFromBlob as dB, unwrapDeksFromPaperEntry as dC, unwrapMagicLinkGrant as dD, validatePassphrase as dE, validatePublicEnvelopeInput as dF, validateSchemaInput as dG, validateSchemaOutput as dH, writeMagicLinkGrant as dI, evaluateExportCapability as da, evaluateImportCapability as db, findAuthenticator as dc, hasExportCapability as dd, hasImportCapability as de, hasRecoveryEnrolled as df, isMagicLinkGrantExpired as dg, isPublicEnvelope as dh, issueDelegation as di, recoverPassphrase as dj, rotatePassphrase as dk, listMagicLinkGrants as dl, listUsers as dm, listUsersWithEnvelopes as dn, loadActiveDelegations as dp, loadPaperRecoveryEntries as dq, magicLinkGrantRecordId as dr, mintPaperRecoveryEntry as ds, mintWrappedDeksBlob as dt, readMagicLinkGrantRecord as du, recoverUser as dv, removeAuthenticator as dw, resolveSchema as dx, revokeDelegation as dy, revokeMagicLinkGrant 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 };
11854
+ 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 FactorRequirement 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 CollectionDescriptor as bA, type CollectionStats as bB, type Conflict as bC, type ConflictPolicy as bD, type ConflictStrategy as bE, type CrossTierAccessEvent as bF, DEFAULT_PUBLIC_ENVELOPE_SCHEMA as bG, DELEGATIONS_COLLECTION as bH, type DeepPartial as bI, type DeepPartialOrNull as bJ, type DelegationToken as bK, type DeleteManyResult as bL, type DerivationDescriptor as bM, type DirtyEntry as bN, type DumpSchemaOptions as bO, ELEVATION_AUDIT_COLLECTION as bP, ElevatedHandle as bQ, type EnrollAuthenticatorOptions as bR, type EnrollAuthenticatorWrappingDEKsOptions as bS, type EnrollAuthenticatorWrappingKEKOptions as bT, type EnrollRecoveryResult as bU, type ExportCapability as bV, type ExportChunk as bW, type ExportFormat as bX, type ExportStreamOptions as bY, type FactorKind as bZ, type FactorProofBundle as b_, type MVQueryContext as ba, type RegisteredMV as bb, MaterializedViewRegistry as bc, type MaterializedFromMeta as bd, type MaterializedViewOutput as be, type UnionSource as bf, type UserEnvelope as bg, type PublicEnvelope as bh, type GateName as bi, type GatePolicy as bj, type VaultPolicy as bk, type ActiveTier as bl, type FactorProof as bm, type PersistedSchemaEnvelope as bn, type DirectoryConfig as bo, type UserVisibility as bp, Vault as bq, type AccessibleVault as br, BUNDLE_STORE_POLICY as bs, type BuiltInGateName as bt, type BundleRecipient as bu, type CacheOptions as bv, type CacheStats as bw, type ChangeEvent as bx, type CollectionChangeEvent as by, type CollectionConflictResolver as bz, DictionaryHandle as c, type PutManyItemOptions as c$, type FieldDescriptor as c0, type FieldSource as c1, type GhostRecord as c2, type GrantOptions as c3, type HistoryConfig as c4, type HistoryEntry as c5, INDEXED_STORE_POLICY as c6, type ImportCapability as c7, type InferOutput as c8, type InternalCollectionStats as c9, type NoydbBundleStore as cA, type NoydbEventMap as cB, type NoydbOptions as cC, type OverlayViewDescriptor as cD, PUBLIC_ENVELOPE_FIELDS as cE, type PaperRecoveryDoc as cF, type PaperRecoveryEntry as cG, type PassphrasePolicy as cH, type PassphraseValidationResult as cI, type Permission as cJ, type Permissions as cK, type PersistedSchemaKind as cL, type PlaintextTranslatorContext as cM, type PlaintextTranslatorFn as cN, PresenceHandle as cO, type PresencePeer as cP, type PublicEnvelopeField as cQ, type PublicEnvelopeSchema as cR, type PublicEnvelopeText as cS, type PullMode as cT, type PullOptions as cU, type PullPolicy as cV, type PullResult as cW, type PushMode as cX, type PushOptions as cY, type PushPolicy as cZ, type PushResult as c_, type IssueDelegationOptions as ca, type IssueMagicLinkGrantOptions as cb, type KeyringAuthenticator as cc, type KeyringAuthenticatorWrappingDEKs as cd, type KeyringAuthenticatorWrappingKEK as ce, type KeyringFile as cf, type ListAccessibleVaultsOptions as cg, type ListPageResult as ch, type ListUsersOptions as ci, type LiveUserEnvelope as cj, type LocaleReadOptions as ck, Lru as cl, type LruOptions as cm, type LruStats as cn, MAGIC_LINK_CONTENT_INFO_PREFIX as co, MAGIC_LINK_GRANTS_COLLECTION as cp, MAGIC_LINK_KEK_INFO_PREFIX as cq, type MagicLinkGrantPayload as cr, type MagicLinkGrantRecord as cs, type MaterializedViewDescriptor as ct, MemorySealingKeyProvider as cu, NOYDB_BACKUP_VERSION as cv, NOYDB_FORMAT_VERSION as cw, NOYDB_KEYRING_VERSION as cx, NOYDB_SYNC_VERSION as cy, Noydb as cz, type DictionaryOptions as d, type WeakPassphraseReason as d$, type PutManyOptions as d0, type PutManyResult as d1, type QueryAcrossOptions as d2, type QueryAcrossResult as d3, type QuickUnlockState as d4, QuickUnlockStore as d5, type ReAuthOperation as d6, type RecoverPassphraseInput as d7, type RecoverPassphraseResult as d8, type RecoverUserOptions as d9, type SyncPolicy as dA, SyncScheduler as dB, type SyncSchedulerStatus as dC, type SyncStatus as dD, type SyncTarget as dE, type SyncTargetRole as dF, SyncTransaction as dG, type SyncTransactionResult as dH, type TierMode as dI, type TranslatorAuditEntry as dJ, type TxOp as dK, USER_ENVELOPE_COLLECTION as dL, USER_ENVELOPE_MAX_BYTES as dM, type Unsubscribe as dN, type UpdateAuthenticatorOptions as dO, type UpdateUserOptions as dP, UserApi as dQ, type UserEnvelopeCheckGate as dR, UserEnvelopeOversizedError as dS, type UserEnvelopePresented as dT, type UserInfo as dU, type VaultBackup as dV, type VaultPolicyOnDisk as dW, type VaultSchemaSnapshot as dX, type VaultSnapshot as dY, type WarningRules as dZ, WeakPassphraseError as d_, type RecoveryProof as da, type ResolvedPublicEnvelopeSchema as db, type RevokeOptions as dc, type RotatePassphraseInput as dd, type RotateRecoveryOptions as de, type RotateRecoveryResult as df, SEALED_PASSPHRASE_RECORD_ID as dg, type SealedEnvelope as dh, type SealedPassphrase as di, type SealingKeyProvider as dj, type SessionPolicy as dk, type SetPublicEnvelopeInput as dl, type ShamirRecoveryDoc as dm, type ShamirRecoveryEntry as dn, type ShamirRecoveryProvider as dp, type SlotRewrapCeremony as dq, type SlotRewrapContext as dr, type StandardSchemaV1 as ds, type StandardSchemaV1Issue as dt, type StandardSchemaV1SyncResult as du, type StoreAuth as dv, type StoreAuthKind as dw, type StoreCapabilities as dx, SyncEngine as dy, type SyncMetadata as dz, type I18nTextDescriptor as e, type WrappedDeksBlob as e0, assertStrongPassphrase as e1, buildRecipientKeyringFile as e2, burnPaperRecoveryEntry as e3, createNoydb as e4, createStore as e5, deriveMagicLinkContentKey as e6, enrollAuthenticator as e7, estimateEntropy as e8, evaluateExportCapability as e9, revokeDelegation as eA, revokeMagicLinkGrant as eB, savePaperRecoveryEntries as eC, saveSealedPassphrase as eD, saveShamirRecoveryEntries as eE, unwrapDeksFromBlob as eF, unwrapDeksFromPaperEntry as eG, unwrapDeksFromShamirEntry as eH, unwrapMagicLinkGrant as eI, validatePassphrase as eJ, validatePublicEnvelopeInput as eK, validateSchemaInput as eL, validateSchemaOutput as eM, writeMagicLinkGrant as eN, changeSecret as eO, createOwnerKeyring as eP, ensureCollectionDEK as eQ, grant as eR, loadKeyring as eS, persistKeyring as eT, revoke as eU, updateAuthenticator as eV, updateKeyringIdentity as eW, evaluateImportCapability as ea, findAuthenticator as eb, hasExportCapability as ec, hasImportCapability as ed, hasRecoveryEnrolled as ee, isMagicLinkGrantExpired as ef, isPublicEnvelope as eg, issueDelegation as eh, recoverPassphrase as ei, rotatePassphrase as ej, listMagicLinkGrants as ek, listUsers as el, listUsersWithEnvelopes as em, loadActiveDelegations as en, loadPaperRecoveryEntries as eo, loadSealedPassphrase as ep, loadShamirRecoveryEntries as eq, magicLinkGrantRecordId as er, mintPaperRecoveryEntry as es, mintShamirRecoveryEntry as et, mintWrappedDeksBlob as eu, parseSealedEnvelope as ev, readMagicLinkGrantRecord as ew, recoverUser as ex, removeAuthenticator as ey, resolveSchema 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 };