@openclaw/fs-safe 0.15.0 → 0.17.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 (311) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +36 -7
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +2 -8
  5. package/dist/advanced.d.ts +3 -0
  6. package/dist/advanced.d.ts.map +1 -1
  7. package/dist/advanced.js +3 -0
  8. package/dist/archive-deadline.d.ts.map +1 -1
  9. package/dist/archive-deadline.js +22 -23
  10. package/dist/archive-kind.d.ts +0 -1
  11. package/dist/archive-kind.d.ts.map +1 -1
  12. package/dist/archive-kind.js +5 -17
  13. package/dist/archive-merge.d.ts +1 -0
  14. package/dist/archive-merge.d.ts.map +1 -1
  15. package/dist/archive-merge.js +4 -4
  16. package/dist/archive-native.d.ts +1 -0
  17. package/dist/archive-native.d.ts.map +1 -1
  18. package/dist/archive-native.js +1 -0
  19. package/dist/archive-options.d.ts +2 -0
  20. package/dist/archive-options.d.ts.map +1 -1
  21. package/dist/archive-parser.wasm +0 -0
  22. package/dist/archive-read.d.ts.map +1 -1
  23. package/dist/archive-read.js +6 -7
  24. package/dist/archive-tar-stream.d.ts +3 -0
  25. package/dist/archive-tar-stream.d.ts.map +1 -1
  26. package/dist/archive-tar-stream.js +56 -37
  27. package/dist/archive-tar-wasm.d.ts +16 -4
  28. package/dist/archive-tar-wasm.d.ts.map +1 -1
  29. package/dist/archive-tar-wasm.js +134 -34
  30. package/dist/archive-zip-count.d.ts.map +1 -1
  31. package/dist/archive-zip-count.js +21 -1
  32. package/dist/archive-zip-directory.d.ts.map +1 -1
  33. package/dist/archive-zip-directory.js +23 -1
  34. package/dist/archive-zip-loader.d.ts +2 -0
  35. package/dist/archive-zip-loader.d.ts.map +1 -1
  36. package/dist/archive-zip-loader.js +7 -0
  37. package/dist/archive-zip-names.d.ts.map +1 -1
  38. package/dist/archive-zip-names.js +7 -2
  39. package/dist/archive.d.ts.map +1 -1
  40. package/dist/archive.js +14 -9
  41. package/dist/byte-view.d.ts +3 -0
  42. package/dist/byte-view.d.ts.map +1 -0
  43. package/dist/byte-view.js +13 -0
  44. package/dist/clone-metadata.d.ts +1 -0
  45. package/dist/clone-metadata.d.ts.map +1 -1
  46. package/dist/clone-metadata.js +6 -2
  47. package/dist/create-directory.d.ts +20 -0
  48. package/dist/create-directory.d.ts.map +1 -0
  49. package/dist/create-directory.js +130 -0
  50. package/dist/create-file-async.d.ts +7 -0
  51. package/dist/create-file-async.d.ts.map +1 -0
  52. package/dist/create-file-async.js +121 -0
  53. package/dist/create-file.d.ts +8 -0
  54. package/dist/create-file.d.ts.map +1 -0
  55. package/dist/create-file.js +190 -0
  56. package/dist/create-owned-file.d.ts +8 -0
  57. package/dist/create-owned-file.d.ts.map +1 -0
  58. package/dist/create-owned-file.js +16 -0
  59. package/dist/create.d.ts +4 -0
  60. package/dist/create.d.ts.map +1 -0
  61. package/dist/create.js +2 -0
  62. package/dist/creation-darwin.d.ts +6 -0
  63. package/dist/creation-darwin.d.ts.map +1 -0
  64. package/dist/creation-darwin.js +70 -0
  65. package/dist/creation-file-state.d.ts +19 -0
  66. package/dist/creation-file-state.d.ts.map +1 -0
  67. package/dist/creation-file-state.js +118 -0
  68. package/dist/creation-path.d.ts +21 -0
  69. package/dist/creation-path.d.ts.map +1 -0
  70. package/dist/creation-path.js +71 -0
  71. package/dist/creation-permissions.d.ts +19 -0
  72. package/dist/creation-permissions.d.ts.map +1 -0
  73. package/dist/creation-permissions.js +125 -0
  74. package/dist/directory-durability.d.ts +7 -7
  75. package/dist/directory-durability.d.ts.map +1 -1
  76. package/dist/directory-durability.js +22 -80
  77. package/dist/directory-guard.d.ts +3 -0
  78. package/dist/directory-guard.d.ts.map +1 -1
  79. package/dist/directory-mode-node.d.ts +2 -0
  80. package/dist/directory-mode-node.d.ts.map +1 -1
  81. package/dist/directory-mode-node.js +8 -0
  82. package/dist/directory-receipt.d.ts +24 -0
  83. package/dist/directory-receipt.d.ts.map +1 -0
  84. package/dist/directory-receipt.js +123 -0
  85. package/dist/file-cleanup.d.ts +20 -0
  86. package/dist/file-cleanup.d.ts.map +1 -0
  87. package/dist/file-cleanup.js +81 -0
  88. package/dist/file-contents.d.ts +6 -0
  89. package/dist/file-contents.d.ts.map +1 -0
  90. package/dist/file-contents.js +40 -0
  91. package/dist/file-hash.d.ts.map +1 -1
  92. package/dist/file-hash.js +16 -4
  93. package/dist/file-lock-sync-root-acquire.d.ts.map +1 -1
  94. package/dist/file-lock-sync-root-acquire.js +3 -0
  95. package/dist/file-lock-sync-root-held.d.ts +1 -2
  96. package/dist/file-lock-sync-root-held.d.ts.map +1 -1
  97. package/dist/file-lock-sync-root-held.js +7 -5
  98. package/dist/file-lock-sync-stale-admission.d.ts.map +1 -1
  99. package/dist/file-lock-sync-stale-admission.js +3 -0
  100. package/dist/file-lock-sync.d.ts.map +1 -1
  101. package/dist/file-lock-sync.js +8 -11
  102. package/dist/file-observation.d.ts +1 -1
  103. package/dist/file-observation.d.ts.map +1 -1
  104. package/dist/file-store-boundary.d.ts +2 -6
  105. package/dist/file-store-boundary.d.ts.map +1 -1
  106. package/dist/file-store-boundary.js +3 -9
  107. package/dist/file-store-sync-write.d.ts.map +1 -1
  108. package/dist/file-store-sync-write.js +2 -5
  109. package/dist/file-store.js +3 -3
  110. package/dist/guarded-mkdir.d.ts +1 -0
  111. package/dist/guarded-mkdir.d.ts.map +1 -1
  112. package/dist/guarded-mkdir.js +27 -19
  113. package/dist/install-path.d.ts.map +1 -1
  114. package/dist/install-path.js +2 -5
  115. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  116. package/dist/json-durable-queue-ownership.js +2 -6
  117. package/dist/json-durable-queue-paths.d.ts.map +1 -1
  118. package/dist/json-durable-queue-paths.js +2 -24
  119. package/dist/json-durable-queue.d.ts.map +1 -1
  120. package/dist/json-durable-queue.js +10 -9
  121. package/dist/json.d.ts.map +1 -1
  122. package/dist/json.js +32 -75
  123. package/dist/local-roots.d.ts.map +1 -1
  124. package/dist/local-roots.js +19 -21
  125. package/dist/move-path-cleanup.d.ts +5 -19
  126. package/dist/move-path-cleanup.d.ts.map +1 -1
  127. package/dist/move-path-cleanup.js +57 -21
  128. package/dist/move-path.d.ts.map +1 -1
  129. package/dist/move-path.js +63 -40
  130. package/dist/native-binding.d.ts +11 -1
  131. package/dist/native-binding.d.ts.map +1 -1
  132. package/dist/native-fallback-warning.d.ts +4 -0
  133. package/dist/native-fallback-warning.d.ts.map +1 -0
  134. package/dist/native-fallback-warning.js +11 -0
  135. package/dist/native-operations.d.ts +0 -2
  136. package/dist/native-operations.d.ts.map +1 -1
  137. package/dist/native-operations.js +0 -24
  138. package/dist/native-parent-admission.d.ts +2 -0
  139. package/dist/native-parent-admission.d.ts.map +1 -1
  140. package/dist/native-parent-admission.js +3 -2
  141. package/dist/native-pinned-write-windows.d.ts +1 -1
  142. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  143. package/dist/native-pinned-write-windows.js +173 -28
  144. package/dist/native-pinned-write.d.ts.map +1 -1
  145. package/dist/native-pinned-write.js +19 -3
  146. package/dist/native-policy-parent-windows.d.ts.map +1 -1
  147. package/dist/native-policy-parent-windows.js +15 -6
  148. package/dist/native-staged-file.d.ts +5 -3
  149. package/dist/native-staged-file.d.ts.map +1 -1
  150. package/dist/native-staged-file.js +90 -40
  151. package/dist/native.js +2 -2
  152. package/dist/opened-realpath.d.ts.map +1 -1
  153. package/dist/opened-realpath.js +11 -2
  154. package/dist/owner-dacl.d.ts.map +1 -1
  155. package/dist/owner-dacl.js +10 -4
  156. package/dist/path.d.ts.map +1 -1
  157. package/dist/path.js +2 -1
  158. package/dist/permissions.d.ts.map +1 -1
  159. package/dist/permissions.js +3 -17
  160. package/dist/pinned-write-input.d.ts +4 -0
  161. package/dist/pinned-write-input.d.ts.map +1 -0
  162. package/dist/pinned-write-input.js +35 -0
  163. package/dist/pinned-write-mode.d.ts +5 -0
  164. package/dist/pinned-write-mode.d.ts.map +1 -0
  165. package/dist/pinned-write-mode.js +31 -0
  166. package/dist/pinned-write-staged.d.ts +6 -0
  167. package/dist/pinned-write-staged.d.ts.map +1 -0
  168. package/dist/pinned-write-staged.js +186 -0
  169. package/dist/pinned-write-types.d.ts +3 -0
  170. package/dist/pinned-write-types.d.ts.map +1 -1
  171. package/dist/pinned-write.d.ts.map +1 -1
  172. package/dist/pinned-write.js +41 -147
  173. package/dist/private-directory.d.ts.map +1 -1
  174. package/dist/private-directory.js +18 -4
  175. package/dist/private-producer-handoff-sync.d.ts +14 -0
  176. package/dist/private-producer-handoff-sync.d.ts.map +1 -0
  177. package/dist/private-producer-handoff-sync.js +114 -0
  178. package/dist/private-producer-handoff.d.ts +22 -4
  179. package/dist/private-producer-handoff.d.ts.map +1 -1
  180. package/dist/private-producer-handoff.js +140 -77
  181. package/dist/publish-copy-stage.d.ts +2 -1
  182. package/dist/publish-copy-stage.d.ts.map +1 -1
  183. package/dist/publish-copy-stage.js +16 -7
  184. package/dist/publish-file.d.ts +2 -2
  185. package/dist/publish-file.d.ts.map +1 -1
  186. package/dist/publish-file.js +58 -98
  187. package/dist/regular-file.d.ts.map +1 -1
  188. package/dist/regular-file.js +35 -44
  189. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  190. package/dist/replace-file-copy-fallback.js +28 -26
  191. package/dist/replace-file-copy-source.d.ts.map +1 -1
  192. package/dist/replace-file-copy-source.js +13 -22
  193. package/dist/replace-file-descriptor.d.ts.map +1 -1
  194. package/dist/replace-file-descriptor.js +10 -16
  195. package/dist/replace-file-temp-owner.d.ts +0 -7
  196. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  197. package/dist/replace-file-temp-owner.js +9 -60
  198. package/dist/replace-file.d.ts.map +1 -1
  199. package/dist/replace-file.js +9 -13
  200. package/dist/root-create-input.d.ts +2 -1
  201. package/dist/root-create-input.d.ts.map +1 -1
  202. package/dist/root-create-input.js +13 -4
  203. package/dist/root-directory-creation.d.ts +3 -3
  204. package/dist/root-directory-creation.d.ts.map +1 -1
  205. package/dist/root-directory-creation.js +15 -3
  206. package/dist/root-directory-list.d.ts.map +1 -1
  207. package/dist/root-directory-list.js +20 -3
  208. package/dist/root-file-final-admission.d.ts +1 -1
  209. package/dist/root-file-final-admission.d.ts.map +1 -1
  210. package/dist/root-file-final-admission.js +5 -2
  211. package/dist/root-file.d.ts.map +1 -1
  212. package/dist/root-file.js +3 -2
  213. package/dist/root-impl.d.ts.map +1 -1
  214. package/dist/root-impl.js +78 -27
  215. package/dist/root-move-noreplace.d.ts +2 -0
  216. package/dist/root-move-noreplace.d.ts.map +1 -1
  217. package/dist/root-move-noreplace.js +22 -13
  218. package/dist/root-options.d.ts +12 -4
  219. package/dist/root-options.d.ts.map +1 -1
  220. package/dist/root-path-stat.d.ts.map +1 -1
  221. package/dist/root-path-stat.js +59 -7
  222. package/dist/root-read-admission.d.ts.map +1 -1
  223. package/dist/root-read-admission.js +7 -2
  224. package/dist/root-remove.d.ts.map +1 -1
  225. package/dist/root-remove.js +15 -1
  226. package/dist/root-write-publication.js +1 -1
  227. package/dist/secret-file.d.ts.map +1 -1
  228. package/dist/secret-file.js +1 -0
  229. package/dist/secure-file-windows.d.ts +6 -0
  230. package/dist/secure-file-windows.d.ts.map +1 -1
  231. package/dist/secure-file-windows.js +34 -117
  232. package/dist/secure-file.js +2 -2
  233. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  234. package/dist/sidecar-lock-acquire.js +4 -6
  235. package/dist/sidecar-lock-handle.d.ts +3 -0
  236. package/dist/sidecar-lock-handle.d.ts.map +1 -1
  237. package/dist/sidecar-lock-handle.js +6 -0
  238. package/dist/sidecar-lock-reclaim.d.ts +1 -1
  239. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  240. package/dist/sidecar-lock-reclaim.js +11 -8
  241. package/dist/sidecar-lock-root.d.ts.map +1 -1
  242. package/dist/sidecar-lock-root.js +2 -1
  243. package/dist/sidecar-lock.d.ts.map +1 -1
  244. package/dist/sidecar-lock.js +3 -5
  245. package/dist/staged-directory.d.ts +2 -2
  246. package/dist/staged-directory.d.ts.map +1 -1
  247. package/dist/staged-directory.js +6 -6
  248. package/dist/staged-file-settlement.d.ts +17 -0
  249. package/dist/staged-file-settlement.d.ts.map +1 -0
  250. package/dist/staged-file-settlement.js +57 -0
  251. package/dist/strict-file-identity.d.ts +1 -1
  252. package/dist/strict-file-identity.d.ts.map +1 -1
  253. package/dist/strict-file-identity.js +9 -9
  254. package/dist/symlink-parents.d.ts.map +1 -1
  255. package/dist/symlink-parents.js +2 -27
  256. package/dist/temp-workspace-owner.js +4 -4
  257. package/dist/unicode-path.d.ts.map +1 -1
  258. package/dist/unicode-path.js +3 -0
  259. package/dist/walk.d.ts.map +1 -1
  260. package/dist/walk.js +4 -2
  261. package/dist/windows-owner.d.ts.map +1 -1
  262. package/dist/windows-owner.js +2 -1
  263. package/dist/windows-security-bridge.cs +336 -0
  264. package/dist/windows-security-bridge.ps1 +15 -0
  265. package/dist/windows-security-command.d.ts +26 -0
  266. package/dist/windows-security-command.d.ts.map +1 -0
  267. package/dist/windows-security-command.js +363 -0
  268. package/dist/windows-security-facts.d.ts +6 -0
  269. package/dist/windows-security-facts.d.ts.map +1 -0
  270. package/dist/windows-security-facts.js +108 -0
  271. package/dist/write-file-handle.d.ts +7 -0
  272. package/dist/write-file-handle.d.ts.map +1 -1
  273. package/dist/write-file-handle.js +23 -0
  274. package/dist/write-open-flags.d.ts.map +1 -1
  275. package/dist/write-open-flags.js +1 -8
  276. package/dist/write-queue.d.ts.map +1 -1
  277. package/dist/write-queue.js +1 -4
  278. package/docs/advanced.md +71 -2
  279. package/docs/archive.md +102 -39
  280. package/docs/atomic.md +29 -5
  281. package/docs/config.md +6 -2
  282. package/docs/contributing.md +48 -4
  283. package/docs/copy.md +2 -0
  284. package/docs/creation.md +132 -0
  285. package/docs/durability.md +59 -0
  286. package/docs/file-contents.md +68 -0
  287. package/docs/install.md +31 -7
  288. package/docs/json.md +5 -4
  289. package/docs/local-roots.md +2 -0
  290. package/docs/migrating-to-0.5.md +15 -6
  291. package/docs/migrating-to-0.6.md +9 -4
  292. package/docs/mutation-policy-proof.md +5 -3
  293. package/docs/native-helper.md +22 -9
  294. package/docs/native.md +47 -15
  295. package/docs/path.md +4 -4
  296. package/docs/permissions.md +37 -14
  297. package/docs/public-api.md +5 -0
  298. package/docs/quickstart.md +1 -1
  299. package/docs/reading.md +2 -2
  300. package/docs/regular-file.md +3 -0
  301. package/docs/root.md +43 -0
  302. package/docs/secret-file.md +11 -2
  303. package/docs/secure-file.md +9 -4
  304. package/docs/sidecar-lock.md +14 -5
  305. package/docs/staged-file.md +9 -3
  306. package/docs/store.md +3 -1
  307. package/docs/temp.md +4 -1
  308. package/docs/types.md +18 -2
  309. package/docs/walk.md +7 -0
  310. package/docs/writing.md +80 -7
  311. package/package.json +18 -15
