@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.cjs
CHANGED
|
@@ -2061,6 +2061,10 @@ __export(src_exports, {
|
|
|
2061
2061
|
TxCollection: () => TxCollection,
|
|
2062
2062
|
TxContext: () => TxContext,
|
|
2063
2063
|
TxVault: () => TxVault,
|
|
2064
|
+
USER_ENVELOPE_COLLECTION: () => USER_ENVELOPE_COLLECTION,
|
|
2065
|
+
USER_ENVELOPE_MAX_BYTES: () => USER_ENVELOPE_MAX_BYTES,
|
|
2066
|
+
UserApi: () => UserApi,
|
|
2067
|
+
UserEnvelopeOversizedError: () => UserEnvelopeOversizedError,
|
|
2064
2068
|
ValidationError: () => ValidationError,
|
|
2065
2069
|
Vault: () => Vault,
|
|
2066
2070
|
VaultFrame: () => VaultFrame,
|
|
@@ -2093,6 +2097,7 @@ __export(src_exports, {
|
|
|
2093
2097
|
decryptDeterministic: () => decryptDeterministic,
|
|
2094
2098
|
dekKey: () => dekKey,
|
|
2095
2099
|
deleteCredential: () => deleteCredential,
|
|
2100
|
+
deleteUserEnvelope: () => deleteUserEnvelope,
|
|
2096
2101
|
deriveMagicLinkContentKey: () => deriveMagicLinkContentKey,
|
|
2097
2102
|
derivePresenceKey: () => derivePresenceKey,
|
|
2098
2103
|
describeAllUsersAuth: () => describeAllUsersAuth,
|
|
@@ -2144,10 +2149,14 @@ __export(src_exports, {
|
|
|
2144
2149
|
keyringRotatePassphrase: () => rotatePassphrase,
|
|
2145
2150
|
listCredentials: () => listCredentials,
|
|
2146
2151
|
listMagicLinkGrants: () => listMagicLinkGrants,
|
|
2152
|
+
listUserEnvelopeIds: () => listUserEnvelopeIds,
|
|
2153
|
+
listUsers: () => listUsers,
|
|
2154
|
+
listUsersWithEnvelopes: () => listUsersWithEnvelopes,
|
|
2147
2155
|
loadActiveDelegations: () => loadActiveDelegations,
|
|
2148
2156
|
loadDevUnlock: () => loadDevUnlock,
|
|
2149
2157
|
loadPaperRecoveryEntries: () => loadPaperRecoveryEntries,
|
|
2150
2158
|
loadPublicEnvelope: () => loadPublicEnvelope,
|
|
2159
|
+
loadUserEnvelope: () => loadUserEnvelope,
|
|
2151
2160
|
loadVaultPolicy: () => loadVaultPolicy,
|
|
2152
2161
|
magicLinkGrantRecordId: () => magicLinkGrantRecordId,
|
|
2153
2162
|
max: () => max,
|
|
@@ -2181,6 +2190,7 @@ __export(src_exports, {
|
|
|
2181
2190
|
runTransaction: () => runTransaction,
|
|
2182
2191
|
savePaperRecoveryEntries: () => savePaperRecoveryEntries,
|
|
2183
2192
|
savePublicEnvelope: () => savePublicEnvelope,
|
|
2193
|
+
saveUserEnvelope: () => saveUserEnvelope,
|
|
2184
2194
|
saveVaultPolicy: () => saveVaultPolicy,
|
|
2185
2195
|
sha256Hex: () => sha256Hex3,
|
|
2186
2196
|
sum: () => sum,
|
|
@@ -4904,6 +4914,81 @@ function estimateEntropy(passphrase) {
|
|
|
4904
4914
|
return Math.round(result.words * Math.log2(7776));
|
|
4905
4915
|
}
|
|
4906
4916
|
|
|
4917
|
+
// src/meta/user-envelope/types.ts
|
|
4918
|
+
init_errors();
|
|
4919
|
+
var USER_ENVELOPE_MAX_BYTES = 64 * 1024;
|
|
4920
|
+
var USER_ENVELOPE_COLLECTION = "_users";
|
|
4921
|
+
var UserEnvelopeOversizedError = class extends NoydbError {
|
|
4922
|
+
bytes;
|
|
4923
|
+
limit;
|
|
4924
|
+
constructor(bytes, limit = USER_ENVELOPE_MAX_BYTES) {
|
|
4925
|
+
super(
|
|
4926
|
+
"USER_ENVELOPE_OVERSIZED",
|
|
4927
|
+
`User envelope payload is ${bytes} bytes; soft cap is ${limit} bytes. Move large data into the vault's regular collections.`
|
|
4928
|
+
);
|
|
4929
|
+
this.name = "UserEnvelopeOversizedError";
|
|
4930
|
+
this.bytes = bytes;
|
|
4931
|
+
this.limit = limit;
|
|
4932
|
+
}
|
|
4933
|
+
};
|
|
4934
|
+
|
|
4935
|
+
// src/meta/user-envelope/storage.ts
|
|
4936
|
+
init_types();
|
|
4937
|
+
init_crypto();
|
|
4938
|
+
init_errors();
|
|
4939
|
+
async function loadUserEnvelope(store, vault, keyringId, dek) {
|
|
4940
|
+
const envelope = await store.get(vault, USER_ENVELOPE_COLLECTION, keyringId);
|
|
4941
|
+
if (!envelope) return null;
|
|
4942
|
+
const plaintext = await decrypt(envelope._iv, envelope._data, dek);
|
|
4943
|
+
const data = JSON.parse(plaintext);
|
|
4944
|
+
return {
|
|
4945
|
+
keyringId,
|
|
4946
|
+
data,
|
|
4947
|
+
_v: envelope._v,
|
|
4948
|
+
_ts: envelope._ts
|
|
4949
|
+
};
|
|
4950
|
+
}
|
|
4951
|
+
async function saveUserEnvelope(store, vault, keyringId, payload, dek, expectedVersion) {
|
|
4952
|
+
const json = JSON.stringify(payload);
|
|
4953
|
+
const bytes = new TextEncoder().encode(json).byteLength;
|
|
4954
|
+
if (bytes > USER_ENVELOPE_MAX_BYTES) {
|
|
4955
|
+
throw new UserEnvelopeOversizedError(bytes);
|
|
4956
|
+
}
|
|
4957
|
+
const prior = await store.get(vault, USER_ENVELOPE_COLLECTION, keyringId);
|
|
4958
|
+
if (expectedVersion !== void 0) {
|
|
4959
|
+
const priorVersion = prior?._v ?? 0;
|
|
4960
|
+
if (priorVersion !== expectedVersion) {
|
|
4961
|
+
throw new ConflictError(
|
|
4962
|
+
priorVersion,
|
|
4963
|
+
`User envelope for "${keyringId}" expected version ${expectedVersion}, actual ${priorVersion}`
|
|
4964
|
+
);
|
|
4965
|
+
}
|
|
4966
|
+
}
|
|
4967
|
+
const nextVersion = (prior?._v ?? 0) + 1;
|
|
4968
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
4969
|
+
const { iv, data } = await encrypt(json, dek);
|
|
4970
|
+
const envelope = {
|
|
4971
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
4972
|
+
_v: nextVersion,
|
|
4973
|
+
_ts: ts,
|
|
4974
|
+
_iv: iv,
|
|
4975
|
+
_data: data
|
|
4976
|
+
};
|
|
4977
|
+
await store.put(vault, USER_ENVELOPE_COLLECTION, keyringId, envelope);
|
|
4978
|
+
return {
|
|
4979
|
+
keyringId,
|
|
4980
|
+
data: payload,
|
|
4981
|
+
_v: nextVersion,
|
|
4982
|
+
_ts: ts
|
|
4983
|
+
};
|
|
4984
|
+
}
|
|
4985
|
+
async function deleteUserEnvelope(store, vault, keyringId) {
|
|
4986
|
+
await store.delete(vault, USER_ENVELOPE_COLLECTION, keyringId);
|
|
4987
|
+
}
|
|
4988
|
+
async function listUserEnvelopeIds(store, vault) {
|
|
4989
|
+
return store.list(vault, USER_ENVELOPE_COLLECTION);
|
|
4990
|
+
}
|
|
4991
|
+
|
|
4907
4992
|
// src/team/keyring.ts
|
|
4908
4993
|
var ADMIN_GRANTABLE_TARGETS = ["operator", "viewer", "client", "admin"];
|
|
4909
4994
|
function canGrant(callerRole, targetRole) {
|
|
@@ -4956,13 +5041,15 @@ async function createOwnerKeyring(adapter, vault, userId, passphrase, passphrase
|
|
|
4956
5041
|
}
|
|
4957
5042
|
const salt = generateSalt();
|
|
4958
5043
|
const kek = await deriveKey(passphrase, salt);
|
|
5044
|
+
const userEnvelopeDek = await generateDEK();
|
|
5045
|
+
const wrappedUserEnvelopeDek = await wrapKey(userEnvelopeDek, kek);
|
|
4959
5046
|
const keyringFile = {
|
|
4960
5047
|
_noydb_keyring: NOYDB_KEYRING_VERSION,
|
|
4961
5048
|
user_id: userId,
|
|
4962
5049
|
display_name: userId,
|
|
4963
5050
|
role: "owner",
|
|
4964
5051
|
permissions: {},
|
|
4965
|
-
deks: {},
|
|
5052
|
+
deks: { [USER_ENVELOPE_COLLECTION]: wrappedUserEnvelopeDek },
|
|
4966
5053
|
salt: bufferToBase64(salt),
|
|
4967
5054
|
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4968
5055
|
granted_by: userId
|
|
@@ -4973,7 +5060,7 @@ async function createOwnerKeyring(adapter, vault, userId, passphrase, passphrase
|
|
|
4973
5060
|
displayName: userId,
|
|
4974
5061
|
role: "owner",
|
|
4975
5062
|
permissions: {},
|
|
4976
|
-
deks: /* @__PURE__ */ new Map(),
|
|
5063
|
+
deks: /* @__PURE__ */ new Map([[USER_ENVELOPE_COLLECTION, userEnvelopeDek]]),
|
|
4977
5064
|
kek,
|
|
4978
5065
|
salt,
|
|
4979
5066
|
authenticators: []
|
|
@@ -5029,6 +5116,17 @@ async function grant(adapter, vault, callerKeyring, options) {
|
|
|
5029
5116
|
...options.importCapability !== void 0 && { import_capability: options.importCapability }
|
|
5030
5117
|
};
|
|
5031
5118
|
await writeKeyringFile(adapter, vault, options.userId, keyringFile);
|
|
5119
|
+
const userEnvelopeDek = callerKeyring.deks.get(USER_ENVELOPE_COLLECTION);
|
|
5120
|
+
if (userEnvelopeDek) {
|
|
5121
|
+
const initialPayload = options.initialProfile ?? {};
|
|
5122
|
+
await saveUserEnvelope(
|
|
5123
|
+
adapter,
|
|
5124
|
+
vault,
|
|
5125
|
+
options.userId,
|
|
5126
|
+
initialPayload,
|
|
5127
|
+
userEnvelopeDek
|
|
5128
|
+
);
|
|
5129
|
+
}
|
|
5032
5130
|
}
|
|
5033
5131
|
async function findAdminDescendants(adapter, vault, rootUserId) {
|
|
5034
5132
|
const allUserIds = await adapter.list(vault, "_keyring");
|
|
@@ -5091,6 +5189,7 @@ async function revoke(adapter, vault, callerKeyring, options) {
|
|
|
5091
5189
|
}
|
|
5092
5190
|
for (const userId of usersToRevoke) {
|
|
5093
5191
|
await adapter.delete(vault, "_keyring", userId);
|
|
5192
|
+
await deleteUserEnvelope(adapter, vault, userId);
|
|
5094
5193
|
}
|
|
5095
5194
|
if (options.rotateKeys !== false && affectedCollections.size > 0) {
|
|
5096
5195
|
await rotateKeys(adapter, vault, callerKeyring, [...affectedCollections]);
|
|
@@ -5249,6 +5348,20 @@ async function listUsers(adapter, vault) {
|
|
|
5249
5348
|
}
|
|
5250
5349
|
return users;
|
|
5251
5350
|
}
|
|
5351
|
+
async function listUsersWithEnvelopes(adapter, vault, userEnvelopeDek) {
|
|
5352
|
+
const users = await listUsers(adapter, vault);
|
|
5353
|
+
const out = [];
|
|
5354
|
+
for (const user of users) {
|
|
5355
|
+
const envelope = await loadUserEnvelope(
|
|
5356
|
+
adapter,
|
|
5357
|
+
vault,
|
|
5358
|
+
user.userId,
|
|
5359
|
+
userEnvelopeDek
|
|
5360
|
+
);
|
|
5361
|
+
out.push({ user, envelope });
|
|
5362
|
+
}
|
|
5363
|
+
return out;
|
|
5364
|
+
}
|
|
5252
5365
|
async function ensureCollectionDEK(adapter, vault, keyring) {
|
|
5253
5366
|
const inFlight = /* @__PURE__ */ new Map();
|
|
5254
5367
|
return async (collectionName) => {
|
|
@@ -5713,6 +5826,220 @@ async function writeKeyringFile2(store, vault, userId, file) {
|
|
|
5713
5826
|
// src/index.ts
|
|
5714
5827
|
init_public_envelope();
|
|
5715
5828
|
|
|
5829
|
+
// src/meta/user-envelope/api.ts
|
|
5830
|
+
var UserApi = class {
|
|
5831
|
+
constructor(adapter, vaultName, writerKeyringId, getDek, checkGate2) {
|
|
5832
|
+
this.adapter = adapter;
|
|
5833
|
+
this.vaultName = vaultName;
|
|
5834
|
+
this.writerKeyringId = writerKeyringId;
|
|
5835
|
+
this.getDek = getDek;
|
|
5836
|
+
this.checkGate = checkGate2;
|
|
5837
|
+
}
|
|
5838
|
+
adapter;
|
|
5839
|
+
vaultName;
|
|
5840
|
+
writerKeyringId;
|
|
5841
|
+
getDek;
|
|
5842
|
+
checkGate;
|
|
5843
|
+
/** keyringId → set of listeners. Wildcard '*' fires on every change. */
|
|
5844
|
+
listeners = /* @__PURE__ */ new Map();
|
|
5845
|
+
// ─── Write-self ──────────────────────────────────────────────────────
|
|
5846
|
+
/** Read the writer's own envelope. Returns null if never written. */
|
|
5847
|
+
async me() {
|
|
5848
|
+
const dek = await this.getDek();
|
|
5849
|
+
return loadUserEnvelope(this.adapter, this.vaultName, this.writerKeyringId, dek);
|
|
5850
|
+
}
|
|
5851
|
+
/**
|
|
5852
|
+
* Deep-merge a partial patch into the writer's own envelope. Creates
|
|
5853
|
+
* the envelope on first call. Optimistic-concurrency safe — a stale
|
|
5854
|
+
* `_v` (parallel writer on another device) throws `ConflictError`.
|
|
5855
|
+
*
|
|
5856
|
+
* Gated by the `edit-own-profile` policy gate (default `minTier: 3`).
|
|
5857
|
+
* Pass `presented` to satisfy tightened policies that require a
|
|
5858
|
+
* factor proof (e.g. STRICT_POLICY's TOTP requirement).
|
|
5859
|
+
*/
|
|
5860
|
+
async updateMe(patch, presented) {
|
|
5861
|
+
if (this.checkGate) await this.checkGate("edit-own-profile", presented);
|
|
5862
|
+
const dek = await this.getDek();
|
|
5863
|
+
const current = await loadUserEnvelope(
|
|
5864
|
+
this.adapter,
|
|
5865
|
+
this.vaultName,
|
|
5866
|
+
this.writerKeyringId,
|
|
5867
|
+
dek
|
|
5868
|
+
);
|
|
5869
|
+
const merged = current ? deepMerge(current.data, patch) : patch;
|
|
5870
|
+
const written = await saveUserEnvelope(
|
|
5871
|
+
this.adapter,
|
|
5872
|
+
this.vaultName,
|
|
5873
|
+
this.writerKeyringId,
|
|
5874
|
+
merged,
|
|
5875
|
+
dek,
|
|
5876
|
+
current?._v ?? 0
|
|
5877
|
+
);
|
|
5878
|
+
this.fireChange(this.writerKeyringId, written);
|
|
5879
|
+
return written;
|
|
5880
|
+
}
|
|
5881
|
+
/**
|
|
5882
|
+
* Replace the writer's own envelope with `payload`. Use sparingly —
|
|
5883
|
+
* `updateMe` is the canonical mutation. No `expectedVersion` check;
|
|
5884
|
+
* callers explicitly take last-write-wins semantics.
|
|
5885
|
+
*
|
|
5886
|
+
* Gated by `edit-own-profile`. See `updateMe` for `presented` usage.
|
|
5887
|
+
*/
|
|
5888
|
+
async setMe(payload, presented) {
|
|
5889
|
+
if (this.checkGate) await this.checkGate("edit-own-profile", presented);
|
|
5890
|
+
const dek = await this.getDek();
|
|
5891
|
+
const written = await saveUserEnvelope(
|
|
5892
|
+
this.adapter,
|
|
5893
|
+
this.vaultName,
|
|
5894
|
+
this.writerKeyringId,
|
|
5895
|
+
payload,
|
|
5896
|
+
dek
|
|
5897
|
+
);
|
|
5898
|
+
this.fireChange(this.writerKeyringId, written);
|
|
5899
|
+
return written;
|
|
5900
|
+
}
|
|
5901
|
+
// ─── Read-anyone ─────────────────────────────────────────────────────
|
|
5902
|
+
/**
|
|
5903
|
+
* Read another principal's envelope by their keyringId. Returns null
|
|
5904
|
+
* if the principal exists but has no envelope yet, or if the
|
|
5905
|
+
* keyringId does not exist at all.
|
|
5906
|
+
*
|
|
5907
|
+
* Gated by `view-team-profiles` (default `minTier: 2`) — but ONLY for
|
|
5908
|
+
* cross-principal reads. Reading your own envelope (`keyringId ===
|
|
5909
|
+
* self`) is never gated; that's just `me()` written long-form.
|
|
5910
|
+
*/
|
|
5911
|
+
async get(keyringId, presented) {
|
|
5912
|
+
if (this.checkGate && keyringId !== this.writerKeyringId) {
|
|
5913
|
+
await this.checkGate("view-team-profiles", presented);
|
|
5914
|
+
}
|
|
5915
|
+
const dek = await this.getDek();
|
|
5916
|
+
return loadUserEnvelope(this.adapter, this.vaultName, keyringId, dek);
|
|
5917
|
+
}
|
|
5918
|
+
/**
|
|
5919
|
+
* Read every persisted envelope in the vault. Order is store-defined.
|
|
5920
|
+
*
|
|
5921
|
+
* Gated by `view-team-profiles`. Default policy (`minTier: 2`) lets
|
|
5922
|
+
* any authenticated session read all envelopes. Two privacy-strict
|
|
5923
|
+
* opt-outs:
|
|
5924
|
+
*
|
|
5925
|
+
* - `view-team-profiles.enabled: false` → list() returns only the
|
|
5926
|
+
* caller's own envelope (silent self-fallback, no thrown error).
|
|
5927
|
+
* - `view-team-profiles.minTier: 1` + insufficient tier → throws
|
|
5928
|
+
* `PolicyDeniedError` with `reason: 'insufficient-tier'`. The
|
|
5929
|
+
* caller is expected to elevate, not silently degrade.
|
|
5930
|
+
*
|
|
5931
|
+
* The asymmetry is deliberate: `enabled: false` is a deliberate
|
|
5932
|
+
* design choice ("nobody sees teammate profiles in this app");
|
|
5933
|
+
* `insufficient-tier` is "you need to authenticate further". Different
|
|
5934
|
+
* UX prompts for different intents.
|
|
5935
|
+
*/
|
|
5936
|
+
async list(presented) {
|
|
5937
|
+
if (this.checkGate) {
|
|
5938
|
+
try {
|
|
5939
|
+
await this.checkGate("view-team-profiles", presented);
|
|
5940
|
+
} catch (err) {
|
|
5941
|
+
if (err instanceof PolicyDeniedError && err.reason === "disabled") {
|
|
5942
|
+
const me = await this.me();
|
|
5943
|
+
return me ? [me] : [];
|
|
5944
|
+
}
|
|
5945
|
+
throw err;
|
|
5946
|
+
}
|
|
5947
|
+
}
|
|
5948
|
+
const dek = await this.getDek();
|
|
5949
|
+
const ids = await listUserEnvelopeIds(this.adapter, this.vaultName);
|
|
5950
|
+
const envelopes = await Promise.all(
|
|
5951
|
+
ids.map((id) => loadUserEnvelope(this.adapter, this.vaultName, id, dek))
|
|
5952
|
+
);
|
|
5953
|
+
return envelopes.filter((e) => e !== null);
|
|
5954
|
+
}
|
|
5955
|
+
// ─── Reactive ────────────────────────────────────────────────────────
|
|
5956
|
+
/**
|
|
5957
|
+
* Listen for changes to a specific keyringId's envelope. The callback
|
|
5958
|
+
* fires synchronously after every successful local `updateMe` /
|
|
5959
|
+
* `setMe` for that principal.
|
|
5960
|
+
*
|
|
5961
|
+
* Cross-instance changes (a teammate edits their profile on their
|
|
5962
|
+
* device, the sync engine pulls the diff onto this device) will fire
|
|
5963
|
+
* subscribers when the sync layer replays the write through this API.
|
|
5964
|
+
* In v1, subscribers do NOT fire on raw store changes — wire your sync
|
|
5965
|
+
* layer to call back through `vault.user.setMe` / `updateMe` if you
|
|
5966
|
+
* need that.
|
|
5967
|
+
*
|
|
5968
|
+
* Pass keyringId `'*'` to fire on every change in the vault.
|
|
5969
|
+
*/
|
|
5970
|
+
subscribe(keyringId, cb) {
|
|
5971
|
+
let listeners = this.listeners.get(keyringId);
|
|
5972
|
+
if (!listeners) {
|
|
5973
|
+
listeners = /* @__PURE__ */ new Set();
|
|
5974
|
+
this.listeners.set(keyringId, listeners);
|
|
5975
|
+
}
|
|
5976
|
+
const wrapped = cb;
|
|
5977
|
+
listeners.add(wrapped);
|
|
5978
|
+
return () => {
|
|
5979
|
+
listeners?.delete(wrapped);
|
|
5980
|
+
if (listeners && listeners.size === 0) {
|
|
5981
|
+
this.listeners.delete(keyringId);
|
|
5982
|
+
}
|
|
5983
|
+
};
|
|
5984
|
+
}
|
|
5985
|
+
/**
|
|
5986
|
+
* Reactive handle that caches the current value and re-reads on every
|
|
5987
|
+
* change for the given keyringId. Convenient for framework bindings:
|
|
5988
|
+
*
|
|
5989
|
+
* const live = vault.user.live<UserShape>(vault.userId)
|
|
5990
|
+
* live.subscribe(env => render(env?.data))
|
|
5991
|
+
*
|
|
5992
|
+
* Initial value is `null` until the first `current()` call materializes
|
|
5993
|
+
* it via `vault.user.get()`. Call `stop()` when done to release the
|
|
5994
|
+
* subscription.
|
|
5995
|
+
*/
|
|
5996
|
+
live(keyringId) {
|
|
5997
|
+
let value = null;
|
|
5998
|
+
let primed = false;
|
|
5999
|
+
const unsubscribe = this.subscribe(keyringId, (env) => {
|
|
6000
|
+
value = env;
|
|
6001
|
+
});
|
|
6002
|
+
return {
|
|
6003
|
+
current() {
|
|
6004
|
+
if (!primed) {
|
|
6005
|
+
primed = true;
|
|
6006
|
+
}
|
|
6007
|
+
return value;
|
|
6008
|
+
},
|
|
6009
|
+
subscribe: (cb) => this.subscribe(keyringId, cb),
|
|
6010
|
+
stop: unsubscribe
|
|
6011
|
+
};
|
|
6012
|
+
}
|
|
6013
|
+
// ─── Internal: change emission ───────────────────────────────────────
|
|
6014
|
+
fireChange(keyringId, env) {
|
|
6015
|
+
const targeted = this.listeners.get(keyringId);
|
|
6016
|
+
if (targeted) for (const l of targeted) l(env);
|
|
6017
|
+
const wildcard = this.listeners.get("*");
|
|
6018
|
+
if (wildcard) for (const l of wildcard) l(env);
|
|
6019
|
+
}
|
|
6020
|
+
};
|
|
6021
|
+
function deepMerge(source, patch) {
|
|
6022
|
+
if (!isPlainObject(source) || !isPlainObject(patch)) {
|
|
6023
|
+
return patch;
|
|
6024
|
+
}
|
|
6025
|
+
const out = { ...source };
|
|
6026
|
+
for (const [key, patchVal] of Object.entries(patch)) {
|
|
6027
|
+
const sourceVal = source[key];
|
|
6028
|
+
if (isPlainObject(sourceVal) && isPlainObject(patchVal)) {
|
|
6029
|
+
out[key] = deepMerge(sourceVal, patchVal);
|
|
6030
|
+
} else {
|
|
6031
|
+
out[key] = patchVal;
|
|
6032
|
+
}
|
|
6033
|
+
}
|
|
6034
|
+
return out;
|
|
6035
|
+
}
|
|
6036
|
+
function isPlainObject(x) {
|
|
6037
|
+
if (x === null || typeof x !== "object") return false;
|
|
6038
|
+
if (Array.isArray(x)) return false;
|
|
6039
|
+
const proto = Object.getPrototypeOf(x);
|
|
6040
|
+
return proto === Object.prototype || proto === null;
|
|
6041
|
+
}
|
|
6042
|
+
|
|
5716
6043
|
// src/policy/storage.ts
|
|
5717
6044
|
init_types();
|
|
5718
6045
|
var META_COLLECTION = "_meta";
|
|
@@ -11114,6 +11441,19 @@ var Vault = class {
|
|
|
11114
11441
|
i18nStrategy;
|
|
11115
11442
|
syncStrategy;
|
|
11116
11443
|
getDEK;
|
|
11444
|
+
/**
|
|
11445
|
+
* Per-principal user envelope API.
|
|
11446
|
+
*
|
|
11447
|
+
* - Write-self: `me()`, `updateMe(patch)`, `setMe(payload)` — always
|
|
11448
|
+
* target this vault session's keyringId. There is no method to write
|
|
11449
|
+
* another principal's envelope (own-only write rule, structural).
|
|
11450
|
+
* - Read-anyone: `get(keyringId)`, `list()` — read other principals'
|
|
11451
|
+
* envelopes, subject to the `view-team-profiles` policy gate (#22).
|
|
11452
|
+
* - Reactive: `subscribe(id, cb)`, `live(id)` — fire on local writes.
|
|
11453
|
+
*
|
|
11454
|
+
* @see docs/superpowers/specs/2026-05-05-user-envelope-design.md
|
|
11455
|
+
*/
|
|
11456
|
+
user;
|
|
11117
11457
|
/**
|
|
11118
11458
|
* Optional callback that re-derives an UnlockedKeyring from the
|
|
11119
11459
|
* adapter using the active user's passphrase. Called by `load()`
|
|
@@ -11246,6 +11586,13 @@ var Vault = class {
|
|
|
11246
11586
|
this.locale = opts.locale;
|
|
11247
11587
|
this.translateText = opts.plaintextTranslator;
|
|
11248
11588
|
this.getDEK = this.makeGetDEK();
|
|
11589
|
+
this.user = new UserApi(
|
|
11590
|
+
this.adapter,
|
|
11591
|
+
this.name,
|
|
11592
|
+
this.keyring.userId,
|
|
11593
|
+
() => this.getDEK(USER_ENVELOPE_COLLECTION),
|
|
11594
|
+
(gate, presented) => this.noydb.checkGate(this.name, gate, presented)
|
|
11595
|
+
);
|
|
11249
11596
|
}
|
|
11250
11597
|
/**
|
|
11251
11598
|
* Construct (or reconstruct) the lazy DEK resolver. Captures the
|
|
@@ -13125,7 +13472,17 @@ var PERSONAL_POLICY = Object.freeze({
|
|
|
13125
13472
|
"view-user-auth": {
|
|
13126
13473
|
minTier: 1,
|
|
13127
13474
|
enabled: false
|
|
13128
|
-
}
|
|
13475
|
+
},
|
|
13476
|
+
// ─── User envelope gates (#22) ────────────────────────────────────
|
|
13477
|
+
// edit-own-profile: tier 3 floor — any active session can edit their
|
|
13478
|
+
// own profile/preferences. Tightening to require a TOTP for
|
|
13479
|
+
// profile changes is a one-line override.
|
|
13480
|
+
// view-team-profiles: tier 2 floor — an authenticated session can
|
|
13481
|
+
// read teammates' profiles (display names, avatars, locales).
|
|
13482
|
+
// Setting `enabled: false` makes vault.user.list() return only
|
|
13483
|
+
// self (privacy-strict opt-out).
|
|
13484
|
+
"edit-own-profile": { minTier: 3 },
|
|
13485
|
+
"view-team-profiles": { minTier: 2 }
|
|
13129
13486
|
}
|
|
13130
13487
|
});
|
|
13131
13488
|
var STRICT_POLICY = Object.freeze({
|
|
@@ -13173,7 +13530,16 @@ var STRICT_POLICY = Object.freeze({
|
|
|
13173
13530
|
"view-user-auth": {
|
|
13174
13531
|
minTier: 1,
|
|
13175
13532
|
enabled: false
|
|
13176
|
-
}
|
|
13533
|
+
},
|
|
13534
|
+
// ─── User envelope gates (#22) ────────────────────────────────────
|
|
13535
|
+
// STRICT: profile edits require a TOTP/email-OTP factor (typical
|
|
13536
|
+
// shared-workstation hardening — your name/avatar shouldn't change
|
|
13537
|
+
// without a fresh second-factor proof).
|
|
13538
|
+
"edit-own-profile": {
|
|
13539
|
+
minTier: 2,
|
|
13540
|
+
factors: [{ anyOf: ["totp", "email-otp"] }]
|
|
13541
|
+
},
|
|
13542
|
+
"view-team-profiles": { minTier: 2 }
|
|
13177
13543
|
}
|
|
13178
13544
|
});
|
|
13179
13545
|
function mergePolicy(base, override) {
|
|
@@ -13864,6 +14230,36 @@ var Noydb = class {
|
|
|
13864
14230
|
off(event, handler) {
|
|
13865
14231
|
this.emitter.off(event, handler);
|
|
13866
14232
|
}
|
|
14233
|
+
/**
|
|
14234
|
+
* Soft-lock a single vault: clear its in-memory keyring, DEKs, vault
|
|
14235
|
+
* instance, sync engine, policy enforcer, and active-tier entry —
|
|
14236
|
+
* WITHOUT destroying the `Noydb` instance.
|
|
14237
|
+
*
|
|
14238
|
+
* Designed for "lock screen" UX: the user taps **Lock** and DEKs are
|
|
14239
|
+
* scrubbed from memory immediately, but the same `Noydb` instance can
|
|
14240
|
+
* be re-unlocked via {@link unlockViaAuthenticator} (tier 2) or
|
|
14241
|
+
* {@link unlockViaPin} (tier 3) without re-running `createNoydb`.
|
|
14242
|
+
*
|
|
14243
|
+
* **QuickUnlock state is preserved.** That's the whole point — the
|
|
14244
|
+
* user can still resume via PIN without a full credential re-prompt.
|
|
14245
|
+
* The on-disk `_meta/policy` document is also kept in cache (it
|
|
14246
|
+
* survives lock; nothing about it changes when DEKs are scrubbed).
|
|
14247
|
+
*
|
|
14248
|
+
* No-op when `vault` is not currently in cache (idempotent).
|
|
14249
|
+
*
|
|
14250
|
+
* Unblocks vLannaAi/niwat#33.
|
|
14251
|
+
*
|
|
14252
|
+
* @see #17
|
|
14253
|
+
*/
|
|
14254
|
+
lockVault(vault) {
|
|
14255
|
+
this.syncEngines.get(vault)?.stopAutoSync();
|
|
14256
|
+
this.syncEngines.delete(vault);
|
|
14257
|
+
this.policyEnforcers.get(vault)?.destroy();
|
|
14258
|
+
this.policyEnforcers.delete(vault);
|
|
14259
|
+
this.keyringCache.delete(vault);
|
|
14260
|
+
this.vaultCache.delete(vault);
|
|
14261
|
+
this.activeTier.delete(vault);
|
|
14262
|
+
}
|
|
13867
14263
|
close() {
|
|
13868
14264
|
this.closed = true;
|
|
13869
14265
|
if (this.sessionTimer) {
|
|
@@ -14058,6 +14454,92 @@ var Noydb = class {
|
|
|
14058
14454
|
const keyring = await this.getKeyring(vault);
|
|
14059
14455
|
return keyring.authenticators;
|
|
14060
14456
|
}
|
|
14457
|
+
/**
|
|
14458
|
+
* Native WebAuthn enrollment using the **real** internal keyring (#16).
|
|
14459
|
+
*
|
|
14460
|
+
* Why this exists: when a consumer is using `createNoydb({ secret })`,
|
|
14461
|
+
* they cannot reach the live `UnlockedKeyring` to feed it to
|
|
14462
|
+
* `enrollWebAuthn(keyring, vault, opts)` from `@noy-db/on-webauthn`.
|
|
14463
|
+
* Constructing a synthetic keyring (the previous workaround) produces
|
|
14464
|
+
* a slot whose `wrapped_kek` references the synthetic payload, not
|
|
14465
|
+
* the live session — so `unlockViaAuthenticator()` later replaces the
|
|
14466
|
+
* live DEK map with stale wrapped DEKs and every decrypt fails.
|
|
14467
|
+
*
|
|
14468
|
+
* This method runs `ceremony` with the REAL keyring (still in
|
|
14469
|
+
* `keyringCache`). The ceremony performs the WebAuthn enrollment and
|
|
14470
|
+
* returns the slot options that hub then persists via the standard
|
|
14471
|
+
* tier-2 enrollAuthenticator path.
|
|
14472
|
+
*
|
|
14473
|
+
* Layering note: hub does not import `@noy-db/on-webauthn` (that
|
|
14474
|
+
* would invert the dep graph). The consumer wires it in:
|
|
14475
|
+
*
|
|
14476
|
+
* ```ts
|
|
14477
|
+
* import { enrollWebAuthn } from '@noy-db/on-webauthn'
|
|
14478
|
+
*
|
|
14479
|
+
* await db.enrollWebAuthn('demo', async (keyring) => {
|
|
14480
|
+
* const e = await enrollWebAuthn(keyring, 'demo', { rp: {...} })
|
|
14481
|
+
* return {
|
|
14482
|
+
* id: `webauthn-${e.credentialId.slice(0, 8)}`,
|
|
14483
|
+
* method: 'webauthn',
|
|
14484
|
+
* wrapped_kek: e.wrappedPayload,
|
|
14485
|
+
* meta: {
|
|
14486
|
+
* credentialId: e.credentialId,
|
|
14487
|
+
* wrapIv: e.wrapIv,
|
|
14488
|
+
* prfUsed: e.prfUsed,
|
|
14489
|
+
* beFlag: e.beFlag,
|
|
14490
|
+
* requireSingleDevice: e.requireSingleDevice,
|
|
14491
|
+
* },
|
|
14492
|
+
* }
|
|
14493
|
+
* })
|
|
14494
|
+
* ```
|
|
14495
|
+
*
|
|
14496
|
+
* Returns the WebAuthn `credentialId` (extracted from `meta.credentialId`)
|
|
14497
|
+
* for the caller's lookup index (a bootstrap vault, a PublicEnvelope,
|
|
14498
|
+
* a server-side allowlist).
|
|
14499
|
+
*
|
|
14500
|
+
* Gated by `enroll-authenticator` like `enrollAuthenticator()` itself.
|
|
14501
|
+
*
|
|
14502
|
+
* @see #16
|
|
14503
|
+
*/
|
|
14504
|
+
async enrollWebAuthn(vault, ceremony, presented) {
|
|
14505
|
+
await this.checkGate(vault, "enroll-authenticator", presented);
|
|
14506
|
+
const keyring = await this.getKeyring(vault);
|
|
14507
|
+
const slotOptions = await ceremony(keyring);
|
|
14508
|
+
if (slotOptions.method !== "webauthn") {
|
|
14509
|
+
throw new ValidationError(
|
|
14510
|
+
`enrollWebAuthn: ceremony returned method "${slotOptions.method}"; expected "webauthn". Use db.enrollAuthenticator() for non-webauthn methods.`
|
|
14511
|
+
);
|
|
14512
|
+
}
|
|
14513
|
+
const credentialId = slotOptions.meta.credentialId;
|
|
14514
|
+
if (typeof credentialId !== "string" || credentialId.length === 0) {
|
|
14515
|
+
throw new ValidationError(
|
|
14516
|
+
"enrollWebAuthn: ceremony result must include `meta.credentialId` (base64 string). See @noy-db/on-webauthn enrollWebAuthn() return shape."
|
|
14517
|
+
);
|
|
14518
|
+
}
|
|
14519
|
+
const next = await enrollAuthenticator(this.options.store, vault, keyring, slotOptions);
|
|
14520
|
+
this.keyringCache.set(vault, next);
|
|
14521
|
+
return { credentialId };
|
|
14522
|
+
}
|
|
14523
|
+
/**
|
|
14524
|
+
* Filter the slot list to webauthn-method slots only. Useful for
|
|
14525
|
+
* "you have N WebAuthn credentials enrolled" UI surfaces and for
|
|
14526
|
+
* deciding when a new device prompt should appear. Identity is
|
|
14527
|
+
* `id` + `enrolled_at`; the `meta.credentialId` (base64) is used by
|
|
14528
|
+
* `allowCredentials` at unlock time.
|
|
14529
|
+
*
|
|
14530
|
+
* @see #16
|
|
14531
|
+
*/
|
|
14532
|
+
async listWebAuthnSlots(vault) {
|
|
14533
|
+
const keyring = await this.getKeyring(vault);
|
|
14534
|
+
return keyring.authenticators.filter((a) => a.method === "webauthn").map((a) => {
|
|
14535
|
+
const credentialId = a.meta.credentialId;
|
|
14536
|
+
return {
|
|
14537
|
+
id: a.id,
|
|
14538
|
+
enrolledAt: a.enrolled_at,
|
|
14539
|
+
credentialId: typeof credentialId === "string" ? credentialId : ""
|
|
14540
|
+
};
|
|
14541
|
+
});
|
|
14542
|
+
}
|
|
14061
14543
|
/**
|
|
14062
14544
|
* Resolve a slot by id, then hand the wrapped-KEK ciphertext + meta
|
|
14063
14545
|
* to the caller-supplied verifier. The verifier is the
|
|
@@ -14277,6 +14759,15 @@ var Noydb = class {
|
|
|
14277
14759
|
this.options.secret,
|
|
14278
14760
|
{ validate: this.options.validatePassphrase === true }
|
|
14279
14761
|
);
|
|
14762
|
+
} else if (err instanceof InvalidKeyError && this.options.onInvalidKey === "reset") {
|
|
14763
|
+
await this.options.store.delete(vault, "_keyring", this.options.user);
|
|
14764
|
+
keyring = await createOwnerKeyring(
|
|
14765
|
+
this.options.store,
|
|
14766
|
+
vault,
|
|
14767
|
+
this.options.user,
|
|
14768
|
+
this.options.secret,
|
|
14769
|
+
{ validate: this.options.validatePassphrase === true }
|
|
14770
|
+
);
|
|
14280
14771
|
} else {
|
|
14281
14772
|
throw err;
|
|
14282
14773
|
}
|
|
@@ -16062,6 +16553,10 @@ function shortJSON(value) {
|
|
|
16062
16553
|
TxCollection,
|
|
16063
16554
|
TxContext,
|
|
16064
16555
|
TxVault,
|
|
16556
|
+
USER_ENVELOPE_COLLECTION,
|
|
16557
|
+
USER_ENVELOPE_MAX_BYTES,
|
|
16558
|
+
UserApi,
|
|
16559
|
+
UserEnvelopeOversizedError,
|
|
16065
16560
|
ValidationError,
|
|
16066
16561
|
Vault,
|
|
16067
16562
|
VaultFrame,
|
|
@@ -16094,6 +16589,7 @@ function shortJSON(value) {
|
|
|
16094
16589
|
decryptDeterministic,
|
|
16095
16590
|
dekKey,
|
|
16096
16591
|
deleteCredential,
|
|
16592
|
+
deleteUserEnvelope,
|
|
16097
16593
|
deriveMagicLinkContentKey,
|
|
16098
16594
|
derivePresenceKey,
|
|
16099
16595
|
describeAllUsersAuth,
|
|
@@ -16145,10 +16641,14 @@ function shortJSON(value) {
|
|
|
16145
16641
|
keyringRotatePassphrase,
|
|
16146
16642
|
listCredentials,
|
|
16147
16643
|
listMagicLinkGrants,
|
|
16644
|
+
listUserEnvelopeIds,
|
|
16645
|
+
listUsers,
|
|
16646
|
+
listUsersWithEnvelopes,
|
|
16148
16647
|
loadActiveDelegations,
|
|
16149
16648
|
loadDevUnlock,
|
|
16150
16649
|
loadPaperRecoveryEntries,
|
|
16151
16650
|
loadPublicEnvelope,
|
|
16651
|
+
loadUserEnvelope,
|
|
16152
16652
|
loadVaultPolicy,
|
|
16153
16653
|
magicLinkGrantRecordId,
|
|
16154
16654
|
max,
|
|
@@ -16182,6 +16682,7 @@ function shortJSON(value) {
|
|
|
16182
16682
|
runTransaction,
|
|
16183
16683
|
savePaperRecoveryEntries,
|
|
16184
16684
|
savePublicEnvelope,
|
|
16685
|
+
saveUserEnvelope,
|
|
16185
16686
|
saveVaultPolicy,
|
|
16186
16687
|
sha256Hex,
|
|
16187
16688
|
sum,
|