@noy-db/hub 0.1.0-pre.8 → 0.2.0-pre.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-HC7Z5EQZ.js → chunk-4TFSM22V.js} +4 -4
- package/dist/{chunk-7XBQS42M.js → chunk-537VFZTR.js} +4 -4
- package/dist/{chunk-M62XNWRA.js → chunk-5DWL3JBF.js} +2 -2
- package/dist/{chunk-RSPLI376.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-WN6UK7PM.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-2WGMYBYS.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-YVFTBQHL.js → chunk-PA6R5ZCI.js} +217 -10
- 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-Y4CMTMUW.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-R2ZTGEVP.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-PJK6IOBC.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-BygpnIWe.d.ts → dev-unlock-D9s-loPr.d.ts} +1 -1
- package/dist/{dev-unlock-BZKx666y.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-B0eU2Qv9.d.ts → hash-DXXXusyk.d.ts} +1 -1
- package/dist/{hash-CIyfmKsg.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-Dp4tKCjX.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-DsVbTDZI.d.cts → index-hdFvZkBP.d.cts} +174 -3
- package/dist/index.cjs +5929 -1089
- 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 +2402 -672
- 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-UQIMMKO5.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-3QTQADDW.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-DD9eKKNc.d.ts → types-C4lwMKKF.d.cts} +2771 -322
- package/dist/{types-arFMsCtn.d.cts → types-DW9RGSSs.d.ts} +2771 -322
- 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-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-GOUT6DND.js.map +0 -1
- package/dist/chunk-M5INGEFC.js.map +0 -1
- package/dist/chunk-PJK6IOBC.js.map +0 -1
- package/dist/chunk-SCZXXXU4.js.map +0 -1
- package/dist/chunk-TDR6T5CJ.js.map +0 -1
- package/dist/chunk-TOQK4KAN.js +0 -79
- package/dist/chunk-TOQK4KAN.js.map +0 -1
- package/dist/chunk-WN6UK7PM.js.map +0 -1
- package/dist/chunk-Y4CMTMUW.js.map +0 -1
- package/dist/chunk-YVFTBQHL.js.map +0 -1
- /package/dist/{chunk-HC7Z5EQZ.js.map → chunk-4TFSM22V.js.map} +0 -0
- /package/dist/{chunk-7XBQS42M.js.map → chunk-537VFZTR.js.map} +0 -0
- /package/dist/{chunk-M62XNWRA.js.map → chunk-5DWL3JBF.js.map} +0 -0
- /package/dist/{chunk-RSPLI376.js.map → chunk-5SCJ5UEF.js.map} +0 -0
- /package/dist/{chunk-ZFKD4QMV.js.map → chunk-DYECX3IX.js.map} +0 -0
- /package/dist/{chunk-2WGMYBYS.js.map → chunk-NIOHFJPJ.js.map} +0 -0
- /package/dist/{chunk-USKYUS74.js.map → chunk-P7EQ2S5O.js.map} +0 -0
- /package/dist/{chunk-R2ZTGEVP.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-UQIMMKO5.js.map → derivations/index.js.map} +0 -0
- /package/dist/{public-envelope-3QTQADDW.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,12 +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
|
-
|
|
127
|
-
|
|
192
|
+
updateKeyringIdentity,
|
|
193
|
+
validatePassphrase,
|
|
194
|
+
visibilityRecordId
|
|
195
|
+
} from "./chunk-PA6R5ZCI.js";
|
|
128
196
|
import {
|
|
129
197
|
BUNDLE_STORE_POLICY,
|
|
130
198
|
INDEXED_STORE_POLICY,
|
|
@@ -144,7 +212,7 @@ import {
|
|
|
144
212
|
revokeAllSessions,
|
|
145
213
|
revokeSession,
|
|
146
214
|
validateSessionPolicy
|
|
147
|
-
} from "./chunk-
|
|
215
|
+
} from "./chunk-KESP7GOK.js";
|
|
148
216
|
import {
|
|
149
217
|
generateULID,
|
|
150
218
|
isULID
|
|
@@ -154,22 +222,22 @@ import {
|
|
|
154
222
|
VaultInstant,
|
|
155
223
|
diff,
|
|
156
224
|
formatDiff
|
|
157
|
-
} from "./chunk-
|
|
225
|
+
} from "./chunk-MIQHZESA.js";
|
|
158
226
|
import {
|
|
159
227
|
LEDGER_COLLECTION,
|
|
160
228
|
LEDGER_DELTAS_COLLECTION,
|
|
161
229
|
LedgerStore,
|
|
162
230
|
applyPatch,
|
|
163
231
|
computePatch
|
|
164
|
-
} from "./chunk-
|
|
232
|
+
} from "./chunk-UA4RI7OT.js";
|
|
165
233
|
import {
|
|
166
234
|
canonicalJson,
|
|
167
235
|
envelopePayloadHash,
|
|
168
236
|
hashEntry,
|
|
169
237
|
paddedIndex,
|
|
170
238
|
parseIndex,
|
|
171
|
-
sha256Hex
|
|
172
|
-
} from "./chunk-
|
|
239
|
+
sha256Hex as sha256Hex2
|
|
240
|
+
} from "./chunk-2AXFIYHT.js";
|
|
173
241
|
import {
|
|
174
242
|
DEFAULT_JOIN_MAX_ROWS,
|
|
175
243
|
NO_AGGREGATE,
|
|
@@ -179,29 +247,32 @@ import {
|
|
|
179
247
|
buildLiveQuery,
|
|
180
248
|
executePlan,
|
|
181
249
|
resetJoinWarnings
|
|
182
|
-
} from "./chunk-
|
|
250
|
+
} from "./chunk-23TTQXVO.js";
|
|
183
251
|
import {
|
|
184
252
|
CollectionIndexes
|
|
185
|
-
} from "./chunk-
|
|
253
|
+
} from "./chunk-YMYK7US4.js";
|
|
254
|
+
import {
|
|
255
|
+
avg,
|
|
256
|
+
count,
|
|
257
|
+
max,
|
|
258
|
+
min,
|
|
259
|
+
sum
|
|
260
|
+
} from "./chunk-5ZGZ6HIZ.js";
|
|
186
261
|
import {
|
|
187
262
|
Aggregation,
|
|
188
263
|
GROUPBY_MAX_CARDINALITY,
|
|
189
264
|
GROUPBY_WARN_CARDINALITY,
|
|
190
265
|
GroupedAggregation,
|
|
191
266
|
GroupedQuery,
|
|
192
|
-
|
|
193
|
-
count,
|
|
267
|
+
GroupedQueryN,
|
|
194
268
|
groupAndReduce,
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
reduceRecords,
|
|
198
|
-
sum
|
|
199
|
-
} from "./chunk-TDR6T5CJ.js";
|
|
269
|
+
reduceRecords
|
|
270
|
+
} from "./chunk-XGSOTWYX.js";
|
|
200
271
|
import {
|
|
201
272
|
evaluateClause,
|
|
202
273
|
evaluateFieldClause,
|
|
203
274
|
readPath
|
|
204
|
-
} from "./chunk-
|
|
275
|
+
} from "./chunk-MRIBLZL3.js";
|
|
205
276
|
import {
|
|
206
277
|
BLOB_CHUNKS_COLLECTION,
|
|
207
278
|
BLOB_COLLECTION,
|
|
@@ -216,58 +287,74 @@ import {
|
|
|
216
287
|
detectMimeType,
|
|
217
288
|
isPreCompressed,
|
|
218
289
|
runCompaction
|
|
219
|
-
} from "./chunk-
|
|
290
|
+
} from "./chunk-VMIO4IXG.js";
|
|
220
291
|
import {
|
|
221
292
|
NOYDB_BACKUP_VERSION,
|
|
222
293
|
NOYDB_FORMAT_VERSION,
|
|
223
294
|
NOYDB_KEYRING_VERSION,
|
|
224
295
|
NOYDB_SYNC_VERSION,
|
|
225
296
|
createStore
|
|
226
|
-
} from "./chunk-
|
|
297
|
+
} from "./chunk-YS3POABP.js";
|
|
227
298
|
import {
|
|
228
299
|
base64ToBuffer,
|
|
229
300
|
bufferToBase64,
|
|
230
301
|
decrypt,
|
|
231
302
|
decryptBytes,
|
|
232
303
|
decryptDeterministic,
|
|
233
|
-
deriveKey,
|
|
234
304
|
derivePresenceKey,
|
|
235
305
|
encrypt,
|
|
236
306
|
encryptBytes,
|
|
237
307
|
encryptDeterministic,
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
wrapKey
|
|
241
|
-
} from "./chunk-MR4424N3.js";
|
|
308
|
+
sha256Hex
|
|
309
|
+
} from "./chunk-WCA2NROQ.js";
|
|
242
310
|
import {
|
|
243
311
|
AlreadyElevatedError,
|
|
312
|
+
AmendmentForbiddenError,
|
|
244
313
|
BackupCorruptedError,
|
|
245
314
|
BackupLedgerError,
|
|
246
315
|
BundleIntegrityError,
|
|
316
|
+
BundleSealMismatchError,
|
|
247
317
|
BundleVersionConflictError,
|
|
248
318
|
ConflictError,
|
|
249
319
|
DanglingReferenceError,
|
|
250
320
|
DecryptionError,
|
|
251
321
|
DelegationTargetMissingError,
|
|
322
|
+
DerivationCapExceededError,
|
|
323
|
+
DerivationCycleError,
|
|
324
|
+
DerivationDepthError,
|
|
325
|
+
DerivationOutputShapeError,
|
|
326
|
+
DerivationOutputUnknownError,
|
|
252
327
|
DictKeyInUseError,
|
|
253
328
|
DictKeyMissingError,
|
|
329
|
+
DirectoryDisabledError,
|
|
254
330
|
ElevationExpiredError,
|
|
255
331
|
ExportCapabilityError,
|
|
332
|
+
FieldFrozenError,
|
|
256
333
|
FilenameSanitizationError,
|
|
257
334
|
GroupCardinalityError,
|
|
258
335
|
ImportCapabilityError,
|
|
259
336
|
IndexRequiredError,
|
|
260
337
|
IndexWriteFailureError,
|
|
261
338
|
InvalidKeyError,
|
|
339
|
+
InvariantError,
|
|
262
340
|
JoinTooLargeError,
|
|
341
|
+
KeyringCorruptError,
|
|
263
342
|
KeyringExpiredError,
|
|
264
343
|
LedgerContentionError,
|
|
265
344
|
LocaleNotSpecifiedError,
|
|
345
|
+
MaterializedViewConfigError,
|
|
346
|
+
MaterializedViewCycleError,
|
|
347
|
+
MaterializedViewSourceUnknownError,
|
|
348
|
+
MaterializedViewTooLargeError,
|
|
266
349
|
MissingTranslationError,
|
|
267
350
|
NetworkError,
|
|
268
351
|
NoAccessError,
|
|
269
352
|
NotFoundError,
|
|
270
353
|
NoydbError,
|
|
354
|
+
OverlayBaseIsVirtualError,
|
|
355
|
+
OverlayCollectionUnavailableError,
|
|
356
|
+
OverlayIdMismatchError,
|
|
357
|
+
OverlayNameCollisionError,
|
|
271
358
|
PathEscapeError,
|
|
272
359
|
PeriodClosedError,
|
|
273
360
|
PermissionDeniedError,
|
|
@@ -275,6 +362,7 @@ import {
|
|
|
275
362
|
ReadOnlyAtInstantError,
|
|
276
363
|
ReadOnlyError,
|
|
277
364
|
ReadOnlyFrameError,
|
|
365
|
+
RecordLockedError,
|
|
278
366
|
ReservedCollectionNameError,
|
|
279
367
|
SchemaValidationError,
|
|
280
368
|
SessionExpiredError,
|
|
@@ -286,7 +374,7 @@ import {
|
|
|
286
374
|
TierNotGrantedError,
|
|
287
375
|
TranslatorNotConfiguredError,
|
|
288
376
|
ValidationError
|
|
289
|
-
} from "./chunk-
|
|
377
|
+
} from "./chunk-ADQ5MQ54.js";
|
|
290
378
|
|
|
291
379
|
// src/schema.ts
|
|
292
380
|
async function validateSchemaInput(schema, value, context) {
|
|
@@ -326,6 +414,109 @@ function formatPath(path) {
|
|
|
326
414
|
).join(".");
|
|
327
415
|
}
|
|
328
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
|
+
|
|
329
520
|
// src/refs.ts
|
|
330
521
|
var RefIntegrityError = class extends NoydbError {
|
|
331
522
|
collection;
|
|
@@ -426,56 +617,6 @@ var RefRegistry = class {
|
|
|
426
617
|
}
|
|
427
618
|
};
|
|
428
619
|
|
|
429
|
-
// src/team/authenticators.ts
|
|
430
|
-
async function enrollAuthenticator(store, vault, keyring, options) {
|
|
431
|
-
const existing = keyring.authenticators.find((a) => a.id === options.id);
|
|
432
|
-
if (existing) {
|
|
433
|
-
throw new ValidationError(
|
|
434
|
-
`enrollAuthenticator: slot id "${options.id}" already exists in vault "${vault}". Remove the slot first or pick a unique id.`
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
const base = {
|
|
438
|
-
id: options.id,
|
|
439
|
-
method: options.method,
|
|
440
|
-
enrolled_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
441
|
-
enrolled_via_tier: options.enrolled_via_tier ?? 1,
|
|
442
|
-
meta: options.meta
|
|
443
|
-
};
|
|
444
|
-
const slot = options.wrapKind === "deks" ? {
|
|
445
|
-
...base,
|
|
446
|
-
wrapKind: "deks",
|
|
447
|
-
wrapped_deks: options.wrapped_deks,
|
|
448
|
-
iv: options.iv
|
|
449
|
-
} : {
|
|
450
|
-
...base,
|
|
451
|
-
wrapped_kek: options.wrapped_kek
|
|
452
|
-
};
|
|
453
|
-
const next = appendSlot(keyring, slot);
|
|
454
|
-
await persistKeyring(store, vault, next);
|
|
455
|
-
return next;
|
|
456
|
-
}
|
|
457
|
-
async function removeAuthenticator(store, vault, keyring, slotId) {
|
|
458
|
-
const filtered = keyring.authenticators.filter((a) => a.id !== slotId);
|
|
459
|
-
if (filtered.length === keyring.authenticators.length) {
|
|
460
|
-
return keyring;
|
|
461
|
-
}
|
|
462
|
-
const next = {
|
|
463
|
-
...keyring,
|
|
464
|
-
authenticators: filtered
|
|
465
|
-
};
|
|
466
|
-
await persistKeyring(store, vault, next);
|
|
467
|
-
return next;
|
|
468
|
-
}
|
|
469
|
-
function findAuthenticator(keyring, slotId) {
|
|
470
|
-
return keyring.authenticators.find((a) => a.id === slotId);
|
|
471
|
-
}
|
|
472
|
-
function appendSlot(keyring, slot) {
|
|
473
|
-
return {
|
|
474
|
-
...keyring,
|
|
475
|
-
authenticators: [...keyring.authenticators, slot]
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
|
|
479
620
|
// src/session/unlock-state.ts
|
|
480
621
|
var QuickUnlockStore = class {
|
|
481
622
|
states = /* @__PURE__ */ new Map();
|
|
@@ -517,357 +658,6 @@ var QuickUnlockStore = class {
|
|
|
517
658
|
}
|
|
518
659
|
};
|
|
519
660
|
|
|
520
|
-
// src/policy/errors.ts
|
|
521
|
-
var PolicyDeniedError = class extends NoydbError {
|
|
522
|
-
gate;
|
|
523
|
-
reason;
|
|
524
|
-
required;
|
|
525
|
-
constructor(gate, reason, required, message) {
|
|
526
|
-
super(
|
|
527
|
-
"POLICY_DENIED",
|
|
528
|
-
message ?? `Gate "${gate}" denied: ${reason}.`
|
|
529
|
-
);
|
|
530
|
-
this.name = "PolicyDeniedError";
|
|
531
|
-
this.gate = gate;
|
|
532
|
-
this.reason = reason;
|
|
533
|
-
this.required = required;
|
|
534
|
-
}
|
|
535
|
-
};
|
|
536
|
-
var RecoveryNotEnrolledError = class extends NoydbError {
|
|
537
|
-
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.') {
|
|
538
|
-
super("RECOVERY_NOT_ENROLLED", message);
|
|
539
|
-
this.name = "RecoveryNotEnrolledError";
|
|
540
|
-
}
|
|
541
|
-
};
|
|
542
|
-
var RecoveryProfileNotImplementedError = class extends NoydbError {
|
|
543
|
-
profile;
|
|
544
|
-
tracking;
|
|
545
|
-
constructor(profile, tracking) {
|
|
546
|
-
super(
|
|
547
|
-
"RECOVERY_PROFILE_NOT_IMPLEMENTED",
|
|
548
|
-
`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.`
|
|
549
|
-
);
|
|
550
|
-
this.name = "RecoveryProfileNotImplementedError";
|
|
551
|
-
this.profile = profile;
|
|
552
|
-
this.tracking = tracking;
|
|
553
|
-
}
|
|
554
|
-
};
|
|
555
|
-
|
|
556
|
-
// src/team/wrapped-deks.ts
|
|
557
|
-
var PBKDF2_ITERATIONS = 6e5;
|
|
558
|
-
var SALT_BYTES = 32;
|
|
559
|
-
var IV_BYTES = 12;
|
|
560
|
-
var subtle = globalThis.crypto.subtle;
|
|
561
|
-
async function mintWrappedDeksBlob(deks, credential) {
|
|
562
|
-
const salt = crypto.getRandomValues(new Uint8Array(SALT_BYTES));
|
|
563
|
-
const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
|
|
564
|
-
const wrappingKey = await deriveWrappingKey(credential, salt);
|
|
565
|
-
const exported = {};
|
|
566
|
-
for (const [coll, dek] of deks) {
|
|
567
|
-
const raw = await subtle.exportKey("raw", dek);
|
|
568
|
-
exported[coll] = bytesToBase64(new Uint8Array(raw));
|
|
569
|
-
}
|
|
570
|
-
const plaintext = new TextEncoder().encode(JSON.stringify({ deks: exported }));
|
|
571
|
-
const ciphertext = await subtle.encrypt(
|
|
572
|
-
{ name: "AES-GCM", iv },
|
|
573
|
-
wrappingKey,
|
|
574
|
-
plaintext
|
|
575
|
-
);
|
|
576
|
-
return {
|
|
577
|
-
salt: bytesToBase64(salt),
|
|
578
|
-
iv: bytesToBase64(iv),
|
|
579
|
-
wrappedDeks: bytesToBase64(new Uint8Array(ciphertext))
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
async function unwrapDeksFromBlob(blob, credential) {
|
|
583
|
-
const wrappingKey = await deriveWrappingKey(credential, base64ToBytes(blob.salt));
|
|
584
|
-
const plaintext = await subtle.decrypt(
|
|
585
|
-
{ name: "AES-GCM", iv: base64ToBytes(blob.iv) },
|
|
586
|
-
wrappingKey,
|
|
587
|
-
base64ToBytes(blob.wrappedDeks)
|
|
588
|
-
);
|
|
589
|
-
const parsed = JSON.parse(new TextDecoder().decode(plaintext));
|
|
590
|
-
const deks = /* @__PURE__ */ new Map();
|
|
591
|
-
for (const [coll, b64] of Object.entries(parsed.deks)) {
|
|
592
|
-
const raw = base64ToBytes(b64);
|
|
593
|
-
const key = await subtle.importKey(
|
|
594
|
-
"raw",
|
|
595
|
-
raw,
|
|
596
|
-
{ name: "AES-GCM", length: 256 },
|
|
597
|
-
true,
|
|
598
|
-
["encrypt", "decrypt"]
|
|
599
|
-
);
|
|
600
|
-
deks.set(coll, key);
|
|
601
|
-
}
|
|
602
|
-
return deks;
|
|
603
|
-
}
|
|
604
|
-
async function deriveWrappingKey(credential, salt) {
|
|
605
|
-
const ikm = await subtle.importKey(
|
|
606
|
-
"raw",
|
|
607
|
-
new TextEncoder().encode(credential),
|
|
608
|
-
"PBKDF2",
|
|
609
|
-
false,
|
|
610
|
-
["deriveKey"]
|
|
611
|
-
);
|
|
612
|
-
return subtle.deriveKey(
|
|
613
|
-
{
|
|
614
|
-
name: "PBKDF2",
|
|
615
|
-
salt,
|
|
616
|
-
iterations: PBKDF2_ITERATIONS,
|
|
617
|
-
hash: "SHA-256"
|
|
618
|
-
},
|
|
619
|
-
ikm,
|
|
620
|
-
{ name: "AES-GCM", length: 256 },
|
|
621
|
-
false,
|
|
622
|
-
["encrypt", "decrypt"]
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
function bytesToBase64(b) {
|
|
626
|
-
let s = "";
|
|
627
|
-
for (const x of b) s += String.fromCharCode(x);
|
|
628
|
-
return btoa(s);
|
|
629
|
-
}
|
|
630
|
-
function base64ToBytes(b64) {
|
|
631
|
-
const s = atob(b64);
|
|
632
|
-
const out = new Uint8Array(s.length);
|
|
633
|
-
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
|
|
634
|
-
return out;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// src/team/recovery.ts
|
|
638
|
-
var PAPER_DOC_ID = "recovery-paper";
|
|
639
|
-
async function loadPaperRecoveryEntries(store, vault) {
|
|
640
|
-
const env = await store.get(vault, "_meta", PAPER_DOC_ID);
|
|
641
|
-
if (!env) return [];
|
|
642
|
-
try {
|
|
643
|
-
const doc = JSON.parse(env._data);
|
|
644
|
-
if (doc.profile !== "paper" || !Array.isArray(doc.entries)) return [];
|
|
645
|
-
return doc.entries;
|
|
646
|
-
} catch {
|
|
647
|
-
return [];
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
async function savePaperRecoveryEntries(store, vault, entries) {
|
|
651
|
-
const doc = {
|
|
652
|
-
_noydb_recovery: 1,
|
|
653
|
-
profile: "paper",
|
|
654
|
-
entries
|
|
655
|
-
};
|
|
656
|
-
const envelope = {
|
|
657
|
-
_noydb: NOYDB_FORMAT_VERSION,
|
|
658
|
-
_v: 1,
|
|
659
|
-
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
660
|
-
_iv: "",
|
|
661
|
-
_data: JSON.stringify(doc)
|
|
662
|
-
};
|
|
663
|
-
await store.put(vault, "_meta", PAPER_DOC_ID, envelope);
|
|
664
|
-
}
|
|
665
|
-
async function burnPaperRecoveryEntry(store, vault, codeId) {
|
|
666
|
-
const entries = await loadPaperRecoveryEntries(store, vault);
|
|
667
|
-
const remaining = entries.filter((e) => e.codeId !== codeId);
|
|
668
|
-
await savePaperRecoveryEntries(store, vault, remaining);
|
|
669
|
-
}
|
|
670
|
-
async function hasRecoveryEnrolled(store, vault) {
|
|
671
|
-
const paper = await loadPaperRecoveryEntries(store, vault);
|
|
672
|
-
return paper.length > 0;
|
|
673
|
-
}
|
|
674
|
-
async function mintPaperRecoveryEntry(deks, code, codeId) {
|
|
675
|
-
const blob = await mintWrappedDeksBlob(deks, code);
|
|
676
|
-
return {
|
|
677
|
-
...blob,
|
|
678
|
-
codeId,
|
|
679
|
-
enrolledAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
680
|
-
};
|
|
681
|
-
}
|
|
682
|
-
async function unwrapDeksFromPaperEntry(entry, code) {
|
|
683
|
-
return unwrapDeksFromBlob(entry, code);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// src/team/rotate-recover.ts
|
|
687
|
-
async function rotatePassphrase(store, vault, userId, input) {
|
|
688
|
-
if (!input.allowWeakPassphrase) {
|
|
689
|
-
assertStrongPassphrase(input.newPassphrase, input.passphrasePolicy);
|
|
690
|
-
}
|
|
691
|
-
const env = await store.get(vault, "_keyring", userId);
|
|
692
|
-
if (!env) {
|
|
693
|
-
throw new NoAccessError(`No keyring found for user "${userId}" in vault "${vault}".`);
|
|
694
|
-
}
|
|
695
|
-
const file = JSON.parse(env._data);
|
|
696
|
-
const oldSalt = base64ToBuffer(file.salt);
|
|
697
|
-
const oldKek = await deriveKey(input.oldPassphrase, oldSalt);
|
|
698
|
-
const deks = /* @__PURE__ */ new Map();
|
|
699
|
-
for (const [coll, wrapped] of Object.entries(file.deks)) {
|
|
700
|
-
deks.set(coll, await unwrapKey(wrapped, oldKek));
|
|
701
|
-
}
|
|
702
|
-
const newSalt = generateSalt();
|
|
703
|
-
const newKek = await deriveKey(input.newPassphrase, newSalt);
|
|
704
|
-
const wrappedDeks = {};
|
|
705
|
-
for (const [coll, dek] of deks) {
|
|
706
|
-
wrappedDeks[coll] = await wrapKey(dek, newKek);
|
|
707
|
-
}
|
|
708
|
-
const oldSlots = file.authenticators ?? [];
|
|
709
|
-
const newSlots = [];
|
|
710
|
-
if (input.slotCeremonies && oldSlots.length > 0) {
|
|
711
|
-
for (const oldSlot of oldSlots) {
|
|
712
|
-
const ceremony = input.slotCeremonies[oldSlot.id];
|
|
713
|
-
if (!ceremony) continue;
|
|
714
|
-
const result = await ceremony({ newKek, newDeks: deks, oldSlot });
|
|
715
|
-
if (result.id !== oldSlot.id) {
|
|
716
|
-
throw new ValidationError(
|
|
717
|
-
`slotCeremonies['${oldSlot.id}'] returned id="${result.id}". The id must match the rotated slot \u2014 a ceremony cannot change a slot's identity.`
|
|
718
|
-
);
|
|
719
|
-
}
|
|
720
|
-
if (result.method !== oldSlot.method) {
|
|
721
|
-
throw new ValidationError(
|
|
722
|
-
`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.`
|
|
723
|
-
);
|
|
724
|
-
}
|
|
725
|
-
const baseFields = {
|
|
726
|
-
id: result.id,
|
|
727
|
-
method: result.method,
|
|
728
|
-
// Preserve original enrolled_at — rotation is rewrapping, not
|
|
729
|
-
// re-enrollment. The slot's enrolment timestamp tracks when
|
|
730
|
-
// the user originally added the slot, not when it was last
|
|
731
|
-
// rewrapped. Forensics consumers reading enrolled_at are
|
|
732
|
-
// tracking the slot's ORIGIN, not its CURRENT wrapping.
|
|
733
|
-
enrolled_at: oldSlot.enrolled_at,
|
|
734
|
-
enrolled_via_tier: result.enrolled_via_tier ?? oldSlot.enrolled_via_tier,
|
|
735
|
-
meta: result.meta
|
|
736
|
-
};
|
|
737
|
-
const newSlot = result.wrapKind === "deks" ? {
|
|
738
|
-
...baseFields,
|
|
739
|
-
wrapKind: "deks",
|
|
740
|
-
wrapped_deks: result.wrapped_deks,
|
|
741
|
-
iv: result.iv
|
|
742
|
-
} : {
|
|
743
|
-
...baseFields,
|
|
744
|
-
wrapped_kek: result.wrapped_kek
|
|
745
|
-
};
|
|
746
|
-
newSlots.push(newSlot);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
const next = {
|
|
750
|
-
...file,
|
|
751
|
-
_noydb_keyring: NOYDB_KEYRING_VERSION,
|
|
752
|
-
deks: wrappedDeks,
|
|
753
|
-
salt: bufferToBase64(newSalt),
|
|
754
|
-
authenticators: newSlots
|
|
755
|
-
};
|
|
756
|
-
await writeKeyringFile(store, vault, userId, next);
|
|
757
|
-
return {
|
|
758
|
-
userId: file.user_id,
|
|
759
|
-
displayName: file.display_name,
|
|
760
|
-
role: file.role,
|
|
761
|
-
permissions: file.permissions,
|
|
762
|
-
deks,
|
|
763
|
-
kek: newKek,
|
|
764
|
-
salt: newSalt,
|
|
765
|
-
authenticators: newSlots,
|
|
766
|
-
...file.export_capability !== void 0 && { exportCapability: file.export_capability },
|
|
767
|
-
...file.import_capability !== void 0 && { importCapability: file.import_capability }
|
|
768
|
-
};
|
|
769
|
-
}
|
|
770
|
-
async function recoverPassphrase(store, vault, userId, input) {
|
|
771
|
-
if (!input.allowWeakPassphrase) {
|
|
772
|
-
assertStrongPassphrase(input.newPassphrase, input.passphrasePolicy);
|
|
773
|
-
}
|
|
774
|
-
switch (input.recoveryProof.profile) {
|
|
775
|
-
case "paper":
|
|
776
|
-
return recoverViaPaperCode(store, vault, userId, input);
|
|
777
|
-
case "shamir":
|
|
778
|
-
throw new RecoveryProfileNotImplementedError(
|
|
779
|
-
"shamir",
|
|
780
|
-
"https://github.com/vLannaAi/noy-db/issues/10"
|
|
781
|
-
);
|
|
782
|
-
case "multi-channel":
|
|
783
|
-
throw new RecoveryProfileNotImplementedError(
|
|
784
|
-
"multi-channel",
|
|
785
|
-
"https://github.com/vLannaAi/noy-db/issues/10"
|
|
786
|
-
);
|
|
787
|
-
case "admin-mediated":
|
|
788
|
-
throw new RecoveryProfileNotImplementedError(
|
|
789
|
-
"admin-mediated",
|
|
790
|
-
"https://github.com/vLannaAi/noy-db/issues/10"
|
|
791
|
-
);
|
|
792
|
-
default: {
|
|
793
|
-
const _exhaustive = input.recoveryProof;
|
|
794
|
-
throw new Error(`Unknown recovery profile: ${String(_exhaustive)}`);
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
async function recoverViaPaperCode(store, vault, userId, input) {
|
|
799
|
-
if (input.recoveryProof.profile !== "paper") throw new Error("unreachable");
|
|
800
|
-
const { code } = input.recoveryProof.payload;
|
|
801
|
-
const env = await store.get(vault, "_keyring", userId);
|
|
802
|
-
if (!env) {
|
|
803
|
-
throw new NoAccessError(`No keyring found for user "${userId}" in vault "${vault}".`);
|
|
804
|
-
}
|
|
805
|
-
const file = JSON.parse(env._data);
|
|
806
|
-
const entries = await loadPaperRecoveryEntries(store, vault);
|
|
807
|
-
if (entries.length === 0) {
|
|
808
|
-
throw new NoAccessError(
|
|
809
|
-
`No paper-recovery entries enrolled for vault "${vault}". Enroll via \`db.enrollRecovery({ profile: "paper", entries })\` before relying on recovery.`
|
|
810
|
-
);
|
|
811
|
-
}
|
|
812
|
-
const normalized = normalizePaperCode(code);
|
|
813
|
-
let recovered;
|
|
814
|
-
for (const entry of entries) {
|
|
815
|
-
try {
|
|
816
|
-
const deks2 = await unwrapDeksFromPaperEntry(entry, normalized);
|
|
817
|
-
recovered = { deks: deks2, entry };
|
|
818
|
-
break;
|
|
819
|
-
} catch {
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
if (!recovered) {
|
|
823
|
-
throw new InvalidKeyError(
|
|
824
|
-
"Recovery code does not match any enrolled paper entry. The code may have been previously used (single-use) or typed incorrectly."
|
|
825
|
-
);
|
|
826
|
-
}
|
|
827
|
-
const deks = recovered.deks;
|
|
828
|
-
const newSalt = generateSalt();
|
|
829
|
-
const newKek = await deriveKey(input.newPassphrase, newSalt);
|
|
830
|
-
const wrappedDeks = {};
|
|
831
|
-
for (const [coll, dek] of deks) {
|
|
832
|
-
wrappedDeks[coll] = await wrapKey(dek, newKek);
|
|
833
|
-
}
|
|
834
|
-
const next = {
|
|
835
|
-
...file,
|
|
836
|
-
_noydb_keyring: NOYDB_KEYRING_VERSION,
|
|
837
|
-
deks: wrappedDeks,
|
|
838
|
-
salt: bufferToBase64(newSalt),
|
|
839
|
-
authenticators: []
|
|
840
|
-
// tier-2 slots wrap old KEK, drop them
|
|
841
|
-
};
|
|
842
|
-
await writeKeyringFile(store, vault, userId, next);
|
|
843
|
-
await burnPaperRecoveryEntry(store, vault, recovered.entry.codeId);
|
|
844
|
-
return {
|
|
845
|
-
userId: file.user_id,
|
|
846
|
-
displayName: file.display_name,
|
|
847
|
-
role: file.role,
|
|
848
|
-
permissions: file.permissions,
|
|
849
|
-
deks,
|
|
850
|
-
kek: newKek,
|
|
851
|
-
salt: newSalt,
|
|
852
|
-
authenticators: [],
|
|
853
|
-
...file.export_capability !== void 0 && { exportCapability: file.export_capability },
|
|
854
|
-
...file.import_capability !== void 0 && { importCapability: file.import_capability }
|
|
855
|
-
};
|
|
856
|
-
}
|
|
857
|
-
function normalizePaperCode(input) {
|
|
858
|
-
return input.toUpperCase().replace(/[\s\-_]/g, "");
|
|
859
|
-
}
|
|
860
|
-
async function writeKeyringFile(store, vault, userId, file) {
|
|
861
|
-
const envelope = {
|
|
862
|
-
_noydb: 1,
|
|
863
|
-
_v: 1,
|
|
864
|
-
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
865
|
-
_iv: "",
|
|
866
|
-
_data: JSON.stringify(file)
|
|
867
|
-
};
|
|
868
|
-
await store.put(vault, "_keyring", userId, envelope);
|
|
869
|
-
}
|
|
870
|
-
|
|
871
661
|
// src/meta/user-envelope/api.ts
|
|
872
662
|
var UserApi = class {
|
|
873
663
|
constructor(adapter, vaultName, writerKeyringId, getDek, checkGate2) {
|
|
@@ -895,6 +685,17 @@ var UserApi = class {
|
|
|
895
685
|
* the envelope on first call. Optimistic-concurrency safe — a stale
|
|
896
686
|
* `_v` (parallel writer on another device) throws `ConflictError`.
|
|
897
687
|
*
|
|
688
|
+
* Patch semantics (#57):
|
|
689
|
+
* - `undefined` (or omitted key) — skip; existing value preserved
|
|
690
|
+
* - `null` — delete the field from the merged result
|
|
691
|
+
* - any other value — overwrite (deep-merge for plain objects,
|
|
692
|
+
* replace for primitives / arrays)
|
|
693
|
+
*
|
|
694
|
+
* To clear a field, pass `null` rather than `undefined`. Callers
|
|
695
|
+
* with shape `T = string | null` where `null` is a meaningful value
|
|
696
|
+
* should use `setMe` for that specific field instead — `null` here
|
|
697
|
+
* always means delete.
|
|
698
|
+
*
|
|
898
699
|
* Gated by the `edit-own-profile` policy gate (default `minTier: 3`).
|
|
899
700
|
* Pass `presented` to satisfy tightened policies that require a
|
|
900
701
|
* factor proof (e.g. STRICT_POLICY's TOTP requirement).
|
|
@@ -940,6 +741,41 @@ var UserApi = class {
|
|
|
940
741
|
this.fireChange(this.writerKeyringId, written);
|
|
941
742
|
return written;
|
|
942
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
|
+
}
|
|
943
779
|
// ─── Read-anyone ─────────────────────────────────────────────────────
|
|
944
780
|
/**
|
|
945
781
|
* Read another principal's envelope by their keyringId. Returns null
|
|
@@ -1066,9 +902,17 @@ function deepMerge(source, patch) {
|
|
|
1066
902
|
}
|
|
1067
903
|
const out = { ...source };
|
|
1068
904
|
for (const [key, patchVal] of Object.entries(patch)) {
|
|
905
|
+
if (patchVal === void 0) {
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
if (patchVal === null) {
|
|
909
|
+
delete out[key];
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
1069
912
|
const sourceVal = source[key];
|
|
1070
|
-
if (isPlainObject(
|
|
1071
|
-
|
|
913
|
+
if (isPlainObject(patchVal)) {
|
|
914
|
+
const recurseSource = isPlainObject(sourceVal) ? sourceVal : {};
|
|
915
|
+
out[key] = deepMerge(recurseSource, patchVal);
|
|
1072
916
|
} else {
|
|
1073
917
|
out[key] = patchVal;
|
|
1074
918
|
}
|
|
@@ -1124,7 +968,7 @@ async function describeAuthConfig(store, vault) {
|
|
|
1124
968
|
lines.push(` Phrase format: ${policy.passphrase?.minWords ?? 6}+ words, lowercase letters, \u2265${policy.passphrase?.minWordLength ?? 3} chars/word`);
|
|
1125
969
|
lines.push(" Strength validator: enforced (override available for tests only)");
|
|
1126
970
|
lines.push("");
|
|
1127
|
-
lines.push("Tier 2 \u2014 Authenticate (
|
|
971
|
+
lines.push("Tier 2 \u2014 Authenticate (routine login)");
|
|
1128
972
|
lines.push(" Allowed methods: WebAuthn (passkey), OIDC, Password");
|
|
1129
973
|
lines.push(" Slots per user: unlimited");
|
|
1130
974
|
lines.push("");
|
|
@@ -1236,68 +1080,131 @@ function sanitizeId(s) {
|
|
|
1236
1080
|
return s.replace(/[^a-zA-Z0-9]/g, "_");
|
|
1237
1081
|
}
|
|
1238
1082
|
|
|
1239
|
-
// src/team/
|
|
1240
|
-
var
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
)
|
|
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
|
+
}
|
|
1259
1105
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
)
|
|
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;
|
|
1264
1113
|
}
|
|
1265
|
-
|
|
1266
|
-
if (
|
|
1267
|
-
throw new
|
|
1114
|
+
async unseal(sealed) {
|
|
1115
|
+
if (sealed.length < 4) {
|
|
1116
|
+
throw new Error("MemorySealingKeyProvider: sealed input too short");
|
|
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
|
+
}
|
|
1268
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;
|
|
1269
1131
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
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
|
+
};
|
|
1272
1155
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
throw new PrivilegeEscalationError(coll);
|
|
1280
|
-
}
|
|
1281
|
-
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
|
+
};
|
|
1282
1162
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
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)
|
|
1292
1171
|
};
|
|
1293
|
-
const
|
|
1294
|
-
|
|
1295
|
-
|
|
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,
|
|
1296
1176
|
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1177
|
+
// AES-GCM bypassed — the sealing layer is the security boundary.
|
|
1297
1178
|
_iv: "",
|
|
1298
|
-
_data: JSON.stringify(
|
|
1179
|
+
_data: JSON.stringify(persisted)
|
|
1299
1180
|
};
|
|
1300
|
-
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);
|
|
1301
1208
|
}
|
|
1302
1209
|
|
|
1303
1210
|
// src/crdt/strategy.ts
|
|
@@ -1573,6 +1480,79 @@ var NO_BLOBS = {
|
|
|
1573
1480
|
}
|
|
1574
1481
|
};
|
|
1575
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
|
+
|
|
1576
1556
|
// src/collection.ts
|
|
1577
1557
|
var fallbackWarned = /* @__PURE__ */ new Set();
|
|
1578
1558
|
function warnOnceFallback(adapterName) {
|
|
@@ -1580,7 +1560,7 @@ function warnOnceFallback(adapterName) {
|
|
|
1580
1560
|
fallbackWarned.add(adapterName);
|
|
1581
1561
|
if (typeof process !== "undefined" && process.env["NODE_ENV"] === "test") return;
|
|
1582
1562
|
console.warn(
|
|
1583
|
-
`[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.`
|
|
1584
1564
|
);
|
|
1585
1565
|
}
|
|
1586
1566
|
var Collection = class {
|
|
@@ -1790,6 +1770,34 @@ var Collection = class {
|
|
|
1790
1770
|
* adapter on first use.
|
|
1791
1771
|
*/
|
|
1792
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;
|
|
1793
1801
|
/**
|
|
1794
1802
|
* Optional back-reference to the owning compartment's ref
|
|
1795
1803
|
* enforcer. When present, `Collection.put` calls
|
|
@@ -1860,6 +1868,9 @@ var Collection = class {
|
|
|
1860
1868
|
this.syncAdapter = opts.syncAdapter;
|
|
1861
1869
|
this.onAccess = opts.onAccess;
|
|
1862
1870
|
this.periodGuard = opts.periodGuard;
|
|
1871
|
+
this.guardSource = opts.guardSource;
|
|
1872
|
+
this.derivationSource = opts.derivationSource;
|
|
1873
|
+
this.materializedViewSource = opts.materializedViewSource;
|
|
1863
1874
|
this.tiers = opts.tiers && opts.tiers.length > 0 ? new Set(opts.tiers) : null;
|
|
1864
1875
|
this.tierMode = opts.tierMode ?? "invisibility";
|
|
1865
1876
|
this.onCrossTierAccess = opts.onCrossTierAccess;
|
|
@@ -1984,6 +1995,16 @@ var Collection = class {
|
|
|
1984
1995
|
* `null` if not found.
|
|
1985
1996
|
*/
|
|
1986
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
|
+
}
|
|
1987
2008
|
let record;
|
|
1988
2009
|
if (this.lazy && this.lru) {
|
|
1989
2010
|
const cached = this.lru.get(id);
|
|
@@ -2047,11 +2068,53 @@ var Collection = class {
|
|
|
2047
2068
|
if (opts?.pollIntervalMs !== void 0) presenceOpts.pollIntervalMs = opts.pollIntervalMs;
|
|
2048
2069
|
return this.syncStrategy.buildPresence(presenceOpts);
|
|
2049
2070
|
}
|
|
2050
|
-
/**
|
|
2051
|
-
|
|
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) {
|
|
2052
2083
|
if (!hasWritePermission(this.keyring, this.name)) {
|
|
2053
2084
|
throw new ReadOnlyError();
|
|
2054
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
|
+
}
|
|
2055
2118
|
if (this.periodGuard !== void 0) {
|
|
2056
2119
|
const existingEnv = await this.adapter.get(this.vault, this.name, id);
|
|
2057
2120
|
let priorRecord = null;
|
|
@@ -2160,6 +2223,7 @@ var Collection = class {
|
|
|
2160
2223
|
payloadHash: await this.historyStrategy.envelopePayloadHash(envelope2)
|
|
2161
2224
|
};
|
|
2162
2225
|
if (existingResolved) appendInput.delta = this.historyStrategy.computePatch(resolvedRecord, existingResolved.record);
|
|
2226
|
+
if (options?.reason !== void 0) appendInput.reason = options.reason;
|
|
2163
2227
|
await this.ledger.append(appendInput);
|
|
2164
2228
|
}
|
|
2165
2229
|
if (this.lazy && this.lru) {
|
|
@@ -2177,6 +2241,8 @@ var Collection = class {
|
|
|
2177
2241
|
await this.onDirty?.(this.name, id, "put", version2);
|
|
2178
2242
|
this.emitter.emit("change", { vault: this.vault, collection: this.name, id, action: "put" });
|
|
2179
2243
|
await this.onAccess?.("put", id);
|
|
2244
|
+
await this.dispatchDerivations(id, record, version2);
|
|
2245
|
+
await this.dispatchMaterializedViews(id, record);
|
|
2180
2246
|
return;
|
|
2181
2247
|
}
|
|
2182
2248
|
let existing;
|
|
@@ -2223,6 +2289,7 @@ var Collection = class {
|
|
|
2223
2289
|
if (existing) {
|
|
2224
2290
|
appendInput.delta = this.historyStrategy.computePatch(record, existing.record);
|
|
2225
2291
|
}
|
|
2292
|
+
if (options?.reason !== void 0) appendInput.reason = options.reason;
|
|
2226
2293
|
await this.ledger.append(appendInput);
|
|
2227
2294
|
}
|
|
2228
2295
|
if (this.lazy && this.lru) {
|
|
@@ -2240,13 +2307,256 @@ var Collection = class {
|
|
|
2240
2307
|
action: "put"
|
|
2241
2308
|
});
|
|
2242
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
|
+
}
|
|
2243
2458
|
}
|
|
2244
2459
|
/** Delete a record by ID. */
|
|
2245
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) {
|
|
2246
2518
|
if (!hasWritePermission(this.keyring, this.name)) {
|
|
2247
2519
|
throw new ReadOnlyError();
|
|
2248
2520
|
}
|
|
2249
|
-
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) {
|
|
2250
2560
|
const existingEnv = await this.adapter.get(this.vault, this.name, id);
|
|
2251
2561
|
let priorRecord = null;
|
|
2252
2562
|
if (existingEnv) {
|
|
@@ -2261,7 +2571,7 @@ var Collection = class {
|
|
|
2261
2571
|
null
|
|
2262
2572
|
);
|
|
2263
2573
|
}
|
|
2264
|
-
if (this.refEnforcer !== void 0) {
|
|
2574
|
+
if (!internal && this.refEnforcer !== void 0) {
|
|
2265
2575
|
await this.refEnforcer.enforceRefsOnDelete(this.name, id);
|
|
2266
2576
|
}
|
|
2267
2577
|
let existing;
|
|
@@ -2313,6 +2623,87 @@ var Collection = class {
|
|
|
2313
2623
|
action: "delete"
|
|
2314
2624
|
});
|
|
2315
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
|
+
}
|
|
2316
2707
|
}
|
|
2317
2708
|
/**
|
|
2318
2709
|
* List all records in the collection.
|
|
@@ -2330,6 +2721,10 @@ var Collection = class {
|
|
|
2330
2721
|
`Collection "${this.name}": list() is not available in lazy mode (prefetch: false). Use collection.scan({ pageSize }) to iterate over the full collection.`
|
|
2331
2722
|
);
|
|
2332
2723
|
}
|
|
2724
|
+
if (this.materializedViewSource !== void 0) {
|
|
2725
|
+
const { resolveStaleMVOnRead } = await import("./stale-HSC5YO2O.js");
|
|
2726
|
+
await resolveStaleMVOnRead(this.materializedViewSource, this.name);
|
|
2727
|
+
}
|
|
2333
2728
|
await this.ensureHydrated();
|
|
2334
2729
|
const records = [...this.cache.values()].map((e) => e.record);
|
|
2335
2730
|
if (!locale) return records;
|
|
@@ -2404,22 +2799,42 @@ var Collection = class {
|
|
|
2404
2799
|
}
|
|
2405
2800
|
}
|
|
2406
2801
|
}
|
|
2407
|
-
const
|
|
2802
|
+
const txCtx = this.derivationSource?.createTxContext() ?? null;
|
|
2803
|
+
if (txCtx !== null && this.derivationSource) {
|
|
2804
|
+
this.derivationSource.setActiveTxContext(txCtx);
|
|
2805
|
+
}
|
|
2806
|
+
const localExecuted = [];
|
|
2408
2807
|
try {
|
|
2409
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);
|
|
2410
2815
|
await this.put(id, record);
|
|
2411
|
-
executed.push({ id, prior: priors.get(id) ?? null });
|
|
2412
2816
|
}
|
|
2413
|
-
return { ok: true, success:
|
|
2817
|
+
return { ok: true, success: entries.map(([id]) => id), failures: [] };
|
|
2414
2818
|
} catch (err) {
|
|
2415
|
-
|
|
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;
|
|
2416
2823
|
try {
|
|
2417
|
-
if (
|
|
2418
|
-
|
|
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
|
+
}
|
|
2419
2830
|
} catch {
|
|
2420
2831
|
}
|
|
2421
2832
|
}
|
|
2422
2833
|
throw err;
|
|
2834
|
+
} finally {
|
|
2835
|
+
if (txCtx !== null && this.derivationSource) {
|
|
2836
|
+
this.derivationSource.clearActiveTxContext(txCtx);
|
|
2837
|
+
}
|
|
2423
2838
|
}
|
|
2424
2839
|
}
|
|
2425
2840
|
/**
|
|
@@ -2785,6 +3200,11 @@ var Collection = class {
|
|
|
2785
3200
|
* .aggregate({ total: sum('amount'), n: count() })
|
|
2786
3201
|
* ```
|
|
2787
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
|
+
*
|
|
2788
3208
|
* Returns a `ScanBuilder<T>` instead of the raw async iterator
|
|
2789
3209
|
* that previous versions used. The builder implements
|
|
2790
3210
|
* `AsyncIterable<T>`, so every existing `for await … of` call
|
|
@@ -2828,6 +3248,38 @@ var Collection = class {
|
|
|
2828
3248
|
}
|
|
2829
3249
|
// ─── Internal ──────────────────────────────────────────────────
|
|
2830
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
|
+
}
|
|
2831
3283
|
async ensureHydrated() {
|
|
2832
3284
|
if (this.hydrated) return;
|
|
2833
3285
|
const ids = await this.adapter.list(this.vault, this.name);
|
|
@@ -3799,97 +4251,246 @@ var NO_PERIODS = {
|
|
|
3799
4251
|
}
|
|
3800
4252
|
};
|
|
3801
4253
|
|
|
3802
|
-
// src/
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
const info = new TextEncoder().encode(MAGIC_LINK_CONTENT_INFO_PREFIX + vault);
|
|
3812
|
-
const ikm = await subtle2.importKey("raw", ikmBytes, "HKDF", false, ["deriveKey"]);
|
|
3813
|
-
return subtle2.deriveKey(
|
|
3814
|
-
{ name: "HKDF", hash: "SHA-256", salt: saltBuffer, info },
|
|
3815
|
-
ikm,
|
|
3816
|
-
{ name: "AES-GCM", length: 256 },
|
|
3817
|
-
false,
|
|
3818
|
-
["encrypt", "decrypt"]
|
|
3819
|
-
);
|
|
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";
|
|
3820
4263
|
}
|
|
3821
|
-
|
|
3822
|
-
const
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
if (
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
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;
|
|
3829
4293
|
}
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
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 } : {}
|
|
3853
4334
|
};
|
|
3854
|
-
|
|
3855
|
-
return { recordId, payload };
|
|
4335
|
+
return snap;
|
|
3856
4336
|
}
|
|
3857
|
-
async function
|
|
3858
|
-
const env = await store.get(vault, MAGIC_LINK_GRANTS_COLLECTION, recordId);
|
|
3859
|
-
if (!env) return null;
|
|
4337
|
+
async function safeListAllCollections(adapter, vault) {
|
|
3860
4338
|
try {
|
|
3861
|
-
const
|
|
3862
|
-
return
|
|
4339
|
+
const snap = await adapter.loadAll(vault);
|
|
4340
|
+
return Object.keys(snap);
|
|
3863
4341
|
} catch {
|
|
3864
|
-
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;
|
|
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;
|
|
3865
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
|
+
};
|
|
3866
4421
|
}
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
const
|
|
3870
|
-
const out =
|
|
3871
|
-
for (const
|
|
3872
|
-
const
|
|
3873
|
-
|
|
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
|
+
};
|
|
3874
4441
|
}
|
|
3875
4442
|
return out;
|
|
3876
4443
|
}
|
|
3877
|
-
|
|
3878
|
-
|
|
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;
|
|
3879
4454
|
}
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
const
|
|
3883
|
-
|
|
3884
|
-
|
|
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
|
+
};
|
|
3885
4466
|
}
|
|
3886
|
-
return
|
|
4467
|
+
return out;
|
|
3887
4468
|
}
|
|
3888
|
-
function
|
|
3889
|
-
|
|
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 [];
|
|
3890
4485
|
}
|
|
3891
|
-
function
|
|
3892
|
-
|
|
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);
|
|
3893
4494
|
}
|
|
3894
4495
|
|
|
3895
4496
|
// src/vault.ts
|
|
@@ -3934,6 +4535,40 @@ var Vault = class {
|
|
|
3934
4535
|
historyStrategy;
|
|
3935
4536
|
i18nStrategy;
|
|
3936
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;
|
|
3937
4572
|
getDEK;
|
|
3938
4573
|
/**
|
|
3939
4574
|
* Per-principal user envelope API.
|
|
@@ -3981,6 +4616,16 @@ var Vault = class {
|
|
|
3981
4616
|
* docstring.
|
|
3982
4617
|
*/
|
|
3983
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 = [];
|
|
3984
4629
|
/**
|
|
3985
4630
|
* Per-vault foreign-key reference registry. Collections
|
|
3986
4631
|
* register their `refs` option here on construction; the
|
|
@@ -4075,6 +4720,7 @@ var Vault = class {
|
|
|
4075
4720
|
this.historyStrategy = opts.historyStrategy ?? NO_HISTORY;
|
|
4076
4721
|
this.i18nStrategy = opts.i18nStrategy ?? NO_I18N;
|
|
4077
4722
|
this.syncStrategy = opts.syncStrategy ?? NO_SYNC;
|
|
4723
|
+
void opts.guardStrategies;
|
|
4078
4724
|
this.historyConfig = opts.historyConfig ?? { enabled: true };
|
|
4079
4725
|
this.reloadKeyring = opts.reloadKeyring;
|
|
4080
4726
|
this.locale = opts.locale;
|
|
@@ -4134,6 +4780,16 @@ var Vault = class {
|
|
|
4134
4780
|
* Collection constructor for the rationale.
|
|
4135
4781
|
*/
|
|
4136
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
|
+
}
|
|
4137
4793
|
if (isDictCollectionName(collectionName)) {
|
|
4138
4794
|
throw new ReservedCollectionNameError(collectionName);
|
|
4139
4795
|
}
|
|
@@ -4181,7 +4837,38 @@ var Vault = class {
|
|
|
4181
4837
|
defaultLocale: this.locale,
|
|
4182
4838
|
onRegisterConflictResolver: this.onRegisterConflictResolver,
|
|
4183
4839
|
onAccess: (op, id) => this._logConsent(op, collectionName, id),
|
|
4184
|
-
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
|
+
} : {}
|
|
4185
4872
|
};
|
|
4186
4873
|
if (options?.indexes !== void 0) collOpts.indexes = options.indexes;
|
|
4187
4874
|
if (options?.reconcileOnOpen !== void 0) collOpts.reconcileOnOpen = options.reconcileOnOpen;
|
|
@@ -4218,9 +4905,39 @@ var Vault = class {
|
|
|
4218
4905
|
}
|
|
4219
4906
|
coll = new Collection(collOpts);
|
|
4220
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
|
+
}
|
|
4221
4928
|
}
|
|
4222
4929
|
return coll;
|
|
4223
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
|
+
}
|
|
4224
4941
|
/**
|
|
4225
4942
|
* Validate i18nText fields on a `put()`. Called by Collection just
|
|
4226
4943
|
* before the adapter write, after schema validation. Throws
|
|
@@ -4804,6 +5521,270 @@ var Vault = class {
|
|
|
4804
5521
|
}
|
|
4805
5522
|
return this.ledgerStore;
|
|
4806
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").
|
|
5768
|
+
*/
|
|
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
|
+
);
|
|
5774
|
+
}
|
|
5775
|
+
/**
|
|
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).
|
|
5784
|
+
*/
|
|
5785
|
+
_getLedgerOrNull() {
|
|
5786
|
+
return this.getLedgerOrNull();
|
|
5787
|
+
}
|
|
4807
5788
|
/**
|
|
4808
5789
|
* Return a read-only view of this vault as it existed at
|
|
4809
5790
|
* `timestamp`. Time-machine queries are reconstructed from the
|
|
@@ -4956,7 +5937,7 @@ var Vault = class {
|
|
|
4956
5937
|
* collection.
|
|
4957
5938
|
*/
|
|
4958
5939
|
async delegate(opts) {
|
|
4959
|
-
const { issueDelegation: issueDelegation2, DELEGATIONS_COLLECTION: DELEGATIONS_COLLECTION2 } = await import("./delegation-
|
|
5940
|
+
const { issueDelegation: issueDelegation2, DELEGATIONS_COLLECTION: DELEGATIONS_COLLECTION2 } = await import("./delegation-YBA4X4JN.js");
|
|
4960
5941
|
if (!this.keyring.kek) {
|
|
4961
5942
|
throw new ValidationError(
|
|
4962
5943
|
"issueDelegation: keyring.kek is null \u2014 issuing a delegation requires a tier-1 unlock. Re-authenticate at tier 1 (passphrase) first."
|
|
@@ -4978,7 +5959,7 @@ var Vault = class {
|
|
|
4978
5959
|
* if the id does not exist.
|
|
4979
5960
|
*/
|
|
4980
5961
|
async revokeDelegation(id) {
|
|
4981
|
-
const { revokeDelegation: revokeDelegation2, DELEGATIONS_COLLECTION: DELEGATIONS_COLLECTION2 } = await import("./delegation-
|
|
5962
|
+
const { revokeDelegation: revokeDelegation2, DELEGATIONS_COLLECTION: DELEGATIONS_COLLECTION2 } = await import("./delegation-YBA4X4JN.js");
|
|
4982
5963
|
await revokeDelegation2(this.adapter, this.name, id);
|
|
4983
5964
|
void DELEGATIONS_COLLECTION2;
|
|
4984
5965
|
}
|
|
@@ -5303,6 +6284,54 @@ var Vault = class {
|
|
|
5303
6284
|
const snapshot = await this.adapter.loadAll(this.name);
|
|
5304
6285
|
return Object.keys(snapshot);
|
|
5305
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
|
+
}
|
|
5306
6335
|
/**
|
|
5307
6336
|
* Return the stable opaque bundle handle for this vault,
|
|
5308
6337
|
* generating and persisting a fresh ULID on first call.
|
|
@@ -5378,7 +6407,7 @@ var Vault = class {
|
|
|
5378
6407
|
* @see docs/subsystems/public-envelope.md
|
|
5379
6408
|
*/
|
|
5380
6409
|
async getPublicEnvelope(opts = {}) {
|
|
5381
|
-
const { readPublicEnvelope: readPublicEnvelope2 } = await import("./public-envelope-
|
|
6410
|
+
const { readPublicEnvelope: readPublicEnvelope2 } = await import("./public-envelope-PY6NKFLI.js");
|
|
5382
6411
|
return readPublicEnvelope2(this.adapter, this.name, opts);
|
|
5383
6412
|
}
|
|
5384
6413
|
/**
|
|
@@ -5403,7 +6432,7 @@ var Vault = class {
|
|
|
5403
6432
|
}
|
|
5404
6433
|
}
|
|
5405
6434
|
const internalSnapshot = {};
|
|
5406
|
-
for (const internalName of [LEDGER_COLLECTION, LEDGER_DELTAS_COLLECTION]) {
|
|
6435
|
+
for (const internalName of [LEDGER_COLLECTION, LEDGER_DELTAS_COLLECTION, SCHEMAS_COLLECTION]) {
|
|
5407
6436
|
const ids = await this.adapter.list(this.name, internalName);
|
|
5408
6437
|
if (ids.length === 0) continue;
|
|
5409
6438
|
const records = {};
|
|
@@ -5552,6 +6581,7 @@ var Vault = class {
|
|
|
5552
6581
|
for (let i = allEntries.length - 1; i >= 0; i--) {
|
|
5553
6582
|
const entry = allEntries[i];
|
|
5554
6583
|
if (!entry) continue;
|
|
6584
|
+
if (entry.op === "amendment") continue;
|
|
5555
6585
|
const key = `${entry.collection}/${entry.id}`;
|
|
5556
6586
|
if (seen.has(key)) continue;
|
|
5557
6587
|
seen.add(key);
|
|
@@ -5573,7 +6603,7 @@ var Vault = class {
|
|
|
5573
6603
|
message: `Ledger expects data record "${collection}/${id}" to exist, but the adapter has no envelope for it.`
|
|
5574
6604
|
};
|
|
5575
6605
|
}
|
|
5576
|
-
const actualHash = await
|
|
6606
|
+
const actualHash = await sha256Hex2(envelope._data);
|
|
5577
6607
|
if (actualHash !== expectedHash) {
|
|
5578
6608
|
return {
|
|
5579
6609
|
ok: false,
|
|
@@ -5974,8 +7004,18 @@ var PERSONAL_POLICY = Object.freeze({
|
|
|
5974
7004
|
minTier: 1,
|
|
5975
7005
|
enabled: true
|
|
5976
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 },
|
|
5977
7011
|
"enroll-authenticator": { minTier: 1 },
|
|
5978
7012
|
"remove-authenticator": { minTier: 1 },
|
|
7013
|
+
// update-authenticator: meta-only mutation (slot rename, label
|
|
7014
|
+
// changes). Symmetric with enroll/remove under PERSONAL — tier-1
|
|
7015
|
+
// unlock alone. The structural anti-slot-swap guard inside the
|
|
7016
|
+
// implementation enforces wrap-material/id/method immutability
|
|
7017
|
+
// regardless of this gate's settings.
|
|
7018
|
+
"update-authenticator": { minTier: 1 },
|
|
5979
7019
|
"rotate-unlock": { minTier: 2 },
|
|
5980
7020
|
"enroll-user": { minTier: 1 },
|
|
5981
7021
|
"revoke-user": { minTier: 1 },
|
|
@@ -5985,6 +7025,12 @@ var PERSONAL_POLICY = Object.freeze({
|
|
|
5985
7025
|
// virtue of being a co-owner). Tier-1 unlock is the floor; the
|
|
5986
7026
|
// STRICT preset adds a recovery/email-OTP requirement.
|
|
5987
7027
|
"peer-recover-user": { minTier: 1 },
|
|
7028
|
+
// update-user: post-grant identity mutation (role/displayName/
|
|
7029
|
+
// permissions). PERSONAL_POLICY treats this on par with enroll-user
|
|
7030
|
+
// / revoke-user — tier-1 unlock alone. The role-elevation guard
|
|
7031
|
+
// inside the implementation is the structural backstop that this
|
|
7032
|
+
// gate's settings cannot weaken.
|
|
7033
|
+
"update-user": { minTier: 1 },
|
|
5988
7034
|
"export-bundle": { minTier: 1 },
|
|
5989
7035
|
"export-plaintext": {
|
|
5990
7036
|
minTier: 1,
|
|
@@ -6021,6 +7067,14 @@ var STRICT_POLICY = Object.freeze({
|
|
|
6021
7067
|
minTier: 1,
|
|
6022
7068
|
enabled: true
|
|
6023
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
|
+
},
|
|
6024
7078
|
"enroll-authenticator": {
|
|
6025
7079
|
minTier: 1,
|
|
6026
7080
|
factors: [{ anyOf: ["totp", "email-otp"] }]
|
|
@@ -6029,6 +7083,15 @@ var STRICT_POLICY = Object.freeze({
|
|
|
6029
7083
|
minTier: 1,
|
|
6030
7084
|
factors: [{ anyOf: ["totp", "email-otp"] }]
|
|
6031
7085
|
},
|
|
7086
|
+
// STRICT update-authenticator: same factor floor as enroll/remove.
|
|
7087
|
+
// Even though meta changes don't touch wrap material, a malicious
|
|
7088
|
+
// rename could mislead the user about which device a slot
|
|
7089
|
+
// corresponds to ("MacBook Touch ID" → "iPhone Touch ID" on a
|
|
7090
|
+
// shared workstation). STRICT requires a fresh factor proof.
|
|
7091
|
+
"update-authenticator": {
|
|
7092
|
+
minTier: 1,
|
|
7093
|
+
factors: [{ anyOf: ["totp", "email-otp"] }]
|
|
7094
|
+
},
|
|
6032
7095
|
"rotate-unlock": { minTier: 1 },
|
|
6033
7096
|
"enroll-user": {
|
|
6034
7097
|
minTier: 1,
|
|
@@ -6051,6 +7114,18 @@ var STRICT_POLICY = Object.freeze({
|
|
|
6051
7114
|
minTier: 1,
|
|
6052
7115
|
factors: [{ anyOf: ["recovery", "totp", "email-otp", "webauthn-roaming"] }]
|
|
6053
7116
|
},
|
|
7117
|
+
// STRICT update-user: matches the enroll-user / revoke-user shape
|
|
7118
|
+
// (off-device factor required). Update-user is admin-shaped — it
|
|
7119
|
+
// mutates someone else's role/permissions; STRICT requires a fresh
|
|
7120
|
+
// off-device factor proof so the operator affirmatively re-asserts
|
|
7121
|
+
// identity at the moment of mutation. Platform-bound factors
|
|
7122
|
+
// (Touch ID / password / PIN) intentionally excluded: same logic as
|
|
7123
|
+
// peer-recover-user — the off-device requirement is the whole
|
|
7124
|
+
// point under STRICT.
|
|
7125
|
+
"update-user": {
|
|
7126
|
+
minTier: 1,
|
|
7127
|
+
factors: [{ anyOf: ["totp", "email-otp"] }]
|
|
7128
|
+
},
|
|
6054
7129
|
"export-bundle": {
|
|
6055
7130
|
minTier: 1,
|
|
6056
7131
|
factors: [{ anyOf: ["totp", "email-otp"] }],
|
|
@@ -6188,6 +7263,14 @@ var Noydb = class {
|
|
|
6188
7263
|
* `_meta/policy` load; replaced by `db.updatePolicy()`.
|
|
6189
7264
|
*/
|
|
6190
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;
|
|
6191
7274
|
/** Per-vault tier-3 (PIN / quick-resume) state — issue #11. */
|
|
6192
7275
|
quickUnlock = new QuickUnlockStore();
|
|
6193
7276
|
/**
|
|
@@ -6203,6 +7286,17 @@ var Noydb = class {
|
|
|
6203
7286
|
txStrategy;
|
|
6204
7287
|
sessionStrategy;
|
|
6205
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;
|
|
6206
7300
|
// ─── plaintextTranslator state ─────────────────────────
|
|
6207
7301
|
/**
|
|
6208
7302
|
* In-process translation cache. Key is `"${field}\x00${collection}\x00${from}\x00${to}\x00${text}"`.
|
|
@@ -6287,7 +7381,7 @@ var Noydb = class {
|
|
|
6287
7381
|
}
|
|
6288
7382
|
return comp;
|
|
6289
7383
|
}
|
|
6290
|
-
const keyring = await this.
|
|
7384
|
+
const keyring = await this.getKeyringInternal(name);
|
|
6291
7385
|
if (!this.activeTier.has(name)) {
|
|
6292
7386
|
this.activeTier.set(name, 1);
|
|
6293
7387
|
}
|
|
@@ -6354,6 +7448,7 @@ var Noydb = class {
|
|
|
6354
7448
|
...this.options.historyStrategy !== void 0 ? { historyStrategy: this.options.historyStrategy } : {},
|
|
6355
7449
|
...this.options.i18nStrategy !== void 0 ? { i18nStrategy: this.options.i18nStrategy } : {},
|
|
6356
7450
|
...this.options.syncStrategy !== void 0 ? { syncStrategy: this.options.syncStrategy } : {},
|
|
7451
|
+
...this.options.guardStrategies !== void 0 ? { guardStrategies: this.options.guardStrategies } : {},
|
|
6357
7452
|
locale: opts?.locale,
|
|
6358
7453
|
// Thread the translator hook so Collection.put() can invoke it
|
|
6359
7454
|
plaintextTranslator: this.options.plaintextTranslator ? (text, from, to, field, collection) => this.invokeTranslator(text, from, to, field, collection) : void 0,
|
|
@@ -6374,6 +7469,10 @@ var Noydb = class {
|
|
|
6374
7469
|
return refreshed;
|
|
6375
7470
|
} : void 0
|
|
6376
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 ?? []);
|
|
6377
7476
|
this.vaultCache.set(name, comp);
|
|
6378
7477
|
return comp;
|
|
6379
7478
|
}
|
|
@@ -6400,7 +7499,8 @@ var Noydb = class {
|
|
|
6400
7499
|
...this.options.shadowStrategy !== void 0 ? { shadowStrategy: this.options.shadowStrategy } : {},
|
|
6401
7500
|
...this.options.historyStrategy !== void 0 ? { historyStrategy: this.options.historyStrategy } : {},
|
|
6402
7501
|
...this.options.i18nStrategy !== void 0 ? { i18nStrategy: this.options.i18nStrategy } : {},
|
|
6403
|
-
...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 } : {}
|
|
6404
7504
|
});
|
|
6405
7505
|
this.vaultCache.set(name, comp2);
|
|
6406
7506
|
return comp2;
|
|
@@ -6428,23 +7528,93 @@ var Noydb = class {
|
|
|
6428
7528
|
...this.options.historyStrategy !== void 0 ? { historyStrategy: this.options.historyStrategy } : {},
|
|
6429
7529
|
...this.options.i18nStrategy !== void 0 ? { i18nStrategy: this.options.i18nStrategy } : {},
|
|
6430
7530
|
...this.options.syncStrategy !== void 0 ? { syncStrategy: this.options.syncStrategy } : {},
|
|
7531
|
+
...this.options.guardStrategies !== void 0 ? { guardStrategies: this.options.guardStrategies } : {},
|
|
6431
7532
|
emitter: this.emitter
|
|
6432
7533
|
});
|
|
6433
7534
|
this.vaultCache.set(name, comp);
|
|
6434
7535
|
return comp;
|
|
6435
7536
|
}
|
|
6436
|
-
/**
|
|
6437
|
-
|
|
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) {
|
|
6438
7548
|
this.checkPolicyOperation(vault, "grant");
|
|
6439
|
-
|
|
7549
|
+
await this.checkGate(vault, "enroll-user", factors);
|
|
7550
|
+
const keyring = await this.getKeyringInternal(vault);
|
|
6440
7551
|
await grant(this.options.store, vault, keyring, options);
|
|
6441
7552
|
}
|
|
6442
|
-
/**
|
|
6443
|
-
|
|
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) {
|
|
6444
7563
|
this.checkPolicyOperation(vault, "revoke");
|
|
6445
|
-
|
|
7564
|
+
await this.checkGate(vault, "revoke-user", factors);
|
|
7565
|
+
const keyring = await this.getKeyringInternal(vault);
|
|
6446
7566
|
await revoke(this.options.store, vault, keyring, options);
|
|
6447
7567
|
}
|
|
7568
|
+
/**
|
|
7569
|
+
* Mutate post-grant identity fields on an existing keyring — `role`,
|
|
7570
|
+
* `displayName`, and/or `permissions`. Pure plaintext-header rewrite:
|
|
7571
|
+
* no DEK rewrap, no KEK required, no authenticator slots touched.
|
|
7572
|
+
* Tier-2 enrollments and recovery codes survive.
|
|
7573
|
+
*
|
|
7574
|
+
* Different from `db.revoke + db.grant`:
|
|
7575
|
+
*
|
|
7576
|
+
* - Same `userId`, same DEK wrappings, same `granted_by`, same
|
|
7577
|
+
* `_users/<keyringId>` envelope. Only the specified header
|
|
7578
|
+
* fields move. Last-write-wins via the standard keyring put.
|
|
7579
|
+
* - No cascade on role demotion (admins demoted to operator keep
|
|
7580
|
+
* the keyrings they previously granted; the cascade rules are
|
|
7581
|
+
* a `db.revoke` concern, not `db.updateUser`).
|
|
7582
|
+
* - Tier-2 slots NOT dropped — the wrapping is unaffected.
|
|
7583
|
+
*
|
|
7584
|
+
* Role-elevation guard: BOTH the old and new role must satisfy
|
|
7585
|
+
* `db.grant`'s hierarchy. Owner can do anything; admin manages
|
|
7586
|
+
* admin/operator/viewer/client laterally; admin cannot promote to
|
|
7587
|
+
* owner OR demote from owner. The guard runs regardless of the
|
|
7588
|
+
* `update-user` policy gate's settings — gates can only be more
|
|
7589
|
+
* permissive than the structural floor, never less.
|
|
7590
|
+
*
|
|
7591
|
+
* Gated by `update-user`. `STRICT_POLICY` requires a TOTP/email-OTP
|
|
7592
|
+
* factor proof so the operator affirmatively re-asserts identity at
|
|
7593
|
+
* the moment of mutation; `PERSONAL_POLICY` accepts a tier-1 unlock
|
|
7594
|
+
* alone.
|
|
7595
|
+
*
|
|
7596
|
+
* ```ts
|
|
7597
|
+
* await db.updateUser('acme', {
|
|
7598
|
+
* userId: 'bob',
|
|
7599
|
+
* role: 'operator', // promote
|
|
7600
|
+
* permissions: { invoices: 'rw' },
|
|
7601
|
+
* }, { factors: [{ kind: 'totp' }] })
|
|
7602
|
+
* ```
|
|
7603
|
+
*
|
|
7604
|
+
* @throws `NoAccessError` when no keyring exists for the target.
|
|
7605
|
+
* @throws `PermissionDeniedError` when the role hierarchy rejects.
|
|
7606
|
+
* @throws `ValidationError` when no field is provided.
|
|
7607
|
+
*
|
|
7608
|
+
* @see #54
|
|
7609
|
+
*/
|
|
7610
|
+
async updateUser(vault, options, factors) {
|
|
7611
|
+
await this.checkGate(vault, "update-user", factors);
|
|
7612
|
+
const keyring = await this.getKeyringInternal(vault);
|
|
7613
|
+
await updateKeyringIdentity(this.options.store, vault, keyring, options);
|
|
7614
|
+
if (options.userId === this.options.user) {
|
|
7615
|
+
this.keyringCache.delete(vault);
|
|
7616
|
+
}
|
|
7617
|
+
}
|
|
6448
7618
|
/**
|
|
6449
7619
|
* Rotate the DEKs for the given collections in a vault.
|
|
6450
7620
|
*
|
|
@@ -6465,7 +7635,7 @@ var Noydb = class {
|
|
|
6465
7635
|
*/
|
|
6466
7636
|
async rotate(vault, collections) {
|
|
6467
7637
|
this.checkPolicyOperation(vault, "rotate");
|
|
6468
|
-
const keyring = await this.
|
|
7638
|
+
const keyring = await this.getKeyringInternal(vault);
|
|
6469
7639
|
await rotateKeys(this.options.store, vault, keyring, collections);
|
|
6470
7640
|
this.keyringCache.set(vault, keyring);
|
|
6471
7641
|
}
|
|
@@ -6568,7 +7738,7 @@ var Noydb = class {
|
|
|
6568
7738
|
this.options.secret
|
|
6569
7739
|
);
|
|
6570
7740
|
} catch (err) {
|
|
6571
|
-
if (err instanceof NoAccessError || err instanceof InvalidKeyError) {
|
|
7741
|
+
if (err instanceof NoAccessError || err instanceof InvalidKeyError || err instanceof KeyringCorruptError) {
|
|
6572
7742
|
continue;
|
|
6573
7743
|
}
|
|
6574
7744
|
throw err;
|
|
@@ -6665,15 +7835,23 @@ var Noydb = class {
|
|
|
6665
7835
|
}
|
|
6666
7836
|
return results;
|
|
6667
7837
|
}
|
|
6668
|
-
/**
|
|
6669
|
-
|
|
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) {
|
|
6670
7847
|
this.checkPolicyOperation(vault, "changeSecret");
|
|
6671
|
-
const keyring = await this.
|
|
7848
|
+
const keyring = await this.getKeyringInternal(vault);
|
|
6672
7849
|
const updated = await changeSecret(
|
|
6673
7850
|
this.options.store,
|
|
6674
7851
|
vault,
|
|
6675
7852
|
keyring,
|
|
6676
|
-
newPassphrase
|
|
7853
|
+
newPassphrase,
|
|
7854
|
+
options
|
|
6677
7855
|
);
|
|
6678
7856
|
this.keyringCache.set(vault, updated);
|
|
6679
7857
|
}
|
|
@@ -6718,10 +7896,18 @@ var Noydb = class {
|
|
|
6718
7896
|
}
|
|
6719
7897
|
return result;
|
|
6720
7898
|
}
|
|
6721
|
-
transaction(arg) {
|
|
7899
|
+
transaction(arg, maybeFn) {
|
|
6722
7900
|
if (typeof arg === "function") {
|
|
6723
7901
|
return this.txStrategy.runTransaction(this, arg);
|
|
6724
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
|
+
}
|
|
6725
7911
|
const vault = arg;
|
|
6726
7912
|
const comp = this.vaultCache.get(vault);
|
|
6727
7913
|
if (!comp) {
|
|
@@ -6742,6 +7928,59 @@ var Noydb = class {
|
|
|
6742
7928
|
get _store() {
|
|
6743
7929
|
return this.options.store;
|
|
6744
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
|
+
}
|
|
6745
7984
|
/** Get sync status for a vault. */
|
|
6746
7985
|
syncStatus(vault) {
|
|
6747
7986
|
const engine = this.syncEngines.get(vault);
|
|
@@ -6750,6 +7989,15 @@ var Noydb = class {
|
|
|
6750
7989
|
}
|
|
6751
7990
|
return engine.status();
|
|
6752
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
|
+
}
|
|
6753
8001
|
getSyncEngine(vault) {
|
|
6754
8002
|
const engine = this.syncEngines.get(vault);
|
|
6755
8003
|
if (!engine) {
|
|
@@ -6894,6 +8142,40 @@ var Noydb = class {
|
|
|
6894
8142
|
this.policyCache.set(vault, merged);
|
|
6895
8143
|
return merged;
|
|
6896
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
|
+
}
|
|
6897
8179
|
/**
|
|
6898
8180
|
* Evaluate a policy gate against the active session tier and the
|
|
6899
8181
|
* presented factor proofs. Throws {@link PolicyDeniedError} on
|
|
@@ -6904,40 +8186,61 @@ var Noydb = class {
|
|
|
6904
8186
|
* or app-defined (`app:*`).
|
|
6905
8187
|
* @param presented Caller-supplied factor proofs.
|
|
6906
8188
|
*/
|
|
6907
|
-
async checkGate(vault, gate,
|
|
8189
|
+
async checkGate(vault, gate, factors) {
|
|
6908
8190
|
const policy = await this.getPolicy(vault);
|
|
6909
8191
|
const tier = this.activeTier.get(vault) ?? 1;
|
|
6910
8192
|
await checkGate(policy, gate, {
|
|
6911
8193
|
activeTier: tier,
|
|
6912
|
-
...
|
|
6913
|
-
...
|
|
8194
|
+
...factors?.factors !== void 0 ? { factors: factors.factors } : {},
|
|
8195
|
+
...factors?.sharedDevice !== void 0 ? { sharedDevice: factors.sharedDevice } : {}
|
|
6914
8196
|
});
|
|
6915
8197
|
}
|
|
6916
8198
|
/** Read or persist the vault policy at `_meta/policy` on first open. */
|
|
6917
|
-
async bootstrapPolicy(vault) {
|
|
8199
|
+
async bootstrapPolicy(vault, opts) {
|
|
6918
8200
|
const onDisk = await loadVaultPolicy(this.options.store, vault);
|
|
6919
8201
|
if (onDisk) {
|
|
6920
8202
|
this.policyCache.set(vault, onDisk);
|
|
6921
|
-
await this.assertRecoveryEnrolled(vault, onDisk);
|
|
8203
|
+
await this.assertRecoveryEnrolled(vault, onDisk, opts);
|
|
6922
8204
|
return;
|
|
6923
8205
|
}
|
|
6924
8206
|
const initial = this.options.policy ? mergePolicy(PERSONAL_POLICY, this.options.policy) : PERSONAL_POLICY;
|
|
6925
8207
|
await saveVaultPolicy(this.options.store, vault, initial);
|
|
6926
8208
|
this.policyCache.set(vault, initial);
|
|
6927
|
-
await this.assertRecoveryEnrolled(vault, initial);
|
|
6928
|
-
}
|
|
6929
|
-
/**
|
|
6930
|
-
* Throw {@link RecoveryNotEnrolledError}
|
|
6931
|
-
*
|
|
6932
|
-
*
|
|
6933
|
-
*
|
|
6934
|
-
*
|
|
6935
|
-
*
|
|
6936
|
-
*
|
|
6937
|
-
*
|
|
6938
|
-
*
|
|
6939
|
-
|
|
6940
|
-
|
|
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
|
+
}
|
|
6941
8244
|
if (this.options.requireRecovery !== true) return;
|
|
6942
8245
|
const gate = policy.gates["recover-passphrase"];
|
|
6943
8246
|
if (gate?.enabled === false) return;
|
|
@@ -6966,9 +8269,9 @@ var Noydb = class {
|
|
|
6966
8269
|
* Gated by `enroll-authenticator`; `presented` carries any factor
|
|
6967
8270
|
* proofs the active policy demands.
|
|
6968
8271
|
*/
|
|
6969
|
-
async enrollAuthenticator(vault, options,
|
|
6970
|
-
await this.checkGate(vault, "enroll-authenticator",
|
|
6971
|
-
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);
|
|
6972
8275
|
const next = await enrollAuthenticator(this.options.store, vault, keyring, options);
|
|
6973
8276
|
this.keyringCache.set(vault, next);
|
|
6974
8277
|
}
|
|
@@ -6977,17 +8280,51 @@ var Noydb = class {
|
|
|
6977
8280
|
* non-existent slot is a successful no-op. Gated by
|
|
6978
8281
|
* `remove-authenticator`.
|
|
6979
8282
|
*/
|
|
6980
|
-
async removeAuthenticator(vault, slotId,
|
|
6981
|
-
await this.checkGate(vault, "remove-authenticator",
|
|
6982
|
-
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);
|
|
6983
8286
|
const next = await removeAuthenticator(this.options.store, vault, keyring, slotId);
|
|
6984
8287
|
this.keyringCache.set(vault, next);
|
|
6985
8288
|
}
|
|
6986
8289
|
/** Read the slot list for a vault. Internal — `describeAuthConfig` (#13) consumes this. */
|
|
6987
8290
|
async listAuthenticators(vault) {
|
|
6988
|
-
const keyring = await this.
|
|
8291
|
+
const keyring = await this.getKeyringInternal(vault);
|
|
6989
8292
|
return keyring.authenticators;
|
|
6990
8293
|
}
|
|
8294
|
+
/**
|
|
8295
|
+
* Mutate the `meta` blob on an existing authenticator slot — slot
|
|
8296
|
+
* rename, label change, attachment of UI hints. The slot's `id`,
|
|
8297
|
+
* `method`, and wrap material (`wrapped_kek` / `wrapped_deks` + `iv`)
|
|
8298
|
+
* are immutable through this method. Anti-slot-swap is structural,
|
|
8299
|
+
* not gate-driven.
|
|
8300
|
+
*
|
|
8301
|
+
* `meta` patch semantics (#57-aligned):
|
|
8302
|
+
* - Top-level merge — absent keys preserved
|
|
8303
|
+
* - `null` value — delete that meta key
|
|
8304
|
+
* - Other values — replace verbatim
|
|
8305
|
+
*
|
|
8306
|
+
* Use case: per-slot nickname for "iPhone Touch ID" vs "MacBook
|
|
8307
|
+
* Touch ID" disambiguation in admin UIs. The slot id (auto-derived
|
|
8308
|
+
* from credentialId prefix) is not human-friendly; `meta.nickname`
|
|
8309
|
+
* is.
|
|
8310
|
+
*
|
|
8311
|
+
* Gated by `update-authenticator`. PERSONAL_POLICY: tier-1 unlock
|
|
8312
|
+
* alone (matches enroll/remove). STRICT_POLICY: tier-1 +
|
|
8313
|
+
* TOTP/email-OTP factor proof — a malicious rename on a shared
|
|
8314
|
+
* workstation could mislead the user about which device a slot
|
|
8315
|
+
* corresponds to, so STRICT requires fresh factor binding.
|
|
8316
|
+
*
|
|
8317
|
+
* @throws `NoAccessError` when no slot with the given id exists.
|
|
8318
|
+
* @throws `ValidationError` when no patch field is provided.
|
|
8319
|
+
*
|
|
8320
|
+
* @see #55
|
|
8321
|
+
*/
|
|
8322
|
+
async updateAuthenticator(vault, slotId, options, factors) {
|
|
8323
|
+
await this.checkGate(vault, "update-authenticator", factors);
|
|
8324
|
+
const keyring = await this.getKeyringInternal(vault);
|
|
8325
|
+
const next = await updateAuthenticator(this.options.store, vault, keyring, slotId, options);
|
|
8326
|
+
this.keyringCache.set(vault, next);
|
|
8327
|
+
}
|
|
6991
8328
|
/**
|
|
6992
8329
|
* Native WebAuthn enrollment using the **real** internal keyring (#16).
|
|
6993
8330
|
*
|
|
@@ -7035,9 +8372,9 @@ var Noydb = class {
|
|
|
7035
8372
|
*
|
|
7036
8373
|
* @see #16
|
|
7037
8374
|
*/
|
|
7038
|
-
async enrollWebAuthn(vault, ceremony,
|
|
7039
|
-
await this.checkGate(vault, "enroll-authenticator",
|
|
7040
|
-
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);
|
|
7041
8378
|
const slotOptions = await ceremony(keyring);
|
|
7042
8379
|
if (slotOptions.method !== "webauthn") {
|
|
7043
8380
|
throw new ValidationError(
|
|
@@ -7064,7 +8401,7 @@ var Noydb = class {
|
|
|
7064
8401
|
* @see #16
|
|
7065
8402
|
*/
|
|
7066
8403
|
async listWebAuthnSlots(vault) {
|
|
7067
|
-
const keyring = await this.
|
|
8404
|
+
const keyring = await this.getKeyringInternal(vault);
|
|
7068
8405
|
return keyring.authenticators.filter((a) => a.method === "webauthn").map((a) => {
|
|
7069
8406
|
const credentialId = a.meta.credentialId;
|
|
7070
8407
|
return {
|
|
@@ -7084,7 +8421,7 @@ var Noydb = class {
|
|
|
7084
8421
|
* `checkGate` calls see a tier-2 unlock.
|
|
7085
8422
|
*/
|
|
7086
8423
|
async unlockViaAuthenticator(vault, slotId, verify) {
|
|
7087
|
-
const keyring = await this.
|
|
8424
|
+
const keyring = await this.getKeyringInternal(vault);
|
|
7088
8425
|
const slot = findAuthenticator(keyring, slotId);
|
|
7089
8426
|
if (!slot) {
|
|
7090
8427
|
throw new ValidationError(
|
|
@@ -7182,6 +8519,14 @@ var Noydb = class {
|
|
|
7182
8519
|
* @throws `InvalidKeyError` when `oldPassphrase` is wrong.
|
|
7183
8520
|
*/
|
|
7184
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
|
+
}
|
|
7185
8530
|
await this.checkGate(vault, "rotate-passphrase", factors);
|
|
7186
8531
|
const userId = this.options.user;
|
|
7187
8532
|
const next = await rotatePassphrase(this.options.store, vault, userId, input);
|
|
@@ -7198,7 +8543,7 @@ var Noydb = class {
|
|
|
7198
8543
|
await this.checkGate(vault, "recover-passphrase", factors);
|
|
7199
8544
|
const userId = this.options.user;
|
|
7200
8545
|
const entriesBeforeRecovery = await loadPaperRecoveryEntries(this.options.store, vault);
|
|
7201
|
-
const next = await recoverPassphrase(this.options.store, vault, userId, input);
|
|
8546
|
+
const next = await recoverPassphrase(this.options.shamirRecovery, this.options.store, vault, userId, input);
|
|
7202
8547
|
this.keyringCache.set(vault, next);
|
|
7203
8548
|
const rotateRemaining = input.rotateRemainingCodes ?? true;
|
|
7204
8549
|
const remainingAfterBurn = Math.max(0, entriesBeforeRecovery.length - 1);
|
|
@@ -7218,6 +8563,256 @@ var Noydb = class {
|
|
|
7218
8563
|
await savePaperRecoveryEntries(this.options.store, vault, newEntries);
|
|
7219
8564
|
return { newCodes: codes };
|
|
7220
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
|
+
}
|
|
7221
8816
|
/**
|
|
7222
8817
|
* Atomic peer-recovery — re-wraps an EXISTING user's keyring under
|
|
7223
8818
|
* a fresh temp passphrase in a single store write. Closes #34's
|
|
@@ -7263,7 +8858,7 @@ var Noydb = class {
|
|
|
7263
8858
|
*/
|
|
7264
8859
|
async recoverUser(vault, options, factors) {
|
|
7265
8860
|
await this.checkGate(vault, "peer-recover-user", factors);
|
|
7266
|
-
const callerKeyring = await this.
|
|
8861
|
+
const callerKeyring = await this.getKeyringInternal(vault);
|
|
7267
8862
|
await recoverUser(this.options.store, vault, callerKeyring, options);
|
|
7268
8863
|
if (options.userId === this.options.user) {
|
|
7269
8864
|
this.keyringCache.delete(vault);
|
|
@@ -7301,21 +8896,40 @@ var Noydb = class {
|
|
|
7301
8896
|
* ```
|
|
7302
8897
|
*/
|
|
7303
8898
|
async enrollRecovery(vault, enrollment) {
|
|
7304
|
-
if (enrollment.profile
|
|
7305
|
-
|
|
7306
|
-
|
|
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
|
|
7307
8917
|
);
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
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
|
+
);
|
|
7314
8927
|
}
|
|
7315
|
-
/** Read the persisted
|
|
8928
|
+
/** Read the persisted recovery entries (paper + Shamir). Used by `describeAuthConfig` (#13). */
|
|
7316
8929
|
async listRecoveryEntries(vault) {
|
|
7317
8930
|
const paper = await loadPaperRecoveryEntries(this.options.store, vault);
|
|
7318
|
-
|
|
8931
|
+
const shamir = await loadShamirRecoveryEntries(this.options.store, vault);
|
|
8932
|
+
return { paper, shamir };
|
|
7319
8933
|
}
|
|
7320
8934
|
// ─── Tier-3 enroll / unlock (issue #11) ────────────────────────
|
|
7321
8935
|
/**
|
|
@@ -7327,8 +8941,8 @@ var Noydb = class {
|
|
|
7327
8941
|
* Gated by `rotate-unlock` (the same gate covers "set" and "rotate"
|
|
7328
8942
|
* because tier-3 is a single-slot rolling secret).
|
|
7329
8943
|
*/
|
|
7330
|
-
async enrollUnlock(vault, state,
|
|
7331
|
-
await this.checkGate(vault, "rotate-unlock",
|
|
8944
|
+
async enrollUnlock(vault, state, factors) {
|
|
8945
|
+
await this.checkGate(vault, "rotate-unlock", factors);
|
|
7332
8946
|
this.quickUnlock.set(vault, state);
|
|
7333
8947
|
}
|
|
7334
8948
|
/**
|
|
@@ -7355,8 +8969,17 @@ var Noydb = class {
|
|
|
7355
8969
|
/**
|
|
7356
8970
|
* Public accessor for the unlocked keyring of a vault — issue #28.
|
|
7357
8971
|
*
|
|
7358
|
-
* Returns
|
|
7359
|
-
*
|
|
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
|
+
*
|
|
7360
8983
|
* Used by `@noy-db/on-*` ceremonies that need the live DEK set
|
|
7361
8984
|
* (paper recovery via {@link mintPaperRecoveryEntry}, tier-3 PIN
|
|
7362
8985
|
* enrolment via on-pin's `enrollPin`, custom on-* ceremonies that
|
|
@@ -7371,11 +8994,33 @@ var Noydb = class {
|
|
|
7371
8994
|
* ```ts
|
|
7372
8995
|
* const keyring = await db.getKeyring('acme')
|
|
7373
8996
|
* // keyring.deks: Map<collection, CryptoKey>
|
|
7374
|
-
* // keyring.kek: CryptoKey (
|
|
8997
|
+
* // keyring.kek: CryptoKey | null (null for tier-3 / wrap-DEKs sessions)
|
|
7375
8998
|
* // keyring.role / .permissions / .authenticators
|
|
7376
8999
|
* ```
|
|
7377
9000
|
*/
|
|
7378
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) {
|
|
7379
9024
|
if (this.options.encrypt === false) {
|
|
7380
9025
|
return createPlaintextKeyring(this.options.user);
|
|
7381
9026
|
}
|
|
@@ -7386,20 +9031,36 @@ var Noydb = class {
|
|
|
7386
9031
|
this.keyringCache.set(vault, keyring2);
|
|
7387
9032
|
return keyring2;
|
|
7388
9033
|
}
|
|
7389
|
-
|
|
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) {
|
|
7390
9045
|
throw new ValidationError("A secret (passphrase) or getKeyring callback is required when encryption is enabled");
|
|
7391
9046
|
}
|
|
7392
9047
|
let keyring;
|
|
7393
9048
|
try {
|
|
7394
|
-
keyring = await loadKeyring(this.options.store, vault, this.options.user,
|
|
9049
|
+
keyring = await loadKeyring(this.options.store, vault, this.options.user, effectiveSecret);
|
|
7395
9050
|
} catch (err) {
|
|
7396
9051
|
if (err instanceof NoAccessError) {
|
|
7397
9052
|
keyring = await createOwnerKeyring(
|
|
7398
9053
|
this.options.store,
|
|
7399
9054
|
vault,
|
|
7400
9055
|
this.options.user,
|
|
7401
|
-
|
|
7402
|
-
{
|
|
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
|
+
}
|
|
7403
9064
|
);
|
|
7404
9065
|
} else if (err instanceof InvalidKeyError && this.options.onInvalidKey === "reset") {
|
|
7405
9066
|
await this.options.store.delete(vault, "_keyring", this.options.user);
|
|
@@ -7407,8 +9068,10 @@ var Noydb = class {
|
|
|
7407
9068
|
this.options.store,
|
|
7408
9069
|
vault,
|
|
7409
9070
|
this.options.user,
|
|
7410
|
-
|
|
7411
|
-
{
|
|
9071
|
+
effectiveSecret,
|
|
9072
|
+
{
|
|
9073
|
+
validate: this.options.passphraseMode === "managed" ? false : this.options.validatePassphrase === true
|
|
9074
|
+
}
|
|
7412
9075
|
);
|
|
7413
9076
|
} else {
|
|
7414
9077
|
throw err;
|
|
@@ -7420,10 +9083,28 @@ var Noydb = class {
|
|
|
7420
9083
|
};
|
|
7421
9084
|
async function createNoydb(options) {
|
|
7422
9085
|
const encrypted = options.encrypt !== false;
|
|
9086
|
+
const managed = options.passphraseMode === "managed";
|
|
7423
9087
|
if (options.secret && options.getKeyring) {
|
|
7424
9088
|
throw new ValidationError("Provide either `secret` or `getKeyring`, not both");
|
|
7425
9089
|
}
|
|
7426
|
-
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) {
|
|
7427
9108
|
throw new ValidationError("A secret (passphrase) or getKeyring callback is required when encryption is enabled");
|
|
7428
9109
|
}
|
|
7429
9110
|
return new Noydb(options);
|
|
@@ -7591,6 +9272,7 @@ function shortJSON(value) {
|
|
|
7591
9272
|
export {
|
|
7592
9273
|
Aggregation,
|
|
7593
9274
|
AlreadyElevatedError,
|
|
9275
|
+
AmendmentForbiddenError,
|
|
7594
9276
|
BLOB_CHUNKS_COLLECTION,
|
|
7595
9277
|
BLOB_COLLECTION,
|
|
7596
9278
|
BLOB_INDEX_COLLECTION,
|
|
@@ -7601,6 +9283,7 @@ export {
|
|
|
7601
9283
|
BackupLedgerError,
|
|
7602
9284
|
BlobSet,
|
|
7603
9285
|
BundleIntegrityError,
|
|
9286
|
+
BundleSealMismatchError,
|
|
7604
9287
|
BundleVersionConflictError,
|
|
7605
9288
|
CONSENT_AUDIT_COLLECTION,
|
|
7606
9289
|
Collection,
|
|
@@ -7614,28 +9297,39 @@ export {
|
|
|
7614
9297
|
DEFAULT_PUBLIC_ENVELOPE_SCHEMA,
|
|
7615
9298
|
DELEGATIONS_COLLECTION,
|
|
7616
9299
|
DICT_COLLECTION_PREFIX,
|
|
9300
|
+
DIRECTORY_RECORD_ID,
|
|
7617
9301
|
DanglingReferenceError,
|
|
7618
9302
|
DecryptionError,
|
|
7619
9303
|
DelegationTargetMissingError,
|
|
9304
|
+
DerivationCapExceededError,
|
|
9305
|
+
DerivationCycleError,
|
|
9306
|
+
DerivationDepthError,
|
|
9307
|
+
DerivationOutputShapeError,
|
|
9308
|
+
DerivationOutputUnknownError,
|
|
7620
9309
|
DictKeyInUseError,
|
|
7621
9310
|
DictKeyMissingError,
|
|
7622
9311
|
DictionaryHandle,
|
|
9312
|
+
DirectoryDisabledError,
|
|
7623
9313
|
ELEVATION_AUDIT_COLLECTION,
|
|
7624
9314
|
ElevatedHandle,
|
|
7625
9315
|
ElevationExpiredError,
|
|
7626
9316
|
ExportCapabilityError,
|
|
9317
|
+
FieldFrozenError,
|
|
7627
9318
|
FilenameSanitizationError,
|
|
7628
9319
|
GROUPBY_MAX_CARDINALITY,
|
|
7629
9320
|
GROUPBY_WARN_CARDINALITY,
|
|
7630
9321
|
GroupCardinalityError,
|
|
7631
9322
|
GroupedAggregation,
|
|
7632
9323
|
GroupedQuery,
|
|
9324
|
+
GroupedQueryN,
|
|
7633
9325
|
INDEXED_STORE_POLICY,
|
|
7634
9326
|
ImportCapabilityError,
|
|
7635
9327
|
IndexRequiredError,
|
|
7636
9328
|
IndexWriteFailureError,
|
|
7637
9329
|
InvalidKeyError,
|
|
9330
|
+
InvariantError,
|
|
7638
9331
|
JoinTooLargeError,
|
|
9332
|
+
KeyringCorruptError,
|
|
7639
9333
|
KeyringExpiredError,
|
|
7640
9334
|
LEDGER_COLLECTION,
|
|
7641
9335
|
LEDGER_DELTAS_COLLECTION,
|
|
@@ -7647,6 +9341,12 @@ export {
|
|
|
7647
9341
|
MAGIC_LINK_GRANTS_COLLECTION,
|
|
7648
9342
|
MAGIC_LINK_KEK_INFO_PREFIX,
|
|
7649
9343
|
META_COLLECTION,
|
|
9344
|
+
ManagedRecoveryNotEnrolledError,
|
|
9345
|
+
MaterializedViewConfigError,
|
|
9346
|
+
MaterializedViewCycleError,
|
|
9347
|
+
MaterializedViewSourceUnknownError,
|
|
9348
|
+
MaterializedViewTooLargeError,
|
|
9349
|
+
MemorySealingKeyProvider,
|
|
7650
9350
|
MissingTranslationError,
|
|
7651
9351
|
NOYDB_BACKUP_VERSION,
|
|
7652
9352
|
NOYDB_BUNDLE_FORMAT_VERSION,
|
|
@@ -7660,6 +9360,10 @@ export {
|
|
|
7660
9360
|
NotFoundError,
|
|
7661
9361
|
Noydb,
|
|
7662
9362
|
NoydbError,
|
|
9363
|
+
OverlayBaseIsVirtualError,
|
|
9364
|
+
OverlayCollectionUnavailableError,
|
|
9365
|
+
OverlayIdMismatchError,
|
|
9366
|
+
OverlayNameCollisionError,
|
|
7663
9367
|
PERIODS_COLLECTION,
|
|
7664
9368
|
PERSONAL_POLICY,
|
|
7665
9369
|
POLICY_RECORD_ID,
|
|
@@ -7677,12 +9381,15 @@ export {
|
|
|
7677
9381
|
ReadOnlyAtInstantError,
|
|
7678
9382
|
ReadOnlyError,
|
|
7679
9383
|
ReadOnlyFrameError,
|
|
9384
|
+
RecordLockedError,
|
|
7680
9385
|
RecoveryNotEnrolledError,
|
|
7681
9386
|
RecoveryProfileNotImplementedError,
|
|
7682
9387
|
RefIntegrityError,
|
|
7683
9388
|
RefRegistry,
|
|
7684
9389
|
RefScopeError,
|
|
7685
9390
|
ReservedCollectionNameError,
|
|
9391
|
+
SCHEMAS_COLLECTION,
|
|
9392
|
+
SEALED_PASSPHRASE_RECORD_ID,
|
|
7686
9393
|
STRICT_POLICY,
|
|
7687
9394
|
SYNC_CREDENTIALS_COLLECTION,
|
|
7688
9395
|
ScanBuilder,
|
|
@@ -7705,6 +9412,7 @@ export {
|
|
|
7705
9412
|
USER_ENVELOPE_MAX_BYTES,
|
|
7706
9413
|
UserApi,
|
|
7707
9414
|
UserEnvelopeOversizedError,
|
|
9415
|
+
VISIBILITY_RECORD_PREFIX,
|
|
7708
9416
|
ValidationError,
|
|
7709
9417
|
Vault,
|
|
7710
9418
|
VaultFrame,
|
|
@@ -7738,7 +9446,9 @@ export {
|
|
|
7738
9446
|
dekKey,
|
|
7739
9447
|
deleteCredential,
|
|
7740
9448
|
deleteUserEnvelope,
|
|
9449
|
+
deleteUserVisibility,
|
|
7741
9450
|
deriveMagicLinkContentKey,
|
|
9451
|
+
derivePersistedSchema,
|
|
7742
9452
|
derivePresenceKey,
|
|
7743
9453
|
describeAllUsersAuth,
|
|
7744
9454
|
describeAuthConfig,
|
|
@@ -7784,6 +9494,7 @@ export {
|
|
|
7784
9494
|
isPublicEnvelope,
|
|
7785
9495
|
isSessionAlive,
|
|
7786
9496
|
isULID,
|
|
9497
|
+
isZodSchema,
|
|
7787
9498
|
issueDelegation,
|
|
7788
9499
|
recoverPassphrase as keyringRecoverPassphrase,
|
|
7789
9500
|
rotatePassphrase as keyringRotatePassphrase,
|
|
@@ -7795,7 +9506,10 @@ export {
|
|
|
7795
9506
|
loadActiveDelegations,
|
|
7796
9507
|
loadDevUnlock,
|
|
7797
9508
|
loadPaperRecoveryEntries,
|
|
9509
|
+
loadPersistedSchema,
|
|
7798
9510
|
loadPublicEnvelope,
|
|
9511
|
+
loadSealedPassphrase,
|
|
9512
|
+
loadShamirRecoveryEntries,
|
|
7799
9513
|
loadUserEnvelope,
|
|
7800
9514
|
loadVaultPolicy,
|
|
7801
9515
|
magicLinkGrantRecordId,
|
|
@@ -7804,17 +9518,24 @@ export {
|
|
|
7804
9518
|
mergePolicy,
|
|
7805
9519
|
min,
|
|
7806
9520
|
mintPaperRecoveryEntry,
|
|
9521
|
+
mintShamirRecoveryEntry,
|
|
7807
9522
|
mintWrappedDeksBlob,
|
|
7808
9523
|
paddedIndex,
|
|
7809
9524
|
parseBytes,
|
|
7810
9525
|
parseIndex,
|
|
9526
|
+
parseSealedEnvelope,
|
|
9527
|
+
persistDirectoryConfig,
|
|
9528
|
+
persistSchemaIfNeeded,
|
|
9529
|
+
persistUserVisibility,
|
|
7811
9530
|
putCredential,
|
|
9531
|
+
readDirectoryConfig,
|
|
7812
9532
|
readMagicLinkGrantRecord,
|
|
7813
9533
|
readNoydbBundle,
|
|
7814
9534
|
readNoydbBundleHeader,
|
|
7815
9535
|
readNoydbBundlePublicEnvelope,
|
|
7816
9536
|
readPath,
|
|
7817
9537
|
readPublicEnvelope,
|
|
9538
|
+
readUserVisibility,
|
|
7818
9539
|
recoverUser,
|
|
7819
9540
|
reduceRecords,
|
|
7820
9541
|
ref,
|
|
@@ -7832,13 +9553,17 @@ export {
|
|
|
7832
9553
|
routeStore,
|
|
7833
9554
|
runTransaction,
|
|
7834
9555
|
savePaperRecoveryEntries,
|
|
9556
|
+
savePersistedSchema,
|
|
7835
9557
|
savePublicEnvelope,
|
|
9558
|
+
saveSealedPassphrase,
|
|
9559
|
+
saveShamirRecoveryEntries,
|
|
7836
9560
|
saveUserEnvelope,
|
|
7837
9561
|
saveVaultPolicy,
|
|
7838
|
-
sha256Hex,
|
|
9562
|
+
sha256Hex2 as sha256Hex,
|
|
7839
9563
|
sum,
|
|
7840
9564
|
unwrapDeksFromBlob,
|
|
7841
9565
|
unwrapDeksFromPaperEntry,
|
|
9566
|
+
unwrapDeksFromShamirEntry,
|
|
7842
9567
|
unwrapMagicLinkGrant,
|
|
7843
9568
|
validateI18nTextValue,
|
|
7844
9569
|
validatePassphrase,
|
|
@@ -7846,11 +9571,16 @@ export {
|
|
|
7846
9571
|
validateSchemaInput,
|
|
7847
9572
|
validateSchemaOutput,
|
|
7848
9573
|
validateSessionPolicy,
|
|
9574
|
+
visibilityRecordId,
|
|
7849
9575
|
withCache,
|
|
7850
9576
|
withCircuitBreaker,
|
|
9577
|
+
withDerivation,
|
|
9578
|
+
withGuard,
|
|
7851
9579
|
withHealthCheck,
|
|
7852
9580
|
withLogging,
|
|
9581
|
+
withMaterializedView,
|
|
7853
9582
|
withMetrics,
|
|
9583
|
+
withOverlayedView,
|
|
7854
9584
|
withRetry,
|
|
7855
9585
|
wrapBundleStore,
|
|
7856
9586
|
wrapStore,
|