package/docs/path.md CHANGED
@@ -44,7 +44,7 @@ opened or mutated.
44
44
 
45
45
  ### `isPathInsideWithRealpath(rootDir, target, opts?)`
46
46
 
47
- Synchronous. Same as `isPathInside`, but resolves both inputs through `realpath` first. Use this when you want the canonical answer and either input might be a symlink.
47
+ Synchronous. First requires lexical containment with `isPathInside`, then resolves both inputs through `realpath` and checks containment again. A lexically outside path is rejected even if its resolved target is inside the root.
48
48
 
49
49
  ```ts
50
50
  isPathInsideWithRealpath("/srv/uploads", "/srv/symlink-to-elsewhere"); // false
@@ -121,7 +121,7 @@ The check is intentionally not a normal consumer policy knob. Safe read APIs rej
121
121
 
122
122
  ### `isNotFoundPathError(err)`
123
123
 
124
- `true` if the error is a `NodeJS.ErrnoException` with code `ENOENT` (file or directory missing).
124
+ `true` if the error has code `ENOENT` (file or directory missing) or `ENOTDIR` (a path component is not a directory).
125
125
 
126
126
  ```ts
127
127
  try {
@@ -207,8 +207,8 @@ import {
207
207
  } from "@openclaw/fs-safe/advanced";
208
208
  ```
209
209
 
210
- - `assertNoPathAliasEscape({ rootRealPath, candidatePath, policy })` — async. Asserts the candidate's resolved real path is inside the root. Configurable via `PATH_ALIAS_POLICIES` (which currently ships only the default `"strict"` policy).
211
- - `assertNoHardlinkedFinalPath({ filePath })` — async. Throws if the file at `filePath` has `nlink > 1`.
210
+ - `assertNoPathAliasEscape({ absolutePath, rootPath, boundaryLabel, policy? })` — async. Applies root path resolution and final hardlink checks. `policy` defaults to `PATH_ALIAS_POLICIES.strict`; `PATH_ALIAS_POLICIES.unlinkTarget` permits final symlink and hardlink aliases for unlink operations.
211
+ - `assertNoHardlinkedFinalPath({ filePath, root, boundaryLabel, allowFinalHardlinkForUnlink? })` — async. Rejects a regular file with `nlink > 1`; missing paths and nonregular resolved entries are ignored. Setting `allowFinalHardlinkForUnlink: true` skips this check for unlink operations.
212
212
 
213
213
  Use these when writing a custom helper that wants the same guards `root()` uses but with different surrounding logic.
214
214
 
@@ -42,7 +42,7 @@ POSIX remediation strings shell-quote paths with whitespace or metacharacters
42
42
  and protect option-like paths with `--`, so they can be presented as commands
43
43
  without letting the inspected pathname add shell syntax.
44
44
 
45
- `inspectPathPermissions()` follows symlink targets for the effective mode but tells you whether the original path was a symlink. On POSIX it reports owner/group/world bits. On Windows it delegates to the ACL helpers below and also reports `ownerSid` plus `ownerTrusted` when ownership can be verified. `ownerTrusted` is true only for a local volume owned by the current user, LocalSystem, or built-in Administrators; remote filesystems fail closed. This remains a pathname reporting API with the fallbacks described below. `readSecureFile()` does not use those pathname fallbacks on Windows: it requires descriptor-bound native owner/DACL facts for the exact handle it reads.
45
+ `inspectPathPermissions()` follows symlink targets for the effective mode but tells you whether the original path was a symlink. On POSIX it reports owner/group/world bits. On Windows it delegates to the ACL helpers below and also reports `ownerSid` plus `ownerTrusted` when ownership can be verified. `ownerTrusted` is true only for a local volume owned by the current user, LocalSystem, or built-in Administrators; remote filesystems fail closed. This remains a pathname reporting API with the fallbacks described below. `readSecureFile()` obtains descriptor-bound owner/DACL facts for the exact handle it reads, using native support or the packaged PowerShell/C# bridge in `auto` and `off` modes.
46
46
 
47
47
  ## Advanced Windows ACL helpers
48
48
 
@@ -69,9 +69,10 @@ resolveWindowsUserPrincipal(env);
69
69
  ```
70
70
 
71
71
  The fallback Windows inspector reads the owner and DACL together through one
72
- built-in Windows PowerShell/.NET query. It returns canonical SIDs and numeric
73
- access masks, so Unicode paths and account names do not pass through lossy
74
- console display text. `inspectWindowsAcl()` uses native descriptor facts for
72
+ built-in Windows PowerShell/.NET query. The query addresses its JSON command by
73
+ module name and limits module discovery to PowerShell's bundled system modules.
74
+ It returns canonical SIDs and numeric access masks, so Unicode paths and account
75
+ names do not pass through lossy console display text. `inspectWindowsAcl()` uses native descriptor facts for
75
76
  complete local ACLs with nonzero inherited ACEs (or empty/null DACLs) when the
76
77
  optional Windows binding is available. It applies
77
78
  the same classifier to native facts and the fallback query, returning canonical
@@ -171,10 +172,12 @@ Object-specific and other ACE layouts are not guessed: they are omitted,
171
172
  `complete` becomes false, and their numeric types appear in
172
173
  `unsupportedAceTypes`, allowing a security-sensitive caller to fail closed.
173
174
  Non-Windows systems return `{ status: "unsupported-platform", platform }`.
174
- Windows requires the native binding; if it is unavailable or forced
175
- off, the call throws `FsSafeError("helper-unavailable")`. The existing coarse
176
- `inspectPathPermissions()` API still owns its compatibility fallback and trust
177
- classification.
175
+ Windows prefers the native binding. In native `auto` or `off` mode, a missing
176
+ binding or capability uses the packaged PowerShell/C# bridge with
177
+ the same raw ACE projection. Native `require` rejects either absence with
178
+ `FsSafeError("helper-unavailable")` and starts no command. An available native
179
+ query's failure is terminal. The existing coarse `inspectPathPermissions()` API
180
+ still owns its compatibility fallback and trust classification.
178
181
 
179
182
  ## Private directories
180
183
 
@@ -188,10 +191,11 @@ await createPrivateDirectory(sqliteDirectory);
188
191
  await openSqlite(path.join(sqliteDirectory, "sessions.sqlite"));
189
192
  ```
190
193
 
191
- On Windows with native support, this creates the directory and applies a
194
+ On Windows, this creates the directory and applies a
192
195
  protected owner + LocalSystem + Administrators full-control DACL directly with
193
- an atomic security descriptor; no PowerShell or `icacls` process is launched.
194
- The native operation retains the parent and exact created-directory handles
196
+ an atomic security descriptor. The native route launches no command. When its
197
+ binding or capability is unavailable, native `auto` and `off` modes use the
198
+ packaged PowerShell/C# bridge. Both routes retain the parent and exact created-directory handles
195
199
  through ACL and final pathname validation. If validation fails, it attempts only
196
200
  nonrecursive deletion through the created handle, preserving any pathname
197
201
  replacement. If cleanup also fails, the error retains the original failure and
@@ -216,13 +220,32 @@ also rejects explicit `.` and `..` components, including spellings such as
216
220
  `.\private` and `parent\..\private`, as a compatibility restriction. Simple
217
221
  relative names without these components remain supported.
218
222
 
219
- This API is Windows-only and native-only; it fails closed with
220
- `FsSafeError("helper-unavailable")` on other platforms, when native mode is off,
221
- or when the binding is unavailable. POSIX callers should create private
223
+ This API is Windows-only; it fails closed with `FsSafeError("helper-unavailable")`
224
+ on other platforms. Native `require` also fails if the binding or capability is
225
+ missing and never starts a command. An available native operation's failure is
226
+ terminal. POSIX callers should create private
222
227
  directories through their existing trusted-root creation policy rather than a
223
228
  pathname-only compatibility shim. Existing Windows permission inspection still
224
229
  retains its structured .NET compatibility fallback.
225
230
 
231
+ The raw owner/DACL and private-directory fallbacks each emit one path-free
232
+ `FS_SAFE_NATIVE_FALLBACK` warning per process. PowerShell startup and C#
233
+ compilation add overhead to each call; install the native package for frequent
234
+ operations. These routes run the package's readable, fixed scripts under normal
235
+ system PowerShell policy; see the [Windows security fallback prerequisites](install.md#windows-security-fallback).
236
+ If command support is unavailable, disallowed, or fails, the operation rejects.
237
+ Private-directory creation never falls back to inherited permissions.
238
+ The asynchronous creation command has a 30-second deadline. After a timeout or
239
+ transport failure, fs-safe requests termination and waits at most one further
240
+ second before rejecting and closing its output pipes. The error distinguishes
241
+ observed process exit from an unconfirmed termination attempt. If the OS refuses
242
+ termination, the command can still create the directory after rejection. An
243
+ already-created object retains its protected DACL, but pathname validation and
244
+ owned-handle cleanup may not finish. An error therefore does not prove the
245
+ pathname is absent; a retry can report `EEXIST`. Before retrying or using the
246
+ pathname, establish that the earlier operation stopped and verify any existing
247
+ directory's security. The library does not attempt pathname-based cleanup.
248
+
226
249
  Use `createIcaclsResetCommand()` when you need a structured command and argv pair. Use `formatIcaclsResetCommand()` when you only need a remediation string for a user-facing message.
227
250
 
228
251
  ## Types
@@ -20,6 +20,9 @@ The root subpath also exports `openLocalFileSafely`, `readLocalFileSafely`, and
20
20
  `resolveOpenedFileRealPathForHandle` for trusted absolute-file composition.
21
21
  They do not create a root boundary around arbitrary caller input; prefer
22
22
  `root()` for untrusted paths.
23
+ The handle resolver verifies exact descriptor and pathname identities, with one
24
+ bounded retry for unknown Windows observations. It borrows the handle without
25
+ reading, reopening, closing it, or changing its cursor.
23
26
 
24
27
  The error helpers are `categorizeFsSafeError` and `FsSafeErrorDetails`. The
25
28
  deprecated native-configuration bridge retains the `FsSafePythonConfig` type.
@@ -135,6 +138,8 @@ The durability surface also exports the synchronous strict
135
138
  `Sha256FileSyncInput`, `Sha256FileOptions`, and `Sha256FileResult`.
136
139
  `sha256FileSync()` provides synchronous pathname or borrowed-descriptor hashing
137
140
  with the same byte-budget and digest-result contracts as `sha256File()`.
141
+ `DirectoryReceipt<T>` accepts `Stats` or `BigIntStats` input metadata; its default
142
+ type argument and returned durability receipts remain numeric `Stats`.
138
143
 
139
144
  ## Archives
140
145
 
@@ -60,7 +60,7 @@ await fs.move("notes/today.txt", "notes/archive/today.txt", { overwrite: true })
60
60
  await fs.remove("notes/archive/today.txt");
61
61
  ```
62
62
 
63
- `move()` defaults to no clobber. That mode requires the native helper so a concurrent target cannot be replaced between an absence check and the rename; without it, the call fails with `helper-unavailable`. Pass `{ overwrite: true }` when replacing the target is intentional. `remove()` works on files and empty directories. For non-empty directories, list and remove children first or use [`replaceDirectoryAtomic`](atomic.md#replacedirectoryatomic).
63
+ `move()` defaults to no clobber. That mode requires the native helper so a concurrent target cannot be replaced between an absence check and the rename; without it, the call fails with `helper-unavailable`. Pass `{ overwrite: true }` when replacing the target is intentional. `remove()` removes files and empty directories by default. To remove a non-empty directory, pass `{ recursive: true }`; use `maxEntries`, `maxDepth`, and `signal` to bound the work. See [`root()`](root.md) for removal ordering, limits, and partial-removal semantics.
64
64
 
65
65
  ## 5. Inspect
66
66
 
package/docs/reading.md CHANGED
@@ -28,7 +28,7 @@ Regardless of shape, every read goes through the same boundary checks:
28
28
  4. Reject `..` traversal and absolute spellings when they resolve outside the root. In-root absolute spellings remain accepted; `readAbsolute` makes that intent explicit.
29
29
  5. Open with `O_NOFOLLOW` where available. Any remaining symlink in the path triggers `symlink` unless the call's `symlinks` policy is `follow-within-root`.
30
30
  6. Compare the pre-open bigint path identity with the open fd, then perform one best-effort final admission: check the captured root identity, compare the policy-aware pathname with the fd, freshly canonicalize and re-admit that target inside the captured root, compare its exact bigint identity without following a final symlink with the fd, and check the root again. Both final pathname observations run even when their spellings match. An observed swap triggers `path-mismatch` or `outside-workspace`; a missing final path triggers `not-found`.
31
- 7. If `hardlinks: "reject"`, refuse files with `nlink > 1` (`hardlink`).
31
+ 7. If `hardlinks: "reject"`, refuse files with `nlink > 1` (`hardlink`), including links introduced before either fresh final pathname observation. Root-file helpers apply the same final check when `rejectHardlinks` is enabled; directory admission is unaffected.
32
32
  8. If `maxBytes` is set, refuse reads larger than the cap (`too-large`).
33
33
 
34
34
  The final fence closes a rejected descriptor before any Root read consumes bytes or
@@ -100,7 +100,7 @@ type RootReadOptions = {
100
100
  hardlinks?: "reject" | "allow"; // override defaults.hardlinks
101
101
  maxBytes?: number; // refuse reads larger than this many bytes
102
102
  nonBlockingRead?: boolean; // compatibility hint; safe opens are already nonblocking where supported
103
- symlinks?: "reject" | "follow-within-root"; // override defaults.symlinks
103
+ symlinks?: "reject" | "follow-within-root" | "follow-parents-within-root"; // override defaults.symlinks
104
104
  };
105
105
  ```
106
106
 
@@ -110,6 +110,9 @@ descriptor, and current pathname identities remain exact bigints through the
110
110
  append boundary; rounded-equal replacements and persistent unknown Windows
111
111
  identities reject before chmod or writing bytes. With
112
112
  `rejectSymlinkParents: true`, it also rejects symlinked ancestor directories.
113
+ Supported option values are captured once before filesystem work, so replacing
114
+ the content, encoding, mode or cap cannot change an in-flight append. Byte-array
115
+ contents remain caller-owned; leave them unchanged until the append completes.
113
116
 
114
117
  On POSIX, `O_NONBLOCK` prevents a no-reader FIFO substituted before open from
115
118
  stalling admission. A confirmed non-regular target is refused before chmod or
package/docs/root.md CHANGED
@@ -130,8 +130,32 @@ fs.mkdir(rel, options?) // mkdir -p (creates missing parents)
130
130
  fs.ensureRoot(options?) // accepts "" / "." as the root itself
131
131
  ```
132
132
 
133
+ `mkdir`, `ensureRoot`, `create`, and `createJson` accept `private: true`.
134
+ Missing directories are created with private permissions, and an existing
135
+ requested directory must already be private. Existing ancestors are not
136
+ chmodded or assigned new ACLs. Private files use owner-only POSIX permissions
137
+ or a protected Windows DACL granting access to the current user, System, and
138
+ Administrators. On macOS, private directories and files must also have no ACL;
139
+ creation rejects relevant inheritable parent ACLs, while noninheriting parent
140
+ ACLs remain allowed. A native helper with `inspectDarwinAcl` is required. Native
141
+ `off`, a missing helper, or an older helper without that capability rejects with
142
+ `helper-unavailable` before creating parents or stages. See [creation](creation.md)
143
+ for platform support, synchronous leaf creation, and failure handling.
144
+
145
+ ```ts
146
+ await fs.mkdir("private-data", { private: true });
147
+ await fs.create("private-data/credential", "synthetic credential", { private: true });
148
+ ```
149
+
133
150
  `write`, `create`, `append`, `writeJson`, and `createJson` accept `mode?: number`; use `0o600` for credentials and other private state. `writeJson` also accepts the same options as `JSON.stringify` plus `trailingNewline?: boolean` (defaults `true` so the file ends in `\n`).
134
151
 
152
+ Buffered `create` and `createJson` also accept `atomic?: boolean`. With `true`,
153
+ complete content is staged before exclusive publication even in native-off mode;
154
+ the fallback requires hardlinks. Omitted or `false` keeps the existing buffered
155
+ publication behavior. Streamed creates always stage complete content. The flag
156
+ does not change `durable` or promise stronger containment or crash durability.
157
+ See [atomic creation and settlement](writing.md#atomic-buffered-creation).
158
+
135
159
  `create` also accepts `AsyncIterable<Uint8Array>` with `RootCreateStreamOptions`:
136
160
  the same path, authority, mode, and durability options, plus `maxBytes` and
137
161
  `signal`, without `encoding` or `renameIdentity`. It consumes one chunk at a
@@ -143,6 +167,8 @@ cleanup, and filesystem requirements.
143
167
  `append` accepts `prependNewlineIfNeeded: true` to separate text from existing
144
168
  content when neither side supplies a newline. String data uses its `encoding`
145
169
  for the newline check, including UTF-16LE; Buffer data uses a single LF byte.
170
+ Empty strings and Buffers add no separator; an empty append still creates a
171
+ missing file.
146
172
 
147
173
  These five methods and `copyIn` also accept `durable?: boolean`: the per-call value overrides
148
174
  `Root.defaults.durable`, which defaults to `true` when omitted. An explicitly
@@ -152,6 +178,11 @@ and parent-directory fsync calls. Use it only for reconstructible data: a crash
152
178
  may lose the write or leave the previous file. See [Writing](writing.md#write-options)
153
179
  for platform details.
154
180
 
181
+ `create` and `createJson` additionally accept `durable: "file"` to require file
182
+ synchronization, including propagating `EPERM`. Parent-directory synchronization
183
+ retains its existing best-effort behavior. This option applies to buffered and
184
+ streamed creation and does not select a publication strategy.
185
+
155
186
  `copyIn` accepts a `RootCopySource`: a trusted absolute source path or a file
156
187
  within another Root. The guarded form supplies `root` with only its `open` and
157
188
  `stat` read capabilities, plus `relativePath`:
@@ -279,6 +310,13 @@ from that dispatch. A thrown value rejects the operation unchanged; an async
279
310
  or thenable-returning callback rejects with `TypeError` before that mutation.
280
311
  Synchronous return values are ignored. Callbacks can run multiple times and
281
312
  must inspect current authority each time.
313
+ Directory creation rechecks the retained parent after the callback and before
314
+ submitting mkdir, so a replacement is rejected before creating that component.
315
+ Overwrite moves recheck the retained root, parents, source identity and both
316
+ routes after the callback, including destination parents that were missing
317
+ during preparation. Removals recheck cancellation, retained ancestry and exact
318
+ leaf identity before dispatch; `force` tolerates a missing leaf, not replaced
319
+ ancestry. Removing an admitted hardlink still leaves its other names intact.
282
320
 
283
321
  Already dispatched I/O cannot be revoked. Identity-checked cleanup, final
284
322
  permissions, and durability finish under the existing operation owner even
@@ -300,6 +338,11 @@ the caller, which must check authority before its own later writes.
300
338
 
301
339
  All mutation methods accept `denyMutations?: { paths?: string[]; prefixes?: string[] }`. Entries must be absolute paths. `paths` blocks those exact paths; `prefixes` blocks those paths and their descendants. fs-safe preserves path strings exactly and canonicalizes through existing ancestors before comparing, so a symlinked ancestor to a denied location is still denied. Denied mutations throw `FsSafeError` with code `denied-path`. Use this for caller-specific sensitive paths, not as a replacement for the root boundary, symlink, or hardlink checks.
302
340
 
341
+ `move()` snapshots its merged default and per-call mutation policy before
342
+ asynchronous preparation. Later changes to the original policy objects or arrays
343
+ apply to subsequent calls. Use `assertBeforeMutation` for live revocation of an
344
+ in-flight move.
345
+
303
346
  For writes, creates, streams, and copies, parent creation admits the prospective
304
347
  file and each missing directory before creating that directory, including on the
305
348
  Windows native route. An exact deny on an existing parent does not prevent using
@@ -133,6 +133,15 @@ startWebhookVerifier(signingKey);
133
133
 
134
134
  Async. Creates the parent directory at `dirMode` (default `0o700`) if missing, writes content to a sibling temp file, finalizes `mode` (default `0o600`) through an owned descriptor after content writes, and atomically renames over the destination. Publication verification checks the final file identity and mode.
135
135
 
136
+ On POSIX, both native and JavaScript writers verify actual `0o600` permission
137
+ bits through the retained descriptor before writing content. A filesystem that
138
+ reports successful chmod without enforcing those bits fails with
139
+ `insecure-permissions` before any payload is written, including when an explicit
140
+ `dirMode` permits other users to traverse the parent. The requested final `mode`
141
+ is still applied after content writes, including restrictive and special-bit
142
+ overrides. This mode-bit check does not require native ACL inspection; JavaScript
143
+ secret writes remain available on macOS.
144
+
136
145
  Concurrent writes to distinct leaves may share creation of a missing parent.
137
146
  After a parent-creation race, the helper re-inspects the entry and requires a
138
147
  non-symlink directory, then revalidates root/parent guards, containment, and
@@ -270,9 +279,9 @@ await withTimeout(
270
279
 
271
280
  ## Threat model notes
272
281
 
273
- - These helpers protect the secret file from **other processes with the same UID** that respect filesystem permissions. They do not defend against root or against attackers who can read process memory.
282
+ - On POSIX, the default `0600` file and `0700` directory modes restrict group and other access. They do not protect against processes with the same UID, root, attackers who can read process memory, or access granted by additional ACL entries.
274
283
  - Validation failures are tripwires, not authorization. Investigate before clearing a rejected credential file.
275
- - If the destination directory is on a tmpfs that does not honor mode bits, the helpers will set the mode bits but the OS may ignore them. Audit your platform.
284
+ - On POSIX, a file that still reports a mode other than `0600` after initialization is rejected with `insecure-permissions` before payload is written. Matching mode reports alone cannot prove that an arbitrary filesystem actually enforces those permissions.
276
285
 
277
286
  ## See also
278
287
 
@@ -27,9 +27,13 @@ The helper:
27
27
  - enforces `maxBytes` before and after reading
28
28
  - closes the handle on success, error, and timeout
29
29
 
30
- On POSIX, unsafe permissions mean group/world writable, and group/world readable unless `permissions.allowReadableByOthers` is true. On Windows, the helper queries owner, DACL, and locality from the same open descriptor that supplies the bytes. The native query returns the 32-bit volume serial and 64-bit file-index projection used by Node, which must equal Node's bigint descriptor receipt before its ACL facts are trusted. This avoids JavaScript number rounding but does not represent the full 128-bit file identity available on ReFS. Only the current user, LocalSystem, and built-in Administrators are trusted owner classes.
30
+ On POSIX, unsafe permissions mean group/world writable, and group/world readable unless `permissions.allowReadableByOthers` is true. On Windows, the helper queries owner, DACL, and locality from the same open descriptor that supplies the bytes. Both native and system-command queries return the 32-bit volume serial and 64-bit file-index projection used by Node, which must equal Node's bigint descriptor receipt before its ACL facts are trusted. This avoids JavaScript number rounding but does not represent the full 128-bit file identity available on ReFS. Only the current user, LocalSystem, and built-in Administrators are trusted owner classes.
31
31
 
32
- Windows secure reads require the matching current optional native package. A missing or stale helper, fd-to-handle conversion failure, denied `READ_CONTROL`, remote handle, incomplete descriptor, or unsupported ACE form rejects with `permission-unverified` before content is read. A malformed or different handle identity rejects with `path-mismatch`. There is no pathname-command fallback for `readSecureFile()`; the standalone reporting APIs in [`permissions`](permissions.md) retain their documented fallbacks. `permissions.allowInsecure` remains the explicit escape hatch and bypasses the ACL query.
32
+ Windows secure reads prefer the matching optional native package. In native `auto` or `off` mode, a missing binding or descriptor-inspection capability uses a packaged, readable PowerShell script and adjacent C# source to inspect the borrowed file handle. This route requires the [Windows security fallback prerequisites](install.md#windows-security-fallback), including permission to run the scripts under normal system policy. The command does not read file contents or reopen the pathname. Successful inspection waits for the child to exit and its output pipes to close. This emits a path-free `FS_SAFE_NATIVE_FALLBACK` warning once per process for secure reads and adds PowerShell startup and compilation overhead to each inspection.
33
+
34
+ Descriptor commands have a 30-second deadline. After a timeout or transport failure, fs-safe requests termination, waits at most one further second, and then rejects even if process exit or pipe closure remains unconfirmed. It closes its own output pipes and reports the observed exit separately from the termination attempt in the error cause. If the OS refuses termination, the child may retain its independently inherited Windows handle; closing the caller's descriptor cannot retarget that handle. No file bytes are returned from a failed inspection.
35
+
36
+ Native `require` still rejects a missing binding or capability with `permission-unverified`, without starting a command. An available native helper's failure is terminal. On either route, fd-to-handle conversion failure, denied `READ_CONTROL`, a remote handle, incomplete descriptor, unsupported ACE form, or unavailable command support rejects with `permission-unverified` before content is read. A malformed or different handle identity rejects with `path-mismatch`. The standalone reporting APIs in [`permissions`](permissions.md) retain their documented pathname fallbacks. `permissions.allowInsecure` remains the explicit escape hatch and bypasses the ACL query.
33
37
 
34
38
  Descriptor, pathname, and realpath identity checks use bigint stats internally to avoid JavaScript number rounding. The returned `stat` remains a normal Node `Stats` object with numeric fields. A zero Windows device or inode is unverified, never a match: the helper re-inspects that identity once using the same descriptor or pathname, then rejects persistent ambiguity with `path-mismatch`. A definite mismatch rejects immediately; retries retain known identity components and still enforce symlink policy.
35
39
 
@@ -92,13 +96,14 @@ On an actual Windows process with effective `platform: "win32"`, `inject.env` an
92
96
  | `timeout` | `timeoutMs` elapsed while reading. |
93
97
 
94
98
  Windows descriptor-inspection failures are operational `permission-unverified`
95
- errors and refuse the read. The original native exception is retained as
99
+ errors and refuse the read. The original native or descriptor-command exception is retained as
96
100
  `cause`; treat causes as restricted local diagnostic data. No pathname or ACL
97
101
  content is copied into the display message. Test adapters that simulate Windows
98
102
  on another operating system retain the standalone pathname inspector's
99
103
  structured command diagnostics (`ownerError`, `command`, `durationMs`,
100
104
  `timedOut`, `exitCode`, `signal`, and bounded escaped `stderr`). Actual Windows
101
- secure reads do not start those commands. No retries are performed.
105
+ secure reads never invoke the injected pathname inspector: their optional
106
+ command route inspects the borrowed descriptor instead. No retries are performed.
102
107
 
103
108
  ## See also
104
109
 
@@ -55,6 +55,7 @@ invocation is not mutation authority. A failing final parser keeps its error
55
55
  even if guard ownership has also changed.
56
56
  `manager.reset()` invalidates admission bookkeeping but preserves a pending
57
57
  Root guard; let its original attempt settle before retrying that guarded path.
58
+ It stops compromise monitoring for forgotten holders, including callbacks from checks already in flight.
58
59
 
59
60
  Always release locks in a `finally` block. Application-managed graceful shutdown can await `release()` or `manager.drain()` before terminating. Explicit `process.exit()`, uncaught failures, crashes, default signal handling, and fatal termination (including `SIGKILL`) do not reliably run asynchronous Root cleanup and may leave sidecars. Recover only after an application-owned liveness policy proves the holder cannot still be writing.
60
61
 
@@ -64,7 +65,7 @@ Each new sidecar also carries an internal random ownership token encoded as JSON
64
65
 
65
66
  The raw sidecar bytes are not a canonical JSON representation: tools that trim or rewrite the trailing whitespace invalidate the ownership token, so release leaves the changed sidecar in place and fails closed. The token distinguishes cooperating acquisitions; it is not a secret and does not make pathname compare-and-remove atomic against a hostile process that can replace files outside the lock protocol.
66
67
 
67
- `release()` propagates an I/O failure that prevents deletion of an unchanged, owned sidecar; it never reports successful cleanup while leaving that lock behind. The handle and manager retain the exact cleanup receipt after a failure, so the same handle can retry `release()` and `manager.drain()` can retry retained cleanup. A changed sidecar remains an ownership mismatch rather than a deletion failure and is left untouched. If both a `withFileLock()` callback and release fail, the release error is the primary `SuppressedError.error` and the callback failure remains available as `SuppressedError.suppressed`. Failed asynchronous acquisition cleanup uses the same shape, with the cleanup error primary and the acquisition failure suppressed. On Node runtimes without the global `SuppressedError` constructor, fs-safe returns the equivalent `Error` shape with the same name and properties.
68
+ `release()` propagates an I/O failure that prevents deletion of an unchanged, owned sidecar; it never reports successful cleanup while leaving that lock behind. The handle and manager retain the exact cleanup receipt after a failure, so the same handle can retry `release()` and `manager.drain()` can retry retained cleanup. A changed sidecar remains an ownership mismatch rather than a deletion failure and is left untouched. If both a `withFileLock()` or `withFileLockSync()` callback and release fail, the release error is the primary `SuppressedError.error` and the callback failure remains available as `SuppressedError.suppressed`. Failed asynchronous acquisition cleanup uses the same shape, with the cleanup error primary and the acquisition failure suppressed. On Node runtimes without the global `SuppressedError` constructor, fs-safe returns the equivalent `Error` shape with the same name and properties.
68
69
 
69
70
  ## API
70
71
 
@@ -295,10 +296,11 @@ Discarding an acquisition observation is not proof that the pathname is absent:
295
296
  another owner may already have created the next record. Every discarded
296
297
  observation consumes the normal retry/deadline budget and requires fresh
297
298
  exclusive creation. It supplies no release, reclaim, or held-lock authority.
298
- If that successor disappears during the recovery metadata probe, the waiter
299
- may discard the probe only with an operation-local receipt for an admitted
300
- regular file with one link, followed by current Root and canonical ancestor
301
- checks. A generic metadata error does not permit this retry, and public
299
+ If that successor disappears or is replaced during the recovery metadata probe,
300
+ the waiter may discard the probe only with an operation-local receipt for an
301
+ admitted regular file with one link. Replacement also requires a single exact
302
+ observation of a different regular file with one link. Current Root and canonical
303
+ ancestor checks must still pass. A generic metadata error does not permit this retry, and public
302
304
  `Root.stat()` still rejects a file that changes during observation.
303
305
  Generic `Root.open()` and held-owner/reclaim reads still reject failed opens.
304
306
  Moving an already-matched pinned descriptor without unlinking it, unknown or
@@ -335,6 +337,12 @@ sidecar no longer matches or after a verification I/O failure. This is
335
337
  detection, not revocation of work already in progress. Asynchronous checks are
336
338
  serialized, so a slow verification never overlaps the next timer tick.
337
339
 
340
+ Ownership-only checks compare serialized bytes, tokens, and file identities
341
+ without decoding an unused default JSON payload. Stale-policy reads still
342
+ decode the payload. Explicit `parsePayload` callbacks keep their existing
343
+ verification and asynchronous-cleanup calls, receivers, and errors;
344
+ synchronous release continues without invoking a custom parser.
345
+
338
346
  The compromise-check interval is validated before payload evaluation or
339
347
  filesystem acquisition. Omit it or pass `0` to disable monitoring; enabled
340
348
  intervals must be finite and between 1 and 2,147,483,647 milliseconds. Values
@@ -437,6 +445,7 @@ error.
437
445
  The sync payload, reclaim, and parsing callbacks must also be synchronous. This
438
446
  shape is appropriate for a short boot migration; it is a poor fit for a server
439
447
  request because retry backoff uses a blocking wait.
448
+ Synchronous `shouldReclaim` and `shouldRemoveStaleLock` reject Promise or thenable results with `TypeError` before deleting the observed sidecar; an asynchronous result is never approval.
440
449
 
441
450
  If termination skips the relevant cleanup handler or cleanup fails, the sidecar remains. In particular, `process.exit()` skips asynchronous Root cleanup; await explicit release or drain during application-managed graceful shutdown. Once `staleMs` elapses (or your `shouldReclaim` returns true), acquisition fails closed by default instead of deleting by path.
442
451
 
@@ -49,7 +49,7 @@ remain with the caller.
49
49
 
50
50
  ```ts
51
51
  function stageFileInDirectory(options: {
52
- directory: string | DirectoryReceipt;
52
+ directory: string | DirectoryReceipt<Stats | BigIntStats>;
53
53
  content: string | Uint8Array;
54
54
  mode?: number;
55
55
  }): Promise<StagedFile>;
@@ -65,6 +65,11 @@ interface StagedFile extends AsyncDisposable {
65
65
  Strings are UTF-8. `mode` is the requested **published** mode and defaults to
66
66
  `0600`; exact final modes, including `000`, are supported. The unpublished file
67
67
  stays at `0600` throughout preparation and any awaited application checks.
68
+ The retained descriptor's actual mode is checked before writing payload bytes,
69
+ by `assertCurrent()`, and before publication; a successful but ineffective
70
+ `chmod` fails with `insecure-permissions`. Final mode verification after
71
+ publication can fail with a `published` receipt while preserving the completed
72
+ file. These are POSIX mode checks, not ACL or ownership admission.
68
73
  After rename succeeds and the published entry passes identity validation, the
69
74
  owner applies the requested mode through its retained file descriptor. Content
70
75
  was synchronized during preparation; publication always synchronizes the parent.
@@ -76,8 +81,9 @@ Creation uses an exclusive, no-follow, close-on-exec open of a generated direct
76
81
  child name. Writes use that descriptor. Inspection uses non-following metadata
77
82
  operations, never a potentially blocking reopen of the leaf.
78
83
 
79
- A supplied directory receipt must still match at admission. Its numeric
80
- identity must be exactly representable; ambiguous identity fails closed.
84
+ A supplied directory receipt must still match at admission. Caller receipts can
85
+ carry numeric `Stats` or exact `BigIntStats`; untracked numeric identities must
86
+ be exactly representable. Ambiguous identity fails closed.
81
87
  Returned receipts are frozen descriptive snapshots, not mutable authority.
82
88
  Changing a supplied receipt after admission cannot retarget the lifecycle.
83
89
 
package/docs/store.md CHANGED
@@ -89,7 +89,9 @@ claim before publication. If another consumer acknowledged, quarantined, or
89
89
  replaced that claim, the migration rejects with `FsSafeError("path-mismatch")`
90
90
  and leaves the newer generation or failed evidence intact. A stale migration
91
91
  rejects both single and batch loads; ordinary callback failures retain their
92
- existing single-load rejection and batch-skip behavior.
92
+ existing single-load rejection and batch-skip behavior. A caller or migration
93
+ error with code `ENOENT` is still a failure, not a missing queue entry; only a
94
+ claim that is absent or disappears before reading returns `null` from a single load.
93
95
 
94
96
  On Windows, migration releases its read pin once at this publication boundary
95
97
  because an open target can block replacement. It rechecks the exact pathname
package/docs/temp.md CHANGED
@@ -155,12 +155,15 @@ uses guarded pathname-recursive removal. This fallback never recursively
155
155
  removes the public workspace name, but it is not atomic conditional deletion: a
156
156
  same-privilege peer that discovers and replaces the private quarantine after
157
157
  verification can still redirect the final pathname removal.
158
+ If admitting a cleanup parent fails and closing its descriptor also fails,
159
+ creation rejects with both failures in an `AggregateError`. This does not select
160
+ compatible fallback or retry the indeterminate descriptor close.
158
161
 
159
162
  Set `cleanupSafety: "require-bounded"` when that concurrent attacker is in scope.
160
163
  Creation then requires native no-replace directory rename, native owned-tree
161
164
  removal, and a readable retained parent descriptor **before** child creation.
162
165
  On POSIX, the final requested `dirMode` must also include owner read
163
- and search (`(dirMode & 0o500) === 0o500`). Preflight failure throws
166
+ and search (`(dirMode & 0o500) === 0o500`). An unavailable capability throws
164
167
  `FsSafeError("helper-unavailable")` without creating a child or calling a scoped
165
168
  callback. The child descriptor is opened
166
169
  while the new directory still has its private creation mode, before an explicit
package/docs/types.md CHANGED
@@ -128,6 +128,8 @@ type RootOptions = {
128
128
  ## `RootReadOptions` / `RootWriteOptions` / `RootCopyOptions`
129
129
 
130
130
  ```ts
131
+ import type { CopyCloneMode, RootCopyPublicationReceipt } from "@openclaw/fs-safe";
132
+
131
133
  type RootReadOptions = Pick<RootDefaults, "hardlinks" | "maxBytes" | "nonBlockingRead" | "symlinks">;
132
134
  type RootWriteOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "durable" | "mkdir" | "mode" | "renameIdentity" | "mutationSymlinks"> & {
133
135
  encoding?: BufferEncoding;
@@ -135,6 +137,11 @@ type RootWriteOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutatio
135
137
  };
136
138
  type RootCopyOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "durable" | "maxBytes" | "mkdir" | "mode" | "mutationSymlinks"> & {
137
139
  sourceHardlinks?: "reject" | "allow";
140
+ overwrite?: boolean;
141
+ clone?: CopyCloneMode;
142
+ signal?: AbortSignal;
143
+ preserveSourceMode?: boolean;
144
+ onDestinationPublished?: (receipt: RootCopyPublicationReceipt) => void;
138
145
  };
139
146
  type RootOpenWritableOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mkdir" | "mode" | "mutationSymlinks"> & {
140
147
  writeMode?: "replace" | "append" | "update";
@@ -150,8 +157,17 @@ type RootAppendOptions = RootWriteOptions & {
150
157
  type RootMoveOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks"> & {
151
158
  overwrite?: boolean;
152
159
  };
153
- type RootRemoveOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks">;
154
- type RootMkdirOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks">;
160
+ type RootRemoveOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks"> & {
161
+ recursive?: boolean;
162
+ force?: boolean;
163
+ order?: "filesystem" | "sorted";
164
+ maxEntries?: number;
165
+ maxDepth?: number;
166
+ signal?: AbortSignal;
167
+ };
168
+ type RootMkdirOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks"> & {
169
+ private?: boolean;
170
+ };
155
171
  ```
156
172
 
157
173
  Per-method option shapes. Each picks the `RootDefaults` keys that apply, plus method-specific extras.
package/docs/walk.md CHANGED
@@ -47,6 +47,10 @@ type WalkDirectoryFailure = {
47
47
 
48
48
  `depth` starts at `1` for direct children of `rootDir`. `relativePath` is always relative to the supplied root. `scannedEntryCount` counts directory entries examined, including entries filtered out by `include`.
49
49
 
50
+ Each entry's `path` is absolute and retains the normalized spelling of the
51
+ supplied root, including followed directory aliases. Paths do not switch to
52
+ the canonical symlink target during descent.
53
+
50
54
  `walkDirectory()` and `walkDirectorySync()` always return `failedDirs`; the property remains optional on the exported `WalkDirectoryResult` type so existing callers that manually construct the legacy result shape remain source-compatible. It lists every directory whose `realpath`/`readdir` threw, so its contents are absent from `entries`. `error` is the thrown value (a `NodeJS.ErrnoException` at runtime), so callers can distinguish a benign missing-directory race (`ENOENT`) from a real read failure (`EACCES`, `EIO`, `ESTALE`, …). The walk-root failure has an empty `relativePath` and `depth: 0`. Failures resolving a symlink's target kind are not reported here.
51
55
 
52
56
  ## Options
@@ -227,6 +231,9 @@ its exact identity, and rechecks it and the Root identity around each metadata
227
231
  batch or individual filesystem-order observation. Sorted batches contain no
228
232
  await or caller code between their before/after checks. It tracks canonical
229
233
  directories to stop symlink cycles.
234
+ Directory rechecks retain exact identities while using ordinary numeric metadata
235
+ when it represents those identities without rounding. Large identities and
236
+ Windows unknown-identity retries keep the bigint inspection path.
230
237
  Neither mode holds a descriptor for every path component, so it is not a process sandbox against a hostile peer that
231
238
  can continuously swap and restore directories. Each individual lookup retains
232
239
  the documented Node `Root` boundary checks.