@openclaw/fs-safe 0.8.3 → 0.8.5

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 (74) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/dist/absolute-path.d.ts.map +1 -1
  3. package/dist/absolute-path.js +8 -7
  4. package/dist/archive-native.d.ts +1 -0
  5. package/dist/archive-native.d.ts.map +1 -1
  6. package/dist/archive-native.js +15 -68
  7. package/dist/archive-plan.d.ts +20 -0
  8. package/dist/archive-plan.d.ts.map +1 -0
  9. package/dist/archive-plan.js +55 -0
  10. package/dist/archive-tar-inspect.d.ts +7 -0
  11. package/dist/archive-tar-inspect.d.ts.map +1 -0
  12. package/dist/archive-tar-inspect.js +59 -0
  13. package/dist/archive-tar.d.ts +3 -1
  14. package/dist/archive-tar.d.ts.map +1 -1
  15. package/dist/archive-tar.js +12 -49
  16. package/dist/archive.d.ts +1 -0
  17. package/dist/archive.d.ts.map +1 -1
  18. package/dist/archive.js +20 -36
  19. package/dist/bounded-read.d.ts.map +1 -1
  20. package/dist/bounded-read.js +31 -4
  21. package/dist/directory-durability.js +5 -5
  22. package/dist/directory-guard.d.ts.map +1 -1
  23. package/dist/directory-guard.js +5 -6
  24. package/dist/file-store-boundary.js +1 -1
  25. package/dist/file-store-prune.js +17 -8
  26. package/dist/file-store.js +2 -2
  27. package/dist/guarded-mkdir.d.ts.map +1 -1
  28. package/dist/guarded-mkdir.js +5 -4
  29. package/dist/move-path.js +1 -1
  30. package/dist/native-binding.d.ts +2 -1
  31. package/dist/native-binding.d.ts.map +1 -1
  32. package/dist/native-pinned-write-windows.js +6 -3
  33. package/dist/native-pinned-write.js +3 -3
  34. package/dist/native-staged-file.d.ts +2 -2
  35. package/dist/native-staged-file.d.ts.map +1 -1
  36. package/dist/native-staged-file.js +12 -8
  37. package/dist/opened-realpath.d.ts.map +1 -1
  38. package/dist/opened-realpath.js +10 -9
  39. package/dist/path-policy.js +2 -2
  40. package/dist/pinned-write.d.ts +1 -0
  41. package/dist/pinned-write.d.ts.map +1 -1
  42. package/dist/pinned-write.js +15 -11
  43. package/dist/regular-file.js +8 -8
  44. package/dist/replace-file-descriptor.d.ts.map +1 -1
  45. package/dist/replace-file-descriptor.js +8 -4
  46. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  47. package/dist/replace-file-temp-owner.js +13 -7
  48. package/dist/root-context.js +5 -5
  49. package/dist/root-impl.d.ts +2 -1
  50. package/dist/root-impl.d.ts.map +1 -1
  51. package/dist/root-impl.js +35 -26
  52. package/dist/root-path-existing.d.ts.map +1 -1
  53. package/dist/root-path-existing.js +2 -3
  54. package/dist/root-path-symlink.d.ts.map +1 -1
  55. package/dist/root-path-symlink.js +2 -3
  56. package/dist/root-path.d.ts.map +1 -1
  57. package/dist/root-path.js +3 -4
  58. package/dist/root-paths.d.ts.map +1 -1
  59. package/dist/root-paths.js +16 -15
  60. package/dist/root-write-mode.d.ts.map +1 -1
  61. package/dist/root-write-mode.js +5 -4
  62. package/dist/root-write-verification.js +6 -6
  63. package/dist/secret-file.js +2 -2
  64. package/dist/strict-file-identity.d.ts +1 -1
  65. package/dist/strict-file-identity.d.ts.map +1 -1
  66. package/dist/symlink-parents.d.ts.map +1 -1
  67. package/dist/symlink-parents.js +1 -2
  68. package/docs/archive.md +57 -0
  69. package/docs/atomic.md +2 -0
  70. package/docs/reading.md +2 -0
  71. package/docs/root.md +10 -1
  72. package/docs/types.md +2 -1
  73. package/docs/writing.md +26 -3
  74. package/package.json +8 -8
package/docs/archive.md CHANGED
@@ -422,6 +422,63 @@ Normal link/filter policy still governs the described member. Canonical
422
422
  pre-strip filter paths, decoded-stream ceilings, and physical EOF checks apply
