@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/team/index.cjs
CHANGED
|
@@ -24,15 +24,48 @@ __export(team_exports, {
|
|
|
24
24
|
SYNC_CREDENTIALS_COLLECTION: () => SYNC_CREDENTIALS_COLLECTION,
|
|
25
25
|
SyncEngine: () => SyncEngine,
|
|
26
26
|
SyncTransaction: () => SyncTransaction,
|
|
27
|
+
buildRecipientKeyringFile: () => buildRecipientKeyringFile,
|
|
28
|
+
burnPaperRecoveryEntry: () => burnPaperRecoveryEntry,
|
|
29
|
+
changeSecret: () => changeSecret,
|
|
30
|
+
createOwnerKeyring: () => createOwnerKeyring,
|
|
27
31
|
credentialStatus: () => credentialStatus,
|
|
28
32
|
deleteCredential: () => deleteCredential,
|
|
33
|
+
deriveMagicLinkContentKey: () => deriveMagicLinkContentKey,
|
|
34
|
+
enrollAuthenticator: () => enrollAuthenticator,
|
|
35
|
+
ensureCollectionDEK: () => ensureCollectionDEK,
|
|
29
36
|
evaluateExportCapability: () => evaluateExportCapability,
|
|
30
37
|
evaluateImportCapability: () => evaluateImportCapability,
|
|
38
|
+
findAuthenticator: () => findAuthenticator,
|
|
31
39
|
getCredential: () => getCredential,
|
|
40
|
+
grant: () => grant,
|
|
32
41
|
hasExportCapability: () => hasExportCapability,
|
|
33
42
|
hasImportCapability: () => hasImportCapability,
|
|
43
|
+
isMagicLinkGrantExpired: () => isMagicLinkGrantExpired,
|
|
34
44
|
listCredentials: () => listCredentials,
|
|
35
|
-
|
|
45
|
+
listMagicLinkGrants: () => listMagicLinkGrants,
|
|
46
|
+
listUsers: () => listUsers,
|
|
47
|
+
listUsersWithEnvelopes: () => listUsersWithEnvelopes,
|
|
48
|
+
loadKeyring: () => loadKeyring,
|
|
49
|
+
loadPaperRecoveryEntries: () => loadPaperRecoveryEntries,
|
|
50
|
+
magicLinkGrantRecordId: () => magicLinkGrantRecordId,
|
|
51
|
+
mintPaperRecoveryEntry: () => mintPaperRecoveryEntry,
|
|
52
|
+
mintWrappedDeksBlob: () => mintWrappedDeksBlob,
|
|
53
|
+
persistKeyring: () => persistKeyring,
|
|
54
|
+
putCredential: () => putCredential,
|
|
55
|
+
readMagicLinkGrantRecord: () => readMagicLinkGrantRecord,
|
|
56
|
+
recoverPassphrase: () => recoverPassphrase,
|
|
57
|
+
recoverUser: () => recoverUser,
|
|
58
|
+
removeAuthenticator: () => removeAuthenticator,
|
|
59
|
+
revoke: () => revoke,
|
|
60
|
+
revokeMagicLinkGrant: () => revokeMagicLinkGrant,
|
|
61
|
+
rotatePassphrase: () => rotatePassphrase,
|
|
62
|
+
savePaperRecoveryEntries: () => savePaperRecoveryEntries,
|
|
63
|
+
unwrapDeksFromBlob: () => unwrapDeksFromBlob,
|
|
64
|
+
unwrapDeksFromPaperEntry: () => unwrapDeksFromPaperEntry,
|
|
65
|
+
unwrapMagicLinkGrant: () => unwrapMagicLinkGrant,
|
|
66
|
+
updateAuthenticator: () => updateAuthenticator,
|
|
67
|
+
updateKeyringIdentity: () => updateKeyringIdentity,
|
|
68
|
+
writeMagicLinkGrant: () => writeMagicLinkGrant
|
|
36
69
|
});
|
|
37
70
|
module.exports = __toCommonJS(team_exports);
|
|
38
71
|
|
|
@@ -63,12 +96,83 @@ var TamperedError = class extends NoydbError {
|
|
|
63
96
|
this.name = "TamperedError";
|
|
64
97
|
}
|
|
65
98
|
};
|
|
99
|
+
var InvalidKeyError = class extends NoydbError {
|
|
100
|
+
constructor(message = "Invalid key \u2014 wrong passphrase or corrupted keyring") {
|
|
101
|
+
super("INVALID_KEY", message);
|
|
102
|
+
this.name = "InvalidKeyError";
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
var KeyringCorruptError = class extends NoydbError {
|
|
106
|
+
failedCollections;
|
|
107
|
+
intactCount;
|
|
108
|
+
constructor(opts) {
|
|
109
|
+
super(
|
|
110
|
+
"KEYRING_CORRUPT",
|
|
111
|
+
opts.message ?? `Keyring has ${opts.failedCollections.length} corrupted wrapped DEK(s) (${opts.failedCollections.join(", ")}); ${opts.intactCount} other DEK(s) unwrapped successfully \u2014 the passphrase is correct, the entries are damaged. Do NOT use onInvalidKey: 'reset' here \u2014 that would destroy the intact DEKs.`
|
|
112
|
+
);
|
|
113
|
+
this.name = "KeyringCorruptError";
|
|
114
|
+
this.failedCollections = opts.failedCollections;
|
|
115
|
+
this.intactCount = opts.intactCount;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var NoAccessError = class extends NoydbError {
|
|
119
|
+
constructor(message = "No access \u2014 user does not have a key for this collection") {
|
|
120
|
+
super("NO_ACCESS", message);
|
|
121
|
+
this.name = "NoAccessError";
|
|
122
|
+
}
|
|
123
|
+
};
|
|
66
124
|
var PermissionDeniedError = class extends NoydbError {
|
|
67
125
|
constructor(message = "Permission denied \u2014 insufficient role for this operation") {
|
|
68
126
|
super("PERMISSION_DENIED", message);
|
|
69
127
|
this.name = "PermissionDeniedError";
|
|
70
128
|
}
|
|
71
129
|
};
|
|
130
|
+
var KeyringExpiredError = class extends NoydbError {
|
|
131
|
+
userId;
|
|
132
|
+
expiresAt;
|
|
133
|
+
constructor(opts) {
|
|
134
|
+
super(
|
|
135
|
+
"KEYRING_EXPIRED",
|
|
136
|
+
`Keyring "${opts.userId}" expired at ${opts.expiresAt}. The slot refuses to unlock past its expiry timestamp.`
|
|
137
|
+
);
|
|
138
|
+
this.name = "KeyringExpiredError";
|
|
139
|
+
this.userId = opts.userId;
|
|
140
|
+
this.expiresAt = opts.expiresAt;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
var PrivilegeEscalationError = class extends NoydbError {
|
|
144
|
+
offendingCollection;
|
|
145
|
+
constructor(offendingCollection, message) {
|
|
146
|
+
super(
|
|
147
|
+
"PRIVILEGE_ESCALATION",
|
|
148
|
+
message ?? `Privilege escalation: grantor has no DEK for collection "${offendingCollection}" and cannot grant access to it.`
|
|
149
|
+
);
|
|
150
|
+
this.name = "PrivilegeEscalationError";
|
|
151
|
+
this.offendingCollection = offendingCollection;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
var DirectoryDisabledError = class extends NoydbError {
|
|
155
|
+
vault;
|
|
156
|
+
constructor(vault) {
|
|
157
|
+
super(
|
|
158
|
+
"DIRECTORY_DISABLED",
|
|
159
|
+
`Vault "${vault}" has its user directory disabled. Only owners and admins can call listUsersWithEnvelopes() here. Use db.setDirectoryEnabled(vault, true) to re-enable.`
|
|
160
|
+
);
|
|
161
|
+
this.name = "DirectoryDisabledError";
|
|
162
|
+
this.vault = vault;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
var DelegationTargetMissingError = class extends NoydbError {
|
|
166
|
+
toUser;
|
|
167
|
+
constructor(toUser) {
|
|
168
|
+
super(
|
|
169
|
+
"DELEGATION_TARGET_MISSING",
|
|
170
|
+
`Delegation target user "${toUser}" has no keyring in this vault`
|
|
171
|
+
);
|
|
172
|
+
this.name = "DelegationTargetMissingError";
|
|
173
|
+
this.toUser = toUser;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
72
176
|
var ConflictError = class extends NoydbError {
|
|
73
177
|
/** The actual stored version at the time of conflict. */
|
|
74
178
|
version;
|
|
@@ -86,9 +190,32 @@ var ValidationError = class extends NoydbError {
|
|
|
86
190
|
};
|
|
87
191
|
|
|
88
192
|
// src/crypto.ts
|
|
193
|
+
var PBKDF2_ITERATIONS = 6e5;
|
|
194
|
+
var SALT_BYTES = 32;
|
|
89
195
|
var IV_BYTES = 12;
|
|
90
196
|
var KEY_BITS = 256;
|
|
91
197
|
var subtle = globalThis.crypto.subtle;
|
|
198
|
+
async function deriveKey(passphrase, salt) {
|
|
199
|
+
const keyMaterial = await subtle.importKey(
|
|
200
|
+
"raw",
|
|
201
|
+
new TextEncoder().encode(passphrase),
|
|
202
|
+
"PBKDF2",
|
|
203
|
+
false,
|
|
204
|
+
["deriveKey"]
|
|
205
|
+
);
|
|
206
|
+
return subtle.deriveKey(
|
|
207
|
+
{
|
|
208
|
+
name: "PBKDF2",
|
|
209
|
+
salt,
|
|
210
|
+
iterations: PBKDF2_ITERATIONS,
|
|
211
|
+
hash: "SHA-256"
|
|
212
|
+
},
|
|
213
|
+
keyMaterial,
|
|
214
|
+
{ name: "AES-KW", length: KEY_BITS },
|
|
215
|
+
false,
|
|
216
|
+
["wrapKey", "unwrapKey"]
|
|
217
|
+
);
|
|
218
|
+
}
|
|
92
219
|
async function generateDEK() {
|
|
93
220
|
return subtle.generateKey(
|
|
94
221
|
{ name: "AES-GCM", length: KEY_BITS },
|
|
@@ -101,6 +228,21 @@ async function wrapKey(dek, kek) {
|
|
|
101
228
|
const wrapped = await subtle.wrapKey("raw", dek, kek, "AES-KW");
|
|
102
229
|
return bufferToBase64(wrapped);
|
|
103
230
|
}
|
|
231
|
+
async function unwrapKey(wrappedBase64, kek) {
|
|
232
|
+
try {
|
|
233
|
+
return await subtle.unwrapKey(
|
|
234
|
+
"raw",
|
|
235
|
+
base64ToBuffer(wrappedBase64),
|
|
236
|
+
kek,
|
|
237
|
+
"AES-KW",
|
|
238
|
+
{ name: "AES-GCM", length: KEY_BITS },
|
|
239
|
+
true,
|
|
240
|
+
["encrypt", "decrypt"]
|
|
241
|
+
);
|
|
242
|
+
} catch {
|
|
243
|
+
throw new InvalidKeyError();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
104
246
|
async function encrypt(plaintext, dek) {
|
|
105
247
|
const iv = generateIV();
|
|
106
248
|
const encoded = new TextEncoder().encode(plaintext);
|
|
@@ -160,6 +302,9 @@ async function derivePresenceKey(dek, collectionName) {
|
|
|
160
302
|
function generateIV() {
|
|
161
303
|
return globalThis.crypto.getRandomValues(new Uint8Array(IV_BYTES));
|
|
162
304
|
}
|
|
305
|
+
function generateSalt() {
|
|
306
|
+
return globalThis.crypto.getRandomValues(new Uint8Array(SALT_BYTES));
|
|
307
|
+
}
|
|
163
308
|
function bufferToBase64(buffer) {
|
|
164
309
|
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
165
310
|
let binary = "";
|
|
@@ -177,7 +322,686 @@ function base64ToBuffer(base64) {
|
|
|
177
322
|
return bytes;
|
|
178
323
|
}
|
|
179
324
|
|
|
325
|
+
// src/directory/storage.ts
|
|
326
|
+
var META_COLLECTION = "_meta";
|
|
327
|
+
var DIRECTORY_RECORD_ID = "directory";
|
|
328
|
+
async function readDirectoryConfig(store, vault) {
|
|
329
|
+
const envelope = await store.get(vault, META_COLLECTION, DIRECTORY_RECORD_ID);
|
|
330
|
+
if (!envelope) return void 0;
|
|
331
|
+
try {
|
|
332
|
+
const parsed = JSON.parse(envelope._data);
|
|
333
|
+
if (!isDirectoryConfig(parsed)) return void 0;
|
|
334
|
+
return parsed;
|
|
335
|
+
} catch {
|
|
336
|
+
return void 0;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function isDirectoryConfig(x) {
|
|
340
|
+
if (x === null || typeof x !== "object") return false;
|
|
341
|
+
if (!("enabled" in x)) return false;
|
|
342
|
+
return typeof x.enabled === "boolean";
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/directory/visibility.ts
|
|
346
|
+
var VISIBILITY_RECORD_PREFIX = "visibility/";
|
|
347
|
+
function visibilityRecordId(keyringId) {
|
|
348
|
+
return VISIBILITY_RECORD_PREFIX + keyringId;
|
|
349
|
+
}
|
|
350
|
+
async function readUserVisibility(store, vault, keyringId) {
|
|
351
|
+
const envelope = await store.get(vault, META_COLLECTION, visibilityRecordId(keyringId));
|
|
352
|
+
if (!envelope) return void 0;
|
|
353
|
+
try {
|
|
354
|
+
const parsed = JSON.parse(envelope._data);
|
|
355
|
+
if (!isUserVisibility(parsed)) return void 0;
|
|
356
|
+
return parsed;
|
|
357
|
+
} catch {
|
|
358
|
+
return void 0;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async function deleteUserVisibility(store, vault, keyringId) {
|
|
362
|
+
await store.delete(vault, META_COLLECTION, visibilityRecordId(keyringId));
|
|
363
|
+
}
|
|
364
|
+
function isUserVisibility(x) {
|
|
365
|
+
if (x === null || typeof x !== "object") return false;
|
|
366
|
+
if (!("hidden" in x)) return false;
|
|
367
|
+
return typeof x.hidden === "boolean";
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/validation.ts
|
|
371
|
+
var WeakPassphraseError = class extends NoydbError {
|
|
372
|
+
reason;
|
|
373
|
+
suggestion;
|
|
374
|
+
constructor(reason, suggestion) {
|
|
375
|
+
super("WEAK_PASSPHRASE", `Weak passphrase (${reason}). ${suggestion}`);
|
|
376
|
+
this.name = "WeakPassphraseError";
|
|
377
|
+
this.reason = reason;
|
|
378
|
+
this.suggestion = suggestion;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
var DEFAULT_MIN_WORDS = 6;
|
|
382
|
+
var DEFAULT_MIN_WORD_LENGTH = 3;
|
|
383
|
+
var SUGGESTIONS = {
|
|
384
|
+
empty: "Provide a phrase of at least 6 lowercase words separated by single spaces.",
|
|
385
|
+
"invalid-chars": "Use only lowercase letters [a-z] and single spaces. No punctuation, symbols, digits, or uppercase.",
|
|
386
|
+
"leading-or-trailing-space": "Trim leading and trailing spaces.",
|
|
387
|
+
"double-space": "Use exactly one space between words.",
|
|
388
|
+
"too-few-words": 'Use at least 6 words by default (8 under strict policy). Example: "correct horse battery staple printer toaster".',
|
|
389
|
+
"word-too-short": 'Each word must be at least 3 characters. Drop short fillers like "a", "is", "of".',
|
|
390
|
+
"repeated-adjacent": "Avoid repeating the same word twice in a row."
|
|
391
|
+
};
|
|
392
|
+
function validatePassphrase(s, opts) {
|
|
393
|
+
if (opts?.customValidator) {
|
|
394
|
+
return opts.customValidator(s);
|
|
395
|
+
}
|
|
396
|
+
const minWords = opts?.minWords ?? DEFAULT_MIN_WORDS;
|
|
397
|
+
const minWordLength = opts?.minWordLength ?? DEFAULT_MIN_WORD_LENGTH;
|
|
398
|
+
const rejectRepeated = opts?.rejectRepeatedAdjacent ?? true;
|
|
399
|
+
if (s.length === 0) {
|
|
400
|
+
return { ok: false, reason: "empty" };
|
|
401
|
+
}
|
|
402
|
+
if (s !== s.trim()) {
|
|
403
|
+
return { ok: false, reason: "leading-or-trailing-space" };
|
|
404
|
+
}
|
|
405
|
+
if (s.includes(" ")) {
|
|
406
|
+
return { ok: false, reason: "double-space" };
|
|
407
|
+
}
|
|
408
|
+
const charPattern = opts?.pattern ?? /^[a-z]+( [a-z]+)*$/;
|
|
409
|
+
if (!charPattern.test(s)) {
|
|
410
|
+
return { ok: false, reason: "invalid-chars" };
|
|
411
|
+
}
|
|
412
|
+
const words = s.split(" ");
|
|
413
|
+
if (words.length < minWords) {
|
|
414
|
+
return { ok: false, reason: "too-few-words", minimum: minWords, got: words.length };
|
|
415
|
+
}
|
|
416
|
+
for (const w of words) {
|
|
417
|
+
if (w.length < minWordLength) {
|
|
418
|
+
return { ok: false, reason: "word-too-short", minimum: minWordLength, got: w.length };
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (rejectRepeated) {
|
|
422
|
+
for (let i = 1; i < words.length; i++) {
|
|
423
|
+
if (words[i] === words[i - 1]) {
|
|
424
|
+
return { ok: false, reason: "repeated-adjacent" };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return { ok: true, words: words.length };
|
|
429
|
+
}
|
|
430
|
+
function assertStrongPassphrase(s, opts) {
|
|
431
|
+
if (opts?.allowWeakPassphrase) return;
|
|
432
|
+
const result = validatePassphrase(s, opts);
|
|
433
|
+
if (result.ok) return;
|
|
434
|
+
throw new WeakPassphraseError(result.reason, SUGGESTIONS[result.reason]);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/meta/user-envelope/types.ts
|
|
438
|
+
var USER_ENVELOPE_MAX_BYTES = 64 * 1024;
|
|
439
|
+
var USER_ENVELOPE_COLLECTION = "_users";
|
|
440
|
+
var UserEnvelopeOversizedError = class extends NoydbError {
|
|
441
|
+
bytes;
|
|
442
|
+
limit;
|
|
443
|
+
constructor(bytes, limit = USER_ENVELOPE_MAX_BYTES) {
|
|
444
|
+
super(
|
|
445
|
+
"USER_ENVELOPE_OVERSIZED",
|
|
446
|
+
`User envelope payload is ${bytes} bytes; soft cap is ${limit} bytes. Move large data into the vault's regular collections.`
|
|
447
|
+
);
|
|
448
|
+
this.name = "UserEnvelopeOversizedError";
|
|
449
|
+
this.bytes = bytes;
|
|
450
|
+
this.limit = limit;
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
// src/meta/user-envelope/storage.ts
|
|
455
|
+
async function loadUserEnvelope(store, vault, keyringId, dek) {
|
|
456
|
+
const envelope = await store.get(vault, USER_ENVELOPE_COLLECTION, keyringId);
|
|
457
|
+
if (!envelope) return null;
|
|
458
|
+
const plaintext = await decrypt(envelope._iv, envelope._data, dek);
|
|
459
|
+
const data = JSON.parse(plaintext);
|
|
460
|
+
return {
|
|
461
|
+
keyringId,
|
|
462
|
+
data,
|
|
463
|
+
_v: envelope._v,
|
|
464
|
+
_ts: envelope._ts
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
async function saveUserEnvelope(store, vault, keyringId, payload, dek, expectedVersion) {
|
|
468
|
+
const json = JSON.stringify(payload);
|
|
469
|
+
const bytes = new TextEncoder().encode(json).byteLength;
|
|
470
|
+
if (bytes > USER_ENVELOPE_MAX_BYTES) {
|
|
471
|
+
throw new UserEnvelopeOversizedError(bytes);
|
|
472
|
+
}
|
|
473
|
+
const prior = await store.get(vault, USER_ENVELOPE_COLLECTION, keyringId);
|
|
474
|
+
if (expectedVersion !== void 0) {
|
|
475
|
+
const priorVersion = prior?._v ?? 0;
|
|
476
|
+
if (priorVersion !== expectedVersion) {
|
|
477
|
+
throw new ConflictError(
|
|
478
|
+
priorVersion,
|
|
479
|
+
`User envelope for "${keyringId}" expected version ${expectedVersion}, actual ${priorVersion}`
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const nextVersion = (prior?._v ?? 0) + 1;
|
|
484
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
485
|
+
const { iv, data } = await encrypt(json, dek);
|
|
486
|
+
const envelope = {
|
|
487
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
488
|
+
_v: nextVersion,
|
|
489
|
+
_ts: ts,
|
|
490
|
+
_iv: iv,
|
|
491
|
+
_data: data
|
|
492
|
+
};
|
|
493
|
+
await store.put(vault, USER_ENVELOPE_COLLECTION, keyringId, envelope);
|
|
494
|
+
return {
|
|
495
|
+
keyringId,
|
|
496
|
+
data: payload,
|
|
497
|
+
_v: nextVersion,
|
|
498
|
+
_ts: ts
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
async function deleteUserEnvelope(store, vault, keyringId) {
|
|
502
|
+
await store.delete(vault, USER_ENVELOPE_COLLECTION, keyringId);
|
|
503
|
+
}
|
|
504
|
+
|
|
180
505
|
// src/team/keyring.ts
|
|
506
|
+
var ADMIN_GRANTABLE_TARGETS = ["operator", "viewer", "client", "admin"];
|
|
507
|
+
function canGrant(callerRole, targetRole) {
|
|
508
|
+
if (callerRole === "owner") return true;
|
|
509
|
+
if (callerRole === "admin") return ADMIN_GRANTABLE_TARGETS.includes(targetRole);
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
function canRevoke(callerRole, targetRole) {
|
|
513
|
+
if (targetRole === "owner") return false;
|
|
514
|
+
if (callerRole === "owner") return true;
|
|
515
|
+
if (callerRole === "admin") return ADMIN_GRANTABLE_TARGETS.includes(targetRole);
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
function canUpdateRole(callerRole, targetRole) {
|
|
519
|
+
if (callerRole === "owner") return true;
|
|
520
|
+
if (callerRole === "admin") return ADMIN_GRANTABLE_TARGETS.includes(targetRole);
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
var CANARY_PLAINTEXT_BYTES = new Uint8Array(32);
|
|
524
|
+
var canaryKeyPromise = null;
|
|
525
|
+
function getCanaryKey() {
|
|
526
|
+
if (canaryKeyPromise === null) {
|
|
527
|
+
canaryKeyPromise = globalThis.crypto.subtle.importKey(
|
|
528
|
+
"raw",
|
|
529
|
+
CANARY_PLAINTEXT_BYTES,
|
|
530
|
+
{ name: "AES-GCM", length: 256 },
|
|
531
|
+
true,
|
|
532
|
+
// extractable so AES-KW can wrap it
|
|
533
|
+
["encrypt", "decrypt"]
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
return canaryKeyPromise;
|
|
537
|
+
}
|
|
538
|
+
async function mintKeyringCanary(kek) {
|
|
539
|
+
const canaryKey = await getCanaryKey();
|
|
540
|
+
return wrapKey(canaryKey, kek);
|
|
541
|
+
}
|
|
542
|
+
async function verifyKeyringCanary(wrappedCanary, kek) {
|
|
543
|
+
try {
|
|
544
|
+
await unwrapKey(wrappedCanary, kek);
|
|
545
|
+
return true;
|
|
546
|
+
} catch {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
async function loadKeyring(adapter, vault, userId, passphrase) {
|
|
551
|
+
const envelope = await adapter.get(vault, "_keyring", userId);
|
|
552
|
+
if (!envelope) {
|
|
553
|
+
throw new NoAccessError(`No keyring found for user "${userId}" in vault "${vault}"`);
|
|
554
|
+
}
|
|
555
|
+
const keyringFile = JSON.parse(envelope._data);
|
|
556
|
+
if (keyringFile.expires_at !== void 0) {
|
|
557
|
+
const cutoff = Date.parse(keyringFile.expires_at);
|
|
558
|
+
if (Number.isFinite(cutoff) && Date.now() >= cutoff) {
|
|
559
|
+
throw new KeyringExpiredError({ userId: keyringFile.user_id, expiresAt: keyringFile.expires_at });
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const salt = base64ToBuffer(keyringFile.salt);
|
|
563
|
+
const kek = await deriveKey(passphrase, salt);
|
|
564
|
+
const canaryOk = keyringFile.canary !== void 0 ? await verifyKeyringCanary(keyringFile.canary, kek) : null;
|
|
565
|
+
const deks = /* @__PURE__ */ new Map();
|
|
566
|
+
const failedCollections = [];
|
|
567
|
+
let firstUnwrapError = null;
|
|
568
|
+
for (const [collName, wrappedDek] of Object.entries(keyringFile.deks)) {
|
|
569
|
+
try {
|
|
570
|
+
const dek = await unwrapKey(wrappedDek, kek);
|
|
571
|
+
deks.set(collName, dek);
|
|
572
|
+
} catch (err) {
|
|
573
|
+
failedCollections.push(collName);
|
|
574
|
+
if (firstUnwrapError === null) firstUnwrapError = err;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (canaryOk === true) {
|
|
578
|
+
if (failedCollections.length > 0) {
|
|
579
|
+
throw new KeyringCorruptError({ failedCollections, intactCount: deks.size });
|
|
580
|
+
}
|
|
581
|
+
} else if (canaryOk === false) {
|
|
582
|
+
if (deks.size > 0) {
|
|
583
|
+
throw new KeyringCorruptError({
|
|
584
|
+
failedCollections: [...failedCollections, "_canary"],
|
|
585
|
+
intactCount: deks.size
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
throw firstUnwrapError instanceof Error ? firstUnwrapError : new InvalidKeyError();
|
|
589
|
+
} else {
|
|
590
|
+
if (failedCollections.length > 0) {
|
|
591
|
+
if (deks.size > 0) {
|
|
592
|
+
throw new KeyringCorruptError({ failedCollections, intactCount: deks.size });
|
|
593
|
+
}
|
|
594
|
+
throw firstUnwrapError instanceof Error ? firstUnwrapError : new InvalidKeyError();
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return {
|
|
598
|
+
userId: keyringFile.user_id,
|
|
599
|
+
displayName: keyringFile.display_name,
|
|
600
|
+
role: keyringFile.role,
|
|
601
|
+
permissions: keyringFile.permissions,
|
|
602
|
+
deks,
|
|
603
|
+
kek,
|
|
604
|
+
salt,
|
|
605
|
+
authenticators: keyringFile.authenticators ?? [],
|
|
606
|
+
...keyringFile.export_capability !== void 0 && { exportCapability: keyringFile.export_capability },
|
|
607
|
+
...keyringFile.import_capability !== void 0 && { importCapability: keyringFile.import_capability },
|
|
608
|
+
...keyringFile.policy !== void 0 && { policy: keyringFile.policy }
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
async function createOwnerKeyring(adapter, vault, userId, passphrase, passphraseOpts) {
|
|
612
|
+
if (passphraseOpts?.validate && !passphraseOpts.allowWeakPassphrase) {
|
|
613
|
+
assertStrongPassphrase(passphrase, passphraseOpts);
|
|
614
|
+
}
|
|
615
|
+
const salt = generateSalt();
|
|
616
|
+
const kek = await deriveKey(passphrase, salt);
|
|
617
|
+
const userEnvelopeDek = await generateDEK();
|
|
618
|
+
const wrappedUserEnvelopeDek = await wrapKey(userEnvelopeDek, kek);
|
|
619
|
+
const canary = await mintKeyringCanary(kek);
|
|
620
|
+
const keyringFile = {
|
|
621
|
+
_noydb_keyring: NOYDB_KEYRING_VERSION,
|
|
622
|
+
user_id: userId,
|
|
623
|
+
display_name: userId,
|
|
624
|
+
role: "owner",
|
|
625
|
+
permissions: {},
|
|
626
|
+
deks: { [USER_ENVELOPE_COLLECTION]: wrappedUserEnvelopeDek },
|
|
627
|
+
salt: bufferToBase64(salt),
|
|
628
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
629
|
+
granted_by: userId,
|
|
630
|
+
canary
|
|
631
|
+
};
|
|
632
|
+
await writeKeyringFile(adapter, vault, userId, keyringFile);
|
|
633
|
+
return {
|
|
634
|
+
userId,
|
|
635
|
+
displayName: userId,
|
|
636
|
+
role: "owner",
|
|
637
|
+
permissions: {},
|
|
638
|
+
deks: /* @__PURE__ */ new Map([[USER_ENVELOPE_COLLECTION, userEnvelopeDek]]),
|
|
639
|
+
kek,
|
|
640
|
+
salt,
|
|
641
|
+
authenticators: []
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
async function grant(adapter, vault, callerKeyring, options) {
|
|
645
|
+
if (!callerKeyring.kek) {
|
|
646
|
+
throw new ValidationError(
|
|
647
|
+
"grant: caller keyring has no KEK \u2014 tier-2 wrap-DEKs and tier-3 PIN-resume sessions cannot grant access to other users. Re-authenticate at tier 1 (passphrase) before granting."
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
if (!canGrant(callerKeyring.role, options.role)) {
|
|
651
|
+
throw new PermissionDeniedError(
|
|
652
|
+
`Role "${callerKeyring.role}" cannot grant role "${options.role}"`
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
if (options.validatePassphrase && !options.allowWeakPassphrase) {
|
|
656
|
+
assertStrongPassphrase(options.passphrase);
|
|
657
|
+
}
|
|
658
|
+
const permissions = resolvePermissions(options.role, options.permissions);
|
|
659
|
+
const newSalt = generateSalt();
|
|
660
|
+
const newKek = await deriveKey(options.passphrase, newSalt);
|
|
661
|
+
const wrappedDeks = {};
|
|
662
|
+
for (const collName of Object.keys(permissions)) {
|
|
663
|
+
const dek = callerKeyring.deks.get(collName);
|
|
664
|
+
if (dek) {
|
|
665
|
+
wrappedDeks[collName] = await wrapKey(dek, newKek);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (options.role === "owner" || options.role === "admin" || options.role === "viewer") {
|
|
669
|
+
for (const [collName, dek] of callerKeyring.deks) {
|
|
670
|
+
if (!(collName in wrappedDeks)) {
|
|
671
|
+
wrappedDeks[collName] = await wrapKey(dek, newKek);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
for (const [collName, dek] of callerKeyring.deks) {
|
|
676
|
+
if (collName.startsWith("_") && !(collName in wrappedDeks)) {
|
|
677
|
+
wrappedDeks[collName] = await wrapKey(dek, newKek);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
for (const collName of Object.keys(wrappedDeks)) {
|
|
681
|
+
if (!callerKeyring.deks.has(collName)) {
|
|
682
|
+
throw new PrivilegeEscalationError(collName);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const canary = await mintKeyringCanary(newKek);
|
|
686
|
+
const keyringFile = {
|
|
687
|
+
_noydb_keyring: NOYDB_KEYRING_VERSION,
|
|
688
|
+
user_id: options.userId,
|
|
689
|
+
display_name: options.displayName,
|
|
690
|
+
role: options.role,
|
|
691
|
+
permissions,
|
|
692
|
+
deks: wrappedDeks,
|
|
693
|
+
salt: bufferToBase64(newSalt),
|
|
694
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
695
|
+
granted_by: callerKeyring.userId,
|
|
696
|
+
canary,
|
|
697
|
+
...options.exportCapability !== void 0 && { export_capability: options.exportCapability },
|
|
698
|
+
...options.importCapability !== void 0 && { import_capability: options.importCapability }
|
|
699
|
+
};
|
|
700
|
+
await writeKeyringFile(adapter, vault, options.userId, keyringFile);
|
|
701
|
+
const userEnvelopeDek = callerKeyring.deks.get(USER_ENVELOPE_COLLECTION);
|
|
702
|
+
if (userEnvelopeDek) {
|
|
703
|
+
const initialPayload = options.initialProfile ?? {};
|
|
704
|
+
await saveUserEnvelope(
|
|
705
|
+
adapter,
|
|
706
|
+
vault,
|
|
707
|
+
options.userId,
|
|
708
|
+
initialPayload,
|
|
709
|
+
userEnvelopeDek
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
async function findAdminDescendants(adapter, vault, rootUserId) {
|
|
714
|
+
const allUserIds = await adapter.list(vault, "_keyring");
|
|
715
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
716
|
+
for (const userId of allUserIds) {
|
|
717
|
+
const env = await adapter.get(vault, "_keyring", userId);
|
|
718
|
+
if (!env) continue;
|
|
719
|
+
const kf = JSON.parse(env._data);
|
|
720
|
+
if (kf.role !== "admin") continue;
|
|
721
|
+
if (kf.user_id === rootUserId) continue;
|
|
722
|
+
const list = childrenByParent.get(kf.granted_by) ?? [];
|
|
723
|
+
list.push(kf.user_id);
|
|
724
|
+
childrenByParent.set(kf.granted_by, list);
|
|
725
|
+
}
|
|
726
|
+
const visited = /* @__PURE__ */ new Set();
|
|
727
|
+
const order = [];
|
|
728
|
+
const stack = [...childrenByParent.get(rootUserId) ?? []];
|
|
729
|
+
while (stack.length > 0) {
|
|
730
|
+
const next = stack.pop();
|
|
731
|
+
if (visited.has(next)) continue;
|
|
732
|
+
visited.add(next);
|
|
733
|
+
order.push(next);
|
|
734
|
+
for (const grandchild of childrenByParent.get(next) ?? []) {
|
|
735
|
+
if (!visited.has(grandchild)) stack.push(grandchild);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return order;
|
|
739
|
+
}
|
|
740
|
+
async function revoke(adapter, vault, callerKeyring, options) {
|
|
741
|
+
const targetEnvelope = await adapter.get(vault, "_keyring", options.userId);
|
|
742
|
+
if (!targetEnvelope) {
|
|
743
|
+
throw new NoAccessError(`User "${options.userId}" has no keyring in vault "${vault}"`);
|
|
744
|
+
}
|
|
745
|
+
const targetKeyring = JSON.parse(targetEnvelope._data);
|
|
746
|
+
if (!canRevoke(callerKeyring.role, targetKeyring.role)) {
|
|
747
|
+
throw new PermissionDeniedError(
|
|
748
|
+
`Role "${callerKeyring.role}" cannot revoke role "${targetKeyring.role}"`
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
const cascadeMode = options.cascade ?? "strict";
|
|
752
|
+
const usersToRevoke = [options.userId];
|
|
753
|
+
const affectedCollections = new Set(Object.keys(targetKeyring.deks));
|
|
754
|
+
if (targetKeyring.role === "admin") {
|
|
755
|
+
const descendants = await findAdminDescendants(adapter, vault, options.userId);
|
|
756
|
+
if (descendants.length > 0) {
|
|
757
|
+
if (cascadeMode === "warn") {
|
|
758
|
+
console.warn(
|
|
759
|
+
`[noy-db] revoke(${options.userId}): cascade='warn' \u2014 leaving ${descendants.length} descendant admin(s) in place: ${descendants.join(", ")}. These admins were granted by the revoked user (transitively) and will become orphans in the delegation tree.`
|
|
760
|
+
);
|
|
761
|
+
} else {
|
|
762
|
+
for (const userId of descendants) {
|
|
763
|
+
const descEnv = await adapter.get(vault, "_keyring", userId);
|
|
764
|
+
if (!descEnv) continue;
|
|
765
|
+
const descKf = JSON.parse(descEnv._data);
|
|
766
|
+
usersToRevoke.push(userId);
|
|
767
|
+
for (const c of Object.keys(descKf.deks)) affectedCollections.add(c);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
for (const userId of usersToRevoke) {
|
|
773
|
+
await adapter.delete(vault, "_keyring", userId);
|
|
774
|
+
await deleteUserEnvelope(adapter, vault, userId);
|
|
775
|
+
await deleteUserVisibility(adapter, vault, userId);
|
|
776
|
+
}
|
|
777
|
+
if (options.rotateKeys !== false && affectedCollections.size > 0) {
|
|
778
|
+
await rotateKeys(adapter, vault, callerKeyring, [...affectedCollections]);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
async function updateKeyringIdentity(adapter, vault, callerKeyring, options) {
|
|
782
|
+
if (options.role === void 0 && options.displayName === void 0 && options.permissions === void 0) {
|
|
783
|
+
throw new ValidationError(
|
|
784
|
+
`updateUser: at least one of role / displayName / permissions must be provided (userId: "${options.userId}").`
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
const env = await adapter.get(vault, "_keyring", options.userId);
|
|
788
|
+
if (!env) {
|
|
789
|
+
throw new NoAccessError(
|
|
790
|
+
`updateUser: user "${options.userId}" has no keyring in vault "${vault}".`
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
const target = JSON.parse(env._data);
|
|
794
|
+
if (!canUpdateRole(callerKeyring.role, target.role)) {
|
|
795
|
+
throw new PermissionDeniedError(
|
|
796
|
+
`Role "${callerKeyring.role}" cannot update a keyring with role "${target.role}"`
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
if (options.role !== void 0 && options.role !== target.role && !canUpdateRole(callerKeyring.role, options.role)) {
|
|
800
|
+
throw new PermissionDeniedError(
|
|
801
|
+
`Role "${callerKeyring.role}" cannot promote target to role "${options.role}"`
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
const next = {
|
|
805
|
+
...target,
|
|
806
|
+
...options.role !== void 0 && { role: options.role },
|
|
807
|
+
...options.displayName !== void 0 && {
|
|
808
|
+
// null clears the field (stored as ""); a string sets it.
|
|
809
|
+
display_name: options.displayName ?? ""
|
|
810
|
+
},
|
|
811
|
+
...options.permissions !== void 0 && { permissions: options.permissions }
|
|
812
|
+
};
|
|
813
|
+
await writeKeyringFile(adapter, vault, options.userId, next);
|
|
814
|
+
}
|
|
815
|
+
async function rotateKeys(adapter, vault, callerKeyring, collections) {
|
|
816
|
+
const newDeks = /* @__PURE__ */ new Map();
|
|
817
|
+
for (const collName of collections) {
|
|
818
|
+
newDeks.set(collName, await generateDEK());
|
|
819
|
+
}
|
|
820
|
+
for (const collName of collections) {
|
|
821
|
+
const oldDek = callerKeyring.deks.get(collName);
|
|
822
|
+
const newDek = newDeks.get(collName);
|
|
823
|
+
if (!oldDek) continue;
|
|
824
|
+
const ids = await adapter.list(vault, collName);
|
|
825
|
+
for (const id of ids) {
|
|
826
|
+
const envelope = await adapter.get(vault, collName, id);
|
|
827
|
+
if (!envelope || !envelope._iv) continue;
|
|
828
|
+
const plaintext = await decrypt(envelope._iv, envelope._data, oldDek);
|
|
829
|
+
const { iv, data } = await encrypt(plaintext, newDek);
|
|
830
|
+
const newEnvelope = {
|
|
831
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
832
|
+
_v: envelope._v,
|
|
833
|
+
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
834
|
+
_iv: iv,
|
|
835
|
+
_data: data
|
|
836
|
+
};
|
|
837
|
+
await adapter.put(vault, collName, id, newEnvelope);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
for (const [collName, newDek] of newDeks) {
|
|
841
|
+
callerKeyring.deks.set(collName, newDek);
|
|
842
|
+
}
|
|
843
|
+
await persistKeyring(adapter, vault, callerKeyring);
|
|
844
|
+
const userIds = await adapter.list(vault, "_keyring");
|
|
845
|
+
for (const userId of userIds) {
|
|
846
|
+
if (userId === callerKeyring.userId) continue;
|
|
847
|
+
const userEnvelope = await adapter.get(vault, "_keyring", userId);
|
|
848
|
+
if (!userEnvelope) continue;
|
|
849
|
+
const userKeyringFile = JSON.parse(userEnvelope._data);
|
|
850
|
+
const updatedDeks = { ...userKeyringFile.deks };
|
|
851
|
+
for (const collName of collections) {
|
|
852
|
+
delete updatedDeks[collName];
|
|
853
|
+
}
|
|
854
|
+
const updatedPermissions = { ...userKeyringFile.permissions };
|
|
855
|
+
for (const collName of collections) {
|
|
856
|
+
delete updatedPermissions[collName];
|
|
857
|
+
}
|
|
858
|
+
const updatedKeyring = {
|
|
859
|
+
...userKeyringFile,
|
|
860
|
+
deks: updatedDeks,
|
|
861
|
+
permissions: updatedPermissions
|
|
862
|
+
};
|
|
863
|
+
await writeKeyringFile(adapter, vault, userId, updatedKeyring);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
async function changeSecret(adapter, vault, keyring, newPassphrase, passphraseOpts) {
|
|
867
|
+
if (!passphraseOpts?.allowWeakPassphrase) {
|
|
868
|
+
assertStrongPassphrase(newPassphrase, passphraseOpts);
|
|
869
|
+
}
|
|
870
|
+
const newSalt = generateSalt();
|
|
871
|
+
const newKek = await deriveKey(newPassphrase, newSalt);
|
|
872
|
+
const wrappedDeks = {};
|
|
873
|
+
for (const [collName, dek] of keyring.deks) {
|
|
874
|
+
wrappedDeks[collName] = await wrapKey(dek, newKek);
|
|
875
|
+
}
|
|
876
|
+
const canary = await mintKeyringCanary(newKek);
|
|
877
|
+
const keyringFile = {
|
|
878
|
+
_noydb_keyring: NOYDB_KEYRING_VERSION,
|
|
879
|
+
user_id: keyring.userId,
|
|
880
|
+
display_name: keyring.displayName,
|
|
881
|
+
role: keyring.role,
|
|
882
|
+
permissions: keyring.permissions,
|
|
883
|
+
deks: wrappedDeks,
|
|
884
|
+
salt: bufferToBase64(newSalt),
|
|
885
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
886
|
+
granted_by: keyring.userId,
|
|
887
|
+
canary
|
|
888
|
+
};
|
|
889
|
+
await writeKeyringFile(adapter, vault, keyring.userId, keyringFile);
|
|
890
|
+
return {
|
|
891
|
+
userId: keyring.userId,
|
|
892
|
+
displayName: keyring.displayName,
|
|
893
|
+
role: keyring.role,
|
|
894
|
+
permissions: keyring.permissions,
|
|
895
|
+
deks: keyring.deks,
|
|
896
|
+
// Same DEKs, different wrapping
|
|
897
|
+
kek: newKek,
|
|
898
|
+
salt: newSalt,
|
|
899
|
+
// Tier-2 slots are NOT preserved through `changeSecret` —
|
|
900
|
+
// each slot wraps the OLD KEK, so the new keyring has no
|
|
901
|
+
// authenticator slots until the user re-enrolls. The higher-level
|
|
902
|
+
// `db.rotatePassphrase()` (#10) preserves slots by rewrapping the
|
|
903
|
+
// KEK reference, not the KEK itself.
|
|
904
|
+
authenticators: [],
|
|
905
|
+
...keyring.policy !== void 0 && { policy: keyring.policy }
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
async function buildRecipientKeyringFile(callerKeyring, recipient) {
|
|
909
|
+
if (!callerKeyring.kek) {
|
|
910
|
+
throw new ValidationError(
|
|
911
|
+
"buildRecipientKeyringFile: caller keyring has no KEK \u2014 tier-2 wrap-DEKs and tier-3 PIN-resume sessions cannot create bundle recipients. Re-authenticate at tier 1 (passphrase) before building a bundle."
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
const role = recipient.role ?? "viewer";
|
|
915
|
+
const permissions = resolvePermissions(role, recipient.permissions);
|
|
916
|
+
const newSalt = generateSalt();
|
|
917
|
+
const newKek = await deriveKey(recipient.passphrase, newSalt);
|
|
918
|
+
const wrappedDeks = {};
|
|
919
|
+
for (const collName of Object.keys(permissions)) {
|
|
920
|
+
const dek = callerKeyring.deks.get(collName);
|
|
921
|
+
if (dek) {
|
|
922
|
+
wrappedDeks[collName] = await wrapKey(dek, newKek);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
if (role === "owner" || role === "admin" || role === "viewer") {
|
|
926
|
+
for (const [collName, dek] of callerKeyring.deks) {
|
|
927
|
+
if (!(collName in wrappedDeks)) {
|
|
928
|
+
wrappedDeks[collName] = await wrapKey(dek, newKek);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
for (const [collName, dek] of callerKeyring.deks) {
|
|
933
|
+
if (collName.startsWith("_") && !(collName in wrappedDeks)) {
|
|
934
|
+
wrappedDeks[collName] = await wrapKey(dek, newKek);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
for (const collName of Object.keys(wrappedDeks)) {
|
|
938
|
+
if (!callerKeyring.deks.has(collName)) {
|
|
939
|
+
throw new PrivilegeEscalationError(collName);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
const canary = await mintKeyringCanary(newKek);
|
|
943
|
+
return {
|
|
944
|
+
_noydb_keyring: NOYDB_KEYRING_VERSION,
|
|
945
|
+
user_id: recipient.id,
|
|
946
|
+
display_name: recipient.displayName ?? recipient.id,
|
|
947
|
+
role,
|
|
948
|
+
permissions,
|
|
949
|
+
deks: wrappedDeks,
|
|
950
|
+
salt: bufferToBase64(newSalt),
|
|
951
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
952
|
+
granted_by: callerKeyring.userId,
|
|
953
|
+
canary,
|
|
954
|
+
...recipient.exportCapability !== void 0 ? { export_capability: recipient.exportCapability } : {},
|
|
955
|
+
...recipient.importCapability !== void 0 ? { import_capability: recipient.importCapability } : {},
|
|
956
|
+
...recipient.expiresAt !== void 0 ? { expires_at: recipient.expiresAt } : {}
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
async function listUsers(adapter, vault) {
|
|
960
|
+
const userIds = await adapter.list(vault, "_keyring");
|
|
961
|
+
const users = [];
|
|
962
|
+
for (const userId of userIds) {
|
|
963
|
+
const envelope = await adapter.get(vault, "_keyring", userId);
|
|
964
|
+
if (!envelope) continue;
|
|
965
|
+
const kf = JSON.parse(envelope._data);
|
|
966
|
+
users.push({
|
|
967
|
+
userId: kf.user_id,
|
|
968
|
+
displayName: kf.display_name,
|
|
969
|
+
role: kf.role,
|
|
970
|
+
permissions: kf.permissions,
|
|
971
|
+
createdAt: kf.created_at,
|
|
972
|
+
grantedBy: kf.granted_by
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
return users;
|
|
976
|
+
}
|
|
977
|
+
async function listUsersWithEnvelopes(adapter, vault, userEnvelopeDek, callerRole, options = {}) {
|
|
978
|
+
const isPrivileged = callerRole === "owner" || callerRole === "admin";
|
|
979
|
+
const dirConfig = await readDirectoryConfig(adapter, vault);
|
|
980
|
+
if (dirConfig?.enabled === false && !isPrivileged) {
|
|
981
|
+
throw new DirectoryDisabledError(vault);
|
|
982
|
+
}
|
|
983
|
+
if (options.includeHidden && !isPrivileged) {
|
|
984
|
+
throw new PermissionDeniedError(
|
|
985
|
+
"Permission denied \u2014 listUsersWithEnvelopes({ includeHidden: true }) requires owner or admin role"
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
const users = await listUsers(adapter, vault);
|
|
989
|
+
const out = [];
|
|
990
|
+
for (const user of users) {
|
|
991
|
+
if (!options.includeHidden) {
|
|
992
|
+
const visibility = await readUserVisibility(adapter, vault, user.userId);
|
|
993
|
+
if (visibility?.hidden) continue;
|
|
994
|
+
}
|
|
995
|
+
const envelope = await loadUserEnvelope(
|
|
996
|
+
adapter,
|
|
997
|
+
vault,
|
|
998
|
+
user.userId,
|
|
999
|
+
userEnvelopeDek
|
|
1000
|
+
);
|
|
1001
|
+
out.push({ user, envelope });
|
|
1002
|
+
}
|
|
1003
|
+
return out;
|
|
1004
|
+
}
|
|
181
1005
|
async function ensureCollectionDEK(adapter, vault, keyring) {
|
|
182
1006
|
const inFlight = /* @__PURE__ */ new Map();
|
|
183
1007
|
return async (collectionName) => {
|
|
@@ -209,6 +1033,7 @@ async function persistKeyring(adapter, vault, keyring) {
|
|
|
209
1033
|
for (const [collName, dek] of keyring.deks) {
|
|
210
1034
|
wrappedDeks[collName] = await wrapKey(dek, keyring.kek);
|
|
211
1035
|
}
|
|
1036
|
+
const canary = await mintKeyringCanary(keyring.kek);
|
|
212
1037
|
const keyringFile = {
|
|
213
1038
|
_noydb_keyring: NOYDB_KEYRING_VERSION,
|
|
214
1039
|
user_id: keyring.userId,
|
|
@@ -219,6 +1044,7 @@ async function persistKeyring(adapter, vault, keyring) {
|
|
|
219
1044
|
salt: bufferToBase64(keyring.salt),
|
|
220
1045
|
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
221
1046
|
granted_by: keyring.userId,
|
|
1047
|
+
canary,
|
|
222
1048
|
...keyring.exportCapability !== void 0 && { export_capability: keyring.exportCapability },
|
|
223
1049
|
...keyring.importCapability !== void 0 && { import_capability: keyring.importCapability },
|
|
224
1050
|
...keyring.authenticators.length > 0 && { authenticators: keyring.authenticators },
|
|
@@ -259,6 +1085,10 @@ function evaluateImportCapability(capability, _role, tier, format) {
|
|
|
259
1085
|
}
|
|
260
1086
|
return capability?.bundle === true;
|
|
261
1087
|
}
|
|
1088
|
+
function resolvePermissions(role, explicit) {
|
|
1089
|
+
if (role === "owner" || role === "admin" || role === "viewer") return {};
|
|
1090
|
+
return explicit ?? {};
|
|
1091
|
+
}
|
|
262
1092
|
async function writeKeyringFile(adapter, vault, userId, keyringFile) {
|
|
263
1093
|
const envelope = {
|
|
264
1094
|
_noydb: 1,
|
|
@@ -270,6 +1100,695 @@ async function writeKeyringFile(adapter, vault, userId, keyringFile) {
|
|
|
270
1100
|
await adapter.put(vault, "_keyring", userId, envelope);
|
|
271
1101
|
}
|
|
272
1102
|
|
|
1103
|
+
// src/team/authenticators.ts
|
|
1104
|
+
async function enrollAuthenticator(store, vault, keyring, options) {
|
|
1105
|
+
const existing = keyring.authenticators.find((a) => a.id === options.id);
|
|
1106
|
+
if (existing) {
|
|
1107
|
+
throw new ValidationError(
|
|
1108
|
+
`enrollAuthenticator: slot id "${options.id}" already exists in vault "${vault}". Remove the slot first or pick a unique id.`
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
const base = {
|
|
1112
|
+
id: options.id,
|
|
1113
|
+
method: options.method,
|
|
1114
|
+
enrolled_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1115
|
+
enrolled_via_tier: options.enrolled_via_tier ?? 1,
|
|
1116
|
+
meta: options.meta
|
|
1117
|
+
};
|
|
1118
|
+
const slot = options.wrapKind === "deks" ? {
|
|
1119
|
+
...base,
|
|
1120
|
+
wrapKind: "deks",
|
|
1121
|
+
wrapped_deks: options.wrapped_deks,
|
|
1122
|
+
iv: options.iv
|
|
1123
|
+
} : {
|
|
1124
|
+
...base,
|
|
1125
|
+
wrapped_kek: options.wrapped_kek
|
|
1126
|
+
};
|
|
1127
|
+
const next = appendSlot(keyring, slot);
|
|
1128
|
+
await persistKeyring(store, vault, next);
|
|
1129
|
+
return next;
|
|
1130
|
+
}
|
|
1131
|
+
async function updateAuthenticator(store, vault, keyring, slotId, options) {
|
|
1132
|
+
if (options.meta === void 0) {
|
|
1133
|
+
throw new ValidationError(
|
|
1134
|
+
`updateAuthenticator: at least one of meta must be provided (slotId: "${slotId}").`
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
const idx = keyring.authenticators.findIndex((a) => a.id === slotId);
|
|
1138
|
+
if (idx === -1) {
|
|
1139
|
+
throw new NoAccessError(
|
|
1140
|
+
`updateAuthenticator: slot "${slotId}" not found in vault "${vault}".`
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
const existing = keyring.authenticators[idx];
|
|
1144
|
+
const mergedMeta = { ...existing.meta };
|
|
1145
|
+
for (const [k, v] of Object.entries(options.meta)) {
|
|
1146
|
+
if (v === void 0) continue;
|
|
1147
|
+
if (v === null) {
|
|
1148
|
+
delete mergedMeta[k];
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
mergedMeta[k] = v;
|
|
1152
|
+
}
|
|
1153
|
+
const next = { ...existing, meta: mergedMeta };
|
|
1154
|
+
const nextSlots = [...keyring.authenticators];
|
|
1155
|
+
nextSlots[idx] = next;
|
|
1156
|
+
const nextKeyring = {
|
|
1157
|
+
...keyring,
|
|
1158
|
+
authenticators: nextSlots
|
|
1159
|
+
};
|
|
1160
|
+
await persistKeyring(store, vault, nextKeyring);
|
|
1161
|
+
return nextKeyring;
|
|
1162
|
+
}
|
|
1163
|
+
async function removeAuthenticator(store, vault, keyring, slotId) {
|
|
1164
|
+
const filtered = keyring.authenticators.filter((a) => a.id !== slotId);
|
|
1165
|
+
if (filtered.length === keyring.authenticators.length) {
|
|
1166
|
+
return keyring;
|
|
1167
|
+
}
|
|
1168
|
+
const next = {
|
|
1169
|
+
...keyring,
|
|
1170
|
+
authenticators: filtered
|
|
1171
|
+
};
|
|
1172
|
+
await persistKeyring(store, vault, next);
|
|
1173
|
+
return next;
|
|
1174
|
+
}
|
|
1175
|
+
function findAuthenticator(keyring, slotId) {
|
|
1176
|
+
return keyring.authenticators.find((a) => a.id === slotId);
|
|
1177
|
+
}
|
|
1178
|
+
function appendSlot(keyring, slot) {
|
|
1179
|
+
return {
|
|
1180
|
+
...keyring,
|
|
1181
|
+
authenticators: [...keyring.authenticators, slot]
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// src/policy/errors.ts
|
|
1186
|
+
var RecoveryProfileNotImplementedError = class extends NoydbError {
|
|
1187
|
+
profile;
|
|
1188
|
+
tracking;
|
|
1189
|
+
constructor(profile, tracking) {
|
|
1190
|
+
super(
|
|
1191
|
+
"RECOVERY_PROFILE_NOT_IMPLEMENTED",
|
|
1192
|
+
`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.`
|
|
1193
|
+
);
|
|
1194
|
+
this.name = "RecoveryProfileNotImplementedError";
|
|
1195
|
+
this.profile = profile;
|
|
1196
|
+
this.tracking = tracking;
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
// src/team/wrapped-deks.ts
|
|
1201
|
+
var PBKDF2_ITERATIONS2 = 6e5;
|
|
1202
|
+
var SALT_BYTES2 = 32;
|
|
1203
|
+
var IV_BYTES2 = 12;
|
|
1204
|
+
var subtle2 = globalThis.crypto.subtle;
|
|
1205
|
+
async function mintWrappedDeksBlob(deks, credential) {
|
|
1206
|
+
const salt = crypto.getRandomValues(new Uint8Array(SALT_BYTES2));
|
|
1207
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES2));
|
|
1208
|
+
const wrappingKey = await deriveWrappingKey(credential, salt);
|
|
1209
|
+
const exported = {};
|
|
1210
|
+
for (const [coll, dek] of deks) {
|
|
1211
|
+
const raw = await subtle2.exportKey("raw", dek);
|
|
1212
|
+
exported[coll] = bytesToBase64(new Uint8Array(raw));
|
|
1213
|
+
}
|
|
1214
|
+
const plaintext = new TextEncoder().encode(JSON.stringify({ deks: exported }));
|
|
1215
|
+
const ciphertext = await subtle2.encrypt(
|
|
1216
|
+
{ name: "AES-GCM", iv },
|
|
1217
|
+
wrappingKey,
|
|
1218
|
+
plaintext
|
|
1219
|
+
);
|
|
1220
|
+
return {
|
|
1221
|
+
salt: bytesToBase64(salt),
|
|
1222
|
+
iv: bytesToBase64(iv),
|
|
1223
|
+
wrappedDeks: bytesToBase64(new Uint8Array(ciphertext))
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
async function unwrapDeksFromBlob(blob, credential) {
|
|
1227
|
+
const wrappingKey = await deriveWrappingKey(credential, base64ToBytes(blob.salt));
|
|
1228
|
+
const plaintext = await subtle2.decrypt(
|
|
1229
|
+
{ name: "AES-GCM", iv: base64ToBytes(blob.iv) },
|
|
1230
|
+
wrappingKey,
|
|
1231
|
+
base64ToBytes(blob.wrappedDeks)
|
|
1232
|
+
);
|
|
1233
|
+
const parsed = JSON.parse(new TextDecoder().decode(plaintext));
|
|
1234
|
+
const deks = /* @__PURE__ */ new Map();
|
|
1235
|
+
for (const [coll, b64] of Object.entries(parsed.deks)) {
|
|
1236
|
+
const raw = base64ToBytes(b64);
|
|
1237
|
+
const key = await subtle2.importKey(
|
|
1238
|
+
"raw",
|
|
1239
|
+
raw,
|
|
1240
|
+
{ name: "AES-GCM", length: 256 },
|
|
1241
|
+
true,
|
|
1242
|
+
["encrypt", "decrypt"]
|
|
1243
|
+
);
|
|
1244
|
+
deks.set(coll, key);
|
|
1245
|
+
}
|
|
1246
|
+
return deks;
|
|
1247
|
+
}
|
|
1248
|
+
async function deriveWrappingKey(credential, salt) {
|
|
1249
|
+
const ikm = await subtle2.importKey(
|
|
1250
|
+
"raw",
|
|
1251
|
+
new TextEncoder().encode(credential),
|
|
1252
|
+
"PBKDF2",
|
|
1253
|
+
false,
|
|
1254
|
+
["deriveKey"]
|
|
1255
|
+
);
|
|
1256
|
+
return subtle2.deriveKey(
|
|
1257
|
+
{
|
|
1258
|
+
name: "PBKDF2",
|
|
1259
|
+
salt,
|
|
1260
|
+
iterations: PBKDF2_ITERATIONS2,
|
|
1261
|
+
hash: "SHA-256"
|
|
1262
|
+
},
|
|
1263
|
+
ikm,
|
|
1264
|
+
{ name: "AES-GCM", length: 256 },
|
|
1265
|
+
false,
|
|
1266
|
+
["encrypt", "decrypt"]
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
function bytesToBase64(b) {
|
|
1270
|
+
let s = "";
|
|
1271
|
+
for (const x of b) s += String.fromCharCode(x);
|
|
1272
|
+
return btoa(s);
|
|
1273
|
+
}
|
|
1274
|
+
function base64ToBytes(b64) {
|
|
1275
|
+
const s = atob(b64);
|
|
1276
|
+
const out = new Uint8Array(s.length);
|
|
1277
|
+
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
|
|
1278
|
+
return out;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// src/team/recovery.ts
|
|
1282
|
+
var PAPER_DOC_ID = "recovery-paper";
|
|
1283
|
+
async function loadPaperRecoveryEntries(store, vault) {
|
|
1284
|
+
const env = await store.get(vault, "_meta", PAPER_DOC_ID);
|
|
1285
|
+
if (!env) return [];
|
|
1286
|
+
try {
|
|
1287
|
+
const doc = JSON.parse(env._data);
|
|
1288
|
+
if (doc.profile !== "paper" || !Array.isArray(doc.entries)) return [];
|
|
1289
|
+
return doc.entries;
|
|
1290
|
+
} catch {
|
|
1291
|
+
return [];
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
async function savePaperRecoveryEntries(store, vault, entries) {
|
|
1295
|
+
const doc = {
|
|
1296
|
+
_noydb_recovery: 1,
|
|
1297
|
+
profile: "paper",
|
|
1298
|
+
entries
|
|
1299
|
+
};
|
|
1300
|
+
const envelope = {
|
|
1301
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
1302
|
+
_v: 1,
|
|
1303
|
+
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1304
|
+
_iv: "",
|
|
1305
|
+
_data: JSON.stringify(doc)
|
|
1306
|
+
};
|
|
1307
|
+
await store.put(vault, "_meta", PAPER_DOC_ID, envelope);
|
|
1308
|
+
}
|
|
1309
|
+
async function burnPaperRecoveryEntry(store, vault, codeId) {
|
|
1310
|
+
const entries = await loadPaperRecoveryEntries(store, vault);
|
|
1311
|
+
const remaining = entries.filter((e) => e.codeId !== codeId);
|
|
1312
|
+
await savePaperRecoveryEntries(store, vault, remaining);
|
|
1313
|
+
}
|
|
1314
|
+
var SHAMIR_DOC_ID = "recovery-shamir";
|
|
1315
|
+
async function loadShamirRecoveryEntries(store, vault) {
|
|
1316
|
+
const env = await store.get(vault, "_meta", SHAMIR_DOC_ID);
|
|
1317
|
+
if (!env) return [];
|
|
1318
|
+
try {
|
|
1319
|
+
const doc = JSON.parse(env._data);
|
|
1320
|
+
if (doc.profile !== "shamir" || !Array.isArray(doc.entries)) return [];
|
|
1321
|
+
return doc.entries;
|
|
1322
|
+
} catch {
|
|
1323
|
+
return [];
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
async function unwrapDeksFromShamirEntry(provider, entry, shareStrings) {
|
|
1327
|
+
if (shareStrings.length < entry.k) {
|
|
1328
|
+
throw new Error(
|
|
1329
|
+
`Insufficient shares: this Shamir entry needs ${entry.k} of ${entry.n}, but ${shareStrings.length} were provided.`
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
const secret = provider.combineShares(shareStrings);
|
|
1333
|
+
try {
|
|
1334
|
+
return await unwrapDeksFromBlob(entry, bytesToBase642(secret));
|
|
1335
|
+
} finally {
|
|
1336
|
+
secret.fill(0);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
function bytesToBase642(b) {
|
|
1340
|
+
let s = "";
|
|
1341
|
+
for (const x of b) s += String.fromCharCode(x);
|
|
1342
|
+
return btoa(s);
|
|
1343
|
+
}
|
|
1344
|
+
async function mintPaperRecoveryEntry(deks, code, codeId) {
|
|
1345
|
+
const blob = await mintWrappedDeksBlob(deks, code);
|
|
1346
|
+
return {
|
|
1347
|
+
...blob,
|
|
1348
|
+
codeId,
|
|
1349
|
+
enrolledAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
async function unwrapDeksFromPaperEntry(entry, code) {
|
|
1353
|
+
return unwrapDeksFromBlob(entry, code);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// src/team/rotate-recover.ts
|
|
1357
|
+
async function rotatePassphrase(store, vault, userId, input) {
|
|
1358
|
+
if (!input.allowWeakPassphrase) {
|
|
1359
|
+
assertStrongPassphrase(input.newPassphrase, input.passphrasePolicy);
|
|
1360
|
+
}
|
|
1361
|
+
const env = await store.get(vault, "_keyring", userId);
|
|
1362
|
+
if (!env) {
|
|
1363
|
+
throw new NoAccessError(`No keyring found for user "${userId}" in vault "${vault}".`);
|
|
1364
|
+
}
|
|
1365
|
+
const file = JSON.parse(env._data);
|
|
1366
|
+
const oldSalt = base64ToBuffer(file.salt);
|
|
1367
|
+
const oldKek = await deriveKey(input.oldPassphrase, oldSalt);
|
|
1368
|
+
const deks = /* @__PURE__ */ new Map();
|
|
1369
|
+
for (const [coll, wrapped] of Object.entries(file.deks)) {
|
|
1370
|
+
deks.set(coll, await unwrapKey(wrapped, oldKek));
|
|
1371
|
+
}
|
|
1372
|
+
const newSalt = generateSalt();
|
|
1373
|
+
const newKek = await deriveKey(input.newPassphrase, newSalt);
|
|
1374
|
+
const wrappedDeks = {};
|
|
1375
|
+
for (const [coll, dek] of deks) {
|
|
1376
|
+
wrappedDeks[coll] = await wrapKey(dek, newKek);
|
|
1377
|
+
}
|
|
1378
|
+
const oldSlots = file.authenticators ?? [];
|
|
1379
|
+
const newSlots = [];
|
|
1380
|
+
if (input.slotCeremonies && oldSlots.length > 0) {
|
|
1381
|
+
for (const oldSlot of oldSlots) {
|
|
1382
|
+
const ceremony = input.slotCeremonies[oldSlot.id];
|
|
1383
|
+
if (!ceremony) continue;
|
|
1384
|
+
const result = await ceremony({ newKek, newDeks: deks, oldSlot });
|
|
1385
|
+
if (result.id !== oldSlot.id) {
|
|
1386
|
+
throw new ValidationError(
|
|
1387
|
+
`slotCeremonies['${oldSlot.id}'] returned id="${result.id}". The id must match the rotated slot \u2014 a ceremony cannot change a slot's identity.`
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
if (result.method !== oldSlot.method) {
|
|
1391
|
+
throw new ValidationError(
|
|
1392
|
+
`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.`
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
const oldWrapKind = oldSlot.wrapKind ?? "kek";
|
|
1396
|
+
const newWrapKind = result.wrapKind ?? "kek";
|
|
1397
|
+
if (oldWrapKind !== newWrapKind) {
|
|
1398
|
+
throw new ValidationError(
|
|
1399
|
+
`slotCeremonies['${oldSlot.id}'] returned wrapKind="${newWrapKind}", expected "${oldWrapKind}". The wrap format must match the rotated slot \u2014 a ceremony cannot change the wrap shape (e.g. wrap-KEK \u2192 wrap-DEKs) under cover of rotation, since that would silently change the session tier produced at unlock.`
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
const baseFields = {
|
|
1403
|
+
id: result.id,
|
|
1404
|
+
method: result.method,
|
|
1405
|
+
// Preserve original enrolled_at — rotation is rewrapping, not
|
|
1406
|
+
// re-enrollment. The slot's enrolment timestamp tracks when
|
|
1407
|
+
// the user originally added the slot, not when it was last
|
|
1408
|
+
// rewrapped. Forensics consumers reading enrolled_at are
|
|
1409
|
+
// tracking the slot's ORIGIN, not its CURRENT wrapping.
|
|
1410
|
+
enrolled_at: oldSlot.enrolled_at,
|
|
1411
|
+
enrolled_via_tier: result.enrolled_via_tier ?? oldSlot.enrolled_via_tier,
|
|
1412
|
+
meta: result.meta
|
|
1413
|
+
};
|
|
1414
|
+
const newSlot = result.wrapKind === "deks" ? {
|
|
1415
|
+
...baseFields,
|
|
1416
|
+
wrapKind: "deks",
|
|
1417
|
+
wrapped_deks: result.wrapped_deks,
|
|
1418
|
+
iv: result.iv
|
|
1419
|
+
} : {
|
|
1420
|
+
...baseFields,
|
|
1421
|
+
wrapped_kek: result.wrapped_kek
|
|
1422
|
+
};
|
|
1423
|
+
newSlots.push(newSlot);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
const canary = await mintKeyringCanary(newKek);
|
|
1427
|
+
const next = {
|
|
1428
|
+
...file,
|
|
1429
|
+
_noydb_keyring: NOYDB_KEYRING_VERSION,
|
|
1430
|
+
deks: wrappedDeks,
|
|
1431
|
+
salt: bufferToBase64(newSalt),
|
|
1432
|
+
authenticators: newSlots,
|
|
1433
|
+
canary
|
|
1434
|
+
};
|
|
1435
|
+
await writeKeyringFile2(store, vault, userId, next);
|
|
1436
|
+
return {
|
|
1437
|
+
userId: file.user_id,
|
|
1438
|
+
displayName: file.display_name,
|
|
1439
|
+
role: file.role,
|
|
1440
|
+
permissions: file.permissions,
|
|
1441
|
+
deks,
|
|
1442
|
+
kek: newKek,
|
|
1443
|
+
salt: newSalt,
|
|
1444
|
+
authenticators: newSlots,
|
|
1445
|
+
...file.export_capability !== void 0 && { exportCapability: file.export_capability },
|
|
1446
|
+
...file.import_capability !== void 0 && { importCapability: file.import_capability }
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
async function recoverPassphrase(provider, store, vault, userId, input) {
|
|
1450
|
+
if (!input.allowWeakPassphrase) {
|
|
1451
|
+
assertStrongPassphrase(input.newPassphrase, input.passphrasePolicy);
|
|
1452
|
+
}
|
|
1453
|
+
const profile = input.recoveryProof.profile;
|
|
1454
|
+
if (profile === "paper") {
|
|
1455
|
+
return recoverViaPaperCode(store, vault, userId, input);
|
|
1456
|
+
}
|
|
1457
|
+
if (profile === "shamir") {
|
|
1458
|
+
return recoverViaShamir(provider, store, vault, userId, input);
|
|
1459
|
+
}
|
|
1460
|
+
throw new RecoveryProfileNotImplementedError(
|
|
1461
|
+
profile,
|
|
1462
|
+
"https://github.com/vLannaAi/noy-db/issues/196"
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
async function recoverViaPaperCode(store, vault, userId, input) {
|
|
1466
|
+
if (input.recoveryProof.profile !== "paper") throw new Error("unreachable");
|
|
1467
|
+
const { code } = input.recoveryProof.payload;
|
|
1468
|
+
const env = await store.get(vault, "_keyring", userId);
|
|
1469
|
+
if (!env) {
|
|
1470
|
+
throw new NoAccessError(`No keyring found for user "${userId}" in vault "${vault}".`);
|
|
1471
|
+
}
|
|
1472
|
+
const file = JSON.parse(env._data);
|
|
1473
|
+
const entries = await loadPaperRecoveryEntries(store, vault);
|
|
1474
|
+
if (entries.length === 0) {
|
|
1475
|
+
throw new NoAccessError(
|
|
1476
|
+
`No paper-recovery entries enrolled for vault "${vault}". Enroll via \`db.enrollRecovery({ profile: "paper", entries })\` before relying on recovery.`
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
const normalized = normalizePaperCode(code);
|
|
1480
|
+
let recovered;
|
|
1481
|
+
for (const entry of entries) {
|
|
1482
|
+
try {
|
|
1483
|
+
const deks2 = await unwrapDeksFromPaperEntry(entry, normalized);
|
|
1484
|
+
recovered = { deks: deks2, entry };
|
|
1485
|
+
break;
|
|
1486
|
+
} catch {
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
if (!recovered) {
|
|
1490
|
+
throw new InvalidKeyError(
|
|
1491
|
+
"Recovery code does not match any enrolled paper entry. The code may have been previously used (single-use) or typed incorrectly."
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
const deks = recovered.deks;
|
|
1495
|
+
const newSalt = generateSalt();
|
|
1496
|
+
const newKek = await deriveKey(input.newPassphrase, newSalt);
|
|
1497
|
+
const wrappedDeks = {};
|
|
1498
|
+
for (const [coll, dek] of deks) {
|
|
1499
|
+
wrappedDeks[coll] = await wrapKey(dek, newKek);
|
|
1500
|
+
}
|
|
1501
|
+
const canary = await mintKeyringCanary(newKek);
|
|
1502
|
+
const next = {
|
|
1503
|
+
...file,
|
|
1504
|
+
_noydb_keyring: NOYDB_KEYRING_VERSION,
|
|
1505
|
+
deks: wrappedDeks,
|
|
1506
|
+
salt: bufferToBase64(newSalt),
|
|
1507
|
+
authenticators: [],
|
|
1508
|
+
// tier-2 slots wrap old KEK, drop them
|
|
1509
|
+
canary
|
|
1510
|
+
};
|
|
1511
|
+
await burnPaperRecoveryEntry(store, vault, recovered.entry.codeId);
|
|
1512
|
+
await writeKeyringFile2(store, vault, userId, next);
|
|
1513
|
+
return {
|
|
1514
|
+
userId: file.user_id,
|
|
1515
|
+
displayName: file.display_name,
|
|
1516
|
+
role: file.role,
|
|
1517
|
+
permissions: file.permissions,
|
|
1518
|
+
deks,
|
|
1519
|
+
kek: newKek,
|
|
1520
|
+
salt: newSalt,
|
|
1521
|
+
authenticators: [],
|
|
1522
|
+
...file.export_capability !== void 0 && { exportCapability: file.export_capability },
|
|
1523
|
+
...file.import_capability !== void 0 && { importCapability: file.import_capability }
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
function normalizePaperCode(input) {
|
|
1527
|
+
return input.toUpperCase().replace(/[\s\-_]/g, "");
|
|
1528
|
+
}
|
|
1529
|
+
async function recoverViaShamir(provider, store, vault, userId, input) {
|
|
1530
|
+
if (input.recoveryProof.profile !== "shamir") throw new Error("unreachable");
|
|
1531
|
+
const { entryId: requestedEntryId, shares: shareStrings } = input.recoveryProof.payload;
|
|
1532
|
+
if (shareStrings.length === 0) {
|
|
1533
|
+
throw new ValidationError(
|
|
1534
|
+
"Shamir recovery requires at least one share; received an empty array."
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
const env = await store.get(vault, "_keyring", userId);
|
|
1538
|
+
if (!env) {
|
|
1539
|
+
throw new NoAccessError(`No keyring found for user "${userId}" in vault "${vault}".`);
|
|
1540
|
+
}
|
|
1541
|
+
const file = JSON.parse(env._data);
|
|
1542
|
+
const allEntries = await loadShamirRecoveryEntries(store, vault);
|
|
1543
|
+
if (allEntries.length === 0) {
|
|
1544
|
+
throw new NoAccessError(
|
|
1545
|
+
`No Shamir-recovery entries enrolled for vault "${vault}". Enroll via \`db.enrollRecovery({ profile: "shamir", k, n })\` before relying on recovery.`
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
if (!provider) {
|
|
1549
|
+
throw new Error(
|
|
1550
|
+
"shamir recovery requires a ShamirRecoveryProvider \u2014 pass shamirRecovery: shamirRecoveryProvider() from '@noy-db/on-shamir' to createNoydb()"
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
let candidates;
|
|
1554
|
+
if (requestedEntryId !== void 0) {
|
|
1555
|
+
candidates = allEntries.filter((e) => e.entryId === requestedEntryId);
|
|
1556
|
+
if (candidates.length === 0) {
|
|
1557
|
+
throw new NoAccessError(
|
|
1558
|
+
`No Shamir-recovery entry with entryId="${requestedEntryId}" found in vault "${vault}". Available entries: ` + allEntries.map((e) => `"${e.entryId}"`).join(", ")
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
} else {
|
|
1562
|
+
candidates = allEntries;
|
|
1563
|
+
}
|
|
1564
|
+
let recoveredDeks;
|
|
1565
|
+
for (const entry of candidates) {
|
|
1566
|
+
if (shareStrings.length < entry.k) {
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
try {
|
|
1570
|
+
const deks = await unwrapDeksFromShamirEntry(provider, entry, shareStrings);
|
|
1571
|
+
recoveredDeks = deks;
|
|
1572
|
+
break;
|
|
1573
|
+
} catch {
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
if (!recoveredDeks) {
|
|
1577
|
+
const minK = Math.min(...candidates.map((e) => e.k));
|
|
1578
|
+
if (shareStrings.length < minK) {
|
|
1579
|
+
throw new InvalidKeyError(
|
|
1580
|
+
`Insufficient Shamir shares to combine: the smallest enrolled threshold is ${minK}, but only ${shareStrings.length} share${shareStrings.length === 1 ? " was" : "s were"} provided.`
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
throw new InvalidKeyError(
|
|
1584
|
+
"Shamir shares do not match any enrolled entry. Possible causes: shares were tampered with, came from a different enrollment, or the entry was rotated after these shares were distributed."
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1587
|
+
const newSalt = generateSalt();
|
|
1588
|
+
const newKek = await deriveKey(input.newPassphrase, newSalt);
|
|
1589
|
+
const wrappedDeks = {};
|
|
1590
|
+
for (const [coll, dek] of recoveredDeks) {
|
|
1591
|
+
wrappedDeks[coll] = await wrapKey(dek, newKek);
|
|
1592
|
+
}
|
|
1593
|
+
const canary = await mintKeyringCanary(newKek);
|
|
1594
|
+
const next = {
|
|
1595
|
+
...file,
|
|
1596
|
+
_noydb_keyring: NOYDB_KEYRING_VERSION,
|
|
1597
|
+
deks: wrappedDeks,
|
|
1598
|
+
salt: bufferToBase64(newSalt),
|
|
1599
|
+
authenticators: [],
|
|
1600
|
+
// tier-2 slots wrap old KEK, drop them on recovery
|
|
1601
|
+
canary
|
|
1602
|
+
};
|
|
1603
|
+
await writeKeyringFile2(store, vault, userId, next);
|
|
1604
|
+
return {
|
|
1605
|
+
userId: file.user_id,
|
|
1606
|
+
displayName: file.display_name,
|
|
1607
|
+
role: file.role,
|
|
1608
|
+
permissions: file.permissions,
|
|
1609
|
+
deks: recoveredDeks,
|
|
1610
|
+
kek: newKek,
|
|
1611
|
+
salt: newSalt,
|
|
1612
|
+
authenticators: [],
|
|
1613
|
+
...file.export_capability !== void 0 && { exportCapability: file.export_capability },
|
|
1614
|
+
...file.import_capability !== void 0 && { importCapability: file.import_capability }
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
async function writeKeyringFile2(store, vault, userId, file) {
|
|
1618
|
+
const envelope = {
|
|
1619
|
+
_noydb: 1,
|
|
1620
|
+
_v: 1,
|
|
1621
|
+
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1622
|
+
_iv: "",
|
|
1623
|
+
_data: JSON.stringify(file)
|
|
1624
|
+
};
|
|
1625
|
+
await store.put(vault, "_keyring", userId, envelope);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// src/team/peer-recover.ts
|
|
1629
|
+
var ADMIN_RECOVERABLE_TARGETS = ["operator", "viewer", "client", "admin"];
|
|
1630
|
+
function canRecover(callerRole, targetRole) {
|
|
1631
|
+
if (callerRole === "owner") return true;
|
|
1632
|
+
if (callerRole === "admin") return ADMIN_RECOVERABLE_TARGETS.includes(targetRole);
|
|
1633
|
+
return false;
|
|
1634
|
+
}
|
|
1635
|
+
async function recoverUser(store, vault, callerKeyring, options) {
|
|
1636
|
+
const env = await store.get(vault, "_keyring", options.userId);
|
|
1637
|
+
if (!env) {
|
|
1638
|
+
throw new NoAccessError(
|
|
1639
|
+
`recoverUser: user "${options.userId}" has no keyring in vault "${vault}".`
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
const target = JSON.parse(env._data);
|
|
1643
|
+
const targetRole = options.role ?? target.role;
|
|
1644
|
+
if (!canRecover(callerKeyring.role, targetRole)) {
|
|
1645
|
+
throw new PermissionDeniedError(
|
|
1646
|
+
`Role "${callerKeyring.role}" cannot recover role "${targetRole}"`
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
if (!canRecover(callerKeyring.role, target.role)) {
|
|
1650
|
+
throw new PermissionDeniedError(
|
|
1651
|
+
`Role "${callerKeyring.role}" cannot recover role "${target.role}"`
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
for (const coll of Object.keys(target.deks)) {
|
|
1655
|
+
if (!callerKeyring.deks.has(coll)) {
|
|
1656
|
+
throw new PrivilegeEscalationError(coll);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
if (options.validatePassphrase && !options.allowWeakPassphrase) {
|
|
1660
|
+
assertStrongPassphrase(options.passphrase, options.passphrasePolicy);
|
|
1661
|
+
}
|
|
1662
|
+
const newSalt = generateSalt();
|
|
1663
|
+
const newKek = await deriveKey(options.passphrase, newSalt);
|
|
1664
|
+
const wrappedDeks = {};
|
|
1665
|
+
for (const coll of Object.keys(target.deks)) {
|
|
1666
|
+
const callerDek = callerKeyring.deks.get(coll);
|
|
1667
|
+
if (!callerDek) {
|
|
1668
|
+
throw new PrivilegeEscalationError(coll);
|
|
1669
|
+
}
|
|
1670
|
+
wrappedDeks[coll] = await wrapKey(callerDek, newKek);
|
|
1671
|
+
}
|
|
1672
|
+
const canary = await mintKeyringCanary(newKek);
|
|
1673
|
+
const next = {
|
|
1674
|
+
...target,
|
|
1675
|
+
_noydb_keyring: NOYDB_KEYRING_VERSION,
|
|
1676
|
+
role: targetRole,
|
|
1677
|
+
display_name: options.displayName ?? target.display_name,
|
|
1678
|
+
deks: wrappedDeks,
|
|
1679
|
+
salt: bufferToBase64(newSalt),
|
|
1680
|
+
granted_by: callerKeyring.userId,
|
|
1681
|
+
authenticators: [],
|
|
1682
|
+
canary
|
|
1683
|
+
};
|
|
1684
|
+
const envelope = {
|
|
1685
|
+
_noydb: 1,
|
|
1686
|
+
_v: 1,
|
|
1687
|
+
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1688
|
+
_iv: "",
|
|
1689
|
+
_data: JSON.stringify(next)
|
|
1690
|
+
};
|
|
1691
|
+
await store.put(vault, "_keyring", options.userId, envelope);
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// src/team/tiers.ts
|
|
1695
|
+
function dekKey(collection, tier) {
|
|
1696
|
+
if (tier <= 0) return collection;
|
|
1697
|
+
return `${collection}#${tier}`;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// src/team/magic-link-grant.ts
|
|
1701
|
+
var MAGIC_LINK_GRANTS_COLLECTION = "_magic_link_grants";
|
|
1702
|
+
var MAGIC_LINK_CONTENT_INFO_PREFIX = "noydb-magic-link-content-v1:";
|
|
1703
|
+
async function deriveMagicLinkContentKey(serverSecret, token, vault) {
|
|
1704
|
+
const subtle3 = globalThis.crypto.subtle;
|
|
1705
|
+
const ikmBytes = serverSecret instanceof Uint8Array ? serverSecret : new TextEncoder().encode(serverSecret);
|
|
1706
|
+
const tokenBytes = new TextEncoder().encode(token);
|
|
1707
|
+
const saltBuffer = await subtle3.digest("SHA-256", tokenBytes);
|
|
1708
|
+
const info = new TextEncoder().encode(MAGIC_LINK_CONTENT_INFO_PREFIX + vault);
|
|
1709
|
+
const ikm = await subtle3.importKey("raw", ikmBytes, "HKDF", false, ["deriveKey"]);
|
|
1710
|
+
return subtle3.deriveKey(
|
|
1711
|
+
{ name: "HKDF", hash: "SHA-256", salt: saltBuffer, info },
|
|
1712
|
+
ikm,
|
|
1713
|
+
{ name: "AES-GCM", length: 256 },
|
|
1714
|
+
false,
|
|
1715
|
+
["encrypt", "decrypt"]
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
async function writeMagicLinkGrant(store, vault, grantor, contentKey, grantKek, recordId, opts) {
|
|
1719
|
+
const collectionName = opts.collection ?? null;
|
|
1720
|
+
const sourceKey = collectionName ? dekKey(collectionName, opts.tier) : `__any#${opts.tier}`;
|
|
1721
|
+
const sourceDek = grantor.deks.get(sourceKey);
|
|
1722
|
+
if (!sourceDek) {
|
|
1723
|
+
throw new DelegationTargetMissingError(
|
|
1724
|
+
`grantor cannot find tier ${opts.tier} DEK for ${collectionName ?? "(any)"}`
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1727
|
+
const wrappedDek = await wrapKey(sourceDek, grantKek);
|
|
1728
|
+
const until = typeof opts.until === "string" ? opts.until : opts.until.toISOString();
|
|
1729
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1730
|
+
const payload = {
|
|
1731
|
+
id: recordId,
|
|
1732
|
+
toUser: opts.toUser,
|
|
1733
|
+
fromUser: grantor.userId,
|
|
1734
|
+
tier: opts.tier,
|
|
1735
|
+
collection: collectionName,
|
|
1736
|
+
...opts.record && { record: opts.record },
|
|
1737
|
+
until,
|
|
1738
|
+
wrappedDek,
|
|
1739
|
+
createdAt,
|
|
1740
|
+
...opts.note && { note: opts.note }
|
|
1741
|
+
};
|
|
1742
|
+
const { iv, data } = await encrypt(JSON.stringify(payload), contentKey);
|
|
1743
|
+
const envelope = {
|
|
1744
|
+
_noydb: 1,
|
|
1745
|
+
_v: 1,
|
|
1746
|
+
_ts: createdAt,
|
|
1747
|
+
_iv: iv,
|
|
1748
|
+
_data: data,
|
|
1749
|
+
_by: grantor.userId
|
|
1750
|
+
};
|
|
1751
|
+
await store.put(vault, MAGIC_LINK_GRANTS_COLLECTION, recordId, envelope);
|
|
1752
|
+
return { recordId, payload };
|
|
1753
|
+
}
|
|
1754
|
+
async function readMagicLinkGrantRecord(store, vault, contentKey, recordId) {
|
|
1755
|
+
const env = await store.get(vault, MAGIC_LINK_GRANTS_COLLECTION, recordId);
|
|
1756
|
+
if (!env) return null;
|
|
1757
|
+
try {
|
|
1758
|
+
const json = await decrypt(env._iv, env._data, contentKey);
|
|
1759
|
+
return JSON.parse(json);
|
|
1760
|
+
} catch {
|
|
1761
|
+
return null;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
async function listMagicLinkGrants(store, vault, contentKey, token) {
|
|
1765
|
+
const ids = await store.list(vault, MAGIC_LINK_GRANTS_COLLECTION);
|
|
1766
|
+
const matching = ids.filter((id) => id === token || id.startsWith(`${token}:`));
|
|
1767
|
+
const out = [];
|
|
1768
|
+
for (const id of matching) {
|
|
1769
|
+
const payload = await readMagicLinkGrantRecord(store, vault, contentKey, id);
|
|
1770
|
+
if (payload) out.push(payload);
|
|
1771
|
+
}
|
|
1772
|
+
return out;
|
|
1773
|
+
}
|
|
1774
|
+
async function unwrapMagicLinkGrant(payload, grantKek) {
|
|
1775
|
+
return unwrapKey(payload.wrappedDek, grantKek);
|
|
1776
|
+
}
|
|
1777
|
+
async function revokeMagicLinkGrant(store, vault, token) {
|
|
1778
|
+
const ids = await store.list(vault, MAGIC_LINK_GRANTS_COLLECTION);
|
|
1779
|
+
const matching = ids.filter((id) => id === token || id.startsWith(`${token}:`));
|
|
1780
|
+
for (const id of matching) {
|
|
1781
|
+
await store.delete(vault, MAGIC_LINK_GRANTS_COLLECTION, id);
|
|
1782
|
+
}
|
|
1783
|
+
return matching.length;
|
|
1784
|
+
}
|
|
1785
|
+
function magicLinkGrantRecordId(token, index) {
|
|
1786
|
+
return index === 0 ? token : `${token}:${index}`;
|
|
1787
|
+
}
|
|
1788
|
+
function isMagicLinkGrantExpired(payload, now = /* @__PURE__ */ new Date()) {
|
|
1789
|
+
return payload.until <= now.toISOString();
|
|
1790
|
+
}
|
|
1791
|
+
|
|
273
1792
|
// src/store/sync-policy.ts
|
|
274
1793
|
var SyncScheduler = class {
|
|
275
1794
|
policy;
|
|
@@ -1233,14 +2752,47 @@ async function credentialStatus(adapter, vault, keyring, adapterId) {
|
|
|
1233
2752
|
SYNC_CREDENTIALS_COLLECTION,
|
|
1234
2753
|
SyncEngine,
|
|
1235
2754
|
SyncTransaction,
|
|
2755
|
+
buildRecipientKeyringFile,
|
|
2756
|
+
burnPaperRecoveryEntry,
|
|
2757
|
+
changeSecret,
|
|
2758
|
+
createOwnerKeyring,
|
|
1236
2759
|
credentialStatus,
|
|
1237
2760
|
deleteCredential,
|
|
2761
|
+
deriveMagicLinkContentKey,
|
|
2762
|
+
enrollAuthenticator,
|
|
2763
|
+
ensureCollectionDEK,
|
|
1238
2764
|
evaluateExportCapability,
|
|
1239
2765
|
evaluateImportCapability,
|
|
2766
|
+
findAuthenticator,
|
|
1240
2767
|
getCredential,
|
|
2768
|
+
grant,
|
|
1241
2769
|
hasExportCapability,
|
|
1242
2770
|
hasImportCapability,
|
|
2771
|
+
isMagicLinkGrantExpired,
|
|
1243
2772
|
listCredentials,
|
|
1244
|
-
|
|
2773
|
+
listMagicLinkGrants,
|
|
2774
|
+
listUsers,
|
|
2775
|
+
listUsersWithEnvelopes,
|
|
2776
|
+
loadKeyring,
|
|
2777
|
+
loadPaperRecoveryEntries,
|
|
2778
|
+
magicLinkGrantRecordId,
|
|
2779
|
+
mintPaperRecoveryEntry,
|
|
2780
|
+
mintWrappedDeksBlob,
|
|
2781
|
+
persistKeyring,
|
|
2782
|
+
putCredential,
|
|
2783
|
+
readMagicLinkGrantRecord,
|
|
2784
|
+
recoverPassphrase,
|
|
2785
|
+
recoverUser,
|
|
2786
|
+
removeAuthenticator,
|
|
2787
|
+
revoke,
|
|
2788
|
+
revokeMagicLinkGrant,
|
|
2789
|
+
rotatePassphrase,
|
|
2790
|
+
savePaperRecoveryEntries,
|
|
2791
|
+
unwrapDeksFromBlob,
|
|
2792
|
+
unwrapDeksFromPaperEntry,
|
|
2793
|
+
unwrapMagicLinkGrant,
|
|
2794
|
+
updateAuthenticator,
|
|
2795
|
+
updateKeyringIdentity,
|
|
2796
|
+
writeMagicLinkGrant
|
|
1245
2797
|
});
|
|
1246
2798
|
//# sourceMappingURL=index.cjs.map
|