@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
@@ -7,7 +7,7 @@ function helperUnavailable(message, cause) {
7
7
  ...(cause instanceof Error ? { cause } : {}),
8
8
  });
9
9
  }
10
- function copyOperationalCode(target, source) {
10
+ export function copyOperationalCode(target, source) {
11
11
  const code = source?.code;
12
12
  if (typeof code === "string") {
13
13
  Object.defineProperty(target, "code", { configurable: true, value: code });
@@ -0,0 +1,50 @@
1
+ import type { StagedFileReceipt } from "./staged-file-types.js";
2
+ /** Exact producer observations: never supply rounded Number identities. */
3
+ export type RetainedFileExpected = Readonly<Pick<StagedFileReceipt["identity"], "dev" | "ino" | "size" | "mtimeNs" | "ctimeNs"> & {
4
+ /** Lowercase SHA-256 of the producer's expected bytes. */
5
+ sha256: string;
6
+ }>;
7
+ export type RetainedFileIssue = Readonly<{
8
+ phase: string;
9
+ code: string;
10
+ message: string;
11
+ cause?: unknown;
12
+ }>;
13
+ /** Descriptive facts, not a deletion capability or a durable transaction receipt. */
14
+ export type RetainedFileResult = Readonly<{
15
+ status: "unsupported" | "not-attempted" | "preserved-mismatch" | "disposition-accepted" | "name-absent-after-settlement" | "failed" | "indeterminate";
16
+ phase: string;
17
+ /** Full native 64-bit volume serial and 128-bit file ID, when observed. */
18
+ identity?: string;
19
+ disposition: "not-attempted" | "accepted" | "rejected" | "indeterminate";
20
+ namespace: "not-observed" | "absent" | "original" | "foreign" | "unknown";
21
+ resources: "closed" | "close-failed";
22
+ persistence: "not-proven";
23
+ errors: readonly RetainedFileIssue[];
24
+ }>;
25
+ export type RetainedFileReceipt = Readonly<{
26
+ directory: string;
27
+ parent: Readonly<{
28
+ dev: bigint;
29
+ ino: bigint;
30
+ }>;
31
+ basename: string;
32
+ expected: RetainedFileExpected;
33
+ identity: string;
34
+ }>;
35
+ export interface RetainedFile extends Disposable {
36
+ readonly receipt: RetainedFileReceipt;
37
+ /** One-shot, synchronous authority admission; always settles owned resources. */
38
+ remove(): RetainedFileResult;
39
+ /** Closes only. Never requests deletion, including after an exception. */
40
+ dispose(): RetainedFileResult;
41
+ }
42
+ export type RetainedFileAdmission = Readonly<{
43
+ status: "retained";
44
+ file: RetainedFile;
45
+ }> | RetainedFileResult;
46
+ export type RetainFileInDirectoryOptions = Readonly<Pick<RetainedFileReceipt, "directory" | "parent" | "basename" | "expected"> & {
47
+ assertBeforeMutation: () => void;
48
+ /** Synchronous verification budget, default 16 MiB; maximum 64 MiB. */
49
+ maxBytes?: number;
50
+ }>;
@@ -0,0 +1,3 @@
1
+ import type { RetainedFileAdmission, RetainFileInDirectoryOptions } from "./retained-file-types.js";
2
+ /** Retain one existing regular file on supported local Windows NTFS. No fallback. */
3
+ export declare function retainFileInDirectory(options: RetainFileInDirectoryOptions): RetainedFileAdmission;
@@ -0,0 +1,121 @@
1
+ import { FsSafeError } from "./errors.js";
2
+ import { getNativeBinding } from "./native.js";
3
+ import { assertSynchronousCallbackResult } from "./mutation-authority.js";
4
+ function freeze(result) {
5
+ return Object.freeze({ ...result, errors: Object.freeze(result.errors.map((error) => Object.freeze({ ...error }))) });
6
+ }
7
+ function unsupported(cause) {
8
+ return freeze({ status: "unsupported", phase: "admission", disposition: "not-attempted",
9
+ namespace: "not-observed", resources: "closed", persistence: "not-proven",
10
+ errors: [{ phase: "admission", code: "helper-unavailable",
11
+ message: "existing-file retention requires the maintained Windows native capability", cause }] });
12
+ }
13
+ function exact(value, name, zero = false) {
14
+ if (typeof value !== "bigint" || value < (zero ? 0n : 1n) || value > 0xffffffffffffffffn) {
15
+ throw new TypeError(`${name} must be an exact ${zero ? "nonnegative" : "positive"} unsigned 64-bit bigint`);
16
+ }
17
+ return value;
18
+ }
19
+ class Owner {
20
+ receipt;
21
+ #native;
22
+ #assert;
23
+ #result;
24
+ #running = false;
25
+ #reentered = false;
26
+ constructor(native, receipt, assertion) {
27
+ this.#native = native;
28
+ this.receipt = receipt;
29
+ this.#assert = assertion;
30
+ Object.freeze(this);
31
+ }
32
+ #settle(remove) {
33
+ if (this.#result)
34
+ return this.#result;
35
+ if (this.#running) {
36
+ this.#reentered = true;
37
+ throw new TypeError("retained-file settlement cannot be reentered");
38
+ }
39
+ this.#running = true;
40
+ let rejection;
41
+ if (remove) {
42
+ try {
43
+ assertSynchronousCallbackResult(this.#assert(), "assertBeforeMutation");
44
+ if (this.#reentered)
45
+ throw new TypeError("retained-file authority reentered settlement");
46
+ }
47
+ catch (cause) {
48
+ rejection = { cause };
49
+ }
50
+ }
51
+ try {
52
+ const result = this.#native.settle(remove && !rejection);
53
+ this.#result = freeze(rejection ? { ...result, phase: "authority", errors: [
54
+ { phase: "authority", code: "denied-path", message: "current mutation authority rejected removal", cause: rejection.cause },
55
+ ...result.errors,
56
+ ] } : result);
57
+ }
58
+ catch (cause) {
59
+ // An unexpected binding exception cannot establish native effect or close.
60
+ // Do not retry or release a dependency on an unknown native outcome.
61
+ this.#result = freeze({ status: "indeterminate", phase: "binding", identity: this.receipt.identity,
62
+ disposition: remove && !rejection ? "indeterminate" : "not-attempted", namespace: "unknown",
63
+ resources: "close-failed", persistence: "not-proven", errors: [
64
+ ...(rejection ? [{ phase: "authority", code: "denied-path", message: "current mutation authority rejected removal", cause: rejection.cause }] : []),
65
+ { phase: "binding", code: "helper-failed", message: "native settlement did not return a result", cause },
66
+ ] });
67
+ }
68
+ return this.#result;
69
+ }
70
+ remove() { return this.#settle(true); }
71
+ dispose() { return this.#settle(false); }
72
+ [Symbol.dispose]() {
73
+ const result = this.dispose();
74
+ if (result.resources !== "closed") {
75
+ throw new FsSafeError("helper-failed", "retained-file resource settlement is uncertain", { details: { result } });
76
+ }
77
+ }
78
+ }
79
+ /** Retain one existing regular file on supported local Windows NTFS. No fallback. */
80
+ export function retainFileInDirectory(options) {
81
+ // Snapshot once before any native resource admission or caller authority runs.
82
+ const directory = options.directory;
83
+ const basename = options.basename;
84
+ const sourceParent = options.parent;
85
+ const parent = Object.freeze({ dev: exact(sourceParent.dev, "parent.dev"), ino: exact(sourceParent.ino, "parent.ino") });
86
+ const source = options.expected;
87
+ const expected = Object.freeze({ dev: exact(source.dev, "expected.dev"), ino: exact(source.ino, "expected.ino"),
88
+ size: exact(source.size, "expected.size", true), mtimeNs: exact(source.mtimeNs, "expected.mtimeNs", true),
89
+ ctimeNs: exact(source.ctimeNs, "expected.ctimeNs", true), sha256: source.sha256 });
90
+ const assertion = options.assertBeforeMutation;
91
+ const maxBytes = options.maxBytes ?? 16 * 1024 * 1024;
92
+ if (typeof directory !== "string" || typeof basename !== "string" || typeof assertion !== "function"
93
+ || typeof expected.sha256 !== "string" || !/^[0-9a-f]{64}$/u.test(expected.sha256)
94
+ || !Number.isSafeInteger(maxBytes) || maxBytes <= 0 || maxBytes > 64 * 1024 * 1024 || expected.size > BigInt(maxBytes)) {
95
+ throw new TypeError("invalid retained-file directory, authority or bounded expected-byte contract");
96
+ }
97
+ if (process.platform !== "win32")
98
+ return unsupported();
99
+ let binding;
100
+ try {
101
+ binding = getNativeBinding();
102
+ }
103
+ catch (cause) {
104
+ return unsupported(cause);
105
+ }
106
+ if (!binding?.retainWindowsFile)
107
+ return unsupported();
108
+ let native;
109
+ try {
110
+ native = binding.retainWindowsFile(directory, basename, parent.dev, parent.ino, expected.dev, expected.ino, expected.size, expected.mtimeNs, expected.ctimeNs, expected.sha256, maxBytes);
111
+ }
112
+ catch (cause) {
113
+ // Factory errors have no returned owner; never infer absence or deletion.
114
+ return freeze({ ...unsupported(cause), status: "indeterminate", resources: "close-failed" });
115
+ }
116
+ const admission = native.admission;
117
+ if (admission.status !== "retained")
118
+ return freeze(admission);
119
+ const receipt = Object.freeze({ directory, parent, basename, expected, identity: admission.identity });
120
+ return Object.freeze({ status: "retained", file: new Owner(native, receipt, assertion) });
121
+ }
@@ -107,7 +107,7 @@ export function rootRelativeReadPath(root, filePath) {
107
107
  resolveCandidateRoot: base === root.rootDir && root.rootDir !== root.rootReal,
108
108
  });
109
109
  if (admitted)
110
- return admitted.relativePath;
110
+ return `.${path.sep}${admitted.relativePath}`;
111
111
  continue;
112
112
  }