423
423
  to plain/gzip TAR and native zstd/bzip2 alike.
424
424
 
425
+ ## `inspectTarArchive`
426
+
427
+ Inspect accepted TAR members without creating an extracted tree. This operation
428
+ uses the same complete Rust/WASM admission and TypeScript extraction planner as
429
+ `extractArchive`, with zero stripping. It detects plain TAR or gzip from the
430
+ input bytes; ZIP, zstd, and bzip2 are not part of this inspection API.
431
+
432
+ ```ts
433
+ import { inspectTarArchive } from "@openclaw/fs-safe/archive";
434
+
435
+ const entries = await inspectTarArchive({
436
+ archivePath: "/srv/uploads/tree.tar.gz",
437
+ timeoutMs: 30_000,
438
+ limits: {
439
+ maxArchiveBytes: 16 * 1024 * 1024,
440
+ maxEntries: 5_000,
441
+ maxEntryBytes: 16 * 1024 * 1024,
442
+ maxExtractedBytes: 64 * 1024 * 1024,
443
+ },
444
+ entryFilter: ({ kind }) => kind === "file" || kind === "directory" ? "extract" : "skip",
445
+ onFiltered: "reject-archive",
446
+ });
447
+ ```
448
+
449
+ `InspectTarArchiveOptions` accepts `archivePath`, `timeoutMs`, `limits`,
450
+ `entryFilter`, and `onFiltered`, with the same defaults and error classes as
451
+ extraction. The result is a frozen array of frozen `InspectedTarEntry` records:
452
+ `{ path: string; kind: "file" | "directory"; size: number }`, in archive order.
453
+ `size` is the effective declared payload length. `path` is extraction's
454
+ canonical pre-strip identity: case, Unicode spelling, BOM, and embedded LF are
455
+ preserved, while separators and dot components follow the existing archive path
456
+ contract. No human-readable tar listing or escape decoding is involved.
457
+
458
+ Full framing, gzip integrity, EOF, metadata, decoded-byte, and manifest-budget
459
+ validation finishes before the caller's filter runs. The shared planner then
460
+ applies traversal, collision, depth, blocked-type, and accepted-payload limits.
461
+ A failure returns no partial result. Filter callbacks are decisions, not admission
462
+ receipts: later policy, collision, or budget checks can still reject the archive.
463
+ Only the resolved Promise/result is authorization-worthy; do not perform
464
+ irreversible actions from a filter callback.
465
+
466
+ Root-only records count toward entry limits but produce no result; PAX/GNU metadata headers are not members. Only accepted
467
+ file/directory members appear, not implicit parent directories. Unsupported
468
+ records follow extraction's omission policy unless the filter rejects them, as
469
+ in the example. AppleDouble records encoded as ordinary files are ordinary
470
+ members, not hidden metadata.
471
+
472
+ Inspection pins and privately stages its input, then cleans up that copy. It
473
+ neither creates destination paths nor tests destination permissions or platform
474
+ filename restrictions. Its result is evidence about those inspected bytes, not
475
+ an extraction capability or a promise that a later file at `archivePath` is
476
+ unchanged. Callers making authorization decisions must retain the same private
477
+ immutable archive or verify byte identity before extracting with matching
478
+ filter/limit settings. Extraction always performs its own admission and guarded
479
+ publication. Native `off`, `auto`, and `require` retain their existing selection
480
+ and availability semantics; inspection does not fall back after native failure.
481
+
425
482
  ## `resolveArchiveKind`
426
483
 
427
484
  ```ts
package/docs/atomic.md CHANGED
@@ -200,6 +200,8 @@ await movePathWithCopyFallback({
200
200
  ```
201
201
 
202
202
  Use it when source and destination might live on different filesystems (containers, tmpfs, separate volumes).
203
+ The hardlink policy is captured when the move starts. Changing or reusing the
204
+ options object later does not change the policy of an in-flight move.
203
205
  `sourceHardlinks: "reject"` performs a recursive preflight capped at 50,000
204
206
  entries before any mutation. Because link count and rename cannot be one atomic
205
207
  portable operation, this mode always commits a fresh inode/tree through the
package/docs/reading.md CHANGED
@@ -12,6 +12,8 @@ const opened = await fs.open("large.log"); // FileHandle for strea
12
12
 
13
13
  ## What every read does
