@noy-db/as-zip 0.1.0-pre.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +170 -0
- package/dist/index.cjs +648 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +349 -0
- package/dist/index.d.ts +349 -0
- package/dist/index.js +603 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/sha1.ts","../src/aes.ts","../src/zip.ts","../src/read.ts"],"sourcesContent":["/**\n * **@noy-db/as-zip** — composite record + blob archive for noy-db.\n *\n * Bundles a collection's records AND every record's attached blobs\n * into a single zip archive. The canonical \"download this audit\n * trail\" / \"migrate this case folder\" primitive. Part of the\n * `@noy-db/as-*` portable-artefact family — plaintext tier,\n * document sub-family.\n *\n * ## Authorisation\n *\n * One capability check: `assertCanExport('plaintext', 'zip')`.\n * A composite archive is semantically the `'zip'` format from the\n * auth model's POV — requiring separate `'json'`, `'csv'`, `'blob'`\n * grants for a single call would fragment the grant surface without\n * adding any real isolation (the archive concatenates them anyway).\n *\n * The owner grants the composite capability explicitly:\n *\n * ```ts\n * await db.grant('firm', {\n * userId: 'auditor',\n * role: 'viewer',\n * passphrase: '…',\n * exportCapability: { plaintext: ['zip'] },\n * })\n * ```\n *\n * ## Archive layout\n *\n * ```\n * archive.zip\n * ├── manifest.json # index + provenance (record count, blob count, exported-at, exported-by)\n * ├── records.json # array of decrypted records\n * └── attachments/\n * ├── <recordId>/<slot> # raw blob bytes, MIME-native\n * └── ...\n * ```\n *\n * The folder-per-record layout makes composite entities (email +\n * body + attachments, invoice + scan + receipt) navigable in\n * Finder/Explorer without tooling.\n *\n * See [`docs/patterns/as-exports.md`](https://github.com/vLannaAi/noy-db/blob/main/docs/patterns/as-exports.md).\n *\n * @packageDocumentation\n */\n\nimport type { Vault } from '@noy-db/hub'\nimport { writeZip, type ZipEntry } from './zip.js'\n\n// Re-export the low-level encoder so consumers who want to build\n// custom archives (non-noy-db payloads) can reuse it directly.\nexport { writeZip, crc32, type ZipEntry, type WriteZipOptions } from './zip.js'\n\n// Re-export the reader + cipher errors. ZipReadError is thrown on\n// format violations; ZipCipherError on wrong-password or tampered\n// ciphertext (the two are surfaced as the same class so callers can\n// catch one and not the other).\nexport { readZip, type ReadZipEntry, type ReadZipOptions, ZipReadError } from './read.js'\nexport { ZipCipherError } from './aes.js'\n\n/** Record-selection options. */\nexport interface AsZipRecordsOptions {\n /** Collection to export. Must be in the caller's read ACL. */\n readonly collection: string\n /**\n * Optional predicate against each decrypted record. When omitted,\n * every record is included. Runs after decryption, before zip\n * assembly — doesn't reduce I/O, just the final set.\n */\n readonly filter?: (record: unknown) => boolean\n}\n\n/** Blob-selection options. */\nexport interface AsZipAttachmentsOptions {\n /**\n * Which slots to include per record. `'*'` (default) includes\n * every slot on every record; a string[] selects specific slot\n * names. Pass an empty array to skip blob inclusion entirely.\n */\n readonly slots?: readonly string[] | '*'\n}\n\n/** Top-level options for every entry point. */\nexport interface AsZipOptions {\n readonly records: AsZipRecordsOptions\n readonly attachments?: AsZipAttachmentsOptions\n /**\n * Optional WinZip-AES-256 passphrase. When set, every entry\n * inside the archive (records + attachments + manifest) is\n * encrypted with WinZip-AES-256 and the recipient must supply the\n * passphrase to extract.\n *\n * **Interop note:** the implementation is strictly to spec but has\n * not been validated against 7-Zip / Archive Utility / WinRAR in\n * this checkout. Round-trips against the package's own reader pass.\n * See https://github.com/vLannaAi/noy-db/issues/304 for the\n * cross-tool validation matrix.\n *\n * **Security framing:** this is the *interop* layer for\n * cross-platform handoff to archive tools, not the *encryption*\n * layer for sharing noy-db state. Use `as-noydb` for multi-recipient\n * + revocable + audited egress.\n */\n readonly password?: string\n}\n\n/** Options for `download()` — adds an optional filename. */\nexport interface AsZipDownloadOptions extends AsZipOptions {\n /** Filename offered to the browser. Default `'<collection>.zip'`. */\n readonly filename?: string\n}\n\n/** Options for `write()` — requires explicit risk acknowledgement. */\nexport interface AsZipWriteOptions extends AsZipOptions {\n /**\n * Required for Node file-write calls — consumer acknowledgement\n * that plaintext bytes will persist on disk past the current\n * process lifetime (Tier 3 risk).\n */\n readonly acknowledgeRisks: true\n}\n\n/** Manifest entry — one per record that landed in the archive. */\nexport interface ManifestRecord {\n readonly id: string\n readonly attachments: ReadonlyArray<{\n readonly slot: string\n readonly path: string\n readonly size: number\n readonly mimeType?: string\n }>\n}\n\n/** Archive-level manifest. Serialised as `manifest.json`. */\nexport interface ArchiveManifest {\n readonly _noydb_archive: 1\n readonly collection: string\n readonly exportedAt: string\n readonly recordCount: number\n readonly attachmentCount: number\n readonly records: readonly ManifestRecord[]\n}\n\n/**\n * Assemble the archive bytes. Pure beyond the auth check + store\n * reads. Records are written as `records.json`; blobs as\n * `attachments/<recordId>/<slot>`. A `manifest.json` index lands\n * at the archive root so extractors can walk the content without\n * re-reading every file.\n */\nexport async function toBytes(vault: Vault, options: AsZipOptions): Promise<Uint8Array> {\n vault.assertCanExport('plaintext', 'zip')\n\n const collectionName = options.records.collection\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const collection = vault.collection<any>(collectionName)\n\n // Pull every id the caller can see + the decrypted record for each.\n const ids = await collection.list().then((rs) => extractIds(rs))\n const records: Array<{ id: string; record: unknown }> = []\n for (const id of ids) {\n const r = await collection.get(id)\n if (r === null) continue\n if (options.records.filter && !options.records.filter(r)) continue\n records.push({ id, record: r })\n }\n\n // Gather attachments per record, filtered by slot selection.\n const slotsSelector = options.attachments?.slots ?? '*'\n const attachmentEntries: Array<{ path: string; bytes: Uint8Array; size: number; slot: string; mimeType?: string; recordId: string }> = []\n const includeAll = slotsSelector === '*'\n const includeSet = includeAll ? null : new Set<string>(slotsSelector)\n\n for (const { id } of records) {\n const blobSet = collection.blob(id)\n const slotsList = await blobSet.list()\n for (const slot of slotsList) {\n if (!includeAll && !includeSet!.has(slot.name)) continue\n const bytes = await blobSet.get(slot.name)\n if (!bytes) continue\n const safeId = sanitiseFsSegment(id)\n const safeSlot = sanitiseFsSegment(slot.name)\n const entry = {\n path: `attachments/${safeId}/${safeSlot}`,\n bytes,\n size: bytes.length,\n slot: slot.name,\n recordId: id,\n ...(slot.mimeType !== undefined && { mimeType: slot.mimeType }),\n }\n attachmentEntries.push(entry)\n }\n }\n\n // Manifest + records.json.\n const recordIndex: ManifestRecord[] = records.map(({ id }) => {\n const attach = attachmentEntries\n .filter((a) => a.recordId === id)\n .map(({ slot, path, size, mimeType }) => ({\n slot,\n path,\n size,\n ...(mimeType !== undefined && { mimeType }),\n }))\n return { id, attachments: attach }\n })\n const manifest: ArchiveManifest = {\n _noydb_archive: 1,\n collection: collectionName,\n exportedAt: new Date().toISOString(),\n recordCount: records.length,\n attachmentCount: attachmentEntries.length,\n records: recordIndex,\n }\n\n const encoder = new TextEncoder()\n const entries: ZipEntry[] = [\n { path: 'manifest.json', bytes: encoder.encode(JSON.stringify(manifest, null, 2)) },\n {\n path: 'records.json',\n bytes: encoder.encode(\n JSON.stringify(\n records.map((r) => {\n const pojo = toPojo(r.record)\n const base = pojo && typeof pojo === 'object' && !Array.isArray(pojo)\n ? (pojo as Record<string, unknown>)\n : { value: pojo }\n return { _id: r.id, ...base }\n }),\n null,\n 2,\n ),\n ),\n },\n ]\n for (const a of attachmentEntries) {\n entries.push({ path: a.path, bytes: a.bytes })\n }\n\n return writeZip(entries, options.password !== undefined ? { password: options.password } : {})\n}\n\n/**\n * Browser download — wraps `toBytes()` in a `Blob` and triggers the\n * browser's download prompt. Requires `URL.createObjectURL` +\n * `document.createElement`. Throws in headless Node.\n */\nexport async function download(vault: Vault, options: AsZipDownloadOptions): Promise<void> {\n const bytes = await toBytes(vault, options)\n const filename = options.filename ?? `${options.records.collection}.zip`\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const blob = new Blob([bytes as any], { type: 'application/zip' })\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = filename\n a.click()\n URL.revokeObjectURL(url)\n}\n\n/**\n * Node file-write — persists the archive to disk. Requires\n * explicit `acknowledgeRisks: true` (Tier 3 egress — the archive\n * contains plaintext records + blob bytes).\n */\nexport async function write(\n vault: Vault,\n path: string,\n options: AsZipWriteOptions,\n): Promise<void> {\n if (options.acknowledgeRisks !== true) {\n throw new Error(\n 'as-zip.write: acknowledgeRisks: true is required for on-disk plaintext output. ' +\n 'This call creates a persistent plaintext archive outside noy-db\\'s encrypted ' +\n 'storage — see docs/patterns/as-exports.md §\"The three tiers of \\\\\"plaintext out\\\\\"\"',\n )\n }\n const bytes = await toBytes(vault, options)\n const { writeFile } = await import('node:fs/promises')\n await writeFile(path, bytes)\n}\n\n// ── Import ─────────────────────────\n\nimport { diffVault, type VaultDiff } from '@noy-db/hub'\nimport { readZip } from './read.js'\n\nexport type ImportPolicy = 'merge' | 'replace' | 'insert-only'\n\nexport interface AsZipImportOptions {\n /** Target collection for the records. */\n readonly collection: string\n /** Field on each record that carries its id. Default `'id'`. */\n readonly idKey?: string\n /** Reconciliation policy. Default `'merge'`. */\n readonly policy?: ImportPolicy\n /** WinZip-AES-256 passphrase if the archive is encrypted. */\n readonly password?: string\n}\n\nexport interface AsZipImportPlan {\n readonly plan: VaultDiff\n readonly policy: ImportPolicy\n apply(): Promise<void>\n}\n\n/**\n * Read a `.zip` archive (optionally WinZip-AES-256 encrypted), parse\n * `records.json` from it, and return an `ImportPlan` whose `apply()`\n * writes the changes through the normal collection API.\n *\n * Pairs with `toBytes` for round-trip workflows. The records JSON\n * format matches what `toBytes` writes — array of records under\n * the configured collection's id key.\n */\nexport async function fromBytes(\n vault: Vault,\n bytes: Uint8Array,\n options: AsZipImportOptions,\n): Promise<AsZipImportPlan> {\n vault.assertCanImport('plaintext', 'zip')\n const policy: ImportPolicy = options.policy ?? 'merge'\n const idKey = options.idKey ?? 'id'\n\n const entries = await readZip(bytes, options.password !== undefined ? { password: options.password } : {})\n const recordsEntry = entries.find((e) => e.path === 'records.json')\n if (!recordsEntry) {\n throw new Error('as-zip.fromBytes: archive is missing records.json')\n }\n let parsed: unknown\n try {\n parsed = JSON.parse(new TextDecoder().decode(recordsEntry.bytes))\n } catch (err) {\n throw new Error(`as-zip.fromBytes: records.json is not valid JSON (${(err as Error).message})`)\n }\n if (!Array.isArray(parsed)) {\n throw new Error('as-zip.fromBytes: records.json must be a JSON array of records')\n }\n\n const plan = await diffVault(vault, { [options.collection]: parsed as Record<string, unknown>[] }, {\n collections: [options.collection],\n idKey,\n })\n\n return {\n plan,\n policy,\n async apply(): Promise<void> {\n // Routes through the txStrategy seam — throws clearly when\n // withTransactions() isn't opted in. Atomicity rolls back any\n // partial writes if a put fails mid-batch.\n await vault.noydb.transaction((tx) => {\n const txVault = tx.vault(vault.name)\n for (const entry of plan.added) {\n txVault.collection(entry.collection).put(entry.id, entry.record)\n }\n if (policy !== 'insert-only') {\n for (const entry of plan.modified) {\n txVault.collection(entry.collection).put(entry.id, entry.record)\n }\n }\n if (policy === 'replace') {\n for (const entry of plan.deleted) {\n txVault.collection(entry.collection).delete(entry.id)\n }\n }\n })\n },\n }\n}\n\n// ── internals ─────────────────────────────────────────────────────\n\n/**\n * Collection.list() returns an array of records OR record ids\n * depending on the call shape. as-zip always needs ids + records\n * separately; we call list() to materialise records then build the\n * id list from `_id`/record identity. Fall back to treating the\n * list as ids directly when records don't carry a canonical id.\n */\nfunction extractIds(list: unknown[]): string[] {\n const out: string[] = []\n for (const item of list) {\n if (typeof item === 'string') {\n out.push(item)\n continue\n }\n if (item && typeof item === 'object') {\n const rec = item as Record<string, unknown>\n // Canonical — a `.id` field. Falls back to `_id` for\n // consumers that store ids in a mongo-style field.\n const id = rec.id ?? rec._id\n if (typeof id === 'string') out.push(id)\n }\n }\n return out\n}\n\n/** Stringify-safe view of a record — drops functions, coerces Dates to ISO. */\nfunction toPojo(value: unknown): unknown {\n if (value === null || value === undefined) return value\n if (value instanceof Date) return value.toISOString()\n if (Array.isArray(value)) return value.map(toPojo)\n if (typeof value === 'object') {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(value)) out[k] = toPojo(v)\n return out\n }\n return value\n}\n\n/**\n * Filesystem-friendly path segment — replaces characters that\n * unzippers on Windows / Linux / macOS may reject or interpret\n * specially (path separators, control chars, reserved names).\n * Preserves the id's readability for sensible ULID / UUID / numeric\n * cases.\n */\nfunction sanitiseFsSegment(s: string): string {\n // eslint-disable-next-line no-control-regex\n return s.replace(/[/\\\\:*?\"<>|\\x00-\\x1f]/g, '_') || '_'\n}\n","/**\n * Thin Web-Crypto wrappers for SHA-1 / HMAC-SHA1.\n *\n * SHA-1 is unsafe for new collision-resistance applications, but\n * WinZip-AES specifies it as both the PBKDF2 hash and the HMAC\n * primitive — switching primitives would break interop with every\n * archive tool that consumes the format. This module exists so the\n * WinZip-AES code path doesn't grow `crypto.subtle` boilerplate at\n * every call site, and so any future primitive swap touches one\n * file.\n *\n * @module\n */\n\nexport async function sha1(bytes: Uint8Array): Promise<Uint8Array> {\n const digest = await crypto.subtle.digest('SHA-1', bytes as BufferSource)\n return new Uint8Array(digest)\n}\n\nexport async function hmacSha1(key: Uint8Array, bytes: Uint8Array): Promise<Uint8Array> {\n const cryptoKey = await crypto.subtle.importKey(\n 'raw',\n key as BufferSource,\n { name: 'HMAC', hash: 'SHA-1' },\n false,\n ['sign'],\n )\n const sig = await crypto.subtle.sign('HMAC', cryptoKey, bytes as BufferSource)\n return new Uint8Array(sig)\n}\n","/**\n * WinZip-AES encryption for ZIP entries — write + read.\n *\n * Implements the AES-256 variant of the WinZip-AES extension as\n * documented in\n * https://www.winzip.com/win/en/aes_info.html and PKWARE Appendix E.\n *\n * **AES-256 only by design.** ZipCrypto and AES-128/192 are refused at\n * both write and read time — the security story for the package is\n * \"if you need encryption, use AES-256.\" Adding weaker tiers would\n * give consumers the impression of meaningful choice.\n *\n * ## Layout of an encrypted entry's data region\n *\n * ```\n * ┌──────────┬──────────┬─────────────┬──────────┐\n * │ salt(16) │ verif(2) │ ciphertext │ auth(10) │\n * └──────────┴──────────┴─────────────┴──────────┘\n * ```\n *\n * - **salt** — 16 random bytes per entry\n * - **verifier** — 2 bytes, last 2 bytes of the PBKDF2 output;\n * used to fail-fast on a wrong password without running CTR\n * - **ciphertext** — input bytes XOR-ed with AES-CTR keystream\n * - **auth** — first 10 bytes of HMAC-SHA1(ciphertext, authKey),\n * where authKey is the second half of the PBKDF2 output\n *\n * ## Local file header changes\n *\n * For an encrypted entry:\n *\n * - `compression method` field is forced to **99** (AES marker)\n * - flag **bit 0** is set (encryption)\n * - extra field tag **0x9901** is appended with 7 bytes:\n * - `2 bytes` vendor version → `0x0002` (AE-2)\n * - `2 bytes` vendor ID → `'AE'` (`0x4541` LE)\n * - `1 byte` AES strength → `0x03` (AES-256)\n * - `2 bytes` real method → `0x0000` (STORE)\n * - For AE-2, `crc` is forced to 0 (the spec recommends this — the\n * authentication code already covers integrity)\n *\n * ## Counter convention (subtle!)\n *\n * AES-CTR per WinZip uses a **little-endian** 16-byte counter. The\n * counter starts at **1** (not 0) and increments by 1 per 16-byte\n * block. The lower 8 bytes hold the counter; the upper 8 bytes are 0.\n *\n * @module\n */\n\nimport { sha1, hmacSha1 } from './sha1.js'\n\n/** AES-256 key strength byte in the 0x9901 extra field. */\nexport const WZAES_STRENGTH_256 = 0x03\n/** Salt size in bytes for AES-256. */\nexport const WZAES_SALT_LEN = 16\n/** Verifier size in bytes (constant across strengths). */\nexport const WZAES_VERIFIER_LEN = 2\n/** Authentication code size in bytes (HMAC-SHA1 truncated). */\nexport const WZAES_AUTH_LEN = 10\n/** PBKDF2 iteration count per the spec. */\nexport const WZAES_PBKDF2_ITERATIONS = 1000\n/** Compression method marker used in the LFH for encrypted entries. */\nexport const WZAES_METHOD_MARKER = 99\n/** Vendor ID `'AE'` little-endian. */\nexport const WZAES_VENDOR_ID = 0x4541\n/** Vendor version 0x0002 = AE-2 (CRC field zeroed). */\nexport const WZAES_VENDOR_VERSION_AE2 = 0x0002\n/** Real compression method we use for encrypted entries: STORE. */\nexport const WZAES_REAL_METHOD = 0\n/** Extra-field header ID for WinZip-AES. */\nexport const WZAES_EXTRA_TAG = 0x9901\n\nexport class ZipCipherError extends Error {\n readonly code = 'ZIP_CIPHER_ERROR'\n constructor(message: string) {\n super(message)\n this.name = 'ZipCipherError'\n }\n}\n\nexport interface WzAesEncrypted {\n readonly extraField: Uint8Array // 11 bytes: tag(2) + size(2) + 7 vendor bytes\n readonly dataRegion: Uint8Array // salt + verifier + ciphertext + auth\n}\n\n/**\n * Encrypt a single entry's bytes per WinZip-AES-256.\n *\n * Returns the bytes that go in place of the entry's \"file data\"\n * region, plus the 11-byte extra field (tag+size+payload) the LFH\n * and central-directory header need.\n */\nexport async function encryptEntryWzAes(\n plaintext: Uint8Array,\n password: string,\n): Promise<WzAesEncrypted> {\n if (password.length === 0) {\n throw new ZipCipherError('encryptEntryWzAes: password must be a non-empty string')\n }\n\n const salt = crypto.getRandomValues(new Uint8Array(WZAES_SALT_LEN))\n const { encryptKey, authKey, verifier } = await deriveKeys(password, salt)\n\n const ciphertext = await aesCtrXor(plaintext, encryptKey)\n const authCode = await hmacSha1(authKey, ciphertext)\n const auth = authCode.slice(0, WZAES_AUTH_LEN)\n\n const data = new Uint8Array(salt.length + verifier.length + ciphertext.length + auth.length)\n let pos = 0\n data.set(salt, pos); pos += salt.length\n data.set(verifier, pos); pos += verifier.length\n data.set(ciphertext, pos); pos += ciphertext.length\n data.set(auth, pos)\n\n return {\n extraField: buildExtraField(),\n dataRegion: data,\n }\n}\n\n/**\n * Decrypt a single entry's data region produced by `encryptEntryWzAes`.\n *\n * Refuses to decrypt unless the verifier matches (wrong password\n * fast-path) AND the HMAC matches (tamper detection). Both produce a\n * `ZipCipherError`; callers cannot distinguish wrong-password from\n * tampered, by design.\n */\nexport async function decryptEntryWzAes(\n data: Uint8Array,\n password: string,\n): Promise<Uint8Array> {\n if (data.length < WZAES_SALT_LEN + WZAES_VERIFIER_LEN + WZAES_AUTH_LEN) {\n throw new ZipCipherError('decryptEntryWzAes: data region is shorter than the WinZip-AES envelope')\n }\n const salt = data.slice(0, WZAES_SALT_LEN)\n const verifier = data.slice(WZAES_SALT_LEN, WZAES_SALT_LEN + WZAES_VERIFIER_LEN)\n const ciphertext = data.slice(\n WZAES_SALT_LEN + WZAES_VERIFIER_LEN,\n data.length - WZAES_AUTH_LEN,\n )\n const auth = data.slice(data.length - WZAES_AUTH_LEN)\n\n const { encryptKey, authKey, verifier: expectedVerifier } = await deriveKeys(password, salt)\n\n if (!constantTimeEqual(verifier, expectedVerifier)) {\n throw new ZipCipherError('decryptEntryWzAes: wrong password (verifier mismatch)')\n }\n\n const expectedAuth = (await hmacSha1(authKey, ciphertext)).slice(0, WZAES_AUTH_LEN)\n if (!constantTimeEqual(auth, expectedAuth)) {\n throw new ZipCipherError('decryptEntryWzAes: authentication code mismatch (tampered ciphertext or wrong password)')\n }\n\n return aesCtrXor(ciphertext, encryptKey)\n}\n\n// ─── PBKDF2 + key split ────────────────────────────────────────────────\n\n/**\n * PBKDF2-SHA1 with 1000 iterations, output length 66 bytes for AES-256:\n *\n * bytes 0..31 → AES-256 encryption key\n * bytes 32..63 → HMAC-SHA1 key\n * bytes 64..65 → 2-byte verifier\n */\nasync function deriveKeys(password: string, salt: Uint8Array): Promise<{\n encryptKey: CryptoKey\n authKey: Uint8Array\n verifier: Uint8Array\n}> {\n const passBytes = new TextEncoder().encode(password)\n\n const baseKey = await crypto.subtle.importKey(\n 'raw',\n passBytes as BufferSource,\n { name: 'PBKDF2' },\n false,\n ['deriveBits'],\n )\n const totalBits = (32 + 32 + 2) * 8\n const bits = await crypto.subtle.deriveBits(\n {\n name: 'PBKDF2',\n hash: 'SHA-1',\n salt: salt as BufferSource,\n iterations: WZAES_PBKDF2_ITERATIONS,\n },\n baseKey,\n totalBits,\n )\n const out = new Uint8Array(bits)\n\n const encryptRaw = out.slice(0, 32)\n const authKey = out.slice(32, 64)\n const verifier = out.slice(64, 66)\n\n const encryptKey = await crypto.subtle.importKey(\n 'raw',\n encryptRaw as BufferSource,\n { name: 'AES-CTR' },\n false,\n ['encrypt', 'decrypt'],\n )\n\n return { encryptKey, authKey, verifier }\n}\n\n// ─── AES-CTR with WinZip's little-endian, 1-based counter ─────────────\n\n/**\n * Apply AES-CTR keystream to `data`. WinZip-AES uses a 16-byte block\n * counter, **little-endian**, starting at **1** and incrementing by 1\n * per block.\n *\n * Web Crypto's AES-CTR uses a 128-bit big-endian counter, so we can't\n * call subtle.encrypt() directly with a single counter and let it run.\n * Instead we generate the keystream block-by-block by encrypting the\n * raw counter bytes (with AES-ECB-equivalent: AES-CTR with a single\n * block of plaintext), then XOR.\n */\nasync function aesCtrXor(data: Uint8Array, key: CryptoKey): Promise<Uint8Array> {\n const out = new Uint8Array(data.length)\n let blockNum = 1n\n let pos = 0\n // 16 zero bytes — encrypted in CTR mode with our chosen counter\n // block, the result is exactly AES_K(counter). XOR-ing that with\n // the data block gives WinZip's CTR-mode output for that 16 bytes.\n const zeroBlock = new Uint8Array(16) as BufferSource\n while (pos < data.length) {\n const counterBlock = new Uint8Array(16)\n // Lower 8 bytes = counter LE. Upper 8 bytes = 0.\n let n = blockNum\n for (let i = 0; i < 8; i++) {\n counterBlock[i] = Number(n & 0xffn)\n n >>= 8n\n }\n // AES-CTR with counter = our counterBlock, length = 128, plaintext\n // = 16 zeros, returns AES_K(counterBlock) directly. The CTR\n // increment never fires because we only encrypt one block.\n const block = await crypto.subtle.encrypt(\n { name: 'AES-CTR', counter: counterBlock as BufferSource, length: 128 },\n key,\n zeroBlock,\n )\n const keystream = new Uint8Array(block)\n const remain = Math.min(16, data.length - pos)\n for (let i = 0; i < remain; i++) {\n out[pos + i] = data[pos + i]! ^ keystream[i]!\n }\n pos += remain\n blockNum += 1n\n }\n return out\n}\n\n// ─── Extra field assembly ─────────────────────────────────────────────\n\nfunction buildExtraField(): Uint8Array {\n // Header ID(2) + size(2) + payload(7) = 11 bytes total\n const ef = new Uint8Array(11)\n const view = new DataView(ef.buffer)\n view.setUint16(0, WZAES_EXTRA_TAG, true) // tag\n view.setUint16(2, 7, true) // payload size\n view.setUint16(4, WZAES_VENDOR_VERSION_AE2, true) // vendor version (AE-2)\n view.setUint16(6, WZAES_VENDOR_ID, true) // vendor ID 'AE'\n ef[8] = WZAES_STRENGTH_256 // strength: AES-256\n view.setUint16(9, WZAES_REAL_METHOD, true) // real method (STORE)\n return ef\n}\n\n/**\n * Parse the WinZip-AES extra field of an entry. Returns null when\n * none is present, throws ZipCipherError when one is present but\n * declares a strength other than AES-256.\n */\nexport function parseWzAesExtraField(extra: Uint8Array): { vendorVersion: number } | null {\n let pos = 0\n while (pos + 4 <= extra.length) {\n const tag = readU16(extra, pos)\n const size = readU16(extra, pos + 2)\n if (tag === WZAES_EXTRA_TAG) {\n if (size !== 7) {\n throw new ZipCipherError(\n `WinZip-AES extra field has size ${size}, expected 7`,\n )\n }\n const vendorVersion = readU16(extra, pos + 4)\n const vendorId = readU16(extra, pos + 6)\n const strength = extra[pos + 8]!\n if (vendorId !== WZAES_VENDOR_ID) {\n throw new ZipCipherError(\n `WinZip-AES extra field vendor id 0x${vendorId.toString(16)}, expected 0x${WZAES_VENDOR_ID.toString(16)}`,\n )\n }\n if (strength !== WZAES_STRENGTH_256) {\n throw new ZipCipherError(\n `WinZip-AES strength ${strength} not supported — AES-256 only`,\n )\n }\n return { vendorVersion }\n }\n pos += 4 + size\n }\n return null\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────\n\nfunction constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false\n let diff = 0\n for (let i = 0; i < a.length; i++) diff |= (a[i]! ^ b[i]!)\n return diff === 0\n}\n\nfunction readU16(bytes: Uint8Array, offset: number): number {\n return bytes[offset]! | (bytes[offset + 1]! << 8)\n}\n\n// Re-export hashing helpers so the consumer barrel can pull them too.\nexport { sha1, hmacSha1 }\n","/**\n * Minimal zero-dependency ZIP writer — STORE method only (no compression).\n *\n * Produces single-disk zip archives readable by `unzip`, macOS Finder,\n * Windows Explorer, and every well-behaved unzipper. Store-method is\n * sufficient here because:\n *\n * 1. Most blobs reaching this writer are already compressed — PDFs,\n * PNGs, JPEGs, `.zip`-inside-zip, encrypted `.noydb` bundles.\n * Re-deflating yields near-zero savings at non-trivial CPU cost.\n * 2. STORE keeps the encoder compact (~150 lines) and dependency-free.\n * Deflate would need a ~5KB gzip-style implementation or an\n * external dep; we're zero-deps by design.\n * 3. Consumers who need further compression can deflate the whole zip\n * after it's produced (`gzip archive.zip`).\n *\n * ## Wire format used\n *\n * Per PKWARE APPNOTE (the canonical ZIP spec):\n *\n * ```\n * [Local File Header 1][File 1 data]\n * [Local File Header 2][File 2 data]\n * ...\n * [Central Directory File Header 1]\n * [Central Directory File Header 2]\n * ...\n * [End of Central Directory Record]\n * ```\n *\n * All multi-byte integers are little-endian. Filenames are UTF-8\n * (flag bit 11 set so unzippers interpret them correctly). The\n * per-file CRC-32 uses the standard polynomial 0xEDB88320.\n *\n * ## What this does NOT support\n *\n * - Deflate / bzip2 / lzma / zstd compression methods.\n * - Zip64 (files / archives > 4 GiB). 32-bit size fields apply.\n * - Multi-disk / spanned archives.\n * - Encryption (the ZIP-level kind — noy-db records are already\n * encrypted at the envelope layer before reaching this writer).\n * - Symlinks, extended attributes, NTFS/UNIX permissions.\n *\n * @module\n */\n\nimport {\n encryptEntryWzAes,\n WZAES_METHOD_MARKER,\n WZAES_REAL_METHOD,\n} from './aes.js'\n\nconst TEXT_ENCODER = new TextEncoder()\n\n/** Input entry to `writeZip`. */\nexport interface ZipEntry {\n /** Slash-delimited path inside the archive (e.g. `\"records.json\"`, `\"attachments/01H.../raw.pdf\"`). */\n readonly path: string\n /** Raw bytes. Pass plaintext as-is; the writer doesn't interpret content. */\n readonly bytes: Uint8Array\n /** Optional file mtime. Defaults to the call-time `Date`. */\n readonly mtime?: Date\n}\n\n/** Optional archive-wide settings for `writeZip`. */\nexport interface WriteZipOptions {\n /**\n * When set, every entry is encrypted with WinZip-AES-256 sealed\n * under this passphrase. Recipients open with stock archive\n * tooling (7-Zip, Archive Utility, WinRAR, modern unzip builds)\n * by typing the same passphrase.\n *\n * Single passphrase across all entries by design — per-entry\n * passwords don't fit a noy-db export workflow and would muddy\n * the API.\n */\n readonly password?: string\n}\n\n/**\n * Build a complete ZIP archive byte stream from `entries`. The result\n * is the full archive including central directory and EOCD — ready\n * to `fs.writeFile` or hand to `new Blob([bytes])`.\n *\n * When `options.password` is set, every entry is encrypted with\n * WinZip-AES-256 (vendor version AE-2). Output remains a valid\n * single-disk ZIP that archive tools recognising WinZip-AES prompt\n * for the password on extract.\n */\nexport async function writeZip(\n entries: readonly ZipEntry[],\n options: WriteZipOptions = {},\n): Promise<Uint8Array> {\n // Pre-compute per-entry binary so we know total size + central\n // directory offsets before we build the wrapper header.\n const localParts: Uint8Array[] = []\n const cdParts: Uint8Array[] = []\n let offset = 0\n\n for (const entry of entries) {\n const nameBytes = TEXT_ENCODER.encode(entry.path)\n const dosTime = toDosTime(entry.mtime ?? new Date())\n\n let dataBytes: Uint8Array\n let extraField: Uint8Array\n let methodField: number\n let crcField: number\n let flagsField = 0x0800 // UTF-8 names (bit 11)\n\n if (options.password !== undefined) {\n const enc = await encryptEntryWzAes(entry.bytes, options.password)\n dataBytes = enc.dataRegion\n extraField = enc.extraField\n methodField = WZAES_METHOD_MARKER // 99 — AES marker\n // AE-2 zeroes the CRC (auth code already covers integrity).\n crcField = 0\n // Bit 0 set to indicate encryption.\n flagsField |= 0x0001\n void WZAES_REAL_METHOD // imported for parity\n } else {\n dataBytes = entry.bytes\n extraField = new Uint8Array(0)\n methodField = 0 // STORE\n crcField = crc32(entry.bytes)\n }\n\n const size = dataBytes.length\n const uncompressedSize = options.password !== undefined\n ? entry.bytes.length\n : size\n\n // Local file header: 30 bytes fixed + filename + extra + data.\n const lfh = new Uint8Array(30 + nameBytes.length + extraField.length)\n const lfhView = new DataView(lfh.buffer)\n lfhView.setUint32(0, 0x04034b50, true) // Signature PK\\3\\4\n lfhView.setUint16(4, 20, true) // Version needed: 2.0\n lfhView.setUint16(6, flagsField, true) // Flags\n lfhView.setUint16(8, methodField, true) // Method\n lfhView.setUint16(10, dosTime.time, true) // Mod time\n lfhView.setUint16(12, dosTime.date, true) // Mod date\n lfhView.setUint32(14, crcField, true) // CRC-32 (0 for AE-2)\n lfhView.setUint32(18, size, true) // Compressed size (encrypted region for AES)\n lfhView.setUint32(22, uncompressedSize, true)// Uncompressed size (plaintext length)\n lfhView.setUint16(26, nameBytes.length, true)\n lfhView.setUint16(28, extraField.length, true)\n lfh.set(nameBytes, 30)\n if (extraField.length > 0) lfh.set(extraField, 30 + nameBytes.length)\n\n localParts.push(lfh, dataBytes)\n\n // Central directory header: 46 bytes fixed + filename + extra.\n const cdh = new Uint8Array(46 + nameBytes.length + extraField.length)\n const cdhView = new DataView(cdh.buffer)\n cdhView.setUint32(0, 0x02014b50, true) // Signature\n cdhView.setUint16(4, 20, true) // Version made by\n cdhView.setUint16(6, 20, true) // Version needed\n cdhView.setUint16(8, flagsField, true) // Flags\n cdhView.setUint16(10, methodField, true) // Method\n cdhView.setUint16(12, dosTime.time, true)\n cdhView.setUint16(14, dosTime.date, true)\n cdhView.setUint32(16, crcField, true)\n cdhView.setUint32(20, size, true)\n cdhView.setUint32(24, uncompressedSize, true)\n cdhView.setUint16(28, nameBytes.length, true)\n cdhView.setUint16(30, extraField.length, true)\n cdhView.setUint16(32, 0, true) // Comment length\n cdhView.setUint16(34, 0, true) // Disk number\n cdhView.setUint16(36, 0, true) // Internal attrs\n cdhView.setUint32(38, 0, true) // External attrs\n cdhView.setUint32(42, offset, true) // Local header offset\n cdh.set(nameBytes, 46)\n if (extraField.length > 0) cdh.set(extraField, 46 + nameBytes.length)\n cdParts.push(cdh)\n\n offset += lfh.length + dataBytes.length\n }\n\n const localTotal = offset\n const cdSize = cdParts.reduce((n, p) => n + p.length, 0)\n\n // End-of-Central-Directory record (EOCD) — 22 bytes fixed.\n const eocd = new Uint8Array(22)\n const eocdView = new DataView(eocd.buffer)\n eocdView.setUint32(0, 0x06054b50, true) // Signature\n eocdView.setUint16(4, 0, true) // Disk number\n eocdView.setUint16(6, 0, true) // CD disk\n eocdView.setUint16(8, entries.length, true) // Records this disk\n eocdView.setUint16(10, entries.length, true) // Records total\n eocdView.setUint32(12, cdSize, true) // CD size\n eocdView.setUint32(16, localTotal, true) // CD offset\n eocdView.setUint16(20, 0, true) // Comment length\n\n // Concat everything.\n const out = new Uint8Array(localTotal + cdSize + eocd.length)\n let pos = 0\n for (const part of localParts) {\n out.set(part, pos)\n pos += part.length\n }\n for (const part of cdParts) {\n out.set(part, pos)\n pos += part.length\n }\n out.set(eocd, pos)\n return out\n}\n\n// ── CRC-32 ───────────────────────────────────────────────────────────\n\n/**\n * CRC-32 using polynomial 0xEDB88320 (standard ZIP/PNG/ethernet).\n * Table-driven for speed; the table is lazy-built on first call.\n */\nlet CRC_TABLE: Uint32Array | null = null\n\nfunction ensureCrcTable(): Uint32Array {\n if (CRC_TABLE) return CRC_TABLE\n const t = new Uint32Array(256)\n for (let n = 0; n < 256; n++) {\n let c = n\n for (let k = 0; k < 8; k++) {\n c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1\n }\n t[n] = c >>> 0\n }\n CRC_TABLE = t\n return t\n}\n\nexport function crc32(bytes: Uint8Array): number {\n const t = ensureCrcTable()\n let c = 0xffffffff\n for (let i = 0; i < bytes.length; i++) {\n c = t[(c ^ bytes[i]!) & 0xff]! ^ (c >>> 8)\n }\n return (c ^ 0xffffffff) >>> 0\n}\n\n// ── DOS time encoding ────────────────────────────────────────────────\n\n/**\n * Encode a JS `Date` to ZIP's MS-DOS time + date fields.\n * Year floor is 1980 (DOS epoch); seconds have 2-second resolution.\n */\nfunction toDosTime(date: Date): { time: number; date: number } {\n const y = Math.max(1980, date.getUTCFullYear()) - 1980\n const m = date.getUTCMonth() + 1\n const d = date.getUTCDate()\n const hh = date.getUTCHours()\n const mm = date.getUTCMinutes()\n const ss = Math.floor(date.getUTCSeconds() / 2)\n return {\n date: (y << 9) | (m << 5) | d,\n time: (hh << 11) | (mm << 5) | ss,\n }\n}\n","/**\n * Minimal zero-dependency ZIP reader — STORE method only, with\n * optional WinZip-AES-256 decryption.\n *\n * Mirror of `./zip.ts`'s writer. Reads the central directory, walks\n * each entry, decrypts when the AES marker (method 99) is set and a\n * password was supplied. Same scope limits as the writer:\n *\n * - **No DEFLATE / bzip2 / lzma / zstd.** STORE-only. Trying to read\n * a deflated entry throws `ZipReadError`.\n * - **No Zip64.** 32-bit size + offset fields apply.\n * - **AES-256 only.** AES-128/192 and ZipCrypto are refused — same\n * stance as the writer.\n *\n * @module\n */\n\nimport { decryptEntryWzAes, parseWzAesExtraField, ZipCipherError, WZAES_METHOD_MARKER } from './aes.js'\n\nconst TEXT_DECODER = new TextDecoder()\n\nexport class ZipReadError extends Error {\n readonly code = 'ZIP_READ_ERROR'\n constructor(message: string) {\n super(message)\n this.name = 'ZipReadError'\n }\n}\n\nexport interface ReadZipEntry {\n readonly path: string\n readonly bytes: Uint8Array\n /** True iff the entry was decrypted from a WinZip-AES region. */\n readonly encrypted: boolean\n}\n\nexport interface ReadZipOptions {\n /** WinZip-AES-256 passphrase. Required for encrypted archives. */\n readonly password?: string\n}\n\n/**\n * Parse a ZIP archive and return its entries. When the archive\n * contains AES-encrypted entries (method 99 with the 0x9901 extra\n * field), `options.password` MUST be supplied. Wrong password\n * surfaces a `ZipCipherError` from the underlying decrypt step.\n */\nexport async function readZip(\n bytes: Uint8Array,\n options: ReadZipOptions = {},\n): Promise<ReadZipEntry[]> {\n if (bytes.length < 22) {\n throw new ZipReadError('readZip: archive shorter than the EOCD record (22 bytes)')\n }\n\n const eocdOffset = locateEOCD(bytes)\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n const cdSize = view.getUint32(eocdOffset + 12, true)\n const cdOffset = view.getUint32(eocdOffset + 16, true)\n const recordCount = view.getUint16(eocdOffset + 10, true)\n\n if (cdOffset + cdSize > bytes.length) {\n throw new ZipReadError('readZip: central directory extends past end of file')\n }\n\n const out: ReadZipEntry[] = []\n let pos = cdOffset\n for (let i = 0; i < recordCount; i++) {\n if (view.getUint32(pos, true) !== 0x02014b50) {\n throw new ZipReadError(`readZip: missing central directory file header signature at offset ${pos}`)\n }\n const flags = view.getUint16(pos + 8, true)\n const method = view.getUint16(pos + 10, true)\n const compressedSize = view.getUint32(pos + 20, true)\n const uncompressedSize = view.getUint32(pos + 24, true)\n const nameLen = view.getUint16(pos + 28, true)\n const extraLen = view.getUint16(pos + 30, true)\n const commentLen = view.getUint16(pos + 32, true)\n const lfhOffset = view.getUint32(pos + 42, true)\n\n const path = TEXT_DECODER.decode(bytes.subarray(pos + 46, pos + 46 + nameLen))\n const extra = bytes.subarray(pos + 46 + nameLen, pos + 46 + nameLen + extraLen)\n pos += 46 + nameLen + extraLen + commentLen\n\n // Locate file data — skip the local file header.\n if (lfhOffset + 30 > bytes.length) {\n throw new ZipReadError(`readZip: local header offset ${lfhOffset} past end of file`)\n }\n if (view.getUint32(lfhOffset, true) !== 0x04034b50) {\n throw new ZipReadError(`readZip: missing local file header signature for \"${path}\"`)\n }\n const lfhNameLen = view.getUint16(lfhOffset + 26, true)\n const lfhExtraLen = view.getUint16(lfhOffset + 28, true)\n const dataStart = lfhOffset + 30 + lfhNameLen + lfhExtraLen\n if (dataStart + compressedSize > bytes.length) {\n throw new ZipReadError(`readZip: data region for \"${path}\" extends past end of file`)\n }\n const dataRegion = bytes.subarray(dataStart, dataStart + compressedSize)\n\n const encryptedFlag = (flags & 0x0001) !== 0\n const isWzAes = method === WZAES_METHOD_MARKER\n\n if (isWzAes) {\n if (!encryptedFlag) {\n throw new ZipReadError(\n `readZip: entry \"${path}\" carries the AES marker but the encryption flag is unset`,\n )\n }\n const wz = parseWzAesExtraField(extra)\n if (!wz) {\n throw new ZipReadError(\n `readZip: entry \"${path}\" uses method 99 but is missing the 0x9901 extra field`,\n )\n }\n if (options.password === undefined) {\n throw new ZipReadError(\n `readZip: entry \"${path}\" is AES-encrypted but no password was supplied`,\n )\n }\n const plaintext = await decryptEntryWzAes(dataRegion, options.password)\n // Sanity: uncompressedSize records the plaintext length for AE-2.\n if (uncompressedSize !== 0 && plaintext.length !== uncompressedSize) {\n throw new ZipReadError(\n `readZip: entry \"${path}\" plaintext length ${plaintext.length} ≠ declared ${uncompressedSize}`,\n )\n }\n out.push({ path, bytes: plaintext, encrypted: true })\n continue\n }\n\n if (encryptedFlag) {\n // Encrypted but not WinZip-AES → ZipCrypto or weak AES variant.\n throw new ZipCipherError(\n `readZip: entry \"${path}\" uses an encryption method other than WinZip-AES-256 — refusing to decrypt`,\n )\n }\n if (method !== 0) {\n throw new ZipReadError(\n `readZip: entry \"${path}\" uses compression method ${method} — only STORE (0) is supported`,\n )\n }\n out.push({ path, bytes: new Uint8Array(dataRegion), encrypted: false })\n }\n return out\n}\n\n/**\n * Locate the End-of-Central-Directory record. ZIP stores it near the\n * end of the file with an optional comment trailing it; scan\n * backwards from the end looking for the EOCD signature.\n */\nfunction locateEOCD(bytes: Uint8Array): number {\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n // Maximum comment is 65535 bytes — stop the scan there.\n const minStart = Math.max(0, bytes.length - 22 - 65535)\n for (let pos = bytes.length - 22; pos >= minStart; pos--) {\n if (view.getUint32(pos, true) === 0x06054b50) return pos\n }\n throw new ZipReadError('readZip: EOCD signature not found — input is not a valid ZIP')\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { VaultDiff, Vault } from '@noy-db/hub';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal zero-dependency ZIP writer — STORE method only (no compression).
|
|
5
|
+
*
|
|
6
|
+
* Produces single-disk zip archives readable by `unzip`, macOS Finder,
|
|
7
|
+
* Windows Explorer, and every well-behaved unzipper. Store-method is
|
|
8
|
+
* sufficient here because:
|
|
9
|
+
*
|
|
10
|
+
* 1. Most blobs reaching this writer are already compressed — PDFs,
|
|
11
|
+
* PNGs, JPEGs, `.zip`-inside-zip, encrypted `.noydb` bundles.
|
|
12
|
+
* Re-deflating yields near-zero savings at non-trivial CPU cost.
|
|
13
|
+
* 2. STORE keeps the encoder compact (~150 lines) and dependency-free.
|
|
14
|
+
* Deflate would need a ~5KB gzip-style implementation or an
|
|
15
|
+
* external dep; we're zero-deps by design.
|
|
16
|
+
* 3. Consumers who need further compression can deflate the whole zip
|
|
17
|
+
* after it's produced (`gzip archive.zip`).
|
|
18
|
+
*
|
|
19
|
+
* ## Wire format used
|
|
20
|
+
*
|
|
21
|
+
* Per PKWARE APPNOTE (the canonical ZIP spec):
|
|
22
|
+
*
|
|
23
|
+
* ```
|
|
24
|
+
* [Local File Header 1][File 1 data]
|
|
25
|
+
* [Local File Header 2][File 2 data]
|
|
26
|
+
* ...
|
|
27
|
+
* [Central Directory File Header 1]
|
|
28
|
+
* [Central Directory File Header 2]
|
|
29
|
+
* ...
|
|
30
|
+
* [End of Central Directory Record]
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* All multi-byte integers are little-endian. Filenames are UTF-8
|
|
34
|
+
* (flag bit 11 set so unzippers interpret them correctly). The
|
|
35
|
+
* per-file CRC-32 uses the standard polynomial 0xEDB88320.
|
|
36
|
+
*
|
|
37
|
+
* ## What this does NOT support
|
|
38
|
+
*
|
|
39
|
+
* - Deflate / bzip2 / lzma / zstd compression methods.
|
|
40
|
+
* - Zip64 (files / archives > 4 GiB). 32-bit size fields apply.
|
|
41
|
+
* - Multi-disk / spanned archives.
|
|
42
|
+
* - Encryption (the ZIP-level kind — noy-db records are already
|
|
43
|
+
* encrypted at the envelope layer before reaching this writer).
|
|
44
|
+
* - Symlinks, extended attributes, NTFS/UNIX permissions.
|
|
45
|
+
*
|
|
46
|
+
* @module
|
|
47
|
+
*/
|
|
48
|
+
/** Input entry to `writeZip`. */
|
|
49
|
+
interface ZipEntry {
|
|
50
|
+
/** Slash-delimited path inside the archive (e.g. `"records.json"`, `"attachments/01H.../raw.pdf"`). */
|
|
51
|
+
readonly path: string;
|
|
52
|
+
/** Raw bytes. Pass plaintext as-is; the writer doesn't interpret content. */
|
|
53
|
+
readonly bytes: Uint8Array;
|
|
54
|
+
/** Optional file mtime. Defaults to the call-time `Date`. */
|
|
55
|
+
readonly mtime?: Date;
|
|
56
|
+
}
|
|
57
|
+
/** Optional archive-wide settings for `writeZip`. */
|
|
58
|
+
interface WriteZipOptions {
|
|
59
|
+
/**
|
|
60
|
+
* When set, every entry is encrypted with WinZip-AES-256 sealed
|
|
61
|
+
* under this passphrase. Recipients open with stock archive
|
|
62
|
+
* tooling (7-Zip, Archive Utility, WinRAR, modern unzip builds)
|
|
63
|
+
* by typing the same passphrase.
|
|
64
|
+
*
|
|
65
|
+
* Single passphrase across all entries by design — per-entry
|
|
66
|
+
* passwords don't fit a noy-db export workflow and would muddy
|
|
67
|
+
* the API.
|
|
68
|
+
*/
|
|
69
|
+
readonly password?: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Build a complete ZIP archive byte stream from `entries`. The result
|
|
73
|
+
* is the full archive including central directory and EOCD — ready
|
|
74
|
+
* to `fs.writeFile` or hand to `new Blob([bytes])`.
|
|
75
|
+
*
|
|
76
|
+
* When `options.password` is set, every entry is encrypted with
|
|
77
|
+
* WinZip-AES-256 (vendor version AE-2). Output remains a valid
|
|
78
|
+
* single-disk ZIP that archive tools recognising WinZip-AES prompt
|
|
79
|
+
* for the password on extract.
|
|
80
|
+
*/
|
|
81
|
+
declare function writeZip(entries: readonly ZipEntry[], options?: WriteZipOptions): Promise<Uint8Array>;
|
|
82
|
+
declare function crc32(bytes: Uint8Array): number;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Minimal zero-dependency ZIP reader — STORE method only, with
|
|
86
|
+
* optional WinZip-AES-256 decryption.
|
|
87
|
+
*
|
|
88
|
+
* Mirror of `./zip.ts`'s writer. Reads the central directory, walks
|
|
89
|
+
* each entry, decrypts when the AES marker (method 99) is set and a
|
|
90
|
+
* password was supplied. Same scope limits as the writer:
|
|
91
|
+
*
|
|
92
|
+
* - **No DEFLATE / bzip2 / lzma / zstd.** STORE-only. Trying to read
|
|
93
|
+
* a deflated entry throws `ZipReadError`.
|
|
94
|
+
* - **No Zip64.** 32-bit size + offset fields apply.
|
|
95
|
+
* - **AES-256 only.** AES-128/192 and ZipCrypto are refused — same
|
|
96
|
+
* stance as the writer.
|
|
97
|
+
*
|
|
98
|
+
* @module
|
|
99
|
+
*/
|
|
100
|
+
declare class ZipReadError extends Error {
|
|
101
|
+
readonly code = "ZIP_READ_ERROR";
|
|
102
|
+
constructor(message: string);
|
|
103
|
+
}
|
|
104
|
+
interface ReadZipEntry {
|
|
105
|
+
readonly path: string;
|
|
106
|
+
readonly bytes: Uint8Array;
|
|
107
|
+
/** True iff the entry was decrypted from a WinZip-AES region. */
|
|
108
|
+
readonly encrypted: boolean;
|
|
109
|
+
}
|
|
110
|
+
interface ReadZipOptions {
|
|
111
|
+
/** WinZip-AES-256 passphrase. Required for encrypted archives. */
|
|
112
|
+
readonly password?: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Parse a ZIP archive and return its entries. When the archive
|
|
116
|
+
* contains AES-encrypted entries (method 99 with the 0x9901 extra
|
|
117
|
+
* field), `options.password` MUST be supplied. Wrong password
|
|
118
|
+
* surfaces a `ZipCipherError` from the underlying decrypt step.
|
|
119
|
+
*/
|
|
120
|
+
declare function readZip(bytes: Uint8Array, options?: ReadZipOptions): Promise<ReadZipEntry[]>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* WinZip-AES encryption for ZIP entries — write + read.
|
|
124
|
+
*
|
|
125
|
+
* Implements the AES-256 variant of the WinZip-AES extension as
|
|
126
|
+
* documented in
|
|
127
|
+
* https://www.winzip.com/win/en/aes_info.html and PKWARE Appendix E.
|
|
128
|
+
*
|
|
129
|
+
* **AES-256 only by design.** ZipCrypto and AES-128/192 are refused at
|
|
130
|
+
* both write and read time — the security story for the package is
|
|
131
|
+
* "if you need encryption, use AES-256." Adding weaker tiers would
|
|
132
|
+
* give consumers the impression of meaningful choice.
|
|
133
|
+
*
|
|
134
|
+
* ## Layout of an encrypted entry's data region
|
|
135
|
+
*
|
|
136
|
+
* ```
|
|
137
|
+
* ┌──────────┬──────────┬─────────────┬──────────┐
|
|
138
|
+
* │ salt(16) │ verif(2) │ ciphertext │ auth(10) │
|
|
139
|
+
* └──────────┴──────────┴─────────────┴──────────┘
|
|
140
|
+
* ```
|
|
141
|
+
*
|
|
142
|
+
* - **salt** — 16 random bytes per entry
|
|
143
|
+
* - **verifier** — 2 bytes, last 2 bytes of the PBKDF2 output;
|
|
144
|
+
* used to fail-fast on a wrong password without running CTR
|
|
145
|
+
* - **ciphertext** — input bytes XOR-ed with AES-CTR keystream
|
|
146
|
+
* - **auth** — first 10 bytes of HMAC-SHA1(ciphertext, authKey),
|
|
147
|
+
* where authKey is the second half of the PBKDF2 output
|
|
148
|
+
*
|
|
149
|
+
* ## Local file header changes
|
|
150
|
+
*
|
|
151
|
+
* For an encrypted entry:
|
|
152
|
+
*
|
|
153
|
+
* - `compression method` field is forced to **99** (AES marker)
|
|
154
|
+
* - flag **bit 0** is set (encryption)
|
|
155
|
+
* - extra field tag **0x9901** is appended with 7 bytes:
|
|
156
|
+
* - `2 bytes` vendor version → `0x0002` (AE-2)
|
|
157
|
+
* - `2 bytes` vendor ID → `'AE'` (`0x4541` LE)
|
|
158
|
+
* - `1 byte` AES strength → `0x03` (AES-256)
|
|
159
|
+
* - `2 bytes` real method → `0x0000` (STORE)
|
|
160
|
+
* - For AE-2, `crc` is forced to 0 (the spec recommends this — the
|
|
161
|
+
* authentication code already covers integrity)
|
|
162
|
+
*
|
|
163
|
+
* ## Counter convention (subtle!)
|
|
164
|
+
*
|
|
165
|
+
* AES-CTR per WinZip uses a **little-endian** 16-byte counter. The
|
|
166
|
+
* counter starts at **1** (not 0) and increments by 1 per 16-byte
|
|
167
|
+
* block. The lower 8 bytes hold the counter; the upper 8 bytes are 0.
|
|
168
|
+
*
|
|
169
|
+
* @module
|
|
170
|
+
*/
|
|
171
|
+
|
|
172
|
+
declare class ZipCipherError extends Error {
|
|
173
|
+
readonly code = "ZIP_CIPHER_ERROR";
|
|
174
|
+
constructor(message: string);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* **@noy-db/as-zip** — composite record + blob archive for noy-db.
|
|
179
|
+
*
|
|
180
|
+
* Bundles a collection's records AND every record's attached blobs
|
|
181
|
+
* into a single zip archive. The canonical "download this audit
|
|
182
|
+
* trail" / "migrate this case folder" primitive. Part of the
|
|
183
|
+
* `@noy-db/as-*` portable-artefact family — plaintext tier,
|
|
184
|
+
* document sub-family.
|
|
185
|
+
*
|
|
186
|
+
* ## Authorisation
|
|
187
|
+
*
|
|
188
|
+
* One capability check: `assertCanExport('plaintext', 'zip')`.
|
|
189
|
+
* A composite archive is semantically the `'zip'` format from the
|
|
190
|
+
* auth model's POV — requiring separate `'json'`, `'csv'`, `'blob'`
|
|
191
|
+
* grants for a single call would fragment the grant surface without
|
|
192
|
+
* adding any real isolation (the archive concatenates them anyway).
|
|
193
|
+
*
|
|
194
|
+
* The owner grants the composite capability explicitly:
|
|
195
|
+
*
|
|
196
|
+
* ```ts
|
|
197
|
+
* await db.grant('firm', {
|
|
198
|
+
* userId: 'auditor',
|
|
199
|
+
* role: 'viewer',
|
|
200
|
+
* passphrase: '…',
|
|
201
|
+
* exportCapability: { plaintext: ['zip'] },
|
|
202
|
+
* })
|
|
203
|
+
* ```
|
|
204
|
+
*
|
|
205
|
+
* ## Archive layout
|
|
206
|
+
*
|
|
207
|
+
* ```
|
|
208
|
+
* archive.zip
|
|
209
|
+
* ├── manifest.json # index + provenance (record count, blob count, exported-at, exported-by)
|
|
210
|
+
* ├── records.json # array of decrypted records
|
|
211
|
+
* └── attachments/
|
|
212
|
+
* ├── <recordId>/<slot> # raw blob bytes, MIME-native
|
|
213
|
+
* └── ...
|
|
214
|
+
* ```
|
|
215
|
+
*
|
|
216
|
+
* The folder-per-record layout makes composite entities (email +
|
|
217
|
+
* body + attachments, invoice + scan + receipt) navigable in
|
|
218
|
+
* Finder/Explorer without tooling.
|
|
219
|
+
*
|
|
220
|
+
* See [`docs/patterns/as-exports.md`](https://github.com/vLannaAi/noy-db/blob/main/docs/patterns/as-exports.md).
|
|
221
|
+
*
|
|
222
|
+
* @packageDocumentation
|
|
223
|
+
*/
|
|
224
|
+
|
|
225
|
+
/** Record-selection options. */
|
|
226
|
+
interface AsZipRecordsOptions {
|
|
227
|
+
/** Collection to export. Must be in the caller's read ACL. */
|
|
228
|
+
readonly collection: string;
|
|
229
|
+
/**
|
|
230
|
+
* Optional predicate against each decrypted record. When omitted,
|
|
231
|
+
* every record is included. Runs after decryption, before zip
|
|
232
|
+
* assembly — doesn't reduce I/O, just the final set.
|
|
233
|
+
*/
|
|
234
|
+
readonly filter?: (record: unknown) => boolean;
|
|
235
|
+
}
|
|
236
|
+
/** Blob-selection options. */
|
|
237
|
+
interface AsZipAttachmentsOptions {
|
|
238
|
+
/**
|
|
239
|
+
* Which slots to include per record. `'*'` (default) includes
|
|
240
|
+
* every slot on every record; a string[] selects specific slot
|
|
241
|
+
* names. Pass an empty array to skip blob inclusion entirely.
|
|
242
|
+
*/
|
|
243
|
+
readonly slots?: readonly string[] | '*';
|
|
244
|
+
}
|
|
245
|
+
/** Top-level options for every entry point. */
|
|
246
|
+
interface AsZipOptions {
|
|
247
|
+
readonly records: AsZipRecordsOptions;
|
|
248
|
+
readonly attachments?: AsZipAttachmentsOptions;
|
|
249
|
+
/**
|
|
250
|
+
* Optional WinZip-AES-256 passphrase. When set, every entry
|
|
251
|
+
* inside the archive (records + attachments + manifest) is
|
|
252
|
+
* encrypted with WinZip-AES-256 and the recipient must supply the
|
|
253
|
+
* passphrase to extract.
|
|
254
|
+
*
|
|
255
|
+
* **Interop note:** the implementation is strictly to spec but has
|
|
256
|
+
* not been validated against 7-Zip / Archive Utility / WinRAR in
|
|
257
|
+
* this checkout. Round-trips against the package's own reader pass.
|
|
258
|
+
* See https://github.com/vLannaAi/noy-db/issues/304 for the
|
|
259
|
+
* cross-tool validation matrix.
|
|
260
|
+
*
|
|
261
|
+
* **Security framing:** this is the *interop* layer for
|
|
262
|
+
* cross-platform handoff to archive tools, not the *encryption*
|
|
263
|
+
* layer for sharing noy-db state. Use `as-noydb` for multi-recipient
|
|
264
|
+
* + revocable + audited egress.
|
|
265
|
+
*/
|
|
266
|
+
readonly password?: string;
|
|
267
|
+
}
|
|
268
|
+
/** Options for `download()` — adds an optional filename. */
|
|
269
|
+
interface AsZipDownloadOptions extends AsZipOptions {
|
|
270
|
+
/** Filename offered to the browser. Default `'<collection>.zip'`. */
|
|
271
|
+
readonly filename?: string;
|
|
272
|
+
}
|
|
273
|
+
/** Options for `write()` — requires explicit risk acknowledgement. */
|
|
274
|
+
interface AsZipWriteOptions extends AsZipOptions {
|
|
275
|
+
/**
|
|
276
|
+
* Required for Node file-write calls — consumer acknowledgement
|
|
277
|
+
* that plaintext bytes will persist on disk past the current
|
|
278
|
+
* process lifetime (Tier 3 risk).
|
|
279
|
+
*/
|
|
280
|
+
readonly acknowledgeRisks: true;
|
|
281
|
+
}
|
|
282
|
+
/** Manifest entry — one per record that landed in the archive. */
|
|
283
|
+
interface ManifestRecord {
|
|
284
|
+
readonly id: string;
|
|
285
|
+
readonly attachments: ReadonlyArray<{
|
|
286
|
+
readonly slot: string;
|
|
287
|
+
readonly path: string;
|
|
288
|
+
readonly size: number;
|
|
289
|
+
readonly mimeType?: string;
|
|
290
|
+
}>;
|
|
291
|
+
}
|
|
292
|
+
/** Archive-level manifest. Serialised as `manifest.json`. */
|
|
293
|
+
interface ArchiveManifest {
|
|
294
|
+
readonly _noydb_archive: 1;
|
|
295
|
+
readonly collection: string;
|
|
296
|
+
readonly exportedAt: string;
|
|
297
|
+
readonly recordCount: number;
|
|
298
|
+
readonly attachmentCount: number;
|
|
299
|
+
readonly records: readonly ManifestRecord[];
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Assemble the archive bytes. Pure beyond the auth check + store
|
|
303
|
+
* reads. Records are written as `records.json`; blobs as
|
|
304
|
+
* `attachments/<recordId>/<slot>`. A `manifest.json` index lands
|
|
305
|
+
* at the archive root so extractors can walk the content without
|
|
306
|
+
* re-reading every file.
|
|
307
|
+
*/
|
|
308
|
+
declare function toBytes(vault: Vault, options: AsZipOptions): Promise<Uint8Array>;
|
|
309
|
+
/**
|
|
310
|
+
* Browser download — wraps `toBytes()` in a `Blob` and triggers the
|
|
311
|
+
* browser's download prompt. Requires `URL.createObjectURL` +
|
|
312
|
+
* `document.createElement`. Throws in headless Node.
|
|
313
|
+
*/
|
|
314
|
+
declare function download(vault: Vault, options: AsZipDownloadOptions): Promise<void>;
|
|
315
|
+
/**
|
|
316
|
+
* Node file-write — persists the archive to disk. Requires
|
|
317
|
+
* explicit `acknowledgeRisks: true` (Tier 3 egress — the archive
|
|
318
|
+
* contains plaintext records + blob bytes).
|
|
319
|
+
*/
|
|
320
|
+
declare function write(vault: Vault, path: string, options: AsZipWriteOptions): Promise<void>;
|
|
321
|
+
|
|
322
|
+
type ImportPolicy = 'merge' | 'replace' | 'insert-only';
|
|
323
|
+
interface AsZipImportOptions {
|
|
324
|
+
/** Target collection for the records. */
|
|
325
|
+
readonly collection: string;
|
|
326
|
+
/** Field on each record that carries its id. Default `'id'`. */
|
|
327
|
+
readonly idKey?: string;
|
|
328
|
+
/** Reconciliation policy. Default `'merge'`. */
|
|
329
|
+
readonly policy?: ImportPolicy;
|
|
330
|
+
/** WinZip-AES-256 passphrase if the archive is encrypted. */
|
|
331
|
+
readonly password?: string;
|
|
332
|
+
}
|
|
333
|
+
interface AsZipImportPlan {
|
|
334
|
+
readonly plan: VaultDiff;
|
|
335
|
+
readonly policy: ImportPolicy;
|
|
336
|
+
apply(): Promise<void>;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Read a `.zip` archive (optionally WinZip-AES-256 encrypted), parse
|
|
340
|
+
* `records.json` from it, and return an `ImportPlan` whose `apply()`
|
|
341
|
+
* writes the changes through the normal collection API.
|
|
342
|
+
*
|
|
343
|
+
* Pairs with `toBytes` for round-trip workflows. The records JSON
|
|
344
|
+
* format matches what `toBytes` writes — array of records under
|
|
345
|
+
* the configured collection's id key.
|
|
346
|
+
*/
|
|
347
|
+
declare function fromBytes(vault: Vault, bytes: Uint8Array, options: AsZipImportOptions): Promise<AsZipImportPlan>;
|
|
348
|
+
|
|
349
|
+
export { type ArchiveManifest, type AsZipAttachmentsOptions, type AsZipDownloadOptions, type AsZipImportOptions, type AsZipImportPlan, type AsZipOptions, type AsZipRecordsOptions, type AsZipWriteOptions, type ImportPolicy, type ManifestRecord, type ReadZipEntry, type ReadZipOptions, type WriteZipOptions, ZipCipherError, type ZipEntry, ZipReadError, crc32, download, fromBytes, readZip, toBytes, write, writeZip };
|