@noy-db/hub 0.1.0-pre.14 → 0.1.0-pre.16

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 (232) 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 +15 -8
  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 +5 -4
  9. package/dist/blobs/index.d.ts +5 -4
  10. package/dist/blobs/index.js +4 -4
  11. package/dist/bundle/index.cjs +214 -7
  12. package/dist/bundle/index.cjs.map +1 -1
  13. package/dist/bundle/index.d.cts +5 -4
  14. package/dist/bundle/index.d.ts +5 -4
  15. package/dist/bundle/index.js +6 -4
  16. package/dist/{chunk-CX2S2IBB.js → chunk-23TTQXVO.js} +53 -79
  17. package/dist/chunk-23TTQXVO.js.map +1 -0
  18. package/dist/{chunk-WLZWITX3.js → chunk-2AXFIYHT.js} +1 -1
  19. package/dist/chunk-2AXFIYHT.js.map +1 -0
  20. package/dist/{chunk-SBOSKCXD.js → chunk-2R6G2WST.js} +209 -13
  21. package/dist/chunk-2R6G2WST.js.map +1 -0
  22. package/dist/{chunk-HENWE2QQ.js → chunk-34YSDCDP.js} +2 -2
  23. package/dist/{chunk-3J5PYYUL.js → chunk-5DWL3JBF.js} +2 -2
  24. package/dist/{chunk-562AT3V6.js → chunk-5NGHX6ME.js} +5 -5
  25. package/dist/chunk-5ZGZ6HIZ.js +100 -0
  26. package/dist/chunk-5ZGZ6HIZ.js.map +1 -0
  27. package/dist/{chunk-ZJICDHNH.js → chunk-6HPZY4ON.js} +5 -4
  28. package/dist/chunk-6HPZY4ON.js.map +1 -0
  29. package/dist/{chunk-LTKFPLTW.js → chunk-A3K2ECAM.js} +192 -11
  30. package/dist/chunk-A3K2ECAM.js.map +1 -0
  31. package/dist/{chunk-W7M3E5M5.js → chunk-ADQ5MQ54.js} +48 -1
  32. package/dist/{chunk-W7M3E5M5.js.map → chunk-ADQ5MQ54.js.map} +1 -1
  33. package/dist/{chunk-Q3OWJPJI.js → chunk-DPMFBCV6.js} +32 -19
  34. package/dist/chunk-DPMFBCV6.js.map +1 -0
  35. package/dist/{chunk-AD4FMCXZ.js → chunk-DYBQG5PQ.js} +2 -2
  36. package/dist/{chunk-PNTWORBA.js → chunk-DYECX3IX.js} +2 -2
  37. package/dist/chunk-EGQYGYIU.js +51 -0
  38. package/dist/chunk-EGQYGYIU.js.map +1 -0
  39. package/dist/{chunk-RGHN6WV7.js → chunk-FCXOFQAJ.js} +2 -2
  40. package/dist/{chunk-PMMNH7VZ.js → chunk-H7XROQJM.js} +1 -1
  41. package/dist/chunk-H7XROQJM.js.map +1 -0
  42. package/dist/chunk-HB3Z2GCR.js +124 -0
  43. package/dist/chunk-HB3Z2GCR.js.map +1 -0
  44. package/dist/{chunk-CRNMA2FB.js → chunk-I6MX32UC.js} +4 -4
  45. package/dist/{chunk-PHSGPPV7.js → chunk-KESP7GOK.js} +3 -3
  46. package/dist/{chunk-FB34MDSJ.js → chunk-KJJKHKFP.js} +4 -4
  47. package/dist/{chunk-J3E2UMET.js → chunk-MIQHZESA.js} +3 -3
  48. package/dist/{chunk-TNYWH7Z2.js → chunk-MKSA2V7A.js} +2 -2
  49. package/dist/{chunk-E4ZSYZSN.js → chunk-NVBUGT76.js} +4 -4
  50. package/dist/{chunk-XFVOTOYQ.js → chunk-O4RHR7FV.js} +6 -6
  51. package/dist/{chunk-EEEFQTYN.js → chunk-OMLIZL2P.js} +2 -2
  52. package/dist/{chunk-GVPFL6XF.js → chunk-P7EQ2S5O.js} +2 -2
  53. package/dist/{chunk-E3VAKWJ7.js → chunk-QQUUAXDY.js} +7 -6
  54. package/dist/chunk-QQUUAXDY.js.map +1 -0
  55. package/dist/chunk-RD5LYKD6.js +82 -0
  56. package/dist/chunk-RD5LYKD6.js.map +1 -0
  57. package/dist/{chunk-7QGJ7RTL.js → chunk-RRMDLTIL.js} +4 -4
  58. package/dist/{chunk-432R4RDC.js → chunk-SIZWEV2Y.js} +35 -5
  59. package/dist/chunk-SIZWEV2Y.js.map +1 -0
  60. package/dist/{chunk-U7YDBNFC.js → chunk-UZXLQCHP.js} +2 -2
  61. package/dist/{chunk-PPJA7KL6.js → chunk-V6SGP2KX.js} +3 -3
  62. package/dist/{chunk-7AQKLLI5.js → chunk-WCA2NROQ.js} +2 -2
  63. package/dist/{chunk-ERV4AXVO.js → chunk-XGSOTWYX.js} +90 -131
  64. package/dist/chunk-XGSOTWYX.js.map +1 -0
  65. package/dist/{chunk-5Y4AKEZ6.js → chunk-Z72JH4KG.js} +2 -2
  66. package/dist/{chunk-IXFP66OV.js → chunk-ZNOEIM6Y.js} +2 -2
  67. package/dist/consent/index.cjs.map +1 -1
  68. package/dist/consent/index.d.cts +5 -4
  69. package/dist/consent/index.d.ts +5 -4
  70. package/dist/consent/index.js +3 -3
  71. package/dist/{crypto-VTEMMJK6.js → crypto-A7FRXYHC.js} +3 -3
  72. package/dist/{delegation-HYQULPZR.js → delegation-YBA4X4JN.js} +5 -5
  73. package/dist/derivations/index.cjs +97 -1
  74. package/dist/derivations/index.cjs.map +1 -1
  75. package/dist/derivations/index.d.cts +37 -10
  76. package/dist/derivations/index.d.ts +37 -10
  77. package/dist/derivations/index.js +6 -4
  78. package/dist/{dev-unlock-CpJ9rHW2.d.ts → dev-unlock-C8u6MRfS.d.ts} +1 -1
  79. package/dist/{dev-unlock-DEUUpWoR.d.cts → dev-unlock-gBjwzB6A.d.cts} +1 -1
  80. package/dist/executor-7E3VFGW7.js +11 -0
  81. package/dist/executor-CEWX2FQI.js +8 -0
  82. package/dist/executor-X4SQ3ZLC.js +8 -0
  83. package/dist/fanout-sidecar-ACJNXFU6.js +51 -0
  84. package/dist/fanout-sidecar-ACJNXFU6.js.map +1 -0
  85. package/dist/guards/index.cjs.map +1 -1
  86. package/dist/guards/index.d.cts +6 -5
  87. package/dist/guards/index.d.ts +6 -5
  88. package/dist/guards/index.js +6 -6
  89. package/dist/{hash-_9VyW44M.d.cts → hash-BXy5DuTD.d.cts} +1 -1
  90. package/dist/{hash-CJ9LfuY5.d.ts → hash-C6KOPK9Q.d.ts} +1 -1
  91. package/dist/history/index.cjs +2 -1
  92. package/dist/history/index.cjs.map +1 -1
  93. package/dist/history/index.d.cts +6 -5
  94. package/dist/history/index.d.ts +6 -5
  95. package/dist/history/index.js +6 -6
  96. package/dist/i18n/index.cjs.map +1 -1
  97. package/dist/i18n/index.d.cts +5 -4
  98. package/dist/i18n/index.d.ts +5 -4
  99. package/dist/i18n/index.js +6 -6
  100. package/dist/{index-s31g3m-D.d.cts → index-BA7A-MJs.d.cts} +96 -4
  101. package/dist/{index-BWAXR9tV.d.ts → index-CLK67MZE.d.ts} +96 -4
  102. package/dist/{index-7nd15fgy.d.ts → index-CNwA-B6-.d.ts} +70 -2
  103. package/dist/{index-CFJKJXWa.d.cts → index-CmVgTkqk.d.cts} +70 -2
  104. package/dist/index.cjs +2400 -571
  105. package/dist/index.cjs.map +1 -1
  106. package/dist/index.d.cts +128 -18
  107. package/dist/index.d.ts +128 -18
  108. package/dist/index.js +1217 -107
  109. package/dist/index.js.map +1 -1
  110. package/dist/indexing/index.cjs.map +1 -1
  111. package/dist/indexing/index.js +2 -2
  112. package/dist/{ledger-LHLIVPJ3.js → ledger-GHIZQLKR.js} +6 -6
  113. package/dist/materialized-views/index.cjs +255 -21
  114. package/dist/materialized-views/index.cjs.map +1 -1
  115. package/dist/materialized-views/index.d.cts +7 -6
  116. package/dist/materialized-views/index.d.ts +7 -6
  117. package/dist/materialized-views/index.js +9 -5
  118. package/dist/overlay-views/index.cjs.map +1 -1
  119. package/dist/overlay-views/index.d.cts +6 -5
  120. package/dist/overlay-views/index.d.ts +6 -5
  121. package/dist/overlay-views/index.js +3 -3
  122. package/dist/periods/index.cjs +2 -1
  123. package/dist/periods/index.cjs.map +1 -1
  124. package/dist/periods/index.d.cts +5 -4
  125. package/dist/periods/index.d.ts +5 -4
  126. package/dist/periods/index.js +6 -6
  127. package/dist/{public-envelope-66ZBXVXK.js → public-envelope-CRJAVW3E.js} +4 -4
  128. package/dist/query/index.cjs +139 -113
  129. package/dist/query/index.cjs.map +1 -1
  130. package/dist/query/index.d.cts +2 -2
  131. package/dist/query/index.d.ts +2 -2
  132. package/dist/query/index.js +13 -9
  133. package/dist/{registry-JYALMHGM.js → registry-3L3N3PTG.js} +3 -3
  134. package/dist/registry-O47PUPSY.js +8 -0
  135. package/dist/registry-WLLMODKN.js +8 -0
  136. package/dist/session/index.cjs.map +1 -1
  137. package/dist/session/index.d.cts +6 -5
  138. package/dist/session/index.d.ts +6 -5
  139. package/dist/session/index.js +3 -3
  140. package/dist/shadow/index.cjs.map +1 -1
  141. package/dist/shadow/index.d.cts +5 -4
  142. package/dist/shadow/index.d.ts +5 -4
  143. package/dist/shadow/index.js +2 -2
  144. package/dist/{stale-EET5YFYL.js → stale-HSC5YO2O.js} +2 -2
  145. package/dist/store/index.cjs.map +1 -1
  146. package/dist/store/index.d.cts +5 -4
  147. package/dist/store/index.d.ts +5 -4
  148. package/dist/store/index.js +2 -2
  149. package/dist/{strategy-D-SrOLCl.d.cts → strategy-DSTrsZ8t.d.cts} +72 -19
  150. package/dist/{strategy-D-SrOLCl.d.ts → strategy-DSTrsZ8t.d.ts} +72 -19
  151. package/dist/sync/index.cjs.map +1 -1
  152. package/dist/sync/index.d.cts +4 -3
  153. package/dist/sync/index.d.ts +4 -3
  154. package/dist/sync/index.js +4 -4
  155. package/dist/team/index.cjs +136 -6
  156. package/dist/team/index.cjs.map +1 -1
  157. package/dist/team/index.d.cts +5 -4
  158. package/dist/team/index.d.ts +5 -4
  159. package/dist/team/index.js +7 -7
  160. package/dist/tx/index.cjs +2 -1
  161. package/dist/tx/index.cjs.map +1 -1
  162. package/dist/tx/index.d.cts +5 -4
  163. package/dist/tx/index.d.ts +5 -4
  164. package/dist/tx/index.js +2 -2
  165. package/dist/{types-2JGVRzO2.d.cts → types-DDy02bWN.d.cts} +1159 -257
  166. package/dist/{types-DIOLYPUq.d.ts → types-DImsOlJe.d.ts} +1159 -257
  167. package/dist/util/index.cjs.map +1 -1
  168. package/dist/util/index.js +1 -1
  169. package/dist/{with-derivation-ClhfQvRa.d.ts → with-derivation-3AdLeSw8.d.ts} +1 -1
  170. package/dist/{with-derivation-3JX8SMxY.d.cts → with-derivation-CkCHx_J8.d.cts} +1 -1
  171. package/dist/{with-guard-Br3YPxRW.d.ts → with-guard-Br9zLEdi.d.ts} +1 -1
  172. package/dist/{with-guard-fJVfWBLs.d.cts → with-guard-DtKsFExw.d.cts} +1 -1
  173. package/dist/with-materialized-view-CMhEX4FK.d.cts +27 -0
  174. package/dist/with-materialized-view-Zal2bE1T.d.ts +27 -0
  175. package/dist/{with-overlayed-view-BLUKxdiJ.d.cts → with-overlayed-view-CA6zHrBt.d.cts} +1 -1
  176. package/dist/{with-overlayed-view-CtvJxhU3.d.ts → with-overlayed-view-R4BNG9OB.d.ts} +1 -1
  177. package/package.json +14 -2
  178. package/dist/chunk-432R4RDC.js.map +0 -1
  179. package/dist/chunk-CX2S2IBB.js.map +0 -1
  180. package/dist/chunk-E3VAKWJ7.js.map +0 -1
  181. package/dist/chunk-ERV4AXVO.js.map +0 -1
  182. package/dist/chunk-GV47JHIF.js +0 -29
  183. package/dist/chunk-GV47JHIF.js.map +0 -1
  184. package/dist/chunk-LTKFPLTW.js.map +0 -1
  185. package/dist/chunk-PMMNH7VZ.js.map +0 -1
  186. package/dist/chunk-Q2L5ZYX7.js +0 -66
  187. package/dist/chunk-Q2L5ZYX7.js.map +0 -1
  188. package/dist/chunk-Q3OWJPJI.js.map +0 -1
  189. package/dist/chunk-SBK6CETA.js +0 -30
  190. package/dist/chunk-SBK6CETA.js.map +0 -1
  191. package/dist/chunk-SBOSKCXD.js.map +0 -1
  192. package/dist/chunk-WLZWITX3.js.map +0 -1
  193. package/dist/chunk-ZJICDHNH.js.map +0 -1
  194. package/dist/executor-3H3NSIVZ.js +0 -8
  195. package/dist/executor-7ZOXGY7P.js +0 -8
  196. package/dist/executor-A5LTUAG2.js +0 -9
  197. package/dist/registry-J2UI37XJ.js +0 -8
  198. package/dist/registry-UMAL72WL.js +0 -8
  199. package/dist/with-materialized-view-CHho83hN.d.cts +0 -15
  200. package/dist/with-materialized-view-CgPphnza.d.ts +0 -15
  201. /package/dist/{chunk-HENWE2QQ.js.map → chunk-34YSDCDP.js.map} +0 -0
  202. /package/dist/{chunk-3J5PYYUL.js.map → chunk-5DWL3JBF.js.map} +0 -0
  203. /package/dist/{chunk-562AT3V6.js.map → chunk-5NGHX6ME.js.map} +0 -0
  204. /package/dist/{chunk-AD4FMCXZ.js.map → chunk-DYBQG5PQ.js.map} +0 -0
  205. /package/dist/{chunk-PNTWORBA.js.map → chunk-DYECX3IX.js.map} +0 -0
  206. /package/dist/{chunk-RGHN6WV7.js.map → chunk-FCXOFQAJ.js.map} +0 -0
  207. /package/dist/{chunk-CRNMA2FB.js.map → chunk-I6MX32UC.js.map} +0 -0
  208. /package/dist/{chunk-PHSGPPV7.js.map → chunk-KESP7GOK.js.map} +0 -0
  209. /package/dist/{chunk-FB34MDSJ.js.map → chunk-KJJKHKFP.js.map} +0 -0
  210. /package/dist/{chunk-J3E2UMET.js.map → chunk-MIQHZESA.js.map} +0 -0
  211. /package/dist/{chunk-TNYWH7Z2.js.map → chunk-MKSA2V7A.js.map} +0 -0
  212. /package/dist/{chunk-E4ZSYZSN.js.map → chunk-NVBUGT76.js.map} +0 -0
  213. /package/dist/{chunk-XFVOTOYQ.js.map → chunk-O4RHR7FV.js.map} +0 -0
  214. /package/dist/{chunk-EEEFQTYN.js.map → chunk-OMLIZL2P.js.map} +0 -0
  215. /package/dist/{chunk-GVPFL6XF.js.map → chunk-P7EQ2S5O.js.map} +0 -0
  216. /package/dist/{chunk-7QGJ7RTL.js.map → chunk-RRMDLTIL.js.map} +0 -0
  217. /package/dist/{chunk-U7YDBNFC.js.map → chunk-UZXLQCHP.js.map} +0 -0
  218. /package/dist/{chunk-PPJA7KL6.js.map → chunk-V6SGP2KX.js.map} +0 -0
  219. /package/dist/{chunk-7AQKLLI5.js.map → chunk-WCA2NROQ.js.map} +0 -0
  220. /package/dist/{chunk-5Y4AKEZ6.js.map → chunk-Z72JH4KG.js.map} +0 -0
  221. /package/dist/{chunk-IXFP66OV.js.map → chunk-ZNOEIM6Y.js.map} +0 -0
  222. /package/dist/{crypto-VTEMMJK6.js.map → crypto-A7FRXYHC.js.map} +0 -0
  223. /package/dist/{delegation-HYQULPZR.js.map → delegation-YBA4X4JN.js.map} +0 -0
  224. /package/dist/{executor-3H3NSIVZ.js.map → executor-7E3VFGW7.js.map} +0 -0
  225. /package/dist/{executor-7ZOXGY7P.js.map → executor-CEWX2FQI.js.map} +0 -0
  226. /package/dist/{executor-A5LTUAG2.js.map → executor-X4SQ3ZLC.js.map} +0 -0
  227. /package/dist/{ledger-LHLIVPJ3.js.map → ledger-GHIZQLKR.js.map} +0 -0
  228. /package/dist/{public-envelope-66ZBXVXK.js.map → public-envelope-CRJAVW3E.js.map} +0 -0
  229. /package/dist/{registry-J2UI37XJ.js.map → registry-3L3N3PTG.js.map} +0 -0
  230. /package/dist/{registry-JYALMHGM.js.map → registry-O47PUPSY.js.map} +0 -0
  231. /package/dist/{registry-UMAL72WL.js.map → registry-WLLMODKN.js.map} +0 -0
  232. /package/dist/{stale-EET5YFYL.js.map → stale-HSC5YO2O.js.map} +0 -0
package/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __esm = (fn, res) => function __init() {
7
9
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
@@ -18,6 +20,14 @@ var __copyProps = (to, from, except, desc) => {
18
20
  }
19
21
  return to;
20
22
  };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
21
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
22
32
 
23
33
  // src/types.ts
@@ -36,7 +46,7 @@ var init_types = __esm({
36
46
  });
37
47
 
38
48
  // src/errors.ts
