@openclaw/fs-safe 0.7.2 → 0.8.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.
Files changed (159) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +5 -1
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +9 -10
  5. package/dist/archive-merge.d.ts +18 -0
  6. package/dist/archive-merge.d.ts.map +1 -0
  7. package/dist/archive-merge.js +157 -0
  8. package/dist/archive-native.d.ts.map +1 -1
  9. package/dist/archive-native.js +25 -5
  10. package/dist/archive-policy.d.ts +1 -1
  11. package/dist/archive-policy.d.ts.map +1 -1
  12. package/dist/archive-policy.js +1 -1
  13. package/dist/archive-read.d.ts.map +1 -1
  14. package/dist/archive-read.js +3 -2
  15. package/dist/archive-staging.d.ts +13 -8
  16. package/dist/archive-staging.d.ts.map +1 -1
  17. package/dist/archive-staging.js +44 -192
  18. package/dist/archive-tar-meta.d.ts.map +1 -1
  19. package/dist/archive-tar-meta.js +9 -5
  20. package/dist/archive-tar-runtime.d.ts +1 -0
  21. package/dist/archive-tar-runtime.d.ts.map +1 -1
  22. package/dist/archive-zip-admission.d.ts +2 -1
  23. package/dist/archive-zip-admission.d.ts.map +1 -1
  24. package/dist/archive-zip-admission.js +2 -2
  25. package/dist/archive-zip-directory.d.ts +8 -1
  26. package/dist/archive-zip-directory.d.ts.map +1 -1
  27. package/dist/archive-zip-directory.js +6 -2
  28. package/dist/archive-zip-entry.d.ts +1 -1
  29. package/dist/archive-zip-entry.d.ts.map +1 -1
  30. package/dist/archive-zip-names.d.ts +1 -1
  31. package/dist/archive-zip-names.d.ts.map +1 -1
  32. package/dist/archive-zip-names.js +1 -0
  33. package/dist/archive.d.ts.map +1 -1
  34. package/dist/archive.js +17 -25
  35. package/dist/directory-guard.d.ts +17 -5
  36. package/dist/directory-guard.d.ts.map +1 -1
  37. package/dist/directory-guard.js +8 -6
  38. package/dist/directory-mode-node.d.ts +8 -0
  39. package/dist/directory-mode-node.d.ts.map +1 -0
  40. package/dist/directory-mode-node.js +93 -0
  41. package/dist/directory-mode-owner.d.ts +21 -0
  42. package/dist/directory-mode-owner.d.ts.map +1 -0
  43. package/dist/directory-mode-owner.js +73 -0
  44. package/dist/file-observation.d.ts +1 -1
  45. package/dist/file-observation.d.ts.map +1 -1
  46. package/dist/file-store-boundary.d.ts +2 -0
  47. package/dist/file-store-boundary.d.ts.map +1 -1
  48. package/dist/file-store-boundary.js +13 -0
  49. package/dist/file-store.d.ts.map +1 -1
  50. package/dist/file-store.js +8 -11
  51. package/dist/filename.d.ts +6 -0
  52. package/dist/filename.d.ts.map +1 -1
  53. package/dist/filename.js +36 -0
  54. package/dist/guarded-mkdir.d.ts +2 -0
  55. package/dist/guarded-mkdir.d.ts.map +1 -1
  56. package/dist/guarded-mkdir.js +2 -2
  57. package/dist/guarded-mutation.d.ts +2 -2
  58. package/dist/guarded-mutation.d.ts.map +1 -1
  59. package/dist/json-document-store.d.ts +3 -0
  60. package/dist/json-document-store.d.ts.map +1 -1
  61. package/dist/json-document-store.js +2 -0
  62. package/dist/json-durable-queue-ownership.d.ts +3 -1
  63. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  64. package/dist/json-durable-queue-ownership.js +7 -4
  65. package/dist/json-durable-queue.d.ts.map +1 -1
  66. package/dist/json-durable-queue.js +25 -28
  67. package/dist/local-roots.d.ts.map +1 -1
  68. package/dist/local-roots.js +18 -56
  69. package/dist/move-path-cleanup.d.ts +43 -0
  70. package/dist/move-path-cleanup.d.ts.map +1 -0
  71. package/dist/move-path-cleanup.js +159 -0
  72. package/dist/move-path.d.ts.map +1 -1
  73. package/dist/move-path.js +30 -84
  74. package/dist/native-operations.d.ts +2 -4
  75. package/dist/native-operations.d.ts.map +1 -1
  76. package/dist/native-operations.js +7 -9
  77. package/dist/native-pinned-write-windows.d.ts +2 -2
  78. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  79. package/dist/native-pinned-write-windows.js +5 -5
  80. package/dist/native-pinned-write.d.ts.map +1 -1
  81. package/dist/native-pinned-write.js +14 -4
  82. package/dist/native-staged-file.d.ts +2 -2
  83. package/dist/native-staged-file.d.ts.map +1 -1
  84. package/dist/opened-file-failure.d.ts +1 -0
  85. package/dist/opened-file-failure.d.ts.map +1 -1
  86. package/dist/opened-file-failure.js +4 -0
  87. package/dist/output-sibling.d.ts.map +1 -1
  88. package/dist/output-sibling.js +9 -3
  89. package/dist/pinned-write.d.ts +2 -2
  90. package/dist/pinned-write.d.ts.map +1 -1
  91. package/dist/pinned-write.js +32 -13
  92. package/dist/publish-file.d.ts.map +1 -1
  93. package/dist/publish-file.js +7 -3
  94. package/dist/regular-file.d.ts +1 -1
  95. package/dist/regular-file.d.ts.map +1 -1
  96. package/dist/regular-file.js +35 -6
  97. package/dist/replace-file-descriptor.d.ts +9 -1
  98. package/dist/replace-file-descriptor.d.ts.map +1 -1
  99. package/dist/replace-file-descriptor.js +58 -10
  100. package/dist/replace-file-temp-owner.d.ts +7 -0
  101. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  102. package/dist/replace-file-temp-owner.js +29 -0
  103. package/dist/replace-file.d.ts +1 -0
  104. package/dist/replace-file.d.ts.map +1 -1
  105. package/dist/replace-file.js +12 -38
  106. package/dist/root-context.d.ts +4 -0
  107. package/dist/root-context.d.ts.map +1 -1
  108. package/dist/root-context.js +19 -0
  109. package/dist/root-impl.d.ts +43 -0
  110. package/dist/root-impl.d.ts.map +1 -1
  111. package/dist/root-impl.js +13 -11
  112. package/dist/root-path-symlink.d.ts +6 -2
  113. package/dist/root-path-symlink.d.ts.map +1 -1
  114. package/dist/root-path-symlink.js +7 -6
  115. package/dist/root-path.d.ts +1 -0
  116. package/dist/root-path.d.ts.map +1 -1
  117. package/dist/root-path.js +6 -2
  118. package/dist/root-walk.d.ts.map +1 -1
  119. package/dist/root-walk.js +4 -0
  120. package/dist/root-write-verification.d.ts +3 -2
  121. package/dist/root-write-verification.d.ts.map +1 -1
  122. package/dist/root-write-verification.js +6 -0
  123. package/dist/secret-file.d.ts +9 -0
  124. package/dist/secret-file.d.ts.map +1 -1
  125. package/dist/secret-file.js +136 -74
  126. package/dist/sibling-temp.d.ts.map +1 -1
  127. package/dist/sibling-temp.js +9 -3
  128. package/dist/sidecar-lock-acquire.d.ts +1 -0
  129. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  130. package/dist/sidecar-lock-acquire.js +24 -5
  131. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  132. package/dist/sidecar-lock-reclaim.js +6 -4
  133. package/dist/sidecar-lock-types.d.ts +7 -0
  134. package/dist/sidecar-lock-types.d.ts.map +1 -1
  135. package/dist/sidecar-lock.d.ts +2 -0
  136. package/dist/sidecar-lock.d.ts.map +1 -1
  137. package/dist/sidecar-lock.js +44 -3
  138. package/dist/write-open-flags.d.ts +5 -0
  139. package/dist/write-open-flags.d.ts.map +1 -0
  140. package/dist/write-open-flags.js +35 -0
  141. package/docs/advanced.md +1 -1
  142. package/docs/archive.md +56 -7
  143. package/docs/atomic.md +8 -2
  144. package/docs/contributing.md +13 -0
  145. package/docs/durability.md +3 -2
  146. package/docs/errors.md +3 -1
  147. package/docs/file-store.md +38 -8
  148. package/docs/local-roots.md +4 -3
  149. package/docs/output.md +4 -1
  150. package/docs/private-file-store.md +11 -2
  151. package/docs/reading.md +3 -1
  152. package/docs/regular-file.md +16 -2
  153. package/docs/root.md +4 -2
  154. package/docs/secret-file.md +37 -2
  155. package/docs/sidecar-lock.md +33 -5
  156. package/docs/store.md +5 -1
  157. package/docs/temp.md +19 -1
  158. package/docs/writing.md +24 -3
  159. package/package.json +14 -10
