@openclaw/fs-safe 0.18.1 → 0.18.2

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 (55) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/advanced.d.ts +2 -1
  3. package/dist/advanced.js +1 -1
  4. package/dist/archive-merge.js +0 -1
  5. package/dist/archive-staging.d.ts +1 -2
  6. package/dist/archive-staging.js +0 -2
  7. package/dist/archive-zip-preflight.d.ts +0 -2
  8. package/dist/archive-zip-preflight.js +0 -1
  9. package/dist/archive.d.ts +6 -3
  10. package/dist/archive.js +5 -3
  11. package/dist/directory-guard.d.ts +0 -1
  12. package/dist/directory-guard.js +0 -3
  13. package/dist/directory-mode-node.d.ts +20 -2
  14. package/dist/directory-mode-node.js +77 -1
  15. package/dist/file-lock-sync-acquisition.js +1 -1
  16. package/dist/file-lock-sync-root-options.d.ts +2 -10
  17. package/dist/json-durable-queue-ownership.d.ts +1 -5
  18. package/dist/json-durable-queue.d.ts +0 -1
  19. package/dist/json-durable-queue.js +0 -1
  20. package/dist/permission-exec.d.ts +6 -0
  21. package/dist/permissions-public.d.ts +2 -1
  22. package/dist/permissions-windows.d.ts +3 -6
  23. package/dist/permissions.d.ts +3 -10
  24. package/dist/permissions.js +0 -1
  25. package/dist/pinned-open.d.ts +1 -0
  26. package/dist/pinned-open.js +1 -1
  27. package/dist/replace-directory.js +5 -17
  28. package/dist/replace-file-copy-fallback.d.ts +1 -8
  29. package/dist/replace-file-descriptor.d.ts +3 -11
  30. package/dist/replace-file-descriptor.js +1 -1
  31. package/dist/retained-directory-replacement.d.ts +1 -0
  32. package/dist/retained-directory-replacement.js +1 -1
  33. package/dist/root-file-final-admission.d.ts +1 -1
  34. package/dist/root-file-final-admission.js +1 -6
  35. package/dist/root-file.js +2 -5
  36. package/dist/secret-file.js +11 -16
  37. package/dist/secret-read-policy.js +1 -1
  38. package/dist/secure-file-windows.js +4 -9
  39. package/dist/secure-temp-dir.js +2 -7
  40. package/dist/sidecar-lock-acquire.js +1 -1
  41. package/dist/sidecar-lock-reclaim.js +5 -6
  42. package/dist/stat-observation.js +3 -10
  43. package/dist/strict-file-identity.d.ts +2 -0
  44. package/dist/strict-file-identity.js +6 -6
  45. package/dist/temp-target.js +3 -8
  46. package/dist/temp-workspace-admission.js +6 -12
  47. package/dist/temp-workspace-child-admission.js +2 -8
  48. package/dist/temp-workspace-descriptor.js +1 -2
  49. package/dist/windows-owner.d.ts +3 -6
  50. package/dist/windows-security-bridge.cs +0 -2
  51. package/docs/secret-file.md +9 -0
  52. package/docs/sidecar-lock.md +2 -0
  53. package/package.json +8 -8
  54. package/dist/directory-mode-owner.d.ts +0 -20
  55. package/dist/directory-mode-owner.js +0 -78
@@ -5,8 +5,7 @@ import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
5
5
  import { canonicalPathFromExistingAncestor } from "./absolute-path.js";
6
6
  import { readFileDescriptorBoundedSync } from "./bounded-read.js";
7
7
  import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
8
- import { pinNodeDirectoryForMode } from "./directory-mode-node.js";
9
- import { assertOwnedDirectory } from "./directory-mode-owner.js";
8
+ import { pinNodeDirectoryForMode, assertOwnedDirectory } from "./directory-mode-node.js";
10
9
  import { FsSafeError } from "./errors.js";
11
10
  import { openPinnedFileSync } from "./pinned-open.js";
12
11
  import { runPinnedWriteHelper } from "./pinned-write.js";
@@ -199,7 +198,11 @@ async function ensurePrivateDirectory(rootDir, targetDir, mode) {
199
198
  }
200
199
  return { rootGuard, parentGuard: targetGuard };
201
200
  }
