@noy-db/hub 0.1.0-pre.10
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/LICENSE +21 -0
- package/README.md +197 -0
- package/dist/aggregate/index.cjs +476 -0
- package/dist/aggregate/index.cjs.map +1 -0
- package/dist/aggregate/index.d.cts +38 -0
- package/dist/aggregate/index.d.ts +38 -0
- package/dist/aggregate/index.js +53 -0
- package/dist/aggregate/index.js.map +1 -0
- package/dist/blobs/index.cjs +1480 -0
- package/dist/blobs/index.cjs.map +1 -0
- package/dist/blobs/index.d.cts +45 -0
- package/dist/blobs/index.d.ts +45 -0
- package/dist/blobs/index.js +48 -0
- package/dist/blobs/index.js.map +1 -0
- package/dist/bundle/index.cjs +496 -0
- package/dist/bundle/index.cjs.map +1 -0
- package/dist/bundle/index.d.cts +7 -0
- package/dist/bundle/index.d.ts +7 -0
- package/dist/bundle/index.js +51 -0
- package/dist/bundle/index.js.map +1 -0
- package/dist/chunk-2QR2PQTT.js +217 -0
- package/dist/chunk-2QR2PQTT.js.map +1 -0
- package/dist/chunk-72UIIX3E.js +1109 -0
- package/dist/chunk-72UIIX3E.js.map +1 -0
- package/dist/chunk-A4NFZKRW.js +722 -0
- package/dist/chunk-A4NFZKRW.js.map +1 -0
- package/dist/chunk-AOYCZP2H.js +793 -0
- package/dist/chunk-AOYCZP2H.js.map +1 -0
- package/dist/chunk-CIMZBAZB.js +72 -0
- package/dist/chunk-CIMZBAZB.js.map +1 -0
- package/dist/chunk-E3AGCGJ4.js +160 -0
- package/dist/chunk-E3AGCGJ4.js.map +1 -0
- package/dist/chunk-EKX3YVCI.js +97 -0
- package/dist/chunk-EKX3YVCI.js.map +1 -0
- package/dist/chunk-EMIGCR7X.js +39 -0
- package/dist/chunk-EMIGCR7X.js.map +1 -0
- package/dist/chunk-EMMRIE3C.js +72 -0
- package/dist/chunk-EMMRIE3C.js.map +1 -0
- package/dist/chunk-EUNIORPU.js +680 -0
- package/dist/chunk-EUNIORPU.js.map +1 -0
- package/dist/chunk-FZU343FL.js +32 -0
- package/dist/chunk-FZU343FL.js.map +1 -0
- package/dist/chunk-GHGXG53C.js +795 -0
- package/dist/chunk-GHGXG53C.js.map +1 -0
- package/dist/chunk-GKA4BGJN.js +79 -0
- package/dist/chunk-GKA4BGJN.js.map +1 -0
- package/dist/chunk-HG2OWBLX.js +430 -0
- package/dist/chunk-HG2OWBLX.js.map +1 -0
- package/dist/chunk-IGAROPKM.js +34 -0
- package/dist/chunk-IGAROPKM.js.map +1 -0
- package/dist/chunk-J66GRPNH.js +111 -0
- package/dist/chunk-J66GRPNH.js.map +1 -0
- package/dist/chunk-LVMMDXFT.js +275 -0
- package/dist/chunk-LVMMDXFT.js.map +1 -0
- package/dist/chunk-M5INGEFC.js +84 -0
- package/dist/chunk-M5INGEFC.js.map +1 -0
- package/dist/chunk-NBYQNDXA.js +557 -0
- package/dist/chunk-NBYQNDXA.js.map +1 -0
- package/dist/chunk-NPC4LFV5.js +132 -0
- package/dist/chunk-NPC4LFV5.js.map +1 -0
- package/dist/chunk-NSWHB5VQ.js +1285 -0
- package/dist/chunk-NSWHB5VQ.js.map +1 -0
- package/dist/chunk-OLM4LA6K.js +392 -0
- package/dist/chunk-OLM4LA6K.js.map +1 -0
- package/dist/chunk-UAFBZWFB.js +155 -0
- package/dist/chunk-UAFBZWFB.js.map +1 -0
- package/dist/chunk-UF3BUNQZ.js +1 -0
- package/dist/chunk-UF3BUNQZ.js.map +1 -0
- package/dist/chunk-UMMAVAYW.js +17 -0
- package/dist/chunk-UMMAVAYW.js.map +1 -0
- package/dist/chunk-UPY7WLBH.js +381 -0
- package/dist/chunk-UPY7WLBH.js.map +1 -0
- package/dist/chunk-W63BWEJH.js +311 -0
- package/dist/chunk-W63BWEJH.js.map +1 -0
- package/dist/chunk-WIGI5OJK.js +90 -0
- package/dist/chunk-WIGI5OJK.js.map +1 -0
- package/dist/chunk-XNL2TKKR.js +490 -0
- package/dist/chunk-XNL2TKKR.js.map +1 -0
- package/dist/chunk-XWNUJPIS.js +367 -0
- package/dist/chunk-XWNUJPIS.js.map +1 -0
- package/dist/chunk-YWKJZZGV.js +715 -0
- package/dist/chunk-YWKJZZGV.js.map +1 -0
- package/dist/consent/index.cjs +204 -0
- package/dist/consent/index.cjs.map +1 -0
- package/dist/consent/index.d.cts +24 -0
- package/dist/consent/index.d.ts +24 -0
- package/dist/consent/index.js +23 -0
- package/dist/consent/index.js.map +1 -0
- package/dist/crdt/index.cjs +152 -0
- package/dist/crdt/index.cjs.map +1 -0
- package/dist/crdt/index.d.cts +30 -0
- package/dist/crdt/index.d.ts +30 -0
- package/dist/crdt/index.js +24 -0
- package/dist/crdt/index.js.map +1 -0
- package/dist/crypto-6PNIHP7W.js +44 -0
- package/dist/crypto-6PNIHP7W.js.map +1 -0
- package/dist/delegation-WVIVMF73.js +17 -0
- package/dist/delegation-WVIVMF73.js.map +1 -0
- package/dist/dev-unlock-D4xB0_gs.d.cts +263 -0
- package/dist/dev-unlock-Dz8GEbd3.d.ts +263 -0
- package/dist/hash--EflSV65.d.cts +63 -0
- package/dist/hash-CRdXYnv3.d.ts +63 -0
- package/dist/history/index.cjs +1215 -0
- package/dist/history/index.cjs.map +1 -0
- package/dist/history/index.d.cts +62 -0
- package/dist/history/index.d.ts +62 -0
- package/dist/history/index.js +79 -0
- package/dist/history/index.js.map +1 -0
- package/dist/i18n/index.cjs +840 -0
- package/dist/i18n/index.cjs.map +1 -0
- package/dist/i18n/index.d.cts +38 -0
- package/dist/i18n/index.d.ts +38 -0
- package/dist/i18n/index.js +68 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/index-CD1VnONm.d.cts +415 -0
- package/dist/index-CLRxPs-W.d.cts +1960 -0
- package/dist/index-CUi9wfss.d.ts +415 -0
- package/dist/index-DtV93TMP.d.ts +1960 -0
- package/dist/index.cjs +17387 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +565 -0
- package/dist/index.d.ts +565 -0
- package/dist/index.js +7525 -0
- package/dist/index.js.map +1 -0
- package/dist/indexing/index.cjs +736 -0
- package/dist/indexing/index.cjs.map +1 -0
- package/dist/indexing/index.d.cts +36 -0
- package/dist/indexing/index.d.ts +36 -0
- package/dist/indexing/index.js +77 -0
- package/dist/indexing/index.js.map +1 -0
- package/dist/lazy-builder-BwEoBQZ9.d.ts +304 -0
- package/dist/lazy-builder-CZVLKh0Z.d.cts +304 -0
- package/dist/ledger-HBBH2NPZ.js +33 -0
- package/dist/ledger-HBBH2NPZ.js.map +1 -0
- package/dist/mime-magic-CBBSOkjm.d.cts +50 -0
- package/dist/mime-magic-CBBSOkjm.d.ts +50 -0
- package/dist/periods/index.cjs +1035 -0
- package/dist/periods/index.cjs.map +1 -0
- package/dist/periods/index.d.cts +21 -0
- package/dist/periods/index.d.ts +21 -0
- package/dist/periods/index.js +25 -0
- package/dist/periods/index.js.map +1 -0
- package/dist/predicate-SBHmi6D0.d.cts +161 -0
- package/dist/predicate-SBHmi6D0.d.ts +161 -0
- package/dist/public-envelope-TLQA6REO.js +31 -0
- package/dist/public-envelope-TLQA6REO.js.map +1 -0
- package/dist/query/index.cjs +1999 -0
- package/dist/query/index.cjs.map +1 -0
- package/dist/query/index.d.cts +3 -0
- package/dist/query/index.d.ts +3 -0
- package/dist/query/index.js +73 -0
- package/dist/query/index.js.map +1 -0
- package/dist/session/index.cjs +495 -0
- package/dist/session/index.cjs.map +1 -0
- package/dist/session/index.d.cts +45 -0
- package/dist/session/index.d.ts +45 -0
- package/dist/session/index.js +51 -0
- package/dist/session/index.js.map +1 -0
- package/dist/shadow/index.cjs +133 -0
- package/dist/shadow/index.cjs.map +1 -0
- package/dist/shadow/index.d.cts +16 -0
- package/dist/shadow/index.d.ts +16 -0
- package/dist/shadow/index.js +20 -0
- package/dist/shadow/index.js.map +1 -0
- package/dist/store/index.cjs +1083 -0
- package/dist/store/index.cjs.map +1 -0
- package/dist/store/index.d.cts +491 -0
- package/dist/store/index.d.ts +491 -0
- package/dist/store/index.js +37 -0
- package/dist/store/index.js.map +1 -0
- package/dist/strategy-BSxFXGzb.d.cts +110 -0
- package/dist/strategy-BSxFXGzb.d.ts +110 -0
- package/dist/strategy-D-SrOLCl.d.cts +548 -0
- package/dist/strategy-D-SrOLCl.d.ts +548 -0
- package/dist/sync/index.cjs +1062 -0
- package/dist/sync/index.cjs.map +1 -0
- package/dist/sync/index.d.cts +42 -0
- package/dist/sync/index.d.ts +42 -0
- package/dist/sync/index.js +28 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/team/index.cjs +2606 -0
- package/dist/team/index.cjs.map +1 -0
- package/dist/team/index.d.cts +117 -0
- package/dist/team/index.d.ts +117 -0
- package/dist/team/index.js +106 -0
- package/dist/team/index.js.map +1 -0
- package/dist/tx/index.cjs +212 -0
- package/dist/tx/index.cjs.map +1 -0
- package/dist/tx/index.d.cts +20 -0
- package/dist/tx/index.d.ts +20 -0
- package/dist/tx/index.js +20 -0
- package/dist/tx/index.js.map +1 -0
- package/dist/types-DSFLtbKg.d.ts +9702 -0
- package/dist/types-zwwMOqkg.d.cts +9702 -0
- package/dist/ulid-COREQ2RQ.js +9 -0
- package/dist/ulid-COREQ2RQ.js.map +1 -0
- package/dist/util/index.cjs +230 -0
- package/dist/util/index.cjs.map +1 -0
- package/dist/util/index.d.cts +77 -0
- package/dist/util/index.d.ts +77 -0
- package/dist/util/index.js +190 -0
- package/dist/util/index.js.map +1 -0
- package/package.json +244 -0
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NOYDB_FORMAT_VERSION,
|
|
3
|
+
NOYDB_KEYRING_VERSION
|
|
4
|
+
} from "./chunk-UMMAVAYW.js";
|
|
5
|
+
import {
|
|
6
|
+
base64ToBuffer,
|
|
7
|
+
bufferToBase64,
|
|
8
|
+
decrypt,
|
|
9
|
+
deriveKey,
|
|
10
|
+
encrypt,
|
|
11
|
+
generateDEK,
|
|
12
|
+
generateSalt,
|
|
13
|
+
unwrapKey,
|
|
14
|
+
wrapKey
|
|
15
|
+
} from "./chunk-LVMMDXFT.js";
|
|
16
|
+
import {
|
|
17
|
+
ConflictError,
|
|
18
|
+
InvalidKeyError,
|
|
19
|
+
KeyringCorruptError,
|
|
20
|
+
KeyringExpiredError,
|
|
21
|
+
NoAccessError,
|
|
22
|
+
NoydbError,
|
|
23
|
+
PermissionDeniedError,
|
|
24
|
+
PrivilegeEscalationError,
|
|
25
|
+
ValidationError
|
|
26
|
+
} from "./chunk-NBYQNDXA.js";
|
|
27
|
+
|
|
28
|
+
// src/validation.ts
|
|
29
|
+
var WeakPassphraseError = class extends NoydbError {
|
|
30
|
+
reason;
|
|
31
|
+
suggestion;
|
|
32
|
+
constructor(reason, suggestion) {
|
|
33
|
+
super("WEAK_PASSPHRASE", `Weak passphrase (${reason}). ${suggestion}`);
|
|
34
|
+
this.name = "WeakPassphraseError";
|
|
35
|
+
this.reason = reason;
|
|
36
|
+
this.suggestion = suggestion;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var DEFAULT_MIN_WORDS = 6;
|
|
40
|
+
var DEFAULT_MIN_WORD_LENGTH = 3;
|
|
41
|
+
var SUGGESTIONS = {
|
|
42
|
+
empty: "Provide a phrase of at least 6 lowercase words separated by single spaces.",
|
|
43
|
+
"invalid-chars": "Use only lowercase letters [a-z] and single spaces. No punctuation, symbols, digits, or uppercase.",
|
|
44
|
+
"leading-or-trailing-space": "Trim leading and trailing spaces.",
|
|
45
|
+
"double-space": "Use exactly one space between words.",
|
|
46
|
+
"too-few-words": 'Use at least 6 words by default (8 under strict policy). Example: "correct horse battery staple printer toaster".',
|
|
47
|
+
"word-too-short": 'Each word must be at least 3 characters. Drop short fillers like "a", "is", "of".',
|
|
48
|
+
"repeated-adjacent": "Avoid repeating the same word twice in a row."
|
|
49
|
+
};
|
|
50
|
+
function validatePassphrase(s, opts) {
|
|
51
|
+
if (opts?.customValidator) {
|
|
52
|
+
return opts.customValidator(s);
|
|
53
|
+
}
|
|
54
|
+
const minWords = opts?.minWords ?? DEFAULT_MIN_WORDS;
|
|
55
|
+
const minWordLength = opts?.minWordLength ?? DEFAULT_MIN_WORD_LENGTH;
|
|
56
|
+
const rejectRepeated = opts?.rejectRepeatedAdjacent ?? true;
|
|
57
|
+
if (s.length === 0) {
|
|
58
|
+
return { ok: false, reason: "empty" };
|
|
59
|
+
}
|
|
60
|
+
if (s !== s.trim()) {
|
|
61
|
+
return { ok: false, reason: "leading-or-trailing-space" };
|
|
62
|
+
}
|
|
63
|
+
if (s.includes(" ")) {
|
|
64
|
+
return { ok: false, reason: "double-space" };
|
|
65
|
+
}
|
|
66
|
+
const charPattern = opts?.pattern ?? /^[a-z]+( [a-z]+)*$/;
|
|
67
|
+
if (!charPattern.test(s)) {
|
|
68
|
+
return { ok: false, reason: "invalid-chars" };
|
|
69
|
+
}
|
|
70
|
+
const words = s.split(" ");
|
|
71
|
+
if (words.length < minWords) {
|
|
72
|
+
return { ok: false, reason: "too-few-words", minimum: minWords, got: words.length };
|
|
73
|
+
}
|
|
74
|
+
for (const w of words) {
|
|
75
|
+
if (w.length < minWordLength) {
|
|
76
|
+
return { ok: false, reason: "word-too-short", minimum: minWordLength, got: w.length };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (rejectRepeated) {
|
|
80
|
+
for (let i = 1; i < words.length; i++) {
|
|
81
|
+
if (words[i] === words[i - 1]) {
|
|
82
|
+
return { ok: false, reason: "repeated-adjacent" };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { ok: true, words: words.length };
|
|
87
|
+
}
|
|
88
|
+
function assertStrongPassphrase(s, opts) {
|
|
89
|
+
if (opts?.allowWeakPassphrase) return;
|
|
90
|
+
const result = validatePassphrase(s, opts);
|
|
91
|
+
if (result.ok) return;
|
|
92
|
+
throw new WeakPassphraseError(result.reason, SUGGESTIONS[result.reason]);
|
|
93
|
+
}
|
|
94
|
+
function estimateEntropy(passphrase) {
|
|
95
|
+
const result = validatePassphrase(passphrase);
|
|
96
|
+
if (!result.ok) return 0;
|
|
97
|
+
return Math.round(result.words * Math.log2(7776));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/meta/user-envelope/types.ts
|
|
101
|
+
var USER_ENVELOPE_MAX_BYTES = 64 * 1024;
|
|
102
|
+
var USER_ENVELOPE_COLLECTION = "_users";
|
|
103
|
+
var UserEnvelopeOversizedError = class extends NoydbError {
|
|
104
|
+
bytes;
|
|
105
|
+
limit;
|
|
106
|
+
constructor(bytes, limit = USER_ENVELOPE_MAX_BYTES) {
|
|
107
|
+
super(
|
|
108
|
+
"USER_ENVELOPE_OVERSIZED",
|
|
109
|
+
`User envelope payload is ${bytes} bytes; soft cap is ${limit} bytes. Move large data into the vault's regular collections.`
|
|
110
|
+
);
|
|
111
|
+
this.name = "UserEnvelopeOversizedError";
|
|
112
|
+
this.bytes = bytes;
|
|
113
|
+
this.limit = limit;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// src/meta/user-envelope/storage.ts
|
|
118
|
+
async function loadUserEnvelope(store, vault, keyringId, dek) {
|
|
119
|
+
const envelope = await store.get(vault, USER_ENVELOPE_COLLECTION, keyringId);
|
|
120
|
+
if (!envelope) return null;
|
|
121
|
+
const plaintext = await decrypt(envelope._iv, envelope._data, dek);
|
|
122
|
+
const data = JSON.parse(plaintext);
|
|
123
|
+
return {
|
|
124
|
+
keyringId,
|
|
125
|
+
data,
|
|
126
|
+
_v: envelope._v,
|
|
127
|
+
_ts: envelope._ts
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
async function saveUserEnvelope(store, vault, keyringId, payload, dek, expectedVersion) {
|
|
131
|
+
const json = JSON.stringify(payload);
|
|
132
|
+
const bytes = new TextEncoder().encode(json).byteLength;
|
|
133
|
+
if (bytes > USER_ENVELOPE_MAX_BYTES) {
|
|
134
|
+
throw new UserEnvelopeOversizedError(bytes);
|
|
135
|
+
}
|
|
136
|
+
const prior = await store.get(vault, USER_ENVELOPE_COLLECTION, keyringId);
|
|
137
|
+
if (expectedVersion !== void 0) {
|
|
138
|
+
const priorVersion = prior?._v ?? 0;
|
|
139
|
+
if (priorVersion !== expectedVersion) {
|
|
140
|
+
throw new ConflictError(
|
|
141
|
+
priorVersion,
|
|
142
|
+
`User envelope for "${keyringId}" expected version ${expectedVersion}, actual ${priorVersion}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const nextVersion = (prior?._v ?? 0) + 1;
|
|
147
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
148
|
+
const { iv, data } = await encrypt(json, dek);
|
|
149
|
+
const envelope = {
|
|
150
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
151
|
+
_v: nextVersion,
|
|
152
|
+
_ts: ts,
|
|
153
|
+
_iv: iv,
|
|
154
|
+
_data: data
|
|
155
|
+
};
|
|
156
|
+
await store.put(vault, USER_ENVELOPE_COLLECTION, keyringId, envelope);
|
|
157
|
+
return {
|
|
158
|
+
keyringId,
|
|
159
|
+
data: payload,
|
|
160
|
+
_v: nextVersion,
|
|
161
|
+
_ts: ts
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
async function deleteUserEnvelope(store, vault, keyringId) {
|
|
165
|
+
await store.delete(vault, USER_ENVELOPE_COLLECTION, keyringId);
|
|
166
|
+
}
|
|
167
|
+
async function listUserEnvelopeIds(store, vault) {
|
|
168
|
+
return store.list(vault, USER_ENVELOPE_COLLECTION);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/team/keyring.ts
|
|
172
|
+
var ADMIN_GRANTABLE_TARGETS = ["operator", "viewer", "client", "admin"];
|
|
173
|
+
function canGrant(callerRole, targetRole) {
|
|
174
|
+
if (callerRole === "owner") return true;
|
|
175
|
+
if (callerRole === "admin") return ADMIN_GRANTABLE_TARGETS.includes(targetRole);
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
function canRevoke(callerRole, targetRole) {
|
|
179
|
+
if (targetRole === "owner") return false;
|
|
180
|
+
if (callerRole === "owner") return true;
|
|
181
|
+
if (callerRole === "admin") return ADMIN_GRANTABLE_TARGETS.includes(targetRole);
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
function canUpdateRole(callerRole, targetRole) {
|
|
185
|
+
if (callerRole === "owner") return true;
|
|
186
|
+
if (callerRole === "admin") return ADMIN_GRANTABLE_TARGETS.includes(targetRole);
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
var CANARY_PLAINTEXT_BYTES = new Uint8Array(32);
|
|
190
|
+
var canaryKeyPromise = null;
|
|
191
|
+
function getCanaryKey() {
|
|
192
|
+
if (canaryKeyPromise === null) {
|
|
193
|
+
canaryKeyPromise = globalThis.crypto.subtle.importKey(
|
|
194
|
+
"raw",
|
|
195
|
+
CANARY_PLAINTEXT_BYTES,
|
|
196
|
+
{ name: "AES-GCM", length: 256 },
|
|
197
|
+
true,
|
|
198
|
+
// extractable so AES-KW can wrap it
|
|
199
|
+
["encrypt", "decrypt"]
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return canaryKeyPromise;
|
|
203
|
+
}
|
|
204
|
+
async function mintKeyringCanary(kek) {
|
|
205
|
+
const canaryKey = await getCanaryKey();
|
|
206
|
+
return wrapKey(canaryKey, kek);
|
|
207
|
+
}
|
|
208
|
+
async function verifyKeyringCanary(wrappedCanary, kek) {
|
|
209
|
+
try {
|
|
210
|
+
await unwrapKey(wrappedCanary, kek);
|
|
211
|
+
return true;
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function loadKeyring(adapter, vault, userId, passphrase) {
|
|
217
|
+
const envelope = await adapter.get(vault, "_keyring", userId);
|
|
218
|
+
if (!envelope) {
|
|
219
|
+
throw new NoAccessError(`No keyring found for user "${userId}" in vault "${vault}"`);
|
|
220
|
+
}
|
|
221
|
+
const keyringFile = JSON.parse(envelope._data);
|
|
222
|
+
if (keyringFile.expires_at !== void 0) {
|
|
223
|
+
const cutoff = Date.parse(keyringFile.expires_at);
|
|
224
|
+
if (Number.isFinite(cutoff) && Date.now() >= cutoff) {
|
|
225
|
+
throw new KeyringExpiredError({ userId: keyringFile.user_id, expiresAt: keyringFile.expires_at });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const salt = base64ToBuffer(keyringFile.salt);
|
|
229
|
+
const kek = await deriveKey(passphrase, salt);
|
|
230
|
+
const canaryOk = keyringFile.canary !== void 0 ? await verifyKeyringCanary(keyringFile.canary, kek) : null;
|
|
231
|
+
const deks = /* @__PURE__ */ new Map();
|
|
232
|
+
const failedCollections = [];
|
|
233
|
+
let firstUnwrapError = null;
|
|
234
|
+
for (const [collName, wrappedDek] of Object.entries(keyringFile.deks)) {
|
|
235
|
+
try {
|
|
236
|
+
const dek = await unwrapKey(wrappedDek, kek);
|
|
237
|
+
deks.set(collName, dek);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
failedCollections.push(collName);
|
|
240
|
+
if (firstUnwrapError === null) firstUnwrapError = err;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (canaryOk === true) {
|
|
244
|
+
if (failedCollections.length > 0) {
|
|
245
|
+
throw new KeyringCorruptError({ failedCollections, intactCount: deks.size });
|
|
246
|
+
}
|
|
247
|
+
} else if (canaryOk === false) {
|
|
248
|
+
if (deks.size > 0) {
|
|
249
|
+
throw new KeyringCorruptError({
|
|
250
|
+
failedCollections: [...failedCollections, "_canary"],
|
|
251
|
+
intactCount: deks.size
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
throw firstUnwrapError instanceof Error ? firstUnwrapError : new InvalidKeyError();
|
|
255
|
+
} else {
|
|
256
|
+
if (failedCollections.length > 0) {
|
|
257
|
+
if (deks.size > 0) {
|
|
258
|
+
throw new KeyringCorruptError({ failedCollections, intactCount: deks.size });
|
|
259
|
+
}
|
|
260
|
+
throw firstUnwrapError instanceof Error ? firstUnwrapError : new InvalidKeyError();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
userId: keyringFile.user_id,
|
|
265
|
+
displayName: keyringFile.display_name,
|
|
266
|
+
role: keyringFile.role,
|
|
267
|
+
permissions: keyringFile.permissions,
|
|
268
|
+
deks,
|
|
269
|
+
kek,
|
|
270
|
+
salt,
|
|
271
|
+
authenticators: keyringFile.authenticators ?? [],
|
|
272
|
+
...keyringFile.export_capability !== void 0 && { exportCapability: keyringFile.export_capability },
|
|
273
|
+
...keyringFile.import_capability !== void 0 && { importCapability: keyringFile.import_capability },
|
|
274
|
+
...keyringFile.policy !== void 0 && { policy: keyringFile.policy }
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
async function createOwnerKeyring(adapter, vault, userId, passphrase, passphraseOpts) {
|
|
278
|
+
if (passphraseOpts?.validate && !passphraseOpts.allowWeakPassphrase) {
|
|
279
|
+
assertStrongPassphrase(passphrase, passphraseOpts);
|
|
280
|
+
}
|
|
281
|
+
const salt = generateSalt();
|
|
282
|
+
const kek = await deriveKey(passphrase, salt);
|
|
283
|
+
const userEnvelopeDek = await generateDEK();
|
|
284
|
+
const wrappedUserEnvelopeDek = await wrapKey(userEnvelopeDek, kek);
|
|
285
|
+
const canary = await mintKeyringCanary(kek);
|
|
286
|
+
const keyringFile = {
|
|
287
|
+
_noydb_keyring: NOYDB_KEYRING_VERSION,
|
|
288
|
+
user_id: userId,
|
|
289
|
+
display_name: userId,
|
|
290
|
+
role: "owner",
|
|
291
|
+
permissions: {},
|
|
292
|
+
deks: { [USER_ENVELOPE_COLLECTION]: wrappedUserEnvelopeDek },
|
|
293
|
+
salt: bufferToBase64(salt),
|
|
294
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
295
|
+
granted_by: userId,
|
|
296
|
+
canary
|
|
297
|
+
};
|
|
298
|
+
await writeKeyringFile(adapter, vault, userId, keyringFile);
|
|
299
|
+
return {
|
|
300
|
+
userId,
|
|
301
|
+
displayName: userId,
|
|
302
|
+
role: "owner",
|
|
303
|
+
permissions: {},
|
|
304
|
+
deks: /* @__PURE__ */ new Map([[USER_ENVELOPE_COLLECTION, userEnvelopeDek]]),
|
|
305
|
+
kek,
|
|
306
|
+
salt,
|
|
307
|
+
authenticators: []
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
async function grant(adapter, vault, callerKeyring, options) {
|
|
311
|
+
if (!callerKeyring.kek) {
|
|
312
|
+
throw new ValidationError(
|
|
313
|
+
"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."
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
if (!canGrant(callerKeyring.role, options.role)) {
|
|
317
|
+
throw new PermissionDeniedError(
|
|
318
|
+
`Role "${callerKeyring.role}" cannot grant role "${options.role}"`
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
if (options.validatePassphrase && !options.allowWeakPassphrase) {
|
|
322
|
+
assertStrongPassphrase(options.passphrase);
|
|
323
|
+
}
|
|
324
|
+
const permissions = resolvePermissions(options.role, options.permissions);
|
|
325
|
+
const newSalt = generateSalt();
|
|
326
|
+
const newKek = await deriveKey(options.passphrase, newSalt);
|
|
327
|
+
const wrappedDeks = {};
|
|
328
|
+
for (const collName of Object.keys(permissions)) {
|
|
329
|
+
const dek = callerKeyring.deks.get(collName);
|
|
330
|
+
if (dek) {
|
|
331
|
+
wrappedDeks[collName] = await wrapKey(dek, newKek);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (options.role === "owner" || options.role === "admin" || options.role === "viewer") {
|
|
335
|
+
for (const [collName, dek] of callerKeyring.deks) {
|
|
336
|
+
if (!(collName in wrappedDeks)) {
|
|
337
|
+
wrappedDeks[collName] = await wrapKey(dek, newKek);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
for (const [collName, dek] of callerKeyring.deks) {
|
|
342
|
+
if (collName.startsWith("_") && !(collName in wrappedDeks)) {
|
|
343
|
+
wrappedDeks[collName] = await wrapKey(dek, newKek);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
for (const collName of Object.keys(wrappedDeks)) {
|
|
347
|
+
if (!callerKeyring.deks.has(collName)) {
|
|
348
|
+
throw new PrivilegeEscalationError(collName);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const canary = await mintKeyringCanary(newKek);
|
|
352
|
+
const keyringFile = {
|
|
353
|
+
_noydb_keyring: NOYDB_KEYRING_VERSION,
|
|
354
|
+
user_id: options.userId,
|
|
355
|
+
display_name: options.displayName,
|
|
356
|
+
role: options.role,
|
|
357
|
+
permissions,
|
|
358
|
+
deks: wrappedDeks,
|
|
359
|
+
salt: bufferToBase64(newSalt),
|
|
360
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
361
|
+
granted_by: callerKeyring.userId,
|
|
362
|
+
canary,
|
|
363
|
+
...options.exportCapability !== void 0 && { export_capability: options.exportCapability },
|
|
364
|
+
...options.importCapability !== void 0 && { import_capability: options.importCapability }
|
|
365
|
+
};
|
|
366
|
+
await writeKeyringFile(adapter, vault, options.userId, keyringFile);
|
|
367
|
+
const userEnvelopeDek = callerKeyring.deks.get(USER_ENVELOPE_COLLECTION);
|
|
368
|
+
if (userEnvelopeDek) {
|
|
369
|
+
const initialPayload = options.initialProfile ?? {};
|
|
370
|
+
await saveUserEnvelope(
|
|
371
|
+
adapter,
|
|
372
|
+
vault,
|
|
373
|
+
options.userId,
|
|
374
|
+
initialPayload,
|
|
375
|
+
userEnvelopeDek
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async function findAdminDescendants(adapter, vault, rootUserId) {
|
|
380
|
+
const allUserIds = await adapter.list(vault, "_keyring");
|
|
381
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
382
|
+
for (const userId of allUserIds) {
|
|
383
|
+
const env = await adapter.get(vault, "_keyring", userId);
|
|
384
|
+
if (!env) continue;
|
|
385
|
+
const kf = JSON.parse(env._data);
|
|
386
|
+
if (kf.role !== "admin") continue;
|
|
387
|
+
if (kf.user_id === rootUserId) continue;
|
|
388
|
+
const list = childrenByParent.get(kf.granted_by) ?? [];
|
|
389
|
+
list.push(kf.user_id);
|
|
390
|
+
childrenByParent.set(kf.granted_by, list);
|
|
391
|
+
}
|
|
392
|
+
const visited = /* @__PURE__ */ new Set();
|
|
393
|
+
const order = [];
|
|
394
|
+
const stack = [...childrenByParent.get(rootUserId) ?? []];
|
|
395
|
+
while (stack.length > 0) {
|
|
396
|
+
const next = stack.pop();
|
|
397
|
+
if (visited.has(next)) continue;
|
|
398
|
+
visited.add(next);
|
|
399
|
+
order.push(next);
|
|
400
|
+
for (const grandchild of childrenByParent.get(next) ?? []) {
|
|
401
|
+
if (!visited.has(grandchild)) stack.push(grandchild);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return order;
|
|
405
|
+
}
|
|
406
|
+
async function revoke(adapter, vault, callerKeyring, options) {
|
|
407
|
+
const targetEnvelope = await adapter.get(vault, "_keyring", options.userId);
|
|
408
|
+
if (!targetEnvelope) {
|
|
409
|
+
throw new NoAccessError(`User "${options.userId}" has no keyring in vault "${vault}"`);
|
|
410
|
+
}
|
|
411
|
+
const targetKeyring = JSON.parse(targetEnvelope._data);
|
|
412
|
+
if (!canRevoke(callerKeyring.role, targetKeyring.role)) {
|
|
413
|
+
throw new PermissionDeniedError(
|
|
414
|
+
`Role "${callerKeyring.role}" cannot revoke role "${targetKeyring.role}"`
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
const cascadeMode = options.cascade ?? "strict";
|
|
418
|
+
const usersToRevoke = [options.userId];
|
|
419
|
+
const affectedCollections = new Set(Object.keys(targetKeyring.deks));
|
|
420
|
+
if (targetKeyring.role === "admin") {
|
|
421
|
+
const descendants = await findAdminDescendants(adapter, vault, options.userId);
|
|
422
|
+
if (descendants.length > 0) {
|
|
423
|
+
if (cascadeMode === "warn") {
|
|
424
|
+
console.warn(
|
|
425
|
+
`[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.`
|
|
426
|
+
);
|
|
427
|
+
} else {
|
|
428
|
+
for (const userId of descendants) {
|
|
429
|
+
const descEnv = await adapter.get(vault, "_keyring", userId);
|
|
430
|
+
if (!descEnv) continue;
|
|
431
|
+
const descKf = JSON.parse(descEnv._data);
|
|
432
|
+
usersToRevoke.push(userId);
|
|
433
|
+
for (const c of Object.keys(descKf.deks)) affectedCollections.add(c);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
for (const userId of usersToRevoke) {
|
|
439
|
+
await adapter.delete(vault, "_keyring", userId);
|
|
440
|
+
await deleteUserEnvelope(adapter, vault, userId);
|
|
441
|
+
}
|
|
442
|
+
if (options.rotateKeys !== false && affectedCollections.size > 0) {
|
|
443
|
+
await rotateKeys(adapter, vault, callerKeyring, [...affectedCollections]);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async function updateKeyringIdentity(adapter, vault, callerKeyring, options) {
|
|
447
|
+
if (options.role === void 0 && options.displayName === void 0 && options.permissions === void 0) {
|
|
448
|
+
throw new ValidationError(
|
|
449
|
+
`updateUser: at least one of role / displayName / permissions must be provided (userId: "${options.userId}").`
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
const env = await adapter.get(vault, "_keyring", options.userId);
|
|
453
|
+
if (!env) {
|
|
454
|
+
throw new NoAccessError(
|
|
455
|
+
`updateUser: user "${options.userId}" has no keyring in vault "${vault}".`
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
const target = JSON.parse(env._data);
|
|
459
|
+
if (!canUpdateRole(callerKeyring.role, target.role)) {
|
|
460
|
+
throw new PermissionDeniedError(
|
|
461
|
+
`Role "${callerKeyring.role}" cannot update a keyring with role "${target.role}"`
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
if (options.role !== void 0 && options.role !== target.role && !canUpdateRole(callerKeyring.role, options.role)) {
|
|
465
|
+
throw new PermissionDeniedError(
|
|
466
|
+
`Role "${callerKeyring.role}" cannot promote target to role "${options.role}"`
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
const next = {
|
|
470
|
+
...target,
|
|
471
|
+
...options.role !== void 0 && { role: options.role },
|
|
472
|
+
...options.displayName !== void 0 && {
|
|
473
|
+
// null clears the field (stored as ""); a string sets it.
|
|
474
|
+
display_name: options.displayName ?? ""
|
|
475
|
+
},
|
|
476
|
+
...options.permissions !== void 0 && { permissions: options.permissions }
|
|
477
|
+
};
|
|
478
|
+
await writeKeyringFile(adapter, vault, options.userId, next);
|
|
479
|
+
}
|
|
480
|
+
async function rotateKeys(adapter, vault, callerKeyring, collections) {
|
|
481
|
+
const newDeks = /* @__PURE__ */ new Map();
|
|
482
|
+
for (const collName of collections) {
|
|
483
|
+
newDeks.set(collName, await generateDEK());
|
|
484
|
+
}
|
|
485
|
+
for (const collName of collections) {
|
|
486
|
+
const oldDek = callerKeyring.deks.get(collName);
|
|
487
|
+
const newDek = newDeks.get(collName);
|
|
488
|
+
if (!oldDek) continue;
|
|
489
|
+
const ids = await adapter.list(vault, collName);
|
|
490
|
+
for (const id of ids) {
|
|
491
|
+
const envelope = await adapter.get(vault, collName, id);
|
|
492
|
+
if (!envelope || !envelope._iv) continue;
|
|
493
|
+
const plaintext = await decrypt(envelope._iv, envelope._data, oldDek);
|
|
494
|
+
const { iv, data } = await encrypt(plaintext, newDek);
|
|
495
|
+
const newEnvelope = {
|
|
496
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
497
|
+
_v: envelope._v,
|
|
498
|
+
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
499
|
+
_iv: iv,
|
|
500
|
+
_data: data
|
|
501
|
+
};
|
|
502
|
+
await adapter.put(vault, collName, id, newEnvelope);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
for (const [collName, newDek] of newDeks) {
|
|
506
|
+
callerKeyring.deks.set(collName, newDek);
|
|
507
|
+
}
|
|
508
|
+
await persistKeyring(adapter, vault, callerKeyring);
|
|
509
|
+
const userIds = await adapter.list(vault, "_keyring");
|
|
510
|
+
for (const userId of userIds) {
|
|
511
|
+
if (userId === callerKeyring.userId) continue;
|
|
512
|
+
const userEnvelope = await adapter.get(vault, "_keyring", userId);
|
|
513
|
+
if (!userEnvelope) continue;
|
|
514
|
+
const userKeyringFile = JSON.parse(userEnvelope._data);
|
|
515
|
+
const updatedDeks = { ...userKeyringFile.deks };
|
|
516
|
+
for (const collName of collections) {
|
|
517
|
+
delete updatedDeks[collName];
|
|
518
|
+
}
|
|
519
|
+
const updatedPermissions = { ...userKeyringFile.permissions };
|
|
520
|
+
for (const collName of collections) {
|
|
521
|
+
delete updatedPermissions[collName];
|
|
522
|
+
}
|
|
523
|
+
const updatedKeyring = {
|
|
524
|
+
...userKeyringFile,
|
|
525
|
+
deks: updatedDeks,
|
|
526
|
+
permissions: updatedPermissions
|
|
527
|
+
};
|
|
528
|
+
await writeKeyringFile(adapter, vault, userId, updatedKeyring);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
async function changeSecret(adapter, vault, keyring, newPassphrase, passphraseOpts) {
|
|
532
|
+
if (!passphraseOpts?.allowWeakPassphrase) {
|
|
533
|
+
assertStrongPassphrase(newPassphrase, passphraseOpts);
|
|
534
|
+
}
|
|
535
|
+
const newSalt = generateSalt();
|
|
536
|
+
const newKek = await deriveKey(newPassphrase, newSalt);
|
|
537
|
+
const wrappedDeks = {};
|
|
538
|
+
for (const [collName, dek] of keyring.deks) {
|
|
539
|
+
wrappedDeks[collName] = await wrapKey(dek, newKek);
|
|
540
|
+
}
|
|
541
|
+
const canary = await mintKeyringCanary(newKek);
|
|
542
|
+
const keyringFile = {
|
|
543
|
+
_noydb_keyring: NOYDB_KEYRING_VERSION,
|
|
544
|
+
user_id: keyring.userId,
|
|
545
|
+
display_name: keyring.displayName,
|
|
546
|
+
role: keyring.role,
|
|
547
|
+
permissions: keyring.permissions,
|
|
548
|
+
deks: wrappedDeks,
|
|
549
|
+
salt: bufferToBase64(newSalt),
|
|
550
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
551
|
+
granted_by: keyring.userId,
|
|
552
|
+
canary
|
|
553
|
+
};
|
|
554
|
+
await writeKeyringFile(adapter, vault, keyring.userId, keyringFile);
|
|
555
|
+
return {
|
|
556
|
+
userId: keyring.userId,
|
|
557
|
+
displayName: keyring.displayName,
|
|
558
|
+
role: keyring.role,
|
|
559
|
+
permissions: keyring.permissions,
|
|
560
|
+
deks: keyring.deks,
|
|
561
|
+
// Same DEKs, different wrapping
|
|
562
|
+
kek: newKek,
|
|
563
|
+
salt: newSalt,
|
|
564
|
+
// Tier-2 slots are NOT preserved through `changeSecret` —
|
|
565
|
+
// each slot wraps the OLD KEK, so the new keyring has no
|
|
566
|
+
// authenticator slots until the user re-enrolls. The higher-level
|
|
567
|
+
// `db.rotatePassphrase()` (#10) preserves slots by rewrapping the
|
|
568
|
+
// KEK reference, not the KEK itself.
|
|
569
|
+
authenticators: [],
|
|
570
|
+
...keyring.policy !== void 0 && { policy: keyring.policy }
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
async function buildRecipientKeyringFile(callerKeyring, recipient) {
|
|
574
|
+
if (!callerKeyring.kek) {
|
|
575
|
+
throw new ValidationError(
|
|
576
|
+
"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."
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
const role = recipient.role ?? "viewer";
|
|
580
|
+
const permissions = resolvePermissions(role, recipient.permissions);
|
|
581
|
+
const newSalt = generateSalt();
|
|
582
|
+
const newKek = await deriveKey(recipient.passphrase, newSalt);
|
|
583
|
+
const wrappedDeks = {};
|
|
584
|
+
for (const collName of Object.keys(permissions)) {
|
|
585
|
+
const dek = callerKeyring.deks.get(collName);
|
|
586
|
+
if (dek) {
|
|
587
|
+
wrappedDeks[collName] = await wrapKey(dek, newKek);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (role === "owner" || role === "admin" || role === "viewer") {
|
|
591
|
+
for (const [collName, dek] of callerKeyring.deks) {
|
|
592
|
+
if (!(collName in wrappedDeks)) {
|
|
593
|
+
wrappedDeks[collName] = await wrapKey(dek, newKek);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
for (const [collName, dek] of callerKeyring.deks) {
|
|
598
|
+
if (collName.startsWith("_") && !(collName in wrappedDeks)) {
|
|
599
|
+
wrappedDeks[collName] = await wrapKey(dek, newKek);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
for (const collName of Object.keys(wrappedDeks)) {
|
|
603
|
+
if (!callerKeyring.deks.has(collName)) {
|
|
604
|
+
throw new PrivilegeEscalationError(collName);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const canary = await mintKeyringCanary(newKek);
|
|
608
|
+
return {
|
|
609
|
+
_noydb_keyring: NOYDB_KEYRING_VERSION,
|
|
610
|
+
user_id: recipient.id,
|
|
611
|
+
display_name: recipient.displayName ?? recipient.id,
|
|
612
|
+
role,
|
|
613
|
+
permissions,
|
|
614
|
+
deks: wrappedDeks,
|
|
615
|
+
salt: bufferToBase64(newSalt),
|
|
616
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
617
|
+
granted_by: callerKeyring.userId,
|
|
618
|
+
canary,
|
|
619
|
+
...recipient.exportCapability !== void 0 ? { export_capability: recipient.exportCapability } : {},
|
|
620
|
+
...recipient.importCapability !== void 0 ? { import_capability: recipient.importCapability } : {},
|
|
621
|
+
...recipient.expiresAt !== void 0 ? { expires_at: recipient.expiresAt } : {}
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
async function listUsers(adapter, vault) {
|
|
625
|
+
const userIds = await adapter.list(vault, "_keyring");
|
|
626
|
+
const users = [];
|
|
627
|
+
for (const userId of userIds) {
|
|
628
|
+
const envelope = await adapter.get(vault, "_keyring", userId);
|
|
629
|
+
if (!envelope) continue;
|
|
630
|
+
const kf = JSON.parse(envelope._data);
|
|
631
|
+
users.push({
|
|
632
|
+
userId: kf.user_id,
|
|
633
|
+
displayName: kf.display_name,
|
|
634
|
+
role: kf.role,
|
|
635
|
+
permissions: kf.permissions,
|
|
636
|
+
createdAt: kf.created_at,
|
|
637
|
+
grantedBy: kf.granted_by
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
return users;
|
|
641
|
+
}
|
|
642
|
+
async function listUsersWithEnvelopes(adapter, vault, userEnvelopeDek) {
|
|
643
|
+
const users = await listUsers(adapter, vault);
|
|
644
|
+
const out = [];
|
|
645
|
+
for (const user of users) {
|
|
646
|
+
const envelope = await loadUserEnvelope(
|
|
647
|
+
adapter,
|
|
648
|
+
vault,
|
|
649
|
+
user.userId,
|
|
650
|
+
userEnvelopeDek
|
|
651
|
+
);
|
|
652
|
+
out.push({ user, envelope });
|
|
653
|
+
}
|
|
654
|
+
return out;
|
|
655
|
+
}
|
|
656
|
+
async function ensureCollectionDEK(adapter, vault, keyring) {
|
|
657
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
658
|
+
return async (collectionName) => {
|
|
659
|
+
const existing = keyring.deks.get(collectionName);
|
|
660
|
+
if (existing) return existing;
|
|
661
|
+
const pending = inFlight.get(collectionName);
|
|
662
|
+
if (pending) return pending;
|
|
663
|
+
const promise = (async () => {
|
|
664
|
+
const dek = await generateDEK();
|
|
665
|
+
keyring.deks.set(collectionName, dek);
|
|
666
|
+
await persistKeyring(adapter, vault, keyring);
|
|
667
|
+
return dek;
|
|
668
|
+
})();
|
|
669
|
+
inFlight.set(collectionName, promise);
|
|
670
|
+
try {
|
|
671
|
+
return await promise;
|
|
672
|
+
} finally {
|
|
673
|
+
inFlight.delete(collectionName);
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
function hasWritePermission(keyring, collectionName) {
|
|
678
|
+
if (keyring.role === "owner" || keyring.role === "admin") return true;
|
|
679
|
+
if (keyring.role === "viewer" || keyring.role === "client") return false;
|
|
680
|
+
return keyring.permissions[collectionName] === "rw";
|
|
681
|
+
}
|
|
682
|
+
function hasAccess(keyring, collectionName) {
|
|
683
|
+
if (keyring.role === "owner" || keyring.role === "admin" || keyring.role === "viewer") return true;
|
|
684
|
+
return collectionName in keyring.permissions;
|
|
685
|
+
}
|
|
686
|
+
async function persistKeyring(adapter, vault, keyring) {
|
|
687
|
+
if (!keyring.kek) {
|
|
688
|
+
throw new ValidationError(
|
|
689
|
+
"persistKeyring: keyring.kek is null \u2014 cannot wrap DEKs without the KEK. This typically means the keyring was opened via tier-3 PIN resume, session restore, or a wrap-DEKs tier-2 unlock. Re-authenticate at tier 1 (passphrase) before persisting."
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
const wrappedDeks = {};
|
|
693
|
+
for (const [collName, dek] of keyring.deks) {
|
|
694
|
+
wrappedDeks[collName] = await wrapKey(dek, keyring.kek);
|
|
695
|
+
}
|
|
696
|
+
const canary = await mintKeyringCanary(keyring.kek);
|
|
697
|
+
const keyringFile = {
|
|
698
|
+
_noydb_keyring: NOYDB_KEYRING_VERSION,
|
|
699
|
+
user_id: keyring.userId,
|
|
700
|
+
display_name: keyring.displayName,
|
|
701
|
+
role: keyring.role,
|
|
702
|
+
permissions: keyring.permissions,
|
|
703
|
+
deks: wrappedDeks,
|
|
704
|
+
salt: bufferToBase64(keyring.salt),
|
|
705
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
706
|
+
granted_by: keyring.userId,
|
|
707
|
+
canary,
|
|
708
|
+
...keyring.exportCapability !== void 0 && { export_capability: keyring.exportCapability },
|
|
709
|
+
...keyring.importCapability !== void 0 && { import_capability: keyring.importCapability },
|
|
710
|
+
...keyring.authenticators.length > 0 && { authenticators: keyring.authenticators },
|
|
711
|
+
...keyring.policy !== void 0 && { policy: keyring.policy }
|
|
712
|
+
};
|
|
713
|
+
await writeKeyringFile(adapter, vault, keyring.userId, keyringFile);
|
|
714
|
+
}
|
|
715
|
+
function defaultBundleCapability(role) {
|
|
716
|
+
return role === "owner" || role === "admin";
|
|
717
|
+
}
|
|
718
|
+
function hasExportCapability(keyring, tier, format) {
|
|
719
|
+
const cap = keyring.exportCapability;
|
|
720
|
+
if (tier === "plaintext") {
|
|
721
|
+
const allowed = cap?.plaintext ?? [];
|
|
722
|
+
return allowed.includes("*") || format !== void 0 && allowed.includes(format);
|
|
723
|
+
}
|
|
724
|
+
return cap?.bundle ?? defaultBundleCapability(keyring.role);
|
|
725
|
+
}
|
|
726
|
+
function evaluateExportCapability(capability, role, tier, format) {
|
|
727
|
+
if (tier === "plaintext") {
|
|
728
|
+
const allowed = capability?.plaintext ?? [];
|
|
729
|
+
return allowed.includes("*") || format !== void 0 && allowed.includes(format);
|
|
730
|
+
}
|
|
731
|
+
return capability?.bundle ?? defaultBundleCapability(role);
|
|
732
|
+
}
|
|
733
|
+
function hasImportCapability(keyring, tier, format) {
|
|
734
|
+
const cap = keyring.importCapability;
|
|
735
|
+
if (tier === "plaintext") {
|
|
736
|
+
const allowed = cap?.plaintext ?? [];
|
|
737
|
+
return allowed.includes("*") || format !== void 0 && allowed.includes(format);
|
|
738
|
+
}
|
|
739
|
+
return cap?.bundle === true;
|
|
740
|
+
}
|
|
741
|
+
function evaluateImportCapability(capability, _role, tier, format) {
|
|
742
|
+
if (tier === "plaintext") {
|
|
743
|
+
const allowed = capability?.plaintext ?? [];
|
|
744
|
+
return allowed.includes("*") || format !== void 0 && allowed.includes(format);
|
|
745
|
+
}
|
|
746
|
+
return capability?.bundle === true;
|
|
747
|
+
}
|
|
748
|
+
function resolvePermissions(role, explicit) {
|
|
749
|
+
if (role === "owner" || role === "admin" || role === "viewer") return {};
|
|
750
|
+
return explicit ?? {};
|
|
751
|
+
}
|
|
752
|
+
async function writeKeyringFile(adapter, vault, userId, keyringFile) {
|
|
753
|
+
const envelope = {
|
|
754
|
+
_noydb: 1,
|
|
755
|
+
_v: 1,
|
|
756
|
+
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
757
|
+
_iv: "",
|
|
758
|
+
_data: JSON.stringify(keyringFile)
|
|
759
|
+
};
|
|
760
|
+
await adapter.put(vault, "_keyring", userId, envelope);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
export {
|
|
764
|
+
WeakPassphraseError,
|
|
765
|
+
validatePassphrase,
|
|
766
|
+
assertStrongPassphrase,
|
|
767
|
+
estimateEntropy,
|
|
768
|
+
USER_ENVELOPE_MAX_BYTES,
|
|
769
|
+
USER_ENVELOPE_COLLECTION,
|
|
770
|
+
UserEnvelopeOversizedError,
|
|
771
|
+
loadUserEnvelope,
|
|
772
|
+
saveUserEnvelope,
|
|
773
|
+
deleteUserEnvelope,
|
|
774
|
+
listUserEnvelopeIds,
|
|
775
|
+
mintKeyringCanary,
|
|
776
|
+
loadKeyring,
|
|
777
|
+
createOwnerKeyring,
|
|
778
|
+
grant,
|
|
779
|
+
revoke,
|
|
780
|
+
updateKeyringIdentity,
|
|
781
|
+
rotateKeys,
|
|
782
|
+
changeSecret,
|
|
783
|
+
buildRecipientKeyringFile,
|
|
784
|
+
listUsers,
|
|
785
|
+
listUsersWithEnvelopes,
|
|
786
|
+
ensureCollectionDEK,
|
|
787
|
+
hasWritePermission,
|
|
788
|
+
hasAccess,
|
|
789
|
+
persistKeyring,
|
|
790
|
+
hasExportCapability,
|
|
791
|
+
evaluateExportCapability,
|
|
792
|
+
hasImportCapability,
|
|
793
|
+
evaluateImportCapability
|
|
794
|
+
};
|
|
795
|
+
//# sourceMappingURL=chunk-GHGXG53C.js.map
|