@openclaw/fs-safe 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +28 -6
  3. package/dist/advanced.d.ts +1 -0
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +1 -0
  6. package/dist/archive-kind.d.ts +0 -1
  7. package/dist/archive-kind.d.ts.map +1 -1
  8. package/dist/archive-kind.js +5 -17
  9. package/dist/archive-parser.wasm +0 -0
  10. package/dist/archive-read.d.ts.map +1 -1
  11. package/dist/archive-read.js +6 -7
  12. package/dist/archive-tar-stream.d.ts +3 -0
  13. package/dist/archive-tar-stream.d.ts.map +1 -1
  14. package/dist/archive-tar-stream.js +56 -37
  15. package/dist/archive-tar-wasm.d.ts +16 -4
  16. package/dist/archive-tar-wasm.d.ts.map +1 -1
  17. package/dist/archive-tar-wasm.js +134 -34
  18. package/dist/archive.d.ts.map +1 -1
  19. package/dist/archive.js +5 -6
  20. package/dist/clone-metadata.d.ts +1 -0
  21. package/dist/clone-metadata.d.ts.map +1 -1
  22. package/dist/clone-metadata.js +6 -2
  23. package/dist/create-directory.d.ts +20 -0
  24. package/dist/create-directory.d.ts.map +1 -0
  25. package/dist/create-directory.js +130 -0
  26. package/dist/create-file-async.d.ts +7 -0
  27. package/dist/create-file-async.d.ts.map +1 -0
  28. package/dist/create-file-async.js +121 -0
  29. package/dist/create-file.d.ts +8 -0
  30. package/dist/create-file.d.ts.map +1 -0
  31. package/dist/create-file.js +190 -0
  32. package/dist/create-owned-file.d.ts +8 -0
  33. package/dist/create-owned-file.d.ts.map +1 -0
  34. package/dist/create-owned-file.js +16 -0
  35. package/dist/create.d.ts +4 -0
  36. package/dist/create.d.ts.map +1 -0
  37. package/dist/create.js +2 -0
  38. package/dist/creation-darwin.d.ts +7 -0
  39. package/dist/creation-darwin.d.ts.map +1 -0
  40. package/dist/creation-darwin.js +79 -0
  41. package/dist/creation-file-state.d.ts +19 -0
  42. package/dist/creation-file-state.d.ts.map +1 -0
  43. package/dist/creation-file-state.js +118 -0
  44. package/dist/creation-path.d.ts +21 -0
  45. package/dist/creation-path.d.ts.map +1 -0
  46. package/dist/creation-path.js +71 -0
  47. package/dist/creation-permissions.d.ts +19 -0
  48. package/dist/creation-permissions.d.ts.map +1 -0
  49. package/dist/creation-permissions.js +125 -0
  50. package/dist/directory-durability.d.ts +1 -1
  51. package/dist/directory-durability.d.ts.map +1 -1
  52. package/dist/directory-durability.js +22 -80
  53. package/dist/directory-guard.d.ts +3 -0
  54. package/dist/directory-guard.d.ts.map +1 -1
  55. package/dist/directory-mode-node.d.ts +2 -0
  56. package/dist/directory-mode-node.d.ts.map +1 -1
  57. package/dist/directory-mode-node.js +8 -0
  58. package/dist/directory-receipt.d.ts +24 -0
  59. package/dist/directory-receipt.d.ts.map +1 -0
  60. package/dist/directory-receipt.js +127 -0
  61. package/dist/file-cleanup.d.ts +19 -0
  62. package/dist/file-cleanup.d.ts.map +1 -0
  63. package/dist/file-cleanup.js +78 -0
  64. package/dist/file-observation.d.ts +1 -1
  65. package/dist/file-observation.d.ts.map +1 -1
  66. package/dist/file-store-boundary.d.ts +2 -6
  67. package/dist/file-store-boundary.d.ts.map +1 -1
  68. package/dist/file-store-boundary.js +3 -9
  69. package/dist/file-store-sync-write.d.ts.map +1 -1
  70. package/dist/file-store-sync-write.js +2 -5
  71. package/dist/guarded-mkdir.d.ts +1 -0
  72. package/dist/guarded-mkdir.d.ts.map +1 -1
  73. package/dist/guarded-mkdir.js +36 -7
  74. package/dist/move-path.js +1 -1
  75. package/dist/native-binding.d.ts +11 -1
  76. package/dist/native-binding.d.ts.map +1 -1
  77. package/dist/native-fallback-warning.d.ts +4 -0
  78. package/dist/native-fallback-warning.d.ts.map +1 -0
  79. package/dist/native-fallback-warning.js +11 -0
  80. package/dist/native-operations.d.ts +0 -2
  81. package/dist/native-operations.d.ts.map +1 -1
  82. package/dist/native-operations.js +0 -24
  83. package/dist/native-parent-admission.d.ts +2 -0
  84. package/dist/native-parent-admission.d.ts.map +1 -1
  85. package/dist/native-parent-admission.js +3 -2
  86. package/dist/native-pinned-write-windows.d.ts +1 -1
  87. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  88. package/dist/native-pinned-write-windows.js +173 -28
  89. package/dist/native-pinned-write.d.ts.map +1 -1
  90. package/dist/native-pinned-write.js +19 -3
  91. package/dist/native-policy-parent-windows.d.ts.map +1 -1
  92. package/dist/native-policy-parent-windows.js +15 -6
  93. package/dist/native-staged-file.d.ts +3 -2
  94. package/dist/native-staged-file.d.ts.map +1 -1
  95. package/dist/native-staged-file.js +86 -39
  96. package/dist/owner-dacl.d.ts.map +1 -1
  97. package/dist/owner-dacl.js +10 -4
  98. package/dist/pinned-write-input.d.ts +4 -0
  99. package/dist/pinned-write-input.d.ts.map +1 -0
  100. package/dist/pinned-write-input.js +25 -0
  101. package/dist/pinned-write-mode.d.ts +5 -0
  102. package/dist/pinned-write-mode.d.ts.map +1 -0
  103. package/dist/pinned-write-mode.js +24 -0
  104. package/dist/pinned-write-staged.d.ts +6 -0
  105. package/dist/pinned-write-staged.d.ts.map +1 -0
  106. package/dist/pinned-write-staged.js +187 -0
  107. package/dist/pinned-write-types.d.ts +3 -0
  108. package/dist/pinned-write-types.d.ts.map +1 -1
  109. package/dist/pinned-write.d.ts.map +1 -1
  110. package/dist/pinned-write.js +35 -145
  111. package/dist/private-directory.d.ts.map +1 -1
  112. package/dist/private-directory.js +18 -4
  113. package/dist/private-producer-handoff-sync.d.ts +14 -0
  114. package/dist/private-producer-handoff-sync.d.ts.map +1 -0
  115. package/dist/private-producer-handoff-sync.js +114 -0
  116. package/dist/private-producer-handoff.d.ts +22 -4
  117. package/dist/private-producer-handoff.d.ts.map +1 -1
  118. package/dist/private-producer-handoff.js +140 -77
  119. package/dist/publish-copy-stage.d.ts +2 -1
  120. package/dist/publish-copy-stage.d.ts.map +1 -1
  121. package/dist/publish-copy-stage.js +16 -7
  122. package/dist/publish-file.d.ts.map +1 -1
  123. package/dist/publish-file.js +2 -2
  124. package/dist/replace-file-temp-owner.d.ts +0 -7
  125. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  126. package/dist/replace-file-temp-owner.js +3 -54
  127. package/dist/root-create-input.d.ts +2 -1
  128. package/dist/root-create-input.d.ts.map +1 -1
  129. package/dist/root-create-input.js +13 -4
  130. package/dist/root-directory-creation.d.ts +3 -3
  131. package/dist/root-directory-creation.d.ts.map +1 -1
  132. package/dist/root-directory-creation.js +15 -3
  133. package/dist/root-impl.d.ts.map +1 -1
  134. package/dist/root-impl.js +28 -7
  135. package/dist/root-move-noreplace.d.ts.map +1 -1
  136. package/dist/root-move-noreplace.js +22 -13
  137. package/dist/root-options.d.ts +12 -4
  138. package/dist/root-options.d.ts.map +1 -1
  139. package/dist/root-path-stat.d.ts.map +1 -1
  140. package/dist/root-path-stat.js +59 -7
  141. package/dist/root-write-publication.js +1 -1
  142. package/dist/secret-file.d.ts.map +1 -1
  143. package/dist/secret-file.js +1 -0
  144. package/dist/secure-file-windows.d.ts +6 -0
  145. package/dist/secure-file-windows.d.ts.map +1 -1
  146. package/dist/secure-file-windows.js +34 -117
  147. package/dist/secure-file.js +2 -2
  148. package/dist/sidecar-lock-root.d.ts.map +1 -1
  149. package/dist/sidecar-lock-root.js +2 -1
  150. package/dist/staged-directory.d.ts.map +1 -1
  151. package/dist/staged-directory.js +6 -6
  152. package/dist/staged-file-settlement.d.ts +17 -0
  153. package/dist/staged-file-settlement.d.ts.map +1 -0
  154. package/dist/staged-file-settlement.js +57 -0
  155. package/dist/windows-owner.d.ts.map +1 -1
  156. package/dist/windows-owner.js +2 -1
  157. package/dist/windows-security-bridge.cs +336 -0
  158. package/dist/windows-security-bridge.ps1 +15 -0
  159. package/dist/windows-security-command.d.ts +26 -0
  160. package/dist/windows-security-command.d.ts.map +1 -0
  161. package/dist/windows-security-command.js +363 -0
  162. package/dist/windows-security-facts.d.ts +6 -0
  163. package/dist/windows-security-facts.d.ts.map +1 -0
  164. package/dist/windows-security-facts.js +108 -0
  165. package/docs/advanced.md +3 -1
  166. package/docs/archive.md +61 -37
  167. package/docs/config.md +6 -2
  168. package/docs/contributing.md +44 -4
  169. package/docs/copy.md +2 -0
  170. package/docs/creation.md +128 -0
  171. package/docs/durability.md +24 -0
  172. package/docs/install.md +31 -7
  173. package/docs/migrating-to-0.5.md +15 -6
  174. package/docs/migrating-to-0.6.md +9 -4
  175. package/docs/native-helper.md +22 -9
  176. package/docs/native.md +38 -7
  177. package/docs/permissions.md +37 -14
  178. package/docs/root.md +34 -0
  179. package/docs/secret-file.md +11 -2
  180. package/docs/secure-file.md +9 -4
  181. package/docs/sidecar-lock.md +5 -4
  182. package/docs/staged-file.md +5 -0
  183. package/docs/writing.md +71 -5
  184. package/package.json +18 -15
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.16.0 - 2026-09-19
6
+
7
+ ### Highlights
8
+
9
+ - **Private files and directories through existing APIs:** add `private: true` to Root `mkdir()`, `ensureRoot()`, `create()`, and `createJson()`, with verified owner-only permissions and creation-time Windows ACL protection. New `/advanced` helpers `createDirectory()`, `createDirectorySync()`, and `createFileSync()` create one exclusive entry under an existing trusted parent; the file helper returns an owned, disposable descriptor. ([#487](https://github.com/openclaw/fs-safe/pull/487), [#482](https://github.com/openclaw/fs-safe/issues/482))
10
+ - **Safer secret and private writes:** verify the actual staging permissions before writing content, rejecting filesystems that accept permission changes without enforcing the required private mode. ([#491](https://github.com/openclaw/fs-safe/pull/491), [#493](https://github.com/openclaw/fs-safe/pull/493))
11
+ - **Atomic buffered file creation:** opt into `atomic: true` on `Root.create()` or `createJson()` to keep the destination absent until its content is ready, including without the native addon. Independently select `durable: "file"` when file-flush failures must propagate. Existing defaults remain unchanged. ([#486](https://github.com/openclaw/fs-safe/pull/486))
12
+ - **Compressed TAR without native installation:** extract and read bounded entries from zstd and bzip2 TAR archives through bundled WASM codecs, even with all optional dependencies omitted. The fallback retains the shared Rust TAR parser, full-stream validation, byte limits, and guarded publication, with no runtime interpreter or download. ([#488](https://github.com/openclaw/fs-safe/pull/488))
13
+ - **Windows security operations without the addon:** use packaged, readable PowerShell/C# helpers for raw owner/DACL inspection, private-directory creation, and secure-file reads in native `auto` or `off` mode when capabilities are unavailable. Private permissions apply at creation, and secure reads inspect the same open handle that supplies the bytes. ([#476](https://github.com/openclaw/fs-safe/pull/476))
14
+
15
+ ### Compatibility and upgrade notes
16
+
17
+ - Private creation on macOS requires the matching native helper's ACL-inspection capability; disabled, missing, or older helpers reject before creating parents or stages. Relevant inheritable parent ACLs reject creation, while noninheriting ACLs remain allowed. Existing entries are never repaired or made less restrictive. Nonprivate creation and native-free secret-file writes retain their behavior; see the [creation contract](https://github.com/openclaw/fs-safe/blob/v0.16.0/docs/creation.md) for supported modes and platform limits.
18
+ - Native-free `atomic: true` creation requires hardlinks and rejects unsupported filesystems without publishing partial content. Private Windows file publication also requires hardlinks on the same local filesystem. Streamed creates continue to stage complete content; atomic visibility does not strengthen pathname containment or guarantee crash durability.
19
+ - `durable: "file"` applies to buffered, streamed, and JSON creates and propagates file-sync errors, including `EPERM`. Parent-directory synchronization remains best-effort, independently of the selected publication strategy.
20
+ - Windows fallbacks require system PowerShell and permission to run the packaged scripts and `.NET Add-Type` under normal system policy. They emit a path-free `FS_SAFE_NATIVE_FALLBACK` warning once per capability per process and add startup and compilation overhead; disallowed execution fails closed. No system policy is bypassed.
21
+ - Native `require` remains strict, and failures from available native operations never trigger a fallback retry. No-clobber `Root.move()` still requires native support. ZIP fallback still needs optional `jszip`, and `inspectTarArchive()` continues to accept only plain TAR and gzip.
22
+ - Atomic, streamed, and private creation can report cleanup, close, mode, or synchronization errors after a complete file has been published. Preserve the publication and cleanup receipts when handling failures; completed destinations remain in place, and indeterminate outcomes retain names for recovery.
23
+
24
+ ### Security and correctness
25
+
26
+ - Verify `0600` before secret-file payload writes in native and JavaScript writers. Verify actual native private-file ownership and permissions before payload writes and at publication, rejecting filesystems that accept permission changes without enforcing them; retain explicit final modes and public staging's `0600` guarantees. ([#491](https://github.com/openclaw/fs-safe/pull/491), [#493](https://github.com/openclaw/fs-safe/pull/493))
27
+ - Compare exact directory identities before and after durability syncs, retry unknown Windows observations once, and reject rounded or unverifiable caller receipts. Bind numeric metadata to the same observation and preserve private identity authority across receipt mutation, durable creation, publication, and retained staging. ([#483](https://github.com/openclaw/fs-safe/pull/483))
28
+ - Retain streamed `Root.create()` authority callbacks and abort signals across producer waits so replacing caller options cannot detach a revoked lease or redirect cancellation; preserve callback receivers and owned-stage cleanup. ([#490](https://github.com/openclaw/fs-safe/pull/490))
29
+ - Recheck retained parent and staging identities after final publication authority callbacks on JavaScript and native writers, preserving substituted entries before their bytes can be published. Flush native Windows creation through the retained writable descriptor and preserve published destinations after finalization failures.
30
+ - Snapshot `Root.move()` mutation policy before asynchronous admission so changes to caller-owned deny paths or prefixes cannot change an in-flight move. Recheck native no-clobber source identity, type, and hardlink policy after the final authority callback; live revocation remains available through `assertBeforeMutation`.
31
+ - Keep asynchronous Root-backed file locks waiting through successive owner handoffs instead of failing with `path-mismatch`, while retaining identity checks and requiring a fresh exclusive acquisition.
32
+ - Return one unsupported clone-metadata result per input without the native addon on non-macOS platforms in `auto` and `off` modes. Preserve input validation, strict `require` mode, and native-only APFS metadata on macOS. ([#489](https://github.com/openclaw/fs-safe/pull/489))
33
+
34
+ ### Diagnostics and maintenance
35
+
36
+ - Address Windows security commands by their built-in module names and restrict discovery to PowerShell's bundled system modules, avoiding broad discovery scans on helper startup. ([#484](https://github.com/openclaw/fs-safe/pull/484))
37
+ - Clarify that `resolveExistingPathsWithinRoot()` permits missing paths while `resolveStrictExistingPathsWithinRoot()` requires existing regular files, and distinguish native beneath mechanisms from the best-effort containment reported by public Root open, read, and writable-open results. ([#485](https://github.com/openclaw/fs-safe/pull/485))
38
+ - Honor case-insensitive Windows build environment names so configured WASM compilers and archivers remain selected in worker processes; preserve child-only compiler flags without duplicate case variants. ([#492](https://github.com/openclaw/fs-safe/pull/492))
39
+ - Refresh JavaScript and Rust dependencies, pnpm, CodeQL, and archive/release build toolchains, including current NAPI interoperability fixes; retain Node 22 support and Rust 1.88 compatibility. Includes the Dependabot updates in [#481](https://github.com/openclaw/fs-safe/pull/481).
40
+
3
41
  ## 0.15.0 - 2026-09-18
4
42
 
5
43
  ### Highlights
package/README.md CHANGED
@@ -45,7 +45,7 @@ The same idea has landed in other languages. Go [added `os.Root` and `OpenInRoot
45
45
  | `path.resolve().startsWith()` | string check only | – | – | – | – |
46
46
  | [`write-file-atomic`](https://www.npmjs.com/package/write-file-atomic) | – | ✓ | – | – | – |
47
47
  | Go [`os.Root`](https://go.dev/blog/osroot) / Rust [`cap-std`](https://github.com/bytecodealliance/cap-std) | ✓ | platform | ✓ | ✓ | – |
48
- | **`@openclaw/fs-safe`** | **✓** | **✓** | **✓** | **Linux atomic; others best-effort** | **✓ (ZIP/TAR; native zstd/bzip2)** |
48
+ | **`@openclaw/fs-safe`** | **✓** | **✓** | **✓** | **Linux atomic; others best-effort** | **✓ (ZIP/TAR/gzip/zstd/bzip2)** |
49
49
 
50
50
  ## Not a sandbox
51
51
 
@@ -57,7 +57,7 @@ This is a **library-level guardrail**, not OS-level isolation. It does not repla
57
57
  pnpm add @openclaw/fs-safe
58
58
  ```
59
59
 
60
- Node 22 or newer. Core root/path/json/temp helpers avoid framework dependencies. With all optional dependencies omitted, public subpaths remain safe to import and fallback-capable operations work in `auto` or `off`. Native-only features, including no-clobber `Root.move()`, remain unavailable and fail with `helper-unavailable`. TAR/gzip fallback uses the bundled WASM build of the same Rust parser as native and works with optional dependencies omitted. ZIP fallback still needs optional `jszip`. See the [0.6 migration guide](docs/migrating-to-0.6.md).
60
+ Node 22 or newer. Core root/path/json/temp helpers avoid framework dependencies. With all optional dependencies omitted, public subpaths remain safe to import and fallback-capable operations work in `auto` or `off`. Native-only features, including no-clobber `Root.move()` and [`private: true` creation on macOS](docs/creation.md#permission-options), remain unavailable and fail with `helper-unavailable`. TAR, gzip, zstd, and bzip2 extraction and bounded entry reads use the same Rust TAR parser through bundled WASM when native support is disabled or absent. Zstd/bzip2 codecs are bundled too; gzip uses Node's built-in decoder. ZIP fallback still needs optional `jszip`. See the [0.6 migration guide](docs/migrating-to-0.6.md).
61
61
 
62
62
  Bun 1.4.2 is also supported with the [Bun runtime requirements](docs/install.md#bun-runtime), including the matching Rust addon on macOS and Linux. JIT-disabled Bun works too.
63
63
 
@@ -70,7 +70,7 @@ environment policy:
70
70
  import { configureFsSafeNative } from "@openclaw/fs-safe";
71
71
 
72
72
  configureFsSafeNative({ mode: "auto" }); // default: native when available
73
- configureFsSafeNative({ mode: "off" }); // guarded JavaScript only
73
+ configureFsSafeNative({ mode: "off" }); // disable the addon; use supported fallbacks
74
74
  configureFsSafeNative({ mode: "require" }); // fail closed if the binding is unavailable
75
75
  ```
76
76
 
@@ -158,6 +158,23 @@ const opened = await fs.open("notes/today.txt");
158
158
  await fs.create("notes/README.md", "seed\n"); // throws if it already exists
159
159
  ```
160
160
 
161
+ Use `private: true` on `mkdir()`, `ensureRoot()`, `create()`, or `createJson()`
162
+ for private creation. On macOS, this requires native ACL inspection before
163
+ creating parents or stages and verifies owner-only permissions with no ACL
164
+ before writing payload bytes. Native `off` or a missing ACL capability rejects
165
+ with `helper-unavailable`; nonprivate creation is unchanged. See the
166
+ [creation contract](docs/creation.md#permission-options) for parent ACL handling
167
+ and platform support.
168
+
169
+ Pass `{ atomic: true }` to buffered `create()` or `createJson()` to keep the
170
+ destination absent until complete content is ready, including with native support
171
+ disabled. The JavaScript fallback requires hardlinks and never downgrades to a
172
+ partial visible file. Omitted or `false` retains the existing buffered behavior.
173
+ Atomic visibility is separate from the existing `durable` synchronization policy.
174
+ Use `durable: "file"` on `create()` or `createJson()` when file-flush errors,
175
+ including `EPERM`, must propagate. It combines with `atomic: true` without
176
+ requiring strict parent-directory synchronization.
177
+
161
178
  `create()` also accepts an `AsyncIterable<Uint8Array>` for large or incrementally
162
179
  produced files. Streamed creates keep the destination absent until all chunks
163
180
  are written, support `maxBytes` and `signal`, and recheck mutation authority
@@ -259,7 +276,7 @@ contract. Low-level helpers that OpenClaw needs to compose higher-level APIs are
259
276
  | `@openclaw/fs-safe/permissions` | POSIX mode and Windows ACL inspection, raw owner/ACE facts, private-directory creation, and remediation helpers |
260
277
  | `@openclaw/fs-safe/walk` | budget-bounded directory walking with symlink policy, filters, and truncation accounting; not root-bounded |
261
278
  | `@openclaw/fs-safe/copy` | directory copying with `clone: "auto"`, `"always"`, or `"never"`; native APFS, Btrfs, ReFS, XFS, and ZFS cloning, portable byte copying, and clone metadata; see [directory copying](docs/copy.md) |
262
- | `@openclaw/fs-safe/archive` | policy-driven ZIP/TAR extraction, clamp/filter policy, metadata/path-depth limits, native gzip/zstd/bzip2, and bounded entry reads |
279
+ | `@openclaw/fs-safe/archive` | policy-driven ZIP/TAR extraction, clamp/filter policy, metadata/path-depth limits, gzip/zstd/bzip2 support, and bounded entry reads |
263
280
  | `@openclaw/fs-safe/advanced` | lower-level composition helpers such as path scopes, root-file open, bounded descriptor reads, [borrowed-handle and descriptor copying](docs/copy.md#borrowed-filehandle-transfers), [exact directory identity](docs/directory-identity.md), [case probing](docs/path-case.md), [suffix-alias probing](docs/path-suffix-aliases.md), [in-place writes](docs/in-place-write.md), [versioned install-ID encoding](docs/install-path.md#safepathsegmenthashedv2), filename sanitizing, temp-file targets, sibling-temp writes, local-root readers, regular-file helpers, `pathExists`, and `withTimeout`; less stable than focused public subpaths |
264
281
  | `@openclaw/fs-safe/errors` | `FsSafeError`, closed codes/categories, causes, and operation-specific details receipts |
265
282
  | `@openclaw/fs-safe/types` | shared types: `DirEntry`, `PathStat`, … |
@@ -466,8 +483,13 @@ instead of a root-relative workspace path. It opens the file first, validates th
466
483
  same handle it will read from, checks trusted directories, owner, POSIX mode or
467
484
  Windows ACLs, hardlink count, size, and optional timeout, then reads through the
468
485
  pinned handle. On Windows, both the bytes and the owner/DACL facts come from that
469
- handle; secure reads require the matching current native package and do not fall
470
- back to a pathname ACL command.
486
+ handle. In native `auto` or `off` mode, a packaged, readable PowerShell script can
487
+ inspect that borrowed handle when the native capability is unavailable, subject
488
+ to the [Windows security fallback prerequisites](docs/install.md#windows-security-fallback).
489
+ It emits one fallback warning per process for secure reads and adds PowerShell
490
+ startup and compilation overhead per call. Native `require` remains strict, and
491
+ native operation failures are terminal. Neither route reopens the pathname to
492
+ inspect its ACL.
471
493
 
472
494
  ```ts
473
495
  import { readSecureFile } from "@openclaw/fs-safe/secure-file";
@@ -1,4 +1,5 @@
1
1
  export { createAsyncLock } from "./async-lock.js";
2
+ export { createDirectory, createDirectorySync, createFileSync, type CreateDirectoryOptions, type CreateFileOptions, type OwnedFileDescriptorSync, } from "./create.js";
2
3
  export { copyFileHandle, copyFileDescriptorSync, type CopyFileHandleOptions } from "./file-handle-transfer.js";
3
4
  export { overwriteFileHandle, type OverwriteFileHandleOptions } from "./overwrite-file-handle.js";
4
5
  export { probePathCaseInsensitiveSync, type ProbePathCaseOptions } from "./path-case.js";
@@ -1 +1 @@
1
- {"version":3,"file":"advanced.d.ts","sourceRoot":"","sources":["../src/advanced.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"advanced.d.ts","sourceRoot":"","sources":["../src/advanced.ts"],"names":[],"mappings":"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"}
package/dist/advanced.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // public subpaths; prefer root/json/store/temp/archive unless you are building a
3
3
  // higher-level primitive.
4
4
  export { createAsyncLock } from "./async-lock.js";
5
+ export { createDirectory, createDirectorySync, createFileSync, } from "./create.js";
5
6
  export { copyFileHandle, copyFileDescriptorSync } from "./file-handle-transfer.js";
6
7
  export { overwriteFileHandle } from "./overwrite-file-handle.js";
7
8
  export { probePathCaseInsensitiveSync } from "./path-case.js";
@@ -4,6 +4,5 @@ type ResolvePackedRootDirOptions = {
4
4
  rootMarkers?: string[];
5
5
  };
6
6
  export declare function resolvePackedRootDir(extractDir: string, options?: ResolvePackedRootDirOptions): Promise<string>;
7
- export declare function assertPortableArchiveKind(kind: ArchiveKind): void;
8
7
  export {};
9
8
  //# sourceMappingURL=archive-kind.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"archive-kind.d.ts","sourceRoot":"","sources":["../src/archive-kind.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,WAAW,GAAG,UAAU,GAAG,KAAK,CAAC;AAmBnE,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAcvE;AAED,KAAK,2BAA2B,GAAG;IACjC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AAkBF,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,2BAA2B,GACpC,OAAO,CAAC,MAAM,CAAC,CA4BjB;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAQjE"}
1
+ {"version":3,"file":"archive-kind.d.ts","sourceRoot":"","sources":["../src/archive-kind.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,WAAW,GAAG,UAAU,GAAG,KAAK,CAAC;AAQnE,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAgBvE;AAED,KAAK,2BAA2B,GAAG;IACjC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AAkBF,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,2BAA2B,GACpC,OAAO,CAAC,MAAM,CAAC,CA4BjB"}
@@ -1,29 +1,23 @@
1
1
  import fsSync from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { FsSafeError } from "./errors.js";
5
4
  import { getNativeBinding } from "./native.js";
6
5
  import { normalizeLowercaseStringOrEmpty } from "./string-coerce.js";
7
6
  const TAR_SUFFIXES = [".tgz", ".tar.gz", ".tar"];
8
- const NATIVE_TAR_SUFFIXES = [
7
+ const COMPRESSED_TAR_SUFFIXES = [
9
8
  { suffixes: [".tbz2", ".tbz", ".tar.bz2"], kind: "tar-bzip2" },
10
9
  { suffixes: [".tzst", ".tar.zst", ".tar.zstd"], kind: "tar-zstd" },
11
10
  ];
12
- function requireNativeArchiveKind(kind) {
13
- if (!getNativeBinding()) {
14
- throw new FsSafeError("helper-unavailable", `${kind} archives require the matching optional native platform package; ` +
15
- "install @openclaw/fs-safe with optional dependencies enabled on a supported platform and use FS_SAFE_NATIVE_MODE=auto or require");
16
- }
17
- return kind;
18
- }
19
11
  export function resolveArchiveKind(filePath) {
20
12
  const lower = normalizeLowercaseStringOrEmpty(filePath);
21
13
  if (lower.endsWith(".zip")) {
22
14
  return "zip";
23
15
  }
24
- for (const { suffixes, kind } of NATIVE_TAR_SUFFIXES) {
16
+ for (const { suffixes, kind } of COMPRESSED_TAR_SUFFIXES) {
25
17
  if (suffixes.some((suffix) => lower.endsWith(suffix))) {
26
- return requireNativeArchiveKind(kind);
18
+ // Preserve strict require diagnostics even when only resolving a suffix.
19
+ getNativeBinding();
20
+ return kind;
27
21
  }
28
22
  }
29
23
  if (TAR_SUFFIXES.some((suffix) => lower.endsWith(suffix))) {
@@ -75,9 +69,3 @@ export async function resolvePackedRootDir(extractDir, options) {
75
69
  }
76
70
  return path.join(extractDir, onlyDir);
77
71
  }
78
- export function assertPortableArchiveKind(kind) {
79
- if (kind === "tar-zstd" || kind === "tar-bzip2") {
80
- throw new FsSafeError("helper-unavailable", `${kind} archives require the matching optional native platform package; ` +
81
- "install @openclaw/fs-safe with optional dependencies enabled on a supported platform and use FS_SAFE_NATIVE_MODE=auto or require");
82
- }
83
- }
Binary file
@@ -1 +1 @@
1
- {"version":3,"file":"archive-read.d.ts","sourceRoot":"","sources":["../src/archive-read.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAiD,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAqOpG,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAAE,GAChD,OAAO,CAAC,MAAM,CAAC,CAkBjB"}
1
+ {"version":3,"file":"archive-read.d.ts","sourceRoot":"","sources":["../src/archive-read.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAqOzE,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAAE,GAChD,OAAO,CAAC,MAAM,CAAC,CAiBjB"}
@@ -7,7 +7,7 @@ import { readBoundedAsync } from "./bounded-read.js";
7
7
  import { ArchiveFormatError, ArchiveSecurityError, isArchiveFormatErrorMessage, } from "./archive-errors.js";
8
8
  import { formatErrorDetail } from "./error-detail.js";
9
9
  import { stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
10
- import { assertPortableArchiveKind, resolveArchiveKind } from "./archive-kind.js";
10
+ import { resolveArchiveKind } from "./archive-kind.js";
11
11
  import { DEFAULT_MAX_ARCHIVE_BYTES_ZIP, ArchiveLimitError, ARCHIVE_LIMIT_ERROR_CODE, } from "./archive-limits.js";
12
12
  import { isGzipBuffer } from "./archive-gzip-tail.js";
13
13
  import { inspectTar, replayTar } from "./archive-tar-stream.js";
@@ -139,11 +139,11 @@ async function readZipEntry(buffer, entryPath, maxBytes, admitted) {
139
139
  throw normalizeZipIntegrityError(error);
140
140
  }
141
141
  }
142
- async function readTarEntry(archiveBuffer, entryPath, maxBytes) {
142
+ async function readTarEntry(archiveBuffer, entryPath, maxBytes, kind) {
143
143
  const seenPaths = new Set();
144
144
  let selected;
145
145
  const limits = resolveTarMeterLimits();
146
- await inspectTar({ archiveBuffer, limits, onMember(info) {
146
+ await inspectTar({ archiveBuffer, kind, limits, onMember(info) {
147
147
  const normalized = canonicalEntryPath(info.path);
148
148
  if (seenPaths.has(normalized)) {
149
149
  throw new ArchiveSecurityError("entry-path", `archive contains duplicate entry path: ${formatErrorDetail(normalized)}`);
@@ -159,7 +159,7 @@ async function readTarEntry(archiveBuffer, entryPath, maxBytes) {
159
159
  }
160
160
  if (selected.size > maxBytes)
161
161
  throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
162
- if (!isGzipBuffer(archiveBuffer)) {
162
+ if (kind === "tar" && !isGzipBuffer(archiveBuffer)) {
163
163
  // Complete admission already validated this private snapshot through EOF.
164
164
  // Copy the admitted payload so the result cannot expose or mutate its input.
165
165
  const end = selected.offset + selected.size;
@@ -170,7 +170,7 @@ async function readTarEntry(archiveBuffer, entryPath, maxBytes) {
170
170
  return Buffer.from(archiveBuffer.subarray(selected.offset, end));
171
171
  }
172
172
  let result;
173
- await replayTar({ archiveBuffer, limits, members: [selected], async consume(member, payload) {
173
+ await replayTar({ archiveBuffer, kind, limits, members: [selected], async consume(member, payload) {
174
174
  result = await readAdmittedTarPayload(payload, member.size);
175
175
  } });
176
176
  return result;
@@ -238,7 +238,6 @@ export async function readArchiveEntry(archivePath, entryPath, options) {
238
238
  const native = getNativeBinding();
239
239
  if (native)
240
240
  return await readNativeBufferEntry(native, buffer, kind, requestedEntry, entryPath, options.maxBytes, zipEntries);
241
- assertPortableArchiveKind(kind);
242
241
  return kind === "zip" ? await readZipEntry(buffer, requestedEntry, options.maxBytes, zipEntries)
243
- : await readTarEntry(buffer, requestedEntry, options.maxBytes);
242
+ : await readTarEntry(buffer, requestedEntry, options.maxBytes, kind);
244
243
  }
@@ -1,3 +1,4 @@
1
+ import type { ArchiveKind } from "./archive-kind.js";
1
2
  import type { TarMeterLimits } from "./archive-limits.js";
2
3
  import { type AdmittedTarMember } from "./archive-tar-wasm.js";
3
4
  /** Buffers are private immutable snapshots, just like the staged file route. */
@@ -11,6 +12,7 @@ type TarInput = {
11
12
  export declare function inspectTar(params: TarInput & {
12
13
  limits: TarMeterLimits;
13
14
  signal?: AbortSignal;
15
+ kind?: Exclude<ArchiveKind, "zip">;
14
16
  onMember?: (entry: AdmittedTarMember) => void;
15
17
  }): Promise<void>;
16
18
  /** Replay in physical order, retaining at most one decoded chunk. Every range
@@ -18,6 +20,7 @@ export declare function inspectTar(params: TarInput & {
18
20
  export declare function replayTar<T extends AdmittedTarMember>(params: TarInput & {
19
21
  limits: TarMeterLimits;
20
22
  signal?: AbortSignal;
23
+ kind?: Exclude<ArchiveKind, "zip">;
21
24
  members: readonly T[];
22
25
  consume(member: T, payload: AsyncIterable<Buffer>): Promise<void>;
23
26
  }): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"archive-tar-stream.d.ts","sourceRoot":"","sources":["../src/archive-tar-stream.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAmB,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAGhF,gFAAgF;AAChF,KAAK,QAAQ,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,KAAK,CAAA;CAAE,GAAG;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC;AAuDhH,wBAAsB,UAAU,CAAC,MAAM,EAAE,QAAQ,GAAG;IAClD,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC7C,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;CAC/C,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhB;AAED;2DAC2D;AAC3D,wBAAsB,SAAS,CAAC,CAAC,SAAS,iBAAiB,EAAE,MAAM,EAAE,QAAQ,GAAG;IAC9E,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC7C,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC;IACtB,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnE,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BhB"}
1
+ {"version":3,"file":"archive-tar-stream.d.ts","sourceRoot":"","sources":["../src/archive-tar-stream.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAmC,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAGhG,gFAAgF;AAChF,KAAK,QAAQ,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,KAAK,CAAA;CAAE,GAAG;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC;AA0EhH,wBAAsB,UAAU,CAAC,MAAM,EAAE,QAAQ,GAAG;IAClD,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IACjF,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;CAC/C,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhB;AAED;2DAC2D;AAC3D,wBAAsB,SAAS,CAAC,CAAC,SAAS,iBAAiB,EAAE,MAAM,EAAE,QAAQ,GAAG;IAC9E,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IACjF,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC;IACtB,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnE,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BhB"}
@@ -1,10 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import { Readable, Writable } from "node:stream";
3
- import { pipeline } from "node:stream/promises";
3
+ import { finished, pipeline } from "node:stream/promises";
4
4
  import { createGunzip } from "node:zlib";
5
5
  import { GzipInput, isGzipBuffer, validateGzipBufferTail, validateGzipContainerTail } from "./archive-gzip-tail.js";
6
6
  import { ArchiveFormatError } from "./archive-errors.js";
7
- import { TarParserStream } from "./archive-tar-wasm.js";
7
+ import { TarParserStream, TarWasmSession } from "./archive-tar-wasm.js";
8
8
  import { readFileWindowFully } from "./positional-read.js";
9
9
  function* bufferChunks(buffer) {
10
10
  for (let offset = 0; offset < buffer.length; offset += 65536)
@@ -23,45 +23,64 @@ async function gzipFile(filePath) {
23
23
  }
24
24
  async function withTarStream(params, consume) {
25
25
  const buffer = params.archiveBuffer;
26
- const gzip = buffer !== undefined ? isGzipBuffer(buffer) : await gzipFile(params.archivePath);
27
- const parser = new TarParserStream(params.limits, params.onMember);
28
- const input = buffer !== undefined
29
- ? Readable.from(bufferChunks(buffer), { objectMode: false, highWaterMark: 65536 })
30
- : fs.createReadStream(params.archivePath, { highWaterMark: 65536 });
31
- // Match the WASM input window for both staged files and buffered reads.
32
- const decoder = gzip ? createGunzip({ chunkSize: 65536 }) : undefined;
33
- const gzipInput = decoder ? new GzipInput(decoder) : undefined;
34
- const destroy = (error) => {
35
- input.destroy(error);
36
- gzipInput?.destroy(error);
37
- decoder?.destroy(error);
38
- parser.destroy(error);
39
- };
40
- const pumps = decoder && gzipInput
41
- ? [pipeline(decoder, parser, { signal: params.signal }), pipeline(input, gzipInput, { signal: params.signal })]
42
- : [pipeline(input, parser, { signal: params.signal })];
43
- // Either pump tears down both routes; join every pump even after the first failure.
44
- const settled = Promise.all(pumps.map((pump) => pump.then(() => undefined, (cause) => {
45
- const error = cause instanceof Error ? cause : new Error(String(cause));
46
- destroy(error);
47
- return error;
48
- }))).then((errors) => errors.find((error) => error !== undefined));
26
+ const kind = params.kind ?? "tar";
27
+ const gzip = kind === "tar" && (buffer !== undefined ? isGzipBuffer(buffer) : await gzipFile(params.archivePath));
28
+ const session = new TarWasmSession(params.limits);
49
29
  try {
50
- const result = await consume(parser);
51
- const error = await settled;
52
- if (error)
53
- throw error;
54
- if (gzipInput) {
55
- if (buffer !== undefined)
56
- await validateGzipBufferTail(buffer, gzipInput.tailOffset, params.signal);
57
- else
58
- await validateGzipContainerTail(params.archivePath, gzipInput.tailOffset, params.signal);
30
+ const parser = new TarParserStream(params.limits, params.onMember, session);
31
+ const input = buffer !== undefined
32
+ ? Readable.from(bufferChunks(buffer), { objectMode: false, highWaterMark: 65536 })
33
+ : fs.createReadStream(params.archivePath, { highWaterMark: 65536 });
34
+ // Match the WASM input window for both staged files and buffered reads.
35
+ const decoder = gzip ? createGunzip({ chunkSize: 65536 }) : undefined;
36
+ const gzipInput = decoder ? new GzipInput(decoder) : undefined;
37
+ const destroy = (error) => {
38
+ input.destroy(error);
39
+ gzipInput?.destroy(error);
40
+ decoder?.destroy(error);
41
+ parser.destroy(error);
42
+ };
43
+ // A pipeline with an async-generator stage can settle before its file's
44
+ // asynchronous close. Join the actual close events before freeing WASM.
45
+ const closed = Promise.all([input, parser, ...(decoder ? [decoder] : []), ...(gzipInput ? [gzipInput] : [])].map(stream => {
46
+ const closing = new Promise(resolve => { stream.once("close", resolve); });
47
+ return finished(stream, { cleanup: true }).then(() => undefined, (cause) => cause instanceof Error ? cause : new Error(String(cause))).then(async (error) => { await closing; return error; });
48
+ }));
49
+ const pumps = decoder && gzipInput
50
+ ? [pipeline(decoder, parser, { signal: params.signal }), pipeline(input, gzipInput, { signal: params.signal })]
51
+ : kind !== "tar"
52
+ ? [pipeline(input, (source) => session.decode(source, kind, params.signal), parser, { signal: params.signal })]
53
+ : [pipeline(input, parser, { signal: params.signal })];
54
+ // Either pump tears down both routes; join every pump even after the first failure.
55
+ const settled = Promise.all(pumps.map((pump) => pump.then(() => undefined, (cause) => {
56
+ const error = cause instanceof Error ? cause : new Error(String(cause));
57
+ destroy(error);
58
+ return error;
59
+ }))).then((errors) => errors.find((error) => error !== undefined));
60
+ try {
61
+ const result = await consume(parser);
62
+ const error = await settled;
63
+ if (error)
64
+ throw error;
65
+ const closeError = (await closed).find(error => error !== undefined);
66
+ if (closeError)
67
+ throw closeError;
68
+ if (gzipInput) {
69
+ if (buffer !== undefined)
70
+ await validateGzipBufferTail(buffer, gzipInput.tailOffset, params.signal);
71
+ else
72
+ await validateGzipContainerTail(params.archivePath, gzipInput.tailOffset, params.signal);
73
+ }
74
+ return result;
75
+ }
76
+ finally {
77
+ destroy();
78
+ await settled;
79
+ await closed;
59
80
  }
60
- return result;
61
81
  }
62
82
  finally {
63
- destroy();
64
- await settled;
83
+ session.dispose();
65
84
  }
66
85
  }
67
86
  export async function inspectTar(params) {
@@ -4,13 +4,25 @@ import type { TarEntryInfo } from "./archive-tar.js";
4
4
  export type AdmittedTarMember = TarEntryInfo & {
5
5
  offset: number;
6
6
  };
7
- /** Backpressure-aware transport only; all TAR semantics live in the Rust core. */
8
- export declare class TarParserStream extends Transform {
9
- private readonly onMember?;
7
+ /** One bounded memory domain owns both admission and decompression. Its owner
8
+ * must join the decoder and parser before disposing this shared instance. */
9
+ export declare class TarWasmSession {
10
10
  private abi;
11
- constructor(limits: TarMeterLimits, onMember?: ((entry: AdmittedTarMember) => void) | undefined);
11
+ constructor(limits: TarMeterLimits);
12
12
  private bytes;
13
13
  private text;
14
+ parse(chunk: Buffer, onMember?: (entry: AdmittedTarMember) => void): void;
15
+ finish(): void;
16
+ private codecError;
17
+ decode(source: AsyncIterable<Buffer>, kind: "tar-zstd" | "tar-bzip2", signal?: AbortSignal): AsyncGenerator<Buffer>;
18
+ dispose(): void;
19
+ }
20
+ /** Backpressure-aware transport only; all TAR semantics live in the Rust core. */
21
+ export declare class TarParserStream extends Transform {
22
+ private readonly onMember?;
23
+ private readonly session;
24
+ private readonly ownsSession;
25
+ constructor(limits: TarMeterLimits, onMember?: ((entry: AdmittedTarMember) => void) | undefined, session?: TarWasmSession);
14
26
  _transform(chunk: Buffer, _encoding: BufferEncoding, callback: TransformCallback): void;
15
27
  _flush(callback: TransformCallback): void;
16
28
  _destroy(error: Error | null, callback: (error: Error | null) => void): void;
@@ -1 +1 @@
1
- {"version":3,"file":"archive-tar-wasm.d.ts","sourceRoot":"","sources":["../src/archive-tar-wasm.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAyClE,kFAAkF;AAClF,qBAAa,eAAgB,SAAQ,SAAS;IAER,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAD9D,OAAO,CAAC,GAAG,CAAkB;IAC7B,YAAY,MAAM,EAAE,cAAc,EAAmB,QAAQ,CAAC,GAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,aAAA,EAQjG;IACD,OAAO,CAAC,KAAK;IAQb,OAAO,CAAC,IAAI;IAIH,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAqB/F;IACQ,MAAM,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAKjD;IACQ,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAKpF;CACF"}
1
+ {"version":3,"file":"archive-tar-wasm.d.ts","sourceRoot":"","sources":["../src/archive-tar-wasm.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAGhE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAmDlE;6EAC6E;AAC7E,qBAAa,cAAc;IACzB,OAAO,CAAC,GAAG,CAAkB;IAC7B,YAAY,MAAM,EAAE,cAAc,EAMjC;IACD,OAAO,CAAC,KAAK;IAQb,OAAO,CAAC,IAAI;IAIZ,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAAG,IAAI,CAkBxE;IACD,MAAM,IAAI,IAAI,CAEb;IACD,OAAO,CAAC,UAAU;IAIX,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,UAAU,GAAG,WAAW,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,cAAc,CAAC,MAAM,CAAC,CA2DzH;IACD,OAAO,IAAI,IAAI,CAOd;CACF;AAED,kFAAkF;AAClF,qBAAa,eAAgB,SAAQ,SAAS;IAGR,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAF9D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAU;IACtC,YAAY,MAAM,EAAE,cAAc,EAAmB,QAAQ,CAAC,GAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,aAAA,EAAE,OAAO,CAAC,EAAE,cAAc,EAI3H;IACQ,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAG/F;IACQ,MAAM,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAGjD;IACQ,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAIpF;CACF"}