202
- function snapshotSecretFileWriteParams(params, rootDir, filePath) {
201
+ function snapshotSecretFileWriteParams(params) {
202
+ const rootDir = params.rootDir;
203
+ const filePath = params.filePath;
204
+ assertNoWindowsPathAlias(rootDir, "filesystem", "private secret root uses a Windows filesystem namespace alias");
205
+ assertNoWindowsPathAlias(filePath, "filesystem", "private secret path uses a Windows filesystem namespace alias");
203
206
  return {
204
207
  rootDir,
205
208
  filePath,
@@ -305,31 +308,23 @@ async function materializeSecretFileAtomic(params, createOnly) {
305
308
  });
306
309
  }
307
310
  export async function writeSecretFileAtomic(params) {
308
- const rootDir = params.rootDir;
309
- const filePath = params.filePath;
310
- assertNoWindowsPathAlias(rootDir, "filesystem", "private secret root uses a Windows filesystem namespace alias");
311
- assertNoWindowsPathAlias(filePath, "filesystem", "private secret path uses a Windows filesystem namespace alias");
312
- const ownedParams = snapshotSecretFileWriteParams(params, rootDir, filePath);
313
- const canonicalPath = await secretFileWriteQueueKey(filePath);
311
+ const ownedParams = snapshotSecretFileWriteParams(params);
312
+ const canonicalPath = await secretFileWriteQueueKey(ownedParams.filePath);
314
313
  await serializePathWrite(canonicalPath, async () => {
315
314
  await materializeSecretFileAtomic(ownedParams, false);
316
315
  });
317
316
  }
318
317
  export async function createSecretFileAtomic(params) {
319
318
  try {
320
- const rootDir = params.rootDir;
321
- const filePath = params.filePath;
322
- assertNoWindowsPathAlias(rootDir, "filesystem", "private secret root uses a Windows filesystem namespace alias");
323
- assertNoWindowsPathAlias(filePath, "filesystem", "private secret path uses a Windows filesystem namespace alias");
324
- const ownedParams = snapshotSecretFileWriteParams(params, rootDir, filePath);
325
- const canonicalPath = await secretFileWriteQueueKey(filePath);
319
+ const ownedParams = snapshotSecretFileWriteParams(params);
320
+ const canonicalPath = await secretFileWriteQueueKey(ownedParams.filePath);
326
321
  await serializePathWrite(canonicalPath, async () => {
327
322
  await materializeSecretFileAtomic(ownedParams, true);
328
323
  });
329
324
  }
330
325
  catch (error) {
331
326
  if ((error instanceof FsSafeError && error.code === "already-exists") ||
332
- error.code === "EEXIST") {
327
+ error?.code === "EEXIST") {
333
328
  throw new FsSafeError("secret-exists", "Private secret file already exists.", { cause: error });
334
329
  }
335
330
  throw error;
@@ -7,7 +7,7 @@ import { inspectFileIdentitySync } from "./strict-file-identity.js";
7
7
  import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
8
8
  export const DEFAULT_SECRET_FILE_MAX_BYTES = 16 * 1024;
9
9
  export function secretPathErrorCode(error) {
10
- const code = error.code;
10
+ const code = error?.code;
11
11
  return code === "ENOENT" || code === "ENOTDIR" ? "not-found" : "invalid-path";
12
12
  }
13
13
  export function secretReadError(code, action, label, resolvedPath, error) {
@@ -1,5 +1,5 @@
1
1
  import { FsSafeError } from "./errors.js";
2
- import { recordFileObservationFailure } from "./file-observation.js";
2
+ import { fileIdentityMismatchError } from "./strict-file-identity.js";
3
3
  import { getNativeBinding } from "./native.js";
4
4
  import { getFsSafeNativeConfig } from "./native-config.js";
5
5
  import { warnNativeFallback } from "./native-fallback-warning.js";
@@ -10,20 +10,15 @@ const TRUSTED_OWNER_CLASSES = new Set(["current-user", "system", "administrators
10
10
  function permissionUnverified(message, cause) {
11
11
  throw new FsSafeError("permission-unverified", message, cause === undefined ? {} : { cause });
12
12
  }
13
- function identityMismatch() {
14
- const error = new FsSafeError("path-mismatch", "file identity changed or could not be verified");
15
- recordFileObservationFailure(error, "identity");
16
- throw error;
17
- }
18
13
  function isRecord(value) {
19
14
  return typeof value === "object" && value !== null;
20
15
  }
21
16
  function parseIdentity(value) {
22
17
  if (typeof value !== "string")
23
- identityMismatch();
18
+ throw fileIdentityMismatchError();
24
19
  const match = IDENTITY_RE.exec(value);
25
20
  if (!match)
26
- identityMismatch();
21
+ throw fileIdentityMismatchError();
27
22
  return {
28
23
  dev: BigInt(`0x${match[1]}`),
29
24
  ino: BigInt(`0x${match[2]}`),
@@ -34,7 +29,7 @@ function inspectDescriptorResult(params, result, mechanism) {
34
29
  permissionUnverified("Windows descriptor ACL facts were malformed");
35
30
  const observed = parseIdentity(result.identity);
36
31
  if (observed.dev !== params.identity.dev || observed.ino !== params.identity.ino) {
37
- identityMismatch();
32
+ throw fileIdentityMismatchError();
38
33
  }
39
34
  const facts = validateSecureWindowsSecurityFacts(result.security);
40
35
  return {
@@ -3,15 +3,10 @@ import { tmpdir as getOsTmpDir } from "node:os";
3
3
  import path from "node:path";
4
4
  import { directoryEntryPath } from "./directory-entry-path.js";
5
5
  import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
6
+ import { hasNodeErrorCode } from "./path.js";
6
7
  import { assertSafePathSegment } from "./safe-path-segment.js";
7
8
  import { assertNoWindowsPathAlias, pathForWindowsFilesystem } from "./windows-path-alias.js";
8
9
  import { captureSecureTempRepairAdapter, repairSecureTempDirectory, secureTempDirectoryReceipt, } from "./secure-temp-repair.js";
9
- function isNodeErrorWithCode(err, code) {
10
- return (typeof err === "object" &&
11
- err !== null &&
12
- "code" in err &&
13
- err.code === code);
14
- }
15
10
  export function resolveSecureTempRoot(options) {
16
11
  const { fallbackPrefix: prefix, accessSync: suppliedAccess, lstatSync: suppliedLstat, mkdirSync: suppliedMkdir, descriptor: suppliedDescriptor, getuid: suppliedGetuid, tmpdir: suppliedTmpdir, platform: suppliedPlatform, preferredDir, skipPreferredOnWindows, warn: suppliedWarn, warningPrefix: suppliedWarningPrefix, unsafeFallbackLabel: suppliedFallbackLabel, } = options;
17
12
  const platform = suppliedPlatform ?? process.platform;
@@ -87,7 +82,7 @@ export function resolveSecureTempRoot(options) {
87
82
  candidate = lstatSync(candidatePath);
88
83
  }
89
84
  catch (error) {
90
- return { kind: isNodeErrorWithCode(error, "ENOENT") ? "missing" : "invalid", error };
85
+ return { kind: hasNodeErrorCode(error, "ENOENT") ? "missing" : "invalid", error };
91
86
  }
92
87
  let receipt;
93
88
  try {
@@ -102,7 +102,7 @@ export async function acquireSidecarLock(options, context) {
102
102
  await waitForRetry();
103
103
  }
104
104
  catch (waitError) {
105
- if (waitError.code === "file_lock_timeout")
105
+ if (waitError?.code === "file_lock_timeout")
106
106
  throw denial;
107
107
  throw waitError;
108
108
  }
@@ -357,13 +357,12 @@ export async function removeStaleSidecarLockIfAllowed(params) {
357
357
  if (params.assertGuardHeld)
358
358
  await params.assertGuardHeld();
359
359
  params.assertAuthorized?.();
360
+ if (params.lockRoot) {
361
+ await params.lockRoot.remove(relativeSidecarLockPath(params.lockRoot, params.lockPath), { assertBeforeMutation: params.assertAuthorized });
362
+ return "removed";
363
+ }
360
364
  try {
361
- if (params.lockRoot) {
362
- await params.lockRoot.remove(relativeSidecarLockPath(params.lockRoot, params.lockPath), { assertBeforeMutation: params.assertAuthorized });
363
- }
364
- else {
365
- await fs.rm(params.lockPath);
366
- }
365
+ await fs.rm(params.lockPath);
367
366
  return "removed";
368
367
  }
369
368
  catch (err) {
@@ -1,11 +1,4 @@
1
- import { FsSafeError } from "./errors.js";
2
- import { recordFileObservationFailure } from "./file-observation.js";
3
- import { inspectFileIdentitySync } from "./strict-file-identity.js";
4
- function identityMismatch() {
5
- const error = new FsSafeError("path-mismatch", "file identity changed or could not be verified");
6
- recordFileObservationFailure(error, "identity");
7
- throw error;
8
- }
1
+ import { fileIdentityMismatchError, inspectFileIdentitySync } from "./strict-file-identity.js";
9
2
  function safeNumber(value) {
10
3
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
11
4
  }
@@ -35,7 +28,7 @@ function observeStatSync(inspect, expected, initial, platform = process.platform
35
28
  const ino = numericIno ? BigInt(rawIno) : typeof rawIno === "bigint" ? rawIno : undefined;
36
29
  if (expected && ((dev !== undefined && dev !== expected.dev) ||
37
30
  (ino !== undefined && ino !== expected.ino)))
38
- identityMismatch();
31
+ throw fileIdentityMismatchError();
39
32
  if (numericDev && numericIno) {
40
33
  return (captureIdentity
41
34
  ? { stat, identity: expected ?? { dev: dev, ino: ino } }
@@ -47,7 +40,7 @@ function observeStatSync(inspect, expected, initial, platform = process.platform
47
40
  const current = inspect(true);
48
41
  if ((dev !== undefined && current.dev !== dev) ||
49
42
  (ino !== undefined && current.ino !== ino))
50
- identityMismatch();
43
+ throw fileIdentityMismatchError();
51
44
  return current;
52
45
  }, expected, platform);
53
46
  return (captureIdentity
@@ -1,5 +1,7 @@
1
1
  import type { BigIntStats } from "node:fs";
2
+ import { FsSafeError } from "./errors.js";
2
3
  type ExactFileIdentity = Pick<BigIntStats, "dev" | "ino">;
4
+ export declare function fileIdentityMismatchError(): FsSafeError;
3
5
  export declare function inspectFileIdentity<T extends ExactFileIdentity>(inspect: () => T | Promise<T>, expected?: ExactFileIdentity, platform?: NodeJS.Platform): Promise<T>;
4
6
  export declare function inspectFileIdentitySync<T extends ExactFileIdentity>(inspect: () => T, expected?: ExactFileIdentity, platform?: NodeJS.Platform, mismatch?: () => Error): T;
5
7
  export {};
@@ -1,6 +1,6 @@
1
1
  import { FsSafeError } from "./errors.js";
2
2
  import { recordFileObservationFailure } from "./file-observation.js";
3
- function identityMismatch() {
3
+ export function fileIdentityMismatchError() {
4
4
  const error = new FsSafeError("path-mismatch", "file identity changed or could not be verified");
5
5
  recordFileObservationFailure(error, "identity");
6
6
  return error;
@@ -13,20 +13,20 @@ function identityCheck(expected, platform) {
13
13
  const value = stat[field];
14
14
  // Numeric receipts cannot recover identity bits already lost to rounding.
15
15
  if (typeof value !== "bigint")
16
- throw identityMismatch();
16
+ throw fileIdentityMismatchError();
17
17
  if (platform === "win32" && value === 0n) {
18
18
  complete = false;
19
19
  }
20
20
  else {
21
21
  if (known[field] !== undefined && known[field] !== value)
22
- throw identityMismatch();
22
+ throw fileIdentityMismatchError();
23
23
  known[field] = value;
24
24
  }
25
25
  }
26
26
  return complete;
27
27
  };
28
28
  if (expected && !check(expected))
29
- throw identityMismatch();
29
+ throw fileIdentityMismatchError();
30
30
  return check;
31
31
  }
32
32
  // Retry only unknown Windows identities, retaining every known component so a
@@ -38,9 +38,9 @@ export async function inspectFileIdentity(inspect, expected, platform = process.
38
38
  if (check(stat))
39
39
  return stat;
40
40
  }
41
- throw identityMismatch();
41
+ throw fileIdentityMismatchError();
42
42
  }
43
- export function inspectFileIdentitySync(inspect, expected, platform = process.platform, mismatch = identityMismatch) {
43
+ export function inspectFileIdentitySync(inspect, expected, platform = process.platform, mismatch = fileIdentityMismatchError) {
44
44
  let knownDev;
45
45
  let knownIno;
46
46
  if (expected) {
@@ -6,6 +6,7 @@ import { suffixWindowsReservedDeviceName } from "./filename.js";
6
6
  import { sameFileIdentityForCleanup } from "./file-identity.js";
7
7
  import { assertSafePathSegment, sanitizeSafePathSegment, trimHyphenEdges } from "./safe-path-segment.js";
8
8
  import { resolveSecureTempRoot } from "./secure-temp-dir.js";
9
+ import { hasNodeErrorCode } from "./path.js";
9
10
  import { registerTempPathForExit } from "./temp-cleanup.js";
10
11
  import { assertNoWindowsPathAlias, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
11
12
  const HYPHEN_CHAR_CODE = 0x2d;
@@ -76,12 +77,6 @@ export function buildRandomTempFilePath(params) {
76
77
  assertNoWindowsPathAlias(filePath, "filesystem", "temp file path uses a Windows filesystem namespace alias");
77
78
  return filePath;
78
79
  }
79
- function isNodeErrorWithCode(err, code) {
80
- return (typeof err === "object" &&
81
- err !== null &&
82
- "code" in err &&
83
- err.code === code);
84
- }
85
80
  async function cleanupTempDir(dir, identity, onCleanupError) {
86
81
  try {
87
82
  const current = fsSync.lstatSync(dir, { bigint: true });
@@ -91,7 +86,7 @@ async function cleanupTempDir(dir, identity, onCleanupError) {
91
86
  await fs.rm(dir, { recursive: true, force: true });
92
87
  }
93
88
  catch (err) {
94
- if (!isNodeErrorWithCode(err, "ENOENT")) {
89
+ if (!hasNodeErrorCode(err, "ENOENT")) {
95
90
  onCleanupError?.(err);
96
91
  }
97
92
  }
@@ -174,7 +169,7 @@ export async function createOwnedTempFile(params) {
174
169
  await owner.cleanup();
175
170
  }
176
171
  catch (err) {
177
- if (!isNodeErrorWithCode(err, "ENOENT")) {
172
+ if (!hasNodeErrorCode(err, "ENOENT")) {
178
173
  params.onCleanupError?.(err);
179
174
  }
180
175
  }
@@ -4,10 +4,9 @@ import path from "node:path";
4
4
  import { inspectDirectoryIdentitySync, observeDirectoryIdentitySync, } from "./directory-guard.js";
5
5
  import { admitTempWorkspaceChild, admitTempWorkspaceChildSync, assertTrustedTempWorkspaceDirectory, inspectTempWorkspaceDescriptorIdentitySync, projectTempWorkspaceNumericIdentity, TEMP_WORKSPACE_NUMERIC_IDENTITY_REPLAY, } from "./temp-workspace-child-admission.js";
6
6
  import { FsSafeError } from "./errors.js";
7
- import { recordFileObservationFailure } from "./file-observation.js";
8
7
  import { realpathSync } from "./realpath.js";
9
8
  import { assertNoWindowsPathAlias, pathForWindowsFilesystem, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
10
- import { inspectFileIdentitySync } from "./strict-file-identity.js";
9
+ import { fileIdentityMismatchError, inspectFileIdentitySync } from "./strict-file-identity.js";
11
10
  const WINDOWS = process.platform === "win32";
12
11
  function effectiveOwner() {
13
12
  // Windows mode/uid fields do not describe ACL authority. The supplied root's
@@ -72,11 +71,6 @@ function assertCanonicalRoot(entry) {
72
71
  throw new FsSafeError("path-mismatch", "temp workspace root ancestry changed");
73
72
  }
74
73
  }
75
- function identityMismatch() {
76
- const error = new FsSafeError("path-mismatch", "file identity changed or could not be verified");
77
- recordFileObservationFailure(error, "identity");
78
- throw error;
79
- }
80
74
  function inspectSnapshotIdentity(entry) {
81
75
  const expected = entry.numericIdentity;
82
76
  if (!expected) {
@@ -87,21 +81,21 @@ function inspectSnapshotIdentity(entry) {
87
81
  const devKnown = Number.isSafeInteger(stat.dev) && stat.dev >= 0 && (!WINDOWS || stat.dev !== 0);
88
82
  if (devKnown) {
89
83
  if (stat.dev !== expected.dev)
90
- identityMismatch();
84
+ throw fileIdentityMismatchError();
91
85
  }
92
86
  else if (WINDOWS)
93
87
  requiresExactRetry = true;
94
88
  else
95
- identityMismatch();
89
+ throw fileIdentityMismatchError();
96
90
  const inoKnown = Number.isSafeInteger(stat.ino) && stat.ino >= 0 && (!WINDOWS || stat.ino !== 0);
97
91
  if (inoKnown) {
98
92
  if (stat.ino !== expected.ino)
99
- identityMismatch();
93
+ throw fileIdentityMismatchError();
100
94
  }
101
95
  else if (WINDOWS)
102
96
  requiresExactRetry = true;
103
97
  else
104
- identityMismatch();
98
+ throw fileIdentityMismatchError();
105
99
  if (!requiresExactRetry)
106
100
  return stat;
107
101
  // Read exact identity only once. The strict helper may re-check this constant
@@ -157,7 +151,7 @@ function canonicalAncestry(root) {
157
151
  }
158
152
  function exactIdentityMatches(current, expected) {
159
153
  if (current.dev !== expected.dev || current.ino !== expected.ino)
160
- identityMismatch();
154
+ throw fileIdentityMismatchError();
161
155
  }
162
156
  function associateTempWorkspaceRoot(entry, ownerUid, descriptorFd) {
163
157
  const stat = inspectTempWorkspaceDescriptorIdentitySync(descriptorFd, entry.identity, entry.numericIdentity);
@@ -2,8 +2,7 @@ import fsSync, {} from "node:fs";
2
2
  import { inspectDirectoryIdentitySync, observeDirectoryIdentitySync } from "./directory-guard.js";
3
3
  import { pinNodeDirectoryForMode, pinNodeDirectoryForModeSync } from "./directory-mode-node.js";
4
4
  import { FsSafeError } from "./errors.js";
5
- import { recordFileObservationFailure } from "./file-observation.js";
6
- import { inspectFileIdentitySync } from "./strict-file-identity.js";
5
+ import { fileIdentityMismatchError, inspectFileIdentitySync } from "./strict-file-identity.js";
7
6
  export const TEMP_WORKSPACE_NUMERIC_IDENTITY_REPLAY = process.platform === "linux" || process.platform === "darwin";
8
7
  export function projectTempWorkspaceNumericIdentity(identity) {
9
8
  const dev = Number(identity.dev);
@@ -14,15 +13,10 @@ export function projectTempWorkspaceNumericIdentity(identity) {
14
13
  }
15
14
  return Object.freeze({ dev, ino });
16
15
  }
17
- function identityMismatch() {
18
- const error = new FsSafeError("path-mismatch", "file identity changed or could not be verified");
19
- recordFileObservationFailure(error, "identity");
20
- throw error;
21
- }
22
16
  function inspectNumericIdentity(current, expected) {
23
17
  if (!Number.isSafeInteger(current.dev) || current.dev < 0 || current.dev !== expected.dev ||
24
18
  !Number.isSafeInteger(current.ino) || current.ino < 0 || current.ino !== expected.ino) {
25
- identityMismatch();
19
+ throw fileIdentityMismatchError();
26
20
  }
27
21
  return current;
28
22
  }
@@ -1,7 +1,6 @@
1
1
  import fsSync from "node:fs";
2
2
  import fs from "node:fs/promises";
3
- import { nodeDirectorySearchOnlyFlags } from "./directory-mode-node.js";
4
- import { assertOwnedDirectory } from "./directory-mode-owner.js";
3
+ import { nodeDirectorySearchOnlyFlags, assertOwnedDirectory } from "./directory-mode-node.js";
5
4
  import { FsSafeError } from "./errors.js";
6
5
  import { assertNoWindowsPathAlias, pathForWindowsFilesystem, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
7
6
  import { inspectFileIdentitySync } from "./strict-file-identity.js";
@@ -1,9 +1,9 @@
1
- import { type PermissionCommandFailure } from "./permission-exec.js";
1
+ import { type PermissionFailureFields } from "./permission-exec.js";
2
2
  export type WindowsOwnerExec = (command: string, args: string[]) => Promise<{
3
3
  stdout: string;
4
4
  stderr: string;
5
5
  }>;
6
- export type WindowsOwnerSummary = {
6
+ export type WindowsOwnerSummary = Omit<PermissionFailureFields & {
7
7
  sid?: string;
8
8
  currentUserSid?: string;
9
9
  daclPresent?: boolean;
@@ -11,10 +11,7 @@ export type WindowsOwnerSummary = {
11
11
  aclError?: string;
12
12
  remote?: boolean;
13
13
  trusted?: boolean;
14
- error?: string;
15
- errorDetail?: PermissionCommandFailure;
16
- errorCause?: unknown;
17
- };
14
+ }, never>;
18
15
  export type WindowsOwnerAce = {
19
16
  sid: string;
20
17
  mask: number;
@@ -2,8 +2,6 @@
2
2
  // descriptor never passes through CommonSecurityDescriptor's ACE normalization.
3
3
  using System;
4
4
  using System.Collections.Generic;
5
- using System.ComponentModel;
6
- using System.IO;
7
5
  using System.Runtime.InteropServices;
8
6
  using System.Security.AccessControl;
9
7
  using System.Security.Principal;
@@ -113,6 +113,10 @@ If an already validated descriptor fails while reading, both readers throw an
113
113
  operational `FsSafeError` with `code: "read-failed"`; inspect `cause` for the
114
114
  underlying Node filesystem code such as `EIO`.
115
115
 
116
+ Caught `null` or `undefined` inspection and read failures are reported as
117
+ structured errors with an `Error` cause carrying `"null"` or `"undefined"`,
118
+ instead of an internal `TypeError` while inspecting the thrown value.
119
+
116
120
  A synchronous reader closes its descriptor once. A close failure preserves an
117
121
  earlier read or identity-validation error; after a successful read, the close
118
122
  failure is reported before trimming or rejecting empty content.
@@ -137,6 +141,11 @@ startWebhookVerifier(signingKey);
137
141
 
138
142
  Async. Creates the parent directory at `dirMode` (default `0o700`) if missing, writes content to a sibling temp file, finalizes `mode` (default `0o600`) through an owned descriptor after content writes, and atomically renames over the destination. Publication verification checks the final file identity and mode.
139
143
 
144
+ Both secret writers capture top-level parameter values when called, before
145
+ asynchronous filesystem preparation; a supplied byte buffer is captured by
146
+ reference. A parameter getter throwing `null` or `undefined` rejects with that
147
+ same value before filesystem inspection.
148
+
140
149
  On POSIX, both native and JavaScript writers verify actual `0o600` permission
141
150
  bits through the retained descriptor before writing content. A filesystem that
142
151
  reports successful chmod without enforcing those bits fails with
@@ -267,6 +267,8 @@ Pass `lockRoot` to place sidecar create, read, verification, and removal behind
267
267
  an existing `Root` capability. `lockPath` must resolve inside that root.
268
268
  Identity-conditioned removal remains the only release and reclaim deletion
269
269
  path.
270
+ Root mutation refusals during stale removal propagate unchanged, including
271
+ `null`, `undefined`, and errors carrying `ENOENT`; they do not become missing-sidecar retries.
270
272
 
271
273
  Async Root-backed acquisition normalizes the target's parent without creating
272
274
  it, checking the retained Root before and after normalization. A deleted or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe",
3
- "version": "0.18.1",
3
+ "version": "0.18.2",
4
4
  "description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
5
5
  "keywords": [
6
6
  "filesystem",
@@ -169,13 +169,13 @@
169
169
  "archive:producer-smoke": "node scripts/archive-producer-smoke.mjs"
170
170
  },
171
171
  "optionalDependencies": {
172
- "@openclaw/fs-safe-darwin-arm64": "0.18.1",
173
- "@openclaw/fs-safe-darwin-x64": "0.18.1",
174
- "@openclaw/fs-safe-linux-arm64-gnu": "0.18.1",
175
- "@openclaw/fs-safe-linux-arm64-musl": "0.18.1",
176
- "@openclaw/fs-safe-linux-x64-gnu": "0.18.1",
177
- "@openclaw/fs-safe-linux-x64-musl": "0.18.1",
178
- "@openclaw/fs-safe-win32-x64-msvc": "0.18.1",
172
+ "@openclaw/fs-safe-darwin-arm64": "0.18.2",
173
+ "@openclaw/fs-safe-darwin-x64": "0.18.2",
174
+ "@openclaw/fs-safe-linux-arm64-gnu": "0.18.2",
175
+ "@openclaw/fs-safe-linux-arm64-musl": "0.18.2",
176
+ "@openclaw/fs-safe-linux-x64-gnu": "0.18.2",
177
+ "@openclaw/fs-safe-linux-x64-musl": "0.18.2",
178
+ "@openclaw/fs-safe-win32-x64-msvc": "0.18.2",
179
179
  "jszip": "^3.10.2"
180
180
  },
181
181
  "devDependencies": {
@@ -1,20 +0,0 @@
1
- import type { BigIntStats, Stats } from "node:fs";
2
- export type DirectoryModeChecks = {
3
- check?: () => void;
4
- beforeChmod?: () => Promise<void>;
5
- };
6
- export type DirectoryModeOwner = {
7
- verify(check?: () => void): Promise<void>;
8
- apply(mode: number, checks?: DirectoryModeChecks): Promise<void>;
9
- close(): Promise<void>;
10
- };
11
- export declare function assertOwnedDirectory(expected: Stats | BigIntStats, actual: Stats | BigIntStats): void;
12
- /** Serializes use and close: even a queued path-based fd operation retains its descriptor. */
13
- export declare function ownDirectoryMode(params: {
14
- inspect: () => Promise<number>;
15
- chmod: (mode: number) => Promise<void>;
16
- prepareChmod?: () => Promise<void>;
17
- verifyChmod?: () => Promise<void>;
18
- close: () => Promise<void>;
19
- ignoreChmodError?: boolean;
20
- }): DirectoryModeOwner;
@@ -1,78 +0,0 @@
1
- import { FsSafeError } from "./errors.js";
2
- import { sameFileIdentityForCleanup } from "./file-identity.js";
3
- export function assertOwnedDirectory(expected, actual) {
4
- if (actual.isSymbolicLink() || !actual.isDirectory()) {
5
- throw new FsSafeError("not-file", "directory mode target must be a real directory");
6
- }
7
- if (!sameFileIdentityForCleanup(expected, actual)) {
8
- throw new FsSafeError("path-mismatch", "directory changed before its mode could be applied");
9
- }
10
- }
11
- /** Serializes use and close: even a queued path-based fd operation retains its descriptor. */
12
- export function ownDirectoryMode(params) {
13
- let pending = Promise.resolve();
14
- let closing;
15
- const enqueue = (run) => {
16
- if (closing)
17
- return Promise.reject(new FsSafeError("path-mismatch", "directory mode owner is closed"));
18
- const operation = pending.then(run);
19
- pending = operation.catch(() => undefined);
20
- return operation;
21
- };
22
- return {
23
- verify: (check) => enqueue(async () => {
24
- check?.();
25
- await params.inspect();
26
- check?.();
27
- }),
28
- apply: (mode, checks = {}) => enqueue(async () => {
29
- // inspect() reports permission bits only; chmod ignores file-type bits,
30
- // so tolerate raw stat modes (e.g. S_IFDIR | 0o755) by masking up front.
31
- mode &= 0o7777;
32
- checks.check?.();
33
- const currentMode = await params.inspect();
34
- checks.check?.();
35
- if (currentMode === mode && !checks.beforeChmod && !checks.check)
36
- return;
37
- if (currentMode !== mode) {
38
- await params.prepareChmod?.();
39
- checks.check?.();
40
- }
41
- await checks.beforeChmod?.();
42
- checks.check?.();
43
- // Hooks/ancestor checks can yield; recheck the original named association.
44
- await params.inspect();
45
- checks.check?.();
46
- let dispatchDeadlineFailure;
47
- if (currentMode !== mode) {
48
- checks.check?.();
49
- try {
50
- await params.chmod(mode);
51
- }
52
- catch (error) {
53
- if (!params.ignoreChmodError)
54
- throw error;
55
- }
56
- // Expiry cannot release the fd while post-dispatch verification is pending.
57
- try {
58
- checks.check?.();
59
- }
60
- catch (error) {
61
- dispatchDeadlineFailure = { error };
62
- }
63
- await params.verifyChmod?.();
64
- }
65
- const finalMode = await params.inspect();
66
- if (dispatchDeadlineFailure)
67
- throw dispatchDeadlineFailure.error;
68
- checks.check?.();
69
- if (!params.ignoreChmodError && finalMode !== mode) {
70
- throw new FsSafeError("path-mismatch", "directory final mode could not be verified");
71
- }
72
- }),
73
- close() {
74
- closing ??= pending.then(params.close);
75
- return closing;
76
- },
77
- };
78
- }