@openclaw/fs-safe 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +18 -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 +25 -5
  8. package/dist/archive-input.d.ts +7 -0
  9. package/dist/archive-input.d.ts.map +1 -1
  10. package/dist/archive-input.js +11 -1
  11. package/dist/archive-limits.js +1 -1
  12. package/dist/archive-native.d.ts.map +1 -1
  13. package/dist/archive-native.js +6 -3
  14. package/dist/archive-policy.d.ts.map +1 -1
  15. package/dist/archive-policy.js +2 -1
  16. package/dist/archive-read.d.ts.map +1 -1
  17. package/dist/archive-read.js +30 -12
  18. package/dist/archive-staging.d.ts.map +1 -1
  19. package/dist/archive-staging.js +3 -2
  20. package/dist/archive-tar-meta.d.ts.map +1 -1
  21. package/dist/archive-tar-meta.js +7 -1
  22. package/dist/archive-tar.d.ts.map +1 -1
  23. package/dist/archive-tar.js +7 -4
  24. package/dist/archive-zip-entry.d.ts +1 -0
  25. package/dist/archive-zip-entry.d.ts.map +1 -1
  26. package/dist/archive-zip-integrity.d.ts +4 -0
  27. package/dist/archive-zip-integrity.d.ts.map +1 -0
  28. package/dist/archive-zip-integrity.js +44 -0
  29. package/dist/archive-zip-preflight.d.ts.map +1 -1
  30. package/dist/archive-zip-preflight.js +6 -1
  31. package/dist/archive.d.ts.map +1 -1
  32. package/dist/archive.js +5 -2
  33. package/dist/bounded-read-stream.d.ts.map +1 -1
  34. package/dist/bounded-read-stream.js +13 -2
  35. package/dist/deny-mutations.d.ts +1 -0
  36. package/dist/deny-mutations.d.ts.map +1 -1
  37. package/dist/deny-mutations.js +6 -39
  38. package/dist/device-path.d.ts +1 -0
  39. package/dist/device-path.d.ts.map +1 -1
  40. package/dist/device-path.js +6 -6
  41. package/dist/directory-guard.d.ts.map +1 -1
  42. package/dist/directory-guard.js +5 -4
  43. package/dist/error-detail.d.ts +2 -0
  44. package/dist/error-detail.d.ts.map +1 -0
  45. package/dist/error-detail.js +4 -0
  46. package/dist/errors.d.ts.map +1 -1
  47. package/dist/errors.js +3 -0
  48. package/dist/file-hash.d.ts.map +1 -1
  49. package/dist/file-hash.js +4 -1
  50. package/dist/file-lock-sync.d.ts.map +1 -1
  51. package/dist/file-lock-sync.js +88 -35
  52. package/dist/file-store-boundary.d.ts +6 -0
  53. package/dist/file-store-boundary.d.ts.map +1 -1
  54. package/dist/file-store-boundary.js +20 -5
  55. package/dist/file-store-sync-write.d.ts +9 -0
  56. package/dist/file-store-sync-write.d.ts.map +1 -0
  57. package/dist/file-store-sync-write.js +97 -0
  58. package/dist/file-store.d.ts.map +1 -1
  59. package/dist/file-store.js +36 -128
  60. package/dist/filename.d.ts.map +1 -1
  61. package/dist/filename.js +27 -0
  62. package/dist/guarded-mkdir.d.ts.map +1 -1
  63. package/dist/guarded-mkdir.js +5 -6
  64. package/dist/install-path.js +1 -1
  65. package/dist/json-document-store.d.ts.map +1 -1
  66. package/dist/json-document-store.js +19 -13
  67. package/dist/json.d.ts.map +1 -1
  68. package/dist/json.js +1 -3
  69. package/dist/local-file-access.d.ts +3 -2
  70. package/dist/local-file-access.d.ts.map +1 -1
  71. package/dist/local-file-access.js +12 -6
  72. package/dist/local-roots.d.ts.map +1 -1
  73. package/dist/local-roots.js +35 -30
  74. package/dist/move-path.d.ts.map +1 -1
  75. package/dist/move-path.js +51 -12
  76. package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
  77. package/dist/native/darwin-x64/fs-safe-native.node +0 -0
  78. package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
  79. package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
  80. package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
  81. package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
  82. package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
  83. package/dist/native-binding.d.ts +1 -0
  84. package/dist/native-binding.d.ts.map +1 -1
  85. package/dist/native-operations.d.ts.map +1 -1
  86. package/dist/native-operations.js +14 -1
  87. package/dist/native-pinned-write.d.ts.map +1 -1
  88. package/dist/native-pinned-write.js +44 -17
  89. package/dist/output-sibling.d.ts.map +1 -1
  90. package/dist/output-sibling.js +6 -5
  91. package/dist/path-policy.d.ts.map +1 -1
  92. package/dist/path-policy.js +1 -7
  93. package/dist/path.d.ts.map +1 -1
  94. package/dist/path.js +4 -0
  95. package/dist/permission-exec.d.ts +6 -0
  96. package/dist/permission-exec.d.ts.map +1 -0
  97. package/dist/permission-exec.js +28 -0
  98. package/dist/permissions-windows.d.ts +56 -0
  99. package/dist/permissions-windows.d.ts.map +1 -0
  100. package/dist/permissions-windows.js +285 -0
  101. package/dist/permissions.d.ts +3 -46
  102. package/dist/permissions.d.ts.map +1 -1
  103. package/dist/permissions.js +17 -328
  104. package/dist/pinned-write.js +2 -1
  105. package/dist/regular-file.d.ts.map +1 -1
  106. package/dist/regular-file.js +21 -2
  107. package/dist/replace-directory.d.ts.map +1 -1
  108. package/dist/replace-directory.js +10 -1
  109. package/dist/replace-file-copy-fallback.d.ts +3 -0
  110. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  111. package/dist/replace-file-copy-fallback.js +18 -11
  112. package/dist/replace-file-descriptor.d.ts +33 -0
  113. package/dist/replace-file-descriptor.d.ts.map +1 -0
  114. package/dist/replace-file-descriptor.js +96 -0
  115. package/dist/replace-file.d.ts +9 -2
  116. package/dist/replace-file.d.ts.map +1 -1
  117. package/dist/replace-file.js +64 -72
  118. package/dist/root-context.d.ts +8 -0
  119. package/dist/root-context.d.ts.map +1 -1
  120. package/dist/root-context.js +43 -2
  121. package/dist/root-errors.d.ts +7 -0
  122. package/dist/root-errors.d.ts.map +1 -1
  123. package/dist/root-errors.js +46 -1
  124. package/dist/root-file.d.ts +1 -0
  125. package/dist/root-file.d.ts.map +1 -1
  126. package/dist/root-file.js +2 -0
  127. package/dist/root-impl.d.ts.map +1 -1
  128. package/dist/root-impl.js +213 -168
  129. package/dist/root-path-symlink.d.ts +3 -0
  130. package/dist/root-path-symlink.d.ts.map +1 -0
  131. package/dist/root-path-symlink.js +35 -0
  132. package/dist/root-path.d.ts +1 -0
  133. package/dist/root-path.d.ts.map +1 -1
  134. package/dist/root-path.js +129 -190
  135. package/dist/root-walk.d.ts.map +1 -1
  136. package/dist/root-walk.js +15 -0
  137. package/dist/safe-path-segment.d.ts +2 -0
  138. package/dist/safe-path-segment.d.ts.map +1 -1
  139. package/dist/safe-path-segment.js +12 -0
  140. package/dist/secret-file.d.ts.map +1 -1
  141. package/dist/secret-file.js +19 -3
  142. package/dist/secure-temp-dir.d.ts.map +1 -1
  143. package/dist/secure-temp-dir.js +6 -1
  144. package/dist/short-path.d.ts +2 -0
  145. package/dist/short-path.d.ts.map +1 -0
  146. package/dist/short-path.js +7 -0
  147. package/dist/sidecar-lock-acquire.d.ts +30 -0
  148. package/dist/sidecar-lock-acquire.d.ts.map +1 -0
  149. package/dist/sidecar-lock-acquire.js +260 -0
  150. package/dist/sidecar-lock-policy.d.ts +3 -0
  151. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  152. package/dist/sidecar-lock-policy.js +20 -3
  153. package/dist/sidecar-lock-reclaim.d.ts +5 -1
  154. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  155. package/dist/sidecar-lock-reclaim.js +59 -9
  156. package/dist/sidecar-lock.d.ts.map +1 -1
  157. package/dist/sidecar-lock.js +27 -226
  158. package/dist/temp-target.d.ts.map +1 -1
  159. package/dist/temp-target.js +15 -4
  160. package/dist/test-hooks.d.ts +1 -0
  161. package/dist/test-hooks.d.ts.map +1 -1
  162. package/dist/timing.d.ts +2 -0
  163. package/dist/timing.d.ts.map +1 -1
  164. package/dist/timing.js +9 -0
  165. package/dist/walk.d.ts.map +1 -1
  166. package/dist/walk.js +15 -0
  167. package/docs/advanced.md +1 -1
  168. package/docs/archive.md +27 -20
  169. package/docs/assets/readme-banner.jpg +0 -0
  170. package/docs/atomic.md +48 -19
  171. package/docs/errors.md +31 -7
  172. package/docs/file-store.md +10 -1
  173. package/docs/filename.md +17 -16
  174. package/docs/index.md +1 -0
  175. package/docs/install-path.md +9 -6
  176. package/docs/json.md +8 -2
  177. package/docs/local-roots.md +79 -103
  178. package/docs/native-helper.md +11 -5
  179. package/docs/native.md +2 -2
  180. package/docs/output.md +8 -3
  181. package/docs/path-scope.md +4 -1
  182. package/docs/path.md +7 -3
  183. package/docs/permissions.md +8 -0
  184. package/docs/public-api.md +109 -0
  185. package/docs/quickstart.md +6 -2
  186. package/docs/reading.md +13 -10
  187. package/docs/root.md +26 -4
  188. package/docs/secret-file.md +17 -4
  189. package/docs/security-model.md +21 -4
  190. package/docs/sidecar-lock.md +15 -5
  191. package/docs/temp.md +37 -5
  192. package/docs/test-hooks.md +2 -0
  193. package/docs/types.md +32 -15
  194. package/docs/writing.md +25 -12
  195. package/package.json +7 -3
  196. package/dist/archive-file-io.d.ts +0 -9
  197. package/dist/archive-file-io.d.ts.map +0 -1
  198. package/dist/archive-file-io.js +0 -11
  199. package/dist/file-store-source.d.ts +0 -5
  200. package/dist/file-store-source.d.ts.map +0 -1
  201. package/dist/file-store-source.js +0 -30
  202. package/dist/file-url.d.ts +0 -2
  203. package/dist/file-url.d.ts.map +0 -1
  204. package/dist/file-url.js +0 -1
  205. package/dist/path-stat.d.ts +0 -4
  206. package/dist/path-stat.d.ts.map +0 -1
  207. package/dist/path-stat.js +0 -15
  208. package/dist/windows-permissions-native.d.ts +0 -9
  209. package/dist/windows-permissions-native.d.ts.map +0 -1
  210. package/dist/windows-permissions-native.js +0 -31
