import type { NoydbOptions, NoydbEventMap, GrantOptions, RevokeOptions, UpdateUserOptions, UserInfo, PushResult, PullResult, PushOptions, PullOptions, SyncStatus, NoydbStore, AccessibleVault, ListAccessibleVaultsOptions, QueryAcrossOptions, QueryAcrossResult, TranslatorAuditEntry, WriteConflict, UserApiFactory, ActiveTier, FactorProofBundle, GateName, VaultPolicy } from './types.js'; import type { PassphrasePolicy } from './validation.js'; import { type RotatePassphraseInput, type RecoverPassphraseInput, type RecoverPassphraseResult, type RotateRecoveryOptions, type RotateRecoveryResult, type EnrollRecoveryResult, type RecoveryEnrollmentInput, type RecoveryProof } from '../with-party/team/rotate-recover.js'; import type { RecoverUserOptions } from '../with-party/team/peer-recover.js'; import { type PaperRecoveryEntry, type ShamirRecoveryEntry } from '../with-party/team/recovery.js'; import { type CoordinationProvider } from '../port/by/default-provider.js'; import type { PublicEnvelope } from '../with-party/directory/public-envelope/types.js'; import type { SetPublicEnvelopeInput } from '../with-party/directory/public-envelope/schema.js'; import { Vault } from './vault.js'; import type { VaultMeta } from '../with-shape/introspection/meta.js'; import { WriteQueueTracker, type WriteQueue } from './write-queue.js'; import { WriteHookRegistry, type WriteHook, type Unsubscribe } from '../port/with/write-hooks.js'; import { ServiceBus } from '../port/with/service-bus.js'; import { type TabCoordinationOptions, type TabRole, type TabPresence } from '../with-party/tab-coordination.js'; import type { UnlockedKeyring } from '../with-party/team/keyring.js'; import { type EnrollAuthenticatorOptions, type UpdateAuthenticatorOptions } from '../with-party/team/authenticators.js'; import { type QuickUnlockState } from '../with-party/session/unlock-state.js'; import type { KeyringAuthenticator } from './types.js'; import type { SyncEngine } from '../with-party/team/sync.js'; import type { SyncTransaction } from '../with-party/team/sync-transaction.js'; import { type SnapshotMeta } from '../with-fork/snapshots/strategy.js'; import type { AmendmentTxOptions } from '../with-commit/tx/transaction.js'; import { TxContext } from '../with-commit/tx/transaction.js'; import type { DryRunResult } from '../with-commit/tx/dry-run.js'; import { type ForgetStrategy } from '../with-audit/forget/strategy.js'; import { type CustodyStrategy } from '../with-party/custody/strategy.js'; /** NoydbOptions with the store resolved to a non-optional value (internal use only). */ type ResolvedNoydbOptions = NoydbOptions & { readonly store: NoydbStore; }; /** The top-level NOYDB instance. */ export declare class Noydb { #private; private readonly options; private readonly emitter; private readonly writeQueueTracker; private readonly writeHooks; private readonly subsystemBus; private readonly clientId; /** Session that owns this instance's writers (one user's writers across vaults). */ private readonly sessionId; /** Drain-barrier coordination transport for the schema fence. */ private readonly coordinationProvider; /** Pre-resolved `vault.user` API factory (mirrors `coordinationProvider` above). Public so `Vault` can call it. */ readonly userApiFactory: UserApiFactory; private readonly vaultCache; /** * In-flight `openVault` promise per name — concurrent opens of the same * vault must converge on ONE Vault instance. Without this, two callers * racing past the `vaultCache` miss each construct a Vault (and later two * Collections with independent DEKs for the same store slice), so a record * written through one fails decryption through the other with a spurious * `TamperedError` (#564). */ private readonly vaultOpening; private readonly keyringCache; private readonly syncEngines; /** * Per-vault active session tier — defaults to `1` after a passphrase * unlock; tier-2 / tier-3 unlocks downgrade it. Used by * {@link checkGate} to evaluate `gate.minTier`. */ private readonly activeTier; /** * Per-vault loaded policy. Cached after the first * `_meta/policy` load; replaced by `db.updatePolicy()`. */ private readonly policyCache; /** * One-shot bypass for the managed-mode strong-recovery check. * Set true by {@link openVaultAndEnrollRecovery} for the duration of * the bootstrap window so the keyring can be created before the * strong recovery is enrolled. Always cleared (try/finally). * @internal */ private _skipNextManagedRecoveryCheck; /** Per-vault tier-3 (PIN / quick-resume) state. */ private readonly quickUnlock; private closed; private sessionTimer; /** Same-device multi-tab coordinator; created on `enableTabCoordination()`. */ private tabCoordinator; /** Cross-tab write relay; created on `enableTabCoordination()`. */ private writeRelay; /** Per-vault policy enforcers. */ private readonly policyEnforcers; private readonly txStrategy; private readonly forgetStrategy; /** * Opt-in sovereign-custody (FR-6) strategy — `NO_CUSTODY` (throwing) unless * `withCustody()` was passed. Public so `Vault` routes `vault.custody.liberate` * through it; grant/revoke route through it from this class. @internal */ readonly custodyStrategy: CustodyStrategy; /** * Opt-in multi-user team strategy (#267 keyring-grant → team split) — * `NO_TEAM` (throwing) unless `withTeam()` was passed; the keyring * engines are linked only by the active strategy, not by this file. */ private readonly teamStrategy; private readonly sessionStrategy; private readonly syncStrategy; private readonly snapshots; private readonly policyManager; /** Pre-resolved policy-gate engine function (mirrors `coordinationProvider`/`userApiFactory` above). */ private readonly policyCheckGate; private readonly team; /** * Currently-running multi-record transaction, set by * `runTransaction` at the start of Phase 2 (commit) and cleared in * the same function's `finally` block. Side-effect writes triggered * during a staged op's `Collection.put` (today: eager derivation * outputs) register their pre-write envelope on `_executed` here so * a mid-batch failure rolls them back alongside the main staged ops. * `null` outside of Phase 2. * @internal */ private _activeTxContext; /** * In-process translation cache. Key is `"${field}\x00${collection}\x00${from}\x00${to}\x00${text}"`. * Cleared on `close()` alongside the KEK and DEKs. */ private readonly translatorCache; /** Audit log for all translator invocations in this session. Cleared on `close()`. */ private readonly _translatorAuditLog; constructor(options: ResolvedNoydbOptions); /** @internal — resolved forget strategy (NO_FORGET when not configured). */ get _forgetStrategy(): ForgetStrategy; private resetSessionTimer; /** * Touch the policy enforcer for a vault (records activity, resets * idle timer). Also touches the legacy session timer. No-op if no enforcer. */ private touchPolicy; /** * Check that a policy-guarded operation is permitted. * Throws `SessionPolicyError` if re-auth is required. */ private checkPolicyOperation; /** * Open a vault by name. * * @param name Vault identifier. * @param opts Optional settings for this session. * @param opts.locale Default locale for i18n/dictKey field resolution *. Set here to avoid passing `{ locale }` * on every individual `get()`/`list()` call. * @param opts.meta Vault descriptive metadata (label, description, etc.). First-wins: applied on first open, ignored on subsequent opens. */ openVault(name: string, opts?: { locale?: string; create?: boolean; meta?: VaultMeta; }): Promise; /** Synchronous vault access (must call openVault first, or auto-opens). */ vault(name: string): Vault; /** * Grant access to a user for a vault. * * Gated by `enroll-user`. `STRICT_POLICY` requires a TOTP / email-OTP * factor proof so the operator affirmatively re-asserts identity at * the moment of grant; `PERSONAL_POLICY` accepts a tier-1 unlock alone. * * The legacy `requireReAuthFor: ['grant']` session-policy check still * fires on top — both are independent opt-ins. * Opt-in (#267): throws {@link TeamNotEnabledError} without `withTeam()`. */ grant(vault: string, options: GrantOptions, factors?: FactorProofBundle): Promise; /** * Revoke a user's access to a vault. * * Gated by `revoke-user`. `STRICT_POLICY` requires a TOTP / email-OTP * factor proof; `PERSONAL_POLICY` accepts a tier-1 unlock alone. * * The legacy `requireReAuthFor: ['revoke']` session-policy check still * fires on top — both are independent opt-ins. * Opt-in (#267): throws {@link TeamNotEnabledError} without `withTeam()`. */ revoke(vault: string, options: RevokeOptions, factors?: FactorProofBundle): Promise; /** * Grant the FR-6 `custodian` role to a user (owner-only custody API). * * A custodian operates every collection (rw + access) but is provably * unable to grant / revoke / rotate / extract-and-sever. Only the Deed * owner may mint one. Defended in depth: the `grant-custodian` gate * (fail-closed) AND an explicit `keyring.role !== 'owner'` check — the * gate enforces host policy, the role check enforces the cryptographic * owner-only invariant even if a host mis-configures the gate. */ grantCustodian(vault: string, options: Omit, factors?: FactorProofBundle): Promise; /** @internal — grant-custodian engine, reached only via withCustody(); the * keyring engine arrives as an argument so the floor never carries it (#267). */ _grantCustodianImpl(engine: (adapter: NoydbStore, vault: string, callerKeyring: UnlockedKeyring, options: GrantOptions) => Promise, vault: string, options: Omit, factors?: FactorProofBundle): Promise; /** * Revoke a custodian (owner-only custody API). * * Mirrors {@link revoke} but pins the caller to the Deed owner: defended * in depth by the `revoke-user` gate AND an explicit `keyring.role !== * 'owner'` check, so an admin cannot unwind a custodianship. */ revokeCustodian(vault: string, options: RevokeOptions, factors?: FactorProofBundle): Promise; /** @internal — revoke-custodian engine, reached only via withCustody(). * Mirrors `_grantCustodianImpl` (#267: engine passed in). */ _revokeCustodianImpl(engine: (adapter: NoydbStore, vault: string, callerKeyring: UnlockedKeyring, options: RevokeOptions) => Promise, vault: string, options: RevokeOptions, factors?: FactorProofBundle): Promise; /** * Mutate post-grant identity fields on an existing keyring — `role`, * `displayName`, and/or `permissions`. Pure plaintext-header rewrite: * no DEK rewrap, no KEK required, no authenticator slots touched. * Tier-2 enrollments and recovery codes survive. * * Different from `db.revoke + db.grant`: * * - Same `userId`, same DEK wrappings, same `granted_by`, same * `_users/` envelope. Only the specified header * fields move. Last-write-wins via the standard keyring put. * - No cascade on role demotion (admins demoted to operator keep * the keyrings they previously granted; the cascade rules are * a `db.revoke` concern, not `db.updateUser`). * - Tier-2 slots NOT dropped — the wrapping is unaffected. * * Role-elevation guard: BOTH the old and new role must satisfy * `db.grant`'s hierarchy. Owner can do anything; admin manages * admin/operator/viewer/client laterally; admin cannot promote to * owner OR demote from owner. The guard runs regardless of the * `update-user` policy gate's settings — gates can only be more * permissive than the structural floor, never less. * * Gated by `update-user`. `STRICT_POLICY` requires a TOTP/email-OTP * factor proof so the operator affirmatively re-asserts identity at * the moment of mutation; `PERSONAL_POLICY` accepts a tier-1 unlock * alone. * * ```ts * await db.updateUser('acme', { * userId: 'bob', * role: 'operator', // promote * permissions: { invoices: 'rw' }, * }, { factors: [{ kind: 'totp' }] }) * ``` * * @throws `NoAccessError` when no keyring exists for the target. * @throws `PermissionDeniedError` when the role hierarchy rejects. * @throws `ValidationError` when no field is provided. */ updateUser(vault: string, options: UpdateUserOptions, factors?: FactorProofBundle): Promise; /** * Rotate the DEKs for the given collections in a vault. * * Generates fresh DEKs, re-encrypts every record in each collection, * and re-wraps the new DEKs into every remaining user's keyring. The * old DEKs become unreachable — useful as a defense-in-depth measure * after a suspected key leak, or as the scheduled half of a * key-rotation policy. * * Unlike `revoke({ rotateKeys: true })`, this call does NOT remove * any users — every current member keeps access, but with fresh * keys. This is the "just rotate" path; the "revoke and rotate" * path still lives in `revoke()`. * * Exposed on Noydb (rather than only on the lower-level keyring * module) so CLI and admin tooling can trigger rotation without * reaching into internals. See `noy-db rotate` for the CLI wrapper. * Opt-in (#267): throws {@link TeamNotEnabledError} without `withTeam()`. */ rotate(vault: string, collections: string[]): Promise; /** List all users with access to a vault. */ listUsers(vault: string): Promise; /** * Enumerate every vault the calling principal can unwrap, * optionally filtered by minimum role. * * The walk is a two-step pipeline: first ask the adapter for the * universe of compartments it stores, then for each one attempt to * load the calling user's keyring with the in-memory passphrase. * Compartments where the user has no keyring file (`NoAccessError`) * or where the passphrase doesn't unwrap (`InvalidKeyError`) are * silently dropped from the result — the existence of those * compartments is **not** confirmed in the return value. * * Requires the optional `NoydbStore.listVaults()` capability. * Throws `StoreCapabilityError` against stores that don't * implement it (today: store-aws-dynamo, store-aws-s3, store-browser-local, store-browser-idb). For those backends the * consumer should either pass an explicit candidate list to * `queryAcross()` directly, or maintain a vault index out of * band. * * **Privacy note.** This method's return value never reveals the * existence of a vault the caller cannot unwrap. The adapter * sees the enumeration call (it has to — it owns the storage), but * downstream consumers of `listAccessibleVaults()` only see * the filtered list. That's the boundary the existence-leak * guarantee draws. * * **Known edge case.** A vault whose keyring file * happens to have an empty wrapped-DEKs map (because the owner * granted access before any collection was created) will pass the * `loadKeyring` probe with *any* passphrase — there are no DEKs to * unwrap, so the integrity-checked unwrap that normally rejects * wrong passphrases never runs. The result is that an unrelated * principal who happens to know the user-id and the vault * name can show up in `listAccessibleVaults()` as having * access to that empty vault. They cannot read any actual * data (their DEK set is empty), so this is a metadata leak * (vault name + user-id), not a content leak. Hardening this * via a passphrase canary in the keyring file is a deferred * follow-up. * * **Cost.** O(compartments × keyring-load) — one `loadKeyring` * attempt per vault in the universe. Each attempt does one * adapter `get` + one PBKDF2 derivation + N AES-KW unwraps. For * dozens of compartments this is fine; for thousands the consumer * should cache the result and refresh on grant/revoke events. A * future optimization could batch the keyring reads via * `loadAll('_keyring')` if such a thing existed at the adapter * layer, but the contract doesn't expose that. * * @example * ```ts * // All compartments I can unwrap * const all = await db.listAccessibleVaults() * * // Only compartments where I'm at least admin * const admin = await db.listAccessibleVaults({ minRole: 'admin' }) * * // Only compartments I own * const owned = await db.listAccessibleVaults({ minRole: 'owner' }) * ``` */ listAccessibleVaults(options?: ListAccessibleVaultsOptions): Promise; /** * Run a per-vault callback against a list of compartments and * collect the results. * * Pure orchestration — there is no new crypto, no new sync, no new * authorization layer. Each vault is opened via the existing * `openVault()` path (which honors the cache primed by * `listAccessibleVaults`), the callback runs against the * resulting `Vault` instance, and the result (or thrown * error) is captured into the per-vault slot. * * **Per-vault errors do not abort the fan-out.** If one * vault's callback throws, that vault's slot carries * the error and the remaining compartments still run. The caller * decides how to handle the partition between success and failure. * This is the right shape for cross-tenant reports where one * tenant's outage shouldn't hide the other tenants' data. * * **Concurrency** is opt-in via `options.concurrency`. The default * is `1` (sequential) — conservative because per-vault * callbacks typically do their own I/O and an unbounded fan-out * can exhaust adapter connections (DynamoDB throughput, S3 socket * limits, browser fetch concurrency). Bump to 4-8 for cloud-backed * adapters where parallelism is the whole point. * * @example * ```ts * // Cross-tenant invoice totals as a flat list * const accessible = await db.listAccessibleVaults({ minRole: 'admin' }) * const results = await db.queryAcross( * accessible.map((c) => c.id), * async (comp) => { * return comp.collection('invoices').query() * .where('month', '==', '2026-03') * .toArray() * }, * { concurrency: 4 }, * ) * // results: Array<{ vault, result?: Invoice[], error?: Error }> * * // Compose with exportStream() — cross-vault plaintext export * const exports = await db.queryAcross(accessible.map((c) => c.id), async (comp) => { * const out: unknown[] = [] * for await (const chunk of comp.exportStream()) out.push(chunk) * return out * }) * ``` */ queryAcross(vaultIds: string[], fn: (vault: Vault) => Promise, options?: QueryAcrossOptions): Promise[]>; /** * @internal True once `close()` has been called. Read by outward * orchestration frameworks whose entry points can't see the private * `closed` field. */ get isClosed(): boolean; /** * @internal — true when an encrypted shard vault is provisioned * (its keyring exists in the store). */ _shardVaultProvisioned(vaultId: string): Promise; /** * @internal — the physical backend store a vault id maps to. A * `routeStore` resolves the vault-prefix route via its `resolveBackend`; * a plain store is its own backend. Used by the federation data-residency * guard to read the placement backend's `capabilities.region`. */ _resolveBackend(vaultId: string): NoydbStore; /** * Change the current user's passphrase for a vault. * * Validates the new passphrase against the strength rules. Pass * `{ allowWeakPassphrase: true }` to skip — typically only useful for * fixtures and migrations. Pass a `PassphrasePolicy` to override the * default rules (e.g. consumer-tunable `pattern` / `customValidator`). */ changeSecret(vault: string, newPassphrase: string, options?: PassphrasePolicy & { allowWeakPassphrase?: boolean; }): Promise; /** Push local changes to remote for a vault. */ push(vault: string, options?: PushOptions): Promise; /** * Pull remote changes to local for a vault. A `backup`/`archive` primary is a * push-only sink and is never pulled from (#616) — returns an empty result. */ pull(vault: string, options?: PullOptions): Promise; /** * Bidirectional sync: pull then push for all targets. * `sync-peer` targets do pull+push; `backup`/`archive` targets do push-only. */ sync(vault: string, options?: { push?: PushOptions; pull?: PullOptions; }): Promise<{ pull: PullResult; push: PushResult; }>; /** * Multi-record atomic transaction. * * The callback stages writes across any number of vaults / * collections; on return the hub pre-flights version checks, then * commits every staged op. If the body throws, nothing is * persisted. If any staged op fails its `expectedVersion` check, * the batch throws `ConflictError` with zero writes performed. If a * mid-commit failure occurs after one or more ops have already * written, each executed op is reverted best-effort (see * `runTransaction` for the crash-window caveat). * * Distinct from `transaction(vault: string) → SyncTransaction` * which batches push/pull across sync peers. */ transaction(fn: (tx: TxContext) => Promise | T): Promise; /** * Open an amendment-mode transaction. Requires `admin` or `owner` * role on every vault touched by the body; throws * `AmendmentForbiddenError` on first non-privileged `tx.vault(name)` * call. Guard `check` callbacks are SKIPPED inside an amendment — * the staged change-set is fed to each guard's `amendment.invariant` * after the body returns, and the multi-record summary is appended * to the vault's ledger as `op: 'amendment'`. */ transaction(options: AmendmentTxOptions, fn: (tx: TxContext) => Promise | T): Promise; /** * Dry-run a transaction: run the body to stage ops, then return * the directly-affected diff + collected guard violations WITHOUT * committing (no adapter writes, no write hooks). MV/derivation cascade * is not simulated. Requires `withTransactions()`. */ transaction(options: { readonly dryRun: true; }, fn: (tx: TxContext) => unknown): Promise; /** * Create a sync transaction for the given vault. * The vault must already be open via `openVault()`. * Call `tx.put()` / `tx.delete()` to stage changes, then `tx.commit()` * to write all locally and push atomically to remote. */ transaction(vault: string): SyncTransaction; /** * Internal accessor for the primary store — used by the tx * executor to perform raw adapter reads for pre-flight CAS and * raw writes for rollback. Not part of the public API. * * @internal */ get _store(): NoydbStore; /** * Currently-running multi-record transaction, or `null` outside * Phase 2. `Collection.dispatchDerivations` consults this so a * recursive derived-output write inside `Collection.put` can register * its envelope onto `ctx._executed` and roll back with the main * staged ops on mid-batch failure. * * @internal */ get _activeTxContextOrNull(): TxContext | null; /** * Called by `runTransaction` at Phase 2 start, and by * `Collection.putManyAtomic` (via `derivationSource.setActiveTxContext`) * for its own Phase 2 loop. Nested or concurrent (non-nested) * transactions on the same Noydb instance are NOT supported — * overwriting an active context means another transaction is still * running and its `_executed` list would be cross-contaminated by * the nested writes. We tolerate the overwrite (best-effort, no * throw) to keep the rare interleaving from breaking consumers who * currently get lucky with timing, but applications should ensure * their multi-record commits are serialised on a single Noydb. * * @internal */ _setActiveTxContext(ctx: TxContext): void; /** * Factory for a transient `TxContext` bound to this Noydb. Used by * `Collection.putManyAtomic` (via `derivationSource.createTxContext`) * to publish an active context for the duration of its bulk-atomic * Phase 2 loop, so recursive derivation-output writes register on * `ctx._executed` and roll back together with the source ops. * * @internal */ _createTxContext(): TxContext; /** * Called by `runTransaction` in its `finally`. Only clears when the * passed ctx matches the active one — a defensive no-op if some * other code path already cleared it. * * @internal */ _clearActiveTxContext(ctx: TxContext): void; /** Get sync status for a vault. */ syncStatus(vault: string): SyncStatus; private getSyncEngine; _forEachSyncEngine(vault: string, fn: (engine: SyncEngine) => void): void; on(event: K, handler: (data: NoydbEventMap[K]) => void): void; off(event: K, handler: (data: NoydbEventMap[K]) => void): void; /** * Observable write-queue for this hub instance. Reflects outstanding * in-flight writes across all collections. See {@link WriteQueue}. * * @example * window.addEventListener('beforeunload', (e) => { * if (db.writeQueue.pending) { e.preventDefault(); e.returnValue = '' } * }) */ get writeQueue(): WriteQueue; /** * @internal Mutable tracker behind {@link writeQueue}. Threaded into * each Collection (via Vault) so `put`/`delete` can `track()` writes. * Not part of the public surface — consumers use `writeQueue`. */ get _writeQueueTracker(): WriteQueueTracker; /** * Register a hook that runs before each write. Awaited; a throw * aborts the write. Returns an unsubscribe function. */ onBeforeWrite(handler: WriteHook): Unsubscribe; /** * Register a hook that runs after each committed write. Awaited; * a handler error is warned, never rolled back. Returns an unsubscribe fn. */ onAfterWrite(handler: WriteHook): Unsubscribe; /** Subscribe to cross-tab write conflicts. Returns an unsubscribe. */ onWriteConflict(fn: (c: WriteConflict) => void): Unsubscribe; /** * Enable same-device multi-tab coordination: primary/secondary * election + presence. Browser-only — a graceful no-op (role 'unknown') * when Web Locks / BroadcastChannel are unavailable and nothing is * injected. Idempotent; returns a disposer. */ enableTabCoordination(opts?: TabCoordinationOptions): { dispose: () => void; }; private disableTabCoordination; get tabRole(): TabRole; activeTabs(): TabPresence[]; onTabRoleChange(fn: (r: TabRole) => void): Unsubscribe; onActiveTabsChange(fn: (t: TabPresence[]) => void): Unsubscribe; /** @internal The write-hook registry, threaded into each Collection. */ get _writeHooks(): WriteHookRegistry; /** @internal The observe bus, threaded into every Collection. */ get _subsystemBus(): ServiceBus; /** @internal Stable per-instance id for schema-cutover coordination. */ get _clientId(): string; /** @internal Session that owns this instance's writers. */ get _sessionId(): string; /** * @internal Drain-barrier coordination transport for the schema fence. * The default store-backed provider reproduces today's fence behavior; a * `by-*` real-time transport is injected via `coordinationStrategy`. */ get coordination(): CoordinationProvider; /** * #693: true when multi-tab coordination is active at all — presence/election * alone (`propagateWrites: false`) or full write-propagation — a peer tab can * write this instance's shared store out-of-band either way, so per-collection * marker-id sets are not authoritative and the re-create gate must fall back to * a store read. Live (tab coordination is enable/disable-able at runtime). */ get _tabCoordinationActive(): boolean; /** * Soft-lock a single vault: clear its in-memory keyring, DEKs, vault * instance, sync engine, policy enforcer, and active-tier entry — * WITHOUT destroying the `Noydb` instance. * * Designed for "lock screen" UX: the user taps **Lock** and DEKs are * scrubbed from memory immediately, but the same `Noydb` instance can * be re-unlocked via {@link unlockViaAuthenticator} (tier 2) or * {@link unlockViaPin} (tier 3) without re-running `createNoydb`. * * **QuickUnlock state is preserved.** That's the whole point — the * user can still resume via PIN without a full credential re-prompt. * The on-disk `_meta/policy` document is also kept in cache (it * survives lock; nothing about it changes when DEKs are scrubbed). * * No-op when `vault` is not currently in cache (idempotent). */ lockVault(vault: string): void; close(): void; /** * Returns a snapshot of all translator invocations since the last * `close()`. Useful for testing and compliance auditing. The log is * in-memory only — it is cleared when `db.close()` is called. * * Entries deliberately omit content hashes. See `TranslatorAuditEntry` * and issue for the rationale. */ translatorAuditLog(): readonly TranslatorAuditEntry[]; /** * Invoke the configured `plaintextTranslator` (or serve from cache). * Records one `TranslatorAuditEntry` per call regardless of cache hit. * Called by `Vault` during `put()` for `autoTranslate: true` fields. * * @internal — not part of the public API surface */ invokeTranslator(text: string, from: string, to: string, field: string, collection: string): Promise; /** * Read the active policy for a vault. Loads from `_meta/policy` on * first call; subsequent calls hit the in-memory cache. Throws * `ValidationError` if the vault has not been opened. */ getPolicy(vault: string): Promise; /** * Replace the policy document at `_meta/policy` and update the * in-memory cache. Gated by the `enroll-user` policy (a policy * change is fundamentally a privilege-management action). */ updatePolicy(vault: string, override: Partial): Promise; /** * Read the current vault-level user-directory toggle. Returns * the default-on shape (`{ enabled: true }`) when no `_meta/directory` * document has been persisted yet. * * No role gate — anyone who can open the vault can read the toggle. */ getDirectoryEnabled(vault: string): Promise; /** * Toggle the vault's user-directory listing on or off. * Owner-only. When disabled, `listUsersWithEnvelopes()` throws * {@link import('./errors.js').DirectoryDisabledError} for callers * whose role is neither `owner` nor `admin`. * * Honest caveat: this is a UX flag, not a privacy guarantee. The * keyring file at `_keyring/` and the envelope ciphertext at * `_users/` remain observable to anyone with direct store * read access — only the hub-level enumeration is gated. See * `https://github.com/vLannaAi/noy-db-docs/blob/main/content/docs/services/user-envelope.md` → "Directory visibility". */ setDirectoryEnabled(vault: string, enabled: boolean): Promise; /** * Evaluate a policy gate against the active session tier and the * presented factor proofs. Throws {@link PolicyDeniedError} on * denial; resolves with `void` on success. * * @param vault The vault whose policy applies. * @param gate Gate name — built-in (e.g. `'rotate-passphrase'`) * or app-defined (`app:*`). * @param presented Caller-supplied factor proofs. */ checkGate(vault: string, gate: GateName, factors?: FactorProofBundle): Promise; /** Read or persist the vault policy at `_meta/policy` on first open. */ private bootstrapPolicy; /** * Throw {@link RecoveryNotEnrolledError} or * {@link ManagedRecoveryNotEnrolledError} when recovery enrollment * is missing. * * Two enforcement modes: * * 1. **Managed-mode mandatory strong-recovery.** When * `passphraseMode === 'managed'`, the vault MUST have at least * one **strong** recovery profile (Shamir today). Paper alone is * rejected because under managed mode the user has no memorized * passphrase, so losing the paper sheet = losing every record. * This check is unconditional — independent of `requireRecovery` * and the `recover-passphrase` gate. * * 2. **Opt-in strict mandatory-recovery.** When * `requireRecovery: true` is set on createNoydb (and the gate is * not explicitly disabled), require ANY recovery profile (paper * or shamir). This is the v0.x default-off behavior; v1.0 may * flip it default-on. * * The managed-mode check fires from {@link bootstrapPolicy} unless * the `skipManagedCheck` flag is set (used by * {@link openVaultAndEnrollRecovery} to allow atomic create-and-enroll). */ private assertRecoveryEnrolled; /** * Internal accessor used by tier-2/tier-3 unlock paths * to mark the active session tier. * @internal */ _setActiveTier(vault: string, tier: ActiveTier): void; /** Add a tier-2 authenticator slot — see {@link TeamFacade.enrollAuthenticator}. */ enrollAuthenticator(vault: string, options: EnrollAuthenticatorOptions, factors?: FactorProofBundle): Promise; /** Remove a tier-2 authenticator slot — see {@link TeamFacade.removeAuthenticator}. */ removeAuthenticator(vault: string, slotId: string, factors?: FactorProofBundle): Promise; /** Read the slot list for a vault — see {@link TeamFacade.listAuthenticators}. */ listAuthenticators(vault: string): Promise>; /** Mutate an authenticator slot's `meta` — see {@link TeamFacade.updateAuthenticator}. */ updateAuthenticator(vault: string, slotId: string, options: UpdateAuthenticatorOptions, factors?: FactorProofBundle): Promise; /** Native WebAuthn enrollment — see {@link TeamFacade.enrollWebAuthn}. */ enrollWebAuthn(vault: string, ceremony: (keyring: UnlockedKeyring) => Promise, factors?: FactorProofBundle): Promise<{ credentialId: string; }>; /** List webauthn-method slots — see {@link TeamFacade.listWebAuthnSlots}. */ listWebAuthnSlots(vault: string): Promise>; /** Unlock via a tier-2 authenticator slot — see {@link TeamFacade.unlockViaAuthenticator}. */ unlockViaAuthenticator(vault: string, slotId: string, verify: (slot: KeyringAuthenticator) => Promise): Promise; /** * Set the owner-curated public envelope for a vault. Throws * `ValidationError` if the developer did not opt the hub into * `publicEnvelope` via `NoydbOptions`, or if the input violates * the resolved schema (oversized icon, disallowed MIME, oversized * string, unknown field). * * `createdAt` is set on the first write and preserved on every * subsequent write. `updatedAt` is refreshed on every write. * `version` is monotonic — increments on every successful write. */ setPublicEnvelope(vault: string, input: SetPublicEnvelopeInput): Promise; /** * Read the public envelope for a vault. Returns `undefined` when * none has been written. Pass `locale` to resolve any locale-map * fields to plain strings; omitting `locale` returns the raw map. * * Works even when the developer didn't enable * `publicEnvelope` — reads are passive and never throw on a * missing schema (the envelope is plaintext and exists on disk * regardless). */ getPublicEnvelope(vault: string, opts?: { readonly locale?: string; }): Promise; /** English summary of the configured auth model — see {@link TeamFacade.describeAuthConfig}. */ describeAuthConfig(vault: string): Promise; /** Mermaid `flowchart TB` source for the auth graph — see {@link TeamFacade.diagramAuthConfig}. */ diagramAuthConfig(vault: string): Promise; /** Per-user enrollment summary — see {@link TeamFacade.describeUserAuth}. */ describeUserAuth(vault: string, userId: string, factors?: FactorProofBundle): Promise; /** Bulk per-user enrollment summary — see {@link TeamFacade.describeAllUsersAuth}. */ describeAllUsersAuth(vault: string, factors?: FactorProofBundle): Promise>; /** Rotate the user's passphrase (user remembers old) — see {@link TeamFacade.rotatePassphrase}. */ rotatePassphrase(vault: string, input: RotatePassphraseInput, factors?: FactorProofBundle): Promise; /** Reset the passphrase using a recovery proof — see {@link TeamFacade.recoverPassphrase}. */ recoverPassphrase(vault: string, input: RecoverPassphraseInput, factors?: FactorProofBundle): Promise; /** Deliberate paper/Shamir recovery-code regeneration — see {@link TeamFacade.rotateRecovery}. */ rotateRecovery(vault: string, options: RotateRecoveryOptions, factors?: FactorProofBundle): Promise; /** Atomic create-and-enroll for managed-mode vaults — see {@link TeamFacade.openVaultAndEnrollRecovery}. */ openVaultAndEnrollRecovery(vault: string, opts: { readonly recovery: ReadonlyArray; readonly locale?: string; }): Promise<{ readonly vault: Vault; readonly recoveryEnrollments: ReadonlyArray; }>; /** Recovery flow under managed-passphrase mode — see {@link TeamFacade.recoverManagedPassphrase}. */ recoverManagedPassphrase(vault: string, options: { readonly recoveryProof: RecoveryProof; readonly passphrasePolicy?: PassphrasePolicy; }): Promise; /** Atomic peer-recovery of an existing user's keyring — see {@link TeamFacade.recoverUser}. */ recoverUser(vault: string, options: RecoverUserOptions, factors?: FactorProofBundle): Promise; /** Persist a recovery enrollment (paper or Shamir) — see {@link TeamFacade.enrollRecovery}. */ enrollRecovery(vault: string, enrollment: RecoveryEnrollmentInput): Promise; /** Read the persisted recovery entries (paper + Shamir) — see {@link TeamFacade.listRecoveryEntries}. */ listRecoveryEntries(vault: string): Promise<{ paper: ReadonlyArray; shamir: ReadonlyArray; }>; /** Register a tier-3 quick-unlock state — see {@link TeamFacade.enrollUnlock}. */ enrollUnlock(vault: string, state: QuickUnlockState, factors?: FactorProofBundle): Promise; /** Resume a session via the registered tier-3 state — see {@link TeamFacade.unlockViaPin}. */ unlockViaPin(vault: string, resume: (state: QuickUnlockState) => Promise): Promise; /** Drop the tier-3 state for a vault — see {@link TeamFacade.clearQuickUnlock}. */ clearQuickUnlock(vault: string): void; /** Public defensive-copy accessor for the unlocked keyring — see {@link TeamFacade.getKeyring}. */ getKeyring(vault: string): Promise; /** * Live-reference variant used by the hub's own code paths. Internal * mutations on `deks` (e.g. {@link ensureCollectionDEK} adding a * collection key) need to land on the cached keyring so subsequent * accesses see them. Not exposed publicly — callers outside hub * should use {@link getKeyring}, which returns a defensive copy. */ private _getKeyringInternal; /** * Take an on-demand checkpoint of the given vault. * Requires `snapshotStrategy: withSnapshots({ store })` in `createNoydb`. * @throws ValidationError when the vault is not open */ snapshot(vault: string, opts?: { label?: string; note?: string; }): Promise; /** * List all snapshots for the given vault, newest first. * Reads only the sidecar index — does not download snapshot bytes. */ listSnapshots(vault: string): Promise; /** * Restore the vault to a previously snapshotted state. * Runs `verifyBackupIntegrity()` automatically on restore. * @throws SnapshotNotFoundError when `version` doesn't exist in the store * @throws ValidationError when the vault is not open */ restoreSnapshot(vault: string, version: string): Promise; } /** Create a new NOYDB instance. */ export declare function createNoydb(options: NoydbOptions): Promise; export {};