@openclaw/openshell-sandbox 2026.9.6 → 2026.9.7

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 (244) hide show
  1. package/assets/icon.png +0 -0
  2. package/dist/index.js +75 -229
  3. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +102 -0
  4. package/node_modules/@openclaw/fs-safe/README.md +24 -6
  5. package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +6 -1
  6. package/node_modules/@openclaw/fs-safe/dist/advanced.js +3 -1
  7. package/node_modules/@openclaw/fs-safe/dist/archive-durability.js +1 -1
  8. package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +1 -2
  9. package/node_modules/@openclaw/fs-safe/dist/archive-plan.d.ts +2 -7
  10. package/node_modules/@openclaw/fs-safe/dist/archive-read.js +9 -18
  11. package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -2
  12. package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +3 -3
  13. package/node_modules/@openclaw/fs-safe/dist/archive-zip-directory.js +4 -0
  14. package/node_modules/@openclaw/fs-safe/dist/archive-zip-entry.d.ts +11 -11
  15. package/node_modules/@openclaw/fs-safe/dist/archive-zip-entry.js +3 -35
  16. package/node_modules/@openclaw/fs-safe/dist/archive-zip-integrity.d.ts +2 -2
  17. package/node_modules/@openclaw/fs-safe/dist/archive-zip-integrity.js +2 -12
  18. package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.d.ts +7 -3
  19. package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.js +13 -10
  20. package/node_modules/@openclaw/fs-safe/dist/archive-zip-manifest.js +3 -1
  21. package/node_modules/@openclaw/fs-safe/dist/archive-zip-preflight.d.ts +2 -3
  22. package/node_modules/@openclaw/fs-safe/dist/archive-zip-preflight.js +16 -8
  23. package/node_modules/@openclaw/fs-safe/dist/archive.d.ts +6 -3
  24. package/node_modules/@openclaw/fs-safe/dist/archive.js +22 -19
  25. package/node_modules/@openclaw/fs-safe/dist/atomic.d.ts +1 -1
  26. package/node_modules/@openclaw/fs-safe/dist/copy-publication.d.ts +1 -3
  27. package/node_modules/@openclaw/fs-safe/dist/copy-publication.js +2 -6
  28. package/node_modules/@openclaw/fs-safe/dist/deny-mutation-match.d.ts +2 -0
  29. package/node_modules/@openclaw/fs-safe/dist/deny-mutation-match.js +71 -0
  30. package/node_modules/@openclaw/fs-safe/dist/deny-mutations.js +4 -3
  31. package/node_modules/@openclaw/fs-safe/dist/directory-guard.d.ts +0 -1
  32. package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +0 -3
  33. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.d.ts +20 -2
  34. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.js +77 -1
  35. package/node_modules/@openclaw/fs-safe/dist/directory-receipt.js +5 -7
  36. package/node_modules/@openclaw/fs-safe/dist/effective-uid.js +1 -4
  37. package/node_modules/@openclaw/fs-safe/dist/errors.d.ts +3 -1
  38. package/node_modules/@openclaw/fs-safe/dist/errors.js +3 -2
  39. package/node_modules/@openclaw/fs-safe/dist/file-identity.js +10 -2
  40. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-acquisition.js +1 -1
  41. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-admission.js +5 -1
  42. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-held.d.ts +4 -11
  43. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-held.js +1 -4
  44. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.d.ts +3 -6
  45. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.js +1 -1
  46. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-options.d.ts +2 -10
  47. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root.d.ts +2 -4
  48. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +2 -2
  49. package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.d.ts +3 -7
  50. package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.js +7 -10
  51. package/node_modules/@openclaw/fs-safe/dist/file-store-prune.js +7 -6
  52. package/node_modules/@openclaw/fs-safe/dist/file-store.d.ts +4 -7
  53. package/node_modules/@openclaw/fs-safe/dist/file-store.js +19 -21
  54. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +23 -31
  55. package/node_modules/@openclaw/fs-safe/dist/guest.js +21 -12
  56. package/node_modules/@openclaw/fs-safe/dist/json-document-store.d.ts +4 -9
  57. package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-ownership.d.ts +1 -5
  58. package/node_modules/@openclaw/fs-safe/dist/json-durable-queue.d.ts +0 -1
  59. package/node_modules/@openclaw/fs-safe/dist/json-durable-queue.js +2 -7
  60. package/node_modules/@openclaw/fs-safe/dist/local-file-access.js +2 -5
  61. package/node_modules/@openclaw/fs-safe/dist/local-file-descriptor.d.ts +2 -5
  62. package/node_modules/@openclaw/fs-safe/dist/local-roots.d.ts +2 -7
  63. package/node_modules/@openclaw/fs-safe/dist/move-path-cleanup.js +4 -4
  64. package/node_modules/@openclaw/fs-safe/dist/mutation-authority.js +5 -0
  65. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +51 -14
  66. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +8 -2
  67. package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.d.ts +13 -0
  68. package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.js +303 -0
  69. package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch-worker.d.ts +1 -0
  70. package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch-worker.js +54 -0
  71. package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.d.ts +5 -0
  72. package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.js +64 -0
  73. package/node_modules/@openclaw/fs-safe/dist/owner-dacl.d.ts +2 -0
  74. package/node_modules/@openclaw/fs-safe/dist/owner-dacl.js +3 -0
  75. package/node_modules/@openclaw/fs-safe/dist/path.js +17 -1
  76. package/node_modules/@openclaw/fs-safe/dist/permission-exec.d.ts +6 -0
  77. package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +3 -6
  78. package/node_modules/@openclaw/fs-safe/dist/permissions-public.d.ts +3 -1
  79. package/node_modules/@openclaw/fs-safe/dist/permissions-public.js +1 -0
  80. package/node_modules/@openclaw/fs-safe/dist/permissions-windows.d.ts +3 -6
  81. package/node_modules/@openclaw/fs-safe/dist/permissions.d.ts +3 -10
  82. package/node_modules/@openclaw/fs-safe/dist/permissions.js +0 -1
  83. package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.d.ts +0 -1
  84. package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.js +4 -2
  85. package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-shared-route.d.ts +2 -8
  86. package/node_modules/@openclaw/fs-safe/dist/pinned-open.d.ts +1 -1
  87. package/node_modules/@openclaw/fs-safe/dist/pinned-open.js +2 -3
  88. package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +4 -0
  89. package/node_modules/@openclaw/fs-safe/dist/read-opened-file.d.ts +2 -5
  90. package/node_modules/@openclaw/fs-safe/dist/regular-file.js +3 -3
  91. package/node_modules/@openclaw/fs-safe/dist/replace-directory.js +5 -17
  92. package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.d.ts +4 -0
  93. package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.js +36 -0
  94. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.d.ts +4 -10
  95. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +66 -38
  96. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.d.ts +7 -11
  97. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +10 -2
  98. package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.d.ts +21 -0
  99. package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.js +123 -0
  100. package/node_modules/@openclaw/fs-safe/dist/replace-file-mutation.d.ts +26 -0
  101. package/node_modules/@openclaw/fs-safe/dist/replace-file-mutation.js +47 -0
  102. package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.d.ts +2 -2
  103. package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.js +16 -4
  104. package/node_modules/@openclaw/fs-safe/dist/replace-file-types.d.ts +55 -0
  105. package/node_modules/@openclaw/fs-safe/dist/replace-file-types.js +1 -0
  106. package/node_modules/@openclaw/fs-safe/dist/replace-file.d.ts +3 -55
  107. package/node_modules/@openclaw/fs-safe/dist/replace-file.js +31 -11
  108. package/node_modules/@openclaw/fs-safe/dist/retained-directory-replacement.d.ts +1 -0
  109. package/node_modules/@openclaw/fs-safe/dist/retained-directory-replacement.js +1 -1
  110. package/node_modules/@openclaw/fs-safe/dist/retained-file-types.d.ts +50 -0
  111. package/node_modules/@openclaw/fs-safe/dist/retained-file-types.js +1 -0
  112. package/node_modules/@openclaw/fs-safe/dist/retained-file.d.ts +3 -0
  113. package/node_modules/@openclaw/fs-safe/dist/retained-file.js +121 -0
  114. package/node_modules/@openclaw/fs-safe/dist/root-context.js +3 -2
  115. package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.d.ts +9 -0
  116. package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +28 -0
  117. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +9 -1
  118. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +88 -37
  119. package/node_modules/@openclaw/fs-safe/dist/root-file-final-admission.d.ts +1 -1
  120. package/node_modules/@openclaw/fs-safe/dist/root-file-final-admission.js +1 -6
  121. package/node_modules/@openclaw/fs-safe/dist/root-file.js +2 -5
  122. package/node_modules/@openclaw/fs-safe/dist/root-handle-context.d.ts +4 -0
  123. package/node_modules/@openclaw/fs-safe/dist/root-handle-context.js +12 -0
  124. package/node_modules/@openclaw/fs-safe/dist/root-impl.d.ts +3 -3
  125. package/node_modules/@openclaw/fs-safe/dist/root-impl.js +8 -5
  126. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +2 -7
  127. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +3 -3
  128. package/node_modules/@openclaw/fs-safe/dist/root-observed-path.d.ts +0 -1
  129. package/node_modules/@openclaw/fs-safe/dist/root-observed-path.js +0 -3
  130. package/node_modules/@openclaw/fs-safe/dist/root-path-observation.d.ts +4 -11
  131. package/node_modules/@openclaw/fs-safe/dist/root-path.js +7 -10
  132. package/node_modules/@openclaw/fs-safe/dist/root-paths.d.ts +2 -6
  133. package/node_modules/@openclaw/fs-safe/dist/root-remove-identity.d.ts +1 -3
  134. package/node_modules/@openclaw/fs-safe/dist/root-walk.d.ts +19 -12
  135. package/node_modules/@openclaw/fs-safe/dist/root-walk.js +56 -20
  136. package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +1 -6
  137. package/node_modules/@openclaw/fs-safe/dist/root-write-complete-parent.d.ts +2 -0
  138. package/node_modules/@openclaw/fs-safe/dist/root-write-complete-parent.js +1 -1
  139. package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.d.ts +1 -0
  140. package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.js +8 -2
  141. package/node_modules/@openclaw/fs-safe/dist/secret-file.d.ts +6 -2
  142. package/node_modules/@openclaw/fs-safe/dist/secret-file.js +12 -16
  143. package/node_modules/@openclaw/fs-safe/dist/secret-read-policy.js +1 -1
  144. package/node_modules/@openclaw/fs-safe/dist/secure-file-windows.js +5 -14
  145. package/node_modules/@openclaw/fs-safe/dist/secure-file.js +3 -2
  146. package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.js +2 -7
  147. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +4 -4
  148. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-admission-parser.d.ts +1 -2
  149. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-handle.d.ts +2 -8
  150. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-policy.d.ts +2 -7
  151. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.d.ts +2 -2
  152. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +11 -12
  153. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-stale-admission.d.ts +1 -5
  154. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock.js +9 -7
  155. package/node_modules/@openclaw/fs-safe/dist/staged-symlink-types.d.ts +39 -0
  156. package/node_modules/@openclaw/fs-safe/dist/staged-symlink-types.js +1 -0
  157. package/node_modules/@openclaw/fs-safe/dist/stat-observation.js +3 -10
  158. package/node_modules/@openclaw/fs-safe/dist/strict-file-identity.d.ts +2 -0
  159. package/node_modules/@openclaw/fs-safe/dist/strict-file-identity.js +6 -6
  160. package/node_modules/@openclaw/fs-safe/dist/symlink-parents.js +58 -7
  161. package/node_modules/@openclaw/fs-safe/dist/temp-target.js +8 -10
  162. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-admission.js +28 -33
  163. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +1 -1
  164. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +16 -17
  165. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +1 -2
  166. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +4 -9
  167. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.d.ts +8 -0
  168. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.js +52 -0
  169. package/node_modules/@openclaw/fs-safe/dist/test-hooks.d.ts +5 -1
  170. package/node_modules/@openclaw/fs-safe/dist/text-atomic.d.ts +2 -1
  171. package/node_modules/@openclaw/fs-safe/dist/text-atomic.js +2 -0
  172. package/node_modules/@openclaw/fs-safe/dist/trash.js +27 -1
  173. package/node_modules/@openclaw/fs-safe/dist/walk.d.ts +2 -5
  174. package/node_modules/@openclaw/fs-safe/dist/watch-alias.d.ts +6 -0
  175. package/node_modules/@openclaw/fs-safe/dist/watch-alias.js +88 -0
  176. package/node_modules/@openclaw/fs-safe/dist/watch-hints.d.ts +9 -0
  177. package/node_modules/@openclaw/fs-safe/dist/watch-hints.js +93 -0
  178. package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +36 -0
  179. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +73 -0
  180. package/node_modules/@openclaw/fs-safe/dist/watch-scan.d.ts +28 -0
  181. package/node_modules/@openclaw/fs-safe/dist/watch-scan.js +300 -0
  182. package/node_modules/@openclaw/fs-safe/dist/watch-stream.d.ts +8 -0
  183. package/node_modules/@openclaw/fs-safe/dist/watch-stream.js +32 -0
  184. package/node_modules/@openclaw/fs-safe/dist/watch-types.d.ts +60 -0
  185. package/node_modules/@openclaw/fs-safe/dist/watch-types.js +1 -0
  186. package/node_modules/@openclaw/fs-safe/dist/watch.d.ts +5 -0
  187. package/node_modules/@openclaw/fs-safe/dist/watch.js +530 -0
  188. package/node_modules/@openclaw/fs-safe/dist/windows-owner.d.ts +3 -7
  189. package/node_modules/@openclaw/fs-safe/dist/windows-owner.js +0 -1
  190. package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.cs +6 -6
  191. package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.ps1 +78 -3
  192. package/node_modules/@openclaw/fs-safe/dist/windows-security-command.d.ts +8 -0
  193. package/node_modules/@openclaw/fs-safe/dist/windows-security-command.js +66 -15
  194. package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.d.ts +4 -0
  195. package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.js +6 -2
  196. package/node_modules/@openclaw/fs-safe/docs/advanced.md +4 -2
  197. package/node_modules/@openclaw/fs-safe/docs/archive.md +16 -0
  198. package/node_modules/@openclaw/fs-safe/docs/atomic.md +93 -3
  199. package/node_modules/@openclaw/fs-safe/docs/contributing.md +102 -4
  200. package/node_modules/@openclaw/fs-safe/docs/copy.md +2 -0
  201. package/node_modules/@openclaw/fs-safe/docs/durability.md +7 -0
  202. package/node_modules/@openclaw/fs-safe/docs/file-store.md +5 -0
  203. package/node_modules/@openclaw/fs-safe/docs/guest.md +7 -1
  204. package/node_modules/@openclaw/fs-safe/docs/index.md +1 -0
  205. package/node_modules/@openclaw/fs-safe/docs/install.md +30 -0
  206. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +20 -4
  207. package/node_modules/@openclaw/fs-safe/docs/native.md +63 -1
  208. package/node_modules/@openclaw/fs-safe/docs/permissions.md +66 -0
  209. package/node_modules/@openclaw/fs-safe/docs/public-api.md +30 -3
  210. package/node_modules/@openclaw/fs-safe/docs/retained-file.md +115 -0
  211. package/node_modules/@openclaw/fs-safe/docs/root.md +35 -9
  212. package/node_modules/@openclaw/fs-safe/docs/secret-file.md +19 -0
  213. package/node_modules/@openclaw/fs-safe/docs/security-model.md +4 -1
  214. package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +5 -1
  215. package/node_modules/@openclaw/fs-safe/docs/staged-symlink.md +124 -0
  216. package/node_modules/@openclaw/fs-safe/docs/store.md +3 -1
  217. package/node_modules/@openclaw/fs-safe/docs/temp.md +24 -4
  218. package/node_modules/@openclaw/fs-safe/docs/testing.md +210 -0
  219. package/node_modules/@openclaw/fs-safe/docs/types.md +6 -0
  220. package/node_modules/@openclaw/fs-safe/docs/walk.md +34 -1
  221. package/node_modules/@openclaw/fs-safe/docs/watch.md +251 -0
  222. package/node_modules/@openclaw/fs-safe/docs/writing.md +21 -0
  223. package/node_modules/@openclaw/fs-safe/package.json +13 -9
  224. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  225. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  226. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  227. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  228. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  229. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  230. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  231. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  232. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  233. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  234. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  235. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  236. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
  237. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
  238. package/node_modules/p-limit/index.d.ts +2 -0
  239. package/node_modules/p-limit/index.js +17 -1
  240. package/node_modules/p-limit/package.json +1 -1
  241. package/node_modules/p-limit/readme.md +2 -0
  242. package/package.json +6 -6
  243. package/node_modules/@openclaw/fs-safe/dist/directory-mode-owner.d.ts +0 -20
  244. package/node_modules/@openclaw/fs-safe/dist/directory-mode-owner.js +0 -78
