@openclaw/fs-safe 0.6.0 → 0.7.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.
- package/CHANGELOG.md +79 -0
- package/README.md +7 -0
- package/dist/archive-deadline.d.ts +3 -0
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +44 -8
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +1 -0
- package/dist/archive-errors.d.ts +1 -0
- package/dist/archive-errors.d.ts.map +1 -1
- package/dist/archive-errors.js +3 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +26 -16
- package/dist/archive-limits.d.ts +11 -3
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +24 -0
- package/dist/archive-native.d.ts +3 -2
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +24 -9
- package/dist/archive-policy.d.ts +2 -0
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +9 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +69 -41
- package/dist/archive-staging.d.ts +3 -0
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +91 -43
- package/dist/archive-tar-admission.d.ts +7 -0
- package/dist/archive-tar-admission.d.ts.map +1 -0
- package/dist/archive-tar-admission.js +43 -0
- package/dist/archive-tar-gnu.d.ts +2 -0
- package/dist/archive-tar-gnu.d.ts.map +1 -0
- package/dist/archive-tar-gnu.js +20 -0
- package/dist/archive-tar-header.d.ts +8 -0
- package/dist/archive-tar-header.d.ts.map +1 -0
- package/dist/archive-tar-header.js +47 -0
- package/dist/archive-tar-meta.d.ts +29 -1
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +104 -35
- package/dist/archive-tar-runtime.d.ts +3 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-tar-runtime.js +3 -0
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +8 -2
- package/dist/archive-zip-names.js +2 -2
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +38 -27
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +2 -4
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +3 -10
- package/dist/byte-budget.d.ts +5 -0
- package/dist/byte-budget.d.ts.map +1 -0
- package/dist/byte-budget.js +9 -0
- package/dist/directory-guard.d.ts +2 -1
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +10 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +81 -30
- package/dist/file-observation.d.ts +9 -0
- package/dist/file-observation.d.ts.map +1 -0
- package/dist/file-observation.js +22 -0
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +9 -5
- package/dist/file-store-limit.d.ts +2 -0
- package/dist/file-store-limit.d.ts.map +1 -0
- package/dist/file-store-limit.js +8 -0
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +38 -8
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +26 -26
- package/dist/json-durable-queue-directory.d.ts +2 -0
- package/dist/json-durable-queue-directory.d.ts.map +1 -0
- package/dist/json-durable-queue-directory.js +20 -0
- package/dist/json-durable-queue-ownership.d.ts +14 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -0
- package/dist/json-durable-queue-ownership.js +168 -0
- package/dist/json-durable-queue-retirement.d.ts +9 -0
- package/dist/json-durable-queue-retirement.d.ts.map +1 -0
- package/dist/json-durable-queue-retirement.js +126 -0
- package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
- package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
- package/dist/json-durable-queue-transfer-lock.js +19 -0
- package/dist/json-durable-queue.d.ts +1 -0
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +90 -57
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +32 -13
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +4 -2
- package/dist/native-binding.d.ts +12 -3
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +1 -1
- package/dist/opened-file-failure.d.ts +7 -0
- package/dist/opened-file-failure.d.ts.map +1 -0
- package/dist/opened-file-failure.js +41 -0
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +8 -2
- package/dist/output-sibling.d.ts.map +1 -1
- package/dist/output-sibling.js +11 -110
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +4 -2
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +27 -48
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +10 -10
- package/dist/private-temp-workspace.d.ts +3 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +81 -56
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +2 -4
- package/dist/read-opened-file.d.ts.map +1 -1
- package/dist/read-opened-file.js +6 -4
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +136 -82
- package/dist/replace-file-copy-fallback.d.ts +3 -1
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +21 -36
- package/dist/replace-file-copy-source.d.ts +21 -0
- package/dist/replace-file-copy-source.d.ts.map +1 -0
- package/dist/replace-file-copy-source.js +112 -0
- package/dist/replace-file-descriptor.d.ts +13 -3
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +32 -7
- package/dist/replace-file-rename-policy.d.ts +7 -0
- package/dist/replace-file-rename-policy.d.ts.map +1 -0
- package/dist/replace-file-rename-policy.js +30 -0
- package/dist/replace-file-temp-owner.d.ts +46 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -0
- package/dist/replace-file-temp-owner.js +346 -0
- package/dist/replace-file.d.ts +6 -1
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +72 -58
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +113 -82
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +6 -3
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +6 -5
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +17 -13
- package/dist/sibling-staged-file.d.ts +15 -0
- package/dist/sibling-staged-file.d.ts.map +1 -0
- package/dist/sibling-staged-file.js +148 -0
- package/dist/sibling-temp.d.ts +3 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +30 -74
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +91 -37
- package/dist/sidecar-lock-handle.d.ts +6 -2
- package/dist/sidecar-lock-handle.d.ts.map +1 -1
- package/dist/sidecar-lock-handle.js +17 -3
- package/dist/sidecar-lock-policy.d.ts +2 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +29 -0
- package/dist/sidecar-lock-reclaim.d.ts +16 -5
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +60 -28
- package/dist/sidecar-lock-root.d.ts +3 -0
- package/dist/sidecar-lock-root.d.ts.map +1 -0
- package/dist/sidecar-lock-root.js +76 -0
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +28 -16
- package/dist/strict-file-identity.d.ts.map +1 -1
- package/dist/strict-file-identity.js +4 -1
- package/dist/suppressed-error.d.ts +6 -0
- package/dist/suppressed-error.d.ts.map +1 -0
- package/dist/suppressed-error.js +15 -0
- package/dist/temp-cleanup.d.ts +2 -0
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +25 -10
- package/dist/temp-workspace-owner.d.ts +23 -0
- package/dist/temp-workspace-owner.d.ts.map +1 -0
- package/dist/temp-workspace-owner.js +320 -0
- package/dist/temp.d.ts +1 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/test-hooks.d.ts +5 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/docs/advanced.md +8 -1
- package/docs/archive.md +202 -26
- package/docs/atomic.md +11 -2
- package/docs/durability.md +1 -1
- package/docs/file-store.md +2 -0
- package/docs/install.md +8 -4
- package/docs/json.md +11 -4
- package/docs/native-helper.md +15 -3
- package/docs/native.md +33 -6
- package/docs/output.md +16 -3
- package/docs/public-api.md +6 -3
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +1 -1
- package/docs/regular-file.md +9 -2
- package/docs/root.md +2 -0
- package/docs/secret-file.md +5 -1
- package/docs/secure-file.md +4 -2
- package/docs/security-model.md +18 -0
- package/docs/sidecar-lock.md +60 -4
- package/docs/store.md +13 -0
- package/docs/temp.md +128 -14
- package/docs/writing.md +6 -0
- package/package.json +10 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,84 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.1 - 2026-09-01
|
|
4
|
+
|
|
5
|
+
### Highlights
|
|
6
|
+
|
|
7
|
+
- **Make lock handoffs reliable:** recover from disappearing ownership records within retry budgets, preserve callback errors, and safely retry failed synchronous release cleanup.
|
|
8
|
+
- **Reject corrupt native ZIP reads:** verify decoded entry length against the declared size before returning bytes, while retaining byte caps and CRC checks.
|
|
9
|
+
- **Preserve long destination filenames:** use independent private staging names for Root writes and copies, ZIP extraction, and synchronous JSON writes instead of lengthening the destination basename.
|
|
10
|
+
|
|
11
|
+
### File locks
|
|
12
|
+
|
|
13
|
+
- Recover Root-backed asynchronous handoffs from unlinked opened records with exact descriptor identity and unlink evidence, including Windows resolver `EPERM`/`EBADF` failures. Recheck canonical ancestors and charge discarded observations to retry/deadline budgets without granting release or reclaim authority. ([#189](https://github.com/openclaw/fs-safe/pull/189))
|
|
14
|
+
- Retry Root lock contention when a holder changes between pre-open inspection and opening the file. Discard only the verified stale observation, recheck canonical ancestors, and require fresh exclusive creation; generic reads, creator admission, and release/reclaim authority remain unchanged.
|
|
15
|
+
- Verify reopened Root-created sidecars against the creator's exact serialized bytes and ownership token before admission. Reject replacements and unlinked descriptors, and retain the original creator receipt for failed-acquisition cleanup. ([#189](https://github.com/openclaw/fs-safe/pull/189))
|
|
16
|
+
- Align Windows synchronous lock-parent canonicalization with Root, including short-name expansion, and bound lock-file create/open denials and missing or changed snapshot retries. Preserve the original `EPERM` when a retry budget is exhausted. ([#189](https://github.com/openclaw/fs-safe/pull/189))
|
|
17
|
+
- Determine synchronous lock staleness from the captured payload timestamp or snapshot mtime, avoiding false stale decisions after unlink or replacement. ([#189](https://github.com/openclaw/fs-safe/pull/189))
|
|
18
|
+
- Retain failed synchronous release cleanup for safe retries without double-decrementing references, re-closing a consumed or reused descriptor, or deleting a replacement sidecar. ([#189](https://github.com/openclaw/fs-safe/pull/189))
|
|
19
|
+
- Propagate payload, JSON serialization, and parsing failures unchanged without retrying the failing callback. Scope Root failure evidence to each observation so historical errors cannot trigger retries in later, nested, or concurrent acquisitions. ([#190](https://github.com/openclaw/fs-safe/pull/190))
|
|
20
|
+
|
|
21
|
+
### File writes and archives
|
|
22
|
+
|
|
23
|
+
- Keep private staging names independent of destination basenames across guarded Root writes and copies, native Windows writes, ZIP extraction, and `writeJsonSync()`. Preserve public producer filenames, custom `tempPrefix`, and literal JSON parent-path and drive-relative semantics. ([#191](https://github.com/openclaw/fs-safe/pull/191))
|
|
24
|
+
- Avoid opening an existing target merely to inherit its mode during create-only Root writes, while preserving boundary, alias, hardlink, and type checks. ([#189](https://github.com/openclaw/fs-safe/pull/189))
|
|
25
|
+
- Reject native `readArchiveEntry()` ZIP payloads whose decoded length differs from the declared uncompressed size with `ArchiveFormatError("archive-header-invalid")`, matching JavaScript behavior while preserving `maxBytes` and CRC checks. ([#189](https://github.com/openclaw/fs-safe/pull/189))
|
|
26
|
+
|
|
27
|
+
### Docs and validation
|
|
28
|
+
|
|
29
|
+
- Clarify that invalid UTF-8 or nonzero bytes after NUL terminators in fixed TAR name, linkname, and USTAR prefix fields reject with `ArchiveSecurityError("entry-path")`; this documents existing behavior. ([#189](https://github.com/openclaw/fs-safe/pull/189))
|
|
30
|
+
- Add cross-process contention and release-retry proofs, plus focused lock-admission and ZIP-integrity coverage in JavaScript-fallback and required-native CI paths. Refresh the release checklist for eight-package trusted publishing.
|
|
31
|
+
|
|
32
|
+
## 0.7.0 - 2026-08-31
|
|
33
|
+
|
|
34
|
+
### Highlights
|
|
35
|
+
|
|
36
|
+
- **Safer reads and writes:** extend exact identity checks and retained-descriptor publication to reject substituted inputs and detect publication mismatches, including rounded-equal Windows file IDs.
|
|
37
|
+
- **Opt into bounded temp cleanup:** `cleanupSafety: "require-bounded"` prevents recursive traversal of substituted workspace trees, while the compatible default remains available without native support. See the [cleanup guarantees and POSIX limits](https://fs-safe.io/temp.html#private-temp-workspaces).
|
|
38
|
+
- **Harden untrusted archive handling:** reject hidden path aliases, ambiguous TAR metadata, corrupt gzip streams, and excessive decoded data before publication; align filtering and entry reads across JavaScript and native backends.
|
|
39
|
+
- **Preserve queued work and durability:** generation-bound claims protect newer same-ID entries from acknowledgement or quarantine, and queue transitions and synchronous store writes sync their filesystem changes.
|
|
40
|
+
- **Make lock handoffs and timeouts reliable:** recover from ownership records unlinked during contention, surface release failures, and finish in-flight archive publication or rollback before returning a timeout.
|
|
41
|
+
|
|
42
|
+
### Compatibility and upgrade notes
|
|
43
|
+
|
|
44
|
+
- **Claim queued entries before processing.** Call `loadJsonDurableQueueEntry()` or the batch loader before acknowledgement, rather than pairing a direct read with `ackJsonDurableQueueEntry()`. A pending entry without a processing claim now rejects; missing entries remain idempotent no-ops. Queue and failed directories must share a filesystem with hardlink support for quarantine. See the [queue recovery contract](https://fs-safe.io/store.html).
|
|
45
|
+
- **Handle the new cleanup result.** `TempWorkspaceCleanupResult` adds `"indeterminate"` for cleanup whose safe completion cannot be established. Exhaustive result handlers must accept it; do not treat it as successful removal or blindly delete retained artifacts.
|
|
46
|
+
- **Archive filters receive canonical pre-strip paths.** `entryFilter` now sees normalized separators, dot components, and effective metadata names rather than alternate spellings of the same entry. Update filters that match raw aliases; invalid `onFiltered` values reject before extraction rather than enabling skipping.
|
|
47
|
+
- **Validate limits and handle surfaced errors.** Root, FileStore, secure/secret/regular reads, durable queues, and external output require `maxBytes` to be a non-negative safe integer or positive `Infinity`; zero is enforced and `undefined` retains configured defaults. Lock retry/deadline numbers are validated. Handle propagated release, failed-acquire cleanup, and durability failures; exhaustive archive-limit handlers must accept `archive-decoded-size-exceeds-limit`.
|
|
48
|
+
|
|
49
|
+
### File safety and temp cleanup
|
|
50
|
+
|
|
51
|
+
- Verify regular-file reads, root-file adapters, archive staging, `Root.copyIn()` sources, and durable JSON queue reads with lossless bigint identities. Reject replacements whose IDs round to the same JavaScript number and unresolved Windows identities before reading bytes; public numeric `Stats` receipts remain unchanged.
|
|
52
|
+
- Verify async and sync regular-file append targets against exact pre-open, descriptor, and current-path identities, rejecting rounded-equal replacements and persistent unknown Windows identities before chmod or append.
|
|
53
|
+
- Retain async and sync atomic replacement descriptors across `beforeRename`, retries, copy fallback, and publication. Reject substituted, non-regular, or hardlinked stages and preserve unowned cleanup paths. `replaceFileAtomic()` and its sync variant add `renameIdentity: "verify-content-with-lock"` for rename-unstable FUSE mounts: an explicit weaker identity contract for cooperating writers, not protection against same-authority actors ignoring the lock.
|
|
54
|
+
- Pin callback-produced sibling temps through mode application, optional fsync, rename, and publication checks. Preserve unverified paths and producer modes, retain disabled sync defaults and best-effort file/directory chmod, and admit read-only descriptors unless `writeSiblingTempFile` requests file sync.
|
|
55
|
+
- Apply `writeJsonSync()` best-effort file-mode tightening only through a reopened, single-link regular descriptor matching the staged bigint identity, so a swap cannot chmod an unrelated file.
|
|
56
|
+
- Add native handle-relative temp workspace removal with collision-safe quarantine, enumerated-child identity checks, mount-crossing rejection, and symlink/reparse leaf removal without target traversal. Windows deletes exact opened handles; POSIX retains a documented final name-based race bounded to one substituted leaf or empty directory entry, never recursive traversal of a substituted nonempty tree.
|
|
57
|
+
- Require usable Linux `openat2` with `RESOLVE_NO_XDEV` at runtime for bounded cleanup. Compatible mode falls back when unavailable; `cleanupSafety: "require-bounded"` rejects before creating a child.
|
|
58
|
+
- Open attacker-raceable secure, secret, archive, queue, publication, fallback, and lock-file read paths nonblocking on POSIX, so a FIFO substitution cannot stall admission, deadlines, or cleanup verification.
|
|
59
|
+
|
|
60
|
+
### Archive extraction and reads
|
|
61
|
+
|
|
62
|
+
- Prevent archive path aliases from bypassing excluded subtrees: pass canonical pre-strip paths to `entryFilter` across JavaScript/native ZIP, TAR, gzip, zstd, and bzip2, while retaining raw-path validation, stripping, collision checks, and filter rejection policy.
|
|
63
|
+
- Reject ambiguous TAR EOF framing, unsafe numeric sizes, and bodies on raw directory/link entries before parsing. Enforce an absolute decoded-byte ceiling through physical EOF, including metadata and zero padding, before extraction publication or entry-read success; report `archive-decoded-size-exceeds-limit` on overflow.
|
|
64
|
+
- Bound retained TAR manifests with a shared budget capped at 64 MiB, independent of compressed input size. Validate checksums, strict fixed-field UTF-8, NFC/NFD component lengths, GNU effective trailing separators, and linkname/type consistency before metadata processing or caller policy.
|
|
65
|
+
- Validate GNU long-name/link bodies on both backends before parsing: reject malformed UTF-8/NUL structure, repeated or dangling metadata, mixed PAX/GNU chains, and unsafe effective names, while preserving original bytes and valid L+K pairs.
|
|
66
|
+
- Apply ordered path, count, strip, depth, collision, and filter policy even to TAR records parsers would ignore (`V`, `A`, `I`, `M`, and unknown typeflags). Safely omit accepted unsupported records, validate raw names hidden by metadata or NUL terminators, and align native device/FIFO filtering and GNUDumpDir handling with JavaScript.
|
|
67
|
+
- Stop starting archive destination mutations at the timeout boundary and wait for any already-running destination mutation and rollback before rejecting. Non-mutating work retains prompt deadlines; publication cannot continue after the timeout is reported.
|
|
68
|
+
- Match `readArchiveEntry()` to extraction's validated canonical pre-strip paths and effective metadata names across all supported formats/backends. Preserve directory, link, collision, integrity, and byte-limit checks; read `maxBytes` remains a requested-entry budget, separate from archive-wide decoding and metadata limits.
|
|
69
|
+
- Charge TAR `maxEntryBytes` and `maxExtractedBytes` only to entries accepted after strip/filter policy, not skipped or fully stripped members. Raw framing, logical entry counts, metadata, and absolute decoded-byte limits still apply to the whole archive.
|
|
70
|
+
- Accept highly compressible gzip TARs within fs-safe's explicit limits consistently across backends by disabling node-tar's extra 1000x ratio threshold only after complete decoded-byte admission. Archive, decoded, entry, output, and deadline limits remain enforced.
|
|
71
|
+
- Preserve large finite TAR limits such as `Number.MAX_VALUE` in native `auto`/`require` modes through shared internal metadata/decoded-byte and u32 entry-count clamping before backend selection; retain high-level payload budgets and reject malformed direct native limits.
|
|
72
|
+
- Update native gzip/DEFLATE decoding to flate2 1.1.10 and miniz_oxide 0.9.1, including upstream incomplete-stream fixes. Truncated bodies, missing trailers, and checksum failures reject before extraction publishes files or an entry read returns bytes.
|
|
73
|
+
|
|
74
|
+
### Durable queues, stores, and locks
|
|
75
|
+
|
|
76
|
+
- Claim durable queue generations under a fail-closed cross-process lock using no-replace hardlinks and recoverable source retirement. Acknowledgement and quarantine preserve newer same-ID replacements, and quarantine collisions preserve existing failed-entry evidence.
|
|
77
|
+
- Fsync affected directories for queue creation, claims, acknowledgement, quarantine, marker cleanup, and retirement; propagate durability failures. Synchronous file-store writes now fsync temps before rename and parent directories after publication.
|
|
78
|
+
- Retry Root-backed sidecar acquisition when the owner unlinks its record after the waiter opens it, but only after the same Root capability proves absence. Replacements remain fail-closed; generic `Root.open()` behavior is unchanged.
|
|
79
|
+
- Propagate asynchronous sidecar release and failed-acquire deletion failures, preserve paired errors, and retain failed release state for a safe retry through the same handle or manager drain.
|
|
80
|
+
- Clamp synchronous lock backoff to the remaining finite deadline instead of overshooting the timeout or blocking forever.
|
|
81
|
+
|
|
3
82
|
## 0.6.0 - 2026-08-29
|
|
4
83
|
|
|
5
84
|
### Highlights
|
package/README.md
CHANGED
|
@@ -393,6 +393,13 @@ cleanup. Retry, dedupe, and transport semantics stay with the caller.
|
|
|
393
393
|
`tempWorkspace()` exposes `write()`, `writeText()`, `writeJson()`, `copyIn()`, and `read()` for
|
|
394
394
|
single-file scratch workflows without hand-rolled path joins, plus a `store: FileStore` view of
|
|
395
395
|
the workspace dir for the richer cases (`writeStream`, `readJsonIfExists`, `store.json<T>(rel)`).
|
|
396
|
+
Compatible creation and cleanup remain available without native support.
|
|
397
|
+
Set `cleanupSafety: "require-bounded"` to require collision-safe quarantine and
|
|
398
|
+
descriptor-bounded recursive cleanup before creating a child. See the
|
|
399
|
+
[temp workspace contract](docs/temp.md).
|
|
400
|
+
Linux bounded cleanup requires the exact `openat2`/`RESOLVE_NO_XDEV` capability
|
|
401
|
+
at runtime; compatible mode falls back when unavailable, while `require-bounded`
|
|
402
|
+
rejects before child creation.
|
|
396
403
|
|
|
397
404
|
`tempFile()` is the smaller one-file temp helper. It is intentionally an
|
|
398
405
|
advanced primitive: use `tempWorkspace()` for the stable temp surface and reach
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
export type ExtractionDeadline = {
|
|
2
2
|
signal: AbortSignal;
|
|
3
3
|
check: () => void;
|
|
4
|
+
ownDestinationMutation: <T>(run: () => Promise<T>) => Promise<T>;
|
|
5
|
+
waitForDestinationMutations: () => Promise<void>;
|
|
4
6
|
dispose: () => void;
|
|
5
7
|
};
|
|
8
|
+
export declare function ownExtractionDestinationMutation<T>(deadline: ExtractionDeadline | undefined, run: () => Promise<T>): Promise<T>;
|
|
6
9
|
export declare function createPipelineTimeoutError(err: unknown, deadline: ExtractionDeadline): unknown;
|
|
7
10
|
export declare function waitForDeadline<T>(promise: Promise<T>, deadline: ExtractionDeadline): Promise<T>;
|
|
8
11
|
export declare function withExtractionDeadline<T>(timeoutMs: number, label: string, run: (deadline: ExtractionDeadline) => Promise<T>): Promise<T>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-deadline.d.ts","sourceRoot":"","sources":["../src/archive-deadline.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;
|
|
1
|
+
{"version":3,"file":"archive-deadline.d.ts","sourceRoot":"","sources":["../src/archive-deadline.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,sBAAsB,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACjE,2BAA2B,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF,wBAAsB,gCAAgC,CAAC,CAAC,EACtD,QAAQ,EAAE,kBAAkB,GAAG,SAAS,EACxC,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACpB,OAAO,CAAC,CAAC,CAAC,CAEZ;AAWD,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,kBAAkB,GAC3B,OAAO,CAST;AAED,wBAAsB,eAAe,CAAC,CAAC,EACrC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,QAAQ,EAAE,kBAAkB,GAC3B,OAAO,CAAC,CAAC,CAAC,CAgBZ;AAyDD,wBAAsB,sBAAsB,CAAC,CAAC,EAC5C,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,OAAO,CAAC,CAAC,CAAC,GAChD,OAAO,CAAC,CAAC,CAAC,CAkBZ"}
|
package/dist/archive-deadline.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export async function ownExtractionDestinationMutation(deadline, run) {
|
|
2
|
+
return deadline ? await deadline.ownDestinationMutation(run) : await run();
|
|
3
|
+
}
|
|
1
4
|
function signalReason(signal, fallback) {
|
|
2
5
|
const reason = signal.reason;
|
|
3
6
|
return reason instanceof Error ? reason : fallback ?? new Error(String(reason));
|
|
@@ -30,26 +33,48 @@ export async function waitForDeadline(promise, deadline) {
|
|
|
30
33
|
}),
|
|
31
34
|
]);
|
|
32
35
|
}
|
|
36
|
+
function createDestinationMutationOwner(check) {
|
|
37
|
+
const active = new Set();
|
|
38
|
+
return {
|
|
39
|
+
ownDestinationMutation: async (run) => {
|
|
40
|
+
check();
|
|
41
|
+
const operation = Promise.resolve().then(run);
|
|
42
|
+
const tracked = operation.then(() => undefined, () => undefined);
|
|
43
|
+
active.add(tracked);
|
|
44
|
+
void tracked.finally(() => active.delete(tracked));
|
|
45
|
+
return await operation;
|
|
46
|
+
},
|
|
47
|
+
waitForDestinationMutations: async () => {
|
|
48
|
+
while (active.size > 0) {
|
|
49
|
+
await Promise.all(active);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
33
54
|
function createExtractionDeadline(timeoutMs, label) {
|
|
34
55
|
const controller = new AbortController();
|
|
56
|
+
const timeoutError = new Error(`${label} timed out after ${timeoutMs}ms`);
|
|
57
|
+
const check = () => {
|
|
58
|
+
if (controller.signal.aborted) {
|
|
59
|
+
throw signalReason(controller.signal, timeoutError);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const mutationOwner = createDestinationMutationOwner(check);
|
|
35
63
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
36
64
|
return {
|
|
37
65
|
signal: controller.signal,
|
|
38
|
-
check
|
|
66
|
+
check,
|
|
67
|
+
...mutationOwner,
|
|
39
68
|
dispose: () => undefined,
|
|
40
69
|
};
|
|
41
70
|
}
|
|
42
|
-
const timeoutError = new Error(`${label} timed out after ${timeoutMs}ms`);
|
|
43
71
|
const timeoutId = setTimeout(() => {
|
|
44
72
|
controller.abort(timeoutError);
|
|
45
73
|
}, timeoutMs);
|
|
46
74
|
return {
|
|
47
75
|
signal: controller.signal,
|
|
48
|
-
check
|
|
49
|
-
|
|
50
|
-
throw signalReason(controller.signal, timeoutError);
|
|
51
|
-
}
|
|
52
|
-
},
|
|
76
|
+
check,
|
|
77
|
+
...mutationOwner,
|
|
53
78
|
dispose: () => {
|
|
54
79
|
clearTimeout(timeoutId);
|
|
55
80
|
},
|
|
@@ -57,9 +82,20 @@ function createExtractionDeadline(timeoutMs, label) {
|
|
|
57
82
|
}
|
|
58
83
|
export async function withExtractionDeadline(timeoutMs, label, run) {
|
|
59
84
|
const deadline = createExtractionDeadline(timeoutMs, label);
|
|
85
|
+
const operation = Promise.resolve().then(async () => await run(deadline));
|
|
60
86
|
try {
|
|
61
87
|
deadline.check();
|
|
62
|
-
|
|
88
|
+
try {
|
|
89
|
+
return await waitForDeadline(operation, deadline);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (deadline.signal.aborted && error === deadlineReason(deadline)) {
|
|
93
|
+
// Preserve prompt timeout settlement for non-mutating work, but never
|
|
94
|
+
// return while live destination publication or rollback is still owned.
|
|
95
|
+
await deadline.waitForDestinationMutations();
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
63
99
|
}
|
|
64
100
|
finally {
|
|
65
101
|
deadline.dispose();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-entry.d.ts","sourceRoot":"","sources":["../src/archive-entry.ts"],"names":[],"mappings":"AAKA,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAIzD;AAED,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAChC,IAAI,CA2DN;
|
|
1
|
+
{"version":3,"file":"archive-entry.d.ts","sourceRoot":"","sources":["../src/archive-entry.ts"],"names":[],"mappings":"AAKA,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAIzD;AAED,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAChC,IAAI,CA2DN;AAGD,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc1F;AAED,wBAAgB,8BAA8B,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAgBlG;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,MAAM,CAWT"}
|
package/dist/archive-entry.js
CHANGED
|
@@ -40,6 +40,7 @@ export function validateArchiveEntryPath(entryPath, params) {
|
|
|
40
40
|
throw new ArchiveSecurityError("entry-path", `archive entry contains a parent segment: ${formatErrorDetail(entryPath)}`);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
// Validate raw paths first. Zero stripping gives the canonical policy identity.
|
|
43
44
|
export function stripArchivePath(entryPath, stripComponents) {
|
|
44
45
|
const raw = normalizeArchiveEntryPath(entryPath);
|
|
45
46
|
if (!raw || raw === "." || raw === "./") {
|
package/dist/archive-errors.d.ts
CHANGED
|
@@ -9,4 +9,5 @@ export declare class ArchiveFormatError extends Error {
|
|
|
9
9
|
constructor(message: string, options?: ErrorOptions);
|
|
10
10
|
}
|
|
11
11
|
export declare function isArchiveFormatErrorMessage(message: string): boolean;
|
|
12
|
+
export declare function isArchiveTarPathErrorMessage(message: string): boolean;
|
|
12
13
|
//# sourceMappingURL=archive-errors.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-errors.d.ts","sourceRoot":"","sources":["../src/archive-errors.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,sBAAsB,GAAG,wBAAwB,CAAC;AAE9D,MAAM,MAAM,wBAAwB,GAChC,2BAA2B,GAC3B,qBAAqB,GACrB,+BAA+B,GAC/B,gBAAgB,GAChB,YAAY,GACZ,YAAY,CAAC;AAEjB,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IAExC,YAAY,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,EAIlF;CACF;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IAEtC,YAAY,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,EAIlD;CACF;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAKpE"}
|
|
1
|
+
{"version":3,"file":"archive-errors.d.ts","sourceRoot":"","sources":["../src/archive-errors.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,sBAAsB,GAAG,wBAAwB,CAAC;AAE9D,MAAM,MAAM,wBAAwB,GAChC,2BAA2B,GAC3B,qBAAqB,GACrB,+BAA+B,GAC/B,gBAAgB,GAChB,YAAY,GACZ,YAAY,CAAC;AAEjB,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IAExC,YAAY,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,EAIlF;CACF;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IAEtC,YAAY,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,EAIlD;CACF;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAKpE;AAED,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAErE"}
|
package/dist/archive-errors.js
CHANGED
|
@@ -18,3 +18,6 @@ export function isArchiveFormatErrorMessage(message) {
|
|
|
18
18
|
return (message.includes("archive-header-invalid") ||
|
|
19
19
|
message.includes("archive entry size did not match its manifest"));
|
|
20
20
|
}
|
|
21
|
+
export function isArchiveTarPathErrorMessage(message) {
|
|
22
|
+
return message.includes("archive-gnu-path-invalid") || message.includes("archive-entry-path-invalid");
|
|
23
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-input.d.ts","sourceRoot":"","sources":["../src/archive-input.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAGL,KAAK,4BAA4B,EAClC,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"archive-input.d.ts","sourceRoot":"","sources":["../src/archive-input.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAGL,KAAK,4BAA4B,EAClC,MAAM,qBAAqB,CAAC;AAM7B,MAAM,MAAM,iBAAiB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC;AAM/E,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,kBAAkB,CAAC;CAC9B,GAAG,OAAO,CAAC,IAAI,CAAC,CAchB;AAED,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,4BAA4B,CAAC;IACrC,QAAQ,EAAE,kBAAkB,CAAC;CAC9B,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAiE7B"}
|
package/dist/archive-input.js
CHANGED
|
@@ -2,7 +2,9 @@ import { constants as fsConstants } from "node:fs";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, } from "./archive-limits.js";
|
|
5
|
-
import {
|
|
5
|
+
import { FsSafeError } from "./errors.js";
|
|
6
|
+
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
7
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
6
8
|
import { tempFile } from "./temp-target.js";
|
|
7
9
|
async function closeFileHandle(handle) {
|
|
8
10
|
if (handle)
|
|
@@ -22,15 +24,17 @@ export async function writeFileHandleFully(params) {
|
|
|
22
24
|
export async function stageArchiveFileForExtraction(params) {
|
|
23
25
|
params.deadline.check();
|
|
24
26
|
const sourcePath = path.resolve(params.archivePath);
|
|
25
|
-
const initialStat = await
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const initialStat = await inspectFileIdentity(async () => {
|
|
28
|
+
const stat = await fs.lstat(sourcePath, { bigint: true });
|
|
29
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
30
|
+
throw new Error(`archive is not a regular file: ${params.archivePath}`);
|
|
31
|
+
}
|
|
32
|
+
return stat;
|
|
33
|
+
});
|
|
29
34
|
if (initialStat.size > params.limits.maxArchiveBytes) {
|
|
30
35
|
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ARCHIVE_SIZE_EXCEEDS_LIMIT);
|
|
31
36
|
}
|
|
32
|
-
const
|
|
33
|
-
const handle = await fs.open(sourcePath, fsConstants.O_RDONLY | noFollow);
|
|
37
|
+
const handle = await fs.open(sourcePath, resolveReadOpenFlags());
|
|
34
38
|
let staged;
|
|
35
39
|
let output;
|
|
36
40
|
try {
|
|
@@ -38,15 +42,18 @@ export async function stageArchiveFileForExtraction(params) {
|
|
|
38
42
|
prefix: "fs-safe-archive-input",
|
|
39
43
|
fileName: path.basename(sourcePath),
|
|
40
44
|
});
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
const opened = await inspectFileIdentity(async () => {
|
|
46
|
+
const stat = await handle.stat({ bigint: true });
|
|
47
|
+
if (!stat.isFile())
|
|
48
|
+
throw new Error("archive changed during validation");
|
|
49
|
+
return stat;
|
|
50
|
+
}, initialStat);
|
|
51
|
+
await inspectFileIdentity(async () => {
|
|
52
|
+
const stat = await fs.lstat(sourcePath, { bigint: true });
|
|
53
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
54
|
+
throw new Error("archive changed during validation");
|
|
55
|
+
return stat;
|
|
56
|
+
}, opened);
|
|
50
57
|
const flags = fsConstants.O_WRONLY |
|
|
51
58
|
fsConstants.O_CREAT |
|
|
52
59
|
fsConstants.O_EXCL |
|
|
@@ -74,6 +81,9 @@ export async function stageArchiveFileForExtraction(params) {
|
|
|
74
81
|
catch (error) {
|
|
75
82
|
await closeFileHandle(output);
|
|
76
83
|
await staged?.cleanup().catch(() => undefined);
|
|
84
|
+
if (error instanceof FsSafeError && error.code === "path-mismatch") {
|
|
85
|
+
throw new FsSafeError("path-mismatch", "archive changed during validation", { cause: error });
|
|
86
|
+
}
|
|
77
87
|
throw error;
|
|
78
88
|
}
|
|
79
89
|
finally {
|
package/dist/archive-limits.d.ts
CHANGED
|
@@ -4,11 +4,11 @@ export type ArchiveExtractLimits = {
|
|
|
4
4
|
* Max archive file bytes (compressed).
|
|
5
5
|
*/
|
|
6
6
|
maxArchiveBytes?: number;
|
|
7
|
-
/** Max
|
|
7
|
+
/** Max logical archive entries, including skipped/stripped members. */
|
|
8
8
|
maxEntries?: number;
|
|
9
|
-
/** Max extracted bytes
|
|
9
|
+
/** Max extracted bytes across entries accepted after strip/filter policy. */
|
|
10
10
|
maxExtractedBytes?: number;
|
|
11
|
-
/** Max
|
|
11
|
+
/** Max bytes in an entry accepted after strip/filter policy. */
|
|
12
12
|
maxEntryBytes?: number;
|
|
13
13
|
/** Max bytes in one PAX, GNU long-name, or related TAR metadata entry. */
|
|
14
14
|
maxMetaEntryBytes?: number;
|
|
@@ -26,6 +26,7 @@ export declare const ARCHIVE_LIMIT_ERROR_CODE: {
|
|
|
26
26
|
readonly ENTRY_COUNT_EXCEEDS_LIMIT: "archive-entry-count-exceeds-limit";
|
|
27
27
|
readonly ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT: "archive-entry-extracted-size-exceeds-limit";
|
|
28
28
|
readonly EXTRACTED_SIZE_EXCEEDS_LIMIT: "archive-extracted-size-exceeds-limit";
|
|
29
|
+
readonly DECODED_SIZE_EXCEEDS_LIMIT: "archive-decoded-size-exceeds-limit";
|
|
29
30
|
readonly META_ENTRY_SIZE_EXCEEDS_LIMIT: "archive-meta-entry-size-exceeds-limit";
|
|
30
31
|
readonly MANIFEST_SIZE_EXCEEDS_LIMIT: "archive-manifest-size-exceeds-limit";
|
|
31
32
|
readonly ENTRY_PATH_COMPONENTS_EXCEEDS_LIMIT: "archive-entry-path-components-exceeds-limit";
|
|
@@ -36,6 +37,13 @@ export declare class ArchiveLimitError extends Error {
|
|
|
36
37
|
constructor(code: ArchiveLimitErrorCode);
|
|
37
38
|
}
|
|
38
39
|
export type ResolvedArchiveExtractLimits = Required<ArchiveExtractLimits>;
|
|
40
|
+
export type TarMeterLimits = Pick<ResolvedArchiveExtractLimits, "maxEntries" | "maxMetaEntryBytes"> & {
|
|
41
|
+
maxDecodedBytes: number;
|
|
42
|
+
maxManifestBytes: number;
|
|
43
|
+
};
|
|
44
|
+
export declare const MAX_TAR_MANIFEST_BYTES: number;
|
|
45
|
+
export declare function tarManifestEntryCost(path: string): number;
|
|
46
|
+
export declare function resolveTarMeterLimits(options?: ArchiveExtractLimits): TarMeterLimits;
|
|
39
47
|
export declare function resolveExtractLimits(limits?: ArchiveExtractLimits): ResolvedArchiveExtractLimits;
|
|
40
48
|
export declare function assertArchiveEntryPathComponentsWithinLimit(entryPath: string, limits: ResolvedArchiveExtractLimits): void;
|
|
41
49
|
export declare function assertArchiveEntryCountWithinLimit(entryCount: number, limits: ResolvedArchiveExtractLimits): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-limits.d.ts","sourceRoot":"","sources":["../src/archive-limits.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,MAAM,oBAAoB,GAAG;IACjC;;OAEG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,
|
|
1
|
+
{"version":3,"file":"archive-limits.d.ts","sourceRoot":"","sources":["../src/archive-limits.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,MAAM,oBAAoB,GAAG;IACjC;;OAEG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0EAA0E;IAC1E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,wEAAwE;IACxE,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC,CAAC;AAEF,eAAO,MAAM,6BAA6B,QAAoB,CAAC;AAC/D,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAC1C,eAAO,MAAM,2BAA2B,QAAoB,CAAC;AAC7D,eAAO,MAAM,uBAAuB,QAAoB,CAAC;AACzD,eAAO,MAAM,4BAA4B,QAAc,CAAC;AACxD,eAAO,MAAM,iCAAiC,MAAM,CAAC;AAErD,eAAO,MAAM,wBAAwB;aACnC,0BAA0B,EAAE,4BAA4B;aACxD,yBAAyB,EAAE,mCAAmC;aAC9D,kCAAkC,EAAE,4CAA4C;aAChF,4BAA4B,EAAE,sCAAsC;aACpE,0BAA0B,EAAE,oCAAoC;aAChE,6BAA6B,EAAE,uCAAuC;aACtE,2BAA2B,EAAE,qCAAqC;aAClE,mCAAmC,EAAE,6CAA6C;CAC1E,CAAC;AAEX,MAAM,MAAM,qBAAqB,GAC/B,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,OAAO,wBAAwB,CAAC,CAAC;AAiB3E,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IAErC,YAAY,IAAI,EAAE,qBAAqB,EAItC;CACF;AAED,MAAM,MAAM,4BAA4B,GAAG,QAAQ,CAAC,oBAAoB,CAAC,CAAC;AAE1E,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,4BAA4B,EAC5D,YAAY,GAAG,mBAAmB,CAAC,GAAG;IAAE,eAAe,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9F,eAAO,MAAM,sBAAsB,QAAmB,CAAC;AAEvD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,cAAc,CAkBpF;AAUD,wBAAgB,oBAAoB,CAClC,MAAM,CAAC,EAAE,oBAAoB,GAC5B,4BAA4B,CAY9B;AAED,wBAAgB,2CAA2C,CACzD,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,4BAA4B,GACnC,IAAI,CASN;AAED,wBAAgB,kCAAkC,CAChD,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,4BAA4B,QAKrC;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,4BAA4B,GAAG;IAC7E,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC,CAiCA;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE;IACnD,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC,GAAG,SAAS,CAYZ"}
|
package/dist/archive-limits.js
CHANGED
|
@@ -10,6 +10,7 @@ export const ARCHIVE_LIMIT_ERROR_CODE = {
|
|
|
10
10
|
ENTRY_COUNT_EXCEEDS_LIMIT: "archive-entry-count-exceeds-limit",
|
|
11
11
|
ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT: "archive-entry-extracted-size-exceeds-limit",
|
|
12
12
|
EXTRACTED_SIZE_EXCEEDS_LIMIT: "archive-extracted-size-exceeds-limit",
|
|
13
|
+
DECODED_SIZE_EXCEEDS_LIMIT: "archive-decoded-size-exceeds-limit",
|
|
13
14
|
META_ENTRY_SIZE_EXCEEDS_LIMIT: "archive-meta-entry-size-exceeds-limit",
|
|
14
15
|
MANIFEST_SIZE_EXCEEDS_LIMIT: "archive-manifest-size-exceeds-limit",
|
|
15
16
|
ENTRY_PATH_COMPONENTS_EXCEEDS_LIMIT: "archive-entry-path-components-exceeds-limit",
|
|
@@ -19,6 +20,7 @@ const ARCHIVE_LIMIT_ERROR_MESSAGE = {
|
|
|
19
20
|
[ARCHIVE_LIMIT_ERROR_CODE.ENTRY_COUNT_EXCEEDS_LIMIT]: "archive entry count exceeds limit",
|
|
20
21
|
[ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT]: "archive entry extracted size exceeds limit",
|
|
21
22
|
[ARCHIVE_LIMIT_ERROR_CODE.EXTRACTED_SIZE_EXCEEDS_LIMIT]: "archive extracted size exceeds limit",
|
|
23
|
+
[ARCHIVE_LIMIT_ERROR_CODE.DECODED_SIZE_EXCEEDS_LIMIT]: "archive decoded size exceeds limit",
|
|
22
24
|
[ARCHIVE_LIMIT_ERROR_CODE.META_ENTRY_SIZE_EXCEEDS_LIMIT]: "archive metadata entry size exceeds limit",
|
|
23
25
|
[ARCHIVE_LIMIT_ERROR_CODE.MANIFEST_SIZE_EXCEEDS_LIMIT]: "archive manifest size exceeds limit",
|
|
24
26
|
[ARCHIVE_LIMIT_ERROR_CODE.ENTRY_PATH_COMPONENTS_EXCEEDS_LIMIT]: "archive entry path components exceed limit",
|
|
@@ -31,6 +33,28 @@ export class ArchiveLimitError extends Error {
|
|
|
31
33
|
this.code = code;
|
|
32
34
|
}
|
|
33
35
|
}
|
|
36
|
+
export const MAX_TAR_MANIFEST_BYTES = 64 * 1024 * 1024;
|
|
37
|
+
export function tarManifestEntryCost(path) {
|
|
38
|
+
return 64 + 2 * Buffer.byteLength(path, "utf8");
|
|
39
|
+
}
|
|
40
|
+
export function resolveTarMeterLimits(options) {
|
|
41
|
+
const limits = resolveExtractLimits(options);
|
|
42
|
+
const payload = Math.min(limits.maxExtractedBytes, Number.MAX_SAFE_INTEGER);
|
|
43
|
+
const overhead = Math.min(limits.maxArchiveBytes, Number.MAX_SAFE_INTEGER);
|
|
44
|
+
// Saturate before multiplying, including when public limits approach MAX_VALUE.
|
|
45
|
+
const pathBytes = Math.max(256, Math.min(limits.maxMetaEntryBytes, Math.min(Math.max(1, limits.maxEntryPathComponents), MAX_TAR_MANIFEST_BYTES / 256) * 256));
|
|
46
|
+
const perEntry = 64 + 2 * pathBytes;
|
|
47
|
+
const maxManifestBytes = limits.maxEntries > Math.floor(MAX_TAR_MANIFEST_BYTES / perEntry)
|
|
48
|
+
? MAX_TAR_MANIFEST_BYTES : limits.maxEntries * perEntry;
|
|
49
|
+
// TAR sizes are safe integers; native logical entry counts use u32.
|
|
50
|
+
return {
|
|
51
|
+
maxEntries: Math.min(limits.maxEntries, 0xffff_ffff),
|
|
52
|
+
maxMetaEntryBytes: Math.min(limits.maxMetaEntryBytes, Number.MAX_SAFE_INTEGER),
|
|
53
|
+
maxManifestBytes,
|
|
54
|
+
maxDecodedBytes: payload > Number.MAX_SAFE_INTEGER - overhead
|
|
55
|
+
? Number.MAX_SAFE_INTEGER : payload + overhead,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
34
58
|
function clampLimit(value) {
|
|
35
59
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
36
60
|
return undefined;
|
package/dist/archive-native.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtractionDeadline } from "./archive-deadline.js";
|
|
2
2
|
import type { ArchiveKind } from "./archive-kind.js";
|
|
3
|
-
import { type
|
|
3
|
+
import { type ResolvedArchiveExtractLimits, type TarMeterLimits } from "./archive-limits.js";
|
|
4
4
|
import type { ExtractArchiveOptions } from "./archive-options.js";
|
|
5
5
|
import type { NativeBinding } from "./native.js";
|
|
6
6
|
export declare function extractNativeArchive(params: {
|
|
@@ -9,7 +9,8 @@ export declare function extractNativeArchive(params: {
|
|
|
9
9
|
destDir: string;
|
|
10
10
|
kind: ArchiveKind;
|
|
11
11
|
stripComponents?: number;
|
|
12
|
-
limits
|
|
12
|
+
limits: ResolvedArchiveExtractLimits;
|
|
13
|
+
tarLimits: TarMeterLimits;
|
|
13
14
|
deadline: ExtractionDeadline;
|
|
14
15
|
entryModes?: ExtractArchiveOptions["entryModes"];
|
|
15
16
|
entryFilter?: ExtractArchiveOptions["entryFilter"];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-native.d.ts","sourceRoot":"","sources":["../src/archive-native.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"archive-native.d.ts","sourceRoot":"","sources":["../src/archive-native.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAML,KAAK,4BAA4B,EACjC,KAAK,cAAc,EACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAOlE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA2BjD,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,OAAO,EAAE,aAAa,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,WAAW,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,4BAA4B,CAAC;IACrC,SAAS,EAAE,cAAc,CAAC;IAC1B,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,UAAU,CAAC,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC;IACjD,WAAW,CAAC,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAC;IACnD,UAAU,CAAC,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC;CAClD,GAAG,OAAO,CAAC,IAAI,CAAC,CAsIhB"}
|
package/dist/archive-native.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { constants as fsConstants } from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
-
import { ArchiveFormatError, ArchiveSecurityError, isArchiveFormatErrorMessage, } from "./archive-errors.js";
|
|
3
|
+
import { ArchiveFormatError, ArchiveSecurityError, isArchiveFormatErrorMessage, isArchiveTarPathErrorMessage, } from "./archive-errors.js";
|
|
4
4
|
import { formatErrorDetail } from "./error-detail.js";
|
|
5
5
|
import { createArchiveOutputPathTracker, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
|
|
6
6
|
import { stageArchiveFileForExtraction } from "./archive-input.js";
|
|
7
|
-
import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, assertArchiveEntryCountWithinLimit, assertArchiveEntryPathComponentsWithinLimit, createByteBudgetTracker,
|
|
7
|
+
import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, assertArchiveEntryCountWithinLimit, assertArchiveEntryPathComponentsWithinLimit, createByteBudgetTracker, } from "./archive-limits.js";
|
|
8
8
|
import { resolveArchiveEntryMode, shouldExtractArchiveEntry } from "./archive-policy.js";
|
|
9
9
|
import { mergeExtractedTreeIntoDestination, prepareArchiveDestinationDir, withStagedArchiveDestination, } from "./archive-staging.js";
|
|
10
10
|
import { admitZipFile } from "./archive-zip-admission.js";
|
|
@@ -17,6 +17,9 @@ function policyKind(kind) {
|
|
|
17
17
|
}
|
|
18
18
|
function throwMappedNativeError(error) {
|
|
19
19
|
if (error instanceof Error) {
|
|
20
|
+
if (isArchiveTarPathErrorMessage(error.message)) {
|
|
21
|
+
throw new ArchiveSecurityError("entry-path", error.message, { cause: error });
|
|
22
|
+
}
|
|
20
23
|
for (const code of Object.values(ARCHIVE_LIMIT_ERROR_CODE)) {
|
|
21
24
|
if (error.message.includes(code))
|
|
22
25
|
throw new ArchiveLimitError(code);
|
|
@@ -31,7 +34,7 @@ function throwMappedNativeError(error) {
|
|
|
31
34
|
throw error;
|
|
32
35
|
}
|
|
33
36
|
export async function extractNativeArchive(params) {
|
|
34
|
-
const limits =
|
|
37
|
+
const { limits, tarLimits } = params;
|
|
35
38
|
const stagedArchive = await stageArchiveFileForExtraction({
|
|
36
39
|
archivePath: params.archivePath,
|
|
37
40
|
limits,
|
|
@@ -47,13 +50,19 @@ export async function extractNativeArchive(params) {
|
|
|
47
50
|
run: async (stagingDir) => {
|
|
48
51
|
params.deadline.check();
|
|
49
52
|
const manifest = await params.binding
|
|
50
|
-
.inspectArchiveNative(stagedArchive.path, params.kind,
|
|
53
|
+
.inspectArchiveNative(stagedArchive.path, params.kind, tarLimits, params.deadline.signal)
|
|
51
54
|
.catch(throwMappedNativeError);
|
|
52
55
|
params.deadline.check();
|
|
53
56
|
assertArchiveEntryCountWithinLimit(manifest.length, limits);
|
|
54
57
|
if (physicalCount !== undefined && manifest.length !== physicalCount) {
|
|
55
58
|
throw new ArchiveSecurityError("entry-path", "zip decoder collapsed entry names");
|
|
56
59
|
}
|
|
60
|
+
// Recheck the native manifest at the shared policy boundary before
|
|
61
|
+
// any caller callback observes an entry.
|
|
62
|
+
if (params.kind !== "zip") {
|
|
63
|
+
for (const entry of manifest)
|
|
64
|
+
validateArchiveEntryPath(entry.path);
|
|
65
|
+
}
|
|
57
66
|
const strip = Math.max(0, Math.floor(params.stripComponents ?? 0));
|
|
58
67
|
const budget = createByteBudgetTracker(limits);
|
|
59
68
|
const trackOutputPath = createArchiveOutputPathTracker();
|
|
@@ -61,7 +70,10 @@ export async function extractNativeArchive(params) {
|
|
|
61
70
|
for (const entry of manifest) {
|
|
62
71
|
params.deadline.check();
|
|
63
72
|
validateArchiveEntryPath(entry.path);
|
|
64
|
-
const
|
|
73
|
+
const canonicalPath = stripArchivePath(entry.path, 0);
|
|
74
|
+
if (!canonicalPath)
|
|
75
|
+
continue;
|
|
76
|
+
const relPath = stripArchivePath(canonicalPath, strip);
|
|
65
77
|
if (!relPath)
|
|
66
78
|
continue;
|
|
67
79
|
validateArchiveEntryPath(relPath);
|
|
@@ -72,21 +84,22 @@ export async function extractNativeArchive(params) {
|
|
|
72
84
|
if (!shouldExtractArchiveEntry({
|
|
73
85
|
filter: params.entryFilter,
|
|
74
86
|
onFiltered: params.onFiltered,
|
|
75
|
-
entry: { path:
|
|
87
|
+
entry: { path: canonicalPath, kind, size: entry.size },
|
|
76
88
|
})) {
|
|
77
89
|
continue;
|
|
78
90
|
}
|
|
79
91
|
if (entry.kind === "sparse") {
|
|
80
92
|
throw new ArchiveFormatError(`GNU sparse archive entry is not supported: ${formatErrorDetail(entry.path)}`);
|
|
81
93
|
}
|
|
82
|
-
if (kind === "symlink") {
|
|
94
|
+
if (kind === "symlink" || entry.kind === "blocked") {
|
|
83
95
|
const label = params.kind === "zip" ? "zip" : "tar";
|
|
84
96
|
throw new ArchiveSecurityError("entry-link", `${label} entry is a link: ${formatErrorDetail(entry.path)}`);
|
|
85
97
|
}
|
|
86
98
|
if (!Number.isSafeInteger(entry.size) || entry.size < 0) {
|
|
87
99
|
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
|
|
88
100
|
}
|
|
89
|
-
|
|
101
|
+
// GNUDumpDir can carry a body; charge accepted TAR sizes as JS does.
|
|
102
|
+
if (kind === "file" || (kind === "directory" && params.kind !== "zip")) {
|
|
90
103
|
budget.startEntry();
|
|
91
104
|
budget.addEntrySize(entry.size);
|
|
92
105
|
}
|
|
@@ -108,7 +121,7 @@ export async function extractNativeArchive(params) {
|
|
|
108
121
|
(typeof fsConstants.O_DIRECTORY === "number" ? fsConstants.O_DIRECTORY : 0));
|
|
109
122
|
try {
|
|
110
123
|
params.deadline.check();
|
|
111
|
-
await params.binding.extractArchiveNative(stagedArchive.path, params.kind, directory.fd, plan,
|
|
124
|
+
await params.binding.extractArchiveNative(stagedArchive.path, params.kind, directory.fd, plan, tarLimits, params.deadline.signal).catch(throwMappedNativeError);
|
|
112
125
|
}
|
|
113
126
|
finally {
|
|
114
127
|
await directory.close().catch(() => undefined);
|
|
@@ -118,7 +131,9 @@ export async function extractNativeArchive(params) {
|
|
|
118
131
|
sourceDir: stagingDir,
|
|
119
132
|
destinationDir: params.destDir,
|
|
120
133
|
destinationRealDir,
|
|
134
|
+
deadline: params.deadline,
|
|
121
135
|
});
|
|
136
|
+
params.deadline.check();
|
|
122
137
|
},
|
|
123
138
|
});
|
|
124
139
|
}
|
package/dist/archive-policy.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export type ArchiveEntryKind = "file" | "directory" | "symlink" | "other";
|
|
|
2
2
|
export type ArchiveEntryModePolicy = "clamp" | "preserve";
|
|
3
3
|
export type ArchiveFilteredEntryPolicy = "reject-archive" | "skip-entry";
|
|
4
4
|
export type ArchiveEntryFilter = (entry: {
|
|
5
|
+
/** Validated canonical archive path before stripping: / separators, no empty or . components. */
|
|
5
6
|
path: string;
|
|
6
7
|
kind: ArchiveEntryKind;
|
|
7
8
|
size: number;
|
|
@@ -12,6 +13,7 @@ export declare function resolveArchiveEntryMode(params: {
|
|
|
12
13
|
archivedMode?: number;
|
|
13
14
|
policy?: ArchiveEntryModePolicy;
|
|
14
15
|
}): number;
|
|
16
|
+
export declare function resolveArchiveFilteredEntryPolicy(value: unknown): ArchiveFilteredEntryPolicy;
|
|
15
17
|
export declare function shouldExtractArchiveEntry(params: {
|
|
16
18
|
filter?: ArchiveEntryFilter;
|
|
17
19
|
onFiltered?: ArchiveFilteredEntryPolicy;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-policy.d.ts","sourceRoot":"","sources":["../src/archive-policy.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AAC1E,MAAM,MAAM,sBAAsB,GAAG,OAAO,GAAG,UAAU,CAAC;AAC1D,MAAM,MAAM,0BAA0B,GAAG,gBAAgB,GAAG,YAAY,CAAC;AACzE,MAAM,MAAM,kBAAkB,GAAG,CAAC,KAAK,EAAE;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;CACd,KAAK,SAAS,GAAG,MAAM,CAAC;AAEzB,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAK1E;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,sBAAsB,CAAC;CACjC,GAAG,MAAM,CAST;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE;IAChD,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,UAAU,CAAC,EAAE,0BAA0B,CAAC;IACxC,KAAK,EAAE,UAAU,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC;CAC1C,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"archive-policy.d.ts","sourceRoot":"","sources":["../src/archive-policy.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AAC1E,MAAM,MAAM,sBAAsB,GAAG,OAAO,GAAG,UAAU,CAAC;AAC1D,MAAM,MAAM,0BAA0B,GAAG,gBAAgB,GAAG,YAAY,CAAC;AACzE,MAAM,MAAM,kBAAkB,GAAG,CAAC,KAAK,EAAE;IACvC,iGAAiG;IACjG,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;CACd,KAAK,SAAS,GAAG,MAAM,CAAC;AAEzB,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAK1E;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,sBAAsB,CAAC;CACjC,GAAG,MAAM,CAST;AAED,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,OAAO,GACb,0BAA0B,CAM5B;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE;IAChD,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,UAAU,CAAC,EAAE,0BAA0B,CAAC;IACxC,KAAK,EAAE,UAAU,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC;CAC1C,GAAG,OAAO,CAYV"}
|
package/dist/archive-policy.js
CHANGED
|
@@ -19,11 +19,19 @@ export function resolveArchiveEntryMode(params) {
|
|
|
19
19
|
}
|
|
20
20
|
return archivedMode & 0o100 ? 0o755 : 0o644;
|
|
21
21
|
}
|
|
22
|
+
export function resolveArchiveFilteredEntryPolicy(value) {
|
|
23
|
+
if (value === undefined || value === "reject-archive")
|
|
24
|
+
return "reject-archive";
|
|
25
|
+
if (value === "skip-entry")
|
|
26
|
+
return "skip-entry";
|
|
27
|
+
throw new RangeError('archive onFiltered must be "reject-archive" or "skip-entry"');
|
|
28
|
+
}
|
|
22
29
|
export function shouldExtractArchiveEntry(params) {
|
|
30
|
+
const onFiltered = resolveArchiveFilteredEntryPolicy(params.onFiltered);
|
|
23
31
|
if (!params.filter || params.filter(params.entry) === "extract") {
|
|
24
32
|
return true;
|
|
25
33
|
}
|
|
26
|
-
if (
|
|
34
|
+
if (onFiltered === "reject-archive") {
|
|
27
35
|
throw new ArchiveSecurityError("entry-filtered", `archive entry rejected by filter: ${formatErrorDetail(params.entry.path)}`);
|
|
28
36
|
}
|
|
29
37
|
return false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-read.d.ts","sourceRoot":"","sources":["../src/archive-read.ts"],"names":[],"mappings":"AAcA,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"archive-read.d.ts","sourceRoot":"","sources":["../src/archive-read.ts"],"names":[],"mappings":"AAcA,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAgOzE,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAAE,GAChD,OAAO,CAAC,MAAM,CAAC,CAuFjB"}
|