@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
@@ -0,0 +1,111 @@
1
+ import path from "node:path";
2
+ const parent = (name) => path.dirname(name) === "." ? "" : path.dirname(name);
3
+ const below = (directory, name) => !directory || name.startsWith(directory + path.sep);
4
+ const identity = (fingerprint) => fingerprint?.split(":").slice(0, 3).join(":");
5
+ /** Only previously enumerated directories can be reconciled without discovering topology. */
6
+ export function watchRescanScopes(scopes, snapshot, changes) {
7
+ if (scopes.some((scope, index) => scope.kind === "tree" && scope.depth > 0 && scopes.some((other, next) => index !== next && (scope.path === other.path || below(scope.path, other.path)))))
8
+ return;
9
+ const directories = new Set();
10
+ for (const change of changes) {
11
+ // A directory hint can cover descendant edits and new native registrations.
12
+ if (snapshot.listed?.has(change.path))
13
+ return undefined;
14
+ if (scopes.some(scope => scope.path === change.path && scope.kind === "tree" && scope.depth > 0))
15
+ return undefined;
16
+ if (scopes.some(scope => scope.path === change.path))
17
+ continue;
18
+ const directory = parent(change.path);
19
+ if (!snapshot.listed?.has(directory))
20
+ return undefined;
21
+ directories.add(directory);
22
+ }
23
+ return [
24
+ ...scopes.map(scope => ({ ...scope, depth: 0 })),
25
+ ...[...directories].map(name => ({ path: name, kind: "tree", depth: 1 })),
26
+ ];
27
+ }
28
+ /** Merge only freshly observed slices. Any topology/anchor ambiguity requires a full pass. */
29
+ export function mergeWatchRescan(scopes, before, slice, requested) {
30
+ if (slice.overflow || slice.structural?.size)
31
+ return;
32
+ // An exclusion or a blocked path may stop before a requested directory is listed.
33
+ for (const scope of requested)
34
+ if (scope.kind === "tree" && scope.depth > 0 && !slice.listed?.has(scope.path))
35
+ return;
36
+ for (const [name, current] of slice.directories) {
37
+ const prior = before.directories.get(name);
38
+ if (!prior || prior.dev !== current.dev || prior.ino !== current.ino)
39
+ return;
40
+ }
41
+ for (const scope of scopes) {
42
+ if (identity(before.entries.get(scope.path)) !== identity(slice.entries.get(scope.path)))
43
+ return;
44
+ // A missing/blocked intermediate scope must not retain old descendant registrations.
45
+ for (const name of before.directories.keys())
46
+ if ((name === scope.path || below(name, scope.path)) &&
47
+ !slice.directories.has(name) && name !== scope.path)
48
+ return;
49
+ }
50
+ const directories = slice.listed;
51
+ const selected = new Set(scopes.map(scope => scope.path));
52
+ for (const directory of directories.keys()) {
53
+ for (const name of before.childPaths?.get(directory)?.values() ?? [])
54
+ selected.add(name);
55
+ for (const name of slice.childPaths?.get(directory)?.values() ?? [])
56
+ selected.add(name);
57
+ }
58
+ for (const [name, previous] of before.entries) {
59
+ if (!selected.has(name))
60
+ continue;
61
+ const current = slice.entries.get(name);
62
+ if ((previous.startsWith("directory:") || current?.startsWith("directory:")) && identity(previous) !== identity(current))
63
+ return;
64
+ }
65
+ for (const [name, current] of slice.entries) {
66
+ if (selected.has(name) && current.startsWith("directory:") && identity(current) !== identity(before.entries.get(name)))
67
+ return;
68
+ }
69
+ // Exclusion changes can retire or admit a subtree, including callback-driven changes.
70
+ for (const [name, kind] of before.excluded ?? [])
71
+ if (selected.has(name) && kind === "directory" && slice.entries.has(name))
72
+ return;
73
+ const entries = new Map(before.entries);
74
+ for (const name of selected)
75
+ if (!slice.entries.has(name))
76
+ entries.delete(name);
77
+ for (const [name, value] of slice.entries)
78
+ if (selected.has(name))
79
+ entries.set(name, value);
80
+ const excluded = new Map(before.excluded), excludedDirectories = new Map(before.excludedDirectories);
81
+ for (const name of selected) {
82
+ excluded.delete(name);
83
+ excludedDirectories.delete(name);
84
+ }
85
+ for (const [name, kind] of slice.excluded ?? [])
86
+ if (selected.has(name)) {
87
+ excluded.set(name, kind);
88
+ const canonical = slice.excludedDirectories?.get(name);
89
+ if (canonical)
90
+ excludedDirectories.set(name, canonical);
91
+ }
92
+ const listed = new Map(before.listed);
93
+ let scanned = before.scanned;
94
+ for (const [name, count] of directories) {
95
+ scanned += count - (listed.get(name) ?? 0);
96
+ listed.set(name, count);
97
+ }
98
+ return { ...before, entries, excluded, excludedDirectories, listed, scanned,
99
+ entryAnchors: slice.entryAnchors,
100
+ scopeAnchors: new Map(scopes.flatMap(scope => {
101
+ const anchor = slice.scopeAnchors?.get(scope.path);
102
+ return anchor ? [[scope.path, anchor]] : [];
103
+ })),
104
+ directoryPaths: new Map([...before.directoryPaths ?? [], ...slice.directoryPaths ?? []]),
105
+ childPaths: new Map([...before.childPaths ?? [], ...slice.childPaths ?? []]),
106
+ listingPaths: new Map([...before.listingPaths ?? [], ...slice.listingPaths ?? []]),
107
+ targets: new Map(scopes.flatMap(scope => {
108
+ const target = slice.targets.get(scope.path);
109
+ return target ? [[scope.path, target]] : [];
110
+ })), structural: slice.structural, overflow: false, };
111
+ }
@@ -1,12 +1,21 @@
1
1
  import { type RootContext } from "./root-context.js";
