@noy-db/as-zip 0.7.0 → 0.8.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/LICENSE CHANGED
@@ -1,21 +1,202 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 vLannaAi
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright 2026 vLannaAi
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/NOTICE ADDED
@@ -0,0 +1 @@
1
+ noy-db — Copyright 2026 vLannaAi
package/README.md CHANGED
@@ -15,18 +15,32 @@ Pass `password` to encrypt every entry with WinZip-AES-256
15
15
  (vendor version AE-2):
16
16
 
17
17
  ```ts
18
- const archive = await asZip.toBytes(vault, {
18
+ import { toBytes } from '@noy-db/as-zip'
19
+
20
+ const archive = await toBytes(vault, {
19
21
  records: { collection: 'invoices' },
20
22
  password: 'shared-with-recipient-2026',
21
23
  })
22
24
  ```
23
25
 
24
- The output is still a valid single-disk ZIP that archive tools
25
- recognising WinZip-AES (7-Zip, Archive Utility, WinRAR, modern
26
- unzip builds) prompt for the password on extract. Read back via:
26
+ The output is still a valid single-disk ZIP. 7-Zip prompts for the
27
+ password on extract, verified on Linux, macOS and Windows.
28
+
29
+ ⚠️ **Known exception:** some distro-packaged `unar` builds cannot
30
+ read WinZip-AES-256 — measured on `ubuntu-latest`, 2026-09-05,
31
+ [run 33944815086][interop-run], where every vector failed with
32
+ *"Missing or wrong password"*. macOS's `unar` reads the identical
33
+ bytes, as does 7-Zip on every platform, so the limit is in that
34
+ build rather than in the archive. Read back via:
35
+
36
+ [interop-run]: https://github.com/noy-db/as/actions/runs/33944815086
27
37
 
