@openclaw/fs-safe 0.8.1 → 0.8.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 (195) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +2 -2
  3. package/dist/advanced.d.ts +2 -2
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +2 -2
  6. package/dist/archive-entry.d.ts.map +1 -1
  7. package/dist/archive-entry.js +5 -0
  8. package/dist/archive-gzip-tail.d.ts +18 -0
  9. package/dist/archive-gzip-tail.d.ts.map +1 -0
  10. package/dist/archive-gzip-tail.js +74 -0
  11. package/dist/archive-kind.d.ts +1 -0
  12. package/dist/archive-kind.d.ts.map +1 -1
  13. package/dist/archive-kind.js +6 -0
  14. package/dist/archive-limits.d.ts +0 -1
  15. package/dist/archive-limits.d.ts.map +1 -1
  16. package/dist/archive-limits.js +0 -3
  17. package/dist/archive-native.d.ts.map +1 -1
  18. package/dist/archive-native.js +5 -8
  19. package/dist/archive-parser-errors.d.ts +2 -0
  20. package/dist/archive-parser-errors.d.ts.map +1 -0
  21. package/dist/archive-parser-errors.js +12 -0
  22. package/dist/archive-parser.wasm +0 -0
  23. package/dist/archive-read.d.ts.map +1 -1
  24. package/dist/archive-read.js +25 -73
  25. package/dist/archive-tar-stream.d.ts +18 -0
  26. package/dist/archive-tar-stream.d.ts.map +1 -0
  27. package/dist/archive-tar-stream.js +95 -0
  28. package/dist/archive-tar-wasm.d.ts +18 -0
  29. package/dist/archive-tar-wasm.d.ts.map +1 -0
  30. package/dist/archive-tar-wasm.js +99 -0
  31. package/dist/archive-tar.d.ts +0 -1
  32. package/dist/archive-tar.d.ts.map +1 -1
  33. package/dist/archive-tar.js +0 -22
  34. package/dist/archive.d.ts.map +1 -1
  35. package/dist/archive.js +44 -114
  36. package/dist/atomic.d.ts +1 -1
  37. package/dist/atomic.d.ts.map +1 -1
  38. package/dist/atomic.js +1 -1
  39. package/dist/bounded-read-stream.d.ts +1 -5
  40. package/dist/bounded-read-stream.d.ts.map +1 -1
  41. package/dist/bounded-read-stream.js +5 -13
  42. package/dist/byte-budget.d.ts +0 -1
  43. package/dist/byte-budget.d.ts.map +1 -1
  44. package/dist/byte-budget.js +1 -1
  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 +5 -2
  48. package/dist/directory-mode-owner.d.ts.map +1 -1
  49. package/dist/directory-mode-owner.js +5 -0
  50. package/dist/error-detail.d.ts +1 -0
  51. package/dist/error-detail.d.ts.map +1 -1
  52. package/dist/error-detail.js +6 -0
  53. package/dist/file-store-boundary.d.ts +1 -1
  54. package/dist/file-store-boundary.d.ts.map +1 -1
  55. package/dist/file-store-boundary.js +2 -13
  56. package/dist/file-store.js +1 -1
  57. package/dist/file-sync.d.ts +5 -0
  58. package/dist/file-sync.d.ts.map +1 -0
  59. package/dist/file-sync.js +19 -0
  60. package/dist/guarded-mutation.d.ts +3 -0
  61. package/dist/guarded-mutation.d.ts.map +1 -1
  62. package/dist/guarded-mutation.js +5 -1
  63. package/dist/home-dir.d.ts +0 -10
  64. package/dist/home-dir.d.ts.map +1 -1
  65. package/dist/home-dir.js +0 -28
  66. package/dist/json-durable-queue-ownership.d.ts +1 -0
  67. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  68. package/dist/json-durable-queue-ownership.js +139 -18
  69. package/dist/json-durable-queue.d.ts.map +1 -1
  70. package/dist/json-durable-queue.js +5 -10
  71. package/dist/json-store.d.ts.map +1 -1
  72. package/dist/json-store.js +1 -1
  73. package/dist/local-roots.d.ts.map +1 -1
  74. package/dist/local-roots.js +2 -3
  75. package/dist/move-path-cleanup.d.ts +1 -1
  76. package/dist/move-path-cleanup.d.ts.map +1 -1
  77. package/dist/move-path-cleanup.js +4 -2
  78. package/dist/move-path.d.ts +9 -0
  79. package/dist/move-path.d.ts.map +1 -1
  80. package/dist/move-path.js +53 -16
  81. package/dist/native-operations.d.ts +0 -1
  82. package/dist/native-operations.d.ts.map +1 -1
  83. package/dist/native-operations.js +0 -10
  84. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  85. package/dist/native-pinned-write-windows.js +5 -4
  86. package/dist/native-staged-file.d.ts +8 -1
  87. package/dist/native-staged-file.d.ts.map +1 -1
  88. package/dist/native-staged-file.js +30 -9
  89. package/dist/opened-realpath.d.ts +12 -1
  90. package/dist/opened-realpath.d.ts.map +1 -1
  91. package/dist/opened-realpath.js +8 -7
  92. package/dist/output.d.ts.map +1 -1
  93. package/dist/output.js +26 -2
  94. package/dist/path-policy.js +1 -1
  95. package/dist/permissions.d.ts +1 -0
  96. package/dist/permissions.d.ts.map +1 -1
  97. package/dist/permissions.js +3 -0
  98. package/dist/pinned-write.d.ts +1 -2
  99. package/dist/pinned-write.d.ts.map +1 -1
  100. package/dist/pinned-write.js +2 -11
  101. package/dist/private-temp-workspace.js +1 -1
  102. package/dist/publish-file.js +2 -2
  103. package/dist/replace-file-copy-source.d.ts.map +1 -1
  104. package/dist/replace-file-copy-source.js +2 -7
  105. package/dist/replace-file-descriptor.d.ts.map +1 -1
  106. package/dist/replace-file-descriptor.js +5 -17
  107. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  108. package/dist/replace-file-temp-owner.js +2 -7
  109. package/dist/replace-file.d.ts.map +1 -1
  110. package/dist/replace-file.js +21 -8
  111. package/dist/root-errors.d.ts +1 -0
  112. package/dist/root-errors.d.ts.map +1 -1
  113. package/dist/root-errors.js +9 -0
  114. package/dist/root-file.d.ts.map +1 -1
  115. package/dist/root-file.js +17 -28
  116. package/dist/root-impl.d.ts.map +1 -1
  117. package/dist/root-impl.js +12 -33
  118. package/dist/root-path.d.ts.map +1 -1
  119. package/dist/root-path.js +49 -94
  120. package/dist/root-write-mode.d.ts +6 -0
  121. package/dist/root-write-mode.d.ts.map +1 -0
  122. package/dist/root-write-mode.js +60 -0
  123. package/dist/root-write-verification.js +2 -2
  124. package/dist/safe-path-segment.d.ts +1 -0
  125. package/dist/safe-path-segment.d.ts.map +1 -1
  126. package/dist/safe-path-segment.js +1 -1
  127. package/dist/secret-file.d.ts.map +1 -1
  128. package/dist/secret-file.js +4 -9
  129. package/dist/secret-read-async.d.ts.map +1 -1
  130. package/dist/secret-read-async.js +5 -10
  131. package/dist/secret-read-policy.d.ts +4 -0
  132. package/dist/secret-read-policy.d.ts.map +1 -1
  133. package/dist/secret-read-policy.js +12 -0
  134. package/dist/sibling-staged-file.d.ts.map +1 -1
  135. package/dist/sibling-staged-file.js +2 -7
  136. package/dist/sidecar-lock-policy.d.ts +0 -1
  137. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  138. package/dist/sidecar-lock-policy.js +0 -4
  139. package/dist/string-coerce.d.ts +0 -9
  140. package/dist/string-coerce.d.ts.map +1 -1
  141. package/dist/string-coerce.js +0 -56
  142. package/dist/temp-target.d.ts.map +1 -1
  143. package/dist/temp-target.js +1 -12
  144. package/docs/archive.md +66 -61
  145. package/docs/atomic.md +53 -19
  146. package/docs/contributing.md +35 -1
  147. package/docs/install.md +1 -1
  148. package/docs/native-helper.md +5 -0
  149. package/docs/native.md +16 -10
  150. package/docs/secret-file.md +5 -0
  151. package/docs/staged-file.md +6 -2
  152. package/docs/writing.md +5 -0
  153. package/package.json +19 -16
  154. package/dist/archive-tar-admission.d.ts +0 -7
  155. package/dist/archive-tar-admission.d.ts.map +0 -1
  156. package/dist/archive-tar-admission.js +0 -43
  157. package/dist/archive-tar-gnu.d.ts +0 -2
  158. package/dist/archive-tar-gnu.d.ts.map +0 -1
  159. package/dist/archive-tar-gnu.js +0 -20
  160. package/dist/archive-tar-header.d.ts +0 -8
  161. package/dist/archive-tar-header.d.ts.map +0 -1
  162. package/dist/archive-tar-header.js +0 -47
  163. package/dist/archive-tar-meta.d.ts +0 -34
  164. package/dist/archive-tar-meta.d.ts.map +0 -1
  165. package/dist/archive-tar-meta.js +0 -277
  166. package/dist/archive-tar-pax.d.ts +0 -8
  167. package/dist/archive-tar-pax.d.ts.map +0 -1
  168. package/dist/archive-tar-pax.js +0 -100
  169. package/dist/archive-tar-runtime.d.ts +0 -49
  170. package/dist/archive-tar-runtime.d.ts.map +0 -1
  171. package/dist/archive-tar-runtime.js +0 -22
  172. package/dist/fsync.d.ts +0 -2
  173. package/dist/fsync.d.ts.map +0 -1
  174. package/dist/fsync.js +0 -1
  175. package/dist/json-durable-queue-retirement.d.ts +0 -9
  176. package/dist/json-durable-queue-retirement.d.ts.map +0 -1
  177. package/dist/json-durable-queue-retirement.js +0 -126
  178. package/dist/json-durable-queue-transfer-lock.d.ts +0 -2
  179. package/dist/json-durable-queue-transfer-lock.d.ts.map +0 -1
  180. package/dist/json-durable-queue-transfer-lock.js +0 -19
  181. package/dist/mode.d.ts +0 -2
  182. package/dist/mode.d.ts.map +0 -1
  183. package/dist/mode.js +0 -3
  184. package/dist/output-sibling.d.ts +0 -8
  185. package/dist/output-sibling.d.ts.map +0 -1
  186. package/dist/output-sibling.js +0 -30
  187. package/dist/read-error.d.ts +0 -2
  188. package/dist/read-error.d.ts.map +0 -1
  189. package/dist/read-error.js +0 -11
  190. package/dist/short-path.d.ts +0 -2
  191. package/dist/short-path.d.ts.map +0 -1
  192. package/dist/short-path.js +0 -7
  193. package/dist/staged-file.d.ts +0 -10
  194. package/dist/staged-file.d.ts.map +0 -1
  195. package/dist/staged-file.js +0 -15
