@openclaw/fs-safe 0.10.0 → 0.12.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 (302) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/LICENSE +1 -0
  3. package/README.md +39 -6
  4. package/dist/absolute-path.d.ts.map +1 -1
  5. package/dist/absolute-path.js +3 -2
  6. package/dist/advanced.d.ts +5 -1
  7. package/dist/advanced.d.ts.map +1 -1
  8. package/dist/advanced.js +5 -1
  9. package/dist/archive-durability.d.ts +6 -6
  10. package/dist/archive-durability.d.ts.map +1 -1
  11. package/dist/archive-durability.js +1 -1
  12. package/dist/archive-entry.d.ts.map +1 -1
  13. package/dist/archive-entry.js +8 -8
  14. package/dist/archive-gzip-tail.d.ts +1 -0
  15. package/dist/archive-gzip-tail.d.ts.map +1 -1
  16. package/dist/archive-gzip-tail.js +16 -9
  17. package/dist/archive-input.d.ts.map +1 -1
  18. package/dist/archive-input.js +4 -2
  19. package/dist/archive-merge.d.ts +5 -1
  20. package/dist/archive-merge.d.ts.map +1 -1
  21. package/dist/archive-merge.js +15 -12
  22. package/dist/archive-native.js +4 -4
  23. package/dist/archive-parser.wasm +0 -0
  24. package/dist/archive-read.d.ts.map +1 -1
  25. package/dist/archive-read.js +54 -42
  26. package/dist/archive-staging.d.ts +6 -3
  27. package/dist/archive-staging.d.ts.map +1 -1
  28. package/dist/archive-staging.js +42 -22
  29. package/dist/archive-tar-stream.d.ts.map +1 -1
  30. package/dist/archive-tar-stream.js +7 -6
  31. package/dist/archive-tar-wasm.d.ts.map +1 -1
  32. package/dist/archive-tar-wasm.js +16 -13
  33. package/dist/archive-zip-admission.d.ts +1 -1
  34. package/dist/archive-zip-admission.d.ts.map +1 -1
  35. package/dist/archive-zip-admission.js +48 -12
  36. package/dist/archive-zip-loader.d.ts +6 -0
  37. package/dist/archive-zip-loader.d.ts.map +1 -0
  38. package/dist/archive-zip-loader.js +38 -0
  39. package/dist/archive-zip-names.d.ts.map +1 -1
  40. package/dist/archive-zip-names.js +26 -9
  41. package/dist/archive-zip-preflight.d.ts +2 -3
  42. package/dist/archive-zip-preflight.d.ts.map +1 -1
  43. package/dist/archive-zip-preflight.js +2 -34
  44. package/dist/archive.d.ts.map +1 -1
  45. package/dist/archive.js +12 -10
  46. package/dist/bounded-read.d.ts +5 -0
  47. package/dist/bounded-read.d.ts.map +1 -1
  48. package/dist/bounded-read.js +18 -11
  49. package/dist/copy-file-input.d.ts +0 -1
  50. package/dist/copy-file-input.d.ts.map +1 -1
  51. package/dist/copy-file-input.js +4 -26
  52. package/dist/copy-tree-portable.d.ts.map +1 -1
  53. package/dist/copy-tree-portable.js +57 -26
  54. package/dist/copy.d.ts +1 -1
  55. package/dist/copy.d.ts.map +1 -1
  56. package/dist/copy.js +3 -1
  57. package/dist/device-path.d.ts.map +1 -1
  58. package/dist/device-path.js +5 -3
  59. package/dist/directory-durability.d.ts.map +1 -1
  60. package/dist/directory-durability.js +5 -4
  61. package/dist/directory-guard.d.ts +11 -1
  62. package/dist/directory-guard.d.ts.map +1 -1
  63. package/dist/directory-guard.js +53 -11
  64. package/dist/durability.d.ts +1 -1
  65. package/dist/durability.d.ts.map +1 -1
  66. package/dist/durability.js +1 -1
  67. package/dist/file-handle-transfer.d.ts +14 -0
  68. package/dist/file-handle-transfer.d.ts.map +1 -0
  69. package/dist/file-handle-transfer.js +64 -0
  70. package/dist/file-hash.d.ts +3 -0
  71. package/dist/file-hash.d.ts.map +1 -1
  72. package/dist/file-hash.js +99 -32
  73. package/dist/file-lock-sync.d.ts.map +1 -1
  74. package/dist/file-lock-sync.js +8 -4
  75. package/dist/file-store-boundary.d.ts.map +1 -1
  76. package/dist/file-store-boundary.js +7 -5
  77. package/dist/file-store-path.d.ts +3 -0
  78. package/dist/file-store-path.d.ts.map +1 -0
  79. package/dist/file-store-path.js +27 -0
  80. package/dist/file-store-prune.d.ts.map +1 -1
  81. package/dist/file-store-prune.js +15 -5
  82. package/dist/file-store-sync-write.d.ts.map +1 -1
  83. package/dist/file-store-sync-write.js +56 -44
  84. package/dist/file-store.d.ts.map +1 -1
  85. package/dist/file-store.js +2 -18
  86. package/dist/filename.d.ts +1 -0
  87. package/dist/filename.d.ts.map +1 -1
  88. package/dist/filename.js +32 -14
  89. package/dist/guarded-mkdir.d.ts +2 -0
  90. package/dist/guarded-mkdir.d.ts.map +1 -1
  91. package/dist/guarded-mkdir.js +52 -16
  92. package/dist/guest-dispatch-python.d.ts +2 -0
  93. package/dist/guest-dispatch-python.d.ts.map +1 -0
  94. package/dist/guest-dispatch-python.js +117 -0
  95. package/dist/guest-native-python.d.ts +4 -0
  96. package/dist/guest-native-python.d.ts.map +1 -0
  97. package/dist/guest-native-python.js +135 -0
  98. package/dist/guest.d.ts +9 -0
  99. package/dist/guest.d.ts.map +1 -0
  100. package/dist/guest.js +421 -0
  101. package/dist/index.d.ts +1 -1
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/install-path.d.ts +6 -0
  104. package/dist/install-path.d.ts.map +1 -1
  105. package/dist/install-path.js +16 -2
  106. package/dist/json-durable-queue-directory.js +3 -3
  107. package/dist/json-durable-queue-ownership.d.ts +2 -0
  108. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  109. package/dist/json-durable-queue-ownership.js +47 -3
  110. package/dist/json-durable-queue-read.d.ts +6 -0
  111. package/dist/json-durable-queue-read.d.ts.map +1 -0
  112. package/dist/json-durable-queue-read.js +59 -0
  113. package/dist/json-durable-queue.d.ts +1 -1
  114. package/dist/json-durable-queue.d.ts.map +1 -1
  115. package/dist/json-durable-queue.js +43 -84
  116. package/dist/json.d.ts.map +1 -1
  117. package/dist/json.js +2 -1
  118. package/dist/local-roots.d.ts.map +1 -1
  119. package/dist/local-roots.js +2 -1
  120. package/dist/move-path-stage.d.ts.map +1 -1
  121. package/dist/move-path-stage.js +2 -1
  122. package/dist/move-path.d.ts.map +1 -1
  123. package/dist/move-path.js +4 -3
  124. package/dist/mutation-authority.d.ts +1 -0
  125. package/dist/mutation-authority.d.ts.map +1 -1
  126. package/dist/mutation-authority.js +4 -4
  127. package/dist/native-binding.d.ts +14 -2
  128. package/dist/native-binding.d.ts.map +1 -1
  129. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  130. package/dist/native-pinned-write-windows.js +3 -2
  131. package/dist/native-pinned-write.d.ts.map +1 -1
  132. package/dist/native-pinned-write.js +2 -1
  133. package/dist/opened-realpath.d.ts.map +1 -1
  134. package/dist/opened-realpath.js +5 -4
  135. package/dist/output.d.ts +2 -0
  136. package/dist/output.d.ts.map +1 -1
  137. package/dist/output.js +2 -0
  138. package/dist/overwrite-file-handle.d.ts +8 -0
  139. package/dist/overwrite-file-handle.d.ts.map +1 -0
  140. package/dist/overwrite-file-handle.js +42 -0
  141. package/dist/path-case.d.ts +7 -0
  142. package/dist/path-case.d.ts.map +1 -0
  143. package/dist/path-case.js +136 -0
  144. package/dist/path-scope-lexical.d.ts +14 -0
  145. package/dist/path-scope-lexical.d.ts.map +1 -0
  146. package/dist/path-scope-lexical.js +27 -0
  147. package/dist/path.d.ts.map +1 -1
  148. package/dist/path.js +22 -3
  149. package/dist/permissions-windows.d.ts +1 -1
  150. package/dist/permissions-windows.d.ts.map +1 -1
  151. package/dist/permissions-windows.js +48 -6
  152. package/dist/pinned-open.d.ts.map +1 -1
  153. package/dist/pinned-open.js +3 -1
  154. package/dist/pinned-write.d.ts +2 -2
  155. package/dist/pinned-write.d.ts.map +1 -1
  156. package/dist/pinned-write.js +3 -1
  157. package/dist/private-temp-workspace.d.ts.map +1 -1
  158. package/dist/private-temp-workspace.js +6 -4
  159. package/dist/realpath.d.ts +4 -0
  160. package/dist/realpath.d.ts.map +1 -0
  161. package/dist/realpath.js +43 -0
  162. package/dist/recursive-mkdir-path.d.ts +3 -0
  163. package/dist/recursive-mkdir-path.d.ts.map +1 -0
  164. package/dist/recursive-mkdir-path.js +8 -0
  165. package/dist/replace-directory.d.ts.map +1 -1
  166. package/dist/replace-directory.js +2 -1
  167. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  168. package/dist/replace-file-copy-fallback.js +23 -31
  169. package/dist/replace-file-copy-source.d.ts.map +1 -1
  170. package/dist/replace-file-copy-source.js +7 -12
  171. package/dist/replace-file-mode.d.ts +3 -0
  172. package/dist/replace-file-mode.d.ts.map +1 -0
  173. package/dist/replace-file-mode.js +10 -0
  174. package/dist/replace-file.d.ts +1 -0
  175. package/dist/replace-file.d.ts.map +1 -1
  176. package/dist/replace-file.js +14 -8
  177. package/dist/root-boundary.d.ts +25 -0
  178. package/dist/root-boundary.d.ts.map +1 -0
  179. package/dist/root-boundary.js +177 -0
  180. package/dist/root-context.d.ts.map +1 -1
  181. package/dist/root-context.js +40 -13
  182. package/dist/root-create-input.d.ts +10 -0
  183. package/dist/root-create-input.d.ts.map +1 -0
  184. package/dist/root-create-input.js +80 -0
  185. package/dist/root-directory-list.d.ts +3 -1
  186. package/dist/root-directory-list.d.ts.map +1 -1
  187. package/dist/root-directory-list.js +31 -5
  188. package/dist/root-entries.d.ts +11 -0
  189. package/dist/root-entries.d.ts.map +1 -0
  190. package/dist/root-entries.js +61 -0
  191. package/dist/root-errors.d.ts +5 -5
  192. package/dist/root-errors.d.ts.map +1 -1
  193. package/dist/root-errors.js +13 -12
  194. package/dist/root-impl.d.ts +13 -3
  195. package/dist/root-impl.d.ts.map +1 -1
  196. package/dist/root-impl.js +93 -70
  197. package/dist/root-move-preflight.d.ts +8 -0
  198. package/dist/root-move-preflight.d.ts.map +1 -0
  199. package/dist/root-move-preflight.js +16 -0
  200. package/dist/root-options.d.ts +12 -1
  201. package/dist/root-options.d.ts.map +1 -1
  202. package/dist/root-path-existing.d.ts +2 -0
  203. package/dist/root-path-existing.d.ts.map +1 -1
  204. package/dist/root-path-existing.js +14 -5
  205. package/dist/root-path-symlink.d.ts.map +1 -1
  206. package/dist/root-path-symlink.js +3 -2
  207. package/dist/root-path.d.ts +2 -0
  208. package/dist/root-path.d.ts.map +1 -1
  209. package/dist/root-path.js +54 -26
  210. package/dist/root-paths.d.ts +2 -6
  211. package/dist/root-paths.d.ts.map +1 -1
  212. package/dist/root-paths.js +24 -32
  213. package/dist/root-remove.d.ts +5 -0
  214. package/dist/root-remove.d.ts.map +1 -0
  215. package/dist/root-remove.js +286 -0
  216. package/dist/root-symlink-policy.d.ts +2 -1
  217. package/dist/root-symlink-policy.d.ts.map +1 -1
  218. package/dist/root-symlink-policy.js +2 -2
  219. package/dist/root-walk.d.ts.map +1 -1
  220. package/dist/root-walk.js +2 -1
  221. package/dist/root-write-mode.d.ts +2 -0
  222. package/dist/root-write-mode.d.ts.map +1 -1
  223. package/dist/root-write-mode.js +21 -7
  224. package/dist/root-write-verification.d.ts.map +1 -1
  225. package/dist/root-write-verification.js +12 -3
  226. package/dist/root.d.ts +2 -1
  227. package/dist/root.d.ts.map +1 -1
  228. package/dist/safe-path-segment.d.ts.map +1 -1
  229. package/dist/safe-path-segment.js +3 -1
  230. package/dist/secret-file.d.ts.map +1 -1
  231. package/dist/secret-file.js +2 -1
  232. package/dist/secret-read-async.d.ts.map +1 -1
  233. package/dist/secret-read-async.js +2 -1
  234. package/dist/secure-file-windows.d.ts +10 -0
  235. package/dist/secure-file-windows.d.ts.map +1 -0
  236. package/dist/secure-file-windows.js +186 -0
  237. package/dist/secure-file.d.ts.map +1 -1
  238. package/dist/secure-file.js +27 -7
  239. package/dist/secure-temp-dir.d.ts.map +1 -1
  240. package/dist/secure-temp-dir.js +2 -1
  241. package/dist/sibling-staged-file.d.ts +2 -0
  242. package/dist/sibling-staged-file.d.ts.map +1 -1
  243. package/dist/sibling-staged-file.js +49 -8
  244. package/dist/sibling-temp.d.ts +2 -0
  245. package/dist/sibling-temp.d.ts.map +1 -1
  246. package/dist/sibling-temp.js +8 -5
  247. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  248. package/dist/sidecar-lock-acquire.js +16 -5
  249. package/dist/sidecar-lock-policy.d.ts +2 -0
  250. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  251. package/dist/sidecar-lock-policy.js +17 -0
  252. package/dist/sidecar-lock.d.ts.map +1 -1
  253. package/dist/sidecar-lock.js +2 -3
  254. package/dist/staged-directory.d.ts.map +1 -1
  255. package/dist/staged-directory.js +4 -3
  256. package/dist/temp-target.d.ts +14 -12
  257. package/dist/temp-target.d.ts.map +1 -1
  258. package/dist/temp-target.js +15 -7
  259. package/dist/trash.d.ts.map +1 -1
  260. package/dist/trash.js +7 -5
  261. package/dist/unicode-path.d.ts +3 -0
  262. package/dist/unicode-path.d.ts.map +1 -0
  263. package/dist/unicode-path.js +13 -0
  264. package/dist/walk.d.ts.map +1 -1
  265. package/dist/walk.js +14 -12
  266. package/dist/write-file-handle.d.ts +1 -0
  267. package/dist/write-file-handle.d.ts.map +1 -1
  268. package/dist/write-file-handle.js +3 -2
  269. package/docs/advanced.md +7 -1
  270. package/docs/archive.md +31 -5
  271. package/docs/atomic.md +17 -1
  272. package/docs/config.md +1 -0
  273. package/docs/contributing.md +33 -2
  274. package/docs/copy.md +75 -6
  275. package/docs/directory-identity.md +85 -0
  276. package/docs/durability.md +40 -3
  277. package/docs/entries.md +109 -0
  278. package/docs/errors.md +3 -3
  279. package/docs/file-store.md +21 -0
  280. package/docs/filename.md +9 -2
  281. package/docs/guest.md +146 -0
  282. package/docs/in-place-write.md +81 -0
  283. package/docs/index.md +2 -0
  284. package/docs/install-path.md +59 -13
  285. package/docs/install.md +34 -0
  286. package/docs/native-helper.md +14 -5
  287. package/docs/native.md +18 -2
  288. package/docs/output.md +32 -6
  289. package/docs/path-case.md +64 -0
  290. package/docs/path-scope.md +1 -1
  291. package/docs/path.md +1 -1
  292. package/docs/permissions.md +37 -3
  293. package/docs/public-api.md +36 -2
  294. package/docs/root.md +33 -3
  295. package/docs/secure-file.md +17 -14
  296. package/docs/security-model.md +1 -1
  297. package/docs/sidecar-lock.md +12 -3
  298. package/docs/store.md +22 -1
  299. package/docs/temp.md +39 -6
  300. package/docs/types.md +1 -1
  301. package/docs/writing.md +153 -3
  302. package/package.json +14 -8