2
2
  import { type RootDirectoryObservationGuard } from "./root-directory-list.js";
3
+ import type { RootDirectoryListingPaths } from "./root-directory-list-types.js";
3
4
  import type { WatchEntry, WatchOptions, WatchScope } from "./watch-types.js";
4
5
  export type DirectoryIdentity = Readonly<{
5
6
  dev: bigint;
6
7
  ino: bigint;
7
8
  }>;
9
+ export type WatchEntryAnchor = {
10
+ directory: string;
11
+ name: string;
12
+ target?: DirectoryIdentity & {
13
+ kind: WatchEntry["kind"];
14
+ };
15
+ };
8
16
  export type WatchSnapshot = {
9
17
  entries: Map<string, string>;
18
+ entryAnchors?: Map<string, WatchEntryAnchor>;
10
19
  excluded?: Map<string, WatchEntry["kind"]>;
11
20
  excludedDirectories?: Map<string, string>;
12
21
  directoryPaths?: Map<string, string>;
@@ -15,6 +24,10 @@ export type WatchSnapshot = {
15
24
  scanned: number;
16
25
  structural?: Set<string>;
17
26
  overflow?: boolean;
27
+ scopeAnchors?: Map<string, WatchEntryAnchor>;
28
+ listed?: Map<string, number>;
29
+ childPaths?: Map<string, Map<string, string>>;
30
+ listingPaths?: Map<string, RootDirectoryListingPaths>;
18
31
  };
19
32
  export declare function watchScopes(input: readonly WatchScope[]): readonly WatchScope[];
20
33
  /** A descendant's unavailable metadata never grants it authority or retires the Root. */
@@ -57,7 +57,10 @@ export function isWatchPathError(error) {
57
57
  export async function scanWatch(root, scopes, options, signal, register, onCleanupFailure) {
58
58
  const exclusions = new Map();
59
59
  const excludedDirectories = new Map(), directoryPaths = new Map();
60
- const result = { entries: new Map(), excluded: exclusions, excludedDirectories, directoryPaths, directories: new Map(), targets: new Map(), scanned: 0 };
60
+ const entryAnchors = new Map();
61
+ const scopeAnchors = new Map();
62
+ const listed = new Map(), childPaths = new Map(), listingPaths = new Map();
63
+ const result = { entries: new Map(), entryAnchors, scopeAnchors, excluded: exclusions, excludedDirectories, directoryPaths, directories: new Map(), targets: new Map(), scanned: 0, listed, childPaths, listingPaths };
61
64
  const attempts = new Map();
62
65
  const guards = new Map();
63
66
  const walked = new Map();
@@ -70,7 +73,11 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
70
73
  result.overflow = true;
71
74
  // Discard partially observed names when their enclosing directory lost admission.
72
75
  const below = (name) => name === relative || !relative || name.startsWith(relative + path.sep);
73
- for (const map of [result.entries, exclusions, excludedDirectories, directoryPaths, result.targets, result.directories, guards, walked]) {
76
+ for (const anchors of [entryAnchors, scopeAnchors])
77
+ for (const [scope, anchor] of anchors)
78
+ if (below(anchor.directory))
79
+ anchors.delete(scope);
80
+ for (const map of [result.entries, exclusions, excludedDirectories, directoryPaths, result.targets, result.directories, guards, walked, listed, childPaths, listingPaths]) {
74
81
  for (const name of map.keys())
75
82
  if (below(name))
76
83
  map.delete(name);
@@ -167,8 +174,11 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
167
174
  for (let attempt = 0; attempt < 3; attempt++) {
168
175
  try {
169
176
  guard = await directory(relative);
177
+ listed.set(relative, 0);
170
178
  listing = await openRootDirectoryListing(root, guard.realPath, {
171
- order: "filesystem", snapshot: false, signal, exactIdentity: true, skipVanished: true, onCleanupFailure, admitEntry: () => { examined(); return true; },
179
+ order: "filesystem", snapshot: false, signal, exactIdentity: true, skipVanished: true, onCleanupFailure,
180
+ previousPaths: options.previous?.listingPaths?.get(relative),
181
+ admitEntry: () => { examined(); listed.set(relative, (listed.get(relative) ?? 0) + 1); return true; },
172
182
  });
173
183
  // The listing has its own guard. Both identities must agree before reading names.
174
184
  await assertRootDirectoryObservationGuard(root, guard);
@@ -182,6 +192,10 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
182
192
  }
183
193
  if (!listing)
184
194
  return;
195
+ listingPaths.set(relative, listing.paths);
196
+ const names = new Map();
197
+ childPaths.set(relative, names);
198
+ const previousNames = options.previous?.childPaths?.get(relative);
185
199
  let failed = false;
186
200
  let operationError;
187
201
  try {
@@ -195,7 +209,8 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
195
209
  if (!next.identity)
196
210
  throw new FsSafeError("path-mismatch", "watch listing lacks exact identity");
197
211
  const entry = next.entry;
198
- const name = relative ? path.join(relative, entry.name) : entry.name;
212
+ const name = previousNames?.get(entry.name) ?? (relative ? path.join(relative, entry.name) : entry.name);
213
+ names.set(entry.name, name);
199
214
  if (await excluded(name, entry))
200
215
  continue;
201
216
  result.entries.set(name, fingerprint(entry, next.identity));
@@ -231,6 +246,9 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
231
246
  signal.throwIfAborted();
232
247
  if (!scope.path) {
233
248
  const guard = await directory("");
249
+ scopeAnchors.set(scope.path, { directory: "", name: "" });
250
+ if (scope.kind === "entry")
251
+ entryAnchors.set(scope.path, { directory: "", name: "" });
234
252
  result.entries.set("", fingerprint({ name: "", ...pathStatFromStats(guard.stat) }, guard.stat));
235
253
  if (scope.kind === "tree" && scope.depth > 0)
236
254
  await tree("", scope.depth);
@@ -241,6 +259,10 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
241
259
  try {
242
260
  for (let i = 0; i < segments.length; i++) {
243
261
  const guard = await directory(relative);
262
+ const anchor = { directory: relative, name: segments[i] };
263
+ scopeAnchors.set(scope.path, anchor);
264
+ if (scope.kind === "entry")
265
+ entryAnchors.set(scope.path, anchor);
244
266
  examined();
245
267
  // Filesystem lookup, not lowercase/prefix matching, owns case, Unicode and
246
268
  // short-name aliases. It also preserves case-sensitive Windows directories.
@@ -251,6 +273,7 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
251
273
  const name = relative ? path.join(relative, segments[i]) : segments[i];
252
274
  if (await excluded(name, found.entry))
253
275
  break;
276
+ anchor.target = { ...found.identity, kind: kind(found.entry) };
254
277
  if (i === segments.length - 1) {
255
278
  result.entries.set(name, fingerprint(found.entry, found.identity));
256
279
  result.targets.set(name, found.identity);
@@ -15,6 +15,8 @@ function shallowest(paths, limit) {
15
15
  export function watchStreamPaths(snapshot, scopes) {
16
16
  const anchors = [];
17
17
  for (const scope of scopes) {
18
+ if (scope.kind === "entry")
19
+ continue;
18
20
  let name = scope.kind === "tree" && scope.depth !== 0 ? scope.path : path.dirname(scope.path);
19
21
  if (name === ".")
20
22
  name = "";
@@ -4,9 +4,9 @@ import { assertRootIdentityCurrent } from "./root-context.js";
4
4
  import { rootHandleContext } from "./root-handle-context.js";
5
5
  import { createSuppressedError } from "./suppressed-error.js";
6
6
  import { getFsSafeTestHooks } from "./test-hooks.js";
7
- import { admittedNativeChanges } from "./watch-alias.js";
7
+ import { mergeWatchRescan, watchRescanScopes } from "./watch-rescan.js";
8
8
  import { watchStreamPaths } from "./watch-stream.js";
9
- import { changedEntries, excludedWatchPath, guardedHintChanges, scopedChanges } from "./watch-hints.js";
9
+ import { admittedNativeChanges, changedEntries, excludedWatchPath, guardedHintChanges, scopedChanges } from "./watch-hints.js";
10
10
  import path from "node:path";
11
11
  import { watchBinding, NativeWatchBackend } from "./watch-native.js";
12
12
  import { getFsSafeNativeConfig } from "./native-config.js";
@@ -60,6 +60,7 @@ export function watch(root, input) {
60
60
  const maxPendingPaths = budget(options.maxPendingPaths, 256, "maxPendingPaths", 4096);
61
61
  if (typeof options.onInvalidate !== "function")
62
62
  throw new TypeError("watch requires onInvalidate");
63
+ const callerSignal = options.signal ? AbortSignal.any([options.signal]) : undefined;
63
64
  const makeGeneration = (scopes) => ({
64
65
  scopes: watchScopes(scopes), abort: new AbortController(), waiter: deferred(),
65
66
  });
@@ -80,6 +81,8 @@ export function watch(root, input) {
80
81
  let resetRequested = false;
81
82
  let refreshBackend = false;
82
83
  let pending = false;
84
+ let fullRequested = false;
85
+ let pendingUncertain = false;
83
86
  let pendingWaiter;
84
87
  let runningWaiter;
85
88
  let pendingHint = false;
@@ -126,6 +129,7 @@ export function watch(root, input) {
126
129
  pendingHint = false;
127
130
  pendingChanges = new Map();
128
131
  pendingBackendOverflow = false;
132
+ pendingUncertain = false;
129
133
  };
130
134
  const notifyHealth = () => {
131
135
  try {
@@ -203,7 +207,7 @@ export function watch(root, input) {
203
207
  }
204
208
  };
205
209
  const scheduleInterval = () => {
206
- if (terminal || failure !== undefined)
210
+ if (timer || fullRequested || terminal || failure !== undefined)
207
211
  return;
208
212
  timer = setTimeout(() => { timer = undefined; void request().catch(() => { }); }, intervalMs);
209
213
  if (!persistent)
@@ -232,16 +236,27 @@ export function watch(root, input) {
232
236
  pendingChanges = undefined;
233
237
  else if (pendingChanges)
234
238
  for (const hint of batch.hints) {
235
- if (typeof hint.name === "string" && excludedWatchPath(snapshot, hint.directory ? path.join(hint.directory, hint.name) : hint.name))
239
+ if (hint.event === "rename")
240
+ pendingUncertain = true;
241
+ if (typeof hint.name === "string" && excludedWatchPath(snapshot, hint.directory ? path.join(hint.directory, hint.name) : hint.name)) {
242
+ pendingUncertain = true;
243
+ continue;
244
+ }
245
+ // Multiple bounded native batches can still fill the JS coalescing window.
246
+ // A Root fold retains discovery while letting the guarded diff own detail.
247
+ if (pendingChanges.get("subtree-root")?.event === "subtree")
236
248
  continue;
237
- const key = JSON.stringify([hint.directory, hint.name]);
249
+ const key = JSON.stringify([hint.directory, hint.name, hint.event === "children" || hint.event === "subtree" ? hint.event : "entry"]);
238
250
  if (!pendingChanges.has(key) && pendingChanges.size >= maxPendingPaths) {
239
- pendingChanges = undefined;
251
+ pendingChanges.clear();
252
+ pendingChanges.set("subtree-root", { directory: "", name: "", event: "subtree" });
240
253
  break;
241
254
  }
242
255
  const previous = pendingChanges.get(key);
243
256
  pendingChanges.set(key, previous?.event === "rename" ? previous : hint);
244
257
  }
258
+ if (!batch.hints.length)
259
+ pendingUncertain = true;
245
260
  pendingHint = true;
246
261
  pending = true;
247
262
  if (hintTimer)
@@ -251,7 +266,7 @@ export function watch(root, input) {
251
266
  if (terminal || current !== g || failure !== undefined)
252
267
  return;
253
268
  // Raw backend filenames stay private. Reconcile before publishing detail.
254
- void request().catch(() => { });
269
+ void request(false).catch(() => { });
255
270
  }, coalesceMs);
256
271
  if (!persistent)
257
272
  hintTimer.unref();
@@ -273,6 +288,7 @@ export function watch(root, input) {
273
288
  await retireBackend();
274
289
  check(g);
275
290
  refreshBackend = false;
291
+ fullRequested = true;
276
292
  }
277
293
  if (resetRequested) {
278
294
  await retireBackend();
@@ -280,17 +296,40 @@ export function watch(root, input) {
280
296
  snapshot = undefined;
281
297
  resetRequested = false;
282
298
  }
283
- state = snapshot ? "reconciling" : "starting";
284
- notifyHealth();
285
- check(g);
286
299
  const hadHints = pendingHint;
287
300
  const hadBackendOverflow = pendingBackendOverflow;
288
301
  const hints = pendingChanges;
302
+ const full = fullRequested || pendingUncertain;
303
+ fullRequested = false;
304
+ pendingUncertain = false;
289
305
  pendingHint = false;
290
306
  pendingChanges = new Map();
291
307
  pendingBackendOverflow = false;
292
308
  clearTimeout(hintTimer);
293
309
  hintTimer = undefined;
310
+ let scanScopes = g.scopes;
311
+ if (snapshot && hadHints && hints && !full) {
312
+ try {
313
+ const admitted = await admittedNativeChanges(context, g.scopes, snapshot, snapshot, {
314
+ hints: [...hints.values()], overflow: false,
315
+ }, g.abort.signal, maxPendingPaths, true);
316
+ check(g);
317
+ if (admitted?.length === 0)
318
+ return;
319
+ if (admitted)
320
+ scanScopes = watchRescanScopes(g.scopes, snapshot, admitted) ?? g.scopes;
321
+ }
322
+ catch (error) {
323
+ check(g);
324
+ await assertRootIdentityCurrent(context);
325
+ if (!isWatchPathError(error))
326
+ throw error;
327
+ // An uncertain alias or stale identity never suppresses reconciliation.
328
+ }
329
+ }
330
+ state = snapshot ? "reconciling" : "starting";
331
+ notifyHealth();
332
+ check(g);
294
333
  if (mode === "events" && !backend && g.scopes.length) {
295
334
  try {
296
335
  const candidate = new NativeWatchBackend(binding, context, batch => {
@@ -312,38 +351,83 @@ export function watch(root, input) {
312
351
  }
313
352
  check(g);
314
353
  }
315
- const next = await scanWatch(context, g.scopes, { exclude: options.exclude, maxDirectories, maxEntries, maxPendingPaths, admitting: !snapshot, previous: snapshot }, g.abort.signal, async (name, identity, guard) => {
316
- check(g);
317
- const existing = registered.get(name);
318
- const acquire = !existing || existing.dev !== identity.dev || existing.ino !== identity.ino;
319
- await getFsSafeTestHooks()?.beforeWatchRegistration?.(guard.realPath);
320
- check(g);
321
- if (acquire) {
322
- try {
323
- backend?.add(name, identity);
324
- }
325
- catch (error) {
326
- if (snapshot || !fallBack(error))
327
- throw error;
328
- await retireBackend();
354
+ let next;
355
+ for (let attempt = 0;; attempt++) {
356
+ if (scanScopes === g.scopes) {
357
+ clearTimeout(timer);
358
+ timer = undefined;
359
+ }
360
+ try {
361
+ next = await scanWatch(context, scanScopes, { exclude: options.exclude, maxDirectories, maxEntries, maxPendingPaths, admitting: !snapshot, previous: snapshot }, g.abort.signal, async (name, identity, guard) => {
362
+ check(g);
363
+ const existing = registered.get(name);
364
+ const acquire = !existing || existing.dev !== identity.dev || existing.ino !== identity.ino;
365
+ await getFsSafeTestHooks()?.beforeWatchRegistration?.(guard.realPath);
366
+ check(g);
367
+ if (acquire) {
368
+ try {
369
+ backend?.add(name, identity);
370
+ }
371
+ catch (error) {
372
+ if (snapshot || !fallBack(error))
373
+ throw error;
374
+ await retireBackend();
375
+ check(g);
376
+ }
377
+ }
378
+ await getFsSafeTestHooks()?.afterWatchRegistration?.(guard.realPath);
329
379
  check(g);
380
+ if (acquire)
381
+ registered.set(name, identity);
382
+ }, retainRetirement);
383
+ }
384
+ catch (error) {
385
+ // A slice can repeat ancestor lookups that a full traversal shares.
386
+ // Let the authoritative full pass decide whether the configured limit is exceeded.
387
+ if (scanScopes !== g.scopes && error instanceof FsSafeError && error.code === "too-large") {
388
+ scanScopes = g.scopes;
389
+ continue;
330
390
  }
391
+ throw error;
331
392
  }
332
- await getFsSafeTestHooks()?.afterWatchRegistration?.(guard.realPath);
333
393
  check(g);
334
- if (acquire)
335
- registered.set(name, identity);
336
- }, retainRetirement);
337
- check(g);
338
- if (backend?.configure(watchStreamPaths(next, g.scopes))) {
339
- // The replacement stream is live before the next guarded pass covers the handover.
340
- pending = true;
394
+ if (scanScopes !== g.scopes) {
395
+ const merged = mergeWatchRescan(g.scopes, snapshot, next, scanScopes);
396
+ if (!merged || merged.scanned > maxEntries || merged.directories.size > maxDirectories || (merged.excluded?.size ?? 0) > maxEntries) {
397
+ scanScopes = g.scopes;
398
+ continue;
399
+ }
400
+ next = merged;
401
+ }
402
+ try {
403
+ const entriesChanged = backend?.entries(next);
404
+ const streamsChanged = backend?.configure(watchStreamPaths(next, g.scopes));
405
+ if (entriesChanged || streamsChanged) {
406
+ // Complete a guarded pass with the new transport before publishing readiness.
407
+ // Bound handovers under churn; later passes still reconcile ongoing changes.
408
+ scanScopes = g.scopes;
409
+ if (attempt < 2)
410
+ continue;
411
+ pending = true;
412
+ fullRequested = true;
413
+ }
414
+ break;
415
+ }
416
+ catch (error) {
417
+ await assertRootIdentityCurrent(context);
418
+ check(g);
419
+ if (attempt >= 2 || !isWatchPathError(error))
420
+ throw error;
421
+ scanScopes = g.scopes;
422
+ // A replacement between scan and descriptor admission requires a fresh identity.
423
+ }
341
424
  }
342
425
  // Retire stale inventory before the next pass; that crawl installs fresh anchors first.
343
426
  if ([...registered.keys()].some(name => !next.directories.has(name))) {
344
427
  refreshBackend = true;
345
428
  }
346
- observedDirectories = next.directories.size;
429
+ observedDirectories = backend?.directories === undefined ? next.directories.size
430
+ : backend.directories + (g.scopes.some(scope => scope.kind === "tree") ? next.directories.size : 0);
347
431
  const initial = !snapshot;
348
432
  let observed = next.overflow ? undefined : changedEntries(snapshot, next, maxPendingPaths);
349
433
  if (observed) {
@@ -384,6 +468,8 @@ export function watch(root, input) {
384
468
  state = "ready";
385
469
  notifyHealth();
386
470
  check(g);
471
+ // Rearm even when sustained hints keep the pump's coalesced request occupied.
472
+ scheduleInterval();
387
473
  };
388
474
  const pump = () => {
389
475
  if (active || terminal || failure !== undefined)
@@ -435,13 +521,16 @@ export function watch(root, input) {
435
521
  }
436
522
  });
437
523
  };
438
- const request = () => {
524
+ const request = (full = true) => {
439
525
  if (terminal)
440
526
  return Promise.reject(retired());
441
527
  if (failure !== undefined)
442
528
  return Promise.reject(failure);
443
- clearTimeout(timer);
444
- timer = undefined;
529
+ if (full) {
530
+ clearTimeout(timer);
531
+ timer = undefined;
532
+ fullRequested = true;
533
+ }
445
534
  pending = true;
446
535
  pendingWaiter ??= deferred();
447
536
  const result = pendingWaiter.promise;
@@ -459,7 +548,8 @@ export function watch(root, input) {
459
548
  pendingWaiter = undefined;
460
549
  runningWaiter = undefined;
461
550
  clearTimers();
462
- options.signal?.removeEventListener("abort", abort);
551
+ if (callerSignal)
552
+ callerSignal.onabort = null;
463
553
  // Start physical stop immediately, without waiting behind an in-flight scan.
464
554
  try {
465
555
  backend?.close();
@@ -485,7 +575,7 @@ export function watch(root, input) {
485
575
  const abort = () => { void close(); };
486
576
  const subscription = {
487
577
  ready, health, close, [Symbol.asyncDispose]: close,
488
- reconcile: request,
578
+ reconcile: () => request(),
489
579
  setScopes(scopes) {
490
580
  if (terminal)
491
581
  return Promise.reject(retired());
@@ -521,8 +611,9 @@ export function watch(root, input) {
521
611
  return next.waiter.promise;
522
612
  },
523
613
  };
524
- options.signal?.addEventListener("abort", abort, { once: true });
525
- if (options.signal?.aborted)
614
+ if (callerSignal)
615
+ callerSignal.onabort = abort;
616
+ if (callerSignal?.aborted)
526
617
  abort();
527
618
  else
528
619
  pump();
@@ -1,5 +1,13 @@
1
1
  import { FsSafeError } from "./errors.js";
2
2
  export type WindowsPathAliasKind = "filesystem" | "relative";
3
+ /**
4
+ * True when a Windows path could reach a UNC share or device namespace that
5
+ * none of the trusted boundary paths live on: its share or device differs from
6
+ * theirs or cannot be identified from its spelling, and it is not spelled
7
+ * exactly under one of them. Reject such input before any filesystem call:
8
+ * even lstat on `\\host\share\x` makes Windows contact host.
9
+ */
10
+ export declare function isForeignWindowsShareOrDevicePath(value: string, trustedPaths: readonly (string | undefined)[], platform?: NodeJS.Platform | string): boolean;
3
11
  /**
4
12
  * Capture an ordinary Windows drive-relative path without normalizing its raw
5
13
  * suffix. This is only for public APIs whose existing contract accepts such
@@ -1,6 +1,28 @@
1
1
  import path from "node:path";
2
2
  import { FsSafeError } from "./errors.js";
3
- import { hasWindowsDrivePrefix, rootedWindowsDriveColonIndex, windowsNamespaceMarker, } from "./windows-path-syntax.js";
3
+ import { hasWindowsDrivePrefix, rootedWindowsDriveColonIndex, windowsNamespaceMarker, windowsSegmentsClimbAbove, windowsShareOrDeviceRoot, } from "./windows-path-syntax.js";
4
+ function isExactlyUnderWindowsPath(trusted, value) {
5
+ const root = trusted.replaceAll("/", "\\").replace(/(?<=.)\\+$/, "");
6
+ const candidate = value.replaceAll("/", "\\");
7
+ if (candidate !== root && !candidate.startsWith(root.endsWith("\\") ? root : `${root}\\`))
8
+ return false;
9
+ return !windowsSegmentsClimbAbove(candidate.slice(root.length).split("\\"));
10
+ }
11
+ /**
12
+ * True when a Windows path could reach a UNC share or device namespace that
13
+ * none of the trusted boundary paths live on: its share or device differs from
14
+ * theirs or cannot be identified from its spelling, and it is not spelled
15
+ * exactly under one of them. Reject such input before any filesystem call:
16
+ * even lstat on `\\host\share\x` makes Windows contact host.
17
+ */
18
+ export function isForeignWindowsShareOrDevicePath(value, trustedPaths, platform = process.platform) {
19
+ if (platform !== "win32")
20
+ return false;
21
+ const key = windowsShareOrDeviceRoot(value);
22
+ if (key === undefined)
23
+ return false;
24
+ return !trustedPaths.some(trusted => trusted !== undefined && ((key !== null && windowsShareOrDeviceRoot(trusted) === key) || isExactlyUnderWindowsPath(trusted, value)));
25
+ }
4
26
  function isBareWindowsNamespaceDrive(value) {
5
27
  return value.length === 6 && windowsNamespaceMarker(value) !== undefined &&
6
28
  hasWindowsDrivePrefix(value, 4);
@@ -106,11 +128,7 @@ export function assertNoWindowsPathAliasForPlatform(value, kind, message, platfo
106
128
  }
107
129
  }
108
130
  export function assertNoWindowsPathAlias(value, kind = "filesystem", message = "path uses a Windows filesystem namespace alias", platform = process.platform) {
109
- if (hasWindowsPathAlias(value, kind, platform)) {
110
- throw new FsSafeError("invalid-path", message, {
111
- details: { reason: "windows-path-alias" },
112
- });
113
- }
131
+ assertNoWindowsPathAliasForPlatform(value, kind, message, platform);
114
132
  }
115
133
  export function isWindowsPathAliasError(error) {
116
134
  return error instanceof FsSafeError && error.details?.reason === "windows-path-alias";
@@ -3,3 +3,16 @@ export declare function hasWindowsDrivePrefix(value: string, offset?: number): b
3
3
  /** Classify a raw prefix without normalizing any path components. */
4
4
  export declare function windowsNamespaceMarker(value: string): "." | "?" | undefined;
5
5
  export declare function rootedWindowsDriveColonIndex(value: string): number;
6
+ /**
7
+ * True when dot-like components climb above the start of `segments`. Win32
8
+ * trims trailing dots and spaces, so any all-dot or all-space component other
9
+ * than `.` is counted as a parent step.
10
+ */
11
+ export declare function windowsSegmentsClimbAbove(segments: readonly string[]): boolean;
12
+ /**
13
+ * Comparable share or device root of a path spelled with two leading
14
+ * separators, excluding namespaced drive roots such as `\\?\C:\`.
15
+ * Returns undefined for drive, rooted, and relative spellings, and null when
16
+ * the spelling alone cannot establish which share or device it reaches.
17
+ */
18
+ export declare function windowsShareOrDeviceRoot(value: string): string | null | undefined;