import type { NoydbStore, EncryptedEnvelope, HistoryConfig, ExportStreamOptions, ExportChunk, CollectionConflictResolver, CrossTierAccessEvent, TierMode, Role, VaultUserApi } from './types.js'; import type { Noydb } from './noydb.js'; import type { IssueDelegationOptions, DelegationToken } from '../with-party/team/delegation.js'; import { Collection } from './collection.js'; import type { CacheOptions } from './collection.js'; import type { JoinableSource } from './query/index.js'; import type { OnDirtyCallback } from './collection.js'; import type { UnlockedKeyring, BundleRecipient } from '../with-party/team/keyring.js'; import { type PortabilityStrategy } from '../with-audit/portability/strategy.js'; import type { MaterializedViewRegistry } from '../with-formula/materialized-views/registry.js'; import type { MaterializedViewStrategyHandle } from '../with-formula/materialized-views/types.js'; import type { OverlayedViewRegistry } from '../with-formula/overlay-views/registry.js'; import type { OverlayedViewStrategyHandle } from '../with-formula/overlay-views/types.js'; import type { PublicEnvelope } from '../with-party/directory/public-envelope/types.js'; import type { ExportFormat, KeyringFile, SensitiveOpt, IndexFieldName, IndexDefFor, MoneyFieldsOpt } from './types.js'; import { ElevatedHandle } from '../with-commit/tx/elevated-handle.js'; import type { NoydbEventEmitter } from './events.js'; import type { StandardSchemaV1 } from './schema.js'; import type { BlobStrategy } from '../port/with/blob-strategy.js'; import type { ObjectProjection } from '../with-shape/blobs/object-projection.js'; import type { ArchiveStrategy } from '../with-fork/archive/index.js'; import type { ArchivePolicy, ArchiveResult, ArchiveRunOptions } from '../with-fork/archive/index.js'; import { type SequenceHandle, type FormattedSequenceHandle, type SequenceOptions } from '../with-commit/sequence/index.js'; import { type SequenceStrategy } from '../with-commit/sequence/strategy.js'; import { type Assignment } from '../with-commit/numbering/index.js'; import type { DeferredNumberingConfig } from '../with-commit/numbering/descriptor.js'; import type { IndexStrategy } from '../with-lookup/indexing/strategy.js'; import type { LazyStrategy } from '../port/with/lazy-strategy.js'; import type { AggregateStrategy } from '../with-lookup/aggregate/strategy.js'; import type { CrdtStrategy } from './types.js'; import type { TiersStrategy, TierMoveResult } from '../with-audit/tiers/strategy.js'; import type { SearchStrategy } from '../with-lookup/search/strategy.js'; import { type CargoStrategy } from '../with-cargo/strategy.js'; import { type BrokerStrategy, type CredentialBrokerHandle } from '../port/with/broker-strategy.js'; import type { LedgerStore } from '../with-commit/history/ledger/store.js'; import type { VaultInstant } from '../with-commit/history/time-machine.js'; import { type HistoryStrategy } from '../with-commit/history/strategy.js'; import { type ForgetStrategy, type ForgetResult } from '../with-audit/forget/strategy.js'; import { type SubjectRef } from '../with-audit/forget/subject-index.js'; import type { VaultFrame } from '../with-fork/shadow/vault-frame.js'; import { type ShadowStrategy } from '../with-fork/shadow/strategy.js'; import type { ConsentContext, ConsentAuditEntry, ConsentAuditFilter, ConsentOp } from '../with-audit/consent/consent.js'; import { type ConsentStrategy } from '../with-audit/consent/strategy.js'; import { type PeriodsStrategy } from '../with-audit/periods/strategy.js'; import { type RefDescriptor, type RefViolation } from './refs.js'; import type { DictionaryHandle, DictionaryOptions, DictKeyDescriptor, StaticDictDescriptor } from '../port/with/i18n-strategy.js'; import { // #653 type LookupDescriptor } from '../port/with/lookup-strategy.js'; import { type LinkSpec, type LinkSetHandle } from '../with-shape/links/names.js'; import type { EmbeddingDescriptor } from '../with-lookup/embeddings/index.js'; import type { ComputedFields } from '../with-formula/computed/index.js'; import { type I18nStrategy, type I18nTextDescriptor } from '../port/with/i18n-strategy.js'; import { type ViaFieldSpec } from './via/compose.js'; import { ViaGraph } from './via/graph.js'; import { type RollupDeleteIntent } from './via/dispatch.js'; import { type SyncStrategy } from '../with-party/team/sync-strategy.js'; import type { GuardRegistry } from '../with-audit/guards/registry.js'; import type { GuardStrategyHandleAny } from '../with-audit/guards/types.js'; import type { ReadOnlyVaultFacade } from '../with-audit/guards/read-only-facade.js'; import type { DerivationRegistry } from '../with-formula/derivations/registry.js'; import type { DerivationStrategyHandle } from '../with-formula/derivations/types.js'; import type { ConflictPolicy } from './types.js'; import type { CrdtMode } from '../with-commit/crdt/crdt.js'; import type { JoinedHandle } from '../with-shape/satellites/types.js'; import { type PeriodRecord, type ClosePeriodOptions, type OpenPeriodOptions } from '../with-audit/periods/index.js'; import { type EnclaveKey } from './enclave/index.js'; import type { RecipientSealer } from '../with-party/team/managed-passphrase.js'; import { type ExportBlobsOptions, type ExportBlobsHandle } from '../with-shape/blobs/export-blobs.js'; import { type BlobFieldsConfig, type CompactRunOptions, type CompactionResult } from '../with-shape/blobs/blob-compaction.js'; import { type IssueMagicLinkGrantOptions, type MagicLinkGrantRecord } from '../with-party/team/magic-link-grant.js'; import { CustodyApi } from '../with-party/custody/index.js'; import { SchemaFenceController } from '../with-shape/schema-update/fence-controller.js'; import { type FenceDoc } from '../with-shape/schema-update/fence.js'; import type { SchemaUpdateStrategy } from '../with-shape/schema-update/types.js'; import type { AttestationFieldSchema, RevocationList } from '@noy-db/attestation'; import { type AttestationStrategy } from '../with-audit/attestation/vault-facade.js'; import { type SealedRecordStrategy } from '../with-audit/sealed-record/strategy.js'; import type { DumpSchemaOptions, VaultSchemaSnapshot, SchemaIntrospection } from '../with-shape/introspection/types.js'; import type { VaultIntrospectState } from '../with-shape/introspection/walk.js'; import type { FieldMeta } from '../with-shape/introspection/field-meta.js'; import type { CollectionMeta, VaultMeta } from '../with-shape/introspection/meta.js'; import { type ClassifiedEntry, type ClassifiedStrategy } from '../port/with/classified-strategy.js'; /** A vault (tenant namespace) containing collections. */ export declare class Vault { #private; private readonly adapter; /** The vault's name as passed to `openVault()`. Stable for the instance lifetime. */ readonly name: string; /** Backreference to the parent `Noydb` — lets vault-scoped services reach the strategy seam * without threading `db` through every API (type-only import keeps the module graph acyclic). */ readonly noydb: Noydb; /** The active in-memory keyring. NOT readonly — `load()` refreshes it after restoring a * different keyring file, or in-memory/on-disk DEKs drift apart and decrypt fails with TamperedError. */ private keyring; private readonly encrypted; private readonly emitter; private readonly onDirty; private readonly onRegisterConflictResolver; private readonly syncAdapter; private readonly getPurgeableTargets; private readonly historyConfig; /** tree-shake seam for the optional blob service; `undefined` means "blobs are off for this * vault" — every `collection.blob(id)` call throws with a pointer at `@noy-db/hub/blobs`. */ private readonly blobStrategy; private readonly objectStore; /** Cold-storage archival strategy (the archive target store). */ private readonly archiveStrategy; /** Per-collection record archival policies. Indexed by collection name. */ private readonly archiveRegistry; private readonly indexStrategy; private readonly lazyStrategy; private readonly aggregateStrategy; private readonly crdtStrategy; private readonly tiersStrategy; private readonly searchStrategy; /** Cargo (partition extraction) strategy — `NO_CARGO` (throwing) unless `withCargo()` was passed. * Public so the `extractPartition` free function (which takes a `Vault`) routes through it. */ readonly cargoStrategy: CargoStrategy; private readonly brokerStrategy; private readonly sealedRecordStrategy; private readonly portabilityStrategy; private readonly sequenceStrategy; private readonly consentStrategy; private readonly periods; private readonly linksEnforcer; private readonly shadowStrategy; private readonly historyStrategy; private readonly forgetStrategy; private readonly i18nStrategy; private readonly syncStrategy; private readonly classifiedStrategy; /** Per-vault guard registry; `null` until `_initGuards()` runs (or for vaults that never register * one) — the runtime class is dynamic-imported on demand to keep it out of the floor bundle. */ private guardRegistry; /** Per-vault derivation/MV/overlay registries — same lazy-load contract as * `guardRegistry`: each stays `null` until its `_init*()` runs with at * least one strategy/MV/overlay handle. */ private derivationRegistry; private materializedViewRegistry; private overlayedViewRegistry; /** Per-vault dependency graph (#638) — field postures + derivation/MV/overlay/ * computed/via-deps edges. Metadata-only (field names, postures, grains — * never values or key material); always present, unlike the registries above. */ readonly graph: ViaGraph; /** #638 Task 4 — open sync/cutover/restore touch collector, or `undefined` between batches. */ private _graphBatch; /** Cached read-only facades for guard/derivation callbacks — split by * resolution layer (`layer:'guard'` vs `'derivation'`, each field's * `onMissing` policy differs). Allocated eagerly inside `_initGuards()`/ * `_initDerivations()` so read accessors stay synchronous; `null` for * vaults without that service. */ private guardFacade; private derivationFacade; private getDEK; /** * Per-principal user envelope API. * * - Write-self: `me()`, `updateMe(patch)`, `setMe(payload)` — always * target this vault session's keyringId. There is no method to write * another principal's envelope (own-only write rule, structural). * - Read-anyone: `get(keyringId)`, `list()` — read other principals' * envelopes, subject to the `view-team-profiles` policy gate. * - Reactive: `subscribe(id, cb)`, `live(id)` — fire on local writes. * * @see docs/superpowers/specs/2026-05-05-user-envelope-design.md */ readonly user: VaultUserApi; /** * FR-6 custody API — the sovereign-custody surface, mirroring `vault.user.*`. * * - `grantCustodian(opts)` / `revokeCustodian(opts)` — owner-only: mint / * remove a `custodian` who operates the vault fully but can never grant / * rotate / sever / extract. * - `liberate(opts)` — custodian-only: the audited claim of ownership over a * sealed-owner (Deed) vault (mints a DISTINCT new owner; ledger-audited). * * @see docs/superpowers/specs/2026-06-17-fr6-deed-custodian-liberate-design.md */ readonly custody: CustodyApi; /** Re-derives an UnlockedKeyring from the adapter using the active user's passphrase; called by * `load()` after the on-disk keyring file has been replaced. Provided by Noydb at openVault() * time; `undefined` in tests that construct Vault directly (load() then skips the refresh). */ private readonly reloadKeyring; private readonly collectionCache; private satelliteRegistry; /** Vault-level schema cutover fence/controller. */ readonly schemaFence: SchemaFenceController; /** * per-collection `blobFields` retention/TTL config. * Populated on `collection({ blobFields })` and read by * `vault.compact()`. Indexed by collection name. */ private readonly blobFieldsRegistry; /** * Attestation facade (issue/revoke + the per-collection field-schema * registry). Built in the constructor. */ private readonly attestation; /** * Per-vault ledger store. Lazy-initialized on first * `collection()` call (which passes it through to the Collection) * or on first `ledger()` call from user code. * * One LedgerStore is shared across all collections in a vault * because the hash chain is vault-scoped: the chain head is a * single "what did this vault do last" identifier, not a * per-collection one. Two collections appending concurrently is the * single-writer concurrency concern documented in the LedgerStore * docstring. */ private ledgerStore; /** Lazily-built atomic-sequence store. See {@link sequence}. */ private sequenceStore; /** Lazily-built deferred-numbering engine. See {@link runNumberingPass}. */ private deferredNumbering; /** Registered deferred-numbering series, keyed by series name. */ private readonly numberingConfigs; /** * Background writes for persisted-schema envelopes (#schema-dump v0 * slice 1). One promise per `collection({ persistJsonSchema: true })` * registration that actually fired a derive call. Fire-and-forget * from the collection factory; tests await * {@link _drainPendingSchemaWrites} before asserting on storage. * Production code does not need to drain — the writes are * idempotent fingerprints, not correctness invariants. */ private _pendingSchemaWrites; /** * Per-vault foreign-key reference registry. Collections * register their `refs` option here on construction; the * vault uses the registry on every put/delete/checkIntegrity * call. One instance lives for the compartment's lifetime. */ private readonly refRegistry; /** * Vault-default locale. Set via * `openVault(name, { locale })`. Used as the fallback locale * when per-call `{ locale }` options are not specified on individual * `get()`/`list()` calls. */ private locale; /** * Vault-level descriptive metadata. Set once at construction via * `openVault(name, { meta })`. First-wins: re-opening a cached vault * with different meta leaves the original untouched. */ private readonly vaultMeta; /** * Current consent scope. Set by `withConsent()` and * restored in its finally block. When non-null, every collection * access inside the scope writes one entry to `_consent_audit`. * * Single-slot by design — two concurrent withConsent calls on the * same Vault stomp each other. Adopters needing per-flight scoping * should use separate Vault instances. */ private consentContext; /** dictKeyField registry: collection name → field name → dictionary name — `DictionaryHandle.rename()`'s reference-update source; populated by `collection()` from `dictKeyFields`. */ private readonly dictKeyFieldRegistry; /** Names of `staticDict()`-backed dictionaries (skip rename tracking; `vault.dictionary(name)` refuses mutation on these via `StaticDictReadonlyError`). Populated at `collection()` config time. */ private readonly staticDictNames; /** Static-dict descriptors by dictionary name — backs the read-path label resolver and the `resolveDictSource` snapshot. Last writer wins across collections (tables match by construction). */ private readonly staticByName; /** Per-collection field name → StaticDictDescriptor, validated by `enforceStaticDictOnPut`. */ private readonly staticDescriptorByField; /** i18nText fields: collection name → field name → descriptor. Used by `applyI18nLocale`/`validateI18nTextValue`; populated by `collection()` from `i18nFields`. Private — the #664 `via/reconcile.ts` dispatch reads these through {@link _viaReconcileCtx}'s structural bag, not `this` directly (a plain structural bag, never a `Vault` import — S5 port-layering forbids the kernel spine from reaching a with-* service, and this file's own reconcile-ladder logic moved OUT into that unguarded module). */ private readonly i18nFieldRegistry; /** Cache of DictionaryHandle instances, one per dictionary name. */ private readonly dictionaryCache; /** #650 Task 4 (#647) — declared reserved-lookup collection name → dimension name, for dimensions * with `backing:'reserved'` (dict()/dictKey()). The explicit sync-pull registry: NOT a blanket * underscore-glob — populated at `collection()`-declare time and at `dictionary()`-call time. */ private readonly reservedLookupCollections; /** Registered link specs, keyed by link name; set by `vault.link()`. */ private readonly linkRegistry; /** Cache of link-set handles, one per link name (lazy -- see links()). */ private readonly linkSetCache; /** — subscribers for cross-tier access events. */ private readonly crossTierSubs; /** — currently-active elevation, or null. One per vault. */ private activeElevation; /** * Optional translator callback threaded from `Noydb.invokeTranslator`. * Present only when `plaintextTranslator` was configured on `createNoydb()`. */ private readonly translateText; constructor(opts: { adapter: NoydbStore; name: string; noydb: Noydb; keyring: UnlockedKeyring; encrypted: boolean; emitter: NoydbEventEmitter; onDirty?: OnDirtyCallback | undefined; historyConfig?: HistoryConfig | undefined; reloadKeyring?: (() => Promise) | undefined; /** Vault-default locale. */ locale?: string | undefined; /** Translator callback from Noydb. */ plaintextTranslator?: ((text: string, from: string, to: string, field: string, collection: string) => Promise) | undefined; /** * callback to register a per-collection envelope-level * conflict resolver with the SyncEngine. Present when sync is configured. */ onRegisterConflictResolver?: ((name: string, resolver: CollectionConflictResolver) => void) | undefined; /** — optional remote/sync adapter for presence broadcasting. */ syncAdapter?: NoydbStore | undefined; /** #615 — push-only sync targets (backup/archive) this vault may target-purge. Default: none. */ getPurgeableTargets?: () => readonly { store: NoydbStore; role: 'backup' | 'archive'; label?: string; }[]; /** * tree-shake seam — strategy for optional blob storage. * Passed through to every `Collection` built by `vault.collection()`. * `undefined` => every `collection.blob(id)` throws with a pointer * at `@noy-db/hub/blobs`. */ blobStrategy?: BlobStrategy | undefined; objectStore?: ObjectProjection | undefined; archiveStrategy?: ArchiveStrategy | undefined; indexStrategy?: IndexStrategy | undefined; lazyStrategy?: LazyStrategy | undefined; aggregateStrategy?: AggregateStrategy | undefined; crdtStrategy?: CrdtStrategy | undefined; tiersStrategy?: TiersStrategy | undefined; searchStrategy?: SearchStrategy | undefined; cargoStrategy?: CargoStrategy | undefined; brokerStrategy?: BrokerStrategy | undefined; consentStrategy?: ConsentStrategy | undefined; periodsStrategy?: PeriodsStrategy | undefined; shadowStrategy?: ShadowStrategy | undefined; historyStrategy?: HistoryStrategy | undefined; i18nStrategy?: I18nStrategy | undefined; syncStrategy?: SyncStrategy | undefined; guardStrategies?: ReadonlyArray | undefined; numberingConfigs?: ReadonlyArray | undefined; forgetStrategy?: ForgetStrategy | undefined; attestationStrategy?: AttestationStrategy | undefined; classifiedStrategy?: ClassifiedStrategy | undefined; sealedRecordStrategy?: SealedRecordStrategy | undefined; portabilityStrategy?: PortabilityStrategy | undefined; sequenceStrategy?: SequenceStrategy | undefined; /** Vault-level descriptive metadata — set once at construction (first-wins). */ meta?: VaultMeta | undefined; }); /** * Construct (or reconstruct) the lazy DEK resolver. Captures the * CURRENT value of `this.keyring` and `this.adapter` in a closure, * memoizing the inner getDEKFn after first use so subsequent * lookups are O(1). * * `load()` calls this after refreshing `this.keyring` to discard * the prior session's cached DEKs. */ private makeGetDEK; /** * Open a typed collection within this vault. * * - `options.indexes` declares secondary indexes for the query DSL. * Indexes are computed in memory after decryption; adapters never * see plaintext index data. * - `options.prefetch` (default `true`) controls hydration. Eager mode * loads everything on first access; lazy mode (`prefetch: false`) * loads records on demand and bounds memory via the LRU cache. * - `options.cache` configures the LRU bounds. Required in lazy mode. * Accepts `{ maxRecords, maxBytes: '50MB' | 1024 }`. * - `options.schema` attaches a Standard Schema v1 validator (Zod, * Valibot, ArkType, Effect Schema, etc.). Every `put()` is validated * before encryption; every read is validated after decryption. * Failing records throw `SchemaValidationError`. * - `options.i18nFields` declares per-field `i18nText()` descriptors *. Validated on `put()` and locale-resolved on reads. * - `options.dictKeyFields` declares per-field `dictKey()` descriptors *. `put()` validates keys against the declared set; reads * with `{ locale }` add `Label` virtual fields. * * Throws `ReservedCollectionNameError` for names starting with `_dict_` or * equal to `_sequences`. Use `vault.dictionary(name)` for dict collections * and `vault.sequence(name)` for sequence counters. * * Lazy mode + indexes is rejected at construction time — see the * Collection constructor for the rationale. */ /** Assembles {@link ViaReconcileVaultCtx} — the plain structural bag the #664 `via/reconcile.ts` * dispatch reads/writes — from this vault's OWN privates, rather than passing `this` itself. */ private _viaReconcileCtx; collection(collectionName: string, options?: { indexes?: readonly IndexDefFor>[]; /** — auto-reconcile policy for persisted-index drift. */ reconcileOnOpen?: 'off' | 'dry-run' | 'auto'; prefetch?: boolean; cache?: CacheOptions; schema?: StandardSchemaV1; refs?: Record; /** — declare i18nText fields for locale-aware reads. */ i18nFields?: Record; /** — embedding config for write-time vector derivation + semantic retrieval. */ embeddings?: EmbeddingDescriptor; /** — string fields exposed to client-side `retrieve()`. */ textIndexes?: readonly IndexFieldName[]; /** — pre-build the lexical index on open (eager-only). */ warmIndexOnOpen?: boolean; /** — persist the lexical index as an opaque encrypted blob at `_ftindex/`. */ textIndexPersist?: boolean; /** — declare dictKey / staticDict fields for label resolution on reads. */ dictKeyFields?: Record; /** — declare lookup() / enumOf() / dict() fields (#650 Task 2 — the 'lookup' via binding's three tiers). */ lookupFields?: Record; /** Consumer-neutral per-field descriptors (label/unit/semanticType/sensitivity…). See collection.describe(). */ fieldMeta?: Record; /** The collection's own descriptive metadata (label/description/icon). See collection.describe(). */ meta?: CollectionMeta; /** — declare money() fields for currency-safe decimal storage/formatting. */ moneyFields?: MoneyFieldsOpt; viaFields?: Record; /** — declare computed scalar fields, evaluated on write (schema-owned). Each entry may be * a plain `(record) => value` function, OR `{ fn, deps }` to declare source fields for * taint propagation (#638 Task 7 — supersedes the retired `computedDeps` option). */ computed?: ComputedFields; /** — declare classified() sensitive-field descriptors. See the classified-fields spec. */ classifiedFields?: Record; /** — per-collection conflict resolution policy. */ conflictPolicy?: ConflictPolicy; /** — CRDT mode for collaborative editing without conflicts. */ crdt?: CrdtMode; /** * declare deterministic-encryption fields for blind * equality search. See `Collection` constructor docs for the full * trade-off. Requires `acknowledgeDeterministicRisk: true`. */ deterministicFields?: readonly IndexFieldName[]; /** — explicit ack that deterministic encryption leaks equality. */ acknowledgeDeterministicRisk?: boolean; /** — explicit ack for the classified `equatable` knob (R8 door). Required * when any classified field declares `equatable: true`. */ acknowledgeEquatableRisk?: boolean; /** * — structural group-encryption. Fields sealed into their own * `_sealed[field]` envelope slot (per-field key), kept out of the open * `_data` blob. Default-off; byte-identical output when absent. */ sensitive?: SensitiveOpt; /** * — per-record content-encryption keys. When `true`, every record * body is encrypted under a fresh per-record CEK wrapped under the * collection DEK (`_cek`), stable across versions. Foundation for * per-record erasure / record-scoped sealing. Off by * default; non-adopting collections take the legacy path unchanged. */ perRecordKeys?: boolean; satelliteOf?: string; fields?: readonly string[]; joined?: string; /** * Per-record provenance tracking. When `true`, `put()` calls that * supply a `source` option stamp `_source` / `_sourceTs` onto the * unencrypted envelope metadata. Off by default. */ provenance?: boolean; /** * declarative blob retention / TTL policy per slot * name. Values are `{ retainDays?, evictWhen? }`. Evaluated only * when `vault.compact()` runs. */ blobFields?: BlobFieldsConfig; blobTierPolicy?: 'isolate' | 'dedup'; /** — declarative record archival policy: `{ archiveWhen, legalHold? }`. Evaluated when `vault.archive()` runs. */ archive?: ArchivePolicy; /** — declared tiers for this collection. */ tiers?: readonly number[]; /** — how lower-tier reads see above-tier records. */ tierMode?: TierMode; /** * Opt-in persisted JSON Schema. When `true` AND a Zod `schema` is * provided, hub derives a JSON Schema via `zod-to-json-schema` * (optional peer-dep) and writes an encrypted snapshot to * `_schemas/`. Re-runs on every open; hash-skip * avoids write churn when the schema is unchanged. * * Default: `false`. Non-Zod Standard Schema validators receive a * stub envelope flagging the kind without a JSON Schema body. * * @see docs/superpowers/specs/2026-05-22-schema-dump-design.md */ persistJsonSchema?: boolean; /** * Ordered schema-update strategies. On a detected schema * change, evaluated in order; the first non-`allow` decision wins. * A `reject` is enforced at the write path (`put`/`delete` throw). * Requires `persistJsonSchema: true` (detection needs the baseline). */ schemaUpdate?: readonly SchemaUpdateStrategy[]; /** — declare the per-field schema for document attestation (issue side). */ attestation?: AttestationFieldSchema; /** * Per-collection history & tamper-ledger scoping. Overrides the * vault-wide `history` config for THIS collection only (wholesale, not * merged). `enabled: false` suppresses per-record snapshots for this * collection; `ledger: false` excludes its writes from the vault-wide * hash-chained tamper ledger. Lets you confine version snapshots + * tamper-evidence to the few collections where they carry legal weight, * without paying snapshot + ledger-entry-per-write across operational / * derived collections. Defaults to the vault-wide `history` config. */ historyConfig?: HistoryConfig; /** * Opt-in: keep the working set encrypted in RAM, decrypting on read (future phase). * Default false — the working set is plaintext. */ ramCiphertext?: boolean; }): Collection; /** Full-record handle for a satellite pair registered with `joined:` (spec #591). Narrow type — not a Collection. */ joined>(name: string): JoinedHandle; /** * Migrate an existing satellite's records onto per-record CEKs (#599) — * unblocks R-S7 when a base gains forget coverage after its satellite was * already declared without `perRecordKeys`. Call BEFORE re-declaring with * `{ satelliteOf, perRecordKeys: true }` AND before this collection serves * writes (unfenced walk — see {@link migrateSatelliteCek}); resumable. */ migrateSatellitePerRecordKeys(satelliteName: string): Promise<{ migrated: number; }>; /** * Await all background persisted-schema writes triggered by * `collection({ persistJsonSchema: true })` calls on this vault. * Used in tests; production code does not need to call this. */ _drainPendingSchemaWrites(): Promise; /** * Run a coordinated schema cutover. Drains pending writes, waits * for the active client set to quiesce (the ack-barrier), applies every * pending collection transform in bulk, bumps the vault schema generation, * and clears the fence. Returns the count of collections migrated. * `opts.onPoll` (tests) advances other clients between barrier checks. */ runSchemaCutover(opts?: { onPoll?: () => Promise; }): Promise<{ migrated: number; }>; /** * Refresh a loaded collection's view of one document from a peer * tab's broadcast. No-op when the collection isn't loaded in this tab * (it will read fresh on next open). Mirrors `#runCutoverTransform`'s guard. */ _applyRemoteWrite(collectionName: string, docId: string, action: 'put' | 'delete'): Promise; /** @internal #598: refresh cache entries a sync-applied write rewrote underneath us. No-op if not loaded this session. * #650 Task 4 (#647): a reserved-lookup collection has no `Collection` instance — instead, refresh * its already-warmed `LookupHandle._syncCache` (membership/altIndex/snapshot reads never see a * stale verdict for a pulled vocabulary edit) and collect the touch into any open graph batch * (the sync-apply wave-reachability seam Task 5's ref edges will use). No-op if never warmed. */ _invalidateSyncApplied(collection: string, id: string, action: 'put' | 'delete'): Promise; /** @internal #650 Task 4 (#647) — declared reserved-lookup collection names, for `SyncEngine.pull()`'s explicit prefix registry. */ _reservedLookupCollectionNames(): readonly string[]; /** @internal #653 — one-hop reserved-dict deps of `names`, for `SyncEngine`'s partial-sync * filter expansion. Provably complete at one hop: `vault.collection()` refuses `_dict_*` * names, so a dict can never itself declare lookup fields (no dict-of-dict chain). */ _reservedDictDepsOf(names: readonly string[]): readonly string[]; /** @internal #638 Task 4 — open a graph-dispatch touch batch (sync pull()/push()/cutover). */ _beginGraphBatch(): void; /** @internal #638 Task 4 — `Collection._onRecordMutated`'s sync-apply/cutover/restore hook; a * no-op when no batch is open (`_beginGraphBatch()` was never called for this call chain). */ _collectGraphTouch(collection: string, id: string): void; /** @internal #640 — the sync-apply delete socket: `id`'s resolved rollup-parent intents. */ _collectGraphDelete(collection: string, id: string, intents: readonly RollupDeleteIntent[]): void; /** @internal #638 Task 4 — close the open batch and run ONE dispatch wave over it. */ _flushGraphBatch(): Promise; /** @internal #638 Task 4 — cached-collection lookup for `runGraphDispatchWave` (`VaultLike`); never constructs. */ _getCollection(name: string): Collection> | undefined; /** @internal #640 (#644 item 3) — `VaultLike._emit`: the wave's structured error surfacing. */ _emit(event: string, payload: unknown): void; /** * @internal #589 → #604. Physically remove delete markers with `_ts` strictly * older than `before` (ISO timestamp), across `collections` (or all data * collections). Returns the count removed. * * SAFETY: purging a marker RE-OPENS the #589 resurrection window for any peer * offline since before the cutoff — never-GC is safe precisely because the marker * is always present to win convergence. This is an operator-asserted safe-point * ONLY; #604's period-close lifecycle is what earns that assertion. Do not call * it on live/unsettled data. * * Emits no ledger/event itself — its callers (#604's `freezePeriod`, #615's * `purgePeriodTargets`) own the audit record. The sweep body is shared via * {@link _purgeMarkersOn}, which #615 also aims at push-only sync targets. * * Purges the local adapter only. Markers already pushed to the vault's * push-only (`backup`/`archive`) sync targets are reclaimed by #615's * `vault.purgePeriodTargets`; `sync-peer` targets are left untouched (purging * them would re-open the resurrection window — the deferred half of #611). */ _purgeDeleteMarkers(before: string, collections?: string[]): Promise; /** * @internal #615. Sweep delete markers with `_ts < before` off ANY store * (local adapter or a push-only sync target). Returns the count removed. */ private _purgeMarkersOn; /** * @internal #613. Relocate this closed period's in-window records (those * with `_ts < before`) from the hot store to the configured cold tier, * via the routeStore's `compact({ before })`. Returns the count moved. * Requires a `routeStore` with a cold route (the `coldArchival` * capability); throws a clear error otherwise. Non-destructive — reads * fall through to cold, so this does NOT re-open the #589 window. */ _archiveClosedPeriod(before: string): Promise; /** * @internal #615. Sweep delete markers with `_ts < before` off each of the * vault's push-only sync targets, returning a per-target count. Skips * `sync-peer` targets by construction (getPurgeableTargets yields only * backup/archive). Never touches the local adapter. */ _purgePeriodTargets(before: string): Promise; /** * For a detected conflict: capture this tab's clobbered record, * read the common ancestor from history, converge the cache to the store's * authoritative value (the re-read), and return all three for the * WriteConflict payload. Returns null when the collection isn't loaded. */ _captureAndConverge(collectionName: string, docId: string, action: 'put' | 'delete', baseV: number): Promise<{ local: unknown; remote: unknown; base: unknown; } | null>; /** Recover a stuck cutover fence — reset to normal without bumping. */ abortSchemaCutover(): Promise; /** Current schema-cutover fence state for this vault. Thin live read. */ schemaFenceState(): Promise; /** @internal Start heartbeat + fence watcher once a cutover is registered. Async since #553: FenceWatcher dynamic-imports on demand. */ _ensureFenceCoordination(): Promise; /** @internal Stop the heartbeat/watcher (vault lock/close). */ _stopFenceCoordination(): void; /** @internal Best-effort flush of all open collections' persisted * lexical indexes on close(). Called fire-and-forget from noydb.close(). * Correctness is backstopped by the fingerprint: a missed flush → rebuild on * next load. Only collections with textIndexPersist have a flush(); others no-op. */ _flushSearchIndexes(): Promise; /** @internal Drive one heartbeat + watch cycle deterministically (tests). */ _fenceTick(): Promise; /** * Validate i18nText fields on a `put()`. Called by Collection just * before the adapter write, after schema validation. Throws * `MissingTranslationError` when a required translation is absent. * * Delegates through the Via registry (#623) — a no-op only when i18n was * never declared (mirrors the registry check below). */ enforceI18nOnPut(collectionName: string, record: unknown): void; /** * Validate staticDict codes on a `put()`. For each `staticDict()` * field, every stored code must be a declared key of the descriptor's * table, else `UnknownDictCodeError`. Opt out per descriptor with * `{ validateCodes: false }`. * * Delegates through the Via registry — see {@link enforceI18nOnPut}. The * per-field validation itself lives in `via/lookup/registry.ts` * (#650 Task 1 extraction), reached via `port/with/lookup-strategy.ts`. */ enforceStaticDictOnPut(collectionName: string, record: unknown): void; /** * Open a dictionary by name. Returns a `DictionaryHandle` for CRUD * operations on the `_dict_/` reserved collection. * * The handle is cached — multiple calls with the same name return the * same instance. * * @param name The dictionary name (e.g. `'status'` → `_dict_status/`). * @param options Optional ACL overrides (default `writableBy: 'admin'`). * * @example * ```ts * await company.dictionary('status').putAll({ * draft: { en: 'Draft', th: 'ฉบับร่าง' }, * paid: { en: 'Paid', th: 'ชำระแล้ว' }, * }) * ``` */ dictionary(name: string, options?: DictionaryOptions): DictionaryHandle; /** * Declare a managed many-to-many link set. Registers a * `_links_` junction between two endpoint collections; access its * rows via `vault.links(name)`. Idempotent for an identical re-declaration; * a conflicting one throws. See {@link links}. * * ```ts * vault.link('saleLineLinks', { a: ref('saleLines'), b: ref('purchaseLines'), onDelete: 'cascade' }) * ``` * * `a` / `b` accept either a collection name or a `ref(target)` descriptor * (only its `target` is used — links manage their own integrity). `onDelete` * governs what happens to link rows when an endpoint record is deleted * (`'cascade'` default, `'strict'`, `'warn'`). */ link(name: string, spec: { a: string | RefDescriptor; b: string | RefDescriptor; onDelete?: LinkSpec['onDelete']; }): void; /** * Access a declared link set. Throws if `name` was not first * declared via {@link link}. Returns a cached {@link LinkSetHandle}: * `connect(a, b, meta?)`, `disconnect(a, b)`, `has(a, b)`, `of(id)`, `list()`. */ links(name: string): LinkSetHandle; /** * Build a `JoinableSource` for a dictKey field, for use in dict joins. * Returns a source whose snapshot contains `{ key, labels, ...labels }` * records — one per dictionary entry — keyed by the stable key. Returns * `null` when `field` is not a dictKey in `leftCollection`. * * Body extracted to `via/lookup/registry.ts` (#650 Task 1) — * reached via the `port/with/lookup-strategy.ts` seam. See that * function's doc comment for the static-vs-dynamic-dict split and the * cache-warming caveat. */ resolveDictSource(leftCollection: string, field: string): JoinableSource | null; /** * Set or update the vault-default locale at runtime. * Useful when the user switches their preferred language after opening * the vault. */ setLocale(locale: string | undefined): void; /** Return the current vault-default locale. */ getLocale(): string | undefined; /** Return the vault-level descriptive metadata (set-once at construction). */ getMeta(): VaultMeta | undefined; /** * The user id of the keyring backing this vault session. Useful for * UI affordances ("you are alice"), audit trails, and orchestration * composables that need to stamp records with the current actor. */ get userId(): string; /** * The role of the keyring backing this vault session — one of * `owner | admin | operator | viewer | client`. Useful for UI * affordance gates and approval workflows that need to confirm * the caller can perform a given action before attempting it. */ get role(): Role; /** * Build keyring files for bundle recipients without persisting them * to the source vault. Used by `writePod()` when the bundle * is re-keyed for distinct recipients. * * Each recipient becomes its own `KeyringFile` sealed with that * recipient's passphrase. The DEKs wrapped into each slot are * exactly those the recipient's role + permissions justify, and * never wider than the source keyring's own DEK set * (privilege-escalation check). * * Returns a `Record` ready to substitute for * the `keyrings` field of a `vault.dump()` JSON. Adapter is never * touched; the produced files exist only in the bundle bytes. * * @public */ buildBundleRecipientKeyrings(recipients: readonly BundleRecipient[]): Promise>; /** * Authorize an `@noy-db/as-*` export against the current keyring's * `exportCapability`. Throws `ExportCapabilityError` if * the invoking keyring is not authorised. * * `as-*` packages MUST call this before invoking the underlying * export primitive (`exportStream()` / `writePod()` / …). * * - `assertCanExport('plaintext', 'xlsx')` — check plaintext tier * for a specific format. Defaults to empty for every role; owner * must positively grant. * - `assertCanExport('bundle')` — check encrypted-bundle tier. * Defaults to on for owner/admin, off for others. * * See `docs/patterns/as-exports.md` for the full policy. */ assertCanExport(tier: 'plaintext', format: ExportFormat): void; assertCanExport(tier: 'bundle'): void; /** * Authorize an `@noy-db/as-*` import against the current keyring's * `importCapability` (issue ). Throws `ImportCapabilityError` if * the invoking keyring is not authorised. * * `as-*` reader entry-points (`fromString` / `fromBytes`) MUST call * this before parsing or building an `ImportPlan`. * * - `assertCanImport('plaintext', 'csv')` — check plaintext-tier * import for a specific format. Default-closed for every role. * - `assertCanImport('bundle')` — check `.noydb` bundle-import gate. * Default-closed for every role, including owner — import is more * dangerous than export (corrupts vs leaks). * * Owner who wants to import re-grants own keyring with * `importCapability` set explicitly. */ assertCanImport(tier: 'plaintext', format: ExportFormat): void; assertCanImport(tier: 'bundle'): void; /** * Bulk blob extraction primitive. * * Returns an async-iterable handle over every blob attached to * records in the vault. Single capability check (`plaintext/blob`) * at handle creation; single audit entry to `_export_audit` before * the first yield. Per-blob decryption happens lazily as the * consumer pulls tuples. * * ```ts * const handle = vault.exportBlobs({ * collections: ['invoiceScans'], * where: (rec) => (rec as { clientId?: string }).clientId === 'c-123', * }) * for await (const { bytes, meta, recordRef } of handle) { * await uploadToColdStorage(bytes, recordRef) * } * ``` * * @see `@noy-db/hub/store/export-blobs` for the full option surface. */ /** * Evict blob slots per the per-collection `blobFields` retention * policy. * * Iterates every collection declared with `{ blobFields: {...} }`. * For each record, checks every configured slot against its * policy — `retainDays` (age-based TTL) and/or `evictWhen(record)` * (predicate) — and evicts matching slots. Every eviction writes * one entry to `_blob_eviction_audit` (actor + eTag + reason + * timestamp, no plaintext). Consumer-scheduled; noy-db never runs * this on its own. * * ```ts * await vault.compact() // run full pass * await vault.compact({ dryRun: true }) // preview counts * await vault.compact({ maxEvictions: 1000 }) // cap batch * ``` */ /** * Atomic, gap-free numbering. `vault.sequence('invoice-2026').next()` * returns 1, 2, 3, … with no gaps or duplicates under concurrency, via * an optimistic-CAS counter at `_sequences/`. Each name is an * independent sequence. * * **Online-only:** `next()` throws `SequenceOfflineError` unless the * store advertises `capabilities.casAtomic` — gap-free numbering cannot * be serialized by an offline / non-CAS writer. * * ```ts * const n = await vault.sequence('invoice-2026').next() // 1, then 2, … * const cur = await vault.sequence('invoice-2026').peek() // current value, no allocation * ``` * * Pass a `format` to emit a serial string instead of a bare * integer — `next()` then returns `{ serial, formatted }`. Per-partition * reset is inherent (a new partition tuple starts at 1): * * ```ts * const seq = vault.sequence('fatture', { partition: [2026], format: '{partition.0}/{seq:04}' }) * await seq.next() // { serial: 1, formatted: '2026/0001' } * ``` */ sequence(series: string, opts: SequenceOptions & { format: string; }): FormattedSequenceHandle; sequence(series: string, opts?: SequenceOptions): SequenceHandle; /** @internal — lazily build the deferred-numbering engine with a cache-coherent stamp. */ private deferred; /** * Run a deferred-numbering pass for `series`: assign gap-free serials to all * records whose store-commit-time interval has settled, in store-time order. * Returns the assignments made. See {@link sequence} / `withDeferredNumbering`. */ runNumberingPass(series: string): Promise; compact(options?: CompactRunOptions): Promise; /** * Sweep records eligible by their collection's `archive` policy into the * cold archive store. Relocation is envelope-level (no re-encryption) and * bypasses guards + materialized-view dispatch, so issued/immutable * records over a sealed period can be archived without recomputing * finalized aggregates. A `legalHold` predicate blocks archival. * Requires `archiveStrategy: withArchive({ store })` in `createNoydb`. */ archive(options?: ArchiveRunOptions): Promise; /** Relocate one archived record back to the primary store. Returns false if it was not archived. */ restore(collection: string, id: string): Promise; /** List archived record ids for a collection (or all collections with an archive policy). */ listArchived(collection?: string): Promise>; private _archiveContext; exportBlobs(options?: ExportBlobsOptions): ExportBlobsHandle; issueAttestation(collectionName: string, id: string): Promise<{ docId: string; qr: string; keyId: string; publicKeyB64: string; }>; getDocumentSigningPublicKey(): Promise<{ keyId: string; publicKeyB64: string; }>; revokeAttestation(docId: string): Promise; unrevokeAttestation(docId: string): Promise; getRevokedDocIds(): Promise; publishRevocationList(): Promise; private writeExportAudit; /** * Read-only accessor for the invoking keyring's export capability, * with role-based defaults resolved. Useful for UI affordances * (grey out the export button if no capability) without throwing. */ canExport(tier: 'plaintext', format: ExportFormat): boolean; canExport(tier: 'bundle'): boolean; /** * Decrypt a single envelope using the per-collection DEK, returning * the parsed plaintext record. Internal helper for bundle-pipeline * plaintext filters — keeps DEK access encapsulated * inside Vault so callers don't reach into private state. * * @internal */ _decryptEnvelopeForBundleFilter(env: EncryptedEnvelope, collectionName: string): Promise; /** * Read-only accessor for the invoking keyring's import capability * (issue ). UI affordance — returns false in every default-closed * case (every role with no explicit `importCapability` grant). */ canImport(tier: 'plaintext', format: ExportFormat): boolean; canImport(tier: 'bundle'): boolean; /** * Enforce strict outbound refs on a `put()`. Called by Collection * just before it writes to the adapter. For every strict ref * declared on the collection, check that the target id exists in * the target collection; throw `RefIntegrityError` if not. * * `warn` and `cascade` modes don't affect put semantics — they're * enforced at delete time or via `checkIntegrity()`. */ enforceRefsOnPut(collectionName: string, record: unknown): Promise; /** * Enforce inbound ref modes on a `delete()`. Called by Collection * just before it deletes from the adapter. Walks every inbound * ref that targets this (collection, id) and applies its mode * (`strict` throws, `cascade` deletes referencing records, `warn` * no-ops). Managed-link `onDelete` policy is applied in the same * cascade guard. See `with-shape/links/vault-facade.ts`. */ enforceRefsOnDelete(collectionName: string, id: string): Promise; /** * Look up the `RefDescriptor` the left collection declared for a * given field name. Returns `null` when the field has no ref * declaration. Implements the `joinResolver.resolveRef` half of the * structural interface that `Collection.query()` consumes. */ resolveRef(leftCollection: string, field: string): RefDescriptor | null; /** * Resolve a right-side join source by target collection name. * Returns `null` for unknown collections. Implements the * `joinResolver.resolveSource` half of the structural interface. * Only same-vault targets are resolvable — cross-vault joins are * explicitly forbidden by the architecture. */ resolveSource(collectionName: string): JoinableSource | null; /** * Walk every collection that has declared refs, load its records, * and report any reference whose target id is missing (modes * reported alongside each violation). Returns `{ violations: [...] }` * instead of throwing — the point is to surface a list for display * or repair, not to fail noisily. */ checkIntegrity(): Promise<{ violations: RefViolation[]; }>; /** * Return this compartment's hash-chained audit log. * * The ledger is lazy-initialized on first access and cached for the * lifetime of the Vault instance. Every LedgerStore instance * shares the same adapter and DEK resolver, so `vault.ledger()` * can be called repeatedly without performance cost. * * The LedgerStore itself is the public API: consumers call * `.append()` (via Collection internals), `.head()`, `.verify()`, * and `.entries({ from, to })`. See the LedgerStore docstring for * the full surface and the concurrency caveats. */ ledger(): LedgerStore; /** * Internal accessor — returns the LedgerStore if the history * strategy is opted in, or `null` otherwise. Used by dump/restore/ * verifyBackupIntegrity and by Collection write paths that already * gate on `if (this.ledger)`. The public `ledger()` accessor above * throws on null; this one stays silent so the off-path no-ops. */ private getLedgerOrNull; /** @internal — add a subject→record ref to the encrypted subject index. */ _addSubjectRef(subjectId: string, ref: SubjectRef): Promise; /** @internal — drop a subject→record ref from the encrypted subject index. */ _removeSubjectRef(subjectId: string, ref: SubjectRef): Promise; /** * Rebuild the encrypted subject index from canonical records. The recovery * path for the documented read-modify-write race (RISK #3). Returns the * number of distinct subjects re-indexed. */ rebuildSubjectIndex(): Promise; /** * GDPR crypto-shred of a data subject. Consults the encrypted subject index and, per matching * record: rewrites the LIVE envelope to a tombstone (drops `_iv`/`_data`/`_cek`/`_det`), tombstones * every `_history` version, and fans out to derived residue via the graph (#622) — record-grain * artifacts (MV rows, derivation copies) ERASED, aggregate-grain rollups RECOMPUTED without the * forgotten contribution (skip+audit in a frozen period). The body/history become permanently * undecryptable while the collection DEK and every OTHER record stay intact. Then appends ONE * `op:'forget'` ledger entry whose `payloadHash` is `sha256Hex(subjectId)` — the chain still * `verify()`s, PROVING the subject existed and was erased without retaining any plaintext. * * Reports — but does not silently swallow — two completeness gaps: `unmigratedRecords` (a record * whose body was NOT yet migrated to a per-record CEK — still tombstoned, but pre-shred backups * stay decryptable; migrate, then re-forget) and `blobResidueCollections` (blob attachments, keyed * off a separate `_blob` DEK, out of scope here). * * @throws ForgetStrategyNotConfiguredError when no `withForgetCascade` was set. */ forget(subjectId: string): Promise; /** * Seal ONE record's content-encryption key (CEK) to an `at-*` host so that * host — and only that host — can decrypt exactly that record, with no * access to the vault DEK and no ability to read any other record. * * The grantor (this caller, who holds the collection DEK) reads the record's * live `_cek`, unwraps it under the collection DEK, exports the raw CEK * bytes, builds a {@link SealedCekBinding} `{collection, id, cek, expiresAt}`, * seals that binding for the recipient host via the host's published hint, * and persists a thin {@link SealedCekDeliveryEnvelope} at * `_sealed_cek///`. The binding (not the delivery * envelope) is the security boundary: the host re-verifies `{collection, id}` * and `expiresAt` from inside the sealed payload. * * Only works on a `perRecordKeys` record — a legacy record has no `_cek` to * seal (its body is under the shared collection DEK, which is never exposed * by sealing) → {@link RecordCekNotFoundError}. * * @param collection Collection holding the record. * @param id Record id. * @param hostSealer The recipient host's {@link RecipientSealer}. * @param opts.expiresAt REQUIRED authoritative expiry (ISO 8601), sealed into * the binding the host verifies. * @returns `{ pid, envelopeKey }` — the host provider id and the * `//` key the delivery envelope was written under. */ sealRecordToHost(collection: string, id: string, hostSealer: RecipientSealer, opts: { expiresAt: string; }): Promise<{ pid: string; envelopeKey: string; }>; /** * Revoke a sealed-CEK grant. **Default is SOFT**: deletes the `pid` delivery * envelope, but a host that already fetched it KEEPS decrypt capability (stops * new fetches, not a cryptographic cutoff). Pass **`{ hard: true }`** to rotate * the record CEK ({@link rotateRecordCek}) so any prior sealed CEK can no * longer open the record. See `revokeSealedRecord` in `record-keys/sealing.ts`. */ revokeSealedRecord(collection: string, id: string, pid: string, opts?: { hard?: boolean; }): Promise; /** * HARD-rotate a record's CEK: decrypt the live body under the old CEK, * re-encrypt it under a freshly-minted CEK, write the new live envelope, evict * the in-memory caches, and delete EVERY sealed-CEK delivery envelope for the * record. After this, any host holding a previously-sealed CEK can still * decrypt PRE-rotation history versions (they keep their old `_cek`) but NOT * the rotated live record (its body is under the new CEK → the old CEK fails * the AES-GCM auth tag → `TamperedError`). That asymmetry IS the revocation: * old grants lose the live record. * * Administrative path — bypasses `Collection.put` deliberately (no guards, no * history snapshot, no materialized-view refresh): rotation is a key-rotation * operation, not a business write, and must not version-bump history (which * would re-encrypt the prior version under the NEW CEK and defeat the point). * * @throws {@link RecordCekNotFoundError} if the record is missing or has no `_cek`. */ rotateRecordCek(collection: string, id: string): Promise; /** * Build the {@link SealingContext} the record-keys grantor functions need: * the vault-bound adapter, DEK resolver, actor, and the dual-cache eviction * `rotateRecordCek` performs (per-record CEK cache + decrypted-record cache). */ private _sealingContext; /** * @internal — called by `Noydb.openVault` after construction. * Dynamic-imports `GuardRegistry` + `ReadOnlyVaultFacade` and seeds * the registry with the supplied strategy handles. No-op when the * handles array is empty — keeps the guard service out of the * floor bundle for consumers that don't use guards. * * The read-only facade is eagerly instantiated here so the sync * accessor `_getReadOnlyFacade()` (called from the tx amendment * runner) stays synchronous. */ _initGuards(handles: ReadonlyArray): Promise; /** * @internal — The gate handler in Noydb.#registerGuardGate calls into * this. Returns `null` for vaults that never registered any guard * strategy. Callers MUST gate on null. */ _getGuardRegistry(): GuardRegistry | null; /** * @internal — called by `Noydb.openVault` after construction. * Dynamic-imports `DerivationRegistry` and registers the supplied * derivation strategies (async because `strategyHash` computation * goes through `crypto.subtle.digest`). No-op when the handles * array is empty — keeps the derivation service out of the floor * bundle for consumers that don't use derivations. Throws * `DerivationCycleError` if a cycle is detected after registration. */ _initDerivations(handles: ReadonlyArray): Promise; /** * @internal — consumed by `Collection.put` at write-time. Returns * `null` for vaults that never registered any derivation strategy. */ _getDerivationRegistry(): DerivationRegistry | null; /** * @internal — called by `Noydb.openVault` after collections are * wired. Dynamic-imports `MaterializedViewRegistry`, registers each * MV spec (which invokes its `query()` once for dependency * analysis), then runs the unified cycle detection across the MV + * derivation graphs. No-op when the handles array is empty — keeps * the MV service out of the floor bundle (mirrors the derivation lazy-import pattern). * Throws `MaterializedViewCycleError` if a cycle is detected. */ _initMaterializedViews(handles: ReadonlyArray): Promise; /** * @internal — consumed by `Collection.put` at write-time. Returns * `null` for vaults that never registered any MV strategy. */ _getMaterializedViewRegistry(): MaterializedViewRegistry | null; /** * @internal — called by `Noydb.openVault` after MVs are wired. * Dynamic-imports `OverlayedViewRegistry`, registers each spec, * validates against the MV registry for name/base/overlay collisions. * Throws on validation failure. */ _initOverlayedViews(handles: ReadonlyArray): Promise; /** * @internal — consumed by `Vault.collection()`. Returns `null` for * vaults with no overlays registered. */ _getOverlayedViewRegistry(): OverlayedViewRegistry | null; /** @internal — ctx for `putDerivedOutput`'s frozen-period skip+audit (#638 Task 5). */ private _dispatchCtx; /** * Manual re-materialize for a single registered MV (`refresh: 'manual'` consumers, * stale-bit recovery on vault reopen, bulk-recompute escape hatch after a strategy * change). Returns `{ written, deleted, failed, residueUndecodable, residueDeclined }` (`deleted` * always 0 without tombstoning; #785 splits the #782 leftovers — undecodable vs #718-declined). Throws if not registered. */ refreshView(name: string): Promise<{ written: number; deleted: number; failed: number; residueUndecodable: string[]; residueDeclined: string[]; }>; /** * Re-derive every record in the named source collection (useful after a strategy * change to bring previously-derived records up-to-date). Sequential in v1. */ deriveAll(sourceCollection: string): Promise<{ derived: number; failed: number; skippedFrozen: number; }>; /** * @internal — exposed for `runTransaction({ amendment: true })` so * the amendment invariant runner can pass the SAME read-only vault * facade that the gate handler in Noydb.#registerGuardGate uses. * Eagerly instantiated by `_initGuards()` so this accessor stays * synchronous; returns `null` for vaults that never registered any * guard (amendments require at least one guard, so the caller should * never see null). */ _getReadOnlyFacade(): ReadOnlyVaultFacade | null; /** * Internal lazy-allocator for the derivation read-only facade * (`layer:'derivation'`). Used as a defensive fallback; in practice * `_initDerivations()` eagerly instantiates this, so the lazy path is * a no-op. */ private _ensureReadOnlyFacade; /** * @internal — exposed for `runTransaction({ amendment: true })` * to append the structured `op: 'amendment'` audit entry without * dragging this private accessor onto the public surface or * forcing the tx executor to depend on the history-strategy * shape directly. Returns `null` when no history strategy is * configured, in which case the amendment commits silently * (the records still write through; only the multi-record * audit summary is skipped). */ _getLedgerOrNull(): LedgerStore | null; /** * Return a read-only view of this vault as it existed at * `timestamp`. Time-machine queries are reconstructed from the * per-version history snapshots persisted by every `put()`, then * cross-checked against the ledger for deletes that happened * between the snapshot and the target timestamp. * * ```ts * const q1End = vault.at('2026-03-31T23:59:59Z') * const invoice = await q1End.collection('invoices').get('inv-001') * // → the record as it stood at the close of Q1 2026 * ``` * * `timestamp` accepts an ISO-8601 string or a `Date`. Time-machine * views are read-only — writes throw {@link ReadOnlyAtInstantError}. * Accuracy bounded by history retention: if `historyConfig.maxVersions` * pruned earlier versions, queries before the oldest retained * snapshot return null even for records that existed. * *. */ at(timestamp: string | Date): VaultInstant; /** * Return a read-only "shadow" view of this vault. Every read method * on the returned {@link VaultFrame} delegates to the underlying * live collection; every write method throws * {@link ReadOnlyFrameError}. * * ```ts * const presentation = vault.frame() * const invoices = await presentation.collection('invoices').list() * ``` * * Use for screen-sharing a live vault, demo mode, or compliance * review where the reviewer should not be able to edit. Writes are * blocked at the JavaScript layer — the keyring DEKs are unchanged, * so this is **not** a cryptographic security boundary against a * hostile caller in the same process. See {@link VaultFrame} for * the full caveat. * *. */ frame(): VaultFrame; /** Credential-broker handle (#479); `NO_BROKER` throws unless `brokerStrategy: withBroker(config)` was passed to createNoydb(). */ broker(): CredentialBrokerHandle; /** * Run `fn` under a consent scope. Every `get` / `put` / `delete` * that happens inside `fn` writes one entry to `_consent_audit` * with the supplied `purpose` and `consentHash`. Outside a scope, * no entries are written — consent logging is opt-in by design. * * ```ts * await vault.withConsent( * { purpose: 'quarterly-review', consentHash: '7f3a...' }, * async () => { * const invoices = await vault.collection('invoices').list() * return invoices * }, * ) * ``` * * The scope is a single slot on this Vault instance — two * concurrent `withConsent` calls stomp each other. Use separate * Vault instances (or an external `AsyncLocalStorage` shim) for * per-flight scoping. * *. */ withConsent(ctx: ConsentContext, fn: () => Promise): Promise; /** * Query the consent-audit log. Returns every entry matching the * filter, newest-first isn't enforced — entries carry ULID ids so * sorting by id is insertion-order stable. Caller may sort further. * *. */ consentAudit(filter?: ConsentAuditFilter): Promise; /** * Called by Collection after every access when a consent scope is * active. Internal — not part of the public API. * * @internal */ _logConsent(op: ConsentOp, collection: string, recordId: string): Promise; /** * Subscribe to cross-tier access events. The callback fires every * time a record at a tier above the caller's inherent clearance is * read, written, elevated, or demoted successfully via this vault. * Returns an unsubscribe function. */ onCrossTierAccess(listener: (event: CrossTierAccessEvent) => void): () => void; private emitCrossTier; /** * issue a time-boxed cross-tier delegation. Writes an * encrypted envelope to the reserved `_delegations` collection that * the target user's runtime will pick up next time they open the * vault. * * Caller must hold the tier DEK for the requested tier and * collection. */ delegate(opts: IssueDelegationOptions): Promise; /** * revoke an issued delegation by id. Safe to call even * if the id does not exist. */ revokeDelegation(id: string): Promise; /** * Briefly elevate this vault to a higher tier and return a scoped * handle whose writes land at that tier. Reads on the original * vault continue at the caller's inherent tier; only the returned * handle is privileged. Auto-reverts when `release()` is called or * `ttlMs` elapses, whichever comes first. * * Capability semantics: * - The keyring must already carry a wrap for the target tier on * at least one collection (or be `owner` / `admin`, who can * auto-mint). Otherwise throws {@link TierNotGrantedError}. * - Per-collection capability gates (`canExportPlaintext`, * `canExportBundle`) are NOT bypassed — elevation is a tier * projection, not a privilege escalation path. * - Only one elevation can be active per vault at a time. * Calling `elevate(...)` while another is live throws * {@link AlreadyElevatedError}. * * Audit: * - One `_elevation_audit` envelope is written at start with * `{ id, actor, tier, reason, ttlMs, startedAt, expiresAt }`. * - Each write through the elevated handle additionally fires a * {@link CrossTierAccessEvent} with `authorization: 'elevation'`, * stamped with `reason` and `elevatedFrom`. */ elevate(tier: number, options: { ttlMs: number; reason: string; }): Promise; /** * Internal — invoked by an `ElevatedHandle.collection().put()` call. * Routes through the existing `Collection.putAtTier` code path with * the elevation context attached so the cross-tier event reflects * the right authorization class. Returns `putAtTier`'s `TierMoveResult` (#779). */ _elevatedPut(collectionName: string, id: string, record: T, tier: number, reason: string): Promise; private writeElevationAudit; /** * low-level escape hatch used by `@noy-db/on-magic-link` * to persist a magic-link-bound grant after the auth package has * derived the content key + KEK from `(serverSecret, token, vault)`. * * Callers outside of `@noy-db/on-magic-link` should use * `issueMagicLinkDelegation()` from that package instead — it handles * the HKDF derivation, record-id composition, and batch logic so the * grantor doesn't touch this method directly. */ writeMagicLinkGrant(contentKey: EnclaveKey, grantKek: EnclaveKey, recordId: string, opts: IssueMagicLinkGrantOptions): Promise; /** * Close an accounting period. After this call every record whose * envelope `_ts` is at or before `endDate` is write-locked: further * `put` or `delete` calls against such records throw * {@link PeriodClosedError}. New records (with fresh timestamps) * remain freely writable, and records last written AFTER `endDate` * are unaffected. * * Each closure writes a `PeriodRecord` to the reserved `_periods` * collection. The record carries the hash of the prior period's * record, so a tamper with any closure breaks the chain visible to * {@link listPeriods} + `vault.ledger().verify()`. * * Correctness is tied to the `_ts` field the hub assigns on every * write. Backdating records by editing the envelope directly is * outside the threat model — see SPEC § zero-knowledge envelopes. * *. */ closePeriod(options: ClosePeriodOptions): Promise; /** * Open a new period that carries forward from a prior closed one *. The `carryForward` callback receives a read-only * {@link VaultInstant} view anchored at the prior period's * `endDate` — use it to compute opening balances, closing-trial * snapshots, or any aggregate the new period should inherit. The * returned `{ [collection]: { [id]: record } }` map is written * before the new `PeriodRecord` lands, so the opening entries * materialise with fresh `_ts` values that fall outside every * closed period (the guard lets them through). * * The new period is stored with `kind: 'opened'` and hash-chained * to the same chain the close calls build — `listPeriods()` sees * both closed and opened entries in `closedAt` order. */ openPeriod>>(options: OpenPeriodOptions): Promise; /** Return every closed / opened period in `closedAt` order. */ listPeriods(): Promise; /** Look up a single period by name. Returns `null` if not found. */ getPeriod(name: string): Promise; /** * Freeze a closed period (#604): purges in-window delete markers and * records a `_period_freezes` companion, never mutating the chained * `_periods` record. Idempotent. Purges the LOCAL adapter only — a * synced target's markers survive there and re-import on pull (benign), * and purging re-opens the #589 resurrection window for a peer offline * since before the cutoff (see periods.ts's "Freeze" section). */ freezePeriod(name: string): Promise; /** * Archive a closed period (#613): relocates its in-window records from the * hot store to the configured cold tier (via the routeStore's cold route), * recording a `_period_archives` companion + ledger entry, never mutating * the chained `_periods` record. Non-destructive (reads fall through to * cold) and idempotent. Requires a routeStore with a cold route. */ archivePeriod(name: string): Promise; /** * Target-purge a closed+frozen period (#615): sweeps delete markers off the * vault's push-only sync targets (`backup`/`archive`), recording a * `_period_target_purges` companion + ledger entry, never mutating the chained * `_periods` record. `sync-peer` targets are skipped. Requires the period be * frozen first. Idempotent; a vault with no push-only targets writes no * companion and is re-runnable. */ purgePeriodTargets(name: string): Promise; /** @internal — called by the gate bus before put/delete. */ _assertTsWritable(existing: { ts: string | null; record: Record | null; } | null, incoming: Record | null): Promise; /** List all collection names in this vault. */ collections(): Promise; /** * Emit a structured introspection snapshot of this vault — vault name, * service opt-in matrix, collections + their fields, materialized * views, overlay views, derivations. With `withStats: true`, walks * every collection's envelopes to compute record counts, byte totals, * and oldest/newest timestamps. * * Consumed by the `noydb describe` CLI to produce human-readable * audit YAML/JSON from a `.noydb` bundle. * * Field provenance: * - `persisted`: read from `_schemas/` envelope (Route B opt-in) * - `live-validator`: derived in-process from a Zod schema attached * to the live `Collection` * - `sampled`: inferred from decrypted records (deferred to a follow-up) * - `unknown`: no schema info available * * @see docs/superpowers/specs/2026-05-22-schema-dump-design.md */ dumpSchema(opts?: DumpSchemaOptions): Promise; /** * Lightweight read of the vault's registered schema: collections * (+ doc counts), guards, materialized views, schema-update strategies, * and the unlocked user's grants. Cheap — one `adapter.list` per * collection, no decryption. For a full snapshot + stats use dumpSchema(). * Post-unlock by construction (a Vault only exists with an unlocked keyring). */ introspect(): Promise; /** * Internal accessor for {@link dumpVaultSchema}. Exposes the structural * state the walker needs (collection cache, registries, ref registry, * adapter) without widening the public Vault surface. * * @internal */ _introspectState(): VaultIntrospectState; /** * Return the stable opaque bundle handle for this vault, * generating and persisting a fresh ULID on first call. * * used by `writePod()` to identify the * vault in the unencrypted bundle header without * exposing the vault name. The handle is persisted in * the reserved `_meta` internal collection so subsequent * exports of the same vault produce the same handle — * bundle adapters (Drive, Dropbox, iCloud) will use it * as their primary key. * * **Storage path:** the handle is written via the adapter * directly with collection name `_meta` and id `handle`. The * envelope's `_data` field contains a plain JSON * `{ handle: '...' }` payload — the handle is opaque, doesn't * need encryption, and the bundle header exposes the same * value anyway. This mirrors the storage approach `_keyring` * uses for its plain-JSON wrapped-DEK envelopes (also bypasses * the AES-GCM layer; the `_iv` field is left empty). * * **Cross-process stability:** the handle survives process * restarts because it's persisted on the adapter, not just * cached in memory. A new Vault instance opened on the * same adapter sees the same `_meta/handle` envelope and * returns the same ULID. * * **Round-trip after restore:** the receiving vault of a * `load()` call generates its OWN handle on first export. The * dump body does not include `_meta`, because handle stability * is per-vault-instance, not per-vault-content. Two * separate restorations of the same backup produce two * distinct handles, which is the right behavior — they're * separate vault instances now. */ getBundleHandle(): Promise; /** * Read the owner-curated public envelope for this vault (or * `undefined` if none is persisted). The envelope lives in * `_meta/public-envelope` as plaintext — readable without any KEK * — so `getBundleHandle`-style callers can label a vault before * unlock. * * Mirrors `Noydb.getPublicEnvelope(vault, opts)` but scoped to a * single, already-opened `Vault` instance so the * bundle writer can snapshot it without holding a `Noydb` reference. * * @see https://github.com/vLannaAi/noy-db-docs/blob/main/content/docs/services/public-envelope.md */ getPublicEnvelope(opts?: { readonly locale?: string; }): Promise; /** * Dump vault as a verifiable encrypted JSON backup string. * * backups embed the current ledger head and the full * `_ledger` + `_ledger_deltas` internal collections so the * receiver can run `verifyBackupIntegrity()` after `load()` and * detect any tampering between dump and restore. Backups produced * without a ledger (older formats or hub instances built without * the history strategy) skip the integrity check with a warning — * both modes round-trip cleanly. */ dump(): Promise; /** * Build the `BackupContext` the extracted backup module (`with-pod/backup.ts`) * binds to: the read paths + the post-load mutation seams (`reloadKeyring`, * collection-cache clear, ledger-store reset) that `load()` performs on this * Vault's private state. */ private backupContext; /** * Restore a vault from a verifiable backup. * * After loading, runs `verifyBackupIntegrity()` to confirm: * 1. The hash chain is intact (no `prevHash` mismatches) * 2. The chain head matches the embedded `ledgerHead.hash` * from the backup * 3. Every data envelope's `payloadHash` matches the * corresponding ledger entry — i.e. nobody swapped * ciphertext between dump and restore * * On any failure, throws `BackupLedgerError` (chain or head * mismatch) or `BackupCorruptedError` (data envelope mismatch). * The vault state on the adapter has already been written * by the time we throw, so the caller is responsible for either * accepting the suspect state or wiping it and trying a different * backup. * * Legacy backups (no `ledgerHead` field, no `_internal`) load * with a console warning and skip the integrity check entirely * — there's no chain to verify against. */ load(backupJson: string): Promise; /** * End-to-end backup integrity check. Runs both: * * 1. `ledger.verify()` — walks the hash chain and confirms * every `prevHash` matches the recomputed hash of its * predecessor. * * 2. **Data envelope cross-check** — for every (collection, id) * that has a current value, find the most recent ledger * entry recording a `put` for that pair, recompute the * sha256 of the stored envelope's `_data`, and compare to * the entry's `payloadHash`. Any mismatch means an * out-of-band write modified the data without updating the * ledger. * * Returns a discriminated union so callers can handle the two * failure modes differently: * - `{ ok: true, head, length }` — chain verified and all * data matches; safe to use. * - `{ ok: false, kind: 'chain', divergedAt, message }` — the * chain itself is broken at the given index. * - `{ ok: false, kind: 'data', collection, id, message }` — * a specific data envelope doesn't match its ledger entry. * * This method is exposed so users can call it any time, not just * during `load()`. A scheduled background check is the simplest * way to detect tampering of an in-place vault. */ verifyBackupIntegrity(): Promise; /** * Stream every collection in this vault as decrypted, ACL-scoped * chunks. * * ⚠ **This method decrypts your records.** noy-db's threat model assumes * that records on disk are encrypted; the values yielded here are * plaintext. The consumer is responsible for ensuring the yielded data * is handled in a way that matches the data's sensitivity. If your goal * is encrypted backup or transport between noy-db instances, use * `dump()` instead — it produces a tamper-evident encrypted envelope and * never exposes plaintext. * * ## Behavior * * - **ACL-scoped.** Collections the calling principal cannot read are * silently skipped (same rule as `Collection.list()`). An operator * with `{ invoices: 'rw', clients: 'ro' }` permissions on a * five-collection vault exports only `invoices` and `clients`, * with no error on the others. * - **Streaming.** Returns an `AsyncIterableIterator` so consumers can * process chunks as they arrive without holding the full export in * memory. Note: the underlying adapter call (`loadAll`) is still a * single bulk read — the streaming benefit is on the *output* side. * True per-record adapter streaming arrives with the query DSL. * - **Schema + refs surfaced** as metadata on every chunk so downstream * serializers (`@noy-db/as-csv`, `@noy-db/as-xlsx`, custom * exporters) can produce schema-aware output without reaching into * collection internals. * - **Internal collections filtered.** `_ledger`, `_keyring`, etc. are * never yielded — they're noy-db's own bookkeeping and have no value * in a plaintext export. Use `dump()` for full backup including * internal collections. * * ## Composition * * Once cross-vault queries land, fanning this out across * every vault the caller can unlock is `queryAcross(ids, c => * c.exportStream())` — no new primitive needed. That's part of why this * method belongs in core: it's the single decrypt+ACL+metadata path * that every export-format package will build on, and pushing it into * a `@noy-db/as-*` package would force every format to re-solve * the same problems independently. * * @example * ```ts * for await (const chunk of company.exportStream()) { * // chunk.collection: 'invoices' * // chunk.schema: ZodObject | null * // chunk.refs: { clientId: { target: 'clients', mode: 'strict' } } * // chunk.records: Invoice[] * } * ``` * * @example * ```ts * // Per-record streaming for arbitrarily large collections. * for await (const chunk of company.exportStream({ granularity: 'record' })) { * // chunk.records is always length 1 * await writer.write(serialize(chunk.records[0])) * } * ``` */ exportStream(opts?: ExportStreamOptions): AsyncIterableIterator; /** * Convenience wrapper that consumes `exportStream()` and serializes the * result to a single JSON string. * * ⚠ **`exportJSON()` decrypts your records and produces plaintext.** * * noy-db's threat model assumes that records on disk are encrypted. * This function deliberately violates that assumption: it produces a * JSON string in plaintext, which the consumer is then responsible for * protecting (filesystem permissions, full-disk encryption, secure * transfer, secure deletion). * * Use this function only when: * - You are the authorized owner of the data, **and** * - You have a legitimate downstream tool that requires plaintext * JSON, **and** * - You have a documented plan for how the resulting plaintext will be * protected and eventually destroyed. * * If your goal is encrypted backup or transport between noy-db * instances, use `dump()` instead — it produces a tamper-evident * encrypted envelope, never plaintext. * * ## Why `Promise` instead of writing to a file path * * Core has zero `node:` imports — it runs unchanged in browsers, Node, * Bun, Deno, and edge runtimes. Accepting a file path would force a * `node:fs` import (breaks browsers) or a runtime dynamic import * (doesn't tree-shake, inflates bundles). Returning a string lets the * consumer choose any sink and forces the destination decision to be * explicit at the call site — which is also better for the security * warning. * * @example * ```ts * // Node: write to a file * import { writeFile } from 'node:fs/promises' * await writeFile('./backup.json', await company.exportJSON()) * ``` * * @example * ```ts * // Browser: download as a file * const json = await company.exportJSON() * const blob = new Blob([json], { type: 'application/json' }) * const url = URL.createObjectURL(blob) * // ... attach to an and click * ``` * * @example * ```ts * // Stream upload to a server * await fetch('/upload', { * method: 'POST', * body: await company.exportJSON(), * }) * ``` * * ## On-disk shape * * ```json * { * "_noydb_export": 1, * "_compartment": "acme", * "_exported_at": "2026-04-07T12:00:00.000Z", * "_exported_by": "alice@acme.example", * "collections": { * "invoices": { * "schema": null, * "refs": { "clientId": { "target": "clients", "mode": "strict" } }, * "records": [ ... ] * } * }, * "ledgerHead": { "hash": "...", "index": 42, "ts": "..." } * } * ``` * * `schema` is included for forward compatibility but is currently * always `null` because Standard Schema validators are not JSON- * serializable. Format-package serializers that need the schema * should use `exportStream()` directly and read `chunk.schema` (which * is the live validator object, not a serialization of it). */ exportJSON(opts?: ExportStreamOptions): Promise; }