113
113
  const prefix = ensureTrailingSep(base);
@@ -116,7 +116,8 @@ export function rootRelativeReadPath(root, filePath) {
116
116
  let start = prefix.length;
117
117
  while (raw[start] === path.sep)
118
118
  start += 1;
119
- return raw.slice(start);
119
+ // An admitted absolute tail stays literal, including a leading home marker.
120
+ return `.${path.sep}${raw.slice(start)}`;
120
121
  }
121
122
  }
122
123
  return raw;
@@ -0,0 +1,9 @@
1
+ import { type RootContext } from "./root-context.js";
2
+ import { type RootDirectoryObservationGuard } from "./root-directory-list.js";
3
+ import { type ExactStatIdentity } from "./stat-observation.js";
4
+ import type { DirEntry } from "./types.js";
5
+ /** One literal entry lookup under an operation-local admitted parent; never follow the leaf. */
6
+ export declare function lookupRootDirectoryEntry(root: RootContext, guard: RootDirectoryObservationGuard, name: string): Promise<{
7
+ entry: DirEntry;
8
+ identity: ExactStatIdentity;
9
+ } | undefined>;
@@ -0,0 +1,28 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { FsSafeError } from "./errors.js";
4
+ import { isNotFoundPathError } from "./path.js";
5
+ import { assertValidRootRelativePath } from "./root-context.js";
6
+ import { assertRootDirectoryObservationGuard, pathStatFromStats } from "./root-directory-list.js";
7
+ import { inspectStatObservationSync } from "./stat-observation.js";
8
+ /** One literal entry lookup under an operation-local admitted parent; never follow the leaf. */
9
+ export async function lookupRootDirectoryEntry(root, guard, name) {
10
+ if (!name || name === "." || name === ".." || path.basename(name) !== name || path.isAbsolute(name)) {
11
+ throw new FsSafeError("invalid-path", "directory lookup requires one literal name");
12
+ }
13
+ assertValidRootRelativePath(name);
14
+ await assertRootDirectoryObservationGuard(root, guard);
15
+ try {
16
+ const pathname = path.join(guard.realPath, name);
17
+ const observed = inspectStatObservationSync(bigint => bigint
18
+ ? fs.lstatSync(pathname, { bigint: true }) : fs.lstatSync(pathname));
19
+ await assertRootDirectoryObservationGuard(root, guard);
20
+ return { entry: { name, ...pathStatFromStats(observed.stat) }, identity: observed.identity };
21
+ }
22
+ catch (error) {
23
+ await assertRootDirectoryObservationGuard(root, guard);
24
+ if (isNotFoundPathError(error))
25
+ return undefined;
26
+ throw error;
27
+ }
28
+ }
@@ -2,6 +2,7 @@ import type { BigIntStats, Stats } from "node:fs";
2
2
  import { type AsyncDirectoryGuard } from "./directory-guard.js";
