@openclaw/fs-safe 0.5.6 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +23 -10
  3. package/dist/advanced.d.ts +2 -1
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +1 -0
  6. package/dist/archive-crc32.d.ts +2 -0
  7. package/dist/archive-crc32.d.ts.map +1 -0
  8. package/dist/archive-crc32.js +14 -0
  9. package/dist/archive-deadline.d.ts +3 -0
  10. package/dist/archive-deadline.d.ts.map +1 -1
  11. package/dist/archive-deadline.js +44 -8
  12. package/dist/archive-entry.d.ts.map +1 -1
  13. package/dist/archive-entry.js +1 -0
  14. package/dist/archive-errors.d.ts +1 -0
  15. package/dist/archive-errors.d.ts.map +1 -1
  16. package/dist/archive-errors.js +3 -0
  17. package/dist/archive-input.d.ts.map +1 -1
  18. package/dist/archive-input.js +26 -16
  19. package/dist/archive-kind.js +2 -2
  20. package/dist/archive-limits.d.ts +11 -3
  21. package/dist/archive-limits.d.ts.map +1 -1
  22. package/dist/archive-limits.js +24 -0
  23. package/dist/archive-native.d.ts +3 -2
  24. package/dist/archive-native.d.ts.map +1 -1
  25. package/dist/archive-native.js +31 -9
  26. package/dist/archive-policy.d.ts +2 -0
  27. package/dist/archive-policy.d.ts.map +1 -1
  28. package/dist/archive-policy.js +9 -1
  29. package/dist/archive-read.d.ts.map +1 -1
  30. package/dist/archive-read.js +88 -41
  31. package/dist/archive-staging.d.ts +3 -0
  32. package/dist/archive-staging.d.ts.map +1 -1
  33. package/dist/archive-staging.js +91 -43
  34. package/dist/archive-tar-admission.d.ts +7 -0
  35. package/dist/archive-tar-admission.d.ts.map +1 -0
  36. package/dist/archive-tar-admission.js +43 -0
  37. package/dist/archive-tar-gnu.d.ts +2 -0
  38. package/dist/archive-tar-gnu.d.ts.map +1 -0
  39. package/dist/archive-tar-gnu.js +20 -0
  40. package/dist/archive-tar-header.d.ts +8 -0
  41. package/dist/archive-tar-header.d.ts.map +1 -0
  42. package/dist/archive-tar-header.js +47 -0
  43. package/dist/archive-tar-meta.d.ts +29 -1
  44. package/dist/archive-tar-meta.d.ts.map +1 -1
  45. package/dist/archive-tar-meta.js +135 -19
  46. package/dist/archive-tar-pax.d.ts +8 -0
  47. package/dist/archive-tar-pax.d.ts.map +1 -0
  48. package/dist/archive-tar-pax.js +100 -0
  49. package/dist/archive-tar-runtime.d.ts +4 -0
  50. package/dist/archive-tar-runtime.d.ts.map +1 -1
  51. package/dist/archive-tar-runtime.js +3 -0
  52. package/dist/archive-tar.d.ts.map +1 -1
  53. package/dist/archive-tar.js +8 -2
  54. package/dist/archive-zip-admission.d.ts +7 -0
  55. package/dist/archive-zip-admission.d.ts.map +1 -0
  56. package/dist/archive-zip-admission.js +60 -0
  57. package/dist/archive-zip-count.d.ts +2 -0
  58. package/dist/archive-zip-count.d.ts.map +1 -0
  59. package/dist/archive-zip-count.js +140 -0
  60. package/dist/archive-zip-directory.d.ts +8 -0
  61. package/dist/archive-zip-directory.d.ts.map +1 -0
  62. package/dist/archive-zip-directory.js +224 -0
  63. package/dist/archive-zip-integrity.d.ts.map +1 -1
  64. package/dist/archive-zip-integrity.js +1 -14
  65. package/dist/archive-zip-names.d.ts +12 -0
  66. package/dist/archive-zip-names.d.ts.map +1 -0
  67. package/dist/archive-zip-names.js +99 -0
  68. package/dist/archive-zip-preflight.d.ts +1 -1
  69. package/dist/archive-zip-preflight.d.ts.map +1 -1
  70. package/dist/archive-zip-preflight.js +8 -145
  71. package/dist/archive.d.ts.map +1 -1
  72. package/dist/archive.js +63 -42
  73. package/dist/atomic.d.ts +1 -1
  74. package/dist/atomic.d.ts.map +1 -1
  75. package/dist/bounded-read-stream.d.ts.map +1 -1
  76. package/dist/bounded-read-stream.js +2 -4
  77. package/dist/bounded-read.d.ts.map +1 -1
  78. package/dist/bounded-read.js +3 -10
  79. package/dist/byte-budget.d.ts +5 -0
  80. package/dist/byte-budget.d.ts.map +1 -0
  81. package/dist/byte-budget.js +9 -0
  82. package/dist/file-hash.d.ts.map +1 -1
  83. package/dist/file-hash.js +25 -19
  84. package/dist/file-lock-sync.d.ts.map +1 -1
  85. package/dist/file-lock-sync.js +26 -9
  86. package/dist/file-store-boundary.d.ts.map +1 -1
  87. package/dist/file-store-boundary.js +9 -5
  88. package/dist/file-store-limit.d.ts +2 -0
  89. package/dist/file-store-limit.d.ts.map +1 -0
  90. package/dist/file-store-limit.js +8 -0
  91. package/dist/file-store-sync-write.d.ts.map +1 -1
  92. package/dist/file-store-sync-write.js +38 -8
  93. package/dist/file-store.d.ts.map +1 -1
  94. package/dist/file-store.js +26 -26
  95. package/dist/json-durable-queue-directory.d.ts +2 -0
  96. package/dist/json-durable-queue-directory.d.ts.map +1 -0
  97. package/dist/json-durable-queue-directory.js +20 -0
  98. package/dist/json-durable-queue-ownership.d.ts +14 -0
  99. package/dist/json-durable-queue-ownership.d.ts.map +1 -0
  100. package/dist/json-durable-queue-ownership.js +168 -0
  101. package/dist/json-durable-queue-retirement.d.ts +9 -0
  102. package/dist/json-durable-queue-retirement.d.ts.map +1 -0
  103. package/dist/json-durable-queue-retirement.js +126 -0
  104. package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
  105. package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
  106. package/dist/json-durable-queue-transfer-lock.js +19 -0
  107. package/dist/json-durable-queue.d.ts +1 -0
  108. package/dist/json-durable-queue.d.ts.map +1 -1
  109. package/dist/json-durable-queue.js +90 -57
  110. package/dist/json.d.ts.map +1 -1
  111. package/dist/json.js +27 -8
  112. package/dist/local-roots.d.ts.map +1 -1
  113. package/dist/local-roots.js +4 -2
  114. package/dist/native-binding.d.ts +15 -3
  115. package/dist/native-binding.d.ts.map +1 -1
  116. package/dist/native-operations.d.ts +4 -1
  117. package/dist/native-operations.d.ts.map +1 -1
  118. package/dist/native-operations.js +22 -6
  119. package/dist/native-pinned-write-windows.d.ts +8 -0
  120. package/dist/native-pinned-write-windows.d.ts.map +1 -0
  121. package/dist/native-pinned-write-windows.js +92 -0
  122. package/dist/native-pinned-write.d.ts.map +1 -1
  123. package/dist/native-pinned-write.js +136 -127
  124. package/dist/native-staged-file.d.ts +24 -0
  125. package/dist/native-staged-file.d.ts.map +1 -0
  126. package/dist/native-staged-file.js +337 -0
  127. package/dist/native.d.ts.map +1 -1
  128. package/dist/native.js +4 -4
  129. package/dist/opened-realpath.d.ts +2 -0
  130. package/dist/opened-realpath.d.ts.map +1 -1
  131. package/dist/opened-realpath.js +12 -7
  132. package/dist/output-sibling.d.ts.map +1 -1
  133. package/dist/output-sibling.js +11 -110
  134. package/dist/output.d.ts.map +1 -1
  135. package/dist/output.js +4 -2
  136. package/dist/owner-dacl.d.ts.map +1 -1
  137. package/dist/owner-dacl.js +2 -1
  138. package/dist/permission-exec.d.ts +19 -0
  139. package/dist/permission-exec.d.ts.map +1 -1
  140. package/dist/permission-exec.js +57 -11
  141. package/dist/permissions-public.d.ts +1 -1
  142. package/dist/permissions-public.d.ts.map +1 -1
  143. package/dist/permissions-windows.d.ts +3 -0
  144. package/dist/permissions-windows.d.ts.map +1 -1
  145. package/dist/permissions-windows.js +16 -5
  146. package/dist/permissions.d.ts +5 -0
  147. package/dist/permissions.d.ts.map +1 -1
  148. package/dist/pinned-open.d.ts.map +1 -1
  149. package/dist/pinned-open.js +27 -48
  150. package/dist/pinned-write.d.ts +6 -0
  151. package/dist/pinned-write.d.ts.map +1 -1
  152. package/dist/pinned-write.js +26 -37
  153. package/dist/private-directory.d.ts.map +1 -1
  154. package/dist/private-directory.js +3 -2
  155. package/dist/private-temp-workspace.d.ts +3 -1
  156. package/dist/private-temp-workspace.d.ts.map +1 -1
  157. package/dist/private-temp-workspace.js +81 -56
  158. package/dist/publish-file.d.ts.map +1 -1
  159. package/dist/publish-file.js +2 -4
  160. package/dist/read-opened-file.d.ts.map +1 -1
  161. package/dist/read-opened-file.js +6 -4
  162. package/dist/regular-file.d.ts.map +1 -1
  163. package/dist/regular-file.js +136 -82
  164. package/dist/replace-file-copy-fallback.d.ts +3 -1
  165. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  166. package/dist/replace-file-copy-fallback.js +21 -36
  167. package/dist/replace-file-copy-source.d.ts +21 -0
  168. package/dist/replace-file-copy-source.d.ts.map +1 -0
  169. package/dist/replace-file-copy-source.js +112 -0
  170. package/dist/replace-file-descriptor.d.ts +13 -3
  171. package/dist/replace-file-descriptor.d.ts.map +1 -1
  172. package/dist/replace-file-descriptor.js +32 -7
  173. package/dist/replace-file-rename-policy.d.ts +7 -0
  174. package/dist/replace-file-rename-policy.d.ts.map +1 -0
  175. package/dist/replace-file-rename-policy.js +30 -0
  176. package/dist/replace-file-temp-owner.d.ts +46 -0
  177. package/dist/replace-file-temp-owner.d.ts.map +1 -0
  178. package/dist/replace-file-temp-owner.js +346 -0
  179. package/dist/replace-file.d.ts +6 -1
  180. package/dist/replace-file.d.ts.map +1 -1
  181. package/dist/replace-file.js +72 -58
  182. package/dist/root-impl.d.ts.map +1 -1
  183. package/dist/root-impl.js +118 -98
  184. package/dist/root-paths.d.ts +11 -14
  185. package/dist/root-paths.d.ts.map +1 -1
  186. package/dist/root-paths.js +36 -27
  187. package/dist/root-write-verification.d.ts +11 -0
  188. package/dist/root-write-verification.d.ts.map +1 -0
  189. package/dist/root-write-verification.js +91 -0
  190. package/dist/secret-file.d.ts +1 -6
  191. package/dist/secret-file.d.ts.map +1 -1
  192. package/dist/secret-file.js +49 -120
  193. package/dist/secret-read-async.d.ts +1 -1
  194. package/dist/secret-read-async.d.ts.map +1 -1
  195. package/dist/secret-read-async.js +51 -72
  196. package/dist/secret-read-policy.d.ts +13 -0
  197. package/dist/secret-read-policy.d.ts.map +1 -0
  198. package/dist/secret-read-policy.js +28 -0
  199. package/dist/secret.d.ts +2 -1
  200. package/dist/secret.d.ts.map +1 -1
  201. package/dist/secret.js +2 -1
  202. package/dist/secure-file.d.ts.map +1 -1
  203. package/dist/secure-file.js +42 -29
  204. package/dist/sibling-staged-file.d.ts +15 -0
  205. package/dist/sibling-staged-file.d.ts.map +1 -0
  206. package/dist/sibling-staged-file.js +148 -0
  207. package/dist/sibling-temp.d.ts +3 -0
  208. package/dist/sibling-temp.d.ts.map +1 -1
  209. package/dist/sibling-temp.js +30 -74
  210. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  211. package/dist/sidecar-lock-acquire.js +48 -27
  212. package/dist/sidecar-lock-handle.d.ts +6 -2
  213. package/dist/sidecar-lock-handle.d.ts.map +1 -1
  214. package/dist/sidecar-lock-handle.js +17 -3
  215. package/dist/sidecar-lock-policy.d.ts +2 -0
  216. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  217. package/dist/sidecar-lock-policy.js +29 -0
  218. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  219. package/dist/sidecar-lock-reclaim.js +29 -7
  220. package/dist/sidecar-lock.d.ts.map +1 -1
  221. package/dist/sidecar-lock.js +28 -16
  222. package/dist/staged-directory.d.ts +16 -0
  223. package/dist/staged-directory.d.ts.map +1 -0
  224. package/dist/staged-directory.js +60 -0
  225. package/dist/staged-file-types.d.ts +56 -0
  226. package/dist/staged-file-types.d.ts.map +1 -0
  227. package/dist/staged-file-types.js +1 -0
  228. package/dist/staged-file.d.ts +10 -0
  229. package/dist/staged-file.d.ts.map +1 -0
  230. package/dist/staged-file.js +15 -0
  231. package/dist/strict-file-identity.d.ts +6 -0
  232. package/dist/strict-file-identity.d.ts.map +1 -0
  233. package/dist/strict-file-identity.js +48 -0
  234. package/dist/suppressed-error.d.ts +6 -0
  235. package/dist/suppressed-error.d.ts.map +1 -0
  236. package/dist/suppressed-error.js +15 -0
  237. package/dist/temp-cleanup.d.ts +2 -0
  238. package/dist/temp-cleanup.d.ts.map +1 -1
  239. package/dist/temp-cleanup.js +25 -10
  240. package/dist/temp-workspace-owner.d.ts +23 -0
  241. package/dist/temp-workspace-owner.d.ts.map +1 -0
  242. package/dist/temp-workspace-owner.js +320 -0
  243. package/dist/temp.d.ts +1 -1
  244. package/dist/temp.d.ts.map +1 -1
  245. package/dist/test-hooks.d.ts +5 -0
  246. package/dist/test-hooks.d.ts.map +1 -1
  247. package/dist/windows-owner.d.ts +3 -0
  248. package/dist/windows-owner.d.ts.map +1 -1
  249. package/dist/windows-owner.js +10 -2
  250. package/docs/advanced.md +19 -2
  251. package/docs/archive.md +250 -35
  252. package/docs/atomic.md +11 -2
  253. package/docs/config.md +7 -0
  254. package/docs/contributing.md +45 -7
  255. package/docs/durability.md +18 -5
  256. package/docs/errors.md +16 -1
  257. package/docs/file-store.md +2 -0
  258. package/docs/index.md +3 -1
  259. package/docs/install.md +20 -8
  260. package/docs/json.md +8 -4
  261. package/docs/migrating-to-0.5.md +7 -7
  262. package/docs/migrating-to-0.6.md +43 -0
  263. package/docs/native-helper.md +30 -8
  264. package/docs/native.md +73 -17
  265. package/docs/output.md +10 -0
  266. package/docs/path-scope.md +28 -2
  267. package/docs/permissions.md +13 -2
  268. package/docs/public-api.md +6 -3
  269. package/docs/quickstart.md +1 -1
  270. package/docs/reading.md +1 -1
  271. package/docs/regular-file.md +9 -2
  272. package/docs/root.md +3 -1
  273. package/docs/secret-file.md +12 -0
  274. package/docs/secure-file.md +21 -3
  275. package/docs/security-model.md +33 -1
  276. package/docs/sidecar-lock.md +15 -1
  277. package/docs/staged-file.md +178 -0
  278. package/docs/store.md +13 -0
  279. package/docs/temp.md +128 -14
  280. package/docs/testing.md +1 -1
  281. package/docs/writing.md +19 -0
  282. package/package.json +16 -9
  283. package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
  284. package/dist/native/darwin-x64/fs-safe-native.node +0 -0
  285. package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
  286. package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
  287. package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
  288. package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
  289. package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,94 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0 - 2026-08-31
