dir-archiver 2.2.0 → 3.0.1

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.
@@ -0,0 +1,32 @@
1
+ # How-to: create a CI release artifact
2
+
3
+ ## Goal
4
+ Produce a release ZIP in CI and emit a machine-readable JSON summary.
5
+
6
+ ## Prereqs
7
+ - Node `>=24`
8
+ - `npm install`
9
+ - `npm run build`
10
+
11
+ ## Copy/paste
12
+ Minimal CI shell snippet:
13
+
14
+ ```sh
15
+ node examples/ci-release-artifact.mjs
16
+ ```
17
+
18
+ Equivalent CLI flow:
19
+
20
+ ```sh
21
+ dir-archiver write --source ./dist --output ./release.zip --include-base-directory --json
22
+ dir-archiver detect --input ./release.zip --json
23
+ ```
24
+
25
+ ## What you should see
26
+ - JSON output containing `artifact`, `format`, and `entryCount`.
27
+ - `format` is `zip` when the destination extension is `.zip`.
28
+
29
+ ## Safety notes
30
+ > [!NOTE]
31
+ > Keep `--json` enabled in CI so build steps can parse deterministic fields
32
+ > instead of scraping human-readable text.
@@ -0,0 +1,34 @@
1
+ # How-to: use dir-archiver in CI pipelines
2
+
3
+ ## Goal
4
+ Normalize incoming archives and gate releases with deterministic audit results.
5
+
6
+ ## Prereqs
7
+ - `dir-archiver` available in CI
8
+ - Input archive path from build pipeline
9
+
10
+ ## Copy/paste
11
+ Normalize:
12
+
13
+ ```sh
14
+ dir-archiver normalize \
15
+ --input ./incoming.zip \
16
+ --output ./normalized.zip \
17
+ --profile strict \
18
+ --json
19
+ ```
20
+
21
+ Audit gate:
22
+
23
+ ```sh
24
+ dir-archiver audit --input ./incoming.zip --profile agent --json
25
+ ```
26
+
27
+ ## What you should see
28
+ - Normalize emits JSON report with deterministic summary fields.
29
+ - Audit exits with code `0` when safe and `1` when operational risk is detected.
30
+
31
+ ## Safety notes
32
+ > [!NOTE]
33
+ > Exit code `2` indicates CLI usage mistakes (missing/invalid flags), not
34
+ > archive safety problems.
@@ -0,0 +1,57 @@
1
+ # How-to: extract untrusted archives safely
2
+
3
+ ## Goal
4
+ Prevent path traversal and decompression amplification from turning extraction
5
+ into a filesystem or resource-exhaustion risk.
6
+
7
+ ## Prereqs
8
+ - Node `>=24`
9
+ - `npm install`
10
+ - `npm run build`
11
+
12
+ ## Copy/paste
13
+ Recommended pattern (audit first, then extract with limits):
14
+
15
+ ```ts
16
+ import { audit, extract } from "dir-archiver";
17
+
18
+ const input = "./incoming.zip";
19
+ const report = await audit(input, { profile: "agent" });
20
+ if (!report.ok) {
21
+ console.error(JSON.stringify({ ok: false, issues: report.issues }, null, 2));
22
+ process.exit(1);
23
+ }
24
+
25
+ await extract(input, "./out", {
26
+ profile: "strict",
27
+ maxEntryBytes: 64 * 1024 * 1024,
28
+ maxTotalExtractedBytes: 512 * 1024 * 1024,
29
+ });
30
+ ```
31
+
32
+ Runnable example file:
33
+
34
+ ```sh
35
+ node examples/extract-untrusted.mjs
36
+ ```
37
+
38
+ ## What you should see
39
+ - The audit step succeeds before extraction starts.
40
+ - The example intentionally sets a low extraction limit and reports
41
+ `DIRARCHIVER_RESOURCE_LIMIT`.
42
+
43
+ ## Safety notes
44
+ > [!CAUTION]
45
+ > Never extract untrusted archives without limits. Attackers can use deeply
46
+ > nested or highly compressed entries to trigger large disk writes
47
+ > (`CWE-409`-style decompression amplification).
48
+ >
49
+ > [!CAUTION]
50
+ > Keep `profile: "strict"` or `"agent"` for untrusted input. These profiles
51
+ > reject traversal-style paths and unsafe entry classes during extraction.
52
+ >
53
+ > [!WARNING]
54
+ > Symlink and hardlink handling changes the risk envelope:
55
+ > - `allowSymlinks` defaults to `false`.
56
+ > - `allowHardlinks` currently remains unsupported and triggers
57
+ > `DIRARCHIVER_UNSUPPORTED_ENTRY`.
@@ -0,0 +1,7 @@
1
+ # How-to index
2
+
3
+ Task guides:
4
+
5
+ - [Create CI release artifacts](ci-release-artifact.md)
6
+ - [Extract untrusted archives safely](extract-untrusted.md)
7
+ - [Use in CI pipelines](ci-usage.md)
package/docs/index.md ADDED
@@ -0,0 +1,24 @@
1
+ # Documentation
2
+
3
+ Use this map to pick the right doc quickly.
4
+
5
+ ## Start here
6
+ - [Tutorial: bundle a plugin directory](tutorial/bundle-a-plugin.md)
7
+ - [Tutorial: first archive flow](tutorial/first-archive-flow.md)
8
+
9
+ ## How-to guides
10
+ - [How-to index](how-to/index.md)
11
+ - [Create CI release artifacts](how-to/ci-release-artifact.md)
12
+ - [Extract untrusted archives safely](how-to/extract-untrusted.md)
13
+ - [Use in CI pipelines](how-to/ci-usage.md)
14
+
15
+ ## Reference
16
+ - [Contract](../CONTRACT.md)
17
+ - [CLI reference](reference/cli.md)
18
+ - [Options reference](reference/options.md)
19
+ - [Reference index](reference/index.md)
20
+ - [Security policy](../SECURITY.md)
21
+
22
+ ## Explanation
23
+ - [Explanation index](explanation/index.md)
24
+ - [Profile behavior and tradeoffs](explanation/profiles.md)
@@ -0,0 +1,277 @@
1
+ # CLI reference
2
+
3
+ ## Goal
4
+ Provide one canonical reference for `dir-archiver` CLI commands, flags, JSON
5
+ outputs, and exit-code behavior.
6
+
7
+ ## Prereqs
8
+ - Build CLI once: `npm run build`
9
+ - Binary path in this repo: `dist/cli.js`
10
+
11
+ ## Copy/paste
12
+ Show usage:
13
+
14
+ ```sh
15
+ node dist/cli.js
16
+ ```
17
+
18
+ Run examples:
19
+
20
+ ```sh
21
+ node examples/run-all.mjs
22
+ ```
23
+
24
+ ## What you should see
25
+ - Exit code `2` with usage diagnostics for invalid invocations.
26
+ - Exit code `0` for successful commands.
27
+ - JSON payloads on stdout when `--json` is set.
28
+
29
+ ## Safety notes
30
+ > [!NOTE]
31
+ > For automation, always pass `--json` and consume stable codes/fields instead
32
+ > of parsing human-readable output.
33
+
34
+ ## Common flags
35
+
36
+ | Flag | Alias | Default | Notes |
37
+ | --- | --- | --- | --- |
38
+ | `--source` | `--src` | command-specific | Input directory/file for `write`. |
39
+ | `--input` | `-i` | command-specific | Archive path for `open`, `detect`, `list`, `audit`, `extract`, `normalize`. |
40
+ | `--output` | `--dest`, `-o` | command-specific | Destination path for `write`, `extract`, `normalize`. |
41
+ | `--format` | - | inferred/auto | Values: `zip`, `tar`, `tgz`, `tar.gz`, `gz`, `bz2`, `tar.bz2`, `zst`, `tar.zst`, `br`, `tar.br`, `xz`, `tar.xz`. |
42
+ | `--profile` | - | `strict` | Values: `compat`, `strict`, `agent`. |
43
+ | `--json` | - | `false` | Emit machine-readable JSON on stdout. |
44
+ | `--include-base-directory` | `--includebasedir` | `false` | Wrap archive content under the source directory name in `write`. |
45
+ | `--follow-symlinks` | `--followsymlinks` | `false` | Follow symlink targets when archiving source input. |
46
+ | `--exclude` | - | `[]` | Exclude path globs in `write`. Repeatable. |
47
+ | `--allow-symlinks` | - | `false` | Permit symlink extraction targets. |
48
+ | `--allow-hardlinks` | - | `false` | Permit hardlink extraction targets. |
49
+ | `--max-entry-bytes` | - | unset | Maximum bytes per extracted entry. |
50
+ | `--max-total-extracted-bytes` | - | unset | Maximum aggregate extracted bytes per run. |
51
+
52
+ ## Commands
53
+
54
+ ### `write`
55
+
56
+ Synopsis:
57
+
58
+ ```sh
59
+ dir-archiver write --source <path> --src <path> --output <archive> --dest <archive> [--format <format>] [--include-base-directory|--includebasedir] [--follow-symlinks|--followsymlinks] [--exclude <path>...]
60
+ ```
61
+
62
+ Example:
63
+
64
+ ```sh
65
+ dir-archiver write --source ./plugin --output ./bundle.zip --include-base-directory --exclude .git --json
66
+ ```
67
+
68
+ JSON output shape:
69
+
70
+ ```json
71
+ {
72
+ "format": "zip",
73
+ "source": "./plugin",
74
+ "destination": "./bundle.zip",
75
+ "entryCount": 3,
76
+ "wrappedDirectoryCodec": false
77
+ }
78
+ ```
79
+
80
+ ### `open`
81
+
82
+ Synopsis:
83
+
84
+ ```sh
85
+ dir-archiver open --input <archive> [--format <format>] [--profile <profile>] [--json]
86
+ ```
87
+
88
+ Example:
89
+
90
+ ```sh
91
+ dir-archiver open --input ./bundle.zip --json
92
+ ```
93
+
94
+ JSON output shape:
95
+
96
+ ```json
97
+ {
98
+ "format": "zip",
99
+ "detection": {
100
+ "schemaVersion": "1",
101
+ "inputKind": "file",
102
+ "detected": {
103
+ "container": "zip",
104
+ "compression": "none",
105
+ "layers": ["zip"]
106
+ },
107
+ "confidence": "high",
108
+ "notes": ["Format inferred from magic bytes"]
109
+ }
110
+ }
111
+ ```
112
+
113
+ ### `detect`
114
+
115
+ Synopsis:
116
+
117
+ ```sh
118
+ dir-archiver detect --input <archive> [--json]
119
+ ```
120
+
121
+ Example:
122
+
123
+ ```sh
124
+ dir-archiver detect --input ./bundle.zip --json
125
+ ```
126
+
127
+ JSON output shape:
128
+
129
+ ```json
130
+ {
131
+ "format": "zip",
132
+ "detection": {
133
+ "schemaVersion": "1",
134
+ "inputKind": "file",
135
+ "detected": {
136
+ "container": "zip",
137
+ "compression": "none",
138
+ "layers": ["zip"]
139
+ },
140
+ "confidence": "high",
141
+ "notes": ["Format inferred from magic bytes"]
142
+ }
143
+ }
144
+ ```
145
+
146
+ ### `list`
147
+
148
+ Synopsis:
149
+
150
+ ```sh
151
+ dir-archiver list --input <archive> [--json]
152
+ ```
153
+
154
+ Example:
155
+
156
+ ```sh
157
+ dir-archiver list --input ./bundle.zip --json
158
+ ```
159
+
160
+ JSON output shape:
161
+
162
+ ```json
163
+ {
164
+ "format": "zip",
165
+ "detection": { "...": "same shape as detect/open" },
166
+ "entries": [
167
+ {
168
+ "format": "zip",
169
+ "name": "plugin/index.js",
170
+ "size": "42",
171
+ "isDirectory": false,
172
+ "isSymlink": false
173
+ }
174
+ ]
175
+ }
176
+ ```
177
+
178
+ ### `audit`
179
+
180
+ Synopsis:
181
+
182
+ ```sh
183
+ dir-archiver audit --input <archive> [--profile <profile>] [--json]
184
+ ```
185
+
186
+ Example:
187
+
188
+ ```sh
189
+ dir-archiver audit --input ./bundle.zip --profile agent --json
190
+ ```
191
+
192
+ JSON output shape:
193
+
194
+ ```json
195
+ {
196
+ "schemaVersion": "1",
197
+ "ok": true,
198
+ "summary": {
199
+ "entries": 3,
200
+ "warnings": 0,
201
+ "errors": 0
202
+ },
203
+ "issues": []
204
+ }
205
+ ```
206
+
207
+ ### `extract`
208
+
209
+ Synopsis:
210
+
211
+ ```sh
212
+ dir-archiver extract --input <archive> --output <directory> [--profile <profile>] [--allow-symlinks] [--allow-hardlinks] [--max-entry-bytes <n>] [--max-total-extracted-bytes <n>] [--json]
213
+ ```
214
+
215
+ Example:
216
+
217
+ ```sh
218
+ dir-archiver extract --input ./bundle.zip --output ./out --profile strict --max-total-extracted-bytes 536870912 --json
219
+ ```
220
+
221
+ JSON output shape:
222
+
223
+ ```json
224
+ {
225
+ "format": "zip",
226
+ "destination": "./out",
227
+ "extractedFiles": 2,
228
+ "extractedDirectories": 1,
229
+ "skippedEntries": 0,
230
+ "issues": []
231
+ }
232
+ ```
233
+
234
+ ### `normalize`
235
+
236
+ Synopsis:
237
+
238
+ ```sh
239
+ dir-archiver normalize --input <archive> --output <archive> [--profile <profile>] [--json]
240
+ ```
241
+
242
+ Example:
243
+
244
+ ```sh
245
+ dir-archiver normalize --input ./incoming.zip --output ./normalized.zip --profile strict --json
246
+ ```
247
+
248
+ JSON output shape:
249
+
250
+ ```json
251
+ {
252
+ "format": "zip",
253
+ "report": {
254
+ "schemaVersion": "1",
255
+ "ok": true,
256
+ "summary": {
257
+ "entries": 10,
258
+ "outputEntries": 10,
259
+ "droppedEntries": 0,
260
+ "renamedEntries": 0,
261
+ "warnings": 0,
262
+ "errors": 0
263
+ },
264
+ "issues": []
265
+ }
266
+ }
267
+ ```
268
+
269
+ ## Exit codes and stderr/stdout contract
270
+
271
+ - Exit `0`: success.
272
+ - Exit `1`: operational failure (typed `DirArchiverError` JSON on stderr).
273
+ - Exit `2`: usage/validation failure (`DIRARCHIVER_USAGE` with `issues`).
274
+
275
+ For machine consumers:
276
+ - stdout is reserved for success payloads (especially with `--json`).
277
+ - stderr is reserved for diagnostics/failures.
@@ -0,0 +1,7 @@
1
+ # Reference index
2
+
3
+ Use this page as the canonical reference entrypoint.
4
+
5
+ - [CLI reference](cli.md)
6
+ - [Options reference](options.md)
7
+ - [Contract](../../CONTRACT.md)
@@ -0,0 +1,57 @@
1
+ # Options reference
2
+
3
+ This page is the single reference for public option fields in `dir-archiver`.
4
+ For CLI commands and flags, see [CLI reference](cli.md).
5
+
6
+ ## `OpenOptions`
7
+
8
+ Used by `open`, `detect`, `list`, and `audit`.
9
+
10
+ | Field | Type | Default | Notes |
11
+ | --- | --- | --- | --- |
12
+ | `format` | `ArchiveFormat` | auto-detect | Force a format when detection by bytes/filename is ambiguous. |
13
+ | `profile` | `compat \| strict \| agent` | bytefold default | Safety profile passed to archive readers/audit. |
14
+ | `isStrict` | `boolean` | profile-driven | Explicit strict-mode override. |
15
+ | `limits` | `ArchiveLimits` | none | Resource ceilings for parse/audit operations. |
16
+ | `signal` | `AbortSignal` | none | Cancels long-running operations. |
17
+ | `password` | `string` | none | Password for encrypted archives where supported. |
18
+ | `filename` | `string` | none | Filename hint for extension-based detection. |
19
+
20
+ ## `ExtractOptions`
21
+
22
+ `ExtractOptions` extends `OpenOptions`.
23
+
24
+ | Field | Type | Default | Notes |
25
+ | --- | --- | --- | --- |
26
+ | `profile` | `compat \| strict \| agent` | `strict` | `extract` defaults to strict safety posture. |
27
+ | `allowSymlinks` | `boolean` | `false` | When `false`, symlink entries are skipped. |
28
+ | `allowHardlinks` | `boolean` | `false` | Hard-link entries are currently rejected with `DIRARCHIVER_UNSUPPORTED_ENTRY`. |
29
+ | `maxEntryBytes` | `number` | none | Maximum bytes for one extracted file entry. |
30
+ | `maxTotalExtractedBytes` | `number` | none | Maximum cumulative bytes written during extraction. |
31
+
32
+ ## `WriteOptions`
33
+
34
+ | Field | Type | Default | Notes |
35
+ | --- | --- | --- | --- |
36
+ | `format` | `ArchiveFormat` | inferred from destination extension, fallback `zip` | Also wraps directory + single-file codec requests (`gz` -> `tar.gz`, etc.). |
37
+ | `includeBaseDirectory` | `boolean` | `false` | Include source directory name as archive root path prefix. |
38
+ | `followSymlinks` | `boolean` | `false` | Follow symlinks while traversing source directories. |
39
+ | `exclude` | `string[]` | `[]` | Exact basename or relative-path matches to skip while traversing the source root (not glob patterns). |
40
+ | `profile` | `ArchiveProfile` | none | Present in API type; currently reserved and not forwarded by `write()`. |
41
+ | `limits` | `ArchiveLimits` | none | Present in API type; currently reserved and not forwarded by `write()`. |
42
+
43
+ ## Examples
44
+
45
+ ```ts
46
+ import { extract, write } from "dir-archiver";
47
+
48
+ await extract("./archive.zip", "./out", {
49
+ profile: "strict",
50
+ maxTotalExtractedBytes: 512 * 1024 * 1024,
51
+ });
52
+
53
+ await write("./project", "./project.zip", {
54
+ includeBaseDirectory: true,
55
+ exclude: ["tmp", "nested/skip.txt"],
56
+ });
57
+ ```
@@ -0,0 +1,42 @@
1
+ # Tutorial: bundle a plugin directory
2
+
3
+ ## Goal
4
+ Create a distributable ZIP that keeps a stable root folder and excludes local
5
+ development files.
6
+
7
+ ## Prereqs
8
+ - Node `>=24`
9
+ - `npm install`
10
+ - `npm run build`
11
+
12
+ ## Copy/paste
13
+ CLI-style command:
14
+
15
+ ```sh
16
+ dir-archiver write \
17
+ --includebasedir \
18
+ --src . \
19
+ --dest ../bundle.zip \
20
+ --exclude .git \
21
+ --exclude node_modules \
22
+ --exclude package-lock.json \
23
+ --exclude package.json \
24
+ --json
25
+ ```
26
+
27
+ Runnable example file:
28
+
29
+ ```sh
30
+ node examples/bundle-a-plugin.mjs
31
+ ```
32
+
33
+ ## What you should see
34
+ - A ZIP file is created.
35
+ - JSON output reports `format: "zip"`.
36
+ - Excluded paths (`.git`, `node_modules`, lock/package manifests) are omitted.
37
+
38
+ ## Safety notes
39
+ > [!NOTE]
40
+ > `--includebasedir` preserves one top-level folder in the archive. This keeps
41
+ > extraction deterministic and prevents files from scattering into whichever
42
+ > directory the user extracts into.
@@ -0,0 +1,33 @@
1
+ # Tutorial: first archive flow
2
+
3
+ ## Goal
4
+ Write an archive, detect its format, and extract it with strict safety defaults.
5
+
6
+ ## Prereqs
7
+ - Node `>=24`
8
+ - `npm install`
9
+ - `npm run build`
10
+
11
+ ## Copy/paste
12
+ ```ts
13
+ import { write, detect, extract } from "dir-archiver";
14
+
15
+ await write("./project", "./project.zip", {
16
+ format: "zip",
17
+ includeBaseDirectory: true,
18
+ });
19
+
20
+ const detected = await detect("./project.zip");
21
+ await extract("./project.zip", "./out", { profile: "strict" });
22
+
23
+ console.log(detected.format);
24
+ ```
25
+
26
+ ## What you should see
27
+ - `detected.format` prints `zip`.
28
+ - `./out` contains the extracted files.
29
+
30
+ ## Safety notes
31
+ > [!NOTE]
32
+ > Use `profile: "strict"` for extraction unless you have a documented reason to
33
+ > weaken constraints.
package/jsr.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@ismail-elkorchi/dir-archiver",
3
+ "version": "3.0.1",
4
+ "license": "MIT",
5
+ "exports": {
6
+ ".": "./src/index.ts"
7
+ },
8
+ "imports": {
9
+ "@ismail-elkorchi/bytefold": "jsr:@ismail-elkorchi/bytefold@^0.8.1",
10
+ "@ismail-elkorchi/bytefold/node": "jsr:@ismail-elkorchi/bytefold/node@^0.8.1",
11
+ "@ismail-elkorchi/bytefold/deno": "jsr:@ismail-elkorchi/bytefold/deno@^0.8.1",
12
+ "@ismail-elkorchi/bytefold/bun": "jsr:@ismail-elkorchi/bytefold/bun@^0.8.1"
13
+ },
14
+ "publish": {
15
+ "include": [
16
+ "LICENSE",
17
+ "README.md",
18
+ "CHANGELOG.md",
19
+ "CONTRACT.md",
20
+ "SECURITY.md",
21
+ "CONTRIBUTING.md",
22
+ "SUPPORT.md",
23
+ "docs/**",
24
+ "src/**",
25
+ "jsr.json",
26
+ "package.json"
27
+ ],
28
+ "exclude": [
29
+ "dist/**",
30
+ "test/**",
31
+ "node_modules/**",
32
+ ".github/**"
33
+ ]
34
+ }
35
+ }