@@ -0,0 +1,286 @@
1
+ import fsSync from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { assertAsyncDirectoryGuard, assertSyncDirectoryGuard, createAsyncDirectoryGuard, } from "./directory-guard.js";
5
+ import { assertMutationNotDenied } from "./deny-mutations.js";
6
+ import { FsSafeError } from "./errors.js";
7
+ import { sameFileIdentityForCleanup } from "./file-identity.js";
8
+ import { MutationAuthorityError } from "./mutation-authority.js";
9
+ import { isNotFoundPathError, isPathInside } from "./path.js";
10
+ import { assertRootIdentityCurrent } from "./root-context.js";
11
+ import { normalizeRemoveGuardError, normalizeRemovePathError, rootPathChangedError } from "./root-errors.js";
12
+ import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
13
+ import { inspectFileIdentitySync } from "./strict-file-identity.js";
14
+ import { createSuppressedError } from "./suppressed-error.js";
15
+ import { getFsSafeTestHooks } from "./test-hooks.js";
16
+ const DEFAULT_MAX_ENTRIES = 100_000;
17
+ const DEFAULT_MAX_DEPTH = 64;
18
+ export function validateRemoveOptions(options) {
19
+ if (options.order !== undefined && options.order !== "filesystem" && options.order !== "sorted") {
20
+ throw new TypeError("remove order must be filesystem or sorted");
21
+ }
22
+ for (const key of ["maxEntries", "maxDepth"]) {
23
+ const value = options[key];
24
+ if (value !== undefined && value !== Number.POSITIVE_INFINITY && (!Number.isSafeInteger(value) || value < 0)) {
25
+ throw new RangeError(`${key} must be a non-negative safe integer`);
26
+ }
27
+ }
28
+ if (!options.recursive && (options.maxEntries !== undefined || options.maxDepth !== undefined)) {
29
+ throw new TypeError("remove budgets require recursive: true");
30
+ }
31
+ if (!options.recursive && options.order !== undefined) {
32
+ throw new TypeError("remove order requires recursive: true");
33
+ }
34
+ }
35
+ function assertNotAborted(signal) {
36
+ try {
37
+ signal?.throwIfAborted();
38
+ }
39
+ catch (error) {
40
+ // Preserve arbitrary abort reasons through Root's filesystem error mapping.
41
+ throw new MutationAuthorityError(error);
42
+ }
43
+ }
44
+ async function removeOne(root, targetPath, options) {
45
+ let guard;
46
+ try {
47
+ guard = await createAsyncDirectoryGuard(path.dirname(targetPath));
48
+ }
49
+ catch (error) {
50
+ if (options.force && isNotFoundPathError(error)) {
51
+ await assertRootIdentityCurrent(root);
52
+ assertNotAborted(options.signal);
53
+ return;
54
+ }
55
+ throw normalizeRemoveGuardError(error);
56
+ }
57
+ try {
58
+ await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("remove", targetPath);
59
+ await assertAsyncDirectoryGuard(guard);
60
+ }
61
+ catch (error) {
62
+ throw normalizeRemoveGuardError(error);
63
+ }
64
+ try {
65
+ const isDirectory = fsSync.lstatSync(targetPath).isDirectory();
66
+ assertFinalSymlinkRejected(targetPath, options.mutationSymlinks !== undefined);
67
+ assertNotAborted(options.signal);
68
+ options.assertBeforeMutation?.();
69
+ await (isDirectory ? fs.rmdir(targetPath) : fs.unlink(targetPath));
70
+ }
71
+ catch (error) {
72
+ if (!(options.force && isNotFoundPathError(error)))
73
+ throw normalizeRemovePathError(error);
74
+ }
75
+ await assertAsyncDirectoryGuard(guard).catch(error => { throw normalizeRemoveGuardError(error); });
76
+ assertNotAborted(options.signal);
77
+ }
78
+ export async function removePathInRootFallback(root, targetPath, options) {
79
+ assertNotAborted(options.signal);
80
+ if (!options.recursive) {
81
+ await removeOne(root, targetPath, options);
82
+ return;
83
+ }
84
+ const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
85
+ const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
86
+ const details = (target, phase) => ({
87
+ operation: "remove",
88
+ phase,
89
+ relativePath: path.relative(targetPath, target),
90
+ });
91
+ let examined = 0;
92
+ const rootGuard = await createAsyncDirectoryGuard(root.rootReal, { bigint: true });
93
+ if (!sameFileIdentityForCleanup(rootGuard.stat, root.rootIdentity))
94
+ throw rootPathChangedError();
95
+ const guards = [rootGuard];
96
+ let ancestor = root.rootReal;
97
+ const parentRelative = path.relative(root.rootReal, path.dirname(targetPath));
98
+ for (const segment of parentRelative.split(path.sep).filter(Boolean)) {
99
+ ancestor = path.join(ancestor, segment);
100
+ let guard;
101
+ try {
102
+ guard = await createAsyncDirectoryGuard(ancestor, { bigint: true });
103
+ }
104
+ catch (error) {
105
+ if (isNotFoundPathError(error)) {
106
+ assertCurrent();
107
+ assertNotAborted(options.signal);
108
+ if (options.force)
109
+ return;
110
+ }
111
+ throw normalizeRemoveGuardError(error);
112
+ }
113
+ if (!isPathInside(guard.realPath, ancestor) || !isPathInside(ancestor, guard.realPath)) {
114
+ throw new FsSafeError("path-mismatch", "removal ancestor changed during operation");
115
+ }
116
+ guards.push(guard);
117
+ }
118
+ function assertCurrent() {
119
+ for (const guard of guards) {
120
+ try {
121
+ assertSyncDirectoryGuard(guard);
122
+ }
123
+ catch (error) {
124
+ // force tolerates a vanished leaf, never a lost traversal boundary.
125
+ throw new FsSafeError("path-mismatch", "removal ancestor changed during operation", { cause: error });
126
+ }
127
+ }
128
+ }
129
+ const inspect = (target, expected) => {
130
+ assertCurrent();
131
+ try {
132
+ return inspectFileIdentitySync(() => fsSync.lstatSync(target, { bigint: true }), expected);
133
+ }
134
+ catch (error) {
135
+ if (options.force && isNotFoundPathError(error))
136
+ return;
137
+ throw normalizeRemovePathError(error, details(target, expected ? "remove" : "inspect"));
138
+ }
139
+ };
140
+ async function visit(target, depth, entryCounted = false) {
141
+ assertNotAborted(options.signal);
142
+ if ((!entryCounted && examined >= maxEntries) || depth > maxDepth) {
143
+ throw new FsSafeError("too-large", "recursive removal budget exceeded", {
144
+ details: details(target, "inspect"),
145
+ });
146
+ }
147
+ if (!entryCounted)
148
+ examined += 1;
149
+ await assertMutationNotDenied(target, options.denyMutations, { protectAncestors: true });
150
+ assertNotAborted(options.signal);
151
+ const initial = inspect(target);
152
+ if (!initial)
153
+ return;
154
+ assertFinalSymlinkRejected(target, options.mutationSymlinks !== undefined, details(target, "inspect"));
155
+ if (initial.isDirectory()) {
156
+ let directoryGuard;
157
+ try {
158
+ directoryGuard = await createAsyncDirectoryGuard(target, { bigint: true });
159
+ }
160
+ catch (error) {
161
+ assertCurrent();
162
+ assertNotAborted(options.signal);
163
+ if (options.force && isNotFoundPathError(error))
164
+ return;
165
+ throw normalizeRemovePathError(error, details(target, "inspect"));
166
+ }
167
+ if (!isPathInside(directoryGuard.realPath, target) || !isPathInside(target, directoryGuard.realPath) ||
168
+ !sameFileIdentityForCleanup(directoryGuard.stat, initial)) {
169
+ throw new FsSafeError("path-mismatch", "removal directory changed during operation");
170
+ }
171
+ guards.push(directoryGuard);
172
+ try {
173
+ assertCurrent();
174
+ assertNotAborted(options.signal);
175
+ const handle = await fs.opendir(target, { bufferSize: 1 }).catch(error => {
176
+ if (options.force && isNotFoundPathError(error)) {
177
+ assertNotAborted(options.signal);
178
+ return undefined;
179
+ }
180
+ throw normalizeRemovePathError(error, details(target, "enumerate"));
181
+ });
182
+ if (handle) {
183
+ let failed = false;
184
+ let operationError;
185
+ try {
186
+ let names = options.order === "sorted" ? [] : undefined;
187
+ if (names && maxEntries === Infinity) {
188
+ assertCurrent();
189
+ assertNotAborted(options.signal);
190
+ const snapshot = await fs.readdir(target).catch(error => {
191
+ if (options.force && isNotFoundPathError(error))
192
+ return undefined;
193
+ throw normalizeRemovePathError(error, details(target, "enumerate"));
194
+ });
195
+ assertNotAborted(options.signal);
196
+ if (snapshot) {
197
+ assertCurrent();
198
+ names = snapshot;
199
+ examined += names.length;
200
+ }
201
+ }
202
+ else {
203
+ while (true) {
204
+ assertCurrent();
205
+ assertNotAborted(options.signal);
206
+ const entry = await handle.read().catch(error => {
207
+ if (options.force && isNotFoundPathError(error))
208
+ return undefined;
209
+ throw normalizeRemovePathError(error, details(target, "enumerate"));
210
+ });
211
+ if (entry === undefined) {
212
+ assertNotAborted(options.signal);
213
+ if (names)
214
+ names.length = 0;
215
+ break;
216
+ }
217
+ assertCurrent();
218
+ assertNotAborted(options.signal);
219
+ if (!entry)
220
+ break;
221
+ if (names) {
222
+ if (examined >= maxEntries) {
223
+ throw new FsSafeError("too-large", "recursive removal budget exceeded", {
224
+ details: details(path.join(target, entry.name), "enumerate"),
225
+ });
226
+ }
227
+ examined += 1;
228
+ names.push(entry.name);
229
+ }
230
+ else {
231
+ await visit(path.join(target, entry.name), depth + 1);
232
+ }
233
+ }
234
+ }
235
+ if (names) {
236
+ for (const name of names.sort()) {
237
+ await visit(path.join(target, name), depth + 1, true);
238
+ }
239
+ }
240
+ }
241
+ catch (error) {
242
+ failed = true;
243
+ operationError = error instanceof MutationAuthorityError ? error.rejection : error;
244
+ throw error;
245
+ }
246
+ finally {
247
+ try {
248
+ // Close before rmdir, including on Windows and after revocation.
249
+ await handle.close();
250
+ }
251
+ catch (error) {
252
+ const closeError = normalizeRemovePathError(error, details(target, "enumerate"));
253
+ if (failed)
254
+ throw createSuppressedError(closeError, operationError, "recursive removal and close both failed");
255
+ throw closeError;
256
+ }
257
+ }
258
+ }
259
+ }
260
+ finally {
261
+ guards.pop();
262
+ }
263
+ }
264
+ // A tolerated missing-directory observation still rechecks its target and ancestors.
265
+ await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("remove", target);
266
+ await assertMutationNotDenied(target, options.denyMutations, { protectAncestors: true });
267
+ assertNotAborted(options.signal);
268
+ if (!inspect(target, initial))
269
+ return;
270
+ assertFinalSymlinkRejected(target, options.mutationSymlinks !== undefined, details(target, "remove"));
271
+ assertNotAborted(options.signal);
272
+ options.assertBeforeMutation?.();
273
+ try {
274
+ await (initial.isDirectory() ? fs.rmdir(target) : fs.unlink(target));
275
+ }
276
+ catch (error) {
277
+ if (!(options.force && isNotFoundPathError(error))) {
278
+ throw normalizeRemovePathError(error, details(target, "remove"));
279
+ }
280
+ }
281
+ assertCurrent();
282
+ assertNotAborted(options.signal);
283
+ }
284
+ await assertRootIdentityCurrent(root);
285
+ await visit(targetPath, 0);
286
+ }
@@ -1,3 +1,4 @@
1
+ import { type FsSafeErrorDetails } from "./errors.js";
1
2
  export type SymlinkPolicy = "reject" | "follow-within-root" | "follow-parents-within-root";
