@openclaw/fs-safe 0.13.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/CHANGELOG.md +64 -1
  2. package/README.md +10 -4
  3. package/dist/advanced.d.ts +1 -1
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +1 -1
  6. package/dist/archive-merge.d.ts.map +1 -1
  7. package/dist/archive-merge.js +113 -46
  8. package/dist/archive-read.d.ts.map +1 -1
  9. package/dist/archive-read.js +4 -4
  10. package/dist/archive-zip-directory.d.ts +4 -0
  11. package/dist/archive-zip-directory.d.ts.map +1 -1
  12. package/dist/archive-zip-directory.js +2 -0
  13. package/dist/archive-zip-entry.d.ts +6 -2
  14. package/dist/archive-zip-entry.d.ts.map +1 -1
  15. package/dist/archive-zip-entry.js +23 -8
  16. package/dist/archive-zip-integrity.d.ts.map +1 -1
  17. package/dist/archive-zip-integrity.js +3 -4
  18. package/dist/archive-zip-loader.d.ts.map +1 -1
  19. package/dist/archive-zip-loader.js +107 -31
  20. package/dist/archive-zip-names.d.ts +1 -0
  21. package/dist/archive-zip-names.d.ts.map +1 -1
  22. package/dist/archive-zip-names.js +6 -0
  23. package/dist/archive.js +6 -5
  24. package/dist/bounded-read-stream.d.ts +0 -1
  25. package/dist/bounded-read-stream.d.ts.map +1 -1
  26. package/dist/bounded-read-stream.js +0 -6
  27. package/dist/copy-publication.d.ts +6 -0
  28. package/dist/copy-publication.d.ts.map +1 -1
  29. package/dist/copy-publication.js +3 -0
  30. package/dist/copy-tree-portable.d.ts.map +1 -1
  31. package/dist/copy-tree-portable.js +44 -24
  32. package/dist/copy.d.ts.map +1 -1
  33. package/dist/copy.js +29 -11
  34. package/dist/directory-mode-owner.js +5 -5
  35. package/dist/file-handle-transfer.d.ts +2 -0
  36. package/dist/file-handle-transfer.d.ts.map +1 -1
  37. package/dist/file-handle-transfer.js +57 -2
  38. package/dist/file-identity.d.ts.map +1 -1
  39. package/dist/file-identity.js +18 -4
  40. package/dist/file-lock-sync-admission.d.ts +19 -0
  41. package/dist/file-lock-sync-admission.d.ts.map +1 -0
  42. package/dist/file-lock-sync-admission.js +93 -0
  43. package/dist/file-lock-sync-root-acquire.d.ts +4 -0
  44. package/dist/file-lock-sync-root-acquire.d.ts.map +1 -0
  45. package/dist/file-lock-sync-root-acquire.js +370 -0
  46. package/dist/file-lock-sync-root-arbitration.d.ts +18 -0
  47. package/dist/file-lock-sync-root-arbitration.d.ts.map +1 -0
  48. package/dist/file-lock-sync-root-arbitration.js +66 -0
  49. package/dist/file-lock-sync-root-held.d.ts +34 -0
  50. package/dist/file-lock-sync-root-held.d.ts.map +1 -0
  51. package/dist/file-lock-sync-root-held.js +393 -0
  52. package/dist/file-lock-sync-root-io.d.ts +44 -0
  53. package/dist/file-lock-sync-root-io.d.ts.map +1 -0
  54. package/dist/file-lock-sync-root-io.js +209 -0
  55. package/dist/file-lock-sync-root-mutation.d.ts +17 -0
  56. package/dist/file-lock-sync-root-mutation.d.ts.map +1 -0
  57. package/dist/file-lock-sync-root-mutation.js +277 -0
  58. package/dist/file-lock-sync-root-options.d.ts +20 -0
  59. package/dist/file-lock-sync-root-options.d.ts.map +1 -0
  60. package/dist/file-lock-sync-root-options.js +58 -0
  61. package/dist/file-lock-sync-root-registration.d.ts +2 -0
  62. package/dist/file-lock-sync-root-registration.d.ts.map +1 -0
  63. package/dist/file-lock-sync-root-registration.js +90 -0
  64. package/dist/file-lock-sync-root.d.ts +36 -0
  65. package/dist/file-lock-sync-root.d.ts.map +1 -0
  66. package/dist/file-lock-sync-root.js +361 -0
  67. package/dist/file-lock-sync-stale-admission.d.ts +24 -0
  68. package/dist/file-lock-sync-stale-admission.d.ts.map +1 -0
  69. package/dist/file-lock-sync-stale-admission.js +205 -0
  70. package/dist/file-lock-sync.d.ts.map +1 -1
  71. package/dist/file-lock-sync.js +245 -205
  72. package/dist/file-store-prune.d.ts.map +1 -1
  73. package/dist/file-store-prune.js +5 -1
  74. package/dist/file-store-sync-directory.d.ts.map +1 -1
  75. package/dist/file-store-sync-directory.js +14 -7
  76. package/dist/file-store-sync-write.js +3 -3
  77. package/dist/file-store.d.ts.map +1 -1
  78. package/dist/file-store.js +47 -12
  79. package/dist/guest-dispatch-python.js +1 -1
  80. package/dist/json-document-store.d.ts.map +1 -1
  81. package/dist/json-document-store.js +22 -15
  82. package/dist/json-durable-queue-ownership.d.ts +0 -1
  83. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  84. package/dist/json-durable-queue-ownership.js +0 -6
  85. package/dist/native-binding.d.ts +8 -0
  86. package/dist/native-binding.d.ts.map +1 -1
  87. package/dist/native-parent-admission.d.ts +3 -2
  88. package/dist/native-parent-admission.d.ts.map +1 -1
  89. package/dist/native-parent-admission.js +24 -5
  90. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  91. package/dist/native-pinned-write-windows.js +7 -7
  92. package/dist/native-pinned-write.d.ts.map +1 -1
  93. package/dist/native-pinned-write.js +202 -172
  94. package/dist/native-policy-directory-observation.d.ts +12 -0
  95. package/dist/native-policy-directory-observation.d.ts.map +1 -0
  96. package/dist/native-policy-directory-observation.js +61 -0
  97. package/dist/native-policy-parent-windows.d.ts +14 -0
  98. package/dist/native-policy-parent-windows.d.ts.map +1 -0
  99. package/dist/native-policy-parent-windows.js +200 -0
  100. package/dist/native-rename-outcome.d.ts +4 -0
  101. package/dist/native-rename-outcome.d.ts.map +1 -0
  102. package/dist/native-rename-outcome.js +8 -0
  103. package/dist/native-staged-file.d.ts +2 -2
  104. package/dist/native-staged-file.d.ts.map +1 -1
  105. package/dist/native-staged-file.js +42 -40
  106. package/dist/output.d.ts.map +1 -1
  107. package/dist/output.js +12 -8
  108. package/dist/path-prefix.d.ts.map +1 -1
  109. package/dist/path-prefix.js +30 -8
  110. package/dist/path-suffix-aliases.d.ts +2 -0
  111. package/dist/path-suffix-aliases.d.ts.map +1 -1
  112. package/dist/path-suffix-aliases.js +25 -17
  113. package/dist/permission-exec.d.ts +2 -0
  114. package/dist/permission-exec.d.ts.map +1 -1
  115. package/dist/permission-exec.js +150 -21
  116. package/dist/permissions-windows.js +1 -1
  117. package/dist/pinned-mutation-admission.d.ts.map +1 -1
  118. package/dist/pinned-mutation-admission.js +23 -29
  119. package/dist/pinned-mutation-observation.d.ts +7 -2
  120. package/dist/pinned-mutation-observation.d.ts.map +1 -1
  121. package/dist/pinned-mutation-observation.js +81 -18
  122. package/dist/pinned-mutation-shared-route.d.ts +1 -0
  123. package/dist/pinned-mutation-shared-route.d.ts.map +1 -1
  124. package/dist/pinned-mutation-shared-route.js +1 -1
  125. package/dist/pinned-write-types.d.ts +2 -0
  126. package/dist/pinned-write-types.d.ts.map +1 -1
  127. package/dist/private-temp-workspace.d.ts.map +1 -1
  128. package/dist/private-temp-workspace.js +75 -121
  129. package/dist/regular-file.d.ts.map +1 -1
  130. package/dist/regular-file.js +1 -15
  131. package/dist/replace-directory.d.ts.map +1 -1
  132. package/dist/replace-directory.js +256 -18
  133. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  134. package/dist/replace-file-copy-fallback.js +62 -70
  135. package/dist/replace-file-copy-source.d.ts.map +1 -1
  136. package/dist/replace-file-copy-source.js +10 -12
  137. package/dist/replace-file-temp-owner.d.ts +5 -2
  138. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  139. package/dist/replace-file-temp-owner.js +65 -30
  140. package/dist/replace-file.js +6 -6
  141. package/dist/retained-directory-replacement.d.ts +26 -0
  142. package/dist/retained-directory-replacement.d.ts.map +1 -0
  143. package/dist/retained-directory-replacement.js +193 -0
  144. package/dist/root-boundary.d.ts +1 -0
  145. package/dist/root-boundary.d.ts.map +1 -1
  146. package/dist/root-boundary.js +4 -0
  147. package/dist/root-context.d.ts +0 -8
  148. package/dist/root-context.d.ts.map +1 -1
  149. package/dist/root-context.js +0 -3
  150. package/dist/root-create-input.d.ts +6 -0
  151. package/dist/root-create-input.d.ts.map +1 -1
  152. package/dist/root-create-input.js +5 -1
  153. package/dist/root-directory-creation.d.ts.map +1 -1
  154. package/dist/root-directory-creation.js +5 -4
  155. package/dist/root-directory-list.d.ts +1 -0
  156. package/dist/root-directory-list.d.ts.map +1 -1
  157. package/dist/root-directory-list.js +1 -0
  158. package/dist/root-impl.d.ts.map +1 -1
  159. package/dist/root-impl.js +20 -11
  160. package/dist/root-move-noreplace.d.ts.map +1 -1
  161. package/dist/root-move-noreplace.js +2 -2
  162. package/dist/root-path-errors.d.ts +1 -0
  163. package/dist/root-path-errors.d.ts.map +1 -1
  164. package/dist/root-path-errors.js +11 -2
  165. package/dist/root-path-existing.d.ts.map +1 -1
  166. package/dist/root-path-existing.js +12 -37
  167. package/dist/root-path.js +1 -13
  168. package/dist/root-remove.d.ts +1 -0
  169. package/dist/root-remove.d.ts.map +1 -1
  170. package/dist/root-remove.js +4 -0
  171. package/dist/root-walk.d.ts +1 -1
  172. package/dist/root-walk.d.ts.map +1 -1
  173. package/dist/root-walk.js +17 -2
  174. package/dist/root-write-admission.d.ts +0 -2
  175. package/dist/root-write-admission.d.ts.map +1 -1
  176. package/dist/root-write-admission.js +1 -15
  177. package/dist/root-write-compatibility.d.ts +1 -2
  178. package/dist/root-write-compatibility.d.ts.map +1 -1
  179. package/dist/root-write-compatibility.js +17 -68
  180. package/dist/root-write-complete-parent.d.ts.map +1 -1
  181. package/dist/root-write-complete-parent.js +10 -24
  182. package/dist/root-write-lock-binding.d.ts +15 -0
  183. package/dist/root-write-lock-binding.d.ts.map +1 -0
  184. package/dist/root-write-lock-binding.js +162 -0
  185. package/dist/root-write-verification.d.ts.map +1 -1
  186. package/dist/root-write-verification.js +29 -42
  187. package/dist/secret-file.d.ts.map +1 -1
  188. package/dist/secret-file.js +2 -24
  189. package/dist/secret-read-async.d.ts.map +1 -1
  190. package/dist/secret-read-async.js +3 -24
  191. package/dist/secret-read-policy.d.ts +6 -2
  192. package/dist/secret-read-policy.d.ts.map +1 -1
  193. package/dist/secret-read-policy.js +26 -2
  194. package/dist/sibling-temp.d.ts.map +1 -1
  195. package/dist/sibling-temp.js +23 -12
  196. package/dist/sidecar-lock-acquire.d.ts +2 -28
  197. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  198. package/dist/sidecar-lock-acquire.js +288 -199
  199. package/dist/sidecar-lock-admission-context.d.ts +19 -0
  200. package/dist/sidecar-lock-admission-context.d.ts.map +1 -0
  201. package/dist/sidecar-lock-admission-context.js +60 -0
  202. package/dist/sidecar-lock-admission-parser.d.ts +43 -0
  203. package/dist/sidecar-lock-admission-parser.d.ts.map +1 -0
  204. package/dist/sidecar-lock-admission-parser.js +113 -0
  205. package/dist/sidecar-lock-admission.d.ts +35 -0
  206. package/dist/sidecar-lock-admission.d.ts.map +1 -0
  207. package/dist/sidecar-lock-admission.js +7 -0
  208. package/dist/sidecar-lock-reclaim.d.ts +9 -4
  209. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  210. package/dist/sidecar-lock-reclaim.js +80 -25
  211. package/dist/sidecar-lock-stale-admission.d.ts +39 -0
  212. package/dist/sidecar-lock-stale-admission.d.ts.map +1 -0
  213. package/dist/sidecar-lock-stale-admission.js +232 -0
  214. package/dist/sidecar-lock-target.d.ts +8 -0
  215. package/dist/sidecar-lock-target.d.ts.map +1 -0
  216. package/dist/sidecar-lock-target.js +55 -0
  217. package/dist/sidecar-lock.d.ts.map +1 -1
  218. package/dist/sidecar-lock.js +100 -16
  219. package/dist/staged-directory.d.ts +5 -2
  220. package/dist/staged-directory.d.ts.map +1 -1
  221. package/dist/staged-directory.js +37 -1
  222. package/dist/temp-workspace-admission.d.ts.map +1 -1
  223. package/dist/temp-workspace-admission.js +28 -4
  224. package/dist/temp-workspace-descriptor.d.ts.map +1 -1
  225. package/dist/temp-workspace-descriptor.js +9 -27
  226. package/dist/temp-workspace-owner.d.ts.map +1 -1
  227. package/dist/temp-workspace-owner.js +8 -8
  228. package/dist/walk.d.ts +5 -1
  229. package/dist/walk.d.ts.map +1 -1
  230. package/dist/walk.js +19 -6
  231. package/dist/windows-owner.d.ts.map +1 -1
  232. package/dist/windows-owner.js +2 -2
  233. package/docs/advanced.md +1 -1
  234. package/docs/archive.md +36 -9
  235. package/docs/atomic.md +85 -8
  236. package/docs/copy.md +35 -0
  237. package/docs/file-store.md +19 -0
  238. package/docs/guest.md +1 -1
  239. package/docs/json-store.md +5 -0
  240. package/docs/native-helper.md +10 -3
  241. package/docs/native.md +8 -0
  242. package/docs/output.md +6 -0
  243. package/docs/path-prefix.md +10 -0
  244. package/docs/path-suffix-aliases.md +51 -6
  245. package/docs/permissions.md +13 -0
  246. package/docs/public-api.md +3 -2
  247. package/docs/root.md +22 -3
  248. package/docs/sidecar-lock.md +109 -4
  249. package/docs/staged-file.md +7 -3
  250. package/docs/temp.md +20 -3
  251. package/docs/walk.md +67 -1
  252. package/docs/writing.md +11 -1
  253. package/package.json +10 -10
  254. package/dist/darwin-acl.d.ts +0 -4
  255. package/dist/darwin-acl.d.ts.map +0 -1
  256. package/dist/darwin-acl.js +0 -24
