@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
|
@@ -1819,6 +1819,56 @@ declare function assertStrongPassphrase(s: string, opts?: PassphrasePolicy & {
|
|
|
1819
1819
|
*/
|
|
1820
1820
|
declare function estimateEntropy(passphrase: string): number;
|
|
1821
1821
|
|
|
1822
|
+
/**
|
|
1823
|
+
* Type surface for the per-principal user envelope subsystem.
|
|
1824
|
+
*
|
|
1825
|
+
* @see docs/superpowers/specs/2026-05-05-user-envelope-design.md
|
|
1826
|
+
*
|
|
1827
|
+
* @module
|
|
1828
|
+
*/
|
|
1829
|
+
|
|
1830
|
+
/**
|
|
1831
|
+
* Thin reader view of a user envelope. The on-disk shape is the standard
|
|
1832
|
+
* {@link import('../../types.js').EncryptedEnvelope}; this is what callers
|
|
1833
|
+
* see after the storage layer has decrypted the payload.
|
|
1834
|
+
*
|
|
1835
|
+
* Hub commits to the `keyringId` ⇔ `userId` identity and the `_v` / `_ts`
|
|
1836
|
+
* envelope metadata. The `data` payload is fully app-defined — hub does
|
|
1837
|
+
* not introspect, validate, or reserve any keys inside it.
|
|
1838
|
+
*/
|
|
1839
|
+
interface UserEnvelope<T> {
|
|
1840
|
+
/** The principal id this envelope belongs to. Equals the keyring `user_id`. */
|
|
1841
|
+
readonly keyringId: string;
|
|
1842
|
+
/** App-owned payload. Opaque to hub. */
|
|
1843
|
+
readonly data: T;
|
|
1844
|
+
/** Optimistic-concurrency version. Increments on every write. */
|
|
1845
|
+
readonly _v: number;
|
|
1846
|
+
/** ISO timestamp of the last write. */
|
|
1847
|
+
readonly _ts: string;
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* Soft cap on the JSON-serialized payload size. Generous (a typical
|
|
1851
|
+
* profile + preferences + small app annex is ~1 KiB); rejects accidental
|
|
1852
|
+
* "stuff app state in here" anti-patterns.
|
|
1853
|
+
*/
|
|
1854
|
+
declare const USER_ENVELOPE_MAX_BYTES: number;
|
|
1855
|
+
/**
|
|
1856
|
+
* Reserved store collection name for user envelopes. Starts with `_` so the
|
|
1857
|
+
* keyring grant machinery propagates the DEK to every granted user via the
|
|
1858
|
+
* existing system-collection DEK propagation path in `team/keyring.ts`.
|
|
1859
|
+
*/
|
|
1860
|
+
declare const USER_ENVELOPE_COLLECTION = "_users";
|
|
1861
|
+
/**
|
|
1862
|
+
* Thrown when a user-envelope payload exceeds {@link USER_ENVELOPE_MAX_BYTES}
|
|
1863
|
+
* after JSON-serialization. The error carries the actual size so callers
|
|
1864
|
+
* can decide whether to trim or split.
|
|
1865
|
+
*/
|
|
1866
|
+
declare class UserEnvelopeOversizedError extends NoydbError {
|
|
1867
|
+
readonly bytes: number;
|
|
1868
|
+
readonly limit: number;
|
|
1869
|
+
constructor(bytes: number, limit?: number);
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1822
1872
|
/** In-memory representation of an unlocked keyring. */
|
|
1823
1873
|
interface UnlockedKeyring {
|
|
1824
1874
|
readonly userId: string;
|
|
@@ -1911,6 +1961,29 @@ interface BundleRecipient {
|
|
|
1911
1961
|
* @internal
|
|
1912
1962
|
*/
|
|
1913
1963
|
declare function buildRecipientKeyringFile(callerKeyring: UnlockedKeyring, recipient: BundleRecipient): Promise<KeyringFile>;
|
|
1964
|
+
/** List all users with access to a vault. */
|
|
1965
|
+
declare function listUsers(adapter: NoydbStore, vault: string): Promise<UserInfo[]>;
|
|
1966
|
+
/**
|
|
1967
|
+
* Joined enumeration: every keyring + its `_users/<keyringId>`
|
|
1968
|
+
* envelope side by side. Convenience for admin UIs that want to
|
|
1969
|
+
* render team-member lists with profile data ("Bob — operator —
|
|
1970
|
+
* 'Bob the Auditor' avatar X locale fr-FR") in a single pass.
|
|
1971
|
+
*
|
|
1972
|
+
* `userEnvelopeDek` is the vault's `_users` collection DEK
|
|
1973
|
+
* (`vault.getDEK('_users')`); used to decrypt every envelope.
|
|
1974
|
+
*
|
|
1975
|
+
* Principals without a persisted envelope (legacy keyrings predating
|
|
1976
|
+
* the user-envelope feature) come back with `envelope: null`. The
|
|
1977
|
+
* caller chooses how to render — usually "fall back to keyring's
|
|
1978
|
+
* `displayName`".
|
|
1979
|
+
*
|
|
1980
|
+
* Order matches `listUsers()` (store-defined; sort if you need a
|
|
1981
|
+
* stable display order).
|
|
1982
|
+
*/
|
|
1983
|
+
declare function listUsersWithEnvelopes<T = unknown>(adapter: NoydbStore, vault: string, userEnvelopeDek: CryptoKey): Promise<Array<{
|
|
1984
|
+
user: UserInfo;
|
|
1985
|
+
envelope: UserEnvelope<T> | null;
|
|
1986
|
+
}>>;
|
|
1914
1987
|
/**
|
|
1915
1988
|
* Check whether a keyring is authorised for a given `@noy-db/as-*`
|
|
1916
1989
|
* export tier.
|
|
@@ -3342,7 +3415,11 @@ interface GatePolicy {
|
|
|
3342
3415
|
* and use the same engine; the engine treats unknown names with no
|
|
3343
3416
|
* configured policy as "no gate" (no-op).
|
|
3344
3417
|
*/
|
|
3345
|
-
type BuiltInGateName = 'rotate-passphrase' | 'recover-passphrase' | 'enroll-authenticator' | 'remove-authenticator' | 'rotate-unlock' | 'enroll-user' | 'revoke-user' | 'export-bundle' | 'export-plaintext' | 'view-user-auth'
|
|
3418
|
+
type BuiltInGateName = 'rotate-passphrase' | 'recover-passphrase' | 'enroll-authenticator' | 'remove-authenticator' | 'rotate-unlock' | 'enroll-user' | 'revoke-user' | 'export-bundle' | 'export-plaintext' | 'view-user-auth'
|
|
3419
|
+
/** Authorize a write to one's own user envelope (#22). */
|
|
3420
|
+
| 'edit-own-profile'
|
|
3421
|
+
/** Authorize reading other principals' user envelopes (#22). */
|
|
3422
|
+
| 'view-team-profiles';
|
|
3346
3423
|
/** Either a built-in gate name or an `app:*` custom gate. */
|
|
3347
3424
|
type GateName = BuiltInGateName | `app:${string}`;
|
|
3348
3425
|
/**
|
|
@@ -3627,6 +3704,28 @@ declare class Noydb {
|
|
|
3627
3704
|
private getSyncEngine;
|
|
3628
3705
|
on<K extends keyof NoydbEventMap>(event: K, handler: (data: NoydbEventMap[K]) => void): void;
|
|
3629
3706
|
off<K extends keyof NoydbEventMap>(event: K, handler: (data: NoydbEventMap[K]) => void): void;
|
|
3707
|
+
/**
|
|
3708
|
+
* Soft-lock a single vault: clear its in-memory keyring, DEKs, vault
|
|
3709
|
+
* instance, sync engine, policy enforcer, and active-tier entry —
|
|
3710
|
+
* WITHOUT destroying the `Noydb` instance.
|
|
3711
|
+
*
|
|
3712
|
+
* Designed for "lock screen" UX: the user taps **Lock** and DEKs are
|
|
3713
|
+
* scrubbed from memory immediately, but the same `Noydb` instance can
|
|
3714
|
+
* be re-unlocked via {@link unlockViaAuthenticator} (tier 2) or
|
|
3715
|
+
* {@link unlockViaPin} (tier 3) without re-running `createNoydb`.
|
|
3716
|
+
*
|
|
3717
|
+
* **QuickUnlock state is preserved.** That's the whole point — the
|
|
3718
|
+
* user can still resume via PIN without a full credential re-prompt.
|
|
3719
|
+
* The on-disk `_meta/policy` document is also kept in cache (it
|
|
3720
|
+
* survives lock; nothing about it changes when DEKs are scrubbed).
|
|
3721
|
+
*
|
|
3722
|
+
* No-op when `vault` is not currently in cache (idempotent).
|
|
3723
|
+
*
|
|
3724
|
+
* Unblocks vLannaAi/niwat#33.
|
|
3725
|
+
*
|
|
3726
|
+
* @see #17
|
|
3727
|
+
*/
|
|
3728
|
+
lockVault(vault: string): void;
|
|
3630
3729
|
close(): void;
|
|
3631
3730
|
/**
|
|
3632
3731
|
* Returns a snapshot of all translator invocations since the last
|
|
@@ -3718,6 +3817,73 @@ declare class Noydb {
|
|
|
3718
3817
|
}): Promise<void>;
|
|
3719
3818
|
/** Read the slot list for a vault. Internal — `describeAuthConfig` (#13) consumes this. */
|
|
3720
3819
|
listAuthenticators(vault: string): Promise<ReadonlyArray<KeyringAuthenticator>>;
|
|
3820
|
+
/**
|
|
3821
|
+
* Native WebAuthn enrollment using the **real** internal keyring (#16).
|
|
3822
|
+
*
|
|
3823
|
+
* Why this exists: when a consumer is using `createNoydb({ secret })`,
|
|
3824
|
+
* they cannot reach the live `UnlockedKeyring` to feed it to
|
|
3825
|
+
* `enrollWebAuthn(keyring, vault, opts)` from `@noy-db/on-webauthn`.
|
|
3826
|
+
* Constructing a synthetic keyring (the previous workaround) produces
|
|
3827
|
+
* a slot whose `wrapped_kek` references the synthetic payload, not
|
|
3828
|
+
* the live session — so `unlockViaAuthenticator()` later replaces the
|
|
3829
|
+
* live DEK map with stale wrapped DEKs and every decrypt fails.
|
|
3830
|
+
*
|
|
3831
|
+
* This method runs `ceremony` with the REAL keyring (still in
|
|
3832
|
+
* `keyringCache`). The ceremony performs the WebAuthn enrollment and
|
|
3833
|
+
* returns the slot options that hub then persists via the standard
|
|
3834
|
+
* tier-2 enrollAuthenticator path.
|
|
3835
|
+
*
|
|
3836
|
+
* Layering note: hub does not import `@noy-db/on-webauthn` (that
|
|
3837
|
+
* would invert the dep graph). The consumer wires it in:
|
|
3838
|
+
*
|
|
3839
|
+
* ```ts
|
|
3840
|
+
* import { enrollWebAuthn } from '@noy-db/on-webauthn'
|
|
3841
|
+
*
|
|
3842
|
+
* await db.enrollWebAuthn('demo', async (keyring) => {
|
|
3843
|
+
* const e = await enrollWebAuthn(keyring, 'demo', { rp: {...} })
|
|
3844
|
+
* return {
|
|
3845
|
+
* id: `webauthn-${e.credentialId.slice(0, 8)}`,
|
|
3846
|
+
* method: 'webauthn',
|
|
3847
|
+
* wrapped_kek: e.wrappedPayload,
|
|
3848
|
+
* meta: {
|
|
3849
|
+
* credentialId: e.credentialId,
|
|
3850
|
+
* wrapIv: e.wrapIv,
|
|
3851
|
+
* prfUsed: e.prfUsed,
|
|
3852
|
+
* beFlag: e.beFlag,
|
|
3853
|
+
* requireSingleDevice: e.requireSingleDevice,
|
|
3854
|
+
* },
|
|
3855
|
+
* }
|
|
3856
|
+
* })
|
|
3857
|
+
* ```
|
|
3858
|
+
*
|
|
3859
|
+
* Returns the WebAuthn `credentialId` (extracted from `meta.credentialId`)
|
|
3860
|
+
* for the caller's lookup index (a bootstrap vault, a PublicEnvelope,
|
|
3861
|
+
* a server-side allowlist).
|
|
3862
|
+
*
|
|
3863
|
+
* Gated by `enroll-authenticator` like `enrollAuthenticator()` itself.
|
|
3864
|
+
*
|
|
3865
|
+
* @see #16
|
|
3866
|
+
*/
|
|
3867
|
+
enrollWebAuthn(vault: string, ceremony: (keyring: UnlockedKeyring) => Promise<EnrollAuthenticatorOptions>, presented?: {
|
|
3868
|
+
factors?: ReadonlyArray<FactorProof>;
|
|
3869
|
+
sharedDevice?: boolean;
|
|
3870
|
+
}): Promise<{
|
|
3871
|
+
credentialId: string;
|
|
3872
|
+
}>;
|
|
3873
|
+
/**
|
|
3874
|
+
* Filter the slot list to webauthn-method slots only. Useful for
|
|
3875
|
+
* "you have N WebAuthn credentials enrolled" UI surfaces and for
|
|
3876
|
+
* deciding when a new device prompt should appear. Identity is
|
|
3877
|
+
* `id` + `enrolled_at`; the `meta.credentialId` (base64) is used by
|
|
3878
|
+
* `allowCredentials` at unlock time.
|
|
3879
|
+
*
|
|
3880
|
+
* @see #16
|
|
3881
|
+
*/
|
|
3882
|
+
listWebAuthnSlots(vault: string): Promise<ReadonlyArray<{
|
|
3883
|
+
id: string;
|
|
3884
|
+
enrolledAt: string;
|
|
3885
|
+
credentialId: string;
|
|
3886
|
+
}>>;
|
|
3721
3887
|
/**
|
|
3722
3888
|
* Resolve a slot by id, then hand the wrapped-KEK ciphertext + meta
|
|
3723
3889
|
* to the caller-supplied verifier. The verifier is the
|
|
@@ -4317,6 +4483,170 @@ declare function magicLinkGrantRecordId(token: string, index: number): string;
|
|
|
4317
4483
|
*/
|
|
4318
4484
|
declare function isMagicLinkGrantExpired(payload: MagicLinkGrantPayload, now?: Date): boolean;
|
|
4319
4485
|
|
|
4486
|
+
/**
|
|
4487
|
+
* Public `vault.user.*` API surface.
|
|
4488
|
+
*
|
|
4489
|
+
* Three families:
|
|
4490
|
+
* - Write-self: `me` / `updateMe` / `setMe` — always target the writer's
|
|
4491
|
+
* own keyringId. **Own-only write rule** is structural — no method
|
|
4492
|
+
* exists to write someone else's envelope.
|
|
4493
|
+
* - Read-anyone: `get` / `list` — read other principals' envelopes
|
|
4494
|
+
* (subject to `view-team-profiles` policy gate, wired in #22).
|
|
4495
|
+
* - Reactive: `subscribe` / `live` — in-process event emission on local
|
|
4496
|
+
* writes. Cross-instance updates land via the team/sync engine and
|
|
4497
|
+
* surface to subscribers when the sync diff replays through this API.
|
|
4498
|
+
*
|
|
4499
|
+
* @see docs/superpowers/specs/2026-05-05-user-envelope-design.md
|
|
4500
|
+
*
|
|
4501
|
+
* @module
|
|
4502
|
+
*/
|
|
4503
|
+
|
|
4504
|
+
/**
|
|
4505
|
+
* Recursive partial. Used for `updateMe(patch)` so callers can hand in
|
|
4506
|
+
* deeply-nested partial shapes and have them deep-merged onto the
|
|
4507
|
+
* current envelope.
|
|
4508
|
+
*/
|
|
4509
|
+
type DeepPartial<T> = T extends object ? {
|
|
4510
|
+
[P in keyof T]?: DeepPartial<T[P]>;
|
|
4511
|
+
} : T;
|
|
4512
|
+
/** Cancel a previously-registered subscription. */
|
|
4513
|
+
type Unsubscribe = () => void;
|
|
4514
|
+
/**
|
|
4515
|
+
* Optional factor-proof bundle threaded into gated user-envelope
|
|
4516
|
+
* operations. Same shape as `Noydb.checkGate(vault, gate, presented)`
|
|
4517
|
+
* accepts elsewhere — apps that have already presented a TOTP/email-OTP
|
|
4518
|
+
* for this session pass it here to satisfy tightened policies.
|
|
4519
|
+
*/
|
|
4520
|
+
interface UserEnvelopePresented {
|
|
4521
|
+
readonly factors?: readonly FactorProof[];
|
|
4522
|
+
readonly sharedDevice?: boolean;
|
|
4523
|
+
}
|
|
4524
|
+
/**
|
|
4525
|
+
* Callback used by `UserApi` to validate the active session against a
|
|
4526
|
+
* policy gate. Provided by the `Vault` constructor; in production this
|
|
4527
|
+
* delegates to `Noydb.checkGate(vault, gate, presented)`. In tests, a
|
|
4528
|
+
* no-op stub is fine.
|
|
4529
|
+
*/
|
|
4530
|
+
type UserEnvelopeCheckGate = (gate: 'edit-own-profile' | 'view-team-profiles', presented?: UserEnvelopePresented) => Promise<void>;
|
|
4531
|
+
/**
|
|
4532
|
+
* Reactive handle returned by `live()`. `current` is the most recently
|
|
4533
|
+
* observed value; `subscribe(cb)` fires on subsequent local writes.
|
|
4534
|
+
* `stop()` releases the underlying subscription.
|
|
4535
|
+
*/
|
|
4536
|
+
interface LiveUserEnvelope<T> {
|
|
4537
|
+
current(): UserEnvelope<T> | null;
|
|
4538
|
+
subscribe(cb: (env: UserEnvelope<T> | null) => void): Unsubscribe;
|
|
4539
|
+
stop(): void;
|
|
4540
|
+
}
|
|
4541
|
+
/**
|
|
4542
|
+
* Implementation behind `vault.user`. Constructed once per Vault, holds
|
|
4543
|
+
* the writer's keyringId in closure so `updateMe`/`setMe` cannot target
|
|
4544
|
+
* any other principal — the own-only rule is enforced at the type level
|
|
4545
|
+
* (no `set(otherKeyringId, …)` method) AND at runtime (the
|
|
4546
|
+
* keyringId argument simply doesn't exist on the write path).
|
|
4547
|
+
*/
|
|
4548
|
+
declare class UserApi {
|
|
4549
|
+
private readonly adapter;
|
|
4550
|
+
private readonly vaultName;
|
|
4551
|
+
/** The writer's own keyringId. Frozen at construction time. */
|
|
4552
|
+
private readonly writerKeyringId;
|
|
4553
|
+
private readonly getDek;
|
|
4554
|
+
/**
|
|
4555
|
+
* Policy-gate validator. When omitted, gates are skipped — useful
|
|
4556
|
+
* for low-level tests that exercise the storage layer directly.
|
|
4557
|
+
* Production paths always wire the Noydb-backed implementation.
|
|
4558
|
+
*/
|
|
4559
|
+
private readonly checkGate?;
|
|
4560
|
+
/** keyringId → set of listeners. Wildcard '*' fires on every change. */
|
|
4561
|
+
private readonly listeners;
|
|
4562
|
+
constructor(adapter: NoydbStore, vaultName: string,
|
|
4563
|
+
/** The writer's own keyringId. Frozen at construction time. */
|
|
4564
|
+
writerKeyringId: string, getDek: () => Promise<CryptoKey>,
|
|
4565
|
+
/**
|
|
4566
|
+
* Policy-gate validator. When omitted, gates are skipped — useful
|
|
4567
|
+
* for low-level tests that exercise the storage layer directly.
|
|
4568
|
+
* Production paths always wire the Noydb-backed implementation.
|
|
4569
|
+
*/
|
|
4570
|
+
checkGate?: UserEnvelopeCheckGate | undefined);
|
|
4571
|
+
/** Read the writer's own envelope. Returns null if never written. */
|
|
4572
|
+
me<T = unknown>(): Promise<UserEnvelope<T> | null>;
|
|
4573
|
+
/**
|
|
4574
|
+
* Deep-merge a partial patch into the writer's own envelope. Creates
|
|
4575
|
+
* the envelope on first call. Optimistic-concurrency safe — a stale
|
|
4576
|
+
* `_v` (parallel writer on another device) throws `ConflictError`.
|
|
4577
|
+
*
|
|
4578
|
+
* Gated by the `edit-own-profile` policy gate (default `minTier: 3`).
|
|
4579
|
+
* Pass `presented` to satisfy tightened policies that require a
|
|
4580
|
+
* factor proof (e.g. STRICT_POLICY's TOTP requirement).
|
|
4581
|
+
*/
|
|
4582
|
+
updateMe<T extends object = Record<string, unknown>>(patch: DeepPartial<T>, presented?: UserEnvelopePresented): Promise<UserEnvelope<T>>;
|
|
4583
|
+
/**
|
|
4584
|
+
* Replace the writer's own envelope with `payload`. Use sparingly —
|
|
4585
|
+
* `updateMe` is the canonical mutation. No `expectedVersion` check;
|
|
4586
|
+
* callers explicitly take last-write-wins semantics.
|
|
4587
|
+
*
|
|
4588
|
+
* Gated by `edit-own-profile`. See `updateMe` for `presented` usage.
|
|
4589
|
+
*/
|
|
4590
|
+
setMe<T = unknown>(payload: T, presented?: UserEnvelopePresented): Promise<UserEnvelope<T>>;
|
|
4591
|
+
/**
|
|
4592
|
+
* Read another principal's envelope by their keyringId. Returns null
|
|
4593
|
+
* if the principal exists but has no envelope yet, or if the
|
|
4594
|
+
* keyringId does not exist at all.
|
|
4595
|
+
*
|
|
4596
|
+
* Gated by `view-team-profiles` (default `minTier: 2`) — but ONLY for
|
|
4597
|
+
* cross-principal reads. Reading your own envelope (`keyringId ===
|
|
4598
|
+
* self`) is never gated; that's just `me()` written long-form.
|
|
4599
|
+
*/
|
|
4600
|
+
get<T = unknown>(keyringId: string, presented?: UserEnvelopePresented): Promise<UserEnvelope<T> | null>;
|
|
4601
|
+
/**
|
|
4602
|
+
* Read every persisted envelope in the vault. Order is store-defined.
|
|
4603
|
+
*
|
|
4604
|
+
* Gated by `view-team-profiles`. Default policy (`minTier: 2`) lets
|
|
4605
|
+
* any authenticated session read all envelopes. Two privacy-strict
|
|
4606
|
+
* opt-outs:
|
|
4607
|
+
*
|
|
4608
|
+
* - `view-team-profiles.enabled: false` → list() returns only the
|
|
4609
|
+
* caller's own envelope (silent self-fallback, no thrown error).
|
|
4610
|
+
* - `view-team-profiles.minTier: 1` + insufficient tier → throws
|
|
4611
|
+
* `PolicyDeniedError` with `reason: 'insufficient-tier'`. The
|
|
4612
|
+
* caller is expected to elevate, not silently degrade.
|
|
4613
|
+
*
|
|
4614
|
+
* The asymmetry is deliberate: `enabled: false` is a deliberate
|
|
4615
|
+
* design choice ("nobody sees teammate profiles in this app");
|
|
4616
|
+
* `insufficient-tier` is "you need to authenticate further". Different
|
|
4617
|
+
* UX prompts for different intents.
|
|
4618
|
+
*/
|
|
4619
|
+
list<T = unknown>(presented?: UserEnvelopePresented): Promise<UserEnvelope<T>[]>;
|
|
4620
|
+
/**
|
|
4621
|
+
* Listen for changes to a specific keyringId's envelope. The callback
|
|
4622
|
+
* fires synchronously after every successful local `updateMe` /
|
|
4623
|
+
* `setMe` for that principal.
|
|
4624
|
+
*
|
|
4625
|
+
* Cross-instance changes (a teammate edits their profile on their
|
|
4626
|
+
* device, the sync engine pulls the diff onto this device) will fire
|
|
4627
|
+
* subscribers when the sync layer replays the write through this API.
|
|
4628
|
+
* In v1, subscribers do NOT fire on raw store changes — wire your sync
|
|
4629
|
+
* layer to call back through `vault.user.setMe` / `updateMe` if you
|
|
4630
|
+
* need that.
|
|
4631
|
+
*
|
|
4632
|
+
* Pass keyringId `'*'` to fire on every change in the vault.
|
|
4633
|
+
*/
|
|
4634
|
+
subscribe<T = unknown>(keyringId: string, cb: (env: UserEnvelope<T> | null) => void): Unsubscribe;
|
|
4635
|
+
/**
|
|
4636
|
+
* Reactive handle that caches the current value and re-reads on every
|
|
4637
|
+
* change for the given keyringId. Convenient for framework bindings:
|
|
4638
|
+
*
|
|
4639
|
+
* const live = vault.user.live<UserShape>(vault.userId)
|
|
4640
|
+
* live.subscribe(env => render(env?.data))
|
|
4641
|
+
*
|
|
4642
|
+
* Initial value is `null` until the first `current()` call materializes
|
|
4643
|
+
* it via `vault.user.get()`. Call `stop()` when done to release the
|
|
4644
|
+
* subscription.
|
|
4645
|
+
*/
|
|
4646
|
+
live<T = unknown>(keyringId: string): LiveUserEnvelope<T>;
|
|
4647
|
+
private fireChange;
|
|
4648
|
+
}
|
|
4649
|
+
|
|
4320
4650
|
/** A vault (tenant namespace) containing collections. */
|
|
4321
4651
|
declare class Vault {
|
|
4322
4652
|
private readonly adapter;
|
|
@@ -4360,6 +4690,19 @@ declare class Vault {
|
|
|
4360
4690
|
private readonly i18nStrategy;
|
|
4361
4691
|
private readonly syncStrategy;
|
|
4362
4692
|
private getDEK;
|
|
4693
|
+
/**
|
|
4694
|
+
* Per-principal user envelope API.
|
|
4695
|
+
*
|
|
4696
|
+
* - Write-self: `me()`, `updateMe(patch)`, `setMe(payload)` — always
|
|
4697
|
+
* target this vault session's keyringId. There is no method to write
|
|
4698
|
+
* another principal's envelope (own-only write rule, structural).
|
|
4699
|
+
* - Read-anyone: `get(keyringId)`, `list()` — read other principals'
|
|
4700
|
+
* envelopes, subject to the `view-team-profiles` policy gate (#22).
|
|
4701
|
+
* - Reactive: `subscribe(id, cb)`, `live(id)` — fire on local writes.
|
|
4702
|
+
*
|
|
4703
|
+
* @see docs/superpowers/specs/2026-05-05-user-envelope-design.md
|
|
4704
|
+
*/
|
|
4705
|
+
readonly user: UserApi;
|
|
4363
4706
|
/**
|
|
4364
4707
|
* Optional callback that re-derives an UnlockedKeyring from the
|
|
4365
4708
|
* adapter using the active user's passphrase. Called by `load()`
|
|
@@ -7653,6 +7996,23 @@ interface GrantOptions {
|
|
|
7653
7996
|
* `PassphrasePolicy`. Test fixtures and CLI scripts pass `true`.
|
|
7654
7997
|
*/
|
|
7655
7998
|
readonly allowWeakPassphrase?: boolean;
|
|
7999
|
+
/**
|
|
8000
|
+
* Initial user-envelope payload for the new principal. Sealed under
|
|
8001
|
+
* the same vault DEK (the reserved `_users` collection's DEK) and
|
|
8002
|
+
* persisted alongside the keyring during grant.
|
|
8003
|
+
*
|
|
8004
|
+
* **Bootstrap-only.** Once the new user activates and writes their
|
|
8005
|
+
* own envelope, the own-only write rule kicks in — admins cannot
|
|
8006
|
+
* edit a teammate's envelope after activation. Use this field for
|
|
8007
|
+
* pre-fill at invite time (e.g. "displayName: Bob, locale: en-US")
|
|
8008
|
+
* and let the user take over from there.
|
|
8009
|
+
*
|
|
8010
|
+
* Hub does not introspect the payload; it is JSON-serialized and
|
|
8011
|
+
* encrypted opaquely. Apps own the schema.
|
|
8012
|
+
*
|
|
8013
|
+
* @see docs/superpowers/specs/2026-05-05-user-envelope-design.md → Lifecycle
|
|
8014
|
+
*/
|
|
8015
|
+
readonly initialProfile?: unknown;
|
|
7656
8016
|
}
|
|
7657
8017
|
interface RevokeOptions {
|
|
7658
8018
|
readonly userId: string;
|
|
@@ -8346,6 +8706,22 @@ interface NoydbOptions {
|
|
|
8346
8706
|
* turn this on now.
|
|
8347
8707
|
*/
|
|
8348
8708
|
readonly requireRecovery?: boolean;
|
|
8709
|
+
/**
|
|
8710
|
+
* What to do when `openVault` finds an existing keyring in the store that
|
|
8711
|
+
* cannot be decrypted with the supplied credentials (`InvalidKeyError`).
|
|
8712
|
+
*
|
|
8713
|
+
* - `'error'` (default) — propagate the error. The app must prompt the user
|
|
8714
|
+
* to supply the correct credentials or clear both the data and auth stores.
|
|
8715
|
+
* - `'reset'` — delete the stale keyring and re-initialise the vault from
|
|
8716
|
+
* scratch using the current credentials. Use this when the data store can
|
|
8717
|
+
* become detached from the auth store (e.g. the user cleared the IndexedDB
|
|
8718
|
+
* data records but not the keyring row, or a WebAuthn credential was rotated).
|
|
8719
|
+
* **All previously encrypted data is unrecoverable after a reset.**
|
|
8720
|
+
*
|
|
8721
|
+
* Only applies to the passphrase (`secret`) path. When `getKeyring` is used,
|
|
8722
|
+
* the callback is responsible for handling stale-keyring detection itself.
|
|
8723
|
+
*/
|
|
8724
|
+
readonly onInvalidKey?: 'error' | 'reset';
|
|
8349
8725
|
/**
|
|
8350
8726
|
* Enable the public envelope subsystem (`docs/subsystems/public-envelope.md`).
|
|
8351
8727
|
* Pass `true` for the default schema (every standard field, 256 KB
|
|
@@ -8454,4 +8830,4 @@ interface DeleteManyResult {
|
|
|
8454
8830
|
}>;
|
|
8455
8831
|
}
|
|
8456
8832
|
|
|
8457
|
-
export { type ConsentAuditEntry as $, type BlobObject as A, type BlobStrategy as B, type BlobPutOptions as C, DICT_COLLECTION_PREFIX as D, type BlobResponseOptions as E, BlobSet as F, type BlobStrategyOpenArgs as G, type CompactRunOptions as H, type I18nStrategy as I, type CompactionContext as J, type CompactionResult as K, DEFAULT_CHUNK_SIZE as L, EXPORT_AUDIT_COLLECTION as M, ExportBlobsAbortedError as N, type ExportBlobsAuditEntry as O, PolicyEnforcer as P, type ExportBlobsHandle as Q, type ExportBlobsOptions as R, type SessionStrategy as S, type ExportedBlob as T, type SlotInfo as U, type SlotRecord as V, type VersionRecord as W, createExportBlobsHandle as X, runCompaction as Y, type ConsentStrategy as Z, CONSENT_AUDIT_COLLECTION as _, type DictEntry as a, type
|
|
8833
|
+
export { type ConsentAuditEntry as $, type BlobObject as A, type BlobStrategy as B, type BlobPutOptions as C, DICT_COLLECTION_PREFIX as D, type BlobResponseOptions as E, BlobSet as F, type BlobStrategyOpenArgs as G, type CompactRunOptions as H, type I18nStrategy as I, type CompactionContext as J, type CompactionResult as K, DEFAULT_CHUNK_SIZE as L, EXPORT_AUDIT_COLLECTION as M, ExportBlobsAbortedError as N, type ExportBlobsAuditEntry as O, PolicyEnforcer as P, type ExportBlobsHandle as Q, type ExportBlobsOptions as R, type SessionStrategy as S, type ExportedBlob as T, type SlotInfo as U, type SlotRecord as V, type VersionRecord as W, createExportBlobsHandle as X, runCompaction as Y, type ConsentStrategy as Z, CONSENT_AUDIT_COLLECTION as _, type DictEntry as a, type BuiltInGateName as a$, type ConsentAuditFilter as a0, type ConsentContext as a1, type ConsentOp as a2, loadConsentEntries as a3, writeConsentEntry as a4, type PeriodsStrategy as a5, type CarryForwardContext as a6, type ClosePeriodOptions as a7, type OpenPeriodOptions as a8, PERIODS_COLLECTION as a9, type DiffEntry as aA, type JsonPatch as aB, type JsonPatchOp as aC, type LedgerEntry as aD, LedgerStore as aE, type VaultEngine as aF, VaultInstant as aG, type VerifyResult as aH, applyPatch as aI, canonicalJson as aJ, computePatch as aK, diff as aL, formatDiff as aM, hashEntry as aN, paddedIndex as aO, parseIndex as aP, sha256Hex as aQ, type UserEnvelope as aR, type PublicEnvelope as aS, type GateName as aT, type GatePolicy as aU, type VaultPolicy as aV, type ActiveTier as aW, type FactorProof as aX, Vault as aY, type AccessibleVault as aZ, BUNDLE_STORE_POLICY as a_, type PeriodRecord as aa, type ReadOnlyCollection as ab, appendPeriodLedgerEntry as ac, assertTsWritable as ad, chainAnchor as ae, loadPeriods as af, validatePeriodName as ag, type ShadowStrategy as ah, CollectionFrame as ai, VaultFrame as aj, type TxStrategy as ak, TxCollection as al, TxContext as am, TxVault as an, runTransaction as ao, type SyncStrategy as ap, type Role as aq, type UnlockedKeyring as ar, type HistoryStrategy as as, type NoydbStore as at, type HistoryOptions as au, type EncryptedEnvelope as av, type PruneOptions as aw, type AppendInput as ax, type ChangeType as ay, CollectionInstant as az, type DictKeyDescriptor as b, type Permissions as b$, type BundleRecipient as b0, type CacheOptions as b1, type CacheStats as b2, type ChangeEvent as b3, Collection as b4, type CollectionChangeEvent as b5, type CollectionConflictResolver as b6, type Conflict as b7, type ConflictPolicy as b8, type ConflictStrategy as b9, type KeyringFile as bA, type ListAccessibleVaultsOptions as bB, type ListPageResult as bC, type LiveUserEnvelope as bD, type LocaleReadOptions as bE, Lru as bF, type LruOptions as bG, type LruStats as bH, MAGIC_LINK_CONTENT_INFO_PREFIX as bI, MAGIC_LINK_GRANTS_COLLECTION as bJ, MAGIC_LINK_KEK_INFO_PREFIX as bK, type MagicLinkGrantPayload as bL, type MagicLinkGrantRecord as bM, NOYDB_BACKUP_VERSION as bN, NOYDB_FORMAT_VERSION as bO, NOYDB_KEYRING_VERSION as bP, NOYDB_SYNC_VERSION as bQ, Noydb as bR, type NoydbBundleStore as bS, type NoydbEventMap as bT, type NoydbOptions as bU, PUBLIC_ENVELOPE_FIELDS as bV, type PaperRecoveryDoc as bW, type PaperRecoveryEntry as bX, type PassphrasePolicy as bY, type PassphraseValidationResult as bZ, type Permission as b_, type CrossTierAccessEvent as ba, DEFAULT_PUBLIC_ENVELOPE_SCHEMA as bb, DELEGATIONS_COLLECTION as bc, type DeepPartial as bd, type DelegationToken as be, type DeleteManyResult as bf, type DirtyEntry as bg, ELEVATION_AUDIT_COLLECTION as bh, ElevatedHandle as bi, type EnrollAuthenticatorOptions as bj, type ExportCapability as bk, type ExportChunk as bl, type ExportFormat as bm, type ExportStreamOptions as bn, type FactorKind as bo, type FactorRequirement as bp, type GhostRecord as bq, type GrantOptions as br, type HistoryConfig as bs, type HistoryEntry as bt, INDEXED_STORE_POLICY as bu, type ImportCapability as bv, type InferOutput as bw, type IssueDelegationOptions as bx, type IssueMagicLinkGrantOptions as by, type KeyringAuthenticator as bz, DictionaryHandle as c, assertStrongPassphrase as c$, type PlaintextTranslatorContext as c0, type PlaintextTranslatorFn as c1, PresenceHandle as c2, type PresencePeer as c3, type PublicEnvelopeField as c4, type PublicEnvelopeSchema as c5, type PublicEnvelopeText as c6, type PullMode as c7, type PullOptions as c8, type PullPolicy as c9, SyncEngine as cA, type SyncMetadata as cB, type SyncPolicy as cC, SyncScheduler as cD, type SyncSchedulerStatus as cE, type SyncStatus as cF, type SyncTarget as cG, type SyncTargetRole as cH, SyncTransaction as cI, type SyncTransactionResult as cJ, type TierMode as cK, type TranslatorAuditEntry as cL, type TxOp as cM, USER_ENVELOPE_COLLECTION as cN, USER_ENVELOPE_MAX_BYTES as cO, type Unsubscribe as cP, UserApi as cQ, type UserEnvelopeCheckGate as cR, UserEnvelopeOversizedError as cS, type UserEnvelopePresented as cT, type UserInfo as cU, type VaultBackup as cV, type VaultPolicyOnDisk as cW, type VaultSnapshot as cX, type WarningRules as cY, WeakPassphraseError as cZ, type WeakPassphraseReason as c_, type PullResult as ca, type PushMode as cb, type PushOptions as cc, type PushPolicy as cd, type PushResult as ce, type PutManyItemOptions as cf, type PutManyOptions as cg, type PutManyResult as ch, type QueryAcrossOptions as ci, type QueryAcrossResult as cj, type QuickUnlockState as ck, QuickUnlockStore as cl, type ReAuthOperation as cm, type RecoverPassphraseInput as cn, type RecoveryProof as co, type ResolvedPublicEnvelopeSchema as cp, type RevokeOptions as cq, type RotatePassphraseInput as cr, type SessionPolicy as cs, type SetPublicEnvelopeInput as ct, type StandardSchemaV1 as cu, type StandardSchemaV1Issue as cv, type StandardSchemaV1SyncResult as cw, type StoreAuth as cx, type StoreAuthKind as cy, type StoreCapabilities as cz, type DictionaryOptions as d, buildRecipientKeyringFile as d0, burnPaperRecoveryEntry as d1, createNoydb as d2, createStore as d3, deriveMagicLinkContentKey as d4, enrollAuthenticator as d5, estimateEntropy as d6, evaluateExportCapability as d7, evaluateImportCapability as d8, findAuthenticator as d9, writeMagicLinkGrant as dA, hasExportCapability as da, hasImportCapability as db, hasRecoveryEnrolled as dc, isMagicLinkGrantExpired as dd, isPublicEnvelope as de, issueDelegation as df, recoverPassphrase as dg, rotatePassphrase as dh, listMagicLinkGrants as di, listUsers as dj, listUsersWithEnvelopes as dk, loadActiveDelegations as dl, loadPaperRecoveryEntries as dm, magicLinkGrantRecordId as dn, readMagicLinkGrantRecord as dp, removeAuthenticator as dq, resolveSchema as dr, revokeDelegation as ds, revokeMagicLinkGrant as dt, savePaperRecoveryEntries as du, unwrapMagicLinkGrant as dv, validatePassphrase as dw, validatePublicEnvelopeInput as dx, validateSchemaInput as dy, validateSchemaOutput as dz, type I18nTextDescriptor as e, type I18nTextOptions as f, applyI18nLocale as g, dictCollectionName as h, dictKey as i, i18nText as j, isDictCollectionName as k, isDictKeyDescriptor as l, isI18nTextDescriptor as m, createEnforcer as n, validateSessionPolicy as o, BLOB_CHUNKS_COLLECTION as p, BLOB_COLLECTION as q, resolveI18nText as r, BLOB_EVICTION_AUDIT_COLLECTION as s, BLOB_INDEX_COLLECTION as t, BLOB_SLOTS_PREFIX as u, validateI18nTextValue as v, BLOB_VERSIONS_PREFIX as w, type BlobEvictionEntry as x, type BlobFieldPolicy as y, type BlobFieldsConfig as z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noy-db/hub",
|
|
3
|
-
"version": "0.1.0-pre.
|
|
3
|
+
"version": "0.1.0-pre.7",
|
|
4
4
|
"description": "Zero-knowledge, offline-first, encrypted document store — core library with AES-256-GCM, PBKDF2, multi-user keyring, and sync engine",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "vLannaAi <vicio@lanna.ai>",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/validation.ts","../src/team/keyring.ts"],"sourcesContent":["/**\n * Passphrase validation — phrase format (per the three-tier session-tiers\n * design, locked 2026-05-04).\n *\n * Passphrases are **phrases**: multiple simple words, easy to remember,\n * structurally constrained so a weak choice cannot silently collapse the\n * security floor. The format is intentionally narrow: lowercase letters\n * and single spaces only, no punctuation, no symbols, no digits.\n *\n * - Default minimum: 6 words (~77 bits with the 7,776-word EFF list).\n * - Strict minimum: 8 words (~103 bits).\n * - Per-word minimum: 3 characters (excludes \"a\", \"is\", \"of\").\n * - Adjacent repeats rejected (\"the the\").\n *\n * The hub runs validation default-on at every passphrase ingress\n * (`createOwnerKeyring`, `grant`, `rotatePassphrase`); test fixtures and\n * CLI scripts override via `{ allowWeakPassphrase: true }`.\n *\n * @module\n */\nimport { NoydbError, ValidationError } from './errors.js'\n\n/** All reasons a phrase can be rejected. */\nexport type WeakPassphraseReason =\n | 'empty'\n | 'invalid-chars'\n | 'leading-or-trailing-space'\n | 'double-space'\n | 'too-few-words'\n | 'word-too-short'\n | 'repeated-adjacent'\n\n/** Per-vault knobs. Aligns with `VaultPolicy.passphrase`. */\nexport interface PassphrasePolicy {\n /** Minimum number of words. Default 6. Strict policy uses 8. */\n readonly minWords?: number\n /** Minimum characters per word. Default 3. */\n readonly minWordLength?: number\n /** Reject adjacent identical words (\"the the\"). Default true. */\n readonly rejectRepeatedAdjacent?: boolean\n}\n\n/** Result of a check. Discriminated union — compile-time exhaustive. */\nexport type PassphraseValidationResult =\n | { readonly ok: true; readonly words: number }\n | {\n readonly ok: false\n readonly reason: WeakPassphraseReason\n readonly minimum?: number\n readonly got?: number\n }\n\n/**\n * Thrown by `assertStrongPassphrase()` and by every hub ingress\n * point (`createOwnerKeyring`, `grant`, `rotatePassphrase`) when a\n * supplied phrase fails the structural rules above.\n */\nexport class WeakPassphraseError extends NoydbError {\n readonly reason: WeakPassphraseReason\n readonly suggestion: string\n constructor(reason: WeakPassphraseReason, suggestion: string) {\n super('WEAK_PASSPHRASE', `Weak passphrase (${reason}). ${suggestion}`)\n this.name = 'WeakPassphraseError'\n this.reason = reason\n this.suggestion = suggestion\n }\n}\n\nconst DEFAULT_MIN_WORDS = 6\nconst DEFAULT_MIN_WORD_LENGTH = 3\n\nconst SUGGESTIONS: Record<WeakPassphraseReason, string> = {\n empty: 'Provide a phrase of at least 6 lowercase words separated by single spaces.',\n 'invalid-chars':\n 'Use only lowercase letters [a-z] and single spaces. No punctuation, symbols, digits, or uppercase.',\n 'leading-or-trailing-space': 'Trim leading and trailing spaces.',\n 'double-space': 'Use exactly one space between words.',\n 'too-few-words':\n 'Use at least 6 words by default (8 under strict policy). Example: \"correct horse battery staple printer toaster\".',\n 'word-too-short': 'Each word must be at least 3 characters. Drop short fillers like \"a\", \"is\", \"of\".',\n 'repeated-adjacent': 'Avoid repeating the same word twice in a row.',\n}\n\n/**\n * Inspect a phrase against the format rules and return a structured\n * verdict. Never throws — callers either branch on `ok` or pass the\n * result to {@link assertStrongPassphrase} for the throwing flavour.\n */\nexport function validatePassphrase(\n s: string,\n opts?: PassphrasePolicy,\n): PassphraseValidationResult {\n const minWords = opts?.minWords ?? DEFAULT_MIN_WORDS\n const minWordLength = opts?.minWordLength ?? DEFAULT_MIN_WORD_LENGTH\n const rejectRepeated = opts?.rejectRepeatedAdjacent ?? true\n\n if (s.length === 0) {\n return { ok: false, reason: 'empty' }\n }\n\n if (s !== s.trim()) {\n return { ok: false, reason: 'leading-or-trailing-space' }\n }\n\n if (s.includes(' ')) {\n return { ok: false, reason: 'double-space' }\n }\n\n if (!/^[a-z]+( [a-z]+)*$/.test(s)) {\n return { ok: false, reason: 'invalid-chars' }\n }\n\n const words = s.split(' ')\n\n if (words.length < minWords) {\n return { ok: false, reason: 'too-few-words', minimum: minWords, got: words.length }\n }\n\n for (const w of words) {\n if (w.length < minWordLength) {\n return { ok: false, reason: 'word-too-short', minimum: minWordLength, got: w.length }\n }\n }\n\n if (rejectRepeated) {\n for (let i = 1; i < words.length; i++) {\n if (words[i] === words[i - 1]) {\n return { ok: false, reason: 'repeated-adjacent' }\n }\n }\n }\n\n return { ok: true, words: words.length }\n}\n\n/**\n * Throw {@link WeakPassphraseError} when the phrase fails. Used by\n * `createOwnerKeyring`, `grant`, and `rotatePassphrase` at ingress.\n *\n * Pass `{ allowWeakPassphrase: true }` to bypass — intended for test\n * fixtures, CLI scripts, and dev environments. The override never\n * loosens the cryptographic key derivation; it only relaxes the\n * structural-strength gate.\n */\nexport function assertStrongPassphrase(\n s: string,\n opts?: PassphrasePolicy & { allowWeakPassphrase?: boolean },\n): void {\n if (opts?.allowWeakPassphrase) return\n const result = validatePassphrase(s, opts)\n if (result.ok) return\n throw new WeakPassphraseError(result.reason, SUGGESTIONS[result.reason])\n}\n\n/**\n * Estimate the entropy of a phrase, given the EFF 7,776-word list as\n * the assumed wordlist. ~12.9 bits per word.\n *\n * Returns 0 for any input that fails the phrase format — character-class\n * estimates aren't comparable to phrase entropy, and surfacing 0 makes\n * weak inputs visible in any UI that displays an entropy meter.\n */\nexport function estimateEntropy(passphrase: string): number {\n const result = validatePassphrase(passphrase)\n if (!result.ok) return 0\n return Math.round(result.words * Math.log2(7776))\n}\n\n/**\n * Internal compatibility shim. Older code paths used the throwing\n * `validatePassphrase(s)` directly; some still do via re-exports. Routes\n * to the new `assertStrongPassphrase` so the contract holds for both\n * shapes during the transition. New code should call\n * {@link assertStrongPassphrase} directly.\n *\n * @internal\n */\nexport function legacyAssertPassphrase(s: string): void {\n try {\n assertStrongPassphrase(s)\n } catch (err) {\n if (err instanceof WeakPassphraseError) {\n throw new ValidationError(err.message)\n }\n throw err\n }\n}\n","import type { NoydbStore, KeyringFile, KeyringAuthenticator, Role, Permissions, GrantOptions, RevokeOptions, UserInfo, EncryptedEnvelope, ExportCapability, ExportFormat, ImportCapability, VaultPolicyOnDisk } from '../types.js'\nimport { NOYDB_KEYRING_VERSION, NOYDB_FORMAT_VERSION } from '../types.js'\nimport {\n deriveKey,\n generateDEK,\n generateSalt,\n wrapKey,\n unwrapKey,\n encrypt,\n decrypt,\n bufferToBase64,\n base64ToBuffer,\n} from '../crypto.js'\nimport { NoAccessError, PermissionDeniedError, PrivilegeEscalationError, KeyringExpiredError } from '../errors.js'\nimport { assertStrongPassphrase, type PassphrasePolicy } from '../validation.js'\n\n// ─── Roles that can grant/revoke ───────────────────────────────────────\n\n/**\n * Roles that an `admin` is allowed to grant and revoke.\n *\n * Includes `'admin'` itself: the model bottlenecked all admin\n * onboarding through the single `owner` principal, which made lateral\n * delegation impossible and left a single-owner bus-factor risk\n * unresolved even when multiple trusted humans existed. opens up\n * admin↔admin lateral delegation, with two guardrails:\n *\n * 1. **No privilege escalation.** Enforced in `grant()`: every DEK\n * wrapped into the new admin's keyring must be present in the\n * grantor's own DEK set. Today this is structurally trivially\n * true (admin grants always inherit the full caller DEK set),\n * but the check is wired in so future per-collection admin scoping\n * cannot accidentally bypass it. See `PrivilegeEscalationError`.\n *\n * 2. **Cascade on revoke.** Enforced in `revoke()`: when an admin is\n * revoked, every admin they (transitively) granted is either\n * revoked too (`cascade: 'strict'`, default) or left in place with\n * a console warning (`cascade: 'warn'`). The walk uses the\n * `granted_by` field on each keyring file as the parent pointer.\n */\nconst ADMIN_GRANTABLE_TARGETS: readonly Role[] = ['operator', 'viewer', 'client', 'admin']\n\nfunction canGrant(callerRole: Role, targetRole: Role): boolean {\n if (callerRole === 'owner') return true\n if (callerRole === 'admin') return ADMIN_GRANTABLE_TARGETS.includes(targetRole)\n return false\n}\n\nfunction canRevoke(callerRole: Role, targetRole: Role): boolean {\n if (targetRole === 'owner') return false // owner cannot be revoked\n if (callerRole === 'owner') return true\n if (callerRole === 'admin') return ADMIN_GRANTABLE_TARGETS.includes(targetRole)\n return false\n}\n\n// ─── Unlocked Keyring ──────────────────────────────────────────────────\n\n/** In-memory representation of an unlocked keyring. */\nexport interface UnlockedKeyring {\n readonly userId: string\n readonly displayName: string\n readonly role: Role\n readonly permissions: Permissions\n readonly deks: Map<string, CryptoKey>\n readonly kek: CryptoKey\n readonly salt: Uint8Array\n /**\n * `@noy-db/as-*` export capability. Absent when the\n * keyring was written before this RFC landed — role-based defaults\n * apply via `hasExportCapability`.\n */\n readonly exportCapability?: ExportCapability\n /**\n * `@noy-db/as-*` import capability (issue ). Absent when the\n * keyring was written before landed — default-closed semantics\n * apply via `hasImportCapability` (no plaintext format granted, no\n * bundle import granted, regardless of role).\n */\n readonly importCapability?: ImportCapability\n /**\n * Tier-2 authenticator slots — readonly snapshot loaded from the\n * keyring file. Mutations go through `enrollAuthenticator` /\n * `removeAuthenticator` (issue #11), which write back via\n * `persistKeyring`. Always defined; loads with an empty array for\n * keyrings written before the multi-slot extension landed.\n */\n readonly authenticators: readonly KeyringAuthenticator[]\n /**\n * Reserved per-keyring policy override (forward-compat for Option C\n * — see {@link VaultPolicyOnDisk}). v1.0 round-trips this field but\n * never enforces it; the gate engine uses `_meta/policy` only.\n */\n readonly policy?: VaultPolicyOnDisk\n}\n\n// ─── Load / Create ─────────────────────────────────────────────────────\n\n/** Load and unlock a user's keyring for a vault. */\nexport async function loadKeyring(\n adapter: NoydbStore,\n vault: string,\n userId: string,\n passphrase: string,\n): Promise<UnlockedKeyring> {\n const envelope = await adapter.get(vault, '_keyring', userId)\n\n if (!envelope) {\n throw new NoAccessError(`No keyring found for user \"${userId}\" in vault \"${vault}\"`)\n }\n\n const keyringFile = JSON.parse(envelope._data) as KeyringFile\n\n // — refuse to unwrap an expired slot. Check happens before any\n // KEK derivation so an expired slot doesn't leak timing on the\n // passphrase. Comparison uses Date.parse → ms-since-epoch; an\n // unparseable expires_at is treated as \"no expiry\" so a malformed\n // value can't silently lock users out (it'll surface in tests).\n if (keyringFile.expires_at !== undefined) {\n const cutoff = Date.parse(keyringFile.expires_at)\n if (Number.isFinite(cutoff) && Date.now() >= cutoff) {\n throw new KeyringExpiredError({ userId: keyringFile.user_id, expiresAt: keyringFile.expires_at })\n }\n }\n\n const salt = base64ToBuffer(keyringFile.salt)\n const kek = await deriveKey(passphrase, salt)\n\n const deks = new Map<string, CryptoKey>()\n for (const [collName, wrappedDek] of Object.entries(keyringFile.deks)) {\n const dek = await unwrapKey(wrappedDek, kek)\n deks.set(collName, dek)\n }\n\n return {\n userId: keyringFile.user_id,\n displayName: keyringFile.display_name,\n role: keyringFile.role,\n permissions: keyringFile.permissions,\n deks,\n kek,\n salt,\n authenticators: keyringFile.authenticators ?? [],\n ...(keyringFile.export_capability !== undefined && { exportCapability: keyringFile.export_capability }),\n ...(keyringFile.import_capability !== undefined && { importCapability: keyringFile.import_capability }),\n ...(keyringFile.policy !== undefined && { policy: keyringFile.policy }),\n }\n}\n\n/**\n * Create the initial owner keyring for a new vault.\n *\n * Pass `{ validate: true }` (or a `PassphrasePolicy`) to gate creation\n * on the phrase-format strength rules — `Noydb` threads this from\n * `NoydbOptions.validatePassphrase`. Direct callers (CLI, scripts,\n * test fixtures) opt in explicitly.\n */\nexport async function createOwnerKeyring(\n adapter: NoydbStore,\n vault: string,\n userId: string,\n passphrase: string,\n passphraseOpts?: PassphrasePolicy & { validate?: boolean; allowWeakPassphrase?: boolean },\n): Promise<UnlockedKeyring> {\n if (passphraseOpts?.validate && !passphraseOpts.allowWeakPassphrase) {\n assertStrongPassphrase(passphrase, passphraseOpts)\n }\n const salt = generateSalt()\n const kek = await deriveKey(passphrase, salt)\n\n const keyringFile: KeyringFile = {\n _noydb_keyring: NOYDB_KEYRING_VERSION,\n user_id: userId,\n display_name: userId,\n role: 'owner',\n permissions: {},\n deks: {},\n salt: bufferToBase64(salt),\n created_at: new Date().toISOString(),\n granted_by: userId,\n }\n\n await writeKeyringFile(adapter, vault, userId, keyringFile)\n\n return {\n userId,\n displayName: userId,\n role: 'owner',\n permissions: {},\n deks: new Map(),\n kek,\n salt,\n authenticators: [],\n }\n}\n\n// ─── Grant ─────────────────────────────────────────────────────────────\n\n/** Grant access to a new user. Caller must have grant privilege. */\nexport async function grant(\n adapter: NoydbStore,\n vault: string,\n callerKeyring: UnlockedKeyring,\n options: GrantOptions,\n): Promise<void> {\n if (!canGrant(callerKeyring.role, options.role)) {\n throw new PermissionDeniedError(\n `Role \"${callerKeyring.role}\" cannot grant role \"${options.role}\"`,\n )\n }\n\n // Optional strength validation — opt-in via grant({ validatePassphrase: true })\n // or via the calling Noydb's NoydbOptions.validatePassphrase flag.\n // The override `allowWeakPassphrase: true` skips even when validate is on.\n if (\n (options as { validatePassphrase?: boolean }).validatePassphrase &&\n !options.allowWeakPassphrase\n ) {\n assertStrongPassphrase(options.passphrase)\n }\n\n // Determine which collections the new user gets access to\n const permissions = resolvePermissions(options.role, options.permissions)\n\n // Derive the new user's KEK from their passphrase\n const newSalt = generateSalt()\n const newKek = await deriveKey(options.passphrase, newSalt)\n\n // Wrap the appropriate DEKs with the new user's KEK\n const wrappedDeks: Record<string, string> = {}\n for (const collName of Object.keys(permissions)) {\n const dek = callerKeyring.deks.get(collName)\n if (dek) {\n wrappedDeks[collName] = await wrapKey(dek, newKek)\n }\n }\n\n // For owner/admin/viewer roles, wrap ALL known DEKs\n if (options.role === 'owner' || options.role === 'admin' || options.role === 'viewer') {\n for (const [collName, dek] of callerKeyring.deks) {\n if (!(collName in wrappedDeks)) {\n wrappedDeks[collName] = await wrapKey(dek, newKek)\n }\n }\n }\n\n // For ALL roles, propagate system-prefixed collection DEKs\n // (`_ledger`, `_history`, `_sync`, …). These are internal collections\n // that any user with access to the vault must be able to\n // read and write — for example, the hash-chained ledger writes\n // an entry on every put/delete, so operators and clients with write\n // access to a single data collection still need the `_ledger` DEK.\n //\n // Trade-off: a granted user can decrypt every system-collection\n // entry, including ones they would not otherwise have access to\n // (e.g., an operator on `invoices` can read ledger entries for\n // mutations in `salaries`). This is a metadata leak, not a\n // plaintext leak — the ledger entries record collection names,\n // record ids, and ciphertext hashes, but never plaintext records.\n // Per-collection ledger DEKs are tracked as a follow-up.\n for (const [collName, dek] of callerKeyring.deks) {\n if (collName.startsWith('_') && !(collName in wrappedDeks)) {\n wrappedDeks[collName] = await wrapKey(dek, newKek)\n }\n }\n\n // Anti-privilege-escalation check. Every DEK we just\n // wrapped into the new keyring must come from the caller's own DEK\n // set — the grantor cannot give the grantee access to a collection\n // they themselves can't read. Today this is structurally trivially\n // satisfied because every wrapped DEK was looked up in\n // `callerKeyring.deks` above, but the explicit check is wired in\n // so a future change (per-collection admin scoping, escrow-based\n // re-wrapping, etc.) cannot accidentally let a widening grant\n // through. See `PrivilegeEscalationError` for the rationale.\n for (const collName of Object.keys(wrappedDeks)) {\n if (!callerKeyring.deks.has(collName)) {\n throw new PrivilegeEscalationError(collName)\n }\n }\n\n const keyringFile: KeyringFile = {\n _noydb_keyring: NOYDB_KEYRING_VERSION,\n user_id: options.userId,\n display_name: options.displayName,\n role: options.role,\n permissions,\n deks: wrappedDeks,\n salt: bufferToBase64(newSalt),\n created_at: new Date().toISOString(),\n granted_by: callerKeyring.userId,\n ...(options.exportCapability !== undefined && { export_capability: options.exportCapability }),\n ...(options.importCapability !== undefined && { import_capability: options.importCapability }),\n }\n\n await writeKeyringFile(adapter, vault, options.userId, keyringFile)\n}\n\n// ─── Revoke ────────────────────────────────────────────────────────────\n\n/**\n * Walk every keyring in the vault to find admins that the given\n * `rootUserId` (transitively) granted, via the `granted_by` parent\n * pointer recorded on each keyring file.\n *\n * Returns the set of descendant admin user-ids in DFS order, NOT\n * including the root itself. Non-admin descendants are excluded\n * because operators/viewers/clients cannot grant other users — they\n * are leaves in the delegation tree and cleaning them up is the\n * caller's job (or the next rotate, since they'd lose key access\n * anyway when the cascading admin's collections rotate).\n *\n * The walk uses a visited set keyed by user-id so cycles introduced\n * by re-grants (admin-A revoked, then re-granted later by admin-B who\n * was originally granted by A) terminate cleanly.\n */\nasync function findAdminDescendants(\n adapter: NoydbStore,\n vault: string,\n rootUserId: string,\n): Promise<string[]> {\n const allUserIds = await adapter.list(vault, '_keyring')\n\n // Build a map: parentUserId → child KeyringFiles. We only ever\n // descend into admins, so non-admin children are skipped at the\n // edge level rather than after a recursive call.\n const childrenByParent = new Map<string, string[]>()\n for (const userId of allUserIds) {\n const env = await adapter.get(vault, '_keyring', userId)\n if (!env) continue\n const kf = JSON.parse(env._data) as KeyringFile\n if (kf.role !== 'admin') continue // only admins can grant — leaves are uninteresting\n if (kf.user_id === rootUserId) continue // self-edges are noise\n const list = childrenByParent.get(kf.granted_by) ?? []\n list.push(kf.user_id)\n childrenByParent.set(kf.granted_by, list)\n }\n\n const visited = new Set<string>()\n const order: string[] = []\n const stack: string[] = [...(childrenByParent.get(rootUserId) ?? [])]\n while (stack.length > 0) {\n const next = stack.pop()!\n if (visited.has(next)) continue\n visited.add(next)\n order.push(next)\n for (const grandchild of childrenByParent.get(next) ?? []) {\n if (!visited.has(grandchild)) stack.push(grandchild)\n }\n }\n return order\n}\n\n/** Revoke a user's access. Optionally rotate keys for affected collections. */\nexport async function revoke(\n adapter: NoydbStore,\n vault: string,\n callerKeyring: UnlockedKeyring,\n options: RevokeOptions,\n): Promise<void> {\n // Load the target's keyring to check their role\n const targetEnvelope = await adapter.get(vault, '_keyring', options.userId)\n if (!targetEnvelope) {\n throw new NoAccessError(`User \"${options.userId}\" has no keyring in vault \"${vault}\"`)\n }\n\n const targetKeyring = JSON.parse(targetEnvelope._data) as KeyringFile\n\n if (!canRevoke(callerKeyring.role, targetKeyring.role)) {\n throw new PermissionDeniedError(\n `Role \"${callerKeyring.role}\" cannot revoke role \"${targetKeyring.role}\"`,\n )\n }\n\n // Cascade-on-revoke. Only meaningful when the target is\n // an admin — operators/viewers/clients cannot grant other users so\n // they have no delegation subtree to walk.\n const cascadeMode = options.cascade ?? 'strict'\n const usersToRevoke: string[] = [options.userId]\n const affectedCollections = new Set(Object.keys(targetKeyring.deks))\n\n if (targetKeyring.role === 'admin') {\n const descendants = await findAdminDescendants(adapter, vault, options.userId)\n if (descendants.length > 0) {\n if (cascadeMode === 'warn') {\n // Diagnostic mode: leave the descendants in place but make\n // them visible. The owner / a different admin can clean up\n // manually. The single console.warn is intentionally noisy\n // (a list, not a count) so the operator sees exactly which\n // keyrings will become orphans.\n console.warn(\n `[noy-db] revoke(${options.userId}): cascade='warn' — leaving ` +\n `${descendants.length} descendant admin(s) in place: ` +\n `${descendants.join(', ')}. These admins were granted by the revoked user ` +\n `(transitively) and will become orphans in the delegation tree.`,\n )\n } else {\n // Strict mode (default): pull every descendant into the\n // revoke set. We collect their affected collections too so\n // the single rotation pass at the end covers everything.\n for (const userId of descendants) {\n const descEnv = await adapter.get(vault, '_keyring', userId)\n if (!descEnv) continue\n const descKf = JSON.parse(descEnv._data) as KeyringFile\n usersToRevoke.push(userId)\n for (const c of Object.keys(descKf.deks)) affectedCollections.add(c)\n }\n }\n }\n }\n\n // Delete every keyring in the revoke set. Order doesn't matter\n // because each keyring file is independent on disk; we don't have\n // referential integrity to maintain across deletes.\n for (const userId of usersToRevoke) {\n await adapter.delete(vault, '_keyring', userId)\n }\n\n // Single rotation pass at the end. The cost is O(records in\n // affected collections), NOT O(records × cascade depth) — every\n // descendant's collections were unioned into `affectedCollections`\n // before we got here, so the rotation re-encrypts each affected\n // record exactly once regardless of how deep the cascade went.\n if (options.rotateKeys !== false && affectedCollections.size > 0) {\n await rotateKeys(adapter, vault, callerKeyring, [...affectedCollections])\n }\n}\n\n// ─── Key Rotation ──────────────────────────────────────────────────────\n\n/**\n * Rotate DEKs for specified collections:\n * 1. Generate new DEKs\n * 2. Re-encrypt all records in affected collections\n * 3. Re-wrap new DEKs for all remaining users\n */\nexport async function rotateKeys(\n adapter: NoydbStore,\n vault: string,\n callerKeyring: UnlockedKeyring,\n collections: string[],\n): Promise<void> {\n // Generate new DEKs for each affected collection\n const newDeks = new Map<string, CryptoKey>()\n for (const collName of collections) {\n newDeks.set(collName, await generateDEK())\n }\n\n // Re-encrypt all records in affected collections\n for (const collName of collections) {\n const oldDek = callerKeyring.deks.get(collName)\n const newDek = newDeks.get(collName)!\n if (!oldDek) continue\n\n const ids = await adapter.list(vault, collName)\n for (const id of ids) {\n const envelope = await adapter.get(vault, collName, id)\n if (!envelope || !envelope._iv) continue\n\n // Decrypt with old DEK\n const plaintext = await decrypt(envelope._iv, envelope._data, oldDek)\n\n // Re-encrypt with new DEK\n const { iv, data } = await encrypt(plaintext, newDek)\n const newEnvelope: EncryptedEnvelope = {\n _noydb: NOYDB_FORMAT_VERSION,\n _v: envelope._v,\n _ts: new Date().toISOString(),\n _iv: iv,\n _data: data,\n }\n await adapter.put(vault, collName, id, newEnvelope)\n }\n }\n\n // Update caller's keyring with new DEKs\n for (const [collName, newDek] of newDeks) {\n callerKeyring.deks.set(collName, newDek)\n }\n await persistKeyring(adapter, vault, callerKeyring)\n\n // Update all remaining users' keyrings with re-wrapped new DEKs\n const userIds = await adapter.list(vault, '_keyring')\n for (const userId of userIds) {\n if (userId === callerKeyring.userId) continue\n\n const userEnvelope = await adapter.get(vault, '_keyring', userId)\n if (!userEnvelope) continue\n\n const userKeyringFile = JSON.parse(userEnvelope._data) as KeyringFile\n // Note: we can't derive other users' KEKs to re-wrap DEKs for them.\n // Rotation requires users to re-unlock and be re-granted after the caller\n // re-wraps with the raw DEKs held in memory. See rotation flow below.\n // The trick: import the user's KEK from their salt? No — we need their passphrase.\n //\n // Per the spec: the caller (owner/admin) wraps the new DEKs with each remaining\n // user's KEK. But we can't derive their KEK without their passphrase.\n //\n // Real solution from the spec: the caller wraps the DEK using the approach of\n // reading each user's existing wrapping. Since we can't derive their KEK,\n // we use a RE-KEYING approach: the new DEK is wrapped with a key-wrapping-key\n // that we CAN derive — we use the existing wrapped DEK as proof that the user\n // had access, and we replace it with the new wrapped DEK.\n //\n // Practical approach: Since the owner/admin has all raw DEKs in memory,\n // and each user's keyring contains their salt, we need the users to\n // re-authenticate to get the new wrapped keys. This is the standard approach.\n //\n // For NOYDB Phase 2: we'll update the keyring file to include a \"pending_rekey\"\n // flag. Users will get new DEKs on next login when the owner provides them.\n //\n // SIMPLER approach used here: Since the owner performed the rotation,\n // the owner has both old and new DEKs. We store a \"rekey token\" that the\n // user can use to unwrap: we wrap the new DEK with the OLD DEK (which the\n // user can still unwrap from their keyring, since their keyring has the old\n // wrapped DEK and their KEK can unwrap it).\n\n // Actually even simpler: we just need the user's KEK. We don't have it.\n // The spec says the owner wraps new DEKs for each remaining user.\n // This requires knowing each user's KEK (or having a shared secret).\n //\n // The CORRECT implementation from the spec: the owner/admin has all DEKs.\n // Each user's keyring stores DEKs wrapped with THAT USER's KEK.\n // To re-wrap, we need each user's KEK — which we can't get.\n //\n // Real-world solution: use a KEY ESCROW approach where the owner stores\n // each user's wrapping key (not their passphrase, but a key derived from\n // the grant process). During grant, the owner stores a copy of the new user's\n // KEK (wrapped with the owner's KEK) so they can re-wrap later.\n //\n // For now: mark the user's keyring as needing rekey. The user will need to\n // re-authenticate (owner provides new passphrase or re-grants).\n\n // Update: simplest correct approach — during grant, we store the user's KEK\n // wrapped with the owner's KEK in a separate escrow field. Then during rotation,\n // the owner unwraps the user's KEK from escrow and wraps the new DEKs.\n //\n // BUT: that means we need to change the KeyringFile format.\n // For Phase 2 MVP: just delete the user's old DEK entries and require re-grant.\n // This is secure (revoked keys are gone) but inconvenient (remaining users\n // need re-grant for rotated collections).\n\n // PHASE 2 APPROACH: Remove the affected collection DEKs from remaining users'\n // keyrings. The owner must re-grant access to those collections.\n // This is correct and secure — just requires the owner to re-run grant().\n\n const updatedDeks = { ...userKeyringFile.deks }\n for (const collName of collections) {\n delete updatedDeks[collName]\n }\n\n const updatedPermissions = { ...userKeyringFile.permissions }\n for (const collName of collections) {\n delete updatedPermissions[collName]\n }\n\n const updatedKeyring: KeyringFile = {\n ...userKeyringFile,\n deks: updatedDeks,\n permissions: updatedPermissions,\n }\n\n await writeKeyringFile(adapter, vault, userId, updatedKeyring)\n }\n}\n\n// ─── Change Secret ─────────────────────────────────────────────────────\n\n/**\n * Change the user's passphrase. Re-wraps every DEK under the new KEK.\n *\n * Pass `{ validate: true }` (or a `PassphrasePolicy`) to gate the new\n * phrase on the strength rules. `db.rotatePassphrase()` adds a\n * `checkGate('rotate-passphrase')` step on top of this primitive and\n * always validates.\n */\nexport async function changeSecret(\n adapter: NoydbStore,\n vault: string,\n keyring: UnlockedKeyring,\n newPassphrase: string,\n passphraseOpts?: PassphrasePolicy & { validate?: boolean; allowWeakPassphrase?: boolean },\n): Promise<UnlockedKeyring> {\n if (passphraseOpts?.validate && !passphraseOpts.allowWeakPassphrase) {\n assertStrongPassphrase(newPassphrase, passphraseOpts)\n }\n const newSalt = generateSalt()\n const newKek = await deriveKey(newPassphrase, newSalt)\n\n // Re-wrap all DEKs with the new KEK\n const wrappedDeks: Record<string, string> = {}\n for (const [collName, dek] of keyring.deks) {\n wrappedDeks[collName] = await wrapKey(dek, newKek)\n }\n\n const keyringFile: KeyringFile = {\n _noydb_keyring: NOYDB_KEYRING_VERSION,\n user_id: keyring.userId,\n display_name: keyring.displayName,\n role: keyring.role,\n permissions: keyring.permissions,\n deks: wrappedDeks,\n salt: bufferToBase64(newSalt),\n created_at: new Date().toISOString(),\n granted_by: keyring.userId,\n }\n\n await writeKeyringFile(adapter, vault, keyring.userId, keyringFile)\n\n return {\n userId: keyring.userId,\n displayName: keyring.displayName,\n role: keyring.role,\n permissions: keyring.permissions,\n deks: keyring.deks, // Same DEKs, different wrapping\n kek: newKek,\n salt: newSalt,\n // Tier-2 slots are NOT preserved through `changeSecret` —\n // each slot wraps the OLD KEK, so the new keyring has no\n // authenticator slots until the user re-enrolls. The higher-level\n // `db.rotatePassphrase()` (#10) preserves slots by rewrapping the\n // KEK reference, not the KEK itself.\n authenticators: [],\n ...(keyring.policy !== undefined && { policy: keyring.policy }),\n }\n}\n\n// ─── Bundle recipients ──────────────────────────────────────────\n\n/**\n * Recipient slot in a re-keyed `.noydb` bundle. Each slot becomes its\n * own keyring file inside the bundle, sealed with its own passphrase.\n * Same role/permission semantics as `db.grant()` but no adapter side\n * effect — the slot only exists inside the bundle bytes.\n *\n * @public\n */\nexport interface BundleRecipient {\n /** User id stamped onto the keyring file in the bundle. */\n readonly id: string\n /** Optional display name. Defaults to `id`. */\n readonly displayName?: string\n /** Passphrase the recipient will type to unlock. */\n readonly passphrase: string\n /** Role on the destination vault. Defaults to `'viewer'`. */\n readonly role?: Role\n /**\n * Per-collection permissions. When omitted, role defaults apply.\n * Restricting permissions here ALSO restricts which DEKs are wrapped\n * into the slot — a slot with `{ invoices: 'ro' }` cannot decrypt\n * other collections even though their ciphertext sits in the bundle.\n */\n readonly permissions?: Permissions\n /**\n * Optional `as-*` export grants on the destination vault.\n * Mirrors the `exportCapability` field on a live keyring.\n */\n readonly exportCapability?: ExportCapability\n /**\n * Optional `as-*` import grants on the destination vault.\n * Mirrors the `importCapability` field on a live keyring.\n * Default-closed: no plaintext format granted, no bundle import.\n */\n readonly importCapability?: ImportCapability\n /**\n * Optional bundle-slot expiry. ISO-8601 timestamp; past the\n * cutoff this slot's keyring refuses to load with\n * `KeyringExpiredError`. Time-boxed audit access pattern: \"this\n * slot works for 30 days then becomes opaque to its holder.\"\n */\n readonly expiresAt?: string\n}\n\n/**\n * Build a `KeyringFile` for one bundle recipient, given the source\n * vault's unwrapped DEKs. Mirrors `grant()` minus the adapter write —\n * the produced file is meant to be embedded in the bundle's\n * `keyrings` map, never persisted to the source vault.\n *\n * Privilege-escalation check still runs: every DEK wrapped into the\n * recipient's keyring must come from the source's own DEK set.\n *\n * @internal\n */\nexport async function buildRecipientKeyringFile(\n callerKeyring: UnlockedKeyring,\n recipient: BundleRecipient,\n): Promise<KeyringFile> {\n const role: Role = recipient.role ?? 'viewer'\n const permissions = resolvePermissions(role, recipient.permissions)\n\n const newSalt = generateSalt()\n const newKek = await deriveKey(recipient.passphrase, newSalt)\n\n const wrappedDeks: Record<string, string> = {}\n\n // Collections the recipient was explicitly granted permission to.\n for (const collName of Object.keys(permissions)) {\n const dek = callerKeyring.deks.get(collName)\n if (dek) {\n wrappedDeks[collName] = await wrapKey(dek, newKek)\n }\n }\n\n // owner / admin / viewer: wrap every known DEK (matches grant).\n if (role === 'owner' || role === 'admin' || role === 'viewer') {\n for (const [collName, dek] of callerKeyring.deks) {\n if (!(collName in wrappedDeks)) {\n wrappedDeks[collName] = await wrapKey(dek, newKek)\n }\n }\n }\n\n // Always propagate system-prefixed collection DEKs (`_ledger`, etc.) —\n // the recipient needs them to verify the bundle on import.\n for (const [collName, dek] of callerKeyring.deks) {\n if (collName.startsWith('_') && !(collName in wrappedDeks)) {\n wrappedDeks[collName] = await wrapKey(dek, newKek)\n }\n }\n\n // Anti-privilege-escalation: every wrapped DEK must come from the\n // caller's own DEK set. Belt-and-braces with the lookups above.\n for (const collName of Object.keys(wrappedDeks)) {\n if (!callerKeyring.deks.has(collName)) {\n throw new PrivilegeEscalationError(collName)\n }\n }\n\n return {\n _noydb_keyring: NOYDB_KEYRING_VERSION,\n user_id: recipient.id,\n display_name: recipient.displayName ?? recipient.id,\n role,\n permissions,\n deks: wrappedDeks,\n salt: bufferToBase64(newSalt),\n created_at: new Date().toISOString(),\n granted_by: callerKeyring.userId,\n ...(recipient.exportCapability !== undefined\n ? { export_capability: recipient.exportCapability }\n : {}),\n ...(recipient.importCapability !== undefined\n ? { import_capability: recipient.importCapability }\n : {}),\n ...(recipient.expiresAt !== undefined\n ? { expires_at: recipient.expiresAt }\n : {}),\n }\n}\n\n// ─── List Users ────────────────────────────────────────────────────────\n\n/** List all users with access to a vault. */\nexport async function listUsers(\n adapter: NoydbStore,\n vault: string,\n): Promise<UserInfo[]> {\n const userIds = await adapter.list(vault, '_keyring')\n const users: UserInfo[] = []\n\n for (const userId of userIds) {\n const envelope = await adapter.get(vault, '_keyring', userId)\n if (!envelope) continue\n const kf = JSON.parse(envelope._data) as KeyringFile\n users.push({\n userId: kf.user_id,\n displayName: kf.display_name,\n role: kf.role,\n permissions: kf.permissions,\n createdAt: kf.created_at,\n grantedBy: kf.granted_by,\n })\n }\n\n return users\n}\n\n// ─── DEK Management ────────────────────────────────────────────────────\n\n/** Ensure a DEK exists for a collection. Generates one if new. */\nexport async function ensureCollectionDEK(\n adapter: NoydbStore,\n vault: string,\n keyring: UnlockedKeyring,\n): Promise<(collectionName: string) => Promise<CryptoKey>> {\n // Dedupe concurrent first-time DEK creates per collection. Without\n // this, two concurrent `getDEK('foo')` calls both pass the `existing`\n // check (the Map is empty), both generate fresh DEKs, and the second\n // `set` overwrites the first — making any envelope encrypted with\n // the discarded DEK fail to decrypt later (TamperedError on read).\n // Pre-existing race exposed by the multi-writer ledger work in #296.\n const inFlight = new Map<string, Promise<CryptoKey>>()\n return async (collectionName: string): Promise<CryptoKey> => {\n const existing = keyring.deks.get(collectionName)\n if (existing) return existing\n const pending = inFlight.get(collectionName)\n if (pending) return pending\n\n const promise = (async () => {\n const dek = await generateDEK()\n keyring.deks.set(collectionName, dek)\n await persistKeyring(adapter, vault, keyring)\n return dek\n })()\n inFlight.set(collectionName, promise)\n try {\n return await promise\n } finally {\n inFlight.delete(collectionName)\n }\n }\n}\n\n// ─── Permission Checks ─────────────────────────────────────────────────\n\n/** Check if a user has write permission for a collection. */\nexport function hasWritePermission(keyring: UnlockedKeyring, collectionName: string): boolean {\n if (keyring.role === 'owner' || keyring.role === 'admin') return true\n if (keyring.role === 'viewer' || keyring.role === 'client') return false\n return keyring.permissions[collectionName] === 'rw'\n}\n\n/** Check if a user has any access to a collection. */\nexport function hasAccess(keyring: UnlockedKeyring, collectionName: string): boolean {\n if (keyring.role === 'owner' || keyring.role === 'admin' || keyring.role === 'viewer') return true\n return collectionName in keyring.permissions\n}\n\n// ─── Helpers ───────────────────────────────────────────────────────────\n\n/** Persist a keyring file to the adapter. */\nexport async function persistKeyring(\n adapter: NoydbStore,\n vault: string,\n keyring: UnlockedKeyring,\n): Promise<void> {\n const wrappedDeks: Record<string, string> = {}\n for (const [collName, dek] of keyring.deks) {\n wrappedDeks[collName] = await wrapKey(dek, keyring.kek)\n }\n\n const keyringFile: KeyringFile = {\n _noydb_keyring: NOYDB_KEYRING_VERSION,\n user_id: keyring.userId,\n display_name: keyring.displayName,\n role: keyring.role,\n permissions: keyring.permissions,\n deks: wrappedDeks,\n salt: bufferToBase64(keyring.salt),\n created_at: new Date().toISOString(),\n granted_by: keyring.userId,\n ...(keyring.exportCapability !== undefined && { export_capability: keyring.exportCapability }),\n ...(keyring.importCapability !== undefined && { import_capability: keyring.importCapability }),\n ...(keyring.authenticators.length > 0 && { authenticators: keyring.authenticators }),\n ...(keyring.policy !== undefined && { policy: keyring.policy }),\n }\n\n await writeKeyringFile(adapter, vault, keyring.userId, keyringFile)\n}\n\n// ─── Export capability ──────────────────────────────────────\n\n/**\n * Role-based default policy for the encrypted-bundle capability.\n *\n * Applied when `keyring.exportCapability` is absent or\n * `exportCapability.bundle` is undefined:\n *\n * - `owner` / `admin` → `true` (happy-path backup without friction)\n * - `operator` / `viewer` / `client` → `false` (explicit grant required)\n *\n * Rationale: a bundle is inert without the KEK, so an owner backing up\n * their own vault doesn't need friction; a non-admin role producing a\n * bundle for an external party does, because the bundle outlives\n * keyring revocation.\n */\nfunction defaultBundleCapability(role: Role): boolean {\n return role === 'owner' || role === 'admin'\n}\n\n/**\n * Check whether a keyring is authorised for a given `@noy-db/as-*`\n * export tier.\n *\n * - `tier: 'plaintext'` — returns true iff `exportCapability.plaintext`\n * contains the requested `format` or the `'*'` wildcard. Default for\n * every role is empty — no grant, no plaintext export.\n * - `tier: 'bundle'` — returns `exportCapability.bundle` if present, or\n * the role-based default otherwise (owner/admin → true, else false).\n *\n * `@noy-db/as-*` packages MUST call this before invoking the underlying\n * export primitive. Rogue forks that skip the check are caught by code\n * review — the single-entry-point contract is a convention, not a\n * runtime invariant. Vault-level gated wrappers\n * (`vault.exportRecords` / `exportBlobs` / `writeBundle`) will land in a\n * follow-up PR to enforce at the primitive level.\n */\nexport function hasExportCapability(\n keyring: UnlockedKeyring,\n tier: 'plaintext',\n format: ExportFormat,\n): boolean\nexport function hasExportCapability(\n keyring: UnlockedKeyring,\n tier: 'bundle',\n): boolean\nexport function hasExportCapability(\n keyring: UnlockedKeyring,\n tier: 'plaintext' | 'bundle',\n format?: ExportFormat,\n): boolean {\n const cap = keyring.exportCapability\n if (tier === 'plaintext') {\n const allowed = cap?.plaintext ?? []\n return allowed.includes('*') || (format !== undefined && allowed.includes(format))\n }\n // tier === 'bundle'\n return cap?.bundle ?? defaultBundleCapability(keyring.role)\n}\n\n/**\n * Same-shape inspector for an `ExportCapability` value that isn't yet\n * attached to a keyring (e.g. for previewing a grant before applying).\n * Role must be supplied separately so bundle defaults can be computed.\n */\nexport function evaluateExportCapability(\n capability: ExportCapability | undefined,\n role: Role,\n tier: 'plaintext',\n format: ExportFormat,\n): boolean\nexport function evaluateExportCapability(\n capability: ExportCapability | undefined,\n role: Role,\n tier: 'bundle',\n): boolean\nexport function evaluateExportCapability(\n capability: ExportCapability | undefined,\n role: Role,\n tier: 'plaintext' | 'bundle',\n format?: ExportFormat,\n): boolean {\n if (tier === 'plaintext') {\n const allowed = capability?.plaintext ?? []\n return allowed.includes('*') || (format !== undefined && allowed.includes(format))\n }\n return capability?.bundle ?? defaultBundleCapability(role)\n}\n\n// ─── Import capability (issue ) ────────────────────────────────────\n\n/**\n * Check whether a keyring is authorised for a given `@noy-db/as-*`\n * import tier (issue ).\n *\n * - `tier: 'plaintext'` — true iff `importCapability.plaintext`\n * contains the requested `format` or the `'*'` wildcard.\n * - `tier: 'bundle'` — true iff `importCapability.bundle === true`.\n *\n * **Default-closed for every role on every dimension** — including\n * owner. Import is more dangerous than export (corrupts vs leaks), so\n * the policy refuses to assume intent. Owners must positively grant\n * the capability via `vault.grant({ importCapability: ... })`.\n */\nexport function hasImportCapability(\n keyring: UnlockedKeyring,\n tier: 'plaintext',\n format: ExportFormat,\n): boolean\nexport function hasImportCapability(\n keyring: UnlockedKeyring,\n tier: 'bundle',\n): boolean\nexport function hasImportCapability(\n keyring: UnlockedKeyring,\n tier: 'plaintext' | 'bundle',\n format?: ExportFormat,\n): boolean {\n const cap = keyring.importCapability\n if (tier === 'plaintext') {\n const allowed = cap?.plaintext ?? []\n return allowed.includes('*') || (format !== undefined && allowed.includes(format))\n }\n // tier === 'bundle' — closed default for every role\n return cap?.bundle === true\n}\n\n/**\n * Same-shape inspector for an `ImportCapability` value that isn't yet\n * attached to a keyring (e.g. previewing a grant before applying).\n * `role` is accepted for symmetry with `evaluateExportCapability` even\n * though the import policy ignores it — bundle defaults are\n * role-agnostic and closed.\n */\nexport function evaluateImportCapability(\n capability: ImportCapability | undefined,\n role: Role,\n tier: 'plaintext',\n format: ExportFormat,\n): boolean\nexport function evaluateImportCapability(\n capability: ImportCapability | undefined,\n role: Role,\n tier: 'bundle',\n): boolean\nexport function evaluateImportCapability(\n capability: ImportCapability | undefined,\n _role: Role,\n tier: 'plaintext' | 'bundle',\n format?: ExportFormat,\n): boolean {\n if (tier === 'plaintext') {\n const allowed = capability?.plaintext ?? []\n return allowed.includes('*') || (format !== undefined && allowed.includes(format))\n }\n return capability?.bundle === true\n}\n\nfunction resolvePermissions(role: Role, explicit?: Permissions): Permissions {\n if (role === 'owner' || role === 'admin' || role === 'viewer') return {}\n return explicit ?? {}\n}\n\nasync function writeKeyringFile(\n adapter: NoydbStore,\n vault: string,\n userId: string,\n keyringFile: KeyringFile,\n): Promise<void> {\n const envelope = {\n _noydb: 1 as const,\n _v: 1,\n _ts: new Date().toISOString(),\n _iv: '',\n _data: JSON.stringify(keyringFile),\n }\n await adapter.put(vault, '_keyring', userId, envelope)\n}\n"],"mappings":"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","names":[]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|