@@ -19,7 +19,16 @@ manager version declared in `package.json`.
19
19
  pnpm build
20
20
  ```
21
21
 
22
- Runs `tsc -p tsconfig.json`. Output lands in `dist/`. The package's `prepack` hook re-runs the build before publishing — manual `pnpm build` is only required when you want to inspect the output or run a freshly-built copy locally.
22
+ Runs TypeScript compilation and builds the portable Rust TAR parser for
23
+ `wasm32-unknown-unknown`. Contributors need Rust (the native crate's declared
24
+ minimum or newer) and `rustup target add wasm32-unknown-unknown`; Alpine's
25
+ packaged toolchain uses `rust-wasm`. `pnpm archive:wasm` rebuilds just the parser.
26
+ The import-free asset lands at `dist/archive-parser.wasm`; source tests and
27
+ compiled consumers both resolve that generated artifact. Run `pnpm build`
28
+ before source tests in a fresh checkout. Do not commit `dist/` or built WASM.
29
+ Consumers receive the asset in the npm package and need no compiler.
30
+
31
+ Output lands in `dist/`. The package's `prepack` hook re-runs the build before publishing — manual `pnpm build` is only required when you want to inspect the output or run a freshly-built copy locally.
23
32
 
24
33
  ## Test
25
34
 
@@ -59,8 +68,33 @@ pnpm check
59
68
  This runs the filesystem boundary checks, build, tests, and package
60
69
  tarball/import validation.
61
70
 
71
+ ### Real TAR producers
72
+
73
+ After installing the freshly packed root (and optionally its freshly built host
74
+ binding) in a disposable consumer, run:
75
+
76
+ ```bash
77
+ pnpm archive:producer-smoke ./consumer off
78
+ pnpm archive:producer-smoke ./consumer require
79
+ ```
80
+
81
+ This uses a child bound to canonical cwd/device/inode running `/usr/bin/tar -czf - .`
82
+ with unchanged stdout, and npm tar, on synthetic Unicode/newline/long-name files,
83
+ then the installed package API for exact payload hashes and bounded reads.
84
+ It also rejects a valid PAX override attached to an invalid raw UTF-8 field.
85
+ The `require` command must resolve the freshly packed native binding; the
86
+ `off` command uses the installed WASM asset. No live user files are read.
87
+
62
88
  ### Native consumer installs
63
89
 
90
+ The CI Node 24 and native jobs also run `node scripts/device-path-proof.mjs off`
91
+ and `node scripts/device-path-proof.mjs require` against the built package.
92
+ This extracts real ZIP files and checks bounded member reads, preserving reserved
93
+ device-like names on POSIX while rejecting them and ignored-space aliases on
94
+ Windows. It also verifies ordinary secret reads and typed device-path rejection
95
+ without replacing filesystem functions. Run after `pnpm build`, and build the
96
+ host binding with `pnpm native:build` before the `require` case.
97
+
64
98
  After `pnpm build` and a fresh `pnpm native:build`, run `pnpm package:smoke`.
65
99
  It packs the real root and host binding, then runs root-only npm and the
66
100
  declared pnpm version against a disposable loopback registry. The root's exact
package/docs/install.md CHANGED
@@ -85,7 +85,7 @@ Use the main entry for the common surface, or the focused subpaths when you want
85
85
 
86
86
  ## Runtime dependencies
87
87
 
88
- `@openclaw/fs-safe` lists `jszip` and `tar` as optional dependencies for JavaScript ZIP/TAR [archive extraction](archive.md). They are loaded lazily; the JavaScript archive fallback requires the corresponding codec and reports a missing-optional-dependency error without it. Public subpaths remain safe to import with all optional dependencies omitted, but imports do not prove native availability.
88
+ `@openclaw/fs-safe` bundles an import-free WASM build of its Rust TAR parser for guarded JavaScript TAR/gzip [archive extraction](archive.md), including installs with optional dependencies omitted. ZIP fallback uses lazily loaded optional `jszip` and reports a missing-dependency error without it. Public subpaths remain safe to import with all optional dependencies omitted, but imports do not prove native availability.
89
89
 
90
90
  There are no peer dependencies. Exact-version optional packages carry the seven
91
91
  native targets and npm-compatible OS, CPU, and Linux libc filters install only
@@ -29,6 +29,11 @@ The equivalent environment variables are `FS_SAFE_NATIVE_MODE` and `OPENCLAW_FS_
29
29
  | `off` | Do not load a native package. Use the guarded JavaScript path deterministically. |
