@openclaw/fs-safe 0.18.2 → 0.20.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 (124) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +15 -5
  3. package/dist/advanced.d.ts +2 -0
  4. package/dist/advanced.js +1 -0
  5. package/dist/archive-durability.js +1 -1
  6. package/dist/archive-merge.js +1 -1
  7. package/dist/archive-plan.d.ts +2 -7
  8. package/dist/archive-read.js +9 -18
  9. package/dist/archive-staging.js +3 -1
  10. package/dist/archive-zip-entry.d.ts +11 -11
  11. package/dist/archive-zip-entry.js +3 -35
  12. package/dist/archive-zip-integrity.d.ts +2 -2
  13. package/dist/archive-zip-integrity.js +2 -12
  14. package/dist/archive-zip-loader.d.ts +7 -3
  15. package/dist/archive-zip-loader.js +10 -9
  16. package/dist/archive-zip-preflight.d.ts +2 -1
  17. package/dist/archive-zip-preflight.js +16 -7
  18. package/dist/archive.js +17 -16
  19. package/dist/directory-receipt.js +5 -7
  20. package/dist/effective-uid.js +1 -4
  21. package/dist/errors.d.ts +3 -1
  22. package/dist/errors.js +3 -2
  23. package/dist/file-lock-sync-root-held.d.ts +4 -11
  24. package/dist/file-lock-sync-root-held.js +1 -4
  25. package/dist/file-lock-sync-root-io.d.ts +1 -4
  26. package/dist/file-lock-sync-root.d.ts +2 -4
  27. package/dist/file-store-boundary.d.ts +3 -7
  28. package/dist/file-store-boundary.js +7 -10
  29. package/dist/file-store-prune.js +3 -2
  30. package/dist/file-store.d.ts +4 -7
  31. package/dist/file-store.js +19 -21
  32. package/dist/guest-native-python.js +23 -31
  33. package/dist/guest.js +21 -12
  34. package/dist/json-document-store.d.ts +4 -9
  35. package/dist/json-durable-queue.js +2 -6
  36. package/dist/local-file-access.js +2 -5
  37. package/dist/local-file-descriptor.d.ts +2 -5
  38. package/dist/local-roots.d.ts +2 -7
  39. package/dist/move-path-cleanup.js +4 -4
  40. package/dist/native-binding.d.ts +18 -14
  41. package/dist/native-staged-symlink.d.ts +13 -0
  42. package/dist/native-staged-symlink.js +303 -0
  43. package/dist/owner-dacl-batch-worker.d.ts +1 -0
  44. package/dist/owner-dacl-batch-worker.js +54 -0
  45. package/dist/owner-dacl-batch.d.ts +5 -0
  46. package/dist/owner-dacl-batch.js +64 -0
  47. package/dist/owner-dacl.d.ts +2 -0
  48. package/dist/owner-dacl.js +3 -0
  49. package/dist/path.js +17 -1
  50. package/dist/permission-exec.js +3 -6
  51. package/dist/permissions-public.d.ts +1 -0
  52. package/dist/permissions-public.js +1 -0
  53. package/dist/pinned-mutation-admission.d.ts +0 -1
  54. package/dist/pinned-mutation-shared-route.d.ts +2 -8
  55. package/dist/pinned-open.d.ts +0 -1
  56. package/dist/pinned-open.js +1 -2
  57. package/dist/publish-copy-stage.js +4 -0
  58. package/dist/read-opened-file.d.ts +2 -5
  59. package/dist/regular-file.js +3 -3
  60. package/dist/replace-file-copy-fallback.d.ts +1 -2
  61. package/dist/root-context.js +3 -2
  62. package/dist/root-impl.js +0 -3
  63. package/dist/root-move-noreplace.d.ts +2 -7
  64. package/dist/root-observed-path.d.ts +0 -1
  65. package/dist/root-observed-path.js +0 -3
  66. package/dist/root-path-observation.d.ts +4 -11
  67. package/dist/root-path.js +7 -10
  68. package/dist/root-paths.d.ts +2 -6
  69. package/dist/root-remove-identity.d.ts +1 -3
  70. package/dist/root-walk.js +8 -3
  71. package/dist/root-write-admission.js +1 -6
  72. package/dist/root-write-complete-parent.d.ts +2 -0
  73. package/dist/root-write-complete-parent.js +1 -1
  74. package/dist/safe-path-segment.d.ts +1 -0
  75. package/dist/safe-path-segment.js +8 -2
  76. package/dist/secret-file.d.ts +6 -2
  77. package/dist/secret-file.js +1 -0
  78. package/dist/secure-file-windows.js +1 -5
  79. package/dist/secure-file.js +3 -2
  80. package/dist/sidecar-lock-admission-parser.d.ts +1 -2
  81. package/dist/sidecar-lock-handle.d.ts +2 -8
  82. package/dist/sidecar-lock-policy.d.ts +2 -7
  83. package/dist/sidecar-lock-stale-admission.d.ts +1 -5
  84. package/dist/sidecar-lock.js +5 -3
  85. package/dist/staged-symlink-types.d.ts +49 -0
  86. package/dist/staged-symlink-types.js +1 -0
  87. package/dist/symlink-parents.js +58 -7
  88. package/dist/temp-target.js +4 -2
  89. package/dist/temp-workspace-owner.js +4 -9
  90. package/dist/test-hooks.d.ts +1 -1
  91. package/dist/text-atomic.d.ts +2 -1
  92. package/dist/text-atomic.js +2 -0
  93. package/dist/trash.js +27 -1
  94. package/dist/walk.d.ts +2 -5
  95. package/dist/windows-owner.d.ts +0 -1
  96. package/dist/windows-owner.js +0 -1
  97. package/dist/windows-security-bridge.cs +6 -4
  98. package/dist/windows-security-bridge.ps1 +78 -3
  99. package/dist/windows-security-command.d.ts +8 -0
  100. package/dist/windows-security-command.js +66 -15
  101. package/dist/windows-security-facts.d.ts +4 -0
  102. package/dist/windows-security-facts.js +6 -2
  103. package/docs/advanced.md +3 -2
  104. package/docs/archive.md +10 -0
  105. package/docs/atomic.md +11 -3
  106. package/docs/contributing.md +30 -0
  107. package/docs/copy.md +2 -0
  108. package/docs/file-store.md +5 -0
  109. package/docs/guest.md +7 -1
  110. package/docs/install.md +28 -0
  111. package/docs/native-helper.md +11 -4
  112. package/docs/native.md +45 -1
  113. package/docs/permissions.md +66 -0
  114. package/docs/public-api.md +7 -1
  115. package/docs/root.md +10 -1
  116. package/docs/secret-file.md +10 -0
  117. package/docs/security-model.md +4 -1
  118. package/docs/sidecar-lock.md +2 -0
  119. package/docs/staged-symlink.md +123 -0
  120. package/docs/store.md +3 -1
  121. package/docs/testing.md +28 -0
  122. package/docs/walk.md +12 -0
  123. package/docs/writing.md +21 -0
  124. package/package.json +9 -9
