@openclaw/fs-safe 0.18.1 → 0.19.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 (94) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/advanced.d.ts +2 -1
  3. package/dist/advanced.js +1 -1
  4. package/dist/archive-durability.js +1 -1
  5. package/dist/archive-merge.js +1 -2
  6. package/dist/archive-staging.d.ts +1 -2
  7. package/dist/archive-staging.js +3 -3
  8. package/dist/archive-zip-preflight.d.ts +0 -2
  9. package/dist/archive-zip-preflight.js +0 -1
  10. package/dist/archive.d.ts +6 -3
  11. package/dist/archive.js +5 -3
  12. package/dist/directory-guard.d.ts +0 -1
  13. package/dist/directory-guard.js +0 -3
  14. package/dist/directory-mode-node.d.ts +20 -2
  15. package/dist/directory-mode-node.js +77 -1
  16. package/dist/file-lock-sync-acquisition.js +1 -1
  17. package/dist/file-lock-sync-root-held.d.ts +4 -11
  18. package/dist/file-lock-sync-root-io.d.ts +1 -4
  19. package/dist/file-lock-sync-root-options.d.ts +2 -10
  20. package/dist/file-lock-sync-root.d.ts +2 -4
  21. package/dist/file-store-boundary.d.ts +3 -7
  22. package/dist/file-store-boundary.js +7 -10
  23. package/dist/file-store-prune.js +3 -2
  24. package/dist/file-store.js +19 -21
  25. package/dist/guest-native-python.js +23 -31
  26. package/dist/guest.js +21 -12
  27. package/dist/json-durable-queue-ownership.d.ts +1 -5
  28. package/dist/json-durable-queue.d.ts +0 -1
  29. package/dist/json-durable-queue.js +2 -7
  30. package/dist/local-file-descriptor.d.ts +2 -5
  31. package/dist/local-roots.d.ts +2 -7
  32. package/dist/native-binding.d.ts +12 -13
  33. package/dist/permission-exec.d.ts +6 -0
  34. package/dist/permissions-public.d.ts +2 -1
  35. package/dist/permissions-windows.d.ts +3 -6
  36. package/dist/permissions.d.ts +3 -10
  37. package/dist/permissions.js +0 -1
  38. package/dist/pinned-mutation-shared-route.d.ts +2 -8
  39. package/dist/pinned-open.d.ts +1 -0
  40. package/dist/pinned-open.js +1 -1
  41. package/dist/replace-directory.js +5 -17
  42. package/dist/replace-file-copy-fallback.d.ts +1 -8
  43. package/dist/replace-file-descriptor.d.ts +3 -11
  44. package/dist/replace-file-descriptor.js +1 -1
  45. package/dist/retained-directory-replacement.d.ts +1 -0
  46. package/dist/retained-directory-replacement.js +1 -1
  47. package/dist/root-context.js +3 -2
  48. package/dist/root-file-final-admission.d.ts +1 -1
  49. package/dist/root-file-final-admission.js +1 -6
  50. package/dist/root-file.js +2 -5
  51. package/dist/root-move-noreplace.d.ts +2 -7
  52. package/dist/root-paths.d.ts +2 -6
  53. package/dist/root-remove-identity.d.ts +1 -3
  54. package/dist/root-walk.js +8 -3
  55. package/dist/root-write-admission.js +1 -4
  56. package/dist/root-write-complete-parent.d.ts +2 -0
  57. package/dist/root-write-complete-parent.js +1 -1
  58. package/dist/secret-file.d.ts +6 -2
  59. package/dist/secret-file.js +12 -16
  60. package/dist/secret-read-policy.js +1 -1
  61. package/dist/secure-file-windows.js +5 -14
  62. package/dist/secure-temp-dir.js +2 -7
  63. package/dist/sidecar-lock-acquire.js +1 -1
  64. package/dist/sidecar-lock-admission-parser.d.ts +1 -2
  65. package/dist/sidecar-lock-handle.d.ts +2 -8
  66. package/dist/sidecar-lock-policy.d.ts +2 -7
  67. package/dist/sidecar-lock-reclaim.js +5 -6
  68. package/dist/sidecar-lock-stale-admission.d.ts +1 -5
  69. package/dist/stat-observation.js +3 -10
  70. package/dist/strict-file-identity.d.ts +2 -0
  71. package/dist/strict-file-identity.js +6 -6
  72. package/dist/temp-target.js +3 -8
  73. package/dist/temp-workspace-admission.js +6 -12
  74. package/dist/temp-workspace-child-admission.js +2 -8
  75. package/dist/temp-workspace-descriptor.js +1 -2
  76. package/dist/test-hooks.d.ts +1 -1
  77. package/dist/windows-owner.d.ts +3 -6
  78. package/dist/windows-security-bridge.cs +0 -2
  79. package/dist/windows-security-command.js +1 -4
  80. package/dist/windows-security-facts.d.ts +1 -0
  81. package/dist/windows-security-facts.js +2 -2
  82. package/docs/archive.md +2 -0
  83. package/docs/copy.md +2 -0
  84. package/docs/file-store.md +5 -0
  85. package/docs/guest.md +7 -1
  86. package/docs/native-helper.md +6 -0
  87. package/docs/root.md +10 -1
  88. package/docs/secret-file.md +19 -0
  89. package/docs/sidecar-lock.md +2 -0
  90. package/docs/walk.md +12 -0
  91. package/docs/writing.md +11 -0
  92. package/package.json +8 -8
  93. package/dist/directory-mode-owner.d.ts +0 -20
  94. package/dist/directory-mode-owner.js +0 -78