package/CHANGELOG.md CHANGED
@@ -1,5 +1,76 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.1 - 2026-09-04
4
+
5
+ - Add `retainOnExit` to sidecar lock acquisition so deliberately retained ownership records (for example fail-closed build locks) survive natural process exit; default process-exit release behavior is unchanged.
6
+
7
+ ## 0.8.0 - 2026-09-04
8
+
9
+ ### Compatibility and upgrade notes
10
+
11
+ - **Existing secret directories must already have the requested mode.** Wrong permissions now fail the write with `insecure-permissions` instead of being chmod-repaired; audit and fix existing directories before upgrading.
12
+ - **FileStore keys are enforced portably.** Parent-segment and backslash aliases are rejected with `invalid-path` across async reads, `exists`, and `remove`, matching the sync and write paths.
13
+ - **Lock and queue failures now surface.** Durable queue enqueue, migration, and acknowledgement sync failures propagate instead of returning empty or partial success; sidecar payloads above 1 MiB are rejected with `too-large`; async lock retry counts apply even with infinite deadlines. Handle these rejections explicitly.
14
+ - **Create-only visibility is backend-scoped.** The sibling-temp no-visibility guarantee for `create`, `createJson`, and `write({ overwrite: false })` holds with the native binding (`require` mode, or `auto` when the binding loads); the JavaScript fallback claims the final name before content is written.
15
+
16
+ ### Highlights
17
+
18
+ - **Stricter secret storage:** existing secret directories are never chmod-repaired — wrong permissions fail the write — and directory identity is tracked exactly through private locks and native writes, closing replacement races.
19
+ - **Exact file modes everywhere:** explicit modes, including set-ID bits, are applied after content writes and verified in full at publication, and synchronous appends no longer drop bytes when the kernel writes short.
20
+ - **Restrictive secret modes work:** write-only and no-access files such as `0o200` and `0o000` publish successfully through the writer's retained descriptor, without a readonly reopen or widened permissions.
21
+ - **Resilient locks and queues:** Windows sidecar creation retries genuine denials within the existing budgets, Root-backed locks release on natural process exit, and queue enqueue/migration/ack sync failures propagate instead of returning false success.
22
+ - **Honest backend guarantees:** the no-visibility guarantee for create-only Root writes is scoped to backends with atomic no-replace publication (`require`, or `auto` with a loaded binding); the JavaScript fallback's behavior is documented and regression-pinned.
23
+
24
+ ### Secret files and permissions
25
+
26
+ - Preserve existing secret-directory permissions instead of repairing them; retain lossless directory identities through private locks and native writes, initialize new directories through guarded descriptor authority, honor full directory mode bits, and fail closed for unpinnable parents, including non-root macOS directories created under `umask(0o777)`.
27
+ - Finalize explicit file modes after content writes across pinned writers, verify all `0o7777` POSIX bits for secret publication, and use exact identities before native Windows mode changes and failed-write cleanup; JavaScript fallback cleanup preserves unverified replacements.
28
+ - Complete short synchronous regular-file appends and preserve explicitly requested special mode bits in both append helpers, retaining permission tightening before any data is written.
29
+ - Verify atomic secret writes through the writer's retained descriptor so restrictive POSIX modes such as `0o000` and `0o200` succeed without a readonly reopen or widened permissions.
30
+ - Retry and fully revalidate raced secret-parent creation so concurrent writes to distinct leaves succeed without leaking `EEXIST` or reporting a false `secret-exists` collision.
31
+ - Scope the sibling-temp no-visibility guarantee for create-only Root writes (`create`, `createJson`, `write({ overwrite: false })`) to backends with atomic no-replace publication: the native binding stages privately and renames without clobbering, while the pure-JavaScript fallback claims the final name exclusively before content is written. Regression tests now pin both behaviors.
32
+ - Normalize every exclusive-copy target to mode `0o600` through its owned descriptor, including under restrictive umasks and native macOS clone staging.
33
+
34
+ ### Locks and durable queues
35
+
36
+ - Retry Windows Root-backed sidecar exclusive-create denials within the existing eight-retry and caller budgets, using per-call provenance while preserving callback errors and rejecting replayed failure evidence.
37
+ - Release asynchronously acquired Root-backed sidecar locks on natural event-loop shutdown through their retained ownership receipts, preserving changed sidecars and avoiding cleanup retry loops.
38
+ - Keep async Root-backed lock normalization read-only, rejecting deleted or replaced admitted parents without recreating them while preserving explicit in-root sidecars for external target keys.
39
+ - Reject serialized sidecar lock payloads above 1 MiB of UTF-8 bytes, including ownership overhead, with `too-large` before async or sync acquisition so admitted locks remain verifiable and releasable.
40
+ - Honor explicit async file-lock retry counts independently of infinite deadlines, matching sync locks while preserving unlimited retries when the count is omitted.
41
+ - Propagate durable queue enqueue parent-sync failures and keep published-file identity checks after synchronization, sharing the guarded writer with migrations while retaining retry state.
42
+ - Propagate durable queue batch claim and migration failures instead of returning empty or partial success, and strictly sync migration publication in both loaders while preserving retry state.
43
+ - Resync durable queue acknowledgement retries after final marker unlink before reporting completion or a newer-generation mismatch.
44
+
45
+ ### Paths, stores, and moves
46
+
47
+ - Enforce portable FileStore keys consistently across methods: async reads, `exists`, and `remove` now reject parent-segment and backslash aliases with `invalid-path` for existing roots, matching sync and write methods while preserving missing-root error precedence and Root's confined existing-object compatibility.
48
+ - Prevent no-reader FIFOs from stalling POSIX `Root.openWritable()` and async/sync regular-file append admission, preserving regular-file write semantics and existing-target cleanup fencing.
49
+ - Accept canonical in-root absolute paths in `Root.readAbsolute()` and `Root.reader()` when the Root was configured through a directory symlink or Windows junction.
50
+ - Reject dangling symlink leaves and ancestors under strict absolute-write and missing local-root resolution instead of treating unresolved aliases as safe missing paths.
51
+ - Move dangling symlink sources through staged and cross-device fallback without dereferencing their absent referents, while retaining same-entry, descendant, and source-substitution guards.
52
+ - Retire every copied source name after cross-device moves with allowed in-tree hardlink aliases, while preserving `ESTALE` fences for unverified external mutations.
53
+ - End `Root.walk()` immediately after its single truncation marker, including when a nested depth or entry budget is reached.
54
+ - Bound callback staging components to 255 NFC/NFD bytes so filesystem-valid long destination names work without changing final names or short callback paths.
55
+
56
+ ### Archives
57
+
58
+ - Separate archive staging permissions from final publication modes so zero/write-only files and restrictive directories extract correctly; finalize explicit and implicit directory modes through retained, verified authority and reject unsafe mode application instead of silently skipping it.
59
+ - Preserve pre-existing and substituted destination files when an archive merge fails, leaving guarded copy cleanup to the operation that owns publication.
60
+ - Decode absent TAR modes and supported signed GNU binary permission bits in native manifests, matching JavaScript defaults without changing the native ABI.
61
+ - Close a selected archive input if private staging allocation fails, preventing `readArchiveEntry()` setup errors from retaining file descriptors until garbage collection.
62
+ - Avoid opening the TAR metadata stream until preflight setup succeeds, and always destroy it after pipeline completion, preventing descriptor leaks on setup failures. Thanks @SebTardif.
63
+
64
+ ### Docs, packaging, and validation
65
+
66
+ - Add the narrow `@openclaw/fs-safe/secure-temp-root` package subpath so consumers can import `resolveSecureTempRoot()` and its options type without loading the temp workspace implementation.
67
+ - Exercise secret-directory admission from isolated npm/pnpm consumer installs, retain real-identity and native-load proof, and honor explicit package-proof output paths.
68
+ - Correct lower-level archive helper signatures in the reference docs and guard them against drifting from the public declarations.
69
+ - Keep slow physical-package fixture setup outside the process-exit test deadline, drain fixture work before teardown, and give each adversarial corpus payload its own test lifetime while retaining Windows boundary assertions.
70
+ - Bound workflow-dispatch test subprocesses and terminate their process groups before fixture cleanup, so stuck shell descendants fail validation instead of hanging the suite.
71
+ - Assign cross-platform sidecar contention proof liveness to its whole-worker watchdog instead of false-failing healthy unfair acquisition; production lock timeout behavior is unchanged.
72
+ - Refresh the Node type definitions, align development and CI on pnpm 11.25.0, and update the pinned Pages deployment action to v5.0.1 for polling backoff and jitter. Thanks @dependabot.
73
+
3
74
  ## 0.7.2 - 2026-09-01
