@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
@@ -116,6 +116,30 @@ function assertSnapshot(entry, uid) {
116
116
  assertTrustedTempWorkspaceDirectory(stat, uid);
117
117
  assertCanonicalRoot(entry);
118
118
  }
119
+ function snapshotMissingRootComponent(parent, dir, ownerUid) {
120
+ if (parent.dir !== parent.realPath) {
121
+ assertSnapshot(parent, ownerUid);
122
+ return snapshot(dir, ownerUid);
123
+ }
124
+ const current = inspectSnapshotIdentity(parent);
125
+ assertTrustedTempWorkspaceDirectory(current, ownerUid);
126
+ let realPath;
127
+ try {
128
+ realPath = canonicalTempWorkspacePath(dir);
129
+ }
130
+ catch (error) {
131
+ // Keep parent resolution failures ahead of a missing or unreadable child.
132
+ assertCanonicalRoot(parent);
133
+ throw error;
134
+ }
135
+ if (path.dirname(realPath) !== parent.realPath) {
136
+ assertCanonicalRoot(parent);
137
+ return snapshot(dir, ownerUid);
138
+ }
139
+ // The child's canonical parent confirms the same parent name at this phase.
140
+ // Its exact non-symlink/owner observation still precedes mode initialization.
141
+ return snapshot(dir, ownerUid, realPath);
142
+ }
119
143
  function assertChain(chain, uid) {
120
144
  for (const entry of chain) {
121
145
  const current = inspectSnapshotIdentity(entry);
@@ -321,6 +345,7 @@ export async function admitTempWorkspaceRoot(rootDir) {
321
345
  return admission;
322
346
  const guardedChain = chain;
323
347
  for (const segment of missing) {
348
+ const parentEntry = guardedChain[guardedChain.length - 1];
324
349
  const parent = rootAdmission(guardedChain, ownerUid);
325
350
  const dir = path.join(parent.dir, segment);
326
351
  assertNoWindowsPathAlias(dir, "filesystem");
@@ -334,8 +359,7 @@ export async function admitTempWorkspaceRoot(rootDir) {
334
359
  if (error.code !== "EEXIST")
335
360
  throw error;
336
361
  }
337
- parent.assertCurrent();
338
- const observed = snapshot(dir, ownerUid);
362
+ const observed = snapshotMissingRootComponent(parentEntry, dir, ownerUid);
339
363
  if (created) {
340
364
  const modeInitialization = admitTempWorkspaceChild(dir, observed.stat, parent, 0o700);
341
365
  if (modeInitialization)
@@ -351,6 +375,7 @@ export function admitTempWorkspaceRootSync(rootDir) {
351
375
  return admission;
352
376
  const guardedChain = chain;
353
377
  for (const segment of missing) {
378
+ const parentEntry = guardedChain[guardedChain.length - 1];
354
379
  const parent = rootAdmission(guardedChain, ownerUid);
355
380
  const dir = path.join(parent.dir, segment);
356
381
  assertNoWindowsPathAlias(dir, "filesystem");
@@ -364,8 +389,7 @@ export function admitTempWorkspaceRootSync(rootDir) {
364
389
  if (error.code !== "EEXIST")
365
390
  throw error;
366
391
  }
367
- parent.assertCurrent();
368
- const observed = snapshot(dir, ownerUid);
392
+ const observed = snapshotMissingRootComponent(parentEntry, dir, ownerUid);
369
393
  if (created)
370
394
  admitTempWorkspaceChildSync(dir, observed.stat, parent, 0o700);
371
395
  guardedChain.push(observed.entry);
@@ -1 +1 @@
1
- {"version":3,"file":"temp-workspace-descriptor.d.ts","sourceRoot":"","sources":["../src/temp-workspace-descriptor.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;AAU7B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAMhF,OAAO,EAML,KAAK,yBAAyB,EAC/B,MAAM,8BAA8B,CAAC;AAEtC,KAAK,yBAAyB,GAAG,MAAM,GAAG,QAAQ,CAAC;AASnD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,yBAAyB,CAAC;IAClC,OAAO,EAAE,QAAQ,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,QAAQ,CAAC;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAClD,CAAC,CAAC;CACJ,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAmDpD,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,0BAA0B,GACpC,iBAAiB,CA8BnB;AAED,+EAA+E;AAC/E,qBAAa,0BAA0B;;IAWrC,OAAO,eAqBN;IAED,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB,GAAG,0BAA0B,CAcjF;IAED,MAAM,CAAC,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG;QACjC,QAAQ,EAAE,0BAA0B,CAAC;QACrC,IAAI,EAAE,MAAM,CAAC,WAAW,CAAC;KAC1B,CAoBA;IAED,qBAAqB,IAAI,IAAI,CAE5B;IAUD,iBAAiB,CACf,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,IAAI,EAAE,MAAM,GACX,yBAAyB,CAsB3B;IAoEK,cAAc,CAClB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,YAAY,EAAE,MAAM,IAAI,GACvB,OAAO,CAAC,IAAI,CAAC,CAgCf;IAED,kBAAkB,CAChB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,YAAY,EAAE,MAAM,IAAI,GACvB,IAAI,CAsBN;IAED,IAAI,YAAY,IAAI,OAAO,CAE1B;IAED,cAAc,IAAI,OAAO,CAmDxB;IAED,QAAQ,CAAC,gBAAgB,EAAE,OAAO,GAAG;QACnC,GAAG,EAAE,MAAM,CAAC;QACZ,QAAQ,EAAE,QAAQ,CAAC;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACjD,SAAS,EAAE,sBAAsB,GAAG,SAAS,CAAC;KAC/C,CAiCA;IAED,KAAK,IAAI,IAAI,CAOZ;CACF"}
1
+ {"version":3,"file":"temp-workspace-descriptor.d.ts","sourceRoot":"","sources":["../src/temp-workspace-descriptor.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;AAU7B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAMhF,OAAO,EAML,KAAK,yBAAyB,EAC/B,MAAM,8BAA8B,CAAC;AAEtC,KAAK,yBAAyB,GAAG,MAAM,GAAG,QAAQ,CAAC;AASnD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,yBAAyB,CAAC;IAClC,OAAO,EAAE,QAAQ,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,QAAQ,CAAC;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAClD,CAAC,CAAC;CACJ,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAmDpD,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,0BAA0B,GACpC,iBAAiB,CA8BnB;AAED,+EAA+E;AAC/E,qBAAa,0BAA0B;;IAWrC,OAAO,eAqBN;IAED,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB,GAAG,0BAA0B,CAcjF;IAED,MAAM,CAAC,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG;QACjC,QAAQ,EAAE,0BAA0B,CAAC;QACrC,IAAI,EAAE,MAAM,CAAC,WAAW,CAAC;KAC1B,CAoBA;IAED,qBAAqB,IAAI,IAAI,CAE5B;IAUD,iBAAiB,CACf,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,IAAI,EAAE,MAAM,GACX,yBAAyB,CAsB3B;IAgEK,cAAc,CAClB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,YAAY,EAAE,MAAM,IAAI,GACvB,OAAO,CAAC,IAAI,CAAC,CAgCf;IAED,kBAAkB,CAChB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,YAAY,EAAE,MAAM,IAAI,GACvB,IAAI,CAsBN;IAED,IAAI,YAAY,IAAI,OAAO,CAE1B;IAED,cAAc,IAAI,OAAO,CA2CxB;IAED,QAAQ,CAAC,gBAAgB,EAAE,OAAO,GAAG;QACnC,GAAG,EAAE,MAAM,CAAC;QACZ,QAAQ,EAAE,QAAQ,CAAC;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACjD,SAAS,EAAE,sBAAsB,GAAG,SAAS,CAAC;KAC/C,CA0BA;IAED,KAAK,IAAI,IAAI,CAOZ;CACF"}
@@ -185,18 +185,15 @@ export class TempWorkspaceRetainedChild {
185
185
  return descriptor;
186
186
  }
187
187
  async #assertProcAuthority(fd, ownerUid) {
188
- if ((await fs.statfs("/proc/self/fd", { bigint: true })).type !== 0x9fa0n) {
189
- throw new FsSafeError("path-mismatch", "directory mode requires a trusted procfs fd namespace");
190
- }
191
- const procPath = `/proc/self/fd/${fd}`;
192
- const opened = inspectFileIdentitySync(() => fsSync.fstatSync(fd, { bigint: true }), this.#identity);
193
- const followed = inspectFileIdentitySync(() => fsSync.statSync(procPath, { bigint: true }), this.#identity);
194
- assertOwnedDirectory(opened, followed);
195
- assertTempWorkspaceChildState(opened, ownerUid);
196
- assertTempWorkspaceChildState(followed, ownerUid);
188
+ const filesystem = await fs.statfs("/proc/self/fd", { bigint: true });
189
+ this.#assertProcDescriptorAuthority(fd, ownerUid, filesystem.type);
197
190
  }
198
191
  #assertProcAuthoritySync(fd, ownerUid) {
199
- if (fsSync.statfsSync("/proc/self/fd", { bigint: true }).type !== 0x9fa0n) {
192
+ const filesystem = fsSync.statfsSync("/proc/self/fd", { bigint: true });
193
+ this.#assertProcDescriptorAuthority(fd, ownerUid, filesystem.type);
194
+ }
195
+ #assertProcDescriptorAuthority(fd, ownerUid, filesystemType) {
196
+ if (filesystemType !== 0x9fa0n) {
200
197
  throw new FsSafeError("path-mismatch", "directory mode requires a trusted procfs fd namespace");
201
198
  }
202
199
  const procPath = `/proc/self/fd/${fd}`;
@@ -309,13 +306,7 @@ export class TempWorkspaceRetainedChild {
309
306
  catch (error) {
310
307
  // The readable replacement has not been installed. Give it exactly one
311
308
  // close attempt and leave neither indeterminate descriptor owned here.
312
- try {
313
- fsSync.closeSync(fd);
314
- }
315
- catch (closeError) {
316
- throw new AggregateError([error, closeError], "temp workspace child descriptor replacement close failed");
317
- }
318
- throw error;
309
+ closeAfterAdmissionFailure(fd, error, "temp workspace child descriptor replacement close failed");
319
310
  }
320
311
  this.#fd = fd;
321
312
  this.#access = "read";
@@ -334,16 +325,7 @@ export class TempWorkspaceRetainedChild {
334
325
  const fd = this.#fd;
335
326
  this.#fd = undefined;
336
327
  if (retainDescriptor && !this.canEnumerate) {
337
- try {
338
- fsSync.closeSync(fd);
339
- }
340
- catch (closeError) {
341
- throw new AggregateError([
342
- new FsSafeError("helper-unavailable", "temp workspace cleanup requires a readable child descriptor"),
343
- closeError,
344
- ], "temp workspace child descriptor rejection and close failed");
345
- }
346
- throw new FsSafeError("helper-unavailable", "temp workspace cleanup requires a readable child descriptor");
328
+ closeAfterAdmissionFailure(fd, new FsSafeError("helper-unavailable", "temp workspace cleanup requires a readable child descriptor"), "temp workspace child descriptor rejection and close failed");
347
329
  }
348
330
  if (!retainDescriptor) {
349
331
  fsSync.closeSync(fd);
@@ -1 +1 @@
1
- {"version":3,"file":"temp-workspace-owner.d.ts","sourceRoot":"","sources":["../src/temp-workspace-owner.ts"],"names":[],"mappings":"AAOA,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAEnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAChF,OAAO,EAEL,0BAA0B,EAE1B,KAAK,iBAAiB,EACvB,MAAM,gCAAgC,CAAC;AAGxC,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,SAAS,GAAG,mBAAmB,GAAG,eAAe,CAAC;AACvG,MAAM,MAAM,0BAA0B,GAAG,YAAY,GAAG,iBAAiB,CAAC;AAsB1E,qBAAa,8BAA8B;;IACzC,QAAQ,CAAC,OAAO,EAAE,aAAa,GAAG,SAAS,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAM/C,YACE,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,0BAA0B,EAClC,SAAS,EAAE,0BAA0B,EACrC,OAAO,EAAE,MAAM,EAuEhB;IAED,IAAI,kBAAkB,IAAI,OAAO,CAGhC;IAED,oBAAoB,IAAI,IAAI,CAiB3B;IAUD,oBAAoB,CAAC,YAAY,EAAE,OAAO,GAAG,OAAO,CAenD;IAED,aAAa,IAAI,IAAI,CAEpB;IAED,qBAAqB,IAAI,IAAI,CAE5B;IAED,KAAK,IAAI,IAAI,CAIZ;CACF;AAED,qBAAa,yBAAyB;;IAWpC,YACE,QAAQ,EAAE,0BAA0B,EACpC,UAAU,EAAE,8BAA8B,EAC1C,gBAAgB,EAAE,OAAO,EAO1B;IAkLD,OAAO,IAAI,OAAO,CAAC,0BAA0B,CAAC,CAM7C;IAED,WAAW,IAAI,0BAA0B,CAexC;CACF"}
1
+ {"version":3,"file":"temp-workspace-owner.d.ts","sourceRoot":"","sources":["../src/temp-workspace-owner.ts"],"names":[],"mappings":"AAOA,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAEnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAChF,OAAO,EAEL,0BAA0B,EAE1B,KAAK,iBAAiB,EACvB,MAAM,gCAAgC,CAAC;AAGxC,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,SAAS,GAAG,mBAAmB,GAAG,eAAe,CAAC;AACvG,MAAM,MAAM,0BAA0B,GAAG,YAAY,GAAG,iBAAiB,CAAC;AAuB1E,qBAAa,8BAA8B;;IACzC,QAAQ,CAAC,OAAO,EAAE,aAAa,GAAG,SAAS,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAM/C,YACE,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,0BAA0B,EAClC,SAAS,EAAE,0BAA0B,EACrC,OAAO,EAAE,MAAM,EAuEhB;IAED,IAAI,kBAAkB,IAAI,OAAO,CAGhC;IAED,oBAAoB,IAAI,IAAI,CAiB3B;IAUD,oBAAoB,CAAC,YAAY,EAAE,OAAO,GAAG,OAAO,CAenD;IAED,aAAa,IAAI,IAAI,CAEpB;IAED,qBAAqB,IAAI,IAAI,CAE5B;IAED,KAAK,IAAI,IAAI,CAIZ;CACF;AAED,qBAAa,yBAAyB;;IAWpC,YACE,QAAQ,EAAE,0BAA0B,EACpC,UAAU,EAAE,8BAA8B,EAC1C,gBAAgB,EAAE,OAAO,EAO1B;IAkLD,OAAO,IAAI,OAAO,CAAC,0BAA0B,CAAC,CAM7C;IAED,WAAW,IAAI,0BAA0B,CAexC;CACF"}
@@ -294,22 +294,22 @@ export class TempWorkspaceCleanupOwner {
294
294
  await beforeNativeRemoval(quarantine.path);
295
295
  return this.#mapRemoval(await this.#capability.binding.removeOwnedTree(this.#capability.parent.fd, quarantine.name, this.#directory.fd));
296
296
  }
297
- let removalError;
297
+ let removalFailure;
298
298
  try {
299
299
  this.#assertQuarantine(quarantine);
300
300
  try {
301
301
  await fs.rm(quarantine.path, { recursive: true, force: true });
302
302
  }
303
303
  catch (error) {
304
- removalError = error;
304
+ removalFailure = { error };
305
305
  throw error;
306
306
  }
307
307
  this.#capability.assertCurrent();
308
308
  return "removed";
309
309
  }
310
310
  catch (error) {
311
- if (error === removalError)
312
- throw error;
311
+ if (removalFailure !== undefined)
312
+ throw removalFailure.error;
313
313
  return "indeterminate";
314
314
  }
315
315
  }
@@ -318,22 +318,22 @@ export class TempWorkspaceCleanupOwner {
318
318
  getFsSafeTestHooks()?.beforeTempWorkspaceNativeRemovalSync?.(quarantine.path);
319
319
  return this.#mapRemoval(this.#capability.binding.removeOwnedTreeSync(this.#capability.parent.fd, quarantine.name, this.#directory.fd));
320
320
  }
321
- let removalError;
321
+ let removalFailure;
322
322
  try {
323
323
  this.#assertQuarantine(quarantine);
324
324
  try {
325
325
  fsSync.rmSync(quarantine.path, { recursive: true, force: true });
326
326
  }
327
327
  catch (error) {
328
- removalError = error;
328
+ removalFailure = { error };
329
329
  throw error;
330
330
  }
331
331
  this.#capability.assertCurrent();
332
332
  return "removed";
333
333
  }
334
334
  catch (error) {
335
- if (error === removalError)
336
- throw error;
335
+ if (removalFailure !== undefined)
336
+ throw removalFailure.error;
337
337
  return "indeterminate";
338
338
  }
339
339
  }
package/dist/walk.d.ts CHANGED
@@ -16,6 +16,10 @@ export type WalkDirectoryOptions = {
16
16
  include?: (entry: WalkDirectoryEntry) => boolean;
17
17
  descend?: (entry: WalkDirectoryEntry) => boolean;
18
18
  };
19
+ export type AsyncWalkDirectoryOptions = Omit<WalkDirectoryOptions, "include" | "descend"> & {
20
+ include?: (entry: WalkDirectoryEntry) => boolean | Promise<boolean>;
21
+ descend?: (entry: WalkDirectoryEntry) => boolean | Promise<boolean>;
22
+ };
19
23
  export type WalkDirectoryFailure = {
20
24
  path: string;
21
25
  relativePath: string;
@@ -32,6 +36,6 @@ type WalkDirectoryResultWithFailures = WalkDirectoryResult & {
32
36
  failedDirs: WalkDirectoryFailure[];
33
37
  };
34
38
  export declare function walkDirectorySync(rootDir: string, options?: WalkDirectoryOptions): WalkDirectoryResultWithFailures;
35
- export declare function walkDirectory(rootDir: string, options?: WalkDirectoryOptions): Promise<WalkDirectoryResultWithFailures>;
39
+ export declare function walkDirectory(rootDir: string, options?: AsyncWalkDirectoryOptions): Promise<WalkDirectoryResultWithFailures>;
36
40
  export {};
37
41
  //# sourceMappingURL=walk.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;AAS7B,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AACvE,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE9D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;IACjD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;IAGnB,UAAU,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACrC,CAAC;AAEF,KAAK,+BAA+B,GAAG,mBAAmB,GAAG;IAC3D,UAAU,EAAE,oBAAoB,EAAE,CAAC;CACpC,CAAC;AA+EF,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,+BAA+B,CA2DjC;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,+BAA+B,CAAC,CA2D1C"}
1
+ {"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;AAS7B,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AACvE,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE9D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;IACjD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAAC,oBAAoB,EAAE,SAAS,GAAG,SAAS,CAAC,GAAG;IAC1F,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACpE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACrE,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;IAGnB,UAAU,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACrC,CAAC;AAEF,KAAK,+BAA+B,GAAG,mBAAmB,GAAG;IAC3D,UAAU,EAAE,oBAAoB,EAAE,CAAC;CACpC,CAAC;AAoFF,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,+BAA+B,CA4DjC;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,+BAA+B,CAAC,CAiE1C"}
package/dist/walk.js CHANGED
@@ -16,6 +16,10 @@ function validateWalkOptions(options) {
16
16
  throw new TypeError(`invalid walk symlink policy: ${String(options.symlinks)}`);
17
17
  }
18
18
  }
19
+ function isObjectResult(result) {
20
+ return result !== null &&
21
+ (typeof result === "object" || typeof result === "function");
22
+ }
19
23
  function kindForDirent(dirent) {
20
24
  if (dirent.isDirectory())
21
25
  return "directory";
@@ -85,6 +89,8 @@ export function walkDirectorySync(rootDir, options = {}) {
85
89
  let realDir;
86
90
  const operationPath = pathForWindowsFilesystem(dir);
87
91
  try {
92
+ if (depth > 1 && symlinks !== "follow" && fsSync.lstatSync(operationPath).isSymbolicLink())
93
+ return;
88
94
  realDir = realpathSync(operationPath);
89
95
  }
90
96
  catch (error) {
@@ -146,6 +152,8 @@ export async function walkDirectory(rootDir, options = {}) {
146
152
  let realDir;
147
153
  const operationPath = pathForWindowsFilesystem(dir);
148
154
  try {
155
+ if (depth > 1 && symlinks !== "follow" && fsSync.lstatSync(operationPath).isSymbolicLink())
156
+ return;
149
157
  realDir = realpathSync.native(operationPath);
150
158
  }
151
159
  catch (error) {
@@ -175,15 +183,20 @@ export async function walkDirectory(rootDir, options = {}) {
175
183
  continue;
176
184
  const relativePath = relativeDir ? `${relativeDir}${path.sep}${dirent.name}` : dirent.name;
177
185
  const entry = buildEntry({ relativePath, fullPath, dirent, depth, kind });
178
- if (options.include?.(entry) ?? true) {
186
+ const include = options.include;
187
+ const included = include == null ? true : Reflect.apply(include, options, [entry]);
188
+ if ((isObjectResult(included) ? await included : included) ?? true) {
179
189
  result.entries.push(entry);
180
190
  }
181
191
  if (kind === "directory" &&
182
- (options.maxDepth === undefined || depth < options.maxDepth) &&
183
- (options.descend?.(entry) ?? true)) {
184
- await visit(fullPath, relativePath, depth + 1);
185
- if (result.truncated)
186
- return;
192
+ (options.maxDepth === undefined || depth < options.maxDepth)) {
193
+ const descend = options.descend;
194
+ const descended = descend == null ? true : Reflect.apply(descend, options, [entry]);
195
+ if ((isObjectResult(descended) ? await descended : descended) ?? true) {
196
+ await visit(fullPath, relativePath, depth + 1);
197
+ if (result.truncated)
198
+ return;
199
+ }
187
200
  }
188
201
  }
189
202
  }
@@ -1 +1 @@
1
- {"version":3,"file":"windows-owner.d.ts","sourceRoot":"","sources":["../src/windows-owner.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,sBAAsB,CAAC;AAI9B,MAAM,MAAM,gBAAgB,GAAG,CAC7B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,KACX,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEjD,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,IAAI,CAAC,EAAE,eAAe,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,wBAAwB,CAAC;IACvC,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAqDF,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,EAAE,gBAAgB,CAAC;CACxB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAmD/B"}
1
+ {"version":3,"file":"windows-owner.d.ts","sourceRoot":"","sources":["../src/windows-owner.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,wBAAwB,EAC9B,MAAM,sBAAsB,CAAC;AAI9B,MAAM,MAAM,gBAAgB,GAAG,CAC7B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,KACX,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEjD,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,IAAI,CAAC,EAAE,eAAe,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,wBAAwB,CAAC;IACvC,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAqDF,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,EAAE,gBAAgB,CAAC;CACxB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAmD/B"}
@@ -1,4 +1,4 @@
1
- import { formatPermissionErrorDetail, getPermissionCommandFailure, } from "./permission-exec.js";
1
+ import { formatCaughtPermissionFailure, formatPermissionErrorDetail, getPermissionCommandFailure, } from "./permission-exec.js";
2
2
  import { resolveWindowsSystemCommand } from "./windows-command.js";
3
3
  import { hasWindowsPathAlias } from "./windows-path-alias.js";
4
4
  const SID_RE = /^\*?s-\d+-\d+(-\d+)+$/i;
@@ -89,7 +89,7 @@ export async function inspectWindowsOwner(params) {
89
89
  }
90
90
  catch (err) {
91
91
  return {
92
- error: formatPermissionErrorDetail(String(err)),
92
+ error: formatCaughtPermissionFailure(err),
93
93
  errorDetail: getPermissionCommandFailure(err, command, performance.now() - startedAt),
94
94
  errorCause: err,
95
95
  };
package/docs/advanced.md CHANGED
@@ -69,7 +69,7 @@ Operational filesystem failures such as permissions or I/O errors are rethrown.
69
69
  |---|---|---|
70
70
  | `readFileDescriptorBounded`, `readFileDescriptorBoundedSync`, `readFileHandleBounded` | – | Incremental whole-file reads for already-open descriptors/handles. They consume at most `maxBytes + 1`, do not close the input, and throw `FsSafeError("too-large")` on overflow. |
71
71
  | `readFileWindowFully`, `readFileWindowFullySync`, `ReadFileWindowOptions` | [positional-read.md](positional-read.md) | Fill a caller-owned buffer at an explicit file position, completing short reads and returning the EOF count without moving or closing the descriptor. |
72
- | `copyFileHandle`, `CopyFileHandleOptions` | [copy.md](copy.md#borrowed-filehandle-transfers) | Copy caller-owned regular-file handles from position zero with byte limits, settled cancellation, and a synchronous source observer; preserves cursors and leaves publication and cleanup to the caller. |
72
+ | `copyFileHandle`, `copyFileDescriptorSync`, `CopyFileHandleOptions` | [copy.md](copy.md#borrowed-filehandle-transfers) | Copy caller-owned regular files through async handles or sync descriptors from position zero with byte limits and synchronous callbacks; preserves cursors and leaves publication and cleanup to the caller. |
73
73
  | `overwriteFileHandle`, `OverwriteFileHandleOptions` | [in-place-write.md](in-place-write.md) | Overwrite a borrowed read/write handle with prefix-only preparation and best-effort rollback; preserves its inode, cursor, and caller-owned lifetime. |
74
74
  | `openRootFile`, `openRootFileSync`, `canUseRootFileOpen`, `matchRootFileOpenFailure`, related types | – | Low-level root-bounded open; rejects every symlink component by default, with `symlinks: "follow-parents-within-root"` for contained parent aliases or `"follow-within-root"` for final links too. |
75
75
  | `appendRegularFile`, `appendRegularFileSync`, `readRegularFile`, `readRegularFileSync`, `statRegularFile`, `statRegularFileSync`, `resolveRegularFileAppendFlags`, `AppendRegularFileOptions`, `RegularFileStatResult` | [regular-file.md](regular-file.md) | Type-checked regular-file I/O. |
package/docs/archive.md CHANGED
@@ -124,7 +124,12 @@ Readable directories do not depend on procfs. A Linux search-only directory
124
124
  needing a mode change requires accessible, genuine procfs; unavailable or
125
125
  untrusted authority rejects explicitly instead of silently accepting a wrong
126
126
  mode. Other unsupported search-only routes also fail closed. Windows retains
127
- its existing bounded lack of POSIX mode enforcement.
127
+ its existing bounded lack of POSIX mode enforcement. Best-effort mode handling
128
+ applies only to the mode change itself: an authority or deadline check that
129
+ fails immediately before dispatch still propagates, including a custom
130
+ one-shot structural check. A check failure observed immediately after dispatch
131
+ is retained while post-dispatch authority verification and final mode inspection
132
+ settle, then propagated with its exact JavaScript value, including falsy values.
128
133
 
129
134
  Extraction and TAR inspection first copy the admitted source into a private
130
135
  staging file. This copy reuses at most 512 KiB of scratch space, reduced for
@@ -148,14 +153,24 @@ must agree with this kind, physical index, size, known path, and UNIX-creator mo
148
153
  before extraction or any member read. Bounded ZIP reads retain this metadata from
149
154
  their single admission pass without another input copy or scan.
150
155
 
151
- Portable ZIP preflight, extraction, and reads also check the decoded kind against
152
- admission. Unsupported JSZip metadata rejects with `ArchiveFormatError` before
153
- filters, including UNIX-only directory attributes without a terminal slash or DOS
154
- directory bit, backslash-only directory names without directory attributes, and
155
- non-UNIX creators whose high-word symlink mode JSZip does not expose. Symlinks that
156
- the decoder represents faithfully remain subject to the existing filter and
157
- blocked-link policy. UNIX creator metadata and permission defaults remain as
158
- described above.
156
+ Portable ZIP preflight, extraction, and reads bind every decoder insertion to
157
+ its admitted physical record before JSZip can coerce its type or discard its
158
+ payload. Names, physical order, original directory/permission metadata,
159
+ compression method, compressed and decoded sizes, and CRC must agree. The
160
+ private loader then applies the admitted kind, preserving UNIX-only directory
161
+ attributes, backslash-only directories, and high-word symlinks from any creator.
162
+ Symlinks remain subject to the existing filter and blocked-link policy.
163
+ UNIX socket and block-device type bits do not turn regular-file payloads into
164
+ empty directories. Directory and symlink callbacks receive their physical
165
+ declared sizes; directory bodies are not published as files. Unsupported
166
+ link-like types remain visible as `other` and are safely omitted when accepted.
167
+ UNIX creator metadata and permission defaults remain as described above.
168
+
169
+ The loader adapter belongs to one private JSZip instance and is removed after
170
+ loading, including failure. Public preflight still returns ordinary JSZip entry
171
+ objects, with directory keys ending in `/` and recognizable symlink type bits.
172
+ Compressed data is retained even for declared-zero entries, so an empty-size
173
+ claim cannot bypass payload-size or CRC verification during extraction or reads.
159
174
 
160
175
  Within one ZIP entry, identical local and central name bytes reuse the same
161
176
  decoded validation. Unicode Path admission is shared only when both the raw names
@@ -269,11 +284,23 @@ A failure before publication preserves a pre-existing file, and rejection does
269
284
  not grant authority to delete a substituted file or alias. Failed extraction does not
270
285
  restore overwritten contents. Active destination mutations and their guarded
271
286
  cleanup still finish before rejection; no later destination mutation begins.
287
+ Portable ZIP output is not eligible for publication until its stream closes or
288
+ the defensive `FileHandle` close succeeds. If that fallback close rejects,
289
+ `extractArchive()` propagates the error and publishes no entry from the staged
290
+ tree. Cleanup retains its best-effort `FileHandle` close; it does not transfer
291
+ the descriptor to a raw or native closer.
272
292
  New directories whose finalization was never reached can retain their
273
293
  private working mode after failure. Failure cleanup closes retained descriptors;
274
294
  it does not run a cleanup chmod sweep or roll back the archive. The public merge
275
295
  helper still derives modes from its external source tree and must be able to
276
296
  read that source; it never chmods an unreadable external source to admit it.
297
+ It retains the source root and each active child directory's exact identity
298
+ through traversal and copy verification. Each source file is opened once,
299
+ admitted against that root and its earlier exact identity observation, and
300
+ copied from the admitted descriptor; public file modes use that descriptor's
301
+ ordinary permission bits. Replacing a source ancestor or leaf rejects the
302
+ merge before replacement bytes can be published. These checks do not provide
303
+ a snapshot against writes to the same source inode.
277
304
  That helper retains per-copy durability and immediate postorder directory-mode
278
305
  finalization; the deferred pass described above belongs to `extractArchive()`.
279
306
 
package/docs/atomic.md CHANGED
@@ -92,6 +92,17 @@ await replaceFileAtomic({
92
92
 
93
93
  If `beforeRename` throws, the rename is skipped and the owned temp file is removed — the destination is unchanged. Cleanup unlinks only the exact admitted single-link file; a substitute observed at the temp name is preserved and removed from cleanup authority. The same identity is rechecked before every rename retry, when entering copy fallback, and at the final name after rename. A post-rename verification failure reports the race without rolling back or deleting the published name.
94
94
 
95
+ JavaScript permits `beforeRename` callbacks to throw any value, including
96
+ `undefined`, `null`, `false`, signed zero, `0n`, an empty string, and `NaN`.
97
+ Once such an operation failure reaches temp-owner settlement, atomic replacement
98
+ preserves that value when cleanup and close succeed. With
99
+ `throwOnCleanupError: true`, an additional owned-temp cleanup failure keeps the
100
+ existing cleanup wrapper whose `cause` is the original thrown value. A later
101
+ descriptor-close failure is reported in an `AggregateError`, in operation/cleanup
102
+ then close order. The default `throwOnCleanupError: false` omits only the cleanup
103
+ failure: the temp stays registered for identity-checked process-exit cleanup, the
104
+ descriptor is still closed, and a close failure remains reportable.
105
+
95
106
  Identity checks and pathname rename/unlink remain separate syscalls, not atomic conditional mutations. Use an approved writable parent plus cooperative locking or OS isolation when arbitrary concurrent namespace mutation is in scope.
96
107
 
97
108
  ### FUSE, Windows exFAT/FAT32, and unstable rename identity
@@ -148,6 +159,19 @@ that same descriptor, and synchronizes the result. Any write, mode, or sync
148
159
  failure triggers a byte-and-mode restore and another sync through the same
149
160
  descriptor.
150
161
 
162
+ With `syncTempFile: false`, an exclusive-create copy fallback does not report
163
+ success until its new destination writer closes successfully. This includes
164
+ `"restore-original"` when the destination did not exist. A close rejection or throw is
165
+ propagated exactly, including falsy values. The destination may already contain
166
+ all or part of the replacement, so a close failure does not prove that the old
167
+ destination survived or that the replacement was published. The outer atomic
168
+ operation still attempts identity-bound cleanup of its owned source temp; an
169
+ unverifiable or substituted temp remains preserved. If writing, mode adjustment,
170
+ or another earlier operation also fails, that earlier value remains the reported
171
+ failure and the destination close is attempted once. Successful synchronized
172
+ fallbacks and in-place `"restore-original"` replacements retain their existing best-effort final-close
173
+ handling.
174
+
151
175
  Restore failures are `FsSafeError("helper-failed")` values with typed
152
176
  `details.cleanup` set to `"restored"` or `"restore-failed"`. An original larger
153
177
  than `maxRestoreBytes` fails with `too-large` before mutation. A missing
@@ -168,7 +192,9 @@ synchronous boot paths or test setup code. It returns the same
168
192
 
169
193
  ## `replaceDirectoryAtomic`
170
194
 
171
- Atomically swap one directory's contents with another, using a temporary backup during the swap.
195
+ Publish one staged directory at a target without overwriting a concurrently
196
+ created entry. Despite the historical name, replacing an existing target is a
197
+ guarded two-rename protocol, not an atomic directory exchange.
172
198
 
173
199
  ```ts
174
200
  import { replaceDirectoryAtomic } from "@openclaw/fs-safe/atomic";
@@ -179,15 +205,66 @@ await replaceDirectoryAtomic({
179
205
  });
180
206
  ```
181
207
 
182
- The helper renames `targetDir` to a generated backup path, renames `stagedDir → targetDir`, then removes the backup. If the second rename fails, it tries to restore the original target before rethrowing.
183
- Concurrent replacements of the same resolved target are serialized inside the
184
- current process so their backup, commit, and cleanup phases cannot interleave.
185
- On Windows, ordinary drive-relative staged and target paths are anchored at
186
- entry before namespace-alias admission and resolution.
208
+ Every publication requires the dedicated native identity-fenced,
209
+ descriptor-relative no-replace rename capability and readable retained
210
+ descriptors for the staged and target parents. Older bindings that expose only
211
+ the legacy four-argument no-replace rename fail with `helper-unavailable`
212
+ before target-parent creation or any other replacement effect. This applies
213
+ when the parents are the same or different. There is no JavaScript rename
214
+ fallback, and a cross-device rename still fails. Replacing an existing target
215
+ additionally requires usable native bounded owned-tree cleanup and a readable
216
+ retained descriptor for the original target.
217
+
218
+ If the target is absent, the helper publishes `stagedDir → targetDir` with a
219
+ single no-replace rename. If the target exists, it renames `targetDir` to a
220
+ randomized sibling backup and then renames `stagedDir → targetDir`. The target
221
+ name is temporarily absent between those two operations. A competing entry is
222
+ never overwritten. Publication rollback is attempted only when the staged
223
+ rename is known not to have committed and the target name is still absent; the
224
+ rollback itself is no-replace, so a competitor is preserved and the backup is
225
+ left for recovery.
226
+
227
+ Exact staged-directory identity and parent checks run before and after each
228
+ rename. On Windows, the native rename opens the source relative to the retained
229
+ parent, compares that handle's exact volume and file-index identity with the
230
+ pre-rename bigint receipt, and only then mutates it. POSIX does not provide a
231
+ rename operation that also compares an expected source inode, so on POSIX a
232
+ source-name substitution in the final check-to-rename gap can be moved briefly
233
+ and then detected by the post-rename verification. Similarly, a successful
234
+ rename can be followed by a verification error. Inspect the error's
235
+ `details.publication` value rather than treating rejection as proof that
236
+ publication did not happen.
237
+
238
+ A Windows source-identity mismatch is rejected before mutation, reported
239
+ publicly as `path-mismatch`, and treated as definitely uncommitted so an earlier
240
+ backup can be rolled back. Other `path-mismatch`-shaped native errors are not
241
+ assumed to be pre-commit failures; their publication outcome remains
242
+ indeterminate and observed competitors are preserved.
243
+
244
+ Any native rename error without explicit pre-dispatch provenance has an
245
+ indeterminate outcome, including ordinary errno such as `ENOENT`, `EEXIST`,
246
+ or `EACCES`: a remote filesystem may commit before losing its reply. The helper
247
+ does not guess whether that rename committed or perform another rename or
248
+ cleanup based on that guess; observed names are preserved for caller-directed
249
+ recovery. An error `details.backupPath`, when present, is only the attempted or
250
+ last-observed backup pathname. It does not prove that the path still exists or
251
+ still names the original directory.
252
+
253
+ After a verified commit, the original backup is removed through its retained
254
+ directory identity and bounded native traversal. Cleanup failures reject after
255
+ publication and can leave the backup. On POSIX, cleanup retains the
256
+ [bounded final-entry unlink limitation](temp.md#private-temp-workspaces).
257
+
258
+ Concurrent calls for the same resolved target are serialized inside the current
259
+ process so their backup, publication, and cleanup phases cannot interleave.
260
+ Other processes are not serialized; no-replace renames provide the competitor
261
+ boundary. On Windows, ordinary drive-relative staged and target paths are
262
+ anchored at entry before namespace-alias admission and resolution.
187
263
  `backupPrefix`, when supplied, is sanitized as one path prefix and cannot contain
188
264
  path separators or NUL bytes; the generated backup tail is randomized.
189
265
 
190
- Use it when callers must see a whole staged tree at the target path. For single-file replacement, `replaceFileAtomic` is the right tool.
266
+ Use it when callers must publish a whole staged tree with these recovery
267
+ semantics. For single-file replacement, `replaceFileAtomic` is the right tool.
191
268
 
192
269
  ## `writeTextAtomic`
193
270
 
@@ -396,7 +473,7 @@ type ReplaceFileAtomicSyncFileSystem = {
396
473
  };
397
474
  ```
398
475
 
399
- The async interface already requires `open()`, whose `FileHandle` supplies `chmod()`, so injecting `node:fs` or another conforming adapter needs no new async member. On POSIX, that `open()` must support no-follow directory descriptors as Node does. A custom synchronous filesystem that passes `mode`, `dirMode`, or `preserveExistingMode` must supply `fchmodSync`; omission fails before any file or directory is created and never falls back to a pathname `chmod`. Existing synchronous adapters that request none of those options may omit it; their parent is still opened and identity-checked through a no-follow directory descriptor. Injecting plain `node:fs` supports explicit file and directory modes. Older adapter literals may continue to include `chmod` or `chmodSync` for source compatibility, but those operations are ignored. Copy fallback applies the file mode through its pinned destination descriptor as well, preserving exact modes despite the process umask.
476
+ The async interface already requires `open()`, whose `FileHandle` supplies `chmod()`, so injecting `node:fs` or another conforming adapter needs no new async member. The async temp owner consumes its retained handle before awaiting `close()` during publication handoff and terminal settlement: if a custom adapter releases the resource and then rejects, that rejection is reported without calling `close()` on the same retained handle again. If publication verification opened a replacement handle before the previous retained handle failed to close, the replacement receives one best-effort close attempt. On POSIX, `open()` must support no-follow directory descriptors as Node does. A custom synchronous filesystem that passes `mode`, `dirMode`, or `preserveExistingMode` must supply `fchmodSync`; omission fails before any file or directory is created and never falls back to a pathname `chmod`. Existing synchronous adapters that request none of those options may omit it; their parent is still opened and identity-checked through a no-follow directory descriptor. Injecting plain `node:fs` supports explicit file and directory modes. Older adapter literals may continue to include `chmod` or `chmodSync` for source compatibility, but those operations are ignored. Copy fallback applies the file mode through its pinned destination descriptor as well, preserving exact modes despite the process umask.
400
477
 
401
478
  ## See also
402
479
 
package/docs/copy.md CHANGED
@@ -66,6 +66,8 @@ On Windows, automatic byte copying uses the native binding when available to tra
66
66
 
67
67
  On Linux, automatic byte copying also uses the native binding when available. It reads in chunks up to 1 MiB, using smaller buffers for smaller source-size hints, and leaves leading and trailing zero-filled portions of each chunk unwritten in the new file, avoiding their allocation on filesystems that support sparse files. A final size update preserves trailing holes and all-zero files. This path uses reads and writes, without cloning or copy offload; it still reads the full logical contents and does not promise identical sparse extent layout. `clone: "never"` retains the JavaScript byte-copy path.
68
68
 
69
+ Tree-copy cleanup attempts every acquired close once, even when another close fails. Portable file handles close output before input; completed directories close their pinned source before target; the public wrapper then closes its source before the destination parent. A copy, identity, metadata, native-clone, or cancellation failure already observed at one of those scopes remains the reported value instead of being replaced by cleanup. If that scope otherwise succeeded, its first close failure is reported unchanged. Concurrent siblings have no global structural close order: the first observed sibling failure stops new work while admitted work settles. Portable copying records caller cancellation when it occurs, so a later abort during cleanup cannot replace an earlier copy failure.
70
+
69
71
  Clones preserve file contents, empty directories, timestamps, executable modes where supported, and literal symbolic links. Editing a clone does not modify its source. Unsupported filesystem operations fail; callers may choose their own copy or checkout fallback after the failed operation has settled.
70
72
 
71
73
  Native Windows byte copies can store large zero-filled chunks as sparse ranges when the destination is initially empty and its filesystem supports sparse files. This still reads every source byte and creates an independent copy.
@@ -137,6 +139,37 @@ descriptor inspection does not prove that the source remained unchanged while
137
139
  copying. Keep existing source-fingerprint and publication checks around the
138
140
  transfer when building snapshot operations.
139
141
 
142
+ ### Synchronous descriptor transfers
143
+
144
+ `copyFileDescriptorSync(sourceFd, targetFd, options?)` provides the same
145
+ zero-origin byte transfer for borrowed numeric descriptors. It shares
146
+ `CopyFileHandleOptions` and returns the copied byte count synchronously:
147
+
148
+ ```ts
149
+ import { copyFileDescriptorSync } from "@openclaw/fs-safe/advanced";
150
+
151
+ const bytes = copyFileDescriptorSync(sourceFd, targetFd, {
152
+ maxBytes: expectedSize,
153
+ assertBeforeMutation: assertSnapshotOwnerCurrent,
154
+ });
155
+ ```
156
+
157
+ The same regular-file and exact-identity admission, byte limits, short-I/O
158
+ handling, cursor preservation, and caller-owned cleanup apply. The target must
159
+ be opened without append mode, and both descriptors must remain open and free
160
+ of concurrent I/O, including inside callbacks. Neither helper makes a mutable
161
+ source into a consistent snapshot or truncates an existing destination suffix.
162
+
163
+ The synchronous helper snapshots the four options once and invokes both
164
+ callbacks with no receiver (`this` is `undefined` in strict callbacks).
165
+ `onChunk` receives a borrowed view that must be consumed immediately without
166
+ retaining or mutating it. Both callbacks must finish synchronously; thenables
167
+ throw `TypeError` before the current write. Cancellation is cooperative: a
168
+ pre-aborted signal or an abort triggered by a callback stops the transfer before
169
+ the next write. Timers cannot interrupt synchronous filesystem calls while the
170
+ event loop is blocked. Authority runs before every partial write; an abort
171
+ triggered by that assertion prevents the same write.
172
+
140
173
  ## Ownership and cancellation
141
174
 
142
175
  These are low-level operations on caller-owned absolute paths, not Root-relative methods. The source and destination parent must be real directories. The library pins their descriptors and verifies their identities; it does not establish the caller's authorization to use them. Keep the source immutable for the operation, including writes through other aliases, and keep the destination namespace under the caller's control. Literal symlinks in the cloned contents are preserved rather than followed or sanitized.
@@ -145,6 +178,8 @@ An already aborted signal prevents dispatch. In-flight cancellation stops cancel
145
178
 
146
179
  Completion is not a crash-durability guarantee. The API is suitable for reconstructible templates and checkouts; it does not sync every file or replace application-level publication and recovery rules.
147
180
 
181
+ A close-only rejection reports descriptor settlement, not whether bytes reached stable storage. The destination can already be complete when a close fails, just as other failed or cancelled calls can leave caller-owned output. Inspect or remove that output only after the copying promise settles and under the same source and destination authority assumptions.
182
+
148
183
  Byte copying retains fractional file and directory access/modification timestamps to the precision supported by Node's timestamp APIs and the destination filesystem. This includes dates before 1970 on Unix. On Windows, [Node's unsigned stat seconds](https://github.com/nodejs/node/blob/v26.8.2/src/node_file-inl.h#L93-L104) can report pre-1970 timestamps as dates about 136 years later; byte copying inherits that upstream limitation.
149
184
 
150
185
  ## Platform tests and benchmarks
@@ -133,6 +133,13 @@ the published entry intact for caller-owned recovery. Ordinary write-only and
133
133
  mode-000 outputs do not require a readable descriptor when pathname metadata is
134
134
  available.
135
135
 
136
+ If a synchronous write operation and its final temp-descriptor close both fail,
137
+ the store reports them in operation-then-close order in an `AggregateError`.
138
+ This ordering and the original JavaScript thrown value are preserved even when
139
+ that value is `undefined` or otherwise falsy. An unsuccessful best-effort temp
140
+ unlink remains registered for identity-checked process-exit cleanup; it does not
141
+ prevent the close attempt or replace either reportable failure.
142
+
136
143
  If an opaque pathname cannot be reopened because of an ACL denial or sharing
137
144
  restriction, the synchronous writer intentionally rejects with `path-mismatch`:
138
145
  its exact publication identity cannot be verified. There is no equal-content
@@ -219,6 +226,14 @@ with the same precedence as `write`.
219
226
 
220
227
  Per-call overrides for the store-level defaults:
221
228
 
229
+ Writes capture byte limits, modes, and durability before asynchronous work or
230
+ stream consumption. JSON writes capture those fields and the trailing-newline
231
+ setting before serialization. Later mutation cannot change those captured values.
232
+ Accessors run on the original options object. Ordinary writes retain content
233
+ conversion and byte-limit validation before reading modes and durability.
234
+ The legacy non-private stream `tempPrefix` accessor still runs after staging;
235
+ it does not control the publication policy.
236
+
222
237
  ```ts
223
238
  type FileStoreWriteOptions = {
224
239
  durable?: boolean; // store default, otherwise true
@@ -283,6 +298,10 @@ refreshes are preserved; replacements that are themselves expired remain
283
298
  eligible. This does not require read permission. The existing best-effort
284
299
  external-process race window after dispatch still applies.
285
300
 
301
+ Empty-directory pruning likewise rechecks that the selected entry is still a
302
+ directory immediately before guarded removal. File and symlink replacements
303
+ are preserved, and a directory that becomes nonempty is left in place.
304
+
286
305
  ## Difference from `Root`
287
306
 
288
307
  | `FileStore` | `Root` |