@openclaw/fs-safe 0.5.6 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +23 -10
  3. package/dist/advanced.d.ts +2 -1
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +1 -0
  6. package/dist/archive-crc32.d.ts +2 -0
  7. package/dist/archive-crc32.d.ts.map +1 -0
  8. package/dist/archive-crc32.js +14 -0
  9. package/dist/archive-deadline.d.ts +3 -0
  10. package/dist/archive-deadline.d.ts.map +1 -1
  11. package/dist/archive-deadline.js +44 -8
  12. package/dist/archive-entry.d.ts.map +1 -1
  13. package/dist/archive-entry.js +1 -0
  14. package/dist/archive-errors.d.ts +1 -0
  15. package/dist/archive-errors.d.ts.map +1 -1
  16. package/dist/archive-errors.js +3 -0
  17. package/dist/archive-input.d.ts.map +1 -1
  18. package/dist/archive-input.js +26 -16
  19. package/dist/archive-kind.js +2 -2
  20. package/dist/archive-limits.d.ts +11 -3
  21. package/dist/archive-limits.d.ts.map +1 -1
  22. package/dist/archive-limits.js +24 -0
  23. package/dist/archive-native.d.ts +3 -2
  24. package/dist/archive-native.d.ts.map +1 -1
  25. package/dist/archive-native.js +31 -9
  26. package/dist/archive-policy.d.ts +2 -0
  27. package/dist/archive-policy.d.ts.map +1 -1
  28. package/dist/archive-policy.js +9 -1
  29. package/dist/archive-read.d.ts.map +1 -1
  30. package/dist/archive-read.js +88 -41
  31. package/dist/archive-staging.d.ts +3 -0
  32. package/dist/archive-staging.d.ts.map +1 -1
  33. package/dist/archive-staging.js +91 -43
  34. package/dist/archive-tar-admission.d.ts +7 -0
  35. package/dist/archive-tar-admission.d.ts.map +1 -0
  36. package/dist/archive-tar-admission.js +43 -0
  37. package/dist/archive-tar-gnu.d.ts +2 -0
  38. package/dist/archive-tar-gnu.d.ts.map +1 -0
  39. package/dist/archive-tar-gnu.js +20 -0
  40. package/dist/archive-tar-header.d.ts +8 -0
  41. package/dist/archive-tar-header.d.ts.map +1 -0
  42. package/dist/archive-tar-header.js +47 -0
  43. package/dist/archive-tar-meta.d.ts +29 -1
  44. package/dist/archive-tar-meta.d.ts.map +1 -1
  45. package/dist/archive-tar-meta.js +135 -19
  46. package/dist/archive-tar-pax.d.ts +8 -0
  47. package/dist/archive-tar-pax.d.ts.map +1 -0
  48. package/dist/archive-tar-pax.js +100 -0
  49. package/dist/archive-tar-runtime.d.ts +4 -0
  50. package/dist/archive-tar-runtime.d.ts.map +1 -1
  51. package/dist/archive-tar-runtime.js +3 -0
  52. package/dist/archive-tar.d.ts.map +1 -1
  53. package/dist/archive-tar.js +8 -2
  54. package/dist/archive-zip-admission.d.ts +7 -0
  55. package/dist/archive-zip-admission.d.ts.map +1 -0
  56. package/dist/archive-zip-admission.js +60 -0
  57. package/dist/archive-zip-count.d.ts +2 -0
  58. package/dist/archive-zip-count.d.ts.map +1 -0
  59. package/dist/archive-zip-count.js +140 -0
  60. package/dist/archive-zip-directory.d.ts +8 -0
  61. package/dist/archive-zip-directory.d.ts.map +1 -0
  62. package/dist/archive-zip-directory.js +224 -0
  63. package/dist/archive-zip-integrity.d.ts.map +1 -1
  64. package/dist/archive-zip-integrity.js +1 -14
  65. package/dist/archive-zip-names.d.ts +12 -0
  66. package/dist/archive-zip-names.d.ts.map +1 -0
  67. package/dist/archive-zip-names.js +99 -0
  68. package/dist/archive-zip-preflight.d.ts +1 -1
  69. package/dist/archive-zip-preflight.d.ts.map +1 -1
  70. package/dist/archive-zip-preflight.js +8 -145
  71. package/dist/archive.d.ts.map +1 -1
  72. package/dist/archive.js +63 -42
  73. package/dist/atomic.d.ts +1 -1
  74. package/dist/atomic.d.ts.map +1 -1
  75. package/dist/bounded-read-stream.d.ts.map +1 -1
  76. package/dist/bounded-read-stream.js +2 -4
  77. package/dist/bounded-read.d.ts.map +1 -1
  78. package/dist/bounded-read.js +3 -10
  79. package/dist/byte-budget.d.ts +5 -0
  80. package/dist/byte-budget.d.ts.map +1 -0
  81. package/dist/byte-budget.js +9 -0
  82. package/dist/file-hash.d.ts.map +1 -1
  83. package/dist/file-hash.js +25 -19
  84. package/dist/file-lock-sync.d.ts.map +1 -1
  85. package/dist/file-lock-sync.js +26 -9
  86. package/dist/file-store-boundary.d.ts.map +1 -1
  87. package/dist/file-store-boundary.js +9 -5
  88. package/dist/file-store-limit.d.ts +2 -0
  89. package/dist/file-store-limit.d.ts.map +1 -0
  90. package/dist/file-store-limit.js +8 -0
  91. package/dist/file-store-sync-write.d.ts.map +1 -1
  92. package/dist/file-store-sync-write.js +38 -8
  93. package/dist/file-store.d.ts.map +1 -1
  94. package/dist/file-store.js +26 -26
  95. package/dist/json-durable-queue-directory.d.ts +2 -0
  96. package/dist/json-durable-queue-directory.d.ts.map +1 -0
  97. package/dist/json-durable-queue-directory.js +20 -0
  98. package/dist/json-durable-queue-ownership.d.ts +14 -0
  99. package/dist/json-durable-queue-ownership.d.ts.map +1 -0
  100. package/dist/json-durable-queue-ownership.js +168 -0
  101. package/dist/json-durable-queue-retirement.d.ts +9 -0
  102. package/dist/json-durable-queue-retirement.d.ts.map +1 -0
  103. package/dist/json-durable-queue-retirement.js +126 -0
  104. package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
  105. package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
  106. package/dist/json-durable-queue-transfer-lock.js +19 -0
  107. package/dist/json-durable-queue.d.ts +1 -0
  108. package/dist/json-durable-queue.d.ts.map +1 -1
  109. package/dist/json-durable-queue.js +90 -57
  110. package/dist/json.d.ts.map +1 -1
  111. package/dist/json.js +27 -8
  112. package/dist/local-roots.d.ts.map +1 -1
  113. package/dist/local-roots.js +4 -2
  114. package/dist/native-binding.d.ts +15 -3
  115. package/dist/native-binding.d.ts.map +1 -1
  116. package/dist/native-operations.d.ts +4 -1
  117. package/dist/native-operations.d.ts.map +1 -1
  118. package/dist/native-operations.js +22 -6
  119. package/dist/native-pinned-write-windows.d.ts +8 -0
  120. package/dist/native-pinned-write-windows.d.ts.map +1 -0
  121. package/dist/native-pinned-write-windows.js +92 -0
  122. package/dist/native-pinned-write.d.ts.map +1 -1
  123. package/dist/native-pinned-write.js +136 -127
  124. package/dist/native-staged-file.d.ts +24 -0
  125. package/dist/native-staged-file.d.ts.map +1 -0
  126. package/dist/native-staged-file.js +337 -0
  127. package/dist/native.d.ts.map +1 -1
  128. package/dist/native.js +4 -4
  129. package/dist/opened-realpath.d.ts +2 -0
  130. package/dist/opened-realpath.d.ts.map +1 -1
  131. package/dist/opened-realpath.js +12 -7
  132. package/dist/output-sibling.d.ts.map +1 -1
  133. package/dist/output-sibling.js +11 -110
  134. package/dist/output.d.ts.map +1 -1
  135. package/dist/output.js +4 -2
  136. package/dist/owner-dacl.d.ts.map +1 -1
  137. package/dist/owner-dacl.js +2 -1
  138. package/dist/permission-exec.d.ts +19 -0
  139. package/dist/permission-exec.d.ts.map +1 -1
  140. package/dist/permission-exec.js +57 -11
  141. package/dist/permissions-public.d.ts +1 -1
  142. package/dist/permissions-public.d.ts.map +1 -1
  143. package/dist/permissions-windows.d.ts +3 -0
  144. package/dist/permissions-windows.d.ts.map +1 -1
  145. package/dist/permissions-windows.js +16 -5
  146. package/dist/permissions.d.ts +5 -0
  147. package/dist/permissions.d.ts.map +1 -1
  148. package/dist/pinned-open.d.ts.map +1 -1
  149. package/dist/pinned-open.js +27 -48
  150. package/dist/pinned-write.d.ts +6 -0
  151. package/dist/pinned-write.d.ts.map +1 -1
  152. package/dist/pinned-write.js +26 -37
  153. package/dist/private-directory.d.ts.map +1 -1
  154. package/dist/private-directory.js +3 -2
  155. package/dist/private-temp-workspace.d.ts +3 -1
  156. package/dist/private-temp-workspace.d.ts.map +1 -1
  157. package/dist/private-temp-workspace.js +81 -56
  158. package/dist/publish-file.d.ts.map +1 -1
  159. package/dist/publish-file.js +2 -4
  160. package/dist/read-opened-file.d.ts.map +1 -1
  161. package/dist/read-opened-file.js +6 -4
  162. package/dist/regular-file.d.ts.map +1 -1
  163. package/dist/regular-file.js +136 -82
  164. package/dist/replace-file-copy-fallback.d.ts +3 -1
  165. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  166. package/dist/replace-file-copy-fallback.js +21 -36
  167. package/dist/replace-file-copy-source.d.ts +21 -0
  168. package/dist/replace-file-copy-source.d.ts.map +1 -0
  169. package/dist/replace-file-copy-source.js +112 -0
  170. package/dist/replace-file-descriptor.d.ts +13 -3
  171. package/dist/replace-file-descriptor.d.ts.map +1 -1
  172. package/dist/replace-file-descriptor.js +32 -7
  173. package/dist/replace-file-rename-policy.d.ts +7 -0
  174. package/dist/replace-file-rename-policy.d.ts.map +1 -0
  175. package/dist/replace-file-rename-policy.js +30 -0
  176. package/dist/replace-file-temp-owner.d.ts +46 -0
  177. package/dist/replace-file-temp-owner.d.ts.map +1 -0
  178. package/dist/replace-file-temp-owner.js +346 -0
  179. package/dist/replace-file.d.ts +6 -1
  180. package/dist/replace-file.d.ts.map +1 -1
  181. package/dist/replace-file.js +72 -58
  182. package/dist/root-impl.d.ts.map +1 -1
  183. package/dist/root-impl.js +118 -98
  184. package/dist/root-paths.d.ts +11 -14
  185. package/dist/root-paths.d.ts.map +1 -1
  186. package/dist/root-paths.js +36 -27
  187. package/dist/root-write-verification.d.ts +11 -0
  188. package/dist/root-write-verification.d.ts.map +1 -0
  189. package/dist/root-write-verification.js +91 -0
  190. package/dist/secret-file.d.ts +1 -6
  191. package/dist/secret-file.d.ts.map +1 -1
  192. package/dist/secret-file.js +49 -120
  193. package/dist/secret-read-async.d.ts +1 -1
  194. package/dist/secret-read-async.d.ts.map +1 -1
  195. package/dist/secret-read-async.js +51 -72
  196. package/dist/secret-read-policy.d.ts +13 -0
  197. package/dist/secret-read-policy.d.ts.map +1 -0
  198. package/dist/secret-read-policy.js +28 -0
  199. package/dist/secret.d.ts +2 -1
  200. package/dist/secret.d.ts.map +1 -1
  201. package/dist/secret.js +2 -1
  202. package/dist/secure-file.d.ts.map +1 -1
  203. package/dist/secure-file.js +42 -29
  204. package/dist/sibling-staged-file.d.ts +15 -0
  205. package/dist/sibling-staged-file.d.ts.map +1 -0
  206. package/dist/sibling-staged-file.js +148 -0
  207. package/dist/sibling-temp.d.ts +3 -0
  208. package/dist/sibling-temp.d.ts.map +1 -1
  209. package/dist/sibling-temp.js +30 -74
  210. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  211. package/dist/sidecar-lock-acquire.js +48 -27
  212. package/dist/sidecar-lock-handle.d.ts +6 -2
  213. package/dist/sidecar-lock-handle.d.ts.map +1 -1
  214. package/dist/sidecar-lock-handle.js +17 -3
  215. package/dist/sidecar-lock-policy.d.ts +2 -0
  216. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  217. package/dist/sidecar-lock-policy.js +29 -0
  218. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  219. package/dist/sidecar-lock-reclaim.js +29 -7
  220. package/dist/sidecar-lock.d.ts.map +1 -1
  221. package/dist/sidecar-lock.js +28 -16
  222. package/dist/staged-directory.d.ts +16 -0
  223. package/dist/staged-directory.d.ts.map +1 -0
  224. package/dist/staged-directory.js +60 -0
  225. package/dist/staged-file-types.d.ts +56 -0
  226. package/dist/staged-file-types.d.ts.map +1 -0
  227. package/dist/staged-file-types.js +1 -0
  228. package/dist/staged-file.d.ts +10 -0
  229. package/dist/staged-file.d.ts.map +1 -0
  230. package/dist/staged-file.js +15 -0
  231. package/dist/strict-file-identity.d.ts +6 -0
  232. package/dist/strict-file-identity.d.ts.map +1 -0
  233. package/dist/strict-file-identity.js +48 -0
  234. package/dist/suppressed-error.d.ts +6 -0
  235. package/dist/suppressed-error.d.ts.map +1 -0
  236. package/dist/suppressed-error.js +15 -0
  237. package/dist/temp-cleanup.d.ts +2 -0
  238. package/dist/temp-cleanup.d.ts.map +1 -1
  239. package/dist/temp-cleanup.js +25 -10
  240. package/dist/temp-workspace-owner.d.ts +23 -0
  241. package/dist/temp-workspace-owner.d.ts.map +1 -0
  242. package/dist/temp-workspace-owner.js +320 -0
  243. package/dist/temp.d.ts +1 -1
  244. package/dist/temp.d.ts.map +1 -1
  245. package/dist/test-hooks.d.ts +5 -0
  246. package/dist/test-hooks.d.ts.map +1 -1
  247. package/dist/windows-owner.d.ts +3 -0
  248. package/dist/windows-owner.d.ts.map +1 -1
  249. package/dist/windows-owner.js +10 -2
  250. package/docs/advanced.md +19 -2
  251. package/docs/archive.md +250 -35
  252. package/docs/atomic.md +11 -2
  253. package/docs/config.md +7 -0
  254. package/docs/contributing.md +45 -7
  255. package/docs/durability.md +18 -5
  256. package/docs/errors.md +16 -1
  257. package/docs/file-store.md +2 -0
  258. package/docs/index.md +3 -1
  259. package/docs/install.md +20 -8
  260. package/docs/json.md +8 -4
  261. package/docs/migrating-to-0.5.md +7 -7
  262. package/docs/migrating-to-0.6.md +43 -0
  263. package/docs/native-helper.md +30 -8
  264. package/docs/native.md +73 -17
  265. package/docs/output.md +10 -0
  266. package/docs/path-scope.md +28 -2
  267. package/docs/permissions.md +13 -2
  268. package/docs/public-api.md +6 -3
  269. package/docs/quickstart.md +1 -1
  270. package/docs/reading.md +1 -1
  271. package/docs/regular-file.md +9 -2
  272. package/docs/root.md +3 -1
  273. package/docs/secret-file.md +12 -0
  274. package/docs/secure-file.md +21 -3
  275. package/docs/security-model.md +33 -1
  276. package/docs/sidecar-lock.md +15 -1
  277. package/docs/staged-file.md +178 -0
  278. package/docs/store.md +13 -0
  279. package/docs/temp.md +128 -14
  280. package/docs/testing.md +1 -1
  281. package/docs/writing.md +19 -0
  282. package/package.json +16 -9
  283. package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
  284. package/dist/native/darwin-x64/fs-safe-native.node +0 -0
  285. package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
  286. package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
  287. package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
  288. package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
  289. package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
