@openclaw/fs-safe 0.5.2 → 0.5.4

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 (79) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +3 -2
  3. package/dist/archive-entry.d.ts.map +1 -1
  4. package/dist/archive-entry.js +12 -1
  5. package/dist/archive-errors.d.ts +1 -0
  6. package/dist/archive-errors.d.ts.map +1 -1
  7. package/dist/archive-errors.js +4 -0
  8. package/dist/archive-native.d.ts.map +1 -1
  9. package/dist/archive-native.js +6 -3
  10. package/dist/archive-read.d.ts.map +1 -1
  11. package/dist/archive-read.js +43 -36
  12. package/dist/archive-tar-meta.d.ts.map +1 -1
  13. package/dist/archive-tar-meta.js +8 -0
  14. package/dist/archive-tar-runtime.d.ts +1 -0
  15. package/dist/archive-tar-runtime.d.ts.map +1 -1
  16. package/dist/archive-tar-runtime.js +11 -0
  17. package/dist/archive-tar.d.ts.map +1 -1
  18. package/dist/archive-tar.js +1 -0
  19. package/dist/archive-zip-entry.d.ts +6 -1
  20. package/dist/archive-zip-entry.d.ts.map +1 -1
  21. package/dist/archive-zip-entry.js +11 -1
  22. package/dist/archive-zip-integrity.d.ts +2 -1
  23. package/dist/archive-zip-integrity.d.ts.map +1 -1
  24. package/dist/archive-zip-integrity.js +12 -2
  25. package/dist/archive-zip-preflight.d.ts.map +1 -1
  26. package/dist/archive-zip-preflight.js +8 -2
  27. package/dist/archive.d.ts.map +1 -1
  28. package/dist/archive.js +6 -5
  29. package/dist/errors.d.ts +1 -1
  30. package/dist/errors.d.ts.map +1 -1
  31. package/dist/errors.js +1 -0
  32. package/dist/file-hash.d.ts.map +1 -1
  33. package/dist/file-hash.js +2 -11
  34. package/dist/file-store.d.ts.map +1 -1
  35. package/dist/file-store.js +31 -16
  36. package/dist/move-path.d.ts.map +1 -1
  37. package/dist/move-path.js +2 -10
  38. package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
  39. package/dist/native/darwin-x64/fs-safe-native.node +0 -0
  40. package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
  41. package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
  42. package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
  43. package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
  44. package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
  45. package/dist/pinned-open.d.ts.map +1 -1
  46. package/dist/pinned-open.js +23 -6
  47. package/dist/private-temp-workspace.d.ts.map +1 -1
  48. package/dist/private-temp-workspace.js +45 -4
  49. package/dist/publish-file-failure.d.ts +1 -0
  50. package/dist/publish-file-failure.d.ts.map +1 -1
  51. package/dist/publish-file-failure.js +2 -1
  52. package/dist/publish-file.d.ts.map +1 -1
  53. package/dist/publish-file.js +49 -36
  54. package/dist/read-error.d.ts +2 -0
  55. package/dist/read-error.d.ts.map +1 -0
  56. package/dist/read-error.js +11 -0
  57. package/dist/read-open-flags.d.ts +8 -0
  58. package/dist/read-open-flags.d.ts.map +1 -0
  59. package/dist/read-open-flags.js +13 -0
  60. package/dist/regular-file.d.ts.map +1 -1
  61. package/dist/regular-file.js +3 -8
  62. package/dist/root-impl.d.ts.map +1 -1
  63. package/dist/root-impl.js +6 -6
  64. package/dist/secret-file.d.ts.map +1 -1
  65. package/dist/secret-file.js +10 -3
  66. package/dist/secret-read-async.d.ts.map +1 -1
  67. package/dist/secret-read-async.js +5 -1
  68. package/dist/symlink-parents.d.ts.map +1 -1
  69. package/dist/symlink-parents.js +9 -8
  70. package/dist/trash.d.ts.map +1 -1
  71. package/dist/trash.js +10 -1
  72. package/docs/advanced.md +6 -0
  73. package/docs/archive.md +2 -2
  74. package/docs/errors.md +5 -3
  75. package/docs/file-store.md +4 -0
  76. package/docs/secret-file.md +4 -0
  77. package/docs/temp.md +5 -0
  78. package/docs/types.md +2 -2
  79. package/package.json +10 -3
