/** * `.noydb` container primitives — write, read, header-only read. * *. Wraps a `vault.dump()` JSON string in the * binary container described in `format.ts`. * * **Three primitives:** * * - `writePod(vault, opts?)` — produces the * full container bytes ready to write to disk or upload * - `readPodHeader(bytes)` — parses just the header * without decompressing the body, fast file-type and * metadata read for cloud listing UIs * - `readPod(bytes)` — full read: validates magic, * header, integrity hash, and decompresses the body to * return the original `dump()` JSON string for use with * `vault.load()` * * **Compression strategy:** brotli when available (Node 22+, * Chrome 124+, Firefox 122+), gzip fallback elsewhere. The * algorithm choice is encoded in the format byte at offset 5, * so readers handle either transparently. Brotli wins ~30-50% * on JSON payloads with repeated keys (which vault dumps * are). * * **Why split read/load?** `readPod` returns the * *unwrapped JSON string*, not a Vault object. The caller * is responsible for piping that JSON into * `vault.load(json, passphrase)`. Splitting the layers * keeps the bundle module free of any crypto/passphrase * concerns — it's purely a format layer. The same `readPod` * call can also feed verification tools, format inspectors, or * archive utilities that don't care about decryption. */ import { type NoydbPodHeader } from './format.js'; import type { Vault } from '../kernel/vault.js'; import type { BundleRecipient } from '../with-party/team/keyring.js'; import type { PublicEnvelope } from '../with-party/directory/public-envelope/types.js'; import type { SealingKeyProvider, RecipientSealer, RecipientHint } from '../with-party/team/managed-passphrase.js'; /** * The credential kinds that can be bundled for auto-unlock. * WebAuthn is intentionally excluded — it is hardware-bound and * cannot be embedded as a portable credential. */ export type AutoCredentialKind = 'passphrase' | 'password' | 'pin'; /** * A typed credential for auto-unlock. Carries the credential `kind` * alongside the plaintext `value`, so consumers can dispatch the * correct login/prefill path rather than treating all credentials * as passphrases. * * `bundle.ts` is a pure format layer — it carries the credential * without interpreting it. The consumer is responsible for * dispatching on `kind`. */ export interface AutoCredential { readonly kind: AutoCredentialKind; readonly value: string; } /** * Options accepted by `writePod`. * * - `compression: 'auto'` (default) — try brotli, fall back to gzip * - `compression: 'brotli'` — force brotli, throw if unsupported * - `compression: 'gzip'` — force gzip * - `compression: 'none'` — no compression (round-trip testing only) * * **Slice filtering:** * - `collections` — allowlist of collection names to include. Internal * collections (keyrings, ledger) and excluded user collections are * dropped from the bundle. Records inside included collections are * carried through verbatim. * - `since` — only records whose envelope `_ts` is on/after the given * instant survive. Operates on the unencrypted envelope timestamp, * so plaintext access to records is not required. * * Both filters intersect (AND). When neither is provided the bundle is * a whole-vault snapshot, identical to today's behaviour. */ export interface WritePodOptions { readonly compression?: 'auto' | 'brotli' | 'gzip' | 'none'; /** Allowlist of user-collection names to include. */ readonly collections?: readonly string[]; /** * Drop records whose envelope `_ts` is strictly older than this * instant. Accepts a `Date` or any ISO-8601 string parseable by * `new Date()`. */ readonly since?: Date | string; /** * Plaintext-pipeline record predicate. Decrypts each record * with the vault's per-collection DEK, runs the predicate, and * keeps the original ciphertext for survivors (no re-encrypt — * preserves zero-knowledge cleanly). Records the predicate returns * `false` for are dropped from the bundle. * * Async predicates are supported. Mutating the record from inside * the predicate is undefined behaviour. */ readonly where?: (record: unknown, ctx: { collection: string; id: string; }) => boolean | Promise; /** * Hierarchical-tier ceiling. Records whose envelope `_tier` * is strictly greater than this number are dropped. Operates on the * envelope `_tier` (no decryption needed) — vault.exportStream is * referenced in the issue body for symmetry, but the tier value * lives on the unencrypted envelope. Vault without tiers is a no-op. */ readonly tierAtMost?: number; /** * Single-recipient re-keying shorthand. When set, the * bundle's keyring is replaced with one freshly-derived entry sealed * with this passphrase. The recipient inherits the source keyring's * userId, role, and permissions. Mutually exclusive with `recipients`. */ readonly exportPassphrase?: string; /** * Multi-recipient re-keying. Replaces the bundle's keyring * map with one slot per recipient, each sealed with its own * passphrase. DEKs are unwrapped from the source keyring once and * re-wrapped per recipient — record ciphertext is unchanged. * * Mutually exclusive with `exportPassphrase`. When neither is set, * the bundle inherits the source keyring as-is (today's behaviour, * suited to personal backup-and-restore). */ readonly recipients?: readonly BundleRecipient[]; /** * Auto-unlock — unsealed per-user credentials. * * Generalises `autoPassphrases` to support any bundleable credential * kind (`passphrase` | `password` | `pin`). * * Public-by-design: anyone holding the bundle bytes can read these * plaintext credentials. Use for demo data, sample vaults, * prospect onboarding. * * The `policy: 'public-by-design'` discriminant is mandatory. A * bare `{ perUser }` without it is rejected at write time — the * safety net against a careless call against a production vault. * * Mutually exclusive with `sealedCredentials`, `autoPassphrases`, * and `sealedPassphrases`. */ readonly autoCredentials?: { readonly policy: 'public-by-design'; readonly perUser: Record; }; /** * Auto-unlock — per-user credentials sealed under a * {@link SealingKeyProvider}. * * Generalises `sealedPassphrases` to support any bundleable * credential kind (`passphrase` | `password` | `pin`). * * The hub seals each user's plaintext credential under `provider` * and embeds the resulting sealed envelopes in the bundle. The * recipient must hold a provider with a matching `pid` (i.e., * `provider.id`) to auto-unseal on import. * * `mode: 'self-target'` — sender and recipient share the same * provider identity (same iCloud Keychain entry, same * MDM-provisioned bundle id, same KMS account, etc.). * * `mode: 'recipient-target'` — asymmetric sealing via a * {@link RecipientSealer}. Each user entry carries a * `credential` and a `hint` (the recipient's public material). * The bundle can only be unsealed by the holder of the matching * private key. * * Mutually exclusive with `autoCredentials`, `autoPassphrases`, * and `sealedPassphrases`. */ readonly sealedCredentials?: { readonly mode: 'self-target'; readonly provider: SealingKeyProvider; readonly perUser: Record; } | { readonly mode: 'recipient-target'; readonly provider: RecipientSealer; readonly perUser: Record; }; /** * @deprecated Use `autoCredentials` instead. * * Auto-unlock — unsealed per-user passphrases. * * Public-by-design: anyone holding the bundle bytes can read these * plaintext credentials. Use for demo data, sample vaults, * prospect onboarding. * * The `policy: 'public-by-design'` discriminant is mandatory. A * bare `{ perUser }` without it is rejected at write time — the * safety net against a careless call against a production vault. * * Mutually exclusive with `autoCredentials`, `sealedCredentials`, * and `sealedPassphrases`. */ readonly autoPassphrases?: { readonly policy: 'public-by-design'; readonly perUser: Record; }; /** * @deprecated Use `sealedCredentials` instead. * * Auto-unlock — per-user passphrases sealed under a * {@link SealingKeyProvider} (self-target only). * * The hub seals each user's plaintext passphrase under `provider` * and embeds the resulting sealed envelopes in the bundle. The * recipient must hold a provider with a matching `pid` (i.e., * `provider.id`) to auto-unseal on import. * * `mode: 'self-target'` is the only mode for `sealedPassphrases` — sender * and recipient share the same provider identity (same iCloud Keychain * entry, same MDM-provisioned bundle id, same KMS account, etc.). * For recipient-target sealing via the `RecipientSealer` interface, * use `sealedCredentials` with `mode: 'recipient-target'` (§11.4). * * Mutually exclusive with `autoCredentials`, `sealedCredentials`, * and `autoPassphrases`. */ readonly sealedPassphrases?: { readonly mode: 'self-target'; readonly provider: SealingKeyProvider; readonly perUser: Record; }; } /** @deprecated Use `WritePodOptions`. */ export type WriteNoydbBundleOptions = WritePodOptions; /** * Result returned by `readPod`. The caller is expected to * pass `dumpJson` into `vault.load(json, passphrase)` to * actually restore a vault. Splitting the layers keeps the * bundle module free of crypto concerns — see file-level docs. */ export interface NoydbBundleReadResult { readonly header: NoydbPodHeader; readonly dumpJson: string; /** * Auto-unlock material. Present only when * the header's `autoUnlock` flag is set AND the body's wrapped * structure survived parsing. Values are typed credentials — either * delivered plain (`kind: 'unsealed'`) or unsealed at read time * using one of the supplied `sealingProviders` (`kind: 'sealed'`). * * Consumers dispatch on `cred.kind` to choose the correct login / * prefill path. Pre-0.2 bundles (bare string entries) are coerced * to `{ kind: 'passphrase', value }` on read for back-compat. * * For `kind: 'sealed'` bundles read without `sealingProviders`, the * `value` field is the raw base64 sealed bytes — opaque to the * consumer until unsealed elsewhere. */ readonly autoUnlock?: { readonly kind: 'unsealed' | 'sealed'; readonly perUser: Record; }; } /** * Options accepted by {@link readPod} for the * auto-unlock paths. Without these the reader behaves exactly as before * (header parsed; body returned as `dumpJson`). */ export interface ReadNoydbBundleOptions { /** * Recipient-side sealing providers used to unseal entries from * `sealedPassphrases`. The reader picks the one whose `.id` * matches each entry's `pid`. Multiple providers may be supplied * (different users may seal under different identities). * * When unset and the bundle carries sealed envelopes, the * `autoUnlock.perUser` map remains the SEALED entries unmodified * — callers can inspect them or unseal elsewhere. */ readonly sealingProviders?: readonly SealingKeyProvider[]; /** * Opt-in trial mode for unsealing — when an entry's `pid` doesn't * match a registered provider, try each provider whose alg * matches. Default `false` (strict-pid dispatch per foundation * §11.9.2). Surfaces extra credential prompts; use deliberately. */ readonly attemptUnsealAcrossProviders?: boolean; } /** * Transfer-seal payload. The destination DEKs, exported to raw * bytes and AES-256-GCM-sealed *as a set* under the one-time transfer * key. `adoptPartition` unseals this; `createOwnerOnAdoptedPartition` * re-wraps the raw DEKs under the recipient's KEK. */ export interface TransferSealPayload { readonly v: 1; readonly alg: 'aes-256-gcm-pre-shared'; readonly sealId: string; /** base64(AES-256-GCM(transferKey, JSON of { collection: base64(rawDEK) })) — iv ‖ ct ‖ tag. */ readonly payload: string; } /** * Body wrapper for an extracted, transfer-sealed partition. * Sibling to {@link AutoUnlockBody}; selected by `header.bundleKind === * 'extracted-partition'`. The inner `dump` is a re-keyed projection with * an empty `keyrings` map. */ export interface ExtractedPartitionBody { readonly _noydb_bundle_body: 1; readonly dump: string; readonly _transferSeal: TransferSealPayload; } export declare function buildExtractedPartitionWrapper(dumpJson: string, seal: TransferSealPayload): ExtractedPartitionBody; export declare function parseExtractedPartitionBody(bodyString: string): { dump: string; seal: TransferSealPayload; }; /** Test-only: reset the brotli detection cache between tests. */ export declare function resetBrotliSupportCache(): void; /** * Write a `.noydb` bundle for the given vault. * * Pipeline: * 1. Resolve or create the compartment's stable bundle handle * via `vault.getBundleHandle()` — same handle on * every export from the same vault instance, so cloud * adapters can use it as a primary key. * 2. `vault.dump()` → JSON string with encrypted records * inside. * 3. UTF-8 encode the dump string. * 4. Compress (brotli if available, gzip fallback by default). * 5. Compute SHA-256 of the compressed body for integrity. * 6. Build the minimum-disclosure header from format version, * handle, body length, body sha. * 7. Serialize: magic (4) + flags (1) + algo (1) + headerLen (4) * + header JSON (N) + compressed body (M). * * The output is a single `Uint8Array`. Consumers writing to disk * pass it to `fs.writeFile`; consumers uploading to cloud storage * pass it as the request body. The `@noy-db/file` adapter wraps * this with a `saveBundle(path, vault)` helper. */ /** * Assemble the final `.noydb` container bytes from a body JSON string + * header extras. Shared by `writePod` and `extractPartition` * so both producers go through one compress/hash/prefix path. * * @internal */ export declare function assembleBundleContainer(opts: { handle: string; bodyJsonStr: string; compression: WritePodOptions['compression']; /** Header fields beyond the always-present four. */ headerExtras?: Partial>; }): Promise; export declare function writePod(vault: Vault, opts?: WritePodOptions): Promise; /** @deprecated Use `writePod`. */ export declare const writeNoydbBundle: typeof writePod; /** * Read just the bundle header — no body decompression, no * integrity verification. Intended for cloud-listing UIs that want * to show the handle and size before downloading the full body. * * Returns the same `NoydbPodHeader` shape as the writer, with * minimum-disclosure validation already applied. * * **Cost** — O(prefix + header bytes). The header is normally well * under 1 KB, but may grow to roughly 256 KB when a `publicEnvelope` * with an inline icon is present. Cloud-listing UIs that previously * assumed sub-KB header reads should account for this when sizing * range requests against bundles that may carry icons. */ export declare function readPodHeader(bytes: Uint8Array): NoydbPodHeader; /** @deprecated Use `readPodHeader`. */ export declare const readNoydbBundleHeader: typeof readPodHeader; /** * Read just the bundle's public envelope (`https://github.com/vLannaAi/noy-db-docs/blob/main/content/docs/services/public-envelope.md`) * — without verifying the body or even parsing the dump JSON. Pass * the raw bundle bytes; receive the owner-curated metadata or * `undefined` if the bundle was written without one. * * Locale-resolves any `name` / `description` map fields when `locale` * is supplied. Omitting `locale` returns the raw envelope. * * Same security caveat as the on-vault read path — the public * envelope is **untrusted hint** in v1; the encrypted body remains * the source of truth for vault contents. */ export declare function readNoydbBundlePublicEnvelope(bytes: Uint8Array, opts?: { readonly locale?: string; }): PublicEnvelope | undefined; /** * Read a full `.noydb` bundle: validate magic + header, verify * integrity hash over the body bytes, decompress, and return the * original `vault.dump()` JSON string ready to pass to * `vault.load()`. * * Throws `BundleIntegrityError` if the body's actual SHA-256 does * not match the value declared in the header. Distinct from a * format error so consumers can pattern-match in catch blocks * (corrupted-in-transit vs malformed-by-producer). * * Note: this function does NOT take a passphrase. The dump JSON * inside the body still contains encrypted records — restoring * the vault requires `vault.load(dumpJson, passphrase)` * after this call. Splitting the layers keeps the bundle module * free of crypto concerns and lets the same code feed format * inspectors that never decrypt anything. */ export declare function readPod(bytes: Uint8Array, opts?: ReadNoydbBundleOptions): Promise; /** @deprecated Use `readPod`. */ export declare const readNoydbBundle: typeof readPod;