39
- var NoydbError, DecryptionError, TamperedError, InvalidKeyError, KeyringCorruptError, NoAccessError, ReadOnlyError, ReadOnlyAtInstantError, ReadOnlyFrameError, PermissionDeniedError, ExportCapabilityError, KeyringExpiredError, ImportCapabilityError, StoreCapabilityError, PrivilegeEscalationError, PeriodClosedError, RecordLockedError, FieldFrozenError, InvariantError, AmendmentForbiddenError, DirectoryDisabledError, TierNotGrantedError, ElevationExpiredError, AlreadyElevatedError, TierDemoteDeniedError, DelegationTargetMissingError, ConflictError, LedgerContentionError, BundleVersionConflictError, NetworkError, NotFoundError, ValidationError, SchemaValidationError, GroupCardinalityError, IndexRequiredError, IndexWriteFailureError, BundleIntegrityError, ReservedCollectionNameError, DictKeyMissingError, DictKeyInUseError, MissingTranslationError, LocaleNotSpecifiedError, TranslatorNotConfiguredError, BackupLedgerError, BackupCorruptedError, SessionExpiredError, SessionNotFoundError, SessionPolicyError, JoinTooLargeError, DanglingReferenceError, FilenameSanitizationError, PathEscapeError, DerivationCycleError, DerivationDepthError, DerivationOutputUnknownError, DerivationOutputShapeError, MaterializedViewCycleError, MaterializedViewSourceUnknownError, MaterializedViewTooLargeError, OverlayBaseIsVirtualError, OverlayCollectionUnavailableError, OverlayNameCollisionError, OverlayIdMismatchError;
49
+ var NoydbError, DecryptionError, TamperedError, InvalidKeyError, KeyringCorruptError, NoAccessError, ReadOnlyError, ReadOnlyAtInstantError, ReadOnlyFrameError, PermissionDeniedError, ExportCapabilityError, KeyringExpiredError, ImportCapabilityError, StoreCapabilityError, PrivilegeEscalationError, PeriodClosedError, RecordLockedError, FieldFrozenError, InvariantError, AmendmentForbiddenError, DirectoryDisabledError, TierNotGrantedError, ElevationExpiredError, AlreadyElevatedError, TierDemoteDeniedError, DelegationTargetMissingError, ConflictError, LedgerContentionError, BundleVersionConflictError, NetworkError, NotFoundError, ValidationError, SchemaValidationError, GroupCardinalityError, IndexRequiredError, IndexWriteFailureError, BundleIntegrityError, BundleSealMismatchError, ReservedCollectionNameError, DictKeyMissingError, DictKeyInUseError, MissingTranslationError, LocaleNotSpecifiedError, TranslatorNotConfiguredError, BackupLedgerError, BackupCorruptedError, SessionExpiredError, SessionNotFoundError, SessionPolicyError, JoinTooLargeError, DanglingReferenceError, FilenameSanitizationError, PathEscapeError, DerivationCycleError, DerivationDepthError, DerivationOutputUnknownError, DerivationOutputShapeError, DerivationCapExceededError, MaterializedViewCycleError, MaterializedViewSourceUnknownError, MaterializedViewTooLargeError, MaterializedViewConfigError, OverlayBaseIsVirtualError, OverlayCollectionUnavailableError, OverlayNameCollisionError, OverlayIdMismatchError;
40
50
  var init_errors = __esm({
41
51
  "src/errors.ts"() {
42
52
  "use strict";
@@ -423,6 +433,26 @@ var init_errors = __esm({
423
433
  this.name = "BundleIntegrityError";
424
434
  }
425
435
  };
436
+ BundleSealMismatchError = class extends NoydbError {
437
+ userId;
438
+ pid;
439
+ constructor(userId, pid) {
440
+ super(
441
+ "BUNDLE_SEAL_MISMATCH",
442
+ `bundle carries sealed passphrase for user "${userId}" under provider "${pid}", but no registered provider matches that pid.
443
+
444
+ Resolutions:
445
+ 1. Configure a provider matching the pid and retry import.
446
+ 2. Pass \`attemptUnsealAcrossProviders: true\` to try each registered
447
+ provider regardless of pid (extra credential prompts may surface).
448
+ 3. Inspect the bundle without unsealing \u2014 pass no \`sealingProviders\`
449
+ to receive the sealed entries unmodified for offline analysis.`
450
+ );
451
+ this.name = "BundleSealMismatchError";
452
+ this.userId = userId;
453
+ this.pid = pid;
454
+ }
455
+ };
426
456
  ReservedCollectionNameError = class extends NoydbError {
427
457
  /** The rejected collection name. */
428
458
  collectionName;
@@ -651,6 +681,21 @@ var init_errors = __esm({
651
681
  this.outputKey = outputKey;
652
682
  }
653
683
  };
684
+ DerivationCapExceededError = class extends NoydbError {
685
+ outputKey;
686
+ returned;
687
+ maxFanout;
688
+ constructor(outputKey, returned, maxFanout) {
689
+ super(
690
+ "DERIVATION_CAP_EXCEEDED",
691
+ `Derivation array output "${outputKey}" returned ${returned} rows, exceeding maxFanout=${maxFanout}. Raise \`maxFanout\` on the OutputSpec if this fanout is intended (the cap exists to keep dispatch cost bounded).`
692
+ );
693
+ this.name = "DerivationCapExceededError";
694
+ this.outputKey = outputKey;
695
+ this.returned = returned;
696
+ this.maxFanout = maxFanout;
697
+ }
698
+ };
654
699
  MaterializedViewCycleError = class extends NoydbError {
655
700
  path;
656
701
  constructor(path) {
@@ -690,6 +735,15 @@ var init_errors = __esm({
690
735
  this.limit = limit;
691
736
  }
692
737
  };
738
+ MaterializedViewConfigError = class extends NoydbError {
739
+ constructor(message) {
740
+ super(
741
+ "MATERIALIZED_VIEW_CONFIG",
742
+ `[noy-db] withMaterializedView: ${message}`
743
+ );
744
+ this.name = "MaterializedViewConfigError";
745
+ }
746
+ };
693
747
  OverlayBaseIsVirtualError = class extends NoydbError {
694
748
  overlayName;
695
749
  base;
@@ -1666,7 +1720,8 @@ var init_store = __esm({
1666
1720
  const entry = {
1667
1721
  ...entryBase,
1668
1722
  ...deltaHash !== void 0 ? { deltaHash } : {},
1669
- ...input.amendment !== void 0 ? { amendment: input.amendment } : {}
1723
+ ...input.amendment !== void 0 ? { amendment: input.amendment } : {},
1724
+ ...input.reason !== void 0 ? { reason: input.reason } : {}
1670
1725
  };
1671
1726
  const envelope = await this.encryptEntry(entry);
1672
1727
  await this.adapter.put(
@@ -2069,6 +2124,428 @@ var init_tiers = __esm({
2069
2124
  }
2070
2125
  });
2071
2126
 
2127
+ // src/query/predicate.ts
2128
+ function readPath(record, path) {
2129
+ if (record === null || record === void 0) return void 0;
2130
+ if (!path.includes(".")) {
2131
+ return record[path];
2132
+ }
2133
+ const segments = path.split(".");
2134
+ let cursor = record;
2135
+ for (const segment of segments) {
2136
+ if (cursor === null || cursor === void 0) return void 0;
2137
+ cursor = cursor[segment];
2138
+ }
2139
+ return cursor;
2140
+ }
2141
+ function evaluateFieldClause(record, clause) {
2142
+ const actual = readPath(record, clause.field);
2143
+ const { op, value } = clause;
2144
+ switch (op) {
2145
+ case "==":
2146
+ return actual === value;
2147
+ case "!=":
2148
+ return actual !== value;
2149
+ case "<":
2150
+ return isComparable(actual, value) && actual < value;
2151
+ case "<=":
2152
+ return isComparable(actual, value) && actual <= value;
2153
+ case ">":
2154
+ return isComparable(actual, value) && actual > value;
2155
+ case ">=":
2156
+ return isComparable(actual, value) && actual >= value;
2157
+ case "in":
2158
+ return Array.isArray(value) && value.includes(actual);
2159
+ case "contains":
2160
+ if (typeof actual === "string") return typeof value === "string" && actual.includes(value);
2161
+ if (Array.isArray(actual)) return actual.includes(value);
2162
+ return false;
2163
+ case "startsWith":
2164
+ return typeof actual === "string" && typeof value === "string" && actual.startsWith(value);
2165
+ case "between": {
2166
+ if (!Array.isArray(value) || value.length !== 2) return false;
2167
+ const [lo, hi] = value;
2168
+ if (!isComparable(actual, lo) || !isComparable(actual, hi)) return false;
2169
+ return actual >= lo && actual <= hi;
2170
+ }
2171
+ default: {
2172
+ const _exhaustive = op;
2173
+ void _exhaustive;
2174
+ return false;
2175
+ }
2176
+ }
2177
+ }
2178
+ function isComparable(a, b) {
2179
+ if (typeof a === "number" && typeof b === "number") return true;
2180
+ if (typeof a === "string" && typeof b === "string") return true;
2181
+ if (a instanceof Date && b instanceof Date) return true;
2182
+ return false;
2183
+ }
2184
+ function evaluateClause(record, clause) {
2185
+ switch (clause.type) {
2186
+ case "field":
2187
+ return evaluateFieldClause(record, clause);
2188
+ case "filter":
2189
+ return clause.fn(record);
2190
+ case "wherePredicate":
2191
+ return clause.fn(record, clause.ctx);
2192
+ case "group":
2193
+ if (clause.op === "and") {
2194
+ for (const child of clause.clauses) {
2195
+ if (!evaluateClause(record, child)) return false;
2196
+ }
2197
+ return true;
2198
+ } else {
2199
+ for (const child of clause.clauses) {
2200
+ if (evaluateClause(record, child)) return true;
2201
+ }
2202
+ return false;
2203
+ }
2204
+ }
2205
+ }
2206
+ var init_predicate = __esm({
2207
+ "src/query/predicate.ts"() {
2208
+ "use strict";
2209
+ }
2210
+ });
2211
+
2212
+ // src/aggregate/aggregation.ts
2213
+ function reduceRecords(records, spec) {
2214
+ const state = {};
2215
+ for (const key of Object.keys(spec)) {
2216
+ state[key] = spec[key].init();
2217
+ }
2218
+ for (const record of records) {
2219
+ for (const key of Object.keys(spec)) {
2220
+ state[key] = spec[key].step(state[key], record);
2221
+ }
2222
+ }
2223
+ const result = {};
2224
+ for (const key of Object.keys(spec)) {
2225
+ result[key] = spec[key].finalize(state[key]);
2226
+ }
2227
+ return result;
2228
+ }
2229
+ function buildLiveAggregation(recompute, upstreams) {
2230
+ return new LiveAggregationImpl(recompute, upstreams);
2231
+ }
2232
+ var LiveAggregationImpl, Aggregation;
2233
+ var init_aggregation = __esm({
2234
+ "src/aggregate/aggregation.ts"() {
2235
+ "use strict";
2236
+ LiveAggregationImpl = class {
2237
+ constructor(recompute, upstreams) {
2238
+ this.recompute = recompute;
2239
+ try {
2240
+ this.value = recompute();
2241
+ this.error = void 0;
2242
+ } catch (err) {
2243
+ this.value = void 0;
2244
+ this.error = err;
2245
+ }
2246
+ for (const upstream of upstreams) {
2247
+ const unsub = upstream.subscribe(() => this.refresh());
2248
+ this.unsubscribes.push(unsub);
2249
+ }
2250
+ }
2251
+ recompute;
2252
+ value;
2253
+ error;
2254
+ listeners = /* @__PURE__ */ new Set();
2255
+ unsubscribes = [];
2256
+ stopped = false;
2257
+ refresh() {
2258
+ if (this.stopped) return;
2259
+ try {
2260
+ this.value = this.recompute();
2261
+ this.error = void 0;
2262
+ } catch (err) {
2263
+ this.error = err;
2264
+ }
2265
+ for (const listener of this.listeners) {
2266
+ try {
2267
+ listener();
2268
+ } catch (err) {
2269
+ console.warn("[noy-db] LiveAggregation listener threw:", err);
2270
+ }
2271
+ }
2272
+ }
2273
+ subscribe(cb) {
2274
+ if (this.stopped) {
2275
+ return () => {
2276
+ };
2277
+ }
2278
+ this.listeners.add(cb);
2279
+ return () => {
2280
+ this.listeners.delete(cb);
2281
+ };
2282
+ }
2283
+ stop() {
2284
+ if (this.stopped) return;
2285
+ this.stopped = true;
2286
+ for (const unsub of this.unsubscribes) {
2287
+ try {
2288
+ unsub();
2289
+ } catch (err) {
2290
+ console.warn("[noy-db] LiveAggregation upstream unsubscribe threw:", err);
2291
+ }
2292
+ }
2293
+ this.unsubscribes.length = 0;
2294
+ this.listeners.clear();
2295
+ }
2296
+ };
2297
+ Aggregation = class {
2298
+ constructor(executeRecords, spec, upstreams) {
2299
+ this.executeRecords = executeRecords;
2300
+ this.spec = spec;
2301
+ this.upstreams = upstreams;
2302
+ }
2303
+ executeRecords;
2304
+ spec;
2305
+ upstreams;
2306
+ /**
2307
+ * Execute the query and reduce the results synchronously.
2308
+ * Returns the reduced shape matching the spec — e.g. a spec of
2309
+ * `{ total: sum('amount'), n: count() }` returns
2310
+ * `{ total: number, n: number }`.
2311
+ */
2312
+ run() {
2313
+ return reduceRecords(this.executeRecords(), this.spec);
2314
+ }
2315
+ /**
2316
+ * Build a reactive `LiveAggregation<R>` that re-runs the reduction
2317
+ * whenever any upstream source notifies of a change. The initial
2318
+ * value is computed eagerly in the constructor, so consumers can
2319
+ * read `live.value` immediately after calling `.live()`.
2320
+ *
2321
+ * Always call `live.stop()` when finished — it tears down the
2322
+ * upstream subscriptions. Vue's `onUnmounted` is the canonical
2323
+ * place.
2324
+ *
2325
+ * **Implementation note:** every upstream change triggers a full
2326
+ * re-reduction. Incremental maintenance (O(1) per delta for
2327
+ * sum/count/avg via the reducer protocol's `remove()` method) is a
2328
+ * planned follow-up optimization — the protocol already supports
2329
+ * it, but the executor doesn't drive it yet. Consumers get
2330
+ * correct, reactive values today; future PRs can switch to
2331
+ * delta-based maintenance without changing this API.
2332
+ */
2333
+ live() {
2334
+ const recompute = () => reduceRecords(this.executeRecords(), this.spec);
2335
+ return new LiveAggregationImpl(recompute, this.upstreams);
2336
+ }
2337
+ };
2338
+ }
2339
+ });
2340
+
2341
+ // src/aggregate/canonical-key.ts
2342
+ function canonicalGroupKey(fields, row) {
2343
+ const sorted = [...fields].sort();
2344
+ const parts = [];
2345
+ for (const name of sorted) {
2346
+ const v = row[name];
2347
+ const serialised = v === void 0 ? "undefined" : JSON.stringify(v);
2348
+ parts.push(`${name}=${serialised}`);
2349
+ }
2350
+ return parts.join("|");
2351
+ }
2352
+ var init_canonical_key = __esm({
2353
+ "src/aggregate/canonical-key.ts"() {
2354
+ "use strict";
2355
+ }
2356
+ });
2357
+
2358
+ // src/aggregate/groupby.ts
2359
+ function warnCardinalityApproaching(fields, observed) {
2360
+ const key = JSON.stringify([...fields].sort());
2361
+ if (warnedCardinalityFields.has(key)) return;
2362
+ warnedCardinalityFields.add(key);
2363
+ const label = `[${fields.join(", ")}]`;
2364
+ console.warn(
2365
+ `[noy-db] .groupBy(${label}) produced ${observed} distinct groups, ${Math.round(observed / GROUPBY_MAX_CARDINALITY * 100)}% of the ${GROUPBY_MAX_CARDINALITY}-group ceiling. Narrow the query with .where() before grouping, or switch to a lower-cardinality field.`
2366
+ );
2367
+ }
2368
+ function groupAndReduce(records, fieldOrFields, spec) {
2369
+ const fields = typeof fieldOrFields === "string" ? [fieldOrFields] : fieldOrFields;
2370
+ if (fields.length === 0) {
2371
+ throw new Error(".groupBy() requires at least one field");
2372
+ }
2373
+ const buckets = /* @__PURE__ */ new Map();
2374
+ const fieldLabel = fields.length === 1 ? fields[0] : `[${fields.join(", ")}]`;
2375
+ for (const record of records) {
2376
+ const keyValues = {};
2377
+ for (const f of fields) {
2378
+ keyValues[f] = readPath(record, f);
2379
+ }
2380
+ const dedupKey = canonicalGroupKey(fields, keyValues);
2381
+ let bucket = buckets.get(dedupKey);
2382
+ if (bucket === void 0) {
2383
+ if (buckets.size >= GROUPBY_MAX_CARDINALITY) {
2384
+ throw new GroupCardinalityError(
2385
+ fieldLabel,
2386
+ buckets.size + 1,
2387
+ GROUPBY_MAX_CARDINALITY
2388
+ );
2389
+ }
2390
+ bucket = { keyValues, records: [] };
2391
+ buckets.set(dedupKey, bucket);
2392
+ }
2393
+ bucket.records.push(record);
2394
+ }
2395
+ if (buckets.size >= GROUPBY_WARN_CARDINALITY) {
2396
+ warnCardinalityApproaching(fields, buckets.size);
2397
+ }
2398
+ const reducerKeys = Object.keys(spec);
2399
+ const out = [];
2400
+ for (const bucket of buckets.values()) {
2401
+ const state = {};
2402
+ for (const rk of reducerKeys) {
2403
+ state[rk] = spec[rk].init();
2404
+ }
2405
+ for (const record of bucket.records) {
2406
+ for (const rk of reducerKeys) {
2407
+ state[rk] = spec[rk].step(state[rk], record);
2408
+ }
2409
+ }
2410
+ const row = {};
2411
+ for (const f of fields) {
2412
+ row[f] = bucket.keyValues[f];
2413
+ }
2414
+ for (const rk of reducerKeys) {
2415
+ row[rk] = spec[rk].finalize(state[rk]);
2416
+ }
2417
+ out.push(row);
2418
+ }
2419
+ return out;
2420
+ }
2421
+ var GROUPBY_WARN_CARDINALITY, GROUPBY_MAX_CARDINALITY, warnedCardinalityFields, GroupedQueryBase, GroupedQuery, GroupedQueryN, GroupedAggregation;
2422
+ var init_groupby = __esm({
2423
+ "src/aggregate/groupby.ts"() {
2424
+ "use strict";
2425
+ init_predicate();
2426
+ init_aggregation();
2427
+ init_canonical_key();
2428
+ init_errors();
2429
+ GROUPBY_WARN_CARDINALITY = 1e4;
2430
+ GROUPBY_MAX_CARDINALITY = 1e5;
2431
+ warnedCardinalityFields = /* @__PURE__ */ new Set();
2432
+ GroupedQueryBase = class {
2433
+ constructor(executeRecords, fieldOrFields, upstreams, dictLabelResolver) {
2434
+ this.executeRecords = executeRecords;
2435
+ this.upstreams = upstreams;
2436
+ this.dictLabelResolver = dictLabelResolver;
2437
+ this.fields = typeof fieldOrFields === "string" ? [fieldOrFields] : [...fieldOrFields];
2438
+ }
2439
+ executeRecords;
2440
+ upstreams;
2441
+ dictLabelResolver;
2442
+ /**
2443
+ * Field set this grouped query buckets on. Stored in declaration
2444
+ * order — the same order is preserved on every result row by
2445
+ * `groupAndReduce`. For the single-field constructor, this is
2446
+ * `[field]`.
2447
+ */
2448
+ fields;
2449
+ };
2450
+ GroupedQuery = class extends GroupedQueryBase {
2451
+ /**
2452
+ * Build a grouped aggregation. Returns a `GroupedAggregation`
2453
+ * with `.run()`, `.runAsync()`, and `.live()` terminals — same shape
2454
+ * as the non-grouped `.aggregate()` wrapper, just with an array
2455
+ * result (one row per bucket) instead of a single reduced object.
2456
+ */
2457
+ aggregate(spec) {
2458
+ return new GroupedAggregation(
2459
+ this.executeRecords,
2460
+ this.fields,
2461
+ spec,
2462
+ this.upstreams,
2463
+ this.dictLabelResolver
2464
+ );
2465
+ }
2466
+ };
2467
+ GroupedQueryN = class extends GroupedQueryBase {
2468
+ aggregate(spec) {
2469
+ return new GroupedAggregation(
2470
+ this.executeRecords,
2471
+ this.fields,
2472
+ spec,
2473
+ this.upstreams,
2474
+ this.dictLabelResolver
2475
+ );
2476
+ }
2477
+ };
2478
+ GroupedAggregation = class {
2479
+ constructor(executeRecords, fields, spec, upstreams, dictLabelResolver) {
2480
+ this.executeRecords = executeRecords;
2481
+ this.spec = spec;
2482
+ this.upstreams = upstreams;
2483
+ this.dictLabelResolver = dictLabelResolver;
2484
+ this.fields = typeof fields === "string" ? [fields] : [...fields];
2485
+ }
2486
+ executeRecords;
2487
+ spec;
2488
+ upstreams;
2489
+ dictLabelResolver;
2490
+ fields;
2491
+ /** Execute the query, group, reduce, and return an array of rows. */
2492
+ run() {
2493
+ return groupAndReduce(this.executeRecords(), this.fields, this.spec);
2494
+ }
2495
+ /**
2496
+ * Execute the query, group, reduce, and resolve `<field>Label` for
2497
+ * each result row when the grouping field is a `dictKey` and a
2498
+ * `locale` is provided. Returns `R[]` synchronously when
2499
+ * no locale is specified (identical to `.run()`).
2500
+ *
2501
+ * The `<field>Label` field is appended to each row. Rows whose group
2502
+ * key has no dictionary entry get `<field>Label: undefined`.
2503
+ *
2504
+ * Dict-label resolution is single-field only — multi-key groupings
2505
+ * do not produce a `<field>Label`. The resolver is only attached
2506
+ * by the builder when `fields.length === 1`.
2507
+ */
2508
+ async runAsync(opts) {
2509
+ const rows = groupAndReduce(this.executeRecords(), this.fields, this.spec);
2510
+ if (!opts?.locale || !this.dictLabelResolver || this.fields.length !== 1) return rows;
2511
+ const resolve = this.dictLabelResolver;
2512
+ const locale = opts.locale;
2513
+ const fallback = opts.fallback;
2514
+ const field = this.fields[0];
2515
+ const labelKey = `${field}Label`;
2516
+ return Promise.all(
2517
+ rows.map(async (row) => {
2518
+ const key = row[field];
2519
+ if (typeof key !== "string") return row;
2520
+ const label = await resolve(key, locale, fallback);
2521
+ return { ...row, [labelKey]: label };
2522
+ })
2523
+ );
2524
+ }
2525
+ /**
2526
+ * Build a reactive `LiveAggregation<R[]>` that re-runs the full
2527
+ * group-and-reduce pipeline whenever any upstream source notifies
2528
+ * of a change. Same error-isolation and idempotent-stop contract
2529
+ * as `Aggregation.live()` — the implementation delegates to the
2530
+ * same `LiveAggregationImpl` class by threading a fresh
2531
+ * recompute closure through the existing constructor.
2532
+ *
2533
+ * uses naive full re-run on every change. Incremental
2534
+ * per-bucket maintenance (apply `step` on inserted records,
2535
+ * `remove` on deleted records, route by bucket key) is a future
2536
+ * optimization — the reducer protocol admits it, but wiring
2537
+ * delta-aware source subscriptions is a separate PR.
2538
+ *
2539
+ * Always call `live.stop()` when finished.
2540
+ */
2541
+ live() {
2542
+ const recompute = () => groupAndReduce(this.executeRecords(), this.fields, this.spec);
2543
+ return buildLiveAggregation(recompute, this.upstreams);
2544
+ }
2545
+ };
2546
+ }
2547
+ });
2548
+
2072
2549
  // src/guards/executor.ts
2073
2550
  var executor_exports = {};
2074
2551
  __export(executor_exports, {
@@ -2168,6 +2645,7 @@ var init_executor2 = __esm({
2168
2645
  } catch (err) {
2169
2646
  for (const key of Object.keys(strategy.outputs)) {
2170
2647
  outputs[key] = {
2648
+ kind: "failed",
2171
2649
  value: {},
2172
2650
  ok: false,
2173
2651
  error: err instanceof Error ? err : new Error(String(err))
@@ -2186,9 +2664,64 @@ var init_executor2 = __esm({
2186
2664
  const outSpec = strategy.outputs[key];
2187
2665
  if (!outSpec) continue;
2188
2666
  const value = derived[key];
2667
+ if (outSpec.shape === "array") {
2668
+ if (value === void 0 || value === null) {
2669
+ outputs[key] = { kind: "array", ok: true, entries: [] };
2670
+ continue;
2671
+ }
2672
+ if (!Array.isArray(value)) {
2673
+ throw new DerivationOutputShapeError(
2674
+ key,
2675
+ `shape 'array' expects an array, got ${typeof value}`
2676
+ );
2677
+ }
2678
+ const maxFanout = outSpec.maxFanout ?? 64;
2679
+ if (value.length > maxFanout) {
2680
+ throw new DerivationCapExceededError(key, value.length, maxFanout);
2681
+ }
2682
+ const entries = [];
2683
+ const seenKeys = /* @__PURE__ */ new Set();
2684
+ for (let i = 0; i < value.length; i++) {
2685
+ const row = value[i];
2686
+ if (row === null || typeof row !== "object") {
2687
+ throw new DerivationOutputShapeError(
2688
+ key,
2689
+ `array member at index ${i} must be a non-null object (got ${row === null ? "null" : typeof row})`
2690
+ );
2691
+ }
2692
+ let derivedKey;
2693
+ try {
2694
+ derivedKey = outSpec.key(row);
2695
+ } catch (err) {
2696
+ throw new DerivationOutputShapeError(
2697
+ key,
2698
+ `key extractor threw on array member at index ${i}: ` + (err instanceof Error ? err.message : String(err))
2699
+ );
2700
+ }
2701
+ if (typeof derivedKey !== "string" || derivedKey.length === 0) {
2702
+ throw new DerivationOutputShapeError(
2703
+ key,
2704
+ `key extractor returned ${typeof derivedKey === "string" ? "empty string" : typeof derivedKey} at index ${i}; expected non-empty string`
2705
+ );
2706
+ }
2707
+ if (seenKeys.has(derivedKey)) {
2708
+ throw new DerivationOutputShapeError(
2709
+ key,
2710
+ `duplicate key "${derivedKey}" in array output (index ${i}); each derived row must have a unique key within a single derive() invocation`
2711
+ );
2712
+ }
2713
+ seenKeys.add(derivedKey);
2714
+ entries.push({
2715
+ key: derivedKey,
2716
+ value: { ...row, _derivedFrom: meta }
2717
+ });
2718
+ }
2719
+ outputs[key] = { kind: "array", ok: true, entries };
2720
+ continue;
2721
+ }
2189
2722
  if (value === void 0 || value === null) {
2190
2723
  if (outSpec.optional === true) {
2191
- outputs[key] = { value: {}, ok: true, skipped: true };
2724
+ outputs[key] = { kind: "record", value: {}, ok: true, skipped: true };
2192
2725
  continue;
2193
2726
  }
2194
2727
  throw new DerivationOutputShapeError(
@@ -2203,6 +2736,7 @@ var init_executor2 = __esm({
2203
2736
  );
2204
2737
  }
2205
2738
  outputs[key] = {
2739
+ kind: "record",
2206
2740
  value: { ...value, _derivedFrom: meta },
2207
2741
  ok: true
2208
2742
  };
@@ -2246,6 +2780,12 @@ function summarizeQueryPlan(query) {
2246
2780
  joins: plan.joins.map((j) => ({ field: j.field, as: j.as, target: j.target, mode: j.mode }))
2247
2781
  });
2248
2782
  }
2783
+ function summarizeUnionPlan(spec) {
2784
+ const arms = (spec.unionSources ?? []).map((s) => s.collection).join(",");
2785
+ const groupBy = Array.isArray(spec.groupBy) ? [...spec.groupBy].sort().join(",") : typeof spec.groupBy === "string" ? spec.groupBy : "";
2786
+ const aggKeys = spec.aggregate ? Object.keys(spec.aggregate).sort().join(",") : "";
2787
+ return `union(${arms})|groupBy(${groupBy})|aggregate(${aggKeys})`;
2788
+ }
2249
2789
  var init_dependency_analyzer = __esm({
2250
2790
  "src/materialized-views/dependency-analyzer.ts"() {
2251
2791
  "use strict";
@@ -2365,27 +2905,34 @@ var init_registry = __esm({
2365
2905
  */
2366
2906
  async register(spec, db, options) {
2367
2907
  const dbForQuery = spec.predicates ? wrapDbWithPredicates(db, spec.predicates) : db;
2368
- const q = spec.query(dbForQuery);
2369
- const qAny = q;
2370
- const isQuery = typeof qAny._plan === "function";
2371
2908
  let dependencies;
2372
2909
  let queryPlanSummary;
2373
- if (isQuery) {
2374
- dependencies = analyzeDependencies(q);
2375
- queryPlanSummary = summarizeQueryPlan(q);
2376
- const predicateRefs = extractPredicateRefs(qAny._plan());
2377
- if (predicateRefs.length > 0) {
2378
- queryPlanSummary = JSON.stringify({ plan: queryPlanSummary, predicates: predicateRefs });
2379
- }
2380
- if (spec.sources) for (const s of spec.sources) dependencies.add(s);
2910
+ let qAny = null;
2911
+ let isQuery = false;
2912
+ if (spec.unionSources) {
2913
+ dependencies = new Set(spec.unionSources.map((s) => s.collection));
2914
+ queryPlanSummary = summarizeUnionPlan(spec);
2381
2915
  } else {
2382
- if (!spec.sources || spec.sources.length === 0) {
2383
- throw new Error(
2384
- `withMaterializedView "${spec.name}": query() returned an aggregate (Aggregation or GroupedAggregation) but no \`sources\` field is declared. The dependency analyzer cannot walk through groupBy().aggregate() back to the source \u2014 declare sources: [...] explicitly.`
2385
- );
2916
+ const q = spec.query(dbForQuery);
2917
+ qAny = q;
2918
+ isQuery = typeof qAny._plan === "function";
2919
+ if (isQuery) {
2920
+ dependencies = analyzeDependencies(q);
2921
+ queryPlanSummary = summarizeQueryPlan(q);
2922
+ const predicateRefs = extractPredicateRefs(qAny._plan());
2923
+ if (predicateRefs.length > 0) {
2924
+ queryPlanSummary = JSON.stringify({ plan: queryPlanSummary, predicates: predicateRefs });
2925
+ }
2926
+ if (spec.sources) for (const s of spec.sources) dependencies.add(s);
2927
+ } else {
2928
+ if (!spec.sources || spec.sources.length === 0) {
2929
+ throw new Error(
2930
+ `withMaterializedView "${spec.name}": query() returned an aggregate (Aggregation or GroupedAggregation) but no \`sources\` field is declared. The dependency analyzer cannot walk through groupBy().aggregate() back to the source \u2014 declare sources: [...] explicitly.`
2931
+ );
2932
+ }
2933
+ dependencies = new Set(spec.sources);
2934
+ queryPlanSummary = JSON.stringify({ aggregate: true, sources: [...spec.sources].sort() });
2386
2935
  }
2387
- dependencies = new Set(spec.sources);
2388
- queryPlanSummary = JSON.stringify({ aggregate: true, sources: [...spec.sources].sort() });
2389
2936
  }
2390
2937
  if (options?.knownCollections) {
2391
2938
  for (const dep of dependencies) {
@@ -2514,6 +3061,27 @@ async function materializeQueryResult(q, mvName) {
2514
3061
  `MV "${mvName}": query() must return a Query<T>, Aggregation, or GroupedAggregation. Got something without a .toArray() or .run() terminal.`
2515
3062
  );
2516
3063
  }
3064
+ async function materializeUnionResult(spec, db) {
3065
+ const unified = [];
3066
+ for (const arm of spec.unionSources) {
3067
+ const coll = db.collection(arm.collection);
3068
+ const sourceRows = coll.query().toArray();
3069
+ for (const r of sourceRows) {
3070
+ unified.push(arm.map(r));
3071
+ }
3072
+ }
3073
+ if (!spec.groupBy) return unified;
3074
+ const groupFields = typeof spec.groupBy === "string" ? [spec.groupBy] : spec.groupBy;
3075
+ if (!spec.aggregate) {
3076
+ const seen = /* @__PURE__ */ new Map();
3077
+ for (const row of unified) {
3078
+ const k = canonicalGroupKey(groupFields, row);
3079
+ if (!seen.has(k)) seen.set(k, row);
3080
+ }
3081
+ return [...seen.values()];
3082
+ }
3083
+ return groupAndReduce(unified, groupFields, spec.aggregate);
3084
+ }
2517
3085
  async function listOutputIds(outputColl) {
2518
3086
  const cAny = outputColl;
2519
3087
  const adapter = cAny.adapter;
@@ -2533,6 +3101,8 @@ var init_executor3 = __esm({
2533
3101
  "use strict";
2534
3102
  init_errors();
2535
3103
  init_registry();
3104
+ init_groupby();
3105
+ init_canonical_key();
2536
3106
  DEFAULT_MAX_ROWS = 1e5;
2537
3107
  MaterializedViewExecutor = {
2538
3108
  async refresh(reg, accessor) {
@@ -2543,8 +3113,13 @@ var init_executor3 = __esm({
2543
3113
  const strict = spec.strict ?? false;
2544
3114
  const baseCtx = accessor.getQueryContext();
2545
3115
  const ctxForQuery = spec.predicates ? wrapDbWithPredicates(baseCtx, spec.predicates) : baseCtx;
2546
- const q = spec.query(ctxForQuery);
2547
- const rows = await materializeQueryResult(q, spec.name);
3116
+ let rows;
3117
+ if (spec.unionSources) {
3118
+ rows = await materializeUnionResult(spec, ctxForQuery);
3119
+ } else {
3120
+ const q = spec.query(ctxForQuery);
3121
+ rows = await materializeQueryResult(q, spec.name);
3122
+ }
2548
3123
  if (rows.length > maxRows) {
2549
3124
  throw new MaterializedViewTooLargeError(spec.name, rows.length, maxRows);
2550
3125
  }
@@ -2669,8 +3244,62 @@ var init_stale = __esm({
2669
3244
  }
2670
3245
  });
2671
3246
 
2672
- // src/guards/registry.ts
2673
- var registry_exports2 = {};
3247
+ // src/derivations/fanout-sidecar.ts
3248
+ var fanout_sidecar_exports = {};
3249
+ __export(fanout_sidecar_exports, {
3250
+ deleteFanoutSidecar: () => deleteFanoutSidecar,
3251
+ loadFanoutSidecar: () => loadFanoutSidecar,
3252
+ saveFanoutSidecar: () => saveFanoutSidecar
3253
+ });
3254
+ function recordId(source, sourceId, outputKey) {
3255
+ return `derivations-fanout/${source}/${sourceId}/${outputKey}`;
3256
+ }
3257
+ async function loadFanoutSidecar(store, vault, source, sourceId, outputKey) {
3258
+ const envelope = await store.get(vault, "_meta", recordId(source, sourceId, outputKey));
3259
+ if (!envelope) return void 0;
3260
+ try {
3261
+ const parsed = JSON.parse(envelope._data);
3262
+ if (parsed._noydb_fanout !== 1) return void 0;
3263
+ if (!Array.isArray(parsed.keys)) return void 0;
3264
+ return parsed;
3265
+ } catch {
3266
+ return void 0;
3267
+ }
3268
+ }
3269
+ async function saveFanoutSidecar(store, vault, payload) {
3270
+ const doc = {
3271
+ _noydb_fanout: 1,
3272
+ source: payload.source,
3273
+ sourceId: payload.sourceId,
3274
+ outputKey: payload.outputKey,
3275
+ outputCollection: payload.outputCollection,
3276
+ keys: payload.keys,
3277
+ emittedAt: (/* @__PURE__ */ new Date()).toISOString()
3278
+ };
3279
+ const id = recordId(payload.source, payload.sourceId, payload.outputKey);
3280
+ const prior = await store.get(vault, "_meta", id);
3281
+ const envelope = {
3282
+ _noydb: NOYDB_FORMAT_VERSION,
3283
+ _v: (prior?._v ?? 0) + 1,
3284
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
3285
+ // AES-GCM bypassed — sidecar is system metadata, no user data inside.
3286
+ _iv: "",
3287
+ _data: JSON.stringify(doc)
3288
+ };
3289
+ await store.put(vault, "_meta", id, envelope);
3290
+ }
3291
+ async function deleteFanoutSidecar(store, vault, source, sourceId, outputKey) {
3292
+ await store.delete(vault, "_meta", recordId(source, sourceId, outputKey));
3293
+ }
3294
+ var init_fanout_sidecar = __esm({
3295
+ "src/derivations/fanout-sidecar.ts"() {
3296
+ "use strict";
3297
+ init_types();
3298
+ }
3299
+ });
3300
+
3301
+ // src/guards/registry.ts
3302
+ var registry_exports2 = {};
2674
3303
  __export(registry_exports2, {
2675
3304
  GuardRegistry: () => GuardRegistry
2676
3305
  });
@@ -3076,6 +3705,7 @@ __export(src_exports, {
3076
3705
  BackupLedgerError: () => BackupLedgerError,
3077
3706
  BlobSet: () => BlobSet,
3078
3707
  BundleIntegrityError: () => BundleIntegrityError,
3708
+ BundleSealMismatchError: () => BundleSealMismatchError,
3079
3709
  BundleVersionConflictError: () => BundleVersionConflictError,
3080
3710
  CONSENT_AUDIT_COLLECTION: () => CONSENT_AUDIT_COLLECTION,
3081
3711
  Collection: () => Collection,
@@ -3093,6 +3723,7 @@ __export(src_exports, {
3093
3723
  DanglingReferenceError: () => DanglingReferenceError,
3094
3724
  DecryptionError: () => DecryptionError,
3095
3725
  DelegationTargetMissingError: () => DelegationTargetMissingError,
3726
+ DerivationCapExceededError: () => DerivationCapExceededError,
3096
3727
  DerivationCycleError: () => DerivationCycleError,
3097
3728
  DerivationDepthError: () => DerivationDepthError,
3098
3729
  DerivationOutputShapeError: () => DerivationOutputShapeError,
@@ -3112,6 +3743,7 @@ __export(src_exports, {
3112
3743
  GroupCardinalityError: () => GroupCardinalityError,
3113
3744
  GroupedAggregation: () => GroupedAggregation,
3114
3745
  GroupedQuery: () => GroupedQuery,
3746
+ GroupedQueryN: () => GroupedQueryN,
3115
3747
  INDEXED_STORE_POLICY: () => INDEXED_STORE_POLICY,
3116
3748
  ImportCapabilityError: () => ImportCapabilityError,
3117
3749
  IndexRequiredError: () => IndexRequiredError,
@@ -3131,9 +3763,12 @@ __export(src_exports, {
3131
3763
  MAGIC_LINK_GRANTS_COLLECTION: () => MAGIC_LINK_GRANTS_COLLECTION,
3132
3764
  MAGIC_LINK_KEK_INFO_PREFIX: () => MAGIC_LINK_KEK_INFO_PREFIX,
3133
3765
  META_COLLECTION: () => META_COLLECTION2,
3766
+ ManagedRecoveryNotEnrolledError: () => ManagedRecoveryNotEnrolledError,
3767
+ MaterializedViewConfigError: () => MaterializedViewConfigError,
3134
3768
  MaterializedViewCycleError: () => MaterializedViewCycleError,
3135
3769
  MaterializedViewSourceUnknownError: () => MaterializedViewSourceUnknownError,
3136
3770
  MaterializedViewTooLargeError: () => MaterializedViewTooLargeError,
3771
+ MemorySealingKeyProvider: () => MemorySealingKeyProvider,
3137
3772
  MissingTranslationError: () => MissingTranslationError,
3138
3773
  NOYDB_BACKUP_VERSION: () => NOYDB_BACKUP_VERSION,
3139
3774
  NOYDB_BUNDLE_FORMAT_VERSION: () => NOYDB_BUNDLE_FORMAT_VERSION,
@@ -3175,6 +3810,8 @@ __export(src_exports, {
3175
3810
  RefRegistry: () => RefRegistry,
3176
3811
  RefScopeError: () => RefScopeError,
3177
3812
  ReservedCollectionNameError: () => ReservedCollectionNameError,
3813
+ SCHEMAS_COLLECTION: () => SCHEMAS_COLLECTION,
3814
+ SEALED_PASSPHRASE_RECORD_ID: () => SEALED_PASSPHRASE_RECORD_ID,
3178
3815
  STRICT_POLICY: () => STRICT_POLICY,
3179
3816
  SYNC_CREDENTIALS_COLLECTION: () => SYNC_CREDENTIALS_COLLECTION,
3180
3817
  ScanBuilder: () => ScanBuilder,
@@ -3226,6 +3863,7 @@ __export(src_exports, {
3226
3863
  createSession: () => createSession,
3227
3864
  createStore: () => createStore,
3228
3865
  credentialStatus: () => credentialStatus,
3866
+ decodeShareBase32: () => import_on_shamir2.decodeShareBase32,
3229
3867
  decryptBytes: () => decryptBytes,
3230
3868
  decryptDeterministic: () => decryptDeterministic,
3231
3869
  dekKey: () => dekKey,
@@ -3233,6 +3871,7 @@ __export(src_exports, {
3233
3871
  deleteUserEnvelope: () => deleteUserEnvelope,
3234
3872
  deleteUserVisibility: () => deleteUserVisibility,
3235
3873
  deriveMagicLinkContentKey: () => deriveMagicLinkContentKey,
3874
+ derivePersistedSchema: () => derivePersistedSchema,
3236
3875
  derivePresenceKey: () => derivePresenceKey,
3237
3876
  describeAllUsersAuth: () => describeAllUsersAuth,
3238
3877
  describeAuthConfig: () => describeAuthConfig,
@@ -3247,6 +3886,7 @@ __export(src_exports, {
3247
3886
  diffVault: () => diffVault,
3248
3887
  effectiveClearance: () => effectiveClearance,
3249
3888
  enableDevUnlock: () => enableDevUnlock,
3889
+ encodeShareBase32: () => import_on_shamir2.encodeShareBase32,
3250
3890
  encryptBytes: () => encryptBytes,
3251
3891
  encryptDeterministic: () => encryptDeterministic,
3252
3892
  enrollAuthenticator: () => enrollAuthenticator,
@@ -3278,6 +3918,7 @@ __export(src_exports, {
3278
3918
  isPublicEnvelope: () => isPublicEnvelope,
3279
3919
  isSessionAlive: () => isSessionAlive,
3280
3920
  isULID: () => isULID,
3921
+ isZodSchema: () => isZodSchema,
3281
3922
  issueDelegation: () => issueDelegation,
3282
3923
  keyringRecoverPassphrase: () => recoverPassphrase,
3283
3924
  keyringRotatePassphrase: () => rotatePassphrase,
@@ -3289,7 +3930,10 @@ __export(src_exports, {
3289
3930
  loadActiveDelegations: () => loadActiveDelegations,
3290
3931
  loadDevUnlock: () => loadDevUnlock,
3291
3932
  loadPaperRecoveryEntries: () => loadPaperRecoveryEntries,
3933
+ loadPersistedSchema: () => loadPersistedSchema,
3292
3934
  loadPublicEnvelope: () => loadPublicEnvelope,
3935
+ loadSealedPassphrase: () => loadSealedPassphrase,
3936
+ loadShamirRecoveryEntries: () => loadShamirRecoveryEntries,
3293
3937
  loadUserEnvelope: () => loadUserEnvelope,
3294
3938
  loadVaultPolicy: () => loadVaultPolicy,
3295
3939
  magicLinkGrantRecordId: () => magicLinkGrantRecordId,
@@ -3298,11 +3942,14 @@ __export(src_exports, {
3298
3942
  mergePolicy: () => mergePolicy,
3299
3943
  min: () => min,
3300
3944
  mintPaperRecoveryEntry: () => mintPaperRecoveryEntry,
3945
+ mintShamirRecoveryEntry: () => mintShamirRecoveryEntry,
3301
3946
  mintWrappedDeksBlob: () => mintWrappedDeksBlob,
3302
3947
  paddedIndex: () => paddedIndex,
3303
3948
  parseBytes: () => parseBytes,
3304
3949
  parseIndex: () => parseIndex,
3950
+ parseSealedEnvelope: () => parseSealedEnvelope,
3305
3951
  persistDirectoryConfig: () => persistDirectoryConfig,
3952
+ persistSchemaIfNeeded: () => persistSchemaIfNeeded,
3306
3953
  persistUserVisibility: () => persistUserVisibility,
3307
3954
  putCredential: () => putCredential,
3308
3955
  readDirectoryConfig: () => readDirectoryConfig,
@@ -3330,13 +3977,17 @@ __export(src_exports, {
3330
3977
  routeStore: () => routeStore,
3331
3978
  runTransaction: () => runTransaction,
3332
3979
  savePaperRecoveryEntries: () => savePaperRecoveryEntries,
3980
+ savePersistedSchema: () => savePersistedSchema,
3333
3981
  savePublicEnvelope: () => savePublicEnvelope,
3982
+ saveSealedPassphrase: () => saveSealedPassphrase,
3983
+ saveShamirRecoveryEntries: () => saveShamirRecoveryEntries,
3334
3984
  saveUserEnvelope: () => saveUserEnvelope,
3335
3985
  saveVaultPolicy: () => saveVaultPolicy,
3336
3986
  sha256Hex: () => sha256Hex3,
3337
3987
  sum: () => sum,
3338
3988
  unwrapDeksFromBlob: () => unwrapDeksFromBlob,
3339
3989
  unwrapDeksFromPaperEntry: () => unwrapDeksFromPaperEntry,
3990
+ unwrapDeksFromShamirEntry: () => unwrapDeksFromShamirEntry,
3340
3991
  unwrapMagicLinkGrant: () => unwrapMagicLinkGrant,
3341
3992
  validateI18nTextValue: () => validateI18nTextValue,
3342
3993
  validatePassphrase: () => validatePassphrase,
@@ -5228,7 +5879,8 @@ var ALLOWED_HEADER_KEYS = /* @__PURE__ */ new Set([
5228
5879
  "handle",
5229
5880
  "bodyBytes",
5230
5881
  "bodySha256",
5231
- "publicEnvelope"
5882
+ "publicEnvelope",
5883
+ "autoUnlock"
5232
5884
  ]);
5233
5885
  function validateBundleHeader(parsed) {
5234
5886
  if (parsed === null || typeof parsed !== "object") {
@@ -5283,6 +5935,14 @@ function validateBundleHeader(parsed) {
5283
5935
  );
5284
5936
  }
5285
5937
  }
5938
+ if (h["autoUnlock"] !== void 0) {
5939
+ if (h["autoUnlock"] !== "unsealed" && h["autoUnlock"] !== "sealed") {
5940
+ const got = typeof h["autoUnlock"] === "string" ? `"${h["autoUnlock"]}"` : typeof h["autoUnlock"];
5941
+ throw new Error(
5942
+ `.noydb bundle header.autoUnlock must be 'unsealed' or 'sealed' when present, got ${got}.`
5943
+ );
5944
+ }
5945
+ }
5286
5946
  }
5287
5947
  function encodeBundleHeader(header) {
5288
5948
  validateBundleHeader(header);
@@ -5291,7 +5951,8 @@ function encodeBundleHeader(header) {
5291
5951
  handle: header.handle,
5292
5952
  bodyBytes: header.bodyBytes,
5293
5953
  bodySha256: header.bodySha256,
5294
- ...header.publicEnvelope !== void 0 ? { publicEnvelope: header.publicEnvelope } : {}
5954
+ ...header.publicEnvelope !== void 0 ? { publicEnvelope: header.publicEnvelope } : {},
5955
+ ...header.autoUnlock !== void 0 ? { autoUnlock: header.autoUnlock } : {}
5295
5956
  });
5296
5957
  return new TextEncoder().encode(json);
5297
5958
  }
@@ -5328,6 +5989,167 @@ function hasNoydbBundleMagic(bytes) {
5328
5989
  // src/bundle/bundle.ts
5329
5990
  init_errors();
5330
5991
  init_storage();
5992
+ function validateAutoUnlockOptions(opts) {
5993
+ if (opts.autoPassphrases !== void 0 && opts.sealedPassphrases !== void 0) {
5994
+ throw new ValidationError(
5995
+ "writeNoydbBundle: `autoPassphrases` and `sealedPassphrases` are mutually exclusive. Choose one or the other."
5996
+ );
5997
+ }
5998
+ if (opts.autoPassphrases !== void 0) {
5999
+ if (opts.autoPassphrases.policy !== "public-by-design") {
6000
+ throw new ValidationError(
6001
+ 'writeNoydbBundle: `autoPassphrases` requires `policy: "public-by-design"`. This is an explicit opt-in marker \u2014 bundling plaintext credentials is safe only when those credentials are intended to be public (demo data, sample vaults). For production credentials, use `sealedPassphrases` instead.'
6002
+ );
6003
+ }
6004
+ const userCount = Object.keys(opts.autoPassphrases.perUser).length;
6005
+ if (userCount === 0) {
6006
+ throw new ValidationError(
6007
+ "writeNoydbBundle: `autoPassphrases.perUser` must have at least one entry."
6008
+ );
6009
+ }
6010
+ return "unsealed";
6011
+ }
6012
+ if (opts.sealedPassphrases !== void 0) {
6013
+ if (opts.sealedPassphrases.mode !== "self-target") {
6014
+ throw new ValidationError(
6015
+ `writeNoydbBundle: \`sealedPassphrases.mode\` must be 'self-target' in slice 1 (got '${opts.sealedPassphrases.mode}'). Recipient-target sealing via the RecipientSealer interface is deferred per foundation \xA711.4.`
6016
+ );
6017
+ }
6018
+ if (opts.sealedPassphrases.provider === void 0) {
6019
+ throw new ValidationError(
6020
+ "writeNoydbBundle: `sealedPassphrases.provider` is required (a `SealingKeyProvider`)."
6021
+ );
6022
+ }
6023
+ const userCount = Object.keys(opts.sealedPassphrases.perUser).length;
6024
+ if (userCount === 0) {
6025
+ throw new ValidationError(
6026
+ "writeNoydbBundle: `sealedPassphrases.perUser` must have at least one entry."
6027
+ );
6028
+ }
6029
+ return "sealed";
6030
+ }
6031
+ return null;
6032
+ }
6033
+ async function buildAutoUnlockWrapper(dumpJson, opts) {
6034
+ if (opts.autoPassphrases !== void 0) {
6035
+ return {
6036
+ _noydb_bundle_body: 1,
6037
+ dump: dumpJson,
6038
+ _autoUnlock: {
6039
+ kind: "unsealed",
6040
+ perUser: { ...opts.autoPassphrases.perUser }
6041
+ }
6042
+ };
6043
+ }
6044
+ if (opts.sealedPassphrases === void 0) {
6045
+ throw new Error("unreachable \u2014 validation should have caught this");
6046
+ }
6047
+ const { provider, perUser } = opts.sealedPassphrases;
6048
+ const sealedPerUser = {};
6049
+ const encoder = new TextEncoder();
6050
+ for (const [userId, passphrase] of Object.entries(perUser)) {
6051
+ const sealed = await provider.seal(encoder.encode(passphrase));
6052
+ sealedPerUser[userId] = {
6053
+ pid: provider.id,
6054
+ sealed: bytesToBase64(sealed),
6055
+ alg: "aes-256-gcm"
6056
+ };
6057
+ }
6058
+ return {
6059
+ _noydb_bundle_body: 1,
6060
+ dump: dumpJson,
6061
+ _autoUnlock: { kind: "sealed", perUser: sealedPerUser }
6062
+ };
6063
+ }
6064
+ function parseAutoUnlockBody(bodyString) {
6065
+ let parsed;
6066
+ try {
6067
+ parsed = JSON.parse(bodyString);
6068
+ } catch (err) {
6069
+ throw new BundleIntegrityError(
6070
+ "header declared autoUnlock but body could not be parsed as JSON wrapper: " + (err instanceof Error ? err.message : String(err))
6071
+ );
6072
+ }
6073
+ if (typeof parsed !== "object" || parsed === null) {
6074
+ throw new BundleIntegrityError("autoUnlock body is not a JSON object");
6075
+ }
6076
+ const obj = parsed;
6077
+ if (obj["_noydb_bundle_body"] !== 1) {
6078
+ throw new BundleIntegrityError(
6079
+ "autoUnlock body missing `_noydb_bundle_body: 1` discriminator"
6080
+ );
6081
+ }
6082
+ if (typeof obj["dump"] !== "string") {
6083
+ throw new BundleIntegrityError("autoUnlock body must carry a string `dump` field");
6084
+ }
6085
+ const blob = obj["_autoUnlock"];
6086
+ if (typeof blob !== "object" || blob === null) {
6087
+ throw new BundleIntegrityError("autoUnlock body missing `_autoUnlock` blob");
6088
+ }
6089
+ const blobObj = blob;
6090
+ const kind = blobObj["kind"];
6091
+ if (kind !== "unsealed" && kind !== "sealed") {
6092
+ throw new BundleIntegrityError(
6093
+ `autoUnlock blob has invalid kind ${String(kind)}; expected 'unsealed' or 'sealed'`
6094
+ );
6095
+ }
6096
+ return {
6097
+ dump: obj["dump"],
6098
+ blob
6099
+ };
6100
+ }
6101
+ async function resolveAutoUnlock(blob, opts) {
6102
+ if (blob.kind === "unsealed") {
6103
+ return { kind: "unsealed", perUser: { ...blob.perUser } };
6104
+ }
6105
+ if (opts.sealingProviders === void 0 || opts.sealingProviders.length === 0) {
6106
+ const passthrough = {};
6107
+ for (const [userId, entry] of Object.entries(blob.perUser)) {
6108
+ passthrough[userId] = entry.sealed;
6109
+ }
6110
+ return { kind: "sealed", perUser: passthrough };
6111
+ }
6112
+ const providersByPid = /* @__PURE__ */ new Map();
6113
+ for (const p of opts.sealingProviders) providersByPid.set(p.id, p);
6114
+ const decoder = new TextDecoder();
6115
+ const unsealedMap = {};
6116
+ for (const [userId, entry] of Object.entries(blob.perUser)) {
6117
+ const provider = providersByPid.get(entry.pid);
6118
+ if (provider === void 0) {
6119
+ if (opts.attemptUnsealAcrossProviders === true) {
6120
+ let opened = null;
6121
+ for (const candidate of opts.sealingProviders) {
6122
+ try {
6123
+ const plaintextBytes2 = await candidate.unseal(base64ToBytes(entry.sealed));
6124
+ opened = decoder.decode(plaintextBytes2);
6125
+ break;
6126
+ } catch {
6127
+ }
6128
+ }
6129
+ if (opened === null) {
6130
+ throw new BundleSealMismatchError(userId, entry.pid);
6131
+ }
6132
+ unsealedMap[userId] = opened;
6133
+ continue;
6134
+ }
6135
+ throw new BundleSealMismatchError(userId, entry.pid);
6136
+ }
6137
+ const plaintextBytes = await provider.unseal(base64ToBytes(entry.sealed));
6138
+ unsealedMap[userId] = decoder.decode(plaintextBytes);
6139
+ }
6140
+ return { kind: "sealed", perUser: unsealedMap };
6141
+ }
6142
+ function bytesToBase64(bytes) {
6143
+ let binary = "";
6144
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
6145
+ return btoa(binary);
6146
+ }
6147
+ function base64ToBytes(b64) {
6148
+ const binary = atob(b64);
6149
+ const out = new Uint8Array(binary.length);
6150
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
6151
+ return out;
6152
+ }
5331
6153
  var cachedBrotliSupport = null;
5332
6154
  function supportsBrotliCompression() {
5333
6155
  if (cachedBrotliSupport !== null) return cachedBrotliSupport;
@@ -5483,12 +6305,14 @@ async function writeNoydbBundle(vault, opts = {}) {
5483
6305
  "writeNoydbBundle: pass either exportPassphrase or recipients, not both"
5484
6306
  );
5485
6307
  }
6308
+ const autoUnlockMode = validateAutoUnlockOptions(opts);
5486
6309
  const handle = await vault.getBundleHandle();
5487
6310
  const dumpJson = await vault.dump();
5488
6311
  const rekeyed = await applyRecipientRewrap(vault, dumpJson, opts);
5489
6312
  const plainFiltered = await applyPlaintextFilters(vault, rekeyed, opts);
5490
6313
  const filtered = applySliceFilters(plainFiltered, opts);
5491
- const dumpBytes = new TextEncoder().encode(filtered);
6314
+ const bodyJsonStr = autoUnlockMode === null ? filtered : JSON.stringify(await buildAutoUnlockWrapper(filtered, opts));
6315
+ const dumpBytes = new TextEncoder().encode(bodyJsonStr);
5492
6316
  const { format, streamFormat } = selectCompression(opts.compression);
5493
6317
  const body = streamFormat === null ? dumpBytes : await pumpThroughStream(dumpBytes, new CompressionStream(streamFormat));
5494
6318
  const bodySha256 = await sha256Hex2(body);
@@ -5498,7 +6322,8 @@ async function writeNoydbBundle(vault, opts = {}) {
5498
6322
  handle,
5499
6323
  bodyBytes: body.length,
5500
6324
  bodySha256,
5501
- ...publicEnvelope !== void 0 ? { publicEnvelope } : {}
6325
+ ...publicEnvelope !== void 0 ? { publicEnvelope } : {},
6326
+ ...autoUnlockMode !== null ? { autoUnlock: autoUnlockMode } : {}
5502
6327
  };
5503
6328
  const headerBytes = encodeBundleHeader(header);
5504
6329
  const prefix = new Uint8Array(NOYDB_BUNDLE_PREFIX_BYTES);
@@ -5551,7 +6376,7 @@ function readNoydbBundlePublicEnvelope(bytes, opts = {}) {
5551
6376
  ...env.description !== void 0 ? { description: pickLocale(env.description, opts.locale, env.defaultLocale) } : {}
5552
6377
  };
5553
6378
  }
5554
- async function readNoydbBundle(bytes) {
6379
+ async function readNoydbBundle(bytes, opts = {}) {
5555
6380
  const { header, bodyOffset, algo } = parsePrefixAndHeader(bytes);
5556
6381
  const body = bytes.slice(bodyOffset);
5557
6382
  if (body.length !== header.bodyBytes) {
@@ -5578,8 +6403,13 @@ async function readNoydbBundle(bytes) {
5578
6403
  );
5579
6404
  }
5580
6405
  }
5581
- const dumpJson = new TextDecoder("utf-8", { fatal: true }).decode(dumpBytes);
5582
- return { header, dumpJson };
6406
+ const bodyString = new TextDecoder("utf-8", { fatal: true }).decode(dumpBytes);
6407
+ if (header.autoUnlock === void 0) {
6408
+ return { header, dumpJson: bodyString };
6409
+ }
6410
+ const { dump, blob } = parseAutoUnlockBody(bodyString);
6411
+ const autoUnlock = await resolveAutoUnlock(blob, opts);
6412
+ return { header, dumpJson: dump, autoUnlock };
5583
6413
  }
5584
6414
 
5585
6415
  // src/index.ts
@@ -5624,17 +6454,123 @@ function formatPath(path) {
5624
6454
  ).join(".");
5625
6455
  }
5626
6456
 
6457
+ // src/persisted-schemas/canonicalize.ts
6458
+ function canonicalize(value) {
6459
+ if (value === null || typeof value !== "object") {
6460
+ return JSON.stringify(value);
6461
+ }
6462
+ if (Array.isArray(value)) {
6463
+ return "[" + value.map(canonicalize).join(",") + "]";
6464
+ }
6465
+ const obj = value;
6466
+ const keys = Object.keys(obj).sort();
6467
+ const parts = keys.map((k) => JSON.stringify(k) + ":" + canonicalize(obj[k]));
6468
+ return "{" + parts.join(",") + "}";
6469
+ }
6470
+
6471
+ // src/persisted-schemas/derive.ts
6472
+ init_crypto();
6473
+ function isZodSchema(value) {
6474
+ if (value === null || typeof value !== "object") return false;
6475
+ const def = value._def;
6476
+ if (!def || typeof def !== "object") return false;
6477
+ return typeof def.typeName === "string" && def.typeName.startsWith("Zod");
6478
+ }
6479
+ function detectKind(validator) {
6480
+ if (isZodSchema(validator)) return "Zod";
6481
+ return "Unknown";
6482
+ }
6483
+ async function loadZodConverter() {
6484
+ try {
6485
+ const mod = await import("zod-to-json-schema");
6486
+ if (!mod.zodToJsonSchema) {
6487
+ throw new Error("zod-to-json-schema export missing");
6488
+ }
6489
+ return mod.zodToJsonSchema;
6490
+ } catch (err) {
6491
+ throw new Error(
6492
+ `persistJsonSchema requires the optional peer-dep \`zod-to-json-schema\`. Install it: \`pnpm add zod-to-json-schema\` (or npm/yarn equivalent). Original error: ${err instanceof Error ? err.message : String(err)}`
6493
+ );
6494
+ }
6495
+ }
6496
+ async function derivePersistedSchema(validator) {
6497
+ const kind = detectKind(validator);
6498
+ const derivedAt = (/* @__PURE__ */ new Date()).toISOString();
6499
+ if (kind === "Zod") {
6500
+ const convert = await loadZodConverter();
6501
+ const jsonSchema = convert(validator);
6502
+ const canonical = canonicalize(jsonSchema);
6503
+ const hash = await sha256Hex(new TextEncoder().encode(canonical));
6504
+ return { _noydb_schema: 1, kind, jsonSchema, hash, derivedAt };
6505
+ }
6506
+ return {
6507
+ _noydb_schema: 1,
6508
+ kind,
6509
+ jsonSchema: null,
6510
+ hash: null,
6511
+ reason: `derivation not yet supported for kind=${kind} (v0 supports Zod only)`,
6512
+ derivedAt
6513
+ };
6514
+ }
6515
+
6516
+ // src/persisted-schemas/storage.ts
6517
+ init_crypto();
6518
+ init_types();
6519
+ var SCHEMAS_COLLECTION = "_schemas";
6520
+ async function loadPersistedSchema(store, vault, collection, dek) {
6521
+ const envelope = await store.get(vault, SCHEMAS_COLLECTION, collection);
6522
+ if (!envelope) return void 0;
6523
+ try {
6524
+ const plaintext = await decrypt(envelope._iv, envelope._data, dek);
6525
+ const parsed = JSON.parse(plaintext);
6526
+ if (parsed._noydb_schema !== 1) return void 0;
6527
+ return parsed;
6528
+ } catch {
6529
+ return void 0;
6530
+ }
6531
+ }
6532
+ async function savePersistedSchema(store, vault, collection, dek, payload) {
6533
+ const json = JSON.stringify(payload);
6534
+ const { iv, data } = await encrypt(json, dek);
6535
+ const prior = await store.get(vault, SCHEMAS_COLLECTION, collection);
6536
+ const env = {
6537
+ _noydb: NOYDB_FORMAT_VERSION,
6538
+ _v: (prior?._v ?? 0) + 1,
6539
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
6540
+ _iv: iv,
6541
+ _data: data
6542
+ };
6543
+ await store.put(vault, SCHEMAS_COLLECTION, collection, env);
6544
+ }
6545
+
6546
+ // src/persisted-schemas/register.ts
6547
+ async function persistSchemaIfNeeded(opts) {
6548
+ const fresh = await derivePersistedSchema(opts.validator);
6549
+ const stored = await loadPersistedSchema(opts.store, opts.vault, opts.collectionName, opts.dek);
6550
+ if (stored && isEquivalent(stored, fresh)) {
6551
+ return { written: false, skipped: true, envelope: stored };
6552
+ }
6553
+ await savePersistedSchema(opts.store, opts.vault, opts.collectionName, opts.dek, fresh);
6554
+ return { written: true, skipped: false, envelope: fresh };
6555
+ }
6556
+ function isEquivalent(a, b) {
6557
+ if (a.kind !== b.kind) return false;
6558
+ if (a.hash && b.hash) return a.hash === b.hash;
6559
+ if (a.hash === null && b.hash === null) return true;
6560
+ return false;
6561
+ }
6562
+
5627
6563
  // src/history/history.ts
5628
6564
  var HISTORY_COLLECTION = "_history";
5629
- function matchesPrefix(id, collection, recordId) {
5630
- if (recordId) {
5631
- return id.startsWith(`${collection}:${recordId}:`);
6565
+ function matchesPrefix(id, collection, recordId2) {
6566
+ if (recordId2) {
6567
+ return id.startsWith(`${collection}:${recordId2}:`);
5632
6568
  }
5633
6569
  return id.startsWith(`${collection}:`);
5634
6570
  }
5635
- async function getHistory(adapter, vault, collection, recordId, options) {
6571
+ async function getHistory(adapter, vault, collection, recordId2, options) {
5636
6572
  const allIds = await adapter.list(vault, HISTORY_COLLECTION);
5637
- const matchingIds = allIds.filter((id) => matchesPrefix(id, collection, recordId)).sort().reverse();
6573
+ const matchingIds = allIds.filter((id) => matchesPrefix(id, collection, recordId2)).sort().reverse();
5638
6574
  const entries = [];
5639
6575
  for (const id of matchingIds) {
5640
6576
  const envelope = await adapter.get(vault, HISTORY_COLLECTION, id);
@@ -6967,6 +7903,17 @@ var RecoveryNotEnrolledError = class extends NoydbError {
6967
7903
  this.name = "RecoveryNotEnrolledError";
6968
7904
  }
6969
7905
  };
7906
+ var ManagedRecoveryNotEnrolledError = class extends NoydbError {
7907
+ vault;
7908
+ constructor(vault) {
7909
+ super(
7910
+ "MANAGED_RECOVERY_NOT_ENROLLED",
7911
+ `Managed-mode vault "${vault}" requires at least one strong recovery profile (Shamir today; multi-channel / admin-mediated when they ship). Paper alone is NOT strong under managed mode \u2014 losing the paper sheet would mean losing every record permanently. Bootstrap with \`db.openVaultAndEnrollRecovery("${vault}", { recovery: [{ profile: "shamir", k: 2, n: 3 }] })\`, or call \`db.enrollRecovery(vault, { profile: "shamir", k, n })\` separately, then re-attempt \`openVault\`.`
7912
+ );
7913
+ this.name = "ManagedRecoveryNotEnrolledError";
7914
+ this.vault = vault;
7915
+ }
7916
+ };
6970
7917
  var RecoveryProfileNotImplementedError = class extends NoydbError {
6971
7918
  profile;
6972
7919
  tracking;
@@ -6996,7 +7943,7 @@ async function mintWrappedDeksBlob(deks, credential) {
6996
7943
  const exported = {};
6997
7944
  for (const [coll, dek] of deks) {
6998
7945
  const raw = await subtle2.exportKey("raw", dek);
6999
- exported[coll] = bytesToBase64(new Uint8Array(raw));
7946
+ exported[coll] = bytesToBase642(new Uint8Array(raw));
7000
7947
  }
7001
7948
  const plaintext = new TextEncoder().encode(JSON.stringify({ deks: exported }));
7002
7949
  const ciphertext = await subtle2.encrypt(
@@ -7005,22 +7952,22 @@ async function mintWrappedDeksBlob(deks, credential) {
7005
7952
  plaintext
7006
7953
  );
7007
7954
  return {
7008
- salt: bytesToBase64(salt),
7009
- iv: bytesToBase64(iv),
7010
- wrappedDeks: bytesToBase64(new Uint8Array(ciphertext))
7955
+ salt: bytesToBase642(salt),
7956
+ iv: bytesToBase642(iv),
7957
+ wrappedDeks: bytesToBase642(new Uint8Array(ciphertext))
7011
7958
  };
7012
7959
  }
7013
7960
  async function unwrapDeksFromBlob(blob, credential) {
7014
- const wrappingKey = await deriveWrappingKey(credential, base64ToBytes(blob.salt));
7961
+ const wrappingKey = await deriveWrappingKey(credential, base64ToBytes2(blob.salt));
7015
7962
  const plaintext = await subtle2.decrypt(
7016
- { name: "AES-GCM", iv: base64ToBytes(blob.iv) },
7963
+ { name: "AES-GCM", iv: base64ToBytes2(blob.iv) },
7017
7964
  wrappingKey,
7018
- base64ToBytes(blob.wrappedDeks)
7965
+ base64ToBytes2(blob.wrappedDeks)
7019
7966
  );
7020
7967
  const parsed = JSON.parse(new TextDecoder().decode(plaintext));
7021
7968
  const deks = /* @__PURE__ */ new Map();
7022
7969
  for (const [coll, b64] of Object.entries(parsed.deks)) {
7023
- const raw = base64ToBytes(b64);
7970
+ const raw = base64ToBytes2(b64);
7024
7971
  const key = await subtle2.importKey(
7025
7972
  "raw",
7026
7973
  raw,
@@ -7053,12 +8000,12 @@ async function deriveWrappingKey(credential, salt) {
7053
8000
  ["encrypt", "decrypt"]
7054
8001
  );
7055
8002
  }
7056
- function bytesToBase64(b) {
8003
+ function bytesToBase642(b) {
7057
8004
  let s = "";
7058
8005
  for (const x of b) s += String.fromCharCode(x);
7059
8006
  return btoa(s);
7060
8007
  }
7061
- function base64ToBytes(b64) {
8008
+ function base64ToBytes2(b64) {
7062
8009
  const s = atob(b64);
7063
8010
  const out = new Uint8Array(s.length);
7064
8011
  for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
@@ -7066,6 +8013,8 @@ function base64ToBytes(b64) {
7066
8013
  }
7067
8014
 
7068
8015
  // src/team/recovery.ts
8016
+ var import_on_shamir = require("@noy-db/on-shamir");
8017
+ var import_on_shamir2 = require("@noy-db/on-shamir");
7069
8018
  var PAPER_DOC_ID = "recovery-paper";
7070
8019
  async function loadPaperRecoveryEntries(store, vault) {
7071
8020
  const env = await store.get(vault, "_meta", PAPER_DOC_ID);
@@ -7100,7 +8049,81 @@ async function burnPaperRecoveryEntry(store, vault, codeId) {
7100
8049
  }
7101
8050
  async function hasRecoveryEnrolled(store, vault) {
7102
8051
  const paper = await loadPaperRecoveryEntries(store, vault);
7103
- return paper.length > 0;
8052
+ if (paper.length > 0) return true;
8053
+ const shamir = await loadShamirRecoveryEntries(store, vault);
8054
+ return shamir.length > 0;
8055
+ }
8056
+ async function hasStrongRecoveryEnrolled(store, vault) {
8057
+ const shamir = await loadShamirRecoveryEntries(store, vault);
8058
+ return shamir.length > 0;
8059
+ }
8060
+ var SHAMIR_DOC_ID = "recovery-shamir";
8061
+ async function loadShamirRecoveryEntries(store, vault) {
8062
+ const env = await store.get(vault, "_meta", SHAMIR_DOC_ID);
8063
+ if (!env) return [];
8064
+ try {
8065
+ const doc = JSON.parse(env._data);
8066
+ if (doc.profile !== "shamir" || !Array.isArray(doc.entries)) return [];
8067
+ return doc.entries;
8068
+ } catch {
8069
+ return [];
8070
+ }
8071
+ }
8072
+ async function saveShamirRecoveryEntries(store, vault, entries) {
8073
+ const doc = {
8074
+ _noydb_recovery: 1,
8075
+ profile: "shamir",
8076
+ entries
8077
+ };
8078
+ const envelope = {
8079
+ _noydb: NOYDB_FORMAT_VERSION,
8080
+ _v: 1,
8081
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
8082
+ _iv: "",
8083
+ _data: JSON.stringify(doc)
8084
+ };
8085
+ await store.put(vault, "_meta", SHAMIR_DOC_ID, envelope);
8086
+ }
8087
+ async function mintShamirRecoveryEntry(deks, entryId, k, n, label) {
8088
+ const recoverySecret = crypto.getRandomValues(new Uint8Array(32));
8089
+ try {
8090
+ const credential = bytesToBase643(recoverySecret);
8091
+ const blob = await mintWrappedDeksBlob(deks, credential);
8092
+ const shares = (0, import_on_shamir.splitSecret)(recoverySecret, k, n);
8093
+ const shareStrings = shares.map(import_on_shamir.encodeShareBase32);
8094
+ const xCoords = shares.map((s) => s.x);
8095
+ const entry = {
8096
+ ...blob,
8097
+ entryId,
8098
+ k,
8099
+ n,
8100
+ xCoords,
8101
+ enrolledAt: (/* @__PURE__ */ new Date()).toISOString(),
8102
+ ...label !== void 0 && { label }
8103
+ };
8104
+ return { entry, shareStrings };
8105
+ } finally {
8106
+ recoverySecret.fill(0);
8107
+ }
8108
+ }
8109
+ async function unwrapDeksFromShamirEntry(entry, shares) {
8110
+ if (shares.length < entry.k) {
8111
+ throw new Error(
8112
+ `Insufficient shares: this Shamir entry needs ${entry.k} of ${entry.n}, but ${shares.length} were provided.`
8113
+ );
8114
+ }
8115
+ const secret = (0, import_on_shamir.combineSecret)(shares);
8116
+ try {
8117
+ const credential = bytesToBase643(secret);
8118
+ return await unwrapDeksFromBlob(entry, credential);
8119
+ } finally {
8120
+ secret.fill(0);
8121
+ }
8122
+ }
8123
+ function bytesToBase643(b) {
8124
+ let s = "";
8125
+ for (const x of b) s += String.fromCharCode(x);
8126
+ return btoa(s);
7104
8127
  }
7105
8128
  async function mintPaperRecoveryEntry(deks, code, codeId) {
7106
8129
  const blob = await mintWrappedDeksBlob(deks, code);
@@ -7115,6 +8138,7 @@ async function unwrapDeksFromPaperEntry(entry, code) {
7115
8138
  }
7116
8139
 
7117
8140
  // src/team/rotate-recover.ts
8141
+ var import_on_shamir3 = require("@noy-db/on-shamir");
7118
8142
  init_errors();
7119
8143
  async function rotatePassphrase(store, vault, userId, input) {
7120
8144
  if (!input.allowWeakPassphrase) {
@@ -7213,13 +8237,16 @@ async function recoverPassphrase(store, vault, userId, input) {
7213
8237
  assertStrongPassphrase(input.newPassphrase, input.passphrasePolicy);
7214
8238
  }
7215
8239
  const profile = input.recoveryProof.profile;
7216
- if (profile !== "paper") {
7217
- throw new RecoveryProfileNotImplementedError(
7218
- profile,
7219
- "https://github.com/vLannaAi/noy-db/issues/10"
7220
- );
8240
+ if (profile === "paper") {
8241
+ return recoverViaPaperCode(store, vault, userId, input);
7221
8242
  }
7222
- return recoverViaPaperCode(store, vault, userId, input);
8243
+ if (profile === "shamir") {
8244
+ return recoverViaShamir(store, vault, userId, input);
8245
+ }
8246
+ throw new RecoveryProfileNotImplementedError(
8247
+ profile,
8248
+ "https://github.com/vLannaAi/noy-db/issues/196"
8249
+ );
7223
8250
  }
7224
8251
  async function recoverViaPaperCode(store, vault, userId, input) {
7225
8252
  if (input.recoveryProof.profile !== "paper") throw new Error("unreachable");
@@ -7285,6 +8312,99 @@ async function recoverViaPaperCode(store, vault, userId, input) {
7285
8312
  function normalizePaperCode(input) {
7286
8313
  return input.toUpperCase().replace(/[\s\-_]/g, "");
7287
8314
  }
8315
+ async function recoverViaShamir(store, vault, userId, input) {
8316
+ if (input.recoveryProof.profile !== "shamir") throw new Error("unreachable");
8317
+ const { entryId: requestedEntryId, shares: shareStrings } = input.recoveryProof.payload;
8318
+ if (shareStrings.length === 0) {
8319
+ throw new ValidationError(
8320
+ "Shamir recovery requires at least one share; received an empty array."
8321
+ );
8322
+ }
8323
+ const env = await store.get(vault, "_keyring", userId);
8324
+ if (!env) {
8325
+ throw new NoAccessError(`No keyring found for user "${userId}" in vault "${vault}".`);
8326
+ }
8327
+ const file = JSON.parse(env._data);
8328
+ let decodedShares;
8329
+ try {
8330
+ decodedShares = shareStrings.map((s) => (0, import_on_shamir3.decodeShareBase32)(s));
8331
+ } catch (err) {
8332
+ throw new ValidationError(
8333
+ "One or more Shamir shares could not be decoded. They may be malformed, truncated, or from an incompatible version. Original error: " + (err instanceof Error ? err.message : String(err))
8334
+ );
8335
+ }
8336
+ const allEntries = await loadShamirRecoveryEntries(store, vault);
8337
+ if (allEntries.length === 0) {
8338
+ throw new NoAccessError(
8339
+ `No Shamir-recovery entries enrolled for vault "${vault}". Enroll via \`db.enrollRecovery({ profile: "shamir", k, n })\` before relying on recovery.`
8340
+ );
8341
+ }
8342
+ let candidates;
8343
+ if (requestedEntryId !== void 0) {
8344
+ candidates = allEntries.filter((e) => e.entryId === requestedEntryId);
8345
+ if (candidates.length === 0) {
8346
+ throw new NoAccessError(
8347
+ `No Shamir-recovery entry with entryId="${requestedEntryId}" found in vault "${vault}". Available entries: ` + allEntries.map((e) => `"${e.entryId}"`).join(", ")
8348
+ );
8349
+ }
8350
+ } else {
8351
+ candidates = allEntries;
8352
+ }
8353
+ let recoveredDeks;
8354
+ for (const entry of candidates) {
8355
+ const xCoordSet = new Set(entry.xCoords);
8356
+ const matchingShares = decodedShares.filter((s) => xCoordSet.has(s.x));
8357
+ if (matchingShares.length < entry.k) {
8358
+ continue;
8359
+ }
8360
+ try {
8361
+ const deks = await unwrapDeksFromShamirEntry(entry, matchingShares);
8362
+ recoveredDeks = deks;
8363
+ break;
8364
+ } catch {
8365
+ }
8366
+ }
8367
+ if (!recoveredDeks) {
8368
+ const minK = Math.min(...candidates.map((e) => e.k));
8369
+ if (decodedShares.length < minK) {
8370
+ throw new InvalidKeyError(
8371
+ `Insufficient Shamir shares to combine: the smallest enrolled threshold is ${minK}, but only ${decodedShares.length} share${decodedShares.length === 1 ? " was" : "s were"} provided.`
8372
+ );
8373
+ }
8374
+ throw new InvalidKeyError(
8375
+ "Shamir shares do not match any enrolled entry. Possible causes: shares were tampered with, came from a different enrollment, or the entry was rotated after these shares were distributed."
8376
+ );
8377
+ }
8378
+ const newSalt = generateSalt();
8379
+ const newKek = await deriveKey(input.newPassphrase, newSalt);
8380
+ const wrappedDeks = {};
8381
+ for (const [coll, dek] of recoveredDeks) {
8382
+ wrappedDeks[coll] = await wrapKey(dek, newKek);
8383
+ }
8384
+ const canary = await mintKeyringCanary(newKek);
8385
+ const next = {
8386
+ ...file,
8387
+ _noydb_keyring: NOYDB_KEYRING_VERSION,
8388
+ deks: wrappedDeks,
8389
+ salt: bufferToBase64(newSalt),
8390
+ authenticators: [],
8391
+ // tier-2 slots wrap old KEK, drop them on recovery
8392
+ canary
8393
+ };
8394
+ await writeKeyringFile2(store, vault, userId, next);
8395
+ return {
8396
+ userId: file.user_id,
8397
+ displayName: file.display_name,
8398
+ role: file.role,
8399
+ permissions: file.permissions,
8400
+ deks: recoveredDeks,
8401
+ kek: newKek,
8402
+ salt: newSalt,
8403
+ authenticators: [],
8404
+ ...file.export_capability !== void 0 && { exportCapability: file.export_capability },
8405
+ ...file.import_capability !== void 0 && { importCapability: file.import_capability }
8406
+ };
8407
+ }
7288
8408
  async function writeKeyringFile2(store, vault, userId, file) {
7289
8409
  const envelope = {
7290
8410
  _noydb: 1,
@@ -7722,6 +8842,134 @@ function sanitizeId(s) {
7722
8842
  return s.replace(/[^a-zA-Z0-9]/g, "_");
7723
8843
  }
7724
8844
 
8845
+ // src/team/managed-passphrase.ts
8846
+ init_types();
8847
+ var MemorySealingKeyProvider = class {
8848
+ id;
8849
+ fingerprint;
8850
+ keyBytes;
8851
+ constructor(opts) {
8852
+ this.id = opts.id;
8853
+ const encoded = new TextEncoder().encode(opts.id);
8854
+ let h = 0;
8855
+ for (let i = 0; i < encoded.length; i++) {
8856
+ h = h * 31 + encoded[i] >>> 0;
8857
+ }
8858
+ this.fingerprint = new Uint8Array([
8859
+ h >>> 24 & 255,
8860
+ h >>> 16 & 255,
8861
+ h >>> 8 & 255,
8862
+ h & 255
8863
+ ]);
8864
+ this.keyBytes = new Uint8Array(16);
8865
+ for (let i = 0; i < 16; i++) {
8866
+ this.keyBytes[i] = this.fingerprint[i % 4] ^ i * 17;
8867
+ }
8868
+ }
8869
+ async seal(passphrase) {
8870
+ const out = new Uint8Array(4 + passphrase.length);
8871
+ out.set(this.fingerprint, 0);
8872
+ for (let i = 0; i < passphrase.length; i++) {
8873
+ out[4 + i] = passphrase[i] ^ this.keyBytes[i % 16];
8874
+ }
8875
+ return out;
8876
+ }
8877
+ async unseal(sealed) {
8878
+ if (sealed.length < 4) {
8879
+ throw new Error("MemorySealingKeyProvider: sealed input too short");
8880
+ }
8881
+ for (let i = 0; i < 4; i++) {
8882
+ if (sealed[i] !== this.fingerprint[i]) {
8883
+ throw new Error(
8884
+ `MemorySealingKeyProvider("${this.id}"): provider-id mismatch on unseal (sealed bytes were produced by a different provider)`
8885
+ );
8886
+ }
8887
+ }
8888
+ const body = sealed.subarray(4);
8889
+ const out = new Uint8Array(body.length);
8890
+ for (let i = 0; i < body.length; i++) {
8891
+ out[i] = body[i] ^ this.keyBytes[i % 16];
8892
+ }
8893
+ return out;
8894
+ }
8895
+ };
8896
+ var SEALED_PASSPHRASE_RECORD_ID = "sealed-passphrase";
8897
+ function bytesToBase644(bytes) {
8898
+ let binary = "";
8899
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
8900
+ return btoa(binary);
8901
+ }
8902
+ function base64ToBytes3(b64) {
8903
+ const binary = atob(b64);
8904
+ const out = new Uint8Array(binary.length);
8905
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
8906
+ return out;
8907
+ }
8908
+ function parseSealedEnvelope(raw) {
8909
+ if (typeof raw !== "object" || raw === null) return void 0;
8910
+ const r = raw;
8911
+ if (r._noydb_sealed !== 1) return void 0;
8912
+ if (r.v === 1 && typeof r.pid === "string" && typeof r.payload === "string") {
8913
+ return {
8914
+ _noydb_sealed: 1,
8915
+ providerId: r.pid,
8916
+ sealed: base64ToBytes3(r.payload)
8917
+ };
8918
+ }
8919
+ if (typeof r.providerId === "string" && typeof r.sealed === "string") {
8920
+ return {
8921
+ _noydb_sealed: 1,
8922
+ providerId: r.providerId,
8923
+ sealed: base64ToBytes3(r.sealed)
8924
+ };
8925
+ }
8926
+ return void 0;
8927
+ }
8928
+ async function saveSealedPassphrase(store, vault, payload) {
8929
+ const persisted = {
8930
+ v: 1,
8931
+ _noydb_sealed: 1,
8932
+ pid: payload.providerId,
8933
+ payload: bytesToBase644(payload.sealed)
8934
+ };
8935
+ const prior = await store.get(vault, "_meta", SEALED_PASSPHRASE_RECORD_ID);
8936
+ const env = {
8937
+ _noydb: NOYDB_FORMAT_VERSION,
8938
+ _v: (prior?._v ?? 0) + 1,
8939
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
8940
+ // AES-GCM bypassed — the sealing layer is the security boundary.
8941
+ _iv: "",
8942
+ _data: JSON.stringify(persisted)
8943
+ };
8944
+ await store.put(vault, "_meta", SEALED_PASSPHRASE_RECORD_ID, env);
8945
+ }
8946
+ async function loadSealedPassphrase(store, vault) {
8947
+ const envelope = await store.get(vault, "_meta", SEALED_PASSPHRASE_RECORD_ID);
8948
+ if (!envelope) return void 0;
8949
+ try {
8950
+ return parseSealedEnvelope(JSON.parse(envelope._data));
8951
+ } catch {
8952
+ return void 0;
8953
+ }
8954
+ }
8955
+ async function resolveManagedSecret(store, vault, provider) {
8956
+ const existing = await loadSealedPassphrase(store, vault);
8957
+ if (existing) {
8958
+ if (existing.providerId !== provider.id) {
8959
+ throw new Error(
8960
+ `Managed-mode vault "${vault}" was sealed under provider id "${existing.providerId}" but the current SealingKeyProvider is "${provider.id}". Pass the same provider that originally enrolled the vault, or treat this as a fresh enrollment and clear \`_meta/sealed-passphrase\` first.`
8961
+ );
8962
+ }
8963
+ const plaintext = await provider.unseal(existing.sealed);
8964
+ return bytesToBase644(plaintext);
8965
+ }
8966
+ const random = new Uint8Array(32);
8967
+ globalThis.crypto.getRandomValues(random);
8968
+ const sealed = await provider.seal(random);
8969
+ await saveSealedPassphrase(store, vault, { providerId: provider.id, sealed });
8970
+ return bytesToBase644(random);
8971
+ }
8972
+
7725
8973
  // src/team/peer-recover.ts
7726
8974
  init_types();
7727
8975
  init_crypto();
@@ -7856,116 +9104,40 @@ function notEnabled2(op) {
7856
9104
  var NO_HISTORY = {
7857
9105
  async saveHistory() {
7858
9106
  },
7859
- async getHistoryEntries() {
7860
- throw notEnabled2("collection.history()");
7861
- },
7862
- async getVersionEnvelope() {
7863
- throw notEnabled2("collection.getVersion()");
7864
- },
7865
- async pruneHistory() {
7866
- return 0;
7867
- },
7868
- async clearHistory() {
7869
- return 0;
7870
- },
7871
- async envelopePayloadHash() {
7872
- return "";
7873
- },
7874
- computePatch() {
7875
- return [];
7876
- },
7877
- diff() {
7878
- throw notEnabled2("collection.diff()");
7879
- },
7880
- buildLedger() {
7881
- return null;
7882
- },
7883
- buildVaultInstant() {
7884
- throw notEnabled2("vault.at() / vault.timeMachine()");
7885
- }
7886
- };
7887
-
7888
- // src/query/predicate.ts
7889
- function readPath(record, path) {
7890
- if (record === null || record === void 0) return void 0;
7891
- if (!path.includes(".")) {
7892
- return record[path];
7893
- }
7894
- const segments = path.split(".");
7895
- let cursor = record;
7896
- for (const segment of segments) {
7897
- if (cursor === null || cursor === void 0) return void 0;
7898
- cursor = cursor[segment];
7899
- }
7900
- return cursor;
7901
- }
7902
- function evaluateFieldClause(record, clause) {
7903
- const actual = readPath(record, clause.field);
7904
- const { op, value } = clause;
7905
- switch (op) {
7906
- case "==":
7907
- return actual === value;
7908
- case "!=":
7909
- return actual !== value;
7910
- case "<":
7911
- return isComparable(actual, value) && actual < value;
7912
- case "<=":
7913
- return isComparable(actual, value) && actual <= value;
7914
- case ">":
7915
- return isComparable(actual, value) && actual > value;
7916
- case ">=":
7917
- return isComparable(actual, value) && actual >= value;
7918
- case "in":
7919
- return Array.isArray(value) && value.includes(actual);
7920
- case "contains":
7921
- if (typeof actual === "string") return typeof value === "string" && actual.includes(value);
7922
- if (Array.isArray(actual)) return actual.includes(value);
7923
- return false;
7924
- case "startsWith":
7925
- return typeof actual === "string" && typeof value === "string" && actual.startsWith(value);
7926
- case "between": {
7927
- if (!Array.isArray(value) || value.length !== 2) return false;
7928
- const [lo, hi] = value;
7929
- if (!isComparable(actual, lo) || !isComparable(actual, hi)) return false;
7930
- return actual >= lo && actual <= hi;
7931
- }
7932
- default: {
7933
- const _exhaustive = op;
7934
- void _exhaustive;
7935
- return false;
7936
- }
7937
- }
7938
- }
7939
- function isComparable(a, b) {
7940
- if (typeof a === "number" && typeof b === "number") return true;
7941
- if (typeof a === "string" && typeof b === "string") return true;
7942
- if (a instanceof Date && b instanceof Date) return true;
7943
- return false;
7944
- }
7945
- function evaluateClause(record, clause) {
7946
- switch (clause.type) {
7947
- case "field":
7948
- return evaluateFieldClause(record, clause);
7949
- case "filter":
7950
- return clause.fn(record);
7951
- case "wherePredicate":
7952
- return clause.fn(record, clause.ctx);
7953
- case "group":
7954
- if (clause.op === "and") {
7955
- for (const child of clause.clauses) {
7956
- if (!evaluateClause(record, child)) return false;
7957
- }
7958
- return true;
7959
- } else {
7960
- for (const child of clause.clauses) {
7961
- if (evaluateClause(record, child)) return true;
7962
- }
7963
- return false;
7964
- }
9107
+ async getHistoryEntries() {
9108
+ throw notEnabled2("collection.history()");
9109
+ },
9110
+ async getVersionEnvelope() {
9111
+ throw notEnabled2("collection.getVersion()");
9112
+ },
9113
+ async pruneHistory() {
9114
+ return 0;
9115
+ },
9116
+ async clearHistory() {
9117
+ return 0;
9118
+ },
9119
+ async envelopePayloadHash() {
9120
+ return "";
9121
+ },
9122
+ computePatch() {
9123
+ return [];
9124
+ },
9125
+ diff() {
9126
+ throw notEnabled2("collection.diff()");
9127
+ },
9128
+ buildLedger() {
9129
+ return null;
9130
+ },
9131
+ buildVaultInstant() {
9132
+ throw notEnabled2("vault.at() / vault.timeMachine()");
7965
9133
  }
7966
- }
9134
+ };
9135
+
9136
+ // src/query/builder.ts
9137
+ init_predicate();
7967
9138
 
7968
9139
  // src/query/join.ts
9140
+ init_predicate();
7969
9141
  init_errors();
7970
9142
  var DEFAULT_JOIN_MAX_ROWS = 5e4;
7971
9143
  var JOIN_WARN_FRACTION = 0.8;
@@ -8203,6 +9375,9 @@ var NO_AGGREGATE = {
8203
9375
  groupBy() {
8204
9376
  throw NOT_ENABLED2;
8205
9377
  },
9378
+ groupByN() {
9379
+ throw NOT_ENABLED2;
9380
+ },
8206
9381
  scanAggregate() {
8207
9382
  throw NOT_ENABLED2;
8208
9383
  }
@@ -8586,53 +9761,10 @@ var Query = class _Query {
8586
9761
  }
8587
9762
  return this.aggregateStrategy.aggregate(executeRecords, spec, upstreams);
8588
9763
  }
8589
- /**
8590
- * Partition matching records into buckets keyed by a field, then
8591
- * terminate with `.aggregate(spec)` to compute per-bucket
8592
- * reducers..
8593
- *
8594
- * ```ts
8595
- * const byClient = invoices.query()
8596
- * .where('status', '==', 'open')
8597
- * .groupBy('clientId')
8598
- * .aggregate({ total: sum('amount'), n: count() })
8599
- * .run()
8600
- * // → [ { clientId: 'c1', total: 5250, n: 3 }, … ]
8601
- * ```
8602
- *
8603
- * Result rows carry the group key value under the grouping field
8604
- * name plus every reducer output from the spec. Buckets are
8605
- * emitted in first-seen order — consumers who want a specific
8606
- * ordering should `.sort()` downstream.
8607
- *
8608
- * **Cardinality caps:** a one-shot warning fires at 10_000
8609
- * distinct groups; `GroupCardinalityError` throws at 100_000.
8610
- * Grouping on a high-uniqueness field like `id` or `createdAt` is
8611
- * almost always a query mistake — the error message names the
8612
- * field and observed cardinality and suggests narrowing with
8613
- * `.where()` first.
8614
- *
8615
- * **Null / undefined keys:** records with a missing or explicitly
8616
- * `null` group field get their own buckets. `Map`-based
8617
- * partitioning distinguishes `undefined` from `null`, so the two
8618
- * cases do NOT merge. Consumers who want them merged should
8619
- * coalesce upstream with `.filter()`.
8620
- *
8621
- * **Joins are not applied** — same rationale as `.count()` and
8622
- * `.aggregate()`. Joined fields in are projection-only, so
8623
- * running a join inside a grouping pipeline would be wasteful and
8624
- * could trigger `DanglingReferenceError` in strict mode for a
8625
- * call whose intent is purely to bucket-and-reduce. Grouping by
8626
- * a joined field is explicitly out of scope for — file an
8627
- * issue if a real consumer needs it.
8628
- *
8629
- * **Filter clauses (`.filter(fn)`):** grouped queries still
8630
- * support filter clauses in the underlying plan — they run in
8631
- * the same candidate/filter pipeline that `.aggregate()` uses.
8632
- * The performance caveat is the same: filter clauses cost O(N)
8633
- * per record and can't be index-accelerated.
8634
- */
8635
- groupBy(field) {
9764
+ groupBy(...fields) {
9765
+ if (fields.length === 0) {
9766
+ throw new Error(".groupBy() requires at least one field");
9767
+ }
8636
9768
  const source = this.source;
8637
9769
  const clauses = this.plan.clauses;
8638
9770
  const executeRecords = () => {
@@ -8644,36 +9776,21 @@ var Query = class _Query {
8644
9776
  const subscribe = source.subscribe.bind(source);
8645
9777
  upstreams.push({ subscribe: (cb) => subscribe(cb) });
8646
9778
  }
8647
- const joinCtx = this.joinContext;
8648
- const dictLabelResolver = joinCtx?.resolveDictSource ? (() => {
8649
- const dictSource = joinCtx.resolveDictSource(field);
8650
- if (!dictSource) return void 0;
8651
- const snapshot = dictSource.snapshot();
8652
- const dictMap = /* @__PURE__ */ new Map();
8653
- for (const entry of snapshot) {
8654
- const k = entry["key"];
8655
- const labels = entry["labels"];
8656
- if (typeof k === "string" && labels && typeof labels === "object") {
8657
- dictMap.set(k, labels);
8658
- }
8659
- }
8660
- return async (key, locale, fallback) => {
8661
- const labels = dictMap.get(key);
8662
- if (!labels) return void 0;
8663
- if (labels[locale] !== void 0) return labels[locale];
8664
- const chain = Array.isArray(fallback) ? fallback : fallback ? [fallback] : [];
8665
- for (const fb of chain) {
8666
- if (fb === "any") {
8667
- const any = Object.values(labels)[0];
8668
- if (any !== void 0) return any;
8669
- } else if (labels[fb] !== void 0) {
8670
- return labels[fb];
8671
- }
8672
- }
8673
- return void 0;
8674
- };
8675
- })() : void 0;
8676
- return this.aggregateStrategy.groupBy(executeRecords, field, upstreams, dictLabelResolver);
9779
+ if (fields.length === 1) {
9780
+ const field = fields[0];
9781
+ const dictLabelResolver = buildDictLabelResolver(this.joinContext, field);
9782
+ return this.aggregateStrategy.groupBy(
9783
+ executeRecords,
9784
+ field,
9785
+ upstreams,
9786
+ dictLabelResolver
9787
+ );
9788
+ }
9789
+ return this.aggregateStrategy.groupByN(
9790
+ executeRecords,
9791
+ fields,
9792
+ upstreams
9793
+ );
8677
9794
  }
8678
9795
  /**
8679
9796
  * Re-run the query whenever the source notifies of changes.
@@ -8936,8 +10053,41 @@ function canonicalCtxHash(ctx) {
8936
10053
  }
8937
10054
  return (h >>> 0).toString(16).padStart(8, "0");
8938
10055
  }
10056
+ function buildDictLabelResolver(joinCtx, field) {
10057
+ if (!joinCtx?.resolveDictSource) return void 0;
10058
+ const dictSource = joinCtx.resolveDictSource(field);
10059
+ if (!dictSource) return void 0;
10060
+ const snapshot = dictSource.snapshot();
10061
+ const dictMap = /* @__PURE__ */ new Map();
10062
+ for (const entry of snapshot) {
10063
+ const k = entry["key"];
10064
+ const labels = entry["labels"];
10065
+ if (typeof k === "string" && labels && typeof labels === "object") {
10066
+ dictMap.set(k, labels);
10067
+ }
10068
+ }
10069
+ return async (key, locale, fallback) => {
10070
+ const labels = dictMap.get(key);
10071
+ if (!labels) return void 0;
10072
+ if (labels[locale] !== void 0) return labels[locale];
10073
+ const chain = Array.isArray(fallback) ? fallback : fallback ? [fallback] : [];
10074
+ for (const fb of chain) {
10075
+ if (fb === "any") {
10076
+ const any = Object.values(labels)[0];
10077
+ if (any !== void 0) return any;
10078
+ } else if (labels[fb] !== void 0) {
10079
+ return labels[fb];
10080
+ }
10081
+ }
10082
+ return void 0;
10083
+ };
10084
+ }
10085
+
10086
+ // src/query/index.ts
10087
+ init_predicate();
8939
10088
 
8940
10089
  // src/indexing/eager-indexes.ts
10090
+ init_predicate();
8941
10091
  var CollectionIndexes = class {
8942
10092
  indexes = /* @__PURE__ */ new Map();
8943
10093
  /**
@@ -9062,6 +10212,7 @@ function removeFromIndex(idx, id, record) {
9062
10212
  }
9063
10213
 
9064
10214
  // src/aggregate/reducers.ts
10215
+ init_predicate();
9065
10216
  function count(opts) {
9066
10217
  const _seed = opts?.seed;
9067
10218
  void _seed;
@@ -9149,274 +10300,12 @@ function readNumber(record, field) {
9149
10300
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
9150
10301
  }
9151
10302
 
9152
- // src/aggregate/aggregation.ts
9153
- function reduceRecords(records, spec) {
9154
- const state = {};
9155
- for (const key of Object.keys(spec)) {
9156
- state[key] = spec[key].init();
9157
- }
9158
- for (const record of records) {
9159
- for (const key of Object.keys(spec)) {
9160
- state[key] = spec[key].step(state[key], record);
9161
- }
9162
- }
9163
- const result = {};
9164
- for (const key of Object.keys(spec)) {
9165
- result[key] = spec[key].finalize(state[key]);
9166
- }
9167
- return result;
9168
- }
9169
- var LiveAggregationImpl = class {
9170
- constructor(recompute, upstreams) {
9171
- this.recompute = recompute;
9172
- try {
9173
- this.value = recompute();
9174
- this.error = void 0;
9175
- } catch (err) {
9176
- this.value = void 0;
9177
- this.error = err;
9178
- }
9179
- for (const upstream of upstreams) {
9180
- const unsub = upstream.subscribe(() => this.refresh());
9181
- this.unsubscribes.push(unsub);
9182
- }
9183
- }
9184
- recompute;
9185
- value;
9186
- error;
9187
- listeners = /* @__PURE__ */ new Set();
9188
- unsubscribes = [];
9189
- stopped = false;
9190
- refresh() {
9191
- if (this.stopped) return;
9192
- try {
9193
- this.value = this.recompute();
9194
- this.error = void 0;
9195
- } catch (err) {
9196
- this.error = err;
9197
- }
9198
- for (const listener of this.listeners) {
9199
- try {
9200
- listener();
9201
- } catch (err) {
9202
- console.warn("[noy-db] LiveAggregation listener threw:", err);
9203
- }
9204
- }
9205
- }
9206
- subscribe(cb) {
9207
- if (this.stopped) {
9208
- return () => {
9209
- };
9210
- }
9211
- this.listeners.add(cb);
9212
- return () => {
9213
- this.listeners.delete(cb);
9214
- };
9215
- }
9216
- stop() {
9217
- if (this.stopped) return;
9218
- this.stopped = true;
9219
- for (const unsub of this.unsubscribes) {
9220
- try {
9221
- unsub();
9222
- } catch (err) {
9223
- console.warn("[noy-db] LiveAggregation upstream unsubscribe threw:", err);
9224
- }
9225
- }
9226
- this.unsubscribes.length = 0;
9227
- this.listeners.clear();
9228
- }
9229
- };
9230
- var Aggregation = class {
9231
- constructor(executeRecords, spec, upstreams) {
9232
- this.executeRecords = executeRecords;
9233
- this.spec = spec;
9234
- this.upstreams = upstreams;
9235
- }
9236
- executeRecords;
9237
- spec;
9238
- upstreams;
9239
- /**
9240
- * Execute the query and reduce the results synchronously.
9241
- * Returns the reduced shape matching the spec — e.g. a spec of
9242
- * `{ total: sum('amount'), n: count() }` returns
9243
- * `{ total: number, n: number }`.
9244
- */
9245
- run() {
9246
- return reduceRecords(this.executeRecords(), this.spec);
9247
- }
9248
- /**
9249
- * Build a reactive `LiveAggregation<R>` that re-runs the reduction
9250
- * whenever any upstream source notifies of a change. The initial
9251
- * value is computed eagerly in the constructor, so consumers can
9252
- * read `live.value` immediately after calling `.live()`.
9253
- *
9254
- * Always call `live.stop()` when finished — it tears down the
9255
- * upstream subscriptions. Vue's `onUnmounted` is the canonical
9256
- * place.
9257
- *
9258
- * **Implementation note:** every upstream change triggers a full
9259
- * re-reduction. Incremental maintenance (O(1) per delta for
9260
- * sum/count/avg via the reducer protocol's `remove()` method) is a
9261
- * planned follow-up optimization — the protocol already supports
9262
- * it, but the executor doesn't drive it yet. Consumers get
9263
- * correct, reactive values today; future PRs can switch to
9264
- * delta-based maintenance without changing this API.
9265
- */
9266
- live() {
9267
- const recompute = () => reduceRecords(this.executeRecords(), this.spec);
9268
- return new LiveAggregationImpl(recompute, this.upstreams);
9269
- }
9270
- };
9271
- function buildLiveAggregation(recompute, upstreams) {
9272
- return new LiveAggregationImpl(recompute, upstreams);
9273
- }
9274
-
9275
- // src/aggregate/groupby.ts
9276
- init_errors();
9277
- var GROUPBY_WARN_CARDINALITY = 1e4;
9278
- var GROUPBY_MAX_CARDINALITY = 1e5;
9279
- var warnedCardinalityFields = /* @__PURE__ */ new Set();
9280
- function warnCardinalityApproaching(field, observed) {
9281
- if (warnedCardinalityFields.has(field)) return;
9282
- warnedCardinalityFields.add(field);
9283
- console.warn(
9284
- `[noy-db] .groupBy("${field}") produced ${observed} distinct groups, ${Math.round(observed / GROUPBY_MAX_CARDINALITY * 100)}% of the ${GROUPBY_MAX_CARDINALITY}-group ceiling. Narrow the query with .where() before grouping, or switch to a lower-cardinality field.`
9285
- );
9286
- }
9287
- var GroupedQuery = class {
9288
- constructor(executeRecords, field, upstreams, dictLabelResolver) {
9289
- this.executeRecords = executeRecords;
9290
- this.field = field;
9291
- this.upstreams = upstreams;
9292
- this.dictLabelResolver = dictLabelResolver;
9293
- }
9294
- executeRecords;
9295
- field;
9296
- upstreams;
9297
- dictLabelResolver;
9298
- /**
9299
- * Build a grouped aggregation. Returns a `GroupedAggregation`
9300
- * with `.run()`, `.runAsync()`, and `.live()` terminals — same shape
9301
- * as the non-grouped `.aggregate()` wrapper, just with an array
9302
- * result (one row per bucket) instead of a single reduced object.
9303
- */
9304
- aggregate(spec) {
9305
- return new GroupedAggregation(
9306
- this.executeRecords,
9307
- this.field,
9308
- spec,
9309
- this.upstreams,
9310
- this.dictLabelResolver
9311
- );
9312
- }
9313
- };
9314
- function groupAndReduce(records, field, spec) {
9315
- const buckets = /* @__PURE__ */ new Map();
9316
- for (const record of records) {
9317
- const key = readPath(record, field);
9318
- let bucket = buckets.get(key);
9319
- if (bucket === void 0) {
9320
- if (buckets.size >= GROUPBY_MAX_CARDINALITY) {
9321
- throw new GroupCardinalityError(
9322
- field,
9323
- buckets.size + 1,
9324
- GROUPBY_MAX_CARDINALITY
9325
- );
9326
- }
9327
- bucket = [];
9328
- buckets.set(key, bucket);
9329
- }
9330
- bucket.push(record);
9331
- }
9332
- if (buckets.size >= GROUPBY_WARN_CARDINALITY) {
9333
- warnCardinalityApproaching(field, buckets.size);
9334
- }
9335
- const keys = Object.keys(spec);
9336
- const out = [];
9337
- for (const [groupKey, bucketRecords] of buckets) {
9338
- const state = {};
9339
- for (const key of keys) {
9340
- state[key] = spec[key].init();
9341
- }
9342
- for (const record of bucketRecords) {
9343
- for (const key of keys) {
9344
- state[key] = spec[key].step(state[key], record);
9345
- }
9346
- }
9347
- const row = { [field]: groupKey };
9348
- for (const key of keys) {
9349
- row[key] = spec[key].finalize(state[key]);
9350
- }
9351
- out.push(row);
9352
- }
9353
- return out;
9354
- }
9355
- var GroupedAggregation = class {
9356
- constructor(executeRecords, field, spec, upstreams, dictLabelResolver) {
9357
- this.executeRecords = executeRecords;
9358
- this.field = field;
9359
- this.spec = spec;
9360
- this.upstreams = upstreams;
9361
- this.dictLabelResolver = dictLabelResolver;
9362
- }
9363
- executeRecords;
9364
- field;
9365
- spec;
9366
- upstreams;
9367
- dictLabelResolver;
9368
- /** Execute the query, group, reduce, and return an array of rows. */
9369
- run() {
9370
- return groupAndReduce(this.executeRecords(), this.field, this.spec);
9371
- }
9372
- /**
9373
- * Execute the query, group, reduce, and resolve `<field>Label` for
9374
- * each result row when the grouping field is a `dictKey` and a
9375
- * `locale` is provided. Returns `R[]` synchronously when
9376
- * no locale is specified (identical to `.run()`).
9377
- *
9378
- * The `<field>Label` field is appended to each row. Rows whose group
9379
- * key has no dictionary entry get `<field>Label: undefined`.
9380
- */
9381
- async runAsync(opts) {
9382
- const rows = groupAndReduce(this.executeRecords(), this.field, this.spec);
9383
- if (!opts?.locale || !this.dictLabelResolver) return rows;
9384
- const resolve = this.dictLabelResolver;
9385
- const locale = opts.locale;
9386
- const fallback = opts.fallback;
9387
- const labelKey = `${this.field}Label`;
9388
- return Promise.all(
9389
- rows.map(async (row) => {
9390
- const key = row[this.field];
9391
- if (typeof key !== "string") return row;
9392
- const label = await resolve(key, locale, fallback);
9393
- return { ...row, [labelKey]: label };
9394
- })
9395
- );
9396
- }
9397
- /**
9398
- * Build a reactive `LiveAggregation<R[]>` that re-runs the full
9399
- * group-and-reduce pipeline whenever any upstream source notifies
9400
- * of a change. Same error-isolation and idempotent-stop contract
9401
- * as `Aggregation.live()` — the implementation delegates to the
9402
- * same `LiveAggregationImpl` class by threading a fresh
9403
- * recompute closure through the existing constructor.
9404
- *
9405
- * uses naive full re-run on every change. Incremental
9406
- * per-bucket maintenance (apply `step` on inserted records,
9407
- * `remove` on deleted records, route by bucket key) is a future
9408
- * optimization — the reducer protocol admits it, but wiring
9409
- * delta-aware source subscriptions is a separate PR.
9410
- *
9411
- * Always call `live.stop()` when finished.
9412
- */
9413
- live() {
9414
- const recompute = () => groupAndReduce(this.executeRecords(), this.field, this.spec);
9415
- return buildLiveAggregation(recompute, this.upstreams);
9416
- }
9417
- };
10303
+ // src/query/index.ts
10304
+ init_aggregation();
10305
+ init_groupby();
9418
10306
 
9419
10307
  // src/query/scan-builder.ts
10308
+ init_predicate();
9420
10309
  init_errors();
9421
10310
  var DEFAULT_SCAN_PAGE_SIZE = 100;
9422
10311
  var ScanBuilder = class _ScanBuilder {
@@ -9821,8 +10710,8 @@ function coerceRefKey2(value) {
9821
10710
 
9822
10711
  // src/indexing/persisted-indexes.ts
9823
10712
  var IDX_PREFIX = "_idx/";
9824
- function encodeIdxId(field, recordId) {
9825
- return `${IDX_PREFIX}${field}/${recordId}`;
10713
+ function encodeIdxId(field, recordId2) {
10714
+ return `${IDX_PREFIX}${field}/${recordId2}`;
9826
10715
  }
9827
10716
  function decodeIdxId(id) {
9828
10717
  if (!id.startsWith(IDX_PREFIX)) return null;
@@ -9830,12 +10719,13 @@ function decodeIdxId(id) {
9830
10719
  const firstSlash = rest.indexOf("/");
9831
10720
  if (firstSlash <= 0) return null;
9832
10721
  const field = rest.slice(0, firstSlash);
9833
- const recordId = rest.slice(firstSlash + 1);
9834
- if (recordId.length === 0) return null;
9835
- return { field, recordId };
10722
+ const recordId2 = rest.slice(firstSlash + 1);
10723
+ if (recordId2.length === 0) return null;
10724
+ return { field, recordId: recordId2 };
9836
10725
  }
9837
10726
 
9838
10727
  // src/indexing/lazy-builder.ts
10728
+ init_predicate();
9839
10729
  init_errors();
9840
10730
  var EMPTY_PLAN2 = {
9841
10731
  clauses: [],
@@ -10305,6 +11195,7 @@ var TxCollection = class {
10305
11195
  record
10306
11196
  };
10307
11197
  if (options?.expectedVersion !== void 0) op.expectedVersion = options.expectedVersion;
11198
+ if (options?.reason !== void 0) op.reason = options.reason;
10308
11199
  this._ctx._ops.push(op);
10309
11200
  }
10310
11201
  /**
@@ -10373,7 +11264,7 @@ async function runTransaction(db, fn, options) {
10373
11264
  const prior = priorEnvelopes.get(key) ?? null;
10374
11265
  ctx._executed.push({ op, priorEnvelope: prior });
10375
11266
  if (op.type === "put") {
10376
- await coll.put(op.id, op.record);
11267
+ await coll.put(op.id, op.record, op.reason !== void 0 ? { reason: op.reason } : void 0);
10377
11268
  } else {
10378
11269
  await coll.delete(op.id);
10379
11270
  }
@@ -10515,8 +11406,8 @@ async function resolveStaleOnRead(accessor, outputCollection, id) {
10515
11406
  for (const key of Object.keys(spec.outputs)) {
10516
11407
  const out = result.outputs[key];
10517
11408
  if (!out) continue;
10518
- if (!out.ok) {
10519
- const err = out.error ?? new Error(`derivation output "${key}" failed`);
11409
+ if (out.kind === "failed") {
11410
+ const err = out.error;
10520
11411
  if (spec.strict) {
10521
11412
  throw err;
10522
11413
  }
@@ -10526,6 +11417,12 @@ async function resolveStaleOnRead(accessor, outputCollection, id) {
10526
11417
  );
10527
11418
  continue;
10528
11419
  }
11420
+ if (out.kind === "array") {
11421
+ console.warn(
11422
+ `[derivation] unexpected array-shape output "${key}" in lazy resolve path; array-shape derivations require lifecycle: "eager" (#200 slice 1).`
11423
+ );
11424
+ continue;
11425
+ }
10529
11426
  const outSpec = spec.outputs[key];
10530
11427
  if (!outSpec) continue;
10531
11428
  const outputColl = accessor.getCollection(outSpec.collection);
@@ -11054,8 +11951,18 @@ var Collection = class {
11054
11951
  if (opts?.pollIntervalMs !== void 0) presenceOpts.pollIntervalMs = opts.pollIntervalMs;
11055
11952
  return this.syncStrategy.buildPresence(presenceOpts);
11056
11953
  }
11057
- /** Create or update a record. */
11058
- async put(id, record) {
11954
+ /**
11955
+ * Create or update a record.
11956
+ *
11957
+ * @param id Record identifier.
11958
+ * @param record The record body (validated by the collection's schema
11959
+ * if one was attached at `vault.collection(...)` time).
11960
+ * @param options Optional metadata for audit + import workflows.
11961
+ * `reason` is stamped onto the resulting ledger entry
11962
+ * (see #1) so audit consumers can filter via
11963
+ * `entries.filter(e => e.reason?.startsWith('import:'))`.
11964
+ */
11965
+ async put(id, record, options) {
11059
11966
  if (!hasWritePermission(this.keyring, this.name)) {
11060
11967
  throw new ReadOnlyError();
11061
11968
  }
@@ -11199,6 +12106,7 @@ var Collection = class {
11199
12106
  payloadHash: await this.historyStrategy.envelopePayloadHash(envelope2)
11200
12107
  };
11201
12108
  if (existingResolved) appendInput.delta = this.historyStrategy.computePatch(resolvedRecord, existingResolved.record);
12109
+ if (options?.reason !== void 0) appendInput.reason = options.reason;
11202
12110
  await this.ledger.append(appendInput);
11203
12111
  }
11204
12112
  if (this.lazy && this.lru) {
@@ -11264,6 +12172,7 @@ var Collection = class {
11264
12172
  if (existing) {
11265
12173
  appendInput.delta = this.historyStrategy.computePatch(record, existing.record);
11266
12174
  }
12175
+ if (options?.reason !== void 0) appendInput.reason = options.reason;
11267
12176
  await this.ledger.append(appendInput);
11268
12177
  }
11269
12178
  if (this.lazy && this.lru) {
@@ -11357,8 +12266,8 @@ var Collection = class {
11357
12266
  for (const key of Object.keys(spec.outputs)) {
11358
12267
  const out = result.outputs[key];
11359
12268
  if (!out) continue;
11360
- if (!out.ok) {
11361
- const err = out.error ?? new Error(`derivation output "${key}" failed`);
12269
+ if (out.kind === "failed") {
12270
+ const err = out.error;
11362
12271
  if (spec.strict) throw err;
11363
12272
  console.warn(`[derivation] output "${key}" for source "${spec.source}" id="${id}" failed:`, err);
11364
12273
  continue;
@@ -11367,6 +12276,46 @@ var Collection = class {
11367
12276
  if (!outSpec) continue;
11368
12277
  const outputCollection = this.derivationSource.getCollection(outSpec.collection);
11369
12278
  const txCtx = this.derivationSource.getActiveTxContext();
12279
+ if (out.kind === "array") {
12280
+ const { loadFanoutSidecar: loadFanoutSidecar2, saveFanoutSidecar: saveFanoutSidecar2 } = await Promise.resolve().then(() => (init_fanout_sidecar(), fanout_sidecar_exports));
12281
+ const prior = await loadFanoutSidecar2(
12282
+ this.adapter,
12283
+ this.vault,
12284
+ spec.source,
12285
+ id,
12286
+ key
12287
+ );
12288
+ const prevKeys = new Set(prior?.keys ?? []);
12289
+ const newKeysList = out.entries.map((e) => e.key);
12290
+ const newKeysSet = new Set(newKeysList);
12291
+ for (const k of prevKeys) {
12292
+ if (newKeysSet.has(k)) continue;
12293
+ await outputCollection._internalDelete(k, txCtx);
12294
+ }
12295
+ for (const entry of out.entries) {
12296
+ if (txCtx !== null) {
12297
+ const priorEnvelope = await this.adapter.get(this.vault, outSpec.collection, entry.key);
12298
+ txCtx._executed.push({
12299
+ op: {
12300
+ type: "put",
12301
+ vaultName: this.vault,
12302
+ collectionName: outSpec.collection,
12303
+ id: entry.key
12304
+ },
12305
+ priorEnvelope
12306
+ });
12307
+ }
12308
+ await outputCollection.put(entry.key, entry.value);
12309
+ }
12310
+ await saveFanoutSidecar2(this.adapter, this.vault, {
12311
+ source: spec.source,
12312
+ sourceId: id,
12313
+ outputKey: key,
12314
+ outputCollection: outSpec.collection,
12315
+ keys: newKeysList
12316
+ });
12317
+ continue;
12318
+ }
11370
12319
  if (out.skipped === true) {
11371
12320
  await outputCollection._internalDelete(id, txCtx);
11372
12321
  continue;
@@ -11557,6 +12506,87 @@ var Collection = class {
11557
12506
  action: "delete"
11558
12507
  });
11559
12508
  await this.onAccess?.("delete", id);
12509
+ if (!internal) {
12510
+ await this.dispatchMaterializedViewsOnDelete(id);
12511
+ await this.dispatchArrayDerivationsOnDelete(id);
12512
+ }
12513
+ }
12514
+ /**
12515
+ * Cascade deletes of array-shape derived rows when a source row is
12516
+ * deleted (#200). Reads each registered strategy's fanout sidecar
12517
+ * for this source id, deletes every listed derived row, then
12518
+ * deletes the sidecar itself.
12519
+ *
12520
+ * Record-shape derivations are skipped — see _doDelete's comment
12521
+ * for why the asymmetry is correct.
12522
+ *
12523
+ * @internal
12524
+ */
12525
+ async dispatchArrayDerivationsOnDelete(id) {
12526
+ if (this.derivationSource === void 0) return;
12527
+ const registry = this.derivationSource.registry();
12528
+ const strategies = registry.strategiesForSource(this.name);
12529
+ if (strategies.length === 0) return;
12530
+ let helpers = null;
12531
+ const txCtx = this.derivationSource.getActiveTxContext();
12532
+ for (const { spec } of strategies) {
12533
+ for (const [outputKey, outSpec] of Object.entries(spec.outputs)) {
12534
+ if (outSpec.shape !== "array") continue;
12535
+ if (helpers === null) {
12536
+ helpers = await Promise.resolve().then(() => (init_fanout_sidecar(), fanout_sidecar_exports));
12537
+ }
12538
+ const sidecar = await helpers.loadFanoutSidecar(
12539
+ this.adapter,
12540
+ this.vault,
12541
+ spec.source,
12542
+ id,
12543
+ outputKey
12544
+ );
12545
+ if (!sidecar) continue;
12546
+ const outputCollection = this.derivationSource.getCollection(outSpec.collection);
12547
+ for (const derivedId of sidecar.keys) {
12548
+ await outputCollection._internalDelete(derivedId, txCtx);
12549
+ }
12550
+ await helpers.deleteFanoutSidecar(this.adapter, this.vault, spec.source, id, outputKey);
12551
+ }
12552
+ }
12553
+ }
12554
+ /**
12555
+ * Mirror of {@link dispatchMaterializedViews} for the delete path
12556
+ * (#181). No record content is available (it's gone), so the
12557
+ * `_materializedFrom` skip used by the put-side dispatch doesn't
12558
+ * apply here — instead, the recursion guard is the `internal` gate
12559
+ * at the `_doDelete` call site above.
12560
+ *
12561
+ * @internal
12562
+ */
12563
+ async dispatchMaterializedViewsOnDelete(id) {
12564
+ void id;
12565
+ if (this.materializedViewSource === void 0) return;
12566
+ const registry = this.materializedViewSource.registry();
12567
+ const mvs = registry.mvsForSource(this.name);
12568
+ if (mvs.length === 0) return;
12569
+ let executor = null;
12570
+ let staleHelpers = null;
12571
+ for (const reg of mvs) {
12572
+ const mode = reg.spec.refresh;
12573
+ if (mode === "eager") {
12574
+ if (executor === null) {
12575
+ ;
12576
+ ({ MaterializedViewExecutor: executor } = await Promise.resolve().then(() => (init_executor3(), executor_exports3)));
12577
+ }
12578
+ await executor.refresh(reg, {
12579
+ getCollection: (name) => this.materializedViewSource.getCollection(name),
12580
+ getActiveTxContext: () => this.materializedViewSource.getActiveTxContext(),
12581
+ getQueryContext: () => this.materializedViewSource.getQueryContext()
12582
+ });
12583
+ } else if (mode === "lazy") {
12584
+ if (staleHelpers === null) {
12585
+ staleHelpers = await Promise.resolve().then(() => (init_stale(), stale_exports));
12586
+ }
12587
+ staleHelpers.markMVStale(registry, reg.spec.name);
12588
+ }
12589
+ }
11560
12590
  }
11561
12591
  /**
11562
12592
  * List all records in the collection.
@@ -12220,11 +13250,11 @@ var Collection = class {
12220
13250
  }
12221
13251
  }
12222
13252
  persisted.clear();
12223
- for (const recordId of canonicalIds) {
12224
- const envelope = await this.adapter.get(this.vault, this.name, recordId);
13253
+ for (const recordId2 of canonicalIds) {
13254
+ const envelope = await this.adapter.get(this.vault, this.name, recordId2);
12225
13255
  if (!envelope) continue;
12226
13256
  const record = await this.decryptRecord(envelope, { skipValidation: true });
12227
- await this.maintainPersistedIndexesOnPut(recordId, record, null, envelope._v);
13257
+ await this.maintainPersistedIndexesOnPut(recordId2, record, null, envelope._v);
12228
13258
  }
12229
13259
  this.persistedIndexesLoaded = true;
12230
13260
  }
@@ -13771,13 +14801,13 @@ async function runCompaction(ctx, options = {}) {
13771
14801
  collectionsWithPolicy += 1;
13772
14802
  byCollection[collectionName] = { records: 0, evicted: 0 };
13773
14803
  const ids = await ctx.listRecords(collectionName);
13774
- for (const recordId of ids) {
14804
+ for (const recordId2 of ids) {
13775
14805
  if (evicted >= maxEvictions) break outer;
13776
- const record = await ctx.getRecord(collectionName, recordId).catch(() => null);
14806
+ const record = await ctx.getRecord(collectionName, recordId2).catch(() => null);
13777
14807
  if (record === null) continue;
13778
14808
  records += 1;
13779
14809
  byCollection[collectionName].records += 1;
13780
- const slots = await ctx.listSlots(collectionName, recordId).catch(() => []);
14810
+ const slots = await ctx.listSlots(collectionName, recordId2).catch(() => []);
13781
14811
  for (const slot of slots) {
13782
14812
  if (evicted >= maxEvictions) break outer;
13783
14813
  const policy = config[slot.name];
@@ -13785,11 +14815,11 @@ async function runCompaction(ctx, options = {}) {
13785
14815
  const reason = evaluatePolicy(policy, record, slot, now);
13786
14816
  if (!reason) continue;
13787
14817
  if (!dryRun) {
13788
- await ctx.deleteSlot(collectionName, recordId, slot.name);
14818
+ await ctx.deleteSlot(collectionName, recordId2, slot.name);
13789
14819
  await writeAuditEntry(ctx, {
13790
- id: generateEvictionId(collectionName, recordId, slot.name),
14820
+ id: generateEvictionId(collectionName, recordId2, slot.name),
13791
14821
  collection: collectionName,
13792
- recordId,
14822
+ recordId: recordId2,
13793
14823
  slotName: slot.name,
13794
14824
  blobHash: slot.eTag,
13795
14825
  reason,
@@ -13833,11 +14863,11 @@ function evaluatePolicy(policy, record, slot, now) {
13833
14863
  if (predicateTriggered) return "predicate";
13834
14864
  return null;
13835
14865
  }
13836
- function generateEvictionId(collection, recordId, slotName) {
14866
+ function generateEvictionId(collection, recordId2, slotName) {
13837
14867
  const rand = globalThis.crypto.getRandomValues(new Uint8Array(8));
13838
14868
  let suffix = "";
13839
14869
  for (const b of rand) suffix += b.toString(16).padStart(2, "0");
13840
- return `${collection}__${recordId}__${slotName}__${suffix}`;
14870
+ return `${collection}__${recordId2}__${slotName}__${suffix}`;
13841
14871
  }
13842
14872
  async function writeAuditEntry(ctx, entry) {
13843
14873
  const json = JSON.stringify(entry);
@@ -13888,7 +14918,7 @@ async function deriveMagicLinkContentKey(serverSecret, token, vault) {
13888
14918
  ["encrypt", "decrypt"]
13889
14919
  );
13890
14920
  }
13891
- async function writeMagicLinkGrant(store, vault, grantor, contentKey, grantKek, recordId, opts) {
14921
+ async function writeMagicLinkGrant(store, vault, grantor, contentKey, grantKek, recordId2, opts) {
13892
14922
  const collectionName = opts.collection ?? null;
13893
14923
  const sourceKey = collectionName ? dekKey(collectionName, opts.tier) : `__any#${opts.tier}`;
13894
14924
  const sourceDek = grantor.deks.get(sourceKey);
@@ -13901,7 +14931,7 @@ async function writeMagicLinkGrant(store, vault, grantor, contentKey, grantKek,
13901
14931
  const until = typeof opts.until === "string" ? opts.until : opts.until.toISOString();
13902
14932
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
13903
14933
  const payload = {
13904
- id: recordId,
14934
+ id: recordId2,
13905
14935
  toUser: opts.toUser,
13906
14936
  fromUser: grantor.userId,
13907
14937
  tier: opts.tier,
@@ -13921,11 +14951,11 @@ async function writeMagicLinkGrant(store, vault, grantor, contentKey, grantKek,
13921
14951
  _data: data,
13922
14952
  _by: grantor.userId
13923
14953
  };
13924
- await store.put(vault, MAGIC_LINK_GRANTS_COLLECTION, recordId, envelope);
13925
- return { recordId, payload };
14954
+ await store.put(vault, MAGIC_LINK_GRANTS_COLLECTION, recordId2, envelope);
14955
+ return { recordId: recordId2, payload };
13926
14956
  }
13927
- async function readMagicLinkGrantRecord(store, vault, contentKey, recordId) {
13928
- const env = await store.get(vault, MAGIC_LINK_GRANTS_COLLECTION, recordId);
14957
+ async function readMagicLinkGrantRecord(store, vault, contentKey, recordId2) {
14958
+ const env = await store.get(vault, MAGIC_LINK_GRANTS_COLLECTION, recordId2);
13929
14959
  if (!env) return null;
13930
14960
  try {
13931
14961
  const json = await decrypt(env._iv, env._data, contentKey);
@@ -13962,6 +14992,248 @@ function isMagicLinkGrantExpired(payload, now = /* @__PURE__ */ new Date()) {
13962
14992
  return payload.until <= now.toISOString();
13963
14993
  }
13964
14994
 
14995
+ // src/introspection/fields.ts
14996
+ function jsonSchemaType(node) {
14997
+ if (Array.isArray(node.type)) {
14998
+ const non = node.type.filter((t) => t !== "null");
14999
+ return non[0] ?? "opaque";
15000
+ }
15001
+ if (node.enum && Array.isArray(node.enum)) return "enum";
15002
+ if (typeof node.type === "string") return node.type;
15003
+ return "opaque";
15004
+ }
15005
+ function constraintsFor(node) {
15006
+ const out = {};
15007
+ if (node.enum) out.values = node.enum;
15008
+ if (node.minLength !== void 0) out.minLength = node.minLength;
15009
+ if (node.maxLength !== void 0) out.maxLength = node.maxLength;
15010
+ if (node.pattern !== void 0) out.pattern = node.pattern;
15011
+ if (node.format !== void 0) out.format = node.format;
15012
+ if (node.minimum !== void 0) out.minimum = node.minimum;
15013
+ if (node.maximum !== void 0) out.maximum = node.maximum;
15014
+ if (node.exclusiveMinimum !== void 0) out.gt = node.exclusiveMinimum;
15015
+ if (node.exclusiveMaximum !== void 0) out.lt = node.exclusiveMaximum;
15016
+ return Object.keys(out).length === 0 ? void 0 : out;
15017
+ }
15018
+ function jsonSchemaToFields(jsonSchema, source, refs) {
15019
+ if (!jsonSchema || typeof jsonSchema !== "object") return {};
15020
+ const root = jsonSchema;
15021
+ if (!root.properties || typeof root.properties !== "object") return {};
15022
+ const required = new Set(Array.isArray(root.required) ? root.required : []);
15023
+ const out = {};
15024
+ for (const [name, node] of Object.entries(root.properties)) {
15025
+ const descriptor = {
15026
+ type: jsonSchemaType(node),
15027
+ source,
15028
+ ...required.has(name) ? {} : { optional: true },
15029
+ ...refs?.[name] ? { references: `${refs[name].target}.id` } : {}
15030
+ };
15031
+ const constraints = constraintsFor(node);
15032
+ if (constraints) descriptor.constraints = constraints;
15033
+ out[name] = descriptor;
15034
+ }
15035
+ return out;
15036
+ }
15037
+
15038
+ // src/introspection/walk.ts
15039
+ var INTERNAL_PREFIX = "_";
15040
+ var KNOWN_INTERNAL_NAMES = ["_keyring", "_ledger", "_meta", "_schemas", "_deltas"];
15041
+ async function dumpVaultSchema(vault, opts) {
15042
+ const state = vault._introspectState();
15043
+ const sampleSize = opts.sampleSize ?? 50;
15044
+ const withStats = opts.withStats === true;
15045
+ const cacheNames = [...state.collectionCache.keys()];
15046
+ const storageNames = (await safeListAllCollections(state.adapter, state.name)).filter((n) => !n.startsWith(INTERNAL_PREFIX));
15047
+ const allNames = Array.from(/* @__PURE__ */ new Set([...cacheNames, ...storageNames])).sort();
15048
+ const collections = {};
15049
+ for (const name of allNames) {
15050
+ collections[name] = await describeCollection(state, name, sampleSize, withStats);
15051
+ }
15052
+ const materializedViews = describeMVs(state.mvRegistry);
15053
+ const overlayViews = describeOverlays(state.overlayRegistry);
15054
+ const derivations = describeDerivations(state.derivationRegistry);
15055
+ let internal;
15056
+ if (withStats) {
15057
+ internal = {};
15058
+ for (const name of KNOWN_INTERNAL_NAMES) {
15059
+ const stats = await statsForCollection(state.adapter, state.name, name);
15060
+ if (stats.records > 0) {
15061
+ internal[name] = { records: stats.records, bytes: stats.bytes };
15062
+ }
15063
+ }
15064
+ }
15065
+ const snap = {
15066
+ _noydb_snapshot: 1,
15067
+ vault: state.name,
15068
+ emittedAt: (/* @__PURE__ */ new Date()).toISOString(),
15069
+ subsystems: state.subsystems,
15070
+ collections,
15071
+ materializedViews,
15072
+ overlayViews,
15073
+ derivations,
15074
+ ...internal !== void 0 ? { internal } : {}
15075
+ };
15076
+ return snap;
15077
+ }
15078
+ async function safeListAllCollections(adapter, vault) {
15079
+ try {
15080
+ const snap = await adapter.loadAll(vault);
15081
+ return Object.keys(snap);
15082
+ } catch {
15083
+ return [];
15084
+ }
15085
+ }
15086
+ async function describeCollection(state, collectionName, sampleSize, withStats) {
15087
+ let fields = {};
15088
+ let validator;
15089
+ const refsRaw = state.refRegistry.getOutbound(collectionName);
15090
+ const refs = {};
15091
+ for (const [name, desc] of Object.entries(refsRaw)) {
15092
+ refs[name] = { target: desc.target, mode: desc.mode };
15093
+ }
15094
+ try {
15095
+ const dek = await state.getDEK(collectionName);
15096
+ const persisted = await loadPersistedSchema(state.adapter, state.name, collectionName, dek);
15097
+ if (persisted) {
15098
+ validator = { kind: persisted.kind, source: "persisted" };
15099
+ if (persisted.jsonSchema) {
15100
+ fields = jsonSchemaToFields(persisted.jsonSchema, "persisted", refsRaw);
15101
+ }
15102
+ }
15103
+ } catch {
15104
+ }
15105
+ if (!validator) {
15106
+ const coll = state.collectionCache.get(collectionName);
15107
+ const schema = coll?.getSchema();
15108
+ if (schema) {
15109
+ try {
15110
+ const derived = await derivePersistedSchema(schema);
15111
+ validator = { kind: derived.kind, source: "live-validator" };
15112
+ if (derived.jsonSchema) {
15113
+ fields = jsonSchemaToFields(derived.jsonSchema, "live-validator", refsRaw);
15114
+ }
15115
+ } catch {
15116
+ }
15117
+ }
15118
+ }
15119
+ if (Object.keys(fields).length === 0 && sampleSize > 0) {
15120
+ }
15121
+ const descriptor = {
15122
+ fields,
15123
+ indexes: [],
15124
+ refs,
15125
+ ...validator ? { validator } : {}
15126
+ };
15127
+ if (withStats) {
15128
+ const stats = await statsForCollection(state.adapter, state.name, collectionName);
15129
+ descriptor.stats = stats;
15130
+ }
15131
+ return descriptor;
15132
+ }
15133
+ async function statsForCollection(adapter, vault, collection) {
15134
+ const ids = await adapter.list(vault, collection);
15135
+ if (ids.length === 0) {
15136
+ return { records: 0, bytes: 0, bytesAvg: 0, bytesMin: 0, bytesMax: 0, oldest: "", newest: "" };
15137
+ }
15138
+ let total = 0;
15139
+ let min2 = Number.POSITIVE_INFINITY;
15140
+ let max2 = 0;
15141
+ let oldest = "\uFFFF";
15142
+ let newest = "";
15143
+ for (const id of ids) {
15144
+ const env = await adapter.get(vault, collection, id);
15145
+ if (!env) continue;
15146
+ const size = env._data.length;
15147
+ total += size;
15148
+ if (size < min2) min2 = size;
15149
+ if (size > max2) max2 = size;
15150
+ if (env._ts < oldest) oldest = env._ts;
15151
+ if (env._ts > newest) newest = env._ts;
15152
+ }
15153
+ return {
15154
+ records: ids.length,
15155
+ bytes: total,
15156
+ bytesAvg: Math.round(total / ids.length),
15157
+ bytesMin: min2 === Number.POSITIVE_INFINITY ? 0 : min2,
15158
+ bytesMax: max2,
15159
+ oldest: oldest === "\uFFFF" ? "" : oldest,
15160
+ newest
15161
+ };
15162
+ }
15163
+ function describeMVs(registry) {
15164
+ if (!registry || typeof registry !== "object") return {};
15165
+ const items = listFromRegistry(registry);
15166
+ const out = {};
15167
+ for (const item of items) {
15168
+ const reg = item;
15169
+ const spec = reg.spec;
15170
+ if (!spec?.name) continue;
15171
+ const sources = spec.unionSources ? spec.unionSources.map((u) => u.collection) : reg.dependencies ? [...reg.dependencies].sort() : [];
15172
+ const groupBy = spec.groupBy ? Array.isArray(spec.groupBy) ? [...spec.groupBy] : [spec.groupBy] : void 0;
15173
+ const aggregate = spec.aggregate ? Object.fromEntries(
15174
+ Object.entries(spec.aggregate).map(([k, v]) => [k, summariseAggregateOp(v)])
15175
+ ) : void 0;
15176
+ out[spec.name] = {
15177
+ sources,
15178
+ ...groupBy ? { groupBy } : {},
15179
+ ...aggregate ? { aggregate } : {},
15180
+ refresh: spec.refresh ?? "eager"
15181
+ };
15182
+ }
15183
+ return out;
15184
+ }
15185
+ function describeOverlays(registry) {
15186
+ if (!registry || typeof registry !== "object") return {};
15187
+ const specs = listFromRegistry(registry);
15188
+ const out = {};
15189
+ for (const spec of specs) {
15190
+ const s = spec;
15191
+ if (!s.name || !s.base || !s.overlay) continue;
15192
+ out[s.name] = { base: s.base, overlay: s.overlay };
15193
+ }
15194
+ return out;
15195
+ }
15196
+ function describeDerivations(registry) {
15197
+ if (!registry || typeof registry !== "object") return {};
15198
+ const specs = listFromRegistry(registry);
15199
+ const out = {};
15200
+ for (const spec of specs) {
15201
+ const s = spec;
15202
+ if (!s.name) continue;
15203
+ out[s.name] = {
15204
+ source: s.source ?? "",
15205
+ outputs: s.outputs ?? []
15206
+ };
15207
+ }
15208
+ return out;
15209
+ }
15210
+ function listFromRegistry(reg) {
15211
+ for (const method of ["all", "list", "specs", "values"]) {
15212
+ const fn = reg[method];
15213
+ if (typeof fn === "function") {
15214
+ try {
15215
+ const out = fn.call(reg);
15216
+ if (Array.isArray(out)) return out;
15217
+ if (out && typeof out.values === "function") {
15218
+ return [...out.values()];
15219
+ }
15220
+ } catch {
15221
+ continue;
15222
+ }
15223
+ }
15224
+ }
15225
+ return [];
15226
+ }
15227
+ function summariseAggregateOp(value) {
15228
+ if (value && typeof value === "object") {
15229
+ const op = value.op ?? value.kind;
15230
+ const field = value.field;
15231
+ if (op && field) return `${op}(${field})`;
15232
+ if (op) return op;
15233
+ }
15234
+ return String(value);
15235
+ }
15236
+
13965
15237
  // src/vault.ts
13966
15238
  var Vault = class {
13967
15239
  adapter;
@@ -14085,6 +15357,16 @@ var Vault = class {
14085
15357
  * docstring.
14086
15358
  */
14087
15359
  ledgerStore = null;
15360
+ /**
15361
+ * Background writes for persisted-schema envelopes (#schema-dump v0
15362
+ * slice 1). One promise per `collection({ persistJsonSchema: true })`
15363
+ * registration that actually fired a derive call. Fire-and-forget
15364
+ * from the collection factory; tests await
15365
+ * {@link _drainPendingSchemaWrites} before asserting on storage.
15366
+ * Production code does not need to drain — the writes are
15367
+ * idempotent fingerprints, not correctness invariants.
15368
+ */
15369
+ _pendingSchemaWrites = [];
14088
15370
  /**
14089
15371
  * Per-vault foreign-key reference registry. Collections
14090
15372
  * register their `refs` option here on construction; the
@@ -14364,9 +15646,39 @@ var Vault = class {
14364
15646
  }
14365
15647
  coll = new Collection(collOpts);
14366
15648
  this.collectionCache.set(collectionName, coll);
15649
+ if (options?.persistJsonSchema === true && options.schema !== void 0) {
15650
+ const validator = options.schema;
15651
+ const work = (async () => {
15652
+ try {
15653
+ const dek = await this.getDEK(collectionName);
15654
+ await persistSchemaIfNeeded({
15655
+ store: this.adapter,
15656
+ vault: this.name,
15657
+ collectionName,
15658
+ validator,
15659
+ dek
15660
+ });
15661
+ } catch (err) {
15662
+ console.warn(
15663
+ `[noy-db] persisted-schema write failed for "${collectionName}": ` + (err instanceof Error ? err.message : String(err))
15664
+ );
15665
+ }
15666
+ })();
15667
+ this._pendingSchemaWrites.push(work);
15668
+ }
14367
15669
  }
14368
15670
  return coll;
14369
15671
  }
15672
+ /**
15673
+ * Await all background persisted-schema writes triggered by
15674
+ * `collection({ persistJsonSchema: true })` calls on this vault.
15675
+ * Used in tests; production code does not need to call this.
15676
+ */
15677
+ async _drainPendingSchemaWrites() {
15678
+ const pending = this._pendingSchemaWrites;
15679
+ this._pendingSchemaWrites = [];
15680
+ await Promise.allSettled(pending);
15681
+ }
14370
15682
  /**
14371
15683
  * Validate i18nText fields on a `put()`. Called by Collection just
14372
15684
  * before the adapter write, after schema validation. Throws
@@ -14961,7 +16273,6 @@ var Vault = class {
14961
16273
  * accessor `_getReadOnlyFacade()` (called from the tx amendment
14962
16274
  * runner) stays synchronous.
14963
16275
  */
14964
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
14965
16276
  async _initGuards(handles) {
14966
16277
  if (handles.length === 0) return;
14967
16278
  const [{ GuardRegistry: GuardRegistry2 }, { ReadOnlyVaultFacade: ReadOnlyVaultFacade2 }] = await Promise.all([
@@ -15134,17 +16445,41 @@ var Vault = class {
15134
16445
  let anyFailed = false;
15135
16446
  for (const key of Object.keys(spec.outputs)) {
15136
16447
  const out = result.outputs[key];
15137
- if (!out || !out.ok) {
16448
+ if (!out) continue;
16449
+ if (out.kind === "failed") {
15138
16450
  anyFailed = true;
15139
16451
  continue;
15140
16452
  }
15141
16453
  const outSpec = spec.outputs[key];
15142
16454
  if (!outSpec) continue;
16455
+ const outputColl = this.collection(outSpec.collection);
16456
+ if (out.kind === "array") {
16457
+ const { loadFanoutSidecar: loadFanoutSidecar2, saveFanoutSidecar: saveFanoutSidecar2 } = await Promise.resolve().then(() => (init_fanout_sidecar(), fanout_sidecar_exports));
16458
+ const prior = await loadFanoutSidecar2(this.adapter, this.name, spec.source, id, key);
16459
+ const prevKeys = new Set(prior?.keys ?? []);
16460
+ const newKeysList = out.entries.map((e) => e.key);
16461
+ const newKeysSet = new Set(newKeysList);
16462
+ for (const k of prevKeys) {
16463
+ if (newKeysSet.has(k)) continue;
16464
+ await outputColl._internalDelete(k);
16465
+ }
16466
+ for (const entry of out.entries) {
16467
+ await outputColl.put(entry.key, entry.value);
16468
+ }
16469
+ await saveFanoutSidecar2(this.adapter, this.name, {
16470
+ source: spec.source,
16471
+ sourceId: id,
16472
+ outputKey: key,
16473
+ outputCollection: outSpec.collection,
16474
+ keys: newKeysList
16475
+ });
16476
+ continue;
16477
+ }
15143
16478
  if (out.skipped === true) {
15144
- await this.collection(outSpec.collection)._internalDelete(id);
16479
+ await outputColl._internalDelete(id);
15145
16480
  continue;
15146
16481
  }
15147
- await this.collection(outSpec.collection).put(id, out.value);
16482
+ await outputColl.put(id, out.value);
15148
16483
  }
15149
16484
  if (anyFailed) failed++;
15150
16485
  else derived++;
@@ -15296,7 +16631,7 @@ var Vault = class {
15296
16631
  *
15297
16632
  * @internal
15298
16633
  */
15299
- async _logConsent(op, collection, recordId) {
16634
+ async _logConsent(op, collection, recordId2) {
15300
16635
  const ctx = this.consentContext;
15301
16636
  if (!ctx) return;
15302
16637
  await this.consentStrategy.write(
@@ -15309,7 +16644,7 @@ var Vault = class {
15309
16644
  consentHash: ctx.consentHash,
15310
16645
  op,
15311
16646
  collection,
15312
- recordId
16647
+ recordId: recordId2
15313
16648
  },
15314
16649
  this.getDEK
15315
16650
  );
@@ -15492,14 +16827,14 @@ var Vault = class {
15492
16827
  * the HKDF derivation, record-id composition, and batch logic so the
15493
16828
  * grantor doesn't touch this method directly.
15494
16829
  */
15495
- async writeMagicLinkGrant(contentKey, grantKek, recordId, opts) {
16830
+ async writeMagicLinkGrant(contentKey, grantKek, recordId2, opts) {
15496
16831
  return writeMagicLinkGrant(
15497
16832
  this.adapter,
15498
16833
  this.name,
15499
16834
  this.keyring,
15500
16835
  contentKey,
15501
16836
  grantKek,
15502
- recordId,
16837
+ recordId2,
15503
16838
  opts
15504
16839
  );
15505
16840
  }
@@ -15690,6 +17025,54 @@ var Vault = class {
15690
17025
  const snapshot = await this.adapter.loadAll(this.name);
15691
17026
  return Object.keys(snapshot);
15692
17027
  }
17028
+ /**
17029
+ * Emit a structured introspection snapshot of this vault — vault name,
17030
+ * subsystem opt-in matrix, collections + their fields, materialized
17031
+ * views, overlay views, derivations. With `withStats: true`, walks
17032
+ * every collection's envelopes to compute record counts, byte totals,
17033
+ * and oldest/newest timestamps.
17034
+ *
17035
+ * Consumed by the `noydb describe` CLI to produce human-readable
17036
+ * audit YAML/JSON from a `.noydb` bundle.
17037
+ *
17038
+ * Field provenance:
17039
+ * - `persisted`: read from `_schemas/<col>` envelope (Route B opt-in)
17040
+ * - `live-validator`: derived in-process from a Zod schema attached
17041
+ * to the live `Collection`
17042
+ * - `sampled`: inferred from decrypted records (deferred to a follow-up)
17043
+ * - `unknown`: no schema info available
17044
+ *
17045
+ * @see docs/superpowers/specs/2026-05-22-schema-dump-design.md
17046
+ */
17047
+ async dumpSchema(opts = {}) {
17048
+ return dumpVaultSchema(this, opts);
17049
+ }
17050
+ /**
17051
+ * Internal accessor for {@link dumpVaultSchema}. Exposes the structural
17052
+ * state the walker needs (collection cache, registries, ref registry,
17053
+ * adapter) without widening the public Vault surface.
17054
+ *
17055
+ * @internal
17056
+ */
17057
+ _introspectState() {
17058
+ return {
17059
+ name: this.name,
17060
+ adapter: this.adapter,
17061
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17062
+ collectionCache: this.collectionCache,
17063
+ refRegistry: this.refRegistry,
17064
+ getDEK: this.getDEK,
17065
+ subsystems: {
17066
+ guards: this.guardRegistry !== null,
17067
+ derivations: this.derivationRegistry !== null,
17068
+ materializedViews: this.materializedViewRegistry !== null,
17069
+ overlayViews: this.overlayedViewRegistry !== null
17070
+ },
17071
+ mvRegistry: this.materializedViewRegistry,
17072
+ overlayRegistry: this.overlayedViewRegistry,
17073
+ derivationRegistry: this.derivationRegistry
17074
+ };
17075
+ }
15693
17076
  /**
15694
17077
  * Return the stable opaque bundle handle for this vault,
15695
17078
  * generating and persisting a fresh ULID on first call.
@@ -15790,7 +17173,7 @@ var Vault = class {
15790
17173
  }
15791
17174
  }
15792
17175
  const internalSnapshot = {};
15793
- for (const internalName of [LEDGER_COLLECTION, LEDGER_DELTAS_COLLECTION]) {
17176
+ for (const internalName of [LEDGER_COLLECTION, LEDGER_DELTAS_COLLECTION, SCHEMAS_COLLECTION]) {
15794
17177
  const ids = await this.adapter.list(this.name, internalName);
15795
17178
  if (ids.length === 0) continue;
15796
17179
  const records = {};
@@ -16362,6 +17745,10 @@ var PERSONAL_POLICY = Object.freeze({
16362
17745
  minTier: 1,
16363
17746
  enabled: true
16364
17747
  },
17748
+ // rotate-recovery (#121): deliberate paper-sheet regeneration
17749
+ // when the user remembers their passphrase. PERSONAL matches the
17750
+ // pre-#121 low-level flow's bar — knowing the passphrase is enough.
17751
+ "rotate-recovery": { minTier: 1 },
16365
17752
  "enroll-authenticator": { minTier: 1 },
16366
17753
  "remove-authenticator": { minTier: 1 },
16367
17754
  // update-authenticator: meta-only mutation (slot rename, label
@@ -16421,6 +17808,14 @@ var STRICT_POLICY = Object.freeze({
16421
17808
  minTier: 1,
16422
17809
  enabled: true
16423
17810
  },
17811
+ // rotate-recovery (#121): STRICT requires an off-device factor —
17812
+ // rotating recovery is an off-site-trust event; a stolen unlocked
17813
+ // laptop must not be able to silently mint a new sheet for the
17814
+ // attacker. Matches the `peer-recover-user` STRICT default.
17815
+ "rotate-recovery": {
17816
+ minTier: 1,
17817
+ factors: [{ anyOf: ["totp", "email-otp", "webauthn-roaming"] }]
17818
+ },
16424
17819
  "enroll-authenticator": {
16425
17820
  minTier: 1,
16426
17821
  factors: [{ anyOf: ["totp", "email-otp"] }]
@@ -16609,6 +18004,14 @@ var Noydb = class {
16609
18004
  * `_meta/policy` load; replaced by `db.updatePolicy()`.
16610
18005
  */
16611
18006
  policyCache = /* @__PURE__ */ new Map();
18007
+ /**
18008
+ * One-shot bypass for the managed-mode strong-recovery check (#195).
18009
+ * Set true by {@link openVaultAndEnrollRecovery} for the duration of
18010
+ * the bootstrap window so the keyring can be created before the
18011
+ * strong recovery is enrolled. Always cleared (try/finally).
18012
+ * @internal
18013
+ */
18014
+ _skipNextManagedRecoveryCheck = false;
16612
18015
  /** Per-vault tier-3 (PIN / quick-resume) state — issue #11. */
16613
18016
  quickUnlock = new QuickUnlockStore();
16614
18017
  /**
@@ -17525,30 +18928,51 @@ var Noydb = class {
17525
18928
  });
17526
18929
  }
17527
18930
  /** Read or persist the vault policy at `_meta/policy` on first open. */
17528
- async bootstrapPolicy(vault) {
18931
+ async bootstrapPolicy(vault, opts) {
17529
18932
  const onDisk = await loadVaultPolicy(this.options.store, vault);
17530
18933
  if (onDisk) {
17531
18934
  this.policyCache.set(vault, onDisk);
17532
- await this.assertRecoveryEnrolled(vault, onDisk);
18935
+ await this.assertRecoveryEnrolled(vault, onDisk, opts);
17533
18936
  return;
17534
18937
  }
17535
18938
  const initial = this.options.policy ? mergePolicy(PERSONAL_POLICY, this.options.policy) : PERSONAL_POLICY;
17536
18939
  await saveVaultPolicy(this.options.store, vault, initial);
17537
18940
  this.policyCache.set(vault, initial);
17538
- await this.assertRecoveryEnrolled(vault, initial);
18941
+ await this.assertRecoveryEnrolled(vault, initial, opts);
17539
18942
  }
17540
18943
  /**
17541
- * Throw {@link RecoveryNotEnrolledError} when the developer
17542
- * explicitly opts into strict mandatory-recovery enforcement
17543
- * (`createNoydb({ requireRecovery: true })`) and no recovery
17544
- * entries are persisted.
18944
+ * Throw {@link RecoveryNotEnrolledError} or
18945
+ * {@link ManagedRecoveryNotEnrolledError} when recovery enrollment
18946
+ * is missing.
18947
+ *
18948
+ * Two enforcement modes:
18949
+ *
18950
+ * 1. **Managed-mode mandatory strong-recovery (#195).** When
18951
+ * `passphraseMode === 'managed'`, the vault MUST have at least
18952
+ * one **strong** recovery profile (Shamir today). Paper alone is
18953
+ * rejected because under managed mode the user has no memorized
18954
+ * passphrase, so losing the paper sheet = losing every record.
18955
+ * This check is unconditional — independent of `requireRecovery`
18956
+ * and the `recover-passphrase` gate.
18957
+ *
18958
+ * 2. **Opt-in strict mandatory-recovery.** When
18959
+ * `requireRecovery: true` is set on createNoydb (and the gate is
18960
+ * not explicitly disabled), require ANY recovery profile (paper
18961
+ * or shamir). This is the v0.x default-off behavior; v1.0 may
18962
+ * flip it default-on.
17545
18963
  *
17546
- * The default behavior is lenient `recover-passphrase` is enabled
17547
- * in `PERSONAL_POLICY` but the hub does not block vault open on
17548
- * missing enrollment. v1.0 will flip the default to strict; for now,
17549
- * apps that want the spec-mandated check turn it on per-vault.
18964
+ * The managed-mode check fires from {@link bootstrapPolicy} unless
18965
+ * the `skipManagedCheck` flag is set (used by
18966
+ * {@link openVaultAndEnrollRecovery} to allow atomic create-and-enroll).
17550
18967
  */
17551
- async assertRecoveryEnrolled(vault, policy) {
18968
+ async assertRecoveryEnrolled(vault, policy, opts) {
18969
+ const skipManaged = (opts?.skipManagedCheck ?? false) || this._skipNextManagedRecoveryCheck;
18970
+ if (this.options.passphraseMode === "managed" && !skipManaged) {
18971
+ const enrolled2 = await hasStrongRecoveryEnrolled(this.options.store, vault);
18972
+ if (!enrolled2) {
18973
+ throw new ManagedRecoveryNotEnrolledError(vault);
18974
+ }
18975
+ }
17552
18976
  if (this.options.requireRecovery !== true) return;
17553
18977
  const gate = policy.gates["recover-passphrase"];
17554
18978
  if (gate?.enabled === false) return;
@@ -17827,6 +19251,14 @@ var Noydb = class {
17827
19251
  * @throws `InvalidKeyError` when `oldPassphrase` is wrong.
17828
19252
  */
17829
19253
  async rotatePassphrase(vault, input, factors) {
19254
+ if (this.options.passphraseMode === "managed") {
19255
+ throw new PolicyDeniedError(
19256
+ "rotate-passphrase",
19257
+ "disabled",
19258
+ { minTier: 1, enabled: false },
19259
+ "Managed-passphrase mode (#14): the passphrase is hub-generated and sealed under the SealingKeyProvider \u2014 there is no plaintext to rotate. Use the recovery flow (follow-up issue) to mint a fresh sealed passphrase."
19260
+ );
19261
+ }
17830
19262
  await this.checkGate(vault, "rotate-passphrase", factors);
17831
19263
  const userId = this.options.user;
17832
19264
  const next = await rotatePassphrase(this.options.store, vault, userId, input);
@@ -17863,6 +19295,254 @@ var Noydb = class {
17863
19295
  await savePaperRecoveryEntries(this.options.store, vault, newEntries);
17864
19296
  return { newCodes: codes };
17865
19297
  }
19298
+ /**
19299
+ * Deliberate paper-recovery-code regeneration (#121). User knows their
19300
+ * passphrase but wants a fresh sheet — they lost the printout or
19301
+ * suspect compromise of the off-site copy.
19302
+ *
19303
+ * Symmetric to {@link rotatePassphrase} for the recovery profile:
19304
+ * gated, audit-trackable, ergonomic. Replaces (not appends) the
19305
+ * paper sheet under `_meta/recovery-paper` in a single envelope `put`.
19306
+ *
19307
+ * Gated by the `rotate-recovery` policy gate:
19308
+ * - PERSONAL_POLICY: `{ minTier: 1 }` — knowing the passphrase
19309
+ * suffices, matching the pre-#121 low-level flow's bar.
19310
+ * - STRICT_POLICY: `{ minTier: 1, factors: [{ anyOf: ['totp',
19311
+ * 'email-otp', 'webauthn-roaming'] }] }` — rotation is an
19312
+ * off-site-trust event; require an off-device factor so a
19313
+ * stolen unlocked laptop cannot silently mint a sheet for the
19314
+ * attacker.
19315
+ *
19316
+ * Defaults `count` to the existing sheet size so consumers aren't
19317
+ * surprised by a different code count. Explicit `count` overrides.
19318
+ *
19319
+ * @throws {@link RecoveryProfileNotImplementedError} when `profile`
19320
+ * is anything other than `'paper'` (v1 dispatch limit).
19321
+ * @throws {@link PolicyDeniedError} when the gate denies (missing
19322
+ * factor, tier mismatch, ...).
19323
+ * @throws on missing paper sheet — "nothing to rotate" surfaces as
19324
+ * an error rather than silently minting an entire new sheet.
19325
+ *
19326
+ * @example Default count + show-once UI
19327
+ * ```ts
19328
+ * const { newCodes } = await db.rotateRecovery('acme', { profile: 'paper' })
19329
+ * showCodesToUser(newCodes)
19330
+ * ```
19331
+ *
19332
+ * @example STRICT-policy site with TOTP factor proof
19333
+ * ```ts
19334
+ * await db.rotateRecovery(
19335
+ * 'acme',
19336
+ * { profile: 'paper', count: 10 },
19337
+ * { factors: [{ kind: 'totp', proof: '123456' }] },
19338
+ * )
19339
+ * ```
19340
+ */
19341
+ async rotateRecovery(vault, options, factors) {
19342
+ if (options.profile === "paper") {
19343
+ return this.rotateRecoveryPaper(vault, options, factors);
19344
+ }
19345
+ if (options.profile === "shamir") {
19346
+ return this.rotateRecoveryShamir(vault, options, factors);
19347
+ }
19348
+ throw new RecoveryProfileNotImplementedError(
19349
+ options.profile,
19350
+ "#196"
19351
+ );
19352
+ }
19353
+ async rotateRecoveryPaper(vault, options, factors) {
19354
+ await this.checkGate(vault, "rotate-recovery", factors);
19355
+ const existing = await loadPaperRecoveryEntries(this.options.store, vault);
19356
+ if (existing.length === 0) {
19357
+ throw new Error(
19358
+ `db.rotateRecovery: no recovery codes are enrolled for vault "${vault}". Call db.enrollRecovery({ profile: 'paper', entries }) first; rotateRecovery replaces an existing sheet rather than minting one from scratch.`
19359
+ );
19360
+ }
19361
+ const keyring = await this.getKeyring(vault);
19362
+ const codeGen = options.codeGenerator ?? generateULID;
19363
+ const count2 = options.count ?? existing.length;
19364
+ const codes = [];
19365
+ const newEntries = [];
19366
+ for (let i = 0; i < count2; i++) {
19367
+ const rawCode = codeGen();
19368
+ const entry = await mintPaperRecoveryEntry(keyring.deks, rawCode, generateULID());
19369
+ codes.push(rawCode);
19370
+ newEntries.push(entry);
19371
+ }
19372
+ await savePaperRecoveryEntries(this.options.store, vault, newEntries);
19373
+ return { newCodes: codes, entryId: "paper-batch" };
19374
+ }
19375
+ async rotateRecoveryShamir(vault, options, factors) {
19376
+ await this.checkGate(vault, "rotate-recovery", factors);
19377
+ const existing = await loadShamirRecoveryEntries(this.options.store, vault);
19378
+ if (existing.length === 0) {
19379
+ throw new Error(
19380
+ `db.rotateRecovery: no Shamir recovery entry is enrolled for vault "${vault}". Call db.enrollRecovery({ profile: 'shamir', k, n }) first; rotateRecovery replaces an existing entry rather than minting one from scratch.`
19381
+ );
19382
+ }
19383
+ let targetEntryId;
19384
+ if (options.entryId !== void 0) {
19385
+ const found = existing.find((e) => e.entryId === options.entryId);
19386
+ if (!found) {
19387
+ throw new Error(
19388
+ `db.rotateRecovery: no Shamir entry with entryId="${options.entryId}" found in vault "${vault}". Available: ${existing.map((e) => `"${e.entryId}"`).join(", ")}.`
19389
+ );
19390
+ }
19391
+ targetEntryId = options.entryId;
19392
+ } else {
19393
+ if (existing.length > 1) {
19394
+ throw new Error(
19395
+ `db.rotateRecovery: vault "${vault}" has ${existing.length} Shamir entries enrolled (${existing.map((e) => `"${e.entryId}"`).join(", ")}). Pass \`entryId\` to disambiguate which one to rotate; ambiguous rotation would risk replacing the wrong entry.`
19396
+ );
19397
+ }
19398
+ targetEntryId = existing[0].entryId;
19399
+ }
19400
+ const keyring = await this.getKeyring(vault);
19401
+ const { entry, shareStrings } = await mintShamirRecoveryEntry(
19402
+ keyring.deks,
19403
+ targetEntryId,
19404
+ options.k,
19405
+ options.n,
19406
+ options.label
19407
+ );
19408
+ const next = existing.filter((e) => e.entryId !== targetEntryId).concat(entry);
19409
+ await saveShamirRecoveryEntries(this.options.store, vault, next);
19410
+ return { newShares: shareStrings, entryId: targetEntryId };
19411
+ }
19412
+ /**
19413
+ * **Atomic create-and-enroll for managed-mode vaults (#195).**
19414
+ *
19415
+ * Bootstraps a managed-mode vault and enrolls strong recovery in
19416
+ * a single ceremony. Under `passphraseMode: 'managed'`, every
19417
+ * `openVault` call requires a strong recovery profile (Shamir
19418
+ * today) to be enrolled — otherwise it throws
19419
+ * {@link ManagedRecoveryNotEnrolledError}. This method bypasses
19420
+ * the check temporarily so the keyring can be created, enrolls
19421
+ * the supplied recovery profile(s), then returns the vault.
19422
+ *
19423
+ * For Shamir enrollments, the show-once share strings come back
19424
+ * in `recoveryEnrollments[i].shares`. The hub never retains them
19425
+ * — the caller MUST display them to the user (once) before any
19426
+ * subsequent operation.
19427
+ *
19428
+ * Paper alone is NOT a strong profile under managed mode; passing
19429
+ * `{ profile: 'paper', ... }` without an accompanying shamir entry
19430
+ * is rejected at validation time.
19431
+ *
19432
+ * ```ts
19433
+ * const db = await createNoydb({
19434
+ * store, user: 'alice',
19435
+ * passphraseMode: 'managed',
19436
+ * sealingKey: macosKeychainSealingProvider({ ... }),
19437
+ * })
19438
+ *
19439
+ * const { vault, recoveryEnrollments } = await db.openVaultAndEnrollRecovery('acme', {
19440
+ * recovery: [{ profile: 'shamir', k: 2, n: 3 }],
19441
+ * })
19442
+ * for (const r of recoveryEnrollments) {
19443
+ * if (r.shares) showSharesToUser(r.shares) // ONCE
19444
+ * }
19445
+ * ```
19446
+ *
19447
+ * @throws ValidationError if recovery is empty, or contains no
19448
+ * strong profile under managed mode.
19449
+ */
19450
+ async openVaultAndEnrollRecovery(vault, opts) {
19451
+ if (opts.recovery.length === 0) {
19452
+ throw new ValidationError(
19453
+ "openVaultAndEnrollRecovery: at least one recovery enrollment is required."
19454
+ );
19455
+ }
19456
+ if (this.options.passphraseMode === "managed") {
19457
+ const hasStrong = opts.recovery.some((r) => r.profile === "shamir");
19458
+ if (!hasStrong) {
19459
+ throw new ValidationError(
19460
+ 'openVaultAndEnrollRecovery: managed-mode vaults require at least one strong recovery profile in the `recovery` array. Paper alone is not strong under managed mode (no user passphrase to fall back on). Include { profile: "shamir", k, n } in `recovery`.'
19461
+ );
19462
+ }
19463
+ }
19464
+ this._skipNextManagedRecoveryCheck = true;
19465
+ let vaultHandle;
19466
+ try {
19467
+ vaultHandle = await this.openVault(vault, opts.locale !== void 0 ? { locale: opts.locale } : void 0);
19468
+ } finally {
19469
+ this._skipNextManagedRecoveryCheck = false;
19470
+ }
19471
+ const recoveryEnrollments = [];
19472
+ for (const enrollment of opts.recovery) {
19473
+ recoveryEnrollments.push(await this.enrollRecovery(vault, enrollment));
19474
+ }
19475
+ if (this.options.passphraseMode === "managed") {
19476
+ const policy = this.policyCache.get(vault);
19477
+ if (policy) {
19478
+ await this.assertRecoveryEnrolled(vault, policy);
19479
+ }
19480
+ }
19481
+ return { vault: vaultHandle, recoveryEnrollments };
19482
+ }
19483
+ /**
19484
+ * **Recovery flow under managed-passphrase mode (#195).**
19485
+ *
19486
+ * Replaces the sealed passphrase of a managed-mode vault with a
19487
+ * fresh 256-bit random, sealed under the configured
19488
+ * `SealingKeyProvider`. The user never sees the new passphrase.
19489
+ *
19490
+ * Internally:
19491
+ * 1. Verify the recovery proof (Shamir today) and unwrap the
19492
+ * DEK set.
19493
+ * 2. Mint a fresh 256-bit random as the new effective passphrase.
19494
+ * 3. Rewrap the DEK set under a fresh KEK derived from the new
19495
+ * passphrase (via the existing `recoverPassphrase` path).
19496
+ * 4. Seal the random bytes under the provider and overwrite
19497
+ * `_meta/sealed-passphrase`.
19498
+ * 5. Drop the keyring cache so the next operation re-derives.
19499
+ *
19500
+ * The vault's strong-recovery enrollment is preserved across
19501
+ * recovery (Shamir entries are not burned on use — see #196).
19502
+ *
19503
+ * @throws ValidationError if the Noydb instance is not in managed mode.
19504
+ */
19505
+ async recoverManagedPassphrase(vault, options) {
19506
+ if (this.options.passphraseMode !== "managed") {
19507
+ throw new ValidationError(
19508
+ "recoverManagedPassphrase: this method only applies to vaults opened in managed-passphrase mode. For standard mode, use db.recoverPassphrase."
19509
+ );
19510
+ }
19511
+ const provider = this.options.sealingKey;
19512
+ if (!provider) {
19513
+ throw new ValidationError(
19514
+ 'recoverManagedPassphrase: createNoydb({ passphraseMode: "managed" }) requires `sealingKey` to be supplied; without it the new sealed passphrase cannot be persisted.'
19515
+ );
19516
+ }
19517
+ const randomBytes = new Uint8Array(32);
19518
+ globalThis.crypto.getRandomValues(randomBytes);
19519
+ let binary = "";
19520
+ for (let i = 0; i < randomBytes.length; i++) binary += String.fromCharCode(randomBytes[i]);
19521
+ const newPassphrase = btoa(binary);
19522
+ try {
19523
+ const sealed = await provider.seal(randomBytes);
19524
+ await recoverPassphrase(
19525
+ this.options.store,
19526
+ vault,
19527
+ this.options.user,
19528
+ {
19529
+ newPassphrase,
19530
+ recoveryProof: options.recoveryProof,
19531
+ // The new passphrase IS 256 bits of random; policy gates on
19532
+ // length/entropy don't apply.
19533
+ allowWeakPassphrase: true,
19534
+ ...options.passphrasePolicy !== void 0 ? { passphrasePolicy: options.passphrasePolicy } : {}
19535
+ }
19536
+ );
19537
+ await saveSealedPassphrase(this.options.store, vault, {
19538
+ providerId: provider.id,
19539
+ sealed
19540
+ });
19541
+ } finally {
19542
+ randomBytes.fill(0);
19543
+ }
19544
+ this.keyringCache.delete(vault);
19545
+ }
17866
19546
  /**
17867
19547
  * Atomic peer-recovery — re-wraps an EXISTING user's keyring under
17868
19548
  * a fresh temp passphrase in a single store write. Closes #34's
@@ -17946,21 +19626,39 @@ var Noydb = class {
17946
19626
  * ```
17947
19627
  */
17948
19628
  async enrollRecovery(vault, enrollment) {
17949
- if (enrollment.profile !== "paper") {
17950
- throw new ValidationError(
17951
- `enrollRecovery: only 'paper' is implemented in v0.1.0-pre.5. Profile '${enrollment.profile}' is tracked under issue #10.`
19629
+ if (enrollment.profile === "paper") {
19630
+ const existing = await loadPaperRecoveryEntries(this.options.store, vault);
19631
+ await savePaperRecoveryEntries(this.options.store, vault, [
19632
+ ...existing,
19633
+ ...enrollment.entries
19634
+ ]);
19635
+ return { entryId: "paper-batch" };
19636
+ }
19637
+ if (enrollment.profile === "shamir") {
19638
+ const keyring = await this.getKeyring(vault);
19639
+ const entryId = enrollment.entryId ?? generateULID();
19640
+ const { entry, shareStrings } = await mintShamirRecoveryEntry(
19641
+ keyring.deks,
19642
+ entryId,
19643
+ enrollment.k,
19644
+ enrollment.n,
19645
+ enrollment.label
17952
19646
  );
19647
+ const existing = await loadShamirRecoveryEntries(this.options.store, vault);
19648
+ const next = existing.filter((e) => e.entryId !== entryId).concat(entry);
19649
+ await saveShamirRecoveryEntries(this.options.store, vault, next);
19650
+ return { entryId, shares: shareStrings };
17953
19651
  }
17954
- const existing = await loadPaperRecoveryEntries(this.options.store, vault);
17955
- await savePaperRecoveryEntries(this.options.store, vault, [
17956
- ...existing,
17957
- ...enrollment.entries
17958
- ]);
19652
+ throw new RecoveryProfileNotImplementedError(
19653
+ enrollment.profile,
19654
+ "#196"
19655
+ );
17959
19656
  }
17960
- /** Read the persisted paper-recovery entries. Used by `describeAuthConfig` (#13). */
19657
+ /** Read the persisted recovery entries (paper + Shamir). Used by `describeAuthConfig` (#13). */
17961
19658
  async listRecoveryEntries(vault) {
17962
19659
  const paper = await loadPaperRecoveryEntries(this.options.store, vault);
17963
- return { paper };
19660
+ const shamir = await loadShamirRecoveryEntries(this.options.store, vault);
19661
+ return { paper, shamir };
17964
19662
  }
17965
19663
  // ─── Tier-3 enroll / unlock (issue #11) ────────────────────────
17966
19664
  /**
@@ -18062,20 +19760,36 @@ var Noydb = class {
18062
19760
  this.keyringCache.set(vault, keyring2);
18063
19761
  return keyring2;
18064
19762
  }
18065
- if (!this.options.secret) {
19763
+ let effectiveSecret;
19764
+ if (this.options.passphraseMode === "managed") {
19765
+ effectiveSecret = await resolveManagedSecret(
19766
+ this.options.store,
19767
+ vault,
19768
+ this.options.sealingKey
19769
+ );
19770
+ } else {
19771
+ effectiveSecret = this.options.secret;
19772
+ }
19773
+ if (!effectiveSecret) {
18066
19774
  throw new ValidationError("A secret (passphrase) or getKeyring callback is required when encryption is enabled");
18067
19775
  }
18068
19776
  let keyring;
18069
19777
  try {
18070
- keyring = await loadKeyring(this.options.store, vault, this.options.user, this.options.secret);
19778
+ keyring = await loadKeyring(this.options.store, vault, this.options.user, effectiveSecret);
18071
19779
  } catch (err) {
18072
19780
  if (err instanceof NoAccessError) {
18073
19781
  keyring = await createOwnerKeyring(
18074
19782
  this.options.store,
18075
19783
  vault,
18076
19784
  this.options.user,
18077
- this.options.secret,
18078
- { validate: this.options.validatePassphrase === true }
19785
+ effectiveSecret,
19786
+ {
19787
+ // Managed mode generates 256-bit base64 strings that don't satisfy
19788
+ // the human-passphrase strength rules (no spaces, no "words").
19789
+ // Skip validation in managed mode — the entropy floor is already
19790
+ // 256 bits by construction.
19791
+ validate: this.options.passphraseMode === "managed" ? false : this.options.validatePassphrase === true
19792
+ }
18079
19793
  );
18080
19794
  } else if (err instanceof InvalidKeyError && this.options.onInvalidKey === "reset") {
18081
19795
  await this.options.store.delete(vault, "_keyring", this.options.user);
@@ -18083,8 +19797,10 @@ var Noydb = class {
18083
19797
  this.options.store,
18084
19798
  vault,
18085
19799
  this.options.user,
18086
- this.options.secret,
18087
- { validate: this.options.validatePassphrase === true }
19800
+ effectiveSecret,
19801
+ {
19802
+ validate: this.options.passphraseMode === "managed" ? false : this.options.validatePassphrase === true
19803
+ }
18088
19804
  );
18089
19805
  } else {
18090
19806
  throw err;
@@ -18096,10 +19812,28 @@ var Noydb = class {
18096
19812
  };
18097
19813
  async function createNoydb(options) {
18098
19814
  const encrypted = options.encrypt !== false;
19815
+ const managed = options.passphraseMode === "managed";
18099
19816
  if (options.secret && options.getKeyring) {
18100
19817
  throw new ValidationError("Provide either `secret` or `getKeyring`, not both");
18101
19818
  }
18102
- if (encrypted && !options.secret && !options.getKeyring) {
19819
+ if (managed) {
19820
+ if (options.secret) {
19821
+ throw new ValidationError(
19822
+ '`passphraseMode: "managed"` is mutually exclusive with `secret` \u2014 managed mode generates the passphrase itself. Drop `secret`.'
19823
+ );
19824
+ }
19825
+ if (options.getKeyring) {
19826
+ throw new ValidationError(
19827
+ '`passphraseMode: "managed"` is mutually exclusive with `getKeyring` \u2014 a custom unlock callback would bypass the sealing flow. Drop `getKeyring`.'
19828
+ );
19829
+ }
19830
+ if (!options.sealingKey) {
19831
+ throw new ValidationError(
19832
+ '`passphraseMode: "managed"` requires `sealingKey: SealingKeyProvider` (see @noy-db/seal-macos-keychain / @noy-db/seal-aws-kms / etc.).'
19833
+ );
19834
+ }
19835
+ }
19836
+ if (encrypted && !managed && !options.secret && !options.getKeyring) {
18103
19837
  throw new ValidationError("A secret (passphrase) or getKeyring callback is required when encryption is enabled");
18104
19838
  }
18105
19839
  return new Noydb(options);
@@ -18900,6 +20634,28 @@ function withDerivation(spec) {
18900
20634
  if (typeof spec.derive !== "function") {
18901
20635
  throw new ValidationError("withDerivation: derive must be a function");
18902
20636
  }
20637
+ const lifecycleMode = typeof spec.lifecycle === "string" ? spec.lifecycle : spec.lifecycle.mode;
20638
+ for (const [outputKey, outputSpec] of Object.entries(spec.outputs)) {
20639
+ if (outputSpec.shape === "array") {
20640
+ if (lifecycleMode !== "eager") {
20641
+ throw new ValidationError(
20642
+ `withDerivation: shape 'array' supports lifecycle 'eager' only in this release (#200 slice 1). Output "${outputKey}" declared lifecycle '${lifecycleMode}'. Switch to \`lifecycle: "eager"\` or use shape: "record".`
20643
+ );
20644
+ }
20645
+ if (typeof outputSpec.key !== "function") {
20646
+ throw new ValidationError(
20647
+ `withDerivation: shape 'array' output "${outputKey}" requires \`key: (out) => string\`.`
20648
+ );
20649
+ }
20650
+ if (outputSpec.maxFanout !== void 0) {
20651
+ if (!Number.isInteger(outputSpec.maxFanout) || outputSpec.maxFanout < 1) {
20652
+ throw new ValidationError(
20653
+ `withDerivation: maxFanout for output "${outputKey}" must be a positive integer (got ${String(outputSpec.maxFanout)}).`
20654
+ );
20655
+ }
20656
+ }
20657
+ }
20658
+ }
18903
20659
  return {
18904
20660
  __noydb_strategy: "derivation",
18905
20661
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -18916,9 +20672,60 @@ function withMaterializedView(spec) {
18916
20672
  if (!spec.name || spec.name.length === 0) {
18917
20673
  throw new ValidationError("withMaterializedView: name is required");
18918
20674
  }
18919
- if (typeof spec.query !== "function") {
20675
+ if (spec.query && spec.unionSources) {
20676
+ throw new MaterializedViewConfigError(
20677
+ "query and unionSources are mutually exclusive \u2014 pick one"
20678
+ );
20679
+ }
20680
+ if (!spec.query && !spec.unionSources) {
20681
+ throw new MaterializedViewConfigError(
20682
+ "strategy must declare either query or unionSources"
20683
+ );
20684
+ }
20685
+ if (spec.query !== void 0 && typeof spec.query !== "function") {
18920
20686
  throw new ValidationError("withMaterializedView: query must be a function returning a Query<T>");
18921
20687
  }
20688
+ if (spec.unionSources) {
20689
+ if (spec.unionSources.length < 2) {
20690
+ throw new MaterializedViewConfigError(
20691
+ "unionSources requires at least 2 source collections"
20692
+ );
20693
+ }
20694
+ const seen = /* @__PURE__ */ new Set();
20695
+ for (const s of spec.unionSources) {
20696
+ if (typeof s?.collection !== "string" || s.collection.length === 0) {
20697
+ throw new MaterializedViewConfigError(
20698
+ "each unionSources entry must declare a non-empty `collection` string"
20699
+ );
20700
+ }
20701
+ if (typeof s.map !== "function") {
20702
+ throw new MaterializedViewConfigError(
20703
+ `unionSources entry for "${s.collection}" is missing a \`map\` function`
20704
+ );
20705
+ }
20706
+ if (seen.has(s.collection)) {
20707
+ throw new MaterializedViewConfigError(
20708
+ `unionSources must reference distinct collections (duplicate: "${s.collection}")`
20709
+ );
20710
+ }
20711
+ seen.add(s.collection);
20712
+ }
20713
+ if (Array.isArray(spec.groupBy) && spec.groupBy.length === 0) {
20714
+ throw new MaterializedViewConfigError(
20715
+ `withMaterializedView "${spec.name}": groupBy must not be an empty array \u2014 omit it or provide at least one field name`
20716
+ );
20717
+ }
20718
+ if (spec.aggregate && !spec.groupBy) {
20719
+ throw new MaterializedViewConfigError(
20720
+ `withMaterializedView "${spec.name}": UNION strategy with aggregate requires groupBy \u2014 use groupBy to declare the bucketing keys, or remove aggregate for a pure dedup MV`
20721
+ );
20722
+ }
20723
+ if (spec.predicates) {
20724
+ throw new MaterializedViewConfigError(
20725
+ `withMaterializedView "${spec.name}": predicates are not supported on UNION strategies \u2014 UNION mode does not use a Query<T> chain, so .wherePredicate() cannot fire. Use the query() form, or open an issue if per-arm predicates are needed`
20726
+ );
20727
+ }
20728
+ }
18922
20729
  if (typeof spec.rowKey !== "function") {
18923
20730
  throw new ValidationError("withMaterializedView: rowKey is required (no default; see spec \xA7 Type surface)");
18924
20731
  }
@@ -19715,6 +21522,7 @@ function shortJSON(value) {
19715
21522
  BackupLedgerError,
19716
21523
  BlobSet,
19717
21524
  BundleIntegrityError,
21525
+ BundleSealMismatchError,
19718
21526
  BundleVersionConflictError,
19719
21527
  CONSENT_AUDIT_COLLECTION,
19720
21528
  Collection,
@@ -19732,6 +21540,7 @@ function shortJSON(value) {
19732
21540
  DanglingReferenceError,
19733
21541
  DecryptionError,
19734
21542
  DelegationTargetMissingError,
21543
+ DerivationCapExceededError,
19735
21544
  DerivationCycleError,
19736
21545
  DerivationDepthError,
19737
21546
  DerivationOutputShapeError,
@@ -19751,6 +21560,7 @@ function shortJSON(value) {
19751
21560
  GroupCardinalityError,
19752
21561
  GroupedAggregation,
19753
21562
  GroupedQuery,
21563
+ GroupedQueryN,
19754
21564
  INDEXED_STORE_POLICY,
19755
21565
  ImportCapabilityError,
19756
21566
  IndexRequiredError,
@@ -19770,9 +21580,12 @@ function shortJSON(value) {
19770
21580
  MAGIC_LINK_GRANTS_COLLECTION,
19771
21581
  MAGIC_LINK_KEK_INFO_PREFIX,
19772
21582
  META_COLLECTION,
21583
+ ManagedRecoveryNotEnrolledError,
21584
+ MaterializedViewConfigError,
19773
21585
  MaterializedViewCycleError,
19774
21586
  MaterializedViewSourceUnknownError,
19775
21587
  MaterializedViewTooLargeError,
21588
+ MemorySealingKeyProvider,
19776
21589
  MissingTranslationError,
19777
21590
  NOYDB_BACKUP_VERSION,
19778
21591
  NOYDB_BUNDLE_FORMAT_VERSION,
@@ -19814,6 +21627,8 @@ function shortJSON(value) {
19814
21627
  RefRegistry,
19815
21628
  RefScopeError,
19816
21629
  ReservedCollectionNameError,
21630
+ SCHEMAS_COLLECTION,
21631
+ SEALED_PASSPHRASE_RECORD_ID,
19817
21632
  STRICT_POLICY,
19818
21633
  SYNC_CREDENTIALS_COLLECTION,
19819
21634
  ScanBuilder,
@@ -19865,6 +21680,7 @@ function shortJSON(value) {
19865
21680
  createSession,
19866
21681
  createStore,
19867
21682
  credentialStatus,
21683
+ decodeShareBase32,
19868
21684
  decryptBytes,
19869
21685
  decryptDeterministic,
19870
21686
  dekKey,
@@ -19872,6 +21688,7 @@ function shortJSON(value) {
19872
21688
  deleteUserEnvelope,
19873
21689
  deleteUserVisibility,
19874
21690
  deriveMagicLinkContentKey,
21691
+ derivePersistedSchema,
19875
21692
  derivePresenceKey,
19876
21693
  describeAllUsersAuth,
19877
21694
  describeAuthConfig,
@@ -19886,6 +21703,7 @@ function shortJSON(value) {
19886
21703
  diffVault,
19887
21704
  effectiveClearance,
19888
21705
  enableDevUnlock,
21706
+ encodeShareBase32,
19889
21707
  encryptBytes,
19890
21708
  encryptDeterministic,
19891
21709
  enrollAuthenticator,
@@ -19917,6 +21735,7 @@ function shortJSON(value) {
19917
21735
  isPublicEnvelope,
19918
21736
  isSessionAlive,
19919
21737
  isULID,
21738
+ isZodSchema,
19920
21739
  issueDelegation,
19921
21740
  keyringRecoverPassphrase,
19922
21741
  keyringRotatePassphrase,
@@ -19928,7 +21747,10 @@ function shortJSON(value) {
19928
21747
  loadActiveDelegations,
19929
21748
  loadDevUnlock,
19930
21749
  loadPaperRecoveryEntries,
21750
+ loadPersistedSchema,
19931
21751
  loadPublicEnvelope,
21752
+ loadSealedPassphrase,
21753
+ loadShamirRecoveryEntries,
19932
21754
  loadUserEnvelope,
19933
21755
  loadVaultPolicy,
19934
21756
  magicLinkGrantRecordId,
@@ -19937,11 +21759,14 @@ function shortJSON(value) {
19937
21759
  mergePolicy,
19938
21760
  min,
19939
21761
  mintPaperRecoveryEntry,
21762
+ mintShamirRecoveryEntry,
19940
21763
  mintWrappedDeksBlob,
19941
21764
  paddedIndex,
19942
21765
  parseBytes,
19943
21766
  parseIndex,
21767
+ parseSealedEnvelope,
19944
21768
  persistDirectoryConfig,
21769
+ persistSchemaIfNeeded,
19945
21770
  persistUserVisibility,
19946
21771
  putCredential,
19947
21772
  readDirectoryConfig,
@@ -19969,13 +21794,17 @@ function shortJSON(value) {
19969
21794
  routeStore,
19970
21795
  runTransaction,
19971
21796
  savePaperRecoveryEntries,
21797
+ savePersistedSchema,
19972
21798
  savePublicEnvelope,
21799
+ saveSealedPassphrase,
21800
+ saveShamirRecoveryEntries,
19973
21801
  saveUserEnvelope,
19974
21802
  saveVaultPolicy,
19975
21803
  sha256Hex,
19976
21804
  sum,
19977
21805
  unwrapDeksFromBlob,
19978
21806
  unwrapDeksFromPaperEntry,
21807
+ unwrapDeksFromShamirEntry,
19979
21808
  unwrapMagicLinkGrant,
19980
21809
  validateI18nTextValue,
19981
21810
  validatePassphrase,