@openclaw/fs-safe 0.5.1 → 0.5.3

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 (231) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +19 -6
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +20 -12
  5. package/dist/archive-entry.d.ts +1 -0
  6. package/dist/archive-entry.d.ts.map +1 -1
  7. package/dist/archive-entry.js +37 -6
  8. package/dist/archive-errors.d.ts +1 -0
  9. package/dist/archive-errors.d.ts.map +1 -1
  10. package/dist/archive-errors.js +4 -0
  11. package/dist/archive-input.d.ts +7 -0
  12. package/dist/archive-input.d.ts.map +1 -1
  13. package/dist/archive-input.js +11 -1
  14. package/dist/archive-limits.js +1 -1
  15. package/dist/archive-native.d.ts.map +1 -1
  16. package/dist/archive-native.js +12 -6
  17. package/dist/archive-policy.d.ts.map +1 -1
  18. package/dist/archive-policy.js +2 -1
  19. package/dist/archive-read.d.ts.map +1 -1
  20. package/dist/archive-read.js +62 -37
  21. package/dist/archive-staging.d.ts.map +1 -1
  22. package/dist/archive-staging.js +3 -2
  23. package/dist/archive-tar-meta.d.ts.map +1 -1
  24. package/dist/archive-tar-meta.js +15 -1
  25. package/dist/archive-tar-runtime.d.ts +1 -0
  26. package/dist/archive-tar-runtime.d.ts.map +1 -1
  27. package/dist/archive-tar-runtime.js +11 -0
  28. package/dist/archive-tar.d.ts.map +1 -1
  29. package/dist/archive-tar.js +8 -4
  30. package/dist/archive-zip-entry.d.ts +7 -1
  31. package/dist/archive-zip-entry.d.ts.map +1 -1
  32. package/dist/archive-zip-entry.js +11 -1
  33. package/dist/archive-zip-integrity.d.ts +5 -0
  34. package/dist/archive-zip-integrity.d.ts.map +1 -0
  35. package/dist/archive-zip-integrity.js +54 -0
  36. package/dist/archive-zip-preflight.d.ts.map +1 -1
  37. package/dist/archive-zip-preflight.js +12 -1
  38. package/dist/archive.d.ts.map +1 -1
  39. package/dist/archive.js +9 -5
  40. package/dist/bounded-read-stream.d.ts.map +1 -1
  41. package/dist/bounded-read-stream.js +13 -2
  42. package/dist/deny-mutations.d.ts +1 -0
  43. package/dist/deny-mutations.d.ts.map +1 -1
  44. package/dist/deny-mutations.js +6 -39
  45. package/dist/device-path.d.ts +1 -0
  46. package/dist/device-path.d.ts.map +1 -1
  47. package/dist/device-path.js +6 -6
  48. package/dist/directory-guard.d.ts.map +1 -1
  49. package/dist/directory-guard.js +5 -4
  50. package/dist/error-detail.d.ts +2 -0
  51. package/dist/error-detail.d.ts.map +1 -0
  52. package/dist/error-detail.js +4 -0
  53. package/dist/errors.d.ts +1 -1
  54. package/dist/errors.d.ts.map +1 -1
  55. package/dist/errors.js +4 -0
  56. package/dist/file-hash.d.ts.map +1 -1
  57. package/dist/file-hash.js +4 -1
  58. package/dist/file-lock-sync.d.ts.map +1 -1
  59. package/dist/file-lock-sync.js +88 -35
  60. package/dist/file-store-boundary.d.ts +6 -0
  61. package/dist/file-store-boundary.d.ts.map +1 -1
  62. package/dist/file-store-boundary.js +20 -5
  63. package/dist/file-store-sync-write.d.ts +9 -0
  64. package/dist/file-store-sync-write.d.ts.map +1 -0
  65. package/dist/file-store-sync-write.js +97 -0
  66. package/dist/file-store.d.ts.map +1 -1
  67. package/dist/file-store.js +62 -139
  68. package/dist/filename.d.ts.map +1 -1
  69. package/dist/filename.js +27 -0
  70. package/dist/guarded-mkdir.d.ts.map +1 -1
  71. package/dist/guarded-mkdir.js +5 -6
  72. package/dist/install-path.js +1 -1
  73. package/dist/json-document-store.d.ts.map +1 -1
  74. package/dist/json-document-store.js +19 -13
  75. package/dist/json.d.ts.map +1 -1
  76. package/dist/json.js +1 -3
  77. package/dist/local-file-access.d.ts +3 -2
  78. package/dist/local-file-access.d.ts.map +1 -1
  79. package/dist/local-file-access.js +12 -6
  80. package/dist/local-roots.d.ts.map +1 -1
  81. package/dist/local-roots.js +35 -30
  82. package/dist/move-path.d.ts.map +1 -1
  83. package/dist/move-path.js +51 -12
  84. package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
  85. package/dist/native/darwin-x64/fs-safe-native.node +0 -0
  86. package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
  87. package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
  88. package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
  89. package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
  90. package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
  91. package/dist/native-binding.d.ts +1 -0
  92. package/dist/native-binding.d.ts.map +1 -1
  93. package/dist/native-operations.d.ts.map +1 -1
  94. package/dist/native-operations.js +14 -1
  95. package/dist/native-pinned-write.d.ts.map +1 -1
  96. package/dist/native-pinned-write.js +44 -17
  97. package/dist/output-sibling.d.ts.map +1 -1
  98. package/dist/output-sibling.js +6 -5
  99. package/dist/path-policy.d.ts.map +1 -1
  100. package/dist/path-policy.js +1 -7
  101. package/dist/path.d.ts.map +1 -1
  102. package/dist/path.js +4 -0
  103. package/dist/permission-exec.d.ts +6 -0
  104. package/dist/permission-exec.d.ts.map +1 -0
  105. package/dist/permission-exec.js +28 -0
  106. package/dist/permissions-windows.d.ts +56 -0
  107. package/dist/permissions-windows.d.ts.map +1 -0
  108. package/dist/permissions-windows.js +285 -0
  109. package/dist/permissions.d.ts +3 -46
  110. package/dist/permissions.d.ts.map +1 -1
  111. package/dist/permissions.js +17 -328
  112. package/dist/pinned-open.d.ts.map +1 -1
  113. package/dist/pinned-open.js +21 -4
  114. package/dist/pinned-write.js +2 -1
  115. package/dist/private-temp-workspace.d.ts.map +1 -1
  116. package/dist/private-temp-workspace.js +45 -4
  117. package/dist/read-error.d.ts +2 -0
  118. package/dist/read-error.d.ts.map +1 -0
  119. package/dist/read-error.js +11 -0
  120. package/dist/regular-file.d.ts.map +1 -1
  121. package/dist/regular-file.js +21 -2
  122. package/dist/replace-directory.d.ts.map +1 -1
  123. package/dist/replace-directory.js +10 -1
  124. package/dist/replace-file-copy-fallback.d.ts +3 -0
  125. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  126. package/dist/replace-file-copy-fallback.js +18 -11
  127. package/dist/replace-file-descriptor.d.ts +33 -0
  128. package/dist/replace-file-descriptor.d.ts.map +1 -0
  129. package/dist/replace-file-descriptor.js +96 -0
  130. package/dist/replace-file.d.ts +9 -2
  131. package/dist/replace-file.d.ts.map +1 -1
  132. package/dist/replace-file.js +64 -72
  133. package/dist/root-context.d.ts +8 -0
  134. package/dist/root-context.d.ts.map +1 -1
  135. package/dist/root-context.js +43 -2
  136. package/dist/root-errors.d.ts +7 -0
  137. package/dist/root-errors.d.ts.map +1 -1
  138. package/dist/root-errors.js +46 -1
  139. package/dist/root-file.d.ts +1 -0
  140. package/dist/root-file.d.ts.map +1 -1
  141. package/dist/root-file.js +2 -0
  142. package/dist/root-impl.d.ts.map +1 -1
  143. package/dist/root-impl.js +216 -169
  144. package/dist/root-path-symlink.d.ts +3 -0
  145. package/dist/root-path-symlink.d.ts.map +1 -0
  146. package/dist/root-path-symlink.js +35 -0
  147. package/dist/root-path.d.ts +1 -0
  148. package/dist/root-path.d.ts.map +1 -1
  149. package/dist/root-path.js +129 -190
  150. package/dist/root-walk.d.ts.map +1 -1
  151. package/dist/root-walk.js +15 -0
  152. package/dist/safe-path-segment.d.ts +2 -0
  153. package/dist/safe-path-segment.d.ts.map +1 -1
  154. package/dist/safe-path-segment.js +12 -0
  155. package/dist/secret-file.d.ts.map +1 -1
  156. package/dist/secret-file.js +29 -6
  157. package/dist/secret-read-async.d.ts.map +1 -1
  158. package/dist/secret-read-async.js +5 -1
  159. package/dist/secure-temp-dir.d.ts.map +1 -1
  160. package/dist/secure-temp-dir.js +6 -1
  161. package/dist/short-path.d.ts +2 -0
  162. package/dist/short-path.d.ts.map +1 -0
  163. package/dist/short-path.js +7 -0
  164. package/dist/sidecar-lock-acquire.d.ts +30 -0
  165. package/dist/sidecar-lock-acquire.d.ts.map +1 -0
  166. package/dist/sidecar-lock-acquire.js +260 -0
  167. package/dist/sidecar-lock-policy.d.ts +3 -0
  168. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  169. package/dist/sidecar-lock-policy.js +20 -3
  170. package/dist/sidecar-lock-reclaim.d.ts +5 -1
  171. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  172. package/dist/sidecar-lock-reclaim.js +59 -9
  173. package/dist/sidecar-lock.d.ts.map +1 -1
  174. package/dist/sidecar-lock.js +27 -226
  175. package/dist/symlink-parents.d.ts.map +1 -1
  176. package/dist/symlink-parents.js +9 -8
  177. package/dist/temp-target.d.ts.map +1 -1
  178. package/dist/temp-target.js +15 -4
  179. package/dist/test-hooks.d.ts +1 -0
  180. package/dist/test-hooks.d.ts.map +1 -1
  181. package/dist/timing.d.ts +2 -0
  182. package/dist/timing.d.ts.map +1 -1
  183. package/dist/timing.js +9 -0
  184. package/dist/trash.d.ts.map +1 -1
  185. package/dist/trash.js +10 -1
  186. package/dist/walk.d.ts.map +1 -1
  187. package/dist/walk.js +15 -0
  188. package/docs/advanced.md +7 -1
  189. package/docs/archive.md +27 -20
  190. package/docs/assets/readme-banner.jpg +0 -0
  191. package/docs/atomic.md +48 -19
  192. package/docs/errors.md +34 -8
  193. package/docs/file-store.md +14 -1
  194. package/docs/filename.md +17 -16
  195. package/docs/index.md +1 -0
  196. package/docs/install-path.md +9 -6
  197. package/docs/json.md +8 -2
  198. package/docs/local-roots.md +79 -103
  199. package/docs/native-helper.md +11 -5
  200. package/docs/native.md +2 -2
  201. package/docs/output.md +8 -3
  202. package/docs/path-scope.md +4 -1
  203. package/docs/path.md +7 -3
  204. package/docs/permissions.md +8 -0
  205. package/docs/public-api.md +109 -0
  206. package/docs/quickstart.md +6 -2
  207. package/docs/reading.md +13 -10
  208. package/docs/root.md +26 -4
  209. package/docs/secret-file.md +21 -4
  210. package/docs/security-model.md +21 -4
  211. package/docs/sidecar-lock.md +15 -5
  212. package/docs/temp.md +42 -5
  213. package/docs/test-hooks.md +2 -0
  214. package/docs/types.md +32 -15
  215. package/docs/writing.md +25 -12
  216. package/package.json +15 -4
  217. package/dist/archive-file-io.d.ts +0 -9
  218. package/dist/archive-file-io.d.ts.map +0 -1
  219. package/dist/archive-file-io.js +0 -11
  220. package/dist/file-store-source.d.ts +0 -5
  221. package/dist/file-store-source.d.ts.map +0 -1
  222. package/dist/file-store-source.js +0 -30
  223. package/dist/file-url.d.ts +0 -2
  224. package/dist/file-url.d.ts.map +0 -1
  225. package/dist/file-url.js +0 -1
  226. package/dist/path-stat.d.ts +0 -4
  227. package/dist/path-stat.d.ts.map +0 -1
  228. package/dist/path-stat.js +0 -15
  229. package/dist/windows-permissions-native.d.ts +0 -9
  230. package/dist/windows-permissions-native.d.ts.map +0 -1
  231. package/dist/windows-permissions-native.js +0 -31
