import type { UserEnvelope, DeepPartialOrNull, Unsubscribe, UserEnvelopePresented, LiveUserEnvelope, VaultUserApi, UserApiDeps, UserApiFactory } from '../../../kernel/types.js'; import type { UserVisibility } from '../types.js'; import type { ExportAccessibleOptions } from '../../../with-audit/portability/export-accessible.js'; import type { WithdrawAccessibleOptions, WithdrawResult } from '../../../with-audit/portability/withdraw-accessible.js'; import type { RequestWithdrawalOptions, RequestWithdrawalResult, WithdrawalRequest, WithdrawalRequestStatus, ApproveWithdrawalOptions, RejectWithdrawalOptions } from '../../../with-audit/portability/request-withdrawal.js'; /** * Implementation behind `vault.user`. Constructed once per Vault via * {@link createUserApi}, holds the writer's keyringId in closure so * `updateMe`/`setMe` cannot target any other principal — the own-only * rule is enforced at the type level (no `set(otherKeyringId, …)` * method) AND at runtime (the keyringId argument simply doesn't exist * on the write path). */ export declare class UserApi implements VaultUserApi { /** keyringId → set of listeners. Wildcard '*' fires on every change. */ private readonly listeners; private readonly adapter; private readonly vaultName; /** The writer's own keyringId. Frozen at construction time. */ private readonly writerKeyringId; private readonly getDek; /** * Policy-gate validator. When omitted, gates are skipped — useful * for low-level tests that exercise the storage layer directly. * Production paths always wire the Noydb-backed implementation. */ private readonly checkGate?; /** * Noydb-backed `exportMyAccessibleData`, injected by the Vault * (which holds the keyring + bundle machinery). Omitted in low-level tests. */ private readonly exportAccessible?; /** * Noydb-backed `unilateralWithdrawal`, injected by the Vault. * Destructive — extract + dispose (delete | freeze). Omitted in low-level tests. */ private readonly unilateralWithdraw?; /** * Noydb-backed two-party withdrawal ceremony, injected by the * Vault. requestWithdraw = requester side; the rest = owner side. */ private readonly requestWithdraw?; private readonly listWithdrawals?; private readonly approveWithdraw?; private readonly rejectWithdraw?; constructor(deps: UserApiDeps); /** * File a two-party withdrawal request for the caller's accessible * scope. Non-destructive (writes a pending request); an owner later approves * or rejects. This is the path for read-only roles (`client`/`viewer`) that * cannot self-serve a destructive `unilateralWithdrawal`. Gated by * `user-request-withdrawal` (enabled by default). */ requestWithdrawal(opts?: RequestWithdrawalOptions): Promise; /** Owner side: list filed withdrawal requests (optionally by status). */ listWithdrawalRequests(opts?: { status?: WithdrawalRequestStatus; }): Promise; /** * Owner side: approve a pending request. Extracts the requester's * recorded scope under firm authority, disposes of the source per the * request's disposition, and returns the re-keyed bundle to hand back. Gated * by `approve-user-withdrawal` (tier-2 default) + owner/admin role. */ approveWithdrawal(requestId: string, opts?: ApproveWithdrawalOptions): Promise; /** Owner side: reject a pending request (no data is touched). */ rejectWithdrawal(requestId: string, opts?: RejectWithdrawalOptions): Promise; /** * Single-party withdrawal: export the caller's accessible scope * (re-keyed) and dispose of the source (`delete` or `freeze`). Gated by the * fail-closed built-in `client-unilateral-withdraw` policy — undefined or * disabled → throws (use `requestWithdrawal`). The firm enables it at vault * creation. */ unilateralWithdrawal(opts: WithdrawAccessibleOptions): Promise; /** * Export the calling user's accessible scope as a portable, re-keyed * `.noydb` bundle. Non-destructive and **always allowed** (data sovereignty * by construction) but audited. Scope = the caller's DEK access set. */ exportMyAccessibleData(opts?: ExportAccessibleOptions): Promise; /** Read the writer's own envelope. Returns null if never written. */ me(): Promise | null>; /** * Deep-merge a partial patch into the writer's own envelope. Creates * the envelope on first call. Optimistic-concurrency safe — a stale * `_v` (parallel writer on another device) throws `ConflictError`. * * Patch semantics: * - `undefined` (or omitted key) — skip; existing value preserved * - `null` — delete the field from the merged result * - any other value — overwrite (deep-merge for plain objects, * replace for primitives / arrays) * * To clear a field, pass `null` rather than `undefined`. Callers * with shape `T = string | null` where `null` is a meaningful value * should use `setMe` for that specific field instead — `null` here * always means delete. * * Gated by the `edit-own-profile` policy gate (default `minTier: 3`). * Pass `presented` to satisfy tightened policies that require a * factor proof (e.g. STRICT_POLICY's TOTP requirement). */ updateMe>(patch: DeepPartialOrNull, presented?: UserEnvelopePresented): Promise>; /** * Replace the writer's own envelope with `payload`. Use sparingly — * `updateMe` is the canonical mutation. No `expectedVersion` check; * callers explicitly take last-write-wins semantics. * * Gated by `edit-own-profile`. See `updateMe` for `presented` usage. */ setMe(payload: T, presented?: UserEnvelopePresented): Promise>; /** * Read the current user's visibility flag from * `_meta/visibility/`. Returns `{ hidden: false }` when no * document has been persisted (the default-visible case). */ getMyVisibility(): Promise; /** * Update the current user's visibility in the team directory. * * - `hidden: true` — opt out of the default `listUsersWithEnvelopes` * listing. `owner`/`admin` callers can still see the user by passing * `{ includeHidden: true }`. * - `hidden: false` — opt back in. * * Own-only by construction: the keyringId argument doesn't exist on * this method, so no caller can hide or unhide another principal. * * Honest caveat: this is a UX flag, not a privacy guarantee. The * envelope ciphertext at `_users/` and the keyring file at * `_keyring/` are both still observable to anyone with direct * store read access. See `https://github.com/vLannaAi/noy-db-docs/blob/main/content/docs/services/user-envelope.md` → * "Directory visibility". */ setMyVisibility(visibility: UserVisibility): Promise; /** * Read another principal's envelope by their keyringId. Returns null * if the principal exists but has no envelope yet, or if the * keyringId does not exist at all. * * Gated by `view-team-profiles` (default `minTier: 2`) — but ONLY for * cross-principal reads. Reading your own envelope (`keyringId === * self`) is never gated; that's just `me()` written long-form. */ get(keyringId: string, presented?: UserEnvelopePresented): Promise | null>; /** * Read every persisted envelope in the vault. Order is store-defined. * * Gated by `view-team-profiles`. Default policy (`minTier: 2`) lets * any authenticated session read all envelopes. Two privacy-strict * opt-outs: * * - `view-team-profiles.enabled: false` → list() returns only the * caller's own envelope (silent self-fallback, no thrown error). * - `view-team-profiles.minTier: 1` + insufficient tier → throws * `PolicyDeniedError` with `reason: 'insufficient-tier'`. The * caller is expected to elevate, not silently degrade. * * The asymmetry is deliberate: `enabled: false` is a deliberate * design choice ("nobody sees teammate profiles in this app"); * `insufficient-tier` is "you need to authenticate further". Different * UX prompts for different intents. */ list(presented?: UserEnvelopePresented): Promise[]>; /** * Listen for changes to a specific keyringId's envelope. The callback * fires synchronously after every successful local `updateMe` / * `setMe` for that principal. * * Cross-instance changes (a teammate edits their profile on their * device, the sync engine pulls the diff onto this device) will fire * subscribers when the sync layer replays the write through this API. * In v1, subscribers do NOT fire on raw store changes — wire your sync * layer to call back through `vault.user.setMe` / `updateMe` if you * need that. * * Pass keyringId `'*'` to fire on every change in the vault. */ subscribe(keyringId: string, cb: (env: UserEnvelope | null) => void): Unsubscribe; /** * Reactive handle that caches the current value and re-reads on every * change for the given keyringId. Convenient for framework bindings: * * const live = vault.user.live(vault.userId) * live.subscribe(env => render(env?.data)) * * Initial value is `null` until the first `current()` call materializes * it via `vault.user.get()`. Call `stop()` when done to release the * subscription. */ live(keyringId: string): LiveUserEnvelope; private fireChange; } /** * Build the `vault.user` API implementation from its dependencies. This is * the real {@link UserApiFactory} — `createNoydb()` dynamically imports it * and stashes it on the `Noydb` instance so `Vault`'s constructor can call * it synchronously. */ export declare const createUserApi: UserApiFactory;