28
38
  ```ts
29
- const decoded = await asZip.fromBytes(vault, archive, {
39
+ import { fromBytes } from '@noy-db/as-zip'
40
+
41
+ declare const archive: Uint8Array // the bytes produced above
42
+
43
+ const decoded = await fromBytes(vault, archive, {
30
44
  collection: 'invoices',
31
45
  password: 'shared-with-recipient-2026',
32
46
  })
@@ -36,8 +50,11 @@ await decoded.apply()
36
50
  > AES-256 only. ZipCrypto and AES-128/192 are refused at both write
37
51
  > and read time.
38
52
  >
39
- > Validated against 7-Zip 26.x (Linux/macOS/Windows) and Archive
40
- > Utility (macOS 15) see [`docs/interop-matrix.md`](../../docs/interop-matrix.md).
53
+ > Validated on every push that touches this package, against 7-Zip
54
+ > on ubuntu / macos / windows runners and `unar` on macOS — see
55
+ > [`.github/workflows/interop.yml`](../.github/workflows/interop.yml).
56
+ > The job is blocking and `publish` depends on it, so a release
57
+ > cannot ship past a failing vector.
41
58
  >
42
59
  > **This is the interop layer**, not the encryption layer. For
43
60
  > multi-recipient + revocable + audited noy-db egress, use
@@ -45,7 +62,7 @@ await decoded.apply()
45
62
 
46
63
  Part of the `@noy-db/as-*` portable-artefact family, plaintext
47
64
  tier, document sub-family. See
48
- [`docs/packages-exports.md#authorization-model`](https://github.com/vLannaAi/noy-db/blob/main/docs/packages-exports.md#authorization-model).
65
+ [`docs/packages-exports.md#authorization-model`](https://github.com/noy-db/core/blob/main/docs/packages-exports.md#authorization-model).
49
66
 
50
67
  ## Install
51
68
 
@@ -66,6 +83,7 @@ isolation (the archive concatenates them anyway).
66
83
  ```ts
67
84
  await db.grant('firm', {
68
85
  userId: 'auditor',
86
+ displayName: 'Pranee',
69
87
  role: 'viewer',
70
88
  secret: '…',
71
89
  exportCapability: { plaintext: ['zip'] },
@@ -79,7 +97,9 @@ await db.grant('firm', {
79
97
  ```ts
80
98
  import { toBytes } from '@noy-db/as-zip'
81
99
 
82
- const bytes = await toBytes(vault, {
100
+ interface Invoice { id: string; status: string }
101
+
102
+ const bytes = await toBytes<Invoice>(vault, {
83
103
  records: {
84
104
  collection: 'invoices',
85
105
  filter: (r) => r.status === 'paid', // optional
@@ -130,7 +150,7 @@ Finder/Explorer without tooling.
130
150
 
131
151
  ### `manifest.json` shape
132
152
 
133
- ```ts
153
+ ```jsonc
134
154
  {
135
155
  _noydb_archive: 1,
136
156
  collection: 'invoices',
@@ -156,7 +176,7 @@ import { writeZip, type ZipEntry } from '@noy-db/as-zip'
156
176
 
157
177
  const bytes = writeZip([
158
178
  { path: 'hello.txt', bytes: new TextEncoder().encode('hi') },
159
- { path: 'blob.bin', bytes: someUint8Array },
179
+ { path: 'blob.bin', bytes: new Uint8Array([0xde, 0xad]) },
160
180
  ])
161
181
  ```
162
182
 
package/dist/index.d.ts CHANGED
@@ -219,13 +219,19 @@ declare class ZipCipherError extends Error {
219
219
  * body + attachments, invoice + scan + receipt) navigable in
220
220
  * Finder/Explorer without tooling.
221
221
  *
222
- * See [`docs/patterns/as-exports.md`](https://github.com/vLannaAi/noy-db/blob/main/docs/patterns/as-exports.md).
222
+ * See [`docs/patterns/as-exports.md`](https://github.com/noy-db/core/blob/main/docs/patterns/as-exports.md).
223
223
  *
224
224
  * @packageDocumentation
225
225
  */
226
226
 
227
- /** Record-selection options. */
228
- interface AsZipRecordsOptions {
227
+ /**
228
+ * Record-selection options.
229
+ *
230
+ * `T` is the shape of a decrypted record in this collection. It defaults to
231
+ * `unknown`, so every existing call keeps compiling unchanged; supply it (or
232
+ * annotate `filter`'s parameter) when you want the predicate typed.
233
+ */
234
+ interface AsZipRecordsOptions<T = unknown> {
229
235
  /** Collection to export. Must be in the caller's read ACL. */
230
236
  readonly collection: string;
231
237
  /**
@@ -233,7 +239,7 @@ interface AsZipRecordsOptions {
233
239
  * every record is included. Runs after decryption, before zip
234
240
  * assembly — doesn't reduce I/O, just the final set.
235
241
  */
236
- readonly filter?: (record: unknown) => boolean;
242
+ readonly filter?: (record: T) => boolean;
237
243
  }
238
244
  /** Blob-selection options. */
239
245
  interface AsZipAttachmentsOptions {
@@ -245,8 +251,8 @@ interface AsZipAttachmentsOptions {
245
251
  readonly slots?: readonly string[] | '*';
246
252
  }
247
253
  /** Top-level options for every entry point. */
248
- interface AsZipOptions {
249
- readonly records: AsZipRecordsOptions;
254
+ interface AsZipOptions<T = unknown> {
255
+ readonly records: AsZipRecordsOptions<T>;
250
256
  readonly attachments?: AsZipAttachmentsOptions;
251
257
  /**
252
258
  * Optional WinZip-AES-256 secret. When set, every entry
@@ -254,11 +260,17 @@ interface AsZipOptions {
254
260
  * encrypted with WinZip-AES-256 and the recipient must supply the
255
261
  * secret to extract.
256
262
  *
257
- * **Interop note:** the implementation is strictly to spec but has
258
- * not been validated against 7-Zip / Archive Utility / WinRAR in
259
- * this checkout. Round-trips against the package's own reader pass.
260
- * See https://github.com/vLannaAi/noy-db/issues/304 for the
261
- * cross-tool validation matrix.
263
+ * **Interop note:** validated against 7-Zip on Linux, macOS and
264
+ * Windows, and against `unar` on macOS, by a blocking CI job that
265
+ * `publish` depends on.
266
+ *
267
+ * ⚠️ Some distro-packaged `unar` builds CANNOT read these archives:
268
+ * measured on `ubuntu-latest`, 2026-09-05, CI run 33944815086,
269
+ * where every vector failed with "Missing or wrong password". The
270
+ * identical bytes open under macOS's `unar` and under 7-Zip on
271
+ * every platform, so the limit is in that build rather than in the
272
+ * archive — but a recipient reaching for their distro's `unar` may
273
+ * not get in.
262
274
  *
263
275
  * **Security framing:** this is the *interop* layer for
264
276
  * cross-platform handoff to archive tools, not the *encryption*
@@ -268,12 +280,12 @@ interface AsZipOptions {
268
280
  readonly password?: string;
269
281
  }
270
282
  /** Options for `download()` — adds an optional filename. */
271
- interface AsZipDownloadOptions extends AsZipOptions {
283
+ interface AsZipDownloadOptions<T = unknown> extends AsZipOptions<T> {
272
284
  /** Filename offered to the browser. Default `'<collection>.zip'`. */
273
285
  readonly filename?: string;
274
286
  }
275
287
  /** Options for `write()` — requires explicit risk acknowledgement. */
276
- interface AsZipWriteOptions extends AsZipOptions {
288
+ interface AsZipWriteOptions<T = unknown> extends AsZipOptions<T> {
277
289
  /**
278
290
  * Required for Node file-write calls — consumer acknowledgement
279
291
  * that plaintext bytes will persist on disk past the current
@@ -307,19 +319,19 @@ interface ArchiveManifest {
307
319
  * at the archive root so extractors can walk the content without
308
320
  * re-reading every file.
309
321
  */
310
- declare function toBytes(vault: Vault, options: AsZipOptions): Promise<Uint8Array>;
322
+ declare function toBytes<T = unknown>(vault: Vault, options: AsZipOptions<T>): Promise<Uint8Array>;
311
323
  /**
312
324
  * Browser download — wraps `toBytes()` in a `Blob` and triggers the
313
325
  * browser's download prompt. Requires `URL.createObjectURL` +
314
326
  * `document.createElement`. Throws in headless Node.
315
327
  */
316
- declare function download(vault: Vault, options: AsZipDownloadOptions): Promise<void>;
328
+ declare function download<T = unknown>(vault: Vault, options: AsZipDownloadOptions<T>): Promise<void>;
317
329
  /**
318
330
  * Node file-write — persists the archive to disk. Requires
319
331
  * explicit `acknowledgeRisks: true` (Tier 3 egress — the archive
320
332
  * contains plaintext records + blob bytes).
321
333
  */
322
- declare function write(vault: Vault, path: string, options: AsZipWriteOptions): Promise<void>;
334
+ declare function write<T = unknown>(vault: Vault, path: string, options: AsZipWriteOptions<T>): Promise<void>;
323
335
 
324
336
  interface AsZipImportOptions {
325
337
  /** Target collection for the records. */
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/vLannaAi/noy-db/blob/main/docs/patterns/as-exports.md).\n *\n * @packageDocumentation\n */\n\nimport type { Vault } from '@noy-db/hub'\nimport { writeZip, type ZipEntry } from './zip.js'\n\n// Re-export the low-level encoder so consumers who want to build\n// custom archives (non-noy-db payloads) can reuse it directly.\nexport { writeZip, crc32, type ZipEntry, type WriteZipOptions } from './zip.js'\n\n// Re-export the reader + cipher errors. ZipReadError is thrown on\n// format violations; ZipCipherError on wrong-password or tampered\n// ciphertext (the two are surfaced as the same class so callers can\n// catch one and not the other).\nexport { readZip, type ReadZipEntry, type ReadZipOptions, ZipReadError } from './read.js'\nexport { ZipCipherError } from './aes.js'\n\n/** Record-selection options. */\nexport interface AsZipRecordsOptions {\n /** Collection to export. Must be in the caller's read ACL. */\n readonly collection: string\n /**\n * Optional predicate against each decrypted record. When omitted,\n * every record is included. Runs after decryption, before zip\n * assembly — doesn't reduce I/O, just the final set.\n */\n readonly filter?: (record: unknown) => boolean\n}\n\n/** Blob-selection options. */\nexport interface AsZipAttachmentsOptions {\n /**\n * Which slots to include per record. `'*'` (default) includes\n * every slot on every record; a string[] selects specific slot\n * names. Pass an empty array to skip blob inclusion entirely.\n */\n readonly slots?: readonly string[] | '*'\n}\n\n/** Top-level options for every entry point. */\nexport interface AsZipOptions {\n readonly records: AsZipRecordsOptions\n readonly attachments?: AsZipAttachmentsOptions\n /**\n * Optional WinZip-AES-256 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:** the implementation is strictly to spec but has\n * not been validated against 7-Zip / Archive Utility / WinRAR in\n * this checkout. Round-trips against the package's own reader pass.\n * See https://github.com/vLannaAi/noy-db/issues/304 for the\n * cross-tool validation matrix.\n *\n * **Security framing:** this is the *interop* layer for\n * cross-platform handoff to archive tools, not the *encryption*\n * layer for sharing noy-db state. Use `as-noydb` for multi-recipient\n * + revocable + audited egress.\n */\n readonly password?: string\n}\n\n/** Options for `download()` — adds an optional filename. */\nexport interface AsZipDownloadOptions extends AsZipOptions {\n /** Filename offered to the browser. Default `'<collection>.zip'`. */\n readonly filename?: string\n}\n\n/** Options for `write()` — requires explicit risk acknowledgement. */\nexport interface AsZipWriteOptions extends AsZipOptions {\n /**\n * Required for Node file-write calls — consumer acknowledgement\n * that plaintext bytes will persist on disk past the current\n * process lifetime (Tier 3 risk).\n */\n readonly acknowledgeRisks: true\n}\n\n/** Manifest entry — one per record that landed in the archive. */\nexport interface ManifestRecord {\n readonly id: string\n readonly attachments: ReadonlyArray<{\n readonly slot: string\n readonly path: string\n readonly size: number\n readonly mimeType?: string\n }>\n}\n\n/** Archive-level manifest. Serialised as `manifest.json`. */\nexport interface ArchiveManifest {\n readonly _noydb_archive: 1\n readonly collection: string\n readonly exportedAt: string\n readonly recordCount: number\n readonly attachmentCount: number\n readonly records: readonly ManifestRecord[]\n}\n\n/**\n * Assemble the archive bytes. Pure beyond the auth check + store\n * reads. Records are written as `records.json`; blobs as\n * `attachments/<recordId>/<slot>`. A `manifest.json` index lands\n * at the archive root so extractors can walk the content without\n * re-reading every file.\n */\nexport async function toBytes(vault: Vault, options: AsZipOptions): Promise<Uint8Array> {\n vault.assertCanExport('plaintext', 'zip')\n\n const collectionName = options.records.collection\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const collection = vault.collection<any>(collectionName)\n\n // Pull every id the caller can see + the decrypted record for each.\n const ids = await collection.list().then((rs) => extractIds(rs))\n const records: Array<{ id: string; record: unknown }> = []\n for (const id of ids) {\n const r = await collection.get(id)\n if (r === null) continue\n if (options.records.filter && !options.records.filter(r)) continue\n records.push({ id, record: r })\n }\n\n // Gather attachments per record, filtered by slot selection.\n const slotsSelector = options.attachments?.slots ?? '*'\n const attachmentEntries: Array<{ path: string; bytes: Uint8Array; size: number; slot: string; mimeType?: string; recordId: string }> = []\n const includeAll = slotsSelector === '*'\n const includeSet = includeAll ? null : new Set<string>(slotsSelector)\n\n for (const { id } of records) {\n const blobSet = collection.blob(id)\n const slotsList = await blobSet.list()\n for (const slot of slotsList) {\n if (!includeAll && !includeSet!.has(slot.name)) continue\n const bytes = await blobSet.get(slot.name)\n if (!bytes) continue\n const safeId = sanitiseFsSegment(id)\n const safeSlot = sanitiseFsSegment(slot.name)\n const entry = {\n path: `attachments/${safeId}/${safeSlot}`,\n bytes,\n size: bytes.length,\n slot: slot.name,\n recordId: id,\n ...(slot.mimeType !== undefined && { mimeType: slot.mimeType }),\n }\n attachmentEntries.push(entry)\n }\n }\n\n // Manifest + records.json.\n const recordIndex: ManifestRecord[] = records.map(({ id }) => {\n const attach = attachmentEntries\n .filter((a) => a.recordId === id)\n .map(({ slot, path, size, mimeType }) => ({\n slot,\n path,\n size,\n ...(mimeType !== undefined && { mimeType }),\n }))\n return { id, attachments: attach }\n })\n const manifest: ArchiveManifest = {\n _noydb_archive: 1,\n collection: collectionName,\n exportedAt: new Date().toISOString(),\n recordCount: records.length,\n attachmentCount: attachmentEntries.length,\n records: recordIndex,\n }\n\n const encoder = new TextEncoder()\n const entries: ZipEntry[] = [\n { path: 'manifest.json', bytes: encoder.encode(JSON.stringify(manifest, null, 2)) },\n {\n path: 'records.json',\n bytes: encoder.encode(\n JSON.stringify(\n records.map((r) => {\n const pojo = toPojo(r.record)\n const base = pojo && typeof pojo === 'object' && !Array.isArray(pojo)\n ? (pojo as Record<string, unknown>)\n : { value: pojo }\n return { _id: r.id, ...base }\n }),\n null,\n 2,\n ),\n ),\n },\n ]\n for (const a of attachmentEntries) {\n entries.push({ path: a.path, bytes: a.bytes })\n }\n\n return writeZip(entries, options.password !== undefined ? { password: options.password } : {})\n}\n\n/**\n * Browser download — wraps `toBytes()` in a `Blob` and triggers the\n * browser's download prompt. Requires `URL.createObjectURL` +\n * `document.createElement`. Throws in headless Node.\n */\nexport async function download(vault: Vault, options: AsZipDownloadOptions): Promise<void> {\n const bytes = await toBytes(vault, options)\n const filename = options.filename ?? `${options.records.collection}.zip`\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const blob = new Blob([bytes as any], { type: 'application/zip' })\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = filename\n a.click()\n URL.revokeObjectURL(url)\n}\n\n/**\n * Node file-write — persists the archive to disk. Requires\n * explicit `acknowledgeRisks: true` (Tier 3 egress — the archive\n * contains plaintext records + blob bytes).\n */\nexport async function write(\n vault: Vault,\n path: string,\n options: AsZipWriteOptions,\n): Promise<void> {\n if (options.acknowledgeRisks !== true) {\n throw new Error(\n 'as-zip.write: acknowledgeRisks: true is required for on-disk plaintext output. ' +\n 'This call creates a persistent plaintext archive outside noy-db\\'s encrypted ' +\n 'storage — see docs/patterns/as-exports.md §\"The three tiers of \\\\\"plaintext out\\\\\"\"',\n )\n }\n const bytes = await toBytes(vault, options)\n const { writeFile } = await import('node:fs/promises')\n await writeFile(path, bytes)\n}\n\n// ── Import ─────────────────────────\n\nimport { diffVault } 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 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":[]}
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@noy-db/as-zip",
3
- "version": "0.7.0",
3
+ "version": "0.8.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
- "license": "MIT",
5
+ "license": "Apache-2.0",
6
6
  "author": "vLannaAi <vicio@lanna.ai>",
7
- "homepage": "https://github.com/vLannaAi/noy-db/tree/main/packages/as-zip#readme",
7
+ "homepage": "https://github.com/noy-db/as/tree/main/as-zip#readme",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "git+https://github.com/vLannaAi/noy-db.git",
11
- "directory": "packages/as-zip"
10
+ "url": "git+https://github.com/noy-db/as.git",
11
+ "directory": "as-zip"
12
12
  },
13
13
  "bugs": {
14
- "url": "https://github.com/vLannaAi/noy-db/issues"
14
+ "url": "https://github.com/noy-db/as/issues"
15
15
  },
16
16
  "type": "module",
17
17
  "sideEffects": false,
@@ -26,20 +26,21 @@
26
26
  "files": [
27
27
  "dist",
28
28
  "README.md",
29
- "LICENSE"
29
+ "LICENSE",
30
+ "NOTICE"
30
31
  ],
31
32
  "engines": {
32
33
  "node": ">=22.0.0"
33
34
  },
34
35
  "peerDependencies": {
35
- "@noy-db/hub": "^0.7.0"
36
+ "@noy-db/hub": "^0.7.0 || ^0.7.1-pre.0 || ^0.8.0-pre.0"
36
37
  },
37
38
  "devDependencies": {
39
+ "@noy-db/hub": "0.8.0",
40
+ "@noy-db/test-format-conformance": "0.8.0",
41
+ "@noy-db/to-memory": "0.8.0",
38
42
  "@types/node": "^22.0.0",
39
- "happy-dom": "^18.0.0",
40
- "@noy-db/test-format-conformance": "0.7.0",
41
- "@noy-db/to-memory": "0.7.0",
42
- "@noy-db/hub": "0.7.0"
43
+ "happy-dom": "^18.0.0"
43
44
  },
44
45
  "keywords": [
45
46
  "noy-db",