@openclaw/feishu 2026.9.8 → 2026.10.1-beta.2

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 (225) hide show
  1. package/dist/.setup/{channel-DV4hM2Uf.mjs → channel-CX62T71S.mjs} +146 -245
  2. package/dist/.setup/{channel.runtime-CXQcH5zZ.mjs → channel.runtime-COiaq-Rv.mjs} +6 -12
  3. package/dist/.setup/{chat-D8MlFAZi.mjs → chat-BdyvImsY.mjs} +6 -9
  4. package/dist/.setup/{client-BYG-_IZl.mjs → client-BsBVWJzN.mjs} +1 -1
  5. package/dist/.setup/{doctor-contract-3XC0vbIw.mjs → doctor-contract-B1B9ogdd.mjs} +212 -27
  6. package/dist/.setup/{monitor-D_rsHBZ6.mjs → monitor-7DvwUs4Y.mjs} +5 -6
  7. package/dist/.setup/{monitor.account-DvvY7i6k.mjs → monitor.account-7xVu7XWY.mjs} +190 -393
  8. package/dist/.setup/{probe-_ViqSi0j.mjs → probe-B9o7iey6.mjs} +13 -39
  9. package/dist/.setup/{reply-delivery-result-COAjaukc.mjs → reply-delivery-result-Bt-BWBEA.mjs} +151 -297
  10. package/dist/.setup/{setup-api-C4S5I3ac.mjs → setup-api-DmzX9S01.mjs} +1 -1
  11. package/dist/.setup/{subagent-hooks-BaFoMu8W.mjs → subagent-hooks-CUXg0pC-.mjs} +5 -11
  12. package/dist/.setup/{thread-bindings-EdLXPXzw.mjs → thread-bindings-jW2x5VuN.mjs} +23 -41
  13. package/dist/api.js +234 -297
  14. package/dist/channel-plugin-api.js +1 -1
  15. package/dist/doctor-contract-api.js +2 -2
  16. package/dist/session-binding-contract-api.js +1 -1
  17. package/dist/setup-api.js +1 -1
  18. package/dist/setup-entry.js +1 -1
  19. package/dist/subagent-hooks-api.js +1 -1
  20. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +95 -0
  21. package/node_modules/@openclaw/fs-safe/README.md +70 -379
  22. package/node_modules/@openclaw/fs-safe/dist/absolute-path.js +35 -51
  23. package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +2 -0
  24. package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
  25. package/node_modules/@openclaw/fs-safe/dist/archive-input.js +2 -0
  26. package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
  27. package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -1
  28. package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +9 -10
  29. package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -5
  30. package/node_modules/@openclaw/fs-safe/dist/atomic-io.d.ts +51 -0
  31. package/node_modules/@openclaw/fs-safe/dist/atomic-io.js +242 -0
  32. package/node_modules/@openclaw/fs-safe/dist/config.d.ts +1 -1
  33. package/node_modules/@openclaw/fs-safe/dist/config.js +1 -1
  34. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +1 -1
  35. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +2 -2
  36. package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +2 -0
  37. package/node_modules/@openclaw/fs-safe/dist/create.js +12 -5
  38. package/node_modules/@openclaw/fs-safe/dist/directory-guard.d.ts +11 -0
  39. package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +4 -7
  40. package/node_modules/@openclaw/fs-safe/dist/entry-publication-types.d.ts +55 -0
  41. package/node_modules/@openclaw/fs-safe/dist/entry-publication-types.js +1 -0
  42. package/node_modules/@openclaw/fs-safe/dist/entry-publication.d.ts +8 -0
  43. package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +262 -0
  44. package/node_modules/@openclaw/fs-safe/dist/exclusive-create.d.ts +3 -0
  45. package/node_modules/@openclaw/fs-safe/dist/exclusive-create.js +22 -0
  46. package/node_modules/@openclaw/fs-safe/dist/file-cleanup.d.ts +1 -6
  47. package/node_modules/@openclaw/fs-safe/dist/file-cleanup.js +3 -6
  48. package/node_modules/@openclaw/fs-safe/dist/file-identity.js +3 -6
  49. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.js +2 -2
  50. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-mutation.js +9 -0
  51. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root.js +4 -6
  52. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-stale-admission.js +1 -1
  53. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +8 -0
  54. package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.js +2 -0
  55. package/node_modules/@openclaw/fs-safe/dist/file-store-sync-write.js +20 -18
  56. package/node_modules/@openclaw/fs-safe/dist/file-store.js +4 -10
  57. package/node_modules/@openclaw/fs-safe/dist/fs.d.ts +2 -10
  58. package/node_modules/@openclaw/fs-safe/dist/fs.js +2 -10
  59. package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.d.ts +3 -4
  60. package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.js +6 -18
  61. package/node_modules/@openclaw/fs-safe/dist/guest-dispatch-python.js +1 -1
  62. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +0 -2
  63. package/node_modules/@openclaw/fs-safe/dist/guest.js +3 -9
  64. package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -1
  65. package/node_modules/@openclaw/fs-safe/dist/index.js +1 -1
  66. package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-directory.js +1 -5
  67. package/node_modules/@openclaw/fs-safe/dist/json.js +19 -37
  68. package/node_modules/@openclaw/fs-safe/dist/move-path.js +2 -0
  69. package/node_modules/@openclaw/fs-safe/dist/mutation-authority.d.ts +1 -0
  70. package/node_modules/@openclaw/fs-safe/dist/mutation-authority.js +9 -1
  71. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +47 -2
  72. package/node_modules/@openclaw/fs-safe/dist/native-config.d.ts +1 -9
  73. package/node_modules/@openclaw/fs-safe/dist/native-config.js +6 -40
  74. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +2 -0
  75. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +5 -2
  76. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +11 -316
  77. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent-windows.d.ts +7 -7
  78. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent-windows.js +28 -175
  79. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +15 -0
  80. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +418 -0
  81. package/node_modules/@openclaw/fs-safe/dist/native-rename-outcome.js +9 -4
  82. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +17 -13
  83. package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.js +6 -24
  84. package/node_modules/@openclaw/fs-safe/dist/native.js +5 -1
  85. package/node_modules/@openclaw/fs-safe/dist/path-case.js +10 -9
  86. package/node_modules/@openclaw/fs-safe/dist/path-scope-lexical.js +4 -1
  87. package/node_modules/@openclaw/fs-safe/dist/path-segment-route.d.ts +1 -0
  88. package/node_modules/@openclaw/fs-safe/dist/path-segment-route.js +3 -0
  89. package/node_modules/@openclaw/fs-safe/dist/path.js +4 -1
  90. package/node_modules/@openclaw/fs-safe/dist/permissions-windows.d.ts +0 -1
  91. package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.js +1 -3
  92. package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +2 -0
  93. package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +9 -10
  94. package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +3 -6
  95. package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +2 -2
  96. package/node_modules/@openclaw/fs-safe/dist/publish-file.js +4 -3
  97. package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.d.ts +3 -4
  98. package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.js +18 -32
  99. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.d.ts +5 -17
  100. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +92 -230
  101. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-source.d.ts +4 -15
  102. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-source.js +22 -63
  103. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.d.ts +9 -34
  104. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +78 -83
  105. package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.d.ts +18 -20
  106. package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.js +64 -98
  107. package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.d.ts +16 -33
  108. package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.js +61 -200
  109. package/node_modules/@openclaw/fs-safe/dist/replace-file-types.d.ts +1 -6
  110. package/node_modules/@openclaw/fs-safe/dist/replace-file.js +76 -216
  111. package/node_modules/@openclaw/fs-safe/dist/root-boundary.js +14 -24
  112. package/node_modules/@openclaw/fs-safe/dist/root-context.d.ts +1 -4
  113. package/node_modules/@openclaw/fs-safe/dist/root-context.js +4 -19
  114. package/node_modules/@openclaw/fs-safe/dist/root-create-native.d.ts +52 -0
  115. package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +350 -0
  116. package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +35 -0
  117. package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.js +1 -0
  118. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +2 -27
  119. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +31 -28
  120. package/node_modules/@openclaw/fs-safe/dist/root-directory.js +1 -4
  121. package/node_modules/@openclaw/fs-safe/dist/root-errors.js +34 -0
  122. package/node_modules/@openclaw/fs-safe/dist/root-file.js +9 -21
  123. package/node_modules/@openclaw/fs-safe/dist/root-impl.js +105 -37
  124. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +7 -2
  125. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +67 -38
  126. package/node_modules/@openclaw/fs-safe/dist/root-observed-path.d.ts +1 -1
  127. package/node_modules/@openclaw/fs-safe/dist/root-observed-path.js +0 -2
  128. package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -3
  129. package/node_modules/@openclaw/fs-safe/dist/root-path-existing.js +3 -1
  130. package/node_modules/@openclaw/fs-safe/dist/root-path-stat.js +68 -85
  131. package/node_modules/@openclaw/fs-safe/dist/root-path.js +6 -1
  132. package/node_modules/@openclaw/fs-safe/dist/root-paths-lexical.d.ts +4 -0
  133. package/node_modules/@openclaw/fs-safe/dist/root-paths-lexical.js +1 -1
  134. package/node_modules/@openclaw/fs-safe/dist/root-paths.js +4 -9
  135. package/node_modules/@openclaw/fs-safe/dist/root-remove-native.d.ts +4 -0
  136. package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +311 -0
  137. package/node_modules/@openclaw/fs-safe/dist/root-remove.d.ts +11 -0
  138. package/node_modules/@openclaw/fs-safe/dist/root-remove.js +3 -1
  139. package/node_modules/@openclaw/fs-safe/dist/root-walk.d.ts +2 -2
  140. package/node_modules/@openclaw/fs-safe/dist/root-walk.js +1 -3
  141. package/node_modules/@openclaw/fs-safe/dist/root-write-admission.d.ts +3 -3
  142. package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +8 -10
  143. package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.d.ts +0 -1
  144. package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.js +0 -4
  145. package/node_modules/@openclaw/fs-safe/dist/secret-file.js +12 -9
  146. package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.d.ts +0 -2
  147. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +2 -0
  148. package/node_modules/@openclaw/fs-safe/dist/sibling-temp.js +7 -4
  149. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +13 -6
  150. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.d.ts +0 -3
  151. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +5 -23
  152. package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.d.ts +2 -1
  153. package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.js +11 -6
  154. package/node_modules/@openclaw/fs-safe/dist/temp-cleanup.js +1 -8
  155. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +1 -1
  156. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +2 -9
  157. package/node_modules/@openclaw/fs-safe/dist/text-atomic.d.ts +1 -6
  158. package/node_modules/@openclaw/fs-safe/dist/trash.js +6 -2
  159. package/node_modules/@openclaw/fs-safe/dist/walk.js +100 -46
  160. package/node_modules/@openclaw/fs-safe/dist/watch-hints.d.ts +7 -0
  161. package/node_modules/@openclaw/fs-safe/dist/watch-hints.js +235 -19
  162. package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +21 -2
  163. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +21 -3
  164. package/node_modules/@openclaw/fs-safe/dist/watch-rescan.d.ts +6 -0
  165. package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +111 -0
  166. package/node_modules/@openclaw/fs-safe/dist/watch-scan.d.ts +13 -0
  167. package/node_modules/@openclaw/fs-safe/dist/watch-scan.js +27 -4
  168. package/node_modules/@openclaw/fs-safe/dist/watch-stream.js +2 -0
  169. package/node_modules/@openclaw/fs-safe/dist/watch.js +131 -40
  170. package/node_modules/@openclaw/fs-safe/dist/windows-path-alias.d.ts +8 -0
  171. package/node_modules/@openclaw/fs-safe/dist/windows-path-alias.js +24 -6
  172. package/node_modules/@openclaw/fs-safe/dist/windows-path-syntax.d.ts +13 -0
  173. package/node_modules/@openclaw/fs-safe/dist/windows-path-syntax.js +51 -0
  174. package/node_modules/@openclaw/fs-safe/docs/advanced.md +4 -0
  175. package/node_modules/@openclaw/fs-safe/docs/atomic.md +4 -1
  176. package/node_modules/@openclaw/fs-safe/docs/config.md +6 -14
  177. package/node_modules/@openclaw/fs-safe/docs/contributing.md +4 -14
  178. package/node_modules/@openclaw/fs-safe/docs/durability.md +6 -29
  179. package/node_modules/@openclaw/fs-safe/docs/entry-publication.md +146 -0
  180. package/node_modules/@openclaw/fs-safe/docs/errors.md +16 -0
  181. package/node_modules/@openclaw/fs-safe/docs/file-store.md +28 -5
  182. package/node_modules/@openclaw/fs-safe/docs/index.md +2 -31
  183. package/node_modules/@openclaw/fs-safe/docs/install.md +10 -31
  184. package/node_modules/@openclaw/fs-safe/docs/json.md +1 -1
  185. package/node_modules/@openclaw/fs-safe/docs/local-roots.md +0 -1
  186. package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +12 -12
  187. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +35 -96
  188. package/node_modules/@openclaw/fs-safe/docs/native.md +22 -20
  189. package/node_modules/@openclaw/fs-safe/docs/path.md +1 -1
  190. package/node_modules/@openclaw/fs-safe/docs/permissions.md +1 -2
  191. package/node_modules/@openclaw/fs-safe/docs/public-api.md +1 -2
  192. package/node_modules/@openclaw/fs-safe/docs/reading.md +1 -2
  193. package/node_modules/@openclaw/fs-safe/docs/root.md +26 -184
  194. package/node_modules/@openclaw/fs-safe/docs/secret-file.md +1 -1
  195. package/node_modules/@openclaw/fs-safe/docs/security-model.md +115 -3
  196. package/node_modules/@openclaw/fs-safe/docs/store.md +2 -2
  197. package/node_modules/@openclaw/fs-safe/docs/temp.md +29 -65
  198. package/node_modules/@openclaw/fs-safe/docs/testing.md +90 -58
  199. package/node_modules/@openclaw/fs-safe/docs/types.md +3 -16
  200. package/node_modules/@openclaw/fs-safe/docs/walk.md +33 -5
  201. package/node_modules/@openclaw/fs-safe/docs/watch.md +96 -14
  202. package/node_modules/@openclaw/fs-safe/docs/writing.md +172 -16
  203. package/node_modules/@openclaw/fs-safe/package.json +10 -10
  204. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  205. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  206. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  207. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  208. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  209. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  210. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  211. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  212. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  213. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  214. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  215. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  216. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
  217. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
  218. package/package.json +5 -5
  219. package/skills/feishu-wiki/SKILL.md +1 -1
  220. package/dist/.setup/accounts-wRqItHug.mjs +0 -206
  221. package/node_modules/@openclaw/fs-safe/dist/watch-alias.d.ts +0 -6
  222. package/node_modules/@openclaw/fs-safe/dist/watch-alias.js +0 -88
  223. package/node_modules/@openclaw/fs-safe/docs/mutation-policy-proof.md +0 -69
  224. package/node_modules/@openclaw/fs-safe/docs/private-file-store.md +0 -68
  225. package/node_modules/@openclaw/fs-safe/docs/test-hooks.md +0 -110