4
75
 
5
76
  - Add `movePathWithCopyFallback({ assertBeforeRename })` to synchronously recheck caller-owned authorization immediately before direct or staged rename, preserving refusal errors and rejecting asynchronous callbacks without publishing the move.
package/README.md CHANGED
@@ -176,7 +176,7 @@ Root reads default to `DEFAULT_ROOT_MAX_BYTES` (16 MiB). Pass a larger `maxBytes
176
176
  for expected large reads, or `Number.POSITIVE_INFINITY` when the caller has a
177
177
  separate size budget.
178
178
 
179
- `reader()` returns a callback that reads absolute or relative paths through the same root boundary. It is useful for APIs that accept a `(path) => Promise<Buffer>` loader. Absolute paths outside the root are rejected with `outside-workspace`. `readAbsolute()` has the same absolute-path behavior directly.
179
+ `reader()` returns a callback that reads absolute or relative paths through the same root boundary. It is useful for APIs that accept a `(path) => Promise<Buffer>` loader. For roots configured through a directory symlink or Windows junction, absolute inputs may use either the configured spelling or the canonical real path. Absolute paths outside the root are rejected with `outside-workspace`. `readAbsolute()` has the same absolute-path behavior directly.
180
180
 
181
181
  When you need a writable `FileHandle`, use `openWritable()` and prefer `await using` for cleanup:
182
182
 
@@ -189,6 +189,10 @@ await using opened = await fs.openWritable("logs/current.log", { writeMode: "app
189
189
 
190
190
  `nonBlockingRead` remains as a compatibility hint in `RootDefaults`. Safe read/open operations already use nonblocking descriptor opens where the platform supports them so a raced FIFO cannot pin a worker; filesystem safety policy remains explicit through `hardlinks`, `symlinks`, and `denyMutations`.
191
191
 
192
+ On POSIX, `openWritable()` also uses nonblocking admission for existing targets
193
+ so a no-reader FIFO cannot stall validation. Ordinary regular-file write
194
+ semantics are unchanged; see [writing](docs/writing.md#openwritable-for-streaming).
195
+
192
196
  ```ts
