@noy-db/as-zip 0.1.0-pre.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +170 -0
- package/dist/index.cjs +648 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +349 -0
- package/dist/index.d.ts +349 -0
- package/dist/index.js +603 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { VaultDiff, Vault } from '@noy-db/hub';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal zero-dependency ZIP writer — STORE method only (no compression).
|
|
5
|
+
*
|
|
6
|
+
* Produces single-disk zip archives readable by `unzip`, macOS Finder,
|
|
7
|
+
* Windows Explorer, and every well-behaved unzipper. Store-method is
|
|
8
|
+
* sufficient here because:
|
|
9
|
+
*
|
|
10
|
+
* 1. Most blobs reaching this writer are already compressed — PDFs,
|
|
11
|
+
* PNGs, JPEGs, `.zip`-inside-zip, encrypted `.noydb` bundles.
|
|
12
|
+
* Re-deflating yields near-zero savings at non-trivial CPU cost.
|
|
13
|
+
* 2. STORE keeps the encoder compact (~150 lines) and dependency-free.
|
|
14
|
+
* Deflate would need a ~5KB gzip-style implementation or an
|
|
15
|
+
* external dep; we're zero-deps by design.
|
|
16
|
+
* 3. Consumers who need further compression can deflate the whole zip
|
|
17
|
+
* after it's produced (`gzip archive.zip`).
|
|
18
|
+
*
|
|
19
|
+
* ## Wire format used
|
|
20
|
+
*
|
|
21
|
+
* Per PKWARE APPNOTE (the canonical ZIP spec):
|
|
22
|
+
*
|
|
23
|
+
* ```
|
|
24
|
+
* [Local File Header 1][File 1 data]
|
|
25
|
+
* [Local File Header 2][File 2 data]
|
|
26
|
+
* ...
|
|
27
|
+
* [Central Directory File Header 1]
|
|
28
|
+
* [Central Directory File Header 2]
|
|
29
|
+
* ...
|
|
30
|
+
* [End of Central Directory Record]
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* All multi-byte integers are little-endian. Filenames are UTF-8
|
|
34
|
+
* (flag bit 11 set so unzippers interpret them correctly). The
|
|
35
|
+
* per-file CRC-32 uses the standard polynomial 0xEDB88320.
|
|
36
|
+
*
|
|
37
|
+
* ## What this does NOT support
|
|
38
|
+
*
|
|
39
|
+
* - Deflate / bzip2 / lzma / zstd compression methods.
|
|
40
|
+
* - Zip64 (files / archives > 4 GiB). 32-bit size fields apply.
|
|
41
|
+
* - Multi-disk / spanned archives.
|
|
42
|
+
* - Encryption (the ZIP-level kind — noy-db records are already
|
|
43
|
+
* encrypted at the envelope layer before reaching this writer).
|
|
44
|
+
* - Symlinks, extended attributes, NTFS/UNIX permissions.
|
|
45
|
+
*
|
|
46
|
+
* @module
|
|
47
|
+
*/
|
|
48
|
+
/** Input entry to `writeZip`. */
|
|
49
|
+
interface ZipEntry {
|
|
50
|
+
/** Slash-delimited path inside the archive (e.g. `"records.json"`, `"attachments/01H.../raw.pdf"`). */
|
|
51
|
+
readonly path: string;
|
|
52
|
+
/** Raw bytes. Pass plaintext as-is; the writer doesn't interpret content. */
|
|
53
|
+
readonly bytes: Uint8Array;
|
|
54
|
+
/** Optional file mtime. Defaults to the call-time `Date`. */
|
|
55
|
+
readonly mtime?: Date;
|
|
56
|
+
}
|
|
57
|
+
/** Optional archive-wide settings for `writeZip`. */
|
|
58
|
+
interface WriteZipOptions {
|
|
59
|
+
/**
|
|
60
|
+
* When set, every entry is encrypted with WinZip-AES-256 sealed
|
|
61
|
+
* under this passphrase. Recipients open with stock archive
|
|
62
|
+
* tooling (7-Zip, Archive Utility, WinRAR, modern unzip builds)
|
|
63
|
+
* by typing the same passphrase.
|
|
64
|
+
*
|
|
65
|
+
* Single passphrase across all entries by design — per-entry
|
|
66
|
+
* passwords don't fit a noy-db export workflow and would muddy
|
|
67
|
+
* the API.
|
|
68
|
+
*/
|
|
69
|
+
readonly password?: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Build a complete ZIP archive byte stream from `entries`. The result
|
|
73
|
+
* is the full archive including central directory and EOCD — ready
|
|
74
|
+
* to `fs.writeFile` or hand to `new Blob([bytes])`.
|
|
75
|
+
*
|
|
76
|
+
* When `options.password` is set, every entry is encrypted with
|
|
77
|
+
* WinZip-AES-256 (vendor version AE-2). Output remains a valid
|
|
78
|
+
* single-disk ZIP that archive tools recognising WinZip-AES prompt
|
|
79
|
+
* for the password on extract.
|
|
80
|
+
*/
|
|
81
|
+
declare function writeZip(entries: readonly ZipEntry[], options?: WriteZipOptions): Promise<Uint8Array>;
|
|
82
|
+
declare function crc32(bytes: Uint8Array): number;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Minimal zero-dependency ZIP reader — STORE method only, with
|
|
86
|
+
* optional WinZip-AES-256 decryption.
|
|
87
|
+
*
|
|
88
|
+
* Mirror of `./zip.ts`'s writer. Reads the central directory, walks
|
|
89
|
+
* each entry, decrypts when the AES marker (method 99) is set and a
|
|
90
|
+
* password was supplied. Same scope limits as the writer:
|
|
91
|
+
*
|
|
92
|
+
* - **No DEFLATE / bzip2 / lzma / zstd.** STORE-only. Trying to read
|
|
93
|
+
* a deflated entry throws `ZipReadError`.
|
|
94
|
+
* - **No Zip64.** 32-bit size + offset fields apply.
|
|
95
|
+
* - **AES-256 only.** AES-128/192 and ZipCrypto are refused — same
|
|
96
|
+
* stance as the writer.
|
|
97
|
+
*
|
|
98
|
+
* @module
|
|
99
|
+
*/
|
|
100
|
+
declare class ZipReadError extends Error {
|
|
101
|
+
readonly code = "ZIP_READ_ERROR";
|
|
102
|
+
constructor(message: string);
|
|
103
|
+
}
|
|
104
|
+
interface ReadZipEntry {
|
|
105
|
+
readonly path: string;
|
|
106
|
+
readonly bytes: Uint8Array;
|
|
107
|
+
/** True iff the entry was decrypted from a WinZip-AES region. */
|
|
108
|
+
readonly encrypted: boolean;
|
|
109
|
+
}
|
|
110
|
+
interface ReadZipOptions {
|
|
111
|
+
/** WinZip-AES-256 passphrase. Required for encrypted archives. */
|
|
112
|
+
readonly password?: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Parse a ZIP archive and return its entries. When the archive
|
|
116
|
+
* contains AES-encrypted entries (method 99 with the 0x9901 extra
|
|
117
|
+
* field), `options.password` MUST be supplied. Wrong password
|
|
118
|
+
* surfaces a `ZipCipherError` from the underlying decrypt step.
|
|
119
|
+
*/
|
|
120
|
+
declare function readZip(bytes: Uint8Array, options?: ReadZipOptions): Promise<ReadZipEntry[]>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* WinZip-AES encryption for ZIP entries — write + read.
|
|
124
|
+
*
|
|
125
|
+
* Implements the AES-256 variant of the WinZip-AES extension as
|
|
126
|
+
* documented in
|
|
127
|
+
* https://www.winzip.com/win/en/aes_info.html and PKWARE Appendix E.
|
|
128
|
+
*
|
|
129
|
+
* **AES-256 only by design.** ZipCrypto and AES-128/192 are refused at
|
|
130
|
+
* both write and read time — the security story for the package is
|
|
131
|
+
* "if you need encryption, use AES-256." Adding weaker tiers would
|
|
132
|
+
* give consumers the impression of meaningful choice.
|
|
133
|
+
*
|
|
134
|
+
* ## Layout of an encrypted entry's data region
|
|
135
|
+
*
|
|
136
|
+
* ```
|
|
137
|
+
* ┌──────────┬──────────┬─────────────┬──────────┐
|
|
138
|
+
* │ salt(16) │ verif(2) │ ciphertext │ auth(10) │
|
|
139
|
+
* └──────────┴──────────┴─────────────┴──────────┘
|
|
140
|
+
* ```
|
|
141
|
+
*
|
|
142
|
+
* - **salt** — 16 random bytes per entry
|
|
143
|
+
* - **verifier** — 2 bytes, last 2 bytes of the PBKDF2 output;
|
|
144
|
+
* used to fail-fast on a wrong password without running CTR
|
|
145
|
+
* - **ciphertext** — input bytes XOR-ed with AES-CTR keystream
|
|
146
|
+
* - **auth** — first 10 bytes of HMAC-SHA1(ciphertext, authKey),
|
|
147
|
+
* where authKey is the second half of the PBKDF2 output
|
|
148
|
+
*
|
|
149
|
+
* ## Local file header changes
|
|
150
|
+
*
|
|
151
|
+
* For an encrypted entry:
|
|
152
|
+
*
|
|
153
|
+
* - `compression method` field is forced to **99** (AES marker)
|
|
154
|
+
* - flag **bit 0** is set (encryption)
|
|
155
|
+
* - extra field tag **0x9901** is appended with 7 bytes:
|
|
156
|
+
* - `2 bytes` vendor version → `0x0002` (AE-2)
|
|
157
|
+
* - `2 bytes` vendor ID → `'AE'` (`0x4541` LE)
|
|
158
|
+
* - `1 byte` AES strength → `0x03` (AES-256)
|
|
159
|
+
* - `2 bytes` real method → `0x0000` (STORE)
|
|
160
|
+
* - For AE-2, `crc` is forced to 0 (the spec recommends this — the
|
|
161
|
+
* authentication code already covers integrity)
|
|
162
|
+
*
|
|
163
|
+
* ## Counter convention (subtle!)
|
|
164
|
+
*
|
|
165
|
+
* AES-CTR per WinZip uses a **little-endian** 16-byte counter. The
|
|
166
|
+
* counter starts at **1** (not 0) and increments by 1 per 16-byte
|
|
167
|
+
* block. The lower 8 bytes hold the counter; the upper 8 bytes are 0.
|
|
168
|
+
*
|
|
169
|
+
* @module
|
|
170
|
+
*/
|
|
171
|
+
|
|
172
|
+
declare class ZipCipherError extends Error {
|
|
173
|
+
readonly code = "ZIP_CIPHER_ERROR";
|
|
174
|
+
constructor(message: string);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* **@noy-db/as-zip** — composite record + blob archive for noy-db.
|
|
179
|
+
*
|
|
180
|
+
* Bundles a collection's records AND every record's attached blobs
|
|
181
|
+
* into a single zip archive. The canonical "download this audit
|
|
182
|
+
* trail" / "migrate this case folder" primitive. Part of the
|
|
183
|
+
* `@noy-db/as-*` portable-artefact family — plaintext tier,
|
|
184
|
+
* document sub-family.
|
|
185
|
+
*
|
|
186
|
+
* ## Authorisation
|
|
187
|
+
*
|
|
188
|
+
* One capability check: `assertCanExport('plaintext', 'zip')`.
|
|
189
|
+
* A composite archive is semantically the `'zip'` format from the
|
|
190
|
+
* auth model's POV — requiring separate `'json'`, `'csv'`, `'blob'`
|
|
191
|
+
* grants for a single call would fragment the grant surface without
|
|
192
|
+
* adding any real isolation (the archive concatenates them anyway).
|
|
193
|
+
*
|
|
194
|
+
* The owner grants the composite capability explicitly:
|
|
195
|
+
*
|
|
196
|
+
* ```ts
|
|
197
|
+
* await db.grant('firm', {
|
|
198
|
+
* userId: 'auditor',
|
|
199
|
+
* role: 'viewer',
|
|
200
|
+
* passphrase: '…',
|
|
201
|
+
* exportCapability: { plaintext: ['zip'] },
|
|
202
|
+
* })
|
|
203
|
+
* ```
|
|
204
|
+
*
|
|
205
|
+
* ## Archive layout
|
|
206
|
+
*
|
|
207
|
+
* ```
|
|
208
|
+
* archive.zip
|
|
209
|
+
* ├── manifest.json # index + provenance (record count, blob count, exported-at, exported-by)
|
|
210
|
+
* ├── records.json # array of decrypted records
|
|
211
|
+
* └── attachments/
|
|
212
|
+
* ├── <recordId>/<slot> # raw blob bytes, MIME-native
|
|
213
|
+
* └── ...
|
|
214
|
+
* ```
|
|
215
|
+
*
|
|
216
|
+
* The folder-per-record layout makes composite entities (email +
|
|
217
|
+
* body + attachments, invoice + scan + receipt) navigable in
|
|
218
|
+
* Finder/Explorer without tooling.
|
|
219
|
+
*
|
|
220
|
+
* See [`docs/patterns/as-exports.md`](https://github.com/vLannaAi/noy-db/blob/main/docs/patterns/as-exports.md).
|
|
221
|
+
*
|
|
222
|
+
* @packageDocumentation
|
|
223
|
+
*/
|
|
224
|
+
|
|
225
|
+
/** Record-selection options. */
|
|
226
|
+
interface AsZipRecordsOptions {
|
|
227
|
+
/** Collection to export. Must be in the caller's read ACL. */
|
|
228
|
+
readonly collection: string;
|
|
229
|
+
/**
|
|
230
|
+
* Optional predicate against each decrypted record. When omitted,
|
|
231
|
+
* every record is included. Runs after decryption, before zip
|
|
232
|
+
* assembly — doesn't reduce I/O, just the final set.
|
|
233
|
+
*/
|
|
234
|
+
readonly filter?: (record: unknown) => boolean;
|
|
235
|
+
}
|
|
236
|
+
/** Blob-selection options. */
|
|
237
|
+
interface AsZipAttachmentsOptions {
|
|
238
|
+
/**
|
|
239
|
+
* Which slots to include per record. `'*'` (default) includes
|
|
240
|
+
* every slot on every record; a string[] selects specific slot
|
|
241
|
+
* names. Pass an empty array to skip blob inclusion entirely.
|
|
242
|
+
*/
|
|
243
|
+
readonly slots?: readonly string[] | '*';
|
|
244
|
+
}
|
|
245
|
+
/** Top-level options for every entry point. */
|
|
246
|
+
interface AsZipOptions {
|
|
247
|
+
readonly records: AsZipRecordsOptions;
|
|
248
|
+
readonly attachments?: AsZipAttachmentsOptions;
|
|
249
|
+
/**
|
|
250
|
+
* Optional WinZip-AES-256 passphrase. When set, every entry
|
|
251
|
+
* inside the archive (records + attachments + manifest) is
|
|
252
|
+
* encrypted with WinZip-AES-256 and the recipient must supply the
|
|
253
|
+
* passphrase to extract.
|
|
254
|
+
*
|
|
255
|
+
* **Interop note:** the implementation is strictly to spec but has
|
|
256
|
+
* not been validated against 7-Zip / Archive Utility / WinRAR in
|
|
257
|
+
* this checkout. Round-trips against the package's own reader pass.
|
|
258
|
+
* See https://github.com/vLannaAi/noy-db/issues/304 for the
|
|
259
|
+
* cross-tool validation matrix.
|
|
260
|
+
*
|
|
261
|
+
* **Security framing:** this is the *interop* layer for
|
|
262
|
+
* cross-platform handoff to archive tools, not the *encryption*
|
|
263
|
+
* layer for sharing noy-db state. Use `as-noydb` for multi-recipient
|
|
264
|
+
* + revocable + audited egress.
|
|
265
|
+
*/
|
|
266
|
+
readonly password?: string;
|
|
267
|
+
}
|
|
268
|
+
/** Options for `download()` — adds an optional filename. */
|
|
269
|
+
interface AsZipDownloadOptions extends AsZipOptions {
|
|
270
|
+
/** Filename offered to the browser. Default `'<collection>.zip'`. */
|
|
271
|
+
readonly filename?: string;
|
|
272
|
+
}
|
|
273
|
+
/** Options for `write()` — requires explicit risk acknowledgement. */
|
|
274
|
+
interface AsZipWriteOptions extends AsZipOptions {
|
|
275
|
+
/**
|
|
276
|
+
* Required for Node file-write calls — consumer acknowledgement
|
|
277
|
+
* that plaintext bytes will persist on disk past the current
|
|
278
|
+
* process lifetime (Tier 3 risk).
|
|
279
|
+
*/
|
|
280
|
+
readonly acknowledgeRisks: true;
|
|
281
|
+
}
|
|
282
|
+
/** Manifest entry — one per record that landed in the archive. */
|
|
283
|
+
interface ManifestRecord {
|
|
284
|
+
readonly id: string;
|
|
285
|
+
readonly attachments: ReadonlyArray<{
|
|
286
|
+
readonly slot: string;
|
|
287
|
+
readonly path: string;
|
|
288
|
+
readonly size: number;
|
|
289
|
+
readonly mimeType?: string;
|
|
290
|
+
}>;
|
|
291
|
+
}
|
|
292
|
+
/** Archive-level manifest. Serialised as `manifest.json`. */
|
|
293
|
+
interface ArchiveManifest {
|
|
294
|
+
readonly _noydb_archive: 1;
|
|
295
|
+
readonly collection: string;
|
|
296
|
+
readonly exportedAt: string;
|
|
297
|
+
readonly recordCount: number;
|
|
298
|
+
readonly attachmentCount: number;
|
|
299
|
+
readonly records: readonly ManifestRecord[];
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Assemble the archive bytes. Pure beyond the auth check + store
|
|
303
|
+
* reads. Records are written as `records.json`; blobs as
|
|
304
|
+
* `attachments/<recordId>/<slot>`. A `manifest.json` index lands
|
|
305
|
+
* at the archive root so extractors can walk the content without
|
|
306
|
+
* re-reading every file.
|
|
307
|
+
*/
|
|
308
|
+
declare function toBytes(vault: Vault, options: AsZipOptions): Promise<Uint8Array>;
|
|
309
|
+
/**
|
|
310
|
+
* Browser download — wraps `toBytes()` in a `Blob` and triggers the
|
|
311
|
+
* browser's download prompt. Requires `URL.createObjectURL` +
|
|
312
|
+
* `document.createElement`. Throws in headless Node.
|
|
313
|
+
*/
|
|
314
|
+
declare function download(vault: Vault, options: AsZipDownloadOptions): Promise<void>;
|
|
315
|
+
/**
|
|
316
|
+
* Node file-write — persists the archive to disk. Requires
|
|
317
|
+
* explicit `acknowledgeRisks: true` (Tier 3 egress — the archive
|
|
318
|
+
* contains plaintext records + blob bytes).
|
|
319
|
+
*/
|
|
320
|
+
declare function write(vault: Vault, path: string, options: AsZipWriteOptions): Promise<void>;
|
|
321
|
+
|
|
322
|
+
type ImportPolicy = 'merge' | 'replace' | 'insert-only';
|
|
323
|
+
interface AsZipImportOptions {
|
|
324
|
+
/** Target collection for the records. */
|
|
325
|
+
readonly collection: string;
|
|
326
|
+
/** Field on each record that carries its id. Default `'id'`. */
|
|
327
|
+
readonly idKey?: string;
|
|
328
|
+
/** Reconciliation policy. Default `'merge'`. */
|
|
329
|
+
readonly policy?: ImportPolicy;
|
|
330
|
+
/** WinZip-AES-256 passphrase if the archive is encrypted. */
|
|
331
|
+
readonly password?: string;
|
|
332
|
+
}
|
|
333
|
+
interface AsZipImportPlan {
|
|
334
|
+
readonly plan: VaultDiff;
|
|
335
|
+
readonly policy: ImportPolicy;
|
|
336
|
+
apply(): Promise<void>;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Read a `.zip` archive (optionally WinZip-AES-256 encrypted), parse
|
|
340
|
+
* `records.json` from it, and return an `ImportPlan` whose `apply()`
|
|
341
|
+
* writes the changes through the normal collection API.
|
|
342
|
+
*
|
|
343
|
+
* Pairs with `toBytes` for round-trip workflows. The records JSON
|
|
344
|
+
* format matches what `toBytes` writes — array of records under
|
|
345
|
+
* the configured collection's id key.
|
|
346
|
+
*/
|
|
347
|
+
declare function fromBytes(vault: Vault, bytes: Uint8Array, options: AsZipImportOptions): Promise<AsZipImportPlan>;
|
|
348
|
+
|
|
349
|
+
export { type ArchiveManifest, type AsZipAttachmentsOptions, type AsZipDownloadOptions, type AsZipImportOptions, type AsZipImportPlan, type AsZipOptions, type AsZipRecordsOptions, type AsZipWriteOptions, type ImportPolicy, type ManifestRecord, type ReadZipEntry, type ReadZipOptions, type WriteZipOptions, ZipCipherError, type ZipEntry, ZipReadError, crc32, download, fromBytes, readZip, toBytes, write, writeZip };
|