@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.
- package/dist/aggregate/index.cjs +91 -36
- package/dist/aggregate/index.cjs.map +1 -1
- package/dist/aggregate/index.d.cts +2 -2
- package/dist/aggregate/index.d.ts +2 -2
- package/dist/aggregate/index.js +15 -8
- package/dist/aggregate/index.js.map +1 -1
- package/dist/blobs/index.cjs.map +1 -1
- package/dist/blobs/index.d.cts +5 -4
- package/dist/blobs/index.d.ts +5 -4
- package/dist/blobs/index.js +4 -4
- package/dist/bundle/index.cjs +214 -7
- package/dist/bundle/index.cjs.map +1 -1
- package/dist/bundle/index.d.cts +5 -4
- package/dist/bundle/index.d.ts +5 -4
- package/dist/bundle/index.js +6 -4
- package/dist/{chunk-CX2S2IBB.js → chunk-23TTQXVO.js} +53 -79
- package/dist/chunk-23TTQXVO.js.map +1 -0
- package/dist/{chunk-WLZWITX3.js → chunk-2AXFIYHT.js} +1 -1
- package/dist/chunk-2AXFIYHT.js.map +1 -0
- package/dist/{chunk-SBOSKCXD.js → chunk-2R6G2WST.js} +209 -13
- package/dist/chunk-2R6G2WST.js.map +1 -0
- package/dist/{chunk-HENWE2QQ.js → chunk-34YSDCDP.js} +2 -2
- package/dist/{chunk-3J5PYYUL.js → chunk-5DWL3JBF.js} +2 -2
- package/dist/{chunk-562AT3V6.js → chunk-5NGHX6ME.js} +5 -5
- package/dist/chunk-5ZGZ6HIZ.js +100 -0
- package/dist/chunk-5ZGZ6HIZ.js.map +1 -0
- package/dist/{chunk-ZJICDHNH.js → chunk-6HPZY4ON.js} +5 -4
- package/dist/chunk-6HPZY4ON.js.map +1 -0
- package/dist/{chunk-LTKFPLTW.js → chunk-A3K2ECAM.js} +192 -11
- package/dist/chunk-A3K2ECAM.js.map +1 -0
- package/dist/{chunk-W7M3E5M5.js → chunk-ADQ5MQ54.js} +48 -1
- package/dist/{chunk-W7M3E5M5.js.map → chunk-ADQ5MQ54.js.map} +1 -1
- package/dist/{chunk-Q3OWJPJI.js → chunk-DPMFBCV6.js} +32 -19
- package/dist/chunk-DPMFBCV6.js.map +1 -0
- package/dist/{chunk-AD4FMCXZ.js → chunk-DYBQG5PQ.js} +2 -2
- package/dist/{chunk-PNTWORBA.js → chunk-DYECX3IX.js} +2 -2
- package/dist/chunk-EGQYGYIU.js +51 -0
- package/dist/chunk-EGQYGYIU.js.map +1 -0
- package/dist/{chunk-RGHN6WV7.js → chunk-FCXOFQAJ.js} +2 -2
- package/dist/{chunk-PMMNH7VZ.js → chunk-H7XROQJM.js} +1 -1
- package/dist/chunk-H7XROQJM.js.map +1 -0
- package/dist/chunk-HB3Z2GCR.js +124 -0
- package/dist/chunk-HB3Z2GCR.js.map +1 -0
- package/dist/{chunk-CRNMA2FB.js → chunk-I6MX32UC.js} +4 -4
- package/dist/{chunk-PHSGPPV7.js → chunk-KESP7GOK.js} +3 -3
- package/dist/{chunk-FB34MDSJ.js → chunk-KJJKHKFP.js} +4 -4
- package/dist/{chunk-J3E2UMET.js → chunk-MIQHZESA.js} +3 -3
- package/dist/{chunk-TNYWH7Z2.js → chunk-MKSA2V7A.js} +2 -2
- package/dist/{chunk-E4ZSYZSN.js → chunk-NVBUGT76.js} +4 -4
- package/dist/{chunk-XFVOTOYQ.js → chunk-O4RHR7FV.js} +6 -6
- package/dist/{chunk-EEEFQTYN.js → chunk-OMLIZL2P.js} +2 -2
- package/dist/{chunk-GVPFL6XF.js → chunk-P7EQ2S5O.js} +2 -2
- package/dist/{chunk-E3VAKWJ7.js → chunk-QQUUAXDY.js} +7 -6
- package/dist/chunk-QQUUAXDY.js.map +1 -0
- package/dist/chunk-RD5LYKD6.js +82 -0
- package/dist/chunk-RD5LYKD6.js.map +1 -0
- package/dist/{chunk-7QGJ7RTL.js → chunk-RRMDLTIL.js} +4 -4
- package/dist/{chunk-432R4RDC.js → chunk-SIZWEV2Y.js} +35 -5
- package/dist/chunk-SIZWEV2Y.js.map +1 -0
- package/dist/{chunk-U7YDBNFC.js → chunk-UZXLQCHP.js} +2 -2
- package/dist/{chunk-PPJA7KL6.js → chunk-V6SGP2KX.js} +3 -3
- package/dist/{chunk-7AQKLLI5.js → chunk-WCA2NROQ.js} +2 -2
- package/dist/{chunk-ERV4AXVO.js → chunk-XGSOTWYX.js} +90 -131
- package/dist/chunk-XGSOTWYX.js.map +1 -0
- package/dist/{chunk-5Y4AKEZ6.js → chunk-Z72JH4KG.js} +2 -2
- package/dist/{chunk-IXFP66OV.js → chunk-ZNOEIM6Y.js} +2 -2
- package/dist/consent/index.cjs.map +1 -1
- package/dist/consent/index.d.cts +5 -4
- package/dist/consent/index.d.ts +5 -4
- package/dist/consent/index.js +3 -3
- package/dist/{crypto-VTEMMJK6.js → crypto-A7FRXYHC.js} +3 -3
- package/dist/{delegation-HYQULPZR.js → delegation-YBA4X4JN.js} +5 -5
- package/dist/derivations/index.cjs +97 -1
- package/dist/derivations/index.cjs.map +1 -1
- package/dist/derivations/index.d.cts +37 -10
- package/dist/derivations/index.d.ts +37 -10
- package/dist/derivations/index.js +6 -4
- package/dist/{dev-unlock-CpJ9rHW2.d.ts → dev-unlock-C8u6MRfS.d.ts} +1 -1
- package/dist/{dev-unlock-DEUUpWoR.d.cts → dev-unlock-gBjwzB6A.d.cts} +1 -1
- package/dist/executor-7E3VFGW7.js +11 -0
- package/dist/executor-CEWX2FQI.js +8 -0
- package/dist/executor-X4SQ3ZLC.js +8 -0
- package/dist/fanout-sidecar-ACJNXFU6.js +51 -0
- package/dist/fanout-sidecar-ACJNXFU6.js.map +1 -0
- package/dist/guards/index.cjs.map +1 -1
- package/dist/guards/index.d.cts +6 -5
- package/dist/guards/index.d.ts +6 -5
- package/dist/guards/index.js +6 -6
- package/dist/{hash-_9VyW44M.d.cts → hash-BXy5DuTD.d.cts} +1 -1
- package/dist/{hash-CJ9LfuY5.d.ts → hash-C6KOPK9Q.d.ts} +1 -1
- package/dist/history/index.cjs +2 -1
- package/dist/history/index.cjs.map +1 -1
- package/dist/history/index.d.cts +6 -5
- package/dist/history/index.d.ts +6 -5
- package/dist/history/index.js +6 -6
- package/dist/i18n/index.cjs.map +1 -1
- package/dist/i18n/index.d.cts +5 -4
- package/dist/i18n/index.d.ts +5 -4
- package/dist/i18n/index.js +6 -6
- package/dist/{index-s31g3m-D.d.cts → index-BA7A-MJs.d.cts} +96 -4
- package/dist/{index-BWAXR9tV.d.ts → index-CLK67MZE.d.ts} +96 -4
- package/dist/{index-7nd15fgy.d.ts → index-CNwA-B6-.d.ts} +70 -2
- package/dist/{index-CFJKJXWa.d.cts → index-CmVgTkqk.d.cts} +70 -2
- package/dist/index.cjs +2400 -571
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +128 -18
- package/dist/index.d.ts +128 -18
- package/dist/index.js +1217 -107
- package/dist/index.js.map +1 -1
- package/dist/indexing/index.cjs.map +1 -1
- package/dist/indexing/index.js +2 -2
- package/dist/{ledger-LHLIVPJ3.js → ledger-GHIZQLKR.js} +6 -6
- package/dist/materialized-views/index.cjs +255 -21
- package/dist/materialized-views/index.cjs.map +1 -1
- package/dist/materialized-views/index.d.cts +7 -6
- package/dist/materialized-views/index.d.ts +7 -6
- package/dist/materialized-views/index.js +9 -5
- package/dist/overlay-views/index.cjs.map +1 -1
- package/dist/overlay-views/index.d.cts +6 -5
- package/dist/overlay-views/index.d.ts +6 -5
- package/dist/overlay-views/index.js +3 -3
- package/dist/periods/index.cjs +2 -1
- package/dist/periods/index.cjs.map +1 -1
- package/dist/periods/index.d.cts +5 -4
- package/dist/periods/index.d.ts +5 -4
- package/dist/periods/index.js +6 -6
- package/dist/{public-envelope-66ZBXVXK.js → public-envelope-CRJAVW3E.js} +4 -4
- package/dist/query/index.cjs +139 -113
- package/dist/query/index.cjs.map +1 -1
- package/dist/query/index.d.cts +2 -2
- package/dist/query/index.d.ts +2 -2
- package/dist/query/index.js +13 -9
- package/dist/{registry-JYALMHGM.js → registry-3L3N3PTG.js} +3 -3
- package/dist/registry-O47PUPSY.js +8 -0
- package/dist/registry-WLLMODKN.js +8 -0
- package/dist/session/index.cjs.map +1 -1
- package/dist/session/index.d.cts +6 -5
- package/dist/session/index.d.ts +6 -5
- package/dist/session/index.js +3 -3
- package/dist/shadow/index.cjs.map +1 -1
- package/dist/shadow/index.d.cts +5 -4
- package/dist/shadow/index.d.ts +5 -4
- package/dist/shadow/index.js +2 -2
- package/dist/{stale-EET5YFYL.js → stale-HSC5YO2O.js} +2 -2
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +5 -4
- package/dist/store/index.d.ts +5 -4
- package/dist/store/index.js +2 -2
- package/dist/{strategy-D-SrOLCl.d.cts → strategy-DSTrsZ8t.d.cts} +72 -19
- package/dist/{strategy-D-SrOLCl.d.ts → strategy-DSTrsZ8t.d.ts} +72 -19
- package/dist/sync/index.cjs.map +1 -1
- package/dist/sync/index.d.cts +4 -3
- package/dist/sync/index.d.ts +4 -3
- package/dist/sync/index.js +4 -4
- package/dist/team/index.cjs +136 -6
- package/dist/team/index.cjs.map +1 -1
- package/dist/team/index.d.cts +5 -4
- package/dist/team/index.d.ts +5 -4
- package/dist/team/index.js +7 -7
- package/dist/tx/index.cjs +2 -1
- package/dist/tx/index.cjs.map +1 -1
- package/dist/tx/index.d.cts +5 -4
- package/dist/tx/index.d.ts +5 -4
- package/dist/tx/index.js +2 -2
- package/dist/{types-2JGVRzO2.d.cts → types-DDy02bWN.d.cts} +1159 -257
- package/dist/{types-DIOLYPUq.d.ts → types-DImsOlJe.d.ts} +1159 -257
- package/dist/util/index.cjs.map +1 -1
- package/dist/util/index.js +1 -1
- package/dist/{with-derivation-ClhfQvRa.d.ts → with-derivation-3AdLeSw8.d.ts} +1 -1
- package/dist/{with-derivation-3JX8SMxY.d.cts → with-derivation-CkCHx_J8.d.cts} +1 -1
- package/dist/{with-guard-Br3YPxRW.d.ts → with-guard-Br9zLEdi.d.ts} +1 -1
- package/dist/{with-guard-fJVfWBLs.d.cts → with-guard-DtKsFExw.d.cts} +1 -1
- package/dist/with-materialized-view-CMhEX4FK.d.cts +27 -0
- package/dist/with-materialized-view-Zal2bE1T.d.ts +27 -0
- package/dist/{with-overlayed-view-BLUKxdiJ.d.cts → with-overlayed-view-CA6zHrBt.d.cts} +1 -1
- package/dist/{with-overlayed-view-CtvJxhU3.d.ts → with-overlayed-view-R4BNG9OB.d.ts} +1 -1
- package/package.json +14 -2
- package/dist/chunk-432R4RDC.js.map +0 -1
- package/dist/chunk-CX2S2IBB.js.map +0 -1
- package/dist/chunk-E3VAKWJ7.js.map +0 -1
- package/dist/chunk-ERV4AXVO.js.map +0 -1
- package/dist/chunk-GV47JHIF.js +0 -29
- package/dist/chunk-GV47JHIF.js.map +0 -1
- package/dist/chunk-LTKFPLTW.js.map +0 -1
- package/dist/chunk-PMMNH7VZ.js.map +0 -1
- package/dist/chunk-Q2L5ZYX7.js +0 -66
- package/dist/chunk-Q2L5ZYX7.js.map +0 -1
- package/dist/chunk-Q3OWJPJI.js.map +0 -1
- package/dist/chunk-SBK6CETA.js +0 -30
- package/dist/chunk-SBK6CETA.js.map +0 -1
- package/dist/chunk-SBOSKCXD.js.map +0 -1
- package/dist/chunk-WLZWITX3.js.map +0 -1
- package/dist/chunk-ZJICDHNH.js.map +0 -1
- package/dist/executor-3H3NSIVZ.js +0 -8
- package/dist/executor-7ZOXGY7P.js +0 -8
- package/dist/executor-A5LTUAG2.js +0 -9
- package/dist/registry-J2UI37XJ.js +0 -8
- package/dist/registry-UMAL72WL.js +0 -8
- package/dist/with-materialized-view-CHho83hN.d.cts +0 -15
- package/dist/with-materialized-view-CgPphnza.d.ts +0 -15
- /package/dist/{chunk-HENWE2QQ.js.map → chunk-34YSDCDP.js.map} +0 -0
- /package/dist/{chunk-3J5PYYUL.js.map → chunk-5DWL3JBF.js.map} +0 -0
- /package/dist/{chunk-562AT3V6.js.map → chunk-5NGHX6ME.js.map} +0 -0
- /package/dist/{chunk-AD4FMCXZ.js.map → chunk-DYBQG5PQ.js.map} +0 -0
- /package/dist/{chunk-PNTWORBA.js.map → chunk-DYECX3IX.js.map} +0 -0
- /package/dist/{chunk-RGHN6WV7.js.map → chunk-FCXOFQAJ.js.map} +0 -0
- /package/dist/{chunk-CRNMA2FB.js.map → chunk-I6MX32UC.js.map} +0 -0
- /package/dist/{chunk-PHSGPPV7.js.map → chunk-KESP7GOK.js.map} +0 -0
- /package/dist/{chunk-FB34MDSJ.js.map → chunk-KJJKHKFP.js.map} +0 -0
- /package/dist/{chunk-J3E2UMET.js.map → chunk-MIQHZESA.js.map} +0 -0
- /package/dist/{chunk-TNYWH7Z2.js.map → chunk-MKSA2V7A.js.map} +0 -0
- /package/dist/{chunk-E4ZSYZSN.js.map → chunk-NVBUGT76.js.map} +0 -0
- /package/dist/{chunk-XFVOTOYQ.js.map → chunk-O4RHR7FV.js.map} +0 -0
- /package/dist/{chunk-EEEFQTYN.js.map → chunk-OMLIZL2P.js.map} +0 -0
- /package/dist/{chunk-GVPFL6XF.js.map → chunk-P7EQ2S5O.js.map} +0 -0
- /package/dist/{chunk-7QGJ7RTL.js.map → chunk-RRMDLTIL.js.map} +0 -0
- /package/dist/{chunk-U7YDBNFC.js.map → chunk-UZXLQCHP.js.map} +0 -0
- /package/dist/{chunk-PPJA7KL6.js.map → chunk-V6SGP2KX.js.map} +0 -0
- /package/dist/{chunk-7AQKLLI5.js.map → chunk-WCA2NROQ.js.map} +0 -0
- /package/dist/{chunk-5Y4AKEZ6.js.map → chunk-Z72JH4KG.js.map} +0 -0
- /package/dist/{chunk-IXFP66OV.js.map → chunk-ZNOEIM6Y.js.map} +0 -0
- /package/dist/{crypto-VTEMMJK6.js.map → crypto-A7FRXYHC.js.map} +0 -0
- /package/dist/{delegation-HYQULPZR.js.map → delegation-YBA4X4JN.js.map} +0 -0
- /package/dist/{executor-3H3NSIVZ.js.map → executor-7E3VFGW7.js.map} +0 -0
- /package/dist/{executor-7ZOXGY7P.js.map → executor-CEWX2FQI.js.map} +0 -0
- /package/dist/{executor-A5LTUAG2.js.map → executor-X4SQ3ZLC.js.map} +0 -0
- /package/dist/{ledger-LHLIVPJ3.js.map → ledger-GHIZQLKR.js.map} +0 -0
- /package/dist/{public-envelope-66ZBXVXK.js.map → public-envelope-CRJAVW3E.js.map} +0 -0
- /package/dist/{registry-J2UI37XJ.js.map → registry-3L3N3PTG.js.map} +0 -0
- /package/dist/{registry-JYALMHGM.js.map → registry-O47PUPSY.js.map} +0 -0
- /package/dist/{registry-UMAL72WL.js.map → registry-WLLMODKN.js.map} +0 -0
- /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
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
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
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
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
|
-
|
|
2547
|
-
|
|
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/
|
|
2673
|
-
var
|
|
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
|
|
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
|
|
5582
|
-
|
|
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,
|
|
5630
|
-
if (
|
|
5631
|
-
return id.startsWith(`${collection}:${
|
|
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,
|
|
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,
|
|
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] =
|
|
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:
|
|
7009
|
-
iv:
|
|
7010
|
-
wrappedDeks:
|
|
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,
|
|
7961
|
+
const wrappingKey = await deriveWrappingKey(credential, base64ToBytes2(blob.salt));
|
|
7015
7962
|
const plaintext = await subtle2.decrypt(
|
|
7016
|
-
{ name: "AES-GCM", iv:
|
|
7963
|
+
{ name: "AES-GCM", iv: base64ToBytes2(blob.iv) },
|
|
7017
7964
|
wrappingKey,
|
|
7018
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
7217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
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
|
-
|
|
8648
|
-
|
|
8649
|
-
const
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
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/
|
|
9153
|
-
|
|
9154
|
-
|
|
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,
|
|
9825
|
-
return `${IDX_PREFIX}${field}/${
|
|
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
|
|
9834
|
-
if (
|
|
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 (
|
|
10519
|
-
const err = out.error
|
|
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
|
-
/**
|
|
11058
|
-
|
|
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 (
|
|
11361
|
-
const err = out.error
|
|
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
|
|
12224
|
-
const envelope = await this.adapter.get(this.vault, this.name,
|
|
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(
|
|
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
|
|
14804
|
+
for (const recordId2 of ids) {
|
|
13775
14805
|
if (evicted >= maxEvictions) break outer;
|
|
13776
|
-
const record = await ctx.getRecord(collectionName,
|
|
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,
|
|
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,
|
|
14818
|
+
await ctx.deleteSlot(collectionName, recordId2, slot.name);
|
|
13789
14819
|
await writeAuditEntry(ctx, {
|
|
13790
|
-
id: generateEvictionId(collectionName,
|
|
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,
|
|
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}__${
|
|
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,
|
|
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:
|
|
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,
|
|
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,
|
|
13928
|
-
const env = await store.get(vault, MAGIC_LINK_GRANTS_COLLECTION,
|
|
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
|
|
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
|
|
16479
|
+
await outputColl._internalDelete(id);
|
|
15145
16480
|
continue;
|
|
15146
16481
|
}
|
|
15147
|
-
await
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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}
|
|
17542
|
-
*
|
|
17543
|
-
*
|
|
17544
|
-
*
|
|
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
|
|
17547
|
-
*
|
|
17548
|
-
*
|
|
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
|
|
17950
|
-
|
|
17951
|
-
|
|
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
|
-
|
|
17955
|
-
|
|
17956
|
-
|
|
17957
|
-
|
|
17958
|
-
]);
|
|
19652
|
+
throw new RecoveryProfileNotImplementedError(
|
|
19653
|
+
enrollment.profile,
|
|
19654
|
+
"#196"
|
|
19655
|
+
);
|
|
17959
19656
|
}
|
|
17960
|
-
/** Read the persisted
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
18078
|
-
{
|
|
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
|
-
|
|
18087
|
-
{
|
|
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 (
|
|
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 (
|
|
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,
|