@@ -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" | "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`, and `not-removable` 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.2",
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",
@@ -129,11 +130,13 @@
129
130
  "test": "vitest run",
130
131
  "test:coverage": "vitest run --coverage",
131
132
  "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",
133
+ "check": "pnpm lint:file-size && pnpm lint:fs-boundary && pnpm build && pnpm docs:check && pnpm test && node scripts/check-pack.mjs",
134
+ "docs:check": "node scripts/check-doc-examples.mjs",
133
135
  "docs:site": "node scripts/build-docs-site.mjs",
134
136
  "native:build": "pnpm --filter @openclaw/fs-safe-native-build build",
135
137
  "native:test": "cargo test --manifest-path native/Cargo.toml",
136
138
  "pack:check": "pnpm build && node scripts/check-pack.mjs",
139
+ "public-api:update": "pnpm build && node scripts/check-pack.mjs --update-public-api",
137
140
  "package:smoke": "node scripts/check-release-packages.mjs --allow-host-only --output release-artifacts",
138
141
  "check:changed": "pnpm run check",
139
142
  "release:notes": "node scripts/release-notes.mjs",
@@ -148,9 +151,10 @@
148
151
  "tar": "7.5.22"
149
152
  },
150
153
  "devDependencies": {
151
- "@napi-rs/cli": "3.8.1",
154
+ "@napi-rs/cli": "3.8.2",
152
155
  "@types/node": "^26.1.2",
153
156
  "@vitest/coverage-v8": "4.1.10",
157
+ "sigstore": "5.0.0",
154
158
  "typescript": "^7.0.2",
155
159
  "vite": "8.2.0",
156
160
  "vitest": "^4.1.10"
@@ -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"}
@@ -1,30 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import { FsSafeError } from "./errors.js";
3
- import { readRegularFile } from "./regular-file.js";
4
- export async function readFileStoreCopySource(params) {
5
- const sourceStat = await fs.lstat(params.sourcePath);
6
- if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
7
- throw new FsSafeError("not-file", "source path is not a file");
8
- }
9
- if (params.maxBytes !== undefined && sourceStat.size > params.maxBytes) {
10
- throw new FsSafeError("too-large", `file exceeds maximum size of ${params.maxBytes} bytes`);
11
- }
12
- try {
13
- return (await readRegularFile({ filePath: params.sourcePath, maxBytes: params.maxBytes }))
14
- .buffer;
15
- }
16
- catch (error) {
17
- const message = error instanceof Error ? error.message : String(error);
18
- if (message.includes("regular file") || message.includes("not a regular file")) {
19
- throw new FsSafeError("not-file", "source path is not a file", {
20
- cause: error instanceof Error ? error : undefined,
21
- });
22
- }
23
- if (params.maxBytes !== undefined && message.includes(`exceeds ${params.maxBytes} bytes`)) {
24
- throw new FsSafeError("too-large", `file exceeds maximum size of ${params.maxBytes} bytes`, {
25
- cause: error instanceof Error ? error : undefined,
26
- });
27
- }
28
- throw error;
29
- }
30
- }
@@ -1,2 +0,0 @@
1
- export * from "./local-file-access.js";
2
- //# sourceMappingURL=file-url.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"file-url.d.ts","sourceRoot":"","sources":["../src/file-url.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC"}
package/dist/file-url.js DELETED
@@ -1 +0,0 @@
1
- export * from "./local-file-access.js";
@@ -1,4 +0,0 @@
1
- import type { Stats } from "node:fs";
2
- import type { PathStat } from "./types.js";
3
- export declare function pathStatFromStats(stat: Stats): PathStat;
4
- //# sourceMappingURL=path-stat.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"path-stat.d.ts","sourceRoot":"","sources":["../src/path-stat.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,KAAK,GAAG,QAAQ,CAcvD"}
package/dist/path-stat.js DELETED
@@ -1,15 +0,0 @@
1
- export function pathStatFromStats(stat) {
2
- return {
3
- dev: Number(stat.dev),
4
- gid: Number(stat.gid),
5
- ino: Number(stat.ino),
6
- isDirectory: stat.isDirectory(),
7
- isFile: stat.isFile(),
8
- isSymbolicLink: stat.isSymbolicLink(),
9
- mode: stat.mode,
10
- mtimeMs: stat.mtimeMs,
11
- nlink: stat.nlink,
12
- size: stat.size,
13
- uid: stat.uid,
14
- };
15
- }
@@ -1,9 +0,0 @@
1
- import type { PermissionCheck, SafeStatResult } from "./permissions.js";
2
- export declare function inspectWindowsPermissionsNative(params: {
3
- targetPath: string;
4
- stat: SafeStatResult;
5
- effectiveIsDir: boolean;
6
- effectiveMode: number | null;
7
- bits: number | null;
8
- }): PermissionCheck | undefined;
9
- //# sourceMappingURL=windows-permissions-native.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"windows-permissions-native.d.ts","sourceRoot":"","sources":["../src/windows-permissions-native.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAExE,wBAAgB,+BAA+B,CAAC,MAAM,EAAE;IACtD,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,cAAc,CAAC;IACrB,cAAc,EAAE,OAAO,CAAC;IACxB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,GAAG,eAAe,GAAG,SAAS,CA2B9B"}
@@ -1,31 +0,0 @@
1
- import { getNativeBinding } from "./native.js";
2
- export function inspectWindowsPermissionsNative(params) {
3
- const native = getNativeBinding();
4
- if (!native)
5
- return undefined;
6
- try {
7
- const facts = native.readOwnerAndDacl(params.targetPath);
8
- if (facts.fallbackRequired)
9
- return undefined;
10
- return {
11
- ok: true,
12
- isSymlink: params.stat.isSymlink,
13
- isDir: params.effectiveIsDir,
14
- mode: params.effectiveMode,
15
- bits: params.bits,
16
- source: "windows-acl",
17
- worldWritable: facts.worldWritable,
18
- groupWritable: facts.groupWritable,
19
- worldReadable: facts.worldReadable,
20
- groupReadable: facts.groupReadable,
21
- ownerSid: facts.ownerSid,
22
- ownerTrusted: facts.ownerClass !== "foreign",
23
- aclSummary: `native owner=${facts.ownerClass} world=` +
24
- `${facts.worldReadable ? "r" : "-"}${facts.worldWritable ? "w" : "-"} ` +
25
- `group=${facts.groupReadable ? "r" : "-"}${facts.groupWritable ? "w" : "-"}`,
26
- };
27
- }
28
- catch {
29
- return undefined;
30
- }
31
- }