@noy-db/as-zip 0.8.0-pre.0 → 0.9.0-pre.0

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/README.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @noy-db/as-zip
2
2
 
3
+ <!-- prose-preamble
4
+ // Bindings the illustrative blocks below elide. Typed, not stubbed:
5
+ // `declare` costs no runtime dependency, so the preamble cannot drift
6
+ // from a fixture — and a wrong member call still fails the gate naming
7
+ // the real type.
8
+ import type { Noydb } from '@noy-db/hub'
9
+ declare const db: Noydb
10
+ -->
11
+
3
12
  Composite record + blob archive for noy-db. Bundles a collection's
4
13
  records and every record's attached blobs into one `.zip` — the
5
14
  "download this audit trail" / "migrate this case folder" primitive.
@@ -111,6 +120,38 @@ const bytes = await toBytes<Invoice>(vault, {
111
120
  // → Uint8Array ready for `fs.writeFile` or `new Blob([bytes])`
112
121
  ```
113
122
 
123
+ #### Reproducible archives
124
+
125
+ Pass `mtime` to make the bytes byte-reproducible — needed when the
126
+ archive is content-addressed, hashed as an attestation and rebuilt
127
+ later to check the digest still holds:
128
+
129
+ ```ts
130
+ import { toBytes } from '@noy-db/as-zip'
131
+
132
+ interface Invoice { id: string; status: string }
133
+
134
+ const reproducible = await toBytes<Invoice>(vault, {
135
+ records: { collection: 'invoices' },
136
+ mtime: new Date(0),
137
+ })
138
+ ```
139
+
140
+ One option, because two values reach the bytes on every export and
141
+ pinning one without the other leaves the archive non-reproducible with
142
+ nothing to warn you. `mtime` sets **both** the manifest's `exportedAt`
143
+ and the mod-time on every entry.
144
+
145
+ They fail differently, which is worth knowing: entry mod-times are
146
+ MS-DOS **2-second** granular, so they are only *accidentally*
147
+ reproducible and a back-to-back check passes; `exportedAt` is an ISO
148
+ string at millisecond precision and never matches twice.
149
+
150
+ ⚠️ The two do not read alike for early dates. DOS has no years before
151
+ 1980 and clamps, so `new Date(0)` yields an `exportedAt` of
152
+ `1970-01-01T00:00:00.000Z` while the file dates show `1980-01-01`.
153
+ Both are correct — the formats disagree, not the code.
154
+
114
155
  ### `download(vault, options)` — browser
115
156
 
116
157
  ```ts
@@ -183,6 +224,24 @@ const bytes = writeZip([
183
224
  STORE method (no compression). Single-disk, no Zip64. Files > 4 GiB
184
225
  are not supported.
185
226
 
227
+ Entry mod-times default to the call-time clock, read **once** per
228
+ archive so every entry agrees. A ZIP stores mod-time at MS-DOS
229
+ 2-second granularity, so identical input gives identical bytes only
230
+ while two writes land in the same 2-second bucket. Pass `mtime` — per
231
+ entry, or archive-wide — to make the output byte-reproducible:
232
+
233
+ ```ts
234
+ import { writeZip } from '@noy-db/as-zip'
235
+
236
+ const bytes = writeZip(
237
+ [{ path: 'hello.txt', bytes: new TextEncoder().encode('hi') }],
238
+ { mtime: new Date(0) },
239
+ )
240
+ ```
241
+
242
+ A per-entry `mtime` overrides the archive-wide one. Years before 1980
243
+ clamp to the DOS epoch.
244
+
186
245
  ## Related
187
246
 
188
247
  - `@noy-db/as-blob` — single attachment
package/dist/index.d.ts CHANGED
@@ -60,15 +60,37 @@ interface ZipEntry {
60
60
  interface WriteZipOptions {
61
61
  /**
62
62
  * When set, every entry is encrypted with WinZip-AES-256 sealed
63
- * under this secret. Recipients open with stock archive
64
- * tooling (7-Zip, Archive Utility, WinRAR, modern unzip builds)
65
- * by typing the same secret.
63
+ * under this secret. Recipients open it by typing the same secret.
64
+ *
65
+ * ⚠️ **Not every tool can.** 7-Zip is verified on Linux, macOS and
66
+ * Windows; macOS's `unar` reads these archives too. Some
67
+ * distro-packaged `unar` builds cannot read WinZip-AES-256 —
68
+ * measured on `ubuntu-latest`, 2026-09-05, run 33944815086, where
69
+ * every vector failed with *"Missing or wrong password"* on bytes
70
+ * 7-Zip opens everywhere. The limit is in that build, not in the
71
+ * archive. Name the tool, the platform and the date before widening
72
+ * this claim.
66
73
  *
67
74
  * Single secret across all entries by design — per-entry
68
75
  * passwords don't fit a noy-db export workflow and would muddy
69
76
  * the API.
70
77
  */
71
78
  readonly password?: string;
79
+ /**
80
+ * Mod-time stamped on every entry that does not carry its own
81
+ * `mtime`. Defaults to the call-time clock.
82
+ *
83
+ * Set it to make the output byte-reproducible: a ZIP stores mod-time
84
+ * at MS-DOS 2-second granularity, so identical input otherwise
85
+ * produces identical bytes only while two writes land in the same
86
+ * 2-second bucket. Callers content-addressing the archive (hashing
87
+ * the bytes as an attestation) need a fixed value here — the default
88
+ * is only ACCIDENTALLY reproducible, and a back-to-back check passes.
89
+ *
90
+ * Years before 1980 clamp to the DOS epoch, so `new Date(0)` is a
91
+ * valid choice and lands on 1980-01-01.
92
+ */
93
+ readonly mtime?: Date;
72
94
  }
73
95
  /**
74
96
  * Build a complete ZIP archive byte stream from `entries`. The result
@@ -254,6 +276,27 @@ interface AsZipAttachmentsOptions {
254
276
  interface AsZipOptions<T = unknown> {
255
277
  readonly records: AsZipRecordsOptions<T>;
256
278
  readonly attachments?: AsZipAttachmentsOptions;
279
+ /**
280
+ * The time this archive claims it was made. Defaults to the
281
+ * call-time clock. Sets BOTH the manifest's `exportedAt` and the
282
+ * mod-time on every zip entry — one value, because they are one
283
+ * fact, and because pinning only one of them leaves the archive
284
+ * non-reproducible with nothing to warn you.
285
+ *
286
+ * Set it to make the output byte-reproducible (#4), which a caller
287
+ * content-addressing the archive needs. Two values reach the bytes
288
+ * on every export and they fail differently: entry mod-times are
289
+ * MS-DOS 2-second granular, so they are only ACCIDENTALLY
290
+ * reproducible and a back-to-back check passes; `exportedAt` is an
291
+ * ISO string at millisecond precision and never matches twice.
292
+ *
293
+ * ⚠️ The two will not read alike for early dates. DOS has no years
294
+ * before 1980 and clamps, so `new Date(0)` gives an `exportedAt` of
295
+ * `1970-01-01T00:00:00.000Z` while the file dates show
296
+ * `1980-01-01`. Both are correct; they disagree because the formats
297
+ * do.
298
+ */
299
+ readonly mtime?: Date;
257
300
  /**
258
301
  * Optional WinZip-AES-256 secret. When set, every entry
259
302
  * inside the archive (records + attachments + manifest) is
package/dist/index.js CHANGED
@@ -190,9 +190,10 @@ async function writeZip(entries, options = {}) {
190
190
  const localParts = [];
191
191
  const cdParts = [];
192
192
  let offset = 0;
193
+ const writeTime = options.mtime ?? /* @__PURE__ */ new Date();
193
194
  for (const entry of entries) {
194
195
  const nameBytes = TEXT_ENCODER.encode(entry.path);
195
- const dosTime = toDosTime(entry.mtime ?? /* @__PURE__ */ new Date());
196
+ const dosTime = toDosTime(entry.mtime ?? writeTime);
196
197
  let dataBytes;
197
198
  let extraField;
198
199
  let methodField;
@@ -463,10 +464,11 @@ async function toBytes(vault, options) {
463
464
  }));
464
465
  return { id, attachments: attach };
465
466
  });
467
+ const at = options.mtime ?? /* @__PURE__ */ new Date();
466
468
  const manifest = {
467
469
  _noydb_archive: 1,
468
470
  collection: collectionName,
469
- exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
471
+ exportedAt: at.toISOString(),
470
472
  recordCount: records.length,
471
473
  attachmentCount: attachmentEntries.length,
472
474
  records: recordIndex
@@ -492,7 +494,10 @@ async function toBytes(vault, options) {
492
494
  for (const a of attachmentEntries) {
493
495
  entries.push({ path: a.path, bytes: a.bytes });
494
496
  }
495
- return writeZip(entries, options.password !== void 0 ? { password: options.password } : {});
497
+ return writeZip(entries, {
498
+ ...options.password !== void 0 ? { password: options.password } : {},
499
+ mtime: at
500
+ });
496
501
  }
497
502
  async function download(vault, options) {
498
503
  const bytes = await toBytes(vault, options);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sha1.ts","../src/aes.ts","../src/zip.ts","../src/read.ts","../src/index.ts"],"sourcesContent":["/**\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 secret. Recipients open with stock archive\n * tooling (7-Zip, Archive Utility, WinRAR, modern unzip builds)\n * by typing the same secret.\n *\n * Single secret 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 secret. 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","/**\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 * secret: '…',\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/noy-db/core/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/**\n * Record-selection options.\n *\n * `T` is the shape of a decrypted record in this collection. It defaults to\n * `unknown`, so every existing call keeps compiling unchanged; supply it (or\n * annotate `filter`'s parameter) when you want the predicate typed.\n */\nexport interface AsZipRecordsOptions<T = unknown> {\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: T) => 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<T = unknown> {\n readonly records: AsZipRecordsOptions<T>\n readonly attachments?: AsZipAttachmentsOptions\n /**\n * Optional WinZip-AES-256 secret. 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 * secret to extract.\n *\n * **Interop note:** validated against 7-Zip on Linux, macOS and\n * Windows, and against `unar` on macOS, by a blocking CI job that\n * `publish` depends on.\n *\n * ⚠️ Some distro-packaged `unar` builds CANNOT read these archives:\n * measured on `ubuntu-latest`, 2026-09-05, CI run 33944815086,\n * where every vector failed with \"Missing or wrong password\". The\n * identical bytes open under macOS's `unar` and under 7-Zip on\n * every platform, so the limit is in that build rather than in the\n * archive — but a recipient reaching for their distro's `unar` may\n * not get in.\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<T = unknown> extends AsZipOptions<T> {\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<T = unknown> extends AsZipOptions<T> {\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<T = unknown>(vault: Vault, options: AsZipOptions<T>): 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 // `r` is the decrypted record as it exists at runtime: a plain object.\n // `T` is the caller's assertion about that shape, unverifiable here — the\n // same contract `vault.collection<T>()` already makes. The cast is where\n // that assertion is taken on trust, and it is the only one.\n if (options.records.filter && !options.records.filter(r as T)) 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<T = unknown>(vault: Vault, options: AsZipDownloadOptions<T>): 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<T = unknown>(\n vault: Vault,\n path: string,\n options: AsZipWriteOptions<T>,\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 } from '@noy-db/hub'\nimport { readZip } from './read.js'\n\n// Hub-owned as of 0.7 (ADR 0004). This line replaced a local declaration that\n// existed identically in six as-* packages, with nothing comparing them.\nimport type { ImportPolicy, ImportPlan } from '@noy-db/hub/as'\nexport type { ImportPolicy }\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 secret if the archive is encrypted. */\n readonly password?: string\n}\n\nexport type AsZipImportPlan = ImportPlan\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 transactionsStrategy 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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/sha1.ts","../src/aes.ts","../src/zip.ts","../src/read.ts","../src/index.ts"],"sourcesContent":["/**\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 secret. Recipients open it by typing the same secret.\n *\n * ⚠️ **Not every tool can.** 7-Zip is verified on Linux, macOS and\n * Windows; macOS's `unar` reads these archives too. Some\n * distro-packaged `unar` builds cannot read WinZip-AES-256 —\n * measured on `ubuntu-latest`, 2026-09-05, run 33944815086, where\n * every vector failed with *\"Missing or wrong password\"* on bytes\n * 7-Zip opens everywhere. The limit is in that build, not in the\n * archive. Name the tool, the platform and the date before widening\n * this claim.\n *\n * Single secret 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 * Mod-time stamped on every entry that does not carry its own\n * `mtime`. Defaults to the call-time clock.\n *\n * Set it to make the output byte-reproducible: a ZIP stores mod-time\n * at MS-DOS 2-second granularity, so identical input otherwise\n * produces identical bytes only while two writes land in the same\n * 2-second bucket. Callers content-addressing the archive (hashing\n * the bytes as an attestation) need a fixed value here — the default\n * is only ACCIDENTALLY reproducible, and a back-to-back check passes.\n *\n * Years before 1980 clamp to the DOS epoch, so `new Date(0)` is a\n * valid choice and lands on 1980-01-01.\n */\n readonly mtime?: Date\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 // Read the clock ONCE for the whole archive. Read per entry, a write\n // that straddles a DOS 2-second bucket stamps its own entries\n // inconsistently — the archive then disagrees with itself about when\n // it was made, and nothing reports it.\n const writeTime = options.mtime ?? new Date()\n\n for (const entry of entries) {\n const nameBytes = TEXT_ENCODER.encode(entry.path)\n const dosTime = toDosTime(entry.mtime ?? writeTime)\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 secret. 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","/**\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 * secret: '…',\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/noy-db/core/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/**\n * Record-selection options.\n *\n * `T` is the shape of a decrypted record in this collection. It defaults to\n * `unknown`, so every existing call keeps compiling unchanged; supply it (or\n * annotate `filter`'s parameter) when you want the predicate typed.\n */\nexport interface AsZipRecordsOptions<T = unknown> {\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: T) => 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<T = unknown> {\n readonly records: AsZipRecordsOptions<T>\n readonly attachments?: AsZipAttachmentsOptions\n /**\n * The time this archive claims it was made. Defaults to the\n * call-time clock. Sets BOTH the manifest's `exportedAt` and the\n * mod-time on every zip entry — one value, because they are one\n * fact, and because pinning only one of them leaves the archive\n * non-reproducible with nothing to warn you.\n *\n * Set it to make the output byte-reproducible (#4), which a caller\n * content-addressing the archive needs. Two values reach the bytes\n * on every export and they fail differently: entry mod-times are\n * MS-DOS 2-second granular, so they are only ACCIDENTALLY\n * reproducible and a back-to-back check passes; `exportedAt` is an\n * ISO string at millisecond precision and never matches twice.\n *\n * ⚠️ The two will not read alike for early dates. DOS has no years\n * before 1980 and clamps, so `new Date(0)` gives an `exportedAt` of\n * `1970-01-01T00:00:00.000Z` while the file dates show\n * `1980-01-01`. Both are correct; they disagree because the formats\n * do.\n */\n readonly mtime?: Date\n /**\n * Optional WinZip-AES-256 secret. 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 * secret to extract.\n *\n * **Interop note:** validated against 7-Zip on Linux, macOS and\n * Windows, and against `unar` on macOS, by a blocking CI job that\n * `publish` depends on.\n *\n * ⚠️ Some distro-packaged `unar` builds CANNOT read these archives:\n * measured on `ubuntu-latest`, 2026-09-05, CI run 33944815086,\n * where every vector failed with \"Missing or wrong password\". The\n * identical bytes open under macOS's `unar` and under 7-Zip on\n * every platform, so the limit is in that build rather than in the\n * archive — but a recipient reaching for their distro's `unar` may\n * not get in.\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<T = unknown> extends AsZipOptions<T> {\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<T = unknown> extends AsZipOptions<T> {\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<T = unknown>(vault: Vault, options: AsZipOptions<T>): 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 // `r` is the decrypted record as it exists at runtime: a plain object.\n // `T` is the caller's assertion about that shape, unverifiable here — the\n // same contract `vault.collection<T>()` already makes. The cast is where\n // that assertion is taken on trust, and it is the only one.\n if (options.records.filter && !options.records.filter(r as T)) 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 // Read once, used for both the manifest and every entry header.\n const at = options.mtime ?? new Date()\n const manifest: ArchiveManifest = {\n _noydb_archive: 1,\n collection: collectionName,\n exportedAt: at.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, {\n ...(options.password !== undefined ? { password: options.password } : {}),\n mtime: at,\n })\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<T = unknown>(vault: Vault, options: AsZipDownloadOptions<T>): 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<T = unknown>(\n vault: Vault,\n path: string,\n options: AsZipWriteOptions<T>,\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 } from '@noy-db/hub'\nimport { readZip } from './read.js'\n\n// Hub-owned as of 0.7 (ADR 0004). This line replaced a local declaration that\n// existed identically in six as-* packages, with nothing comparing them.\nimport type { ImportPolicy, ImportPlan } from '@noy-db/hub/as'\nexport type { ImportPolicy }\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 secret if the archive is encrypted. */\n readonly password?: string\n}\n\nexport type AsZipImportPlan = ImportPlan\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 transactionsStrategy 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"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noy-db/as-zip",
3
- "version": "0.8.0-pre.0",
3
+ "version": "0.9.0-pre.0",
4
4
  "description": "Composite record + blob archive export for noy-db — bundles a collection's records plus every attached blob into one zip. Zero dependencies (STORE-only zip encoder). Gated by the RFC #249 `assertCanExport('plaintext', …)` gate with format `'zip'`. Part of the @noy-db/as-* portable-artefact family.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "vLannaAi <vicio@lanna.ai>",
@@ -33,12 +33,12 @@
33
33
  "node": ">=22.0.0"
34
34
  },
35
35
  "peerDependencies": {
36
- "@noy-db/hub": "^0.7.0 || ^0.7.1-pre.0 || ^0.8.0-pre.0"
36
+ "@noy-db/hub": "^0.7.0 || ^0.7.1-pre.0 || ^0.8.0-pre.0 || ^0.9.0-pre.1"
37
37
  },
38
38
  "devDependencies": {
39
- "@noy-db/hub": "0.8.0-pre.0",
40
- "@noy-db/test-format-conformance": "0.8.0-pre.0",
41
- "@noy-db/to-memory": "0.8.0-pre.0",
39
+ "@noy-db/hub": "0.9.0-pre.2",
40
+ "@noy-db/ports": "0.9.0-pre.2",
41
+ "@noy-db/to-memory": "0.8.0",
42
42
  "@types/node": "^22.0.0",
43
43
  "happy-dom": "^18.0.0"
44
44
  },
@@ -56,6 +56,7 @@
56
56
  "access": "public",
57
57
  "tag": "latest"
58
58
  },
59
+ "dependencies": {},
59
60
  "scripts": {
60
61
  "build": "tsup",
61
62
  "test": "vitest run",