@@ -4,7 +4,7 @@ import { FsSafeError } from "./errors.js";
4
4
  import { formatErrorDetail } from "./error-detail.js";
5
5
  import { isNotFoundPathError, isPathInside, isSymlinkOpenError } from "./path.js";
6
6
  import { realpathSync } from "./realpath.js";
7
- import { assertNoWindowsPathAlias, pathForWindowsFilesystem, resolvePathFromBasePreservingWindowsRoot, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
7
+ import { assertNoWindowsPathAlias, isForeignWindowsShareOrDevicePath, pathForWindowsFilesystem, resolvePathFromBasePreservingWindowsRoot, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
8
8
  import { admitPathInsideRoot } from "./root-boundary.js";
9
9
  import { errorCauseOptions } from "./root-errors.js";
10
10
  export function absolutePathWithRawSegments(candidate) {
@@ -29,6 +29,8 @@ export function rawPathRelativeToCanonicalRoot(candidate, rootCanonicalPath, opt
29
29
  assertNoWindowsPathAlias(rootCanonicalPath);
30
30
  const absolute = absolutePathWithRawSegments(candidate);
31
31
  const raw = process.platform === "win32" ? absolute.replaceAll("/", path.sep) : absolute;
32
+ if (isForeignWindowsShareOrDevicePath(raw, [rootCanonicalPath]))
33
+ return undefined;
32
34
  const filesystemRoot = path.parse(raw).root;
33
35
  const segments = raw.slice(filesystemRoot.length).split(path.sep);
34
36
  const finalComponentIndex = segments.findLastIndex(segment => segment !== "" && segment !== ".");
@@ -50,102 +50,85 @@ export async function statResolvedPathInRoot(root, resolvedPath, receipt) {
50
50
  const proof = {};
51
51
  try {
52
52
  // Nested observations in a hook cannot grant this invocation discard authority.
53
- return await fileObservation().run(() => statObservedPathInRoot(root, resolvedPath, proof, receipt));
54
- }
55
- catch (error) {
56
- const failure = proof.failure;
57
- if (failure && failure.error === error)
58
- recordFileObservationFailure(error, failure.kind);
59
- throw error;
60
- }
61
- }
62
- async function statObservedPathInRoot(root, resolvedPath, proof, receipt) {
63
- let admittedTarget = false;
64
- try {
65
- if (receipt) {
66
- if (receipt.kind !== "stat" || receipt.targetPath !== resolvedPath) {
67
- throw new FsSafeError("path-mismatch", "file observation receipt does not match target");
68
- }
69
- admittedTarget = true;
70
- const beforeObservation = getFsSafeTestHooks()?.beforeRootStatObservation;
71
- if (beforeObservation)
72
- await beforeObservation(resolvedPath);
73
- let observed;
74
- let sample, samples = 0;
75
- const comparison = fileObservation();
53
+ return await fileObservation().run(async () => {
54
+ let admittedTarget = false;
76
55
  try {
77
- observed = comparison.run(() => assertStatObservationSync(bigint => {
56
+ let parentGuard;
57
+ let expected;
58
+ if (receipt) {
59
+ if (receipt.kind !== "stat" || receipt.targetPath !== resolvedPath) {
60
+ throw new FsSafeError("path-mismatch", "file observation receipt does not match target");
61
+ }
62
+ }
63
+ else {
64
+ const guardPath = resolvedPath === root.rootReal ? root.rootReal : path.dirname(resolvedPath);
65
+ parentGuard = guardPath === root.rootReal && root.rootGuard
66
+ ? root.rootGuard
67
+ : await createRootDirectoryObservationGuard(root, guardPath);
68
+ try {
69
+ const beforeInitialObservation = getFsSafeTestHooks()?.beforeRootStatInitialObservation;
70
+ if (beforeInitialObservation)
71
+ await beforeInitialObservation(resolvedPath);
72
+ expected = inspectFileIdentitySync(() => fsSync.lstatSync(resolvedPath, { bigint: true }));
73
+ if (expected.isSymbolicLink()) {
74
+ throw new FsSafeError("path-mismatch", "file changed during operation");
75
+ }
76
+ }
77
+ catch (error) {
78
+ // No initial target error is safe to expose until the admitted parent is
79
+ // proven current; a redirect must win over an ordinary lookup failure.
80
+ await assertRootDirectoryObservationGuard(root, parentGuard);
81
+ throw error;
82
+ }
83
+ }
84
+ admittedTarget = true;
85
+ const beforeObservation = getFsSafeTestHooks()?.beforeRootStatObservation;
86
+ if (beforeObservation)
87
+ await beforeObservation(resolvedPath);
88
+ let observed;
89
+ let sample, samples = 0;
90
+ const comparison = fileObservation();
91
+ const inspect = (bigint = true) => {
78
92
  sample = bigint ? fsSync.lstatSync(resolvedPath, { bigint: true }) : fsSync.lstatSync(resolvedPath);
79
93
  samples += 1;
80
94
  return sample;
81
- }, receipt.target.identity));
95
+ };
96
+ try {
97
+ observed = comparison.run(() => receipt
98
+ ? assertStatObservationSync(inspect, receipt.target.identity)
99
+ : inspectFileIdentitySync(inspect, expected));
100
+ }
101
+ catch (error) {
102
+ if (isNotFoundPathError(error)) {
103
+ throw await missingObservedFileError(error, resolvedPath, receipt ? ("stat" in receipt.target ? receipt.target.stat : undefined) : expected, () => receipt ? assertRootPathObservationReceiptCurrent(root, receipt)
104
+ : assertRootDirectoryObservationGuard(root, parentGuard), proof);
105
+ }
106
+ if (comparison.has(error, "identity"))
107
+ await captureChangedObservedFile(error, resolvedPath, receipt ? ("stat" in receipt.target ? receipt.target.stat : undefined) : expected, samples === 1 ? sample : undefined, () => receipt ? assertRootPathObservationReceiptCurrent(root, receipt)
108
+ : assertRootDirectoryObservationGuard(root, parentGuard), proof);
109
+ throw error;
110
+ }
111
+ if (observed.isSymbolicLink()) {
112
+ throw new FsSafeError("path-mismatch", "file changed during operation");
113
+ }
114
+ if (receipt)
115
+ assertRootPathObservationReceiptCurrent(root, receipt, observed);
116
+ else
117
+ await assertRootDirectoryObservationGuard(root, parentGuard);
118
+ return pathStatFromStats(observed);
82
119
  }
83
120
  catch (error) {
84
- if (isNotFoundPathError(error)) {
85
- throw await missingObservedFileError(error, resolvedPath, "stat" in receipt.target ? receipt.target.stat : undefined, () => assertRootPathObservationReceiptCurrent(root, receipt), proof);
121
+ if (!admittedTarget && isNotFoundPathError(error)) {
122
+ throw fileNotFoundError(error instanceof Error ? error : undefined);
86
123
  }
87
- if (comparison.has(error, "identity"))
88
- await captureChangedObservedFile(error, resolvedPath, "stat" in receipt.target ? receipt.target.stat : undefined, samples === 1 ? sample : undefined, () => assertRootPathObservationReceiptCurrent(root, receipt), proof);
89
124
  throw error;
90
125
  }
91
- if (observed.isSymbolicLink()) {
92
- throw new FsSafeError("path-mismatch", "file changed during operation");
93
- }
94
- assertRootPathObservationReceiptCurrent(root, receipt, observed);
95
- return pathStatFromStats(observed);
96
- }
97
- const guardPath = resolvedPath === root.rootReal ? root.rootReal : path.dirname(resolvedPath);
98
- const parentGuard = guardPath === root.rootReal && root.rootGuard
99
- ? root.rootGuard
100
- : await createRootDirectoryObservationGuard(root, guardPath);
101
- let expected;
102
- try {
103
- const beforeInitialObservation = getFsSafeTestHooks()?.beforeRootStatInitialObservation;
104
- if (beforeInitialObservation)
105
- await beforeInitialObservation(resolvedPath);
106
- expected = inspectFileIdentitySync(() => fsSync.lstatSync(resolvedPath, { bigint: true }));
107
- if (expected.isSymbolicLink()) {
108
- throw new FsSafeError("path-mismatch", "file changed during operation");
109
- }
110
- }
111
- catch (error) {
112
- // No initial target error is safe to expose until the admitted parent is
113
- // proven current; a redirect must win over an ordinary lookup failure.
114
- await assertRootDirectoryObservationGuard(root, parentGuard);
115
- throw error;
116
- }
117
- admittedTarget = true;
118
- const beforeObservation = getFsSafeTestHooks()?.beforeRootStatObservation;
119
- if (beforeObservation)
120
- await beforeObservation(resolvedPath);
121
- let observed;
122
- let sample, samples = 0;
123
- const comparison = fileObservation();
124
- try {
125
- observed = comparison.run(() => inspectFileIdentitySync(() => {
126
- sample = fsSync.lstatSync(resolvedPath, { bigint: true });
127
- samples += 1;
128
- return sample;
129
- }, expected));
130
- }
131
- catch (error) {
132
- if (isNotFoundPathError(error)) {
133
- throw await missingObservedFileError(error, resolvedPath, expected, () => assertRootDirectoryObservationGuard(root, parentGuard), proof);
134
- }
135
- if (comparison.has(error, "identity"))
136
- await captureChangedObservedFile(error, resolvedPath, expected, samples === 1 ? sample : undefined, () => assertRootDirectoryObservationGuard(root, parentGuard), proof);
137
- throw error;
138
- }
139
- if (observed.isSymbolicLink()) {
140
- throw new FsSafeError("path-mismatch", "file changed during operation");
141
- }
142
- await assertRootDirectoryObservationGuard(root, parentGuard);
143
- return pathStatFromStats(observed);
126
+ });
144
127
  }
145
128
  catch (error) {
146
- if (!admittedTarget && isNotFoundPathError(error)) {
147
- throw fileNotFoundError(error instanceof Error ? error : undefined);
148
- }
129
+ const failure = proof.failure;
130
+ if (failure && failure.error === error)
131
+ recordFileObservationFailure(error, failure.kind);
149
132
  throw error;
150
133
  }
151
134
  }
@@ -9,7 +9,7 @@ import { absolutePathWithRawSegments, rawPathRelativeToCanonicalRoot, resolveExi
9
9
  import { admitPathInsideRoot, } from "./root-boundary.js";
10
10
  import { RootPathObservationError, captureRootPathObservedDirectory, createRootPathTraversalObservation, inspectRootPathTraversalEntry, } from "./root-path-observation.js";
11
11
  import { assertNoDriveRelativePathSegments } from "./safe-path-segment.js";
12
- import { assertNoWindowsPathAlias, pathForWindowsFilesystem, resolvePathFromBasePreservingWindowsRoot, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
12
+ import { assertNoWindowsPathAlias, isForeignWindowsShareOrDevicePath, pathForWindowsFilesystem, resolvePathFromBasePreservingWindowsRoot, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
13
13
  export { resolvePathViaExistingAncestorSync } from "./root-path-existing.js";
14
14
  export { RootPathObservationError, } from "./root-path-observation.js";
15
15
  export const ROOT_PATH_ALIAS_POLICIES = {
@@ -55,6 +55,11 @@ function resolveRootPathInternal(params, options = {}) {
55
55
  function prepareRootTraversal(params, rootPath, rootCanonicalPath, absolutePath, rawAbsolutePath) {
56
56
  let raw = rawAbsolutePath;
57
57
  let trustedAbsolutePath = false;
58
+ // Even a metadata lookup on a foreign share contacts that host, including the
59
+ // identity check behind a case-folded root prefix.
60
+ if (isForeignWindowsShareOrDevicePath(raw, [rootPath, rootCanonicalPath])) {
61
+ throw pathEscapeError({ rootPath, absolutePath: raw, boundaryLabel: params.boundaryLabel });
62
+ }
58
63
  const direct = admitRawPathInsideRoot(rootPath, raw, params.rootIdentity);
59
64
  if (direct) {
60
65
  raw = direct.path;
@@ -1,4 +1,8 @@
1
1
  import { type ResolvePathWithinRootParams } from "./path-scope-lexical.js";
2
+ export declare function invalidPath(scopeLabel: string): {
3
+ ok: false;
4
+ error: string;
5
+ };
2
6
  export declare function resolvePathWithinRoot(params: ResolvePathWithinRootParams): {
3
7
  ok: true;
4
8
  path: string;
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { resolvePathWithinNormalizedRoot } from "./path-scope-lexical.js";
3
3
  import { hasWindowsPathAlias, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
4
- function invalidPath(scopeLabel) {
4
+ export function invalidPath(scopeLabel) {
5
5
  return { ok: false, error: `Invalid path: must stay within ${scopeLabel}` };
6
6
  }
7
7
  export function resolvePathWithinRoot(params) {
@@ -4,18 +4,12 @@ import { FsSafeError } from "./errors.js";
4
4
  import { isNotFoundPathError, isPathInside, } from "./path.js";
5
5
  import { realpathSync } from "./realpath.js";
6
6
  import { root as openRoot } from "./root.js";
7
- import { hasWindowsPathAlias, pathForWindowsFilesystem, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
8
- import { resolvePathWithinRoot } from "./root-paths-lexical.js";
7
+ import { hasWindowsPathAlias, isForeignWindowsShareOrDevicePath, pathForWindowsFilesystem, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
8
+ import { invalidPath, resolvePathWithinRoot } from "./root-paths-lexical.js";
9
9
  import { resolvePathWithinNormalizedRoot } from "./path-scope-lexical.js";
10
10
  import { assertNoSymlinkSegments, ensureDirectoryWithinRoot, resolveNearestExistingPath, } from "./root-directory.js";
11
11
  export { resolvePathWithinRoot } from "./root-paths-lexical.js";
12
12
  export { ensureDirectoryWithinRoot } from "./root-directory.js";
13
- function invalidPath(scopeLabel) {
14
- return {
15
- ok: false,
16
- error: `Invalid path: must stay within ${scopeLabel}`,
17
- };
18
- }
19
13
  const INVALID_REAL_PATH = Symbol("invalid-real-path");
20
14
  async function resolveRealPathIfExists(targetPath) {
21
15
  if (hasWindowsPathAlias(targetPath, "filesystem"))
@@ -216,7 +210,8 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
216
210
  if (!rootRealPath ||
217
211
  !raw ||
218
212
  !path.isAbsolute(raw) ||
219
- hasWindowsPathAlias(raw, "filesystem")) {
213
+ hasWindowsPathAlias(raw, "filesystem") ||
214
+ isForeignWindowsShareOrDevicePath(raw, [rootDir, rootRealPath])) {
220
215
  return lexicalPathResult;
221
216
  }
222
217
  try {
@@ -0,0 +1,4 @@
1
+ import { type RootContext } from "./root-context.js";
2
+ import { type InternalRemoveOptions } from "./root-remove.js";
3
+ import type { RemovalPathReceipts } from "./root-remove-receipt.js";
4
+ export declare function removePathInRootNative(root: RootContext, target: string, options: InternalRemoveOptions, receipts?: RemovalPathReceipts): Promise<void>;
@@ -0,0 +1,311 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { scheduler } from "node:timers/promises";
4
+ import { assertSyncDirectoryGuard } from "./directory-guard.js";
5
+ import { assertMutationNotDenied } from "./deny-mutations.js";
6
+ import { FsSafeError } from "./errors.js";
7
+ import { MutationAuthorityError } from "./mutation-authority.js";
8
+ import { getNativeBinding } from "./native.js";
9
+ import { openNativeParentAdmission, openNativeRootAdmission } from "./native-parent-admission.js";
10
+ import { isNotFoundPathError, isPathInside } from "./path.js";
11
+ import { assertRootIdentityCurrentSync } from "./root-context.js";
12
+ import { normalizeRemoveGuardError, normalizeRemovePathError } from "./root-errors.js";
13
+ import { captureNonrecursiveRemovalAdmission, nonrecursiveRemovalKind } from "./root-remove.js";
14
+ import { createSuppressedError } from "./suppressed-error.js";
15
+ import { getFsSafeTestHooks } from "./test-hooks.js";
16
+ function unavailable() {
17
+ throw new FsSafeError("helper-unavailable", "native confined removal is unavailable on this platform");
18
+ }
19
+ function normalize(error, details) {
20
+ if (error instanceof FsSafeError || error instanceof MutationAuthorityError ||
21
+ (error instanceof Error && error.name === "SuppressedError"))
22
+ return error;
23
+ const code = error?.code;
24
+ if (code === "path-mismatch")
25
+ return new FsSafeError("path-mismatch", "removal identity changed", { cause: error, details });
26
+ if (["ENOTSUP", "ENOSYS", "EOPNOTSUPP"].includes(code ?? "")) {
27
+ return new FsSafeError("helper-unavailable", "native confined removal is unavailable", { cause: error, details });
28
+ }
29
+ return normalizeRemovePathError(error, details);
30
+ }
31
+ export async function removePathInRootNative(root, target, options, receipts) {
32
+ const binding = getNativeBinding();
33
+ if (!binding?.rootRemovalStat || !binding.rootRemovalUnlink) {
34
+ unavailable();
35
+ }
36
+ const inspect = binding.rootRemovalStat.bind(binding);
37
+ const unlink = binding.rootRemovalUnlink.bind(binding);
38
+ const openDirectory = binding.openRootRemovalDirectory?.bind(binding);
39
+ const boundary = await captureNonrecursiveRemovalAdmission(root, target, options, receipts);
40
+ if (!boundary)
41
+ return;
42
+ const rootAdmission = await openNativeRootAdmission(binding, {
43
+ rootPath: root.rootReal, rootIdentity: root.rootIdentity, operation: "remove", reportCloseErrors: true, searchOnly: true,
44
+ }).catch(error => { throw normalizeRemoveGuardError(error); });
45
+ let parent;
46
+ let initialEntry;
47
+ let initialDirectory;
48
+ let initialDirectoryAttempted = false;
49
+ let operationError;
50
+ let failed = false;
51
+ try {
52
+ const parentPath = path.dirname(target);
53
+ if (!isPathInside(root.rootReal, parentPath))
54
+ throw new FsSafeError("outside-workspace", "removal parent is outside root");
55
+ try {
56
+ parent = await openNativeParentAdmission(binding, rootAdmission, path.relative(root.rootReal, parentPath).split(path.sep).join("/"));
57
+ }
58
+ catch (error) {
59
+ assertRootIdentityCurrentSync(root);
60
+ boundary.assertCurrent();
61
+ if (!(options.force && isNotFoundPathError(error)))
62
+ throw error;
63
+ }
64
+ if (parent) {
65
+ // Bind the native descriptor to the parent admitted before opening it;
66
+ // an in-root redirect must not inherit another directory's approval.
67
+ if (parent.guard.stat.dev !== boundary.parentIdentity.dev || parent.guard.stat.ino !== boundary.parentIdentity.ino) {
68
+ throw new FsSafeError("path-mismatch", "removal parent identity changed");
69
+ }
70
+ if (options.recursive) {
71
+ boundary.assertCurrent();
72
+ try {
73
+ initialEntry = inspect(parent.fd, path.basename(target));
74
+ }
75
+ catch (error) {
76
+ boundary.assertCurrent();
77
+ if (!(options.force && isNotFoundPathError(error)))
78
+ throw error;
79
+ }
80
+ if (initialEntry?.directory) {
81
+ if (!openDirectory)
82
+ unavailable();
83
+ initialDirectoryAttempted = true;
84
+ try {
85
+ initialDirectory = openDirectory(parent.fd, path.basename(target), initialEntry.dev, initialEntry.ino);
86
+ }
87
+ catch (error) {
88
+ boundary.assertCurrent();
89
+ const code = error?.code;
90
+ if (["ENOTSUP", "ENOSYS", "EOPNOTSUPP"].includes(code ?? ""))
91
+ unavailable();
92
+ else if (!(options.force && isNotFoundPathError(error))) {
93
+ throw normalize(error, { operation: "remove", phase: "enumerate", relativePath: "" });
94
+ }
95
+ }
96
+ }
97
+ }
98
+ if (!options.recursive || initialEntry)
99
+ await removeEntries(parent);
100
+ }
101
+ }
102
+ catch (error) {
103
+ failed = true;
104
+ operationError = error instanceof MutationAuthorityError ? error : normalize(error);
105
+ }
106
+ const closeErrors = [];
107
+ try {
108
+ initialDirectory?.close();
109
+ }
110
+ catch (error) {
111
+ closeErrors.push(error);
112
+ }
113
+ try {
114
+ parent?.close();
115
+ }
116
+ catch (error) {
117
+ closeErrors.push(error);
118
+ }
119
+ try {
120
+ await rootAdmission.root.close();
121
+ }
122
+ catch (error) {
123
+ closeErrors.push(error);
124
+ }
125
+ if (closeErrors.length) {
126
+ const error = closeErrors.length === 1 ? closeErrors[0] : new AggregateError(closeErrors, "removal descriptors could not close");
127
+ if (failed)
128
+ throw createSuppressedError(error, operationError instanceof MutationAuthorityError ? operationError.rejection : operationError, "removal and descriptor close failed");
129
+ throw error;
130
+ }
131
+ if (failed)
132
+ throw operationError;
133
+ async function removeEntries(admitted) {
134
+ // The admission already checks Root through the retained parent. This list
135
+ // adds only traversal descendants, avoiding duplicate ancestry fences.
136
+ const guards = [];
137
+ const maxEntries = options.maxEntries ?? 100_000;
138
+ const maxDepth = options.maxDepth ?? 64;
139
+ let examined = 0;
140
+ const details = (entryPath, phase) => ({
141
+ operation: "remove", relativePath: path.relative(target, entryPath), phase,
142
+ });
143
+ async function beforeMutation(entryPath) {
144
+ try {
145
+ await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("remove", entryPath);
146
+ }
147
+ catch (error) {
148
+ throw normalizeRemoveGuardError(error);
149
+ }
150
+ }
151
+ function assertNotAborted() {
152
+ try {
153
+ options.signal?.throwIfAborted();
154
+ }
155
+ catch (error) {
156
+ throw new MutationAuthorityError(error);
157
+ }
158
+ }
159
+ function assertTraversalCurrent() {
160
+ for (const guard of guards) {
161
+ try {
162
+ assertSyncDirectoryGuard(guard);
163
+ }
164
+ catch (cause) {
165
+ throw new FsSafeError("path-mismatch", "removal ancestor changed", { cause });
166
+ }
167
+ }
168
+ }
169
+ function assertCurrent() {
170
+ assertNotAborted();
171
+ boundary.assertCurrent();
172
+ assertTraversalCurrent();
173
+ }
174
+ function observe(fd, name, entryPath, expected) {
175
+ assertCurrent();
176
+ try {
177
+ const entry = inspect(fd, name);
178
+ if (expected && (entry.dev !== expected.dev || entry.ino !== expected.ino || entry.directory !== expected.directory)) {
179
+ throw new FsSafeError("path-mismatch", "removal entry changed");
180
+ }
181
+ if (entry.symlink && options.mutationSymlinks !== undefined) {
182
+ throw new FsSafeError("symlink", "symlink not allowed", { details: details(entryPath, expected ? "remove" : "inspect") });
183
+ }
184
+ return entry;
185
+ }
186
+ catch (error) {
187
+ if (options.force && isNotFoundPathError(error))
188
+ return undefined;
189
+ throw normalize(error, details(entryPath, expected ? "remove" : "inspect"));
190
+ }
191
+ }
192
+ async function visit(fd, name, entryPath, depth, counted = false) {
193
+ assertNotAborted();
194
+ if (options.recursive && ((!counted && examined >= maxEntries) || depth > maxDepth)) {
195
+ throw new FsSafeError("too-large", "recursive removal budget exceeded", { details: details(entryPath, "inspect") });
196
+ }
197
+ if (!counted)
198
+ examined++;
199
+ if (!options.recursive)
200
+ await beforeMutation(entryPath);
201
+ await assertMutationNotDenied(entryPath, options.denyMutations, { protectAncestors: true });
202
+ const initial = observe(fd, name, entryPath, depth === 0 ? initialEntry : undefined);
203
+ if (!initial)
204
+ return;
205
+ if (!initial.directory && options[nonrecursiveRemovalKind] === "directory") {
206
+ throw new FsSafeError("path-mismatch", "store entry is no longer a directory");
207
+ }
208
+ if (initial.directory && options.recursive) {
209
+ let directory;
210
+ try {
211
+ if (depth === 0 && initialDirectoryAttempted) {
212
+ directory = initialDirectory;
213
+ initialDirectory = undefined;
214
+ }
215
+ else
216
+ directory = openDirectory(fd, name, initial.dev, initial.ino);
217
+ }
218
+ catch (error) {
219
+ assertCurrent();
220
+ if (!(options.force && isNotFoundPathError(error)))
221
+ throw normalize(error, details(entryPath, "enumerate"));
222
+ if (!observe(fd, name, entryPath, initial))
223
+ return;
224
+ }
225
+ if (directory) {
226
+ let failure;
227
+ try {
228
+ const stat = fs.fstatSync(directory.fd, { bigint: true });
229
+ const read = () => {
230
+ try {
231
+ return directory.read();
232
+ }
233
+ catch (error) {
234
+ throw normalize(error, details(entryPath, "enumerate"));
235
+ }
236
+ };
237
+ guards.push({ dir: entryPath, realPath: entryPath, stat });
238
+ try {
239
+ assertCurrent();
240
+ if (options.order === "sorted") {
241
+ const names = [];
242
+ for (let child = read(); child !== null; child = read()) {
243
+ await scheduler.yield();
244
+ assertCurrent();
245
+ if (examined >= maxEntries)
246
+ throw new FsSafeError("too-large", "recursive removal budget exceeded", {
247
+ details: details(path.join(entryPath, child), "enumerate"),
248
+ });
249
+ examined++;
250
+ names.push(child);
251
+ }
252
+ for (const child of names.sort())
253
+ await visit(directory.fd, child, path.join(entryPath, child), depth + 1, true);
254
+ }
255
+ else {
256
+ for (let child = read(); child !== null; child = read()) {
257
+ await scheduler.yield();
258
+ await visit(directory.fd, child, path.join(entryPath, child), depth + 1);
259
+ }
260
+ }
261
+ }
262
+ finally {
263
+ guards.pop();
264
+ }
265
+ }
266
+ catch (error) {
267
+ failure = { error: error instanceof MutationAuthorityError ? error.rejection : error };
268
+ throw error;
269
+ }
270
+ finally {
271
+ try {
272
+ directory.close();
273
+ }
274
+ catch (error) {
275
+ const closeError = normalize(error, details(entryPath, "enumerate"));
276
+ if (failure)
277
+ throw createSuppressedError(closeError, failure.error, "removal and directory close failed");
278
+ throw closeError;
279
+ }
280
+ }
281
+ }
282
+ }
283
+ if (options.recursive) {
284
+ await beforeMutation(entryPath);
285
+ await assertMutationNotDenied(entryPath, options.denyMutations, { protectAncestors: true });
286
+ if (!observe(fd, name, entryPath, initial))
287
+ return;
288
+ }
289
+ if (options.assertBeforeMutation) {
290
+ options.assertBeforeMutation();
291
+ if (!observe(fd, name, entryPath, initial))
292
+ return;
293
+ }
294
+ // No await separates the final admission from native identity-checked unlink.
295
+ try {
296
+ unlink(fd, name, initial.dev, initial.ino, initial.directory);
297
+ }
298
+ catch (error) {
299
+ if (!(options.force && isNotFoundPathError(error)))
300
+ throw normalize(error, details(entryPath, "remove"));
301
+ }
302
+ if (options.recursive)
303
+ boundary.assertCurrent();
304
+ else
305
+ boundary.assertAfterMutation();
306
+ assertTraversalCurrent();
307
+ assertNotAborted();
308
+ }
309
+ await visit(admitted.fd, path.basename(target), target, 0);
310
+ }
311
+ }
@@ -1,6 +1,17 @@
1
+ import type { BigIntStats } from "node:fs";
1
2
  import { type RootContext } from "./root-context.js";
2
3
  import type { RootRemoveOptions } from "./root-options.js";
3
4
  import type { RemovalPathReceipts } from "./root-remove-receipt.js";
4
5
  export declare const nonrecursiveRemovalKind: unique symbol;
6
+ export type InternalRemoveOptions = RootRemoveOptions & {
7
+ [nonrecursiveRemovalKind]?: "directory";
8
+ };
5
9
  export declare function validateRemoveOptions(options: RootRemoveOptions): void;
10
+ type NonrecursiveRemovalAdmission = Readonly<{
11
+ parentIdentity: Readonly<Pick<BigIntStats, "dev" | "ino">>;
12
+ assertAfterMutation(): void;
13
+ assertCurrent(): void;
14
+ }>;
15
+ export declare function captureNonrecursiveRemovalAdmission(root: RootContext, targetPath: string, options: RootRemoveOptions, receipts?: RemovalPathReceipts): Promise<NonrecursiveRemovalAdmission | undefined>;
6
16
  export declare function removePathInRootFallback(root: RootContext, targetPath: string, options: RootRemoveOptions, receipts?: RemovalPathReceipts): Promise<void>;
17
+ export {};
@@ -80,7 +80,7 @@ async function assertMissingRemovalPrefixCurrent(rootGuard, intermediates) {
80
80
  throw removalAncestorChanged();
81
81
  }
82
82
  }
83
- async function captureNonrecursiveRemovalAdmission(root, targetPath, options, receipts) {
83
+ export async function captureNonrecursiveRemovalAdmission(root, targetPath, options, receipts) {
84
84
  const parentPath = path.dirname(targetPath);
85
85
  if (!isPathInside(root.rootReal, parentPath)) {
86
86
  throw new FsSafeError("path-mismatch", "removal parent is outside the retained root");
@@ -105,6 +105,7 @@ async function captureNonrecursiveRemovalAdmission(root, targetPath, options, re
105
105
  if (parentPath === root.rootReal) {
106
106
  const rootAssertion = createRemovalDirectoryAssertion(rootGuard.dir, rootGuard.stat, rootGuard.realPath);
107
107
  return Object.freeze({
108
+ parentIdentity: rootAssertion,
108
109
  assertAfterMutation() {
109
110
  // Here Root is also the immediate parent, so retain its established
110
111
  // post-dispatch error classification while checking it only once.
@@ -187,6 +188,7 @@ async function captureNonrecursiveRemovalAdmission(root, targetPath, options, re
187
188
  }
188
189
  };
189
190
  return Object.freeze({
191
+ parentIdentity: parentAssertion,
190
192
  assertAfterMutation() {
191
193
  assertPrefixCurrent();
192
194
  // Preserve the established nonrecursive post-dispatch mapping for the
@@ -1,6 +1,6 @@
1
1
  import type { DirectoryObservationGuard } from "./directory-guard.js";
2
2
  import { type RootPathObservationReceipt } from "./root-path.js";
3
- import type { RootDirectoryListing, RootDirectoryListingOptions } from "./root-directory-list.js";
3
+ import type { RootDirectoryListing, RootDirectoryListingOptions } from "./root-directory-list-types.js";
4
4
  import type { PathStat } from "./types.js";
5
5
  export type RootWalkSymlinkPolicy = "skip" | "follow-within-root" | "include";
6
6
  type LegacyRootWalkSymlinkPolicy = Exclude<RootWalkSymlinkPolicy, "include">;
@@ -40,7 +40,7 @@ type AnyRootWalkOptions = RootWalkOptions | RootWalkOptions<"include"> | RootWal
40
40
  type AnyRootWalkEntry = RootWalkEntry<RootWalkSymlinkPolicy>;
41
41
  type RootWalkCapability = {
42
42
  rootReal: string;
43
- observeRoot(): Promise<DirectoryObservationGuard | undefined>;
43
+ observeRoot(): Promise<DirectoryObservationGuard>;
44
44
  stat(relativePath: string): Promise<PathStat>;
45
45
  list(relativePath: string, options: RootDirectoryListingOptions, receipt?: RootPathObservationReceipt): Promise<RootDirectoryListing>;
46
46
  };
@@ -87,9 +87,6 @@ export async function* walkRoot(root, relativePath, options) {
87
87
  let resolvedDirectory;
88
88
  if (includeChild) {
89
89
  const rootGuard = await root.observeRoot();
90
- if (!rootGuard) {
91
- throw new FsSafeError("helper-unavailable", "root walk requires an exact directory observation");
92
- }
93
90
  const observed = await resolveRootPathWithObservation({
94
91
  ...resolution,
95
92
  rootIdentity: rootGuard.identity,
@@ -192,6 +189,7 @@ export async function* walkRoot(root, relativePath, options) {
192
189
  return;
193
190
  }
194
191
  }
192
+ options.signal?.throwIfAborted();
195
193
  if (!["include", "skip", "skip-subtree"].includes(filterResult)) {
196
194
  throw new TypeError(`invalid root walk entryFilter result: ${String(filterResult)}`);
197
195
  }