4
+
5
+ ### Highlights
6
+
7
+ - **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.
8
+ - **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).
9
+ - **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.
10
+ - **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.
11
+ - **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.
12
+
13
+ ### Compatibility and upgrade notes
14
+
15
+ - **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).
16
+ - **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.
17
+ - **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.
18
+ - **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`.
19
+
20
+ ### File safety and temp cleanup
21
+
22
+ - 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.
23
+ - 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.
24
+ - 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.
25
+ - 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.
26
+ - 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.
27
+ - 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.
28
+ - 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.
29
+ - 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.
30
+
31
+ ### Archive extraction and reads
32
+
33
+ - 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.
34
+ - 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.
35
+ - 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.
36
+ - 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.
37
+ - 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.
38
+ - 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.
39
+ - 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.
40
+ - 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.
41
+ - 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.
42
+ - 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.
43
+ - 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.
44
+
45
+ ### Durable queues, stores, and locks
46
+
47
+ - 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.
48
+ - 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.
49
+ - 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.
50
+ - 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.
51
+ - Clamp synchronous lock backoff to the remaining finite deadline instead of overshooting the timeout or blocking forever.
52
+
53
+ ## 0.6.0 - 2026-08-29
54
+
55
+ ### Highlights
56
+
57
+ - Install only the matching native platform package; deployments using native mode `require` or native-only features must keep optional dependencies enabled. See the [0.6 migration guide](https://fs-safe.io/migrating-to-0.6.html).
58
+ - Strengthen root, secure, secret, and pathname-hash identity checks with lossless bigint comparisons and fail-closed handling of unknown Windows identities.
59
+ - Add native-required Linux/macOS `stageFileInDirectory()` with retained-directory cleanup, private staging, identity-checked publication, and async disposal.
60
+ - Validate physical ZIP metadata and names before decoder normalization or collapse, align bounded TAR/PAX and stripped-path handling, and reject ZIP symlinks disguised as directories.
61
+
62
+ ### Security and Correctness
63
+
64
+ - Validate physical ZIP local/central and Unicode name metadata before decoding, rejecting traversal, hidden duplicates, and conflicting interpretations consistently in extraction and bounded reads.
65
+
66
+ - Verify pathname SHA-256 hashing with lossless pre-open, descriptor, and current-path identities; fail closed on unknown Windows identities after one bounded retry without reopening the file.
67
+ - Preserve underlying command diagnostics (command, timing, timeout flag, exit code/signal, and sanitized stderr) and cause on Windows ACL `permission-unverified` errors without changing verification semantics.
68
+ - Verify secure and secret read identities with lossless bigint stats, reject replaced paths and retargeted aliases, and fail closed on unknown Windows identities after one bounded re-inspection; preserve the secure reader's numeric `Stats` receipt and permission options without letting them bypass identity checks.
69
+ - Apply the same exact identity checks to guarded root reads, rejecting parent-directory replacements even when distinct Windows file IDs round to the same number; retain numeric read receipts and keep Windows write reopens anchored to the writer's exact retained identity.
70
+ - Verify `Root.write()` and `create()` through retained descriptors and exact bigint identities for restrictive final modes, retaining guarded Windows path opens when pathname identity is unavailable; preserve explicit mode zero in `copyIn()`.
71
+ - Preserve filesystem failures from `ensureDirectoryWithinRoot()` and `pathScope().ensureDir()` in an optional operational `FsSafeError` diagnostic with the original cause and bounded, escaped display text, instead of misreporting them as containment violations; keep nonthrowing string results and directory safety checks.
72
+ - Accept bounded local PAX paths, sizes, and descriptive metadata, including inert binary macOS provenance xattrs, consistently in JavaScript and native TAR extraction/reads; reject ambiguous records, extension chains, and sparse semantics while retaining byte/count limits and guarded staging. Return TAR read traversal failures through the public promise instead of escaping the parser callback.
73
+ - Add native-required Linux/macOS `stageFileInDirectory()` under `advanced` with retained-directory abort cleanup, private `0600` staging until publication is identity-checked, exact identity checks, explicit publication/cleanup receipts, and async disposal; share that ownership with POSIX native streaming writes so parent moves no longer strand their unpublished temps.
74
+ - Share native writer admission and direct-child cleanup mechanisms; preserve combined POSIX coordinator operation/disposal failures with `SuppressedError` while keeping staged preparation/cleanup receipts and Windows close policy unchanged.
75
+ - Fail closed when synchronous sidecar compromise checks hit I/O errors and invoke `onCompromised` once instead of throwing from the interval. Thanks @SebTardif.
76
+ - Reject ZIP symlink-mode entries with a trailing slash or DOS directory bit before creating output, aligning JavaScript extraction with native link policy while preserving explicit filtering. Thanks @Yigtwxx.
77
+ - Make the validated stripped path authoritative during JavaScript TAR extraction, fixing `ENOENT` and native path disagreement for dot or empty components, including local PAX paths, while preserving pre-strip filter inputs. Thanks @Yigtwxx.
78
+ - Apply process-wide retry, timeout, and stale-policy defaults to synchronous file locks while preserving per-call overrides and caller-approved guarded recovery. Thanks @Yigtwxx.
79
+
80
+ ### Docs and Tooling
81
+
82
+ - Publish native bindings as platform-filtered optional packages and load only the matching package, avoiding installation of binaries for six unrelated targets; verify root-only npm/pnpm resolution and document omitted-optionals limits and native-only recovery guidance. Thanks @RomneyDa.
83
+ - Raise the Crabbox AWS root volume to 400 GiB to meet the runner image's snapshot minimum and avoid allocation failures before checks run.
84
+ - Restore six missing documentation navigation entries, remove the dangling page, correct the lock-config link, and reject missing, nonexistent, or duplicate registrations before replacing site output. Thanks @Yigtwxx.
85
+ - Fix the standalone native smoke script to use the numeric descriptor returned by `openBeneath()` for identity checks and cleanup.
86
+ - Accept npm 11 array and npm 12 package-name-keyed pack results while validating the intended package and its file metadata.
87
+
88
+ ### Dependencies and maintenance
89
+
90
+ - Refresh the Node and Rust dependency graphs, align development and CI on pnpm 11.24.0, and update the CodeQL, npm, and cargo-zigbuild pins while preserving the two-day dependency cooldown.
91
+
3
92
  ## 0.5.6 - 2026-08-14
4
93
 
5
94
  ### Security and Correctness
package/README.md CHANGED
@@ -26,7 +26,7 @@ Full docs and reference at **[fs-safe.io](https://fs-safe.io)**.
26
26
 
27
27
  ## Contents
28
28
 
29
- [Why this exists](#why-this-exists) · [Not a sandbox](#not-a-sandbox) · [Install](#install) · [0.5 migration](docs/migrating-to-0.5.md) · [Python migration](#migrating-from-the-python-helper) · [Quick start](#quick-start) · [Reading](#reading) · [Subpaths](#subpaths) · [Failure semantics](#failure-semantics-in-the-name) · [Directory durability](#directory-durability) · [Atomic writes](#atomic-writes) · [External outputs](#external-outputs) · [Stores](#stores) · [Secure absolute reads](#secure-absolute-file-reads) · [Walking](#directory-walking) · [Archive extraction](#archive-extraction) · [Path scopes](#advanced-path-scopes) · [Errors](#errors) · [Safety model](#safety-model) · [Limitations](#limitations)
29
+ [Why this exists](#why-this-exists) · [Not a sandbox](#not-a-sandbox) · [Install](#install) · [0.6 migration](docs/migrating-to-0.6.md) · [Python migration](#migrating-from-the-python-helper) · [Quick start](#quick-start) · [Reading](#reading) · [Subpaths](#subpaths) · [Failure semantics](#failure-semantics-in-the-name) · [Directory durability](#directory-durability) · [Atomic writes](#atomic-writes) · [External outputs](#external-outputs) · [Stores](#stores) · [Secure absolute reads](#secure-absolute-file-reads) · [Walking](#directory-walking) · [Archive extraction](#archive-extraction) · [Path scopes](#advanced-path-scopes) · [Errors](#errors) · [Safety model](#safety-model) · [Limitations](#limitations)
30
30
 
31
31
  ## Why this exists
32
32
 
@@ -57,10 +57,10 @@ This is a **library-level guardrail**, not OS-level isolation. It does not repla
57
57
  pnpm add @openclaw/fs-safe
58
58
  ```