package/dist/root-impl.js CHANGED
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { constants as fsConstants } from "node:fs";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
+ import { normalizeMaxBytes } from "./byte-budget.js";
5
6
  import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
6
7
  import { FsSafeError } from "./errors.js";
7
8
  import { syncDirectoryBestEffort } from "./fsync.js";
@@ -9,7 +10,7 @@ import { sameFileIdentity, sameFileIdentityForCleanup, } from "./file-identity.j
9
10
  import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
10
11
  import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
11
12
  import { assertMutationNotDenied, mergeDenyMutationPolicies, } from "./deny-mutations.js";
12
- import { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
13
+ import { resolveOpenedFileRealPathForFd, resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
13
14
  import { runPinnedWriteHelper, runPinnedWriteWithRenamePolicy, } from "./pinned-write.js";
14
15
  import { getNativeBinding } from "./native.js";
15
16
  import { validatePinnedOperationPayload } from "./pinned-operation.js";
@@ -25,6 +26,8 @@ import { stringifyJsonDocument } from "./json-stringify.js";
25
26
  import { walkRoot } from "./root-walk.js";
26
27
  import { registerTempPathForExit } from "./temp-cleanup.js";
27
28
  import { serializePathWrite } from "./write-queue.js";
29
+ import { verifyAtomicWriteResult } from "./root-write-verification.js";
30
+ import { inspectFileIdentity } from "./strict-file-identity.js";
28
31
  export { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
29
32
  function logWarn(message) {
30
33
  if (process.env.FS_SAFE_DEBUG_WARNINGS === "1") {
@@ -74,22 +77,27 @@ async function openVerifiedLocalFile(filePath, options) {
74
77
  assertNoUnsafeDeviceReadPath(filePath);
75
78
  const fsSafeTestHooks = getFsSafeTestHooks();
76
79
  let preOpenStat;
80
+ let observedBeforeOpen = false;
77
81
  // Reject directories before opening so we never surface EISDIR to callers (e.g. tool
78
82
  // results that get sent to messaging channels). See openclaw/openclaw#31186.
79
83
  try {
80
- preOpenStat = await fs.lstat(filePath);
81
- if (preOpenStat.isSymbolicLink() && options?.symlinks !== "follow-within-root") {
82
- throw new FsSafeError("symlink", "symlink not allowed");
83
- }
84
- if (!preOpenStat.isFile() && !preOpenStat.isSymbolicLink()) {
85
- throw new FsSafeError("not-file", "not a file");
86
- }
84
+ preOpenStat = await inspectFileIdentity(async () => {
85
+ const stat = await fs.lstat(filePath, { bigint: true });
86
+ observedBeforeOpen = true;
87
+ if (stat.isSymbolicLink() && options?.symlinks !== "follow-within-root") {
88
+ throw new FsSafeError("symlink", "symlink not allowed");
89
+ }
90
+ if (!stat.isFile() && !stat.isSymbolicLink()) {
91
+ throw new FsSafeError("not-file", "not a file");
92
+ }
93
+ return stat;
94
+ });
87
95
  }
88
96
  catch (err) {
89
- if (err instanceof FsSafeError) {
97
+ if (err instanceof FsSafeError || observedBeforeOpen) {
90
98
  throw err;
91
99
  }
92
- // ENOENT and other lstat errors: fall through and let fs.open handle.
100
+ // Only an initial lookup failure falls through; a failed re-inspection aborts.
93
101
  }
94
102
  if (preOpenStat) {
95
103
  await fsSafeTestHooks?.afterPreOpenLstat?.(filePath);
@@ -127,38 +135,30 @@ async function openVerifiedLocalFile(filePath, options) {
127
135
  if (!stat.isFile()) {
128
136
  throw new FsSafeError("not-file", "not a file");
129
137
  }
130
- if (preOpenStat &&
131
- !preOpenStat.isSymbolicLink() &&
132
- !sameFileIdentity(stat, preOpenStat)) {
133
- throw new FsSafeError("path-mismatch", "path changed before open");
134
- }
138
+ // Keep numeric Stats for the public receipt, never for identity verification.
139
+ const identity = await inspectFileIdentity(() => handle.stat({ bigint: true }), preOpenStat && !preOpenStat.isSymbolicLink() ? preOpenStat : undefined);
135
140
  if (options?.hardlinks === "reject" && stat.nlink > 1) {
136
141
  throw hardlinkedPathNotAllowedError();
137
142
  }
138
- if (options?.symlinks === "follow-within-root") {
139
- const pathStat = await fs.stat(filePath);
140
- if (!sameFileIdentity(stat, pathStat)) {
141
- throw new FsSafeError("path-mismatch", "path changed during read");
142
- }
143
- }
144
- else {
145
- const pathStat = await fs.lstat(filePath);
146
- if (pathStat.isSymbolicLink()) {
143
+ await inspectFileIdentity(async () => {
144
+ const pathStat = options?.symlinks === "follow-within-root"
145
+ ? await fs.stat(filePath, { bigint: true })
146
+ : await fs.lstat(filePath, { bigint: true });
147
+ if (pathStat.isSymbolicLink() && options?.symlinks !== "follow-within-root") {
147
148
  throw new FsSafeError("symlink", "symlink not allowed");
148
149
  }
149
- if (!sameFileIdentity(stat, pathStat)) {
150
- throw new FsSafeError("path-mismatch", "path changed during read");
150
+ return pathStat;
151
+ }, identity);
152
+ await fsSafeTestHooks?.afterOpenedPathIdentityCheck?.(filePath, handle);
153
+ const realPath = await resolveOpenedFileRealPathForFd(handle.fd, identity, filePath);
154
+ await inspectFileIdentity(async () => {
155
+ const realStat = await fs.stat(realPath, { bigint: true });
156
+ if (options?.hardlinks === "reject" && realStat.nlink > 1n) {
157
+ throw hardlinkedPathNotAllowedError();
151
158
  }
152
- }
153
- const realPath = await resolveOpenedFileRealPathForHandle(handle, filePath);
154
- const realStat = await fs.stat(realPath);
155
- if (options?.hardlinks === "reject" && realStat.nlink > 1) {
156
- throw hardlinkedPathNotAllowedError();
157
- }
158
- if (!sameFileIdentity(stat, realStat)) {
159
- throw new FsSafeError("path-mismatch", "path mismatch");
160
- }
161
- return openResult({ handle, realPath, stat });
159
+ return realStat;
160
+ }, identity);
161
+ return { opened: openResult({ handle, realPath, stat }), identity };
162
162
  }
163
163
  catch (err) {
164
164
  await handle.close().catch(() => { });
@@ -212,8 +212,7 @@ class RootHandle {
212
212
  async read(relativePath, options = {}) {
213
213
  return await readFileInRoot(this.context, {
214
214
  relativePath,
215
- ...readDefaults(this.defaults),
216
- ...options,
215
+ ...mergeReadOptions(this.defaults, options),
217
216
  });
218
217
  }
219
218
  async readBytes(relativePath, options = {}) {
@@ -229,8 +228,7 @@ class RootHandle {
229
228
  async readAbsolute(filePath, options = {}) {
230
229
  return await readPathInRoot(this.context, {
231
230
  filePath,
232
- ...readDefaults(this.defaults),
233
- ...options,
231
+ ...mergeReadOptions(this.defaults, options),
234
232
  });
235
233
  }
236
234
  reader(options = {}) {
@@ -315,13 +313,14 @@ class RootHandle {
315
313
  }
316
314
  async copyIn(relativePath, sourcePath, options = {}) {
317
315
  assertValidRootDestinationPath(relativePath);
316
+ const { maxBytes, ...copyOptions } = this.mutationOptions(options);
318
317
  await copyFileInRoot(this.context, {
319
318
  sourcePath,
320
319
  relativePath,
321
- maxBytes: this.defaults.maxBytes,
320
+ maxBytes: normalizeMaxBytes(maxBytes, { defaultValue: this.defaults.maxBytes }),
322
321
  mkdir: this.defaults.mkdir,
323
322
  mode: this.defaults.mode,
324
- ...this.mutationOptions(options),
323
+ ...copyOptions,
325
324
  });
326
325
  }
327
326
  async exists(relativePath) {
@@ -371,12 +370,24 @@ class RootHandle {
371
370
  function readDefaults(defaults) {
372
371
  return {
373
372
  hardlinks: defaults.hardlinks,
374
- maxBytes: defaults.maxBytes ?? DEFAULT_ROOT_MAX_BYTES,
373
+ maxBytes: normalizeMaxBytes(defaults.maxBytes, { defaultValue: DEFAULT_ROOT_MAX_BYTES }),
375
374
  nonBlockingRead: defaults.nonBlockingRead,
376
375
  symlinks: defaults.symlinks,
377
376
  };
378
377
  }
378
+ function mergeReadOptions(defaults, options) {
379
+ const merged = readDefaults(defaults);
380
+ if (options.hardlinks !== undefined)
381
+ merged.hardlinks = options.hardlinks;
382
+ merged.maxBytes = normalizeMaxBytes(options.maxBytes, { defaultValue: merged.maxBytes });
383
+ if (options.nonBlockingRead !== undefined)
384
+ merged.nonBlockingRead = options.nonBlockingRead;
385
+ if (options.symlinks !== undefined)
386
+ merged.symlinks = options.symlinks;
387
+ return merged;
388
+ }
379
389
  export async function root(rootDir, defaults = {}) {
390
+ normalizeMaxBytes(defaults.maxBytes);
380
391
  return new RootHandle(await resolveRootContext(rootDir), defaults);
381
392
  }
382
393
  async function openFileInRoot(root, params) {
@@ -387,10 +398,10 @@ async function openFileInRoot(root, params) {
387
398
  });
388
399
  let opened;
389
400
  try {
390
- opened = await openVerifiedLocalFile(resolved, {
401
+ ({ opened } = await openVerifiedLocalFile(resolved, {
391
402
  nonBlockingRead: params.nonBlockingRead,
392
403
  symlinks: params.symlinks,
393
- });
404
+ }));
394
405
  }
395
406
  catch (err) {
396
407
  if (err instanceof FsSafeError) {
@@ -432,9 +443,10 @@ async function readPathInRoot(root, params) {
432
443
  });
433
444
  }
434
445
  export async function readLocalFileSafely(params) {
446
+ const maxBytes = normalizeMaxBytes(params.maxBytes);
435
447
  const opened = await openLocalFileSafely({ filePath: params.filePath });
436
448
  try {
437
- return await readOpenedFileSafely({ opened, maxBytes: params.maxBytes });
449
+ return await readOpenedFileSafely({ opened, maxBytes });
438
450
  }
439
451
  finally {
440
452
  await opened.handle.close().catch(() => { });
@@ -442,7 +454,7 @@ export async function readLocalFileSafely(params) {
442
454
  }
443
455
  export async function openLocalFileSafely(params) {
444
456
  assertNoNulPathInput(params.filePath, "file path contains a NUL byte");
445
- return await openVerifiedLocalFile(params.filePath);
457
+ return (await openVerifiedLocalFile(params.filePath)).opened;
446
458
  }
447
459
  function emitWriteBoundaryWarning(reason) {
448
460
  logWarn(`security: fs-safe write boundary warning (${reason})`);
@@ -474,26 +486,13 @@ async function writeTempFileForAtomicReplace(params) {
474
486
  await tempHandle.writeFile(params.data);
475
487
  }
476
488
  return {
477
- identity: await tempHandle.stat(),
478
- cleanupIdentity: await tempHandle.stat({ bigint: true }),
489
+ handle: tempHandle,
490
+ identity: await tempHandle.stat({ bigint: true }),
479
491
  };
480
492
  }
481
- finally {
493
+ catch (error) {
482
494
  await tempHandle.close().catch(() => { });
483
- }
484
- }
485
- async function verifyAtomicWriteResult(params) {
486
- const opened = await openVerifiedLocalFile(params.targetPath, { hardlinks: "reject" });
487
- try {
488
- if (!sameFileIdentity(opened.stat, params.expectedIdentity)) {
489
- throw new FsSafeError("path-mismatch", "path changed during write");
490
- }
491
- if (!isPathInside(params.root.rootWithSep, opened.realPath)) {
492
- throw outsideWorkspaceError();
493
- }
494
- }
495
- finally {
496
- await opened.handle.close().catch(() => { });
495
+ throw error;
497
496
  }
498
497
  }
499
498
  async function resolveGuardedWritePathInRoot(root, params) {
@@ -709,9 +708,9 @@ async function writeFileInRoot(root, params) {
709
708
  });
710
709
  }
711
710
  async function commitPinnedWriteInRoot(root, pinned, params) {
712
- let identity;
711
+ let verifyingPublication = false;
713
712
  try {
714
- identity = await runPinnedWriteWithRenamePolicy({
713
+ await runPinnedWriteWithRenamePolicy({
715
714
  rootPath: pinned.rootReal,
716
715
  relativeParentPath: pinned.relativeParentPath,
717
716
  basename: pinned.basename,
@@ -722,9 +721,27 @@ async function commitPinnedWriteInRoot(root, pinned, params) {
722
721
  overwrite: params.overwrite,
723
722
  input: { kind: "buffer", data: params.data, encoding: params.encoding },
724
723
  rootIdentity: root.rootIdentity,
724
+ verifyPublished: async (fd, expectedIdentity, parentGuard) => {
725
+ verifyingPublication = true;
726
+ try {
727
+ await verifyAtomicWriteResult({
728
+ root,
729
+ targetPath: pinned.targetPath,
730
+ fd,
731
+ expectedIdentity,
732
+ parentGuard,
733
+ });
734
+ }
735
+ catch (error) {
736
+ emitWriteBoundaryWarning(`post-write verification failed: ${String(error)}`);
737
+ throw error;
738
+ }
739
+ },
725
740
  });
726
741
  }
727
742
  catch (error) {
743
+ if (verifyingPublication)
744
+ throw error;
728
745
  const errorCode = error?.code;
729
746
  if (errorCode === "file_lock_stale" || errorCode === "file_lock_timeout") {
730
747
  throw error;
@@ -736,22 +753,11 @@ async function commitPinnedWriteInRoot(root, pinned, params) {
736
753
  }
737
754
  throw normalizePinnedWriteError(error);
738
755
  }
739
- try {
740
- await verifyAtomicWriteResult({
741
- root,
742
- targetPath: pinned.targetPath,
743
- expectedIdentity: identity,
744
- });
745
- }
746
- catch (err) {
747
- emitWriteBoundaryWarning(`post-write verification failed: ${String(err)}`);
748
- throw err;
749
- }
750
756
  }
751
757
  async function copyFileInRoot(root, params) {
752
758
  assertValidRootRelativePath(params.relativePath);
753
759
  assertNoNulPathInput(params.sourcePath, "source path contains a NUL byte");
754
- const source = await openVerifiedLocalFile(params.sourcePath, {
760
+ const { opened: source, identity: sourceIdentity } = await openVerifiedLocalFile(params.sourcePath, {
755
761
  hardlinks: params.sourceHardlinks,
756
762
  });
757
763
  if (params.maxBytes !== undefined && source.stat.size > params.maxBytes) {
@@ -762,7 +768,7 @@ async function copyFileInRoot(root, params) {
762
768
  await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
763
769
  const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
764
770
  await serializePathWrite(pinned.targetPath, async () => {
765
- await assertCopySourceCurrent(source);
771
+ await assertCopySourceCurrent(source, sourceIdentity);
766
772
  let identity;
767
773
  try {
768
774
  identity = await runPinnedWriteHelper({
@@ -781,7 +787,7 @@ async function copyFileInRoot(root, params) {
781
787
  throw normalizePinnedWriteError(error);
782
788
  }
783
789
  try {
784
- await assertCopySourcePathCurrent(source);
790
+ await assertCopySourcePathCurrent(source, sourceIdentity);
785
791
  }
786
792
  catch (error) {
787
793
  await removePathIfIdentityUnchanged(pinned.targetPath, identity).catch(() => undefined);
@@ -794,18 +800,18 @@ async function copyFileInRoot(root, params) {
794
800
  await source.handle.close().catch(() => { });
795
801
  }
796
802
  }
797
- async function assertCopySourceCurrent(source) {
798
- const opened = await source.handle.stat();
799
- if (!sameFileIdentity(opened, source.stat)) {
800
- throw new FsSafeError("path-mismatch", "copy source descriptor changed");
801
- }
802
- await assertCopySourcePathCurrent(source);
803
+ async function assertCopySourceCurrent(source, identity) {
804
+ await inspectFileIdentity(() => source.handle.stat({ bigint: true }), identity);
805
+ await assertCopySourcePathCurrent(source, identity);
803
806
  }
804
- async function assertCopySourcePathCurrent(source) {
805
- const current = await fs.lstat(source.realPath);
806
- if (current.isSymbolicLink() || !current.isFile() || !sameFileIdentity(current, source.stat)) {
807
- throw new FsSafeError("path-mismatch", "copy source path changed");
808
- }
807
+ async function assertCopySourcePathCurrent(source, identity) {
808
+ await inspectFileIdentity(async () => {
809
+ const current = await fs.lstat(source.realPath, { bigint: true });
810
+ if (current.isSymbolicLink() || !current.isFile()) {
811
+ throw new FsSafeError("path-mismatch", "copy source path changed");
812
+ }
813
+ return current;
814
+ }, identity);
809
815
  }
810
816
  async function removePathIfIdentityUnchanged(targetPath, identity) {
811
817
  const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
@@ -863,7 +869,7 @@ async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode,
863
869
  targetPath: resolved,
864
870
  relativeParentPath: path.posix.dirname(relativePosix) === "." ? "" : path.posix.dirname(relativePosix),
865
871
  basename,
866
- mode: mode || 0o600,
872
+ mode,
867
873
  };
868
874
  }
869
875
  async function resolvePinnedPathInRoot(root, params) {
@@ -1108,6 +1114,7 @@ async function writeFileFallback(root, params) {
1108
1114
  const destinationGuard = await createAsyncDirectoryGuard(path.dirname(destinationPath));
1109
1115
  let tempPath = null;
1110
1116
  let unregisterTempPath = null;
1117
+ let writtenHandle;
1111
1118
  try {
1112
1119
  tempPath = buildAtomicWriteTempPath(destinationPath);
1113
1120
  unregisterTempPath = registerTempPathForExit(tempPath);
@@ -1115,14 +1122,15 @@ async function writeFileFallback(root, params) {
1115
1122
  tempPath,
1116
1123
  data: params.data,
1117
1124
  encoding: params.encoding,
1118
- mode: mode || 0o600,
1125
+ mode,
1119
1126
  });
1120
- unregisterTempPath.setIdentity(written.cleanupIdentity);
1127
+ writtenHandle = written.handle;
1128
+ unregisterTempPath.setIdentity(written.identity);
1121
1129
  const commitTempPath = tempPath;
1122
1130
  await withAsyncDirectoryGuards([destinationGuard], async () => {
1123
1131
  await fs.rename(commitTempPath, destinationPath);
1132
+ tempPath = null;
1124
1133
  });
1125
- tempPath = null;
1126
1134
  unregisterTempPath();
1127
1135
  unregisterTempPath = null;
1128
1136
  try {
@@ -1130,6 +1138,8 @@ async function writeFileFallback(root, params) {
1130
1138
  root,
1131
1139
  targetPath: destinationPath,
1132
1140
  expectedIdentity: written.identity,
1141
+ fd: written.handle.fd,
1142
+ parentGuard: destinationGuard,
1133
1143
  });
1134
1144
  }
1135
1145
  catch (err) {
@@ -1138,6 +1148,7 @@ async function writeFileFallback(root, params) {
1138
1148
  }
1139
1149
  }
1140
1150
  finally {
1151
+ await writtenHandle?.close().catch(() => undefined);
1141
1152
  if (tempPath) {
1142
1153
  await fs.rm(tempPath, { force: true }).catch(() => { });
1143
1154
  }
@@ -1155,19 +1166,22 @@ async function writeMissingFileFallback(root, params) {
1155
1166
  const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
1156
1167
  let created = false;
1157
1168
  let createdIdentity;
1169
+ let writtenHandle;
1170
+ let verifyingPublication = false;
1158
1171
  try {
1159
1172
  const { handle, writtenStat } = await withAsyncDirectoryGuards([parentGuard], async () => {
1160
1173
  const handle = await fs.open(targetPath, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600);
1161
1174
  created = true;
1162
1175
  try {
1163
1176
  createdIdentity = await handle.stat();
1177
+ const writtenStat = await handle.stat({ bigint: true });
1164
1178
  if (typeof params.data === "string") {
1165
1179
  await handle.writeFile(params.data, params.encoding ?? "utf8");
1166
1180
  }
1167
1181
  else {
1168
1182
  await handle.writeFile(params.data);
1169
1183
  }
1170
- return { handle, writtenStat: await handle.stat() };
1184
+ return { handle, writtenStat };
1171
1185
  }
1172
1186
  catch (error) {
1173
1187
  await handle.close().catch(() => undefined);
@@ -1179,15 +1193,20 @@ async function writeMissingFileFallback(root, params) {
1179
1193
  await handle.close().catch(() => undefined);
1180
1194
  },
1181
1195
  });
1182
- await handle.close();
1196
+ writtenHandle = handle;
1197
+ created = false;
1198
+ verifyingPublication = true;
1183
1199
  await verifyAtomicWriteResult({
1184
1200
  root,
1185
1201
  targetPath,
1186
1202
  expectedIdentity: writtenStat,
1203
+ fd: handle.fd,
1204
+ parentGuard,
1187
1205
  });
1188
- created = false;
1189
1206
  }
1190
1207
  catch (err) {
1208
+ if (verifyingPublication)
1209
+ throw err;
1191
1210
  if (hasNodeErrorCode(err, "EEXIST")) {
1192
1211
  throw new FsSafeError("already-exists", "file already exists", {
1193
1212
  cause: err instanceof Error ? err : undefined,
@@ -1196,6 +1215,7 @@ async function writeMissingFileFallback(root, params) {
1196
1215
  throw err;
1197
1216
  }
1198
1217
  finally {
1218
+ await writtenHandle?.close().catch(() => undefined);
1199
1219
  if (created && createdIdentity) {
1200
1220
  await removePathIfIdentityUnchanged(targetPath, createdIdentity).catch(() => undefined);
1201
1221
  }
@@ -1,7 +1,16 @@
1
+ import { FsSafeError } from "./errors.js";
1
2
  type InvalidPathResult = {
2
3
  ok: false;
3
4
  error: string;
4
5
  };
6
+ type DirectoryResult = {
7
+ ok: true;
8
+ path: string;
9
+ } | {
10
+ ok: false;
11
+ error: string;
12
+ diagnostic?: FsSafeError;
13
+ };
5
14
  type ResolvePathsWithinRootParams = {
6
15
  rootDir: string;
7
16
  requestedPaths: string[];
@@ -39,13 +48,7 @@ export type PathScope = {
39
48
  }>;
40
49
  ensureDir(requestedPath: string, options?: PathScopeResolveOptions & {
41
50
  mode?: number;
42
- }): Promise<{
43
- ok: true;
44
- path: string;
45
- } | {
46
- ok: false;
47
- error: string;
48
- }>;
51
+ }): Promise<DirectoryResult>;
49
52
  };
50
53
  export declare function resolvePathWithinRoot(params: {
51
54
  rootDir: string;
@@ -77,13 +80,7 @@ export declare function ensureDirectoryWithinRoot(params: {
77
80
  scopeLabel: string;
78
81
  defaultDirName?: string;
79
82
  mode?: number;
80
- }): Promise<{
81
- ok: true;
82
- path: string;
83
- } | {
84
- ok: false;
85
- error: string;
86
- }>;
83
+ }): Promise<DirectoryResult>;
87
84
  export declare function resolvePathsWithinRoot(params: ResolvePathsWithinRootParams): ResolvePathsWithinRootResult;
88
85
  export declare function resolveExistingPathsWithinRoot(params: ResolvePathsWithinRootParams): Promise<ResolvePathsWithinRootResult>;
89
86
  export declare function resolveStrictExistingPathsWithinRoot(params: ResolvePathsWithinRootParams): Promise<ResolvePathsWithinRootResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"root-paths.d.ts","sourceRoot":"","sources":["../src/root-paths.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"root-paths.d.ts","sourceRoot":"","sources":["../src/root-paths.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,8 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { formatErrorDetail } from "./error-detail.js";
3
4
  import { FsSafeError } from "./errors.js";
4
- import { isNotFoundPathError, isPathInside, isPathRelativeEscape } from "./path.js";
5
+ import { assertNoNulPathInput, hasNodeErrorCode, isNodeError, isNotFoundPathError, isPathInside, isPathRelativeEscape, } from "./path.js";
5
6
  import { root as openRoot } from "./root.js";
6
7
  function invalidPath(scopeLabel) {
7
8
  return {
@@ -126,7 +127,7 @@ async function resolveNearestExistingPath(targetPath) {
126
127
  async function assertNoSymlinkSegments(params) {
127
128
  const relative = path.relative(params.rootDir, params.targetPath);
128
129
  if (isPathRelativeEscape(relative)) {
129
- throw new Error(`Invalid path: must stay within ${params.scopeLabel}`);
130
+ throw new FsSafeError("outside-workspace", `Invalid path: must stay within ${params.scopeLabel}`);
130
131
  }
131
132
  let current = params.rootDir;
132
133
  for (const segment of relative.split(path.sep).filter(Boolean)) {
@@ -134,10 +135,10 @@ async function assertNoSymlinkSegments(params) {
134
135
  try {
135
136
  const stat = await fs.lstat(current);
136
137
  if (stat.isSymbolicLink()) {
137
- throw new Error(`Invalid path: must not traverse symlinks within ${params.scopeLabel}`);
138
+ throw new FsSafeError("symlink", `Invalid path: must not traverse symlinks within ${params.scopeLabel}`);
138
139
  }
139
140
  if (!stat.isDirectory()) {
140
- throw new Error(`Invalid path: existing segment must be a directory within ${params.scopeLabel}`);
141
+ throw new FsSafeError("not-file", `Invalid path: existing segment must be a directory within ${params.scopeLabel}`);
141
142
  }
142
143
  }
143
144
  catch (err) {
@@ -149,28 +150,29 @@ async function assertNoSymlinkSegments(params) {
149
150
  }
150
151
  }
151
152
  export async function ensureDirectoryWithinRoot(params) {
152
- const lexical = resolvePathWithinRoot({
153
- rootDir: params.rootDir,
154
- requestedPath: params.requestedPath,
155
- scopeLabel: params.scopeLabel,
156
- defaultFileName: params.defaultDirName,
157
- });
158
- if (!lexical.ok) {
159
- return lexical;
160
- }
161
- const rootDir = path.resolve(params.rootDir);
162
- const targetPath = path.resolve(lexical.path);
153
+ const scopeLabel = formatErrorDetail(params.scopeLabel.slice(0, 80));
163
154
  try {
155
+ assertNoNulPathInput(params.rootDir);
156
+ assertNoNulPathInput(params.requestedPath);
157
+ if (!params.requestedPath.trim())
158
+ assertNoNulPathInput(params.defaultDirName ?? "");
159
+ const lexical = resolvePathWithinRoot({
160
+ ...params, scopeLabel, defaultFileName: params.defaultDirName,
161
+ });
162
+ if (!lexical.ok)
163
+ return lexical;
164
+ const rootDir = path.resolve(params.rootDir);
165
+ const targetPath = lexical.path;
164
166
  const rootStat = await fs.lstat(rootDir);
165
167
  if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
166
- return invalidPath(params.scopeLabel);
168
+ return invalidPath(scopeLabel);
167
169
  }
168
- await assertNoSymlinkSegments({ rootDir, targetPath, scopeLabel: params.scopeLabel });
170
+ await assertNoSymlinkSegments({ rootDir, targetPath, scopeLabel });
169
171
  const rootReal = await fs.realpath(rootDir);
170
172
  const nearestExistingPath = await resolveNearestExistingPath(targetPath);
171
173
  const nearestExistingReal = await fs.realpath(nearestExistingPath);
172
174
  if (!isPathInside(rootReal, nearestExistingReal)) {
173
- return invalidPath(params.scopeLabel);
175
+ return invalidPath(scopeLabel);
174
176
  }
175
177
  const relative = path.relative(rootDir, targetPath);
176
178
  let current = rootDir;
@@ -180,7 +182,7 @@ export async function ensureDirectoryWithinRoot(params) {
180
182
  try {
181
183
  const stat = await fs.lstat(current);
182
184
  if (stat.isSymbolicLink() || !stat.isDirectory()) {
183
- return invalidPath(params.scopeLabel);
185
+ return invalidPath(scopeLabel);
184
186
  }
185
187
  break;
186
188
  }
@@ -192,10 +194,7 @@ export async function ensureDirectoryWithinRoot(params) {
192
194
  await fs.mkdir(current, { mode: params.mode });
193
195
  }
194
196
  catch (mkdirErr) {
195
- if (isNotFoundPathError(mkdirErr)) {
196
- throw mkdirErr;
197
- }
198
- if (mkdirErr.code === "EEXIST") {
197
+ if (hasNodeErrorCode(mkdirErr, "EEXIST")) {
199
198
  continue;
200
199
  }
201
200
  throw mkdirErr;
@@ -204,17 +203,27 @@ export async function ensureDirectoryWithinRoot(params) {
204
203
  }
205
204
  const currentReal = await fs.realpath(current);
206
205
  if (!isPathInside(rootReal, currentReal)) {
207
- return invalidPath(params.scopeLabel);
206
+ return invalidPath(scopeLabel);
208
207
  }
209
208
  }
210
209
  const targetReal = await fs.realpath(targetPath);
211
210
  if (!isPathInside(rootReal, targetReal)) {
212
- return invalidPath(params.scopeLabel);
211
+ return invalidPath(scopeLabel);
213
212
  }
214
213
  return { ok: true, path: targetPath };
215
214
  }
216
- catch {
217
- return invalidPath(params.scopeLabel);
215
+ catch (cause) {
216
+ if ((cause instanceof FsSafeError && cause.category === "policy") ||
217
+ hasNodeErrorCode(cause, "ENOTDIR")) {
218
+ return invalidPath(scopeLabel);
219
+ }
220
+ // Native messages include unbounded paths; display only bounded errno metadata.
221
+ const code = isNodeError(cause) && typeof cause.code === "string"
222
+ ? formatErrorDetail(cause.code.slice(0, 40)) : "filesystem operation failed";
223
+ const syscall = isNodeError(cause) && typeof cause.syscall === "string"
224
+ ? ` during ${formatErrorDetail(cause.syscall.slice(0, 40))}` : "";
225
+ const diagnostic = new FsSafeError("helper-failed", `Could not prepare ${scopeLabel}: ${code}${syscall}`, { cause });
226
+ return { ok: false, error: diagnostic.message, diagnostic };
218
227
  }
219
228
  }
220
229
  export function resolvePathsWithinRoot(params) {
@@ -0,0 +1,11 @@
1
+ import { type AsyncDirectoryGuard } from "./directory-guard.js";
2
+ import type { PublishedWriteIdentity } from "./pinned-write.js";
3
+ import { type RootContext } from "./root-context.js";
4
+ export declare function verifyAtomicWriteResult(params: {
5
+ root: RootContext;
6
+ targetPath: string;
7
+ fd: number;
8
+ expectedIdentity: PublishedWriteIdentity;
9
+ parentGuard: AsyncDirectoryGuard;
10
+ }): Promise<void>;
11
+ //# sourceMappingURL=root-write-verification.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root-write-verification.d.ts","sourceRoot":"","sources":["../src/root-write-verification.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAK3F,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,WAAW,EAAE,mBAAmB,CAAC;CAClC,GAAG,OAAO,CAAC,IAAI,CAAC,CA6EhB"}