@noy-db/hub 0.1.0-pre.5 → 0.1.0-pre.7
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-QUDXYI4W.js → chunk-3WCRU7TI.js} +2 -2
- package/dist/{chunk-6NPQTBZN.js → chunk-6IJQ27XN.js} +112 -4
- package/dist/chunk-6IJQ27XN.js.map +1 -0
- package/dist/{chunk-NXUVITPB.js → chunk-B6HF6NTZ.js} +1 -1
- package/dist/{chunk-NXUVITPB.js.map → chunk-B6HF6NTZ.js.map} +1 -1
- package/dist/{chunk-H3DV46AQ.js → chunk-CL37QSND.js} +2 -2
- package/dist/{chunk-LRN3PNI6.js → chunk-FAAWLVTF.js} +2 -2
- package/dist/{chunk-UQQ2XFXI.js → chunk-GILMPJXB.js} +2 -2
- package/dist/{chunk-MIRZMUSQ.js → chunk-INSJBB5W.js} +3 -3
- package/dist/{chunk-E4OOAPBZ.js → chunk-KPF2HHPI.js} +2 -2
- package/dist/{chunk-LMKOSLJY.js → chunk-N2LMZKLR.js} +2 -2
- package/dist/{chunk-QV4WLLKB.js → chunk-NZ4XCIKS.js} +3 -3
- package/dist/consent/index.d.cts +2 -2
- package/dist/consent/index.d.ts +2 -2
- package/dist/{dev-unlock-BgFqShBi.d.ts → dev-unlock-CcJ1qIi7.d.ts} +1 -1
- package/dist/{dev-unlock-qVMxG2Je.d.cts → dev-unlock-Dk14V6lX.d.cts} +1 -1
- package/dist/{hash-BhoL7iUE.d.ts → hash-1Xsqx1jl.d.ts} +1 -1
- package/dist/{hash-Bpvl2eSe.d.cts → hash-h_2U3TFb.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.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-DyRt_5vM.d.cts → index-Cvb0efA_.d.cts} +1 -1
- package/dist/{index-DhK_zqOO.d.ts → index-DZn6Yick.d.ts} +1 -1
- package/dist/index.cjs +505 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -6
- package/dist/index.d.ts +60 -6
- package/dist/index.js +409 -13
- package/dist/index.js.map +1 -1
- package/dist/{ledger-GA4DMJS6.js → ledger-5V67MAIL.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-R4EIEQE6.js → public-envelope-DFJZHXVH.js} +3 -3
- package/dist/session/index.d.cts +3 -3
- package/dist/session/index.d.ts +3 -3
- 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.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-Df72wWCC.d.ts → types-D-6bmD2c.d.ts} +378 -2
- package/dist/{types-BpyE4o_n.d.cts → types-D3QLmhlk.d.cts} +378 -2
- package/package.json +1 -1
- package/dist/chunk-6NPQTBZN.js.map +0 -1
- /package/dist/{chunk-QUDXYI4W.js.map → chunk-3WCRU7TI.js.map} +0 -0
- /package/dist/{chunk-H3DV46AQ.js.map → chunk-CL37QSND.js.map} +0 -0
- /package/dist/{chunk-LRN3PNI6.js.map → chunk-FAAWLVTF.js.map} +0 -0
- /package/dist/{chunk-UQQ2XFXI.js.map → chunk-GILMPJXB.js.map} +0 -0
- /package/dist/{chunk-MIRZMUSQ.js.map → chunk-INSJBB5W.js.map} +0 -0
- /package/dist/{chunk-E4OOAPBZ.js.map → chunk-KPF2HHPI.js.map} +0 -0
- /package/dist/{chunk-LMKOSLJY.js.map → chunk-N2LMZKLR.js.map} +0 -0
- /package/dist/{chunk-QV4WLLKB.js.map → chunk-NZ4XCIKS.js.map} +0 -0
- /package/dist/{ledger-GA4DMJS6.js.map → ledger-5V67MAIL.js.map} +0 -0
- /package/dist/{public-envelope-R4EIEQE6.js.map → public-envelope-DFJZHXVH.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
readNoydbBundlePublicEnvelope,
|
|
32
32
|
resetBrotliSupportCache,
|
|
33
33
|
writeNoydbBundle
|
|
34
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-FAAWLVTF.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-GILMPJXB.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-3WCRU7TI.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-NZ4XCIKS.js";
|
|
73
73
|
import {
|
|
74
74
|
createBundleStore,
|
|
75
75
|
routeStore,
|
|
@@ -89,18 +89,22 @@ import {
|
|
|
89
89
|
getCredential,
|
|
90
90
|
listCredentials,
|
|
91
91
|
putCredential
|
|
92
|
-
} from "./chunk-
|
|
92
|
+
} from "./chunk-INSJBB5W.js";
|
|
93
93
|
import {
|
|
94
94
|
PresenceHandle,
|
|
95
95
|
SyncEngine,
|
|
96
96
|
SyncTransaction
|
|
97
|
-
} from "./chunk-
|
|
97
|
+
} from "./chunk-CL37QSND.js";
|
|
98
98
|
import {
|
|
99
|
+
USER_ENVELOPE_COLLECTION,
|
|
100
|
+
USER_ENVELOPE_MAX_BYTES,
|
|
101
|
+
UserEnvelopeOversizedError,
|
|
99
102
|
WeakPassphraseError,
|
|
100
103
|
assertStrongPassphrase,
|
|
101
104
|
buildRecipientKeyringFile,
|
|
102
105
|
changeSecret,
|
|
103
106
|
createOwnerKeyring,
|
|
107
|
+
deleteUserEnvelope,
|
|
104
108
|
ensureCollectionDEK,
|
|
105
109
|
estimateEntropy,
|
|
106
110
|
evaluateExportCapability,
|
|
@@ -110,13 +114,17 @@ import {
|
|
|
110
114
|
hasExportCapability,
|
|
111
115
|
hasImportCapability,
|
|
112
116
|
hasWritePermission,
|
|
117
|
+
listUserEnvelopeIds,
|
|
113
118
|
listUsers,
|
|
119
|
+
listUsersWithEnvelopes,
|
|
114
120
|
loadKeyring,
|
|
121
|
+
loadUserEnvelope,
|
|
115
122
|
persistKeyring,
|
|
116
123
|
revoke,
|
|
117
124
|
rotateKeys,
|
|
125
|
+
saveUserEnvelope,
|
|
118
126
|
validatePassphrase
|
|
119
|
-
} from "./chunk-
|
|
127
|
+
} from "./chunk-6IJQ27XN.js";
|
|
120
128
|
import {
|
|
121
129
|
BUNDLE_STORE_POLICY,
|
|
122
130
|
INDEXED_STORE_POLICY,
|
|
@@ -153,7 +161,7 @@ import {
|
|
|
153
161
|
LedgerStore,
|
|
154
162
|
applyPatch,
|
|
155
163
|
computePatch
|
|
156
|
-
} from "./chunk-
|
|
164
|
+
} from "./chunk-N2LMZKLR.js";
|
|
157
165
|
import {
|
|
158
166
|
canonicalJson,
|
|
159
167
|
envelopePayloadHash,
|
|
@@ -208,14 +216,14 @@ import {
|
|
|
208
216
|
detectMimeType,
|
|
209
217
|
isPreCompressed,
|
|
210
218
|
runCompaction
|
|
211
|
-
} from "./chunk-
|
|
219
|
+
} from "./chunk-KPF2HHPI.js";
|
|
212
220
|
import {
|
|
213
221
|
NOYDB_BACKUP_VERSION,
|
|
214
222
|
NOYDB_FORMAT_VERSION,
|
|
215
223
|
NOYDB_KEYRING_VERSION,
|
|
216
224
|
NOYDB_SYNC_VERSION,
|
|
217
225
|
createStore
|
|
218
|
-
} from "./chunk-
|
|
226
|
+
} from "./chunk-B6HF6NTZ.js";
|
|
219
227
|
import {
|
|
220
228
|
base64ToBuffer,
|
|
221
229
|
bufferToBase64,
|
|
@@ -772,6 +780,220 @@ async function writeKeyringFile(store, vault, userId, file) {
|
|
|
772
780
|
await store.put(vault, "_keyring", userId, envelope);
|
|
773
781
|
}
|
|
774
782
|
|
|
783
|
+
// src/meta/user-envelope/api.ts
|
|
784
|
+
var UserApi = class {
|
|
785
|
+
constructor(adapter, vaultName, writerKeyringId, getDek, checkGate2) {
|
|
786
|
+
this.adapter = adapter;
|
|
787
|
+
this.vaultName = vaultName;
|
|
788
|
+
this.writerKeyringId = writerKeyringId;
|
|
789
|
+
this.getDek = getDek;
|
|
790
|
+
this.checkGate = checkGate2;
|
|
791
|
+
}
|
|
792
|
+
adapter;
|
|
793
|
+
vaultName;
|
|
794
|
+
writerKeyringId;
|
|
795
|
+
getDek;
|
|
796
|
+
checkGate;
|
|
797
|
+
/** keyringId → set of listeners. Wildcard '*' fires on every change. */
|
|
798
|
+
listeners = /* @__PURE__ */ new Map();
|
|
799
|
+
// ─── Write-self ──────────────────────────────────────────────────────
|
|
800
|
+
/** Read the writer's own envelope. Returns null if never written. */
|
|
801
|
+
async me() {
|
|
802
|
+
const dek = await this.getDek();
|
|
803
|
+
return loadUserEnvelope(this.adapter, this.vaultName, this.writerKeyringId, dek);
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Deep-merge a partial patch into the writer's own envelope. Creates
|
|
807
|
+
* the envelope on first call. Optimistic-concurrency safe — a stale
|
|
808
|
+
* `_v` (parallel writer on another device) throws `ConflictError`.
|
|
809
|
+
*
|
|
810
|
+
* Gated by the `edit-own-profile` policy gate (default `minTier: 3`).
|
|
811
|
+
* Pass `presented` to satisfy tightened policies that require a
|
|
812
|
+
* factor proof (e.g. STRICT_POLICY's TOTP requirement).
|
|
813
|
+
*/
|
|
814
|
+
async updateMe(patch, presented) {
|
|
815
|
+
if (this.checkGate) await this.checkGate("edit-own-profile", presented);
|
|
816
|
+
const dek = await this.getDek();
|
|
817
|
+
const current = await loadUserEnvelope(
|
|
818
|
+
this.adapter,
|
|
819
|
+
this.vaultName,
|
|
820
|
+
this.writerKeyringId,
|
|
821
|
+
dek
|
|
822
|
+
);
|
|
823
|
+
const merged = current ? deepMerge(current.data, patch) : patch;
|
|
824
|
+
const written = await saveUserEnvelope(
|
|
825
|
+
this.adapter,
|
|
826
|
+
this.vaultName,
|
|
827
|
+
this.writerKeyringId,
|
|
828
|
+
merged,
|
|
829
|
+
dek,
|
|
830
|
+
current?._v ?? 0
|
|
831
|
+
);
|
|
832
|
+
this.fireChange(this.writerKeyringId, written);
|
|
833
|
+
return written;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Replace the writer's own envelope with `payload`. Use sparingly —
|
|
837
|
+
* `updateMe` is the canonical mutation. No `expectedVersion` check;
|
|
838
|
+
* callers explicitly take last-write-wins semantics.
|
|
839
|
+
*
|
|
840
|
+
* Gated by `edit-own-profile`. See `updateMe` for `presented` usage.
|
|
841
|
+
*/
|
|
842
|
+
async setMe(payload, presented) {
|
|
843
|
+
if (this.checkGate) await this.checkGate("edit-own-profile", presented);
|
|
844
|
+
const dek = await this.getDek();
|
|
845
|
+
const written = await saveUserEnvelope(
|
|
846
|
+
this.adapter,
|
|
847
|
+
this.vaultName,
|
|
848
|
+
this.writerKeyringId,
|
|
849
|
+
payload,
|
|
850
|
+
dek
|
|
851
|
+
);
|
|
852
|
+
this.fireChange(this.writerKeyringId, written);
|
|
853
|
+
return written;
|
|
854
|
+
}
|
|
855
|
+
// ─── Read-anyone ─────────────────────────────────────────────────────
|
|
856
|
+
/**
|
|
857
|
+
* Read another principal's envelope by their keyringId. Returns null
|
|
858
|
+
* if the principal exists but has no envelope yet, or if the
|
|
859
|
+
* keyringId does not exist at all.
|
|
860
|
+
*
|
|
861
|
+
* Gated by `view-team-profiles` (default `minTier: 2`) — but ONLY for
|
|
862
|
+
* cross-principal reads. Reading your own envelope (`keyringId ===
|
|
863
|
+
* self`) is never gated; that's just `me()` written long-form.
|
|
864
|
+
*/
|
|
865
|
+
async get(keyringId, presented) {
|
|
866
|
+
if (this.checkGate && keyringId !== this.writerKeyringId) {
|
|
867
|
+
await this.checkGate("view-team-profiles", presented);
|
|
868
|
+
}
|
|
869
|
+
const dek = await this.getDek();
|
|
870
|
+
return loadUserEnvelope(this.adapter, this.vaultName, keyringId, dek);
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Read every persisted envelope in the vault. Order is store-defined.
|
|
874
|
+
*
|
|
875
|
+
* Gated by `view-team-profiles`. Default policy (`minTier: 2`) lets
|
|
876
|
+
* any authenticated session read all envelopes. Two privacy-strict
|
|
877
|
+
* opt-outs:
|
|
878
|
+
*
|
|
879
|
+
* - `view-team-profiles.enabled: false` → list() returns only the
|
|
880
|
+
* caller's own envelope (silent self-fallback, no thrown error).
|
|
881
|
+
* - `view-team-profiles.minTier: 1` + insufficient tier → throws
|
|
882
|
+
* `PolicyDeniedError` with `reason: 'insufficient-tier'`. The
|
|
883
|
+
* caller is expected to elevate, not silently degrade.
|
|
884
|
+
*
|
|
885
|
+
* The asymmetry is deliberate: `enabled: false` is a deliberate
|
|
886
|
+
* design choice ("nobody sees teammate profiles in this app");
|
|
887
|
+
* `insufficient-tier` is "you need to authenticate further". Different
|
|
888
|
+
* UX prompts for different intents.
|
|
889
|
+
*/
|
|
890
|
+
async list(presented) {
|
|
891
|
+
if (this.checkGate) {
|
|
892
|
+
try {
|
|
893
|
+
await this.checkGate("view-team-profiles", presented);
|
|
894
|
+
} catch (err) {
|
|
895
|
+
if (err instanceof PolicyDeniedError && err.reason === "disabled") {
|
|
896
|
+
const me = await this.me();
|
|
897
|
+
return me ? [me] : [];
|
|
898
|
+
}
|
|
899
|
+
throw err;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const dek = await this.getDek();
|
|
903
|
+
const ids = await listUserEnvelopeIds(this.adapter, this.vaultName);
|
|
904
|
+
const envelopes = await Promise.all(
|
|
905
|
+
ids.map((id) => loadUserEnvelope(this.adapter, this.vaultName, id, dek))
|
|
906
|
+
);
|
|
907
|
+
return envelopes.filter((e) => e !== null);
|
|
908
|
+
}
|
|
909
|
+
// ─── Reactive ────────────────────────────────────────────────────────
|
|
910
|
+
/**
|
|
911
|
+
* Listen for changes to a specific keyringId's envelope. The callback
|
|
912
|
+
* fires synchronously after every successful local `updateMe` /
|
|
913
|
+
* `setMe` for that principal.
|
|
914
|
+
*
|
|
915
|
+
* Cross-instance changes (a teammate edits their profile on their
|
|
916
|
+
* device, the sync engine pulls the diff onto this device) will fire
|
|
917
|
+
* subscribers when the sync layer replays the write through this API.
|
|
918
|
+
* In v1, subscribers do NOT fire on raw store changes — wire your sync
|
|
919
|
+
* layer to call back through `vault.user.setMe` / `updateMe` if you
|
|
920
|
+
* need that.
|
|
921
|
+
*
|
|
922
|
+
* Pass keyringId `'*'` to fire on every change in the vault.
|
|
923
|
+
*/
|
|
924
|
+
subscribe(keyringId, cb) {
|
|
925
|
+
let listeners = this.listeners.get(keyringId);
|
|
926
|
+
if (!listeners) {
|
|
927
|
+
listeners = /* @__PURE__ */ new Set();
|
|
928
|
+
this.listeners.set(keyringId, listeners);
|
|
929
|
+
}
|
|
930
|
+
const wrapped = cb;
|
|
931
|
+
listeners.add(wrapped);
|
|
932
|
+
return () => {
|
|
933
|
+
listeners?.delete(wrapped);
|
|
934
|
+
if (listeners && listeners.size === 0) {
|
|
935
|
+
this.listeners.delete(keyringId);
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Reactive handle that caches the current value and re-reads on every
|
|
941
|
+
* change for the given keyringId. Convenient for framework bindings:
|
|
942
|
+
*
|
|
943
|
+
* const live = vault.user.live<UserShape>(vault.userId)
|
|
944
|
+
* live.subscribe(env => render(env?.data))
|
|
945
|
+
*
|
|
946
|
+
* Initial value is `null` until the first `current()` call materializes
|
|
947
|
+
* it via `vault.user.get()`. Call `stop()` when done to release the
|
|
948
|
+
* subscription.
|
|
949
|
+
*/
|
|
950
|
+
live(keyringId) {
|
|
951
|
+
let value = null;
|
|
952
|
+
let primed = false;
|
|
953
|
+
const unsubscribe = this.subscribe(keyringId, (env) => {
|
|
954
|
+
value = env;
|
|
955
|
+
});
|
|
956
|
+
return {
|
|
957
|
+
current() {
|
|
958
|
+
if (!primed) {
|
|
959
|
+
primed = true;
|
|
960
|
+
}
|
|
961
|
+
return value;
|
|
962
|
+
},
|
|
963
|
+
subscribe: (cb) => this.subscribe(keyringId, cb),
|
|
964
|
+
stop: unsubscribe
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
// ─── Internal: change emission ───────────────────────────────────────
|
|
968
|
+
fireChange(keyringId, env) {
|
|
969
|
+
const targeted = this.listeners.get(keyringId);
|
|
970
|
+
if (targeted) for (const l of targeted) l(env);
|
|
971
|
+
const wildcard = this.listeners.get("*");
|
|
972
|
+
if (wildcard) for (const l of wildcard) l(env);
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
function deepMerge(source, patch) {
|
|
976
|
+
if (!isPlainObject(source) || !isPlainObject(patch)) {
|
|
977
|
+
return patch;
|
|
978
|
+
}
|
|
979
|
+
const out = { ...source };
|
|
980
|
+
for (const [key, patchVal] of Object.entries(patch)) {
|
|
981
|
+
const sourceVal = source[key];
|
|
982
|
+
if (isPlainObject(sourceVal) && isPlainObject(patchVal)) {
|
|
983
|
+
out[key] = deepMerge(sourceVal, patchVal);
|
|
984
|
+
} else {
|
|
985
|
+
out[key] = patchVal;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return out;
|
|
989
|
+
}
|
|
990
|
+
function isPlainObject(x) {
|
|
991
|
+
if (x === null || typeof x !== "object") return false;
|
|
992
|
+
if (Array.isArray(x)) return false;
|
|
993
|
+
const proto = Object.getPrototypeOf(x);
|
|
994
|
+
return proto === Object.prototype || proto === null;
|
|
995
|
+
}
|
|
996
|
+
|
|
775
997
|
// src/policy/storage.ts
|
|
776
998
|
var META_COLLECTION = "_meta";
|
|
777
999
|
var POLICY_RECORD_ID = "policy";
|
|
@@ -3561,6 +3783,19 @@ var Vault = class {
|
|
|
3561
3783
|
i18nStrategy;
|
|
3562
3784
|
syncStrategy;
|
|
3563
3785
|
getDEK;
|
|
3786
|
+
/**
|
|
3787
|
+
* Per-principal user envelope API.
|
|
3788
|
+
*
|
|
3789
|
+
* - Write-self: `me()`, `updateMe(patch)`, `setMe(payload)` — always
|
|
3790
|
+
* target this vault session's keyringId. There is no method to write
|
|
3791
|
+
* another principal's envelope (own-only write rule, structural).
|
|
3792
|
+
* - Read-anyone: `get(keyringId)`, `list()` — read other principals'
|
|
3793
|
+
* envelopes, subject to the `view-team-profiles` policy gate (#22).
|
|
3794
|
+
* - Reactive: `subscribe(id, cb)`, `live(id)` — fire on local writes.
|
|
3795
|
+
*
|
|
3796
|
+
* @see docs/superpowers/specs/2026-05-05-user-envelope-design.md
|
|
3797
|
+
*/
|
|
3798
|
+
user;
|
|
3564
3799
|
/**
|
|
3565
3800
|
* Optional callback that re-derives an UnlockedKeyring from the
|
|
3566
3801
|
* adapter using the active user's passphrase. Called by `load()`
|
|
@@ -3693,6 +3928,13 @@ var Vault = class {
|
|
|
3693
3928
|
this.locale = opts.locale;
|
|
3694
3929
|
this.translateText = opts.plaintextTranslator;
|
|
3695
3930
|
this.getDEK = this.makeGetDEK();
|
|
3931
|
+
this.user = new UserApi(
|
|
3932
|
+
this.adapter,
|
|
3933
|
+
this.name,
|
|
3934
|
+
this.keyring.userId,
|
|
3935
|
+
() => this.getDEK(USER_ENVELOPE_COLLECTION),
|
|
3936
|
+
(gate, presented) => this.noydb.checkGate(this.name, gate, presented)
|
|
3937
|
+
);
|
|
3696
3938
|
}
|
|
3697
3939
|
/**
|
|
3698
3940
|
* Construct (or reconstruct) the lazy DEK resolver. Captures the
|
|
@@ -4979,7 +5221,7 @@ var Vault = class {
|
|
|
4979
5221
|
* @see docs/subsystems/public-envelope.md
|
|
4980
5222
|
*/
|
|
4981
5223
|
async getPublicEnvelope(opts = {}) {
|
|
4982
|
-
const { readPublicEnvelope: readPublicEnvelope2 } = await import("./public-envelope-
|
|
5224
|
+
const { readPublicEnvelope: readPublicEnvelope2 } = await import("./public-envelope-DFJZHXVH.js");
|
|
4983
5225
|
return readPublicEnvelope2(this.adapter, this.name, opts);
|
|
4984
5226
|
}
|
|
4985
5227
|
/**
|
|
@@ -5572,7 +5814,17 @@ var PERSONAL_POLICY = Object.freeze({
|
|
|
5572
5814
|
"view-user-auth": {
|
|
5573
5815
|
minTier: 1,
|
|
5574
5816
|
enabled: false
|
|
5575
|
-
}
|
|
5817
|
+
},
|
|
5818
|
+
// ─── User envelope gates (#22) ────────────────────────────────────
|
|
5819
|
+
// edit-own-profile: tier 3 floor — any active session can edit their
|
|
5820
|
+
// own profile/preferences. Tightening to require a TOTP for
|
|
5821
|
+
// profile changes is a one-line override.
|
|
5822
|
+
// view-team-profiles: tier 2 floor — an authenticated session can
|
|
5823
|
+
// read teammates' profiles (display names, avatars, locales).
|
|
5824
|
+
// Setting `enabled: false` makes vault.user.list() return only
|
|
5825
|
+
// self (privacy-strict opt-out).
|
|
5826
|
+
"edit-own-profile": { minTier: 3 },
|
|
5827
|
+
"view-team-profiles": { minTier: 2 }
|
|
5576
5828
|
}
|
|
5577
5829
|
});
|
|
5578
5830
|
var STRICT_POLICY = Object.freeze({
|
|
@@ -5620,7 +5872,16 @@ var STRICT_POLICY = Object.freeze({
|
|
|
5620
5872
|
"view-user-auth": {
|
|
5621
5873
|
minTier: 1,
|
|
5622
5874
|
enabled: false
|
|
5623
|
-
}
|
|
5875
|
+
},
|
|
5876
|
+
// ─── User envelope gates (#22) ────────────────────────────────────
|
|
5877
|
+
// STRICT: profile edits require a TOTP/email-OTP factor (typical
|
|
5878
|
+
// shared-workstation hardening — your name/avatar shouldn't change
|
|
5879
|
+
// without a fresh second-factor proof).
|
|
5880
|
+
"edit-own-profile": {
|
|
5881
|
+
minTier: 2,
|
|
5882
|
+
factors: [{ anyOf: ["totp", "email-otp"] }]
|
|
5883
|
+
},
|
|
5884
|
+
"view-team-profiles": { minTier: 2 }
|
|
5624
5885
|
}
|
|
5625
5886
|
});
|
|
5626
5887
|
function mergePolicy(base, override) {
|
|
@@ -6311,6 +6572,36 @@ var Noydb = class {
|
|
|
6311
6572
|
off(event, handler) {
|
|
6312
6573
|
this.emitter.off(event, handler);
|
|
6313
6574
|
}
|
|
6575
|
+
/**
|
|
6576
|
+
* Soft-lock a single vault: clear its in-memory keyring, DEKs, vault
|
|
6577
|
+
* instance, sync engine, policy enforcer, and active-tier entry —
|
|
6578
|
+
* WITHOUT destroying the `Noydb` instance.
|
|
6579
|
+
*
|
|
6580
|
+
* Designed for "lock screen" UX: the user taps **Lock** and DEKs are
|
|
6581
|
+
* scrubbed from memory immediately, but the same `Noydb` instance can
|
|
6582
|
+
* be re-unlocked via {@link unlockViaAuthenticator} (tier 2) or
|
|
6583
|
+
* {@link unlockViaPin} (tier 3) without re-running `createNoydb`.
|
|
6584
|
+
*
|
|
6585
|
+
* **QuickUnlock state is preserved.** That's the whole point — the
|
|
6586
|
+
* user can still resume via PIN without a full credential re-prompt.
|
|
6587
|
+
* The on-disk `_meta/policy` document is also kept in cache (it
|
|
6588
|
+
* survives lock; nothing about it changes when DEKs are scrubbed).
|
|
6589
|
+
*
|
|
6590
|
+
* No-op when `vault` is not currently in cache (idempotent).
|
|
6591
|
+
*
|
|
6592
|
+
* Unblocks vLannaAi/niwat#33.
|
|
6593
|
+
*
|
|
6594
|
+
* @see #17
|
|
6595
|
+
*/
|
|
6596
|
+
lockVault(vault) {
|
|
6597
|
+
this.syncEngines.get(vault)?.stopAutoSync();
|
|
6598
|
+
this.syncEngines.delete(vault);
|
|
6599
|
+
this.policyEnforcers.get(vault)?.destroy();
|
|
6600
|
+
this.policyEnforcers.delete(vault);
|
|
6601
|
+
this.keyringCache.delete(vault);
|
|
6602
|
+
this.vaultCache.delete(vault);
|
|
6603
|
+
this.activeTier.delete(vault);
|
|
6604
|
+
}
|
|
6314
6605
|
close() {
|
|
6315
6606
|
this.closed = true;
|
|
6316
6607
|
if (this.sessionTimer) {
|
|
@@ -6505,6 +6796,92 @@ var Noydb = class {
|
|
|
6505
6796
|
const keyring = await this.getKeyring(vault);
|
|
6506
6797
|
return keyring.authenticators;
|
|
6507
6798
|
}
|
|
6799
|
+
/**
|
|
6800
|
+
* Native WebAuthn enrollment using the **real** internal keyring (#16).
|
|
6801
|
+
*
|
|
6802
|
+
* Why this exists: when a consumer is using `createNoydb({ secret })`,
|
|
6803
|
+
* they cannot reach the live `UnlockedKeyring` to feed it to
|
|
6804
|
+
* `enrollWebAuthn(keyring, vault, opts)` from `@noy-db/on-webauthn`.
|
|
6805
|
+
* Constructing a synthetic keyring (the previous workaround) produces
|
|
6806
|
+
* a slot whose `wrapped_kek` references the synthetic payload, not
|
|
6807
|
+
* the live session — so `unlockViaAuthenticator()` later replaces the
|
|
6808
|
+
* live DEK map with stale wrapped DEKs and every decrypt fails.
|
|
6809
|
+
*
|
|
6810
|
+
* This method runs `ceremony` with the REAL keyring (still in
|
|
6811
|
+
* `keyringCache`). The ceremony performs the WebAuthn enrollment and
|
|
6812
|
+
* returns the slot options that hub then persists via the standard
|
|
6813
|
+
* tier-2 enrollAuthenticator path.
|
|
6814
|
+
*
|
|
6815
|
+
* Layering note: hub does not import `@noy-db/on-webauthn` (that
|
|
6816
|
+
* would invert the dep graph). The consumer wires it in:
|
|
6817
|
+
*
|
|
6818
|
+
* ```ts
|
|
6819
|
+
* import { enrollWebAuthn } from '@noy-db/on-webauthn'
|
|
6820
|
+
*
|
|
6821
|
+
* await db.enrollWebAuthn('demo', async (keyring) => {
|
|
6822
|
+
* const e = await enrollWebAuthn(keyring, 'demo', { rp: {...} })
|
|
6823
|
+
* return {
|
|
6824
|
+
* id: `webauthn-${e.credentialId.slice(0, 8)}`,
|
|
6825
|
+
* method: 'webauthn',
|
|
6826
|
+
* wrapped_kek: e.wrappedPayload,
|
|
6827
|
+
* meta: {
|
|
6828
|
+
* credentialId: e.credentialId,
|
|
6829
|
+
* wrapIv: e.wrapIv,
|
|
6830
|
+
* prfUsed: e.prfUsed,
|
|
6831
|
+
* beFlag: e.beFlag,
|
|
6832
|
+
* requireSingleDevice: e.requireSingleDevice,
|
|
6833
|
+
* },
|
|
6834
|
+
* }
|
|
6835
|
+
* })
|
|
6836
|
+
* ```
|
|
6837
|
+
*
|
|
6838
|
+
* Returns the WebAuthn `credentialId` (extracted from `meta.credentialId`)
|
|
6839
|
+
* for the caller's lookup index (a bootstrap vault, a PublicEnvelope,
|
|
6840
|
+
* a server-side allowlist).
|
|
6841
|
+
*
|
|
6842
|
+
* Gated by `enroll-authenticator` like `enrollAuthenticator()` itself.
|
|
6843
|
+
*
|
|
6844
|
+
* @see #16
|
|
6845
|
+
*/
|
|
6846
|
+
async enrollWebAuthn(vault, ceremony, presented) {
|
|
6847
|
+
await this.checkGate(vault, "enroll-authenticator", presented);
|
|
6848
|
+
const keyring = await this.getKeyring(vault);
|
|
6849
|
+
const slotOptions = await ceremony(keyring);
|
|
6850
|
+
if (slotOptions.method !== "webauthn") {
|
|
6851
|
+
throw new ValidationError(
|
|
6852
|
+
`enrollWebAuthn: ceremony returned method "${slotOptions.method}"; expected "webauthn". Use db.enrollAuthenticator() for non-webauthn methods.`
|
|
6853
|
+
);
|
|
6854
|
+
}
|
|
6855
|
+
const credentialId = slotOptions.meta.credentialId;
|
|
6856
|
+
if (typeof credentialId !== "string" || credentialId.length === 0) {
|
|
6857
|
+
throw new ValidationError(
|
|
6858
|
+
"enrollWebAuthn: ceremony result must include `meta.credentialId` (base64 string). See @noy-db/on-webauthn enrollWebAuthn() return shape."
|
|
6859
|
+
);
|
|
6860
|
+
}
|
|
6861
|
+
const next = await enrollAuthenticator(this.options.store, vault, keyring, slotOptions);
|
|
6862
|
+
this.keyringCache.set(vault, next);
|
|
6863
|
+
return { credentialId };
|
|
6864
|
+
}
|
|
6865
|
+
/**
|
|
6866
|
+
* Filter the slot list to webauthn-method slots only. Useful for
|
|
6867
|
+
* "you have N WebAuthn credentials enrolled" UI surfaces and for
|
|
6868
|
+
* deciding when a new device prompt should appear. Identity is
|
|
6869
|
+
* `id` + `enrolled_at`; the `meta.credentialId` (base64) is used by
|
|
6870
|
+
* `allowCredentials` at unlock time.
|
|
6871
|
+
*
|
|
6872
|
+
* @see #16
|
|
6873
|
+
*/
|
|
6874
|
+
async listWebAuthnSlots(vault) {
|
|
6875
|
+
const keyring = await this.getKeyring(vault);
|
|
6876
|
+
return keyring.authenticators.filter((a) => a.method === "webauthn").map((a) => {
|
|
6877
|
+
const credentialId = a.meta.credentialId;
|
|
6878
|
+
return {
|
|
6879
|
+
id: a.id,
|
|
6880
|
+
enrolledAt: a.enrolled_at,
|
|
6881
|
+
credentialId: typeof credentialId === "string" ? credentialId : ""
|
|
6882
|
+
};
|
|
6883
|
+
});
|
|
6884
|
+
}
|
|
6508
6885
|
/**
|
|
6509
6886
|
* Resolve a slot by id, then hand the wrapped-KEK ciphertext + meta
|
|
6510
6887
|
* to the caller-supplied verifier. The verifier is the
|
|
@@ -6724,6 +7101,15 @@ var Noydb = class {
|
|
|
6724
7101
|
this.options.secret,
|
|
6725
7102
|
{ validate: this.options.validatePassphrase === true }
|
|
6726
7103
|
);
|
|
7104
|
+
} else if (err instanceof InvalidKeyError && this.options.onInvalidKey === "reset") {
|
|
7105
|
+
await this.options.store.delete(vault, "_keyring", this.options.user);
|
|
7106
|
+
keyring = await createOwnerKeyring(
|
|
7107
|
+
this.options.store,
|
|
7108
|
+
vault,
|
|
7109
|
+
this.options.user,
|
|
7110
|
+
this.options.secret,
|
|
7111
|
+
{ validate: this.options.validatePassphrase === true }
|
|
7112
|
+
);
|
|
6727
7113
|
} else {
|
|
6728
7114
|
throw err;
|
|
6729
7115
|
}
|
|
@@ -7015,6 +7401,10 @@ export {
|
|
|
7015
7401
|
TxCollection,
|
|
7016
7402
|
TxContext,
|
|
7017
7403
|
TxVault,
|
|
7404
|
+
USER_ENVELOPE_COLLECTION,
|
|
7405
|
+
USER_ENVELOPE_MAX_BYTES,
|
|
7406
|
+
UserApi,
|
|
7407
|
+
UserEnvelopeOversizedError,
|
|
7018
7408
|
ValidationError,
|
|
7019
7409
|
Vault,
|
|
7020
7410
|
VaultFrame,
|
|
@@ -7047,6 +7437,7 @@ export {
|
|
|
7047
7437
|
decryptDeterministic,
|
|
7048
7438
|
dekKey,
|
|
7049
7439
|
deleteCredential,
|
|
7440
|
+
deleteUserEnvelope,
|
|
7050
7441
|
deriveMagicLinkContentKey,
|
|
7051
7442
|
derivePresenceKey,
|
|
7052
7443
|
describeAllUsersAuth,
|
|
@@ -7098,10 +7489,14 @@ export {
|
|
|
7098
7489
|
rotatePassphrase as keyringRotatePassphrase,
|
|
7099
7490
|
listCredentials,
|
|
7100
7491
|
listMagicLinkGrants,
|
|
7492
|
+
listUserEnvelopeIds,
|
|
7493
|
+
listUsers,
|
|
7494
|
+
listUsersWithEnvelopes,
|
|
7101
7495
|
loadActiveDelegations,
|
|
7102
7496
|
loadDevUnlock,
|
|
7103
7497
|
loadPaperRecoveryEntries,
|
|
7104
7498
|
loadPublicEnvelope,
|
|
7499
|
+
loadUserEnvelope,
|
|
7105
7500
|
loadVaultPolicy,
|
|
7106
7501
|
magicLinkGrantRecordId,
|
|
7107
7502
|
max,
|
|
@@ -7135,6 +7530,7 @@ export {
|
|
|
7135
7530
|
runTransaction,
|
|
7136
7531
|
savePaperRecoveryEntries,
|
|
7137
7532
|
savePublicEnvelope,
|
|
7533
|
+
saveUserEnvelope,
|
|
7138
7534
|
saveVaultPolicy,
|
|
7139
7535
|
sha256Hex,
|
|
7140
7536
|
sum,
|