@@ -15,6 +15,7 @@ const MAX_SUFFIX_DEPTH = 32;
15
15
  const MAX_MKDIR_ATTEMPTS = 128;
16
16
  const MAX_CREATED_DIRECTORIES = 64;
17
17
  const MAX_FORWARD_OBSERVATIONS = 4_096;
18
+ const MAX_SCALED_FORWARD_OBSERVATIONS = 32_768;
18
19
  const PROBE_NAME_LENGTH = 6;
19
20
  const PROBE_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
20
21
  const PROBE_FIRST_ALPHABET = "bdefghijkmoqrstuvwxyz";
@@ -25,7 +26,7 @@ function assertMutationPath(value) {
25
26
  if (value.length > MAX_PATH_LENGTH)
26
27
  unavailable();
27
28
  }
28
- function suffixSegments(value) {
29
+ function suffixSegments(value, maxDepth) {
29
30
  if (typeof value !== "string")
30
31
  throw new TypeError("suffix must be a string");
31
32
  if (value.length > MAX_SUFFIX_LENGTH)
@@ -35,8 +36,8 @@ function suffixSegments(value) {
35
36
  }
36
37
  const windows = process.platform === "win32";
37
38
  const segments = (windows ? value.replaceAll("/", "\\") : value).split(path.sep);
38
- if (segments.length > MAX_SUFFIX_DEPTH)
39
- throw new RangeError("suffix exceeds 32 components");
39
+ if (segments.length > maxDepth)
40
+ throw new RangeError(`suffix exceeds ${maxDepth} components`);
40
41
  if (segments.some(segment => !segment || segment === "." || segment === ".." ||
41
42
  (windows && (segment.includes(":") || /^[ .]+$/u.test(segment) ||
42
43
  isWindowsReservedDeviceName(segment))))) {
@@ -96,14 +97,8 @@ function createPrivateProbeName(nameLength) {
96
97
  return name;
97
98
  }
98
99
  function* asciiCaseProbePairs(nameLength, forbidden) {
99
- let count = 0;
100
- for (let attempt = 0; attempt < 96 && count < 24; attempt++) {
101
- const first = createPrivateProbeName(nameLength);
102
- const second = `${first[0].toUpperCase()}${first.slice(1)}`;
103
- if (!forbidden(first) && !forbidden(second)) {
104
- count++;
105
- yield [first, second];
106
- }
100
+ for (const first of privateProbeNames(nameLength, forbidden)) {
101
+ yield [first, `${first[0].toUpperCase()}${first.slice(1)}`];
107
102
  }
108
103
  }
109
104
  function* privateProbeNames(nameLength, forbidden) {
@@ -128,8 +123,16 @@ class ProbeDirectories {
128
123
  observations = 0;
129
124
  mkdirAttempts = 0;
130
125
  creations = 0;
131
- constructor(requestedDirectory) {
126
+ maxObservations;
127
+ maxMkdirAttempts;
128
+ maxCreations;
129
+ constructor(requestedDirectory, depth) {
132
130
  this.requestedDirectory = requestedDirectory;
131
+ // The suffix-length limit bounds actual depth, so these products remain safe integers.
132
+ const scale = Math.max(1, depth / MAX_SUFFIX_DEPTH);
133
+ this.maxObservations = Math.min(MAX_SCALED_FORWARD_OBSERVATIONS, MAX_FORWARD_OBSERVATIONS * scale * scale);
134
+ this.maxMkdirAttempts = MAX_MKDIR_ATTEMPTS * scale;
135
+ this.maxCreations = MAX_CREATED_DIRECTORIES * scale;
133
136
  this.observe(1);
134
137
  const canonical = realpathSync.native(pathForWindowsFilesystem(requestedDirectory));
135
138
  assertMutationPath(canonical);
@@ -138,7 +141,7 @@ class ProbeDirectories {
138
141
  this.assert(this.root);
139
142
  }
140
143
  reserveObservations(units) {
141
- if (this.observations + units > MAX_FORWARD_OBSERVATIONS)
144
+ if (units > this.maxObservations - this.observations)
142
145
  unavailable();
143
146
  }
144
147
  observe(units, cleanup = false) {
@@ -159,7 +162,7 @@ class ProbeDirectories {
159
162
  }
160
163
  }
161
164
  create(parent, name) {
162
- if (this.mkdirAttempts >= MAX_MKDIR_ATTEMPTS || this.creations >= MAX_CREATED_DIRECTORIES)
165
+ if (this.mkdirAttempts >= this.maxMkdirAttempts || this.creations >= this.maxCreations)
163
166
  unavailable();
164
167
  const probePath = path.join(parent.path, name);
165
168
  assertMutationPath(probePath);
@@ -337,11 +340,16 @@ export function probePathSuffixAliasesSync(options) {
337
340
  const directory = resolvePathPreservingWindowsRoot(requestedDirectory);
338
341
  if (directory.length > MAX_PATH_LENGTH)
339
342
  throw new RangeError("resolved directory exceeds 32768 code units");
343
+ const requestedMaxDepth = options.maxDepth;
344
+ const maxDepth = requestedMaxDepth === undefined ? MAX_SUFFIX_DEPTH : requestedMaxDepth;
345
+ if (!Number.isSafeInteger(maxDepth) || maxDepth < 0) {
346
+ throw new RangeError("maxDepth must be a non-negative safe integer");
347
+ }
340
348
  const leftInput = options.left;
341
349
  const rightInput = options.right;
342
350
  const predicate = options.shouldProbeCaseVariants;
343
- const left = suffixSegments(leftInput);
344
- const right = suffixSegments(rightInput);
351
+ const left = suffixSegments(leftInput, maxDepth);
352
+ const right = suffixSegments(rightInput, maxDepth);
345
353
  if (left.length !== right.length)
346
354
  throw new TypeError("suffixes must have the same component count");
347
355
  if (predicate !== undefined && typeof predicate !== "function") {
@@ -370,7 +378,7 @@ export function probePathSuffixAliasesSync(options) {
370
378
  let observed;
371
379
  let cleaned = true;
372
380
  try {
373
- observed = observeSuffixes(() => owner ??= new ProbeDirectories(directory), left, right, shouldProbe);
381
+ observed = observeSuffixes(() => owner ??= new ProbeDirectories(directory, left.length), left, right, shouldProbe);
374
382
  owner?.assert(owner.root);
375
383
  }
376
384
  catch {
@@ -8,6 +8,8 @@ export type PermissionCommandFailure = {
8
8
  stderr: string;
9
9
  };
10
10
  export declare function formatPermissionErrorDetail(value: string): string;
11
+ /** Formats only caught permission-query failures without invoking user code. */
12
+ export declare function formatCaughtPermissionFailure(error: unknown): string;
11
13
  export declare class PermissionCommandError extends Error implements PermissionCommandFailure {
12
14
  readonly command: string;
13
15
  readonly durationMs: number;
@@ -1 +1 @@
1
- {"version":3,"file":"permission-exec.d.ts","sourceRoot":"","sources":["../src/permission-exec.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,kCAAkC,QAAS,CAAC;AAEzD,MAAM,MAAM,wBAAwB,GAAG;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGjE;AAgBD,qBAAa,sBAAuB,SAAQ,KAAM,YAAW,wBAAwB;IACnF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB,YACE,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,OAAO,EACd,SAAS,SAAqC,EAc/C;CACF;AAED,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,wBAAwB,GAAG,SAAS,CAgBtC;AAED,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,SAAS,SAAqC,GAC7C,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAa7C"}
1
+ {"version":3,"file":"permission-exec.d.ts","sourceRoot":"","sources":["../src/permission-exec.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,kCAAkC,QAAS,CAAC;AAEzD,MAAM,MAAM,wBAAwB,GAAG;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGjE;AA4ED,gFAAgF;AAChF,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAqBpE;AA2CD,qBAAa,sBAAuB,SAAQ,KAAM,YAAW,wBAAwB;IACnF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB,YACE,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,OAAO,EACd,SAAS,SAAqC,EAc/C;CACF;AAED,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,wBAAwB,GAAG,SAAS,CAgCtC;AAED,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,SAAS,SAAqC,GAC7C,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAa7C"}
@@ -1,6 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import path from "node:path";
3
- import { promisify } from "node:util";
3
+ import { promisify, types as utilTypes } from "node:util";
4
4
  import { formatErrorDetail } from "./error-detail.js";
5
5
  const execFileAsync = promisify(execFile);
6
6
  export const DEFAULT_PERMISSION_EXEC_TIMEOUT_MS = 30_000;
@@ -8,15 +8,127 @@ export function formatPermissionErrorDetail(value) {
8
8
  const formatted = formatErrorDetail(value);
9
9
  return formatted.length > 400 ? `${formatted.slice(0, 399)}…` : formatted;
10
10
  }
11
+ const MISSING_DATA_PROPERTY = Object.freeze({ found: false });
12
+ const MAX_CAUGHT_FAILURE_PROTOTYPES = 4;
13
+ const primitiveString = String;
14
+ const isUint8Array = utilTypes.isUint8Array;
15
+ const Uint8ArrayIntrinsic = Uint8Array;
16
+ const typedArrayPrototype = Object.getPrototypeOf(Uint8ArrayIntrinsic.prototype);
17
+ const typedArrayBufferGetter = Object.getOwnPropertyDescriptor(typedArrayPrototype, "buffer").get;
18
+ const typedArrayByteOffsetGetter = Object.getOwnPropertyDescriptor(typedArrayPrototype, "byteOffset").get;
19
+ const typedArrayByteLengthGetter = Object.getOwnPropertyDescriptor(typedArrayPrototype, "byteLength").get;
20
+ const typedArraySet = Object.getOwnPropertyDescriptor(typedArrayPrototype, "set").value;
21
+ const textDecoderDecode = TextDecoder.prototype.decode;
22
+ const stderrDecoder = new TextDecoder("utf-8", {
23
+ fatal: false,
24
+ ignoreBOM: true,
25
+ });
26
+ const MAX_STDERR_BYTES = 1600;
27
+ function isInspectableCaughtObject(value) {
28
+ if ((typeof value !== "object" || value === null) && typeof value !== "function") {
29
+ return false;
30
+ }
31
+ try {
32
+ return !utilTypes.isProxy(value);
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ function dataProperty(value, name) {
39
+ if (!isInspectableCaughtObject(value))
40
+ return MISSING_DATA_PROPERTY;
41
+ let current = value;
42
+ for (let depth = 0; current !== null && depth < MAX_CAUGHT_FAILURE_PROTOTYPES; depth += 1) {
43
+ if (!isInspectableCaughtObject(current))
44
+ return MISSING_DATA_PROPERTY;
45
+ let descriptor;
46
+ try {
47
+ descriptor = Object.getOwnPropertyDescriptor(current, name);
48
+ }
49
+ catch {
50
+ return MISSING_DATA_PROPERTY;
51
+ }
52
+ if (descriptor) {
53
+ return Object.hasOwn(descriptor, "value")
54
+ ? { found: true, value: descriptor.value }
55
+ : MISSING_DATA_PROPERTY;
56
+ }
57
+ try {
58
+ current = Object.getPrototypeOf(current);
59
+ }
60
+ catch {
61
+ return MISSING_DATA_PROPERTY;
62
+ }
63
+ }
64
+ return MISSING_DATA_PROPERTY;
65
+ }
66
+ function caughtPrimitiveDisplay(value) {
67
+ if (value !== null && (typeof value === "object" || typeof value === "function"))
68
+ return undefined;
69
+ return primitiveString(value);
70
+ }
71
+ /** Formats only caught permission-query failures without invoking user code. */
72
+ export function formatCaughtPermissionFailure(error) {
73
+ const primitive = caughtPrimitiveDisplay(error);
74
+ if (primitive !== undefined)
75
+ return formatBoundedCaughtDetail(primitive);
76
+ if (!isInspectableCaughtObject(error)) {
77
+ return "Uninspectable proxy failure";
78
+ }
79
+ const message = dataProperty(error, "message");
80
+ const name = dataProperty(error, "name");
81
+ const rawMessage = message.found && typeof message.value === "string"
82
+ ? message.value : "";
83
+ const rawName = name.found && typeof name.value === "string"
84
+ ? name.value : "";
85
+ const messageText = rawMessage.slice(0, 400);
86
+ const nameText = rawName.slice(0, 400);
87
+ const display = messageText
88
+ ? (nameText ? `${nameText}: ${messageText}` : messageText)
89
+ : nameText || (typeof error === "function" ? "Unknown function failure" : "Unknown object failure");
90
+ return formatBoundedCaughtDetail(display, rawMessage.length > messageText.length || rawName.length > nameText.length);
91
+ }
92
+ function formatBoundedCaughtDetail(value, alreadyTruncated = false) {
93
+ const inputWasTruncated = alreadyTruncated || value.length > 400;
94
+ const formatted = formatPermissionErrorDetail(value.slice(0, 400));
95
+ if (!inputWasTruncated || formatted.endsWith("…"))
96
+ return formatted;
97
+ return `${formatted.slice(0, 399)}…`;
98
+ }
99
+ function safeStderr(value) {
100
+ if (typeof value === "string")
101
+ return formatBoundedCaughtDetail(value);
102
+ try {
103
+ if (!isUint8Array(value))
104
+ return "";
105
+ const backing = Reflect.apply(typedArrayBufferGetter, value, []);
106
+ const byteOffset = Reflect.apply(typedArrayByteOffsetGetter, value, []);
107
+ const byteLength = Reflect.apply(typedArrayByteLengthGetter, value, []);
108
+ const copiedByteLength = byteLength < MAX_STDERR_BYTES
109
+ ? byteLength : MAX_STDERR_BYTES;
110
+ const source = new Uint8ArrayIntrinsic(backing, byteOffset, copiedByteLength);
111
+ const copy = new Uint8ArrayIntrinsic(copiedByteLength);
112
+ Reflect.apply(typedArraySet, copy, [source, 0]);
113
+ const decoded = Reflect.apply(textDecoderDecode, stderrDecoder, [copy]);
114
+ return formatBoundedCaughtDetail(decoded, byteLength > copiedByteLength);
115
+ }
116
+ catch {
117
+ return "";
118
+ }
119
+ }
11
120
  function commandFailureFields(error) {
12
- const fields = error && typeof error === "object" ? error : {};
121
+ const killed = dataProperty(error, "killed");
122
+ const code = dataProperty(error, "code");
123
+ const signal = dataProperty(error, "signal");
124
+ const stderr = dataProperty(error, "stderr");
13
125
  return {
14
- timedOut: "killed" in fields && fields.killed === true &&
15
- "signal" in fields && fields.signal === "SIGKILL",
16
- exitCode: "code" in fields && typeof fields.code === "number" ? fields.code : null,
17
- signal: "signal" in fields && typeof fields.signal === "string" ? fields.signal : null,
18
- stderr: formatPermissionErrorDetail("stderr" in fields && (typeof fields.stderr === "string" || Buffer.isBuffer(fields.stderr))
19
- ? fields.stderr.toString() : ""),
126
+ found: killed.found || code.found || signal.found || stderr.found,
127
+ timedOut: killed.found && killed.value === true &&
128
+ signal.found && signal.value === "SIGKILL",
129
+ exitCode: code.found && typeof code.value === "number" ? code.value : null,
130
+ signal: signal.found && typeof signal.value === "string" ? signal.value : null,
131
+ stderr: stderr.found ? safeStderr(stderr.value) : "",
20
132
  };
21
133
  }
22
134
  export class PermissionCommandError extends Error {
@@ -41,21 +153,38 @@ export class PermissionCommandError extends Error {
41
153
  }
42
154
  }
43
155
  export function getPermissionCommandFailure(error, command, durationMs) {
44
- if (error instanceof PermissionCommandError) {
45
- return {
46
- command: error.command,
47
- durationMs: error.durationMs,
48
- timedOut: error.timedOut,
49
- exitCode: error.exitCode,
50
- signal: error.signal,
51
- stderr: error.stderr,
52
- };
156
+ if (isInspectableCaughtObject(error) && utilTypes.isNativeError(error)) {
157
+ const name = dataProperty(error, "name");
158
+ if (name.found && name.value === "PermissionCommandError") {
159
+ const wrappedCommand = dataProperty(error, "command");
160
+ const wrappedDuration = dataProperty(error, "durationMs");
161
+ const wrappedTimedOut = dataProperty(error, "timedOut");
162
+ const wrappedExitCode = dataProperty(error, "exitCode");
163
+ const wrappedSignal = dataProperty(error, "signal");
164
+ const wrappedStderr = dataProperty(error, "stderr");
165
+ if (wrappedCommand.found && typeof wrappedCommand.value === "string" &&
166
+ wrappedDuration.found && typeof wrappedDuration.value === "number" &&
167
+ Number.isSafeInteger(wrappedDuration.value) && wrappedDuration.value >= 0 &&
168
+ wrappedTimedOut.found && typeof wrappedTimedOut.value === "boolean" &&
169
+ wrappedExitCode.found && (wrappedExitCode.value === null || typeof wrappedExitCode.value === "number") &&
170
+ wrappedSignal.found && (wrappedSignal.value === null || typeof wrappedSignal.value === "string") &&
171
+ wrappedStderr.found) {
172
+ return {
173
+ command: wrappedCommand.value,
174
+ durationMs: wrappedDuration.value,
175
+ timedOut: wrappedTimedOut.value,
176
+ exitCode: wrappedExitCode.value,
177
+ signal: wrappedSignal.value,
178
+ stderr: safeStderr(wrappedStderr.value),
179
+ };
180
+ }
181
+ }
53
182
  }
54
- if (!error || typeof error !== "object" ||
55
- !("code" in error || "signal" in error || "killed" in error || "stderr" in error)) {
183
+ const fields = commandFailureFields(error);
184
+ if (!fields.found)
56
185
  return undefined;
57
- }
58
- return { command, durationMs: Math.round(durationMs), ...commandFailureFields(error) };
186
+ const { found: _found, ...detail } = fields;
187
+ return { command, durationMs: Math.round(durationMs), ...detail };
59
188
  }
60
189
  export async function executePermissionCommand(command, args, timeoutMs = DEFAULT_PERMISSION_EXEC_TIMEOUT_MS) {
61
190
  const startedAt = performance.now();
@@ -96,7 +96,7 @@ export async function inspectWindowsPermissions(params) {
96
96
  env: params.opts?.env,
97
97
  exec: params.opts?.exec ?? defaultPermissionExec,
98
98
  });
99
- if (owner.error) {
99
+ if (owner.error !== undefined) {
100
100
  const error = `Windows owner inspection failed: ${owner.error}`;
101
101
  return { ...unverified, ownerError: owner.error, error, errorDetail: owner.errorDetail, errorCause: owner.errorCause };
102
102
  }
@@ -1 +1 @@
1
- {"version":3,"file":"pinned-mutation-admission.d.ts","sourceRoot":"","sources":["../src/pinned-mutation-admission.ts"],"names":[],"mappings":"AACA,OAAO,EAA2B,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAGvF,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,KAAK,EAKV,4BAA4B,EAC7B,MAAM,mBAAmB,CAAC;AAQ3B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAgCtE,MAAM,MAAM,4BAA4B,GAAG,QAAQ,CAAC;IAClD,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,gBAAgB,CAAC,EAAE,qBAAqB,CAAC;CAC1C,CAAC,CAAC;AAEH,wBAAgB,4BAA4B,CAC1C,aAAa,EAAE,kBAAkB,GAAG,SAAS,EAC7C,gBAAgB,EAAE,qBAAqB,GAAG,SAAS,GAClD,4BAA4B,GAAG,SAAS,CAO1C;AAuJD,wBAAsB,mCAAmC,CAAC,MAAM,EAAE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,oBAAoB,CAAC;IACpC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,yBAAyB,EAAE,MAAM,CAAC;IAClC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,4BAA4B,GAAG,SAAS,CAAC;IACjD,cAAc,IAAI,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACjD,GAAG,OAAO,CAAC;IACV,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,CAAC,EAAE,4BAA4B,CAAC;CAClD,CAAC,CAsQD"}
1
+ {"version":3,"file":"pinned-mutation-admission.d.ts","sourceRoot":"","sources":["../src/pinned-mutation-admission.ts"],"names":[],"mappings":"AACA,OAAO,EAA2B,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAGvF,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,KAAK,EAKV,4BAA4B,EAC7B,MAAM,mBAAmB,CAAC;AAQ3B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAgCtE,MAAM,MAAM,4BAA4B,GAAG,QAAQ,CAAC;IAClD,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,gBAAgB,CAAC,EAAE,qBAAqB,CAAC;CAC1C,CAAC,CAAC;AAEH,wBAAgB,4BAA4B,CAC1C,aAAa,EAAE,kBAAkB,GAAG,SAAS,EAC7C,gBAAgB,EAAE,qBAAqB,GAAG,SAAS,GAClD,4BAA4B,GAAG,SAAS,CAO1C;AAuJD,wBAAsB,mCAAmC,CAAC,MAAM,EAAE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,oBAAoB,CAAC;IACpC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,yBAAyB,EAAE,MAAM,CAAC;IAClC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,4BAA4B,GAAG,SAAS,CAAC;IACjD,cAAc,IAAI,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACjD,GAAG,OAAO,CAAC;IACV,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,CAAC,EAAE,4BAA4B,CAAC;CAClD,CAAC,CAgQD"}
@@ -7,7 +7,7 @@ import { ordinarySharedAbsoluteInsideRoot, simpleSharedRoute, } from "./pinned-m
7
7
  import { resolvePathViaExistingAncestor } from "./root-path-existing.js";
8
8
  import { outsideWorkspaceError } from "./root-errors.js";
9
9
  import { getFsSafeNativeConfig } from "./native-config.js";
10
- import { advanceMutationObservation, mutationObservationCurrent, observeMutationPath, mutationDirectoryObservationCurrent, mutationObservationUsesDirectory, missingMutationSegments, nextMissingMutationPath, } from "./pinned-mutation-observation.js";
10
+ import { advanceMutationObservation, mutationObservationCurrent, observeMutationPath, mutationObservationsCurrent, mutationObservationUsesDirectory, missingMutationSegments, nextMissingMutationPath, } from "./pinned-mutation-observation.js";
11
11
  export function snapshotPinnedMutationPolicy(denyMutations, mutationSymlinks) {
12
12
  if (denyMutations === undefined && mutationSymlinks === undefined)
13
13
  return undefined;
@@ -92,10 +92,12 @@ function captureEpoch(rootReal, target, policy, session) {
92
92
  return undefined;
93
93
  }
94
94
  }
95
- function epochCurrent(epoch) {
95
+ function epochCurrent(epoch, directories) {
96
96
  return !process.versions.bun && epoch.mode === epoch.session.nativeMode &&
97
97
  getFsSafeNativeConfig().mode === epoch.mode &&
98
- epoch.observations.every(mutationObservationCurrent);
98
+ (directories ? mutationObservationsCurrent(epoch.observations, directories) :
99
+ epoch.observations.every(mutationObservationCurrent)) &&
100
+ getFsSafeNativeConfig().mode === epoch.mode;
99
101
  }
100
102
  function assertCachedNotDenied(target, epoch) {
101
103
  if (epoch.paths.some((paths) => paths.some((denied) => isPathInside(denied, target) && isPathInside(target, denied))) ||
@@ -143,10 +145,8 @@ function advanceAuthorizedDirectory(admittedEpoch, admitted, receipt) {
143
145
  target,
144
146
  observations: Object.freeze(complete),
145
147
  });
146
- if (!mutationDirectoryObservationCurrent(receipt.parent) ||
147
- !mutationDirectoryObservationCurrent(receipt.child) ||
148
- !mutationObservationUsesDirectory(target, receipt.child) ||
149
- !epochCurrent(next))
148
+ if (!mutationObservationUsesDirectory(target, receipt.child) ||
149
+ !epochCurrent(next, [receipt.parent, receipt.child]))
150
150
  return undefined;
151
151
  assertCachedNotDenied(target.path, next);
152
152
  assertCachedNotDenied(receipt.child.path, next);
@@ -165,6 +165,10 @@ export async function preparePinnedWriteMutationAdmission(params) {
165
165
  selectedTarget: params.resolvedTargetPath,
166
166
  policy,
167
167
  });
168
+ const nativeRoute = sharedRoute ?? (process.platform === "win32" && params.originalPath?.includes("/")
169
+ ? simpleSharedRoute({ rootReal: params.rootReal, rootIdentity: params.rootIdentity,
170
+ originalPath: params.originalPath.replaceAll("/", "\\"), selectedTarget: params.resolvedTargetPath, policy })
171
+ : undefined);
168
172
  let pinnedSession;
169
173
  let epoch;
170
174
  let pending;
@@ -194,13 +198,13 @@ export async function preparePinnedWriteMutationAdmission(params) {
194
198
  }
195
199
  return current;
196
200
  };
197
- const beginSharedParentWalk = sharedRoute ? () => {
201
+ const createSharedParentWalk = (admittedRoute) => {
198
202
  const session = Object.freeze({
199
203
  kind: "shared",
200
- route: sharedRoute.route,
204
+ route: admittedRoute.route,
201
205
  selectedTarget: params.resolvedTargetPath,
202
206
  nativeMode: getFsSafeNativeConfig().mode,
203
- rootIdentity: sharedRoute.rootIdentity,
207
+ rootIdentity: admittedRoute.rootIdentity,
204
208
  authorizationToken: Object.freeze({}),
205
209
  });
206
210
  let disposed = false;
@@ -232,20 +236,14 @@ export async function preparePinnedWriteMutationAdmission(params) {
232
236
  if (!reusable || reusable.session !== session ||
233
237
  !reusableSharedCreateRequest(request, reusable) ||
234
238
  !mutationObservationUsesDirectory(reusable.target, parent) ||
235
- !epochCurrent(reusable)) {
239
+ !nonzeroDirectoryObservation(parent) || !epochCurrent(reusable, [parent])) {
236
240
  disable();
237
241
  return undefined;
238
242
  }
239
243
  assertCachedNotDenied(request.targetPath, reusable);
240
244
  assertCachedNotDenied(request.mutationPath, reusable);
241
- // This is the required end fence. A returned receipt remains usable
242
- // only synchronously, while the exact parent identity and spelling
243
- // still match the observation captured by this walk.
244
- if (!nonzeroDirectoryObservation(parent) ||
245
- !mutationDirectoryObservationCurrent(parent)) {
246
- disable();
247
- return undefined;
248
- }
245
+ // The epoch check ends with the exact parent fence. The receipt is
246
+ // consumed synchronously before any intervening authority callback.
249
247
  sharedPending = Object.freeze({
250
248
  epoch: reusable,
251
249
  childPath: request.mutationPath,
@@ -320,7 +318,7 @@ export async function preparePinnedWriteMutationAdmission(params) {
320
318
  disable();
321
319
  },
322
320
  });
323
- } : undefined;
321
+ };
324
322
  const mutationAdmission = Object.freeze({
325
323
  rejectParentSymlinks: policy.mutationSymlinks === "reject",
326
324
  beginParentWalk: route ? () => {
@@ -335,26 +333,22 @@ export async function preparePinnedWriteMutationAdmission(params) {
335
333
  pending = undefined;
336
334
  return route;
337
335
  } : undefined,
338
- beginSharedParentWalk,
336
+ beginSharedParentWalk: sharedRoute ? () => createSharedParentWalk(sharedRoute) : undefined,
337
+ beginNativeParentWalk: nativeRoute ? () => createSharedParentWalk(nativeRoute) : undefined,
339
338
  tryAuthorizeAtParent(request, parent) {
340
339
  pending = undefined;
341
340
  const reusable = epoch;
342
341
  if (!reusable || reusable.session !== pinnedSession ||
343
342
  !reusableRequest(request, reusable) ||
344
343
  !mutationObservationUsesDirectory(reusable.target, parent) ||
345
- !epochCurrent(reusable)) {
344
+ !epochCurrent(reusable, [parent])) {
346
345
  epoch = undefined;
347
346
  return undefined;
348
347
  }
349
348
  assertCachedNotDenied(request.targetPath, reusable);
350
349
  assertCachedNotDenied(request.mutationPath, reusable);
351
- // This is the required end fence. The caller can consume the returned
352
- // token without awaiting only while the exact pathname, canonical
353
- // spelling, descriptor identity, type, mode, and link count still match.
354
- if (!mutationDirectoryObservationCurrent(parent)) {
355
- epoch = undefined;
356
- return undefined;
357
- }
350
+ // The epoch check ends at the exact parent pathname, canonical spelling,
351
+ // identity, type, mode and link-count fence, with no intervening await.
358
352
  if (request.phase === "parent-create") {
359
353
  pending = Object.freeze({
360
354
  epoch: reusable,
@@ -6,6 +6,11 @@ export type MutationIdentity = Readonly<{
6
6
  nlink: bigint;
7
7
  }>;
8
8
  declare const checkedDirectoryObservation: unique symbol;
9
+ export type MutationDirectoryCurrent = Readonly<{
10
+ canonicalPath: string;
11
+ identity: MutationIdentity;
12
+ }>;
13
+ export type MutationDirectoryObserver = () => MutationDirectoryCurrent | undefined;
9
14
  export type MutationDirectoryObservation = Readonly<{
10
15
  path: string;
11
16
  canonicalPath: string;
@@ -24,11 +29,11 @@ export type MutationPathObservation = Readonly<{
24
29
  }>;
25
30
  export declare function missingMutationSegments(observation: MutationPathObservation): number;
26
31
  export declare function nextMissingMutationPath(observation: MutationPathObservation): string | undefined;
27
- export declare function checkedMutationDirectory(pathname: string, canonicalPath: string, stat: BigIntStats): MutationDirectoryObservation;
28
- export declare function mutationDirectoryObservationCurrent(observation: MutationDirectoryObservation): boolean;
32
+ export declare function checkedMutationDirectory(pathname: string, canonicalPath: string, stat: Pick<BigIntStats, "dev" | "ino" | "mode" | "nlink">, observeCurrent?: MutationDirectoryObserver): MutationDirectoryObservation;
29
33
  export declare function mutationObservationUsesDirectory(observation: MutationPathObservation, directory: MutationDirectoryObservation): boolean;
30
34
  export declare function observeMutationPath(pathname: string): MutationPathObservation | undefined;
31
35
  export declare function mutationObservationCurrent(observation: MutationPathObservation): boolean;
36
+ export declare function mutationObservationsCurrent(observations: readonly MutationPathObservation[], directories: readonly MutationDirectoryObservation[]): boolean;
32
37
  export declare function advanceMutationObservation(observation: MutationPathObservation, parent: MutationDirectoryObservation, child: MutationDirectoryObservation): MutationPathObservation | undefined;
33
38
  export {};
34
39
  //# sourceMappingURL=pinned-mutation-observation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pinned-mutation-observation.d.ts","sourceRoot":"","sources":["../src/pinned-mutation-observation.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAM/C,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf,CAAC,CAAC;AAEH,QAAA,MAAM,2BAA2B,eAAmD,CAAC;AAErF,MAAM,MAAM,4BAA4B,GAAG,QAAQ,CAAC;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,CAAC,2BAA2B,CAAC,EAAE,IAAI,CAAC;CACrC,CAAC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,gBAAgB,CAAC;IACxB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC,CAAC;AA8BH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,uBAAuB,GAAG,MAAM,CAEpF;AAED,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,uBAAuB,GAAG,MAAM,GAAG,SAAS,CAGhG;AAKD,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,WAAW,GAChB,4BAA4B,CAU9B;AAQD,wBAAgB,mCAAmC,CACjD,WAAW,EAAE,4BAA4B,GACxC,OAAO,CAUT;AAED,wBAAgB,gCAAgC,CAC9C,WAAW,EAAE,uBAAuB,EACpC,SAAS,EAAE,4BAA4B,GACtC,OAAO,CAMT;AAGD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,uBAAuB,GAAG,SAAS,CA0BzF;AAED,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,uBAAuB,GAAG,OAAO,CAaxF;AAID,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,uBAAuB,EACpC,MAAM,EAAE,4BAA4B,EACpC,KAAK,EAAE,4BAA4B,GAClC,uBAAuB,GAAG,SAAS,CAiBrC"}
1
+ {"version":3,"file":"pinned-mutation-observation.d.ts","sourceRoot":"","sources":["../src/pinned-mutation-observation.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAM/C,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf,CAAC,CAAC;AAEH,QAAA,MAAM,2BAA2B,eAAmD,CAAC;AAGrF,MAAM,MAAM,wBAAwB,GAAG,QAAQ,CAAC;IAC9C,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,gBAAgB,CAAC;CAC5B,CAAC,CAAC;AACH,MAAM,MAAM,yBAAyB,GAAG,MAAM,wBAAwB,GAAG,SAAS,CAAC;AAEnF,MAAM,MAAM,4BAA4B,GAAG,QAAQ,CAAC;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,CAAC,2BAA2B,CAAC,EAAE,IAAI,CAAC;CACrC,CAAC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,gBAAgB,CAAC;IACxB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC,CAAC;AA6BH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,uBAAuB,GAAG,MAAM,CAEpF;AAED,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,uBAAuB,GAAG,MAAM,GAAG,SAAS,CAGhG;AAKD,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC,EACzD,cAAc,CAAC,EAAE,yBAAyB,GACzC,4BAA4B,CAY9B;AAQD,wBAAgB,gCAAgC,CAC9C,WAAW,EAAE,uBAAuB,EACpC,SAAS,EAAE,4BAA4B,GACtC,OAAO,CAMT;AAGD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,uBAAuB,GAAG,SAAS,CA0BzF;AAED,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,uBAAuB,GAAG,OAAO,CAaxF;AAKD,wBAAgB,2BAA2B,CACzC,YAAY,EAAE,SAAS,uBAAuB,EAAE,EAChD,WAAW,EAAE,SAAS,4BAA4B,EAAE,GACnD,OAAO,CAiET;AAID,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,uBAAuB,EACpC,MAAM,EAAE,4BAA4B,EACpC,KAAK,EAAE,4BAA4B,GAClC,uBAAuB,GAAG,SAAS,CAiBrC"}
@@ -4,6 +4,7 @@ import { isNotFoundPathError } from "./path.js";
4
4
  import { realpathSync } from "./realpath.js";
5
5
  import { inspectFileIdentitySync } from "./strict-file-identity.js";
6
6
  const checkedDirectoryObservation = Symbol("checked mutation directory observation");
7
+ const directoryObservers = new WeakMap();
7
8
  function identity(stat) {
8
9
  return Object.freeze({ dev: stat.dev, ino: stat.ino, mode: stat.mode, nlink: stat.nlink });
9
10
  }
@@ -17,8 +18,7 @@ function directoryMode(mode) {
17
18
  }
18
19
  function absent(pathname) {
19
20
  try {
20
- fs.lstatSync(pathname);
21
- return false;
21
+ return fs.lstatSync(pathname, { throwIfNoEntry: false }) === undefined;
22
22
  }
23
23
  catch (error) {
24
24
  // ENOTDIR is incomplete evidence, not a missing child of a directory.
@@ -35,35 +35,25 @@ export function nextMissingMutationPath(observation) {
35
35
  // Callers may construct this only from their just-completed exact pathname and
36
36
  // canonical fences. The private brand prevents unverified structural objects
37
37
  // from advancing an admission epoch.
38
- export function checkedMutationDirectory(pathname, canonicalPath, stat) {
39
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
38
+ export function checkedMutationDirectory(pathname, canonicalPath, stat, observeCurrent) {
39
+ if (!directoryMode(stat.mode)) {
40
40
  throw new TypeError("mutation directory evidence must describe a real directory");
41
41
  }
42
- return Object.freeze({
42
+ const observation = Object.freeze({
43
43
  path: pathname,
44
44
  canonicalPath,
45
45
  identity: identity(stat),
46
46
  [checkedDirectoryObservation]: true,
47
47
  });
48
+ if (observeCurrent)
49
+ directoryObservers.set(observation, observeCurrent);
50
+ return observation;
48
51
  }
49
52
  function checkedDirectory(observation) {
50
53
  return observation[checkedDirectoryObservation] === true &&
51
54
  path.isAbsolute(observation.path) && path.isAbsolute(observation.canonicalPath) &&
52
55
  directoryMode(observation.identity.mode);
53
56
  }
54
- export function mutationDirectoryObservationCurrent(observation) {
55
- if (!checkedDirectory(observation))
56
- return false;
57
- try {
58
- const stat = inspectFileIdentitySync(() => fs.lstatSync(observation.path, { bigint: true }));
59
- return stat.isDirectory() && !stat.isSymbolicLink() &&
60
- sameIdentity(observation.identity, stat) &&
61
- realpathSync.native(observation.path) === observation.canonicalPath;
62
- }
63
- catch {
64
- return false;
65
- }
66
- }
67
57
  export function mutationObservationUsesDirectory(observation, directory) {
68
58
  return checkedDirectory(directory) &&
69
59
  observation.ancestor === directory.path &&
@@ -118,6 +108,79 @@ export function mutationObservationCurrent(observation) {
118
108
  return false;
119
109
  }
120
110
  }
111
+ // Share only ordinary spelling observations collected in this synchronous
112
+ // phase. Aliases retain their independent raw/canonical binding checks, and no
113
+ // fact escapes this call or crosses an authority callback or mutation.
114
+ export function mutationObservationsCurrent(observations, directories) {
115
+ const groups = new Map();
116
+ const groupFor = (pathname) => {
117
+ let group = groups.get(pathname);
118
+ if (!group) {
119
+ group = { observations: [], directories: [] };
120
+ groups.set(pathname, group);
121
+ }
122
+ return group;
123
+ };
124
+ try {
125
+ for (const observation of observations) {
126
+ if (observation.ancestor !== observation.canonicalAncestor) {
127
+ if (!mutationObservationCurrent(observation))
128
+ return false;
129
+ }
130
+ else {
131
+ groupFor(observation.ancestor).observations.push(observation);
132
+ }
133
+ }
134
+ for (const directory of directories) {
135
+ if (!checkedDirectory(directory))
136
+ return false;
137
+ groupFor(directory.path).directories.push(directory);
138
+ }
139
+ const current = (pathname, group) => {
140
+ const missing = new Set(group.observations.flatMap((observation) => {
141
+ const next = nextMissingMutationPath(observation);
142
+ return next === undefined ? [] : [next];
143
+ }));
144
+ // Exact directory roles are the end fence. Observe missing children
145
+ // before refreshing that parent, matching the ordered admission fence.
146
+ if (group.directories.length && ![...missing].every(absent))
147
+ return false;
148
+ const observed = group.directories.length
149
+ ? directoryObservers.get(group.directories[0])?.() : undefined;
150
+ const stat = observed?.identity ??
151
+ inspectFileIdentitySync(() => fs.lstatSync(pathname, { bigint: true }));
152
+ const canonical = observed?.canonicalPath ?? realpathSync.native(pathname);
153
+ const isDirectory = directoryMode(stat.mode);
154
+ if (!group.observations.every((observation) => canonical === observation.canonicalAncestor &&
155
+ sameIdentity(observation.entry, stat, isDirectory) &&
156
+ sameIdentity(observation.identity, stat, isDirectory) &&
157
+ (missingMutationSegments(observation) === 0 || isDirectory)))
158
+ return false;
159
+ if (!group.directories.every((directory) => isDirectory &&
160
+ sameIdentity(directory.identity, stat) && canonical === directory.canonicalPath))
161
+ return false;
162
+ return group.directories.length > 0 || [...missing].every(absent);
163
+ };
164
+ for (const [pathname, group] of groups) {
165
+ if (!group.directories.length && !current(pathname, group))
166
+ return false;
167
+ }
168
+ // Preserve the supplied parent/child order, even when a directory also
169
+ // appeared earlier as the Root or a denied path's nearest ancestor.
170
+ const checked = new Set();
171
+ for (const directory of directories) {
172
+ if (checked.has(directory.path))
173
+ continue;
174
+ if (!current(directory.path, groups.get(directory.path)))
175
+ return false;
176
+ checked.add(directory.path);
177
+ }
178
+ return true;
179
+ }
180
+ catch {
181
+ return false;
182
+ }
183
+ }
121
184
  // This is deliberately a pure tentative update. The immediately following
122
185
  // parent authorization validates the complete epoch once before reusing it.
123
186
  export function advanceMutationObservation(observation, parent, child) {
@@ -9,6 +9,7 @@ type SharedMutationPolicy = Readonly<{
9
9
  denyMutations?: DenyMutationPolicy;
10
10
  mutationSymlinks?: MutationSymlinkPolicy;
11
11
  }>;
12
+ export declare function ordinaryWindowsSegments(relativePath: string): boolean;
12
13
  export declare function ordinarySharedAbsoluteInsideRoot(rootReal: string, candidatePath: string, rootIdentity: ExactRootIdentity): boolean;
13
14
  export declare function simpleSharedRoute(params: {
14
15
  rootReal: string;