193
197
  const locked = await root("/srv/workspace", {
194
198
  denyMutations: {
@@ -1 +1 @@
1
- {"version":3,"file":"absolute-path.d.ts","sourceRoot":"","sources":["../src/absolute-path.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAGhE,MAAM,MAAM,yBAAyB,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAU5D,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG,oBAAoB,GAAG;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,6BAA6B,GACrC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC1B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,CAAC;AAmL7D,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAWhE;AAED,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAEnF;AAuBD,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,8BAAmC,GAC3C,OAAO,CAAC,6BAA6B,CAAC,CA+ExC;AAED,wBAAsB,iCAAiC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAazF;AAED,wBAAsB,0BAA0B,CAC9C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,yBAAyB,CAAA;CAAO,GACrD,OAAO,CAAC,oBAAoB,CAAC,CAgB/B;AAED,wBAAsB,2BAA2B,CAC/C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,yBAAyB,CAAA;CAAO,GACrD,OAAO,CAAC,4BAA4B,CAAC,CA6BvC"}
1
+ {"version":3,"file":"absolute-path.d.ts","sourceRoot":"","sources":["../src/absolute-path.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAIhE,MAAM,MAAM,yBAAyB,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAU5D,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG,oBAAoB,GAAG;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,6BAA6B,GACrC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC1B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,CAAC;AAmL7D,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAWhE;AAED,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAEnF;AAuBD,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,8BAAmC,GAC3C,OAAO,CAAC,6BAA6B,CAAC,CA+ExC;AAED,wBAAsB,iCAAiC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAazF;AAED,wBAAsB,0BAA0B,CAC9C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,yBAAyB,CAAA;CAAO,GACrD,OAAO,CAAC,oBAAoB,CAAC,CAgB/B;AAED,wBAAsB,2BAA2B,CAC/C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,yBAAyB,CAAA;CAAO,GACrD,OAAO,CAAC,4BAA4B,CAAC,CA2BvC"}
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, } from "./directory-guard.js";
4
4
  import { FsSafeError } from "./errors.js";
5
5
  import { pathExists } from "./fs.js";
6
+ import { resolveRootPath } from "./root-path.js";
6
7
  function resolveSymlinkPolicy(policy) {
7
8
  if (policy === undefined)
8
9
  return "reject";
@@ -281,16 +282,14 @@ export async function resolveAbsolutePathForWrite(filePath, options = {}) {
281
282
  const parentDir = path.dirname(normalized);
282
283
  const parentExists = await pathExists(parentDir);
283
284
  if (symlinks === "reject") {
284
- const ancestor = await findExistingAncestor(parentDir);
285
- if (ancestor) {
286
- const canonicalAncestor = await fs.realpath(ancestor).catch(() => ancestor);
287
- if (canonicalAncestor !== ancestor) {
288
- const canonicalPath = path.join(canonicalAncestor, path.relative(ancestor, normalized));
289
- throw new FsSafeError("symlink", "path traverses a symlink", {
290
- cause: { canonicalPath },
291
- });
292
- }
293
- }
285
+ const filesystemRoot = path.parse(normalized).root;
286
+ await resolveRootPath({
287
+ absolutePath: normalized,
288
+ rootPath: filesystemRoot,
289
+ rootCanonicalPath: filesystemRoot,
290
+ boundaryLabel: "absolute path",
291
+ rejectSymlinks: true,
292
+ });
294
293
  }
295
294
  const canonicalPath = await canonicalPathFromExistingAncestor(normalized);
296
295
  if (symlinks === "reject" && canonicalPath !== normalized) {
@@ -0,0 +1,18 @@
1
+ import { type ExtractionDeadline } from "./archive-deadline.js";
2
+ export type ArchivePublicationEntry = {
3
+ path: string;
4
+ kind: "file" | "directory";
5
+ mode: number;
6
+ };
7
+ type MergeParams = {
8
+ sourceDir: string;
9
+ destinationDir: string;
10
+ destinationRealDir: string;
11
+ deadline?: ExtractionDeadline;
12
+ };
13
+ export declare function mergePlannedArchiveIntoDestination(params: MergeParams & {
14
+ entries: readonly ArchivePublicationEntry[];
15
+ }): Promise<void>;
16
+ export declare function mergeExtractedTreeIntoDestination(params: MergeParams): Promise<void>;
17
+ export {};
18
+ //# sourceMappingURL=archive-merge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-merge.d.ts","sourceRoot":"","sources":["../src/archive-merge.ts"],"names":[],"mappings":"AAEA,OAAO,EAAoC,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAelG,MAAM,MAAM,uBAAuB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AACjG,KAAK,WAAW,GAAG;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC/B,CAAC;AAEF,wBAAsB,kCAAkC,CACtD,MAAM,EAAE,WAAW,GAAG;IAAE,OAAO,EAAE,SAAS,uBAAuB,EAAE,CAAA;CAAE,GACpE,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,wBAAsB,iCAAiC,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1F"}
@@ -0,0 +1,157 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { ownExtractionDestinationMutation } from "./archive-deadline.js";
4
+ import { assertDirectoryIdentityGuard, assertResolvedInsideDestination, createDirectoryIdentityGuard, createArchiveSymlinkTraversalError, preparePrivateArchiveOutputPath, } from "./archive-staging.js";
5
+ import {} from "./directory-guard.js";
6
+ import {} from "./directory-mode-owner.js";
7
+ import { pinNodeDirectoryForMode } from "./directory-mode-node.js";
8
+ import { FsSafeError } from "./errors.js";
9
+ import { formatErrorDetail } from "./error-detail.js";
10
+ import { isPathInside } from "./path.js";
11
+ import { root } from "./root.js";
12
+ import { getFsSafeTestHooks } from "./test-hooks.js";
13
+ export async function mergePlannedArchiveIntoDestination(params) {
14
+ await mergeTree(params, params.entries);
15
+ }
16
+ export async function mergeExtractedTreeIntoDestination(params) {
17
+ await mergeTree(params);
18
+ }
19
+ async function mergeTree(params, publication) {
20
+ const check = () => params.deadline?.check();
21
+ check();
22
+ const destinationGuard = await createDirectoryIdentityGuard(params.destinationRealDir);
23
+ check();
24
+ const targetRoot = await root(params.destinationRealDir);
25
+ check();
26
+ const sourceGuard = await createDirectoryIdentityGuard(params.sourceDir);
27
+ check();
28
+ const plan = publication ? new Map() : undefined;
29
+ for (const entry of publication ?? []) {
30
+ // Resolve admitted spelling in private staging, preserving the volume's case
31
+ // and Unicode behavior without assigning explicit modes to distinct parents.
32
+ const stagedPath = await fs.realpath(path.join(params.sourceDir, entry.path));
33
+ check();
34
+ if (!isPathInside(sourceGuard.realPath, stagedPath) || plan.has(stagedPath)) {
35
+ throw new FsSafeError("path-mismatch", "archive publication paths changed in staging");
36
+ }
37
+ plan.set(stagedPath, entry);
38
+ }
39
+ const ancestors = [];
40
+ const assertGuards = async () => {
41
+ await assertDirectoryIdentityGuard(destinationGuard);
42
+ check();
43
+ for (const ancestor of ancestors) {
44
+ await assertDirectoryIdentityGuard(ancestor.guard);
45
+ check();
46
+ await ancestor.owner.verify(check);
47
+ check();
48
+ }
49
+ await assertDirectoryIdentityGuard(sourceGuard);
50
+ check();
51
+ };
52
+ const walk = async (sourceDir) => {
53
+ await assertGuards();
54
+ const entries = await fs.readdir(sourceDir, { withFileTypes: true });
55
+ check();
56
+ for (const entry of entries) {
57
+ await assertGuards();
58
+ const sourcePath = path.join(sourceDir, entry.name);
59
+ const relPath = path.relative(params.sourceDir, sourcePath);
60
+ const originalPath = relPath.split(path.sep).join("/");
61
+ const destinationPath = path.join(params.destinationDir, relPath);
62
+ const sourceStat = await fs.lstat(sourcePath);
63
+ check();
64
+ if (sourceStat.isSymbolicLink())
65
+ throw createArchiveSymlinkTraversalError(originalPath);
66
+ const sourceReal = await fs.realpath(sourcePath);
67
+ check();
68
+ if (!isPathInside(sourceGuard.realPath, sourceReal))
69
+ throw createArchiveSymlinkTraversalError(originalPath);
70
+ if (!sourceStat.isFile() && !sourceStat.isDirectory()) {
71
+ throw new Error(`archive staging contains unsupported entry: ${formatErrorDetail(originalPath)}`);
72
+ }
73
+ const kind = sourceStat.isDirectory() ? "directory" : "file";
74
+ const planned = plan?.get(sourceReal);
75
+ if (plan && ((planned && planned.kind !== kind) || (!planned && kind === "file"))) {
76
+ throw new FsSafeError("path-mismatch", "archive staging disagrees with the admitted publication plan");
77
+ }
78
+ const mode = plan ? planned?.mode ?? 0o755 : sourceStat.mode & 0o777;
79
+ await preparePrivateArchiveOutputPath({
80
+ ...params, relPath, outPath: destinationPath, originalPath, isDirectory: kind === "directory",
81
+ }, assertGuards);
82
+ check();
83
+ if (kind === "directory") {
84
+ // Ownership spans open, descendants, finalization and close, including timeout.
85
+ await ownExtractionDestinationMutation(params.deadline, async () => {
86
+ await assertGuards();
87
+ const owner = await pinNodeDirectoryForMode(destinationPath).catch((error) => {
88
+ if (error instanceof FsSafeError && (error.code === "not-file" || error.code === "path-mismatch")) {
89
+ throw createArchiveSymlinkTraversalError(originalPath);
90
+ }
91
+ throw error;
92
+ });
93
+ try {
94
+ check();
95
+ const guard = await createDirectoryIdentityGuard(destinationPath);
96
+ check();
97
+ await owner.verify(check);
98
+ ancestors.push({ guard, owner });
99
+ try {
100
+ await walk(sourcePath);
101
+ await getFsSafeTestHooks()?.beforeArchiveOutputMutation?.("chmod", destinationPath);
102
+ check();
103
+ await assertGuards();
104
+ // Do not recursively verify this owner from inside its serialized apply.
105
+ ancestors.pop();
106
+ await owner.apply(mode, { check, beforeChmod: async () => {
107
+ await assertGuards();
108
+ await assertDirectoryIdentityGuard(guard);
109
+ check();
110
+ await assertResolvedInsideDestination({
111
+ destinationRealDir: params.destinationRealDir, targetPath: destinationPath, originalPath,
112
+ });
113
+ check();
114
+ } });
115
+ check();
116
+ await assertGuards();
117
+ }
118
+ finally {
119
+ if (ancestors.at(-1)?.owner === owner)
120
+ ancestors.pop();
121
+ }
122
+ }
123
+ finally {
124
+ await owner.close();
125
+ }
126
+ });
127
+ }
128
+ else {
129
+ await ownExtractionDestinationMutation(params.deadline, async () => {
130
+ await assertGuards();
131
+ try {
132
+ await targetRoot.copyIn(relPath, sourcePath, { mkdir: true, mode });
133
+ check();
134
+ await assertGuards();
135
+ await assertResolvedInsideDestination({
136
+ destinationRealDir: params.destinationRealDir, targetPath: destinationPath, originalPath,
137
+ });
138
+ check();
139
+ const stat = await fs.lstat(destinationPath);
140
+ check();
141
+ if (stat.isSymbolicLink() || (stat.isFile() && stat.nlink > 1)) {
142
+ throw createArchiveSymlinkTraversalError(originalPath);
143
+ }
144
+ }
145
+ catch (error) {
146
+ // copyIn alone owns cleanup; no receipt permits archive-layer unlink.
147
+ if (error instanceof FsSafeError && (error.code === "hardlink" || error.code === "path-alias")) {
148
+ throw createArchiveSymlinkTraversalError(originalPath);
149
+ }
150
+ throw error;
151
+ }
152
+ });
153
+ }
154
+ }
155
+ };
156
+ await walk(params.sourceDir);
157
+ }
@@ -1 +1 @@
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"}
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;AAQlE,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,CAyJhB"}
@@ -6,7 +6,8 @@ import { createArchiveOutputPathTracker, resolveArchiveOutputPath, stripArchiveP
6
6
  import { stageArchiveFileForExtraction } from "./archive-input.js";
7
7
  import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, assertArchiveEntryCountWithinLimit, assertArchiveEntryPathComponentsWithinLimit, createByteBudgetTracker, } from "./archive-limits.js";
8
8
  import { resolveArchiveEntryMode, shouldExtractArchiveEntry } from "./archive-policy.js";
9
- import { mergeExtractedTreeIntoDestination, prepareArchiveDestinationDir, withStagedArchiveDestination, } from "./archive-staging.js";
9
+ import { prepareArchiveDestinationDir, withStagedArchiveDestination, } from "./archive-staging.js";
10
+ import { mergePlannedArchiveIntoDestination } from "./archive-merge.js";
10
11
  import { admitZipFile } from "./archive-zip-admission.js";
11
12
  function policyKind(kind) {
12
13
  if (kind === "file" || kind === "directory")
@@ -41,8 +42,9 @@ export async function extractNativeArchive(params) {
41
42
  deadline: params.deadline,
42
43
  });
43
44
  try {
45
+ const zipEntries = [];
44
46
  const physicalCount = params.kind === "zip"
45
- ? await admitZipFile(stagedArchive.path, limits, params.deadline)
47
+ ? await admitZipFile(stagedArchive.path, limits, params.deadline, (entry) => { zipEntries.push(entry); })
46
48
  : undefined;
47
49
  const destinationRealDir = await prepareArchiveDestinationDir(params.destDir);
48
50
  await withStagedArchiveDestination({
@@ -57,6 +59,19 @@ export async function extractNativeArchive(params) {
57
59
  if (physicalCount !== undefined && manifest.length !== physicalCount) {
58
60
  throw new ArchiveSecurityError("entry-path", "zip decoder collapsed entry names");
59
61
  }
62
+ if (params.kind === "zip") {
63
+ for (const [ordinal, entry] of manifest.entries()) {
64
+ const physical = zipEntries[ordinal];
65
+ // Native ZIP indices are physical central-directory ordinals. Legacy
66
+ // filename decoding remains native-selected; compare names when known.
67
+ if (!physical || physical.index !== ordinal || entry.index !== ordinal ||
68
+ entry.size !== physical.size ||
69
+ (physical.path !== undefined && stripArchivePath(entry.path, 0) !== stripArchivePath(physical.path, 0)) ||
70
+ (physical.creatorSystem === 3 && entry.mode !== physical.externalAttributes >>> 16)) {
71
+ throw new ArchiveFormatError("ZIP decoder disagrees with admitted directory metadata");
72
+ }
73
+ }
74
+ }
60
75
  // Recheck the native manifest at the shared policy boundary before
61
76
  // any caller callback observes an entry.
62
77
  if (params.kind !== "zip") {
@@ -111,7 +126,11 @@ export async function extractNativeArchive(params) {
111
126
  size: entry.size,
112
127
  mode: resolveArchiveEntryMode({
113
128
  kind,
114
- archivedMode: entry.mode,
129
+ archivedMode: params.kind === "zip"
130
+ ? zipEntries[entry.index].creatorSystem === 3
131
+ ? zipEntries[entry.index].externalAttributes >>> 16
132
+ : undefined
133
+ : entry.mode,
115
134
  policy: params.entryModes,
116
135
  }),
117
136
  });
@@ -121,13 +140,14 @@ export async function extractNativeArchive(params) {
121
140
  (typeof fsConstants.O_DIRECTORY === "number" ? fsConstants.O_DIRECTORY : 0));
122
141
  try {
123
142
  params.deadline.check();
124
- await params.binding.extractArchiveNative(stagedArchive.path, params.kind, directory.fd, plan, tarLimits, params.deadline.signal).catch(throwMappedNativeError);
143
+ await params.binding.extractArchiveNative(stagedArchive.path, params.kind, directory.fd, plan.map((entry) => ({ ...entry, mode: entry.kind === "directory" ? 0o700 : 0o600 })), tarLimits, params.deadline.signal).catch(throwMappedNativeError);
125
144
  }
126
145
  finally {
127
146
  await directory.close().catch(() => undefined);
128
147
  }
129
148
  params.deadline.check();
130
- await mergeExtractedTreeIntoDestination({
149
+ await mergePlannedArchiveIntoDestination({
150
+ entries: plan,
131
151
  sourceDir: stagingDir,
132
152
  destinationDir: params.destDir,
133
153
  destinationRealDir,
@@ -10,7 +10,7 @@ export type ArchiveEntryFilter = (entry: {
10
10
  export declare function archiveEntryKindFromTarType(type: string): ArchiveEntryKind;
11
11
  export declare function resolveArchiveEntryMode(params: {
12
12
  kind: "file" | "directory";
13
- archivedMode?: number;
13
+ archivedMode?: number | null;
14
14
  policy?: ArchiveEntryModePolicy;
15
15
  }): number;
16
16
  export declare function resolveArchiveFilteredEntryPolicy(value: unknown): 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,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"}
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,GAAG,IAAI,CAAC;IAC7B,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"}
@@ -12,7 +12,7 @@ export function archiveEntryKindFromTarType(type) {
12
12
  export function resolveArchiveEntryMode(params) {
13
13
  const archivedMode = (params.archivedMode ?? 0) & 0o777;
14
14
  if (params.policy === "preserve") {
15
- return archivedMode || (params.kind === "directory" ? 0o755 : 0o644);
15
+ return params.archivedMode == null ? (params.kind === "directory" ? 0o755 : 0o644) : archivedMode;
16
16
  }
17
17
  if (params.kind === "directory") {
18
18
  return 0o755;
@@ -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;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"}
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;AAiOzE,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"}
@@ -73,8 +73,9 @@ async function stageArchiveInput(archivePath) {
73
73
  return stat;
74
74
  });
75
75
  const handle = await fs.open(resolved, resolveReadOpenFlags());
76
- const staged = await tempFile({ prefix: "fs-safe-archive-read", fileName: "archive.bin" });
76
+ let staged;
77
77
  try {
78
+ staged = await tempFile({ prefix: "fs-safe-archive-read", fileName: "archive.bin" });
78
79
  const opened = await inspectFileIdentity(async () => {
79
80
  const stat = await handle.stat({ bigint: true });
80
81
  if (!stat.isFile())
@@ -92,7 +93,7 @@ async function stageArchiveInput(archivePath) {
92
93
  return { path: staged.path, buffer, cleanup: staged.cleanup };
93
94
  }
94
95
  catch (error) {
95
- await staged.cleanup().catch(() => undefined);
96
+ await staged?.cleanup().catch(() => undefined);
96
97
  if (error instanceof FsSafeError && error.code === "path-mismatch") {
97
98
  throw new FsSafeError("path-mismatch", "archive changed during validation", { cause: error });
98
99
  }
@@ -1,8 +1,16 @@
1
+ import { type AsyncDirectoryGuard } from "./directory-guard.js";
1
2
  import { type ExtractionDeadline } from "./archive-deadline.js";
2
3
  import { ArchiveSecurityError } from "./archive-errors.js";
3
4
  export { ArchiveSecurityError, type ArchiveSecurityErrorCode } from "./archive-errors.js";
5
+ export declare function createDirectoryIdentityGuard(dir: string): Promise<AsyncDirectoryGuard>;
6
+ export declare function assertDirectoryIdentityGuard(guard: AsyncDirectoryGuard): Promise<void>;
4
7
  export declare function prepareArchiveDestinationDir(destDir: string): Promise<string>;
5
- export declare function prepareArchiveOutputPath(params: {
8
+ export declare function assertResolvedInsideDestination(params: {
9
+ destinationRealDir: string;
10
+ targetPath: string;
11
+ originalPath: string;
12
+ }): Promise<void>;
13
+ type ArchiveOutputPathParams = {
6
14
  destinationDir: string;
7
15
  destinationRealDir: string;
8
16
  relPath: string;
@@ -10,17 +18,14 @@ export declare function prepareArchiveOutputPath(params: {
10
18
  originalPath: string;
11
19
  isDirectory: boolean;
12
20
  deadline?: ExtractionDeadline;
13
- }): Promise<void>;
21
+ };
22
+ export declare function prepareArchiveOutputPath(params: ArchiveOutputPathParams): Promise<void>;
23
+ export declare function preparePrivateArchiveOutputPath(params: ArchiveOutputPathParams, assertGuards?: () => Promise<void>): Promise<void>;
14
24
  export declare function withStagedArchiveDestination<T>(params: {
15
25
  destinationRealDir: string;
16
26
  stagingDirPrefix?: string;
17
27
  run: (stagingDir: string) => Promise<T>;
18
28
  }): Promise<T>;
19
- export declare function mergeExtractedTreeIntoDestination(params: {
20
- sourceDir: string;
21
- destinationDir: string;
22
- destinationRealDir: string;
23
- deadline?: ExtractionDeadline;
24
- }): Promise<void>;
29
+ export { mergeExtractedTreeIntoDestination } from "./archive-merge.js";
25
30
  export declare function createArchiveSymlinkTraversalError(originalPath: string): ArchiveSecurityError;
26
31
  //# sourceMappingURL=archive-staging.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"archive-staging.d.ts","sourceRoot":"","sources":["../src/archive-staging.ts"],"names":[],"mappings":"AAOA,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,oBAAoB,EAErB,MAAM,qBAAqB,CAAC;AAgB7B,OAAO,EAAE,oBAAoB,EAAE,KAAK,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAkC1F,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA4BnF;AA4DD,wBAAsB,wBAAwB,CAAC,MAAM,EAAE;IACrD,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC/B,GAAG,OAAO,CAAC,IAAI,CAAC,CA6DhB;AAsID,wBAAsB,4BAA4B,CAAC,CAAC,EAAE,MAAM,EAAE;IAC5D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,GAAG,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;CACzC,GAAG,OAAO,CAAC,CAAC,CAAC,CA6Bb;AAED,wBAAsB,iCAAiC,CAAC,MAAM,EAAE;IAC9D,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC/B,GAAG,OAAO,CAAC,IAAI,CAAC,CA8FhB;AAED,wBAAgB,kCAAkC,CAAC,YAAY,EAAE,MAAM,GAAG,oBAAoB,CAE7F"}
1
+ {"version":3,"file":"archive-staging.d.ts","sourceRoot":"","sources":["../src/archive-staging.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,oBAAoB,EAErB,MAAM,qBAAqB,CAAC;AAkB7B,OAAO,EAAE,oBAAoB,EAAE,KAAK,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAS1F,wBAAsB,4BAA4B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAS5F;AAED,wBAAsB,4BAA4B,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAY5F;AAED,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA4BnF;AA0BD,wBAAsB,+BAA+B,CAAC,MAAM,EAAE;IAC5D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAahB;AAiBD,KAAK,uBAAuB,GAAG;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC/B,CAAC;AAEF,wBAAsB,wBAAwB,CAAC,MAAM,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAE7F;AAED,wBAAsB,+BAA+B,CACnD,MAAM,EAAE,uBAAuB,EAAE,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAClE,OAAO,CAAC,IAAI,CAAC,CAEf;AA8GD,wBAAsB,4BAA4B,CAAC,CAAC,EAAE,MAAM,EAAE;IAC5D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,GAAG,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;CACzC,GAAG,OAAO,CAAC,CAAC,CAAC,CA6Bb;AAED,OAAO,EAAE,iCAAiC,EAAE,MAAM,oBAAoB,CAAC;AAEvE,wBAAgB,kCAAkC,CAAC,YAAY,EAAE,MAAM,GAAG,oBAAoB,CAE7F"}