59
59
 
60
- Node 22 or newer. Core root/path/json/temp helpers avoid framework dependencies. Archive helpers use optional `jszip` and `tar` dependencies for ZIP/TAR support; installs that omit optional dependencies can still use every non-archive subpath.
60
+ Node 22 or newer. Core root/path/json/temp helpers avoid framework dependencies. With all optional dependencies omitted, public subpaths remain safe to import and non-archive fallback-capable operations work in `auto` or `off`. Native-only features remain unavailable, and operations needing the binding in `require` mode fail with `helper-unavailable`. JavaScript ZIP/TAR fallback also needs the optional `jszip`/`tar` codecs. See the [0.6 migration guide](docs/migrating-to-0.6.md).
61
61
 
62
- The package bundles prebuilt native bindings for seven supported targets. They
63
- supply fd-relative and atomic no-replace primitives that Node does not expose
62
+ The package installs one prebuilt native binding for the current supported target. It
63
+ supplies fd-relative and atomic no-replace primitives that Node does not expose
64
64
  directly. Configure the lazy loader before first use when you need a strict
65
65
  environment policy:
66
66
 
@@ -80,11 +80,11 @@ replace a writable parent between its identity check and Node's pathname
80
80
  mutation can redirect that mutation outside the root before the post-check
81
81
  reports the escape. Use `require` when hostile concurrent mutation is in scope.