3
3
  import { type RootContext } from "./root-context.js";
4
4
  import type { RootPathDirectoryObservationGuard, RootPathObservationReceipt, RootPathParentObservationReceipt } from "./root-path.js";
5
+ import { type ExactStatIdentity } from "./stat-observation.js";
5
6
  import type { DirEntry, PathStat } from "./types.js";
6
7
  export declare function pathStatFromStats(stat: Stats | BigIntStats): PathStat;
7
8
  export type RootDirectoryObservationGuard = AsyncDirectoryGuard<BigIntStats>;
@@ -21,6 +22,7 @@ export type RootDirectoryListing = {
21
22
  next(): Promise<{
22
23
  kind: "entry";
23
24
  entry: DirEntry;
25
+ identity?: ExactStatIdentity;
24
26
  } | {
25
27
  kind: "limit";
26
28
  name: string;
@@ -33,6 +35,12 @@ export type RootDirectoryListingOptions = {
33
35
  snapshot: boolean;
34
36
  maxNames?: number;
35
37
  metadataBatchSize?: number;
38
+ /** Internal exact metadata lane, supported by streaming filesystem order. */
39
+ exactIdentity?: boolean;
40
+ /** Advisory scans may omit vanished leaves after revalidating their parent. */
41
+ skipVanished?: boolean;
42
+ /** Internal owner receives cleanup failures, including acquisition rollback. */
43
+ onCleanupFailure?: (error: unknown) => void;
36
44
  admitEntry(): boolean;
37
45
  };
38
- export declare function openRootDirectoryListing(root: RootContext, directory: string, options: RootDirectoryListingOptions): Promise<RootDirectoryListing>;
46
+ export declare function openRootDirectoryListing(root: RootContext, directory: string, options: RootDirectoryListingOptions, receipt?: RootPathObservationReceipt): Promise<RootDirectoryListing>;
@@ -1,3 +1,4 @@
1
+ import { isUtf8 } from "node:buffer";
1
2
  import fsSync from "node:fs";
2
3
  import fs from "node:fs/promises";
3
4
  import path from "node:path";
@@ -11,8 +12,25 @@ import { errorCauseOptions, rootPathChangedError } from "./root-errors.js";
11
12
  import { admitPathInsideRoot } from "./root-boundary.js";
12
13
  import { createSuppressedError } from "./suppressed-error.js";
13
14
  import { realpathSync } from "./realpath.js";
15
+ import { inspectStatObservationSync } from "./stat-observation.js";
14
16
  import { getFsSafeTestHooks } from "./test-hooks.js";
15
17
  const METADATA_BATCH_SIZE = 32;
18
+ function directoryEntryName(bytes) {
19
+ if (!isUtf8(bytes)) {
20
+ throw new FsSafeError("invalid-path", "directory entry name is not valid UTF-8");
21
+ }
22
+ return bytes.toString("utf8");
23
+ }
24
+ async function readDirectoryEntryName(handle) {
25
+ const entry = await handle.read();
26
+ // Bun returns the raw Buffer directly; Node returns a Dirent whose name is a Buffer.
27
+ // Node's Dir types still declare only string names.
28
+ return entry === null ? undefined
29
+ : directoryEntryName(Buffer.isBuffer(entry) ? entry : entry.name);
30
+ }
31
+ function openDirectoryNames(directory) {
32
+ return fs.opendir(directory, { bufferSize: 1, encoding: "buffer" });
33
+ }
16
34
  export function pathStatFromStats(stat) {
17
35
  const mtimeMs = typeof stat.mtimeMs === "bigint"
18
36
  ? "mtimeNs" in stat && typeof stat.mtimeNs === "bigint"
@@ -173,14 +191,7 @@ function normalizeInitialDirectoryError(error) {
173
191
  export async function listDirectoryPath(root, directory, withFileTypes, receipt) {
174
192
  let guard;
175
193
  if (receipt) {
176
- if (receipt.kind !== "directory" || receipt.targetPath !== directory ||
177
- receipt.directoryGuard.dir !== directory ||
178
- receipt.target !== receipt.directoryGuard ||
179
- (!isNativeDirectoryObservationGuard(receipt.directoryGuard) &&
180
- !receipt.directoryGuard.stat.isDirectory())) {
181
- throw new FsSafeError("path-mismatch", "directory observation receipt does not match target");
182
- }
183
- guard = receipt.directoryGuard;
194
+ guard = directoryGuardFromReceipt(directory, receipt);
184
195
  }
185
196
  else {
186
197
  try {
@@ -192,13 +203,23 @@ export async function listDirectoryPath(root, directory, withFileTypes, receipt)
192
203
  }
193
204
  return await listGuardedDirectoryPath(root, guard, withFileTypes, receipt);
194
205
  }
206
+ function directoryGuardFromReceipt(directory, receipt) {
207
+ if (receipt.kind !== "directory" || receipt.targetPath !== directory ||
208
+ receipt.directoryGuard.dir !== directory ||
209
+ receipt.target !== receipt.directoryGuard ||
210
+ (!isNativeDirectoryObservationGuard(receipt.directoryGuard) &&
211
+ !receipt.directoryGuard.stat.isDirectory())) {
212
+ throw new FsSafeError("path-mismatch", "directory observation receipt does not match target");
213
+ }
214
+ return receipt.directoryGuard;
215
+ }
195
216
  async function listGuardedDirectoryPath(root, guard, withFileTypes, receipt) {
196
217
  let entries;
197
218
  try {
198
219
  const beforeObservation = getFsSafeTestHooks()?.beforeRootListObservation;
199
220
  if (beforeObservation)
200
221
  await beforeObservation(guard.realPath, withFileTypes);
201
- const names = (await fs.readdir(guard.realPath)).sort();
222
+ const names = (await fs.readdir(guard.realPath, { encoding: "buffer" })).map(directoryEntryName).sort();
202
223
  entries = withFileTypes
203
224
  ? names.map(name => ({
204
225
  name,
@@ -221,19 +242,30 @@ async function listGuardedDirectoryPath(root, guard, withFileTypes, receipt) {
221
242
  await assertRootDirectoryObservationGuard(root, guard);
222
243
  return entries;
223
244
  }
224
- export async function openRootDirectoryListing(root, directory, options) {
225
- const admitted = await createRootDirectoryObservationGuard(root, directory).catch((error) => {
226
- throw normalizeDirectoryError(error);
227
- });
228
- // Retain exact admission identities; metadata rechecks can use numeric Stats
229
- // only when every identity component is losslessly representable.
230
- const guard = extendDirectoryObservationGuard({
231
- stat: admitted.stat,
232
- identity: { dev: admitted.stat.dev, ino: admitted.stat.ino },
233
- }, admitted.dir, admitted.realPath);
245
+ export async function openRootDirectoryListing(root, directory, options, receipt) {
246
+ if (options.exactIdentity && options.order !== "filesystem")
247
+ throw new TypeError("exact entry identities require filesystem order");
248
+ let guard;
249
+ if (receipt) {
250
+ guard = directoryGuardFromReceipt(directory, receipt);
251
+ }
252
+ else {
253
+ const admitted = await createRootDirectoryObservationGuard(root, directory).catch((error) => {
254
+ throw normalizeDirectoryError(error);
255
+ });
256
+ // Retain exact admission identities; metadata rechecks can use numeric Stats
257
+ // only when every identity component is losslessly representable.
258
+ guard = extendDirectoryObservationGuard({
259
+ stat: admitted.stat,
260
+ identity: { dev: admitted.stat.dev, ino: admitted.stat.ino },
261
+ }, admitted.dir, admitted.realPath);
262
+ }
234
263
  const assertCurrent = async () => {
235
264
  options.signal?.throwIfAborted();
236
- await assertRootDirectoryObservationGuard(root, guard);
265
+ if (receipt)
266
+ assertRootPathObservationReceiptCurrent(root, receipt);
267
+ else
268
+ await assertRootDirectoryObservationGuard(root, guard);
237
269
  options.signal?.throwIfAborted();
238
270
  };
239
271
  let handle;
@@ -248,23 +280,29 @@ export async function openRootDirectoryListing(root, directory, options) {
248
280
  const close = async () => {
249
281
  const owned = handle;
250
282
  handle = undefined;
251
- await owned?.close();
283
+ try {
284
+ await owned?.close();
285
+ }
286
+ catch (error) {
287
+ options.onCleanupFailure?.(error);
288
+ throw error;
289
+ }
252
290
  };
253
291
  try {
254
292
  await assertCurrent();
255
293
  if (options.order === "filesystem") {
256
294
  // A one-entry buffer keeps the truncation lookahead independent of width.
257
- handle = await fs.opendir(guard.realPath, { bufferSize: 1 });
295
+ handle = await openDirectoryNames(guard.realPath);
258
296
  }
259
297
  else if (options.snapshot) {
260
- snapshot = await listGuardedDirectoryPath(root, guard, true);
298
+ snapshot = await listGuardedDirectoryPath(root, guard, true, receipt);
261
299
  }
262
300
  else if (options.maxNames !== undefined) {
263
301
  names = [];
264
- handle = await fs.opendir(guard.realPath, { bufferSize: 1 });
302
+ handle = await openDirectoryNames(guard.realPath);
265
303
  while (true) {
266
304
  await assertCurrent();
267
- const name = (await handle.read())?.name;
305
+ const name = await readDirectoryEntryName(handle);
268
306
  await assertCurrent();
269
307
  if (name === undefined)
270
308
  break;
@@ -276,7 +314,7 @@ export async function openRootDirectoryListing(root, directory, options) {
276
314
  await close();
277
315
  }
278
316
  else {
279
- names = await fs.readdir(guard.realPath);
317
+ names = (await fs.readdir(guard.realPath, { encoding: "buffer" })).map(directoryEntryName);
280
318
  }
281
319
  await assertCurrent();
282
320
  names?.sort();
@@ -361,17 +399,30 @@ export async function openRootDirectoryListing(root, directory, options) {
361
399
  return { kind: "limit", name: pendingLimit };
362
400
  return;
363
401
  }
364
- await assertCurrent();
365
- const name = (await handle.read())?.name;
366
- await assertCurrent();
367
- if (name === undefined)
368
- return;
369
- if (!options.admitEntry())
370
- return { kind: "limit", name };
371
- // The stream's post-read fence is also the pre-stat fence in this owned operation.
372
- const stat = fsSync.lstatSync(path.join(guard.realPath, name));
373
- await assertCurrent();
374
- return { kind: "entry", entry: { name, ...pathStatFromStats(stat) } };
402
+ for (;;) {
403
+ await assertCurrent();
404
+ const name = await readDirectoryEntryName(handle);
405
+ await assertCurrent();
406
+ if (name === undefined)
407
+ return;
408
+ if (!options.admitEntry())
409
+ return { kind: "limit", name };
410
+ // The stream's post-read fence is also the pre-stat fence in this owned operation.
411
+ const pathname = path.join(guard.realPath, name);
412
+ let observed;
413
+ try {
414
+ observed = options.exactIdentity ? inspectStatObservationSync(bigint => bigint
415
+ ? fsSync.lstatSync(pathname, { bigint: true }) : fsSync.lstatSync(pathname)) : { stat: fsSync.lstatSync(pathname) };
416
+ }
417
+ catch (error) {
418
+ await assertCurrent();
419
+ if (options.skipVanished && isNotFoundPathError(error))
420
+ continue;
421
+ throw error;
422
+ }
423
+ await assertCurrent();
424
+ return { kind: "entry", entry: { name, ...pathStatFromStats(observed.stat) }, identity: observed.identity };
425
+ }
375
426
  }
376
427
  catch (error) {
377
428
  throw normalizeDirectoryError(error);
@@ -1,5 +1,5 @@
1
1
  import fs from "node:fs";
2
- import type { PinnedOpenSyncFinalAdmission } from "./pinned-open.js";
2
+ import { type PinnedOpenSyncFinalAdmission } from "./pinned-open.js";
3
3
  export type RootFileAdmissionFs = Pick<typeof fs, "lstatSync" | "realpathSync">;
4
4
  type ExactIdentity = Readonly<{
5
5
  dev: bigint;
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import { FsSafeError } from "./errors.js";
3
+ import { isExpectedPathError } from "./pinned-open.js";
3
4
  import { realpathSync } from "./realpath.js";
4
5
  import { admitPathInsideRoot } from "./root-boundary.js";
5
6
  import { hardlinkedPathNotAllowedError } from "./root-errors.js";
@@ -8,12 +9,6 @@ import { assertNoWindowsPathAlias, pathForWindowsFilesystem } from "./windows-pa
8
9
  function rootIdentityMismatch(cause) {
9
10
  return new FsSafeError("path-mismatch", "canonical root directory identity changed or could not be verified", cause === undefined ? {} : { cause });
10
11
  }
11
- function isExpectedPathError(error) {
12
- const code = typeof error === "object" && error !== null && "code" in error
13
- ? String(error.code)
14
- : "";
15
- return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
16
- }
17
12
  function inspectCanonicalRoot(ioFs, rootPath, expected) {
18
13
  try {
19
14
  assertNoWindowsPathAlias(rootPath);
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { resolveRootPathSyncWithCanonicalRootObservation, resolveRootPathWithCanonicalRootObservation, } from "./root-path.js";
4
4
  import { readSymlinkResolution } from "./root-symlink-policy.js";
5
- import { openPinnedFileSync, } from "./pinned-open.js";
5
+ import { isExpectedPathError, openPinnedFileSync, } from "./pinned-open.js";
6
6
  import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
7
7
  import { FsSafeError } from "./errors.js";
8
8
  import { createRootFileFinalAdmission, observeCanonicalRoot, } from "./root-file-final-admission.js";
@@ -198,10 +198,7 @@ function snapshotAsyncResolution(params, absolutePath) {
198
198
  function toRootObservationError(error) {
199
199
  if (error instanceof FsSafeError)
200
200
  return toBoundaryValidationError(error);
201
- const code = typeof error === "object" && error !== null && "code" in error
202
- ? String(error.code)
203
- : "";
204
- if (code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP") {
201
+ if (isExpectedPathError(error)) {
205
202
  return { ok: false, reason: "path", error };
206
203
  }
207
204
  return { ok: false, reason: "io", error };
@@ -0,0 +1,4 @@
1
+ import type { RootContext } from "./root-context.js";
2
+ /** Internal registration; never reconstruct authority from public pathname fields. */
3
+ export declare function registerRootHandleContext(handle: object, context: RootContext): void;
4
+ export declare function rootHandleContext(handle: object): RootContext;
@@ -0,0 +1,12 @@
1
+ import { FsSafeError } from "./errors.js";
2
+ const contexts = new WeakMap();
3
+ /** Internal registration; never reconstruct authority from public pathname fields. */
4
+ export function registerRootHandleContext(handle, context) {
5
+ contexts.set(handle, context);
6
+ }
7
+ export function rootHandleContext(handle) {
8
+ const context = contexts.get(handle);
9
+ if (!context)
10
+ throw new FsSafeError("invalid-path", "watch requires a genuine fs-safe Root");
11
+ return context;
12
+ }
@@ -5,7 +5,7 @@ import { type ReadResult } from "./read-opened-file.js";
5
5
  import { type RootEntriesOptions } from "./root-entries.js";
6
6
  import { type RootContext } from "./root-context.js";
7
7
  import type { DirEntry, PathStat } from "./types.js";
8
- import { type RootWalkEntry, type RootWalkOptions } from "./root-walk.js";
8
+ import { type RootWalkEntry, type RootWalkOptions, type RootWalkSymlinkPolicy } from "./root-walk.js";
9
9
  import { type RootAppendOptions, type RootCopyOptions, type RootCopySource, type RootCreateJsonOptions, type RootCreateOptions, type RootCreateStreamOptions, type RootDefaults, type RootMkdirOptions, type RootMoveOptions, type RootOpenOptions, type RootOpenWritableOptions, type RootReadOptions, type RootRemoveOptions, type RootWriteJsonOptions, type RootWriteOptions } from "./root-options.js";
10
10
  export { DEFAULT_ROOT_MAX_BYTES } from "./root-options.js";
11
11
  export { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
@@ -54,7 +54,7 @@ export interface Root {
54
54
  }): Promise<DirEntry[]>;
55
55
  entries(relativePath: string, options?: RootEntriesOptions): AsyncIterableIterator<DirEntry>;
56
56
  move(fromRelative: string, toRelative: string, options?: RootMoveOptions): Promise<void>;
57
- walk(relativePath: string, options: RootWalkOptions): AsyncIterableIterator<RootWalkEntry>;
57
+ walk<Policy extends RootWalkSymlinkPolicy>(relativePath: string, options: RootWalkOptions<Policy>): AsyncIterableIterator<RootWalkEntry<Policy>>;
58
58
  }
59
59
  export declare class RootHandle implements Root {
60
60
  private readonly context;
@@ -97,7 +97,7 @@ export declare class RootHandle implements Root {
97
97
  }): Promise<DirEntry[]>;
98
98
  move(fromRelative: string, toRelative: string, options?: RootMoveOptions): Promise<void>;
99
99
  entries(relativePath: string, options?: RootEntriesOptions): AsyncIterableIterator<DirEntry>;
100
- walk(relativePath: string, options: RootWalkOptions): AsyncIterableIterator<RootWalkEntry>;
100
+ walk<Policy extends RootWalkSymlinkPolicy>(relativePath: string, options: RootWalkOptions<Policy>): AsyncIterableIterator<RootWalkEntry<Policy>>;
101
101
  }
102
102
  export declare function root(rootDir: string, defaults?: RootDefaults): Promise<Root>;
103
103
  export declare function rootFromDirectoryGuard(guard: {
@@ -51,6 +51,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
51
51
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
52
  });
53
53
  import { randomUUID } from "node:crypto";
54
+ import { registerRootHandleContext } from "./root-handle-context.js";
54
55
  import fsSync, { constants as fsConstants } from "node:fs";
55
56
  import fs from "node:fs/promises";
56
57
  import path from "node:path";
@@ -85,7 +86,7 @@ import { listDirectoryPath, openRootDirectoryListing } from "./root-directory-li
85
86
  import { statResolvedPathInRoot } from "./root-path-stat.js";
86
87
  import { entriesInRoot } from "./root-entries.js";
87
88
  import { assertMoveMutationAllowed } from "./root-move-preflight.js";
88
- import { assertRootIdentityCurrent, assertRootIdentityCurrentSync, assertValidRootDestinationPath, assertValidRootRelativePath, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, rootRelativeReadPath, } from "./root-context.js";
89
+ import { assertRootIdentityCurrent, assertRootIdentityCurrentSync, assertValidRootDestinationPath, assertValidRootRelativePath, createRootObservationGuard, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, rootRelativeReadPath, } from "./root-context.js";
89
90
  import { errorCauseOptions, fileNotFoundError, hardlinkedPathNotAllowedError, isAlreadyExistsError, normalizePinnedPathError, normalizePinnedWriteError, outsideWorkspaceError, } from "./root-errors.js";
90
91
  import { getFsSafeTestHooks } from "./test-hooks.js";
91
92
  import { stringifyJsonDocument } from "./json-stringify.js";
@@ -199,6 +200,7 @@ export class RootHandle {
199
200
  this.rootWithSep = context.rootWithSep;
200
201
  this.defaults = defaults;
201
202
  registerFileLockSyncRootAdapter(this, context, defaults);
203
+ registerRootHandleContext(this, context);
202
204
  }
203
205
  mutationOptions(options) {
204
206
  return {
@@ -409,9 +411,13 @@ export class RootHandle {
409
411
  assertValidRootRelativePath(relativePath);
410
412
  return walkRoot({
411
413
  rootReal: this.context.rootReal,
414
+ observeRoot: () => createRootObservationGuard(this.context),
412
415
  stat: relative => this.stat(relative),
413
- list: async (relative, listingOptions) => {
416
+ list: async (relative, listingOptions, receipt) => {
414
417
  validatePinnedRelativePath(relative);
418
+ if (receipt) {
419
+ return await openRootDirectoryListing(this.context, receipt.targetPath, listingOptions, receipt);
420
+ }
415
421
  const resolved = await resolvePinnedPathInRoot(this.context, { relativePath: relative, allowRoot: true });
416
422
  return await openRootDirectoryListing(this.context, resolved.resolved, listingOptions);
417
423
  },
@@ -798,7 +804,6 @@ async function mkdirPathInRoot(root, params) {
798
804
  const prepared = policy && resolved.relativePosix !== ""
799
805
  ? await preparePinnedWriteMutationAdmission({
800
806
  rootReal: resolved.rootReal,
801
- rootWithSep: ensureTrailingSep(resolved.rootReal),
802
807
  rootIdentity: root.rootIdentity,
803
808
  resolvedTargetPath: resolved.resolved,
804
809
  originalPath: params.relativePath,
@@ -1045,10 +1050,8 @@ async function resolvePinnedRootPathInRoot(root, params) {
1045
1050
  throw err;
1046
1051
  throw new FsSafeError("path-alias", "path alias escape blocked", { cause: err });
1047
1052
  }
1048
- const rootWithSep = ensureTrailingSep(resolved.rootCanonicalPath);
1049
1053
  return {
1050
1054
  rootReal: resolved.rootCanonicalPath,
1051
- rootWithSep,
1052
1055
  canonicalPath: resolved.canonicalPath,
1053
1056
  };
1054
1057
  }