@@ -6,6 +6,7 @@ import { assertNoUnsafeDeviceReadPath } from "./device-path.js";
6
6
  import { FsSafeError } from "./errors.js";
7
7
  import { sameFileIdentity } from "./file-identity.js";
8
8
  import { isNotFoundPathError } from "./path.js";
9
+ import { resolveReadOpenFlags } from "./read-open-flags.js";
9
10
  import { assertNoSymlinkParents, assertNoSymlinkParentsSync } from "./symlink-parents.js";
10
11
  export function resolveRegularFileAppendFlags(constants = fsSync.constants) {
11
12
  const noFollow = constants.O_NOFOLLOW;
@@ -14,12 +15,6 @@ export function resolveRegularFileAppendFlags(constants = fsSync.constants) {
14
15
  constants.O_WRONLY |
15
16
  (typeof noFollow === "number" ? noFollow : 0));
16
17
  }
17
- function resolveRegularFileReadFlags() {
18
- return (fsSync.constants.O_RDONLY |
19
- (typeof fsSync.constants.O_NOFOLLOW === "number" && process.platform !== "win32"
20
- ? fsSync.constants.O_NOFOLLOW
21
- : 0));
22
- }
23
18
  function regularFileTooLargeError(filePath, maxBytes, cause) {
24
19
  return new FsSafeError("too-large", `File exceeds ${maxBytes} bytes: ${filePath}`, { cause });
25
20
  }
@@ -72,7 +67,7 @@ export async function readRegularFile(params) {
72
67
  }
73
68
  let handle;
74
69
  try {
75
- handle = await fs.open(params.filePath, resolveRegularFileReadFlags());
70
+ handle = await fs.open(params.filePath, resolveReadOpenFlags());
76
71
  }
