/** * `.noydb` container format — byte layout, header schema, validators. * *. Wraps a `vault.dump()` JSON string in a thin * binary container with a magic-byte prefix, a minimum-disclosure * unencrypted header, and a compressed body. * * **Byte layout** (read in order from offset 0): * * ``` * +--------+--------+--------+--------+ * | N=78 | D=68 | B=66 | 1=49 | Magic 'NDB1' (4 bytes) * +--------+--------+--------+--------+ * | flags | compr | header_length (uint32 BE) | * +--------+--------+--------+--------+--------+--------+--------+ * | header_length bytes of UTF-8 JSON header ... * +--------+--------+ * | compressed body bytes ... * ``` * * Total fixed prefix before the header JSON is **10 bytes**: * - 4 bytes magic * - 1 byte flags * - 1 byte compression algorithm * - 4 bytes header length (uint32 big-endian) * * **Why a binary container** at all? `vault.dump()` already * produces a JSON string with encrypted records inside. Wrapping it * again seems redundant — but the wrap is what makes the file safe * to drop into cloud storage (Drive, Dropbox, iCloud) without * leaking the vault name and exporter identity through the * cloud's metadata API. The minimum-disclosure header is the only * thing visible without downloading and decompressing the body. * The dump JSON inside the body still contains the original * metadata, but that's only readable by someone who already has the * file bytes — the same person who could read the encrypted records * with the right passphrase. * * **Why minimum disclosure** in the header? Because consumers will * inevitably store these in services where the filename, file size, * and any unencrypted metadata are indexed for search. A field like * `vault: "Acme Corp"` would let an attacker (or a curious * cloud admin) enumerate which compartments exist and who exported * them, even with zero access to the encrypted body. The header * carries only what's needed to identify the file as a NOYDB * bundle and verify its integrity — nothing about the contents. */ import type { PublicEnvelope } from '../with-party/directory/public-envelope/types.js'; /** Magic bytes 'NDB1' (ASCII), identifying a NOYDB bundle. */ export declare const NOYDB_BUNDLE_MAGIC: Uint8Array; /** Total fixed prefix before the header JSON: 4+1+1+4 bytes. */ export declare const NOYDB_BUNDLE_PREFIX_BYTES = 10; /** Current bundle format version. Bumped on layout changes. */ export declare const NOYDB_BUNDLE_FORMAT_VERSION = 1; /** * Bitfield interpretation of the flags byte. * * Bit 0 — body is compressed (0 = raw, 1 = compressed) * Bit 1 — header carries an integrity hash over the body bytes * Bits 2-7 — reserved, must be 0 in */ export declare const FLAG_COMPRESSED = 1; export declare const FLAG_HAS_INTEGRITY_HASH = 2; /** * Compression algorithm encoding for the byte at offset 5. * * `none` is admitted for round-trip testing and for callers that * want to bundle without compression (e.g. when piping into a * separately compressed transport). `gzip` is the universally * available baseline (Node 18+, all modern browsers). `brotli` is * preferred when the runtime supports it — typically 30-50% smaller * for JSON payloads — but Node 22+ / Chrome 124+ / Firefox 122+ * are required, so the writer feature-detects at runtime and falls * back to gzip. The reader must handle all three. */ export declare const COMPRESSION_NONE = 0; export declare const COMPRESSION_GZIP = 1; export declare const COMPRESSION_BROTLI = 2; export type CompressionAlgo = 0 | 1 | 2; /** * The unencrypted header carried in every `.noydb` bundle. * * **Minimum-disclosure rules:** these are the ONLY allowed keys. * Any other key in a parsed header causes * `validateBundleHeader` to throw. The set is kept short to * minimize attack surface from cloud-storage metadata indexing — * see the file-level doc comment for the rationale. * * Forbidden in particular: * - `vault` / `_compartment` — would leak the tenant name * - `exporter` / `_exported_by` — would leak user identity * - `timestamp` / `_exported_at` — would leak activity timing * - `kdfParams` / salt fields — would leak crypto config that * could narrow brute-force search space * - any field starting with `_` (reserved by the dump format) */ export interface NoydbPodHeader { /** Bundle format version — bumped on layout changes. */ readonly formatVersion: number; /** * Opaque ULID identifier — generated once per vault and * stable across re-exports of the same vault. Does not * leak any information about contents (the timestamp prefix is * just monotonicity for sortability, not exporter activity — * see `bundle/ulid.ts` for the design notes). */ readonly handle: string; /** Compressed body length in bytes. Lets readers verify completeness without decompressing. */ readonly bodyBytes: number; /** SHA-256 of the compressed body bytes (lowercase hex). Lets readers verify integrity without decompressing. */ readonly bodySha256: string; /** * Owner-curated public envelope (`https://github.com/vLannaAi/noy-db-docs/blob/main/content/docs/services/public-envelope.md`). * Optional — present only when the source vault has a * `_meta/public-envelope` document AND the writer's hub is opted * into the feature. Treat as **untrusted hint**; the body's * encrypted contents remain the source of truth. * * The envelope deliberately widens the minimum-disclosure rule * for explicit, owner-curated label fields (name, icon, …). Every * other unknown header key still rejects at parse time. */ readonly publicEnvelope?: PublicEnvelope; /** * Auto-unlock material indicator. When present, the bundle * body wraps the dump JSON in a structure carrying per-user * passphrases — either plaintext (`'unsealed'`, public-by-design) * or sealed under a `SealingKeyProvider` (`'sealed'`, requires * matching provider on the recipient side). * * Visible pre-decompression so cloud listing UIs can warn before * download: "this bundle opens itself for anyone holding the file" * (unsealed) or "this bundle is sealed for a specific provider" * (sealed). * * Absent → the body is a raw `vault.dump()` JSON string (the * the legacy shape; back-compatible). */ readonly autoUnlock?: 'unsealed' | 'sealed'; /** * Bundle's role in the source → destination lifecycle. * - omitted / 'snapshot' (default): backup/copy of an existing vault. * - 'extracted-partition': re-keyed projection awaiting adoption. */ readonly bundleKind?: 'snapshot' | 'extracted-partition'; /** * Transfer-seal INDICATOR — metadata only, no payload (the * sealed DEKs live in the body). Present iff * bundleKind === 'extracted-partition'. */ readonly transferSeal?: { readonly v: 1; readonly alg: 'aes-256-gcm-pre-shared'; readonly sealId: string; }; } /** @deprecated Use `NoydbPodHeader`. */ export type NoydbBundleHeader = NoydbPodHeader; /** * Validate a parsed bundle header. Throws on any deviation from * the minimum-disclosure schema: * * - Missing required field * - Wrong type for any field * - Any extra key not in `ALLOWED_HEADER_KEYS` * - Unsupported `formatVersion` * - Negative or non-integer `bodyBytes` * - Malformed `handle` (must be 26-char Crockford base32) * - Malformed `bodySha256` (must be 64-char lowercase hex) * * The error messages name the offending field so consumers can * fix the producer rather than the reader. */ export declare function validateBundleHeader(parsed: unknown): asserts parsed is NoydbPodHeader; /** * Encode a header object to UTF-8 JSON bytes after validating * minimum disclosure. Used by the writer to serialize the header * region of the container. */ export declare function encodeBundleHeader(header: NoydbPodHeader): Uint8Array; /** * Parse a bundle header from its UTF-8 JSON bytes. Throws on * invalid JSON or any minimum-disclosure violation. */ export declare function decodeBundleHeader(bytes: Uint8Array): NoydbPodHeader; /** * Read a uint32 from `bytes` at `offset` in big-endian byte order. * No bounds check — callers must guarantee `offset + 4 <= bytes.length`. * Used to decode the header length field; kept inline so the parser * doesn't depend on DataView allocation per call. */ export declare function readUint32BE(bytes: Uint8Array, offset: number): number; /** * Write a uint32 to `bytes` at `offset` in big-endian byte order. * No bounds check — callers must guarantee `offset + 4 <= bytes.length`. */ export declare function writeUint32BE(bytes: Uint8Array, offset: number, value: number): void; /** * Verify the magic prefix of a bundle. Returns true if the first * 4 bytes match `NDB1`. Used by readers as a fast file-type check * before any further parsing. */ export declare function hasNoydbBundleMagic(bytes: Uint8Array): boolean;