package/CHANGELOG.md CHANGED
@@ -2,6 +2,53 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.20.0 - 2026-09-25
6
+
7
+ ### Highlights
8
+
9
+ - **Older Linux systems:** when `openat2` is absent (Linux before 5.6) or blocked by seccomp, native operations fall back to a guarded no-follow walk instead of failing with `ENOSYS`. Nested no-clobber moves keep working, and GNU x64/arm64 bindings now target glibc 2.28, so native operations load on RHEL 8-family systems. ([#686](https://github.com/openclaw/fs-safe/pull/686), [#685](https://github.com/openclaw/fs-safe/pull/685); fixes [#572](https://github.com/openclaw/fs-safe/issues/572), [#511](https://github.com/openclaw/fs-safe/issues/511), [#548](https://github.com/openclaw/fs-safe/issues/548))
10
+ - **Path hardening:** trash moves stay inside allowed roots, symlink-parent checks inspect raw segments before dotdot normalization, relative-escape checks recognize either Windows separator and nested escapes, and safe path segments reject Windows reserved device names. Thanks @SebTardif. ([#611](https://github.com/openclaw/fs-safe/pull/611), [#615](https://github.com/openclaw/fs-safe/pull/615), [#614](https://github.com/openclaw/fs-safe/pull/614), [#612](https://github.com/openclaw/fs-safe/pull/612))
11
+ - **ZIP admission per operation:** portable ZIP reads and extraction bind entry selection to the admitted archive, name, and decoder object, and verify payloads against the admitted CRC and size. Substituted, renamed, or replaced decoder entries are rejected. ([#660](https://github.com/openclaw/fs-safe/pull/660))
12
+
13
+ ### Features
14
+
15
+ - **Retained symlink publication:** `retainSymlinkInDirectory()` on the advanced surface holds an explicitly identified POSIX symlink through exact-slot no-replace publication and explicit recovery, preserving observed foreign replacements and uncertain outcomes. ([#682](https://github.com/openclaw/fs-safe/pull/682))
16
+ - **Batched Windows ACL facts:** `readOwnerAndDaclBatch()` inspects ordered paths in one isolated native worker or one PowerShell process, with a configurable whole-batch timeout and bounded output. Results are bounded during collection; oversized batches reject with `too-large` before later paths are queried. ([#669](https://github.com/openclaw/fs-safe/pull/669), [#680](https://github.com/openclaw/fs-safe/pull/680))
17
+ - **Atomic text options:** `writeTextAtomic()` forwards the existing `beforeRename` hook and `tempPrefix` option to atomic replacement, keeping its validation and identity checks. ([#580](https://github.com/openclaw/fs-safe/pull/580))
18
+
19
+ ### Fixes
20
+
21
+ - **Lock exit cleanup:** leave raw sidecars in place when Windows reports an unknown device or inode, while keeping token-owned cleanup working when the descriptor and path identities legitimately differ (for example on VirtioFS). Thanks @SebTardif. ([#617](https://github.com/openclaw/fs-safe/pull/617))
22
+ - **Retained symlink errors:** preserve uncertain publication outcomes and cached cleanup/recovery failures when inspecting thrown error metadata fails, retaining the original cause without retrying mutations, callbacks, or descriptor closes. ([#684](https://github.com/openclaw/fs-safe/pull/684))
23
+ - **Create collision cleanup:** atomic and streamed creates remove their private stage when the JavaScript fallback observes a competing destination before publication; failures after a link attempt retain their existing recovery evidence. ([#683](https://github.com/openclaw/fs-safe/pull/683))
24
+
25
+ ### Compatibility
26
+
27
+ - Safe path segments now reject Windows reserved device names (`CON`, `NUL`, `COM1`, `CON.json`, …) on every platform, because segments are portable identifiers. Temporary filename sanitization still suffixes them (`CON.txt` becomes `CON_.txt`). ([#612](https://github.com/openclaw/fs-safe/pull/612))
28
+ - `assertNoSymlinkParents()` and guarded appends with `rejectSymlinkParents` reject raw spellings whose dotdot segments would cancel a symlink or re-enter the root after leaving it. ([#615](https://github.com/openclaw/fs-safe/pull/615))
29
+ - Without `openat2`, beneath opens report `best-effort` containment instead of `kernel-atomic`, anonymous `O_TMPFILE` opens report `ENOTSUP`, and bounded tree cleanup fails closed with `helper-unavailable` because it requires `RESOLVE_NO_XDEV`. ([#686](https://github.com/openclaw/fs-safe/pull/686))
30
+ - The Linux GNU binding floor is glibc 2.28 for both x64 and arm64 (arm64 was previously built against 2.17), matching Node 22's own Linux baseline. ([#685](https://github.com/openclaw/fs-safe/pull/685))
31
+
32
+ ## 0.19.0 - 2026-09-24
33
+
34
+ ### Highlights
35
+
36
+ - **Strict secret-file durability:** `createSecretFileAtomic()` accepts `durable: "file"` to require every file flush to succeed, including on `EPERM`; parent-directory synchronization remains best effort. ([#644](https://github.com/openclaw/fs-safe/pull/644))
37
+ - **Literal `~` names:** keep FileStore keys, absolute Root reads, discovered walk entries, and ZIP/TAR entries literal instead of treating them as home-directory shorthand. Reads, writes, removal, pruning, extraction, and durable publication select the intended entry. ([#633](https://github.com/openclaw/fs-safe/pull/633))
38
+ - **Native Unix descriptor safety:** reject negative descriptors in low-level root, query, hash, copy, clone, staging, and cleanup calls without Rust panics or working-directory operations. Modern macOS beneath opens report `EBADF` for negative roots instead of `EIO`. ([#640](https://github.com/openclaw/fs-safe/pull/640), [#643](https://github.com/openclaw/fs-safe/pull/643), [#646](https://github.com/openclaw/fs-safe/pull/646))
39
+
40
+ ### Fixes
41
+
42
+ - **Guest directory permissions:** cross-device moves preserve mode `000`, including nested directories, without changing other modes' umask behavior. Restore top-level permissions through the retained directory descriptor and preserve published entries on failure. Thanks @SebTardif. ([#616](https://github.com/openclaw/fs-safe/pull/616))
43
+ - **ReFS clone cleanup:** remove ordinary partial output when Windows rejects the ignore-readonly deletion flag. Keep readonly attributes intact and include cleanup failures in the original clone error; readonly files or other processes' open handles can still leave output behind. ([#648](https://github.com/openclaw/fs-safe/pull/648))
44
+
45
+ ### Compatibility and documentation
46
+
47
+ - Secret creation's default and boolean durability options retain their behavior, and `writeSecretFileAtomic()` remains boolean-only. Strict file synchronization preserves existing publication and cleanup semantics: a failure after publication can leave a complete file present. ([#644](https://github.com/openclaw/fs-safe/pull/644))
48
+ - `Root.walk("~")` and `Root.walk("~/dir")` expand home shorthand when iteration starts and return admitted canonical paths relative to the Root. Use `./~/dir` for a literal tilde directory, and prefix returned entry paths with `./` when passing them to another Root method. Relative Root `~/name` inputs still expand home; FileStore keys do not. ([#633](https://github.com/openclaw/fs-safe/pull/633))
49
+ - Guest moves still require OS permission to read mode-000 source directories; they do not widen source permissions. If permission restoration fails after publication, the source and published copy remain for caller reconciliation. ([#616](https://github.com/openclaw/fs-safe/pull/616))
50
+ - Clarify that `Root.append()` and `openWritable()` creation modes remain subject to the process umask and do not chmod existing files. This documents existing behavior. ([#636](https://github.com/openclaw/fs-safe/pull/636))
51
+
5
52
  ## 0.18.2 - 2026-09-22
6
53
 
7
54
  ### Fixes
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.
12
12
 
13
- Think Go's `os.Root` / `OpenInRoot` or Rust's [`cap-std`](https://github.com/bytecodealliance/cap-std), but for Node. Hand `root()` a trusted directory and you get back a handle whose every method resolves relative paths against it and defends against `..`, symlink swaps, hardlink aliases, and TOCTOU rename races. The exact containment strength is reported per mechanism: Linux native opens are kernel-atomic; macOS, Windows, and JavaScript paths are best-effort.
13
+ Think Go's `os.Root` / `OpenInRoot` or Rust's [`cap-std`](https://github.com/bytecodealliance/cap-std), but for Node. Hand `root()` a trusted directory and you get back a handle whose every method resolves relative paths against it and defends against `..`, symlink swaps, hardlink aliases, and TOCTOU rename races. The exact containment strength is reported per mechanism: Linux `openat2` opens are kernel-atomic; guarded Linux fallback, macOS, Windows, and JavaScript paths are best-effort.
14
14
 
15
15
  ```ts
16
16
  import { root } from "@openclaw/fs-safe";
@@ -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/gzip/zstd/bzip2)** |
48
+ | **`@openclaw/fs-safe`** | **✓** | **✓** | **✓** | **Linux openat2 atomic; others best-effort** | **✓ (ZIP/TAR/gzip/zstd/bzip2)** |
49
49
 
50
50
  ## Not a sandbox
51
51
 
@@ -84,7 +84,9 @@ reports the escape. Use `require` when hostile concurrent mutation is in scope.
84
84
 
85
85
  Equivalent env var: `FS_SAFE_NATIVE_MODE=auto|off|require`. The seven bindings
86
86
  ship as exact-version optional packages filtered by OS, CPU, and Linux libc, so
87
- a normal install receives only its matching binary. There are no postinstall
87
+ a normal install receives only its matching binary. Linux GNU x64/arm64 bindings
88
+ support [glibc 2.28 or newer](docs/install.md#supported-native-platforms), including
89
+ RHEL 8-family systems. There are no postinstall
88
90
  steps, runtime downloads, or consumer Rust builds. On a platform without a
89
91
  published binding, or when optional dependencies are omitted, `auto` silently retains lexical and canonical root
90
92
  checks, no-follow opens, guarded temp+rename writes, and post-write identity
@@ -94,8 +96,11 @@ tradeoff, and [native architecture](docs/native.md) for the platform mechanisms
94
96
  and policy ownership model.
95
97
 
96
98
  Open results report the mechanism's containment class as `"kernel-atomic"` or
97
- `"best-effort"`. Linux native `openBeneath()` is kernel-atomic; macOS, Windows,
98
- and guarded JavaScript results are best-effort. See the [security model](docs/security-model.md#containment-guarantees-by-platform) before using that fact in higher-level policy.
99
+ `"best-effort"`. Linux native `openBeneath()` is kernel-atomic when `openat2`
100
+ is available. Older kernels and syscall-filtered containers use a guarded
101
+ descriptor-relative walk reporting best-effort; nested no-clobber moves keep
102
+ atomic `renameat2(RENAME_NOREPLACE)`. macOS, Windows, and guarded JavaScript
103
+ results are best-effort. See [Linux compatibility](docs/native.md#linux-without-openat2). See the [security model](docs/security-model.md#containment-guarantees-by-platform) before using that fact in higher-level policy.
99
104
 
100
105
  ## Migrating from the Python helper
101
106
 
@@ -342,6 +347,11 @@ original directory on Linux/macOS and requires native support for this operation
342
347
  It offers atomic replace/no-replace publication, not expected-inode replacement
343
348
  or a crash-durability promise; application checks and coordination remain yours.
344
349
 
350
+ For an already staged POSIX symlink, [`retainSymlinkInDirectory()`](docs/staged-symlink.md)
351
+ admits caller-captured identity and retains that exact inode through no-replace
352
+ publication or explicit recovery. Same-target foreign replacements are not adopted.
353
+ Staging, cooperative locking and crash recovery remain application responsibilities.
354
+
345
355
  `replaceFileAtomic()` writes a sibling temp file, applies its exact mode through the still-open descriptor, optionally fsyncs it, and renames it over the destination. It never follows the published destination path to set file permissions. Mode preservation inherits only rwx bits from an existing non-symlink regular file; special bits, ownership, ACLs, and extended attributes are not copied. Pinned-destination hardlink rejection, rename retry / copy fallback on `EPERM`, bounded original-content restoration after a torn fallback, parent-directory fsync, and a `beforeRename` hook for backup or observer flows are all opt-in. `movePathWithCopyFallback()` stages cross-device moves before commit and removes only the copied source entries, so concurrent source additions or replacements are preserved. Its optional synchronous `assertBeforeMutation` hook rechecks caller authority before renames and each source removal; `onDestinationPublished` reports an exact bigint destination identity before later checks or cleanup can fail. See [mutation authority and publication receipts](docs/atomic.md#mutation-authority-and-publication-receipts).
346
356
 
347
357
  ```ts
@@ -8,6 +8,8 @@ export { resolvePathPrefixSync, type ResolvedPathPrefix } from "./path-prefix.js
8
8
  export { probePathSuffixAliasesSync, type ProbePathSuffixAliasesOptions } from "./path-suffix-aliases.js";
9
9
  export { readDirectoryIdentity, assertDirectoryIdentitySync, type DirectoryIdentity, } from "./directory-guard.js";
10
10
  export { stageFileInDirectory, } from "./native-staged-file.js";
11
+ export { retainSymlinkInDirectory } from "./native-staged-symlink.js";
12
+ export type { StagedSymlink, StagedSymlinkExpected, StagedSymlinkReceipt, PublishedSymlinkReceipt, StagedSymlinkPublication, StagedSymlinkRemoval, StagedSymlinkCleanupReceipt, StagedSymlinkFailureDetails, } from "./staged-symlink-types.js";
11
13
  export type { StagedFile, StagedFileReceipt, PublishedFileReceipt, StagedFilePublication, StagedFileCleanupReceipt, StagedFileFailureDetails, } from "./staged-file-types.js";
12
14
  export { readFileDescriptorBounded, readFileDescriptorBoundedSync, readFileHandleBounded, } from "./bounded-read.js";
13
15
  export { readFileWindowFully, readFileWindowFullySync, type ReadFileWindowOptions, } from "./positional-read.js";
package/dist/advanced.js CHANGED
@@ -11,6 +11,7 @@ export { resolvePathPrefixSync } from "./path-prefix.js";
11
11
  export { probePathSuffixAliasesSync } from "./path-suffix-aliases.js";
12
12
  export { readDirectoryIdentity, assertDirectoryIdentitySync, } from "./directory-guard.js";
13
13
  export { stageFileInDirectory, } from "./native-staged-file.js";
14
+ export { retainSymlinkInDirectory } from "./native-staged-symlink.js";
14
15
  export { readFileDescriptorBounded, readFileDescriptorBoundedSync, readFileHandleBounded, } from "./bounded-read.js";
15
16
  export { readFileWindowFully, readFileWindowFullySync, } from "./positional-read.js";
16
17
  export { writeFileWindowFully } from "./write-file-handle.js";
@@ -78,7 +78,7 @@ export async function finalizeArchivePublication(params) {
78
78
  const env_1 = { stack: [], error: void 0, hasError: false };
79
79
  try {
80
80
  await assertGuards(file.guards);
81
- const opened = __addDisposableResource(env_1, await params.targetRoot.open(file.relativePath, { hardlinks: "reject", symlinks: "reject" })
81
+ const opened = __addDisposableResource(env_1, await params.targetRoot.open(`./${file.relativePath}`, { hardlinks: "reject", symlinks: "reject" })
82
82
  .catch((error) => {
83
83
  if (error instanceof FsSafeError && (error.code === "hardlink" || error.code === "path-alias")) {
84
84
  throw createArchiveSymlinkTraversalError(file.relativePath);
@@ -250,7 +250,7 @@ async function mergeTree(params, publication, durable = true, entryUmask = 0) {
250
250
  }
251
251
  };
252
252
  }
253
- await targetRoot.copyIn(relPath, sourcePath, options);
253
+ await targetRoot.copyIn(`./${relPath}`, sourcePath, options);
254
254
  check();
255
255
  await assertGuards();
256
256
  await assertResolvedInsideDestination({
@@ -54,14 +54,9 @@ export type TarEntryInfo = {
54
54
  mode?: number;
55
55
  };
56
56
  export declare function createTarEntryPlanner(params: ArchivePlanOptions): (entry: TarEntryInfo) => ArchivePlanEntry | null;
57
- export declare function createTarEntryPreflightChecker(params: {
57
+ export declare function createTarEntryPreflightChecker(params: Omit<ArchivePlanOptions & {
58
58
  rootDir: string;
59
- stripComponents?: number;
60
- limits?: ArchiveExtractLimits;
61
- escapeLabel?: string;
62
- entryFilter?: ArchiveEntryFilter;
63
- onFiltered?: ArchiveFilteredEntryPolicy;
64
- }): (entry: TarEntryInfo) => boolean;
59
+ }, "entryModes">): (entry: TarEntryInfo) => boolean;
65
60
  export type ArchiveLogger = {
66
61
  info?: (message: string) => void;
67
62
  warn?: (message: string) => void;
@@ -11,9 +11,8 @@ 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";
14
- import { loadAdmittedZipArchive } from "./archive-zip-loader.js";
14
+ import { loadAdmittedZipArchive, assertZipEntryBinding } from "./archive-zip-loader.js";
15
15
  import { createZipIntegrityTransform, normalizeZipIntegrityError, } from "./archive-zip-integrity.js";
16
- import { isZipSymlinkEntry, zipEntryKind } from "./archive-zip-entry.js";
17
16
  import { FsSafeError } from "./errors.js";
18
17
  import { inspectFileIdentity } from "./strict-file-identity.js";
19
18
  import { resolveReadOpenFlags } from "./read-open-flags.js";
@@ -93,27 +92,19 @@ async function readArchiveInput(archivePath) {
93
92
  }
94
93
  }
95
94
  async function readZipEntry(buffer, entryPath, maxBytes, admitted) {
96
- const archive = await loadAdmittedZipArchive(buffer, admitted);
97
- let entry;
98
- // JSZip keys retain some aliases and may use Unicode Path metadata. Scan the
99
- // effective entries once, after raw ZIP admission has rejected collisions.
100
- for (const candidate of Object.values(archive.files)) {
101
- if (canonicalEntryPath(candidate.name) !== entryPath)
102
- continue;
103
- if (entry) {
104
- throw new ArchiveSecurityError("entry-path", `archive contains duplicate entry path: ${formatErrorDetail(entryPath)}`);
105
- }
106
- entry = candidate;
107
- }
108
- if (!entry || entry.dir) {
95
+ const { archive, entries } = await loadAdmittedZipArchive(buffer, admitted);
96
+ const record = entries.get(entryPath);
97
+ if (!record || record.entry.dir) {
109
98
  throw new Error(`archive entry not found: ${formatErrorDetail(entryPath)}`);
110
99
  }
111
- if (isZipSymlinkEntry(entry)) {
100
+ assertZipEntryBinding(archive, record, entryPath);
101
+ const { entry, kind } = record;
102
+ if (kind === "symlink") {
112
103
  throw new Error(`archive entry is a link: ${formatErrorDetail(entryPath)}`);
113
104
  }
114
- if (zipEntryKind(entry) !== "file")
105
+ if (kind !== "file")
115
106
  throw new Error(`archive entry is not a file: ${formatErrorDetail(entryPath)}`);
116
- const integrity = createZipIntegrityTransform(entry);
107
+ const integrity = createZipIntegrityTransform(record);
117
108
  const stream = typeof entry.nodeStream === "function"
118
109
  ? entry.nodeStream()
119
110
  : Readable.from(await entry.async("nodebuffer"));
@@ -133,7 +133,9 @@ export async function assertResolvedInsideDestination(params) {
133
133
  }
134
134
  async function mkdirArchiveOutput(params) {
135
135
  try {
136
- await params.targetRoot.mkdir(params.relativePath);
136
+ const relativePath = params.relativePath;
137
+ // Archive names are literal; retain Root.mkdir's admission without home expansion.
138
+ await params.targetRoot.mkdir(relativePath === "~" || relativePath.startsWith("~/") ? `./${relativePath}` : relativePath);
137
139
  }
138
140
  catch (error) {
139
141
  if (error instanceof FsSafeError) {
@@ -1,4 +1,4 @@
1
- import { type ArchiveEntryModePolicy, type ArchiveEntryKind } from "./archive-plan.js";
1
+ import type { ArchiveEntryKind } from "./archive-plan.js";
2
2
  import type { ZipDirectoryEntry } from "./archive-zip-directory.js";
3
3
  export type ZipEntry = {
4
4
  name: string;
@@ -12,13 +12,13 @@ export type ZipEntry = {
12
12
  nodeStream?: () => NodeJS.ReadableStream;
13
13
  async: (type: "nodebuffer") => Promise<Buffer>;
14
14
  };
15
- /** Internal: register only after the complete decoder/admission association. */
16
- export declare function registerAdmittedZipEntry(entry: ZipEntry, physical: ZipDirectoryEntry): void;
17
- export declare function zipEntryIntegrityMetadata(entry: ZipEntry): {
18
- crc32?: number;
19
- uncompressedSize?: number;
20
- } | undefined;
21
- export declare function isZipSymlinkEntry(entry: ZipEntry): boolean;
22
- export declare function zipEntryKind(entry: ZipEntry): ArchiveEntryKind;
23
- export declare function zipEntryMode(entry: ZipEntry, policy: ArchiveEntryModePolicy | undefined): number;
24
- export declare function zipEntryDeclaredSize(entry: ZipEntry): number;
15
+ export type AdmittedZipEntry = Readonly<{
16
+ entry: ZipEntry;
17
+ name: string;
18
+ kind: ArchiveEntryKind;
19
+ mode: number | undefined;
20
+ size: number;
21
+ crc32: number;
22
+ }>;
23
+ /** Internal: create only after the complete decoder/admission association. */
24
+ export declare function createAdmittedZipEntry(entry: ZipEntry, name: string, physical: ZipDirectoryEntry): AdmittedZipEntry;
@@ -1,42 +1,10 @@
1
- import { resolveArchiveEntryMode, } from "./archive-plan.js";
2
- const admittedMetadata = new WeakMap();
3
- /** Internal: register only after the complete decoder/admission association. */
4
- export function registerAdmittedZipEntry(entry, physical) {
1
+ /** Internal: create only after the complete decoder/admission association. */
2
+ export function createAdmittedZipEntry(entry, name, physical) {
5
3
  const mode = physical.creatorSystem === 3 ? physical.externalAttributes >>> 16 : undefined;
6
- admittedMetadata.set(entry, { kind: physical.kind, mode, size: physical.size });
7
4
  entry.dir = physical.kind === "directory";
8
5
  // Previously unsupported non-UNIX symlinks must also be recognizable to
9
6
  // public preflight consumers using JSZip's conventional type inspection.
10
7
  entry.unixPermissions = mode ?? (physical.kind === "symlink" ? 0o120000 : null);
11
8
  entry.dosPermissions = physical.creatorSystem === 0 ? physical.externalAttributes & 0x3f : null;
12
- }
13
- export function zipEntryIntegrityMetadata(entry) {
14
- const data = entry._data;
15
- if (!data || "then" in data)
16
- return undefined;
17
- return data;
18
- }
19
- const ZIP_UNIX_FILE_TYPE_MASK = 0o170000;
20
- const ZIP_UNIX_SYMLINK_TYPE = 0o120000;
21
- export function isZipSymlinkEntry(entry) {
22
- return zipEntryKind(entry) === "symlink";
23
- }
24
- export function zipEntryKind(entry) {
25
- const metadata = admittedMetadata.get(entry);
26
- if (metadata)
27
- return metadata.kind;
28
- return typeof entry.unixPermissions === "number" &&
29
- (entry.unixPermissions & ZIP_UNIX_FILE_TYPE_MASK) === ZIP_UNIX_SYMLINK_TYPE
30
- ? "symlink" : entry.dir ? "directory" : "file";
31
- }
32
- export function zipEntryMode(entry, policy) {
33
- const metadata = admittedMetadata.get(entry);
34
- return resolveArchiveEntryMode({
35
- kind: entry.dir ? "directory" : "file",
36
- archivedMode: metadata ? metadata.mode : entry.unixPermissions,
37
- policy,
38
- });
39
- }
40
- export function zipEntryDeclaredSize(entry) {
41
- return admittedMetadata.get(entry)?.size ?? Math.max(0, Math.floor(zipEntryIntegrityMetadata(entry)?.uncompressedSize ?? 0));
9
+ return { entry, name, kind: physical.kind, mode, size: physical.size, crc32: physical.crc32 };
42
10
  }
@@ -1,4 +1,4 @@
1
1
  import { Transform } from "node:stream";
2
- import { type ZipEntry } from "./archive-zip-entry.js";
2
+ import type { AdmittedZipEntry } from "./archive-zip-entry.js";
3
3
  export declare function normalizeZipIntegrityError(error: unknown): Error;
4
- export declare function createZipIntegrityTransform(entry: ZipEntry): Transform;
4
+ export declare function createZipIntegrityTransform(record: AdmittedZipEntry): Transform;
@@ -1,6 +1,5 @@
1
1
  import { Transform } from "node:stream";
2
2
  import { ArchiveFormatError } from "./archive-errors.js";
3
- import { zipEntryIntegrityMetadata, } from "./archive-zip-entry.js";
4
3
  import { updateCrc32 } from "./archive-crc32.js";
5
4
  export function normalizeZipIntegrityError(error) {
6
5
  if (error instanceof Error &&
@@ -9,17 +8,8 @@ export function normalizeZipIntegrityError(error) {
9
8
  }
10
9
  return error instanceof Error ? error : new Error(String(error));
11
10
  }
12
- export function createZipIntegrityTransform(entry) {
13
- const metadata = zipEntryIntegrityMetadata(entry);
14
- const expectedCrc32 = metadata?.crc32;
15
- const expectedSize = metadata?.uncompressedSize;
16
- if (typeof expectedCrc32 !== "number" ||
17
- !Number.isInteger(expectedCrc32) ||
18
- typeof expectedSize !== "number" ||
19
- !Number.isSafeInteger(expectedSize) ||
20
- expectedSize < 0) {
21
- throw new ArchiveFormatError(`zip entry has invalid integrity metadata: ${entry.name}`);
22
- }
11
+ export function createZipIntegrityTransform(record) {
12
+ const { entry, crc32: expectedCrc32, size: expectedSize } = record;
23
13
  let actualCrc32 = 0;
24
14
  let actualSize = 0;
25
15
  return new Transform({
@@ -1,8 +1,12 @@
1
1
  import type { ZipDirectoryEntry } from "./archive-zip-directory.js";
2
- import { type ZipEntry } from "./archive-zip-entry.js";
2
+ import { type AdmittedZipEntry } from "./archive-zip-entry.js";
3
3
  export type ZipArchiveWithFiles = {
4
4
  files: Record<string, unknown>;
5
5
  };
6
- export declare function admittedZipEntries(archive: ZipArchiveWithFiles): readonly ZipEntry[];
6
+ export type ZipArchiveAdmission = {
7
+ archive: ZipArchiveWithFiles;
8
+ entries: ReadonlyMap<string, AdmittedZipEntry>;
9
+ };
10
+ export declare function assertZipEntryBinding(archive: ZipArchiveWithFiles, record: AdmittedZipEntry, path: string): void;
7
11
  /** Internal: the caller has admitted these unchanged bytes and their metadata. */
8
- export declare function loadAdmittedZipArchive(buffer: Buffer | Uint8Array, admitted: ZipDirectoryEntry[]): Promise<ZipArchiveWithFiles>;
12
+ export declare function loadAdmittedZipArchive(buffer: Buffer | Uint8Array, admitted: ZipDirectoryEntry[]): Promise<ZipArchiveAdmission>;
@@ -1,10 +1,13 @@
1
1
  import { ArchiveFormatError, ArchiveSecurityError } from "./archive-errors.js";
2
2
  import { validateArchiveEntryPath } from "./archive-entry.js";
3
- import { registerAdmittedZipEntry } from "./archive-zip-entry.js";
3
+ import { createAdmittedZipEntry } from "./archive-zip-entry.js";
4
4
  import { zipPathKey } from "./archive-zip-names.js";
5
- const orderedEntries = new WeakMap();
6
- export function admittedZipEntries(archive) {
7
- return orderedEntries.get(archive) ?? disagreement();
5
+ export function assertZipEntryBinding(archive, record, path) {
6
+ const { entry } = record;
7
+ const name = entry.name;
8
+ validateArchiveEntryPath(name, { escapeLabel: "archive root" });
9
+ if (archive.files[record.name] !== entry || zipPathKey(name) !== path)
10
+ disagreement();
8
11
  }
9
12
  function disagreement() {
10
13
  throw new ArchiveFormatError("ZIP decoder disagrees with admitted directory metadata");
@@ -110,7 +113,7 @@ export async function loadAdmittedZipArchive(buffer, admitted) {
110
113
  if (remaining.size || inserted.size)
111
114
  disagreement();
112
115
  const files = Object.create(null);
113
- const entries = [];
116
+ const entries = new Map();
114
117
  for (const [name, entry, physical] of normalized) {
115
118
  const appendSlash = physical.kind === "directory" && !name.endsWith("/");
116
119
  const normalizedName = appendSlash ? `${name}/` : name;
@@ -120,12 +123,10 @@ export async function loadAdmittedZipArchive(buffer, admitted) {
120
123
  entry.name = `${entry.name}/`;
121
124
  }
122
125
  files[normalizedName] = entry;
123
- registerAdmittedZipEntry(entry, physical);
124
- entries.push(entry);
126
+ entries.set(physical.portableKey, createAdmittedZipEntry(entry, normalizedName, physical));
125
127
  }
126
128
  archive.files = files;
127
- orderedEntries.set(archive, entries);
128
- return archive;
129
+ return { archive, entries };
129
130
  }
130
131
  async function importOptionalJsZip() {
131
132
  try {
@@ -1,3 +1,4 @@
1
1
  import { type ArchiveExtractLimits } from "./archive-limits.js";
2
- import { type ZipArchiveWithFiles } from "./archive-zip-loader.js";
2
+ import { type ZipArchiveAdmission, type ZipArchiveWithFiles } from "./archive-zip-loader.js";
3
+ export declare function loadZipArchiveWithAdmission(buffer: Buffer | Uint8Array, limits?: ArchiveExtractLimits): Promise<ZipArchiveAdmission>;
3
4
  export declare function loadZipArchiveWithPreflight(buffer: Buffer | Uint8Array, limits?: ArchiveExtractLimits): Promise<ZipArchiveWithFiles>;
@@ -1,12 +1,21 @@
1
1
  import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, resolveExtractLimits, } from "./archive-limits.js";
2
2
  import { admitZipBuffer } from "./archive-zip-admission.js";
3
3
  import { loadAdmittedZipArchive } from "./archive-zip-loader.js";
4
- export async function loadZipArchiveWithPreflight(buffer, limits) {
5
- const resolvedLimits = resolveExtractLimits(limits);
6
- if (buffer.byteLength > resolvedLimits.maxArchiveBytes) {
7
- throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ARCHIVE_SIZE_EXCEEDS_LIMIT);
4
+ export function loadZipArchiveWithAdmission(buffer, limits) {
5
+ try {
6
+ const resolvedLimits = resolveExtractLimits(limits);
7
+ if (buffer.byteLength > resolvedLimits.maxArchiveBytes) {
8
+ throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ARCHIVE_SIZE_EXCEEDS_LIMIT);
9
+ }
10
+ const entries = [];
11
+ admitZipBuffer(buffer, resolvedLimits, entry => { entries.push(entry); });
12
+ return loadAdmittedZipArchive(buffer, entries);
13
+ }
14
+ catch (error) {
15
+ // Keep validation failures in the promise handed to the extraction deadline.
16
+ return Promise.reject(error);
8
17
  }
9
- const entries = [];
10
- admitZipBuffer(buffer, resolvedLimits, entry => { entries.push(entry); });
11
- return await loadAdmittedZipArchive(buffer, entries);
18
+ }
19
+ export async function loadZipArchiveWithPreflight(buffer, limits) {
20
+ return (await loadZipArchiveWithAdmission(buffer, limits)).archive;
12
21
  }
package/dist/archive.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createTarEntryPlanner, createArchiveEntrySelector, resolveArchiveFilteredEntryPolicy, createArchiveEntryPlanner, } from "./archive-plan.js";
1
+ import { createTarEntryPlanner, createArchiveEntrySelector, resolveArchiveFilteredEntryPolicy, resolveArchiveEntryMode, createArchiveEntryPlanner, } from "./archive-plan.js";
2
2
  import { inspectTar, replayTar } from "./archive-tar-stream.js";
3
3
  import { runPinnedWriteHelper } from "./pinned-write.js";
4
4
  import { constants as fsConstants } from "node:fs";
@@ -11,9 +11,7 @@ import { assertArchiveEntryCountWithinLimit, createByteBudgetTracker, createExtr
11
11
  import { resolveArchiveKind } from "./archive-kind.js";
12
12
  import { prepareArchiveDestinationGuard, preparePrivateArchiveOutputPath, } from "./archive-staging.js";
13
13
  import { withStagedArchivePublication } from "./archive-merge.js";
14
- import { loadZipArchiveWithPreflight } from "./archive-zip-preflight.js";
15
- import { admittedZipEntries } from "./archive-zip-loader.js";
16
- import { zipEntryKind, zipEntryDeclaredSize, zipEntryMode, } from "./archive-zip-entry.js";
14
+ import { loadZipArchiveWithAdmission } from "./archive-zip-preflight.js";
17
15
  import { createZipIntegrityTransform, normalizeZipIntegrityError, } from "./archive-zip-integrity.js";
18
16
  import { ArchiveSecurityError, ArchiveFormatError, isArchiveFormatErrorMessage, } from "./archive-errors.js";
19
17
  import { stageArchiveFileForExtraction } from "./archive-input.js";
@@ -51,7 +49,7 @@ async function readZipEntryStream(entry) {
51
49
  async function writeZipFileEntry(params) {
52
50
  params.deadline.check();
53
51
  params.budget.startEntry();
54
- const readable = await readZipEntryStream(params.entry);
52
+ const readable = await readZipEntryStream(params.record.entry);
55
53
  const destinationPath = params.outPath;
56
54
  let tempHandle = null;
57
55
  let handleClosedByStream = false;
@@ -69,7 +67,7 @@ async function writeZipFileEntry(params) {
69
67
  handleClosedByStream = true;
70
68
  });
71
69
  try {
72
- await pipeline(readable, createExtractBudgetTransform({ onChunkBytes: params.budget.addBytes }), createZipIntegrityTransform(params.entry), writable, { signal: params.deadline.signal });
70
+ await pipeline(readable, createExtractBudgetTransform({ onChunkBytes: params.budget.addBytes }), createZipIntegrityTransform(params.record), writable, { signal: params.deadline.signal });
73
71
  }
74
72
  catch (err) {
75
73
  throw normalizeZipIntegrityError(createPipelineTimeoutError(err, params.deadline));
@@ -103,26 +101,29 @@ async function extractZip(params) {
103
101
  deadline.check();
104
102
  const buffer = await fs.readFile(params.archivePath, { signal: deadline.signal });
105
103
  deadline.check();
106
- const zip = await waitForDeadline(loadZipArchiveWithPreflight(buffer, limits), deadline);
104
+ const { entries } = await waitForDeadline(loadZipArchiveWithAdmission(buffer, limits), deadline);
107
105
  deadline.check();
108
- const entries = admittedZipEntries(zip);
109
- assertArchiveEntryCountWithinLimit(entries.length, limits);
106
+ assertArchiveEntryCountWithinLimit(entries.size, limits);
110
107
  const budget = createByteBudgetTracker(limits);
111
108
  await withStagedArchivePublication({ ...params, destinationGuard }, async (stagingDir) => {
112
109
  const { select } = createArchiveEntrySelector({ ...params, rootDir: stagingDir });
113
110
  const acceptedEntries = [];
114
- for (const entry of entries) {
111
+ for (const record of entries.values()) {
115
112
  deadline.check();
116
- const entryKind = zipEntryKind(entry);
117
- const relPath = select({ path: entry.name, kind: entryKind, size: zipEntryDeclaredSize(entry) });
113
+ const { entry, kind: entryKind, size, mode: archivedMode } = record;
114
+ const relPath = select({ path: record.name, kind: entryKind, size });
118
115
  if (relPath === null)
119
116
  continue;
120
117
  if (entryKind === "symlink") {
121
- throw new ArchiveSecurityError("entry-link", `zip entry is a link: ${entry.name}`);
118
+ throw new ArchiveSecurityError("entry-link", `zip entry is a link: ${record.name}`);
122
119
  }
123
120
  if (entryKind === "other")
124
121
  continue;
125
- const mode = zipEntryMode(entry, params.entryModes);
122
+ const mode = resolveArchiveEntryMode({
123
+ kind: entry.dir ? "directory" : "file",
124
+ archivedMode,
125
+ policy: params.entryModes,
126
+ });
126
127
  acceptedEntries.push({ path: relPath, kind: entry.dir ? "directory" : "file", mode });
127
128
  const outPath = path.join(stagingDir, relPath);
128
129
  await preparePrivateArchiveOutputPath({
@@ -130,12 +131,12 @@ async function extractZip(params) {
130
131
  destinationRealDir: stagingDir,
131
132
  relPath,
132
133
  outPath,
133
- originalPath: entry.name,
134
+ originalPath: record.name,
134
135
  isDirectory: entry.dir,
135
136
  deadline,
136
137
  });
137
138
  if (!entry.dir) {
138
- await writeZipFileEntry({ entry, outPath, budget, deadline });
139
+ await writeZipFileEntry({ record, outPath, budget, deadline });
139
140
  }
140
141
  }
141
142
  return acceptedEntries;
@@ -70,11 +70,6 @@ function snapshotDirectoryMetadata(stat, identity) {
70
70
  export function directoryReceiptAuthority(receipt) {
71
71
  return authorities.get(receipt) ?? snapshotDirectoryReceipt(receipt);
72
72
  }
73
- function rememberReceipt(receipt, authority) {
74
- authorities.set(receipt, authority);
75
- identities.set(receipt.identity, authority);
76
- return receipt;
77
- }
78
73
  export function ownDirectoryReceipt(receipt) {
79
74
  return receiptFromAuthority(snapshotDirectoryReceipt(receipt));
80
75
  }
@@ -83,11 +78,14 @@ export function copyRetainedDirectoryReceipt(receipt) {
83
78
  return receiptFromAuthority(authority);
84
79
  }
85
80
  function receiptFromAuthority(authority) {
86
- return rememberReceipt({
81
+ const receipt = {
87
82
  path: authority.path,
88
83
  realPath: authority.realPath,
89
84
  identity: Object.assign(Object.create(fs.Stats.prototype), authority.metadata),
90
- }, authority);
85
+ };
86
+ authorities.set(receipt, authority);
87
+ identities.set(receipt.identity, authority);
88
+ return receipt;
91
89
  }
92
90
  // The caller already admitted the exact observation and its paths.
93
91
  export function createDirectoryReceiptFromIdentity(pathname, realPath, exactStat) {
@@ -1,6 +1,3 @@
1
- function isValidUid(value) {
2
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
3
- }
4
1
  function unavailable(cause) {
5
2
  return new Error("Effective user identity is unavailable.", {
6
3
  ...(cause === undefined ? {} : { cause }),
@@ -18,7 +15,7 @@ export function resolveEffectiveUid() {
18
15
  catch (cause) {
19
16
  throw unavailable(cause);
20
17
  }
21
- if (!isValidUid(uid)) {
18
+ if (!(typeof uid === "number" && Number.isSafeInteger(uid) && uid >= 0)) {
22
19
  throw unavailable();
23
20
  }
24
21
  return uid;
package/dist/errors.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- export type FsSafeErrorCode = "already-exists" | "denied-path" | "device-path" | "hardlink" | "helper-failed" | "helper-unavailable" | "invalid-path" | "insecure-permissions" | "not-empty" | "not-file" | "not-found" | "not-owned" | "not-removable" | "outside-workspace" | "path-alias" | "path-mismatch" | "permission-unverified" | "read-failed" | "secret-exists" | "store-reentrant-update" | "symlink" | "timeout" | "too-large" | "unsupported-platform";
1
+ export type FsSafeErrorCode = (typeof OPERATIONAL_CODE_VALUES)[number] | "already-exists" | "denied-path" | "device-path" | "hardlink" | "invalid-path" | "insecure-permissions" | "not-file" | "not-owned" | "outside-workspace" | "path-alias" | "path-mismatch" | "secret-exists" | "store-reentrant-update" | "symlink" | "too-large";
2
2
  export type FsSafeErrorCategory = "policy" | "operational";
3
3
  export type FsSafeErrorDetails = Readonly<Record<string, unknown>>;
4
+ declare const OPERATIONAL_CODE_VALUES: readonly ["helper-failed", "helper-unavailable", "not-empty", "not-found", "not-removable", "permission-unverified", "read-failed", "timeout", "unsupported-platform"];
4
5
  export declare function categorizeFsSafeError(code: FsSafeErrorCode): FsSafeErrorCategory;
5
6
  export declare class FsSafeError extends Error {
6
7
  readonly code: FsSafeErrorCode;
@@ -11,3 +12,4 @@ export declare class FsSafeError extends Error {
11
12
  details?: FsSafeErrorDetails;
12
13
  });
13
14
  }
15
+ export {};
package/dist/errors.js CHANGED
@@ -1,4 +1,4 @@
1
- const OPERATIONAL_CODES = new Set([
1
+ const OPERATIONAL_CODE_VALUES = [
2
2
  "helper-failed",
3
3
  "helper-unavailable",
4
4
  "not-empty",
@@ -8,7 +8,8 @@ const OPERATIONAL_CODES = new Set([
8
8
  "read-failed",
9
9
  "timeout",
10
10
  "unsupported-platform",
11
- ]);
11
+ ];
12
+ const OPERATIONAL_CODES = new Set(OPERATIONAL_CODE_VALUES);
12
13
  export function categorizeFsSafeError(code) {
13
14
  return OPERATIONAL_CODES.has(code) ? "operational" : "policy";
14
15
  }