14
14
 
15
+ Identity and containment checks use brief synchronous metadata calls, like Node's module resolution, while file data is read asynchronously. Canonical paths retain Node's native realpath spelling, including expansion of Windows short paths.
16
+
15
17
  Regardless of shape, every read goes through the same boundary checks:
16
18
 
17
19
  1. Resolve the input lexically against the canonical real root.
package/docs/root.md CHANGED
@@ -18,6 +18,7 @@ const fs = await root("/srv/workspace", {
18
18
  function root(rootDir: string, defaults?: RootDefaults): Promise<Root>;
19
19
 
20
20
  type RootDefaults = {
21
+ durable?: boolean; // fsync write/create/writeJson/createJson/append; default true
21
22
  hardlinks?: "reject" | "allow"; // refuse files with nlink > 1 on read; defaults to "reject"
22
23
  denyMutations?: DenyMutationPolicy; // absolute paths/prefixes mutation methods may not change
23
24
  maxBytes?: number; // refuse reads larger than this many bytes; defaults to 16 MiB
@@ -99,7 +100,7 @@ fs.write(rel, data, options?) // overwrite-ok atomic write
99
100
  fs.create(rel, data, options?) // throws "already-exists" if target exists
100
101
  fs.writeJson(rel, value, options?) // JSON.stringify + atomic write
101
102
  fs.createJson(rel, value, options?) // create() variant of writeJson
102
- fs.append(rel, data, options?) // append text/buffer; syncs before close
103
+ fs.append(rel, data, options?) // append text/buffer; syncs before close by default
103
104
  fs.copyIn(rel, sourceAbsPath, options?) // copy from outside the root, atomically, with size cap
104
105
  fs.openWritable(rel, options?) // FileHandle for streaming writes; supports await using
105
106
  fs.move(from, to, options?) // rename within the root; defaults to no clobber
@@ -110,6 +111,14 @@ fs.ensureRoot(options?) // accepts "" / "." as the root itself
110
111
 
111
112
  `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`).
112
113
 
114
+ These five methods also accept `durable?: boolean`: the per-call value overrides
115
+ `Root.defaults.durable`, which defaults to `true` when omitted. An explicitly
116
+ `undefined` per-call value preserves the root default. `durable: false` keeps
117
+ the existing publication behavior, modes, and identity checks but skips file
118
+ and parent-directory fsync calls. Use it only for reconstructible data: a crash
119
+ may lose the write or leave the previous file. See [Writing](writing.md#write-options)
120
+ for platform details.
121
+
113
122
  `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`.
114
123
 
115
124
  Root operations that choose a new destination reject a leading Windows
package/docs/types.md CHANGED
@@ -100,6 +100,7 @@ type RenameIdentityPolicy = "strict" | "verify-content-with-lock";
100
100
 
101
101
  type RootDefaults = {
102
102
  denyMutations?: DenyMutationPolicy;
103
+ durable?: boolean; // default true for write/create/writeJson/createJson/append
103
104
  hardlinks?: "reject" | "allow";
104
105
  maxBytes?: number;
105
106
  mkdir?: boolean; // default true for mutation methods
@@ -126,7 +127,7 @@ type RootOptions = {
126
127
 
127
128
  ```ts
128
129
  type RootReadOptions = Pick<RootDefaults, "hardlinks" | "maxBytes" | "nonBlockingRead" | "symlinks">;
129
- type RootWriteOptions = Pick<RootDefaults, "denyMutations" | "mkdir" | "mode" | "renameIdentity"> & {
130
+ type RootWriteOptions = Pick<RootDefaults, "denyMutations" | "durable" | "mkdir" | "mode" | "renameIdentity"> & {
130
131
  encoding?: BufferEncoding;
131
132
  overwrite?: boolean;
132
133
  };
package/docs/writing.md CHANGED
@@ -77,14 +77,37 @@ await fs.write("state/last-run.json", JSON.stringify(run));
77
77
  await fs.write("notes/today.txt", "hello\n", { encoding: "utf8" });
78
78
  ```
79
79
 
80
- `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()`.
80
+ `data` accepts `string | Buffer`. `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()`.
81
+
82
+ #### Write options
83
+
84
+ | Option | Type | Default / behavior |
85
+ |---|---|---|
86
+ | `denyMutations` | `DenyMutationPolicy` | Merged with root-level denies. |
87
+ | `durable` | `boolean` | `true`; use `false` to skip file and parent fsync. |
88
+ | `encoding` | `BufferEncoding` | `"utf8"` for strings. |
89
+ | `mkdir` | `boolean` | `true`; creates missing parents. |
90
+ | `mode` | `number` | Inherited on replacement, otherwise `0o600`. |
91
+ | `overwrite` | `boolean` | `true`; `false` is create-only. |
92
+ | `renameIdentity` | `RenameIdentityPolicy` | `"strict"`. |
93
+
94
+ `write`, `create`, `writeJson`, `createJson`, and `append` accept `durable`.
95
+ Precedence is per-call option, then `Root.defaults.durable`, then `true`;
96
+ an explicitly `undefined` call option preserves the root default.
97
+ `durable: false` keeps the sibling-temp replace/rename behavior of replacement
98
+ writes but skips file and parent-directory fsync calls. Create-only and append
99
+ publication behavior, permissions, identity checks, and error codes are unchanged.
100
+ Use it only for reconstructible data: a crash may lose the write or leave the
101
+ previous file. `copyIn`, `move`, and streaming `openWritable` do not use this option.
102
+ The existing pure-JavaScript Windows writer performs no fsync calls in either
103
+ setting; native Windows writes honor the option. Directory sync remains best-effort.
81
104
 
82
105
  POSIX modes without read permission, including `0o000` and `0o200`, succeed:
83
106
  final verification uses a descriptor retained by the writer rather than reopening
84
107
  the published file. The requested mode is not relaxed for verification.
85
108
  Publication verification compares exact bigint descriptor and pathname identities,
86
109
  including large file indexes that cannot be represented by a JavaScript number.
87
- Native publication syncs content before rename and always syncs the parent directory.
110
+ With durability enabled (the default), native publication syncs content before rename and syncs the parent directory.
88
111
  Modes that retain owner read/write skip the extra mode-only file sync: after a
89
112
  crash, the file may retain staged mode `0o600` instead of the wider requested mode.
90
113
  Modes that remove owner read or write, and corrections of observed wider staging
@@ -143,7 +166,7 @@ type RootWriteJsonOptions = RootWriteOptions & {
143
166
 
144
167
  ### `fs.append(rel, data, options?)`
145
168
 
146
- Open in append mode, write, sync the file handle, and close. Honors `mkdir` for the parent directory and syncs the parent directory when the append creates the file. Pass `prependNewlineIfNeeded: true` to insert a `\n` if the file does not already end in one.
169
+ Open in append mode, write, sync the file handle, and close. Honors `mkdir` for the parent directory and syncs the parent directory when the append creates the file. `durable: false` skips both syncs. Pass `prependNewlineIfNeeded: true` to insert a `\n` if the file does not already end in one.
147
170
 
148
171
  ```ts
149
172
  await fs.append("logs/today.log", `[${ts}] ${line}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
5
5
  "keywords": [
6
6
  "filesystem",
@@ -156,13 +156,13 @@
156
156
  "archive:producer-smoke": "node scripts/archive-producer-smoke.mjs"
157
157
  },
158
158
  "optionalDependencies": {
159
- "@openclaw/fs-safe-darwin-arm64": "0.8.3",
160
- "@openclaw/fs-safe-darwin-x64": "0.8.3",
161
- "@openclaw/fs-safe-linux-arm64-gnu": "0.8.3",
162
- "@openclaw/fs-safe-linux-arm64-musl": "0.8.3",
163
- "@openclaw/fs-safe-linux-x64-gnu": "0.8.3",
164
- "@openclaw/fs-safe-linux-x64-musl": "0.8.3",
165
- "@openclaw/fs-safe-win32-x64-msvc": "0.8.3",
159
+ "@openclaw/fs-safe-darwin-arm64": "0.8.5",
160
+ "@openclaw/fs-safe-darwin-x64": "0.8.5",
161
+ "@openclaw/fs-safe-linux-arm64-gnu": "0.8.5",
162
+ "@openclaw/fs-safe-linux-arm64-musl": "0.8.5",
163
+ "@openclaw/fs-safe-linux-x64-gnu": "0.8.5",
164
+ "@openclaw/fs-safe-linux-x64-musl": "0.8.5",
165
+ "@openclaw/fs-safe-win32-x64-msvc": "0.8.5",
166
166
  "jszip": "^3.10.1"
167
167
  },
168
168
  "devDependencies": {