30
30
  | `require` | Throw `FsSafeError("helper-unavailable")` instead of falling back when an operation needs the native binding and it cannot load. |
31
31
 
32
+ TAR/gzip in the guarded JavaScript path uses a bundled, import-free WASM build
33
+ of the same Rust parser used by native. `off` still disables native filesystem
34
+ code; it does not disable this portable parser. ZIP fallback still requires
35
+ optional `jszip`, and zstd/bzip2 remain native-only.
36
+
32
37
  Configure the mode once during startup. Loading is lazy and cached; changing from `auto` to `require` after a failed load changes failure policy but does not repeatedly probe the binary.
33
38
 
34
39
  [`tempWorkspace()` and its scoped/sync variants](temp.md#private-temp-workspaces)
package/docs/native.md CHANGED
@@ -64,13 +64,15 @@ returns a bounded manifest. TypeScript applies the shared path, filter, strip,
64
64
  mode, and byte policies and returns an index-bound extraction plan. Rust then
65
65
  creates only those planned entries beneath a private staging descriptor.
66
66
 
67
- A raw meter sits between decompression and the TAR crate, with matching
68
- TypeScript admission before node-tar. It parses 512-byte headers and bounded
69
- local PAX `x` metadata, using supported effective sizes to locate the following
70
- member body. GNU long-name/link `L`/`K` payloads remain
71
- supported. `maxMetaEntryBytes` bounds each metadata body before allocation;
72
- unsupported global/old metadata and sparse forms fail closed rather than being
73
- interpreted as ordinary members. See [bounded local PAX support](archive.md#bounded-local-pax-support).
67
+ The `fs-safe-archive-core` Rust workspace crate owns TAR framing, paths, types,
68
+ mode decoding, GNU metadata, and byte-counted local PAX records. The native
69
+ binding and bundled WASM module compile the same source. No `tar::Archive` or
70
+ Node TAR parser reinterprets admitted identities or sizes. Executors replay
71
+ admitted payload ranges after complete bounded admission; native writes retain
72
+ the platform's descriptor-relative primitives, while fallback writes retain
73
+ the guarded Node staging/publication boundary. ZIP behavior is unchanged.
74
+ `maxMetaEntryBytes` bounds bodies before allocation; unsupported global/old
75
+ metadata and sparse forms fail closed. See [bounded local PAX support](archive.md#bounded-local-pax-support).
74
76
 
75
77
  Every raw pass receives only TypeScript's resolved `maxEntries`,
76
78
  `maxMetaEntryBytes`, and `maxDecodedBytes`. Shared resolution caps metadata and
@@ -88,11 +90,15 @@ integer maximum. Every native pass receives that same cap and charges headers,
88
90
  metadata, bodies, padding, EOF blocks, and trailing zeros. It rejects overflow
89
91
  with `archive-decoded-size-exceeds-limit`; no ratio policy is implied.
90
92
  Extraction and entry reads drain the metered reader through physical EOF after
91
- TAR iteration. Trailing framing or decoded-limit failures propagate before
93
+ admitted-range replay. Trailing framing or decoded-limit failures propagate before
92
94
  directory modes are finalized, staging is published, or selected bytes return.
95
+ Native gzip uses the existing flate2 member decoder with an explicit bounded
96
+ member/padding transition; JavaScript retains Node gunzip and validates its
97
+ unconsumed compressed suffix. Only all-zero physical padding after a complete
98
+ validated trailer is accepted, still within the original archive-byte budget.
93
99
  Native reads stop at framing boundaries so a rejected header does not request
94
100
  its body from the decoder; codec buffering can still read ahead internally.
95
- Inspection finishes the complete bounded framing pass before parsing. Directory
101
+ Inspection finishes the complete bounded framing pass before returning its manifest. Directory
96
102
  and link bodies, missing two-block EOF, and nonzero trailers reject on both
97
103
  backends, as detailed in [raw TAR framing](archive.md#raw-tar-framing). Raw and
98
104
  padded sizes above JavaScript's safe-integer maximum reject as invalid framing
@@ -155,7 +161,7 @@ remain TypeScript-owned. What changes is the syscall strength or availability:
155
161
  | Capability | Native path | Guarded JavaScript path |
156
162
  |---|---|---|
157
163
  | Root-relative opens/mutations | Descriptor-relative beneath operations. Pinned writes create parents and publish both replacement and no-replace targets relative to open directory descriptors. Linux reports `kernel-atomic`; macOS and Windows report `best-effort`. macOS uses `O_RESOLVE_BENEATH` when available plus an `F_GETPATH` detector, while Windows rejects reparse traversal in the object-manager call. | Reports `best-effort`: component-wise alias checks, no-follow opens where Node exposes them, private temp/rename, and post-operation identity verification. A same-privilege peer can replace a writable parent after a guard assertion but before Node resolves the pathname mutation; the mutation may land outside the intended root before the post-check detects it. |
158
- | ZIP/TAR/gzip | Rust streaming decode and fd-relative output creation. | JSZip/node-tar into a private stage, then the same guarded merge policy. |
164
+ | ZIP/TAR/gzip | Rust streaming decode and fd-relative output creation. | Optional JSZip or bundled WASM TAR into guarded private staging, then the same guarded merge policy. |
159
165
  | Zstd/bzip2 TAR | Supported. | Unsupported; typed `helper-unavailable`. |
160
166
  | Publication copy | Clone, Linux `copy_file_range`, async native SHA-256. | Exclusive `wx` byte loop and Node SHA-256 with the same content/identity fences. |
161
167
  | `rename-noreplace` | Atomic platform no-replace rename. | Unsupported; no emulation by check-then-rename. |
@@ -91,6 +91,11 @@ credential must also fail on broad permissions or unexpected ownership.
91
91
  the same pinned-handle validation, byte cap, trimming, error codes, and strict
92
92
  versus missing-is-undefined naming semantics.
93
93
 
94
+ Both readers reject known unsafe device and process-fd paths with `device-path`
95
+ before inspection, and check the resolved target before opening it. This includes
96
+ Windows reserved device names and ignored-space aliases such as `nul .txt`.
97
+ Optional readers propagate this error instead of treating the secret as missing.
98
+
94
99
  Both sync and async readers compare lossless bigint identities from the preview,
95
100
  opened descriptor, resolved target, and current input path before reading. POSIX
96
101
  opens are nonblocking, so a raced FIFO is rejected by descriptor type instead of
@@ -66,8 +66,12 @@ Strings are UTF-8. `mode` is the requested **published** mode and defaults to
66
66
  `0600`; exact final modes, including `000`, are supported. The unpublished file
67
67
  stays at `0600` throughout preparation and any awaited application checks.
68
68
  After rename succeeds and the published entry passes identity validation, the
69
- owner applies the requested mode through its retained file descriptor and
70
- synchronizes the file. No parent is created or chmodded.
69
+ owner applies the requested mode through its retained file descriptor. Content
70
+ was synchronized during preparation; publication always synchronizes the parent.
71
+ Modes retaining owner read/write skip the extra mode-only file sync, so a crash
72
+ may leave the tighter staged `0600` instead of the wider requested mode. Modes
73
+ removing owner read or write, and corrections of an observed wider staged mode,
74
+ retain the post-chmod file sync. No parent is created or chmodded.
71
75
  Creation uses an exclusive, no-follow, close-on-exec open of a generated direct
72
76
  child name. Writes use that descriptor. Inspection uses non-following metadata
73
77
  operations, never a potentially blocking reopen of the leaf.
package/docs/writing.md CHANGED
@@ -84,6 +84,11 @@ final verification uses a descriptor retained by the writer rather than reopenin
84
84
  the published file. The requested mode is not relaxed for verification.
85
85
  Publication verification compares exact bigint descriptor and pathname identities,
86
86
  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.
88
+ Modes that retain owner read/write skip the extra mode-only file sync: after a
89
+ crash, the file may retain staged mode `0o600` instead of the wider requested mode.
90
+ Modes that remove owner read or write, and corrections of observed wider staging
91
+ permissions, keep the post-chmod file sync.
87
92
  Later reads still obey OS permissions, and access checks on a pre-existing
88
93
  destination are unchanged. The explicit FUSE compatibility policy still requires
89
94
  a readable destination to prove matching content when rename changes its identity.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
5
5
  "keywords": [
6
6
  "filesystem",
@@ -24,6 +24,7 @@
24
24
  "provenance": true
25
25
  },
26
26
  "files": [
27
+ "dist/archive-parser.wasm",
27
28
  "dist/**/*.js",
28
29
  "dist/**/*.d.ts",
29
30
  "dist/**/*.d.ts.map",
@@ -126,7 +127,7 @@
126
127
  "scripts": {
127
128
  "benchmark": "node scripts/benchmark.mjs",
128
129
  "benchmark:publish": "pnpm build && node scripts/bench-publish.mjs",
129
- "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
130
+ "build": "node scripts/prepack-build.mjs",
130
131
  "lint:file-size": "node scripts/check-file-size.mjs",
131
132
  "lint:fs-boundary": "node scripts/check-fs-boundary-primitives.mjs",
132
133
  "prepack": "node scripts/prepack-build.mjs",
@@ -139,7 +140,7 @@
139
140
  "docs:check": "node scripts/check-doc-examples.mjs",
140
141
  "docs:site": "node scripts/build-docs-site.mjs",
141
142
  "native:build": "pnpm --filter @openclaw/fs-safe-native-build build",
142
- "native:test": "cargo test --manifest-path native/Cargo.toml",
143
+ "native:test": "cargo test --workspace",
143
144
  "pack:check": "pnpm build && node scripts/check-pack.mjs",
144
145
  "public-api:update": "pnpm build && node scripts/check-pack.mjs --update-public-api",
145
146
  "package:collect": "node scripts/check-release-packages.mjs --output release-artifacts",
@@ -150,32 +151,34 @@
150
151
  "crabbox:hydrate": "crabbox actions hydrate",
151
152
  "crabbox:run": "crabbox run",
152
153
  "crabbox:stop": "crabbox stop",
153
- "crabbox:warmup": "crabbox warmup"
154
+ "crabbox:warmup": "crabbox warmup",
155
+ "archive:wasm": "node scripts/build-archive-wasm.mjs",
156
+ "archive:producer-smoke": "node scripts/archive-producer-smoke.mjs"
154
157
  },
155
158
  "optionalDependencies": {
156
- "@openclaw/fs-safe-darwin-arm64": "0.8.1",
157
- "@openclaw/fs-safe-darwin-x64": "0.8.1",
158
- "@openclaw/fs-safe-linux-arm64-gnu": "0.8.1",
159
- "@openclaw/fs-safe-linux-arm64-musl": "0.8.1",
160
- "@openclaw/fs-safe-linux-x64-gnu": "0.8.1",
161
- "@openclaw/fs-safe-linux-x64-musl": "0.8.1",
162
- "@openclaw/fs-safe-win32-x64-msvc": "0.8.1",
163
- "jszip": "^3.10.1",
164
- "tar": "7.5.22"
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",
166
+ "jszip": "^3.10.1"
165
167
  },
166
168
  "devDependencies": {
167
169
  "@emnapi/runtime": "2.0.0-alpha.4",
168
- "@napi-rs/cli": "3.8.6",
170
+ "@napi-rs/cli": "3.9.0",
169
171
  "@types/node": "^26.4.1",
170
- "@vitest/coverage-v8": "4.1.11",
172
+ "@vitest/coverage-v8": "5.0.0",
171
173
  "fast-check": "^4.9.0",
172
174
  "istanbul-lib-coverage": "3.2.2",
173
175
  "istanbul-lib-report": "3.0.1",
174
176
  "istanbul-reports": "3.2.0",
175
177
  "sigstore": "5.0.0",
178
+ "tar": "7.5.22",
176
179
  "typescript": "^7.0.2",
177
180
  "vite": "8.2.2",
178
- "vitest": "^4.1.11"
181
+ "vitest": "^5.0.0"
179
182
  },
180
183
  "engines": {
181
184
  "node": ">=22"
@@ -1,7 +0,0 @@
1
- import type { TarEntryInfo } from "./archive-tar.js";
2
- export declare function rawTarMember(header: Buffer, size: number, effectivePath?: string): TarEntryInfo;
3
- export declare function createTarAdmissionPlan(manifest: readonly TarEntryInfo[], check: (entry: TarEntryInfo) => boolean, strip: number): {
4
- consume(entry: TarEntryInfo): string | null;
5
- finish(): void;
6
- };
7
- //# sourceMappingURL=archive-tar-admission.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"archive-tar-admission.d.ts","sourceRoot":"","sources":["../src/archive-tar-admission.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAYrD,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,YAAY,CAS/F;AAED,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,SAAS,YAAY,EAAE,EACjC,KAAK,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,EACvC,KAAK,EAAE,MAAM,GACZ;IAAE,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,IAAI,IAAI,CAAA;CAAE,CAmBjE"}
@@ -1,43 +0,0 @@
1
- import { ArchiveFormatError } from "./archive-errors.js";
2
- import { stripArchivePath, validateArchiveEntryPath } from "./archive-entry.js";
3
- import { readTarHeaderPaths } from "./archive-tar-header.js";
4
- // node-tar's normalFsTypes/ReadEntry switch. All other non-metadata types
5
- // bypass both its filter and entry event; they still belong to our manifest.
6
- // Header.decode normalizes NUL to "0" before choosing the emitted File type.
7
- const visibleTypes = new Map([
8
- [0, "File"], [0x30, "File"], [0x31, "Link"], [0x32, "SymbolicLink"],
9
- [0x33, "CharacterDevice"], [0x34, "BlockDevice"], [0x35, "Directory"],
10
- [0x36, "FIFO"], [0x37, "ContiguousFile"], [0x44, "GNUDumpDir"],
11
- ]);
12
- export function rawTarMember(header, size, effectivePath) {
13
- const { name, prefix } = readTarHeaderPaths(header);
14
- validateArchiveEntryPath(name);
15
- validateArchiveEntryPath(prefix);
16
- const rawPath = prefix ? `${prefix}/${name}` : name;
17
- validateArchiveEntryPath(rawPath);
18
- const path = effectivePath ?? rawPath;
19
- validateArchiveEntryPath(path);
20
- return { path, size, type: visibleTypes.get(header[156]) ?? "Unsupported" };
21
- }
22
- export function createTarAdmissionPlan(manifest, check, strip) {
23
- const visible = [];
24
- for (const entry of manifest) {
25
- const accepted = check(entry);
26
- if (entry.type !== "Unsupported") {
27
- visible.push({ entry, output: accepted ? stripArchivePath(entry.path, strip) : null });
28
- }
29
- }
30
- let index = 0;
31
- const mismatch = () => new ArchiveFormatError("invalid TAR header: parser disagrees with raw admission");
32
- return {
33
- consume(actual) {
34
- const expected = visible[index++];
35
- if (!expected || expected.entry.type !== actual.type || expected.entry.size !== actual.size ||
36
- stripArchivePath(expected.entry.path, 0) !== stripArchivePath(actual.path, 0))
37
- throw mismatch();
38
- return expected.output;
39
- },
40
- finish() { if (index !== visible.length)
41
- throw mismatch(); },
42
- };
43
- }
@@ -1,2 +0,0 @@
1
- export declare function validateGnuMetadata(body: Buffer, type: "L" | "K"): string;
2
- //# sourceMappingURL=archive-tar-gnu.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"archive-tar-gnu.d.ts","sourceRoot":"","sources":["../src/archive-tar-gnu.ts"],"names":[],"mappings":"AAKA,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,GAAG,GAAG,MAAM,CAazE"}
@@ -1,20 +0,0 @@
1
- import { ArchiveFormatError } from "./archive-errors.js";
2
- import { validateArchiveEntryPath } from "./archive-entry.js";
3
- // Keep the byte grammar aligned with native/src/tar_meter.rs. Parsers differ
4
- // on embedded NULs and malformed UTF-8, so validate before either sees a name.
5
- export function validateGnuMetadata(body, type) {
6
- const value = body.at(-1) === 0 ? body.subarray(0, -1) : body;
7
- if (!value.length || value.includes(0)) {
8
- throw new ArchiveFormatError("invalid GNU metadata: empty name or embedded NUL");
9
- }
10
- let name;
11
- try {
12
- name = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(value);
13
- }
14
- catch {
15
- throw new ArchiveFormatError("invalid GNU metadata: name is not valid UTF-8");
16
- }
17
- if (type === "L")
18
- validateArchiveEntryPath(name);
19
- return name;
20
- }
@@ -1,8 +0,0 @@
1
- export declare function validateTarChecksum(header: Buffer): void;
2
- export declare function readTarHeaderPaths(header: Buffer): {
3
- name: string;
4
- prefix: string;
5
- linkname: string;
6
- };
7
- export declare function validateTarHeader(header: Buffer): void;
8
- //# sourceMappingURL=archive-tar-header.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"archive-tar-header.d.ts","sourceRoot":"","sources":["../src/archive-tar-header.ts"],"names":[],"mappings":"AAGA,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAWxD;AAcD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CASrG;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAUtD"}
@@ -1,47 +0,0 @@
1
- import { ArchiveFormatError, ArchiveSecurityError } from "./archive-errors.js";
2
- const utf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
3
- export function validateTarChecksum(header) {
4
- const field = header.subarray(148, 156);
5
- const end = field.indexOf(0);
6
- const digits = field.subarray(0, end < 0 ? field.length : end).toString("latin1").replace(/^ +| +$/g, "");
7
- const sum = header.reduce((total, byte, index) => total + (index >= 148 && index < 156 ? 32 : byte), 0);
8
- // Require an in-field delimiter: node-tar otherwise reads into the typeflag.
9
- if (!/^[0-7]+$/.test(digits) || ![0, 32].includes(field[7]) ||
10
- (end >= 0 && field.subarray(end).some((byte) => byte !== 0 && byte !== 32)) ||
11
- Number.parseInt(digits, 8) !== sum) {
12
- throw new ArchiveFormatError("invalid TAR header: checksum failure");
13
- }
14
- }
15
- function fixedField(field) {
16
- const zero = field.indexOf(0);
17
- if (zero >= 0 && field.subarray(zero).some((byte) => byte !== 0)) {
18
- throw new ArchiveSecurityError("entry-path", "tar entry path contains bytes after NUL");
19
- }
20
- try {
21
- return utf8.decode(field.subarray(0, zero < 0 ? field.length : zero));
22
- }
23
- catch {
24
- throw new ArchiveSecurityError("entry-path", "tar entry path is not valid UTF-8");
25
- }
26
- }
27
- export function readTarHeaderPaths(header) {
28
- // node-tar's star layout uses a 130-byte prefix followed by atime/ctime.
29
- const prefixEnd = header[475] === 0 ? 475 : 500;
30
- return {
31
- name: fixedField(header.subarray(0, 100)),
32
- prefix: header.subarray(257, 265).equals(Buffer.from("ustar\0" + "00"))
33
- ? fixedField(header.subarray(345, prefixEnd)) : "",
34
- linkname: fixedField(header.subarray(157, 257)),
35
- };
36
- }
37
- export function validateTarHeader(header) {
38
- validateTarChecksum(header);
39
- const { linkname } = readTarHeaderPaths(header);
40
- const isLink = header[156] === 0x31 || header[156] === 0x32;
41
- if (isLink && !linkname) {
42
- throw new ArchiveFormatError("invalid TAR header: linkname required on a link header");
43
- }
44
- if (!isLink && linkname) {
45
- throw new ArchiveFormatError("invalid TAR header: linkname forbidden on a non-link header");
46
- }
47
- }
@@ -1,34 +0,0 @@
1
- import { Transform } from "node:stream";
2
- import { type TarMeterLimits } from "./archive-limits.js";
3
- import type { TarEntryInfo } from "./archive-tar.js";
4
- export declare class TarMetadataMeter extends Transform {
5
- private readonly limits;
6
- private readonly onMember?;
7
- private readonly block;
8
- private blockLength;
9
- private state;
10
- private pendingPax;
11
- private readonly pendingGnu;
12
- private pendingGnuPath;
13
- private zeroBlocks;
14
- private entries;
15
- private remainingDecodedBytes;
16
- private manifestBytes;
17
- constructor(limits: TarMeterLimits, onMember?: ((entry: TarEntryInfo) => void) | undefined);
18
- private countMember;
19
- private invalid;
20
- private paddedSize;
21
- private parseSize;
22
- private finishHeader;
23
- private finishSparseHeader;
24
- private meter;
25
- _transform(chunk: Buffer, _encoding: BufferEncoding, callback: (error?: Error | null, data?: Buffer) => void): void;
26
- _flush(callback: (error?: Error | null) => void): void;
27
- }
28
- export declare function preflightTarMetadata(params: {
29
- archivePath: string;
30
- limits: TarMeterLimits;
31
- signal?: AbortSignal;
32
- onMember?: (entry: TarEntryInfo) => void;
33
- }): Promise<void>;
34
- //# sourceMappingURL=archive-tar-meta.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"archive-tar-meta.d.ts","sourceRoot":"","sources":["../src/archive-tar-meta.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAY,MAAM,aAAa,CAAC;AAIlD,OAAO,EAAqE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAI7H,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAWrD,qBAAa,gBAAiB,SAAQ,SAAS;IAYjC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAAkB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAX/E,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAC3C,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,KAAK,CAAkC;IAC/C,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAwB;IACnD,OAAO,CAAC,cAAc,CAAqB;IAC3C,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,qBAAqB,CAAS;IACtC,OAAO,CAAC,aAAa,CAAK;IAE1B,YAA6B,MAAM,EAAE,cAAc,EAAmB,QAAQ,CAAC,GAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,aAAA,EAM7G;IAED,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,UAAU;IAMlB,OAAO,CAAC,SAAS;IA4BjB,OAAO,CAAC,YAAY;IA6EpB,OAAO,CAAC,kBAAkB;IAgB1B,OAAO,CAAC,KAAK;IAyCJ,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAc3H;IAEQ,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAM9D;CACF;AAaD,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;CAC1C,GAAG,OAAO,CAAC,IAAI,CAAC,CAahB"}