import type { NoydbStore, KeyringFile, KeyringAuthenticator, Role, Permissions, GrantOptions, RevokeOptions, UpdateUserOptions, UserInfo, ExportCapability, ExportFormat, ImportCapability, VaultPolicyOnDisk, UserEnvelope as UserEnvelopeReader } from '../../kernel/types.js'; import { type EnclaveKey } from '../../kernel/enclave/index.js'; import { type PassphrasePolicy } from '../../kernel/validation.js'; /** In-memory representation of an unlocked keyring. */ export interface UnlockedKeyring { readonly userId: string; readonly displayName: string; readonly role: Role; readonly permissions: Permissions; readonly deks: Map; /** * The KEK, when this keyring was unlocked via tier 1 (passphrase) or * a wrap-KEK tier-2 method (WebAuthn / OIDC). `null` when the * keyring was opened via: * * - Unencrypted mode (no KEK exists) * - Tier-3 PIN quick-resume (`@noy-db/on-pin`) * - Wrap-DEKs tier-2 unlock (`@noy-db/on-password`'s * `verifyPasswordSlot`) * - Session-state restore (`session/session.ts`) * - Dev-unlock fixture (`session/dev-unlock.ts`) * * Consumers performing tier-1 operations that need the KEK * (DEK rewrap, keyring persist, delegation issue/unwrap) must * null-check and throw a clear error if absent — re-authenticate * at tier 1 first to recover the KEK. * * Tightened from `EnclaveKey` to `EnclaveKey | null`; the runtime * contract has always allowed null, the type now matches reality. */ readonly kek: EnclaveKey | null; readonly salt: Uint8Array; /** * Debug-plaintext layout flag. Set only on the plaintext keyring created * in `encrypt: false` + `debugPlaintext: true` mode — it lives here * because its lifecycle is identical to the plaintext keyring's (no * encrypted vault ever has it, so this never widens the encrypted surface). * When true, user-collection records are written with their fields inlined * beside the envelope metadata (`_debug: 1`) so native store tooling can * read them without unwrapping `_data`. */ readonly debugPlaintext?: boolean; /** * `@noy-db/as-*` export capability. Absent when the * keyring was written before this RFC landed — role-based defaults * apply via `hasExportCapability`. */ readonly exportCapability?: ExportCapability; /** * `@noy-db/as-*` import capability. Absent when the * keyring was written before the import-capability extension * landed — default-closed semantics * apply via `hasImportCapability` (no plaintext format granted, no * bundle import granted, regardless of role). */ readonly importCapability?: ImportCapability; /** * Tier-2 authenticator slots — readonly snapshot loaded from the * keyring file. Mutations go through `enrollAuthenticator` / * `removeAuthenticator`, which write back via * `persistKeyring`. Always defined; loads with an empty array for * keyrings written before the multi-slot extension landed. */ readonly authenticators: readonly KeyringAuthenticator[]; /** * Reserved per-keyring policy override (forward-compat for Option C * — see {@link VaultPolicyOnDisk}). v1.0 round-trips this field but * never enforces it; the gate engine uses `_meta/policy` only. */ readonly policy?: VaultPolicyOnDisk; } /** Mint a fresh wrapped-canary string. Deterministic for a given KEK. */ export declare function mintKeyringCanary(kek: EnclaveKey): Promise; /** Load and unlock a user's keyring for a vault. */ export declare function loadKeyring(adapter: NoydbStore, vault: string, userId: string, passphrase: string): Promise; /** * Open-policy pre-gate: decide create-vs-fail-closed **before** any * vault write. `openVault` must not self-provision an owner keyring into a * vault held by other principals; create-on-open is allowed only for a * genuinely-new vault (no `_keyring/*` at all). Capability-free — one * `store.list`. Returns when the open may proceed (the caller is a member, or * the vault is genuinely-new and `create` is allowed, in which case the caller * falls through to the normal `createOwnerKeyring` path); throws `NoAccessError` * otherwise. Placed before managed-passphrase secret resolution (which persists * on first open), so a fail-closed open writes nothing. */ export declare function assertKeyringOpenAllowed(store: NoydbStore, vault: string, userId: string, create: boolean): Promise; /** * Create the initial owner keyring for a new vault. * * Pass `{ validate: true }` (or a `PassphrasePolicy`) to gate creation * on the phrase-format strength rules — `Noydb` threads this from * `NoydbOptions.validatePassphrase`. Direct callers (CLI, scripts, * test fixtures) opt in explicitly. */ export declare function createOwnerKeyring(adapter: NoydbStore, vault: string, userId: string, passphrase: string, passphraseOpts?: PassphrasePolicy & { validate?: boolean; allowWeakPassphrase?: boolean; }): Promise; /** Grant access to a new user. Caller must have grant privilege. */ export declare function grant(adapter: NoydbStore, vault: string, callerKeyring: UnlockedKeyring, options: GrantOptions): Promise; /** Revoke a user's access. Optionally rotate keys for affected collections. */ export declare function revoke(adapter: NoydbStore, vault: string, callerKeyring: UnlockedKeyring, options: RevokeOptions): Promise; /** * Mutate `role`, `displayName`, and/or `permissions` on an existing * keyring. Pure plaintext-header rewrite — no DEK rewrap, no KEK * required, no authenticator slots touched. Tier-2 enrollments and * recovery codes survive the operation. * * Role-elevation guard: BOTH the old role AND the new role must * satisfy `canUpdateRole(callerRole, _)`. This blocks the two * privilege-escalation shapes: * - admin elevates someone (or themselves) to owner * - admin demotes an owner to a role they then control * * Owner is always allowed. Admin manages admin / operator / viewer / * client laterally. * * Identity preserved: same userId, same DEK wrappings. Last-write-wins * through the standard keyring put (same concurrency story as `grant` * and `revoke`). * * @throws `NoAccessError` when no keyring exists for the target. * @throws `PermissionDeniedError` when the role hierarchy rejects. * @throws `ValidationError` when the diff is empty (nothing to update). * */ export declare function updateKeyringIdentity(adapter: NoydbStore, vault: string, callerKeyring: UnlockedKeyring, options: UpdateUserOptions): Promise; /** * Rotate DEKs for specified collections: * 1. Generate new DEKs * 2. Re-encrypt all records in affected collections * 3. Re-wrap new DEKs for all remaining users */ export declare function rotateKeys(adapter: NoydbStore, vault: string, callerKeyring: UnlockedKeyring, collections: string[]): Promise; /** * Change the user's passphrase. Re-wraps every DEK under the new KEK. * * Validates the new passphrase against the strength rules unless * `allowWeakPassphrase: true` is passed. Mirrors `rotatePassphrase`'s * default-on validation contract. * * `db.rotatePassphrase()` adds a `checkGate('rotate-passphrase')` step * on top of this primitive and additionally requires the OLD passphrase * for re-derivation; `changeSecret` reuses the cached unlocked KEK so * the OLD passphrase is not retyped. */ export declare function changeSecret(adapter: NoydbStore, vault: string, keyring: UnlockedKeyring, newPassphrase: string, passphraseOpts?: PassphrasePolicy & { allowWeakPassphrase?: boolean; }): Promise; /** * Recipient slot in a re-keyed `.noydb` bundle. Each slot becomes its * own keyring file inside the bundle, sealed with its own passphrase. * Same role/permission semantics as `db.grant()` but no adapter side * effect — the slot only exists inside the bundle bytes. * * @public */ export interface BundleRecipient { /** User id stamped onto the keyring file in the bundle. */ readonly id: string; /** Optional display name. Defaults to `id`. */ readonly displayName?: string; /** Passphrase the recipient will type to unlock. */ readonly passphrase: string; /** Role on the destination vault. Defaults to `'viewer'`. */ readonly role?: Role; /** * Per-collection permissions. When omitted, role defaults apply. * Restricting permissions here ALSO restricts which DEKs are wrapped * into the slot — a slot with `{ invoices: 'ro' }` cannot decrypt * other collections even though their ciphertext sits in the bundle. */ readonly permissions?: Permissions; /** * Optional `as-*` export grants on the destination vault. * Mirrors the `exportCapability` field on a live keyring. */ readonly exportCapability?: ExportCapability; /** * Optional `as-*` import grants on the destination vault. * Mirrors the `importCapability` field on a live keyring. * Default-closed: no plaintext format granted, no bundle import. */ readonly importCapability?: ImportCapability; /** * Optional bundle-slot expiry. ISO-8601 timestamp; past the * cutoff this slot's keyring refuses to load with * `KeyringExpiredError`. Time-boxed audit access pattern: "this * slot works for 30 days then becomes opaque to its holder." */ readonly expiresAt?: string; } /** * Build a `KeyringFile` for one bundle recipient, given the source * vault's unwrapped DEKs. Mirrors `grant()` minus the adapter write — * the produced file is meant to be embedded in the bundle's * `keyrings` map, never persisted to the source vault. * * Privilege-escalation check still runs: every DEK wrapped into the * recipient's keyring must come from the source's own DEK set. * * @internal */ export declare function buildRecipientKeyringFile(callerKeyring: UnlockedKeyring, recipient: BundleRecipient): Promise; /** List all users with access to a vault. */ export declare function listUsers(adapter: NoydbStore, vault: string): Promise; /** * Optional filter knobs for {@link listUsersWithEnvelopes}. * * - `includeHidden` — when true, principals with `_meta/visibility/` * set to `{ hidden: true }` are returned alongside everyone else. * Requires `owner` or `admin` callerRole; lower roles get * {@link import('../../kernel/errors.js').PermissionDeniedError}. */ export interface ListUsersOptions { readonly includeHidden?: boolean; } /** * Joined enumeration: every keyring + its `_users/` * envelope side by side. Convenience for admin UIs that want to * render team-member lists with profile data ("Bob — operator — * 'Bob the Auditor' avatar X locale fr-FR") in a single pass. * * `userEnvelopeDek` is the vault's `_users` collection DEK * (`vault.getDEK('_users')`); used to decrypt every envelope. * * `callerRole` drives the directory-visibility checks: * * - When the vault's `_meta/directory` document has `enabled: false`, * only `owner` and `admin` callers may enumerate; anyone else gets * {@link import('../../kernel/errors.js').DirectoryDisabledError}. * - Principals with `_meta/visibility/` set to `{ hidden: true }` * are filtered out by default. `owner`/`admin` callers can pass * `{ includeHidden: true }` to see them; lower roles passing that * option get `PermissionDeniedError`. * * Honest caveat: these filters are a UX hint, not a security * boundary. The keyring file is still listed at `_keyring/*` and the * envelope ciphertext at `_users/*`. A caller with direct store access * — or a caller that calls this function with `callerRole: 'owner'` * unconditionally — sees every principal. The protection is only as * strong as the role the calling layer passes in. The hub-level wrapper * on `Vault` sources `callerRole` from the unlocked keyring's `role` * field, which is signed-by-construction (it lives in the user's own * keyring file). See `https://github.com/vLannaAi/noy-db-docs/blob/main/content/docs/services/user-envelope.md` → * "Directory visibility". * * Principals without a persisted envelope (legacy keyrings predating * the user-envelope feature) come back with `envelope: null`. The * caller chooses how to render — usually "fall back to keyring's * `displayName`". * * Order matches `listUsers()` (store-defined; sort if you need a * stable display order). */ export declare function listUsersWithEnvelopes(adapter: NoydbStore, vault: string, userEnvelopeDek: EnclaveKey, callerRole: Role, options?: ListUsersOptions): Promise | null; }>>; /** Ensure a DEK exists for a collection. Generates one if new. */ export declare function ensureCollectionDEK(adapter: NoydbStore, vault: string, keyring: UnlockedKeyring): Promise<(collectionName: string) => Promise>; /** Check if a user has write permission for a collection. */ export declare function hasWritePermission(keyring: UnlockedKeyring, collectionName: string): boolean; /** Check if a user has any access to a collection. */ export declare function hasAccess(keyring: UnlockedKeyring, collectionName: string): boolean; /** Persist a keyring file to the adapter. */ export declare function persistKeyring(adapter: NoydbStore, vault: string, keyring: UnlockedKeyring): Promise; /** * Check whether a keyring is authorised for a given `@noy-db/as-*` * export tier. * * - `tier: 'plaintext'` — returns true iff `exportCapability.plaintext` * contains the requested `format` or the `'*'` wildcard. Default for * every role is empty — no grant, no plaintext export. * - `tier: 'bundle'` — returns `exportCapability.bundle` if present, or * the role-based default otherwise (owner/admin → true, else false). * * `@noy-db/as-*` packages MUST call this before invoking the underlying * export primitive. Rogue forks that skip the check are caught by code * review — the single-entry-point contract is a convention, not a * runtime invariant. Vault-level gated wrappers * (`vault.exportRecords` / `exportBlobs` / `writeBundle`) will land in a * follow-up PR to enforce at the primitive level. */ export declare function hasExportCapability(keyring: UnlockedKeyring, tier: 'plaintext', format: ExportFormat): boolean; export declare function hasExportCapability(keyring: UnlockedKeyring, tier: 'bundle'): boolean; /** * Same-shape inspector for an `ExportCapability` value that isn't yet * attached to a keyring (e.g. for previewing a grant before applying). * Role must be supplied separately so bundle defaults can be computed. */ export declare function evaluateExportCapability(capability: ExportCapability | undefined, role: Role, tier: 'plaintext', format: ExportFormat): boolean; export declare function evaluateExportCapability(capability: ExportCapability | undefined, role: Role, tier: 'bundle'): boolean; /** * Check whether a keyring is authorised for a given `@noy-db/as-*` * import tier (issue ). * * - `tier: 'plaintext'` — true iff `importCapability.plaintext` * contains the requested `format` or the `'*'` wildcard. * - `tier: 'bundle'` — true iff `importCapability.bundle === true`. * * **Default-closed for every role on every dimension** — including * owner. Import is more dangerous than export (corrupts vs leaks), so * the policy refuses to assume intent. Owners must positively grant * the capability via `vault.grant({ importCapability: ... })`. */ export declare function hasImportCapability(keyring: UnlockedKeyring, tier: 'plaintext', format: ExportFormat): boolean; export declare function hasImportCapability(keyring: UnlockedKeyring, tier: 'bundle'): boolean; /** * Same-shape inspector for an `ImportCapability` value that isn't yet * attached to a keyring (e.g. previewing a grant before applying). * `role` is accepted for symmetry with `evaluateExportCapability` even * though the import policy ignores it — bundle defaults are * role-agnostic and closed. */ export declare function evaluateImportCapability(capability: ImportCapability | undefined, role: Role, tier: 'plaintext', format: ExportFormat): boolean; export declare function evaluateImportCapability(capability: ImportCapability | undefined, role: Role, tier: 'bundle'): boolean;