82
82
 
83
- Equivalent env var: `FS_SAFE_NATIVE_MODE=auto|off|require`. All seven binaries
84
- ship inside `@openclaw/fs-safe`; there are no platform packages, postinstall
85
- steps, downloads, or consumer Rust builds. This makes the tarball larger than
86
- a per-platform package, but makes installation deterministic. On a platform
87
- without a bundled binary, `auto` silently retains lexical and canonical root
83
+ Equivalent env var: `FS_SAFE_NATIVE_MODE=auto|off|require`. The seven bindings
84
+ ship as exact-version optional packages filtered by OS, CPU, and Linux libc, so
85
+ a normal install receives only its matching binary. There are no postinstall
86
+ steps, runtime downloads, or consumer Rust builds. On a platform without a
87
+ published binding, or when optional dependencies are omitted, `auto` silently retains lexical and canonical root
88
88
  checks, no-follow opens, guarded temp+rename writes, and post-write identity
89
89
  verification. See the [native
90
90
  helper policy](docs/native-helper.md) for the exact boundary and deployment
@@ -97,7 +97,7 @@ and guarded JavaScript results are best-effort. See the [security model](docs/se
97
97
 
98
98
  ## Migrating from the Python helper
99
99
 
100
- Version 0.5 replaces the persistent Python worker with bundled prebuilt native
100
+ Version 0.5 replaces the persistent Python worker with prebuilt native
101
101
  bindings. The modes map directly: `configureFsSafePython({ mode: "auto" })`
102
102
  becomes `configureFsSafeNative({ mode: "auto" })`, and likewise for `off` and
103
103
  `require`. Replace `FS_SAFE_PYTHON_MODE` with `FS_SAFE_NATIVE_MODE`; remove
@@ -284,6 +284,12 @@ publication policy, creation callback, and platform contract.
284
284
 
285
285
  ## Atomic writes
286
286
 
287
+ For preparation that must survive a parent rename until abort cleanup, use
288
+ [`stageFileInDirectory()`](docs/staged-file.md) from `advanced`. It retains the
289
+ original directory on Linux/macOS and requires native support for this operation.
290
+ It offers atomic replace/no-replace publication, not expected-inode replacement
291
+ or a crash-durability promise; application checks and coordination remain yours.
292
+
287
293
  `replaceFileAtomic()` writes a sibling temp file, applies its exact mode through the still-open descriptor, optionally fsyncs it, and renames it over the destination. It never follows the published destination path to set file permissions. Mode preservation, pinned-destination hardlink rejection, rename retry / copy fallback on `EPERM`, bounded original-content restoration after a torn fallback, parent-directory fsync, and a `beforeRename` hook for backup or observer flows are all opt-in. `movePathWithCopyFallback()` stages cross-device moves before commit and removes only the copied source entries, so concurrent source additions or replacements are preserved.
288
294
 
289
295
  ```ts
@@ -387,6 +393,13 @@ cleanup. Retry, dedupe, and transport semantics stay with the caller.
387
393
  `tempWorkspace()` exposes `write()`, `writeText()`, `writeJson()`, `copyIn()`, and `read()` for
388
394
  single-file scratch workflows without hand-rolled path joins, plus a `store: FileStore` view of
389
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.
390
403
 
391
404
  `tempFile()` is the smaller one-file temp helper. It is intentionally an
392
405
  advanced primitive: use `tempWorkspace()` for the stable temp surface and reach
@@ -1,4 +1,5 @@
1
1
  export { createAsyncLock } from "./async-lock.js";
2
+ export { stageFileInDirectory, type StagedFile, type StagedFileReceipt, type PublishedFileReceipt, type StagedFilePublication, type StagedFileCleanupReceipt, type StagedFileFailureDetails, } from "./staged-file.js";
2
3
  export { readFileDescriptorBounded, readFileDescriptorBoundedSync, readFileHandleBounded, } from "./bounded-read.js";
3
4
  export { assertNoUnsafeDeviceReadPath, isUnsafeDeviceReadPath, matchUnsafeDeviceReadPath, type UnsafeDeviceReadPathMatch, type UnsafeDeviceReadPathOptions, type UnsafeDeviceReadPathReason, } from "./device-path.js";
4
5
  export { assertAbsolutePathInput, canonicalPathFromExistingAncestor, ensureAbsoluteDirectory, findExistingAncestor, resolveAbsolutePathForRead, resolveAbsolutePathForWrite, type AbsolutePathSymlinkPolicy, type EnsureAbsoluteDirectoryOptions, type EnsureAbsoluteDirectoryResult, type ResolvedAbsolutePath, type ResolvedWritableAbsolutePath, } from "./absolute-path.js";
@@ -21,5 +22,5 @@ export { resolveHomeRelativePath } from "./home-dir.js";
21
22
  export { appendRegularFile, appendRegularFileSync, readRegularFile, readRegularFileSync, resolveRegularFileAppendFlags, statRegularFile, statRegularFileSync, type AppendRegularFileOptions, type RegularFileStatResult, } from "./regular-file.js";
22
23
  export { buildRandomTempFilePath, sanitizeTempFileName, type TempFile, tempFile, withTempFile, } from "./temp-target.js";
23
24
  export { writeSiblingTempFile, writeViaSiblingTempPath, type WriteSiblingTempFileOptions, type WriteSiblingTempFileResult, } from "./sibling-temp.js";
24
- export { createIcaclsResetCommand, formatIcaclsResetCommand, formatWindowsAclSummary, inspectWindowsAcl, parseIcaclsOutput, resolveWindowsUserPrincipal, summarizeWindowsAcl, type IcaclsResetCommandOptions, type PermissionExec, type WindowsAclEntry, type WindowsAclSummary, } from "./permissions.js";
25
+ export { createIcaclsResetCommand, formatIcaclsResetCommand, formatWindowsAclSummary, inspectWindowsAcl, parseIcaclsOutput, resolveWindowsUserPrincipal, summarizeWindowsAcl, type IcaclsResetCommandOptions, type PermissionCommandFailure, type PermissionExec, type WindowsAclEntry, type WindowsAclSummary, } from "./permissions.js";
25
26
  //# sourceMappingURL=advanced.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"advanced.d.ts","sourceRoot":"","sources":["../src/advanced.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"advanced.d.ts","sourceRoot":"","sources":["../src/advanced.ts"],"names":[],"mappings":"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"}
package/dist/advanced.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // public subpaths; prefer root/json/store/temp/archive unless you are building a
3
3
  // higher-level primitive.
4
4
  export { createAsyncLock } from "./async-lock.js";
5
+ export { stageFileInDirectory, } from "./staged-file.js";
5
6
  export { readFileDescriptorBounded, readFileDescriptorBoundedSync, readFileHandleBounded, } from "./bounded-read.js";
6
7
  export { assertNoUnsafeDeviceReadPath, isUnsafeDeviceReadPath, matchUnsafeDeviceReadPath, } from "./device-path.js";
7
8
  export { assertAbsolutePathInput, canonicalPathFromExistingAncestor, ensureAbsoluteDirectory, findExistingAncestor, resolveAbsolutePathForRead, resolveAbsolutePathForWrite, } from "./absolute-path.js";
@@ -0,0 +1,2 @@
1
+ export declare function updateCrc32(previous: number, buffer: Buffer): number;
2
+ //# sourceMappingURL=archive-crc32.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-crc32.d.ts","sourceRoot":"","sources":["../src/archive-crc32.ts"],"names":[],"mappings":"AAQA,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAMpE"}
@@ -0,0 +1,14 @@
1
+ const CRC32_TABLE = Array.from({ length: 256 }, (_, index) => {
2
+ let value = index;
3
+ for (let bit = 0; bit < 8; bit += 1) {
4
+ value = (value & 1) !== 0 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
5
+ }
6
+ return value >>> 0;
7
+ });
8
+ export function updateCrc32(previous, buffer) {
9
+ let crc = previous ^ -1;
10
+ for (const byte of buffer) {
11
+ crc = (crc >>> 8) ^ (CRC32_TABLE[(crc ^ byte) & 0xff] ?? 0);
12
+ }
13
+ return (crc ^ -1) >>> 0;
14
+ }
@@ -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;AAWF,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;AA4BD,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,CAQZ"}
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"}
@@ -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: () => undefined,
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
- if (controller.signal.aborted) {
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
- return await waitForDeadline(run(deadline), deadline);
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;AAED,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"}
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"}
@@ -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 === "./") {
@@ -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"}
@@ -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;AAI7B,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,CA8D7B"}
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"}
@@ -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 { sameFileIdentity } from "./file-identity.js";
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 fs.lstat(sourcePath);
26
- if (initialStat.isSymbolicLink() || !initialStat.isFile()) {
27
- throw new Error(`archive is not a regular file: ${params.archivePath}`);
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 noFollow = process.platform !== "win32" && "O_NOFOLLOW" in fsConstants ? fsConstants.O_NOFOLLOW : 0;
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 openedStat = await handle.stat();
42
- const pathStat = await fs.lstat(sourcePath);
43
- if (!openedStat.isFile() ||
44
- pathStat.isSymbolicLink() ||
45
- !pathStat.isFile() ||
46
- !sameFileIdentity(initialStat, openedStat) ||
47
- !sameFileIdentity(pathStat, openedStat)) {
48
- throw new Error("archive changed during validation");
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 {
@@ -10,8 +10,8 @@ const NATIVE_TAR_SUFFIXES = [
10
10
  ];
11
11
  function requireNativeArchiveKind(kind) {
12
12
  if (!getNativeBinding()) {
13
- throw new FsSafeError("helper-unavailable", `${kind} archives require a supported bundled native binding; ` +
14
- "use FS_SAFE_NATIVE_MODE=auto or require on a supported platform");
13
+ throw new FsSafeError("helper-unavailable", `${kind} archives require the matching optional native platform package; ` +
14
+ "install @openclaw/fs-safe with optional dependencies enabled on a supported platform and use FS_SAFE_NATIVE_MODE=auto or require");
15
15
  }
16
16
  return kind;
17
17
  }
@@ -4,11 +4,11 @@ export type ArchiveExtractLimits = {
4
4
  * Max archive file bytes (compressed).
5
5
  */
6
6
  maxArchiveBytes?: number;
7
- /** Max number of extracted entries (files + dirs). */
7
+ /** Max logical archive entries, including skipped/stripped members. */
8
8
  maxEntries?: number;
9
- /** Max extracted bytes (sum of all files). */
9
+ /** Max extracted bytes across entries accepted after strip/filter policy. */
10
10
  maxExtractedBytes?: number;
11
- /** Max extracted bytes for a single file entry. */
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,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mDAAmD;IACnD,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,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;AAgB3E,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;AAU1E,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"}
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"}
@@ -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;
@@ -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 ArchiveExtractLimits } from "./archive-limits.js";
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?: ArchiveExtractLimits;
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":"AAcA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAOL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAOlE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAuBjD,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,CAAC,EAAE,oBAAoB,CAAC;IAC9B,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,CAwHhB"}
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"}