2
3
  export type MutationSymlinkPolicy = "reject" | "follow-parents-within-root";
3
4
  export declare function readSymlinkResolution(symlinks: SymlinkPolicy | undefined): {
@@ -9,5 +10,5 @@ export declare function mutationSymlinkResolution(symlinks: MutationSymlinkPolic
9
10
  rejectFinalSymlink: boolean;
10
11
  resolveCanonical: boolean;
11
12
  };
12
- export declare function assertFinalSymlinkRejected(targetPath: string, reject: boolean | undefined): void;
13
+ export declare function assertFinalSymlinkRejected(targetPath: string, reject: boolean | undefined, details?: FsSafeErrorDetails): void;
13
14
  //# sourceMappingURL=root-symlink-policy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"root-symlink-policy.d.ts","sourceRoot":"","sources":["../src/root-symlink-policy.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,oBAAoB,GAAG,4BAA4B,CAAC;AAC3F,MAAM,MAAM,qBAAqB,GAAG,QAAQ,GAAG,4BAA4B,CAAC;AAE5E,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,SAAS;IAErE,cAAc;IACd,kBAAkB;EAErB;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,SAAS;IAKjF,cAAc;IACd,kBAAkB;IAClB,gBAAgB;EAEnB;AAID,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI,CAShG"}
1
+ {"version":3,"file":"root-symlink-policy.d.ts","sourceRoot":"","sources":["../src/root-symlink-policy.ts"],"names":[],"mappings":"AACA,OAAO,EAAe,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGnE,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,oBAAoB,GAAG,4BAA4B,CAAC;AAC3F,MAAM,MAAM,qBAAqB,GAAG,QAAQ,GAAG,4BAA4B,CAAC;AAE5E,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,SAAS;IAErE,cAAc;IACd,kBAAkB;EAErB;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,SAAS;IAKjF,cAAc;IACd,kBAAkB;IAClB,gBAAgB;EAEnB;AAID,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,OAAO,GAAG,SAAS,EAC3B,OAAO,CAAC,EAAE,kBAAkB,GAC3B,IAAI,CASN"}
@@ -19,12 +19,12 @@ export function mutationSymlinkResolution(symlinks) {
19
19
  }
20
20
  // Call after parent fences and immediately before the mutation. This is a
21
21
  // final-component check, not an atomic conditional rename against another process.
22
- export function assertFinalSymlinkRejected(targetPath, reject) {
22
+ export function assertFinalSymlinkRejected(targetPath, reject, details) {
23
23
  if (!reject)
24
24
  return;
25
25
  try {
26
26
  if (fs.lstatSync(targetPath).isSymbolicLink()) {
27
- throw new FsSafeError("symlink", "final symlink not allowed");
27
+ throw new FsSafeError("symlink", "final symlink not allowed", { details });
28
28
  }
29
29
  }
30
30
  catch (error) {
@@ -1 +1 @@
1
- {"version":3,"file":"root-walk.d.ts","sourceRoot":"","sources":["../src/root-walk.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAClG,OAAO,KAAK,EAAY,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGrD,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,oBAAoB,CAAC;AAClE,MAAM,MAAM,qBAAqB,GAAG,UAAU,GAAG,OAAO,CAAC;AACzD,MAAM,MAAM,8BAA8B,GAAG,OAAO,GAAG,iBAAiB,CAAC;AACzE,MAAM,MAAM,yBAAyB,GAAG,SAAS,GAAG,MAAM,GAAG,cAAc,CAAC;AAC5E,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,WAAW,GAAG,OAAO,CAAC;AACnE,MAAM,MAAM,iBAAiB,GAAG,qBAAqB,GAAG,iBAAiB,GAAG,WAAW,CAAC;AAExF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,qBAAqB,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,aAAa,GACrB,iBAAiB,GACjB;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,GACpD;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,iBAAiB,CAAC;IAAC,IAAI,EAAE,CAAC,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAE/E,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,iBAAiB,KAAK,yBAAyB,CAAC;AAE1F,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,QAAQ,GAAG,YAAY,CAAC;IAChC,aAAa,EAAE,qBAAqB,CAAC;IACrC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,aAAa,CAAC,EAAE,qBAAqB,CAAC;IACtC,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAClC,gBAAgB,CAAC,EAAE,8BAA8B,CAAC;CACnD,CAAC;AAEF,KAAK,kBAAkB,GAAG;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC9C,IAAI,CACF,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,oBAAoB,CAAC,CAAC;CAClC,CAAC;AAqBF,wBAAuB,QAAQ,CAC7B,IAAI,EAAE,kBAAkB,EACxB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,eAAe,GACvB,cAAc,CAAC,aAAa,CAAC,CAqK/B"}
1
+ {"version":3,"file":"root-walk.d.ts","sourceRoot":"","sources":["../src/root-walk.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAClG,OAAO,KAAK,EAAY,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGrD,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,oBAAoB,CAAC;AAClE,MAAM,MAAM,qBAAqB,GAAG,UAAU,GAAG,OAAO,CAAC;AACzD,MAAM,MAAM,8BAA8B,GAAG,OAAO,GAAG,iBAAiB,CAAC;AACzE,MAAM,MAAM,yBAAyB,GAAG,SAAS,GAAG,MAAM,GAAG,cAAc,CAAC;AAC5E,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,WAAW,GAAG,OAAO,CAAC;AACnE,MAAM,MAAM,iBAAiB,GAAG,qBAAqB,GAAG,iBAAiB,GAAG,WAAW,CAAC;AAExF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,qBAAqB,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,aAAa,GACrB,iBAAiB,GACjB;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,GACpD;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,iBAAiB,CAAC;IAAC,IAAI,EAAE,CAAC,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAE/E,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,iBAAiB,KAAK,yBAAyB,CAAC;AAE1F,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,QAAQ,GAAG,YAAY,CAAC;IAChC,aAAa,EAAE,qBAAqB,CAAC;IACrC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,aAAa,CAAC,EAAE,qBAAqB,CAAC;IACtC,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAClC,gBAAgB,CAAC,EAAE,8BAA8B,CAAC;CACnD,CAAC;AAEF,KAAK,kBAAkB,GAAG;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC9C,IAAI,CACF,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,oBAAoB,CAAC,CAAC;CAClC,CAAC;AAqBF,wBAAuB,QAAQ,CAC7B,IAAI,EAAE,kBAAkB,EACxB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,eAAe,GACvB,cAAc,CAAC,aAAa,CAAC,CAsK/B"}
package/dist/root-walk.js CHANGED
@@ -98,6 +98,7 @@ export async function* walkRoot(root, relativePath, options) {
98
98
  let failed = false;
99
99
  let operationError;
100
100
  try {
101
+ const relativeDirectory = directory.split(path.sep).join(path.posix.sep);
101
102
  while (true) {
102
103
  let next;
103
104
  try {
@@ -112,7 +113,7 @@ export async function* walkRoot(root, relativePath, options) {
112
113
  return;
113
114
  const name = next.kind === "entry" ? next.entry.name : next.name;
114
115
  const child = directory
115
- ? path.posix.join(directory.split(path.sep).join(path.posix.sep), name)
116
+ ? path.posix.join(relativeDirectory, name)
116
117
  : name;
117
118
  if (next.kind === "limit") {
118
119
  yield onLimit(child);
@@ -1,6 +1,8 @@
1
+ import { type RootBoundaryIdentity } from "./root-boundary.js";
1
2
  export declare function inheritWriteTargetMode(params: {
2
3
  targetPath: string;
3
4
  rootWithSep: string;
5
+ rootIdentity?: RootBoundaryIdentity;
4
6
  requestedMode?: number;
5
7
  }): Promise<number>;
6
8
  //# sourceMappingURL=root-write-mode.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"root-write-mode.d.ts","sourceRoot":"","sources":["../src/root-write-mode.ts"],"names":[],"mappings":"AASA,wBAAsB,sBAAsB,CAAC,MAAM,EAAE;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CAsClB"}
1
+ {"version":3,"file":"root-write-mode.d.ts","sourceRoot":"","sources":["../src/root-write-mode.ts"],"names":[],"mappings":"AAQA,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAGpF,wBAAsB,sBAAsB,CAAC,MAAM,EAAE;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,oBAAoB,CAAC;IACpC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CAkDlB"}
@@ -1,25 +1,33 @@
1
1
  import fsSync from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import { FsSafeError } from "./errors.js";
4
- import { isNotFoundPathError, isPathInside } from "./path.js";
4
+ import { isNotFoundPathError } from "./path.js";
5
5
  import { resolveReadOpenFlags } from "./read-open-flags.js";
6
+ import { realpathSync } from "./realpath.js";
6
7
  import { hardlinkedPathNotAllowedError, outsideWorkspaceError } from "./root-errors.js";
7
8
  import { inspectFileIdentity } from "./strict-file-identity.js";
9
+ import { admitPathInsideRoot } from "./root-boundary.js";
8
10
  // The caller has already resolved and guarded this write target.
9
11
  export async function inheritWriteTargetMode(params) {
10
12
  try {
11
- const existing = await inspectFileIdentity(() => fsSync.lstatSync(params.targetPath, { bigint: true }));
13
+ const admittedTarget = admitPathInsideRoot({
14
+ rootPath: params.rootWithSep,
15
+ candidatePath: params.targetPath,
16
+ rootIdentity: params.rootIdentity,
17
+ });
18
+ if (!admittedTarget)
19
+ throw outsideWorkspaceError();
20
+ const targetPath = admittedTarget.path;
21
+ const existing = await inspectFileIdentity(() => fsSync.lstatSync(targetPath, { bigint: true }));
12
22
  if (existing.isSymbolicLink())
13
23
  throw new FsSafeError("path-alias", "path alias escape blocked");
14
24
  if (!existing.isFile())
15
25
  throw new FsSafeError("not-file", "not a file");
16
26
  if (existing.nlink > 1n)
17
27
  throw hardlinkedPathNotAllowedError();
18
- if (!isPathInside(params.rootWithSep, params.targetPath))
19
- throw outsideWorkspaceError();
20
28
  // Preserve read-open admission of the pre-existing destination. access(2)
21
29
  // is not equivalent: it ignores ACLs on Windows and capabilities on Linux.
22
- const handle = await fs.open(params.targetPath, resolveReadOpenFlags());
30
+ const handle = await fs.open(targetPath, resolveReadOpenFlags());
23
31
  try {
24
32
  // Bind admission to the inode whose metadata is inherited.
25
33
  await inspectFileIdentity(() => fsSync.fstatSync(handle.fd, { bigint: true }), existing);
@@ -30,9 +38,15 @@ export async function inheritWriteTargetMode(params) {
30
38
  try {
31
39
  // A parent can change after guarded resolution. Do not inherit metadata
32
40
  // from an outside inode, even if the parent is restored before publication.
33
- const realPath = fsSync.realpathSync.native(params.targetPath);
34
- if (!isPathInside(params.rootWithSep, realPath))
41
+ const observedRealPath = realpathSync.native(targetPath);
42
+ const admittedRealPath = admitPathInsideRoot({
43
+ rootPath: params.rootWithSep,
44
+ candidatePath: observedRealPath,
45
+ rootIdentity: params.rootIdentity,
46
+ });
47
+ if (!admittedRealPath)
35
48
  throw outsideWorkspaceError();
49
+ const realPath = admittedRealPath.path;
36
50
  await inspectFileIdentity(async () => {
37
51
  const current = fsSync.statSync(realPath, { bigint: true });
38
52
  if (!current.isFile())
@@ -1 +1 @@
1
- {"version":3,"file":"root-write-verification.d.ts","sourceRoot":"","sources":["../src/root-write-verification.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAK9F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAEhE,OAAO,EAA6B,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhF,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,gBAAgB,EAAE,sBAAsB,CAAC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,sBAAsB,CAAC;CACrC,GAAG,OAAO,CAAC,IAAI,CAAC,CAqFhB"}
1
+ {"version":3,"file":"root-write-verification.d.ts","sourceRoot":"","sources":["../src/root-write-verification.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAK9F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAEhE,OAAO,EAA6B,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIhF,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,gBAAgB,EAAE,sBAAsB,CAAC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,sBAAsB,CAAC;CACrC,GAAG,OAAO,CAAC,IAAI,CAAC,CA6FhB"}
@@ -4,10 +4,11 @@ import { assertAsyncDirectoryGuard } from "./directory-guard.js";
4
4
  import { FsSafeError } from "./errors.js";
5
5
  import { sameFileIdentity } from "./file-identity.js";
6
6
  import { resolveOpenedFileRealPathForFd } from "./opened-realpath.js";
7
- import { assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isPathInside, isSymlinkOpenError } from "./path.js";
7
+ import { assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isSymlinkOpenError } from "./path.js";
8
8
  import { resolveReadOpenFlags } from "./read-open-flags.js";
9
9
  import { assertRootIdentityCurrent } from "./root-context.js";
10
10
  import { fileNotFoundError, hardlinkedPathNotAllowedError, outsideWorkspaceError } from "./root-errors.js";
11
+ import { admitPathInsideRoot } from "./root-boundary.js";
11
12
  export async function verifyAtomicWriteResult(params) {
12
13
  let needsPathOpen = false;
13
14
  const assertFile = (stat) => {
@@ -49,7 +50,11 @@ export async function verifyAtomicWriteResult(params) {
49
50
  assertPath(fsSync.lstatSync(params.targetPath, { bigint: true }));
50
51
  const { realPath } = await resolveOpenedFileRealPathForFd(params.fd, stat, params.targetPath);
51
52
  assertPath(fsSync.statSync(realPath, { bigint: true }));
52
- if (!isPathInside(params.root.rootWithSep, realPath)) {
53
+ if (!admitPathInsideRoot({
54
+ rootPath: params.root.rootReal,
55
+ candidatePath: realPath,
56
+ rootIdentity: params.root.rootIdentity,
57
+ })) {
53
58
  throw outsideWorkspaceError();
54
59
  }
55
60
  await assertAsyncDirectoryGuard(params.parentGuard);
@@ -76,7 +81,11 @@ export async function verifyAtomicWriteResult(params) {
76
81
  assertPath(fsSync.lstatSync(params.targetPath, { bigint: true }));
77
82
  const { realPath: reopenedPath } = await resolveOpenedFileRealPathForFd(opened.fd, reopenedStat, params.targetPath);
78
83
  assertPath(fsSync.statSync(reopenedPath, { bigint: true }));
79
- if (!isPathInside(params.root.rootWithSep, reopenedPath)) {
84
+ if (!admitPathInsideRoot({
85
+ rootPath: params.root.rootReal,
86
+ candidatePath: reopenedPath,
87
+ rootIdentity: params.root.rootIdentity,
88
+ })) {
80
89
  throw outsideWorkspaceError();
81
90
  }
82
91
  await assertAsyncDirectoryGuard(params.parentGuard);
package/dist/root.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- export { DEFAULT_ROOT_MAX_BYTES, openLocalFileSafely, readLocalFileSafely, resolveOpenedFileRealPathForHandle, root, type DenyMutationPolicy, type HardlinkPolicy, type MutationSymlinkPolicy, type RenameIdentityPolicy, type OpenResult, type ReadResult, type Root, type RootAppendOptions, type RootCopyOptions, type RootCopySource, type RootCreateJsonOptions, type RootCreateOptions, type RootDefaults, type RootMkdirOptions, type RootMoveOptions, type RootOpenOptions, type RootOpenWritableOptions, type RootOptions, type RootReadOptions, type RootRemoveOptions, type RootWriteJsonOptions, type RootWriteOptions, type SymlinkPolicy, type WritableOpenMode, type WritableOpenResult, } from "./root-impl.js";
1
+ export { DEFAULT_ROOT_MAX_BYTES, openLocalFileSafely, readLocalFileSafely, resolveOpenedFileRealPathForHandle, root, type DenyMutationPolicy, type HardlinkPolicy, type MutationSymlinkPolicy, type RenameIdentityPolicy, type OpenResult, type ReadResult, type Root, type RootAppendOptions, type RootCopyOptions, type RootCopySource, type RootCreateJsonOptions, type RootCreateOptions, type RootCreateStreamOptions, type RootDefaults, type RootMkdirOptions, type RootMoveOptions, type RootOpenOptions, type RootOpenWritableOptions, type RootOptions, type RootReadOptions, type RootRemoveOptions, type RootWriteJsonOptions, type RootWriteOptions, type SymlinkPolicy, type WritableOpenMode, type WritableOpenResult, } from "./root-impl.js";
2
2
  export type { RootCopyPublicationReceipt } from "./copy-publication.js";
3
3
  export type { CopyCloneMode } from "./copy-policy.js";
4
+ export type { RootEntriesOptions } from "./root-entries.js";
4
5
  export type { ContainmentGuarantee } from "./containment.js";
5
6
  export type { RootWalkDataEntry, RootWalkDataEntryKind, RootWalkDirectoryErrorBehavior, RootWalkEntry, RootWalkEntryFilter, RootWalkEntryFilterResult, RootWalkEntryKind, RootWalkLimitBehavior, RootWalkOptions, RootWalkSymlinkPolicy, } from "./root-walk.js";
6
7
  //# sourceMappingURL=root.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"root.d.ts","sourceRoot":"","sources":["../src/root.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,kCAAkC,EAClC,IAAI,EACJ,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AACxE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,YAAY,EACV,iBAAiB,EACjB,qBAAqB,EACrB,8BAA8B,EAC9B,aAAa,EACb,mBAAmB,EACnB,yBAAyB,EACzB,iBAAiB,EACjB,qBAAqB,EACrB,eAAe,EACf,qBAAqB,GACtB,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"root.d.ts","sourceRoot":"","sources":["../src/root.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,kCAAkC,EAClC,IAAI,EACJ,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AACxE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,YAAY,EACV,iBAAiB,EACjB,qBAAqB,EACrB,8BAA8B,EAC9B,aAAa,EACb,mBAAmB,EACnB,yBAAyB,EACzB,iBAAiB,EACjB,qBAAqB,EACrB,eAAe,EACf,qBAAqB,GACtB,MAAM,gBAAgB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"safe-path-segment.d.ts","sourceRoot":"","sources":["../src/safe-path-segment.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,sBAAsB,GAAG;IACnC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKtF;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUrD;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAaT;AAED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAUR;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAWR;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAeR"}
1
+ {"version":3,"file":"safe-path-segment.d.ts","sourceRoot":"","sources":["../src/safe-path-segment.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,sBAAsB,GAAG;IACnC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKtF;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUrD;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAaT;AAED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAUR;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAWR;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAeR"}
@@ -4,12 +4,14 @@ const SAFE_DOT_PREFIX_PATH_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
4
4
  // Windows treats "C:name" as relative to the drive's current directory even
5
5
  // though path.win32.isAbsolute() reports false.
6
6
  const DRIVE_RELATIVE_PREFIX = /^[A-Za-z]:(?![\\/])/;
7
+ // A forward slash ends the segment, so "C:/file" still contains the "C:" segment.
8
+ const DRIVE_RELATIVE_SEGMENT = /(?:^|\/)[A-Za-z]:(?!\\)/;
7
9
  const HYPHEN_CHAR_CODE = 0x2d;
8
10
  export function isDriveRelativePath(value) {
9
11
  return DRIVE_RELATIVE_PREFIX.test(value);
10
12
  }
11
13
  export function assertNoDriveRelativePathSegments(value, label) {
12
- if (value.split("/").some(isDriveRelativePath)) {
14
+ if (DRIVE_RELATIVE_SEGMENT.test(value)) {
13
15
  throw new FsSafeError("invalid-path", `${label} must not contain a drive letter`);
14
16
  }
15
17
  return value;
@@ -1 +1 @@
1
- {"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAK/C,OAAO,EAAkF,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAUhJ,OAAO,EAML,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAIjC,eAAO,MAAM,uBAAuB,MAAQ,CAAC;AAC7C,eAAO,MAAM,wBAAwB,MAAQ,CAAC;AAE9C,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAkER;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,GAAG,SAAS,CAUpB;AA8JD,KAAK,qBAAqB,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAYF,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,IAAI,CAAC,qBAAqB,EAAE,SAAS,CAAC,GAC7C,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAC5C,WAAW,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC,CAuBD;AAwDD,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKxF;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAezF"}
1
+ {"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAM/C,OAAO,EAAkF,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAUhJ,OAAO,EAML,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAIjC,eAAO,MAAM,uBAAuB,MAAQ,CAAC;AAC7C,eAAO,MAAM,wBAAwB,MAAQ,CAAC;AAE9C,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAkER;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,GAAG,SAAS,CAUpB;AA8JD,KAAK,qBAAqB,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAYF,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,IAAI,CAAC,qBAAqB,EAAE,SAAS,CAAC,GAC7C,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAC5C,WAAW,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC,CAuBD;AAwDD,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKxF;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAezF"}
@@ -1,6 +1,7 @@
1
1
  import fs, {} from "node:fs";
2
2
  import fsp from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
4
5
  import { canonicalPathFromExistingAncestor } from "./absolute-path.js";
5
6
  import { readFileDescriptorBoundedSync } from "./bounded-read.js";
6
7
  import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
@@ -166,7 +167,7 @@ async function ensurePrivateDirectory(rootDir, targetDir, mode) {
166
167
  catch (error) {
167
168
  if (error.code !== "ENOENT")
168
169
  throw error;
169
- await fsp.mkdir(path.dirname(resolvedRoot), { recursive: true, mode });
170
+ await fsp.mkdir(recursiveMkdirPath(path.dirname(resolvedRoot)), { recursive: true, mode });
170
171
  createdRoot = await createPrivateDirectory(resolvedRoot, mode);
171
172
  }
172
173
  rootStat = await inspectPrivateDirectory(resolvedRoot, "root");
@@ -1 +1 @@
1
- {"version":3,"file":"secret-read-async.d.ts","sourceRoot":"","sources":["../src/secret-read-async.ts"],"names":[],"mappings":"AAOA,OAAO,EAML,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAEjC,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,CAAC,CA0DjB;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ7B"}
1
+ {"version":3,"file":"secret-read-async.d.ts","sourceRoot":"","sources":["../src/secret-read-async.ts"],"names":[],"mappings":"AAQA,OAAO,EAML,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAEjC,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,CAAC,CA0DjB;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ7B"}
@@ -4,6 +4,7 @@ import { readFileHandleBounded } from "./bounded-read.js";
4
4
  import { assertNoUnsafeDeviceReadPath } from "./device-path.js";
5
5
  import { FsSafeError } from "./errors.js";
6
6
  import { resolveReadOpenFlags } from "./read-open-flags.js";
7
+ import { realpathSync } from "./realpath.js";
7
8
  import { inspectFileIdentity } from "./strict-file-identity.js";
8
9
  import { assertSecretFilePreview, resolveSecretReadPolicy, secretPathErrorCode, secretReadError, trimSecretFileContent, } from "./secret-read-policy.js";
9
10
  export async function readSecretFile(filePath, label, options = {}) {
@@ -29,7 +30,7 @@ export async function readSecretFile(filePath, label, options = {}) {
29
30
  let handle;
30
31
  let raw;
31
32
  try {
32
- const realPath = fsSync.realpathSync.native(resolvedPath);
33
+ const realPath = realpathSync.native(resolvedPath);
33
34
  assertNoUnsafeDeviceReadPath(realPath);
34
35
  handle = await fs.open(realPath, resolveReadOpenFlags());
35
36
  const openedHandle = handle;
@@ -0,0 +1,10 @@
1
+ import type { BigIntStats, Stats } from "node:fs";
2
+ import type { PermissionCheck } from "./permissions.js";
3
+ type ExactIdentity = Pick<BigIntStats, "dev" | "ino">;
4
+ export declare function inspectSecureWindowsDescriptor(params: {
5
+ fd: number;
6
+ identity: ExactIdentity;
7
+ stat: Stats;
8
+ }): PermissionCheck;
9
+ export {};
10
+ //# sourceMappingURL=secure-file-windows.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secure-file-windows.d.ts","sourceRoot":"","sources":["../src/secure-file-windows.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AASlD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AA4BxD,KAAK,aAAa,GAAG,IAAI,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC;AAuHtD,wBAAgB,8BAA8B,CAAC,MAAM,EAAE;IACrD,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,aAAa,CAAC;IACxB,IAAI,EAAE,KAAK,CAAC;CACb,GAAG,eAAe,CA0ClB"}