@@ -202,7 +202,8 @@ function inspectOpenedPermissions(stat, platform) {
202
202
  groupReadable: isGroupReadable(bits),
203
203
  };
204
204
  }
205
- async function assertSecurePermissions(options, stat, realPath, identity, fd) {
205
+ async function assertSecurePermissions(options, opened, fd) {
206
+ const { pathStat: stat, realPath, identity } = opened;
206
207
  if (options.permissions?.allowInsecure) {
207
208
  return undefined;
208
209
  }
@@ -282,7 +283,7 @@ export async function readSecureFile(options) {
282
283
  const opened = await openSecureHandle(options, maxBytes);
283
284
  try {
284
285
  assertTrustedDirs(options, opened.realPath);
285
- const permissions = await assertSecurePermissions(options, opened.pathStat, opened.realPath, opened.identity, opened.handle.fd);
286
+ const permissions = await assertSecurePermissions(options, opened, opened.handle.fd);
286
287
  const buffer = await readHandleWithTimeout(opened.handle, options.io?.timeoutMs, maxBytes);
287
288
  const finalIdentity = inspectFileIdentitySync(() => fsSync.fstatSync(opened.handle.fd, { bigint: true }), opened.identity);
288
289
  if (!finalIdentity.isFile()) {
@@ -3,15 +3,10 @@ import { tmpdir as getOsTmpDir } from "node:os";
3
3
  import path from "node:path";
4
4
  import { directoryEntryPath } from "./directory-entry-path.js";
5
5
  import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
6
+ import { hasNodeErrorCode } from "./path.js";
6
7
  import { assertSafePathSegment } from "./safe-path-segment.js";
7
8
  import { assertNoWindowsPathAlias, pathForWindowsFilesystem } from "./windows-path-alias.js";
8
9
  import { captureSecureTempRepairAdapter, repairSecureTempDirectory, secureTempDirectoryReceipt, } from "./secure-temp-repair.js";
9
- function isNodeErrorWithCode(err, code) {
10
- return (typeof err === "object" &&
11
- err !== null &&
12
- "code" in err &&
13
- err.code === code);
14
- }
15
10
  export function resolveSecureTempRoot(options) {
16
11
  const { fallbackPrefix: prefix, accessSync: suppliedAccess, lstatSync: suppliedLstat, mkdirSync: suppliedMkdir, descriptor: suppliedDescriptor, getuid: suppliedGetuid, tmpdir: suppliedTmpdir, platform: suppliedPlatform, preferredDir, skipPreferredOnWindows, warn: suppliedWarn, warningPrefix: suppliedWarningPrefix, unsafeFallbackLabel: suppliedFallbackLabel, } = options;
17
12
  const platform = suppliedPlatform ?? process.platform;
@@ -87,7 +82,7 @@ export function resolveSecureTempRoot(options) {
87
82
  candidate = lstatSync(candidatePath);
88
83
  }
89
84
  catch (error) {
90
- return { kind: isNodeErrorWithCode(error, "ENOENT") ? "missing" : "invalid", error };
85
+ return { kind: hasNodeErrorCode(error, "ENOENT") ? "missing" : "invalid", error };
91
86
  }
92
87
  let receipt;
93
88
  try {
@@ -102,7 +102,7 @@ export async function acquireSidecarLock(options, context) {
102
102
  await waitForRetry();
103
103
  }
104
104
  catch (waitError) {
105
- if (waitError.code === "file_lock_timeout")
105
+ if (waitError?.code === "file_lock_timeout")
106
106
  throw denial;
107
107
  throw waitError;
108
108
  }
@@ -290,9 +290,9 @@ export async function acquireSidecarLock(options, context) {
290
290
  }
291
291
  await handle.writeFile(raw, "utf8");
292
292
  }
293
- const snapshot = { raw, payload, stat: fsSync.fstatSync(handle.fd), ownershipToken };
293
+ const snapshot = { raw, payload, stat: fsSync.fstatSync(handle.fd, { bigint: true }), ownershipToken };
294
294
  createdSnapshot = snapshot;
295
- if (snapshot.stat.nlink === 0) {
295
+ if (snapshot.stat.nlink === 0n) {
296
296
  await handle.close();
297
297
  handle = null;
298
298
  await waitForRetry();
@@ -361,7 +361,7 @@ export async function acquireSidecarLock(options, context) {
361
361
  if (handle) {
362
362
  failedSnapshot ??= { payload: null };
363
363
  try {
364
- failedSnapshot.stat = fsSync.fstatSync(handle.fd);
364
+ failedSnapshot.stat = fsSync.fstatSync(handle.fd, { bigint: true });
365
365
  }
366
366
  catch {
367
367
  // Best-effort cleanup of a failed exclusive create.
@@ -1,7 +1,7 @@
1
1
  import type { Root } from "./root-impl.js";
2
2
  import type { HeldSidecarLock } from "./sidecar-lock-admission.js";
3
3
  import { readSidecarLockRawSnapshot, type SidecarLockSnapshot } from "./sidecar-lock-reclaim.js";
4
- type SidecarAdmissionRunner = {
4
+ export type SidecarAdmissionRunner = {
5
5
  hasToken(): boolean;
6
6
  run<T>(callback: () => T): T;
7
7
  };
@@ -39,4 +39,3 @@ export declare function observeHeldSidecarParser(params: {
39
39
  parserAccessor(): ((raw: string) => unknown) | undefined;
40
40
  isTransientDenial(error: unknown): boolean;
41
41
  }): Promise<HeldSidecarParserObservation>;
42
- export {};
@@ -1,5 +1,4 @@
1
- import type { Root } from "./root-impl.js";
2
- import { type SidecarLockSnapshot } from "./sidecar-lock-reclaim.js";
1
+ import type { HeldSidecarLock } from "./sidecar-lock-admission.js";
3
2
  import type { SidecarLockHandle } from "./sidecar-lock-types.js";
4
3
  export declare function stopSidecarLockMonitoring(held: {
5
4
  compromiseTimer?: NodeJS.Timeout;
@@ -14,12 +13,7 @@ export declare function createSidecarLockHandle(params: {
14
13
  }): SidecarLockHandle;
15
14
  export declare function createHeldSidecarLockHandle(params: {
16
15
  normalizedTargetPath: string;
17
- held: {
18
- lockPath: string;
19
- snapshot: SidecarLockSnapshot;
20
- lockRoot?: Root;
21
- parsePayload?: (raw: string) => unknown;
22
- };
16
+ held: Pick<HeldSidecarLock, "lockPath" | "snapshot" | "lockRoot" | "parsePayload">;
23
17
  release: (options?: {
24
18
  retry?: boolean;
25
19
  }) => Promise<unknown>;
@@ -1,4 +1,4 @@
1
- import type { SidecarLockRetryOptions } from "./sidecar-lock-types.js";
1
+ import type { SidecarLockReclaimParams, SidecarLockRetryOptions } from "./sidecar-lock-types.js";
2
2
  export declare function validateSidecarLockRetryOptions(retry: SidecarLockRetryOptions): void;
3
3
  export declare function validateSidecarLockTimeoutMs(timeoutMs: number | undefined): void;
4
4
  export declare function validateSidecarLockStaleMs(staleMs: number | undefined): void;
@@ -11,9 +11,4 @@ export declare function sidecarLockRetryDelay(retry: SidecarLockRetryOptions, ti
11
11
  export declare const maxTransientLockDenials = 8;
12
12
  export declare function isTransientLockFileDenial(error: unknown, lockPath: string): boolean;
13
13
  export declare function sidecarLockPayloadCreatedAtMs(payload: unknown): number | null;
14
- export declare function defaultSidecarLockShouldReclaim(params: {
15
- lockPath: string;
16
- payload: unknown;
17
- staleMs: number;
18
- nowMs: number;
19
- }): Promise<boolean>;
14
+ export declare function defaultSidecarLockShouldReclaim(params: Pick<SidecarLockReclaimParams, "lockPath" | "payload" | "staleMs" | "nowMs">): Promise<boolean>;
@@ -1,4 +1,4 @@
1
- import { type Stats } from "node:fs";
1
+ import { type BigIntStats, type Stats } from "node:fs";
2
2
  import type { Root } from "./root-impl.js";
3
3
  export type SidecarLockStaleSnapshot = {
4
4
  lockPath: string;
@@ -9,7 +9,7 @@ export type SidecarLockStaleSnapshot = {
9
9
  export type SidecarLockSnapshot = {
10
10
  raw?: string;
11
11
  payload: unknown;
12
- stat?: Stats;
12
+ stat?: Stats | BigIntStats;
13
13
  ownershipToken?: string;
14
14
  };
15
15
  type SidecarLockRawSnapshot = Omit<SidecarLockSnapshot, "payload"> & {
@@ -90,7 +90,7 @@ export async function readSidecarLockRawSnapshot(lockPath, options = {}) {
90
90
  const raw = (await readFileHandleBounded(opened.handle, MAX_LOCK_PAYLOAD_BYTES)).toString("utf8");
91
91
  return {
92
92
  raw,
93
- stat: opened.stat,
93
+ stat: fsSync.fstatSync(opened.handle.fd, { bigint: true }),
94
94
  };
95
95
  }
96
96
  finally {
@@ -99,7 +99,7 @@ export async function readSidecarLockRawSnapshot(lockPath, options = {}) {
99
99
  }
100
100
  let before;
101
101
  try {
102
- before = fsSync.lstatSync(lockPath);
102
+ before = fsSync.lstatSync(lockPath, { bigint: true });
103
103
  }
104
104
  catch (error) {
105
105
  before = missingSnapshotPath(error);
@@ -132,7 +132,7 @@ export async function readSidecarLockRawSnapshot(lockPath, options = {}) {
132
132
  options.onOpenFailure?.(error);
133
133
  throw error;
134
134
  }
135
- const opened = fsSync.fstatSync(handle.fd);
135
+ const opened = fsSync.fstatSync(handle.fd, { bigint: true });
136
136
  if (!opened.isFile()) {
137
137
  if (options.rejectNonFile) {
138
138
  throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
@@ -144,7 +144,7 @@ export async function readSidecarLockRawSnapshot(lockPath, options = {}) {
144
144
  const raw = (await readFileHandleBounded(handle, MAX_LOCK_PAYLOAD_BYTES)).toString("utf8");
145
145
  let after;
146
146
  try {
147
- after = fsSync.lstatSync(lockPath);
147
+ after = fsSync.lstatSync(lockPath, { bigint: true });
148
148
  }
149
149
  catch (error) {
150
150
  after = missingSnapshotPath(error);
@@ -159,7 +159,7 @@ export async function readSidecarLockRawSnapshot(lockPath, options = {}) {
159
159
  }
160
160
  function lstatSidecarLockSync(lockPath) {
161
161
  try {
162
- return fsSync.lstatSync(lockPath);
162
+ return fsSync.lstatSync(lockPath, { bigint: true });
163
163
  }
164
164
  catch (error) {
165
165
  return missingSnapshotPath(error);
@@ -189,7 +189,7 @@ export function readSidecarLockRawSnapshotSync(lockPath, options = {}) {
189
189
  options.onOpenFailure?.(error);
190
190
  throw error;
191
191
  }
192
- const opened = fsSync.fstatSync(fd);
192
+ const opened = fsSync.fstatSync(fd, { bigint: true });
193
193
  if (!opened.isFile()) {
194
194
  if (options.rejectNonFile) {
195
195
  throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
@@ -357,13 +357,12 @@ export async function removeStaleSidecarLockIfAllowed(params) {
357
357
  if (params.assertGuardHeld)
358
358
  await params.assertGuardHeld();
359
359
  params.assertAuthorized?.();
360
+ if (params.lockRoot) {
361
+ await params.lockRoot.remove(relativeSidecarLockPath(params.lockRoot, params.lockPath), { assertBeforeMutation: params.assertAuthorized });
362
+ return "removed";
363
+ }
360
364
  try {
361
- if (params.lockRoot) {
362
- await params.lockRoot.remove(relativeSidecarLockPath(params.lockRoot, params.lockPath), { assertBeforeMutation: params.assertAuthorized });
363
- }
364
- else {
365
- await fs.rm(params.lockPath);
366
- }
365
+ await fs.rm(params.lockPath);
367
366
  return "removed";
368
367
  }
369
368
  catch (err) {
@@ -1,11 +1,7 @@
1
1
  import { type SidecarReclaimGuard } from "./sidecar-lock-reclaim.js";
2
- import { type SidecarLockParserState } from "./sidecar-lock-admission-parser.js";
2
+ import { type SidecarLockParserState, type SidecarAdmissionRunner } from "./sidecar-lock-admission-parser.js";
3
3
  import type { Root } from "./root-impl.js";
4
4
  import type { SidecarLockAcquireOptions } from "./sidecar-lock-types.js";
5
- type SidecarAdmissionRunner = {
6
- hasToken(): boolean;
7
- run<T>(callback: () => T): T;
8
- };
9
5
  type StaleOptions = Pick<SidecarLockAcquireOptions<Record<string, unknown>>, "shouldReclaim" | "shouldRemoveStaleLock" | "staleRecovery">;
10
6
  export type SidecarLockStaleOptionsState = {
11
7
  shouldReclaimObserved?: boolean;
@@ -1,6 +1,6 @@
1
1
  import fsSync from "node:fs";
2
2
  import { FsSafeError } from "./errors.js";
3
- import { sameFileIdentity } from "./file-identity.js";
3
+ import { sameFileIdentityForCleanup } from "./file-identity.js";
4
4
  import { removeSidecarLockIfUnchanged, sidecarLockSnapshotMatches, } from "./sidecar-lock-reclaim.js";
5
5
  import { acquireSidecarLock } from "./sidecar-lock-acquire.js";
6
6
  import { createHeldSidecarLockHandle, stopSidecarLockMonitoring } from "./sidecar-lock-handle.js";
@@ -50,7 +50,7 @@ function resolveManagerState(key) {
50
50
  function snapshotMatchesSync(lockPath, observed) {
51
51
  let fd;
52
52
  try {
53
- const beforeStat = fsSync.lstatSync(lockPath);
53
+ const beforeStat = fsSync.lstatSync(lockPath, { bigint: true });
54
54
  if (!beforeStat.isFile()) {
55
55
  return false;
56
56
  }
@@ -60,16 +60,18 @@ function snapshotMatchesSync(lockPath, observed) {
60
60
  : 0) |
61
61
  (typeof fsSync.constants.O_NONBLOCK === "number" ? fsSync.constants.O_NONBLOCK : 0);
62
62
  fd = fsSync.openSync(lockPath, openFlags);
63
- const openedStat = fsSync.fstatSync(fd);
64
- if (!openedStat.isFile()) {
63
+ const openedStat = fsSync.fstatSync(fd, { bigint: true });
64
+ // Token-owned files can have different descriptor/path identities on VirtioFS.
65
+ // Require a known descriptor identity without rejecting that supported drift.
66
+ if (!openedStat.isFile() || !sameFileIdentityForCleanup(openedStat, openedStat)) {
65
67
  return false;
66
68
  }
67
- if (observed.raw !== undefined && openedStat.size !== Buffer.byteLength(observed.raw)) {
69
+ if (observed.raw !== undefined && openedStat.size !== BigInt(Buffer.byteLength(observed.raw))) {
68
70
  return false;
69
71
  }
70
72
  const raw = fsSync.readFileSync(fd, "utf8");
71
- const afterStat = fsSync.lstatSync(lockPath);
72
- if (!afterStat.isFile() || !sameFileIdentity(beforeStat, afterStat)) {
73
+ const afterStat = fsSync.lstatSync(lockPath, { bigint: true });
74
+ if (!afterStat.isFile() || !sameFileIdentityForCleanup(beforeStat, afterStat)) {
73
75
  return false;
74
76
  }
75
77
  return sidecarLockSnapshotMatches({ raw, payload: null, stat: afterStat }, observed);
@@ -0,0 +1,39 @@
1
+ import type { PublishedFileReceipt, StagedFileCleanupReceipt, StagedFileReceipt } from "./staged-file-types.js";
2
+ /** Caller-captured admission evidence, never a same-target ownership heuristic. */
3
+ export type StagedSymlinkExpected = Readonly<Pick<StagedFileReceipt["identity"], "dev" | "ino" | "uid" | "gid" | "ctimeNs"> & {
4
+ target: string;
5
+ }>;
6
+ export type StagedSymlinkReceipt = StagedFileReceipt & Readonly<{
7
+ target: string;
8
+ }>;
9
+ export type PublishedSymlinkReceipt = Readonly<Pick<PublishedFileReceipt, "status" | "basename"> & {
10
+ staged: StagedSymlinkReceipt;
11
+ overwrite: false;
12
+ }>;
13
+ export type StagedSymlinkPublication = Readonly<{
14
+ status: "not-published";
15
+ }> | PublishedSymlinkReceipt | Readonly<{
16
+ status: "indeterminate";
17
+ basename: string;
18
+ overwrite: false;
19
+ }>;
20
+ export type StagedSymlinkRemoval = "removed" | "name-absent" | "preserved";
21
+ export type StagedSymlinkCleanupReceipt = Readonly<Pick<StagedFileCleanupReceipt, "temporaryBasename" | "status" | "resources"> & {
22
+ publication: StagedSymlinkPublication;
23
+ }>;
24
+ export type StagedSymlinkFailureDetails = Readonly<{
25
+ phase: "prepare" | "publish" | "remove-published" | "cleanup";
26
+ publication: StagedSymlinkPublication;
27
+ cleanup?: StagedSymlinkCleanupReceipt;
28
+ }>;
29
+ export interface StagedSymlink extends AsyncDisposable {
30
+ readonly receipt: StagedSymlinkReceipt;
31
+ assertCurrent(): Promise<void>;
32
+ publish(basename: string): Promise<PublishedSymlinkReceipt>;
33
+ /** Check the published name against the still-retained original symlink. */
34
+ assertPublished(): Promise<void>;
35
+ /** Explicit recovery only. Never removes an observed foreign replacement. */
36
+ removePublished(): Promise<StagedSymlinkRemoval>;
37
+ /** Closes the owner; only an unattempted, still-owned stage is removed. */
38
+ cleanup(): Promise<StagedSymlinkCleanupReceipt>;
39
+ }
@@ -1,11 +1,4 @@
1
- import { FsSafeError } from "./errors.js";
2
- import { recordFileObservationFailure } from "./file-observation.js";
3
- import { inspectFileIdentitySync } from "./strict-file-identity.js";
4
- function identityMismatch() {
5
- const error = new FsSafeError("path-mismatch", "file identity changed or could not be verified");
6
- recordFileObservationFailure(error, "identity");
7
- throw error;
8
- }
1
+ import { fileIdentityMismatchError, inspectFileIdentitySync } from "./strict-file-identity.js";
9
2
  function safeNumber(value) {
10
3
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
11
4
  }
@@ -35,7 +28,7 @@ function observeStatSync(inspect, expected, initial, platform = process.platform
35
28
  const ino = numericIno ? BigInt(rawIno) : typeof rawIno === "bigint" ? rawIno : undefined;
36
29
  if (expected && ((dev !== undefined && dev !== expected.dev) ||
37
30
  (ino !== undefined && ino !== expected.ino)))
38
- identityMismatch();
31
+ throw fileIdentityMismatchError();
39
32
  if (numericDev && numericIno) {
40
33
  return (captureIdentity
41
34
  ? { stat, identity: expected ?? { dev: dev, ino: ino } }
@@ -47,7 +40,7 @@ function observeStatSync(inspect, expected, initial, platform = process.platform
47
40
  const current = inspect(true);
48
41
  if ((dev !== undefined && current.dev !== dev) ||
49
42
  (ino !== undefined && current.ino !== ino))
50
- identityMismatch();
43
+ throw fileIdentityMismatchError();
51
44
  return current;
52
45
  }, expected, platform);
53
46
  return (captureIdentity
@@ -1,5 +1,7 @@
1
1
  import type { BigIntStats } from "node:fs";
2
+ import { FsSafeError } from "./errors.js";
2
3
  type ExactFileIdentity = Pick<BigIntStats, "dev" | "ino">;
4
+ export declare function fileIdentityMismatchError(): FsSafeError;
3
5
  export declare function inspectFileIdentity<T extends ExactFileIdentity>(inspect: () => T | Promise<T>, expected?: ExactFileIdentity, platform?: NodeJS.Platform): Promise<T>;
4
6
  export declare function inspectFileIdentitySync<T extends ExactFileIdentity>(inspect: () => T, expected?: ExactFileIdentity, platform?: NodeJS.Platform, mismatch?: () => Error): T;
5
7
  export {};
@@ -1,6 +1,6 @@
1
1
  import { FsSafeError } from "./errors.js";
2
2
  import { recordFileObservationFailure } from "./file-observation.js";
3
- function identityMismatch() {
3
+ export function fileIdentityMismatchError() {
4
4
  const error = new FsSafeError("path-mismatch", "file identity changed or could not be verified");
5
5
  recordFileObservationFailure(error, "identity");
6
6
  return error;
@@ -13,20 +13,20 @@ function identityCheck(expected, platform) {
13
13
  const value = stat[field];
14
14
  // Numeric receipts cannot recover identity bits already lost to rounding.
15
15
  if (typeof value !== "bigint")
16
- throw identityMismatch();
16
+ throw fileIdentityMismatchError();
17
17
  if (platform === "win32" && value === 0n) {
18
18
  complete = false;
19
19
  }
20
20
  else {
21
21
  if (known[field] !== undefined && known[field] !== value)
22
- throw identityMismatch();
22
+ throw fileIdentityMismatchError();
23
23
  known[field] = value;
24
24
  }
25
25
  }
26
26
  return complete;
27
27
  };
28
28
  if (expected && !check(expected))
29
- throw identityMismatch();
29
+ throw fileIdentityMismatchError();
30
30
  return check;
31
31
  }
32
32
  // Retry only unknown Windows identities, retaining every known component so a
@@ -38,9 +38,9 @@ export async function inspectFileIdentity(inspect, expected, platform = process.
38
38
  if (check(stat))
39
39
  return stat;
40
40
  }
41
- throw identityMismatch();
41
+ throw fileIdentityMismatchError();
42
42
  }
43
- export function inspectFileIdentitySync(inspect, expected, platform = process.platform, mismatch = identityMismatch) {
43
+ export function inspectFileIdentitySync(inspect, expected, platform = process.platform, mismatch = fileIdentityMismatchError) {
44
44
  let knownDev;
45
45
  let knownIno;
46
46
  if (expected) {
@@ -3,24 +3,46 @@ import path from "node:path";
3
3
  import { FsSafeError } from "./errors.js";
4
4
  import { hasNodeErrorCode, isPathRelativeEscape } from "./path.js";
5
5
  import { assertNoWindowsPathAlias, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
6
+ function outsideRootError(params, root) {
7
+ return new Error(`${params.messagePrefix ?? "Path"} must stay under ${root}.`);
8
+ }
9
+ function pathSegments(value) {
10
+ return value.split(process.platform === "win32" ? /[/\\]+/ : /\/+/)
11
+ .filter((segment) => segment.length > 0 && segment !== ".");
12
+ }
13
+ function rawTargetSegments(root, targetPath) {
14
+ // Resolve only the drive/root or cwd, never the caller's dotdot segments.
15
+ const targetRoot = path.parse(targetPath).root;
16
+ const base = resolvePathPreservingWindowsRoot(targetRoot || ".");
17
+ const absoluteTarget = `${base}${path.sep}${targetPath.slice(targetRoot.length)}`;
18
+ const rootSegments = pathSegments(root);
19
+ const targetSegments = pathSegments(absoluteTarget);
20
+ const fold = (segment) => process.platform === "win32" ? segment.toLowerCase() : segment;
21
+ if (rootSegments.some((segment, index) => fold(segment) !== fold(targetSegments[index] ?? ""))) {
22
+ return undefined;
23
+ }
24
+ return targetSegments.slice(rootSegments.length);
25
+ }
6
26
  function resolvePathWalk(params) {
7
27
  const rawRootDir = params.rootDir;
8
28
  assertNoWindowsPathAlias(rawRootDir, "filesystem", "root dir uses a Windows filesystem namespace alias");
9
29
  const rawTargetPath = params.targetPath;
10
30
  assertNoWindowsPathAlias(rawTargetPath, "filesystem", "target path uses a Windows filesystem namespace alias");
11
31
  const root = resolvePathPreservingWindowsRoot(rawRootDir);
12
- const target = resolvePathPreservingWindowsRoot(rawTargetPath);
13
- const relative = path.relative(root, target);
32
+ const lexicalTarget = resolvePathPreservingWindowsRoot(rawTargetPath);
33
+ const relative = path.relative(root, lexicalTarget);
14
34
  if (isPathRelativeEscape(relative)) {
15
35
  if (params.allowOutsideRoot) {
16
36
  return null;
17
37
  }
18
- throw new Error(`${params.messagePrefix ?? "Path"} must stay under ${root}.`);
38
+ throw outsideRootError(params, root);
19
39
  }
20
- return {
21
- root,
22
- segments: relative && relative !== "." ? relative.split(path.sep).filter(Boolean) : [],
23
- };
40
+ const segments = rawTargetSegments(root, rawTargetPath);
41
+ // A spelling outside the root must not fall back to a normalized walk that
42
+ // could erase a symlink before the filesystem receives the original path.
43
+ if (!segments)
44
+ throw outsideRootError(params, root);
45
+ return { root, segments };
24
46
  }
25
47
  function formatUnsafePath(params, current) {
26
48
  return `${params.messagePrefix ?? "Path"} must not traverse symlinked directory: ${current}`;
@@ -28,18 +50,39 @@ function formatUnsafePath(params, current) {
28
50
  export async function assertNoSymlinkParents(params) {
29
51
  assertNoSymlinkParentsSync(params);
30
52
  }
53
+ function isFilesystemRoot(root) {
54
+ return root === path.parse(root).root;
55
+ }
31
56
  export function assertNoSymlinkParentsSync(params) {
32
57
  const walk = resolvePathWalk(params);
33
58
  if (!walk) {
34
59
  return;
35
60
  }
36
61
  let current = walk.root;
62
+ // `..` may only undo a real directory this walk already lstat'd.
63
+ const walked = [];
37
64
  for (const [index, segment] of walk.segments.entries()) {
65
+ if (segment === "..") {
66
+ const top = walked[walked.length - 1];
67
+ if (top?.kind === "dir") {
68
+ walked.pop();
69
+ current = walked[walked.length - 1]?.path ?? walk.root;
70
+ continue;
71
+ }
72
+ if (top?.kind === "symlink") {
73
+ throw new Error(formatUnsafePath(params, top.path));
74
+ }
75
+ if (!isFilesystemRoot(walk.root)) {
76
+ throw outsideRootError(params, walk.root);
77
+ }
78
+ continue;
79
+ }
38
80
  current = path.join(current, segment);
39
81
  try {
40
82
  const stat = fsSync.lstatSync(current);
41
83
  if (stat.isSymbolicLink()) {
42
84
  if (params.allowRootChildSymlink && path.dirname(current) === walk.root) {
85
+ walked.push({ path: current, kind: "symlink" });
43
86
  continue;
44
87
  }
45
88
  throw new Error(formatUnsafePath(params, current));
@@ -47,9 +90,17 @@ export function assertNoSymlinkParentsSync(params) {
47
90
  if ((params.requireDirectories || index < walk.segments.length - 1) && !stat.isDirectory()) {
48
91
  throw new FsSafeError("not-file", `${params.messagePrefix ?? "Path"} must traverse directories: ${current}`);
49
92
  }
93
+ if (stat.isDirectory()) {
94
+ walked.push({ path: current, kind: "dir" });
95
+ }
50
96
  }
51
97
  catch (err) {
52
98
  if (hasNodeErrorCode(err, "ENOENT") && params.allowMissing !== false) {
99
+ // Win32 can cancel a nonexistent component before filesystem lookup.
100
+ // Returning early would leave later, reachable symlinks unchecked.
101
+ if (process.platform === "win32" && walk.segments.slice(index + 1).includes("..")) {
102
+ throw new FsSafeError("invalid-path", `${params.messagePrefix ?? "Path"} must not cancel a missing directory: ${current}`);
103
+ }
53
104
  return;
54
105
  }
55
106
  throw err;
@@ -4,8 +4,9 @@ import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { suffixWindowsReservedDeviceName } from "./filename.js";
6
6
  import { sameFileIdentityForCleanup } from "./file-identity.js";
7
- import { assertSafePathSegment, sanitizeSafePathSegment, trimHyphenEdges } from "./safe-path-segment.js";
7
+ import { assertSafePathSegment, isSafePathSegment, normalizeSafePathSegment, trimHyphenEdges, } from "./safe-path-segment.js";
8
8
  import { resolveSecureTempRoot } from "./secure-temp-dir.js";
9
+ import { hasNodeErrorCode } from "./path.js";
9
10
  import { registerTempPathForExit } from "./temp-cleanup.js";
10
11
  import { assertNoWindowsPathAlias, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
11
12
  const HYPHEN_CHAR_CODE = 0x2d;
@@ -59,7 +60,10 @@ function sanitizeExtension(extension) {
59
60
  return token ? `.${token}` : "";
60
61
  }
61
62
  export function sanitizeTempFileName(fileName) {
62
- return suffixWindowsReservedDeviceName(sanitizeSafePathSegment(path.basename(fileName)) ?? "download.bin");
63
+ // Suffix reserved stems before admission so CON.txt stays CON_.txt.
64
+ const suffixed = suffixWindowsReservedDeviceName(normalizeSafePathSegment(path.basename(fileName)));
65
+ // The suffix only inserts "_", so this value is already normalized.
66
+ return isSafePathSegment(suffixed, { allowDotPrefix: true }) ? suffixed : "download.bin";
63
67
  }
64
68
  export function buildRandomTempFilePath(params) {
65
69
  const rootDir = resolveTempRoot(params.rootDir);
@@ -76,12 +80,6 @@ export function buildRandomTempFilePath(params) {
76
80
  assertNoWindowsPathAlias(filePath, "filesystem", "temp file path uses a Windows filesystem namespace alias");
77
81
  return filePath;
78
82
  }
79
- function isNodeErrorWithCode(err, code) {
80
- return (typeof err === "object" &&
81
- err !== null &&
82
- "code" in err &&
83
- err.code === code);
84
- }
85
83
  async function cleanupTempDir(dir, identity, onCleanupError) {
86
84
  try {
87
85
  const current = fsSync.lstatSync(dir, { bigint: true });
@@ -91,7 +89,7 @@ async function cleanupTempDir(dir, identity, onCleanupError) {
91
89
  await fs.rm(dir, { recursive: true, force: true });
92
90
  }
93
91
  catch (err) {
94
- if (!isNodeErrorWithCode(err, "ENOENT")) {
92
+ if (!hasNodeErrorCode(err, "ENOENT")) {
95
93
  onCleanupError?.(err);
96
94
  }
97
95
  }
@@ -174,7 +172,7 @@ export async function createOwnedTempFile(params) {
174
172
  await owner.cleanup();
175
173
  }
176
174
  catch (err) {
177
- if (!isNodeErrorWithCode(err, "ENOENT")) {
175
+ if (!hasNodeErrorCode(err, "ENOENT")) {
178
176
  params.onCleanupError?.(err);
179
177
  }
180
178
  }