package/docs/root.md CHANGED
@@ -21,9 +21,10 @@ type RootDefaults = {
21
21
  hardlinks?: "reject" | "allow"; // refuse files with nlink > 1 on read; defaults to "reject"
22
22
  denyMutations?: DenyMutationPolicy; // absolute paths/prefixes mutation methods may not change
23
23
  maxBytes?: number; // refuse reads larger than this many bytes; defaults to 16 MiB
24
- mkdir?: boolean; // create missing parent dirs on write/openWritable/append
24
+ mkdir?: boolean; // create missing parent dirs on write/openWritable/append; default true
25
25
  mode?: number; // file mode applied to new writes; per-call override available
26
- nonBlockingRead?: boolean; // schedule reads on a worker; useful for large files
26
+ nonBlockingRead?: boolean; // compatibility hint; safe opens are already nonblocking where supported
27
+ renameIdentity?: "strict" | "verify-content-with-lock"; // default "strict"
27
28
  symlinks?: "reject" | "follow-within-root"; // policy when a path component is a symlink
28
29
  };
29
30
 
@@ -83,6 +84,10 @@ await using opened = await fs.open("large.log");
83
84
  `containment: "best-effort"`. The field reports the mechanism used; see the
84
85
  [security model](security-model.md#containment-guarantees-by-platform).
85
86
 
87
+ The read methods also accept an absolute spelling that already resolves inside
88
+ the root. `readAbsolute()` and `reader()` make that intent explicit; an absolute
89
+ path outside the root is still rejected.
90
+
86
91
  ### Writes
87
92
 
88
93
  ```ts
@@ -103,6 +108,14 @@ fs.ensureRoot(options?) // accepts "" / "." as the root itself
103
108
 
104
109
  `copyIn` is a one-shot ingest from a trusted absolute source path: it streams the source through the boundary, atomically renames into the root, and respects `maxBytes`.
105
110
 
111
+ Root operations that choose a new destination reject a leading Windows
112
+ drive-relative spelling such as `C:name` on every platform. This applies to
113
+ `write`, `create`, `append`, `openWritable`, `mkdir`, `copyIn`, and the
114
+ destination argument of `move`. In particular, `copyIn(path.basename(source),
115
+ source)` can reject a legal POSIX basename such as `c:photo.png`; callers that
116
+ derive portable destination names from host files must sanitize or map that
117
+ basename first.
118
+
106
119
  `openWritable` opens a writable file with options `mode?: number` and `writeMode?: "replace" | "append" | "update"`. `replace` truncates existing files and is the default; `update` keeps existing contents. Use it for streaming output. Prefer `await using` for cleanup.
107
120
 
108
121
  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.
@@ -119,6 +132,12 @@ fs.resolve(rel) // absolute path inside the root, after canonic
119
132
 
120
133
  These do not pin a later operation. They are safe to expose to UIs and decision points; for the actual read or write, use the verb methods so the operation pins identity at the point of use.
121
134
 
135
+ `resolve()` is the exception to the existing-object rule: because it selects a
136
+ location for later use, it rejects a leading drive-relative spelling. Reads,
137
+ `stat`, `exists`, `list`, `walk`, `remove`, and the source argument of `move`
138
+ accept an existing POSIX filename such as `c:notes.txt`. For `move`, only the
139
+ new destination name is subject to the portable guard.
140
+
122
141
  ## Native helper mode
123
142
 
124
143
  Create-only writes prefer the bundled native helper for fd-relative opens and
@@ -147,11 +166,14 @@ fs.defaults // the RootDefaults you passed
147
166
 
148
167
  ## Failure semantics
149
168
 
150
- Every method throws `FsSafeError` with a `code`. Branch on `err.code`, not message text. Common codes:
169
+ Boundary and policy failures throw `FsSafeError` with a `code`. Parsing callbacks
170
+ and underlying filesystem operations can also surface `SyntaxError` or native
171
+ `NodeJS.ErrnoException` values. Branch on `err.code`, not message text, after
172
+ checking `err instanceof FsSafeError`. Common fs-safe codes:
151
173
 
152
174
  | Code | When it fires |
153
175
  |---|---|
154
- | `invalid-path` | The input path is malformed, including embedded NUL bytes. |
176
+ | `invalid-path` | The input path is malformed, including embedded NUL bytes. Portable relative-path helpers and `FileStore` keys reject drive-relative segments; Root destination and resolution operations reject a leading drive-relative spelling such as `C:name`. |
155
177
  | `outside-workspace` | The input resolves outside the root, or contains a `..` segment that would escape it. |
156
178
  | `not-found` | The target does not exist (or its parent does not, with `mkdir: false`). |
157
179
  | `not-file` | A read or copy targeted a non-regular file (directory, FIFO, socket, …). |
@@ -23,7 +23,7 @@ import {
23
23
  | The file is a credential (token, key, password). | The file is application state. |
24
24
  | You want the parent directory created at `0o700` if missing. | You don't care about the parent directory mode. |
25
25
  | You want a hard size cap on reads (to defend against bogus input). | You're reading bounded JSON state. |
26
- | Mode `0o600` is mandatory, not just nice. | Mode is whatever umask gives you. |
26
+ | Mode `0o600` and the private parent policy are mandatory. | Standalone `writeJson` also defaults to `0o600`, but it does not enforce the secret-directory ownership and permission policy. |
27
27
 
28
28
  ## Constants
29
29
 
@@ -39,7 +39,10 @@ The 16 KiB cap is intentionally aggressive — credentials should be small. If y
39
39
 
40
40
  ### `tryReadSecretFileSync(filePath, label, options?)`
41
41
 
42
- The lenient reader. Returns the trimmed secret string, or `undefined` when the path is missing or blank. Validation failures, unreadable files, oversized files, symlinks, and hardlinks throw `FsSafeError` so callers fail closed on suspicious credential state.
42
+ The missing-is-optional reader. It returns the trimmed secret string, or
43
+ `undefined` when the `filePath` argument is absent/blank or the target does not
44
+ exist. An existing empty file is invalid and throws, as do unreadable,
45
+ oversized, symlink, hardlink, and other validation failures.
43
46
 
44
47
  ```ts
45
48
  import { tryReadSecretFileSync } from "@openclaw/fs-safe/secret";
@@ -65,17 +68,31 @@ const token = readSecretFileSync("/var/lib/app/auth.token", "auth token");
65
68
  ```ts
66
69
  type SecretFileReadOptions = {
67
70
  maxBytes?: number; // default DEFAULT_SECRET_FILE_MAX_BYTES (16 KiB)
68
- rejectSymlink?: boolean;
71
+ rejectSymlink?: boolean; // default false
69
72
  rejectHardlinks?: boolean; // default true
70
73
  };
71
74
  ```
72
75
 
73
- The reader trims the file content and rejects empty results. `rejectSymlink` blocks a symlink path before the pinned read. Hardlinks are rejected by default so another in-tree name cannot alias the credential; pass `rejectHardlinks: false` only when you explicitly trust that layout.
76
+ The reader trims the file content and rejects empty results. Symlink paths are
77
+ followed and pinned by default; set `rejectSymlink: true` when the pathname
78
+ itself must not be an alias. Hardlinks are rejected by default so another
79
+ in-tree name cannot alias the credential; pass `rejectHardlinks: false` only
80
+ when you explicitly trust that layout.
81
+
82
+ These readers do not enforce ownership or mode bits on an existing file. Their
83
+ read contract covers pinned identity, file type, link policy, and byte bounds;
84
+ the `0o600` guarantee belongs to the write helpers below. Use
85
+ [`readSecureFile`](secure-file.md) when reading an externally managed
86
+ credential must also fail on broad permissions or unexpected ownership.
74
87
 
75
88
  `readSecretFile()` and `tryReadSecretFile()` are asynchronous counterparts with
76
89
  the same pinned-handle validation, byte cap, trimming, error codes, and strict
77
90
  versus missing-is-undefined naming semantics.
78
91
 
92
+ If an already validated descriptor fails while reading, both readers throw an
93
+ operational `FsSafeError` with `code: "read-failed"`; inspect `cause` for the
94
+ underlying Node filesystem code such as `EIO`.
95
+
79
96
  Use the async strict reader when a service cannot start safely without the
80
97
  credential:
81
98
 
@@ -22,7 +22,8 @@ case since `5ddca80`. Exposure is limited to consumers that call
22
22
  You hand a `root()` boundary to a piece of code that takes caller-controlled relative paths. The library defends against a caller that:
23
23
 
24
24
  - supplies `..` traversal segments to escape the boundary
25
- - supplies an absolute path where a relative one is expected
25
+ - supplies an absolute path outside a configured root, or to an API whose input
26
+ contract is strictly relative/portable
26
27
  - replaces a path component with a symlink between check and use (TOCTOU)
27
28
  - replaces the destination directory with a symlink right before a write
28
29
  - creates a hardlink that aliases an out-of-tree inode and asks you to read or replace it
@@ -44,7 +45,7 @@ If you need full sandboxing, run the worker under reduced privileges (uid, conta
44
45
 
45
46
  ### Path traversal and absolute paths
46
47
 
47
- Every relative path is resolved against the canonicalized real path of the root, then checked with `isPathInside`. Alias resolution walks components before applying a later `..`, so a symlink cannot change what that parent segment means after validation. Parent traversal that escapes, leading `/` (without `pathScope` opt-in), or any canonical result outside the root throws `outside-workspace`.
48
+ Every path is resolved against the canonicalized real path of the root, then checked with `isPathInside`. Alias resolution walks components before applying a later `..`, so a symlink cannot change what that parent segment means after validation. Parent traversal, an absolute spelling, or any alias whose canonical result is outside the root throws `outside-workspace`; absolute spellings that remain inside the root are accepted.
48
49
 
49
50
  ### Symlinks (read side)
50
51
 
@@ -54,7 +55,20 @@ Per-call `symlinks: "follow-within-root"` allows symlinks whose final target is
54
55
 
55
56
  ### Symlinks (write side)
56
57
 
57
- Writes use a sibling-temp + rename helper that opens the parent directory by fd, then performs the rename `at` the parent fd. Replacing the parent directory with a symlink between the parent-fd open and the rename does not divert the write.
58
+ With the native binding loaded, `write()`, `create()`, and `copyIn()` use a
59
+ sibling temp file and create parents and publish the target relative to pinned
60
+ directory descriptors. Replacement uses descriptor-relative rename just like
61
+ no-replace publication, so replacing the parent pathname does not divert the
62
+ mutation.
63
+
64
+ The JavaScript fallback used by `off`, by `auto` when no binding loads, and by
65
+ the explicit `renameIdentity: "verify-content-with-lock"` compatibility policy
66
+ cannot provide that guarantee because Node exposes no `mkdirat` or `renameat`.
67
+ It asserts directory identity around a pathname mutation and detects many
68
+ swaps, but detection occurs after the kernel may already have followed a new
69
+ parent symlink. A same-privilege peer with write access to the parent can
70
+ therefore cause an out-of-root side effect before the operation throws. Use
71
+ native `require` mode when concurrent hostile mutation is in scope.
58
72
 
59
73
  ### Hardlink aliasing
60
74
 
@@ -64,13 +78,15 @@ When `hardlinks: "reject"` is set, reads stat the target and refuse if `nlink >
64
78
 
65
79
  `resolve()`, `exists()`, `stat()`, and `list()` are explicitly advisory — they answer a question and return. To act on a path with operation-local identity checks, use `read()`, `open()`, `write()`, `create()`, `copyIn()`, `move()`, or `remove()`. The containment table below states which opens are kernel-atomic and which remain best-effort.
66
80
 
81
+ A `root()` handle also remembers the canonical root directory identity. Calls fail with `path-mismatch` if that canonical pathname is replaced, including advisory inspection and walking calls, rather than following a replacement root into another tree.
82
+
67
83
  ### Denied mutations
68
84
 
69
85
  `denyMutations` is an opt-in application policy for `root()` mutation methods. It blocks exact absolute paths with `paths` and whole subtrees with `prefixes`, merging root defaults with per-call entries so a call cannot clear root-level denies. This is not an OS permission boundary: code with access to `node:fs`, a shell, or another process with the same filesystem privileges can bypass it.
70
86
 
71
87
  ### Atomic writes
72
88
 
73
- `replaceFileAtomic` writes to a sibling temp file in the destination directory, optionally `fsync`s it, optionally `fsync`s the parent directory after rename, and atomically renames over the destination. On failure mid-write, the destination is either the old contents (rename never happened) or the new contents (rename succeeded). There is no half-written intermediate state visible at the destination path.
89
+ `replaceFileAtomic` writes to a sibling temp file in the destination directory, optionally `fsync`s it, optionally `fsync`s the parent directory after rename, and atomically renames over the destination. On failure mid-write, the destination is either the old contents (rename never happened) or the new contents (rename succeeded). There is no half-written intermediate state visible at the destination path unless the caller explicitly enables `copyFallbackOnPermissionError`, whose default `copyFallbackRestore: "none"` contract may leave a partial destination after a failed in-place fallback.
74
90
 
75
91
  Within one process, async writes to the same target are queued so their temp-write/rename phases do not overlap. Cross-process writers still need an external protocol such as the sidecar lock helpers.
76
92
 
@@ -120,6 +136,7 @@ The public `OpenResult`, `ReadResult`, and `WritableOpenResult` expose `containm
120
136
  | Absolute paths are escape hatches | APIs that accept or return absolute paths exist for audit, ingest, and advanced composition. Prefer root-relative names in normal application flow. |
121
137
  | Not a mount boundary | `root()` keeps path traversal inside the directory tree and blocks known unsafe read device paths, but it does not make bind mounts or virtual filesystems safe to expose wholesale. |
122
138
  | Per-call, not per-session | Another process with the same privileges can still mutate the tree between calls, and best-effort mechanisms retain documented same-call race windows. Use one verb method to minimize the window and inspect its reported containment class. |
139
+ | JavaScript mutations are detection-based | Without the native binding, Node pathname mutations retain a check-to-syscall race. A writable parent can be swapped so a create, rename, or removal affects an out-of-root path before the fallback detects identity drift. |
123
140
  | Hardlink rejection is best-effort | Link-count checks depend on platform metadata. Treat `hardlinks: "reject"` as a tripwire, not an authorization primitive. |
124
141
  | Mode bits are not a full policy engine | `replaceFileAtomic` and secret-file helpers set requested modes, but you should still set umask and inspect modes when policy requires it. |
125
142
  | Archive extraction is path safety, not content safety | Unsafe entry paths and links are rejected; malicious payload contents remain your application layer's problem. |
@@ -96,6 +96,12 @@ type FileLockRetryOptions = {
96
96
  result is passed to `shouldReclaim` and `shouldRemoveStaleLock`, allowing PID,
97
97
  process-start, argv, or role schemas to remain application-owned.
98
98
 
99
+ On Windows, a pathed `EPERM` from creating or opening the lock file can be a
100
+ short teardown race after another holder unlinks it. The async lock retries that
101
+ specific denial at most eight times. A parent-directory denial, a denial from a
102
+ callback, or a ninth consecutive lock-file denial surfaces as the original
103
+ `EPERM`; it is not converted to `file_lock_timeout`.
104
+
99
105
  ## Owner-scoped reentrancy
100
106
 
101
107
  Version 0.5 removes the unsound process-scoped `allowReentrant` boolean and
@@ -167,10 +173,10 @@ progress.
167
173
  ## Synchronous locks
168
174
 
169
175
  `acquireFileLockSync()` and `withFileLockSync()` mirror filesystem arbitration,
170
- retry, payload parsing, stale policy, guarded identity-conditioned reclaim,
171
- verification, and compromise monitoring. They do not use the async manager
172
- queue, support async callbacks, or provide same-process reentrancy. Retry waits
173
- block the calling thread; use the async API in request-serving code.
176
+ retry, payload parsing, stale policy, owner-scoped reentrancy, guarded
177
+ identity-conditioned reclaim, verification, and compromise monitoring. They do
178
+ not use the async manager queue or support async callbacks. Retry waits block
179
+ the calling thread; use the async API in request-serving code.
174
180
 
175
181
  Always release in a `finally`:
176
182
 
@@ -236,7 +242,11 @@ await locks.drain();
236
242
 
237
243
  ## Stale policy: `shouldReclaim`
238
244
 
239
- The default policy treats locks whose `createdAt` is older than `staleMs` as stale. Pass a custom callback when you want a richer notion of "is the holder still alive":
245
+ The default policy treats locks whose valid `createdAt` is older than `staleMs`
246
+ as stale. A valid current or future timestamp remains authoritative under
247
+ filesystem clock skew; only absent or malformed timestamps fall back to the
248
+ sidecar `mtime`. Pass a custom callback when you want a richer notion of "is the
249
+ holder still alive":
240
250
 
241
251
  ```ts
242
252
  import { kill } from "node:process";
package/docs/temp.md CHANGED
@@ -45,6 +45,11 @@ await runBuild(workspace.dir, inputPath);
45
45
  ```
46
46
 
47
47
  `write` writes at `mode` (default `0o600`); `writeText` and `writeJson` are convenience wrappers for the common scratch-file shapes; `copyIn` ingests an absolute source path through the same atomic-rename machinery as `Root.copyIn`. `read` is a small accessor that reads back any file you wrote into the workspace.
48
+ Both the async and sync `read` methods throw `FsSafeError("not-file")` when the
49
+ named leaf is a directory or another non-regular target, and preserve the
50
+ `not-found`, `hardlink`, or `symlink` code for those stable target states.
51
+ Operational filesystem read failures use `read-failed` with the Node error in
52
+ `cause`.
48
53
 
49
54
  `store` is a `fileStore({ rootDir: workspace.dir, private: true })` handle. Use
50
55
  it when you want the richer store surface, including `writeStream`, `exists`,
@@ -157,11 +162,15 @@ type TempFile = {
157
162
  path: string; // absolute path; safe to write to
158
163
  dir: string; // the enclosing private workspace dir
159
164
  file(fileName?: string): string; // resolve another file in the same dir
160
- cleanup(): Promise<void>; // removes the private workspace dir
165
+ cleanup(): Promise<void>; // removes the original private workspace dir
161
166
  [Symbol.asyncDispose](): Promise<void>; // alias of cleanup()
162
167
  };
163
168
  ```
164
169
 
170
+ Cleanup captures the directory identity at creation time. If that path is
171
+ renamed away and replaced, cleanup preserves the replacement rather than
172
+ recursively deleting a directory it did not create.
173
+
165
174
  ### `withTempFile`
166
175
 
167
176
  Same shape with auto-cleanup:
@@ -239,13 +248,41 @@ const tempRoot = resolveSecureTempRoot({ fallbackPrefix: "my-app" });
239
248
 
240
249
  ```ts
241
250
  type ResolveSecureTempRootOptions = {
242
- fallbackPrefix: string; // base name for the per-user fallback dir
243
- preferredDir?: string; // optional preferred secure temp root
244
- tmpdir?: () => string; // override os.tmpdir()
251
+ fallbackPrefix: string; // one portable path segment; invalid values throw
252
+ preferredDir?: string; // preferred secure temp root
253
+ skipPreferredOnWindows?: boolean;
254
+ unsafeFallbackLabel?: string; // text used in thrown errors
255
+ warningPrefix?: string; // default "[fs-safe]"
256
+ warn?: (message: string) => void; // default console.warn
257
+
258
+ // Platform/test adapters; production callers normally omit these.
259
+ platform?: NodeJS.Platform;
260
+ getuid?: () => number | undefined;
261
+ tmpdir?: () => string;
262
+ accessSync?: typeof import("node:fs").accessSync;
263
+ chmodSync?: typeof import("node:fs").chmodSync;
264
+ lstatSync?: (path: string) => {
265
+ isDirectory(): boolean;
266
+ isSymbolicLink(): boolean;
267
+ mode?: number;
268
+ uid?: number;
269
+ };
270
+ mkdirSync?: (
271
+ path: string,
272
+ options: { recursive: boolean; mode?: number },
273
+ ) => void;
245
274
  };
246
275
  ```
247
276
 
248
- The directory name embeds the user's UID (POSIX) or username so multi-user systems don't collide. On unsupported platforms, falls back to `os.tmpdir()` directly with a `helper-unavailable` error code surfaced to callers that explicitly required the secure root.
277
+ When `process.getuid()` is available, the fallback is
278
+ `<tmpdir>/<fallbackPrefix>-<uid>`. Without a UID (including Windows), it is
279
+ `<tmpdir>/<fallbackPrefix>`; no username is appended. The helper never returns
280
+ the shared `os.tmpdir()` directory itself. It requires the selected path to be
281
+ a writable, non-symlink directory and, when UID/mode facts are available,
282
+ owned by the current user without group/world write bits. It creates or repairs
283
+ the fallback to mode `0o700` where mode bits apply. If it cannot establish that
284
+ state, it throws an ordinary `Error`; there is no native mode or
285
+ `helper-unavailable` branch on this API.
249
286
 
250
287
  ## Common patterns
251
288
 
@@ -37,6 +37,7 @@ type FsSafeTestHooks = {
37
37
  beforeRootFallbackMutation?: (operation: "mkdir" | "move" | "remove", targetPath: string) => Promise<void> | void;
38
38
  afterPinnedWriteFallbackRename?: (targetPath: string) => Promise<void> | void;
39
39
  beforeSiblingTempWrite?: (tempPath: string) => Promise<void> | void;
40
+ beforeSidecarLockSnapshotOpen?: (lockPath: string) => Promise<void> | void;
40
41
  beforeTrashMove?: (targetPath: string, destPath: string) => void;
41
42
  afterPublishTargetCreated?: (method, targetPath, identity) => Promise<void> | void;
42
43
  beforePublishDirectorySync?: (method, targetPath, identity) => Promise<void> | void;
@@ -54,6 +55,7 @@ type FsSafeTestHooks = {
54
55
  | `beforeRootFallbackMutation` | A guarded JS root fallback is about to mkdir, move, or remove. |
55
56
  | `afterPinnedWriteFallbackRename` | A fallback rename committed and post-commit identity checks have not run yet. |
56
57
  | `beforeSiblingTempWrite` | A sibling temp file exists and its writer is about to run. |
58
+ | `beforeSidecarLockSnapshotOpen` | A sidecar lock was inspected and is about to be opened for a bounded snapshot read. |
57
59
  | `beforeTrashMove` | Trash handling is about to move the target. |
58
60
  | `afterPublishTargetCreated` | Exclusive publication created its target and final fences have not run yet. |
59
61
  | `beforePublishDirectorySync` | Publication verified the target and is about to sync its parent directory. |
package/docs/types.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  The types most callers reach for. Shared data shapes are exported from `@openclaw/fs-safe/types`; method-specific option/result types live next to their subpath.
4
4
 
5
+ For atomic replacement, `ReplaceFileAtomicFileSystem` and `ReplaceFileAtomicSyncFileSystem` are exported from `@openclaw/fs-safe/atomic`. Async adapters use `chmod()` on the `FileHandle` returned by their required `open()` operation. The synchronous type adds optional `fchmodSync(fd, mode)`; custom sync adapters that explicitly request `mode` or `preserveExistingMode` must implement it. See [Atomic writes](atomic.md#test-injection).
6
+
5
7
  ```ts
6
8
  import type {
7
9
  BasePathOptions,
@@ -16,15 +18,24 @@ import type {
16
18
 
17
19
  ```ts
18
20
  type PathStat = {
19
- kind: "file" | "directory" | "symlink" | "fifo" | "socket" | "blockDevice" | "characterDevice" | "unknown";
20
- size: number; // bytes
21
- mtimeMs: number; // milliseconds since epoch
22
- mode: number; // POSIX mode bits
23
- nlink: number; // hardlink count
21
+ dev: number;
22
+ gid: number;
23
+ ino: number;
24
+ isDirectory: boolean;
25
+ isFile: boolean;
26
+ isSymbolicLink: boolean;
27
+ mode: number;
28
+ mtimeMs: number;
29
+ nlink: number;
30
+ size: number;
31
+ uid: number;
24
32
  };
25
33
  ```
26
34
 
27
- The shape returned by `Root.stat()`. A trimmed view of `node:fs.Stats` — only the fields the boundary cares about. Use `kind` instead of inspecting the various `is*` methods on a Node `Stats` object; it covers every case in one switchable string.
35
+ The shape returned by `Root.stat()`. It is a serializable view of the identity,
36
+ ownership, mode, size, timestamp, link count, and three file-kind facts the
37
+ boundary uses. Unlike Node's `Stats`, `isFile`, `isDirectory`, and
38
+ `isSymbolicLink` are boolean fields rather than methods.
28
39
 
29
40
  ## `DirEntry`
30
41
 
@@ -34,21 +45,23 @@ type DirEntry = PathStat & {
34
45
  };
35
46
  ```
36
47
 
37
- Returned by `Root.list(rel, { withFileTypes: true })`. Includes the same `kind`/`size`/etc as `PathStat`, plus the entry's `name`.
48
+ Returned by `Root.list(rel, { withFileTypes: true })`. Includes every
49
+ `PathStat` field plus the entry's `name`.
38
50
 
39
51
  ## `BasePathOptions`
40
52
 
41
53
  ```ts
42
54
  type BasePathOptions = {
43
- fastPathMode?: FastPathMode;
55
+ rootDir: string;
56
+ relativePath: string;
44
57
  };
45
58
 
46
59
  type FastPathMode = "auto" | "never" | "require";
47
60
  ```
48
61
 
49
- Options shared by helpers that can take a "fast path" (use cheaper syscalls when the input is already absolute and clearly inside scope). The default is `"auto"` let the helper pick. Force `"never"` in tests if you want to exercise the slow path. Force `"require"` if you need to assert that the fast path is taken (the helper throws if it can't).
50
-
51
- Most callers don't need to touch this.
62
+ `BasePathOptions` is the shared root-plus-relative-path record. `FastPathMode`
63
+ is retained as a public compatibility union; no current exported options record
64
+ consumes it, so setting a fast-path policy is not part of the current API.
52
65
 
53
66
  ## `SafeEncoding`
54
67
 
@@ -83,13 +96,16 @@ type ReadResult = {
83
96
  ## `RootDefaults` / `RootOptions`
84
97
 
85
98
  ```ts
99
+ type RenameIdentityPolicy = "strict" | "verify-content-with-lock";
100
+
86
101
  type RootDefaults = {
87
102
  denyMutations?: DenyMutationPolicy;
88
103
  hardlinks?: "reject" | "allow";
89
104
  maxBytes?: number;
90
- mkdir?: boolean;
105
+ mkdir?: boolean; // default true for mutation methods
91
106
  mode?: number;
92
107
  nonBlockingRead?: boolean;
108
+ renameIdentity?: RenameIdentityPolicy;
93
109
  symlinks?: "reject" | "follow-within-root";
94
110
  };
95
111
 
@@ -110,7 +126,7 @@ type RootOptions = {
110
126
 
111
127
  ```ts
112
128
  type RootReadOptions = Pick<RootDefaults, "hardlinks" | "maxBytes" | "nonBlockingRead" | "symlinks">;
113
- type RootWriteOptions = Pick<RootDefaults, "denyMutations" | "mkdir" | "mode"> & {
129
+ type RootWriteOptions = Pick<RootDefaults, "denyMutations" | "mkdir" | "mode" | "renameIdentity"> & {
114
130
  encoding?: BufferEncoding;
115
131
  overwrite?: boolean;
116
132
  };
@@ -155,13 +171,14 @@ type FsSafeErrorCode =
155
171
  | "helper-unavailable" | "insecure-permissions" | "invalid-path"
156
172
  | "not-empty" | "not-file" | "not-found" | "not-owned"
157
173
  | "not-removable" | "outside-workspace" | "path-alias"
158
- | "path-mismatch" | "permission-unverified" | "symlink"
174
+ | "path-mismatch" | "permission-unverified" | "read-failed" | "secret-exists"
175
+ | "store-reentrant-update" | "symlink"
159
176
  | "timeout" | "too-large" | "unsupported-platform";
160
177
  ```
161
178
 
162
179
  Closed union you switch on. See the [Errors](errors.md) reference for what each one means.
163
180
 
164
- `FsSafeError.category` is `"policy"` for unsafe input/target-state failures and `"operational"` for environment/runtime failures.
181
+ `FsSafeError.category` is `"policy"` for unsafe input or target state rejected by a safety policy and `"operational"` for routine filesystem outcomes or environment/runtime failures. `not-found`, `not-empty`, `not-removable`, and `read-failed` are operational.
165
182
 
166
183
  ## See also
167
184
 
package/docs/writing.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # Writing
2
2
 
3
- The `Root` handle exposes a tight set of write verbs. Each one is atomic at the destination — no half-written intermediate state — and goes through the same boundary checks as reads.
3
+ The `Root` handle exposes a tight set of mutation verbs. Replacement writes
4
+ (`write`, `create`, `writeJson`, `createJson`, and `copyIn`) publish with a
5
+ sibling-temp commit so no half-written replacement appears at the destination.
6
+ `append` and `openWritable` intentionally modify an opened file in place;
7
+ `move`, `remove`, and `mkdir` mutate directory entries rather than file bytes.
8
+ Each verb applies the boundary checks appropriate to its operation.
4
9
 
5
10
  ```ts
6
11
  await fs.write("state.json", body);
@@ -13,16 +18,20 @@ await fs.remove("logs/yesterday.log");
13
18
  await fs.mkdir("snapshots/2026/05");
14
19
  ```
15
20
 
16
- ## What every write does
21
+ ## What replacement writes do
17
22
 
18
23
  1. Resolve the relative target against the canonical root and reject anything that escapes (`outside-workspace`).
19
- 2. If `mkdir: true`, create missing parent directories with the parent fd pinned.
20
- 3. Pin or guard the parent directory for the selected mechanism. Native operations use a parent fd; guarded JavaScript verifies directory identity before and after mutation. Linux beneath opens are kernel-atomic, while macOS, Windows, and JavaScript routes retain the best-effort race boundaries in the [security model](security-model.md#containment-guarantees-by-platform).
24
+ 2. If `mkdir: true`, create missing parent directories relative to a pinned parent fd in the native path, or with per-component identity guards in the JavaScript fallback.
25
+ 3. Pin or guard the parent directory for the selected mechanism. Native operations use a parent fd; guarded JavaScript verifies directory identity before and after mutation. The JavaScript check cannot make the intervening pathname syscall atomic, so a same-privilege peer that can replace the parent may cause an out-of-root side effect before detection. Use native `require` mode for that threat model; see the [security model](security-model.md#symlinks-write-side).
21
26
  4. Write data to a sibling temp file in the same directory.
22
27
  5. Atomically rename the temp file over the destination.
23
28
  6. Stat the resulting fd and verify identity.
24
29
 
25
- A failure at any point either leaves the destination at its previous contents or surfaces an `FsSafeError` — never a partially-written file at the destination path.
30
+ A failure before the final rename leaves the destination at its previous
31
+ contents. A successful rename publishes the complete replacement. This
32
+ old-or-new guarantee does not apply to `append()` or `openWritable()`, which
33
+ write in place, or to lower-level atomic helpers when their explicitly
34
+ non-atomic permission-error copy fallback is enabled.
26
35
 
27
36
  ## Denying mutations
28
37
 
@@ -53,7 +62,7 @@ await fs.write("state/last-run.json", JSON.stringify(run));
53
62
  await fs.write("notes/today.txt", "hello\n", { encoding: "utf8" });
54
63
  ```
55
64
 
56
- `data` accepts `string | Buffer`. `options` are `{ denyMutations?: DenyMutationPolicy; encoding?: BufferEncoding; mkdir?: boolean; mode?: number; overwrite?: boolean }`. `mode` sets the file's POSIX mode; if omitted, falls back to the `mode` from `RootDefaults` and then to umask. `overwrite` defaults to `true`; set it to `false` for the same no-clobber behavior as `create()`.
65
+ `data` accepts `string | Buffer`. `options` are `{ denyMutations?: DenyMutationPolicy; encoding?: BufferEncoding; mkdir?: boolean; mode?: number; overwrite?: boolean; renameIdentity?: RenameIdentityPolicy }`. `mode` sets the file's POSIX mode. If neither the call nor `RootDefaults` supplies it, a replacement preserves the existing file mode and a new file uses `0o600`. `mkdir` and `overwrite` both default to `true`; set `overwrite: false` for the same no-clobber behavior as `create()`.
57
66
 
58
67
  ### `fs.create(rel, data, options?)`
59
68
 
@@ -79,10 +88,7 @@ await fs.writeJson("compact.json", state, { trailingNewline: false });
79
88
  Options:
80
89
 
81
90
  ```ts
82
- type RootWriteJsonOptions = {
83
- encoding?: BufferEncoding;
84
- mkdir?: boolean;
85
- mode?: number;
91
+ type RootWriteJsonOptions = RootWriteOptions & {
86
92
  replacer?: (this: any, key: string, value: any) => any | (number | string)[];
87
93
  space?: number | string;
88
94
  trailingNewline?: boolean; // default true
@@ -112,7 +118,9 @@ await fs.copyIn("inbox/upload.bin", "/tmp/incoming.bin", {
112
118
  });
113
119
  ```
114
120
 
115
- Options: `{ encoding?, mkdir?, maxBytes?, sourceHardlinks? }`. Use `sourceHardlinks: "reject"` to refuse if the source itself is a hardlinked alias.
121
+ Options are `{ denyMutations?, maxBytes?, mkdir?, mode?, sourceHardlinks? }`.
122
+ Use `sourceHardlinks: "reject"` to refuse if the source itself is a hardlinked
123
+ alias. There is no encoding option: copying preserves source bytes.
116
124
 
117
125
  ### `fs.move(from, to, options?)`
118
126
 
@@ -167,7 +175,12 @@ try {
167
175
  }
168
176
  ```
169
177
 
170
- Options: `{ mkdir?, mode?, writeMode? }`, where `writeMode` is `"replace"` (default), `"append"`, or `"update"`. `replace` truncates existing files; `update` keeps existing contents. Streaming writes go directly to the destination — there is no atomic-rename step. If you need both streaming and atomicity, write to a sibling temp yourself and rename when done; the [`atomic`](atomic.md) helpers can do this for you.
178
+ Options are `{ denyMutations?, mkdir?, mode?, writeMode? }`, where `writeMode`
179
+ is `"replace"` (default), `"append"`, or `"update"`. `replace` truncates existing
180
+ files; `update` keeps existing contents. Streaming writes go directly to the
181
+ destination — there is no atomic-rename step. If you need both streaming and
182
+ atomicity, write to a sibling temp yourself and rename when done; the
183
+ [`atomic`](atomic.md) helpers can do this for you.
171
184
 
172
185
  ## Write defaults vs per-call options
173
186
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
5
5
  "keywords": [
6
6
  "filesystem",
@@ -29,6 +29,7 @@
29
29
  "dist/**/*.d.ts.map",
30
30
  "dist/native/**/*.node",
31
31
  "docs/**/*.md",
32
+ "docs/assets/readme-banner.jpg",
32
33
  "README.md",
33
34
  "CHANGELOG.md",
34
35
  "SECURITY.md",
@@ -128,12 +129,16 @@
128
129
  "prepack": "node scripts/prepack-build.mjs",
129
130
  "test": "vitest run",
130
131
  "test:coverage": "vitest run --coverage",
132
+ "test:coverage:collect": "vitest run --coverage --coverage.reporter=json --coverage.thresholds.lines=0 --coverage.thresholds.functions=0 --coverage.thresholds.statements=0 --coverage.thresholds.branches=0",
133
+ "test:coverage:merge": "node scripts/merge-coverage.mjs",
131
134
  "test:security": "vitest run test/fs-safe.test.ts test/read-boundary-bypass.test.ts test/write-boundary-bypass.test.ts test/additional-boundary-bypass.test.ts test/adversarial-boundary-payloads.test.ts",
132
- "check": "pnpm lint:file-size && pnpm lint:fs-boundary && pnpm build && pnpm test && node scripts/check-pack.mjs",
135
+ "check": "pnpm lint:file-size && pnpm lint:fs-boundary && pnpm build && pnpm docs:check && pnpm test && node scripts/check-pack.mjs",
136
+ "docs:check": "node scripts/check-doc-examples.mjs",
133
137
  "docs:site": "node scripts/build-docs-site.mjs",
134
138
  "native:build": "pnpm --filter @openclaw/fs-safe-native-build build",
135
139
  "native:test": "cargo test --manifest-path native/Cargo.toml",
136
140
  "pack:check": "pnpm build && node scripts/check-pack.mjs",
141
+ "public-api:update": "pnpm build && node scripts/check-pack.mjs --update-public-api",
137
142
  "package:smoke": "node scripts/check-release-packages.mjs --allow-host-only --output release-artifacts",
138
143
  "check:changed": "pnpm run check",
139
144
  "release:notes": "node scripts/release-notes.mjs",
@@ -148,11 +153,17 @@
148
153
  "tar": "7.5.22"
149
154
  },
150
155
  "devDependencies": {
151
- "@napi-rs/cli": "3.8.1",
156
+ "@emnapi/runtime": "2.0.0-alpha.3",
157
+ "@napi-rs/cli": "3.8.2",
152
158
  "@types/node": "^26.1.2",
153
159
  "@vitest/coverage-v8": "4.1.10",
160
+ "fast-check": "^4.9.0",
161
+ "istanbul-lib-coverage": "3.2.2",
162
+ "istanbul-lib-report": "3.0.1",
163
+ "istanbul-reports": "3.2.0",
164
+ "sigstore": "5.0.0",
154
165
  "typescript": "^7.0.2",
155
- "vite": "8.2.0",
166
+ "vite": "8.2.1",
156
167
  "vitest": "^4.1.10"
157
168
  },
158
169
  "engines": {
@@ -1,9 +0,0 @@
1
- import type { FileHandle } from "node:fs/promises";
2
- import type { ExtractionDeadline } from "./archive-deadline.js";
3
- export declare function writeFileHandleFully(params: {
4
- handle: FileHandle;
5
- buffer: Buffer;
6
- bytes: number;
7
- deadline: ExtractionDeadline;
8
- }): Promise<void>;
9
- //# sourceMappingURL=archive-file-io.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"archive-file-io.d.ts","sourceRoot":"","sources":["../src/archive-file-io.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,kBAAkB,CAAC;CAC9B,GAAG,OAAO,CAAC,IAAI,CAAC,CAchB"}
@@ -1,11 +0,0 @@
1
- export async function writeFileHandleFully(params) {
2
- let offset = 0;
3
- while (offset < params.bytes) {
4
- params.deadline.check();
5
- const { bytesWritten } = await params.handle.write(params.buffer, offset, params.bytes - offset);
6
- if (bytesWritten <= 0) {
7
- throw new Error("archive staging write made no progress");
8
- }
9
- offset += bytesWritten;
10
- }
11
- }
@@ -1,5 +0,0 @@
1
- export declare function readFileStoreCopySource(params: {
2
- sourcePath: string;
3
- maxBytes?: number;
4
- }): Promise<Buffer>;
5
- //# sourceMappingURL=file-store-source.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"file-store-source.d.ts","sourceRoot":"","sources":["../src/file-store-source.ts"],"names":[],"mappings":"AAIA,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,MAAM,CAAC,CAyBlB"}