77
72
  catch (err) {
78
73
  if (isNotFoundPathError(err)) {
@@ -180,7 +175,7 @@ export function readRegularFileSync(params) {
180
175
  }
181
176
  let fd;
182
177
  try {
183
- fd = fsSync.openSync(params.filePath, resolveRegularFileReadFlags());
178
+ fd = fsSync.openSync(params.filePath, resolveReadOpenFlags());
184
179
  }
185
180
  catch (error) {
186
181
  if (isNotFoundPathError(error)) {
@@ -1 +1 @@
1
- {"version":3,"file":"root-impl.d.ts","sourceRoot":"","sources":["../src/root-impl.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAO7D,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,KAAK,oBAAoB,EAG1B,MAAM,mBAAmB,CAAC;AAY3B,OAAO,EAAwB,KAAK,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAwB9E,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAY,KAAK,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAIpF,YAAY,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,EAAE,kCAAkC,EAAE,MAAM,sBAAsB,CAAC;AAC1E,YAAY,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,UAAU,CAAC;IACnB,WAAW,EAAE,oBAAoB,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;IACZ,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,YAAY,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,oBAAoB,CAAC;AAC5D,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,OAAO,CAAC;AAChD,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE/D,MAAM,MAAM,YAAY,GAAG;IACzB,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,oBAAoB,CAAC;IACtC,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,IAAI,CAChC,YAAY,EACZ,WAAW,GAAG,UAAU,GAAG,iBAAiB,GAAG,UAAU,CAC1D,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;AAEhE,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,YAAY,EAAE,eAAe,GAAG,OAAO,GAAG,MAAM,GAAG,gBAAgB,CAAC,GAAG;IACzG,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,YAAY,EAAE,eAAe,GAAG,OAAO,GAAG,MAAM,CAAC,GAAG;IAC7F,SAAS,CAAC,EAAE,gBAAgB,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,YAAY,EAAE,eAAe,GAAG,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC,GAAG;IAClG,eAAe,CAAC,EAAE,cAAc,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG,gBAAgB,GAAG;IACpD,QAAQ,CAAC,EAAE,UAAU,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,KAAK,CAAC,EAAE,UAAU,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7C,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,EAAE,WAAW,CAAC,CAAC;AACpE,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,oBAAoB,EAAE,WAAW,CAAC,CAAC;AAE5E,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG;IACjD,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG;IAClE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,YAAY,EAAE,eAAe,CAAC,CAAC;AACpE,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,YAAY,EAAE,eAAe,CAAC,CAAC;AAkCnE,eAAO,MAAM,sBAAsB,QAAmB,CAAC;AA6IvD,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAEhC,OAAO,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/C,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC3E,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC3E,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5E,QAAQ,CACN,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,eAAe,GAAG;QAAE,QAAQ,CAAC,EAAE,cAAc,CAAA;KAAE,GACxD,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,QAAQ,CAAC,CAAC,GAAG,OAAO,EAClB,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,eAAe,GAAG;QAAE,QAAQ,CAAC,EAAE,cAAc,CAAA;KAAE,GACxD,OAAO,CAAC,CAAC,CAAC,CAAC;IACd,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/E,MAAM,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACzE,YAAY,CACV,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC/B,MAAM,CACJ,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,GAAG,MAAM,EACrB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,MAAM,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE,KAAK,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,UAAU,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,KAAK,CACH,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,GAAG,MAAM,EACrB,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,MAAM,CACJ,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,GAAG,MAAM,EACrB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,SAAS,CACP,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,OAAO,EACb,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,UAAU,CACR,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,OAAO,EACb,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,MAAM,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3F,MAAM,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC9C,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,KAAK,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACnF,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,aAAa,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IAClF,IAAI,CACF,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,qBAAqB,CAAC,aAAa,CAAC,CAAC;CAC5F;AAyRD,wBAAsB,IAAI,CACxB,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,YAAiB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAEf;AAqFD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,UAAU,CAAC,CAOtB;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAG3F;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,UAAU,CAAC;IACnB,WAAW,EAAE,oBAAoB,CAAC;IAClC,eAAe,EAAE,OAAO,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;IACZ,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC"}
1
+ {"version":3,"file":"root-impl.d.ts","sourceRoot":"","sources":["../src/root-impl.ts"],"names":[],"mappings":"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"}
package/dist/root-impl.js CHANGED
@@ -16,6 +16,7 @@ import { validatePinnedOperationPayload } from "./pinned-operation.js";
16
16
  import { assertNoPathAliasEscape, PATH_ALIAS_POLICIES } from "./path-policy.js";
17
17
  import { assertNoNulPathInput, assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isPathInside, isSymlinkOpenError, } from "./path.js";
18
18
  import { readOpenedFileSafely } from "./read-opened-file.js";
19
+ import { resolveReadOpenFlags } from "./read-open-flags.js";
19
20
  import { resolveRootPath } from "./root-path.js";
20
21
  import { assertRootIdentityCurrent, assertValidRootDestinationPath, assertValidRootRelativePath, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, } from "./root-context.js";
21
22
  import { fileNotFoundError, hardlinkedPathNotAllowedError, isAlreadyExistsError, normalizePinnedPathError, normalizePinnedWriteError, normalizeRemoveGuardError, normalizeRemovePathError, outsideWorkspaceError, } from "./root-errors.js";
@@ -31,11 +32,8 @@ function logWarn(message) {
31
32
  }
32
33
  }
33
34
  const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in fsConstants;
34
- const NONBLOCK_OPEN_FLAG = process.platform !== "win32" && "O_NONBLOCK" in fsConstants ? fsConstants.O_NONBLOCK : 0;
35
- const OPEN_READ_FLAGS = fsConstants.O_RDONLY |
36
- (SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0) |
37
- NONBLOCK_OPEN_FLAG;
38
- const OPEN_READ_FOLLOW_FLAGS = fsConstants.O_RDONLY | NONBLOCK_OPEN_FLAG;
35
+ const OPEN_READ_FLAGS = resolveReadOpenFlags();
36
+ const OPEN_READ_FOLLOW_FLAGS = resolveReadOpenFlags({ followSymlinks: true });
39
37
  const OPEN_WRITE_EXISTING_FLAGS = fsConstants.O_WRONLY | (SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0);
40
38
  const OPEN_WRITE_CREATE_FLAGS = fsConstants.O_WRONLY |
41
39
  fsConstants.O_CREAT |
@@ -86,7 +84,6 @@ async function openVerifiedLocalFile(filePath, options) {
86
84
  if (!preOpenStat.isFile() && !preOpenStat.isSymbolicLink()) {
87
85
  throw new FsSafeError("not-file", "not a file");
88
86
  }
89
- await fsSafeTestHooks?.afterPreOpenLstat?.(filePath);
90
87
  }
91
88
  catch (err) {
92
89
  if (err instanceof FsSafeError) {
@@ -94,6 +91,9 @@ async function openVerifiedLocalFile(filePath, options) {
94
91
  }
95
92
  // ENOENT and other lstat errors: fall through and let fs.open handle.
96
93
  }
94
+ if (preOpenStat) {
95
+ await fsSafeTestHooks?.afterPreOpenLstat?.(filePath);
96
+ }
97
97
  let handle;
98
98
  try {
99
99
  const openFlags = options?.symlinks === "follow-within-root"
@@ -1 +1 @@
1
- {"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,6BAA6B,QAAY,CAAC;AACvD,eAAO,MAAM,uBAAuB,MAAQ,CAAC;AAC7C,eAAO,MAAM,wBAAwB,MAAQ,CAAC;AAE9C,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAoIF,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAQR;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,GAAG,SAAS,CAcpB;AAoID,KAAK,qBAAqB,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAkEF,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKxF;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAezF"}
1
+ {"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,6BAA6B,QAAY,CAAC;AACvD,eAAO,MAAM,uBAAuB,MAAQ,CAAC;AAC7C,eAAO,MAAM,wBAAwB,MAAQ,CAAC;AAE9C,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAyIF,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAQR;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,GAAG,SAAS,CAcpB;AAoID,KAAK,qBAAqB,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAkEF,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKxF;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAezF"}
@@ -102,6 +102,15 @@ function readSecretFileOutcomeSync(filePath, label, options = {}) {
102
102
  };
103
103
  }
104
104
  try {
105
+ if (!sameFileIdentity(previewStat, opened.stat)) {
106
+ const error = new FsSafeError("path-mismatch", "security validation failed");
107
+ return {
108
+ ok: false,
109
+ code: "path-mismatch",
110
+ error,
111
+ message: `Failed to read ${label} file at ${resolvedPath}: ${String(error)}`,
112
+ };
113
+ }
105
114
  const raw = readFileDescriptorBoundedSync(opened.fd, maxBytes).toString("utf8");
106
115
  const secret = raw.trim();
107
116
  if (!secret) {
@@ -117,9 +126,7 @@ function readSecretFileOutcomeSync(filePath, label, options = {}) {
117
126
  const normalized = normalizeSecretReadError(error);
118
127
  return {
119
128
  ok: false,
120
- code: error instanceof FsSafeError && error.code === "too-large"
121
- ? "too-large"
122
- : "invalid-path",
129
+ code: error instanceof FsSafeError ? error.code : "read-failed",
123
130
  error: normalized,
124
131
  message: `Failed to read ${label} file at ${resolvedPath}: ${String(normalized)}`,
125
132
  };
@@ -1 +1 @@
1
- {"version":3,"file":"secret-read-async.d.ts","sourceRoot":"","sources":["../src/secret-read-async.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,kBAAkB,CAAC;AAqF1B,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAM7B"}
1
+ {"version":3,"file":"secret-read-async.d.ts","sourceRoot":"","sources":["../src/secret-read-async.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,kBAAkB,CAAC;AA0F1B,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAM7B"}
@@ -68,7 +68,11 @@ async function readSecretFileOutcome(filePath, label, options) {
68
68
  const normalized = error instanceof Error ? error : new Error(String(error));
69
69
  return {
70
70
  ok: false,
71
- code: error instanceof FsSafeError ? error.code : pathErrorCode(error) === "not-found" ? "not-found" : "path-mismatch",
71
+ code: error instanceof FsSafeError
72
+ ? error.code
73
+ : pathErrorCode(error) === "not-found"
74
+ ? "not-found"
75
+ : "read-failed",
72
76
  error: normalized,
73
77
  message: `Failed to read ${label} file at ${resolvedPath}: ${String(normalized)}`,
74
78
  };
@@ -1 +1 @@
1
- {"version":3,"file":"symlink-parents.d.ts","sourceRoot":"","sources":["../src/symlink-parents.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAyBF,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,6BAA6B,GACpC,OAAO,CAAC,IAAI,CAAC,CA0Bf;AAED,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,6BAA6B,GACpC,IAAI,CA0BN"}
1
+ {"version":3,"file":"symlink-parents.d.ts","sourceRoot":"","sources":["../src/symlink-parents.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAyBF,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,6BAA6B,GACpC,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,6BAA6B,GACpC,IAAI,CA6BN"}
@@ -1,7 +1,8 @@
1
1
  import fsSync from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { isNotFoundPathError, isPathRelativeEscape } from "./path.js";
4
+ import { FsSafeError } from "./errors.js";
5
+ import { hasNodeErrorCode, isPathRelativeEscape } from "./path.js";
5
6
  function resolvePathWalk(params) {
6
7
  const root = path.resolve(params.rootDir);
7
8
  const target = path.resolve(params.targetPath);
@@ -26,7 +27,7 @@ export async function assertNoSymlinkParents(params) {
26
27
  return;
27
28
  }
28
29
  let current = walk.root;
29
- for (const segment of walk.segments) {
30
+ for (const [index, segment] of walk.segments.entries()) {
30
31
  current = path.join(current, segment);
31
32
  try {
32
33
  const stat = await fs.lstat(current);
@@ -36,12 +37,12 @@ export async function assertNoSymlinkParents(params) {
36
37
  }
37
38
  throw new Error(formatUnsafePath(params, current));
38
39
  }
39
- if (params.requireDirectories && !stat.isDirectory()) {
40
- throw new Error(`${params.messagePrefix ?? "Path"} must traverse directories: ${current}`);
40
+ if ((params.requireDirectories || index < walk.segments.length - 1) && !stat.isDirectory()) {
41
+ throw new FsSafeError("not-file", `${params.messagePrefix ?? "Path"} must traverse directories: ${current}`);
41
42
  }
42
43
  }
43
44
  catch (err) {
44
- if (isNotFoundPathError(err) && params.allowMissing !== false) {
45
+ if (hasNodeErrorCode(err, "ENOENT") && params.allowMissing !== false) {
45
46
  return;
46
47
  }
47
48
  throw err;
@@ -54,7 +55,7 @@ export function assertNoSymlinkParentsSync(params) {
54
55
  return;
55
56
  }
56
57
  let current = walk.root;
57
- for (const segment of walk.segments) {
58
+ for (const [index, segment] of walk.segments.entries()) {
58
59
  current = path.join(current, segment);
59
60
  try {
60
61
  const stat = fsSync.lstatSync(current);
@@ -64,8 +65,8 @@ export function assertNoSymlinkParentsSync(params) {
64
65
  }
65
66
  throw new Error(formatUnsafePath(params, current));
66
67
  }
67
- if (params.requireDirectories && !stat.isDirectory()) {
68
- throw new Error(`${params.messagePrefix ?? "Path"} must traverse directories: ${current}`);
68
+ if ((params.requireDirectories || index < walk.segments.length - 1) && !stat.isDirectory()) {
69
+ throw new FsSafeError("not-file", `${params.messagePrefix ?? "Path"} must traverse directories: ${current}`);
69
70
  }
70
71
  }
71
72
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"trash.d.ts","sourceRoot":"","sources":["../src/trash.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,sBAAsB,GAAG;IACnC,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;CACjC,CAAC;AAwKF,wBAAsB,eAAe,CACnC,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,MAAM,CAAC,CAcjB"}
1
+ {"version":3,"file":"trash.d.ts","sourceRoot":"","sources":["../src/trash.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,sBAAsB,GAAG;IACnC,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;CACjC,CAAC;AAkLF,wBAAsB,eAAe,CACnC,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,MAAM,CAAC,CAcjB"}
package/dist/trash.js CHANGED
@@ -117,6 +117,15 @@ function reserveTrashDestination(trashDir, base, timestamp) {
117
117
  }
118
118
  return resolveContainedPath(container, base);
119
119
  }
120
+ function copyTrashTargetSync(target, dest) {
121
+ if (target.stat.isSymbolicLink()) {
122
+ const linkTarget = fs.readlinkSync(target.path);
123
+ assertTrashTargetGuard(target);
124
+ fs.symlinkSync(linkTarget, dest);
125
+ return;
126
+ }
127
+ fs.cpSync(target.path, dest, { recursive: true, force: false, errorOnExist: true });
128
+ }
120
129
  function movePathToDestination(target, dest) {
121
130
  getFsSafeTestHooks()?.beforeTrashMove?.(target.path, dest);
122
131
  assertTrashTargetGuard(target);
@@ -134,7 +143,7 @@ function movePathToDestination(target, dest) {
134
143
  }
135
144
  try {
136
145
  assertTrashTargetGuard(target);
137
- fs.cpSync(target.path, dest, { recursive: true, force: false, errorOnExist: true });
146
+ copyTrashTargetSync(target, dest);
138
147
  assertTrashTargetGuard(target);
139
148
  guardedRmSync({ target: target.path, recursive: true, force: false, verifyAfter: false });
140
149
  return true;
package/docs/advanced.md CHANGED
@@ -82,6 +82,12 @@ try {
82
82
  }
83
83
  ```
84
84
 
85
+ For the symlink-parent guards, `allowMissing` defaults to `true` and permits the
86
+ walk to stop only at an actually absent suffix. When an existing non-directory
87
+ component is followed by another segment, both helpers throw
88
+ `FsSafeError("not-file")` before the platform can expose that state as POSIX
89
+ `ENOTDIR` or Windows `ENOENT`.
90
+
85
91
  ### Local roots and file URLs
86
92
 
87
93
  | Export | Page | Notes |
package/docs/archive.md CHANGED
@@ -148,7 +148,7 @@ codes remain `"destination-not-directory"`, `"destination-symlink"`, and
148
148
 
149
149
  ## What it defends against
150
150
 
151
- - **Path traversal:** entries with `..`, absolute paths, NUL bytes, or Windows drive-relative segments such as `C:secret` and `nested/C:secret` are rejected (`ArchiveSecurityError`).
151
+ - **Path traversal:** entries with `..`, absolute paths, NUL bytes, or Windows drive-relative segments such as `C:secret` and `nested/C:secret` are rejected (`ArchiveSecurityError`). On Windows, path segments containing `:` are also rejected as alternate data stream names before either backend writes to the filesystem.
152
152
  - **Symlink/hardlink entries:** rejected by default. Some archives ship symlink/hardlink entries that point outside the destination once resolved; `extractArchive` does not follow them.
153
153
  - **Ambiguous output names:** duplicate names and distinct names that collide after `stripComponents`, case normalization, or Unicode normalization are rejected instead of relying on backend- or volume-specific overwrite order.
154
154
  - **TOCTOU during merge:** extraction first writes to a private temp dir, then merges into `destDir` using the same boundary checks as `root().write()`. Destination symlink swaps are checked with the selected platform mechanism; non-Linux routes retain the best-effort race window documented in the [security model](security-model.md#containment-guarantees-by-platform).
@@ -255,7 +255,7 @@ import {
255
255
  } from "@openclaw/fs-safe/archive";
256
256
  ```
257
257
 
258
- - `validateArchiveEntryPath(raw, opts)` — throws `ArchiveSecurityError` for `..`, absolute, NUL-containing, drive-relative, or otherwise unsafe entry paths.
258
+ - `validateArchiveEntryPath(raw, opts)` — throws `ArchiveSecurityError` for `..`, absolute, NUL-containing, drive-relative, or otherwise unsafe entry paths, including alternate data stream names on Windows.
259
259
  - `normalizeArchiveEntryPath(raw)` — converts backslashes in the entry path to forward slashes.
260
260
  - `stripArchivePath(entryPath, n)` — strip the leading N path components, returning `null` if not enough remain.
261
261
  - `resolveArchiveOutputPath({ destDir, entryPath })` — combines the entry path with the destination, after validation.
package/docs/errors.md CHANGED
@@ -62,8 +62,8 @@ filesystem boundary violation. Branch on the specific code when the distinction
62
62
  between those operational outcomes matters.
63
63
 
64
64
  The operational set is exactly `helper-failed`, `helper-unavailable`,
65
- `not-empty`, `not-found`, `not-removable`, `permission-unverified`, `timeout`,
66
- and `unsupported-platform`. Every other current `FsSafeErrorCode`, including
65
+ `not-empty`, `not-found`, `not-removable`, `permission-unverified`, `read-failed`,
66
+ `timeout`, and `unsupported-platform`. Every other current `FsSafeErrorCode`, including
67
67
  `store-reentrant-update`, is categorized as `policy`.
68
68
 
69
69
  ## Code union
@@ -87,6 +87,7 @@ type FsSafeErrorCode =
87
87
  | "path-alias"
88
88
  | "path-mismatch"
89
89
  | "permission-unverified"
90
+ | "read-failed"
90
91
  | "secret-exists"
91
92
  | "store-reentrant-update"
92
93
  | "symlink"
@@ -108,7 +109,7 @@ type FsSafeErrorCode =
108
109
  | `insecure-permissions` | A secure file or path permission check found a mode/ACL that allows broader access than requested. | File or directory is group/world writable/readable; Windows ACL grants broad read. |
109
110
  | `invalid-path` | Input was empty, contained NUL, was an unparseable URL, or otherwise unusable. | Caller didn't validate input; input was a network path on Windows, a drive-relative segment in a portable relative path or store key, or a leading drive-relative spelling such as `C:name` used as a Root destination. Existing-object Root lookups retain legal POSIX drive-like names. |
110
111
  | `not-empty` | `remove()` on a non-empty directory. | Use `replaceDirectoryAtomic` or remove children first. |
111
- | `not-file` | Read or copy targeted a non-regular file. | Target was a directory, FIFO, socket, device. |
112
+ | `not-file` | Read or copy targeted a non-regular file, or a path walk found a non-directory ancestor. | Target was a directory, FIFO, socket, device, or an existing file was followed by another segment. |
112
113
  | `not-found` | The target does not exist (or its parent does not, with `mkdir: false`). | Typical missing-file case. |
113
114
  | `not-owned` | A secure file owner check failed. | File is owned by another UID. |
114
115
  | `not-removable` | `remove()` couldn't `unlink`/`rmdir` for a reason other than non-empty. | Permissions, device busy, immutable bit. |
@@ -116,6 +117,7 @@ type FsSafeErrorCode =
116
117
  | `path-alias` | A path alias check failed (e.g. canonical-real-path moved out of the root). | Symlink resolution lands outside the root. |
117
118
  | `path-mismatch` | Post-open identity check failed: the opened fd does not match the resolved path. | TOCTOU — something else swapped the path between resolve and open. |
118
119
  | `permission-unverified` | A secure file check could not verify required permissions. | Windows ACL inspection failed; POSIX ownership/mode was unavailable. |
120
+ | `read-failed` | A validated file could not be read because of an operational filesystem or device failure. | I/O error, media failure, or another runtime read failure; inspect `cause`. |
119
121
  | `secret-exists` | `createSecretFileAtomic()` found an existing final path. | First-writer-wins secret creation lost a race or the credential was already initialized. |
120
122
  | `store-reentrant-update` | A `JsonStore.update()` callback called `update()` or `updateOr()` for the same canonical store before returning. | Reentrant mutation would deadlock or lose an update; return the complete next value from the outer callback. |
121
123
  | `symlink` | Path component is a symlink, policy is `reject`. | Caller followed a symlink they shouldn't have, or `symlinks: "reject"` is set. |
@@ -138,6 +138,10 @@ type FileStoreWriteOptions = {
138
138
  ## Reads
139
139
 
140
140
  `open`, `read`, `readBytes`, `readText`, and `readJson` delegate to a fresh `Root` with `hardlinks: "reject"` and the store's `maxBytes`. Same return shapes as `Root`.
141
+ Async and sync reads both report `not-file` when the key names a directory;
142
+ stable hardlinks and symlinks retain `hardlink` and `symlink`. The `IfExists`
143
+ methods return their nullish result only for `not-found`. Operational filesystem
144
+ read failures use `read-failed` with the Node error retained in `cause`.
141
145
 
142
146
  ## `remove(rel)` / `exists(rel)`
143
147
 
@@ -89,6 +89,10 @@ credential must also fail on broad permissions or unexpected ownership.
89
89
  the same pinned-handle validation, byte cap, trimming, error codes, and strict
90
90
  versus missing-is-undefined naming semantics.
91
91
 
92
+ If an already validated descriptor fails while reading, both readers throw an
93
+ operational `FsSafeError` with `code: "read-failed"`; inspect `cause` for the
94
+ underlying Node filesystem code such as `EIO`.
95
+
92
96
  Use the async strict reader when a service cannot start safely without the
93
97
  credential:
94
98
 
package/docs/temp.md CHANGED
@@ -45,6 +45,11 @@ await runBuild(workspace.dir, inputPath);
45
45
  ```
46
46
 
47
47
  `write` writes at `mode` (default `0o600`); `writeText` and `writeJson` are convenience wrappers for the common scratch-file shapes; `copyIn` ingests an absolute source path through the same atomic-rename machinery as `Root.copyIn`. `read` is a small accessor that reads back any file you wrote into the workspace.
48
+ Both the async and sync `read` methods throw `FsSafeError("not-file")` when the
49
+ named leaf is a directory or another non-regular target, and preserve the
50
+ `not-found`, `hardlink`, or `symlink` code for those stable target states.
51
+ Operational filesystem read failures use `read-failed` with the Node error in
52
+ `cause`.
48
53
 
49
54
  `store` is a `fileStore({ rootDir: workspace.dir, private: true })` handle. Use
50
55
  it when you want the richer store surface, including `writeStream`, `exists`,
package/docs/types.md CHANGED
@@ -171,14 +171,14 @@ type FsSafeErrorCode =
171
171
  | "helper-unavailable" | "insecure-permissions" | "invalid-path"
172
172
  | "not-empty" | "not-file" | "not-found" | "not-owned"
173
173
  | "not-removable" | "outside-workspace" | "path-alias"
174
- | "path-mismatch" | "permission-unverified" | "secret-exists"
174
+ | "path-mismatch" | "permission-unverified" | "read-failed" | "secret-exists"
175
175
  | "store-reentrant-update" | "symlink"
176
176
  | "timeout" | "too-large" | "unsupported-platform";
177
177
  ```
178
178
 
179
179
  Closed union you switch on. See the [Errors](errors.md) reference for what each one means.
180
180
 
181
- `FsSafeError.category` is `"policy"` for unsafe input or target state rejected by a safety policy and `"operational"` for routine filesystem outcomes or environment/runtime failures. `not-found`, `not-empty`, and `not-removable` are operational.
181
+ `FsSafeError.category` is `"policy"` for unsafe input or target state rejected by a safety policy and `"operational"` for routine filesystem outcomes or environment/runtime failures. `not-found`, `not-empty`, `not-removable`, and `read-failed` are operational.
182
182
 
183
183
  ## See also
184
184
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
5
5
  "keywords": [
6
6
  "filesystem",
@@ -129,6 +129,8 @@
129
129
  "prepack": "node scripts/prepack-build.mjs",
130
130
  "test": "vitest run",
131
131
  "test:coverage": "vitest run --coverage",
132
+ "test:coverage:collect": "vitest run --coverage --coverage.reporter=json --coverage.thresholds.lines=0 --coverage.thresholds.functions=0 --coverage.thresholds.statements=0 --coverage.thresholds.branches=0",
133
+ "test:coverage:merge": "node scripts/merge-coverage.mjs",
132
134
  "test:security": "vitest run test/fs-safe.test.ts test/read-boundary-bypass.test.ts test/write-boundary-bypass.test.ts test/additional-boundary-bypass.test.ts test/adversarial-boundary-payloads.test.ts",
133
135
  "check": "pnpm lint:file-size && pnpm lint:fs-boundary && pnpm build && pnpm docs:check && pnpm test && node scripts/check-pack.mjs",
134
136
  "docs:check": "node scripts/check-doc-examples.mjs",
@@ -151,12 +153,17 @@
151
153
  "tar": "7.5.22"
152
154
  },
153
155
  "devDependencies": {
156
+ "@emnapi/runtime": "2.0.0-alpha.3",
154
157
  "@napi-rs/cli": "3.8.2",
155
- "@types/node": "^26.1.2",
158
+ "@types/node": "^26.2.0",
156
159
  "@vitest/coverage-v8": "4.1.10",
160
+ "fast-check": "^4.9.0",
161
+ "istanbul-lib-coverage": "3.2.2",
162
+ "istanbul-lib-report": "3.0.1",
163
+ "istanbul-reports": "3.2.0",
157
164
  "sigstore": "5.0.0",
158
165
  "typescript": "^7.0.2",
159
- "vite": "8.2.0",
166
+ "vite": "8.2.1",
160
167
  "vitest": "^4.1.10"
161
168
  },
162
169
  "engines": {