@noy-db/hub 0.1.0-pre.9 → 0.2.0-pre.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +16 -9
- package/dist/aggregate/index.js.map +1 -1
- package/dist/blobs/index.cjs.map +1 -1
- package/dist/blobs/index.d.cts +6 -6
- package/dist/blobs/index.d.ts +6 -6
- package/dist/blobs/index.js +4 -4
- package/dist/bundle/index.cjs +298 -7
- package/dist/bundle/index.cjs.map +1 -1
- package/dist/bundle/index.d.cts +6 -6
- package/dist/bundle/index.d.ts +6 -6
- package/dist/bundle/index.js +15 -4
- package/dist/{chunk-GOUT6DND.js → chunk-23TTQXVO.js} +173 -91
- package/dist/chunk-23TTQXVO.js.map +1 -0
- package/dist/{chunk-CIMZBAZB.js → chunk-2AXFIYHT.js} +1 -1
- package/dist/chunk-2AXFIYHT.js.map +1 -0
- package/dist/chunk-34YSDCDP.js +73 -0
- package/dist/chunk-34YSDCDP.js.map +1 -0
- package/dist/{chunk-AVVPZ4BC.js → chunk-4TFSM22V.js} +4 -4
- package/dist/{chunk-QGZRWRSL.js → chunk-537VFZTR.js} +4 -4
- package/dist/{chunk-M62XNWRA.js → chunk-5DWL3JBF.js} +2 -2
- package/dist/{chunk-PTVMYYON.js → chunk-5SCJ5UEF.js} +3 -3
- package/dist/chunk-5ZGZ6HIZ.js +100 -0
- package/dist/chunk-5ZGZ6HIZ.js.map +1 -0
- package/dist/chunk-6HPZY4ON.js +291 -0
- package/dist/chunk-6HPZY4ON.js.map +1 -0
- package/dist/{chunk-EXHNQEV4.js → chunk-7H6DOO3E.js} +239 -11
- package/dist/chunk-7H6DOO3E.js.map +1 -0
- package/dist/{chunk-ACLDOTNQ.js → chunk-ADQ5MQ54.js} +275 -3
- package/dist/chunk-ADQ5MQ54.js.map +1 -0
- package/dist/chunk-CBAHB2BF.js +893 -0
- package/dist/chunk-CBAHB2BF.js.map +1 -0
- package/dist/chunk-DPMFBCV6.js +296 -0
- package/dist/chunk-DPMFBCV6.js.map +1 -0
- package/dist/chunk-DYBQG5PQ.js +34 -0
- package/dist/chunk-DYBQG5PQ.js.map +1 -0
- package/dist/{chunk-ZFKD4QMV.js → chunk-DYECX3IX.js} +3 -3
- package/dist/chunk-EGQYGYIU.js +51 -0
- package/dist/chunk-EGQYGYIU.js.map +1 -0
- package/dist/chunk-FCXOFQAJ.js +79 -0
- package/dist/chunk-FCXOFQAJ.js.map +1 -0
- package/dist/chunk-HB3Z2GCR.js +124 -0
- package/dist/chunk-HB3Z2GCR.js.map +1 -0
- package/dist/{chunk-SCZXXXU4.js → chunk-I6MX32UC.js} +7 -32
- package/dist/chunk-I6MX32UC.js.map +1 -0
- package/dist/{chunk-VQBTTTUN.js → chunk-KESP7GOK.js} +4 -4
- package/dist/{chunk-VQBTTTUN.js.map → chunk-KESP7GOK.js.map} +1 -1
- package/dist/{chunk-NXFEYLVG.js → chunk-MIQHZESA.js} +4 -3
- package/dist/{chunk-NXFEYLVG.js.map → chunk-MIQHZESA.js.map} +1 -1
- package/dist/chunk-MKSA2V7A.js +19 -0
- package/dist/chunk-MKSA2V7A.js.map +1 -0
- package/dist/{chunk-M5INGEFC.js → chunk-MRIBLZL3.js} +3 -1
- package/dist/chunk-MRIBLZL3.js.map +1 -0
- package/dist/{chunk-MDDTIZUO.js → chunk-NIOHFJPJ.js} +6 -6
- package/dist/chunk-OMLIZL2P.js +61 -0
- package/dist/chunk-OMLIZL2P.js.map +1 -0
- package/dist/{chunk-USKYUS74.js → chunk-P7EQ2S5O.js} +2 -2
- package/dist/{chunk-WDM5XGGS.js → chunk-PA6R5ZCI.js} +181 -11
- package/dist/chunk-PA6R5ZCI.js.map +1 -0
- package/dist/chunk-PEULZC6M.js +118 -0
- package/dist/chunk-PEULZC6M.js.map +1 -0
- package/dist/chunk-RD5LYKD6.js +82 -0
- package/dist/chunk-RD5LYKD6.js.map +1 -0
- package/dist/chunk-SIZWEV2Y.js +145 -0
- package/dist/chunk-SIZWEV2Y.js.map +1 -0
- package/dist/{chunk-QAVUREFT.js → chunk-UA4RI7OT.js} +12 -6
- package/dist/chunk-UA4RI7OT.js.map +1 -0
- package/dist/chunk-UMLVJTYV.js +20 -0
- package/dist/chunk-UMLVJTYV.js.map +1 -0
- package/dist/chunk-UZXLQCHP.js +53 -0
- package/dist/chunk-UZXLQCHP.js.map +1 -0
- package/dist/{chunk-2CSJGFCB.js → chunk-VMIO4IXG.js} +5 -5
- package/dist/{chunk-MR4424N3.js → chunk-WCA2NROQ.js} +2 -2
- package/dist/{chunk-TDR6T5CJ.js → chunk-XGSOTWYX.js} +91 -132
- package/dist/chunk-XGSOTWYX.js.map +1 -0
- package/dist/{chunk-NPC4LFV5.js → chunk-YMYK7US4.js} +2 -2
- package/dist/{chunk-RKJ6OL7K.js → chunk-YS3POABP.js} +1 -1
- package/dist/chunk-YS3POABP.js.map +1 -0
- package/dist/chunk-Z72JH4KG.js +209 -0
- package/dist/chunk-Z72JH4KG.js.map +1 -0
- package/dist/{chunk-R36SIKES.js → chunk-ZNOEIM6Y.js} +2 -2
- package/dist/consent/index.cjs.map +1 -1
- package/dist/consent/index.d.cts +6 -6
- package/dist/consent/index.d.ts +6 -6
- package/dist/consent/index.js +3 -3
- package/dist/{crypto-IVKU7YTT.js → crypto-A7FRXYHC.js} +3 -3
- package/dist/{delegation-2DBS2EOH.js → delegation-YBA4X4JN.js} +5 -4
- package/dist/derivations/index.cjs +351 -0
- package/dist/derivations/index.cjs.map +1 -0
- package/dist/derivations/index.d.cts +71 -0
- package/dist/derivations/index.d.ts +71 -0
- package/dist/derivations/index.js +27 -0
- package/dist/{dev-unlock-BdPp68qn.d.ts → dev-unlock-D9s-loPr.d.ts} +1 -1
- package/dist/{dev-unlock-Da1B0TIK.d.cts → dev-unlock-DRwVSy2S.d.cts} +1 -1
- package/dist/executor-7E3VFGW7.js +11 -0
- package/dist/executor-CEWX2FQI.js +8 -0
- package/dist/executor-CEWX2FQI.js.map +1 -0
- package/dist/executor-X4SQ3ZLC.js +8 -0
- package/dist/executor-X4SQ3ZLC.js.map +1 -0
- package/dist/fanout-sidecar-VJ52RIEY.js +51 -0
- package/dist/fanout-sidecar-VJ52RIEY.js.map +1 -0
- package/dist/guards/index.cjs +315 -0
- package/dist/guards/index.cjs.map +1 -0
- package/dist/guards/index.d.cts +30 -0
- package/dist/guards/index.d.ts +30 -0
- package/dist/guards/index.js +29 -0
- package/dist/guards/index.js.map +1 -0
- package/dist/{hash-lsoL3eEW.d.ts → hash-DXXXusyk.d.ts} +1 -1
- package/dist/{hash-BEfzPKwo.d.cts → hash-DtRih9MQ.d.cts} +1 -1
- package/dist/history/index.cjs +8 -1
- package/dist/history/index.cjs.map +1 -1
- package/dist/history/index.d.cts +7 -7
- package/dist/history/index.d.ts +7 -7
- package/dist/history/index.js +6 -6
- package/dist/i18n/index.cjs +81 -0
- package/dist/i18n/index.cjs.map +1 -1
- package/dist/i18n/index.d.cts +6 -6
- package/dist/i18n/index.d.ts +6 -6
- package/dist/i18n/index.js +19 -6
- package/dist/i18n/index.js.map +1 -1
- package/dist/{index-8QDuznDr.d.ts → index-4agOpzqd.d.ts} +174 -3
- package/dist/{index-6xNpPsxR.d.cts → index-CNwA-B6-.d.ts} +303 -5
- package/dist/{index-DJTf9yxn.d.ts → index-CmVgTkqk.d.cts} +303 -5
- package/dist/{index-CywCC1qZ.d.cts → index-hdFvZkBP.d.cts} +174 -3
- package/dist/index.cjs +5615 -979
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +207 -16
- package/dist/index.d.ts +207 -16
- package/dist/index.js +2302 -741
- package/dist/index.js.map +1 -1
- package/dist/indexing/index.cjs +2 -0
- package/dist/indexing/index.cjs.map +1 -1
- package/dist/indexing/index.d.cts +3 -3
- package/dist/indexing/index.d.ts +3 -3
- package/dist/indexing/index.js +4 -4
- package/dist/{lazy-builder-CZVLKh0Z.d.cts → lazy-builder-C-rPfWG0.d.cts} +1 -1
- package/dist/{lazy-builder-BwEoBQZ9.d.ts → lazy-builder-Rpd-V3jP.d.ts} +1 -1
- package/dist/{ledger-QZTTHQAQ.js → ledger-3TXNP47J.js} +6 -6
- package/dist/ledger-3TXNP47J.js.map +1 -0
- package/dist/materialized-views/index.cjs +837 -0
- package/dist/materialized-views/index.cjs.map +1 -0
- package/dist/materialized-views/index.d.cts +183 -0
- package/dist/materialized-views/index.d.ts +183 -0
- package/dist/materialized-views/index.js +45 -0
- package/dist/materialized-views/index.js.map +1 -0
- package/dist/overlay-views/index.cjs +359 -0
- package/dist/overlay-views/index.cjs.map +1 -0
- package/dist/overlay-views/index.d.cts +81 -0
- package/dist/overlay-views/index.d.ts +81 -0
- package/dist/overlay-views/index.js +23 -0
- package/dist/overlay-views/index.js.map +1 -0
- package/dist/periods/index.cjs +7 -1
- package/dist/periods/index.cjs.map +1 -1
- package/dist/periods/index.d.cts +6 -6
- package/dist/periods/index.d.ts +6 -6
- package/dist/periods/index.js +6 -6
- package/dist/{predicate-SBHmi6D0.d.cts → predicate-Dnu81tsS.d.cts} +25 -1
- package/dist/{predicate-SBHmi6D0.d.ts → predicate-Dnu81tsS.d.ts} +25 -1
- package/dist/{public-envelope-6JTACYJV.js → public-envelope-PY6NKFLI.js} +4 -4
- package/dist/public-envelope-PY6NKFLI.js.map +1 -0
- package/dist/query/index.cjs +302 -124
- package/dist/query/index.cjs.map +1 -1
- package/dist/query/index.d.cts +3 -3
- package/dist/query/index.d.ts +3 -3
- package/dist/query/index.js +26 -11
- package/dist/read-only-facade-ITU6L7BL.js +7 -0
- package/dist/read-only-facade-ITU6L7BL.js.map +1 -0
- package/dist/registry-3L3N3PTG.js +10 -0
- package/dist/registry-3L3N3PTG.js.map +1 -0
- package/dist/registry-O47PUPSY.js +8 -0
- package/dist/registry-O47PUPSY.js.map +1 -0
- package/dist/registry-RFGGMVNJ.js +7 -0
- package/dist/registry-RFGGMVNJ.js.map +1 -0
- package/dist/registry-WLLMODKN.js +8 -0
- package/dist/registry-WLLMODKN.js.map +1 -0
- package/dist/session/index.cjs +7 -1
- package/dist/session/index.cjs.map +1 -1
- package/dist/session/index.d.cts +7 -7
- package/dist/session/index.d.ts +7 -7
- package/dist/session/index.js +10 -3
- package/dist/session/index.js.map +1 -1
- package/dist/shadow/index.cjs.map +1 -1
- package/dist/shadow/index.d.cts +6 -6
- package/dist/shadow/index.d.ts +6 -6
- package/dist/shadow/index.js +2 -2
- package/dist/stale-HSC5YO2O.js +13 -0
- package/dist/stale-HSC5YO2O.js.map +1 -0
- package/dist/store/index.cjs +14 -0
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +6 -6
- package/dist/store/index.d.ts +6 -6
- package/dist/store/index.js +5 -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 +5 -5
- package/dist/sync/index.d.ts +5 -5
- package/dist/sync/index.js +4 -4
- package/dist/team/index.cjs +1554 -2
- package/dist/team/index.cjs.map +1 -1
- package/dist/team/index.d.cts +6 -6
- package/dist/team/index.d.ts +6 -6
- package/dist/team/index.js +76 -9
- package/dist/tx/index.cjs +296 -44
- package/dist/tx/index.cjs.map +1 -1
- package/dist/tx/index.d.cts +6 -6
- package/dist/tx/index.d.ts +6 -6
- package/dist/tx/index.js +2 -2
- package/dist/{types-Bnb82f5R.d.cts → types-C4lwMKKF.d.cts} +2605 -328
- package/dist/{types-Bo7NSXJr.d.ts → types-DW9RGSSs.d.ts} +2605 -328
- package/dist/util/index.cjs.map +1 -1
- package/dist/util/index.js +1 -1
- package/dist/with-derivation-C8LDlV7t.d.cts +13 -0
- package/dist/with-derivation-g-pGoMzL.d.ts +13 -0
- package/dist/with-guard-DWOCK4Ca.d.ts +18 -0
- package/dist/with-guard-jI1x9Z3k.d.cts +18 -0
- package/dist/with-materialized-view-DaKR-N6J.d.ts +27 -0
- package/dist/with-materialized-view-DcTx4H3j.d.cts +27 -0
- package/dist/with-overlayed-view-D-6oWAgM.d.cts +13 -0
- package/dist/with-overlayed-view-N7jYuNOS.d.ts +13 -0
- package/package.json +53 -2
- package/dist/chunk-4PWAI7Q4.js +0 -79
- package/dist/chunk-4PWAI7Q4.js.map +0 -1
- package/dist/chunk-ACLDOTNQ.js.map +0 -1
- package/dist/chunk-BTDCBVJW.js +0 -160
- package/dist/chunk-BTDCBVJW.js.map +0 -1
- package/dist/chunk-CIMZBAZB.js.map +0 -1
- package/dist/chunk-EXHNQEV4.js.map +0 -1
- package/dist/chunk-GOUT6DND.js.map +0 -1
- package/dist/chunk-M5INGEFC.js.map +0 -1
- package/dist/chunk-QAVUREFT.js.map +0 -1
- package/dist/chunk-RKJ6OL7K.js.map +0 -1
- package/dist/chunk-SCZXXXU4.js.map +0 -1
- package/dist/chunk-TDR6T5CJ.js.map +0 -1
- package/dist/chunk-WDM5XGGS.js.map +0 -1
- /package/dist/{chunk-AVVPZ4BC.js.map → chunk-4TFSM22V.js.map} +0 -0
- /package/dist/{chunk-QGZRWRSL.js.map → chunk-537VFZTR.js.map} +0 -0
- /package/dist/{chunk-M62XNWRA.js.map → chunk-5DWL3JBF.js.map} +0 -0
- /package/dist/{chunk-PTVMYYON.js.map → chunk-5SCJ5UEF.js.map} +0 -0
- /package/dist/{chunk-ZFKD4QMV.js.map → chunk-DYECX3IX.js.map} +0 -0
- /package/dist/{chunk-MDDTIZUO.js.map → chunk-NIOHFJPJ.js.map} +0 -0
- /package/dist/{chunk-USKYUS74.js.map → chunk-P7EQ2S5O.js.map} +0 -0
- /package/dist/{chunk-2CSJGFCB.js.map → chunk-VMIO4IXG.js.map} +0 -0
- /package/dist/{chunk-MR4424N3.js.map → chunk-WCA2NROQ.js.map} +0 -0
- /package/dist/{chunk-NPC4LFV5.js.map → chunk-YMYK7US4.js.map} +0 -0
- /package/dist/{chunk-R36SIKES.js.map → chunk-ZNOEIM6Y.js.map} +0 -0
- /package/dist/{crypto-IVKU7YTT.js.map → crypto-A7FRXYHC.js.map} +0 -0
- /package/dist/{delegation-2DBS2EOH.js.map → delegation-YBA4X4JN.js.map} +0 -0
- /package/dist/{ledger-QZTTHQAQ.js.map → derivations/index.js.map} +0 -0
- /package/dist/{public-envelope-6JTACYJV.js.map → executor-7E3VFGW7.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,22 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DELEGATIONS_COLLECTION,
|
|
3
|
+
issueDelegation,
|
|
4
|
+
loadActiveDelegations,
|
|
5
|
+
revokeDelegation
|
|
6
|
+
} from "./chunk-I6MX32UC.js";
|
|
1
7
|
import {
|
|
2
8
|
DEFAULT_PUBLIC_ENVELOPE_SCHEMA,
|
|
3
9
|
PUBLIC_ENVELOPE_FIELDS,
|
|
4
10
|
resolveSchema
|
|
5
11
|
} from "./chunk-EMIGCR7X.js";
|
|
6
12
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
TxCollection,
|
|
14
|
+
TxContext,
|
|
15
|
+
TxVault,
|
|
16
|
+
revertExecuted,
|
|
17
|
+
runTransaction
|
|
18
|
+
} from "./chunk-6HPZY4ON.js";
|
|
19
|
+
import {
|
|
20
|
+
withDerivation
|
|
21
|
+
} from "./chunk-EGQYGYIU.js";
|
|
22
|
+
import "./chunk-FCXOFQAJ.js";
|
|
23
|
+
import "./chunk-HB3Z2GCR.js";
|
|
24
|
+
import {
|
|
25
|
+
withMaterializedView
|
|
26
|
+
} from "./chunk-RD5LYKD6.js";
|
|
27
|
+
import "./chunk-SIZWEV2Y.js";
|
|
28
|
+
import "./chunk-DPMFBCV6.js";
|
|
29
|
+
import "./chunk-UZXLQCHP.js";
|
|
30
|
+
import {
|
|
31
|
+
OverlayedCollection,
|
|
32
|
+
withOverlayedView
|
|
33
|
+
} from "./chunk-Z72JH4KG.js";
|
|
34
|
+
import "./chunk-OMLIZL2P.js";
|
|
15
35
|
import {
|
|
16
36
|
LazyQuery,
|
|
17
37
|
decodeIdxId,
|
|
18
38
|
encodeIdxId
|
|
19
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-DYECX3IX.js";
|
|
20
40
|
import {
|
|
21
41
|
mergeCrdtStates,
|
|
22
42
|
resolveCrdtSnapshot
|
|
@@ -31,7 +51,7 @@ import {
|
|
|
31
51
|
readNoydbBundlePublicEnvelope,
|
|
32
52
|
resetBrotliSupportCache,
|
|
33
53
|
writeNoydbBundle
|
|
34
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-7H6DOO3E.js";
|
|
35
55
|
import {
|
|
36
56
|
PUBLIC_ENVELOPE_RECORD_ID,
|
|
37
57
|
isPublicEnvelope,
|
|
@@ -39,24 +59,24 @@ import {
|
|
|
39
59
|
readPublicEnvelope,
|
|
40
60
|
savePublicEnvelope,
|
|
41
61
|
validatePublicEnvelopeInput
|
|
42
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-5SCJ5UEF.js";
|
|
43
63
|
import {
|
|
44
64
|
CONSENT_AUDIT_COLLECTION
|
|
45
|
-
} from "./chunk-
|
|
65
|
+
} from "./chunk-5DWL3JBF.js";
|
|
46
66
|
import {
|
|
47
67
|
PERIODS_COLLECTION
|
|
48
|
-
} from "./chunk-
|
|
68
|
+
} from "./chunk-537VFZTR.js";
|
|
49
69
|
import "./chunk-UF3BUNQZ.js";
|
|
70
|
+
import {
|
|
71
|
+
withGuard
|
|
72
|
+
} from "./chunk-MKSA2V7A.js";
|
|
73
|
+
import "./chunk-PEULZC6M.js";
|
|
74
|
+
import "./chunk-UMLVJTYV.js";
|
|
75
|
+
import "./chunk-34YSDCDP.js";
|
|
50
76
|
import {
|
|
51
77
|
CollectionFrame,
|
|
52
78
|
VaultFrame
|
|
53
|
-
} from "./chunk-
|
|
54
|
-
import {
|
|
55
|
-
TxCollection,
|
|
56
|
-
TxContext,
|
|
57
|
-
TxVault,
|
|
58
|
-
runTransaction
|
|
59
|
-
} from "./chunk-BTDCBVJW.js";
|
|
79
|
+
} from "./chunk-ZNOEIM6Y.js";
|
|
60
80
|
import {
|
|
61
81
|
DICT_COLLECTION_PREFIX,
|
|
62
82
|
DictionaryHandle,
|
|
@@ -69,7 +89,7 @@ import {
|
|
|
69
89
|
isI18nTextDescriptor,
|
|
70
90
|
resolveI18nText,
|
|
71
91
|
validateI18nTextValue
|
|
72
|
-
} from "./chunk-
|
|
92
|
+
} from "./chunk-NIOHFJPJ.js";
|
|
73
93
|
import {
|
|
74
94
|
createBundleStore,
|
|
75
95
|
routeStore,
|
|
@@ -81,30 +101,73 @@ import {
|
|
|
81
101
|
withRetry,
|
|
82
102
|
wrapBundleStore,
|
|
83
103
|
wrapStore
|
|
84
|
-
} from "./chunk-
|
|
104
|
+
} from "./chunk-P7EQ2S5O.js";
|
|
85
105
|
import {
|
|
106
|
+
MAGIC_LINK_CONTENT_INFO_PREFIX,
|
|
107
|
+
MAGIC_LINK_GRANTS_COLLECTION,
|
|
108
|
+
MAGIC_LINK_KEK_INFO_PREFIX,
|
|
109
|
+
ManagedRecoveryNotEnrolledError,
|
|
110
|
+
PolicyDeniedError,
|
|
111
|
+
RecoveryNotEnrolledError,
|
|
112
|
+
RecoveryProfileNotImplementedError,
|
|
86
113
|
SYNC_CREDENTIALS_COLLECTION,
|
|
114
|
+
burnPaperRecoveryEntry,
|
|
87
115
|
credentialStatus,
|
|
88
116
|
deleteCredential,
|
|
117
|
+
deriveMagicLinkContentKey,
|
|
118
|
+
enrollAuthenticator,
|
|
119
|
+
findAuthenticator,
|
|
89
120
|
getCredential,
|
|
121
|
+
hasRecoveryEnrolled,
|
|
122
|
+
hasStrongRecoveryEnrolled,
|
|
123
|
+
isMagicLinkGrantExpired,
|
|
90
124
|
listCredentials,
|
|
91
|
-
|
|
92
|
-
|
|
125
|
+
listMagicLinkGrants,
|
|
126
|
+
loadPaperRecoveryEntries,
|
|
127
|
+
loadShamirRecoveryEntries,
|
|
128
|
+
magicLinkGrantRecordId,
|
|
129
|
+
mintPaperRecoveryEntry,
|
|
130
|
+
mintShamirRecoveryEntry,
|
|
131
|
+
mintWrappedDeksBlob,
|
|
132
|
+
putCredential,
|
|
133
|
+
readMagicLinkGrantRecord,
|
|
134
|
+
recoverPassphrase,
|
|
135
|
+
recoverUser,
|
|
136
|
+
removeAuthenticator,
|
|
137
|
+
revokeMagicLinkGrant,
|
|
138
|
+
rotatePassphrase,
|
|
139
|
+
savePaperRecoveryEntries,
|
|
140
|
+
saveShamirRecoveryEntries,
|
|
141
|
+
unwrapDeksFromBlob,
|
|
142
|
+
unwrapDeksFromPaperEntry,
|
|
143
|
+
unwrapDeksFromShamirEntry,
|
|
144
|
+
unwrapMagicLinkGrant,
|
|
145
|
+
updateAuthenticator,
|
|
146
|
+
writeMagicLinkGrant
|
|
147
|
+
} from "./chunk-CBAHB2BF.js";
|
|
148
|
+
import {
|
|
149
|
+
assertTierAccess,
|
|
150
|
+
dekKey,
|
|
151
|
+
effectiveClearance
|
|
152
|
+
} from "./chunk-DYBQG5PQ.js";
|
|
93
153
|
import {
|
|
94
154
|
PresenceHandle,
|
|
95
155
|
SyncEngine,
|
|
96
156
|
SyncTransaction
|
|
97
|
-
} from "./chunk-
|
|
157
|
+
} from "./chunk-4TFSM22V.js";
|
|
98
158
|
import {
|
|
159
|
+
DIRECTORY_RECORD_ID,
|
|
99
160
|
USER_ENVELOPE_COLLECTION,
|
|
100
161
|
USER_ENVELOPE_MAX_BYTES,
|
|
101
162
|
UserEnvelopeOversizedError,
|
|
163
|
+
VISIBILITY_RECORD_PREFIX,
|
|
102
164
|
WeakPassphraseError,
|
|
103
165
|
assertStrongPassphrase,
|
|
104
166
|
buildRecipientKeyringFile,
|
|
105
167
|
changeSecret,
|
|
106
168
|
createOwnerKeyring,
|
|
107
169
|
deleteUserEnvelope,
|
|
170
|
+
deleteUserVisibility,
|
|
108
171
|
ensureCollectionDEK,
|
|
109
172
|
estimateEntropy,
|
|
110
173
|
evaluateExportCapability,
|
|
@@ -119,13 +182,17 @@ import {
|
|
|
119
182
|
listUsersWithEnvelopes,
|
|
120
183
|
loadKeyring,
|
|
121
184
|
loadUserEnvelope,
|
|
122
|
-
|
|
185
|
+
persistDirectoryConfig,
|
|
186
|
+
persistUserVisibility,
|
|
187
|
+
readDirectoryConfig,
|
|
188
|
+
readUserVisibility,
|
|
123
189
|
revoke,
|
|
124
190
|
rotateKeys,
|
|
125
191
|
saveUserEnvelope,
|
|
126
192
|
updateKeyringIdentity,
|
|
127
|
-
validatePassphrase
|
|
128
|
-
|
|
193
|
+
validatePassphrase,
|
|
194
|
+
visibilityRecordId
|
|
195
|
+
} from "./chunk-PA6R5ZCI.js";
|
|
129
196
|
import {
|
|
130
197
|
BUNDLE_STORE_POLICY,
|
|
131
198
|
INDEXED_STORE_POLICY,
|
|
@@ -145,7 +212,7 @@ import {
|
|
|
145
212
|
revokeAllSessions,
|
|
146
213
|
revokeSession,
|
|
147
214
|
validateSessionPolicy
|
|
148
|
-
} from "./chunk-
|
|
215
|
+
} from "./chunk-KESP7GOK.js";
|
|
149
216
|
import {
|
|
150
217
|
generateULID,
|
|
151
218
|
isULID
|
|
@@ -155,22 +222,22 @@ import {
|
|
|
155
222
|
VaultInstant,
|
|
156
223
|
diff,
|
|
157
224
|
formatDiff
|
|
158
|
-
} from "./chunk-
|
|
225
|
+
} from "./chunk-MIQHZESA.js";
|
|
159
226
|
import {
|
|
160
227
|
LEDGER_COLLECTION,
|
|
161
228
|
LEDGER_DELTAS_COLLECTION,
|
|
162
229
|
LedgerStore,
|
|
163
230
|
applyPatch,
|
|
164
231
|
computePatch
|
|
165
|
-
} from "./chunk-
|
|
232
|
+
} from "./chunk-UA4RI7OT.js";
|
|
166
233
|
import {
|
|
167
234
|
canonicalJson,
|
|
168
235
|
envelopePayloadHash,
|
|
169
236
|
hashEntry,
|
|
170
237
|
paddedIndex,
|
|
171
238
|
parseIndex,
|
|
172
|
-
sha256Hex
|
|
173
|
-
} from "./chunk-
|
|
239
|
+
sha256Hex as sha256Hex2
|
|
240
|
+
} from "./chunk-2AXFIYHT.js";
|
|
174
241
|
import {
|
|
175
242
|
DEFAULT_JOIN_MAX_ROWS,
|
|
176
243
|
NO_AGGREGATE,
|
|
@@ -180,29 +247,32 @@ import {
|
|
|
180
247
|
buildLiveQuery,
|
|
181
248
|
executePlan,
|
|
182
249
|
resetJoinWarnings
|
|
183
|
-
} from "./chunk-
|
|
250
|
+
} from "./chunk-23TTQXVO.js";
|
|
184
251
|
import {
|
|
185
252
|
CollectionIndexes
|
|
186
|
-
} from "./chunk-
|
|
253
|
+
} from "./chunk-YMYK7US4.js";
|
|
254
|
+
import {
|
|
255
|
+
avg,
|
|
256
|
+
count,
|
|
257
|
+
max,
|
|
258
|
+
min,
|
|
259
|
+
sum
|
|
260
|
+
} from "./chunk-5ZGZ6HIZ.js";
|
|
187
261
|
import {
|
|
188
262
|
Aggregation,
|
|
189
263
|
GROUPBY_MAX_CARDINALITY,
|
|
190
264
|
GROUPBY_WARN_CARDINALITY,
|
|
191
265
|
GroupedAggregation,
|
|
192
266
|
GroupedQuery,
|
|
193
|
-
|
|
194
|
-
count,
|
|
267
|
+
GroupedQueryN,
|
|
195
268
|
groupAndReduce,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
reduceRecords,
|
|
199
|
-
sum
|
|
200
|
-
} from "./chunk-TDR6T5CJ.js";
|
|
269
|
+
reduceRecords
|
|
270
|
+
} from "./chunk-XGSOTWYX.js";
|
|
201
271
|
import {
|
|
202
272
|
evaluateClause,
|
|
203
273
|
evaluateFieldClause,
|
|
204
274
|
readPath
|
|
205
|
-
} from "./chunk-
|
|
275
|
+
} from "./chunk-MRIBLZL3.js";
|
|
206
276
|
import {
|
|
207
277
|
BLOB_CHUNKS_COLLECTION,
|
|
208
278
|
BLOB_COLLECTION,
|
|
@@ -217,58 +287,74 @@ import {
|
|
|
217
287
|
detectMimeType,
|
|
218
288
|
isPreCompressed,
|
|
219
289
|
runCompaction
|
|
220
|
-
} from "./chunk-
|
|
290
|
+
} from "./chunk-VMIO4IXG.js";
|
|
221
291
|
import {
|
|
222
292
|
NOYDB_BACKUP_VERSION,
|
|
223
293
|
NOYDB_FORMAT_VERSION,
|
|
224
294
|
NOYDB_KEYRING_VERSION,
|
|
225
295
|
NOYDB_SYNC_VERSION,
|
|
226
296
|
createStore
|
|
227
|
-
} from "./chunk-
|
|
297
|
+
} from "./chunk-YS3POABP.js";
|
|
228
298
|
import {
|
|
229
299
|
base64ToBuffer,
|
|
230
300
|
bufferToBase64,
|
|
231
301
|
decrypt,
|
|
232
302
|
decryptBytes,
|
|
233
303
|
decryptDeterministic,
|
|
234
|
-
deriveKey,
|
|
235
304
|
derivePresenceKey,
|
|
236
305
|
encrypt,
|
|
237
306
|
encryptBytes,
|
|
238
307
|
encryptDeterministic,
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
wrapKey
|
|
242
|
-
} from "./chunk-MR4424N3.js";
|
|
308
|
+
sha256Hex
|
|
309
|
+
} from "./chunk-WCA2NROQ.js";
|
|
243
310
|
import {
|
|
244
311
|
AlreadyElevatedError,
|
|
312
|
+
AmendmentForbiddenError,
|
|
245
313
|
BackupCorruptedError,
|
|
246
314
|
BackupLedgerError,
|
|
247
315
|
BundleIntegrityError,
|
|
316
|
+
BundleSealMismatchError,
|
|
248
317
|
BundleVersionConflictError,
|
|
249
318
|
ConflictError,
|
|
250
319
|
DanglingReferenceError,
|
|
251
320
|
DecryptionError,
|
|
252
321
|
DelegationTargetMissingError,
|
|
322
|
+
DerivationCapExceededError,
|
|
323
|
+
DerivationCycleError,
|
|
324
|
+
DerivationDepthError,
|
|
325
|
+
DerivationOutputShapeError,
|
|
326
|
+
DerivationOutputUnknownError,
|
|
253
327
|
DictKeyInUseError,
|
|
254
328
|
DictKeyMissingError,
|
|
329
|
+
DirectoryDisabledError,
|
|
255
330
|
ElevationExpiredError,
|
|
256
331
|
ExportCapabilityError,
|
|
332
|
+
FieldFrozenError,
|
|
257
333
|
FilenameSanitizationError,
|
|
258
334
|
GroupCardinalityError,
|
|
259
335
|
ImportCapabilityError,
|
|
260
336
|
IndexRequiredError,
|
|
261
337
|
IndexWriteFailureError,
|
|
262
338
|
InvalidKeyError,
|
|
339
|
+
InvariantError,
|
|
263
340
|
JoinTooLargeError,
|
|
341
|
+
KeyringCorruptError,
|
|
264
342
|
KeyringExpiredError,
|
|
265
343
|
LedgerContentionError,
|
|
266
344
|
LocaleNotSpecifiedError,
|
|
345
|
+
MaterializedViewConfigError,
|
|
346
|
+
MaterializedViewCycleError,
|
|
347
|
+
MaterializedViewSourceUnknownError,
|
|
348
|
+
MaterializedViewTooLargeError,
|
|
267
349
|
MissingTranslationError,
|
|
268
350
|
NetworkError,
|
|
269
351
|
NoAccessError,
|
|
270
352
|
NotFoundError,
|
|
271
353
|
NoydbError,
|
|
354
|
+
OverlayBaseIsVirtualError,
|
|
355
|
+
OverlayCollectionUnavailableError,
|
|
356
|
+
OverlayIdMismatchError,
|
|
357
|
+
OverlayNameCollisionError,
|
|
272
358
|
PathEscapeError,
|
|
273
359
|
PeriodClosedError,
|
|
274
360
|
PermissionDeniedError,
|
|
@@ -276,6 +362,7 @@ import {
|
|
|
276
362
|
ReadOnlyAtInstantError,
|
|
277
363
|
ReadOnlyError,
|
|
278
364
|
ReadOnlyFrameError,
|
|
365
|
+
RecordLockedError,
|
|
279
366
|
ReservedCollectionNameError,
|
|
280
367
|
SchemaValidationError,
|
|
281
368
|
SessionExpiredError,
|
|
@@ -287,7 +374,7 @@ import {
|
|
|
287
374
|
TierNotGrantedError,
|
|
288
375
|
TranslatorNotConfiguredError,
|
|
289
376
|
ValidationError
|
|
290
|
-
} from "./chunk-
|
|
377
|
+
} from "./chunk-ADQ5MQ54.js";
|
|
291
378
|
|
|
292
379
|
// src/schema.ts
|
|
293
380
|
async function validateSchemaInput(schema, value, context) {
|
|
@@ -327,6 +414,109 @@ function formatPath(path) {
|
|
|
327
414
|
).join(".");
|
|
328
415
|
}
|
|
329
416
|
|
|
417
|
+
// src/persisted-schemas/canonicalize.ts
|
|
418
|
+
function canonicalize(value) {
|
|
419
|
+
if (value === null || typeof value !== "object") {
|
|
420
|
+
return JSON.stringify(value);
|
|
421
|
+
}
|
|
422
|
+
if (Array.isArray(value)) {
|
|
423
|
+
return "[" + value.map(canonicalize).join(",") + "]";
|
|
424
|
+
}
|
|
425
|
+
const obj = value;
|
|
426
|
+
const keys = Object.keys(obj).sort();
|
|
427
|
+
const parts = keys.map((k) => JSON.stringify(k) + ":" + canonicalize(obj[k]));
|
|
428
|
+
return "{" + parts.join(",") + "}";
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/persisted-schemas/derive.ts
|
|
432
|
+
function isZodSchema(value) {
|
|
433
|
+
if (value === null || typeof value !== "object") return false;
|
|
434
|
+
const def = value._def;
|
|
435
|
+
if (!def || typeof def !== "object") return false;
|
|
436
|
+
return typeof def.typeName === "string" && def.typeName.startsWith("Zod");
|
|
437
|
+
}
|
|
438
|
+
function detectKind(validator) {
|
|
439
|
+
if (isZodSchema(validator)) return "Zod";
|
|
440
|
+
return "Unknown";
|
|
441
|
+
}
|
|
442
|
+
async function loadZodConverter() {
|
|
443
|
+
try {
|
|
444
|
+
const mod = await import("zod-to-json-schema");
|
|
445
|
+
if (!mod.zodToJsonSchema) {
|
|
446
|
+
throw new Error("zod-to-json-schema export missing");
|
|
447
|
+
}
|
|
448
|
+
return mod.zodToJsonSchema;
|
|
449
|
+
} catch (err) {
|
|
450
|
+
throw new Error(
|
|
451
|
+
`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)}`
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
async function derivePersistedSchema(validator) {
|
|
456
|
+
const kind = detectKind(validator);
|
|
457
|
+
const derivedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
458
|
+
if (kind === "Zod") {
|
|
459
|
+
const convert = await loadZodConverter();
|
|
460
|
+
const jsonSchema = convert(validator);
|
|
461
|
+
const canonical = canonicalize(jsonSchema);
|
|
462
|
+
const hash = await sha256Hex(new TextEncoder().encode(canonical));
|
|
463
|
+
return { _noydb_schema: 1, kind, jsonSchema, hash, derivedAt };
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
_noydb_schema: 1,
|
|
467
|
+
kind,
|
|
468
|
+
jsonSchema: null,
|
|
469
|
+
hash: null,
|
|
470
|
+
reason: `derivation not yet supported for kind=${kind} (v0 supports Zod only)`,
|
|
471
|
+
derivedAt
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/persisted-schemas/storage.ts
|
|
476
|
+
var SCHEMAS_COLLECTION = "_schemas";
|
|
477
|
+
async function loadPersistedSchema(store, vault, collection, dek) {
|
|
478
|
+
const envelope = await store.get(vault, SCHEMAS_COLLECTION, collection);
|
|
479
|
+
if (!envelope) return void 0;
|
|
480
|
+
try {
|
|
481
|
+
const plaintext = await decrypt(envelope._iv, envelope._data, dek);
|
|
482
|
+
const parsed = JSON.parse(plaintext);
|
|
483
|
+
if (parsed._noydb_schema !== 1) return void 0;
|
|
484
|
+
return parsed;
|
|
485
|
+
} catch {
|
|
486
|
+
return void 0;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async function savePersistedSchema(store, vault, collection, dek, payload) {
|
|
490
|
+
const json = JSON.stringify(payload);
|
|
491
|
+
const { iv, data } = await encrypt(json, dek);
|
|
492
|
+
const prior = await store.get(vault, SCHEMAS_COLLECTION, collection);
|
|
493
|
+
const env = {
|
|
494
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
495
|
+
_v: (prior?._v ?? 0) + 1,
|
|
496
|
+
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
497
|
+
_iv: iv,
|
|
498
|
+
_data: data
|
|
499
|
+
};
|
|
500
|
+
await store.put(vault, SCHEMAS_COLLECTION, collection, env);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// src/persisted-schemas/register.ts
|
|
504
|
+
async function persistSchemaIfNeeded(opts) {
|
|
505
|
+
const fresh = await derivePersistedSchema(opts.validator);
|
|
506
|
+
const stored = await loadPersistedSchema(opts.store, opts.vault, opts.collectionName, opts.dek);
|
|
507
|
+
if (stored && isEquivalent(stored, fresh)) {
|
|
508
|
+
return { written: false, skipped: true, envelope: stored };
|
|
509
|
+
}
|
|
510
|
+
await savePersistedSchema(opts.store, opts.vault, opts.collectionName, opts.dek, fresh);
|
|
511
|
+
return { written: true, skipped: false, envelope: fresh };
|
|
512
|
+
}
|
|
513
|
+
function isEquivalent(a, b) {
|
|
514
|
+
if (a.kind !== b.kind) return false;
|
|
515
|
+
if (a.hash && b.hash) return a.hash === b.hash;
|
|
516
|
+
if (a.hash === null && b.hash === null) return true;
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
|
|
330
520
|
// src/refs.ts
|
|
331
521
|
var RefIntegrityError = class extends NoydbError {
|
|
332
522
|
collection;
|
|
@@ -427,88 +617,6 @@ var RefRegistry = class {
|
|
|
427
617
|
}
|
|
428
618
|
};
|
|
429
619
|
|
|
430
|
-
// src/team/authenticators.ts
|
|
431
|
-
async function enrollAuthenticator(store, vault, keyring, options) {
|
|
432
|
-
const existing = keyring.authenticators.find((a) => a.id === options.id);
|
|
433
|
-
if (existing) {
|
|
434
|
-
throw new ValidationError(
|
|
435
|
-
`enrollAuthenticator: slot id "${options.id}" already exists in vault "${vault}". Remove the slot first or pick a unique id.`
|
|
436
|
-
);
|
|
437
|
-
}
|
|
438
|
-
const base = {
|
|
439
|
-
id: options.id,
|
|
440
|
-
method: options.method,
|
|
441
|
-
enrolled_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
442
|
-
enrolled_via_tier: options.enrolled_via_tier ?? 1,
|
|
443
|
-
meta: options.meta
|
|
444
|
-
};
|
|
445
|
-
const slot = options.wrapKind === "deks" ? {
|
|
446
|
-
...base,
|
|
447
|
-
wrapKind: "deks",
|
|
448
|
-
wrapped_deks: options.wrapped_deks,
|
|
449
|
-
iv: options.iv
|
|
450
|
-
} : {
|
|
451
|
-
...base,
|
|
452
|
-
wrapped_kek: options.wrapped_kek
|
|
453
|
-
};
|
|
454
|
-
const next = appendSlot(keyring, slot);
|
|
455
|
-
await persistKeyring(store, vault, next);
|
|
456
|
-
return next;
|
|
457
|
-
}
|
|
458
|
-
async function updateAuthenticator(store, vault, keyring, slotId, options) {
|
|
459
|
-
if (options.meta === void 0) {
|
|
460
|
-
throw new ValidationError(
|
|
461
|
-
`updateAuthenticator: at least one of meta must be provided (slotId: "${slotId}").`
|
|
462
|
-
);
|
|
463
|
-
}
|
|
464
|
-
const idx = keyring.authenticators.findIndex((a) => a.id === slotId);
|
|
465
|
-
if (idx === -1) {
|
|
466
|
-
throw new NoAccessError(
|
|
467
|
-
`updateAuthenticator: slot "${slotId}" not found in vault "${vault}".`
|
|
468
|
-
);
|
|
469
|
-
}
|
|
470
|
-
const existing = keyring.authenticators[idx];
|
|
471
|
-
const mergedMeta = { ...existing.meta };
|
|
472
|
-
for (const [k, v] of Object.entries(options.meta)) {
|
|
473
|
-
if (v === void 0) continue;
|
|
474
|
-
if (v === null) {
|
|
475
|
-
delete mergedMeta[k];
|
|
476
|
-
continue;
|
|
477
|
-
}
|
|
478
|
-
mergedMeta[k] = v;
|
|
479
|
-
}
|
|
480
|
-
const next = { ...existing, meta: mergedMeta };
|
|
481
|
-
const nextSlots = [...keyring.authenticators];
|
|
482
|
-
nextSlots[idx] = next;
|
|
483
|
-
const nextKeyring = {
|
|
484
|
-
...keyring,
|
|
485
|
-
authenticators: nextSlots
|
|
486
|
-
};
|
|
487
|
-
await persistKeyring(store, vault, nextKeyring);
|
|
488
|
-
return nextKeyring;
|
|
489
|
-
}
|
|
490
|
-
async function removeAuthenticator(store, vault, keyring, slotId) {
|
|
491
|
-
const filtered = keyring.authenticators.filter((a) => a.id !== slotId);
|
|
492
|
-
if (filtered.length === keyring.authenticators.length) {
|
|
493
|
-
return keyring;
|
|
494
|
-
}
|
|
495
|
-
const next = {
|
|
496
|
-
...keyring,
|
|
497
|
-
authenticators: filtered
|
|
498
|
-
};
|
|
499
|
-
await persistKeyring(store, vault, next);
|
|
500
|
-
return next;
|
|
501
|
-
}
|
|
502
|
-
function findAuthenticator(keyring, slotId) {
|
|
503
|
-
return keyring.authenticators.find((a) => a.id === slotId);
|
|
504
|
-
}
|
|
505
|
-
function appendSlot(keyring, slot) {
|
|
506
|
-
return {
|
|
507
|
-
...keyring,
|
|
508
|
-
authenticators: [...keyring.authenticators, slot]
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
|
|
512
620
|
// src/session/unlock-state.ts
|
|
513
621
|
var QuickUnlockStore = class {
|
|
514
622
|
states = /* @__PURE__ */ new Map();
|
|
@@ -550,357 +658,6 @@ var QuickUnlockStore = class {
|
|
|
550
658
|
}
|
|
551
659
|
};
|
|
552
660
|
|
|
553
|
-
// src/policy/errors.ts
|
|
554
|
-
var PolicyDeniedError = class extends NoydbError {
|
|
555
|
-
gate;
|
|
556
|
-
reason;
|
|
557
|
-
required;
|
|
558
|
-
constructor(gate, reason, required, message) {
|
|
559
|
-
super(
|
|
560
|
-
"POLICY_DENIED",
|
|
561
|
-
message ?? `Gate "${gate}" denied: ${reason}.`
|
|
562
|
-
);
|
|
563
|
-
this.name = "PolicyDeniedError";
|
|
564
|
-
this.gate = gate;
|
|
565
|
-
this.reason = reason;
|
|
566
|
-
this.required = required;
|
|
567
|
-
}
|
|
568
|
-
};
|
|
569
|
-
var RecoveryNotEnrolledError = class extends NoydbError {
|
|
570
|
-
constructor(message = 'Recovery profile not enrolled. Pass `recovery: [{ profile: "paper", codes: 10 }]` to `createNoydb()`, or set `policy.gates["recover-passphrase"].enabled = false` to opt out of recovery (passphrase loss = data loss). See docs/subsystems/session-tiers.md.') {
|
|
571
|
-
super("RECOVERY_NOT_ENROLLED", message);
|
|
572
|
-
this.name = "RecoveryNotEnrolledError";
|
|
573
|
-
}
|
|
574
|
-
};
|
|
575
|
-
var RecoveryProfileNotImplementedError = class extends NoydbError {
|
|
576
|
-
profile;
|
|
577
|
-
tracking;
|
|
578
|
-
constructor(profile, tracking) {
|
|
579
|
-
super(
|
|
580
|
-
"RECOVERY_PROFILE_NOT_IMPLEMENTED",
|
|
581
|
-
`Recovery profile "${profile}" is not yet implemented in this hub release. Tracking: ${tracking}. Use the "paper" profile via @noy-db/on-recovery in the meantime.`
|
|
582
|
-
);
|
|
583
|
-
this.name = "RecoveryProfileNotImplementedError";
|
|
584
|
-
this.profile = profile;
|
|
585
|
-
this.tracking = tracking;
|
|
586
|
-
}
|
|
587
|
-
};
|
|
588
|
-
|
|
589
|
-
// src/team/wrapped-deks.ts
|
|
590
|
-
var PBKDF2_ITERATIONS = 6e5;
|
|
591
|
-
var SALT_BYTES = 32;
|
|
592
|
-
var IV_BYTES = 12;
|
|
593
|
-
var subtle = globalThis.crypto.subtle;
|
|
594
|
-
async function mintWrappedDeksBlob(deks, credential) {
|
|
595
|
-
const salt = crypto.getRandomValues(new Uint8Array(SALT_BYTES));
|
|
596
|
-
const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
|
|
597
|
-
const wrappingKey = await deriveWrappingKey(credential, salt);
|
|
598
|
-
const exported = {};
|
|
599
|
-
for (const [coll, dek] of deks) {
|
|
600
|
-
const raw = await subtle.exportKey("raw", dek);
|
|
601
|
-
exported[coll] = bytesToBase64(new Uint8Array(raw));
|
|
602
|
-
}
|
|
603
|
-
const plaintext = new TextEncoder().encode(JSON.stringify({ deks: exported }));
|
|
604
|
-
const ciphertext = await subtle.encrypt(
|
|
605
|
-
{ name: "AES-GCM", iv },
|
|
606
|
-
wrappingKey,
|
|
607
|
-
plaintext
|
|
608
|
-
);
|
|
609
|
-
return {
|
|
610
|
-
salt: bytesToBase64(salt),
|
|
611
|
-
iv: bytesToBase64(iv),
|
|
612
|
-
wrappedDeks: bytesToBase64(new Uint8Array(ciphertext))
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
async function unwrapDeksFromBlob(blob, credential) {
|
|
616
|
-
const wrappingKey = await deriveWrappingKey(credential, base64ToBytes(blob.salt));
|
|
617
|
-
const plaintext = await subtle.decrypt(
|
|
618
|
-
{ name: "AES-GCM", iv: base64ToBytes(blob.iv) },
|
|
619
|
-
wrappingKey,
|
|
620
|
-
base64ToBytes(blob.wrappedDeks)
|
|
621
|
-
);
|
|
622
|
-
const parsed = JSON.parse(new TextDecoder().decode(plaintext));
|
|
623
|
-
const deks = /* @__PURE__ */ new Map();
|
|
624
|
-
for (const [coll, b64] of Object.entries(parsed.deks)) {
|
|
625
|
-
const raw = base64ToBytes(b64);
|
|
626
|
-
const key = await subtle.importKey(
|
|
627
|
-
"raw",
|
|
628
|
-
raw,
|
|
629
|
-
{ name: "AES-GCM", length: 256 },
|
|
630
|
-
true,
|
|
631
|
-
["encrypt", "decrypt"]
|
|
632
|
-
);
|
|
633
|
-
deks.set(coll, key);
|
|
634
|
-
}
|
|
635
|
-
return deks;
|
|
636
|
-
}
|
|
637
|
-
async function deriveWrappingKey(credential, salt) {
|
|
638
|
-
const ikm = await subtle.importKey(
|
|
639
|
-
"raw",
|
|
640
|
-
new TextEncoder().encode(credential),
|
|
641
|
-
"PBKDF2",
|
|
642
|
-
false,
|
|
643
|
-
["deriveKey"]
|
|
644
|
-
);
|
|
645
|
-
return subtle.deriveKey(
|
|
646
|
-
{
|
|
647
|
-
name: "PBKDF2",
|
|
648
|
-
salt,
|
|
649
|
-
iterations: PBKDF2_ITERATIONS,
|
|
650
|
-
hash: "SHA-256"
|
|
651
|
-
},
|
|
652
|
-
ikm,
|
|
653
|
-
{ name: "AES-GCM", length: 256 },
|
|
654
|
-
false,
|
|
655
|
-
["encrypt", "decrypt"]
|
|
656
|
-
);
|
|
657
|
-
}
|
|
658
|
-
function bytesToBase64(b) {
|
|
659
|
-
let s = "";
|
|
660
|
-
for (const x of b) s += String.fromCharCode(x);
|
|
661
|
-
return btoa(s);
|
|
662
|
-
}
|
|
663
|
-
function base64ToBytes(b64) {
|
|
664
|
-
const s = atob(b64);
|
|
665
|
-
const out = new Uint8Array(s.length);
|
|
666
|
-
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
|
|
667
|
-
return out;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// src/team/recovery.ts
|
|
671
|
-
var PAPER_DOC_ID = "recovery-paper";
|
|
672
|
-
async function loadPaperRecoveryEntries(store, vault) {
|
|
673
|
-
const env = await store.get(vault, "_meta", PAPER_DOC_ID);
|
|
674
|
-
if (!env) return [];
|
|
675
|
-
try {
|
|
676
|
-
const doc = JSON.parse(env._data);
|
|
677
|
-
if (doc.profile !== "paper" || !Array.isArray(doc.entries)) return [];
|
|
678
|
-
return doc.entries;
|
|
679
|
-
} catch {
|
|
680
|
-
return [];
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
async function savePaperRecoveryEntries(store, vault, entries) {
|
|
684
|
-
const doc = {
|
|
685
|
-
_noydb_recovery: 1,
|
|
686
|
-
profile: "paper",
|
|
687
|
-
entries
|
|
688
|
-
};
|
|
689
|
-
const envelope = {
|
|
690
|
-
_noydb: NOYDB_FORMAT_VERSION,
|
|
691
|
-
_v: 1,
|
|
692
|
-
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
693
|
-
_iv: "",
|
|
694
|
-
_data: JSON.stringify(doc)
|
|
695
|
-
};
|
|
696
|
-
await store.put(vault, "_meta", PAPER_DOC_ID, envelope);
|
|
697
|
-
}
|
|
698
|
-
async function burnPaperRecoveryEntry(store, vault, codeId) {
|
|
699
|
-
const entries = await loadPaperRecoveryEntries(store, vault);
|
|
700
|
-
const remaining = entries.filter((e) => e.codeId !== codeId);
|
|
701
|
-
await savePaperRecoveryEntries(store, vault, remaining);
|
|
702
|
-
}
|
|
703
|
-
async function hasRecoveryEnrolled(store, vault) {
|
|
704
|
-
const paper = await loadPaperRecoveryEntries(store, vault);
|
|
705
|
-
return paper.length > 0;
|
|
706
|
-
}
|
|
707
|
-
async function mintPaperRecoveryEntry(deks, code, codeId) {
|
|
708
|
-
const blob = await mintWrappedDeksBlob(deks, code);
|
|
709
|
-
return {
|
|
710
|
-
...blob,
|
|
711
|
-
codeId,
|
|
712
|
-
enrolledAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
713
|
-
};
|
|
714
|
-
}
|
|
715
|
-
async function unwrapDeksFromPaperEntry(entry, code) {
|
|
716
|
-
return unwrapDeksFromBlob(entry, code);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
// src/team/rotate-recover.ts
|
|
720
|
-
async function rotatePassphrase(store, vault, userId, input) {
|
|
721
|
-
if (!input.allowWeakPassphrase) {
|
|
722
|
-
assertStrongPassphrase(input.newPassphrase, input.passphrasePolicy);
|
|
723
|
-
}
|
|
724
|
-
const env = await store.get(vault, "_keyring", userId);
|
|
725
|
-
if (!env) {
|
|
726
|
-
throw new NoAccessError(`No keyring found for user "${userId}" in vault "${vault}".`);
|
|
727
|
-
}
|
|
728
|
-
const file = JSON.parse(env._data);
|
|
729
|
-
const oldSalt = base64ToBuffer(file.salt);
|
|
730
|
-
const oldKek = await deriveKey(input.oldPassphrase, oldSalt);
|
|
731
|
-
const deks = /* @__PURE__ */ new Map();
|
|
732
|
-
for (const [coll, wrapped] of Object.entries(file.deks)) {
|
|
733
|
-
deks.set(coll, await unwrapKey(wrapped, oldKek));
|
|
734
|
-
}
|
|
735
|
-
const newSalt = generateSalt();
|
|
736
|
-
const newKek = await deriveKey(input.newPassphrase, newSalt);
|
|
737
|
-
const wrappedDeks = {};
|
|
738
|
-
for (const [coll, dek] of deks) {
|
|
739
|
-
wrappedDeks[coll] = await wrapKey(dek, newKek);
|
|
740
|
-
}
|
|
741
|
-
const oldSlots = file.authenticators ?? [];
|
|
742
|
-
const newSlots = [];
|
|
743
|
-
if (input.slotCeremonies && oldSlots.length > 0) {
|
|
744
|
-
for (const oldSlot of oldSlots) {
|
|
745
|
-
const ceremony = input.slotCeremonies[oldSlot.id];
|
|
746
|
-
if (!ceremony) continue;
|
|
747
|
-
const result = await ceremony({ newKek, newDeks: deks, oldSlot });
|
|
748
|
-
if (result.id !== oldSlot.id) {
|
|
749
|
-
throw new ValidationError(
|
|
750
|
-
`slotCeremonies['${oldSlot.id}'] returned id="${result.id}". The id must match the rotated slot \u2014 a ceremony cannot change a slot's identity.`
|
|
751
|
-
);
|
|
752
|
-
}
|
|
753
|
-
if (result.method !== oldSlot.method) {
|
|
754
|
-
throw new ValidationError(
|
|
755
|
-
`slotCeremonies['${oldSlot.id}'] returned method="${result.method}", expected "${oldSlot.method}". The method must match the rotated slot \u2014 a ceremony cannot change the auth method (e.g. webauthn \u2192 password) under cover of rotation.`
|
|
756
|
-
);
|
|
757
|
-
}
|
|
758
|
-
const baseFields = {
|
|
759
|
-
id: result.id,
|
|
760
|
-
method: result.method,
|
|
761
|
-
// Preserve original enrolled_at — rotation is rewrapping, not
|
|
762
|
-
// re-enrollment. The slot's enrolment timestamp tracks when
|
|
763
|
-
// the user originally added the slot, not when it was last
|
|
764
|
-
// rewrapped. Forensics consumers reading enrolled_at are
|
|
765
|
-
// tracking the slot's ORIGIN, not its CURRENT wrapping.
|
|
766
|
-
enrolled_at: oldSlot.enrolled_at,
|
|
767
|
-
enrolled_via_tier: result.enrolled_via_tier ?? oldSlot.enrolled_via_tier,
|
|
768
|
-
meta: result.meta
|
|
769
|
-
};
|
|
770
|
-
const newSlot = result.wrapKind === "deks" ? {
|
|
771
|
-
...baseFields,
|
|
772
|
-
wrapKind: "deks",
|
|
773
|
-
wrapped_deks: result.wrapped_deks,
|
|
774
|
-
iv: result.iv
|
|
775
|
-
} : {
|
|
776
|
-
...baseFields,
|
|
777
|
-
wrapped_kek: result.wrapped_kek
|
|
778
|
-
};
|
|
779
|
-
newSlots.push(newSlot);
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
const next = {
|
|
783
|
-
...file,
|
|
784
|
-
_noydb_keyring: NOYDB_KEYRING_VERSION,
|
|
785
|
-
deks: wrappedDeks,
|
|
786
|
-
salt: bufferToBase64(newSalt),
|
|
787
|
-
authenticators: newSlots
|
|
788
|
-
};
|
|
789
|
-
await writeKeyringFile(store, vault, userId, next);
|
|
790
|
-
return {
|
|
791
|
-
userId: file.user_id,
|
|
792
|
-
displayName: file.display_name,
|
|
793
|
-
role: file.role,
|
|
794
|
-
permissions: file.permissions,
|
|
795
|
-
deks,
|
|
796
|
-
kek: newKek,
|
|
797
|
-
salt: newSalt,
|
|
798
|
-
authenticators: newSlots,
|
|
799
|
-
...file.export_capability !== void 0 && { exportCapability: file.export_capability },
|
|
800
|
-
...file.import_capability !== void 0 && { importCapability: file.import_capability }
|
|
801
|
-
};
|
|
802
|
-
}
|
|
803
|
-
async function recoverPassphrase(store, vault, userId, input) {
|
|
804
|
-
if (!input.allowWeakPassphrase) {
|
|
805
|
-
assertStrongPassphrase(input.newPassphrase, input.passphrasePolicy);
|
|
806
|
-
}
|
|
807
|
-
switch (input.recoveryProof.profile) {
|
|
808
|
-
case "paper":
|
|
809
|
-
return recoverViaPaperCode(store, vault, userId, input);
|
|
810
|
-
case "shamir":
|
|
811
|
-
throw new RecoveryProfileNotImplementedError(
|
|
812
|
-
"shamir",
|
|
813
|
-
"https://github.com/vLannaAi/noy-db/issues/10"
|
|
814
|
-
);
|
|
815
|
-
case "multi-channel":
|
|
816
|
-
throw new RecoveryProfileNotImplementedError(
|
|
817
|
-
"multi-channel",
|
|
818
|
-
"https://github.com/vLannaAi/noy-db/issues/10"
|
|
819
|
-
);
|
|
820
|
-
case "admin-mediated":
|
|
821
|
-
throw new RecoveryProfileNotImplementedError(
|
|
822
|
-
"admin-mediated",
|
|
823
|
-
"https://github.com/vLannaAi/noy-db/issues/10"
|
|
824
|
-
);
|
|
825
|
-
default: {
|
|
826
|
-
const _exhaustive = input.recoveryProof;
|
|
827
|
-
throw new Error(`Unknown recovery profile: ${String(_exhaustive)}`);
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
async function recoverViaPaperCode(store, vault, userId, input) {
|
|
832
|
-
if (input.recoveryProof.profile !== "paper") throw new Error("unreachable");
|
|
833
|
-
const { code } = input.recoveryProof.payload;
|
|
834
|
-
const env = await store.get(vault, "_keyring", userId);
|
|
835
|
-
if (!env) {
|
|
836
|
-
throw new NoAccessError(`No keyring found for user "${userId}" in vault "${vault}".`);
|
|
837
|
-
}
|
|
838
|
-
const file = JSON.parse(env._data);
|
|
839
|
-
const entries = await loadPaperRecoveryEntries(store, vault);
|
|
840
|
-
if (entries.length === 0) {
|
|
841
|
-
throw new NoAccessError(
|
|
842
|
-
`No paper-recovery entries enrolled for vault "${vault}". Enroll via \`db.enrollRecovery({ profile: "paper", entries })\` before relying on recovery.`
|
|
843
|
-
);
|
|
844
|
-
}
|
|
845
|
-
const normalized = normalizePaperCode(code);
|
|
846
|
-
let recovered;
|
|
847
|
-
for (const entry of entries) {
|
|
848
|
-
try {
|
|
849
|
-
const deks2 = await unwrapDeksFromPaperEntry(entry, normalized);
|
|
850
|
-
recovered = { deks: deks2, entry };
|
|
851
|
-
break;
|
|
852
|
-
} catch {
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
if (!recovered) {
|
|
856
|
-
throw new InvalidKeyError(
|
|
857
|
-
"Recovery code does not match any enrolled paper entry. The code may have been previously used (single-use) or typed incorrectly."
|
|
858
|
-
);
|
|
859
|
-
}
|
|
860
|
-
const deks = recovered.deks;
|
|
861
|
-
const newSalt = generateSalt();
|
|
862
|
-
const newKek = await deriveKey(input.newPassphrase, newSalt);
|
|
863
|
-
const wrappedDeks = {};
|
|
864
|
-
for (const [coll, dek] of deks) {
|
|
865
|
-
wrappedDeks[coll] = await wrapKey(dek, newKek);
|
|
866
|
-
}
|
|
867
|
-
const next = {
|
|
868
|
-
...file,
|
|
869
|
-
_noydb_keyring: NOYDB_KEYRING_VERSION,
|
|
870
|
-
deks: wrappedDeks,
|
|
871
|
-
salt: bufferToBase64(newSalt),
|
|
872
|
-
authenticators: []
|
|
873
|
-
// tier-2 slots wrap old KEK, drop them
|
|
874
|
-
};
|
|
875
|
-
await writeKeyringFile(store, vault, userId, next);
|
|
876
|
-
await burnPaperRecoveryEntry(store, vault, recovered.entry.codeId);
|
|
877
|
-
return {
|
|
878
|
-
userId: file.user_id,
|
|
879
|
-
displayName: file.display_name,
|
|
880
|
-
role: file.role,
|
|
881
|
-
permissions: file.permissions,
|
|
882
|
-
deks,
|
|
883
|
-
kek: newKek,
|
|
884
|
-
salt: newSalt,
|
|
885
|
-
authenticators: [],
|
|
886
|
-
...file.export_capability !== void 0 && { exportCapability: file.export_capability },
|
|
887
|
-
...file.import_capability !== void 0 && { importCapability: file.import_capability }
|
|
888
|
-
};
|
|
889
|
-
}
|
|
890
|
-
function normalizePaperCode(input) {
|
|
891
|
-
return input.toUpperCase().replace(/[\s\-_]/g, "");
|
|
892
|
-
}
|
|
893
|
-
async function writeKeyringFile(store, vault, userId, file) {
|
|
894
|
-
const envelope = {
|
|
895
|
-
_noydb: 1,
|
|
896
|
-
_v: 1,
|
|
897
|
-
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
898
|
-
_iv: "",
|
|
899
|
-
_data: JSON.stringify(file)
|
|
900
|
-
};
|
|
901
|
-
await store.put(vault, "_keyring", userId, envelope);
|
|
902
|
-
}
|
|
903
|
-
|
|
904
661
|
// src/meta/user-envelope/api.ts
|
|
905
662
|
var UserApi = class {
|
|
906
663
|
constructor(adapter, vaultName, writerKeyringId, getDek, checkGate2) {
|
|
@@ -984,6 +741,41 @@ var UserApi = class {
|
|
|
984
741
|
this.fireChange(this.writerKeyringId, written);
|
|
985
742
|
return written;
|
|
986
743
|
}
|
|
744
|
+
// ─── Visibility (#122) ───────────────────────────────────────────────
|
|
745
|
+
/**
|
|
746
|
+
* Read the current user's visibility flag from
|
|
747
|
+
* `_meta/visibility/<keyringId>`. Returns `{ hidden: false }` when no
|
|
748
|
+
* document has been persisted (the default-visible case).
|
|
749
|
+
*/
|
|
750
|
+
async getMyVisibility() {
|
|
751
|
+
const persisted = await readUserVisibility(this.adapter, this.vaultName, this.writerKeyringId);
|
|
752
|
+
return persisted ?? { hidden: false };
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Update the current user's visibility in the team directory.
|
|
756
|
+
*
|
|
757
|
+
* - `hidden: true` — opt out of the default `listUsersWithEnvelopes`
|
|
758
|
+
* listing. `owner`/`admin` callers can still see the user by passing
|
|
759
|
+
* `{ includeHidden: true }`.
|
|
760
|
+
* - `hidden: false` — opt back in.
|
|
761
|
+
*
|
|
762
|
+
* Own-only by construction: the keyringId argument doesn't exist on
|
|
763
|
+
* this method, so no caller can hide or unhide another principal.
|
|
764
|
+
*
|
|
765
|
+
* Honest caveat: this is a UX flag, not a privacy guarantee. The
|
|
766
|
+
* envelope ciphertext at `_users/<keyringId>` and the keyring file at
|
|
767
|
+
* `_keyring/<userId>` are both still observable to anyone with direct
|
|
768
|
+
* store read access. See `docs/subsystems/user-envelope.md` →
|
|
769
|
+
* "Directory visibility".
|
|
770
|
+
*/
|
|
771
|
+
async setMyVisibility(visibility) {
|
|
772
|
+
await persistUserVisibility(
|
|
773
|
+
this.adapter,
|
|
774
|
+
this.vaultName,
|
|
775
|
+
this.writerKeyringId,
|
|
776
|
+
{ hidden: visibility.hidden }
|
|
777
|
+
);
|
|
778
|
+
}
|
|
987
779
|
// ─── Read-anyone ─────────────────────────────────────────────────────
|
|
988
780
|
/**
|
|
989
781
|
* Read another principal's envelope by their keyringId. Returns null
|
|
@@ -1176,7 +968,7 @@ async function describeAuthConfig(store, vault) {
|
|
|
1176
968
|
lines.push(` Phrase format: ${policy.passphrase?.minWords ?? 6}+ words, lowercase letters, \u2265${policy.passphrase?.minWordLength ?? 3} chars/word`);
|
|
1177
969
|
lines.push(" Strength validator: enforced (override available for tests only)");
|
|
1178
970
|
lines.push("");
|
|
1179
|
-
lines.push("Tier 2 \u2014 Authenticate (
|
|
971
|
+
lines.push("Tier 2 \u2014 Authenticate (routine login)");
|
|
1180
972
|
lines.push(" Allowed methods: WebAuthn (passkey), OIDC, Password");
|
|
1181
973
|
lines.push(" Slots per user: unlimited");
|
|
1182
974
|
lines.push("");
|
|
@@ -1288,68 +1080,131 @@ function sanitizeId(s) {
|
|
|
1288
1080
|
return s.replace(/[^a-zA-Z0-9]/g, "_");
|
|
1289
1081
|
}
|
|
1290
1082
|
|
|
1291
|
-
// src/team/
|
|
1292
|
-
var
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
)
|
|
1083
|
+
// src/team/managed-passphrase.ts
|
|
1084
|
+
var MemorySealingKeyProvider = class {
|
|
1085
|
+
id;
|
|
1086
|
+
fingerprint;
|
|
1087
|
+
keyBytes;
|
|
1088
|
+
constructor(opts) {
|
|
1089
|
+
this.id = opts.id;
|
|
1090
|
+
const encoded = new TextEncoder().encode(opts.id);
|
|
1091
|
+
let h = 0;
|
|
1092
|
+
for (let i = 0; i < encoded.length; i++) {
|
|
1093
|
+
h = h * 31 + encoded[i] >>> 0;
|
|
1094
|
+
}
|
|
1095
|
+
this.fingerprint = new Uint8Array([
|
|
1096
|
+
h >>> 24 & 255,
|
|
1097
|
+
h >>> 16 & 255,
|
|
1098
|
+
h >>> 8 & 255,
|
|
1099
|
+
h & 255
|
|
1100
|
+
]);
|
|
1101
|
+
this.keyBytes = new Uint8Array(16);
|
|
1102
|
+
for (let i = 0; i < 16; i++) {
|
|
1103
|
+
this.keyBytes[i] = this.fingerprint[i % 4] ^ i * 17;
|
|
1104
|
+
}
|
|
1311
1105
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
)
|
|
1106
|
+
async seal(passphrase) {
|
|
1107
|
+
const out = new Uint8Array(4 + passphrase.length);
|
|
1108
|
+
out.set(this.fingerprint, 0);
|
|
1109
|
+
for (let i = 0; i < passphrase.length; i++) {
|
|
1110
|
+
out[4 + i] = passphrase[i] ^ this.keyBytes[i % 16];
|
|
1111
|
+
}
|
|
1112
|
+
return out;
|
|
1316
1113
|
}
|
|
1317
|
-
|
|
1318
|
-
if (
|
|
1319
|
-
throw new
|
|
1114
|
+
async unseal(sealed) {
|
|
1115
|
+
if (sealed.length < 4) {
|
|
1116
|
+
throw new Error("MemorySealingKeyProvider: sealed input too short");
|
|
1320
1117
|
}
|
|
1118
|
+
for (let i = 0; i < 4; i++) {
|
|
1119
|
+
if (sealed[i] !== this.fingerprint[i]) {
|
|
1120
|
+
throw new Error(
|
|
1121
|
+
`MemorySealingKeyProvider("${this.id}"): provider-id mismatch on unseal (sealed bytes were produced by a different provider)`
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
const body = sealed.subarray(4);
|
|
1126
|
+
const out = new Uint8Array(body.length);
|
|
1127
|
+
for (let i = 0; i < body.length; i++) {
|
|
1128
|
+
out[i] = body[i] ^ this.keyBytes[i % 16];
|
|
1129
|
+
}
|
|
1130
|
+
return out;
|
|
1321
1131
|
}
|
|
1322
|
-
|
|
1323
|
-
|
|
1132
|
+
};
|
|
1133
|
+
var SEALED_PASSPHRASE_RECORD_ID = "sealed-passphrase";
|
|
1134
|
+
function bytesToBase64(bytes) {
|
|
1135
|
+
let binary = "";
|
|
1136
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
1137
|
+
return btoa(binary);
|
|
1138
|
+
}
|
|
1139
|
+
function base64ToBytes(b64) {
|
|
1140
|
+
const binary = atob(b64);
|
|
1141
|
+
const out = new Uint8Array(binary.length);
|
|
1142
|
+
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
|
|
1143
|
+
return out;
|
|
1144
|
+
}
|
|
1145
|
+
function parseSealedEnvelope(raw) {
|
|
1146
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
1147
|
+
const r = raw;
|
|
1148
|
+
if (r._noydb_sealed !== 1) return void 0;
|
|
1149
|
+
if (r.v === 1 && typeof r.pid === "string" && typeof r.payload === "string") {
|
|
1150
|
+
return {
|
|
1151
|
+
_noydb_sealed: 1,
|
|
1152
|
+
providerId: r.pid,
|
|
1153
|
+
sealed: base64ToBytes(r.payload)
|
|
1154
|
+
};
|
|
1324
1155
|
}
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
throw new PrivilegeEscalationError(coll);
|
|
1332
|
-
}
|
|
1333
|
-
wrappedDeks[coll] = await wrapKey(callerDek, newKek);
|
|
1156
|
+
if (typeof r.providerId === "string" && typeof r.sealed === "string") {
|
|
1157
|
+
return {
|
|
1158
|
+
_noydb_sealed: 1,
|
|
1159
|
+
providerId: r.providerId,
|
|
1160
|
+
sealed: base64ToBytes(r.sealed)
|
|
1161
|
+
};
|
|
1334
1162
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
authenticators: []
|
|
1163
|
+
return void 0;
|
|
1164
|
+
}
|
|
1165
|
+
async function saveSealedPassphrase(store, vault, payload) {
|
|
1166
|
+
const persisted = {
|
|
1167
|
+
v: 1,
|
|
1168
|
+
_noydb_sealed: 1,
|
|
1169
|
+
pid: payload.providerId,
|
|
1170
|
+
payload: bytesToBase64(payload.sealed)
|
|
1344
1171
|
};
|
|
1345
|
-
const
|
|
1346
|
-
|
|
1347
|
-
|
|
1172
|
+
const prior = await store.get(vault, "_meta", SEALED_PASSPHRASE_RECORD_ID);
|
|
1173
|
+
const env = {
|
|
1174
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
1175
|
+
_v: (prior?._v ?? 0) + 1,
|
|
1348
1176
|
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1177
|
+
// AES-GCM bypassed — the sealing layer is the security boundary.
|
|
1349
1178
|
_iv: "",
|
|
1350
|
-
_data: JSON.stringify(
|
|
1179
|
+
_data: JSON.stringify(persisted)
|
|
1351
1180
|
};
|
|
1352
|
-
await store.put(vault, "
|
|
1181
|
+
await store.put(vault, "_meta", SEALED_PASSPHRASE_RECORD_ID, env);
|
|
1182
|
+
}
|
|
1183
|
+
async function loadSealedPassphrase(store, vault) {
|
|
1184
|
+
const envelope = await store.get(vault, "_meta", SEALED_PASSPHRASE_RECORD_ID);
|
|
1185
|
+
if (!envelope) return void 0;
|
|
1186
|
+
try {
|
|
1187
|
+
return parseSealedEnvelope(JSON.parse(envelope._data));
|
|
1188
|
+
} catch {
|
|
1189
|
+
return void 0;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
async function resolveManagedSecret(store, vault, provider) {
|
|
1193
|
+
const existing = await loadSealedPassphrase(store, vault);
|
|
1194
|
+
if (existing) {
|
|
1195
|
+
if (existing.providerId !== provider.id) {
|
|
1196
|
+
throw new Error(
|
|
1197
|
+
`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.`
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
const plaintext = await provider.unseal(existing.sealed);
|
|
1201
|
+
return bytesToBase64(plaintext);
|
|
1202
|
+
}
|
|
1203
|
+
const random = new Uint8Array(32);
|
|
1204
|
+
globalThis.crypto.getRandomValues(random);
|
|
1205
|
+
const sealed = await provider.seal(random);
|
|
1206
|
+
await saveSealedPassphrase(store, vault, { providerId: provider.id, sealed });
|
|
1207
|
+
return bytesToBase64(random);
|
|
1353
1208
|
}
|
|
1354
1209
|
|
|
1355
1210
|
// src/crdt/strategy.ts
|
|
@@ -1625,6 +1480,79 @@ var NO_BLOBS = {
|
|
|
1625
1480
|
}
|
|
1626
1481
|
};
|
|
1627
1482
|
|
|
1483
|
+
// src/derivations/stale.ts
|
|
1484
|
+
var _staleByRegistry = /* @__PURE__ */ new WeakMap();
|
|
1485
|
+
var keyFor = (source, sourceId) => `${source}/${sourceId}`;
|
|
1486
|
+
async function markStale(registry, strategy, sourceId) {
|
|
1487
|
+
let map = _staleByRegistry.get(registry);
|
|
1488
|
+
if (!map) {
|
|
1489
|
+
map = /* @__PURE__ */ new Map();
|
|
1490
|
+
_staleByRegistry.set(registry, map);
|
|
1491
|
+
}
|
|
1492
|
+
const k = keyFor(strategy.source, sourceId);
|
|
1493
|
+
let set = map.get(k);
|
|
1494
|
+
if (!set) {
|
|
1495
|
+
set = /* @__PURE__ */ new Set();
|
|
1496
|
+
map.set(k, set);
|
|
1497
|
+
}
|
|
1498
|
+
set.add(strategy);
|
|
1499
|
+
}
|
|
1500
|
+
async function resolveStaleOnRead(accessor, outputCollection, id) {
|
|
1501
|
+
const registry = accessor.registry();
|
|
1502
|
+
const producers = registry.strategiesProducingOutput(outputCollection);
|
|
1503
|
+
if (producers.length === 0) return;
|
|
1504
|
+
const map = _staleByRegistry.get(registry);
|
|
1505
|
+
if (!map) return;
|
|
1506
|
+
let DerivationExecutor = null;
|
|
1507
|
+
for (const { spec, strategyHash } of producers) {
|
|
1508
|
+
const k = keyFor(spec.source, id);
|
|
1509
|
+
const pending = map.get(k);
|
|
1510
|
+
if (!pending || !pending.has(spec)) continue;
|
|
1511
|
+
const sourceColl = accessor.getCollection(spec.source);
|
|
1512
|
+
const source = await sourceColl.get(id);
|
|
1513
|
+
if (!source) {
|
|
1514
|
+
pending.delete(spec);
|
|
1515
|
+
continue;
|
|
1516
|
+
}
|
|
1517
|
+
const sourceWithId = { ...source, id };
|
|
1518
|
+
if (DerivationExecutor === null) {
|
|
1519
|
+
({ DerivationExecutor } = await import("./executor-X4SQ3ZLC.js"));
|
|
1520
|
+
}
|
|
1521
|
+
const ctx = { vault: accessor.getReadOnlyFacade() };
|
|
1522
|
+
const result = await DerivationExecutor.run(spec, sourceWithId, 0, strategyHash, ctx);
|
|
1523
|
+
for (const key of Object.keys(spec.outputs)) {
|
|
1524
|
+
const out = result.outputs[key];
|
|
1525
|
+
if (!out) continue;
|
|
1526
|
+
if (out.kind === "failed") {
|
|
1527
|
+
const err = out.error;
|
|
1528
|
+
if (spec.strict) {
|
|
1529
|
+
throw err;
|
|
1530
|
+
}
|
|
1531
|
+
console.warn(
|
|
1532
|
+
`[derivation] lazy output "${key}" for source "${spec.source}" id="${id}" failed:`,
|
|
1533
|
+
err
|
|
1534
|
+
);
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
if (out.kind === "array") {
|
|
1538
|
+
console.warn(
|
|
1539
|
+
`[derivation] unexpected array-shape output "${key}" in lazy resolve path; array-shape derivations require lifecycle: "eager" (#200 slice 1).`
|
|
1540
|
+
);
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
const outSpec = spec.outputs[key];
|
|
1544
|
+
if (!outSpec) continue;
|
|
1545
|
+
const outputColl = accessor.getCollection(outSpec.collection);
|
|
1546
|
+
if (out.skipped === true) {
|
|
1547
|
+
await outputColl._internalDelete(id, accessor.getActiveTxContext());
|
|
1548
|
+
continue;
|
|
1549
|
+
}
|
|
1550
|
+
await outputColl.put(id, out.value);
|
|
1551
|
+
}
|
|
1552
|
+
pending.delete(spec);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1628
1556
|
// src/collection.ts
|
|
1629
1557
|
var fallbackWarned = /* @__PURE__ */ new Set();
|
|
1630
1558
|
function warnOnceFallback(adapterName) {
|
|
@@ -1632,7 +1560,7 @@ function warnOnceFallback(adapterName) {
|
|
|
1632
1560
|
fallbackWarned.add(adapterName);
|
|
1633
1561
|
if (typeof process !== "undefined" && process.env["NODE_ENV"] === "test") return;
|
|
1634
1562
|
console.warn(
|
|
1635
|
-
`[noy-db]
|
|
1563
|
+
`[noy-db] Store "${adapterName}" does not implement listPage(); Collection.scan()/listPage() are using a synthetic fallback (slower). Add a listPage method to opt into the streaming fast path.`
|
|
1636
1564
|
);
|
|
1637
1565
|
}
|
|
1638
1566
|
var Collection = class {
|
|
@@ -1842,6 +1770,34 @@ var Collection = class {
|
|
|
1842
1770
|
* adapter on first use.
|
|
1843
1771
|
*/
|
|
1844
1772
|
periodGuard;
|
|
1773
|
+
/**
|
|
1774
|
+
* Optional back-reference to the owning vault's guard registry + a
|
|
1775
|
+
* read-only vault facade. When present, `Collection.put` and
|
|
1776
|
+
* `Collection.delete` consult the registry for guards declared
|
|
1777
|
+
* against this collection and run their `check` + `frozenFields`
|
|
1778
|
+
* before the adapter write. Absent in unit tests that construct
|
|
1779
|
+
* a Collection directly; production code always sets it via
|
|
1780
|
+
* `Vault.collection()`.
|
|
1781
|
+
*
|
|
1782
|
+
* Typed structurally rather than as `Vault` to avoid a circular
|
|
1783
|
+
* import (mirrors the `refEnforcer` / `joinResolver` pattern).
|
|
1784
|
+
*/
|
|
1785
|
+
guardSource;
|
|
1786
|
+
/**
|
|
1787
|
+
* Vault-internal hook for derivation dispatch. When set,
|
|
1788
|
+
* `Collection.put` consults the registry after the source-write
|
|
1789
|
+
* commits and writes derived outputs through `getCollection(name).put`.
|
|
1790
|
+
* Same structural-interface pattern as `guardSource` to avoid a
|
|
1791
|
+
* circular Vault import.
|
|
1792
|
+
*/
|
|
1793
|
+
derivationSource;
|
|
1794
|
+
/**
|
|
1795
|
+
* Vault-internal hook for materialized-view dispatch (#143/#150).
|
|
1796
|
+
* Parallel to `derivationSource` — when set, `Collection.put` fires
|
|
1797
|
+
* `MaterializedViewRegistry.onSourceWrite` after the source-write
|
|
1798
|
+
* commits + after `dispatchDerivations` has run.
|
|
1799
|
+
*/
|
|
1800
|
+
materializedViewSource;
|
|
1845
1801
|
/**
|
|
1846
1802
|
* Optional back-reference to the owning compartment's ref
|
|
1847
1803
|
* enforcer. When present, `Collection.put` calls
|
|
@@ -1912,6 +1868,9 @@ var Collection = class {
|
|
|
1912
1868
|
this.syncAdapter = opts.syncAdapter;
|
|
1913
1869
|
this.onAccess = opts.onAccess;
|
|
1914
1870
|
this.periodGuard = opts.periodGuard;
|
|
1871
|
+
this.guardSource = opts.guardSource;
|
|
1872
|
+
this.derivationSource = opts.derivationSource;
|
|
1873
|
+
this.materializedViewSource = opts.materializedViewSource;
|
|
1915
1874
|
this.tiers = opts.tiers && opts.tiers.length > 0 ? new Set(opts.tiers) : null;
|
|
1916
1875
|
this.tierMode = opts.tierMode ?? "invisibility";
|
|
1917
1876
|
this.onCrossTierAccess = opts.onCrossTierAccess;
|
|
@@ -2036,6 +1995,16 @@ var Collection = class {
|
|
|
2036
1995
|
* `null` if not found.
|
|
2037
1996
|
*/
|
|
2038
1997
|
async get(id, locale) {
|
|
1998
|
+
if (this.derivationSource !== void 0) {
|
|
1999
|
+
const registry = this.derivationSource.registry();
|
|
2000
|
+
if (registry.strategiesProducingOutput(this.name).length > 0) {
|
|
2001
|
+
await resolveStaleOnRead(this.derivationSource, this.name, id);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
if (this.materializedViewSource !== void 0) {
|
|
2005
|
+
const { resolveStaleMVOnRead } = await import("./stale-HSC5YO2O.js");
|
|
2006
|
+
await resolveStaleMVOnRead(this.materializedViewSource, this.name);
|
|
2007
|
+
}
|
|
2039
2008
|
let record;
|
|
2040
2009
|
if (this.lazy && this.lru) {
|
|
2041
2010
|
const cached = this.lru.get(id);
|
|
@@ -2099,11 +2068,53 @@ var Collection = class {
|
|
|
2099
2068
|
if (opts?.pollIntervalMs !== void 0) presenceOpts.pollIntervalMs = opts.pollIntervalMs;
|
|
2100
2069
|
return this.syncStrategy.buildPresence(presenceOpts);
|
|
2101
2070
|
}
|
|
2102
|
-
/**
|
|
2103
|
-
|
|
2071
|
+
/**
|
|
2072
|
+
* Create or update a record.
|
|
2073
|
+
*
|
|
2074
|
+
* @param id Record identifier.
|
|
2075
|
+
* @param record The record body (validated by the collection's schema
|
|
2076
|
+
* if one was attached at `vault.collection(...)` time).
|
|
2077
|
+
* @param options Optional metadata for audit + import workflows.
|
|
2078
|
+
* `reason` is stamped onto the resulting ledger entry
|
|
2079
|
+
* (see #1) so audit consumers can filter via
|
|
2080
|
+
* `entries.filter(e => e.reason?.startsWith('import:'))`.
|
|
2081
|
+
*/
|
|
2082
|
+
async put(id, record, options) {
|
|
2104
2083
|
if (!hasWritePermission(this.keyring, this.name)) {
|
|
2105
2084
|
throw new ReadOnlyError();
|
|
2106
2085
|
}
|
|
2086
|
+
if (this.guardSource) {
|
|
2087
|
+
const registry = this.guardSource.registry();
|
|
2088
|
+
const guards = registry.guardsFor(this.name);
|
|
2089
|
+
if (guards.length > 0) {
|
|
2090
|
+
const existingEnv = await this.adapter.get(this.vault, this.name, id);
|
|
2091
|
+
let existingRecord = null;
|
|
2092
|
+
if (existingEnv) {
|
|
2093
|
+
try {
|
|
2094
|
+
existingRecord = await this.decryptRecord(existingEnv, { skipValidation: true });
|
|
2095
|
+
} catch {
|
|
2096
|
+
existingRecord = null;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
const incomingRecord = record;
|
|
2100
|
+
const ctx = {
|
|
2101
|
+
existing: existingRecord,
|
|
2102
|
+
vault: this.guardSource.readOnlyVault(),
|
|
2103
|
+
userId: this.keyring.userId,
|
|
2104
|
+
role: this.keyring.role
|
|
2105
|
+
};
|
|
2106
|
+
if (registry.isAmendmentActive()) {
|
|
2107
|
+
const vBefore = existingEnv?._v ?? 0;
|
|
2108
|
+
registry.collectChange(this.name, id, existingRecord, incomingRecord, vBefore, vBefore + 1);
|
|
2109
|
+
} else {
|
|
2110
|
+
await registry.runChecks(this.name, incomingRecord, ctx);
|
|
2111
|
+
const { GuardExecutor } = await import("./executor-CEWX2FQI.js");
|
|
2112
|
+
for (const g of guards) {
|
|
2113
|
+
await GuardExecutor.checkFrozenFields(g, id, existingRecord, incomingRecord);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2107
2118
|
if (this.periodGuard !== void 0) {
|
|
2108
2119
|
const existingEnv = await this.adapter.get(this.vault, this.name, id);
|
|
2109
2120
|
let priorRecord = null;
|
|
@@ -2212,6 +2223,7 @@ var Collection = class {
|
|
|
2212
2223
|
payloadHash: await this.historyStrategy.envelopePayloadHash(envelope2)
|
|
2213
2224
|
};
|
|
2214
2225
|
if (existingResolved) appendInput.delta = this.historyStrategy.computePatch(resolvedRecord, existingResolved.record);
|
|
2226
|
+
if (options?.reason !== void 0) appendInput.reason = options.reason;
|
|
2215
2227
|
await this.ledger.append(appendInput);
|
|
2216
2228
|
}
|
|
2217
2229
|
if (this.lazy && this.lru) {
|
|
@@ -2229,6 +2241,8 @@ var Collection = class {
|
|
|
2229
2241
|
await this.onDirty?.(this.name, id, "put", version2);
|
|
2230
2242
|
this.emitter.emit("change", { vault: this.vault, collection: this.name, id, action: "put" });
|
|
2231
2243
|
await this.onAccess?.("put", id);
|
|
2244
|
+
await this.dispatchDerivations(id, record, version2);
|
|
2245
|
+
await this.dispatchMaterializedViews(id, record);
|
|
2232
2246
|
return;
|
|
2233
2247
|
}
|
|
2234
2248
|
let existing;
|
|
@@ -2275,6 +2289,7 @@ var Collection = class {
|
|
|
2275
2289
|
if (existing) {
|
|
2276
2290
|
appendInput.delta = this.historyStrategy.computePatch(record, existing.record);
|
|
2277
2291
|
}
|
|
2292
|
+
if (options?.reason !== void 0) appendInput.reason = options.reason;
|
|
2278
2293
|
await this.ledger.append(appendInput);
|
|
2279
2294
|
}
|
|
2280
2295
|
if (this.lazy && this.lru) {
|
|
@@ -2292,13 +2307,256 @@ var Collection = class {
|
|
|
2292
2307
|
action: "put"
|
|
2293
2308
|
});
|
|
2294
2309
|
await this.onAccess?.("put", id);
|
|
2310
|
+
await this.dispatchDerivations(id, record, version);
|
|
2311
|
+
await this.dispatchMaterializedViews(id, record);
|
|
2312
|
+
}
|
|
2313
|
+
/**
|
|
2314
|
+
* Fire registered MV strategies whose dependency set includes this
|
|
2315
|
+
* collection. Eager-mode MVs re-materialize inline via
|
|
2316
|
+
* `MaterializedViewExecutor.refresh`; lazy / manual modes are
|
|
2317
|
+
* no-ops in the foundation (subtask #150) — wired in #151.
|
|
2318
|
+
*
|
|
2319
|
+
* Skips entirely when the record being written is itself an
|
|
2320
|
+
* MV-emitted row (carries `_materializedFrom`) — defensive guard
|
|
2321
|
+
* against missed cycle detection.
|
|
2322
|
+
*
|
|
2323
|
+
* @internal
|
|
2324
|
+
*/
|
|
2325
|
+
async dispatchMaterializedViews(id, record) {
|
|
2326
|
+
void id;
|
|
2327
|
+
if (this.materializedViewSource === void 0) return;
|
|
2328
|
+
const incoming = record;
|
|
2329
|
+
if (incoming && typeof incoming === "object" && "_materializedFrom" in incoming) return;
|
|
2330
|
+
const registry = this.materializedViewSource.registry();
|
|
2331
|
+
const mvs = registry.mvsForSource(this.name);
|
|
2332
|
+
if (mvs.length === 0) return;
|
|
2333
|
+
let executor = null;
|
|
2334
|
+
let staleHelpers = null;
|
|
2335
|
+
for (const reg of mvs) {
|
|
2336
|
+
const mode = reg.spec.refresh;
|
|
2337
|
+
if (mode === "eager") {
|
|
2338
|
+
if (executor === null) {
|
|
2339
|
+
;
|
|
2340
|
+
({ MaterializedViewExecutor: executor } = await import("./executor-7E3VFGW7.js"));
|
|
2341
|
+
}
|
|
2342
|
+
await executor.refresh(reg, {
|
|
2343
|
+
getCollection: (name) => this.materializedViewSource.getCollection(name),
|
|
2344
|
+
getActiveTxContext: () => this.materializedViewSource.getActiveTxContext(),
|
|
2345
|
+
getQueryContext: () => this.materializedViewSource.getQueryContext()
|
|
2346
|
+
});
|
|
2347
|
+
} else if (mode === "lazy") {
|
|
2348
|
+
if (staleHelpers === null) {
|
|
2349
|
+
staleHelpers = await import("./stale-HSC5YO2O.js");
|
|
2350
|
+
}
|
|
2351
|
+
staleHelpers.markMVStale(registry, reg.spec.name);
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
/**
|
|
2356
|
+
* Fire registered derivation strategies for this source collection.
|
|
2357
|
+
* Eager mode runs `derive` inline and writes each output via the
|
|
2358
|
+
* sibling `Collection.put`; lazy mode marks dependent outputs stale
|
|
2359
|
+
* (D11 stub today). Errors in non-strict mode are logged and
|
|
2360
|
+
* skipped; strict mode propagates the first failing output's error.
|
|
2361
|
+
*
|
|
2362
|
+
* Skips entirely when the record being written is itself a derived
|
|
2363
|
+
* output (carries `_derivedFrom`) — defensive guard against missed
|
|
2364
|
+
* cycle detection.
|
|
2365
|
+
*/
|
|
2366
|
+
async dispatchDerivations(id, record, version) {
|
|
2367
|
+
if (this.derivationSource === void 0) return;
|
|
2368
|
+
const incoming = record;
|
|
2369
|
+
if (incoming && typeof incoming === "object" && "_derivedFrom" in incoming) return;
|
|
2370
|
+
const registry = this.derivationSource.registry();
|
|
2371
|
+
const strategies = registry.strategiesForSource(this.name);
|
|
2372
|
+
if (strategies.length === 0) return;
|
|
2373
|
+
let DerivationExecutor = null;
|
|
2374
|
+
for (const { spec, strategyHash } of strategies) {
|
|
2375
|
+
const mode = typeof spec.lifecycle === "string" ? spec.lifecycle : spec.lifecycle.mode;
|
|
2376
|
+
if (mode === "eager") {
|
|
2377
|
+
if (DerivationExecutor === null) {
|
|
2378
|
+
({ DerivationExecutor } = await import("./executor-X4SQ3ZLC.js"));
|
|
2379
|
+
}
|
|
2380
|
+
const sourceWithId = { ...incoming, id };
|
|
2381
|
+
const ctx = { vault: this.derivationSource.getReadOnlyFacade() };
|
|
2382
|
+
const result = await DerivationExecutor.run(spec, sourceWithId, version, strategyHash, ctx);
|
|
2383
|
+
for (const key of Object.keys(spec.outputs)) {
|
|
2384
|
+
const out = result.outputs[key];
|
|
2385
|
+
if (!out) continue;
|
|
2386
|
+
if (out.kind === "failed") {
|
|
2387
|
+
const err = out.error;
|
|
2388
|
+
if (spec.strict) throw err;
|
|
2389
|
+
console.warn(`[derivation] output "${key}" for source "${spec.source}" id="${id}" failed:`, err);
|
|
2390
|
+
continue;
|
|
2391
|
+
}
|
|
2392
|
+
const outSpec = spec.outputs[key];
|
|
2393
|
+
if (!outSpec) continue;
|
|
2394
|
+
const outputCollection = this.derivationSource.getCollection(outSpec.collection);
|
|
2395
|
+
const txCtx = this.derivationSource.getActiveTxContext();
|
|
2396
|
+
if (out.kind === "array") {
|
|
2397
|
+
const { loadFanoutSidecar, saveFanoutSidecar } = await import("./fanout-sidecar-VJ52RIEY.js");
|
|
2398
|
+
const prior = await loadFanoutSidecar(
|
|
2399
|
+
this.adapter,
|
|
2400
|
+
this.vault,
|
|
2401
|
+
spec.source,
|
|
2402
|
+
id,
|
|
2403
|
+
key
|
|
2404
|
+
);
|
|
2405
|
+
const prevKeys = new Set(prior?.keys ?? []);
|
|
2406
|
+
const newKeysList = out.entries.map((e) => e.key);
|
|
2407
|
+
const newKeysSet = new Set(newKeysList);
|
|
2408
|
+
for (const k of prevKeys) {
|
|
2409
|
+
if (newKeysSet.has(k)) continue;
|
|
2410
|
+
await outputCollection._internalDelete(k, txCtx);
|
|
2411
|
+
}
|
|
2412
|
+
for (const entry of out.entries) {
|
|
2413
|
+
if (txCtx !== null) {
|
|
2414
|
+
const priorEnvelope = await this.adapter.get(this.vault, outSpec.collection, entry.key);
|
|
2415
|
+
txCtx._executed.push({
|
|
2416
|
+
op: {
|
|
2417
|
+
type: "put",
|
|
2418
|
+
vaultName: this.vault,
|
|
2419
|
+
collectionName: outSpec.collection,
|
|
2420
|
+
id: entry.key
|
|
2421
|
+
},
|
|
2422
|
+
priorEnvelope
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
await outputCollection.put(entry.key, entry.value);
|
|
2426
|
+
}
|
|
2427
|
+
await saveFanoutSidecar(this.adapter, this.vault, {
|
|
2428
|
+
source: spec.source,
|
|
2429
|
+
sourceId: id,
|
|
2430
|
+
outputKey: key,
|
|
2431
|
+
outputCollection: outSpec.collection,
|
|
2432
|
+
keys: newKeysList
|
|
2433
|
+
});
|
|
2434
|
+
continue;
|
|
2435
|
+
}
|
|
2436
|
+
if (out.skipped === true) {
|
|
2437
|
+
await outputCollection._internalDelete(id, txCtx);
|
|
2438
|
+
continue;
|
|
2439
|
+
}
|
|
2440
|
+
if (txCtx !== null) {
|
|
2441
|
+
const prior = await this.adapter.get(this.vault, outSpec.collection, id);
|
|
2442
|
+
txCtx._executed.push({
|
|
2443
|
+
op: {
|
|
2444
|
+
type: "put",
|
|
2445
|
+
vaultName: this.vault,
|
|
2446
|
+
collectionName: outSpec.collection,
|
|
2447
|
+
id
|
|
2448
|
+
},
|
|
2449
|
+
priorEnvelope: prior
|
|
2450
|
+
});
|
|
2451
|
+
}
|
|
2452
|
+
await outputCollection.put(id, out.value);
|
|
2453
|
+
}
|
|
2454
|
+
} else {
|
|
2455
|
+
await markStale(registry, spec, id);
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2295
2458
|
}
|
|
2296
2459
|
/** Delete a record by ID. */
|
|
2297
2460
|
async delete(id) {
|
|
2461
|
+
await this._doDelete(id, false);
|
|
2462
|
+
}
|
|
2463
|
+
/**
|
|
2464
|
+
* @internal — system-internal delete that bypasses user-facing
|
|
2465
|
+
* delete hooks (`onDelete`, accounting-period guard, FK ref
|
|
2466
|
+
* enforcer). Used by derivation tombstones (#144) and MV refresh
|
|
2467
|
+
* (Dim 14 v2) — system housekeeping shouldn't trip user invariants
|
|
2468
|
+
* registered against the output collection. The ledger entry and
|
|
2469
|
+
* history snapshot still fire so backup integrity and time-travel
|
|
2470
|
+
* reconstruction stay consistent.
|
|
2471
|
+
*
|
|
2472
|
+
* Returns silently for delete-of-absent (idempotent contract — both
|
|
2473
|
+
* paths honour this: the `txCtx === null` path also reads the prior
|
|
2474
|
+
* envelope and short-circuits before the ledger/event side-effects).
|
|
2475
|
+
*
|
|
2476
|
+
* When a `txCtx` is supplied, the prior envelope is captured and
|
|
2477
|
+
* pushed onto `txCtx._executed` BEFORE the delete fires — mirrors
|
|
2478
|
+
* the #133 rollback hardening for puts. Callers outside a
|
|
2479
|
+
* multi-record transaction pass `null` and skip the tracking.
|
|
2480
|
+
*
|
|
2481
|
+
* Amendment composition: if `_internalDelete` runs while a vault's
|
|
2482
|
+
* `GuardRegistry` has an amendment window open, the `{before, after:
|
|
2483
|
+
* null}` change pair is pushed onto the amendment change-set the
|
|
2484
|
+
* same way a user-initiated delete would. The `onDelete` user-hook
|
|
2485
|
+
* is still skipped (housekeeping must not trip user invariants in
|
|
2486
|
+
* normal mode), but the amendment's invariant DOES see the change
|
|
2487
|
+
* — so a `RCT-CANCEL-001`-style invariant pairing can reject a
|
|
2488
|
+
* derivation-driven tombstone fired during an admin amendment.
|
|
2489
|
+
*
|
|
2490
|
+
* Constraint to surface to consumers: output collections of
|
|
2491
|
+
* derivations with `optional: true` outputs should not be the
|
|
2492
|
+
* targets of `strict` or `cascade` inbound foreign-key refs —
|
|
2493
|
+
* `_internalDelete` bypasses the ref enforcer by design (the
|
|
2494
|
+
* `onDelete` bypass primitive). Treat the housekeeping path as
|
|
2495
|
+
* "system can tombstone its own emissions regardless of FK shape."
|
|
2496
|
+
*
|
|
2497
|
+
* Permission handling is unchanged: the caller must still hold
|
|
2498
|
+
* write permission on the collection (derivations run under the
|
|
2499
|
+
* user's keyring).
|
|
2500
|
+
*/
|
|
2501
|
+
async _internalDelete(id, txCtx = null) {
|
|
2502
|
+
const prior = await this.adapter.get(this.vault, this.name, id);
|
|
2503
|
+
if (prior === null) return;
|
|
2504
|
+
if (txCtx !== null) {
|
|
2505
|
+
txCtx._executed.push({
|
|
2506
|
+
op: {
|
|
2507
|
+
type: "delete",
|
|
2508
|
+
vaultName: this.vault,
|
|
2509
|
+
collectionName: this.name,
|
|
2510
|
+
id
|
|
2511
|
+
},
|
|
2512
|
+
priorEnvelope: prior
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
await this._doDelete(id, true);
|
|
2516
|
+
}
|
|
2517
|
+
async _doDelete(id, internal) {
|
|
2298
2518
|
if (!hasWritePermission(this.keyring, this.name)) {
|
|
2299
2519
|
throw new ReadOnlyError();
|
|
2300
2520
|
}
|
|
2301
|
-
if (this.
|
|
2521
|
+
if (this.guardSource) {
|
|
2522
|
+
const registry = this.guardSource.registry();
|
|
2523
|
+
const guards = registry.guardsFor(this.name);
|
|
2524
|
+
if (guards.length > 0) {
|
|
2525
|
+
const existingEnv = await this.adapter.get(this.vault, this.name, id);
|
|
2526
|
+
if (existingEnv) {
|
|
2527
|
+
let existingRecord = null;
|
|
2528
|
+
try {
|
|
2529
|
+
existingRecord = await this.decryptRecord(existingEnv, { skipValidation: true });
|
|
2530
|
+
} catch {
|
|
2531
|
+
existingRecord = null;
|
|
2532
|
+
}
|
|
2533
|
+
if (registry.isAmendmentActive()) {
|
|
2534
|
+
const vBefore = existingEnv._v;
|
|
2535
|
+
registry.collectChange(
|
|
2536
|
+
this.name,
|
|
2537
|
+
id,
|
|
2538
|
+
existingRecord,
|
|
2539
|
+
null,
|
|
2540
|
+
vBefore,
|
|
2541
|
+
vBefore
|
|
2542
|
+
);
|
|
2543
|
+
} else if (!internal) {
|
|
2544
|
+
const ctx = {
|
|
2545
|
+
existing: existingRecord,
|
|
2546
|
+
vault: this.guardSource.readOnlyVault(),
|
|
2547
|
+
userId: this.keyring.userId,
|
|
2548
|
+
role: this.keyring.role
|
|
2549
|
+
};
|
|
2550
|
+
await registry.runOnDelete(
|
|
2551
|
+
this.name,
|
|
2552
|
+
existingRecord ?? {},
|
|
2553
|
+
ctx
|
|
2554
|
+
);
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
if (!internal && this.periodGuard !== void 0) {
|
|
2302
2560
|
const existingEnv = await this.adapter.get(this.vault, this.name, id);
|
|
2303
2561
|
let priorRecord = null;
|
|
2304
2562
|
if (existingEnv) {
|
|
@@ -2313,7 +2571,7 @@ var Collection = class {
|
|
|
2313
2571
|
null
|
|
2314
2572
|
);
|
|
2315
2573
|
}
|
|
2316
|
-
if (this.refEnforcer !== void 0) {
|
|
2574
|
+
if (!internal && this.refEnforcer !== void 0) {
|
|
2317
2575
|
await this.refEnforcer.enforceRefsOnDelete(this.name, id);
|
|
2318
2576
|
}
|
|
2319
2577
|
let existing;
|
|
@@ -2365,6 +2623,87 @@ var Collection = class {
|
|
|
2365
2623
|
action: "delete"
|
|
2366
2624
|
});
|
|
2367
2625
|
await this.onAccess?.("delete", id);
|
|
2626
|
+
if (!internal) {
|
|
2627
|
+
await this.dispatchMaterializedViewsOnDelete(id);
|
|
2628
|
+
await this.dispatchArrayDerivationsOnDelete(id);
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
/**
|
|
2632
|
+
* Cascade deletes of array-shape derived rows when a source row is
|
|
2633
|
+
* deleted (#200). Reads each registered strategy's fanout sidecar
|
|
2634
|
+
* for this source id, deletes every listed derived row, then
|
|
2635
|
+
* deletes the sidecar itself.
|
|
2636
|
+
*
|
|
2637
|
+
* Record-shape derivations are skipped — see _doDelete's comment
|
|
2638
|
+
* for why the asymmetry is correct.
|
|
2639
|
+
*
|
|
2640
|
+
* @internal
|
|
2641
|
+
*/
|
|
2642
|
+
async dispatchArrayDerivationsOnDelete(id) {
|
|
2643
|
+
if (this.derivationSource === void 0) return;
|
|
2644
|
+
const registry = this.derivationSource.registry();
|
|
2645
|
+
const strategies = registry.strategiesForSource(this.name);
|
|
2646
|
+
if (strategies.length === 0) return;
|
|
2647
|
+
let helpers = null;
|
|
2648
|
+
const txCtx = this.derivationSource.getActiveTxContext();
|
|
2649
|
+
for (const { spec } of strategies) {
|
|
2650
|
+
for (const [outputKey, outSpec] of Object.entries(spec.outputs)) {
|
|
2651
|
+
if (outSpec.shape !== "array") continue;
|
|
2652
|
+
if (helpers === null) {
|
|
2653
|
+
helpers = await import("./fanout-sidecar-VJ52RIEY.js");
|
|
2654
|
+
}
|
|
2655
|
+
const sidecar = await helpers.loadFanoutSidecar(
|
|
2656
|
+
this.adapter,
|
|
2657
|
+
this.vault,
|
|
2658
|
+
spec.source,
|
|
2659
|
+
id,
|
|
2660
|
+
outputKey
|
|
2661
|
+
);
|
|
2662
|
+
if (!sidecar) continue;
|
|
2663
|
+
const outputCollection = this.derivationSource.getCollection(outSpec.collection);
|
|
2664
|
+
for (const derivedId of sidecar.keys) {
|
|
2665
|
+
await outputCollection._internalDelete(derivedId, txCtx);
|
|
2666
|
+
}
|
|
2667
|
+
await helpers.deleteFanoutSidecar(this.adapter, this.vault, spec.source, id, outputKey);
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
/**
|
|
2672
|
+
* Mirror of {@link dispatchMaterializedViews} for the delete path
|
|
2673
|
+
* (#181). No record content is available (it's gone), so the
|
|
2674
|
+
* `_materializedFrom` skip used by the put-side dispatch doesn't
|
|
2675
|
+
* apply here — instead, the recursion guard is the `internal` gate
|
|
2676
|
+
* at the `_doDelete` call site above.
|
|
2677
|
+
*
|
|
2678
|
+
* @internal
|
|
2679
|
+
*/
|
|
2680
|
+
async dispatchMaterializedViewsOnDelete(id) {
|
|
2681
|
+
void id;
|
|
2682
|
+
if (this.materializedViewSource === void 0) return;
|
|
2683
|
+
const registry = this.materializedViewSource.registry();
|
|
2684
|
+
const mvs = registry.mvsForSource(this.name);
|
|
2685
|
+
if (mvs.length === 0) return;
|
|
2686
|
+
let executor = null;
|
|
2687
|
+
let staleHelpers = null;
|
|
2688
|
+
for (const reg of mvs) {
|
|
2689
|
+
const mode = reg.spec.refresh;
|
|
2690
|
+
if (mode === "eager") {
|
|
2691
|
+
if (executor === null) {
|
|
2692
|
+
;
|
|
2693
|
+
({ MaterializedViewExecutor: executor } = await import("./executor-7E3VFGW7.js"));
|
|
2694
|
+
}
|
|
2695
|
+
await executor.refresh(reg, {
|
|
2696
|
+
getCollection: (name) => this.materializedViewSource.getCollection(name),
|
|
2697
|
+
getActiveTxContext: () => this.materializedViewSource.getActiveTxContext(),
|
|
2698
|
+
getQueryContext: () => this.materializedViewSource.getQueryContext()
|
|
2699
|
+
});
|
|
2700
|
+
} else if (mode === "lazy") {
|
|
2701
|
+
if (staleHelpers === null) {
|
|
2702
|
+
staleHelpers = await import("./stale-HSC5YO2O.js");
|
|
2703
|
+
}
|
|
2704
|
+
staleHelpers.markMVStale(registry, reg.spec.name);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2368
2707
|
}
|
|
2369
2708
|
/**
|
|
2370
2709
|
* List all records in the collection.
|
|
@@ -2382,6 +2721,10 @@ var Collection = class {
|
|
|
2382
2721
|
`Collection "${this.name}": list() is not available in lazy mode (prefetch: false). Use collection.scan({ pageSize }) to iterate over the full collection.`
|
|
2383
2722
|
);
|
|
2384
2723
|
}
|
|
2724
|
+
if (this.materializedViewSource !== void 0) {
|
|
2725
|
+
const { resolveStaleMVOnRead } = await import("./stale-HSC5YO2O.js");
|
|
2726
|
+
await resolveStaleMVOnRead(this.materializedViewSource, this.name);
|
|
2727
|
+
}
|
|
2385
2728
|
await this.ensureHydrated();
|
|
2386
2729
|
const records = [...this.cache.values()].map((e) => e.record);
|
|
2387
2730
|
if (!locale) return records;
|
|
@@ -2456,22 +2799,42 @@ var Collection = class {
|
|
|
2456
2799
|
}
|
|
2457
2800
|
}
|
|
2458
2801
|
}
|
|
2459
|
-
const
|
|
2802
|
+
const txCtx = this.derivationSource?.createTxContext() ?? null;
|
|
2803
|
+
if (txCtx !== null && this.derivationSource) {
|
|
2804
|
+
this.derivationSource.setActiveTxContext(txCtx);
|
|
2805
|
+
}
|
|
2806
|
+
const localExecuted = [];
|
|
2460
2807
|
try {
|
|
2461
2808
|
for (const [id, record] of entries) {
|
|
2809
|
+
const entry = {
|
|
2810
|
+
op: { type: "put", vaultName: this.vault, collectionName: this.name, id },
|
|
2811
|
+
priorEnvelope: priors.get(id) ?? null
|
|
2812
|
+
};
|
|
2813
|
+
if (txCtx !== null) txCtx._executed.push(entry);
|
|
2814
|
+
else localExecuted.push(entry);
|
|
2462
2815
|
await this.put(id, record);
|
|
2463
|
-
executed.push({ id, prior: priors.get(id) ?? null });
|
|
2464
2816
|
}
|
|
2465
|
-
return { ok: true, success:
|
|
2817
|
+
return { ok: true, success: entries.map(([id]) => id), failures: [] };
|
|
2466
2818
|
} catch (err) {
|
|
2467
|
-
|
|
2819
|
+
const executedForRevert = txCtx !== null ? txCtx._executed : localExecuted;
|
|
2820
|
+
await revertExecuted(executedForRevert, this.adapter);
|
|
2821
|
+
for (const { op } of [...executedForRevert].reverse()) {
|
|
2822
|
+
if (op.vaultName !== this.vault) continue;
|
|
2468
2823
|
try {
|
|
2469
|
-
if (
|
|
2470
|
-
|
|
2824
|
+
if (op.collectionName === this.name) {
|
|
2825
|
+
await this._invalidateCacheEntry(op.id);
|
|
2826
|
+
} else if (this.derivationSource) {
|
|
2827
|
+
const sibling = this.derivationSource.getCollection(op.collectionName);
|
|
2828
|
+
await sibling._invalidateCacheEntry(op.id);
|
|
2829
|
+
}
|
|
2471
2830
|
} catch {
|
|
2472
2831
|
}
|
|
2473
2832
|
}
|
|
2474
2833
|
throw err;
|
|
2834
|
+
} finally {
|
|
2835
|
+
if (txCtx !== null && this.derivationSource) {
|
|
2836
|
+
this.derivationSource.clearActiveTxContext(txCtx);
|
|
2837
|
+
}
|
|
2475
2838
|
}
|
|
2476
2839
|
}
|
|
2477
2840
|
/**
|
|
@@ -2837,6 +3200,11 @@ var Collection = class {
|
|
|
2837
3200
|
* .aggregate({ total: sum('amount'), n: count() })
|
|
2838
3201
|
* ```
|
|
2839
3202
|
*
|
|
3203
|
+
* **Lazy-MV gap (#157):** `scan()` is synchronous-build and does
|
|
3204
|
+
* NOT trigger lazy materialized-view resolve-on-read. For lazy
|
|
3205
|
+
* MVs, call `list()` (which DOES resolve) or `vault.refreshView(name)`
|
|
3206
|
+
* before scanning. Same shape as the `query()` limitation.
|
|
3207
|
+
*
|
|
2840
3208
|
* Returns a `ScanBuilder<T>` instead of the raw async iterator
|
|
2841
3209
|
* that previous versions used. The builder implements
|
|
2842
3210
|
* `AsyncIterable<T>`, so every existing `for await … of` call
|
|
@@ -2880,6 +3248,38 @@ var Collection = class {
|
|
|
2880
3248
|
}
|
|
2881
3249
|
// ─── Internal ──────────────────────────────────────────────────
|
|
2882
3250
|
/** Load all records from adapter into memory cache. */
|
|
3251
|
+
/**
|
|
3252
|
+
* @internal — refresh the in-memory cache entry for a single id by
|
|
3253
|
+
* re-reading from the adapter. Used by the transaction executor's
|
|
3254
|
+
* Phase-3 revert path: that path writes the prior envelope directly
|
|
3255
|
+
* via the raw store (to avoid re-firing Collection-level side
|
|
3256
|
+
* effects), which would otherwise leave this Collection's eager
|
|
3257
|
+
* cache holding the rolled-back value. After revert, the executor
|
|
3258
|
+
* calls this hook so subsequent `get` / `query` reads see the
|
|
3259
|
+
* actual on-disk state.
|
|
3260
|
+
*
|
|
3261
|
+
* Lazy mode: drops the LRU entry; the next `get` repopulates from
|
|
3262
|
+
* the adapter. Eager mode: re-reads the envelope and either sets
|
|
3263
|
+
* the cache entry (record still present) or deletes it (record was
|
|
3264
|
+
* gone before the tx and the revert deleted it again).
|
|
3265
|
+
*/
|
|
3266
|
+
async _invalidateCacheEntry(id) {
|
|
3267
|
+
if (this.lazy && this.lru) {
|
|
3268
|
+
this.lru.remove(id);
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
3271
|
+
if (!this.hydrated) return;
|
|
3272
|
+
const previous = this.cache.get(id);
|
|
3273
|
+
const envelope = await this.adapter.get(this.vault, this.name, id);
|
|
3274
|
+
if (!envelope) {
|
|
3275
|
+
this.cache.delete(id);
|
|
3276
|
+
if (previous) this.indexes?.remove(id, previous.record);
|
|
3277
|
+
return;
|
|
3278
|
+
}
|
|
3279
|
+
const record = await this.decryptRecord(envelope);
|
|
3280
|
+
this.cache.set(id, { record, version: envelope._v });
|
|
3281
|
+
this.indexes?.upsert(id, record, previous ? previous.record : null);
|
|
3282
|
+
}
|
|
2883
3283
|
async ensureHydrated() {
|
|
2884
3284
|
if (this.hydrated) return;
|
|
2885
3285
|
const ids = await this.adapter.list(this.vault, this.name);
|
|
@@ -3851,97 +4251,246 @@ var NO_PERIODS = {
|
|
|
3851
4251
|
}
|
|
3852
4252
|
};
|
|
3853
4253
|
|
|
3854
|
-
// src/
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
const info = new TextEncoder().encode(MAGIC_LINK_CONTENT_INFO_PREFIX + vault);
|
|
3864
|
-
const ikm = await subtle2.importKey("raw", ikmBytes, "HKDF", false, ["deriveKey"]);
|
|
3865
|
-
return subtle2.deriveKey(
|
|
3866
|
-
{ name: "HKDF", hash: "SHA-256", salt: saltBuffer, info },
|
|
3867
|
-
ikm,
|
|
3868
|
-
{ name: "AES-GCM", length: 256 },
|
|
3869
|
-
false,
|
|
3870
|
-
["encrypt", "decrypt"]
|
|
3871
|
-
);
|
|
4254
|
+
// src/introspection/fields.ts
|
|
4255
|
+
function jsonSchemaType(node) {
|
|
4256
|
+
if (Array.isArray(node.type)) {
|
|
4257
|
+
const non = node.type.filter((t) => t !== "null");
|
|
4258
|
+
return non[0] ?? "opaque";
|
|
4259
|
+
}
|
|
4260
|
+
if (node.enum && Array.isArray(node.enum)) return "enum";
|
|
4261
|
+
if (typeof node.type === "string") return node.type;
|
|
4262
|
+
return "opaque";
|
|
3872
4263
|
}
|
|
3873
|
-
|
|
3874
|
-
const
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
if (
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
4264
|
+
function constraintsFor(node) {
|
|
4265
|
+
const out = {};
|
|
4266
|
+
if (node.enum) out.values = node.enum;
|
|
4267
|
+
if (node.minLength !== void 0) out.minLength = node.minLength;
|
|
4268
|
+
if (node.maxLength !== void 0) out.maxLength = node.maxLength;
|
|
4269
|
+
if (node.pattern !== void 0) out.pattern = node.pattern;
|
|
4270
|
+
if (node.format !== void 0) out.format = node.format;
|
|
4271
|
+
if (node.minimum !== void 0) out.minimum = node.minimum;
|
|
4272
|
+
if (node.maximum !== void 0) out.maximum = node.maximum;
|
|
4273
|
+
if (node.exclusiveMinimum !== void 0) out.gt = node.exclusiveMinimum;
|
|
4274
|
+
if (node.exclusiveMaximum !== void 0) out.lt = node.exclusiveMaximum;
|
|
4275
|
+
return Object.keys(out).length === 0 ? void 0 : out;
|
|
4276
|
+
}
|
|
4277
|
+
function jsonSchemaToFields(jsonSchema, source, refs) {
|
|
4278
|
+
if (!jsonSchema || typeof jsonSchema !== "object") return {};
|
|
4279
|
+
const root = jsonSchema;
|
|
4280
|
+
if (!root.properties || typeof root.properties !== "object") return {};
|
|
4281
|
+
const required = new Set(Array.isArray(root.required) ? root.required : []);
|
|
4282
|
+
const out = {};
|
|
4283
|
+
for (const [name, node] of Object.entries(root.properties)) {
|
|
4284
|
+
const descriptor = {
|
|
4285
|
+
type: jsonSchemaType(node),
|
|
4286
|
+
source,
|
|
4287
|
+
...required.has(name) ? {} : { optional: true },
|
|
4288
|
+
...refs?.[name] ? { references: `${refs[name].target}.id` } : {}
|
|
4289
|
+
};
|
|
4290
|
+
const constraints = constraintsFor(node);
|
|
4291
|
+
if (constraints) descriptor.constraints = constraints;
|
|
4292
|
+
out[name] = descriptor;
|
|
3881
4293
|
}
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
4294
|
+
return out;
|
|
4295
|
+
}
|
|
4296
|
+
|
|
4297
|
+
// src/introspection/walk.ts
|
|
4298
|
+
var INTERNAL_PREFIX = "_";
|
|
4299
|
+
var KNOWN_INTERNAL_NAMES = ["_keyring", "_ledger", "_meta", "_schemas", "_deltas"];
|
|
4300
|
+
async function dumpVaultSchema(vault, opts) {
|
|
4301
|
+
const state = vault._introspectState();
|
|
4302
|
+
const sampleSize = opts.sampleSize ?? 50;
|
|
4303
|
+
const withStats = opts.withStats === true;
|
|
4304
|
+
const cacheNames = [...state.collectionCache.keys()];
|
|
4305
|
+
const storageNames = (await safeListAllCollections(state.adapter, state.name)).filter((n) => !n.startsWith(INTERNAL_PREFIX));
|
|
4306
|
+
const allNames = Array.from(/* @__PURE__ */ new Set([...cacheNames, ...storageNames])).sort();
|
|
4307
|
+
const collections = {};
|
|
4308
|
+
for (const name of allNames) {
|
|
4309
|
+
collections[name] = await describeCollection(state, name, sampleSize, withStats);
|
|
4310
|
+
}
|
|
4311
|
+
const materializedViews = describeMVs(state.mvRegistry);
|
|
4312
|
+
const overlayViews = describeOverlays(state.overlayRegistry);
|
|
4313
|
+
const derivations = describeDerivations(state.derivationRegistry);
|
|
4314
|
+
let internal;
|
|
4315
|
+
if (withStats) {
|
|
4316
|
+
internal = {};
|
|
4317
|
+
for (const name of KNOWN_INTERNAL_NAMES) {
|
|
4318
|
+
const stats = await statsForCollection(state.adapter, state.name, name);
|
|
4319
|
+
if (stats.records > 0) {
|
|
4320
|
+
internal[name] = { records: stats.records, bytes: stats.bytes };
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4324
|
+
const snap = {
|
|
4325
|
+
_noydb_snapshot: 1,
|
|
4326
|
+
vault: state.name,
|
|
4327
|
+
emittedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4328
|
+
subsystems: state.subsystems,
|
|
4329
|
+
collections,
|
|
4330
|
+
materializedViews,
|
|
4331
|
+
overlayViews,
|
|
4332
|
+
derivations,
|
|
4333
|
+
...internal !== void 0 ? { internal } : {}
|
|
3905
4334
|
};
|
|
3906
|
-
|
|
3907
|
-
return { recordId, payload };
|
|
4335
|
+
return snap;
|
|
3908
4336
|
}
|
|
3909
|
-
async function
|
|
3910
|
-
const env = await store.get(vault, MAGIC_LINK_GRANTS_COLLECTION, recordId);
|
|
3911
|
-
if (!env) return null;
|
|
4337
|
+
async function safeListAllCollections(adapter, vault) {
|
|
3912
4338
|
try {
|
|
3913
|
-
const
|
|
3914
|
-
return
|
|
4339
|
+
const snap = await adapter.loadAll(vault);
|
|
4340
|
+
return Object.keys(snap);
|
|
3915
4341
|
} catch {
|
|
3916
|
-
return
|
|
4342
|
+
return [];
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
async function describeCollection(state, collectionName, sampleSize, withStats) {
|
|
4346
|
+
let fields = {};
|
|
4347
|
+
let validator;
|
|
4348
|
+
const refsRaw = state.refRegistry.getOutbound(collectionName);
|
|
4349
|
+
const refs = {};
|
|
4350
|
+
for (const [name, desc] of Object.entries(refsRaw)) {
|
|
4351
|
+
refs[name] = { target: desc.target, mode: desc.mode };
|
|
4352
|
+
}
|
|
4353
|
+
try {
|
|
4354
|
+
const dek = await state.getDEK(collectionName);
|
|
4355
|
+
const persisted = await loadPersistedSchema(state.adapter, state.name, collectionName, dek);
|
|
4356
|
+
if (persisted) {
|
|
4357
|
+
validator = { kind: persisted.kind, source: "persisted" };
|
|
4358
|
+
if (persisted.jsonSchema) {
|
|
4359
|
+
fields = jsonSchemaToFields(persisted.jsonSchema, "persisted", refsRaw);
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
} catch {
|
|
4363
|
+
}
|
|
4364
|
+
if (!validator) {
|
|
4365
|
+
const coll = state.collectionCache.get(collectionName);
|
|
4366
|
+
const schema = coll?.getSchema();
|
|
4367
|
+
if (schema) {
|
|
4368
|
+
try {
|
|
4369
|
+
const derived = await derivePersistedSchema(schema);
|
|
4370
|
+
validator = { kind: derived.kind, source: "live-validator" };
|
|
4371
|
+
if (derived.jsonSchema) {
|
|
4372
|
+
fields = jsonSchemaToFields(derived.jsonSchema, "live-validator", refsRaw);
|
|
4373
|
+
}
|
|
4374
|
+
} catch {
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
}
|
|
4378
|
+
if (Object.keys(fields).length === 0 && sampleSize > 0) {
|
|
4379
|
+
}
|
|
4380
|
+
const descriptor = {
|
|
4381
|
+
fields,
|
|
4382
|
+
indexes: [],
|
|
4383
|
+
refs,
|
|
4384
|
+
...validator ? { validator } : {}
|
|
4385
|
+
};
|
|
4386
|
+
if (withStats) {
|
|
4387
|
+
const stats = await statsForCollection(state.adapter, state.name, collectionName);
|
|
4388
|
+
descriptor.stats = stats;
|
|
3917
4389
|
}
|
|
4390
|
+
return descriptor;
|
|
4391
|
+
}
|
|
4392
|
+
async function statsForCollection(adapter, vault, collection) {
|
|
4393
|
+
const ids = await adapter.list(vault, collection);
|
|
4394
|
+
if (ids.length === 0) {
|
|
4395
|
+
return { records: 0, bytes: 0, bytesAvg: 0, bytesMin: 0, bytesMax: 0, oldest: "", newest: "" };
|
|
4396
|
+
}
|
|
4397
|
+
let total = 0;
|
|
4398
|
+
let min2 = Number.POSITIVE_INFINITY;
|
|
4399
|
+
let max2 = 0;
|
|
4400
|
+
let oldest = "\uFFFF";
|
|
4401
|
+
let newest = "";
|
|
4402
|
+
for (const id of ids) {
|
|
4403
|
+
const env = await adapter.get(vault, collection, id);
|
|
4404
|
+
if (!env) continue;
|
|
4405
|
+
const size = env._data.length;
|
|
4406
|
+
total += size;
|
|
4407
|
+
if (size < min2) min2 = size;
|
|
4408
|
+
if (size > max2) max2 = size;
|
|
4409
|
+
if (env._ts < oldest) oldest = env._ts;
|
|
4410
|
+
if (env._ts > newest) newest = env._ts;
|
|
4411
|
+
}
|
|
4412
|
+
return {
|
|
4413
|
+
records: ids.length,
|
|
4414
|
+
bytes: total,
|
|
4415
|
+
bytesAvg: Math.round(total / ids.length),
|
|
4416
|
+
bytesMin: min2 === Number.POSITIVE_INFINITY ? 0 : min2,
|
|
4417
|
+
bytesMax: max2,
|
|
4418
|
+
oldest: oldest === "\uFFFF" ? "" : oldest,
|
|
4419
|
+
newest
|
|
4420
|
+
};
|
|
3918
4421
|
}
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
const
|
|
3922
|
-
const out =
|
|
3923
|
-
for (const
|
|
3924
|
-
const
|
|
3925
|
-
|
|
4422
|
+
function describeMVs(registry) {
|
|
4423
|
+
if (!registry || typeof registry !== "object") return {};
|
|
4424
|
+
const items = listFromRegistry(registry);
|
|
4425
|
+
const out = {};
|
|
4426
|
+
for (const item of items) {
|
|
4427
|
+
const reg = item;
|
|
4428
|
+
const spec = reg.spec;
|
|
4429
|
+
if (!spec?.name) continue;
|
|
4430
|
+
const sources = spec.unionSources ? spec.unionSources.map((u) => u.collection) : reg.dependencies ? [...reg.dependencies].sort() : [];
|
|
4431
|
+
const groupBy = spec.groupBy ? Array.isArray(spec.groupBy) ? [...spec.groupBy] : [spec.groupBy] : void 0;
|
|
4432
|
+
const aggregate = spec.aggregate ? Object.fromEntries(
|
|
4433
|
+
Object.entries(spec.aggregate).map(([k, v]) => [k, summariseAggregateOp(v)])
|
|
4434
|
+
) : void 0;
|
|
4435
|
+
out[spec.name] = {
|
|
4436
|
+
sources,
|
|
4437
|
+
...groupBy ? { groupBy } : {},
|
|
4438
|
+
...aggregate ? { aggregate } : {},
|
|
4439
|
+
refresh: spec.refresh ?? "eager"
|
|
4440
|
+
};
|
|
3926
4441
|
}
|
|
3927
4442
|
return out;
|
|
3928
4443
|
}
|
|
3929
|
-
|
|
3930
|
-
|
|
4444
|
+
function describeOverlays(registry) {
|
|
4445
|
+
if (!registry || typeof registry !== "object") return {};
|
|
4446
|
+
const specs = listFromRegistry(registry);
|
|
4447
|
+
const out = {};
|
|
4448
|
+
for (const spec of specs) {
|
|
4449
|
+
const s = spec;
|
|
4450
|
+
if (!s.name || !s.base || !s.overlay) continue;
|
|
4451
|
+
out[s.name] = { base: s.base, overlay: s.overlay };
|
|
4452
|
+
}
|
|
4453
|
+
return out;
|
|
3931
4454
|
}
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
const
|
|
3935
|
-
|
|
3936
|
-
|
|
4455
|
+
function describeDerivations(registry) {
|
|
4456
|
+
if (!registry || typeof registry !== "object") return {};
|
|
4457
|
+
const specs = listFromRegistry(registry);
|
|
4458
|
+
const out = {};
|
|
4459
|
+
for (const spec of specs) {
|
|
4460
|
+
const s = spec;
|
|
4461
|
+
if (!s.name) continue;
|
|
4462
|
+
out[s.name] = {
|
|
4463
|
+
source: s.source ?? "",
|
|
4464
|
+
outputs: s.outputs ?? []
|
|
4465
|
+
};
|
|
3937
4466
|
}
|
|
3938
|
-
return
|
|
4467
|
+
return out;
|
|
3939
4468
|
}
|
|
3940
|
-
function
|
|
3941
|
-
|
|
4469
|
+
function listFromRegistry(reg) {
|
|
4470
|
+
for (const method of ["all", "list", "specs", "values"]) {
|
|
4471
|
+
const fn = reg[method];
|
|
4472
|
+
if (typeof fn === "function") {
|
|
4473
|
+
try {
|
|
4474
|
+
const out = fn.call(reg);
|
|
4475
|
+
if (Array.isArray(out)) return out;
|
|
4476
|
+
if (out && typeof out.values === "function") {
|
|
4477
|
+
return [...out.values()];
|
|
4478
|
+
}
|
|
4479
|
+
} catch {
|
|
4480
|
+
continue;
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
return [];
|
|
3942
4485
|
}
|
|
3943
|
-
function
|
|
3944
|
-
|
|
4486
|
+
function summariseAggregateOp(value) {
|
|
4487
|
+
if (value && typeof value === "object") {
|
|
4488
|
+
const op = value.op ?? value.kind;
|
|
4489
|
+
const field = value.field;
|
|
4490
|
+
if (op && field) return `${op}(${field})`;
|
|
4491
|
+
if (op) return op;
|
|
4492
|
+
}
|
|
4493
|
+
return String(value);
|
|
3945
4494
|
}
|
|
3946
4495
|
|
|
3947
4496
|
// src/vault.ts
|
|
@@ -3986,6 +4535,40 @@ var Vault = class {
|
|
|
3986
4535
|
historyStrategy;
|
|
3987
4536
|
i18nStrategy;
|
|
3988
4537
|
syncStrategy;
|
|
4538
|
+
/**
|
|
4539
|
+
* Per-vault guard registry. `null` until `_initGuards()` runs; stays
|
|
4540
|
+
* `null` for vaults that never register any guard strategy. The
|
|
4541
|
+
* runtime class is dynamic-imported on demand so consumers that
|
|
4542
|
+
* never use guards don't pull `GuardRegistry`/`GuardExecutor` into
|
|
4543
|
+
* their bundle (#130).
|
|
4544
|
+
*/
|
|
4545
|
+
guardRegistry = null;
|
|
4546
|
+
/**
|
|
4547
|
+
* Per-vault derivation registry. Same lazy-load contract as
|
|
4548
|
+
* `guardRegistry` — `null` until `_initDerivations()` runs with at
|
|
4549
|
+
* least one strategy handle. See #130 for the bundle motivation.
|
|
4550
|
+
*/
|
|
4551
|
+
derivationRegistry = null;
|
|
4552
|
+
/**
|
|
4553
|
+
* Per-vault materialized-view registry (#143/#150). Same lazy-load
|
|
4554
|
+
* contract as `derivationRegistry` — `null` until
|
|
4555
|
+
* `_initMaterializedViews()` runs with at least one MV handle.
|
|
4556
|
+
*/
|
|
4557
|
+
materializedViewRegistry = null;
|
|
4558
|
+
/**
|
|
4559
|
+
* Per-vault overlay registry (#154). Same lazy-load contract as
|
|
4560
|
+
* `materializedViewRegistry` — `null` until `_initOverlayedViews()`
|
|
4561
|
+
* runs with at least one handle.
|
|
4562
|
+
*/
|
|
4563
|
+
overlayedViewRegistry = null;
|
|
4564
|
+
/**
|
|
4565
|
+
* Cached read-only facade handed to guard callbacks via `ctx.vault`,
|
|
4566
|
+
* and to derivation callbacks via `derive(source, ctx)`. Allocated
|
|
4567
|
+
* eagerly inside `_initGuards()` and/or `_initDerivations()` so read
|
|
4568
|
+
* accessors stay synchronous (callers in `tx/transaction.ts` rely on
|
|
4569
|
+
* that). Stays `null` for vaults with neither subsystem configured.
|
|
4570
|
+
*/
|
|
4571
|
+
readOnlyFacade = null;
|
|
3989
4572
|
getDEK;
|
|
3990
4573
|
/**
|
|
3991
4574
|
* Per-principal user envelope API.
|
|
@@ -4033,6 +4616,16 @@ var Vault = class {
|
|
|
4033
4616
|
* docstring.
|
|
4034
4617
|
*/
|
|
4035
4618
|
ledgerStore = null;
|
|
4619
|
+
/**
|
|
4620
|
+
* Background writes for persisted-schema envelopes (#schema-dump v0
|
|
4621
|
+
* slice 1). One promise per `collection({ persistJsonSchema: true })`
|
|
4622
|
+
* registration that actually fired a derive call. Fire-and-forget
|
|
4623
|
+
* from the collection factory; tests await
|
|
4624
|
+
* {@link _drainPendingSchemaWrites} before asserting on storage.
|
|
4625
|
+
* Production code does not need to drain — the writes are
|
|
4626
|
+
* idempotent fingerprints, not correctness invariants.
|
|
4627
|
+
*/
|
|
4628
|
+
_pendingSchemaWrites = [];
|
|
4036
4629
|
/**
|
|
4037
4630
|
* Per-vault foreign-key reference registry. Collections
|
|
4038
4631
|
* register their `refs` option here on construction; the
|
|
@@ -4127,6 +4720,7 @@ var Vault = class {
|
|
|
4127
4720
|
this.historyStrategy = opts.historyStrategy ?? NO_HISTORY;
|
|
4128
4721
|
this.i18nStrategy = opts.i18nStrategy ?? NO_I18N;
|
|
4129
4722
|
this.syncStrategy = opts.syncStrategy ?? NO_SYNC;
|
|
4723
|
+
void opts.guardStrategies;
|
|
4130
4724
|
this.historyConfig = opts.historyConfig ?? { enabled: true };
|
|
4131
4725
|
this.reloadKeyring = opts.reloadKeyring;
|
|
4132
4726
|
this.locale = opts.locale;
|
|
@@ -4186,6 +4780,16 @@ var Vault = class {
|
|
|
4186
4780
|
* Collection constructor for the rationale.
|
|
4187
4781
|
*/
|
|
4188
4782
|
collection(collectionName, options) {
|
|
4783
|
+
const overlayRegistry = this.overlayedViewRegistry;
|
|
4784
|
+
if (overlayRegistry !== null && overlayRegistry.isOverlay(collectionName)) {
|
|
4785
|
+
const spec = overlayRegistry.byName(collectionName);
|
|
4786
|
+
if (spec) {
|
|
4787
|
+
const base = this.collection(spec.base);
|
|
4788
|
+
const overlay = this.collection(spec.overlay);
|
|
4789
|
+
const baseRowKey = overlayRegistry.resolveBaseRowKey(collectionName, this.materializedViewRegistry);
|
|
4790
|
+
return new OverlayedCollection(spec, base, overlay, baseRowKey);
|
|
4791
|
+
}
|
|
4792
|
+
}
|
|
4189
4793
|
if (isDictCollectionName(collectionName)) {
|
|
4190
4794
|
throw new ReservedCollectionNameError(collectionName);
|
|
4191
4795
|
}
|
|
@@ -4233,7 +4837,38 @@ var Vault = class {
|
|
|
4233
4837
|
defaultLocale: this.locale,
|
|
4234
4838
|
onRegisterConflictResolver: this.onRegisterConflictResolver,
|
|
4235
4839
|
onAccess: (op, id) => this._logConsent(op, collectionName, id),
|
|
4236
|
-
periodGuard: (existing, incoming) => this._assertTsWritable(existing, incoming)
|
|
4840
|
+
periodGuard: (existing, incoming) => this._assertTsWritable(existing, incoming),
|
|
4841
|
+
// Guard / derivation sources are only wired when the
|
|
4842
|
+
// corresponding registry has been initialised. Vaults without
|
|
4843
|
+
// guards/derivations skip this entirely so `Collection.put`'s
|
|
4844
|
+
// `if (this.guardSource)` / `if (this.derivationSource)`
|
|
4845
|
+
// branches no-op without ever touching the subsystem code.
|
|
4846
|
+
...this.guardRegistry !== null ? {
|
|
4847
|
+
guardSource: {
|
|
4848
|
+
registry: () => this.guardRegistry,
|
|
4849
|
+
readOnlyVault: () => this._ensureReadOnlyFacade()
|
|
4850
|
+
}
|
|
4851
|
+
} : {},
|
|
4852
|
+
...this.derivationRegistry !== null ? {
|
|
4853
|
+
derivationSource: {
|
|
4854
|
+
registry: () => this.derivationRegistry,
|
|
4855
|
+
getCollection: (name) => this.collection(name),
|
|
4856
|
+
getReadOnlyFacade: () => this._ensureReadOnlyFacade(),
|
|
4857
|
+
getActiveTxContext: () => this.noydb._activeTxContextOrNull,
|
|
4858
|
+
createTxContext: () => this.noydb._createTxContext(),
|
|
4859
|
+
setActiveTxContext: (ctx) => this.noydb._setActiveTxContext(ctx),
|
|
4860
|
+
clearActiveTxContext: (ctx) => this.noydb._clearActiveTxContext(ctx)
|
|
4861
|
+
}
|
|
4862
|
+
} : {},
|
|
4863
|
+
...this.materializedViewRegistry !== null ? {
|
|
4864
|
+
materializedViewSource: {
|
|
4865
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
4866
|
+
registry: () => this.materializedViewRegistry,
|
|
4867
|
+
getCollection: (name) => this.collection(name),
|
|
4868
|
+
getActiveTxContext: () => this.noydb._activeTxContextOrNull,
|
|
4869
|
+
getQueryContext: () => this
|
|
4870
|
+
}
|
|
4871
|
+
} : {}
|
|
4237
4872
|
};
|
|
4238
4873
|
if (options?.indexes !== void 0) collOpts.indexes = options.indexes;
|
|
4239
4874
|
if (options?.reconcileOnOpen !== void 0) collOpts.reconcileOnOpen = options.reconcileOnOpen;
|
|
@@ -4270,9 +4905,39 @@ var Vault = class {
|
|
|
4270
4905
|
}
|
|
4271
4906
|
coll = new Collection(collOpts);
|
|
4272
4907
|
this.collectionCache.set(collectionName, coll);
|
|
4908
|
+
if (options?.persistJsonSchema === true && options.schema !== void 0) {
|
|
4909
|
+
const validator = options.schema;
|
|
4910
|
+
const work = (async () => {
|
|
4911
|
+
try {
|
|
4912
|
+
const dek = await this.getDEK(collectionName);
|
|
4913
|
+
await persistSchemaIfNeeded({
|
|
4914
|
+
store: this.adapter,
|
|
4915
|
+
vault: this.name,
|
|
4916
|
+
collectionName,
|
|
4917
|
+
validator,
|
|
4918
|
+
dek
|
|
4919
|
+
});
|
|
4920
|
+
} catch (err) {
|
|
4921
|
+
console.warn(
|
|
4922
|
+
`[noy-db] persisted-schema write failed for "${collectionName}": ` + (err instanceof Error ? err.message : String(err))
|
|
4923
|
+
);
|
|
4924
|
+
}
|
|
4925
|
+
})();
|
|
4926
|
+
this._pendingSchemaWrites.push(work);
|
|
4927
|
+
}
|
|
4273
4928
|
}
|
|
4274
4929
|
return coll;
|
|
4275
4930
|
}
|
|
4931
|
+
/**
|
|
4932
|
+
* Await all background persisted-schema writes triggered by
|
|
4933
|
+
* `collection({ persistJsonSchema: true })` calls on this vault.
|
|
4934
|
+
* Used in tests; production code does not need to call this.
|
|
4935
|
+
*/
|
|
4936
|
+
async _drainPendingSchemaWrites() {
|
|
4937
|
+
const pending = this._pendingSchemaWrites;
|
|
4938
|
+
this._pendingSchemaWrites = [];
|
|
4939
|
+
await Promise.allSettled(pending);
|
|
4940
|
+
}
|
|
4276
4941
|
/**
|
|
4277
4942
|
* Validate i18nText fields on a `put()`. Called by Collection just
|
|
4278
4943
|
* before the adapter write, after schema validation. Throws
|
|
@@ -4816,45 +5481,309 @@ var Vault = class {
|
|
|
4816
5481
|
return { violations };
|
|
4817
5482
|
}
|
|
4818
5483
|
/**
|
|
4819
|
-
* Return this compartment's hash-chained audit log.
|
|
4820
|
-
*
|
|
4821
|
-
* The ledger is lazy-initialized on first access and cached for the
|
|
4822
|
-
* lifetime of the Vault instance. Every LedgerStore instance
|
|
4823
|
-
* shares the same adapter and DEK resolver, so `vault.ledger()`
|
|
4824
|
-
* can be called repeatedly without performance cost.
|
|
4825
|
-
*
|
|
4826
|
-
* The LedgerStore itself is the public API: consumers call
|
|
4827
|
-
* `.append()` (via Collection internals), `.head()`, `.verify()`,
|
|
4828
|
-
* and `.entries({ from, to })`. See the LedgerStore docstring for
|
|
4829
|
-
* the full surface and the concurrency caveats.
|
|
5484
|
+
* Return this compartment's hash-chained audit log.
|
|
5485
|
+
*
|
|
5486
|
+
* The ledger is lazy-initialized on first access and cached for the
|
|
5487
|
+
* lifetime of the Vault instance. Every LedgerStore instance
|
|
5488
|
+
* shares the same adapter and DEK resolver, so `vault.ledger()`
|
|
5489
|
+
* can be called repeatedly without performance cost.
|
|
5490
|
+
*
|
|
5491
|
+
* The LedgerStore itself is the public API: consumers call
|
|
5492
|
+
* `.append()` (via Collection internals), `.head()`, `.verify()`,
|
|
5493
|
+
* and `.entries({ from, to })`. See the LedgerStore docstring for
|
|
5494
|
+
* the full surface and the concurrency caveats.
|
|
5495
|
+
*/
|
|
5496
|
+
ledger() {
|
|
5497
|
+
const store = this.getLedgerOrNull();
|
|
5498
|
+
if (!store) {
|
|
5499
|
+
throw new Error(
|
|
5500
|
+
'vault.ledger() requires the history strategy. Import `{ withHistory }` from "@noy-db/hub/history" and pass it to `createNoydb({ historyStrategy: withHistory() })`.'
|
|
5501
|
+
);
|
|
5502
|
+
}
|
|
5503
|
+
return store;
|
|
5504
|
+
}
|
|
5505
|
+
/**
|
|
5506
|
+
* Internal accessor — returns the LedgerStore if the history
|
|
5507
|
+
* strategy is opted in, or `null` otherwise. Used by dump/restore/
|
|
5508
|
+
* verifyBackupIntegrity and by Collection write paths that already
|
|
5509
|
+
* gate on `if (this.ledger)`. The public `ledger()` accessor above
|
|
5510
|
+
* throws on null; this one stays silent so the off-path no-ops.
|
|
5511
|
+
*/
|
|
5512
|
+
getLedgerOrNull() {
|
|
5513
|
+
if (!this.ledgerStore) {
|
|
5514
|
+
this.ledgerStore = this.historyStrategy.buildLedger({
|
|
5515
|
+
adapter: this.adapter,
|
|
5516
|
+
vault: this.name,
|
|
5517
|
+
encrypted: this.encrypted,
|
|
5518
|
+
getDEK: this.getDEK,
|
|
5519
|
+
actor: this.keyring.userId
|
|
5520
|
+
});
|
|
5521
|
+
}
|
|
5522
|
+
return this.ledgerStore;
|
|
5523
|
+
}
|
|
5524
|
+
/**
|
|
5525
|
+
* @internal — called by `Noydb.openVault` after construction.
|
|
5526
|
+
* Dynamic-imports `GuardRegistry` + `ReadOnlyVaultFacade` and seeds
|
|
5527
|
+
* the registry with the supplied strategy handles. No-op when the
|
|
5528
|
+
* handles array is empty — keeps the guard subsystem out of the
|
|
5529
|
+
* floor bundle for consumers that don't use guards (#130).
|
|
5530
|
+
*
|
|
5531
|
+
* The read-only facade is eagerly instantiated here so the sync
|
|
5532
|
+
* accessor `_getReadOnlyFacade()` (called from the tx amendment
|
|
5533
|
+
* runner) stays synchronous.
|
|
5534
|
+
*/
|
|
5535
|
+
async _initGuards(handles) {
|
|
5536
|
+
if (handles.length === 0) return;
|
|
5537
|
+
const [{ GuardRegistry }, { ReadOnlyVaultFacade }] = await Promise.all([
|
|
5538
|
+
import("./registry-RFGGMVNJ.js"),
|
|
5539
|
+
import("./read-only-facade-ITU6L7BL.js")
|
|
5540
|
+
]);
|
|
5541
|
+
const registry = new GuardRegistry();
|
|
5542
|
+
for (const h of handles) registry.register(h.spec);
|
|
5543
|
+
this.guardRegistry = registry;
|
|
5544
|
+
this.readOnlyFacade = new ReadOnlyVaultFacade(this);
|
|
5545
|
+
}
|
|
5546
|
+
/**
|
|
5547
|
+
* @internal — Collection.put calls into this. Returns `null` for
|
|
5548
|
+
* vaults that never registered any guard strategy. Callers MUST
|
|
5549
|
+
* gate on null (the existing `if (this.guardSource)` branches in
|
|
5550
|
+
* `Collection` already do this transitively).
|
|
5551
|
+
*/
|
|
5552
|
+
_getGuardRegistry() {
|
|
5553
|
+
return this.guardRegistry;
|
|
5554
|
+
}
|
|
5555
|
+
/**
|
|
5556
|
+
* @internal — called by `Noydb.openVault` after construction.
|
|
5557
|
+
* Dynamic-imports `DerivationRegistry` and registers the supplied
|
|
5558
|
+
* derivation strategies (async because `strategyHash` computation
|
|
5559
|
+
* goes through `crypto.subtle.digest`). No-op when the handles
|
|
5560
|
+
* array is empty — keeps the derivation subsystem out of the floor
|
|
5561
|
+
* bundle for consumers that don't use derivations (#130). Throws
|
|
5562
|
+
* `DerivationCycleError` if a cycle is detected after registration.
|
|
5563
|
+
*/
|
|
5564
|
+
async _initDerivations(handles) {
|
|
5565
|
+
if (handles.length === 0) return;
|
|
5566
|
+
const [{ DerivationRegistry }, { ReadOnlyVaultFacade }] = await Promise.all([
|
|
5567
|
+
import("./registry-WLLMODKN.js"),
|
|
5568
|
+
import("./read-only-facade-ITU6L7BL.js")
|
|
5569
|
+
]);
|
|
5570
|
+
const registry = new DerivationRegistry();
|
|
5571
|
+
for (const h of handles) {
|
|
5572
|
+
await registry.register(h.spec);
|
|
5573
|
+
}
|
|
5574
|
+
registry.validate();
|
|
5575
|
+
this.derivationRegistry = registry;
|
|
5576
|
+
if (this.readOnlyFacade === null) {
|
|
5577
|
+
this.readOnlyFacade = new ReadOnlyVaultFacade(this);
|
|
5578
|
+
}
|
|
5579
|
+
}
|
|
5580
|
+
/**
|
|
5581
|
+
* @internal — consumed by `Collection.put` at write-time. Returns
|
|
5582
|
+
* `null` for vaults that never registered any derivation strategy.
|
|
5583
|
+
*/
|
|
5584
|
+
_getDerivationRegistry() {
|
|
5585
|
+
return this.derivationRegistry;
|
|
5586
|
+
}
|
|
5587
|
+
/**
|
|
5588
|
+
* @internal — called by `Noydb.openVault` after collections are
|
|
5589
|
+
* wired. Dynamic-imports `MaterializedViewRegistry`, registers each
|
|
5590
|
+
* MV spec (which invokes its `query()` once for dependency
|
|
5591
|
+
* analysis), then runs the unified cycle detection across the MV +
|
|
5592
|
+
* derivation graphs. No-op when the handles array is empty — keeps
|
|
5593
|
+
* the MV subsystem out of the floor bundle (mirrors v1 #130).
|
|
5594
|
+
* Throws `MaterializedViewCycleError` if a cycle is detected.
|
|
5595
|
+
*/
|
|
5596
|
+
async _initMaterializedViews(handles) {
|
|
5597
|
+
if (handles.length === 0) return;
|
|
5598
|
+
const { MaterializedViewRegistry } = await import("./registry-3L3N3PTG.js");
|
|
5599
|
+
const registry = new MaterializedViewRegistry();
|
|
5600
|
+
this.materializedViewRegistry = registry;
|
|
5601
|
+
const db = this;
|
|
5602
|
+
for (const h of handles) {
|
|
5603
|
+
await registry.register(h.spec, db);
|
|
5604
|
+
}
|
|
5605
|
+
registry.validate(this.derivationRegistry);
|
|
5606
|
+
}
|
|
5607
|
+
/**
|
|
5608
|
+
* @internal — consumed by `Collection.put` at write-time. Returns
|
|
5609
|
+
* `null` for vaults that never registered any MV strategy.
|
|
5610
|
+
*/
|
|
5611
|
+
_getMaterializedViewRegistry() {
|
|
5612
|
+
return this.materializedViewRegistry;
|
|
5613
|
+
}
|
|
5614
|
+
/**
|
|
5615
|
+
* @internal — called by `Noydb.openVault` after MVs are wired.
|
|
5616
|
+
* Dynamic-imports `OverlayedViewRegistry`, registers each spec,
|
|
5617
|
+
* validates against the MV registry for name/base/overlay collisions.
|
|
5618
|
+
* Throws on validation failure.
|
|
5619
|
+
*/
|
|
5620
|
+
async _initOverlayedViews(handles) {
|
|
5621
|
+
if (handles.length === 0) return;
|
|
5622
|
+
const { OverlayedViewRegistry } = await import("./registry-O47PUPSY.js");
|
|
5623
|
+
const registry = new OverlayedViewRegistry();
|
|
5624
|
+
const mvRegistry = this.materializedViewRegistry;
|
|
5625
|
+
const overlayNames = /* @__PURE__ */ new Set();
|
|
5626
|
+
for (const h of handles) overlayNames.add(h.spec.name);
|
|
5627
|
+
const isMVOutput = (name) => {
|
|
5628
|
+
if (!mvRegistry) return false;
|
|
5629
|
+
for (const reg of mvRegistry.all()) {
|
|
5630
|
+
if (reg.outputCollection === name) return true;
|
|
5631
|
+
}
|
|
5632
|
+
return false;
|
|
5633
|
+
};
|
|
5634
|
+
for (const h of handles) {
|
|
5635
|
+
registry.register(h.spec, {
|
|
5636
|
+
isOverlayName: (n) => overlayNames.has(n) && n !== h.spec.name,
|
|
5637
|
+
isMVOutput
|
|
5638
|
+
});
|
|
5639
|
+
}
|
|
5640
|
+
this.overlayedViewRegistry = registry;
|
|
5641
|
+
}
|
|
5642
|
+
/**
|
|
5643
|
+
* @internal — consumed by `Vault.collection()`. Returns `null` for
|
|
5644
|
+
* vaults with no overlays registered.
|
|
5645
|
+
*/
|
|
5646
|
+
_getOverlayedViewRegistry() {
|
|
5647
|
+
return this.overlayedViewRegistry;
|
|
5648
|
+
}
|
|
5649
|
+
/**
|
|
5650
|
+
* Manual re-materialize for a single registered MV (#151). Useful
|
|
5651
|
+
* for `refresh: 'manual'` MVs (whose consumer drives refreshes
|
|
5652
|
+
* externally), for stale-bit recovery on vault re-open, and as the
|
|
5653
|
+
* explicit bulk-recompute escape hatch after a strategy change.
|
|
5654
|
+
*
|
|
5655
|
+
* Returns `{ written, deleted, failed }`. `deleted` is always 0 in
|
|
5656
|
+
* foundation + this sub-issue — tombstoning lands in #152.
|
|
5657
|
+
*
|
|
5658
|
+
* Throws if `name` is not a registered MV.
|
|
5659
|
+
*/
|
|
5660
|
+
async refreshView(name) {
|
|
5661
|
+
const registry = this.materializedViewRegistry;
|
|
5662
|
+
if (registry === null) {
|
|
5663
|
+
return { written: 0, deleted: 0, failed: 0 };
|
|
5664
|
+
}
|
|
5665
|
+
const reg = registry.byName(name);
|
|
5666
|
+
if (!reg) {
|
|
5667
|
+
throw new Error(`refreshView: no MV registered with name "${name}"`);
|
|
5668
|
+
}
|
|
5669
|
+
const { MaterializedViewExecutor } = await import("./executor-7E3VFGW7.js");
|
|
5670
|
+
const result = await MaterializedViewExecutor.refresh(reg, {
|
|
5671
|
+
getCollection: (n) => this.collection(n),
|
|
5672
|
+
getActiveTxContext: () => this.noydb._activeTxContextOrNull,
|
|
5673
|
+
getQueryContext: () => this
|
|
5674
|
+
});
|
|
5675
|
+
const { clearMVStale } = await import("./stale-HSC5YO2O.js");
|
|
5676
|
+
clearMVStale(registry, name);
|
|
5677
|
+
return result;
|
|
5678
|
+
}
|
|
5679
|
+
/**
|
|
5680
|
+
* Re-derive every record in the named source collection. Useful
|
|
5681
|
+
* after a strategy change to bring previously-derived records
|
|
5682
|
+
* up-to-date.
|
|
5683
|
+
*
|
|
5684
|
+
* Sequential in v1; parallelisation deferred to v2.
|
|
5685
|
+
*/
|
|
5686
|
+
async deriveAll(sourceCollection) {
|
|
5687
|
+
const registry = this._getDerivationRegistry();
|
|
5688
|
+
if (registry === null) return { derived: 0, failed: 0 };
|
|
5689
|
+
const strategies = registry.strategiesForSource(sourceCollection);
|
|
5690
|
+
if (strategies.length === 0) return { derived: 0, failed: 0 };
|
|
5691
|
+
const { DerivationExecutor } = await import("./executor-X4SQ3ZLC.js");
|
|
5692
|
+
const sourceColl = this.collection(sourceCollection);
|
|
5693
|
+
const records = await sourceColl.list();
|
|
5694
|
+
const ctx = { vault: this.readOnlyFacade ?? new (await import("./read-only-facade-ITU6L7BL.js")).ReadOnlyVaultFacade(this) };
|
|
5695
|
+
let derived = 0;
|
|
5696
|
+
let failed = 0;
|
|
5697
|
+
for (const record of records) {
|
|
5698
|
+
if (typeof record !== "object" || record === null) continue;
|
|
5699
|
+
const id = record.id;
|
|
5700
|
+
if (typeof id !== "string") continue;
|
|
5701
|
+
for (const { spec, strategyHash } of strategies) {
|
|
5702
|
+
const sourceWithId = { ...record, id };
|
|
5703
|
+
const result = await DerivationExecutor.run(spec, sourceWithId, 0, strategyHash, ctx);
|
|
5704
|
+
let anyFailed = false;
|
|
5705
|
+
for (const key of Object.keys(spec.outputs)) {
|
|
5706
|
+
const out = result.outputs[key];
|
|
5707
|
+
if (!out) continue;
|
|
5708
|
+
if (out.kind === "failed") {
|
|
5709
|
+
anyFailed = true;
|
|
5710
|
+
continue;
|
|
5711
|
+
}
|
|
5712
|
+
const outSpec = spec.outputs[key];
|
|
5713
|
+
if (!outSpec) continue;
|
|
5714
|
+
const outputColl = this.collection(outSpec.collection);
|
|
5715
|
+
if (out.kind === "array") {
|
|
5716
|
+
const { loadFanoutSidecar, saveFanoutSidecar } = await import("./fanout-sidecar-VJ52RIEY.js");
|
|
5717
|
+
const prior = await loadFanoutSidecar(this.adapter, this.name, spec.source, id, key);
|
|
5718
|
+
const prevKeys = new Set(prior?.keys ?? []);
|
|
5719
|
+
const newKeysList = out.entries.map((e) => e.key);
|
|
5720
|
+
const newKeysSet = new Set(newKeysList);
|
|
5721
|
+
for (const k of prevKeys) {
|
|
5722
|
+
if (newKeysSet.has(k)) continue;
|
|
5723
|
+
await outputColl._internalDelete(k);
|
|
5724
|
+
}
|
|
5725
|
+
for (const entry of out.entries) {
|
|
5726
|
+
await outputColl.put(entry.key, entry.value);
|
|
5727
|
+
}
|
|
5728
|
+
await saveFanoutSidecar(this.adapter, this.name, {
|
|
5729
|
+
source: spec.source,
|
|
5730
|
+
sourceId: id,
|
|
5731
|
+
outputKey: key,
|
|
5732
|
+
outputCollection: outSpec.collection,
|
|
5733
|
+
keys: newKeysList
|
|
5734
|
+
});
|
|
5735
|
+
continue;
|
|
5736
|
+
}
|
|
5737
|
+
if (out.skipped === true) {
|
|
5738
|
+
await outputColl._internalDelete(id);
|
|
5739
|
+
continue;
|
|
5740
|
+
}
|
|
5741
|
+
await outputColl.put(id, out.value);
|
|
5742
|
+
}
|
|
5743
|
+
if (anyFailed) failed++;
|
|
5744
|
+
else derived++;
|
|
5745
|
+
}
|
|
5746
|
+
}
|
|
5747
|
+
return { derived, failed };
|
|
5748
|
+
}
|
|
5749
|
+
/**
|
|
5750
|
+
* @internal — exposed for `runTransaction({ amendment: true })` so
|
|
5751
|
+
* the amendment invariant runner can pass the SAME read-only vault
|
|
5752
|
+
* facade that the per-record `Collection.put` guard hook uses
|
|
5753
|
+
* (`guardSource.readOnlyVault()` above). Eagerly instantiated by
|
|
5754
|
+
* `_initGuards()` so this accessor stays synchronous; returns
|
|
5755
|
+
* `null` for vaults that never registered any guard (amendments
|
|
5756
|
+
* require at least one guard, so the caller should never see null).
|
|
5757
|
+
*/
|
|
5758
|
+
_getReadOnlyFacade() {
|
|
5759
|
+
return this.readOnlyFacade;
|
|
5760
|
+
}
|
|
5761
|
+
/**
|
|
5762
|
+
* Internal lazy-allocator for the read-only facade. Used by the
|
|
5763
|
+
* per-collection `guardSource.readOnlyVault` callback when guards
|
|
5764
|
+
* ARE configured but `_initGuards()` raced with the first guard
|
|
5765
|
+
* invocation (theoretically impossible — `Noydb.openVault` awaits
|
|
5766
|
+
* `_initGuards` before returning — but we keep the defensive lazy
|
|
5767
|
+
* path so the closure's contract stays "always returns a facade").
|
|
4830
5768
|
*/
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
);
|
|
4837
|
-
}
|
|
4838
|
-
return store;
|
|
5769
|
+
_ensureReadOnlyFacade() {
|
|
5770
|
+
if (this.readOnlyFacade !== null) return this.readOnlyFacade;
|
|
5771
|
+
throw new Error(
|
|
5772
|
+
"Vault: guard hook fired before _initGuards() completed. This typically means the vault was opened via the sync fallback path (Noydb.vault(name)) without first calling await db.openVault(name). See issue #132."
|
|
5773
|
+
);
|
|
4839
5774
|
}
|
|
4840
5775
|
/**
|
|
4841
|
-
*
|
|
4842
|
-
*
|
|
4843
|
-
*
|
|
4844
|
-
*
|
|
4845
|
-
*
|
|
5776
|
+
* @internal — exposed for `runTransaction({ amendment: true })`
|
|
5777
|
+
* to append the structured `op: 'amendment'` audit entry without
|
|
5778
|
+
* dragging this private accessor onto the public surface or
|
|
5779
|
+
* forcing the tx executor to depend on the history-strategy
|
|
5780
|
+
* shape directly. Returns `null` when no history strategy is
|
|
5781
|
+
* configured, in which case the amendment commits silently
|
|
5782
|
+
* (the records still write through; only the multi-record
|
|
5783
|
+
* audit summary is skipped).
|
|
4846
5784
|
*/
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
this.ledgerStore = this.historyStrategy.buildLedger({
|
|
4850
|
-
adapter: this.adapter,
|
|
4851
|
-
vault: this.name,
|
|
4852
|
-
encrypted: this.encrypted,
|
|
4853
|
-
getDEK: this.getDEK,
|
|
4854
|
-
actor: this.keyring.userId
|
|
4855
|
-
});
|
|
4856
|
-
}
|
|
4857
|
-
return this.ledgerStore;
|
|
5785
|
+
_getLedgerOrNull() {
|
|
5786
|
+
return this.getLedgerOrNull();
|
|
4858
5787
|
}
|
|
4859
5788
|
/**
|
|
4860
5789
|
* Return a read-only view of this vault as it existed at
|
|
@@ -5008,7 +5937,7 @@ var Vault = class {
|
|
|
5008
5937
|
* collection.
|
|
5009
5938
|
*/
|
|
5010
5939
|
async delegate(opts) {
|
|
5011
|
-
const { issueDelegation: issueDelegation2, DELEGATIONS_COLLECTION: DELEGATIONS_COLLECTION2 } = await import("./delegation-
|
|
5940
|
+
const { issueDelegation: issueDelegation2, DELEGATIONS_COLLECTION: DELEGATIONS_COLLECTION2 } = await import("./delegation-YBA4X4JN.js");
|
|
5012
5941
|
if (!this.keyring.kek) {
|
|
5013
5942
|
throw new ValidationError(
|
|
5014
5943
|
"issueDelegation: keyring.kek is null \u2014 issuing a delegation requires a tier-1 unlock. Re-authenticate at tier 1 (passphrase) first."
|
|
@@ -5030,7 +5959,7 @@ var Vault = class {
|
|
|
5030
5959
|
* if the id does not exist.
|
|
5031
5960
|
*/
|
|
5032
5961
|
async revokeDelegation(id) {
|
|
5033
|
-
const { revokeDelegation: revokeDelegation2, DELEGATIONS_COLLECTION: DELEGATIONS_COLLECTION2 } = await import("./delegation-
|
|
5962
|
+
const { revokeDelegation: revokeDelegation2, DELEGATIONS_COLLECTION: DELEGATIONS_COLLECTION2 } = await import("./delegation-YBA4X4JN.js");
|
|
5034
5963
|
await revokeDelegation2(this.adapter, this.name, id);
|
|
5035
5964
|
void DELEGATIONS_COLLECTION2;
|
|
5036
5965
|
}
|
|
@@ -5355,6 +6284,54 @@ var Vault = class {
|
|
|
5355
6284
|
const snapshot = await this.adapter.loadAll(this.name);
|
|
5356
6285
|
return Object.keys(snapshot);
|
|
5357
6286
|
}
|
|
6287
|
+
/**
|
|
6288
|
+
* Emit a structured introspection snapshot of this vault — vault name,
|
|
6289
|
+
* subsystem opt-in matrix, collections + their fields, materialized
|
|
6290
|
+
* views, overlay views, derivations. With `withStats: true`, walks
|
|
6291
|
+
* every collection's envelopes to compute record counts, byte totals,
|
|
6292
|
+
* and oldest/newest timestamps.
|
|
6293
|
+
*
|
|
6294
|
+
* Consumed by the `noydb describe` CLI to produce human-readable
|
|
6295
|
+
* audit YAML/JSON from a `.noydb` bundle.
|
|
6296
|
+
*
|
|
6297
|
+
* Field provenance:
|
|
6298
|
+
* - `persisted`: read from `_schemas/<col>` envelope (Route B opt-in)
|
|
6299
|
+
* - `live-validator`: derived in-process from a Zod schema attached
|
|
6300
|
+
* to the live `Collection`
|
|
6301
|
+
* - `sampled`: inferred from decrypted records (deferred to a follow-up)
|
|
6302
|
+
* - `unknown`: no schema info available
|
|
6303
|
+
*
|
|
6304
|
+
* @see docs/superpowers/specs/2026-05-22-schema-dump-design.md
|
|
6305
|
+
*/
|
|
6306
|
+
async dumpSchema(opts = {}) {
|
|
6307
|
+
return dumpVaultSchema(this, opts);
|
|
6308
|
+
}
|
|
6309
|
+
/**
|
|
6310
|
+
* Internal accessor for {@link dumpVaultSchema}. Exposes the structural
|
|
6311
|
+
* state the walker needs (collection cache, registries, ref registry,
|
|
6312
|
+
* adapter) without widening the public Vault surface.
|
|
6313
|
+
*
|
|
6314
|
+
* @internal
|
|
6315
|
+
*/
|
|
6316
|
+
_introspectState() {
|
|
6317
|
+
return {
|
|
6318
|
+
name: this.name,
|
|
6319
|
+
adapter: this.adapter,
|
|
6320
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6321
|
+
collectionCache: this.collectionCache,
|
|
6322
|
+
refRegistry: this.refRegistry,
|
|
6323
|
+
getDEK: this.getDEK,
|
|
6324
|
+
subsystems: {
|
|
6325
|
+
guards: this.guardRegistry !== null,
|
|
6326
|
+
derivations: this.derivationRegistry !== null,
|
|
6327
|
+
materializedViews: this.materializedViewRegistry !== null,
|
|
6328
|
+
overlayViews: this.overlayedViewRegistry !== null
|
|
6329
|
+
},
|
|
6330
|
+
mvRegistry: this.materializedViewRegistry,
|
|
6331
|
+
overlayRegistry: this.overlayedViewRegistry,
|
|
6332
|
+
derivationRegistry: this.derivationRegistry
|
|
6333
|
+
};
|
|
6334
|
+
}
|
|
5358
6335
|
/**
|
|
5359
6336
|
* Return the stable opaque bundle handle for this vault,
|
|
5360
6337
|
* generating and persisting a fresh ULID on first call.
|
|
@@ -5430,7 +6407,7 @@ var Vault = class {
|
|
|
5430
6407
|
* @see docs/subsystems/public-envelope.md
|
|
5431
6408
|
*/
|
|
5432
6409
|
async getPublicEnvelope(opts = {}) {
|
|
5433
|
-
const { readPublicEnvelope: readPublicEnvelope2 } = await import("./public-envelope-
|
|
6410
|
+
const { readPublicEnvelope: readPublicEnvelope2 } = await import("./public-envelope-PY6NKFLI.js");
|
|
5434
6411
|
return readPublicEnvelope2(this.adapter, this.name, opts);
|
|
5435
6412
|
}
|
|
5436
6413
|
/**
|
|
@@ -5455,7 +6432,7 @@ var Vault = class {
|
|
|
5455
6432
|
}
|
|
5456
6433
|
}
|
|
5457
6434
|
const internalSnapshot = {};
|
|
5458
|
-
for (const internalName of [LEDGER_COLLECTION, LEDGER_DELTAS_COLLECTION]) {
|
|
6435
|
+
for (const internalName of [LEDGER_COLLECTION, LEDGER_DELTAS_COLLECTION, SCHEMAS_COLLECTION]) {
|
|
5459
6436
|
const ids = await this.adapter.list(this.name, internalName);
|
|
5460
6437
|
if (ids.length === 0) continue;
|
|
5461
6438
|
const records = {};
|
|
@@ -5604,6 +6581,7 @@ var Vault = class {
|
|
|
5604
6581
|
for (let i = allEntries.length - 1; i >= 0; i--) {
|
|
5605
6582
|
const entry = allEntries[i];
|
|
5606
6583
|
if (!entry) continue;
|
|
6584
|
+
if (entry.op === "amendment") continue;
|
|
5607
6585
|
const key = `${entry.collection}/${entry.id}`;
|
|
5608
6586
|
if (seen.has(key)) continue;
|
|
5609
6587
|
seen.add(key);
|
|
@@ -5625,7 +6603,7 @@ var Vault = class {
|
|
|
5625
6603
|
message: `Ledger expects data record "${collection}/${id}" to exist, but the adapter has no envelope for it.`
|
|
5626
6604
|
};
|
|
5627
6605
|
}
|
|
5628
|
-
const actualHash = await
|
|
6606
|
+
const actualHash = await sha256Hex2(envelope._data);
|
|
5629
6607
|
if (actualHash !== expectedHash) {
|
|
5630
6608
|
return {
|
|
5631
6609
|
ok: false,
|
|
@@ -6026,6 +7004,10 @@ var PERSONAL_POLICY = Object.freeze({
|
|
|
6026
7004
|
minTier: 1,
|
|
6027
7005
|
enabled: true
|
|
6028
7006
|
},
|
|
7007
|
+
// rotate-recovery (#121): deliberate paper-sheet regeneration
|
|
7008
|
+
// when the user remembers their passphrase. PERSONAL matches the
|
|
7009
|
+
// pre-#121 low-level flow's bar — knowing the passphrase is enough.
|
|
7010
|
+
"rotate-recovery": { minTier: 1 },
|
|
6029
7011
|
"enroll-authenticator": { minTier: 1 },
|
|
6030
7012
|
"remove-authenticator": { minTier: 1 },
|
|
6031
7013
|
// update-authenticator: meta-only mutation (slot rename, label
|
|
@@ -6085,6 +7067,14 @@ var STRICT_POLICY = Object.freeze({
|
|
|
6085
7067
|
minTier: 1,
|
|
6086
7068
|
enabled: true
|
|
6087
7069
|
},
|
|
7070
|
+
// rotate-recovery (#121): STRICT requires an off-device factor —
|
|
7071
|
+
// rotating recovery is an off-site-trust event; a stolen unlocked
|
|
7072
|
+
// laptop must not be able to silently mint a new sheet for the
|
|
7073
|
+
// attacker. Matches the `peer-recover-user` STRICT default.
|
|
7074
|
+
"rotate-recovery": {
|
|
7075
|
+
minTier: 1,
|
|
7076
|
+
factors: [{ anyOf: ["totp", "email-otp", "webauthn-roaming"] }]
|
|
7077
|
+
},
|
|
6088
7078
|
"enroll-authenticator": {
|
|
6089
7079
|
minTier: 1,
|
|
6090
7080
|
factors: [{ anyOf: ["totp", "email-otp"] }]
|
|
@@ -6273,6 +7263,14 @@ var Noydb = class {
|
|
|
6273
7263
|
* `_meta/policy` load; replaced by `db.updatePolicy()`.
|
|
6274
7264
|
*/
|
|
6275
7265
|
policyCache = /* @__PURE__ */ new Map();
|
|
7266
|
+
/**
|
|
7267
|
+
* One-shot bypass for the managed-mode strong-recovery check (#195).
|
|
7268
|
+
* Set true by {@link openVaultAndEnrollRecovery} for the duration of
|
|
7269
|
+
* the bootstrap window so the keyring can be created before the
|
|
7270
|
+
* strong recovery is enrolled. Always cleared (try/finally).
|
|
7271
|
+
* @internal
|
|
7272
|
+
*/
|
|
7273
|
+
_skipNextManagedRecoveryCheck = false;
|
|
6276
7274
|
/** Per-vault tier-3 (PIN / quick-resume) state — issue #11. */
|
|
6277
7275
|
quickUnlock = new QuickUnlockStore();
|
|
6278
7276
|
/**
|
|
@@ -6288,6 +7286,17 @@ var Noydb = class {
|
|
|
6288
7286
|
txStrategy;
|
|
6289
7287
|
sessionStrategy;
|
|
6290
7288
|
syncStrategy;
|
|
7289
|
+
/**
|
|
7290
|
+
* Currently-running multi-record transaction, set by
|
|
7291
|
+
* `runTransaction` at the start of Phase 2 (commit) and cleared in
|
|
7292
|
+
* the same function's `finally` block. Side-effect writes triggered
|
|
7293
|
+
* during a staged op's `Collection.put` (today: eager derivation
|
|
7294
|
+
* outputs) register their pre-write envelope on `_executed` here so
|
|
7295
|
+
* a mid-batch failure rolls them back alongside the main staged ops
|
|
7296
|
+
* (#133). `null` outside of Phase 2.
|
|
7297
|
+
* @internal
|
|
7298
|
+
*/
|
|
7299
|
+
_activeTxContext = null;
|
|
6291
7300
|
// ─── plaintextTranslator state ─────────────────────────
|
|
6292
7301
|
/**
|
|
6293
7302
|
* In-process translation cache. Key is `"${field}\x00${collection}\x00${from}\x00${to}\x00${text}"`.
|
|
@@ -6372,7 +7381,7 @@ var Noydb = class {
|
|
|
6372
7381
|
}
|
|
6373
7382
|
return comp;
|
|
6374
7383
|
}
|
|
6375
|
-
const keyring = await this.
|
|
7384
|
+
const keyring = await this.getKeyringInternal(name);
|
|
6376
7385
|
if (!this.activeTier.has(name)) {
|
|
6377
7386
|
this.activeTier.set(name, 1);
|
|
6378
7387
|
}
|
|
@@ -6439,6 +7448,7 @@ var Noydb = class {
|
|
|
6439
7448
|
...this.options.historyStrategy !== void 0 ? { historyStrategy: this.options.historyStrategy } : {},
|
|
6440
7449
|
...this.options.i18nStrategy !== void 0 ? { i18nStrategy: this.options.i18nStrategy } : {},
|
|
6441
7450
|
...this.options.syncStrategy !== void 0 ? { syncStrategy: this.options.syncStrategy } : {},
|
|
7451
|
+
...this.options.guardStrategies !== void 0 ? { guardStrategies: this.options.guardStrategies } : {},
|
|
6442
7452
|
locale: opts?.locale,
|
|
6443
7453
|
// Thread the translator hook so Collection.put() can invoke it
|
|
6444
7454
|
plaintextTranslator: this.options.plaintextTranslator ? (text, from, to, field, collection) => this.invokeTranslator(text, from, to, field, collection) : void 0,
|
|
@@ -6459,6 +7469,10 @@ var Noydb = class {
|
|
|
6459
7469
|
return refreshed;
|
|
6460
7470
|
} : void 0
|
|
6461
7471
|
});
|
|
7472
|
+
await comp._initGuards(this.options.guardStrategies ?? []);
|
|
7473
|
+
await comp._initDerivations(this.options.derivationStrategies ?? []);
|
|
7474
|
+
await comp._initMaterializedViews(this.options.materializedViewStrategies ?? []);
|
|
7475
|
+
await comp._initOverlayedViews(this.options.overlayedViewStrategies ?? []);
|
|
6462
7476
|
this.vaultCache.set(name, comp);
|
|
6463
7477
|
return comp;
|
|
6464
7478
|
}
|
|
@@ -6485,7 +7499,8 @@ var Noydb = class {
|
|
|
6485
7499
|
...this.options.shadowStrategy !== void 0 ? { shadowStrategy: this.options.shadowStrategy } : {},
|
|
6486
7500
|
...this.options.historyStrategy !== void 0 ? { historyStrategy: this.options.historyStrategy } : {},
|
|
6487
7501
|
...this.options.i18nStrategy !== void 0 ? { i18nStrategy: this.options.i18nStrategy } : {},
|
|
6488
|
-
...this.options.syncStrategy !== void 0 ? { syncStrategy: this.options.syncStrategy } : {}
|
|
7502
|
+
...this.options.syncStrategy !== void 0 ? { syncStrategy: this.options.syncStrategy } : {},
|
|
7503
|
+
...this.options.guardStrategies !== void 0 ? { guardStrategies: this.options.guardStrategies } : {}
|
|
6489
7504
|
});
|
|
6490
7505
|
this.vaultCache.set(name, comp2);
|
|
6491
7506
|
return comp2;
|
|
@@ -6513,21 +7528,41 @@ var Noydb = class {
|
|
|
6513
7528
|
...this.options.historyStrategy !== void 0 ? { historyStrategy: this.options.historyStrategy } : {},
|
|
6514
7529
|
...this.options.i18nStrategy !== void 0 ? { i18nStrategy: this.options.i18nStrategy } : {},
|
|
6515
7530
|
...this.options.syncStrategy !== void 0 ? { syncStrategy: this.options.syncStrategy } : {},
|
|
7531
|
+
...this.options.guardStrategies !== void 0 ? { guardStrategies: this.options.guardStrategies } : {},
|
|
6516
7532
|
emitter: this.emitter
|
|
6517
7533
|
});
|
|
6518
7534
|
this.vaultCache.set(name, comp);
|
|
6519
7535
|
return comp;
|
|
6520
7536
|
}
|
|
6521
|
-
/**
|
|
6522
|
-
|
|
7537
|
+
/**
|
|
7538
|
+
* Grant access to a user for a vault.
|
|
7539
|
+
*
|
|
7540
|
+
* Gated by `enroll-user`. `STRICT_POLICY` requires a TOTP / email-OTP
|
|
7541
|
+
* factor proof so the operator affirmatively re-asserts identity at
|
|
7542
|
+
* the moment of grant; `PERSONAL_POLICY` accepts a tier-1 unlock alone.
|
|
7543
|
+
*
|
|
7544
|
+
* The legacy `requireReAuthFor: ['grant']` session-policy check still
|
|
7545
|
+
* fires on top — both are independent opt-ins.
|
|
7546
|
+
*/
|
|
7547
|
+
async grant(vault, options, factors) {
|
|
6523
7548
|
this.checkPolicyOperation(vault, "grant");
|
|
6524
|
-
|
|
7549
|
+
await this.checkGate(vault, "enroll-user", factors);
|
|
7550
|
+
const keyring = await this.getKeyringInternal(vault);
|
|
6525
7551
|
await grant(this.options.store, vault, keyring, options);
|
|
6526
7552
|
}
|
|
6527
|
-
/**
|
|
6528
|
-
|
|
7553
|
+
/**
|
|
7554
|
+
* Revoke a user's access to a vault.
|
|
7555
|
+
*
|
|
7556
|
+
* Gated by `revoke-user`. `STRICT_POLICY` requires a TOTP / email-OTP
|
|
7557
|
+
* factor proof; `PERSONAL_POLICY` accepts a tier-1 unlock alone.
|
|
7558
|
+
*
|
|
7559
|
+
* The legacy `requireReAuthFor: ['revoke']` session-policy check still
|
|
7560
|
+
* fires on top — both are independent opt-ins.
|
|
7561
|
+
*/
|
|
7562
|
+
async revoke(vault, options, factors) {
|
|
6529
7563
|
this.checkPolicyOperation(vault, "revoke");
|
|
6530
|
-
|
|
7564
|
+
await this.checkGate(vault, "revoke-user", factors);
|
|
7565
|
+
const keyring = await this.getKeyringInternal(vault);
|
|
6531
7566
|
await revoke(this.options.store, vault, keyring, options);
|
|
6532
7567
|
}
|
|
6533
7568
|
/**
|
|
@@ -6574,7 +7609,7 @@ var Noydb = class {
|
|
|
6574
7609
|
*/
|
|
6575
7610
|
async updateUser(vault, options, factors) {
|
|
6576
7611
|
await this.checkGate(vault, "update-user", factors);
|
|
6577
|
-
const keyring = await this.
|
|
7612
|
+
const keyring = await this.getKeyringInternal(vault);
|
|
6578
7613
|
await updateKeyringIdentity(this.options.store, vault, keyring, options);
|
|
6579
7614
|
if (options.userId === this.options.user) {
|
|
6580
7615
|
this.keyringCache.delete(vault);
|
|
@@ -6600,7 +7635,7 @@ var Noydb = class {
|
|
|
6600
7635
|
*/
|
|
6601
7636
|
async rotate(vault, collections) {
|
|
6602
7637
|
this.checkPolicyOperation(vault, "rotate");
|
|
6603
|
-
const keyring = await this.
|
|
7638
|
+
const keyring = await this.getKeyringInternal(vault);
|
|
6604
7639
|
await rotateKeys(this.options.store, vault, keyring, collections);
|
|
6605
7640
|
this.keyringCache.set(vault, keyring);
|
|
6606
7641
|
}
|
|
@@ -6703,7 +7738,7 @@ var Noydb = class {
|
|
|
6703
7738
|
this.options.secret
|
|
6704
7739
|
);
|
|
6705
7740
|
} catch (err) {
|
|
6706
|
-
if (err instanceof NoAccessError || err instanceof InvalidKeyError) {
|
|
7741
|
+
if (err instanceof NoAccessError || err instanceof InvalidKeyError || err instanceof KeyringCorruptError) {
|
|
6707
7742
|
continue;
|
|
6708
7743
|
}
|
|
6709
7744
|
throw err;
|
|
@@ -6800,15 +7835,23 @@ var Noydb = class {
|
|
|
6800
7835
|
}
|
|
6801
7836
|
return results;
|
|
6802
7837
|
}
|
|
6803
|
-
/**
|
|
6804
|
-
|
|
7838
|
+
/**
|
|
7839
|
+
* Change the current user's passphrase for a vault.
|
|
7840
|
+
*
|
|
7841
|
+
* Validates the new passphrase against the strength rules. Pass
|
|
7842
|
+
* `{ allowWeakPassphrase: true }` to skip — typically only useful for
|
|
7843
|
+
* fixtures and migrations. Pass a `PassphrasePolicy` to override the
|
|
7844
|
+
* default rules (e.g. consumer-tunable `pattern` / `customValidator`).
|
|
7845
|
+
*/
|
|
7846
|
+
async changeSecret(vault, newPassphrase, options) {
|
|
6805
7847
|
this.checkPolicyOperation(vault, "changeSecret");
|
|
6806
|
-
const keyring = await this.
|
|
7848
|
+
const keyring = await this.getKeyringInternal(vault);
|
|
6807
7849
|
const updated = await changeSecret(
|
|
6808
7850
|
this.options.store,
|
|
6809
7851
|
vault,
|
|
6810
7852
|
keyring,
|
|
6811
|
-
newPassphrase
|
|
7853
|
+
newPassphrase,
|
|
7854
|
+
options
|
|
6812
7855
|
);
|
|
6813
7856
|
this.keyringCache.set(vault, updated);
|
|
6814
7857
|
}
|
|
@@ -6853,10 +7896,18 @@ var Noydb = class {
|
|
|
6853
7896
|
}
|
|
6854
7897
|
return result;
|
|
6855
7898
|
}
|
|
6856
|
-
transaction(arg) {
|
|
7899
|
+
transaction(arg, maybeFn) {
|
|
6857
7900
|
if (typeof arg === "function") {
|
|
6858
7901
|
return this.txStrategy.runTransaction(this, arg);
|
|
6859
7902
|
}
|
|
7903
|
+
if (typeof arg === "object" && arg !== null && arg.amendment === true) {
|
|
7904
|
+
if (typeof maybeFn !== "function") {
|
|
7905
|
+
throw new ValidationError(
|
|
7906
|
+
"db.transaction({ amendment: true }, fn) requires the callback as the second argument."
|
|
7907
|
+
);
|
|
7908
|
+
}
|
|
7909
|
+
return this.txStrategy.runTransaction(this, maybeFn, arg);
|
|
7910
|
+
}
|
|
6860
7911
|
const vault = arg;
|
|
6861
7912
|
const comp = this.vaultCache.get(vault);
|
|
6862
7913
|
if (!comp) {
|
|
@@ -6877,6 +7928,59 @@ var Noydb = class {
|
|
|
6877
7928
|
get _store() {
|
|
6878
7929
|
return this.options.store;
|
|
6879
7930
|
}
|
|
7931
|
+
/**
|
|
7932
|
+
* Currently-running multi-record transaction, or `null` outside
|
|
7933
|
+
* Phase 2. `Collection.dispatchDerivations` consults this so a
|
|
7934
|
+
* recursive derived-output write inside `Collection.put` can register
|
|
7935
|
+
* its envelope onto `ctx._executed` and roll back with the main
|
|
7936
|
+
* staged ops on mid-batch failure (#133).
|
|
7937
|
+
*
|
|
7938
|
+
* @internal
|
|
7939
|
+
*/
|
|
7940
|
+
get _activeTxContextOrNull() {
|
|
7941
|
+
return this._activeTxContext;
|
|
7942
|
+
}
|
|
7943
|
+
/**
|
|
7944
|
+
* Called by `runTransaction` at Phase 2 start, and by
|
|
7945
|
+
* `Collection.putManyAtomic` (via `derivationSource.setActiveTxContext`)
|
|
7946
|
+
* for its own Phase 2 loop. Nested or concurrent (non-nested)
|
|
7947
|
+
* transactions on the same Noydb instance are NOT supported —
|
|
7948
|
+
* overwriting an active context means another transaction is still
|
|
7949
|
+
* running and its `_executed` list would be cross-contaminated by
|
|
7950
|
+
* the nested writes. We tolerate the overwrite (best-effort, no
|
|
7951
|
+
* throw) to keep the rare interleaving from breaking consumers who
|
|
7952
|
+
* currently get lucky with timing, but applications should ensure
|
|
7953
|
+
* their multi-record commits are serialised on a single Noydb.
|
|
7954
|
+
*
|
|
7955
|
+
* @internal
|
|
7956
|
+
*/
|
|
7957
|
+
_setActiveTxContext(ctx) {
|
|
7958
|
+
this._activeTxContext = ctx;
|
|
7959
|
+
}
|
|
7960
|
+
/**
|
|
7961
|
+
* Factory for a transient `TxContext` bound to this Noydb. Used by
|
|
7962
|
+
* `Collection.putManyAtomic` (via `derivationSource.createTxContext`)
|
|
7963
|
+
* to publish an active context for the duration of its bulk-atomic
|
|
7964
|
+
* Phase 2 loop, so recursive derivation-output writes register on
|
|
7965
|
+
* `ctx._executed` and roll back together with the source ops (#133).
|
|
7966
|
+
*
|
|
7967
|
+
* @internal
|
|
7968
|
+
*/
|
|
7969
|
+
_createTxContext() {
|
|
7970
|
+
return new TxContext(this);
|
|
7971
|
+
}
|
|
7972
|
+
/**
|
|
7973
|
+
* Called by `runTransaction` in its `finally`. Only clears when the
|
|
7974
|
+
* passed ctx matches the active one — a defensive no-op if some
|
|
7975
|
+
* other code path already cleared it.
|
|
7976
|
+
*
|
|
7977
|
+
* @internal
|
|
7978
|
+
*/
|
|
7979
|
+
_clearActiveTxContext(ctx) {
|
|
7980
|
+
if (this._activeTxContext === ctx) {
|
|
7981
|
+
this._activeTxContext = null;
|
|
7982
|
+
}
|
|
7983
|
+
}
|
|
6880
7984
|
/** Get sync status for a vault. */
|
|
6881
7985
|
syncStatus(vault) {
|
|
6882
7986
|
const engine = this.syncEngines.get(vault);
|
|
@@ -6885,6 +7989,15 @@ var Noydb = class {
|
|
|
6885
7989
|
}
|
|
6886
7990
|
return engine.status();
|
|
6887
7991
|
}
|
|
7992
|
+
requireShamirProvider() {
|
|
7993
|
+
const p = this.options.shamirRecovery;
|
|
7994
|
+
if (!p) {
|
|
7995
|
+
throw new Error(
|
|
7996
|
+
"shamir recovery requires a ShamirRecoveryProvider \u2014 pass shamirRecovery: shamirRecoveryProvider() from '@noy-db/on-shamir' to createNoydb()"
|
|
7997
|
+
);
|
|
7998
|
+
}
|
|
7999
|
+
return p;
|
|
8000
|
+
}
|
|
6888
8001
|
getSyncEngine(vault) {
|
|
6889
8002
|
const engine = this.syncEngines.get(vault);
|
|
6890
8003
|
if (!engine) {
|
|
@@ -7029,6 +8142,40 @@ var Noydb = class {
|
|
|
7029
8142
|
this.policyCache.set(vault, merged);
|
|
7030
8143
|
return merged;
|
|
7031
8144
|
}
|
|
8145
|
+
/**
|
|
8146
|
+
* Read the current vault-level user-directory toggle (#122). Returns
|
|
8147
|
+
* the default-on shape (`{ enabled: true }`) when no `_meta/directory`
|
|
8148
|
+
* document has been persisted yet.
|
|
8149
|
+
*
|
|
8150
|
+
* No role gate — anyone who can open the vault can read the toggle.
|
|
8151
|
+
*/
|
|
8152
|
+
async getDirectoryEnabled(vault) {
|
|
8153
|
+
if (this.closed) throw new ValidationError("Instance is closed");
|
|
8154
|
+
const persisted = await readDirectoryConfig(this.options.store, vault);
|
|
8155
|
+
return persisted?.enabled ?? true;
|
|
8156
|
+
}
|
|
8157
|
+
/**
|
|
8158
|
+
* Toggle the vault's user-directory listing on or off (#122).
|
|
8159
|
+
* Owner-only. When disabled, `listUsersWithEnvelopes()` throws
|
|
8160
|
+
* {@link import('./errors.js').DirectoryDisabledError} for callers
|
|
8161
|
+
* whose role is neither `owner` nor `admin`.
|
|
8162
|
+
*
|
|
8163
|
+
* Honest caveat: this is a UX flag, not a privacy guarantee. The
|
|
8164
|
+
* keyring file at `_keyring/<userId>` and the envelope ciphertext at
|
|
8165
|
+
* `_users/<keyringId>` remain observable to anyone with direct store
|
|
8166
|
+
* read access — only the hub-level enumeration is gated. See
|
|
8167
|
+
* `docs/subsystems/user-envelope.md` → "Directory visibility".
|
|
8168
|
+
*/
|
|
8169
|
+
async setDirectoryEnabled(vault, enabled) {
|
|
8170
|
+
if (this.closed) throw new ValidationError("Instance is closed");
|
|
8171
|
+
const keyring = await this.getKeyringInternal(vault);
|
|
8172
|
+
if (keyring.role !== "owner") {
|
|
8173
|
+
throw new PermissionDeniedError(
|
|
8174
|
+
`setDirectoryEnabled requires owner role; caller has role "${keyring.role}"`
|
|
8175
|
+
);
|
|
8176
|
+
}
|
|
8177
|
+
await persistDirectoryConfig(this.options.store, vault, { enabled });
|
|
8178
|
+
}
|
|
7032
8179
|
/**
|
|
7033
8180
|
* Evaluate a policy gate against the active session tier and the
|
|
7034
8181
|
* presented factor proofs. Throws {@link PolicyDeniedError} on
|
|
@@ -7039,40 +8186,61 @@ var Noydb = class {
|
|
|
7039
8186
|
* or app-defined (`app:*`).
|
|
7040
8187
|
* @param presented Caller-supplied factor proofs.
|
|
7041
8188
|
*/
|
|
7042
|
-
async checkGate(vault, gate,
|
|
8189
|
+
async checkGate(vault, gate, factors) {
|
|
7043
8190
|
const policy = await this.getPolicy(vault);
|
|
7044
8191
|
const tier = this.activeTier.get(vault) ?? 1;
|
|
7045
8192
|
await checkGate(policy, gate, {
|
|
7046
8193
|
activeTier: tier,
|
|
7047
|
-
...
|
|
7048
|
-
...
|
|
8194
|
+
...factors?.factors !== void 0 ? { factors: factors.factors } : {},
|
|
8195
|
+
...factors?.sharedDevice !== void 0 ? { sharedDevice: factors.sharedDevice } : {}
|
|
7049
8196
|
});
|
|
7050
8197
|
}
|
|
7051
8198
|
/** Read or persist the vault policy at `_meta/policy` on first open. */
|
|
7052
|
-
async bootstrapPolicy(vault) {
|
|
8199
|
+
async bootstrapPolicy(vault, opts) {
|
|
7053
8200
|
const onDisk = await loadVaultPolicy(this.options.store, vault);
|
|
7054
8201
|
if (onDisk) {
|
|
7055
8202
|
this.policyCache.set(vault, onDisk);
|
|
7056
|
-
await this.assertRecoveryEnrolled(vault, onDisk);
|
|
8203
|
+
await this.assertRecoveryEnrolled(vault, onDisk, opts);
|
|
7057
8204
|
return;
|
|
7058
8205
|
}
|
|
7059
8206
|
const initial = this.options.policy ? mergePolicy(PERSONAL_POLICY, this.options.policy) : PERSONAL_POLICY;
|
|
7060
8207
|
await saveVaultPolicy(this.options.store, vault, initial);
|
|
7061
8208
|
this.policyCache.set(vault, initial);
|
|
7062
|
-
await this.assertRecoveryEnrolled(vault, initial);
|
|
7063
|
-
}
|
|
7064
|
-
/**
|
|
7065
|
-
* Throw {@link RecoveryNotEnrolledError}
|
|
7066
|
-
*
|
|
7067
|
-
*
|
|
7068
|
-
*
|
|
7069
|
-
*
|
|
7070
|
-
*
|
|
7071
|
-
*
|
|
7072
|
-
*
|
|
7073
|
-
*
|
|
7074
|
-
|
|
7075
|
-
|
|
8209
|
+
await this.assertRecoveryEnrolled(vault, initial, opts);
|
|
8210
|
+
}
|
|
8211
|
+
/**
|
|
8212
|
+
* Throw {@link RecoveryNotEnrolledError} or
|
|
8213
|
+
* {@link ManagedRecoveryNotEnrolledError} when recovery enrollment
|
|
8214
|
+
* is missing.
|
|
8215
|
+
*
|
|
8216
|
+
* Two enforcement modes:
|
|
8217
|
+
*
|
|
8218
|
+
* 1. **Managed-mode mandatory strong-recovery (#195).** When
|
|
8219
|
+
* `passphraseMode === 'managed'`, the vault MUST have at least
|
|
8220
|
+
* one **strong** recovery profile (Shamir today). Paper alone is
|
|
8221
|
+
* rejected because under managed mode the user has no memorized
|
|
8222
|
+
* passphrase, so losing the paper sheet = losing every record.
|
|
8223
|
+
* This check is unconditional — independent of `requireRecovery`
|
|
8224
|
+
* and the `recover-passphrase` gate.
|
|
8225
|
+
*
|
|
8226
|
+
* 2. **Opt-in strict mandatory-recovery.** When
|
|
8227
|
+
* `requireRecovery: true` is set on createNoydb (and the gate is
|
|
8228
|
+
* not explicitly disabled), require ANY recovery profile (paper
|
|
8229
|
+
* or shamir). This is the v0.x default-off behavior; v1.0 may
|
|
8230
|
+
* flip it default-on.
|
|
8231
|
+
*
|
|
8232
|
+
* The managed-mode check fires from {@link bootstrapPolicy} unless
|
|
8233
|
+
* the `skipManagedCheck` flag is set (used by
|
|
8234
|
+
* {@link openVaultAndEnrollRecovery} to allow atomic create-and-enroll).
|
|
8235
|
+
*/
|
|
8236
|
+
async assertRecoveryEnrolled(vault, policy, opts) {
|
|
8237
|
+
const skipManaged = (opts?.skipManagedCheck ?? false) || this._skipNextManagedRecoveryCheck;
|
|
8238
|
+
if (this.options.passphraseMode === "managed" && !skipManaged) {
|
|
8239
|
+
const enrolled2 = await hasStrongRecoveryEnrolled(this.options.store, vault);
|
|
8240
|
+
if (!enrolled2) {
|
|
8241
|
+
throw new ManagedRecoveryNotEnrolledError(vault);
|
|
8242
|
+
}
|
|
8243
|
+
}
|
|
7076
8244
|
if (this.options.requireRecovery !== true) return;
|
|
7077
8245
|
const gate = policy.gates["recover-passphrase"];
|
|
7078
8246
|
if (gate?.enabled === false) return;
|
|
@@ -7101,9 +8269,9 @@ var Noydb = class {
|
|
|
7101
8269
|
* Gated by `enroll-authenticator`; `presented` carries any factor
|
|
7102
8270
|
* proofs the active policy demands.
|
|
7103
8271
|
*/
|
|
7104
|
-
async enrollAuthenticator(vault, options,
|
|
7105
|
-
await this.checkGate(vault, "enroll-authenticator",
|
|
7106
|
-
const keyring = await this.
|
|
8272
|
+
async enrollAuthenticator(vault, options, factors) {
|
|
8273
|
+
await this.checkGate(vault, "enroll-authenticator", factors);
|
|
8274
|
+
const keyring = await this.getKeyringInternal(vault);
|
|
7107
8275
|
const next = await enrollAuthenticator(this.options.store, vault, keyring, options);
|
|
7108
8276
|
this.keyringCache.set(vault, next);
|
|
7109
8277
|
}
|
|
@@ -7112,15 +8280,15 @@ var Noydb = class {
|
|
|
7112
8280
|
* non-existent slot is a successful no-op. Gated by
|
|
7113
8281
|
* `remove-authenticator`.
|
|
7114
8282
|
*/
|
|
7115
|
-
async removeAuthenticator(vault, slotId,
|
|
7116
|
-
await this.checkGate(vault, "remove-authenticator",
|
|
7117
|
-
const keyring = await this.
|
|
8283
|
+
async removeAuthenticator(vault, slotId, factors) {
|
|
8284
|
+
await this.checkGate(vault, "remove-authenticator", factors);
|
|
8285
|
+
const keyring = await this.getKeyringInternal(vault);
|
|
7118
8286
|
const next = await removeAuthenticator(this.options.store, vault, keyring, slotId);
|
|
7119
8287
|
this.keyringCache.set(vault, next);
|
|
7120
8288
|
}
|
|
7121
8289
|
/** Read the slot list for a vault. Internal — `describeAuthConfig` (#13) consumes this. */
|
|
7122
8290
|
async listAuthenticators(vault) {
|
|
7123
|
-
const keyring = await this.
|
|
8291
|
+
const keyring = await this.getKeyringInternal(vault);
|
|
7124
8292
|
return keyring.authenticators;
|
|
7125
8293
|
}
|
|
7126
8294
|
/**
|
|
@@ -7151,9 +8319,9 @@ var Noydb = class {
|
|
|
7151
8319
|
*
|
|
7152
8320
|
* @see #55
|
|
7153
8321
|
*/
|
|
7154
|
-
async updateAuthenticator(vault, slotId, options,
|
|
7155
|
-
await this.checkGate(vault, "update-authenticator",
|
|
7156
|
-
const keyring = await this.
|
|
8322
|
+
async updateAuthenticator(vault, slotId, options, factors) {
|
|
8323
|
+
await this.checkGate(vault, "update-authenticator", factors);
|
|
8324
|
+
const keyring = await this.getKeyringInternal(vault);
|
|
7157
8325
|
const next = await updateAuthenticator(this.options.store, vault, keyring, slotId, options);
|
|
7158
8326
|
this.keyringCache.set(vault, next);
|
|
7159
8327
|
}
|
|
@@ -7204,9 +8372,9 @@ var Noydb = class {
|
|
|
7204
8372
|
*
|
|
7205
8373
|
* @see #16
|
|
7206
8374
|
*/
|
|
7207
|
-
async enrollWebAuthn(vault, ceremony,
|
|
7208
|
-
await this.checkGate(vault, "enroll-authenticator",
|
|
7209
|
-
const keyring = await this.
|
|
8375
|
+
async enrollWebAuthn(vault, ceremony, factors) {
|
|
8376
|
+
await this.checkGate(vault, "enroll-authenticator", factors);
|
|
8377
|
+
const keyring = await this.getKeyringInternal(vault);
|
|
7210
8378
|
const slotOptions = await ceremony(keyring);
|
|
7211
8379
|
if (slotOptions.method !== "webauthn") {
|
|
7212
8380
|
throw new ValidationError(
|
|
@@ -7233,7 +8401,7 @@ var Noydb = class {
|
|
|
7233
8401
|
* @see #16
|
|
7234
8402
|
*/
|
|
7235
8403
|
async listWebAuthnSlots(vault) {
|
|
7236
|
-
const keyring = await this.
|
|
8404
|
+
const keyring = await this.getKeyringInternal(vault);
|
|
7237
8405
|
return keyring.authenticators.filter((a) => a.method === "webauthn").map((a) => {
|
|
7238
8406
|
const credentialId = a.meta.credentialId;
|
|
7239
8407
|
return {
|
|
@@ -7253,7 +8421,7 @@ var Noydb = class {
|
|
|
7253
8421
|
* `checkGate` calls see a tier-2 unlock.
|
|
7254
8422
|
*/
|
|
7255
8423
|
async unlockViaAuthenticator(vault, slotId, verify) {
|
|
7256
|
-
const keyring = await this.
|
|
8424
|
+
const keyring = await this.getKeyringInternal(vault);
|
|
7257
8425
|
const slot = findAuthenticator(keyring, slotId);
|
|
7258
8426
|
if (!slot) {
|
|
7259
8427
|
throw new ValidationError(
|
|
@@ -7351,6 +8519,14 @@ var Noydb = class {
|
|
|
7351
8519
|
* @throws `InvalidKeyError` when `oldPassphrase` is wrong.
|
|
7352
8520
|
*/
|
|
7353
8521
|
async rotatePassphrase(vault, input, factors) {
|
|
8522
|
+
if (this.options.passphraseMode === "managed") {
|
|
8523
|
+
throw new PolicyDeniedError(
|
|
8524
|
+
"rotate-passphrase",
|
|
8525
|
+
"disabled",
|
|
8526
|
+
{ minTier: 1, enabled: false },
|
|
8527
|
+
"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."
|
|
8528
|
+
);
|
|
8529
|
+
}
|
|
7354
8530
|
await this.checkGate(vault, "rotate-passphrase", factors);
|
|
7355
8531
|
const userId = this.options.user;
|
|
7356
8532
|
const next = await rotatePassphrase(this.options.store, vault, userId, input);
|
|
@@ -7367,7 +8543,7 @@ var Noydb = class {
|
|
|
7367
8543
|
await this.checkGate(vault, "recover-passphrase", factors);
|
|
7368
8544
|
const userId = this.options.user;
|
|
7369
8545
|
const entriesBeforeRecovery = await loadPaperRecoveryEntries(this.options.store, vault);
|
|
7370
|
-
const next = await recoverPassphrase(this.options.store, vault, userId, input);
|
|
8546
|
+
const next = await recoverPassphrase(this.options.shamirRecovery, this.options.store, vault, userId, input);
|
|
7371
8547
|
this.keyringCache.set(vault, next);
|
|
7372
8548
|
const rotateRemaining = input.rotateRemainingCodes ?? true;
|
|
7373
8549
|
const remainingAfterBurn = Math.max(0, entriesBeforeRecovery.length - 1);
|
|
@@ -7387,6 +8563,256 @@ var Noydb = class {
|
|
|
7387
8563
|
await savePaperRecoveryEntries(this.options.store, vault, newEntries);
|
|
7388
8564
|
return { newCodes: codes };
|
|
7389
8565
|
}
|
|
8566
|
+
/**
|
|
8567
|
+
* Deliberate paper-recovery-code regeneration (#121). User knows their
|
|
8568
|
+
* passphrase but wants a fresh sheet — they lost the printout or
|
|
8569
|
+
* suspect compromise of the off-site copy.
|
|
8570
|
+
*
|
|
8571
|
+
* Symmetric to {@link rotatePassphrase} for the recovery profile:
|
|
8572
|
+
* gated, audit-trackable, ergonomic. Replaces (not appends) the
|
|
8573
|
+
* paper sheet under `_meta/recovery-paper` in a single envelope `put`.
|
|
8574
|
+
*
|
|
8575
|
+
* Gated by the `rotate-recovery` policy gate:
|
|
8576
|
+
* - PERSONAL_POLICY: `{ minTier: 1 }` — knowing the passphrase
|
|
8577
|
+
* suffices, matching the pre-#121 low-level flow's bar.
|
|
8578
|
+
* - STRICT_POLICY: `{ minTier: 1, factors: [{ anyOf: ['totp',
|
|
8579
|
+
* 'email-otp', 'webauthn-roaming'] }] }` — rotation is an
|
|
8580
|
+
* off-site-trust event; require an off-device factor so a
|
|
8581
|
+
* stolen unlocked laptop cannot silently mint a sheet for the
|
|
8582
|
+
* attacker.
|
|
8583
|
+
*
|
|
8584
|
+
* Defaults `count` to the existing sheet size so consumers aren't
|
|
8585
|
+
* surprised by a different code count. Explicit `count` overrides.
|
|
8586
|
+
*
|
|
8587
|
+
* @throws {@link RecoveryProfileNotImplementedError} when `profile`
|
|
8588
|
+
* is anything other than `'paper'` (v1 dispatch limit).
|
|
8589
|
+
* @throws {@link PolicyDeniedError} when the gate denies (missing
|
|
8590
|
+
* factor, tier mismatch, ...).
|
|
8591
|
+
* @throws on missing paper sheet — "nothing to rotate" surfaces as
|
|
8592
|
+
* an error rather than silently minting an entire new sheet.
|
|
8593
|
+
*
|
|
8594
|
+
* @example Default count + show-once UI
|
|
8595
|
+
* ```ts
|
|
8596
|
+
* const { newCodes } = await db.rotateRecovery('acme', { profile: 'paper' })
|
|
8597
|
+
* showCodesToUser(newCodes)
|
|
8598
|
+
* ```
|
|
8599
|
+
*
|
|
8600
|
+
* @example STRICT-policy site with TOTP factor proof
|
|
8601
|
+
* ```ts
|
|
8602
|
+
* await db.rotateRecovery(
|
|
8603
|
+
* 'acme',
|
|
8604
|
+
* { profile: 'paper', count: 10 },
|
|
8605
|
+
* { factors: [{ kind: 'totp', proof: '123456' }] },
|
|
8606
|
+
* )
|
|
8607
|
+
* ```
|
|
8608
|
+
*/
|
|
8609
|
+
async rotateRecovery(vault, options, factors) {
|
|
8610
|
+
if (options.profile === "paper") {
|
|
8611
|
+
return this.rotateRecoveryPaper(vault, options, factors);
|
|
8612
|
+
}
|
|
8613
|
+
if (options.profile === "shamir") {
|
|
8614
|
+
return this.rotateRecoveryShamir(vault, options, factors);
|
|
8615
|
+
}
|
|
8616
|
+
throw new RecoveryProfileNotImplementedError(
|
|
8617
|
+
options.profile,
|
|
8618
|
+
"#196"
|
|
8619
|
+
);
|
|
8620
|
+
}
|
|
8621
|
+
async rotateRecoveryPaper(vault, options, factors) {
|
|
8622
|
+
await this.checkGate(vault, "rotate-recovery", factors);
|
|
8623
|
+
const existing = await loadPaperRecoveryEntries(this.options.store, vault);
|
|
8624
|
+
if (existing.length === 0) {
|
|
8625
|
+
throw new Error(
|
|
8626
|
+
`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.`
|
|
8627
|
+
);
|
|
8628
|
+
}
|
|
8629
|
+
const keyring = await this.getKeyring(vault);
|
|
8630
|
+
const codeGen = options.codeGenerator ?? generateULID;
|
|
8631
|
+
const count2 = options.count ?? existing.length;
|
|
8632
|
+
const codes = [];
|
|
8633
|
+
const newEntries = [];
|
|
8634
|
+
for (let i = 0; i < count2; i++) {
|
|
8635
|
+
const rawCode = codeGen();
|
|
8636
|
+
const entry = await mintPaperRecoveryEntry(keyring.deks, rawCode, generateULID());
|
|
8637
|
+
codes.push(rawCode);
|
|
8638
|
+
newEntries.push(entry);
|
|
8639
|
+
}
|
|
8640
|
+
await savePaperRecoveryEntries(this.options.store, vault, newEntries);
|
|
8641
|
+
return { newCodes: codes, entryId: "paper-batch" };
|
|
8642
|
+
}
|
|
8643
|
+
async rotateRecoveryShamir(vault, options, factors) {
|
|
8644
|
+
await this.checkGate(vault, "rotate-recovery", factors);
|
|
8645
|
+
const existing = await loadShamirRecoveryEntries(this.options.store, vault);
|
|
8646
|
+
if (existing.length === 0) {
|
|
8647
|
+
throw new Error(
|
|
8648
|
+
`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.`
|
|
8649
|
+
);
|
|
8650
|
+
}
|
|
8651
|
+
let targetEntryId;
|
|
8652
|
+
if (options.entryId !== void 0) {
|
|
8653
|
+
const found = existing.find((e) => e.entryId === options.entryId);
|
|
8654
|
+
if (!found) {
|
|
8655
|
+
throw new Error(
|
|
8656
|
+
`db.rotateRecovery: no Shamir entry with entryId="${options.entryId}" found in vault "${vault}". Available: ${existing.map((e) => `"${e.entryId}"`).join(", ")}.`
|
|
8657
|
+
);
|
|
8658
|
+
}
|
|
8659
|
+
targetEntryId = options.entryId;
|
|
8660
|
+
} else {
|
|
8661
|
+
if (existing.length > 1) {
|
|
8662
|
+
throw new Error(
|
|
8663
|
+
`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.`
|
|
8664
|
+
);
|
|
8665
|
+
}
|
|
8666
|
+
targetEntryId = existing[0].entryId;
|
|
8667
|
+
}
|
|
8668
|
+
const keyring = await this.getKeyring(vault);
|
|
8669
|
+
const { entry, shareStrings } = await mintShamirRecoveryEntry(
|
|
8670
|
+
this.requireShamirProvider(),
|
|
8671
|
+
keyring.deks,
|
|
8672
|
+
targetEntryId,
|
|
8673
|
+
options.k,
|
|
8674
|
+
options.n,
|
|
8675
|
+
options.label
|
|
8676
|
+
);
|
|
8677
|
+
const next = existing.filter((e) => e.entryId !== targetEntryId).concat(entry);
|
|
8678
|
+
await saveShamirRecoveryEntries(this.options.store, vault, next);
|
|
8679
|
+
return { newShares: shareStrings, entryId: targetEntryId };
|
|
8680
|
+
}
|
|
8681
|
+
/**
|
|
8682
|
+
* **Atomic create-and-enroll for managed-mode vaults (#195).**
|
|
8683
|
+
*
|
|
8684
|
+
* Bootstraps a managed-mode vault and enrolls strong recovery in
|
|
8685
|
+
* a single ceremony. Under `passphraseMode: 'managed'`, every
|
|
8686
|
+
* `openVault` call requires a strong recovery profile (Shamir
|
|
8687
|
+
* today) to be enrolled — otherwise it throws
|
|
8688
|
+
* {@link ManagedRecoveryNotEnrolledError}. This method bypasses
|
|
8689
|
+
* the check temporarily so the keyring can be created, enrolls
|
|
8690
|
+
* the supplied recovery profile(s), then returns the vault.
|
|
8691
|
+
*
|
|
8692
|
+
* For Shamir enrollments, the show-once share strings come back
|
|
8693
|
+
* in `recoveryEnrollments[i].shares`. The hub never retains them
|
|
8694
|
+
* — the caller MUST display them to the user (once) before any
|
|
8695
|
+
* subsequent operation.
|
|
8696
|
+
*
|
|
8697
|
+
* Paper alone is NOT a strong profile under managed mode; passing
|
|
8698
|
+
* `{ profile: 'paper', ... }` without an accompanying shamir entry
|
|
8699
|
+
* is rejected at validation time.
|
|
8700
|
+
*
|
|
8701
|
+
* ```ts
|
|
8702
|
+
* const db = await createNoydb({
|
|
8703
|
+
* store, user: 'alice',
|
|
8704
|
+
* passphraseMode: 'managed',
|
|
8705
|
+
* sealingKey: macosKeychainSealingProvider({ ... }),
|
|
8706
|
+
* })
|
|
8707
|
+
*
|
|
8708
|
+
* const { vault, recoveryEnrollments } = await db.openVaultAndEnrollRecovery('acme', {
|
|
8709
|
+
* recovery: [{ profile: 'shamir', k: 2, n: 3 }],
|
|
8710
|
+
* })
|
|
8711
|
+
* for (const r of recoveryEnrollments) {
|
|
8712
|
+
* if (r.shares) showSharesToUser(r.shares) // ONCE
|
|
8713
|
+
* }
|
|
8714
|
+
* ```
|
|
8715
|
+
*
|
|
8716
|
+
* @throws ValidationError if recovery is empty, or contains no
|
|
8717
|
+
* strong profile under managed mode.
|
|
8718
|
+
*/
|
|
8719
|
+
async openVaultAndEnrollRecovery(vault, opts) {
|
|
8720
|
+
if (opts.recovery.length === 0) {
|
|
8721
|
+
throw new ValidationError(
|
|
8722
|
+
"openVaultAndEnrollRecovery: at least one recovery enrollment is required."
|
|
8723
|
+
);
|
|
8724
|
+
}
|
|
8725
|
+
if (this.options.passphraseMode === "managed") {
|
|
8726
|
+
const hasStrong = opts.recovery.some((r) => r.profile === "shamir");
|
|
8727
|
+
if (!hasStrong) {
|
|
8728
|
+
throw new ValidationError(
|
|
8729
|
+
'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`.'
|
|
8730
|
+
);
|
|
8731
|
+
}
|
|
8732
|
+
}
|
|
8733
|
+
this._skipNextManagedRecoveryCheck = true;
|
|
8734
|
+
let vaultHandle;
|
|
8735
|
+
try {
|
|
8736
|
+
vaultHandle = await this.openVault(vault, opts.locale !== void 0 ? { locale: opts.locale } : void 0);
|
|
8737
|
+
} finally {
|
|
8738
|
+
this._skipNextManagedRecoveryCheck = false;
|
|
8739
|
+
}
|
|
8740
|
+
const recoveryEnrollments = [];
|
|
8741
|
+
for (const enrollment of opts.recovery) {
|
|
8742
|
+
recoveryEnrollments.push(await this.enrollRecovery(vault, enrollment));
|
|
8743
|
+
}
|
|
8744
|
+
if (this.options.passphraseMode === "managed") {
|
|
8745
|
+
const policy = this.policyCache.get(vault);
|
|
8746
|
+
if (policy) {
|
|
8747
|
+
await this.assertRecoveryEnrolled(vault, policy);
|
|
8748
|
+
}
|
|
8749
|
+
}
|
|
8750
|
+
return { vault: vaultHandle, recoveryEnrollments };
|
|
8751
|
+
}
|
|
8752
|
+
/**
|
|
8753
|
+
* **Recovery flow under managed-passphrase mode (#195).**
|
|
8754
|
+
*
|
|
8755
|
+
* Replaces the sealed passphrase of a managed-mode vault with a
|
|
8756
|
+
* fresh 256-bit random, sealed under the configured
|
|
8757
|
+
* `SealingKeyProvider`. The user never sees the new passphrase.
|
|
8758
|
+
*
|
|
8759
|
+
* Internally:
|
|
8760
|
+
* 1. Verify the recovery proof (Shamir today) and unwrap the
|
|
8761
|
+
* DEK set.
|
|
8762
|
+
* 2. Mint a fresh 256-bit random as the new effective passphrase.
|
|
8763
|
+
* 3. Rewrap the DEK set under a fresh KEK derived from the new
|
|
8764
|
+
* passphrase (via the existing `recoverPassphrase` path).
|
|
8765
|
+
* 4. Seal the random bytes under the provider and overwrite
|
|
8766
|
+
* `_meta/sealed-passphrase`.
|
|
8767
|
+
* 5. Drop the keyring cache so the next operation re-derives.
|
|
8768
|
+
*
|
|
8769
|
+
* The vault's strong-recovery enrollment is preserved across
|
|
8770
|
+
* recovery (Shamir entries are not burned on use — see #196).
|
|
8771
|
+
*
|
|
8772
|
+
* @throws ValidationError if the Noydb instance is not in managed mode.
|
|
8773
|
+
*/
|
|
8774
|
+
async recoverManagedPassphrase(vault, options) {
|
|
8775
|
+
if (this.options.passphraseMode !== "managed") {
|
|
8776
|
+
throw new ValidationError(
|
|
8777
|
+
"recoverManagedPassphrase: this method only applies to vaults opened in managed-passphrase mode. For standard mode, use db.recoverPassphrase."
|
|
8778
|
+
);
|
|
8779
|
+
}
|
|
8780
|
+
const provider = this.options.sealingKey;
|
|
8781
|
+
if (!provider) {
|
|
8782
|
+
throw new ValidationError(
|
|
8783
|
+
'recoverManagedPassphrase: createNoydb({ passphraseMode: "managed" }) requires `sealingKey` to be supplied; without it the new sealed passphrase cannot be persisted.'
|
|
8784
|
+
);
|
|
8785
|
+
}
|
|
8786
|
+
const randomBytes = new Uint8Array(32);
|
|
8787
|
+
globalThis.crypto.getRandomValues(randomBytes);
|
|
8788
|
+
let binary = "";
|
|
8789
|
+
for (let i = 0; i < randomBytes.length; i++) binary += String.fromCharCode(randomBytes[i]);
|
|
8790
|
+
const newPassphrase = btoa(binary);
|
|
8791
|
+
try {
|
|
8792
|
+
const sealed = await provider.seal(randomBytes);
|
|
8793
|
+
await recoverPassphrase(
|
|
8794
|
+
this.options.shamirRecovery,
|
|
8795
|
+
this.options.store,
|
|
8796
|
+
vault,
|
|
8797
|
+
this.options.user,
|
|
8798
|
+
{
|
|
8799
|
+
newPassphrase,
|
|
8800
|
+
recoveryProof: options.recoveryProof,
|
|
8801
|
+
// The new passphrase IS 256 bits of random; policy gates on
|
|
8802
|
+
// length/entropy don't apply.
|
|
8803
|
+
allowWeakPassphrase: true,
|
|
8804
|
+
...options.passphrasePolicy !== void 0 ? { passphrasePolicy: options.passphrasePolicy } : {}
|
|
8805
|
+
}
|
|
8806
|
+
);
|
|
8807
|
+
await saveSealedPassphrase(this.options.store, vault, {
|
|
8808
|
+
providerId: provider.id,
|
|
8809
|
+
sealed
|
|
8810
|
+
});
|
|
8811
|
+
} finally {
|
|
8812
|
+
randomBytes.fill(0);
|
|
8813
|
+
}
|
|
8814
|
+
this.keyringCache.delete(vault);
|
|
8815
|
+
}
|
|
7390
8816
|
/**
|
|
7391
8817
|
* Atomic peer-recovery — re-wraps an EXISTING user's keyring under
|
|
7392
8818
|
* a fresh temp passphrase in a single store write. Closes #34's
|
|
@@ -7432,7 +8858,7 @@ var Noydb = class {
|
|
|
7432
8858
|
*/
|
|
7433
8859
|
async recoverUser(vault, options, factors) {
|
|
7434
8860
|
await this.checkGate(vault, "peer-recover-user", factors);
|
|
7435
|
-
const callerKeyring = await this.
|
|
8861
|
+
const callerKeyring = await this.getKeyringInternal(vault);
|
|
7436
8862
|
await recoverUser(this.options.store, vault, callerKeyring, options);
|
|
7437
8863
|
if (options.userId === this.options.user) {
|
|
7438
8864
|
this.keyringCache.delete(vault);
|
|
@@ -7470,21 +8896,40 @@ var Noydb = class {
|
|
|
7470
8896
|
* ```
|
|
7471
8897
|
*/
|
|
7472
8898
|
async enrollRecovery(vault, enrollment) {
|
|
7473
|
-
if (enrollment.profile
|
|
7474
|
-
|
|
7475
|
-
|
|
8899
|
+
if (enrollment.profile === "paper") {
|
|
8900
|
+
const existing = await loadPaperRecoveryEntries(this.options.store, vault);
|
|
8901
|
+
await savePaperRecoveryEntries(this.options.store, vault, [
|
|
8902
|
+
...existing,
|
|
8903
|
+
...enrollment.entries
|
|
8904
|
+
]);
|
|
8905
|
+
return { entryId: "paper-batch" };
|
|
8906
|
+
}
|
|
8907
|
+
if (enrollment.profile === "shamir") {
|
|
8908
|
+
const keyring = await this.getKeyring(vault);
|
|
8909
|
+
const entryId = enrollment.entryId ?? generateULID();
|
|
8910
|
+
const { entry, shareStrings } = await mintShamirRecoveryEntry(
|
|
8911
|
+
this.requireShamirProvider(),
|
|
8912
|
+
keyring.deks,
|
|
8913
|
+
entryId,
|
|
8914
|
+
enrollment.k,
|
|
8915
|
+
enrollment.n,
|
|
8916
|
+
enrollment.label
|
|
7476
8917
|
);
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
8918
|
+
const existing = await loadShamirRecoveryEntries(this.options.store, vault);
|
|
8919
|
+
const next = existing.filter((e) => e.entryId !== entryId).concat(entry);
|
|
8920
|
+
await saveShamirRecoveryEntries(this.options.store, vault, next);
|
|
8921
|
+
return { entryId, shares: shareStrings };
|
|
8922
|
+
}
|
|
8923
|
+
throw new RecoveryProfileNotImplementedError(
|
|
8924
|
+
enrollment.profile,
|
|
8925
|
+
"#196"
|
|
8926
|
+
);
|
|
7483
8927
|
}
|
|
7484
|
-
/** Read the persisted
|
|
8928
|
+
/** Read the persisted recovery entries (paper + Shamir). Used by `describeAuthConfig` (#13). */
|
|
7485
8929
|
async listRecoveryEntries(vault) {
|
|
7486
8930
|
const paper = await loadPaperRecoveryEntries(this.options.store, vault);
|
|
7487
|
-
|
|
8931
|
+
const shamir = await loadShamirRecoveryEntries(this.options.store, vault);
|
|
8932
|
+
return { paper, shamir };
|
|
7488
8933
|
}
|
|
7489
8934
|
// ─── Tier-3 enroll / unlock (issue #11) ────────────────────────
|
|
7490
8935
|
/**
|
|
@@ -7496,8 +8941,8 @@ var Noydb = class {
|
|
|
7496
8941
|
* Gated by `rotate-unlock` (the same gate covers "set" and "rotate"
|
|
7497
8942
|
* because tier-3 is a single-slot rolling secret).
|
|
7498
8943
|
*/
|
|
7499
|
-
async enrollUnlock(vault, state,
|
|
7500
|
-
await this.checkGate(vault, "rotate-unlock",
|
|
8944
|
+
async enrollUnlock(vault, state, factors) {
|
|
8945
|
+
await this.checkGate(vault, "rotate-unlock", factors);
|
|
7501
8946
|
this.quickUnlock.set(vault, state);
|
|
7502
8947
|
}
|
|
7503
8948
|
/**
|
|
@@ -7524,8 +8969,17 @@ var Noydb = class {
|
|
|
7524
8969
|
/**
|
|
7525
8970
|
* Public accessor for the unlocked keyring of a vault — issue #28.
|
|
7526
8971
|
*
|
|
7527
|
-
* Returns
|
|
7528
|
-
*
|
|
8972
|
+
* Returns a **defensive shallow copy** so consumers can read the DEK
|
|
8973
|
+
* map and authenticator list without the risk of mutating the hub's
|
|
8974
|
+
* internal cache (#88). Internal hub code paths use a live reference
|
|
8975
|
+
* via `getKeyringInternal`; ceremonies and external consumers always
|
|
8976
|
+
* get a snapshot.
|
|
8977
|
+
*
|
|
8978
|
+
* The CryptoKey values inside `deks` are not cloned — Web Crypto
|
|
8979
|
+
* keys are opaque handles, and a shared handle is intentional
|
|
8980
|
+
* (encrypt / decrypt go through the same key the cache holds).
|
|
8981
|
+
* Only the container Map / authenticator array is fresh.
|
|
8982
|
+
*
|
|
7529
8983
|
* Used by `@noy-db/on-*` ceremonies that need the live DEK set
|
|
7530
8984
|
* (paper recovery via {@link mintPaperRecoveryEntry}, tier-3 PIN
|
|
7531
8985
|
* enrolment via on-pin's `enrollPin`, custom on-* ceremonies that
|
|
@@ -7540,11 +8994,33 @@ var Noydb = class {
|
|
|
7540
8994
|
* ```ts
|
|
7541
8995
|
* const keyring = await db.getKeyring('acme')
|
|
7542
8996
|
* // keyring.deks: Map<collection, CryptoKey>
|
|
7543
|
-
* // keyring.kek: CryptoKey (
|
|
8997
|
+
* // keyring.kek: CryptoKey | null (null for tier-3 / wrap-DEKs sessions)
|
|
7544
8998
|
* // keyring.role / .permissions / .authenticators
|
|
7545
8999
|
* ```
|
|
7546
9000
|
*/
|
|
7547
9001
|
async getKeyring(vault) {
|
|
9002
|
+
const live = await this.getKeyringInternal(vault);
|
|
9003
|
+
return {
|
|
9004
|
+
...live,
|
|
9005
|
+
deks: new Map(live.deks),
|
|
9006
|
+
permissions: { ...live.permissions },
|
|
9007
|
+
authenticators: live.authenticators.map((a) => ({
|
|
9008
|
+
...a,
|
|
9009
|
+
meta: { ...a.meta }
|
|
9010
|
+
})),
|
|
9011
|
+
...live.policy !== void 0 ? { policy: { ...live.policy } } : {},
|
|
9012
|
+
...live.exportCapability !== void 0 ? { exportCapability: { ...live.exportCapability } } : {},
|
|
9013
|
+
...live.importCapability !== void 0 ? { importCapability: { ...live.importCapability } } : {}
|
|
9014
|
+
};
|
|
9015
|
+
}
|
|
9016
|
+
/**
|
|
9017
|
+
* Live-reference variant used by the hub's own code paths. Internal
|
|
9018
|
+
* mutations on `deks` (e.g. {@link ensureCollectionDEK} adding a
|
|
9019
|
+
* collection key) need to land on the cached keyring so subsequent
|
|
9020
|
+
* accesses see them. Not exposed publicly — callers outside hub
|
|
9021
|
+
* should use {@link getKeyring}, which returns a defensive copy.
|
|
9022
|
+
*/
|
|
9023
|
+
async getKeyringInternal(vault) {
|
|
7548
9024
|
if (this.options.encrypt === false) {
|
|
7549
9025
|
return createPlaintextKeyring(this.options.user);
|
|
7550
9026
|
}
|
|
@@ -7555,20 +9031,36 @@ var Noydb = class {
|
|
|
7555
9031
|
this.keyringCache.set(vault, keyring2);
|
|
7556
9032
|
return keyring2;
|
|
7557
9033
|
}
|
|
7558
|
-
|
|
9034
|
+
let effectiveSecret;
|
|
9035
|
+
if (this.options.passphraseMode === "managed") {
|
|
9036
|
+
effectiveSecret = await resolveManagedSecret(
|
|
9037
|
+
this.options.store,
|
|
9038
|
+
vault,
|
|
9039
|
+
this.options.sealingKey
|
|
9040
|
+
);
|
|
9041
|
+
} else {
|
|
9042
|
+
effectiveSecret = this.options.secret;
|
|
9043
|
+
}
|
|
9044
|
+
if (!effectiveSecret) {
|
|
7559
9045
|
throw new ValidationError("A secret (passphrase) or getKeyring callback is required when encryption is enabled");
|
|
7560
9046
|
}
|
|
7561
9047
|
let keyring;
|
|
7562
9048
|
try {
|
|
7563
|
-
keyring = await loadKeyring(this.options.store, vault, this.options.user,
|
|
9049
|
+
keyring = await loadKeyring(this.options.store, vault, this.options.user, effectiveSecret);
|
|
7564
9050
|
} catch (err) {
|
|
7565
9051
|
if (err instanceof NoAccessError) {
|
|
7566
9052
|
keyring = await createOwnerKeyring(
|
|
7567
9053
|
this.options.store,
|
|
7568
9054
|
vault,
|
|
7569
9055
|
this.options.user,
|
|
7570
|
-
|
|
7571
|
-
{
|
|
9056
|
+
effectiveSecret,
|
|
9057
|
+
{
|
|
9058
|
+
// Managed mode generates 256-bit base64 strings that don't satisfy
|
|
9059
|
+
// the human-passphrase strength rules (no spaces, no "words").
|
|
9060
|
+
// Skip validation in managed mode — the entropy floor is already
|
|
9061
|
+
// 256 bits by construction.
|
|
9062
|
+
validate: this.options.passphraseMode === "managed" ? false : this.options.validatePassphrase === true
|
|
9063
|
+
}
|
|
7572
9064
|
);
|
|
7573
9065
|
} else if (err instanceof InvalidKeyError && this.options.onInvalidKey === "reset") {
|
|
7574
9066
|
await this.options.store.delete(vault, "_keyring", this.options.user);
|
|
@@ -7576,8 +9068,10 @@ var Noydb = class {
|
|
|
7576
9068
|
this.options.store,
|
|
7577
9069
|
vault,
|
|
7578
9070
|
this.options.user,
|
|
7579
|
-
|
|
7580
|
-
{
|
|
9071
|
+
effectiveSecret,
|
|
9072
|
+
{
|
|
9073
|
+
validate: this.options.passphraseMode === "managed" ? false : this.options.validatePassphrase === true
|
|
9074
|
+
}
|
|
7581
9075
|
);
|
|
7582
9076
|
} else {
|
|
7583
9077
|
throw err;
|
|
@@ -7589,10 +9083,28 @@ var Noydb = class {
|
|
|
7589
9083
|
};
|
|
7590
9084
|
async function createNoydb(options) {
|
|
7591
9085
|
const encrypted = options.encrypt !== false;
|
|
9086
|
+
const managed = options.passphraseMode === "managed";
|
|
7592
9087
|
if (options.secret && options.getKeyring) {
|
|
7593
9088
|
throw new ValidationError("Provide either `secret` or `getKeyring`, not both");
|
|
7594
9089
|
}
|
|
7595
|
-
if (
|
|
9090
|
+
if (managed) {
|
|
9091
|
+
if (options.secret) {
|
|
9092
|
+
throw new ValidationError(
|
|
9093
|
+
'`passphraseMode: "managed"` is mutually exclusive with `secret` \u2014 managed mode generates the passphrase itself. Drop `secret`.'
|
|
9094
|
+
);
|
|
9095
|
+
}
|
|
9096
|
+
if (options.getKeyring) {
|
|
9097
|
+
throw new ValidationError(
|
|
9098
|
+
'`passphraseMode: "managed"` is mutually exclusive with `getKeyring` \u2014 a custom unlock callback would bypass the sealing flow. Drop `getKeyring`.'
|
|
9099
|
+
);
|
|
9100
|
+
}
|
|
9101
|
+
if (!options.sealingKey) {
|
|
9102
|
+
throw new ValidationError(
|
|
9103
|
+
'`passphraseMode: "managed"` requires `sealingKey: SealingKeyProvider` (see @noy-db/seal-macos-keychain / @noy-db/seal-aws-kms / etc.).'
|
|
9104
|
+
);
|
|
9105
|
+
}
|
|
9106
|
+
}
|
|
9107
|
+
if (encrypted && !managed && !options.secret && !options.getKeyring) {
|
|
7596
9108
|
throw new ValidationError("A secret (passphrase) or getKeyring callback is required when encryption is enabled");
|
|
7597
9109
|
}
|
|
7598
9110
|
return new Noydb(options);
|
|
@@ -7760,6 +9272,7 @@ function shortJSON(value) {
|
|
|
7760
9272
|
export {
|
|
7761
9273
|
Aggregation,
|
|
7762
9274
|
AlreadyElevatedError,
|
|
9275
|
+
AmendmentForbiddenError,
|
|
7763
9276
|
BLOB_CHUNKS_COLLECTION,
|
|
7764
9277
|
BLOB_COLLECTION,
|
|
7765
9278
|
BLOB_INDEX_COLLECTION,
|
|
@@ -7770,6 +9283,7 @@ export {
|
|
|
7770
9283
|
BackupLedgerError,
|
|
7771
9284
|
BlobSet,
|
|
7772
9285
|
BundleIntegrityError,
|
|
9286
|
+
BundleSealMismatchError,
|
|
7773
9287
|
BundleVersionConflictError,
|
|
7774
9288
|
CONSENT_AUDIT_COLLECTION,
|
|
7775
9289
|
Collection,
|
|
@@ -7783,28 +9297,39 @@ export {
|
|
|
7783
9297
|
DEFAULT_PUBLIC_ENVELOPE_SCHEMA,
|
|
7784
9298
|
DELEGATIONS_COLLECTION,
|
|
7785
9299
|
DICT_COLLECTION_PREFIX,
|
|
9300
|
+
DIRECTORY_RECORD_ID,
|
|
7786
9301
|
DanglingReferenceError,
|
|
7787
9302
|
DecryptionError,
|
|
7788
9303
|
DelegationTargetMissingError,
|
|
9304
|
+
DerivationCapExceededError,
|
|
9305
|
+
DerivationCycleError,
|
|
9306
|
+
DerivationDepthError,
|
|
9307
|
+
DerivationOutputShapeError,
|
|
9308
|
+
DerivationOutputUnknownError,
|
|
7789
9309
|
DictKeyInUseError,
|
|
7790
9310
|
DictKeyMissingError,
|
|
7791
9311
|
DictionaryHandle,
|
|
9312
|
+
DirectoryDisabledError,
|
|
7792
9313
|
ELEVATION_AUDIT_COLLECTION,
|
|
7793
9314
|
ElevatedHandle,
|
|
7794
9315
|
ElevationExpiredError,
|
|
7795
9316
|
ExportCapabilityError,
|
|
9317
|
+
FieldFrozenError,
|
|
7796
9318
|
FilenameSanitizationError,
|
|
7797
9319
|
GROUPBY_MAX_CARDINALITY,
|
|
7798
9320
|
GROUPBY_WARN_CARDINALITY,
|
|
7799
9321
|
GroupCardinalityError,
|
|
7800
9322
|
GroupedAggregation,
|
|
7801
9323
|
GroupedQuery,
|
|
9324
|
+
GroupedQueryN,
|
|
7802
9325
|
INDEXED_STORE_POLICY,
|
|
7803
9326
|
ImportCapabilityError,
|
|
7804
9327
|
IndexRequiredError,
|
|
7805
9328
|
IndexWriteFailureError,
|
|
7806
9329
|
InvalidKeyError,
|
|
9330
|
+
InvariantError,
|
|
7807
9331
|
JoinTooLargeError,
|
|
9332
|
+
KeyringCorruptError,
|
|
7808
9333
|
KeyringExpiredError,
|
|
7809
9334
|
LEDGER_COLLECTION,
|
|
7810
9335
|
LEDGER_DELTAS_COLLECTION,
|
|
@@ -7816,6 +9341,12 @@ export {
|
|
|
7816
9341
|
MAGIC_LINK_GRANTS_COLLECTION,
|
|
7817
9342
|
MAGIC_LINK_KEK_INFO_PREFIX,
|
|
7818
9343
|
META_COLLECTION,
|
|
9344
|
+
ManagedRecoveryNotEnrolledError,
|
|
9345
|
+
MaterializedViewConfigError,
|
|
9346
|
+
MaterializedViewCycleError,
|
|
9347
|
+
MaterializedViewSourceUnknownError,
|
|
9348
|
+
MaterializedViewTooLargeError,
|
|
9349
|
+
MemorySealingKeyProvider,
|
|
7819
9350
|
MissingTranslationError,
|
|
7820
9351
|
NOYDB_BACKUP_VERSION,
|
|
7821
9352
|
NOYDB_BUNDLE_FORMAT_VERSION,
|
|
@@ -7829,6 +9360,10 @@ export {
|
|
|
7829
9360
|
NotFoundError,
|
|
7830
9361
|
Noydb,
|
|
7831
9362
|
NoydbError,
|
|
9363
|
+
OverlayBaseIsVirtualError,
|
|
9364
|
+
OverlayCollectionUnavailableError,
|
|
9365
|
+
OverlayIdMismatchError,
|
|
9366
|
+
OverlayNameCollisionError,
|
|
7832
9367
|
PERIODS_COLLECTION,
|
|
7833
9368
|
PERSONAL_POLICY,
|
|
7834
9369
|
POLICY_RECORD_ID,
|
|
@@ -7846,12 +9381,15 @@ export {
|
|
|
7846
9381
|
ReadOnlyAtInstantError,
|
|
7847
9382
|
ReadOnlyError,
|
|
7848
9383
|
ReadOnlyFrameError,
|
|
9384
|
+
RecordLockedError,
|
|
7849
9385
|
RecoveryNotEnrolledError,
|
|
7850
9386
|
RecoveryProfileNotImplementedError,
|
|
7851
9387
|
RefIntegrityError,
|
|
7852
9388
|
RefRegistry,
|
|
7853
9389
|
RefScopeError,
|
|
7854
9390
|
ReservedCollectionNameError,
|
|
9391
|
+
SCHEMAS_COLLECTION,
|
|
9392
|
+
SEALED_PASSPHRASE_RECORD_ID,
|
|
7855
9393
|
STRICT_POLICY,
|
|
7856
9394
|
SYNC_CREDENTIALS_COLLECTION,
|
|
7857
9395
|
ScanBuilder,
|
|
@@ -7874,6 +9412,7 @@ export {
|
|
|
7874
9412
|
USER_ENVELOPE_MAX_BYTES,
|
|
7875
9413
|
UserApi,
|
|
7876
9414
|
UserEnvelopeOversizedError,
|
|
9415
|
+
VISIBILITY_RECORD_PREFIX,
|
|
7877
9416
|
ValidationError,
|
|
7878
9417
|
Vault,
|
|
7879
9418
|
VaultFrame,
|
|
@@ -7907,7 +9446,9 @@ export {
|
|
|
7907
9446
|
dekKey,
|
|
7908
9447
|
deleteCredential,
|
|
7909
9448
|
deleteUserEnvelope,
|
|
9449
|
+
deleteUserVisibility,
|
|
7910
9450
|
deriveMagicLinkContentKey,
|
|
9451
|
+
derivePersistedSchema,
|
|
7911
9452
|
derivePresenceKey,
|
|
7912
9453
|
describeAllUsersAuth,
|
|
7913
9454
|
describeAuthConfig,
|
|
@@ -7953,6 +9494,7 @@ export {
|
|
|
7953
9494
|
isPublicEnvelope,
|
|
7954
9495
|
isSessionAlive,
|
|
7955
9496
|
isULID,
|
|
9497
|
+
isZodSchema,
|
|
7956
9498
|
issueDelegation,
|
|
7957
9499
|
recoverPassphrase as keyringRecoverPassphrase,
|
|
7958
9500
|
rotatePassphrase as keyringRotatePassphrase,
|
|
@@ -7964,7 +9506,10 @@ export {
|
|
|
7964
9506
|
loadActiveDelegations,
|
|
7965
9507
|
loadDevUnlock,
|
|
7966
9508
|
loadPaperRecoveryEntries,
|
|
9509
|
+
loadPersistedSchema,
|
|
7967
9510
|
loadPublicEnvelope,
|
|
9511
|
+
loadSealedPassphrase,
|
|
9512
|
+
loadShamirRecoveryEntries,
|
|
7968
9513
|
loadUserEnvelope,
|
|
7969
9514
|
loadVaultPolicy,
|
|
7970
9515
|
magicLinkGrantRecordId,
|
|
@@ -7973,17 +9518,24 @@ export {
|
|
|
7973
9518
|
mergePolicy,
|
|
7974
9519
|
min,
|
|
7975
9520
|
mintPaperRecoveryEntry,
|
|
9521
|
+
mintShamirRecoveryEntry,
|
|
7976
9522
|
mintWrappedDeksBlob,
|
|
7977
9523
|
paddedIndex,
|
|
7978
9524
|
parseBytes,
|
|
7979
9525
|
parseIndex,
|
|
9526
|
+
parseSealedEnvelope,
|
|
9527
|
+
persistDirectoryConfig,
|
|
9528
|
+
persistSchemaIfNeeded,
|
|
9529
|
+
persistUserVisibility,
|
|
7980
9530
|
putCredential,
|
|
9531
|
+
readDirectoryConfig,
|
|
7981
9532
|
readMagicLinkGrantRecord,
|
|
7982
9533
|
readNoydbBundle,
|
|
7983
9534
|
readNoydbBundleHeader,
|
|
7984
9535
|
readNoydbBundlePublicEnvelope,
|
|
7985
9536
|
readPath,
|
|
7986
9537
|
readPublicEnvelope,
|
|
9538
|
+
readUserVisibility,
|
|
7987
9539
|
recoverUser,
|
|
7988
9540
|
reduceRecords,
|
|
7989
9541
|
ref,
|
|
@@ -8001,13 +9553,17 @@ export {
|
|
|
8001
9553
|
routeStore,
|
|
8002
9554
|
runTransaction,
|
|
8003
9555
|
savePaperRecoveryEntries,
|
|
9556
|
+
savePersistedSchema,
|
|
8004
9557
|
savePublicEnvelope,
|
|
9558
|
+
saveSealedPassphrase,
|
|
9559
|
+
saveShamirRecoveryEntries,
|
|
8005
9560
|
saveUserEnvelope,
|
|
8006
9561
|
saveVaultPolicy,
|
|
8007
|
-
sha256Hex,
|
|
9562
|
+
sha256Hex2 as sha256Hex,
|
|
8008
9563
|
sum,
|
|
8009
9564
|
unwrapDeksFromBlob,
|
|
8010
9565
|
unwrapDeksFromPaperEntry,
|
|
9566
|
+
unwrapDeksFromShamirEntry,
|
|
8011
9567
|
unwrapMagicLinkGrant,
|
|
8012
9568
|
validateI18nTextValue,
|
|
8013
9569
|
validatePassphrase,
|
|
@@ -8015,11 +9571,16 @@ export {
|
|
|
8015
9571
|
validateSchemaInput,
|
|
8016
9572
|
validateSchemaOutput,
|
|
8017
9573
|
validateSessionPolicy,
|
|
9574
|
+
visibilityRecordId,
|
|
8018
9575
|
withCache,
|
|
8019
9576
|
withCircuitBreaker,
|
|
9577
|
+
withDerivation,
|
|
9578
|
+
withGuard,
|
|
8020
9579
|
withHealthCheck,
|
|
8021
9580
|
withLogging,
|
|
9581
|
+
withMaterializedView,
|
|
8022
9582
|
withMetrics,
|
|
9583
|
+
withOverlayedView,
|
|
8023
9584
|
withRetry,
|
|
8024
9585
|
wrapBundleStore,
|
|
8025
9586
|
wrapStore,
|