@@ -1,6 +1,6 @@
1
1
  import syncFs, { type BigIntStats } from "node:fs";
2
2
  import fs, { type FileHandle } from "node:fs/promises";
3
- import { type DirectoryModeOwner } from "./directory-mode-owner.js";
3
+ import { type DirectoryModeOwner } from "./directory-mode-node.js";
4
4
  type AsyncTempFileSystem = Pick<typeof fs, "lstat" | "open" | "writeFile">;
5
5
  type SyncTempFileSystem = Pick<typeof syncFs, "closeSync" | "fstatSync" | "fsyncSync" | "lstatSync" | "openSync" | "writeFileSync">;
6
6
  export type SyncFchmod = (fd: number, mode: number) => void;
@@ -12,11 +12,8 @@ export declare function pinDirectoryForMode(params: {
12
12
  /** Compatibility for best-effort directory modes; admission and close still fail closed. */
13
13
  ignoreChmodError?: boolean;
14
14
  }): Promise<DirectoryModeOwner | undefined>;
15
- export declare function applyDirectoryMode(params: {
16
- fsModule: AsyncTempFileSystem;
17
- dirPath: string;
15
+ export declare function applyDirectoryMode(params: Parameters<typeof pinDirectoryForMode>[0] & {
18
16
  mode: number;
19
- ignoreChmodError?: boolean;
20
17
  }): Promise<void>;
21
18
  export declare function applyDirectoryModeSync(params: {
22
19
  fsModule: SyncTempFileSystem;
@@ -35,14 +32,9 @@ export declare function writeTempFile(params: {
35
32
  handle: FileHandle;
36
33
  identity: BigIntStats;
37
34
  }>;
38
- export declare function writeTempFileSync(params: {
35
+ export declare function writeTempFileSync(params: Omit<Parameters<typeof writeTempFile>[0], "fsModule"> & {
39
36
  fsModule: SyncTempFileSystem;
40
- tempPath: string;
41
- content: string | Uint8Array;
42
- mode: number;
43
37
  fchmodSync?: SyncFchmod;
44
- sync: boolean;
45
- onIdentity?: (identity: BigIntStats) => void;
46
38
  }): {
47
39
  fd: number;
48
40
  identity: BigIntStats;
@@ -3,7 +3,7 @@ import syncFs, {} from "node:fs";
3
3
  import fs, {} from "node:fs/promises";
4
4
  import { FsSafeError } from "./errors.js";
5
5
  import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
6
- import { ownDirectoryMode } from "./directory-mode-owner.js";
6
+ import { ownDirectoryMode } from "./directory-mode-node.js";
7
7
  export async function syncDirectoryBestEffort(fsModule, dirPath) {
8
8
  let handle;
9
9
  try {
@@ -6,6 +6,7 @@ export type RetainedReplacementParent = Readonly<{
6
6
  realPath: string;
7
7
  identity: Pick<BigIntStats, "dev" | "ino">;
8
8
  }>;
9
+ export declare function copyOperationalCode(target: Error, source: unknown): void;
9
10
  export declare class RetainedDirectoryReplacement {
10
11
  #private;
11
12
  private constructor();
@@ -7,7 +7,7 @@ function helperUnavailable(message, cause) {
7
7
  ...(cause instanceof Error ? { cause } : {}),
8
8
  });
9
9
  }
10
- function copyOperationalCode(target, source) {
10
+ export function copyOperationalCode(target, source) {
11
11
  const code = source?.code;
12
12
  if (typeof code === "string") {
13
13
  Object.defineProperty(target, "code", { configurable: true, value: code });
@@ -107,7 +107,7 @@ export function rootRelativeReadPath(root, filePath) {
107
107
  resolveCandidateRoot: base === root.rootDir && root.rootDir !== root.rootReal,
108
108
  });
109
109
  if (admitted)
110
- return admitted.relativePath;
110
+ return `.${path.sep}${admitted.relativePath}`;
111
111
  continue;
112
112
  }
113
113
  const prefix = ensureTrailingSep(base);
@@ -116,7 +116,8 @@ export function rootRelativeReadPath(root, filePath) {
116
116
  let start = prefix.length;
117
117
  while (raw[start] === path.sep)
118
118
  start += 1;
119
- return raw.slice(start);
119
+ // An admitted absolute tail stays literal, including a leading home marker.
120
+ return `.${path.sep}${raw.slice(start)}`;
120
121
  }
121
122
  }
122
123
  return raw;
@@ -1,5 +1,5 @@
1
1
  import fs from "node:fs";
2
- import type { PinnedOpenSyncFinalAdmission } from "./pinned-open.js";
2
+ import { type PinnedOpenSyncFinalAdmission } from "./pinned-open.js";
3
3
  export type RootFileAdmissionFs = Pick<typeof fs, "lstatSync" | "realpathSync">;
4
4
  type ExactIdentity = Readonly<{
5
5
  dev: bigint;
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import { FsSafeError } from "./errors.js";
3
+ import { isExpectedPathError } from "./pinned-open.js";
3
4
  import { realpathSync } from "./realpath.js";
4
5
  import { admitPathInsideRoot } from "./root-boundary.js";
5
6
  import { hardlinkedPathNotAllowedError } from "./root-errors.js";
@@ -8,12 +9,6 @@ import { assertNoWindowsPathAlias, pathForWindowsFilesystem } from "./windows-pa
8
9
  function rootIdentityMismatch(cause) {
9
10
  return new FsSafeError("path-mismatch", "canonical root directory identity changed or could not be verified", cause === undefined ? {} : { cause });
10
11
  }
11
- function isExpectedPathError(error) {
12
- const code = typeof error === "object" && error !== null && "code" in error
13
- ? String(error.code)
14
- : "";
15
- return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
16
- }
17
12
  function inspectCanonicalRoot(ioFs, rootPath, expected) {
18
13
  try {
19
14
  assertNoWindowsPathAlias(rootPath);
package/dist/root-file.js CHANGED
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { resolveRootPathSyncWithCanonicalRootObservation, resolveRootPathWithCanonicalRootObservation, } from "./root-path.js";
4
4
  import { readSymlinkResolution } from "./root-symlink-policy.js";
5
- import { openPinnedFileSync, } from "./pinned-open.js";
5
+ import { isExpectedPathError, openPinnedFileSync, } from "./pinned-open.js";
6
6
  import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
7
7
  import { FsSafeError } from "./errors.js";
8
8
  import { createRootFileFinalAdmission, observeCanonicalRoot, } from "./root-file-final-admission.js";
@@ -198,10 +198,7 @@ function snapshotAsyncResolution(params, absolutePath) {
198
198
  function toRootObservationError(error) {
199
199
  if (error instanceof FsSafeError)
200
200
  return toBoundaryValidationError(error);
201
- const code = typeof error === "object" && error !== null && "code" in error
202
- ? String(error.code)
203
- : "";
204
- if (code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP") {
201
+ if (isExpectedPathError(error)) {
205
202
  return { ok: false, reason: "path", error };
206
203
  }
207
204
  return { ok: false, reason: "io", error };
@@ -1,13 +1,8 @@
1
1
  import { type BigIntStats, type Stats } from "node:fs";
2
- import { type DenyMutationPolicy } from "./deny-mutations.js";
2
+ import type { RootMoveOptions } from "./root-options.js";
3
3
  import { type RootContext } from "./root-context.js";
4
- import { type MutationSymlinkPolicy } from "./root-symlink-policy.js";
5
4
  export declare function admitMoveSourceStat<T extends Stats | BigIntStats>(stat: T, overwrite?: boolean): T;
6
- export declare function movePathNoReplaceNative(root: RootContext, params: {
7
- assertBeforeMutation?: () => void;
8
- denyMutations?: DenyMutationPolicy;
9
- mutationSymlinks?: MutationSymlinkPolicy;
10
- }, paths: {
5
+ export declare function movePathNoReplaceNative(root: RootContext, params: Pick<RootMoveOptions, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks">, paths: {
11
6
  sourcePath: string;
12
7
  sourceParentPath: string;
13
8
  targetPath: string;
@@ -1,3 +1,4 @@
1
+ import { type ResolvePathWithinRootParams } from "./path-scope-lexical.js";
1
2
  import { type DirectoryResult } from "./root-directory.js";
2
3
  export { resolvePathWithinRoot } from "./root-paths-lexical.js";
3
4
  export { ensureDirectoryWithinRoot } from "./root-directory.js";
@@ -44,12 +45,7 @@ export type PathScope = {
44
45
  mode?: number;
45
46
  }): Promise<DirectoryResult>;
46
47
  };
47
- export declare function resolveWritablePathWithinRoot(params: {
48
- rootDir: string;
49
- requestedPath: string;
50
- scopeLabel: string;
51
- defaultFileName?: string;
52
- }): Promise<{
48
+ export declare function resolveWritablePathWithinRoot(params: ResolvePathWithinRootParams): Promise<{
53
49
  ok: true;
54
50
  path: string;
55
51
  } | {
@@ -1,9 +1,7 @@
1
1
  import type { BigIntStats } from "node:fs";
2
2
  type ExactDirectoryIdentity = Readonly<Pick<BigIntStats, "dev" | "ino">>;
3
- export type RemovalDirectoryAssertion = Readonly<{
3
+ export type RemovalDirectoryAssertion = ExactDirectoryIdentity & Readonly<{
4
4
  path: string;
5
- dev: bigint;
6
- ino: bigint;
7
5
  numericDev?: number;
8
6
  numericIno?: number;
9
7
  platform: NodeJS.Platform;
package/dist/root-walk.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { FsSafeError } from "./errors.js";
3
+ import { expandRelativePathWithHome } from "./root-context.js";
3
4
  import { resolveRootPath, ROOT_PATH_ALIAS_POLICIES } from "./root-path.js";
4
5
  import { createSuppressedError } from "./suppressed-error.js";
5
6
  function validateBudget(name, value) {
@@ -65,9 +66,10 @@ export async function* walkRoot(root, relativePath, options) {
65
66
  options.signal?.throwIfAborted();
66
67
  let listing;
67
68
  try {
69
+ const expandedDirectory = depth === 0 ? await expandRelativePathWithHome(directory) : directory;
68
70
  const skipChildSymlinks = depth > 0 && options.symlinkPolicy === "skip";
69
71
  const resolvedDirectory = await resolveRootPath({
70
- absolutePath: path.resolve(root.rootReal, directory),
72
+ absolutePath: path.resolve(root.rootReal, expandedDirectory),
71
73
  rootPath: root.rootReal,
72
74
  rootCanonicalPath: root.rootReal,
73
75
  boundaryLabel: "root walk",
@@ -87,7 +89,10 @@ export async function* walkRoot(root, relativePath, options) {
87
89
  .relative(root.rootReal, resolvedDirectory.canonicalPath)
88
90
  .split(path.sep)
89
91
  .join(path.posix.sep);
90
- listing = await root.list(listingDirectory, {
92
+ if (depth === 0 && expandedDirectory !== directory)
93
+ directory = listingDirectory;
94
+ // Resolved filesystem names are literal, not caller home-directory shorthand.
95
+ listing = await root.list(`./${listingDirectory}`, {
91
96
  order: options.order ?? "sorted",
92
97
  signal: options.signal,
93
98
  snapshot: maxEntries === Number.POSITIVE_INFINITY,
@@ -139,7 +144,7 @@ export async function* walkRoot(root, relativePath, options) {
139
144
  if (!resolved.exists) {
140
145
  continue;
141
146
  }
142
- const target = await root.stat(path.relative(root.rootReal, resolved.canonicalPath));
147
+ const target = await root.stat(`./${path.relative(root.rootReal, resolved.canonicalPath)}`);
143
148
  kind = target.isDirectory ? "directory" : target.isFile ? "file" : "other";
144
149
  size = target.size;
145
150
  }
@@ -14,7 +14,7 @@ import { mutationSymlinkResolution, } from "./root-symlink-policy.js";
14
14
  import { inspectFileIdentitySync } from "./strict-file-identity.js";
15
15
  import { realpathSync } from "./realpath.js";
16
16
  import { getFsSafeTestHooks } from "./test-hooks.js";
17
- import { assertPreparedRootWriteParentCurrent, } from "./root-write-complete-parent.js";
17
+ import { assertPreparedRootWriteParentCurrent, writeSelectionChanged, } from "./root-write-complete-parent.js";
18
18
  export function createRootWriteSelectionForFd(selection, fd) {
19
19
  const stat = inspectFileIdentitySync(() => fsSync.fstatSync(fd, { bigint: true }));
20
20
  return Object.freeze({
@@ -22,9 +22,6 @@ export function createRootWriteSelectionForFd(selection, fd) {
22
22
  identity: Object.freeze({ dev: stat.dev, ino: stat.ino }),
23
23
  });
24
24
  }
25
- function writeSelectionChanged(cause) {
26
- return new FsSafeError("path-mismatch", "write target changed during operation", errorCauseOptions(cause));
27
- }
28
25
  function inspectRegularSelectionPath(pathname, expected, follow) {
29
26
  let observed;
30
27
  try {
@@ -1,5 +1,6 @@
1
1
  import type { BigIntStats } from "node:fs";
2
2
  import { type AsyncDirectoryGuard } from "./directory-guard.js";
3
+ import { FsSafeError } from "./errors.js";
3
4
  import type { RootContext } from "./root-context.js";
4
5
  import type { GuardedRootWriteTarget } from "./root-write-admission.js";
5
6
  type PreparedWriteTargetObservation = Readonly<{
@@ -36,6 +37,7 @@ export type SharedRootWriteTarget = Readonly<{
36
37
  mutationAdmission: GuardedRootWriteTarget["mutationAdmission"];
37
38
  preparedParent?: PreparedRootWriteParent;
38
39
  }>;
40
+ export declare function writeSelectionChanged(cause?: unknown): FsSafeError;
39
41
  export declare function assertPreparedRootWriteParentCurrent(prepared: PreparedRootWriteParent, verifyTarget?: boolean): void;
40
42
  export declare function prepareSharedRootWriteTarget(root: RootContext, params: SharedRootWriteTargetParams): Promise<SharedRootWriteTarget>;
41
43
  export {};
@@ -12,7 +12,7 @@ import { errorCauseOptions } from "./root-errors.js";
12
12
  import { canReuseParentWithMutationAssertion } from "./root-write-lock-binding.js";
13
13
  import { inspectFileIdentitySync } from "./strict-file-identity.js";
14
14
  import { getFsSafeTestHooks } from "./test-hooks.js";
15
- function writeSelectionChanged(cause) {
15
+ export function writeSelectionChanged(cause) {
16
16
  return new FsSafeError("path-mismatch", "write target changed during operation", errorCauseOptions(cause));
17
17
  }
18
18
  function ordinarySharedWriteRoute(root, relativePath, operationTargetPath) {
@@ -13,7 +13,11 @@ type SecretFileWriteParams = {
13
13
  dirMode?: number;
14
14
  durable?: boolean;
15
15
  };
16
- export declare function prepareSecretFileWrite(params: Omit<SecretFileWriteParams, "content">): Promise<{
16
+ type SecretFileCreateParams = Omit<SecretFileWriteParams, "durable"> & {
17
+ /** "file" requires file synchronization; directory synchronization remains best effort. */
18
+ durable?: boolean | "file";
19
+ };
20
+ export declare function prepareSecretFileWrite(params: Pick<SecretFileWriteParams, "rootDir" | "filePath" | "mode" | "dirMode">): Promise<{
17
21
  mode: number;
18
22
  rootGuard: AsyncDirectoryGuard<BigIntStats>;
19
23
  parentGuard: AsyncDirectoryGuard<BigIntStats>;
@@ -21,5 +25,5 @@ export declare function prepareSecretFileWrite(params: Omit<SecretFileWriteParam
21
25
  finalFilePath: string;
22
26
  }>;
23
27
  export declare function writeSecretFileAtomic(params: SecretFileWriteParams): Promise<void>;
24
- export declare function createSecretFileAtomic(params: SecretFileWriteParams): Promise<void>;
28
+ export declare function createSecretFileAtomic(params: SecretFileCreateParams): Promise<void>;
25
29
  export {};
@@ -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,
@@ -282,6 +285,7 @@ async function materializeSecretFileAtomic(params, createOnly) {
282
285
  mode,
283
286
  verifyPosixMode: true,
284
287
  sync: params.durable !== false,
288
+ strictFileSync: createOnly && params.durable === "file",
285
289
  overwrite: !createOnly,
286
290
  input: { kind: "buffer", data: typeof params.content === "string" ? params.content : Buffer.from(params.content) },
287
291
  rootIdentity: { dev: parentGuard.stat.dev, ino: parentGuard.stat.ino },
@@ -305,31 +309,23 @@ async function materializeSecretFileAtomic(params, createOnly) {
305
309
  });
306
310
  }
307
311
  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);
312
+ const ownedParams = snapshotSecretFileWriteParams(params);
313
+ const canonicalPath = await secretFileWriteQueueKey(ownedParams.filePath);
314
314
  await serializePathWrite(canonicalPath, async () => {
315
315
  await materializeSecretFileAtomic(ownedParams, false);
316
316
  });
317
317
  }
318
318
  export async function createSecretFileAtomic(params) {
319
319
  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);
320
+ const ownedParams = snapshotSecretFileWriteParams(params);
321
+ const canonicalPath = await secretFileWriteQueueKey(ownedParams.filePath);
326
322
  await serializePathWrite(canonicalPath, async () => {
327
323
  await materializeSecretFileAtomic(ownedParams, true);
328
324
  });
329
325
  }
330
326
  catch (error) {
331
327
  if ((error instanceof FsSafeError && error.code === "already-exists") ||
332
- error.code === "EEXIST") {
328
+ error?.code === "EEXIST") {
333
329
  throw new FsSafeError("secret-exists", "Private secret file already exists.", { cause: error });
334
330
  }
335
331
  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,29 +1,20 @@
1
- import { FsSafeError } from "./errors.js";
2
- import { recordFileObservationFailure } from "./file-observation.js";
1
+ import { fileIdentityMismatchError } from "./strict-file-identity.js";
3
2
  import { getNativeBinding } from "./native.js";
4
3
  import { getFsSafeNativeConfig } from "./native-config.js";
5
4
  import { warnNativeFallback } from "./native-fallback-warning.js";
6
5
  import { inspectWindowsDescriptorCommand } from "./windows-security-command.js";
7
- import { validateSecureWindowsSecurityFacts } from "./windows-security-facts.js";
6
+ import { unverified as permissionUnverified, validateSecureWindowsSecurityFacts } from "./windows-security-facts.js";
8
7
  const IDENTITY_RE = /^([0-9a-f]{8}):([0-9a-f]{16})$/;
9
8
  const TRUSTED_OWNER_CLASSES = new Set(["current-user", "system", "administrators"]);
10
- function permissionUnverified(message, cause) {
11
- throw new FsSafeError("permission-unverified", message, cause === undefined ? {} : { cause });
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
9
  function isRecord(value) {
19
10
  return typeof value === "object" && value !== null;
20
11
  }
21
12
  function parseIdentity(value) {
22
13
  if (typeof value !== "string")
23
- identityMismatch();
14
+ throw fileIdentityMismatchError();
24
15
  const match = IDENTITY_RE.exec(value);
25
16
  if (!match)
26
- identityMismatch();
17
+ throw fileIdentityMismatchError();
27
18
  return {
28
19
  dev: BigInt(`0x${match[1]}`),
29
20
  ino: BigInt(`0x${match[2]}`),
@@ -34,7 +25,7 @@ function inspectDescriptorResult(params, result, mechanism) {
34
25
  permissionUnverified("Windows descriptor ACL facts were malformed");
35
26
  const observed = parseIdentity(result.identity);
36
27
  if (observed.dev !== params.identity.dev || observed.ino !== params.identity.ino) {
37
- identityMismatch();
28
+ throw fileIdentityMismatchError();
38
29
  }
39
30
  const facts = validateSecureWindowsSecurityFacts(result.security);
40
31
  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
  }
@@ -1,7 +1,7 @@
1
1
  import type { Root } from "./root-impl.js";
2
2
  import type { HeldSidecarLock } from "./sidecar-lock-admission.js";
3
3
  import { readSidecarLockRawSnapshot, type SidecarLockSnapshot } from "./sidecar-lock-reclaim.js";
4
- type SidecarAdmissionRunner = {
4
+ export type SidecarAdmissionRunner = {
5
5
  hasToken(): boolean;
6
6
  run<T>(callback: () => T): T;
7
7
  };
@@ -39,4 +39,3 @@ export declare function observeHeldSidecarParser(params: {
39
39
  parserAccessor(): ((raw: string) => unknown) | undefined;
40
40
  isTransientDenial(error: unknown): boolean;
41
41
  }): Promise<HeldSidecarParserObservation>;
42
- export {};
@@ -1,5 +1,4 @@
1
- import type { Root } from "./root-impl.js";
2
- import { type SidecarLockSnapshot } from "./sidecar-lock-reclaim.js";
1
+ import type { HeldSidecarLock } from "./sidecar-lock-admission.js";
3
2
  import type { SidecarLockHandle } from "./sidecar-lock-types.js";
4
3
  export declare function stopSidecarLockMonitoring(held: {
5
4
  compromiseTimer?: NodeJS.Timeout;
@@ -14,12 +13,7 @@ export declare function createSidecarLockHandle(params: {
14
13
  }): SidecarLockHandle;
15
14
  export declare function createHeldSidecarLockHandle(params: {
16
15
  normalizedTargetPath: string;
17
- held: {
18
- lockPath: string;
19
- snapshot: SidecarLockSnapshot;
20
- lockRoot?: Root;
21
- parsePayload?: (raw: string) => unknown;
22
- };
16
+ held: Pick<HeldSidecarLock, "lockPath" | "snapshot" | "lockRoot" | "parsePayload">;
23
17
  release: (options?: {
24
18
  retry?: boolean;
25
19
  }) => Promise<unknown>;
@@ -1,4 +1,4 @@
1
- import type { SidecarLockRetryOptions } from "./sidecar-lock-types.js";
1
+ import type { SidecarLockReclaimParams, SidecarLockRetryOptions } from "./sidecar-lock-types.js";
2
2
  export declare function validateSidecarLockRetryOptions(retry: SidecarLockRetryOptions): void;
3
3
  export declare function validateSidecarLockTimeoutMs(timeoutMs: number | undefined): void;
4
4
  export declare function validateSidecarLockStaleMs(staleMs: number | undefined): void;
@@ -11,9 +11,4 @@ export declare function sidecarLockRetryDelay(retry: SidecarLockRetryOptions, ti
11
11
  export declare const maxTransientLockDenials = 8;
12
12
  export declare function isTransientLockFileDenial(error: unknown, lockPath: string): boolean;
13
13
  export declare function sidecarLockPayloadCreatedAtMs(payload: unknown): number | null;
14
- export declare function defaultSidecarLockShouldReclaim(params: {
15
- lockPath: string;
16
- payload: unknown;
17
- staleMs: number;
18
- nowMs: number;
19
- }): Promise<boolean>;
14
+ export declare function defaultSidecarLockShouldReclaim(params: Pick<SidecarLockReclaimParams, "lockPath" | "payload" | "staleMs" | "nowMs">): Promise<boolean>;
@@ -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,7 @@
1
1
  import { type SidecarReclaimGuard } from "./sidecar-lock-reclaim.js";
2
- import { type SidecarLockParserState } from "./sidecar-lock-admission-parser.js";
2
+ import { type SidecarLockParserState, type SidecarAdmissionRunner } from "./sidecar-lock-admission-parser.js";
3
3
  import type { Root } from "./root-impl.js";
4
4
  import type { SidecarLockAcquireOptions } from "./sidecar-lock-types.js";
5
- type SidecarAdmissionRunner = {
6
- hasToken(): boolean;
7
- run<T>(callback: () => T): T;
8
- };
9
5
  type StaleOptions = Pick<SidecarLockAcquireOptions<Record<string, unknown>>, "shouldReclaim" | "shouldRemoveStaleLock" | "staleRecovery">;
10
6
  export type SidecarLockStaleOptionsState = {
11
7
  shouldReclaimObserved?: boolean;
@@ -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) {