@noy-db/hub 0.3.0-pre.4 → 0.3.0-pre.5
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/{backup-MOB27UUN.js → backup-OPONO6Q5.js} +2 -2
- package/dist/bundle/index.js +4 -4
- package/dist/cargo/index.js +4 -4
- package/dist/{chunk-SEQ2JI3Y.js → chunk-44UTIDE2.js} +2 -2
- package/dist/{chunk-5CY35H55.js → chunk-6UOSONVI.js} +3 -3
- package/dist/{chunk-P3V7JTB6.js → chunk-7SY3PTED.js} +2 -2
- package/dist/{chunk-GOUNIHFA.js → chunk-A4DWPOP3.js} +8 -8
- package/dist/{chunk-ZPHDGUZZ.js → chunk-IEUIUSBO.js} +20 -16
- package/dist/chunk-IEUIUSBO.js.map +1 -0
- package/dist/{chunk-K5P46KB3.js → chunk-LEA7WJEZ.js} +2 -2
- package/dist/{chunk-TVRQ3RYH.js → chunk-NQVZB3S7.js} +3 -4
- package/dist/chunk-NQVZB3S7.js.map +1 -0
- package/dist/{chunk-NB47TXGP.js → chunk-OJU3CPFA.js} +2 -2
- package/dist/{chunk-7J7JRYXW.js → chunk-PI5CG2OV.js} +3 -3
- package/dist/{chunk-5W7AOFWA.js → chunk-S2WKOCTU.js} +2 -2
- package/dist/{chunk-KYY7L72M.js → chunk-SAJJWVNQ.js} +58 -29
- package/dist/chunk-SAJJWVNQ.js.map +1 -0
- package/dist/{chunk-CPXPHQGX.js → chunk-TL7QR5T6.js} +15 -10
- package/dist/chunk-TL7QR5T6.js.map +1 -0
- package/dist/{chunk-HLUKZ36Q.js → chunk-ZBDK7T6X.js} +2 -2
- package/dist/{chunk-VAWY44JW.js → chunk-ZT4GWVTX.js} +22 -7
- package/dist/chunk-ZT4GWVTX.js.map +1 -0
- package/dist/{collection-facade-MKEBZDWW.js → collection-facade-THH4ASGX.js} +4 -4
- package/dist/{config-drift-KGM7ZRW4.js → config-drift-3UIJVI5G.js} +3 -3
- package/dist/{extract-partition-RNVSFHAB.js → extract-partition-6JGY3KYR.js} +3 -3
- package/dist/i18n/index.js +2 -2
- package/dist/index.d.ts +8 -1
- package/dist/index.js +14 -14
- package/dist/{liberate-GMGCHIND.js → liberate-AQ7OWBZZ.js} +3 -3
- package/dist/{noydb-KS2O2MRI.js → noydb-WQZNAECY.js} +8 -8
- package/dist/{register-K6DDSIXN.js → register-V5AQTXNI.js} +3 -3
- package/dist/{storage-5E6YB2JY.js → storage-3FKGLQMC.js} +4 -2
- package/dist/team/index.d.ts +27 -2
- package/dist/team/index.js +4 -4
- package/dist/{walk-5C3GPKT2.js → walk-T65TZDN2.js} +2 -2
- package/dist/with/index.js +2 -2
- package/package.json +3 -3
- package/dist/chunk-CPXPHQGX.js.map +0 -1
- package/dist/chunk-KYY7L72M.js.map +0 -1
- package/dist/chunk-TVRQ3RYH.js.map +0 -1
- package/dist/chunk-VAWY44JW.js.map +0 -1
- package/dist/chunk-ZPHDGUZZ.js.map +0 -1
- /package/dist/{backup-MOB27UUN.js.map → backup-OPONO6Q5.js.map} +0 -0
- /package/dist/{chunk-SEQ2JI3Y.js.map → chunk-44UTIDE2.js.map} +0 -0
- /package/dist/{chunk-5CY35H55.js.map → chunk-6UOSONVI.js.map} +0 -0
- /package/dist/{chunk-P3V7JTB6.js.map → chunk-7SY3PTED.js.map} +0 -0
- /package/dist/{chunk-GOUNIHFA.js.map → chunk-A4DWPOP3.js.map} +0 -0
- /package/dist/{chunk-K5P46KB3.js.map → chunk-LEA7WJEZ.js.map} +0 -0
- /package/dist/{chunk-NB47TXGP.js.map → chunk-OJU3CPFA.js.map} +0 -0
- /package/dist/{chunk-7J7JRYXW.js.map → chunk-PI5CG2OV.js.map} +0 -0
- /package/dist/{chunk-5W7AOFWA.js.map → chunk-S2WKOCTU.js.map} +0 -0
- /package/dist/{chunk-HLUKZ36Q.js.map → chunk-ZBDK7T6X.js.map} +0 -0
- /package/dist/{collection-facade-MKEBZDWW.js.map → collection-facade-THH4ASGX.js.map} +0 -0
- /package/dist/{config-drift-KGM7ZRW4.js.map → config-drift-3UIJVI5G.js.map} +0 -0
- /package/dist/{extract-partition-RNVSFHAB.js.map → extract-partition-6JGY3KYR.js.map} +0 -0
- /package/dist/{liberate-GMGCHIND.js.map → liberate-AQ7OWBZZ.js.map} +0 -0
- /package/dist/{noydb-KS2O2MRI.js.map → noydb-WQZNAECY.js.map} +0 -0
- /package/dist/{register-K6DDSIXN.js.map → register-V5AQTXNI.js.map} +0 -0
- /package/dist/{storage-5E6YB2JY.js.map → storage-3FKGLQMC.js.map} +0 -0
- /package/dist/{walk-5C3GPKT2.js.map → walk-T65TZDN2.js.map} +0 -0
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
assertStrongPassphrase,
|
|
13
13
|
mintKeyringCanary,
|
|
14
14
|
persistKeyring
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-ZT4GWVTX.js";
|
|
16
16
|
import {
|
|
17
17
|
openEnvelopeJson
|
|
18
18
|
} from "./chunk-OVO2RZAE.js";
|
|
@@ -586,4 +586,4 @@ export {
|
|
|
586
586
|
removeAuthenticator,
|
|
587
587
|
findAuthenticator
|
|
588
588
|
};
|
|
589
|
-
//# sourceMappingURL=chunk-
|
|
589
|
+
//# sourceMappingURL=chunk-LEA7WJEZ.js.map
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
+
SYNC_CREDENTIALS_COLLECTION,
|
|
2
3
|
ensureCollectionDEK,
|
|
3
4
|
grant,
|
|
4
5
|
revoke,
|
|
5
6
|
rotateKeys
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-ZT4GWVTX.js";
|
|
7
8
|
import {
|
|
8
9
|
openEnvelopeJson
|
|
9
10
|
} from "./chunk-OVO2RZAE.js";
|
|
@@ -33,7 +34,6 @@ function withTeam() {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
// src/with-party/team/sync-credentials.ts
|
|
36
|
-
var SYNC_CREDENTIALS_COLLECTION = "_sync_credentials";
|
|
37
37
|
function requireAdminAccess(keyring) {
|
|
38
38
|
if (keyring.role !== "owner" && keyring.role !== "admin") {
|
|
39
39
|
throw new PermissionDeniedError(
|
|
@@ -90,11 +90,10 @@ async function credentialStatus(adapter, vault, keyring, adapterId) {
|
|
|
90
90
|
|
|
91
91
|
export {
|
|
92
92
|
withTeam,
|
|
93
|
-
SYNC_CREDENTIALS_COLLECTION,
|
|
94
93
|
putCredential,
|
|
95
94
|
getCredential,
|
|
96
95
|
deleteCredential,
|
|
97
96
|
listCredentials,
|
|
98
97
|
credentialStatus
|
|
99
98
|
};
|
|
100
|
-
//# sourceMappingURL=chunk-
|
|
99
|
+
//# sourceMappingURL=chunk-NQVZB3S7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/with-party/team/active.ts","../src/with-party/team/sync-credentials.ts"],"sourcesContent":["/**\n * Enable the multi-user team capability (#267 Track A tail).\n *\n * Pass to `createNoydb({ teamStrategy: withTeam() })` to make `db.grant` /\n * `db.revoke` / `db.rotate` live. The keyring grant/revoke/rotate engines\n * are statically imported HERE — this module is reachable only through the\n * `@noy-db/hub/team` subpath (and the root barrel's tree-shakeable\n * re-export), so a single-user floor bundle never carries them. The facade\n * (`TeamFacade.runGrant` / `runRevoke` / `runRotate`) owns the policy-gate +\n * keyring plumbing and receives the engine as an argument.\n */\nimport type { TeamStrategy } from './strategy.js'\nimport { grant as grantEngine, revoke as revokeEngine, rotateKeys as rotateEngine } from './keyring.js'\n\nexport function withTeam(): TeamStrategy {\n return {\n async grant(team, vault, options, factors) {\n return team.runGrant(grantEngine, vault, options, factors)\n },\n async revoke(team, vault, options, factors) {\n return team.runRevoke(revokeEngine, vault, options, factors)\n },\n async rotate(team, vault, collections) {\n return team.runRotate(rotateEngine, vault, collections)\n },\n }\n}\n","/**\n * _sync_credentials reserved collection —\n *\n * Stores per-adapter OAuth tokens (and any other long-lived sync secrets) as\n * encrypted records inside the vault itself. Tokens are wrapped with the\n * compartment's own DEK, live on disk as ciphertext like any other record, and\n * are accessed only through the dedicated API in this module — never via\n * `vault.collection('_sync_credentials')`.\n *\n * Design decisions\n * ────────────────\n *\n * **Why a reserved collection, not a separate store?**\n * The compartment's existing encryption stack (AES-256-GCM + collection DEK)\n * is exactly the right primitive for protecting OAuth tokens at rest. Using a\n * separate store would require a new encryption surface, new adapter calls,\n * and a new backup/restore path — all of which already exist for collections.\n *\n * **Why not exposed as a regular collection?**\n * The same reason `_keyring` and `_ledger` aren't: they have invariants that\n * must be enforced (naming scheme, no cross-user leakage, no schema\n * validation, no history/ledger writes for privacy). Routing through a\n * dedicated API enforces those invariants.\n *\n * **Token lifecycle:**\n * - `putCredential(vault, adapterId, token)` — store or overwrite\n * - `getCredential(vault, adapterId)` — load and decrypt\n * - `deleteCredential(vault, adapterId)` — remove\n * - `listCredentials(vault)` — enumerate adapter IDs (not tokens)\n *\n * The `adapterId` is the record ID within the `_sync_credentials` collection.\n * It should be a stable, human-readable identifier for the adapter instance\n * (e.g. `'google-drive'`, `'dropbox'`, `'s3-prod'`).\n *\n * **ACL:** only `owner` and `admin` roles can read/write sync credentials.\n * Operators, viewers, and clients cannot call this API. The check is made\n * against the caller's keyring role at call time.\n */\n\nimport type { NoydbStore, EncryptedEnvelope } from '../../kernel/types.js'\nimport { NOYDB_FORMAT_VERSION } from '../../kernel/types.js'\nimport type { UnlockedKeyring } from './keyring.js'\nimport { encrypt, openEnvelopeJson } from '../../kernel/enclave/index.js'\nimport { ensureCollectionDEK } from './keyring.js'\nimport { PermissionDeniedError } from '../../kernel/errors.js'\n\n/**\n * The reserved collection name. Never collides with user collections.\n * Canonical definition lives in `reserved-secret-collections.ts` (shared with\n * the `vault.collection()` guard and grant DEK-propagation); re-exported here\n * for existing consumers.\n */\nexport { SYNC_CREDENTIALS_COLLECTION } from './reserved-secret-collections.js'\nimport { SYNC_CREDENTIALS_COLLECTION } from './reserved-secret-collections.js'\n\n// ─── Token types ──────────────────────────────────────────────────────\n\n/**\n * An OAuth/auth token stored in `_sync_credentials`.\n *\n * Fields mirror the OAuth2 token response shape. `customData` is an escape\n * hatch for adapter-specific secrets (API keys, connection strings, etc.)\n * that don't fit the OAuth2 shape.\n */\nexport interface SyncCredential {\n /** Stable identifier for the adapter instance (e.g. 'google-drive'). */\n readonly adapterId: string\n /** OAuth token type, usually 'Bearer'. */\n readonly tokenType: string\n /** The access token. Expires at `expiresAt` if set. */\n readonly accessToken: string\n /** Long-lived refresh token for renewing the access token. */\n readonly refreshToken?: string\n /** ISO timestamp when `accessToken` expires. Absent means \"no expiry\". */\n readonly expiresAt?: string\n /** Space-separated OAuth scopes. */\n readonly scopes?: string\n /** Adapter-specific opaque data (API keys, endpoints, etc.). */\n readonly customData?: Record<string, string>\n}\n\n// ─── Access check ─────────────────────────────────────────────────────\n\nfunction requireAdminAccess(keyring: UnlockedKeyring): void {\n // FR-6: custodian is INTENTIONALLY excluded. Sync credentials are the\n // firm's hosting/infrastructure secrets (OAuth tokens, connection strings) —\n // not the custodian's operational scope. A custodian operates the DATA but\n // must never mint or read transport credentials that could be used to\n // re-home or impersonate the firm's vault. Do NOT add 'custodian' here.\n if (keyring.role !== 'owner' && keyring.role !== 'admin') {\n throw new PermissionDeniedError(\n `Sync credentials require owner or admin role. Current role: \"${keyring.role}\"`,\n )\n }\n}\n\n// ─── Public API ────────────────────────────────────────────────────────\n\n/**\n * Store or overwrite a sync credential for the given adapter.\n *\n * The credential is encrypted with the `_sync_credentials` collection DEK\n * (auto-generated on first use). The record ID is the `adapterId`.\n *\n * Requires owner or admin role.\n */\nexport async function putCredential(\n adapter: NoydbStore,\n vault: string,\n keyring: UnlockedKeyring,\n credential: SyncCredential,\n): Promise<void> {\n requireAdminAccess(keyring)\n\n const getDek = await ensureCollectionDEK(adapter, vault, keyring)\n const dek = await getDek(SYNC_CREDENTIALS_COLLECTION)\n\n const { iv, data } = await encrypt(JSON.stringify(credential), dek)\n\n const existing = await adapter.get(vault, SYNC_CREDENTIALS_COLLECTION, credential.adapterId)\n const version = existing ? existing._v + 1 : 1\n\n const envelope: EncryptedEnvelope = {\n _noydb: NOYDB_FORMAT_VERSION,\n _v: version,\n _ts: new Date().toISOString(),\n _iv: iv,\n _data: data,\n _by: keyring.userId,\n }\n\n await adapter.put(\n vault,\n SYNC_CREDENTIALS_COLLECTION,\n credential.adapterId,\n envelope,\n existing ? existing._v : undefined,\n )\n}\n\n/**\n * Load and decrypt a sync credential for the given adapter ID.\n *\n * Returns `null` if no credential exists for this adapter.\n * Requires owner or admin role.\n */\nexport async function getCredential(\n adapter: NoydbStore,\n vault: string,\n keyring: UnlockedKeyring,\n adapterId: string,\n): Promise<SyncCredential | null> {\n requireAdminAccess(keyring)\n\n const getDek = await ensureCollectionDEK(adapter, vault, keyring)\n const dek = await getDek(SYNC_CREDENTIALS_COLLECTION)\n\n const envelope = await adapter.get(vault, SYNC_CREDENTIALS_COLLECTION, adapterId)\n if (!envelope) return null\n\n const plaintext = await openEnvelopeJson(envelope, dek)\n return JSON.parse(plaintext) as SyncCredential\n}\n\n/**\n * Delete a sync credential by adapter ID.\n *\n * No-op if the credential doesn't exist. Requires owner or admin role.\n */\nexport async function deleteCredential(\n adapter: NoydbStore,\n vault: string,\n keyring: UnlockedKeyring,\n adapterId: string,\n): Promise<void> {\n requireAdminAccess(keyring)\n await adapter.delete(vault, SYNC_CREDENTIALS_COLLECTION, adapterId)\n}\n\n/**\n * List all adapter IDs that have stored credentials.\n *\n * Returns only the IDs, never the credential payloads. Useful for\n * displaying \"connected adapters\" in UI without decrypting tokens.\n * Requires owner or admin role.\n */\nexport async function listCredentials(\n adapter: NoydbStore,\n vault: string,\n keyring: UnlockedKeyring,\n): Promise<string[]> {\n requireAdminAccess(keyring)\n return adapter.list(vault, SYNC_CREDENTIALS_COLLECTION)\n}\n\n/**\n * Check whether a credential exists and whether its access token has expired.\n *\n * Returns `{ exists: false }` if no credential is stored, or\n * `{ exists: true, expired: boolean }` based on the `expiresAt` field.\n * Requires owner or admin role.\n */\nexport async function credentialStatus(\n adapter: NoydbStore,\n vault: string,\n keyring: UnlockedKeyring,\n adapterId: string,\n): Promise<{ exists: false } | { exists: true; expired: boolean }> {\n const credential = await getCredential(adapter, vault, keyring, adapterId)\n if (!credential) return { exists: false }\n\n const expired = credential.expiresAt\n ? Date.now() > new Date(credential.expiresAt).getTime()\n : false\n\n return { exists: true, expired }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAcO,SAAS,WAAyB;AACvC,SAAO;AAAA,IACL,MAAM,MAAM,MAAM,OAAO,SAAS,SAAS;AACzC,aAAO,KAAK,SAAS,OAAa,OAAO,SAAS,OAAO;AAAA,IAC3D;AAAA,IACA,MAAM,OAAO,MAAM,OAAO,SAAS,SAAS;AAC1C,aAAO,KAAK,UAAU,QAAc,OAAO,SAAS,OAAO;AAAA,IAC7D;AAAA,IACA,MAAM,OAAO,MAAM,OAAO,aAAa;AACrC,aAAO,KAAK,UAAU,YAAc,OAAO,WAAW;AAAA,IACxD;AAAA,EACF;AACF;;;ACyDA,SAAS,mBAAmB,SAAgC;AAM1D,MAAI,QAAQ,SAAS,WAAW,QAAQ,SAAS,SAAS;AACxD,UAAM,IAAI;AAAA,MACR,gEAAgE,QAAQ,IAAI;AAAA,IAC9E;AAAA,EACF;AACF;AAYA,eAAsB,cACpB,SACA,OACA,SACA,YACe;AACf,qBAAmB,OAAO;AAE1B,QAAM,SAAS,MAAM,oBAAoB,SAAS,OAAO,OAAO;AAChE,QAAM,MAAM,MAAM,OAAO,2BAA2B;AAEpD,QAAM,EAAE,IAAI,KAAK,IAAI,MAAM,QAAQ,KAAK,UAAU,UAAU,GAAG,GAAG;AAElE,QAAM,WAAW,MAAM,QAAQ,IAAI,OAAO,6BAA6B,WAAW,SAAS;AAC3F,QAAM,UAAU,WAAW,SAAS,KAAK,IAAI;AAE7C,QAAM,WAA8B;AAAA,IAClC,QAAQ;AAAA,IACR,IAAI;AAAA,IACJ,MAAK,oBAAI,KAAK,GAAE,YAAY;AAAA,IAC5B,KAAK;AAAA,IACL,OAAO;AAAA,IACP,KAAK,QAAQ;AAAA,EACf;AAEA,QAAM,QAAQ;AAAA,IACZ;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX;AAAA,IACA,WAAW,SAAS,KAAK;AAAA,EAC3B;AACF;AAQA,eAAsB,cACpB,SACA,OACA,SACA,WACgC;AAChC,qBAAmB,OAAO;AAE1B,QAAM,SAAS,MAAM,oBAAoB,SAAS,OAAO,OAAO;AAChE,QAAM,MAAM,MAAM,OAAO,2BAA2B;AAEpD,QAAM,WAAW,MAAM,QAAQ,IAAI,OAAO,6BAA6B,SAAS;AAChF,MAAI,CAAC,SAAU,QAAO;AAEtB,QAAM,YAAY,MAAM,iBAAiB,UAAU,GAAG;AACtD,SAAO,KAAK,MAAM,SAAS;AAC7B;AAOA,eAAsB,iBACpB,SACA,OACA,SACA,WACe;AACf,qBAAmB,OAAO;AAC1B,QAAM,QAAQ,OAAO,OAAO,6BAA6B,SAAS;AACpE;AASA,eAAsB,gBACpB,SACA,OACA,SACmB;AACnB,qBAAmB,OAAO;AAC1B,SAAO,QAAQ,KAAK,OAAO,2BAA2B;AACxD;AASA,eAAsB,iBACpB,SACA,OACA,SACA,WACiE;AACjE,QAAM,aAAa,MAAM,cAAc,SAAS,OAAO,SAAS,SAAS;AACzE,MAAI,CAAC,WAAY,QAAO,EAAE,QAAQ,MAAM;AAExC,QAAM,UAAU,WAAW,YACvB,KAAK,IAAI,IAAI,IAAI,KAAK,WAAW,SAAS,EAAE,QAAQ,IACpD;AAEJ,SAAO,EAAE,QAAQ,MAAM,QAAQ;AACjC;","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SCHEMAS_COLLECTION
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-TL7QR5T6.js";
|
|
4
4
|
import {
|
|
5
5
|
BLOB_CHUNKS_COLLECTION,
|
|
6
6
|
BLOB_COLLECTION,
|
|
@@ -450,4 +450,4 @@ export {
|
|
|
450
450
|
extractPartition,
|
|
451
451
|
extractPartitionCore
|
|
452
452
|
};
|
|
453
|
-
//# sourceMappingURL=chunk-
|
|
453
|
+
//# sourceMappingURL=chunk-OJU3CPFA.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
grant,
|
|
3
3
|
revoke
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-ZT4GWVTX.js";
|
|
5
5
|
import {
|
|
6
6
|
CargoNotEnabledError,
|
|
7
7
|
CustodyNotEnabledError
|
|
@@ -24,7 +24,7 @@ function withCustody() {
|
|
|
24
24
|
return host._revokeCustodianImpl(revoke, vault, options, factors);
|
|
25
25
|
},
|
|
26
26
|
async liberate(vault, opts) {
|
|
27
|
-
const { liberateVault } = await import("./liberate-
|
|
27
|
+
const { liberateVault } = await import("./liberate-AQ7OWBZZ.js");
|
|
28
28
|
return liberateVault(vault, opts);
|
|
29
29
|
}
|
|
30
30
|
};
|
|
@@ -83,4 +83,4 @@ export {
|
|
|
83
83
|
NO_CUSTODY,
|
|
84
84
|
CustodyApi
|
|
85
85
|
};
|
|
86
|
-
//# sourceMappingURL=chunk-
|
|
86
|
+
//# sourceMappingURL=chunk-PI5CG2OV.js.map
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
function withCargo() {
|
|
3
3
|
return {
|
|
4
4
|
async extractPartition(vault, opts) {
|
|
5
|
-
const { extractPartitionCore } = await import("./extract-partition-
|
|
5
|
+
const { extractPartitionCore } = await import("./extract-partition-6JGY3KYR.js");
|
|
6
6
|
return extractPartitionCore(vault, opts);
|
|
7
7
|
}
|
|
8
8
|
};
|
|
@@ -11,4 +11,4 @@ function withCargo() {
|
|
|
11
11
|
export {
|
|
12
12
|
withCargo
|
|
13
13
|
};
|
|
14
|
-
//# sourceMappingURL=chunk-
|
|
14
|
+
//# sourceMappingURL=chunk-S2WKOCTU.js.map
|
|
@@ -3,9 +3,12 @@ import {
|
|
|
3
3
|
derivePersistedSchema
|
|
4
4
|
} from "./chunk-ULIHDPKL.js";
|
|
5
5
|
import {
|
|
6
|
-
|
|
6
|
+
loadPersistedSchemaEntry,
|
|
7
7
|
savePersistedSchema
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-TL7QR5T6.js";
|
|
9
|
+
import {
|
|
10
|
+
ConflictError
|
|
11
|
+
} from "./chunk-U23JUUPX.js";
|
|
9
12
|
|
|
10
13
|
// src/with-shape/schema-update/delta.ts
|
|
11
14
|
function computeSchemaDelta(stored, fresh, collection) {
|
|
@@ -49,37 +52,63 @@ async function evaluateStrategies(delta, strategies, ctx) {
|
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
// src/with-shape/persisted-schemas/register.ts
|
|
55
|
+
var MAX_SCHEMA_CAS_RETRIES = 5;
|
|
52
56
|
async function persistSchemaIfNeeded(opts) {
|
|
53
57
|
const fresh = await derivePersistedSchema(opts.validator);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
for (let attempt = 0; ; attempt++) {
|
|
59
|
+
const { version, payload: stored } = await loadPersistedSchemaEntry(
|
|
60
|
+
opts.store,
|
|
61
|
+
opts.vault,
|
|
62
|
+
opts.collectionName,
|
|
63
|
+
opts.dek
|
|
64
|
+
);
|
|
65
|
+
if (stored && isEquivalent(stored, fresh)) {
|
|
66
|
+
return { written: false, skipped: true, envelope: stored, decision: { action: "allow" } };
|
|
67
|
+
}
|
|
68
|
+
let decision = { action: "allow" };
|
|
69
|
+
const strategies = opts.strategies ?? [];
|
|
70
|
+
if (stored && strategies.length > 0 && stored.kind === fresh.kind && isPlainObject(stored.jsonSchema) && isPlainObject(fresh.jsonSchema)) {
|
|
71
|
+
const delta = computeSchemaDelta(stored.jsonSchema, fresh.jsonSchema, opts.collectionName);
|
|
72
|
+
decision = await evaluateStrategies(delta, strategies, { collection: opts.collectionName });
|
|
73
|
+
}
|
|
74
|
+
if (decision.action !== "allow") {
|
|
75
|
+
return { written: false, skipped: false, envelope: stored ?? fresh, decision };
|
|
76
|
+
}
|
|
77
|
+
const toSave = stored?.classified !== void 0 ? { ...fresh, classified: stored.classified } : fresh;
|
|
78
|
+
try {
|
|
79
|
+
await savePersistedSchema(opts.store, opts.vault, opts.collectionName, opts.dek, toSave, version);
|
|
80
|
+
return { written: true, skipped: false, envelope: toSave, decision };
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (err instanceof ConflictError && attempt < MAX_SCHEMA_CAS_RETRIES) continue;
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
66
85
|
}
|
|
67
|
-
const toSave = stored?.classified !== void 0 ? { ...fresh, classified: stored.classified } : fresh;
|
|
68
|
-
await savePersistedSchema(opts.store, opts.vault, opts.collectionName, opts.dek, toSave);
|
|
69
|
-
return { written: true, skipped: false, envelope: toSave, decision };
|
|
70
86
|
}
|
|
71
87
|
async function persistClassifiedMarker(opts) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
classified: opts.marker
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
for (let attempt = 0; ; attempt++) {
|
|
89
|
+
const { version, payload: stored } = await loadPersistedSchemaEntry(
|
|
90
|
+
opts.store,
|
|
91
|
+
opts.vault,
|
|
92
|
+
opts.collectionName,
|
|
93
|
+
opts.dek
|
|
94
|
+
);
|
|
95
|
+
if (stored?.classified !== void 0 && markersEqual(stored.classified, opts.marker)) return;
|
|
96
|
+
const payload = stored !== void 0 ? { ...stored, classified: opts.marker } : {
|
|
97
|
+
_noydb_schema: 1,
|
|
98
|
+
kind: "Unknown",
|
|
99
|
+
jsonSchema: null,
|
|
100
|
+
hash: null,
|
|
101
|
+
derivedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
102
|
+
classified: opts.marker
|
|
103
|
+
};
|
|
104
|
+
try {
|
|
105
|
+
await savePersistedSchema(opts.store, opts.vault, opts.collectionName, opts.dek, payload, version);
|
|
106
|
+
return;
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (err instanceof ConflictError && attempt < MAX_SCHEMA_CAS_RETRIES) continue;
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
83
112
|
}
|
|
84
113
|
function markersEqual(a, b) {
|
|
85
114
|
return sameSet(a.digestOnly, b.digestOnly) && sameSet(a.equatable, b.equatable);
|
|
@@ -103,4 +132,4 @@ export {
|
|
|
103
132
|
persistSchemaIfNeeded,
|
|
104
133
|
persistClassifiedMarker
|
|
105
134
|
};
|
|
106
|
-
//# sourceMappingURL=chunk-
|
|
135
|
+
//# sourceMappingURL=chunk-SAJJWVNQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/with-shape/schema-update/delta.ts","../src/with-shape/schema-update/dispatch.ts","../src/with-shape/persisted-schemas/register.ts"],"sourcesContent":["/**\n * Classify the difference between two derived JSON Schemas.\n *\n * v1 ruleset — top-level object properties + required-ness:\n * - additive ⇔ only new OPTIONAL properties (no removals, no changed\n * properties, no new required field, no required-ness flip)\n * - non-additive ⇔ any removal, any shape/type change, any required-ness\n * flip, or a new REQUIRED property\n * Deeper rules (nested objects, union widening, type narrowing) are\n * deferred. Callers only invoke this with two object schemas.\n */\nimport { canonicalize } from '../persisted-schemas/canonicalize.js'\nimport type { SchemaDelta, FieldChange } from './types.js'\n\ninterface ObjectSchema {\n readonly properties?: Record<string, unknown>\n readonly required?: readonly string[]\n}\n\nexport function computeSchemaDelta(\n stored: object,\n fresh: object,\n collection: string,\n): SchemaDelta {\n const a = stored as ObjectSchema\n const b = fresh as ObjectSchema\n const aProps = a.properties ?? {}\n const bProps = b.properties ?? {}\n const aReq = new Set(a.required ?? [])\n const bReq = new Set(b.required ?? [])\n\n const aKeys = Object.keys(aProps)\n const bKeys = Object.keys(bProps)\n\n const added = bKeys.filter(k => !(k in aProps))\n const removed = aKeys.filter(k => !(k in bProps))\n\n const changed: FieldChange[] = []\n for (const k of bKeys) {\n if (!(k in aProps)) continue\n const shapeChanged = canonicalize(aProps[k]) !== canonicalize(bProps[k])\n const requiredChanged = aReq.has(k) !== bReq.has(k)\n if (shapeChanged || requiredChanged) {\n changed.push({ field: k, requiredChanged, shapeChanged })\n }\n }\n\n let kind: SchemaDelta['kind']\n if (added.length === 0 && removed.length === 0 && changed.length === 0) {\n kind = 'none'\n } else if (\n removed.length === 0 &&\n changed.length === 0 &&\n added.every(k => !bReq.has(k))\n ) {\n kind = 'additive'\n } else {\n kind = 'non-additive'\n }\n\n return { collection, kind, added, removed, changed }\n}\n","/** Ordered strategy evaluation: first non-`allow` decision wins. */\nimport type { SchemaDelta, SchemaUpdateStrategy, UpdateContext, UpdateDecision } from './types.js'\n\nexport async function evaluateStrategies(\n delta: SchemaDelta,\n strategies: readonly SchemaUpdateStrategy[],\n ctx: UpdateContext,\n): Promise<UpdateDecision> {\n for (const strategy of strategies) {\n const decision = await strategy.onSchemaDelta(delta, ctx)\n if (decision.action !== 'allow') return decision\n }\n return { action: 'allow' }\n}\n","/**\n * Orchestrate the derive → hash → skip-or-write cycle for a collection's\n * persisted JSON Schema. Called by the Vault at collection-registration\n * time when the developer opts in via `collection({ persistJsonSchema:\n * true })`.\n *\n * Skip semantics:\n *\n * - Zod validators: skip when the new hash equals the stored hash.\n * - Non-Zod (stub envelopes have hash=null): skip when the stored\n * envelope's `kind` matches the freshly-detected kind (since there's\n * no body to compare yet — a kind change is the only signal).\n *\n * @module\n */\n\nimport { derivePersistedSchema } from './derive.js'\nimport { loadPersistedSchemaEntry, savePersistedSchema } from './storage.js'\nimport { computeSchemaDelta } from '../schema-update/delta.js'\nimport { evaluateStrategies } from '../schema-update/dispatch.js'\nimport { ConflictError } from '../../kernel/errors.js'\nimport type { SchemaUpdateStrategy, UpdateDecision } from '../schema-update/types.js'\nimport type { NoydbStore, ClassifiedMarker } from '../../kernel/types.js'\nimport type { PersistedSchemaEnvelope } from './types.js'\nimport type { EnclaveKey } from '../../kernel/enclave/index.js'\n\n/**\n * Max optimistic-CAS retries for the shared `_schemas/<collection>` record\n * (#583). Only two writers ever contend (the JSON-Schema writer and the\n * classified-marker writer), so one retry suffices; the headroom covers a\n * pathological re-order. Exhausting it rethrows the {@link ConflictError}\n * rather than silently losing a write.\n */\nconst MAX_SCHEMA_CAS_RETRIES = 5\n\nexport interface PersistSchemaResult {\n /** True when a fresh envelope was written to storage. */\n readonly written: boolean\n /** True when an existing envelope matched and the write was skipped. */\n readonly skipped: boolean\n /** The envelope that was either written or matched. */\n readonly envelope: PersistedSchemaEnvelope\n /** The update-strategy decision, present when strategies ran. */\n readonly decision?: UpdateDecision\n}\n\nexport async function persistSchemaIfNeeded(opts: {\n readonly store: NoydbStore\n readonly vault: string\n readonly collectionName: string\n readonly validator: unknown\n readonly dek: EnclaveKey\n readonly strategies?: readonly SchemaUpdateStrategy[]\n}): Promise<PersistSchemaResult> {\n const fresh = await derivePersistedSchema(opts.validator)\n\n // The `_schemas/<collection>` record is shared with the classified-marker\n // writer, so a plain load→save loses whichever writer put first (#583). Read\n // the version at load time and CAS on it; on conflict re-read (picking up the\n // other writer's field), re-evaluate, and retry.\n for (let attempt = 0; ; attempt++) {\n const { version, payload: stored } = await loadPersistedSchemaEntry(\n opts.store, opts.vault, opts.collectionName, opts.dek,\n )\n\n if (stored && isEquivalent(stored, fresh)) {\n return { written: false, skipped: true, envelope: stored, decision: { action: 'allow' } }\n }\n\n // Changed (or first registration). Run update strategies only when we\n // have a comparable JSON-Schema baseline and strategies were registered.\n let decision: UpdateDecision = { action: 'allow' }\n const strategies = opts.strategies ?? []\n if (\n stored &&\n strategies.length > 0 &&\n stored.kind === fresh.kind &&\n isPlainObject(stored.jsonSchema) &&\n isPlainObject(fresh.jsonSchema)\n ) {\n const delta = computeSchemaDelta(stored.jsonSchema, fresh.jsonSchema, opts.collectionName)\n decision = await evaluateStrategies(delta, strategies, { collection: opts.collectionName })\n }\n\n if (decision.action !== 'allow') {\n // reject (or cutover): do NOT overwrite the baseline — the\n // old schema stays the source of truth until the change is resolved.\n return { written: false, skipped: false, envelope: stored ?? fresh, decision }\n }\n\n // Preserve a previously-persisted classified marker (C-A / R10) — the schema\n // derivation knows nothing about it, so a naive overwrite here would drop the\n // config-drift guard's cross-session signal.\n const toSave: PersistedSchemaEnvelope =\n stored?.classified !== undefined ? { ...fresh, classified: stored.classified } : fresh\n try {\n await savePersistedSchema(opts.store, opts.vault, opts.collectionName, opts.dek, toSave, version)\n return { written: true, skipped: false, envelope: toSave, decision }\n } catch (err) {\n if (err instanceof ConflictError && attempt < MAX_SCHEMA_CAS_RETRIES) continue\n throw err\n }\n }\n}\n\n/**\n * Persist (or refresh) the C-A / R10 classified marker into the collection's\n * `_schemas/<collection>` record, preserving any existing derived JSON-Schema\n * body. Idempotent — a no-op when an equivalent marker is already stored.\n * Independent of `persistJsonSchema`: called on the first classified write so a\n * later naive handle can detect the drift cross-session.\n */\nexport async function persistClassifiedMarker(opts: {\n readonly store: NoydbStore\n readonly vault: string\n readonly collectionName: string\n readonly dek: EnclaveKey\n readonly marker: ClassifiedMarker\n}): Promise<void> {\n // Shares the `_schemas/<collection>` record with the JSON-Schema writer;\n // CAS on the loaded version so a concurrent schema (re)registration can't\n // silently drop the marker (and vice-versa) — see #583.\n for (let attempt = 0; ; attempt++) {\n const { version, payload: stored } = await loadPersistedSchemaEntry(\n opts.store, opts.vault, opts.collectionName, opts.dek,\n )\n if (stored?.classified !== undefined && markersEqual(stored.classified, opts.marker)) return\n const payload: PersistedSchemaEnvelope =\n stored !== undefined\n ? { ...stored, classified: opts.marker }\n : {\n _noydb_schema: 1,\n kind: 'Unknown',\n jsonSchema: null,\n hash: null,\n derivedAt: new Date().toISOString(),\n classified: opts.marker,\n }\n try {\n await savePersistedSchema(opts.store, opts.vault, opts.collectionName, opts.dek, payload, version)\n return\n } catch (err) {\n if (err instanceof ConflictError && attempt < MAX_SCHEMA_CAS_RETRIES) continue\n throw err\n }\n }\n}\n\nfunction markersEqual(a: ClassifiedMarker, b: ClassifiedMarker): boolean {\n return sameSet(a.digestOnly, b.digestOnly) && sameSet(a.equatable, b.equatable)\n}\n\nfunction sameSet(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false\n const s = new Set(a)\n return b.every((x) => s.has(x))\n}\n\nfunction isPlainObject(v: unknown): v is object {\n return typeof v === 'object' && v !== null && !Array.isArray(v)\n}\n\nfunction isEquivalent(a: PersistedSchemaEnvelope, b: PersistedSchemaEnvelope): boolean {\n if (a.kind !== b.kind) return false\n // Zod path: real hashes — compare directly.\n if (a.hash && b.hash) return a.hash === b.hash\n // Stub path: both have hash=null. Kind equality is the only signal we have.\n if (a.hash === null && b.hash === null) return true\n // Mixed (one has a hash, the other doesn't) — treat as changed.\n return false\n}\n"],"mappings":"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","names":[]}
|
|
@@ -10,35 +10,40 @@ import {
|
|
|
10
10
|
|
|
11
11
|
// src/with-shape/persisted-schemas/storage.ts
|
|
12
12
|
var SCHEMAS_COLLECTION = "_schemas";
|
|
13
|
-
async function
|
|
13
|
+
async function loadPersistedSchemaEntry(store, vault, collection, dek) {
|
|
14
14
|
const envelope = await store.get(vault, SCHEMAS_COLLECTION, collection);
|
|
15
|
-
if (!envelope) return void 0;
|
|
15
|
+
if (!envelope) return { version: 0, payload: void 0 };
|
|
16
|
+
const version = envelope._v;
|
|
16
17
|
try {
|
|
17
18
|
const plaintext = await openEnvelopeJson(envelope, dek);
|
|
18
19
|
const parsed = JSON.parse(plaintext);
|
|
19
|
-
if (parsed._noydb_schema !== 1) return void 0;
|
|
20
|
-
return parsed;
|
|
20
|
+
if (parsed._noydb_schema !== 1) return { version, payload: void 0 };
|
|
21
|
+
return { version, payload: parsed };
|
|
21
22
|
} catch {
|
|
22
|
-
return void 0;
|
|
23
|
+
return { version, payload: void 0 };
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
|
-
async function
|
|
26
|
+
async function loadPersistedSchema(store, vault, collection, dek) {
|
|
27
|
+
return (await loadPersistedSchemaEntry(store, vault, collection, dek)).payload;
|
|
28
|
+
}
|
|
29
|
+
async function savePersistedSchema(store, vault, collection, dek, payload, expectedVersion) {
|
|
26
30
|
const json = JSON.stringify(payload);
|
|
27
31
|
const { iv, data } = await encrypt(json, dek);
|
|
28
|
-
const
|
|
32
|
+
const baseVersion = expectedVersion ?? (await store.get(vault, SCHEMAS_COLLECTION, collection))?._v ?? 0;
|
|
29
33
|
const env = {
|
|
30
34
|
_noydb: NOYDB_FORMAT_VERSION,
|
|
31
|
-
_v:
|
|
35
|
+
_v: baseVersion + 1,
|
|
32
36
|
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33
37
|
_iv: iv,
|
|
34
38
|
_data: data
|
|
35
39
|
};
|
|
36
|
-
await store.put(vault, SCHEMAS_COLLECTION, collection, env);
|
|
40
|
+
await store.put(vault, SCHEMAS_COLLECTION, collection, env, expectedVersion);
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
export {
|
|
40
44
|
SCHEMAS_COLLECTION,
|
|
45
|
+
loadPersistedSchemaEntry,
|
|
41
46
|
loadPersistedSchema,
|
|
42
47
|
savePersistedSchema
|
|
43
48
|
};
|
|
44
|
-
//# sourceMappingURL=chunk-
|
|
49
|
+
//# sourceMappingURL=chunk-TL7QR5T6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/with-shape/persisted-schemas/storage.ts"],"sourcesContent":["/**\n * Read / write the per-collection persisted-schema envelope. Mirrors the\n * standard noy-db record envelope shape and is **AES-GCM encrypted with\n * the collection's DEK** — the schema body (field names, enum values,\n * constraints) is sensitive metadata, so it gets the same encryption\n * envelope as the records it describes.\n *\n * Storage layout:\n *\n * <vault>/_schemas/<collection> → EncryptedEnvelope\n *\n * The DEK passed to {@link savePersistedSchema} / {@link loadPersistedSchema}\n * is the same key the collection uses for its records.\n *\n * @module\n */\n\nimport { encrypt, openEnvelopeJson, type EnclaveKey } from '../../kernel/enclave/index.js'\nimport { NOYDB_FORMAT_VERSION } from '../../kernel/types.js'\nimport type { NoydbStore, EncryptedEnvelope } from '../../kernel/types.js'\nimport type { PersistedSchemaEnvelope } from './types.js'\n\n/** Reserved collection name where persisted schemas live. */\nexport const SCHEMAS_COLLECTION = '_schemas' as const\n\n/**\n * A decrypted persisted-schema read paired with the wrapping envelope's\n * `_v`. The version is the optimistic-concurrency token: pass it back to\n * {@link savePersistedSchema} as `expectedVersion` to make the write a CAS\n * that rejects a stale put (see #583 — the `_schemas` record is shared by\n * the JSON-Schema writer and the classified-marker writer).\n */\nexport interface PersistedSchemaEntry {\n /** Wrapping envelope `_v` (0 when no record exists yet). */\n readonly version: number\n /** Decrypted payload, or `undefined` when absent/corrupt/wrong-DEK. */\n readonly payload: PersistedSchemaEnvelope | undefined\n}\n\n/**\n * Read the persisted-schema envelope for one collection together with its\n * wrapping `_v`. The version reflects the raw stored envelope even when the\n * payload cannot be decrypted/parsed (so a CAS still guards a corrupt record\n * rather than silently clobbering it).\n */\nexport async function loadPersistedSchemaEntry(\n store: NoydbStore,\n vault: string,\n collection: string,\n dek: EnclaveKey,\n): Promise<PersistedSchemaEntry> {\n const envelope = await store.get(vault, SCHEMAS_COLLECTION, collection)\n if (!envelope) return { version: 0, payload: undefined }\n const version = envelope._v\n try {\n const plaintext = await openEnvelopeJson(envelope, dek)\n const parsed = JSON.parse(plaintext) as PersistedSchemaEnvelope\n if (parsed._noydb_schema !== 1) return { version, payload: undefined }\n return { version, payload: parsed }\n } catch {\n return { version, payload: undefined }\n }\n}\n\n/**\n * Read and decrypt the persisted-schema envelope for one collection.\n * Returns `undefined` when no envelope has been written or when decryption\n * fails (e.g. wrong DEK passed). Tolerates corrupted records — JSON parse\n * failures surface as `undefined`, mirroring `_meta/handle`'s contract.\n */\nexport async function loadPersistedSchema(\n store: NoydbStore,\n vault: string,\n collection: string,\n dek: EnclaveKey,\n): Promise<PersistedSchemaEnvelope | undefined> {\n return (await loadPersistedSchemaEntry(store, vault, collection, dek)).payload\n}\n\n/**\n * Encrypt and persist a schema envelope for one collection. Always\n * overwrites any prior write (callers gate on hash equality before calling\n * to avoid no-op writes).\n *\n * Pass `expectedVersion` — the `version` from {@link loadPersistedSchemaEntry}\n * captured at load time — to make the write an optimistic CAS: on a store with\n * `casAtomic`, `store.put` throws {@link ConflictError} when the record was\n * bumped concurrently, so a lost update on the shared `_schemas` record is\n * rejected instead of silently clobbering the other writer's field (#583).\n * When omitted, keeps the legacy unconditional last-writer-wins behaviour.\n */\nexport async function savePersistedSchema(\n store: NoydbStore,\n vault: string,\n collection: string,\n dek: EnclaveKey,\n payload: PersistedSchemaEnvelope,\n expectedVersion?: number,\n): Promise<void> {\n const json = JSON.stringify(payload)\n const { iv, data } = await encrypt(json, dek)\n const baseVersion = expectedVersion ?? (await store.get(vault, SCHEMAS_COLLECTION, collection))?._v ?? 0\n const env: EncryptedEnvelope = {\n _noydb: NOYDB_FORMAT_VERSION,\n _v: baseVersion + 1,\n _ts: new Date().toISOString(),\n _iv: iv,\n _data: data,\n }\n await store.put(vault, SCHEMAS_COLLECTION, collection, env, expectedVersion)\n}\n"],"mappings":";;;;;;;;;;;AAuBO,IAAM,qBAAqB;AAsBlC,eAAsB,yBACpB,OACA,OACA,YACA,KAC+B;AAC/B,QAAM,WAAW,MAAM,MAAM,IAAI,OAAO,oBAAoB,UAAU;AACtE,MAAI,CAAC,SAAU,QAAO,EAAE,SAAS,GAAG,SAAS,OAAU;AACvD,QAAM,UAAU,SAAS;AACzB,MAAI;AACF,UAAM,YAAY,MAAM,iBAAiB,UAAU,GAAG;AACtD,UAAM,SAAS,KAAK,MAAM,SAAS;AACnC,QAAI,OAAO,kBAAkB,EAAG,QAAO,EAAE,SAAS,SAAS,OAAU;AACrE,WAAO,EAAE,SAAS,SAAS,OAAO;AAAA,EACpC,QAAQ;AACN,WAAO,EAAE,SAAS,SAAS,OAAU;AAAA,EACvC;AACF;AAQA,eAAsB,oBACpB,OACA,OACA,YACA,KAC8C;AAC9C,UAAQ,MAAM,yBAAyB,OAAO,OAAO,YAAY,GAAG,GAAG;AACzE;AAcA,eAAsB,oBACpB,OACA,OACA,YACA,KACA,SACA,iBACe;AACf,QAAM,OAAO,KAAK,UAAU,OAAO;AACnC,QAAM,EAAE,IAAI,KAAK,IAAI,MAAM,QAAQ,MAAM,GAAG;AAC5C,QAAM,cAAc,oBAAoB,MAAM,MAAM,IAAI,OAAO,oBAAoB,UAAU,IAAI,MAAM;AACvG,QAAM,MAAyB;AAAA,IAC7B,QAAQ;AAAA,IACR,IAAI,cAAc;AAAA,IAClB,MAAK,oBAAI,KAAK,GAAE,YAAY;AAAA,IAC5B,KAAK;AAAA,IACL,OAAO;AAAA,EACT;AACA,QAAM,MAAM,IAAI,OAAO,oBAAoB,YAAY,KAAK,eAAe;AAC7E;","names":[]}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "./chunk-VWGCJUTV.js";
|
|
7
7
|
import {
|
|
8
8
|
createOwnerKeyring
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-ZT4GWVTX.js";
|
|
10
10
|
import {
|
|
11
11
|
NOYDB_FORMAT_VERSION
|
|
12
12
|
} from "./chunk-LN22XH4B.js";
|
|
@@ -132,4 +132,4 @@ export {
|
|
|
132
132
|
isDeedVault,
|
|
133
133
|
liberateVault
|
|
134
134
|
};
|
|
135
|
-
//# sourceMappingURL=chunk-
|
|
135
|
+
//# sourceMappingURL=chunk-ZBDK7T6X.js.map
|
|
@@ -106,6 +106,17 @@ function estimateEntropy(passphrase) {
|
|
|
106
106
|
return Math.round(result.words * Math.log2(7776));
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
// src/with-party/team/reserved-secret-collections.ts
|
|
110
|
+
var SYNC_CREDENTIALS_COLLECTION = "_sync_credentials";
|
|
111
|
+
var BROKER_COLLECTION = "_broker";
|
|
112
|
+
var SECRET_BEARING_RESERVED_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
113
|
+
SYNC_CREDENTIALS_COLLECTION,
|
|
114
|
+
BROKER_COLLECTION
|
|
115
|
+
]);
|
|
116
|
+
function isSecretBearingReservedCollection(name) {
|
|
117
|
+
return SECRET_BEARING_RESERVED_COLLECTIONS.has(name);
|
|
118
|
+
}
|
|
119
|
+
|
|
109
120
|
// src/with-party/team/keyring.ts
|
|
110
121
|
var ADMIN_GRANTABLE_TARGETS = ["operator", "viewer", "client", "admin"];
|
|
111
122
|
function canGrant(callerRole, targetRole) {
|
|
@@ -276,8 +287,10 @@ async function grant(adapter, vault, callerKeyring, options) {
|
|
|
276
287
|
const permissions = resolvePermissions(options.role, options.permissions);
|
|
277
288
|
const newSalt = generateSalt();
|
|
278
289
|
const newKek = await deriveKey(options.passphrase, newSalt);
|
|
290
|
+
const granteeMayHoldSecrets = options.role === "owner" || options.role === "admin";
|
|
279
291
|
const wrappedDeks = {};
|
|
280
292
|
for (const collName of Object.keys(permissions)) {
|
|
293
|
+
if (isSecretBearingReservedCollection(collName) && !granteeMayHoldSecrets) continue;
|
|
281
294
|
const dek = callerKeyring.deks.get(collName);
|
|
282
295
|
if (dek) {
|
|
283
296
|
wrappedDeks[collName] = await wrapKey(dek, newKek);
|
|
@@ -285,15 +298,15 @@ async function grant(adapter, vault, callerKeyring, options) {
|
|
|
285
298
|
}
|
|
286
299
|
if (options.role === "owner" || options.role === "admin" || options.role === "custodian" || options.role === "viewer") {
|
|
287
300
|
for (const [collName, dek] of callerKeyring.deks) {
|
|
288
|
-
if (
|
|
289
|
-
|
|
290
|
-
|
|
301
|
+
if (collName in wrappedDeks) continue;
|
|
302
|
+
if (isSecretBearingReservedCollection(collName) && !granteeMayHoldSecrets) continue;
|
|
303
|
+
wrappedDeks[collName] = await wrapKey(dek, newKek);
|
|
291
304
|
}
|
|
292
305
|
}
|
|
293
306
|
for (const [collName, dek] of callerKeyring.deks) {
|
|
294
|
-
if (collName.startsWith("_")
|
|
295
|
-
|
|
296
|
-
|
|
307
|
+
if (!collName.startsWith("_") || collName in wrappedDeks) continue;
|
|
308
|
+
if (isSecretBearingReservedCollection(collName) && !granteeMayHoldSecrets) continue;
|
|
309
|
+
wrappedDeks[collName] = await wrapKey(dek, newKek);
|
|
297
310
|
}
|
|
298
311
|
for (const collName of Object.keys(wrappedDeks)) {
|
|
299
312
|
if (!callerKeyring.deks.has(collName)) {
|
|
@@ -738,6 +751,8 @@ export {
|
|
|
738
751
|
validatePassphrase,
|
|
739
752
|
assertStrongPassphrase,
|
|
740
753
|
estimateEntropy,
|
|
754
|
+
SYNC_CREDENTIALS_COLLECTION,
|
|
755
|
+
isSecretBearingReservedCollection,
|
|
741
756
|
mintKeyringCanary,
|
|
742
757
|
loadKeyring,
|
|
743
758
|
assertKeyringOpenAllowed,
|
|
@@ -759,4 +774,4 @@ export {
|
|
|
759
774
|
hasImportCapability,
|
|
760
775
|
evaluateImportCapability
|
|
761
776
|
};
|
|
762
|
-
//# sourceMappingURL=chunk-
|
|
777
|
+
//# sourceMappingURL=chunk-ZT4GWVTX.js.map
|