@noy-db/hub 0.1.0-pre.7 → 0.1.0-pre.8
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/blobs/index.cjs.map +1 -1
- package/dist/blobs/index.d.cts +2 -2
- package/dist/blobs/index.d.ts +2 -2
- package/dist/blobs/index.js +2 -2
- package/dist/bundle/index.d.cts +2 -2
- package/dist/bundle/index.d.ts +2 -2
- package/dist/bundle/index.js +3 -3
- package/dist/{chunk-NZ4XCIKS.js → chunk-2WGMYBYS.js} +3 -3
- package/dist/{chunk-3WCRU7TI.js → chunk-7XBQS42M.js} +2 -2
- package/dist/{chunk-CL37QSND.js → chunk-HC7Z5EQZ.js} +2 -2
- package/dist/{chunk-B6HF6NTZ.js → chunk-PJK6IOBC.js} +1 -1
- package/dist/chunk-PJK6IOBC.js.map +1 -0
- package/dist/{chunk-KPF2HHPI.js → chunk-R2ZTGEVP.js} +2 -2
- package/dist/{chunk-GILMPJXB.js → chunk-RSPLI376.js} +2 -2
- package/dist/{chunk-XCL3WP6J.js → chunk-SCZXXXU4.js} +2 -1
- package/dist/{chunk-XCL3WP6J.js.map → chunk-SCZXXXU4.js.map} +1 -1
- package/dist/{chunk-INSJBB5W.js → chunk-TOQK4KAN.js} +3 -3
- package/dist/{chunk-UFL4DUEV.js → chunk-VQBTTTUN.js} +1 -1
- package/dist/chunk-VQBTTTUN.js.map +1 -0
- package/dist/{chunk-FAAWLVTF.js → chunk-WN6UK7PM.js} +2 -2
- package/dist/{chunk-N2LMZKLR.js → chunk-Y4CMTMUW.js} +2 -2
- package/dist/{chunk-6IJQ27XN.js → chunk-YVFTBQHL.js} +14 -4
- package/dist/chunk-YVFTBQHL.js.map +1 -0
- package/dist/consent/index.d.cts +2 -2
- package/dist/consent/index.d.ts +2 -2
- package/dist/{delegation-XDJCBTI2.js → delegation-2DBS2EOH.js} +2 -2
- package/dist/{dev-unlock-Dk14V6lX.d.cts → dev-unlock-BZKx666y.d.cts} +1 -1
- package/dist/{dev-unlock-CcJ1qIi7.d.ts → dev-unlock-BygpnIWe.d.ts} +1 -1
- package/dist/{hash-1Xsqx1jl.d.ts → hash-B0eU2Qv9.d.ts} +1 -1
- package/dist/{hash-h_2U3TFb.d.cts → hash-CIyfmKsg.d.cts} +1 -1
- package/dist/history/index.cjs.map +1 -1
- package/dist/history/index.d.cts +3 -3
- package/dist/history/index.d.ts +3 -3
- package/dist/history/index.js +2 -2
- package/dist/i18n/index.cjs +11 -0
- package/dist/i18n/index.cjs.map +1 -1
- package/dist/i18n/index.d.cts +2 -2
- package/dist/i18n/index.d.ts +2 -2
- package/dist/i18n/index.js +3 -3
- package/dist/{index-DZn6Yick.d.ts → index-Dp4tKCjX.d.ts} +1 -1
- package/dist/{index-Cvb0efA_.d.cts → index-DsVbTDZI.d.cts} +1 -1
- package/dist/index.cjs +384 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +377 -72
- package/dist/index.js.map +1 -1
- package/dist/{ledger-5V67MAIL.js → ledger-UQIMMKO5.js} +3 -3
- package/dist/periods/index.cjs.map +1 -1
- package/dist/periods/index.d.cts +2 -2
- package/dist/periods/index.d.ts +2 -2
- package/dist/periods/index.js +3 -3
- package/dist/{public-envelope-DFJZHXVH.js → public-envelope-3QTQADDW.js} +3 -3
- package/dist/session/index.cjs.map +1 -1
- package/dist/session/index.d.cts +3 -3
- package/dist/session/index.d.ts +3 -3
- package/dist/session/index.js +1 -1
- package/dist/shadow/index.d.cts +2 -2
- package/dist/shadow/index.d.ts +2 -2
- package/dist/store/index.d.cts +2 -2
- package/dist/store/index.d.ts +2 -2
- package/dist/sync/index.cjs.map +1 -1
- package/dist/sync/index.d.cts +1 -1
- package/dist/sync/index.d.ts +1 -1
- package/dist/sync/index.js +2 -2
- package/dist/team/index.cjs +11 -0
- package/dist/team/index.cjs.map +1 -1
- package/dist/team/index.d.cts +2 -2
- package/dist/team/index.d.ts +2 -2
- package/dist/team/index.js +4 -4
- package/dist/tx/index.d.cts +2 -2
- package/dist/tx/index.d.ts +2 -2
- package/dist/{types-D-6bmD2c.d.ts → types-DD9eKKNc.d.ts} +644 -72
- package/dist/{types-D3QLmhlk.d.cts → types-arFMsCtn.d.cts} +644 -72
- package/package.json +1 -1
- package/dist/chunk-6IJQ27XN.js.map +0 -1
- package/dist/chunk-B6HF6NTZ.js.map +0 -1
- package/dist/chunk-UFL4DUEV.js.map +0 -1
- /package/dist/{chunk-NZ4XCIKS.js.map → chunk-2WGMYBYS.js.map} +0 -0
- /package/dist/{chunk-3WCRU7TI.js.map → chunk-7XBQS42M.js.map} +0 -0
- /package/dist/{chunk-CL37QSND.js.map → chunk-HC7Z5EQZ.js.map} +0 -0
- /package/dist/{chunk-KPF2HHPI.js.map → chunk-R2ZTGEVP.js.map} +0 -0
- /package/dist/{chunk-GILMPJXB.js.map → chunk-RSPLI376.js.map} +0 -0
- /package/dist/{chunk-INSJBB5W.js.map → chunk-TOQK4KAN.js.map} +0 -0
- /package/dist/{chunk-FAAWLVTF.js.map → chunk-WN6UK7PM.js.map} +0 -0
- /package/dist/{chunk-N2LMZKLR.js.map → chunk-Y4CMTMUW.js.map} +0 -0
- /package/dist/{delegation-XDJCBTI2.js.map → delegation-2DBS2EOH.js.map} +0 -0
- /package/dist/{ledger-5V67MAIL.js.map → ledger-UQIMMKO5.js.map} +0 -0
- /package/dist/{public-envelope-DFJZHXVH.js.map → public-envelope-3QTQADDW.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
issueDelegation,
|
|
12
12
|
loadActiveDelegations,
|
|
13
13
|
revokeDelegation
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-SCZXXXU4.js";
|
|
15
15
|
import {
|
|
16
16
|
LazyQuery,
|
|
17
17
|
decodeIdxId,
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
readNoydbBundlePublicEnvelope,
|
|
32
32
|
resetBrotliSupportCache,
|
|
33
33
|
writeNoydbBundle
|
|
34
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-WN6UK7PM.js";
|
|
35
35
|
import {
|
|
36
36
|
PUBLIC_ENVELOPE_RECORD_ID,
|
|
37
37
|
isPublicEnvelope,
|
|
@@ -39,13 +39,13 @@ import {
|
|
|
39
39
|
readPublicEnvelope,
|
|
40
40
|
savePublicEnvelope,
|
|
41
41
|
validatePublicEnvelopeInput
|
|
42
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-RSPLI376.js";
|
|
43
43
|
import {
|
|
44
44
|
CONSENT_AUDIT_COLLECTION
|
|
45
45
|
} from "./chunk-M62XNWRA.js";
|
|
46
46
|
import {
|
|
47
47
|
PERIODS_COLLECTION
|
|
48
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-7XBQS42M.js";
|
|
49
49
|
import "./chunk-UF3BUNQZ.js";
|
|
50
50
|
import {
|
|
51
51
|
CollectionFrame,
|
|
@@ -69,7 +69,7 @@ import {
|
|
|
69
69
|
isI18nTextDescriptor,
|
|
70
70
|
resolveI18nText,
|
|
71
71
|
validateI18nTextValue
|
|
72
|
-
} from "./chunk-
|
|
72
|
+
} from "./chunk-2WGMYBYS.js";
|
|
73
73
|
import {
|
|
74
74
|
createBundleStore,
|
|
75
75
|
routeStore,
|
|
@@ -89,12 +89,12 @@ import {
|
|
|
89
89
|
getCredential,
|
|
90
90
|
listCredentials,
|
|
91
91
|
putCredential
|
|
92
|
-
} from "./chunk-
|
|
92
|
+
} from "./chunk-TOQK4KAN.js";
|
|
93
93
|
import {
|
|
94
94
|
PresenceHandle,
|
|
95
95
|
SyncEngine,
|
|
96
96
|
SyncTransaction
|
|
97
|
-
} from "./chunk-
|
|
97
|
+
} from "./chunk-HC7Z5EQZ.js";
|
|
98
98
|
import {
|
|
99
99
|
USER_ENVELOPE_COLLECTION,
|
|
100
100
|
USER_ENVELOPE_MAX_BYTES,
|
|
@@ -124,7 +124,7 @@ import {
|
|
|
124
124
|
rotateKeys,
|
|
125
125
|
saveUserEnvelope,
|
|
126
126
|
validatePassphrase
|
|
127
|
-
} from "./chunk-
|
|
127
|
+
} from "./chunk-YVFTBQHL.js";
|
|
128
128
|
import {
|
|
129
129
|
BUNDLE_STORE_POLICY,
|
|
130
130
|
INDEXED_STORE_POLICY,
|
|
@@ -144,7 +144,7 @@ import {
|
|
|
144
144
|
revokeAllSessions,
|
|
145
145
|
revokeSession,
|
|
146
146
|
validateSessionPolicy
|
|
147
|
-
} from "./chunk-
|
|
147
|
+
} from "./chunk-VQBTTTUN.js";
|
|
148
148
|
import {
|
|
149
149
|
generateULID,
|
|
150
150
|
isULID
|
|
@@ -161,7 +161,7 @@ import {
|
|
|
161
161
|
LedgerStore,
|
|
162
162
|
applyPatch,
|
|
163
163
|
computePatch
|
|
164
|
-
} from "./chunk-
|
|
164
|
+
} from "./chunk-Y4CMTMUW.js";
|
|
165
165
|
import {
|
|
166
166
|
canonicalJson,
|
|
167
167
|
envelopePayloadHash,
|
|
@@ -216,14 +216,14 @@ import {
|
|
|
216
216
|
detectMimeType,
|
|
217
217
|
isPreCompressed,
|
|
218
218
|
runCompaction
|
|
219
|
-
} from "./chunk-
|
|
219
|
+
} from "./chunk-R2ZTGEVP.js";
|
|
220
220
|
import {
|
|
221
221
|
NOYDB_BACKUP_VERSION,
|
|
222
222
|
NOYDB_FORMAT_VERSION,
|
|
223
223
|
NOYDB_KEYRING_VERSION,
|
|
224
224
|
NOYDB_SYNC_VERSION,
|
|
225
225
|
createStore
|
|
226
|
-
} from "./chunk-
|
|
226
|
+
} from "./chunk-PJK6IOBC.js";
|
|
227
227
|
import {
|
|
228
228
|
base64ToBuffer,
|
|
229
229
|
bufferToBase64,
|
|
@@ -434,14 +434,22 @@ async function enrollAuthenticator(store, vault, keyring, options) {
|
|
|
434
434
|
`enrollAuthenticator: slot id "${options.id}" already exists in vault "${vault}". Remove the slot first or pick a unique id.`
|
|
435
435
|
);
|
|
436
436
|
}
|
|
437
|
-
const
|
|
437
|
+
const base = {
|
|
438
438
|
id: options.id,
|
|
439
439
|
method: options.method,
|
|
440
440
|
enrolled_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
441
441
|
enrolled_via_tier: options.enrolled_via_tier ?? 1,
|
|
442
|
-
wrapped_kek: options.wrapped_kek,
|
|
443
442
|
meta: options.meta
|
|
444
443
|
};
|
|
444
|
+
const slot = options.wrapKind === "deks" ? {
|
|
445
|
+
...base,
|
|
446
|
+
wrapKind: "deks",
|
|
447
|
+
wrapped_deks: options.wrapped_deks,
|
|
448
|
+
iv: options.iv
|
|
449
|
+
} : {
|
|
450
|
+
...base,
|
|
451
|
+
wrapped_kek: options.wrapped_kek
|
|
452
|
+
};
|
|
445
453
|
const next = appendSlot(keyring, slot);
|
|
446
454
|
await persistKeyring(store, vault, next);
|
|
447
455
|
return next;
|
|
@@ -545,51 +553,38 @@ var RecoveryProfileNotImplementedError = class extends NoydbError {
|
|
|
545
553
|
}
|
|
546
554
|
};
|
|
547
555
|
|
|
548
|
-
// src/team/
|
|
549
|
-
var
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
556
|
+
// src/team/wrapped-deks.ts
|
|
557
|
+
var PBKDF2_ITERATIONS = 6e5;
|
|
558
|
+
var SALT_BYTES = 32;
|
|
559
|
+
var IV_BYTES = 12;
|
|
560
|
+
var subtle = globalThis.crypto.subtle;
|
|
561
|
+
async function mintWrappedDeksBlob(deks, credential) {
|
|
562
|
+
const salt = crypto.getRandomValues(new Uint8Array(SALT_BYTES));
|
|
563
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
|
|
564
|
+
const wrappingKey = await deriveWrappingKey(credential, salt);
|
|
565
|
+
const exported = {};
|
|
566
|
+
for (const [coll, dek] of deks) {
|
|
567
|
+
const raw = await subtle.exportKey("raw", dek);
|
|
568
|
+
exported[coll] = bytesToBase64(new Uint8Array(raw));
|
|
559
569
|
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
571
|
-
_iv: "",
|
|
572
|
-
_data: JSON.stringify(doc)
|
|
570
|
+
const plaintext = new TextEncoder().encode(JSON.stringify({ deks: exported }));
|
|
571
|
+
const ciphertext = await subtle.encrypt(
|
|
572
|
+
{ name: "AES-GCM", iv },
|
|
573
|
+
wrappingKey,
|
|
574
|
+
plaintext
|
|
575
|
+
);
|
|
576
|
+
return {
|
|
577
|
+
salt: bytesToBase64(salt),
|
|
578
|
+
iv: bytesToBase64(iv),
|
|
579
|
+
wrappedDeks: bytesToBase64(new Uint8Array(ciphertext))
|
|
573
580
|
};
|
|
574
|
-
await store.put(vault, "_meta", PAPER_DOC_ID, envelope);
|
|
575
|
-
}
|
|
576
|
-
async function burnPaperRecoveryEntry(store, vault, codeId) {
|
|
577
|
-
const entries = await loadPaperRecoveryEntries(store, vault);
|
|
578
|
-
const remaining = entries.filter((e) => e.codeId !== codeId);
|
|
579
|
-
await savePaperRecoveryEntries(store, vault, remaining);
|
|
580
|
-
}
|
|
581
|
-
async function hasRecoveryEnrolled(store, vault) {
|
|
582
|
-
const paper = await loadPaperRecoveryEntries(store, vault);
|
|
583
|
-
return paper.length > 0;
|
|
584
581
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
async function unwrapDeksFromPaperEntry(entry, code) {
|
|
588
|
-
const wrappingKey = await deriveRecoveryWrappingKey(code, base64ToBytes(entry.salt));
|
|
582
|
+
async function unwrapDeksFromBlob(blob, credential) {
|
|
583
|
+
const wrappingKey = await deriveWrappingKey(credential, base64ToBytes(blob.salt));
|
|
589
584
|
const plaintext = await subtle.decrypt(
|
|
590
|
-
{ name: "AES-GCM", iv: base64ToBytes(
|
|
585
|
+
{ name: "AES-GCM", iv: base64ToBytes(blob.iv) },
|
|
591
586
|
wrappingKey,
|
|
592
|
-
base64ToBytes(
|
|
587
|
+
base64ToBytes(blob.wrappedDeks)
|
|
593
588
|
);
|
|
594
589
|
const parsed = JSON.parse(new TextDecoder().decode(plaintext));
|
|
595
590
|
const deks = /* @__PURE__ */ new Map();
|
|
@@ -606,10 +601,10 @@ async function unwrapDeksFromPaperEntry(entry, code) {
|
|
|
606
601
|
}
|
|
607
602
|
return deks;
|
|
608
603
|
}
|
|
609
|
-
async function
|
|
604
|
+
async function deriveWrappingKey(credential, salt) {
|
|
610
605
|
const ikm = await subtle.importKey(
|
|
611
606
|
"raw",
|
|
612
|
-
new TextEncoder().encode(
|
|
607
|
+
new TextEncoder().encode(credential),
|
|
613
608
|
"PBKDF2",
|
|
614
609
|
false,
|
|
615
610
|
["deriveKey"]
|
|
@@ -618,7 +613,7 @@ async function deriveRecoveryWrappingKey(code, salt) {
|
|
|
618
613
|
{
|
|
619
614
|
name: "PBKDF2",
|
|
620
615
|
salt,
|
|
621
|
-
iterations:
|
|
616
|
+
iterations: PBKDF2_ITERATIONS,
|
|
622
617
|
hash: "SHA-256"
|
|
623
618
|
},
|
|
624
619
|
ikm,
|
|
@@ -627,6 +622,11 @@ async function deriveRecoveryWrappingKey(code, salt) {
|
|
|
627
622
|
["encrypt", "decrypt"]
|
|
628
623
|
);
|
|
629
624
|
}
|
|
625
|
+
function bytesToBase64(b) {
|
|
626
|
+
let s = "";
|
|
627
|
+
for (const x of b) s += String.fromCharCode(x);
|
|
628
|
+
return btoa(s);
|
|
629
|
+
}
|
|
630
630
|
function base64ToBytes(b64) {
|
|
631
631
|
const s = atob(b64);
|
|
632
632
|
const out = new Uint8Array(s.length);
|
|
@@ -634,6 +634,55 @@ function base64ToBytes(b64) {
|
|
|
634
634
|
return out;
|
|
635
635
|
}
|
|
636
636
|
|
|
637
|
+
// src/team/recovery.ts
|
|
638
|
+
var PAPER_DOC_ID = "recovery-paper";
|
|
639
|
+
async function loadPaperRecoveryEntries(store, vault) {
|
|
640
|
+
const env = await store.get(vault, "_meta", PAPER_DOC_ID);
|
|
641
|
+
if (!env) return [];
|
|
642
|
+
try {
|
|
643
|
+
const doc = JSON.parse(env._data);
|
|
644
|
+
if (doc.profile !== "paper" || !Array.isArray(doc.entries)) return [];
|
|
645
|
+
return doc.entries;
|
|
646
|
+
} catch {
|
|
647
|
+
return [];
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
async function savePaperRecoveryEntries(store, vault, entries) {
|
|
651
|
+
const doc = {
|
|
652
|
+
_noydb_recovery: 1,
|
|
653
|
+
profile: "paper",
|
|
654
|
+
entries
|
|
655
|
+
};
|
|
656
|
+
const envelope = {
|
|
657
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
658
|
+
_v: 1,
|
|
659
|
+
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
660
|
+
_iv: "",
|
|
661
|
+
_data: JSON.stringify(doc)
|
|
662
|
+
};
|
|
663
|
+
await store.put(vault, "_meta", PAPER_DOC_ID, envelope);
|
|
664
|
+
}
|
|
665
|
+
async function burnPaperRecoveryEntry(store, vault, codeId) {
|
|
666
|
+
const entries = await loadPaperRecoveryEntries(store, vault);
|
|
667
|
+
const remaining = entries.filter((e) => e.codeId !== codeId);
|
|
668
|
+
await savePaperRecoveryEntries(store, vault, remaining);
|
|
669
|
+
}
|
|
670
|
+
async function hasRecoveryEnrolled(store, vault) {
|
|
671
|
+
const paper = await loadPaperRecoveryEntries(store, vault);
|
|
672
|
+
return paper.length > 0;
|
|
673
|
+
}
|
|
674
|
+
async function mintPaperRecoveryEntry(deks, code, codeId) {
|
|
675
|
+
const blob = await mintWrappedDeksBlob(deks, code);
|
|
676
|
+
return {
|
|
677
|
+
...blob,
|
|
678
|
+
codeId,
|
|
679
|
+
enrolledAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
async function unwrapDeksFromPaperEntry(entry, code) {
|
|
683
|
+
return unwrapDeksFromBlob(entry, code);
|
|
684
|
+
}
|
|
685
|
+
|
|
637
686
|
// src/team/rotate-recover.ts
|
|
638
687
|
async function rotatePassphrase(store, vault, userId, input) {
|
|
639
688
|
if (!input.allowWeakPassphrase) {
|
|
@@ -656,14 +705,53 @@ async function rotatePassphrase(store, vault, userId, input) {
|
|
|
656
705
|
for (const [coll, dek] of deks) {
|
|
657
706
|
wrappedDeks[coll] = await wrapKey(dek, newKek);
|
|
658
707
|
}
|
|
708
|
+
const oldSlots = file.authenticators ?? [];
|
|
709
|
+
const newSlots = [];
|
|
710
|
+
if (input.slotCeremonies && oldSlots.length > 0) {
|
|
711
|
+
for (const oldSlot of oldSlots) {
|
|
712
|
+
const ceremony = input.slotCeremonies[oldSlot.id];
|
|
713
|
+
if (!ceremony) continue;
|
|
714
|
+
const result = await ceremony({ newKek, newDeks: deks, oldSlot });
|
|
715
|
+
if (result.id !== oldSlot.id) {
|
|
716
|
+
throw new ValidationError(
|
|
717
|
+
`slotCeremonies['${oldSlot.id}'] returned id="${result.id}". The id must match the rotated slot \u2014 a ceremony cannot change a slot's identity.`
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
if (result.method !== oldSlot.method) {
|
|
721
|
+
throw new ValidationError(
|
|
722
|
+
`slotCeremonies['${oldSlot.id}'] returned method="${result.method}", expected "${oldSlot.method}". The method must match the rotated slot \u2014 a ceremony cannot change the auth method (e.g. webauthn \u2192 password) under cover of rotation.`
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
const baseFields = {
|
|
726
|
+
id: result.id,
|
|
727
|
+
method: result.method,
|
|
728
|
+
// Preserve original enrolled_at — rotation is rewrapping, not
|
|
729
|
+
// re-enrollment. The slot's enrolment timestamp tracks when
|
|
730
|
+
// the user originally added the slot, not when it was last
|
|
731
|
+
// rewrapped. Forensics consumers reading enrolled_at are
|
|
732
|
+
// tracking the slot's ORIGIN, not its CURRENT wrapping.
|
|
733
|
+
enrolled_at: oldSlot.enrolled_at,
|
|
734
|
+
enrolled_via_tier: result.enrolled_via_tier ?? oldSlot.enrolled_via_tier,
|
|
735
|
+
meta: result.meta
|
|
736
|
+
};
|
|
737
|
+
const newSlot = result.wrapKind === "deks" ? {
|
|
738
|
+
...baseFields,
|
|
739
|
+
wrapKind: "deks",
|
|
740
|
+
wrapped_deks: result.wrapped_deks,
|
|
741
|
+
iv: result.iv
|
|
742
|
+
} : {
|
|
743
|
+
...baseFields,
|
|
744
|
+
wrapped_kek: result.wrapped_kek
|
|
745
|
+
};
|
|
746
|
+
newSlots.push(newSlot);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
659
749
|
const next = {
|
|
660
750
|
...file,
|
|
661
751
|
_noydb_keyring: NOYDB_KEYRING_VERSION,
|
|
662
752
|
deks: wrappedDeks,
|
|
663
753
|
salt: bufferToBase64(newSalt),
|
|
664
|
-
|
|
665
|
-
// re-enrols afterwards via `db.enrollAuthenticator`.
|
|
666
|
-
authenticators: []
|
|
754
|
+
authenticators: newSlots
|
|
667
755
|
};
|
|
668
756
|
await writeKeyringFile(store, vault, userId, next);
|
|
669
757
|
return {
|
|
@@ -674,7 +762,7 @@ async function rotatePassphrase(store, vault, userId, input) {
|
|
|
674
762
|
deks,
|
|
675
763
|
kek: newKek,
|
|
676
764
|
salt: newSalt,
|
|
677
|
-
authenticators:
|
|
765
|
+
authenticators: newSlots,
|
|
678
766
|
...file.export_capability !== void 0 && { exportCapability: file.export_capability },
|
|
679
767
|
...file.import_capability !== void 0 && { importCapability: file.import_capability }
|
|
680
768
|
};
|
|
@@ -1148,6 +1236,70 @@ function sanitizeId(s) {
|
|
|
1148
1236
|
return s.replace(/[^a-zA-Z0-9]/g, "_");
|
|
1149
1237
|
}
|
|
1150
1238
|
|
|
1239
|
+
// src/team/peer-recover.ts
|
|
1240
|
+
var ADMIN_RECOVERABLE_TARGETS = ["operator", "viewer", "client", "admin"];
|
|
1241
|
+
function canRecover(callerRole, targetRole) {
|
|
1242
|
+
if (callerRole === "owner") return true;
|
|
1243
|
+
if (callerRole === "admin") return ADMIN_RECOVERABLE_TARGETS.includes(targetRole);
|
|
1244
|
+
return false;
|
|
1245
|
+
}
|
|
1246
|
+
async function recoverUser(store, vault, callerKeyring, options) {
|
|
1247
|
+
const env = await store.get(vault, "_keyring", options.userId);
|
|
1248
|
+
if (!env) {
|
|
1249
|
+
throw new NoAccessError(
|
|
1250
|
+
`recoverUser: user "${options.userId}" has no keyring in vault "${vault}".`
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
const target = JSON.parse(env._data);
|
|
1254
|
+
const targetRole = options.role ?? target.role;
|
|
1255
|
+
if (!canRecover(callerKeyring.role, targetRole)) {
|
|
1256
|
+
throw new PermissionDeniedError(
|
|
1257
|
+
`Role "${callerKeyring.role}" cannot recover role "${targetRole}"`
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
if (!canRecover(callerKeyring.role, target.role)) {
|
|
1261
|
+
throw new PermissionDeniedError(
|
|
1262
|
+
`Role "${callerKeyring.role}" cannot recover role "${target.role}"`
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
for (const coll of Object.keys(target.deks)) {
|
|
1266
|
+
if (!callerKeyring.deks.has(coll)) {
|
|
1267
|
+
throw new PrivilegeEscalationError(coll);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
if (options.validatePassphrase && !options.allowWeakPassphrase) {
|
|
1271
|
+
assertStrongPassphrase(options.passphrase, options.passphrasePolicy);
|
|
1272
|
+
}
|
|
1273
|
+
const newSalt = generateSalt();
|
|
1274
|
+
const newKek = await deriveKey(options.passphrase, newSalt);
|
|
1275
|
+
const wrappedDeks = {};
|
|
1276
|
+
for (const coll of Object.keys(target.deks)) {
|
|
1277
|
+
const callerDek = callerKeyring.deks.get(coll);
|
|
1278
|
+
if (!callerDek) {
|
|
1279
|
+
throw new PrivilegeEscalationError(coll);
|
|
1280
|
+
}
|
|
1281
|
+
wrappedDeks[coll] = await wrapKey(callerDek, newKek);
|
|
1282
|
+
}
|
|
1283
|
+
const next = {
|
|
1284
|
+
...target,
|
|
1285
|
+
_noydb_keyring: NOYDB_KEYRING_VERSION,
|
|
1286
|
+
role: targetRole,
|
|
1287
|
+
display_name: options.displayName ?? target.display_name,
|
|
1288
|
+
deks: wrappedDeks,
|
|
1289
|
+
salt: bufferToBase64(newSalt),
|
|
1290
|
+
granted_by: callerKeyring.userId,
|
|
1291
|
+
authenticators: []
|
|
1292
|
+
};
|
|
1293
|
+
const envelope = {
|
|
1294
|
+
_noydb: 1,
|
|
1295
|
+
_v: 1,
|
|
1296
|
+
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1297
|
+
_iv: "",
|
|
1298
|
+
_data: JSON.stringify(next)
|
|
1299
|
+
};
|
|
1300
|
+
await store.put(vault, "_keyring", options.userId, envelope);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1151
1303
|
// src/crdt/strategy.ts
|
|
1152
1304
|
var NOT_ENABLED = new Error(
|
|
1153
1305
|
'CRDT mode requires the CRDT strategy. Import `{ withCrdt }` from "@noy-db/hub/crdt" and pass it to `createNoydb({ crdtStrategy: withCrdt() })`.'
|
|
@@ -4804,7 +4956,12 @@ var Vault = class {
|
|
|
4804
4956
|
* collection.
|
|
4805
4957
|
*/
|
|
4806
4958
|
async delegate(opts) {
|
|
4807
|
-
const { issueDelegation: issueDelegation2, DELEGATIONS_COLLECTION: DELEGATIONS_COLLECTION2 } = await import("./delegation-
|
|
4959
|
+
const { issueDelegation: issueDelegation2, DELEGATIONS_COLLECTION: DELEGATIONS_COLLECTION2 } = await import("./delegation-2DBS2EOH.js");
|
|
4960
|
+
if (!this.keyring.kek) {
|
|
4961
|
+
throw new ValidationError(
|
|
4962
|
+
"issueDelegation: keyring.kek is null \u2014 issuing a delegation requires a tier-1 unlock. Re-authenticate at tier 1 (passphrase) first."
|
|
4963
|
+
);
|
|
4964
|
+
}
|
|
4808
4965
|
const targetKek = this.keyring.kek;
|
|
4809
4966
|
const delegationsDek = await this.getDEK(DELEGATIONS_COLLECTION2);
|
|
4810
4967
|
return issueDelegation2(
|
|
@@ -4821,7 +4978,7 @@ var Vault = class {
|
|
|
4821
4978
|
* if the id does not exist.
|
|
4822
4979
|
*/
|
|
4823
4980
|
async revokeDelegation(id) {
|
|
4824
|
-
const { revokeDelegation: revokeDelegation2, DELEGATIONS_COLLECTION: DELEGATIONS_COLLECTION2 } = await import("./delegation-
|
|
4981
|
+
const { revokeDelegation: revokeDelegation2, DELEGATIONS_COLLECTION: DELEGATIONS_COLLECTION2 } = await import("./delegation-2DBS2EOH.js");
|
|
4825
4982
|
await revokeDelegation2(this.adapter, this.name, id);
|
|
4826
4983
|
void DELEGATIONS_COLLECTION2;
|
|
4827
4984
|
}
|
|
@@ -5221,7 +5378,7 @@ var Vault = class {
|
|
|
5221
5378
|
* @see docs/subsystems/public-envelope.md
|
|
5222
5379
|
*/
|
|
5223
5380
|
async getPublicEnvelope(opts = {}) {
|
|
5224
|
-
const { readPublicEnvelope: readPublicEnvelope2 } = await import("./public-envelope-
|
|
5381
|
+
const { readPublicEnvelope: readPublicEnvelope2 } = await import("./public-envelope-3QTQADDW.js");
|
|
5225
5382
|
return readPublicEnvelope2(this.adapter, this.name, opts);
|
|
5226
5383
|
}
|
|
5227
5384
|
/**
|
|
@@ -5795,7 +5952,23 @@ var PERSONAL_POLICY = Object.freeze({
|
|
|
5795
5952
|
gates: {
|
|
5796
5953
|
"rotate-passphrase": {
|
|
5797
5954
|
minTier: 1,
|
|
5798
|
-
|
|
5955
|
+
// Any second factor satisfies the gate — off-device kinds (TOTP,
|
|
5956
|
+
// email-OTP, paper recovery, roaming WebAuthn) are the strongest;
|
|
5957
|
+
// platform-bound kinds (platform WebAuthn, password, PIN) are
|
|
5958
|
+
// accepted because requiring "something off-device" is overkill
|
|
5959
|
+
// for personal/SMB threat models. Consumers needing the off-device
|
|
5960
|
+
// guarantee should use STRICT_POLICY or override this gate.
|
|
5961
|
+
factors: [{
|
|
5962
|
+
anyOf: [
|
|
5963
|
+
"totp",
|
|
5964
|
+
"email-otp",
|
|
5965
|
+
"recovery",
|
|
5966
|
+
"webauthn-roaming",
|
|
5967
|
+
"webauthn-platform",
|
|
5968
|
+
"password",
|
|
5969
|
+
"pin"
|
|
5970
|
+
]
|
|
5971
|
+
}]
|
|
5799
5972
|
},
|
|
5800
5973
|
"recover-passphrase": {
|
|
5801
5974
|
minTier: 1,
|
|
@@ -5806,6 +5979,12 @@ var PERSONAL_POLICY = Object.freeze({
|
|
|
5806
5979
|
"rotate-unlock": { minTier: 2 },
|
|
5807
5980
|
"enroll-user": { minTier: 1 },
|
|
5808
5981
|
"revoke-user": { minTier: 1 },
|
|
5982
|
+
// Peer-recovery is a high-trust intentional op — co-owners
|
|
5983
|
+
// recovering each other should not need an off-device factor in
|
|
5984
|
+
// the personal/SMB threat model (the partner is already vetted by
|
|
5985
|
+
// virtue of being a co-owner). Tier-1 unlock is the floor; the
|
|
5986
|
+
// STRICT preset adds a recovery/email-OTP requirement.
|
|
5987
|
+
"peer-recover-user": { minTier: 1 },
|
|
5809
5988
|
"export-bundle": { minTier: 1 },
|
|
5810
5989
|
"export-plaintext": {
|
|
5811
5990
|
minTier: 1,
|
|
@@ -5859,6 +6038,19 @@ var STRICT_POLICY = Object.freeze({
|
|
|
5859
6038
|
minTier: 1,
|
|
5860
6039
|
factors: [{ anyOf: ["totp", "email-otp"] }]
|
|
5861
6040
|
},
|
|
6041
|
+
// STRICT peer-recovery: the issuer must present a recovery code
|
|
6042
|
+
// OR a fresh off-device second factor at the moment of recovery.
|
|
6043
|
+
// This binds the high-trust operation to a verifiable proof
|
|
6044
|
+
// (recovery sheet photographed by an attacker won't suffice —
|
|
6045
|
+
// they'd also need tier-1 unlock first; this gate is the freshness
|
|
6046
|
+
// binding on top). Roaming WebAuthn (YubiKey-class hardware key)
|
|
6047
|
+
// accepted; platform-bound kinds (Touch ID, password, PIN)
|
|
6048
|
+
// intentionally excluded under STRICT because they don't survive
|
|
6049
|
+
// device theft — the off-device requirement is the whole point.
|
|
6050
|
+
"peer-recover-user": {
|
|
6051
|
+
minTier: 1,
|
|
6052
|
+
factors: [{ anyOf: ["recovery", "totp", "email-otp", "webauthn-roaming"] }]
|
|
6053
|
+
},
|
|
5862
6054
|
"export-bundle": {
|
|
5863
6055
|
minTier: 1,
|
|
5864
6056
|
factors: [{ anyOf: ["totp", "email-otp"] }],
|
|
@@ -7005,22 +7197,108 @@ var Noydb = class {
|
|
|
7005
7197
|
async recoverPassphrase(vault, input, factors) {
|
|
7006
7198
|
await this.checkGate(vault, "recover-passphrase", factors);
|
|
7007
7199
|
const userId = this.options.user;
|
|
7200
|
+
const entriesBeforeRecovery = await loadPaperRecoveryEntries(this.options.store, vault);
|
|
7008
7201
|
const next = await recoverPassphrase(this.options.store, vault, userId, input);
|
|
7009
7202
|
this.keyringCache.set(vault, next);
|
|
7203
|
+
const rotateRemaining = input.rotateRemainingCodes ?? true;
|
|
7204
|
+
const remainingAfterBurn = Math.max(0, entriesBeforeRecovery.length - 1);
|
|
7205
|
+
if (!rotateRemaining || remainingAfterBurn === 0) {
|
|
7206
|
+
return { newCodes: [] };
|
|
7207
|
+
}
|
|
7208
|
+
const codeGen = input.codeGenerator ?? generateULID;
|
|
7209
|
+
const newCodeCount = input.newCodeCount ?? remainingAfterBurn;
|
|
7210
|
+
const codes = [];
|
|
7211
|
+
const newEntries = [];
|
|
7212
|
+
for (let i = 0; i < newCodeCount; i++) {
|
|
7213
|
+
const rawCode = codeGen();
|
|
7214
|
+
const entry = await mintPaperRecoveryEntry(next.deks, rawCode, generateULID());
|
|
7215
|
+
codes.push(rawCode);
|
|
7216
|
+
newEntries.push(entry);
|
|
7217
|
+
}
|
|
7218
|
+
await savePaperRecoveryEntries(this.options.store, vault, newEntries);
|
|
7219
|
+
return { newCodes: codes };
|
|
7220
|
+
}
|
|
7221
|
+
/**
|
|
7222
|
+
* Atomic peer-recovery — re-wraps an EXISTING user's keyring under
|
|
7223
|
+
* a fresh temp passphrase in a single store write. Closes #34's
|
|
7224
|
+
* partial-failure window (the previous compose-from-primitives
|
|
7225
|
+
* pattern was `db.revoke + db.grant`, two writes — if the issuer
|
|
7226
|
+
* cancelled between them the target was locked out entirely).
|
|
7227
|
+
*
|
|
7228
|
+
* Different from `db.revoke + db.grant`:
|
|
7229
|
+
*
|
|
7230
|
+
* - Same `userId`, role, permissions, capabilities preserved.
|
|
7231
|
+
* - DEKs unchanged → every other principal in the vault keeps
|
|
7232
|
+
* access. No key rotation.
|
|
7233
|
+
* - Allows owner→owner natively (#33). The existing
|
|
7234
|
+
* `db.revoke` retains its block — peer-recovery is a separate,
|
|
7235
|
+
* intentionally-named operation.
|
|
7236
|
+
* - Tier-2 slots dropped (they wrap the old KEK).
|
|
7237
|
+
*
|
|
7238
|
+
* Gated by `peer-recover-user`; `STRICT_POLICY` requires a
|
|
7239
|
+
* recovery / TOTP / email-OTP factor proof at the moment of
|
|
7240
|
+
* recovery, so the issuer affirmatively re-asserts identity.
|
|
7241
|
+
*
|
|
7242
|
+
* The recipient should call `db.rotatePassphrase` on first session
|
|
7243
|
+
* to choose their own phrase — the temp acts as a single-use
|
|
7244
|
+
* bridge.
|
|
7245
|
+
*
|
|
7246
|
+
* ```ts
|
|
7247
|
+
* await db.recoverUser('acme', {
|
|
7248
|
+
* userId: 'bob',
|
|
7249
|
+
* passphrase: 'temporary-correct-horse-battery-staple-printer',
|
|
7250
|
+
* }, { factors: [{ kind: 'recovery' }] })
|
|
7251
|
+
* // Bob opens createNoydb({ user: 'bob', secret: tempPhrase })
|
|
7252
|
+
* // and immediately calls db.rotatePassphrase to set his own.
|
|
7253
|
+
* ```
|
|
7254
|
+
*
|
|
7255
|
+
* @throws `NoAccessError` when no keyring exists for the target.
|
|
7256
|
+
* @throws `PermissionDeniedError` when the caller's role can't
|
|
7257
|
+
* recover the target's role (admin→owner is blocked even
|
|
7258
|
+
* under recovery).
|
|
7259
|
+
* @throws `PrivilegeEscalationError` when the caller lacks a DEK
|
|
7260
|
+
* the target previously had access to.
|
|
7261
|
+
*
|
|
7262
|
+
* @see #33 #34 — the issues this method closes.
|
|
7263
|
+
*/
|
|
7264
|
+
async recoverUser(vault, options, factors) {
|
|
7265
|
+
await this.checkGate(vault, "peer-recover-user", factors);
|
|
7266
|
+
const callerKeyring = await this.getKeyring(vault);
|
|
7267
|
+
await recoverUser(this.options.store, vault, callerKeyring, options);
|
|
7268
|
+
if (options.userId === this.options.user) {
|
|
7269
|
+
this.keyringCache.delete(vault);
|
|
7270
|
+
}
|
|
7010
7271
|
}
|
|
7011
7272
|
/**
|
|
7012
7273
|
* Persist a recovery enrollment. v0.1.0-pre.5 accepts the `'paper'`
|
|
7013
|
-
* profile
|
|
7014
|
-
*
|
|
7015
|
-
*
|
|
7016
|
-
*
|
|
7274
|
+
* profile.
|
|
7275
|
+
*
|
|
7276
|
+
* The hub wraps the user's DEK set (not the KEK) under a code-derived
|
|
7277
|
+
* AES-GCM key — see `team/recovery.ts` for the rationale. The mint
|
|
7278
|
+
* helper {@link mintPaperRecoveryEntry} is the canonical primitive;
|
|
7279
|
+
* pair it with `db.getKeyring(vault)` to obtain the live DEK set:
|
|
7017
7280
|
*
|
|
7018
7281
|
* ```ts
|
|
7019
|
-
* import {
|
|
7020
|
-
*
|
|
7282
|
+
* import { mintPaperRecoveryEntry } from '@noy-db/hub'
|
|
7283
|
+
*
|
|
7284
|
+
* const keyring = await db.getKeyring('acme')
|
|
7285
|
+
* const codes: string[] = ['CORRECT-HORSE-1', 'BATTERY-STAPLE-2', ...]
|
|
7286
|
+
* const entries = await Promise.all(
|
|
7287
|
+
* codes.map((code, i) => mintPaperRecoveryEntry(keyring.deks, code, `code-${i}`)),
|
|
7288
|
+
* )
|
|
7021
7289
|
* await db.enrollRecovery('acme', { profile: 'paper', entries })
|
|
7022
7290
|
* showCodesToUser(codes)
|
|
7023
7291
|
* ```
|
|
7292
|
+
*
|
|
7293
|
+
* As of pre.8, `@noy-db/on-recovery`'s `generateRecoveryCodeSet`
|
|
7294
|
+
* delegates to `mintPaperRecoveryEntry` internally — its output is
|
|
7295
|
+
* fed directly to this API. Pick whichever fits your code-gen layer:
|
|
7296
|
+
*
|
|
7297
|
+
* ```ts
|
|
7298
|
+
* import { generateRecoveryCodeSet } from '@noy-db/on-recovery'
|
|
7299
|
+
* const { codes, entries } = await generateRecoveryCodeSet({ deks: keyring.deks, count: 8 })
|
|
7300
|
+
* await db.enrollRecovery('acme', { profile: 'paper', entries })
|
|
7301
|
+
* ```
|
|
7024
7302
|
*/
|
|
7025
7303
|
async enrollRecovery(vault, enrollment) {
|
|
7026
7304
|
if (enrollment.profile !== "paper") {
|
|
@@ -7074,7 +7352,29 @@ var Noydb = class {
|
|
|
7074
7352
|
clearQuickUnlock(vault) {
|
|
7075
7353
|
this.quickUnlock.delete(vault);
|
|
7076
7354
|
}
|
|
7077
|
-
/**
|
|
7355
|
+
/**
|
|
7356
|
+
* Public accessor for the unlocked keyring of a vault — issue #28.
|
|
7357
|
+
*
|
|
7358
|
+
* Returns the cached `UnlockedKeyring` (already in memory after
|
|
7359
|
+
* `createNoydb` + first vault touch); loads it on demand if absent.
|
|
7360
|
+
* Used by `@noy-db/on-*` ceremonies that need the live DEK set
|
|
7361
|
+
* (paper recovery via {@link mintPaperRecoveryEntry}, tier-3 PIN
|
|
7362
|
+
* enrolment via on-pin's `enrollPin`, custom on-* ceremonies that
|
|
7363
|
+
* don't have a hub-side wrapper).
|
|
7364
|
+
*
|
|
7365
|
+
* No new permission gate — this is an accessor over already-unlocked
|
|
7366
|
+
* state. The keyring is materialized only after the calling session
|
|
7367
|
+
* has unlocked the vault at tier 1, 2, or 3, so exposing it does not
|
|
7368
|
+
* widen access. Throws `ValidationError` when encryption is enabled
|
|
7369
|
+
* and no `secret` / `getKeyring` is configured.
|
|
7370
|
+
*
|
|
7371
|
+
* ```ts
|
|
7372
|
+
* const keyring = await db.getKeyring('acme')
|
|
7373
|
+
* // keyring.deks: Map<collection, CryptoKey>
|
|
7374
|
+
* // keyring.kek: CryptoKey (non-extractable; null for tier-3 sessions)
|
|
7375
|
+
* // keyring.role / .permissions / .authenticators
|
|
7376
|
+
* ```
|
|
7377
|
+
*/
|
|
7078
7378
|
async getKeyring(vault) {
|
|
7079
7379
|
if (this.options.encrypt === false) {
|
|
7080
7380
|
return createPlaintextKeyring(this.options.user);
|
|
@@ -7503,6 +7803,8 @@ export {
|
|
|
7503
7803
|
mergeCrdtStates,
|
|
7504
7804
|
mergePolicy,
|
|
7505
7805
|
min,
|
|
7806
|
+
mintPaperRecoveryEntry,
|
|
7807
|
+
mintWrappedDeksBlob,
|
|
7506
7808
|
paddedIndex,
|
|
7507
7809
|
parseBytes,
|
|
7508
7810
|
parseIndex,
|
|
@@ -7513,6 +7815,7 @@ export {
|
|
|
7513
7815
|
readNoydbBundlePublicEnvelope,
|
|
7514
7816
|
readPath,
|
|
7515
7817
|
readPublicEnvelope,
|
|
7818
|
+
recoverUser,
|
|
7516
7819
|
reduceRecords,
|
|
7517
7820
|
ref,
|
|
7518
7821
|
removeAuthenticator,
|
|
@@ -7534,6 +7837,8 @@ export {
|
|
|
7534
7837
|
saveVaultPolicy,
|
|
7535
7838
|
sha256Hex,
|
|
7536
7839
|
sum,
|
|
7840
|
+
unwrapDeksFromBlob,
|
|
7841
|
+
unwrapDeksFromPaperEntry,
|
|
7537
7842
|
unwrapMagicLinkGrant,
|
|
7538
7843
|
validateI18nTextValue,
|
|
7539
7844
|
validatePassphrase,
|