@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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.18.2 - 2026-09-22
6
+
7
+ ### Fixes
8
+
9
+ - **Lock errors:** preserve Root stale-removal rejections and `null` or `undefined` retry failures instead of masking them with timeouts or `TypeError`s. ([#624](https://github.com/openclaw/fs-safe/pull/624))
10
+ - **Secret-file reads:** report caught `null` or `undefined` inspection and read failures as structured errors instead of internal `TypeError`s. ([#623](https://github.com/openclaw/fs-safe/pull/623))
11
+ - **Secret-file creation:** preserve `null` or `undefined` thrown by parameter getters instead of masking them with an internal `TypeError`. ([#625](https://github.com/openclaw/fs-safe/pull/625))
12
+
5
13
  ## 0.18.1 - 2026-09-22
6
14
 
7
15
  ### Fixes
@@ -33,4 +33,5 @@ export { resolveHomeRelativePath } from "./home-dir.js";
33
33
  export { appendRegularFile, appendRegularFileSync, readRegularFile, readRegularFileSync, resolveRegularFileAppendFlags, statRegularFile, statRegularFileSync, type AppendRegularFileOptions, type RegularFileStatResult, } from "./regular-file.js";
34
34
  export { buildRandomTempFilePath, sanitizeTempFileName, type TempFile, tempFile, withTempFile, } from "./temp-target.js";
35
35
  export { writeSiblingTempFile, writeViaSiblingTempPath, type WriteSiblingTempFileOptions, type WriteSiblingTempFileResult, } from "./sibling-temp.js";
36
- export { createIcaclsResetCommand, formatIcaclsResetCommand, formatWindowsAclSummary, inspectWindowsAcl, parseIcaclsOutput, resolveWindowsUserPrincipal, summarizeWindowsAcl, type IcaclsResetCommandOptions, type PermissionCommandFailure, type PermissionExec, type WindowsAclEntry, type WindowsAclSummary, } from "./permissions.js";
36
+ export { createIcaclsResetCommand, formatIcaclsResetCommand, formatWindowsAclSummary, inspectWindowsAcl, parseIcaclsOutput, resolveWindowsUserPrincipal, summarizeWindowsAcl, type IcaclsResetCommandOptions, type PermissionExec, type WindowsAclEntry, type WindowsAclSummary, } from "./permissions-windows.js";
37
+ export type { PermissionCommandFailure } from "./permission-exec.js";
package/dist/advanced.js CHANGED
@@ -35,4 +35,4 @@ export { resolveHomeRelativePath } from "./home-dir.js";
35
35
  export { appendRegularFile, appendRegularFileSync, readRegularFile, readRegularFileSync, resolveRegularFileAppendFlags, statRegularFile, statRegularFileSync, } from "./regular-file.js";
36
36
  export { buildRandomTempFilePath, sanitizeTempFileName, tempFile, withTempFile, } from "./temp-target.js";
37
37
  export { writeSiblingTempFile, writeViaSiblingTempPath, } from "./sibling-temp.js";
38
- export { createIcaclsResetCommand, formatIcaclsResetCommand, formatWindowsAclSummary, inspectWindowsAcl, parseIcaclsOutput, resolveWindowsUserPrincipal, summarizeWindowsAcl, } from "./permissions.js";
38
+ export { createIcaclsResetCommand, formatIcaclsResetCommand, formatWindowsAclSummary, inspectWindowsAcl, parseIcaclsOutput, resolveWindowsUserPrincipal, summarizeWindowsAcl, } from "./permissions-windows.js";
@@ -3,7 +3,6 @@ import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { ownExtractionDestinationMutation } from "./archive-deadline.js";
5
5
  import { assertDirectoryIdentityGuard, assertResolvedInsideDestination, createDirectoryIdentityGuard, createArchiveSymlinkTraversalError, preparePrivateArchiveOutputPath, withStagedArchiveDestination, } from "./archive-staging.js";
6
- import {} from "./directory-mode-owner.js";
7
6
  import { pinNodeDirectoryForMode } from "./directory-mode-node.js";
8
7
  import { assertSyncDirectoryGuard, inspectDirectoryIdentitySync } from "./directory-guard.js";
9
8
  import { FsSafeError } from "./errors.js";
@@ -3,7 +3,6 @@ import { type AsyncDirectoryGuard } from "./directory-guard.js";
3
3
  import { type ExtractionDeadline } from "./archive-deadline.js";
4
4
  import { ArchiveSecurityError } from "./archive-errors.js";
5
5
  export type ArchiveDirectoryGuard = AsyncDirectoryGuard<BigIntStats>;
6
- export { ArchiveSecurityError, type ArchiveSecurityErrorCode } from "./archive-errors.js";
7
6
  export declare function createDirectoryIdentityGuard(dir: string): Promise<ArchiveDirectoryGuard>;
8
7
  export declare function assertDirectoryIdentityGuard(guard: ArchiveDirectoryGuard): Promise<void>;
9
8
  export declare function prepareArchiveDestinationGuard(destDir: string): Promise<ArchiveDirectoryGuard>;
@@ -29,5 +28,5 @@ export declare function withStagedArchiveDestination<T>(params: {
29
28
  stagingDirPrefix?: string;
30
29
  run: (stagingDir: string) => Promise<T>;
31
30
  }): Promise<T>;
32
- export { mergeExtractedTreeIntoDestination } from "./archive-merge.js";
33
31
  export declare function createArchiveSymlinkTraversalError(originalPath: string): ArchiveSecurityError;
32
+ export {};
@@ -21,7 +21,6 @@ const ARCHIVE_STAGING_MODE = 0o700;
21
21
  function checkExtractionDeadline(deadline) {
22
22
  deadline?.check();
23
23
  }
24
- export { ArchiveSecurityError } from "./archive-errors.js";
25
24
  function symlinkTraversalError(originalPath) {
26
25
  return new ArchiveSecurityError("destination-symlink-traversal", `${ERROR_ARCHIVE_ENTRY_TRAVERSES_SYMLINK}: ${formatErrorDetail(originalPath)}`);
27
26
  }
@@ -302,7 +301,6 @@ export async function withStagedArchiveDestination(params) {
302
301
  }
303
302
  }
304
303
  }
305
- export { mergeExtractedTreeIntoDestination } from "./archive-merge.js";
306
304
  export function createArchiveSymlinkTraversalError(originalPath) {
307
305
  return symlinkTraversalError(originalPath);
308
306
  }
@@ -1,5 +1,3 @@
1
1
  import { type ArchiveExtractLimits } from "./archive-limits.js";
2
2
  import { type ZipArchiveWithFiles } from "./archive-zip-loader.js";
3
- export { readZipCentralDirectoryEntryCount } from "./archive-zip-count.js";
4
- export type { ZipArchiveWithFiles } from "./archive-zip-loader.js";
5
3
  export declare function loadZipArchiveWithPreflight(buffer: Buffer | Uint8Array, limits?: ArchiveExtractLimits): Promise<ZipArchiveWithFiles>;
@@ -1,7 +1,6 @@
1
1
  import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, resolveExtractLimits, } from "./archive-limits.js";
2
2
  import { admitZipBuffer } from "./archive-zip-admission.js";
3
3
  import { loadAdmittedZipArchive } from "./archive-zip-loader.js";
4
- export { readZipCentralDirectoryEntryCount } from "./archive-zip-count.js";
5
4
  export async function loadZipArchiveWithPreflight(buffer, limits) {
6
5
  const resolvedLimits = resolveExtractLimits(limits);
7
6
  if (buffer.byteLength > resolvedLimits.maxArchiveBytes) {
package/dist/archive.d.ts CHANGED
@@ -6,10 +6,13 @@ export { resolveArchiveKind, resolvePackedRootDir, type ArchiveKind } from "./ar
6
6
  export { readArchiveEntry } from "./archive-read.js";
7
7
  export { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, DEFAULT_MAX_ARCHIVE_BYTES_ZIP, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_EXTRACTED_BYTES, DEFAULT_MAX_ENTRY_BYTES, DEFAULT_MAX_META_ENTRY_BYTES, DEFAULT_MAX_ENTRY_PATH_COMPONENTS, type ArchiveExtractLimits, type ArchiveLimitErrorCode, } from "./archive-limits.js";
8
8
  export { ArchiveFormatError, type ArchiveFormatErrorCode } from "./archive-errors.js";
9
- export { ArchiveSecurityError, type ArchiveSecurityErrorCode } from "./archive-staging.js";
10
- export { createArchiveSymlinkTraversalError, mergeExtractedTreeIntoDestination, prepareArchiveDestinationDir, prepareArchiveOutputPath, withStagedArchiveDestination, } from "./archive-staging.js";
9
+ export { ArchiveSecurityError, type ArchiveSecurityErrorCode } from "./archive-errors.js";
10
+ export { createArchiveSymlinkTraversalError, prepareArchiveDestinationDir, prepareArchiveOutputPath, withStagedArchiveDestination, } from "./archive-staging.js";
11
+ export { mergeExtractedTreeIntoDestination } from "./archive-merge.js";
11
12
  export { createTarEntryPreflightChecker, type TarEntryInfo } from "./archive-plan.js";
12
- export { loadZipArchiveWithPreflight, readZipCentralDirectoryEntryCount, type ZipArchiveWithFiles, } from "./archive-zip-preflight.js";
13
+ export { loadZipArchiveWithPreflight } from "./archive-zip-preflight.js";
14
+ export { readZipCentralDirectoryEntryCount } from "./archive-zip-count.js";
15
+ export type { ZipArchiveWithFiles } from "./archive-zip-loader.js";
13
16
  export declare function extractArchive(params: ExtractArchiveOptions): Promise<void>;
14
17
  export type InspectTarArchiveOptions = Pick<ExtractArchiveOptions, "archivePath" | "timeoutMs" | "limits" | "entryFilter" | "onFiltered">;
15
18
  export type InspectedTarEntry = Readonly<Pick<ArchivePlanEntry, "path" | "kind" | "size">>;
package/dist/archive.js CHANGED
@@ -29,10 +29,12 @@ export { resolveArchiveKind, resolvePackedRootDir } from "./archive-kind.js";
29
29
  export { readArchiveEntry } from "./archive-read.js";
30
30
  export { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, DEFAULT_MAX_ARCHIVE_BYTES_ZIP, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_EXTRACTED_BYTES, DEFAULT_MAX_ENTRY_BYTES, DEFAULT_MAX_META_ENTRY_BYTES, DEFAULT_MAX_ENTRY_PATH_COMPONENTS, } from "./archive-limits.js";
31
31
  export { ArchiveFormatError } from "./archive-errors.js";
32
- export { ArchiveSecurityError } from "./archive-staging.js";
33
- export { createArchiveSymlinkTraversalError, mergeExtractedTreeIntoDestination, prepareArchiveDestinationDir, prepareArchiveOutputPath, withStagedArchiveDestination, } from "./archive-staging.js";
32
+ export { ArchiveSecurityError } from "./archive-errors.js";
33
+ export { createArchiveSymlinkTraversalError, prepareArchiveDestinationDir, prepareArchiveOutputPath, withStagedArchiveDestination, } from "./archive-staging.js";
34
+ export { mergeExtractedTreeIntoDestination } from "./archive-merge.js";
34
35
  export { createTarEntryPreflightChecker } from "./archive-plan.js";
35
- export { loadZipArchiveWithPreflight, readZipCentralDirectoryEntryCount, } from "./archive-zip-preflight.js";
36
+ export { loadZipArchiveWithPreflight } from "./archive-zip-preflight.js";
37
+ export { readZipCentralDirectoryEntryCount } from "./archive-zip-count.js";
36
38
  const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in fsConstants;
37
39
  const OPEN_WRITE_CREATE_FLAGS = fsConstants.O_WRONLY |
38
40
  fsConstants.O_CREAT |
@@ -36,7 +36,6 @@ export declare function createNearestExistingDirectoryGuard(rootReal: string, ta
36
36
  export declare function createNearestExistingDirectoryGuard(rootReal: string, targetPath: string, options: {
37
37
  bigint: boolean;
38
38
  }): Promise<AnyAsyncDirectoryGuard>;
39
- export declare function createNearestExistingSyncDirectoryGuard(rootReal: string, targetPath: string): SyncDirectoryGuard;
40
39
  export declare function inspectDirectoryIdentity(dir: string, expected?: Pick<BigIntStats, "dev" | "ino">): Promise<BigIntStats>;
41
40
  export declare function observeDirectoryIdentitySync(dir: string, options: {
42
41
  bigint: true;
@@ -84,9 +84,6 @@ function assertDirectoryGuard(guard, mode) {
84
84
  export async function createNearestExistingDirectoryGuard(rootReal, targetPath, options = { bigint: false }) {
85
85
  return nearestExistingDirectoryGuard(rootReal, targetPath, "native", options);
86
86
  }
87
- export function createNearestExistingSyncDirectoryGuard(rootReal, targetPath) {
88
- return nearestExistingDirectoryGuard(rootReal, targetPath, "normalized");
89
- }
90
87
  function nearestExistingDirectoryGuard(rootReal, targetPath, mode, options) {
91
88
  assertNoWindowsPathAlias(rootReal, "filesystem");
92
89
  assertNoWindowsPathAlias(targetPath, "filesystem");
@@ -1,5 +1,23 @@
1
- import { type BigIntStats } from "node:fs";
2
- import { type DirectoryModeOwner } from "./directory-mode-owner.js";
1
+ import { type BigIntStats, type 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;
3
21
  /** Darwin descriptor inspection avoids requesting directory-content reads. */
4
22
  export declare function nodeDarwinDirectoryMetadataFlags(): number;
5
23
  export declare function nodeDirectorySearchOnlyFlags(): {
@@ -3,7 +3,83 @@ import fs from "node:fs/promises";
3
3
  import { FsSafeError } from "./errors.js";
4
4
  import { inspectDirectoryIdentity, inspectDirectoryIdentitySync } from "./directory-guard.js";
5
5
  import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
6
- import { assertOwnedDirectory, ownDirectoryMode } from "./directory-mode-owner.js";
6
+ import { sameFileIdentityForCleanup } from "./file-identity.js";
7
+ export function assertOwnedDirectory(expected, actual) {
8
+ if (actual.isSymbolicLink() || !actual.isDirectory()) {
9
+ throw new FsSafeError("not-file", "directory mode target must be a real directory");
10
+ }
11
+ if (!sameFileIdentityForCleanup(expected, actual)) {
12
+ throw new FsSafeError("path-mismatch", "directory changed before its mode could be applied");
13
+ }
14
+ }
15
+ /** Serializes use and close: even a queued path-based fd operation retains its descriptor. */
16
+ export function ownDirectoryMode(params) {
17
+ let pending = Promise.resolve();
18
+ let closing;
19
+ const enqueue = (run) => {
20
+ if (closing)
21
+ return Promise.reject(new FsSafeError("path-mismatch", "directory mode owner is closed"));
22
+ const operation = pending.then(run);
23
+ pending = operation.catch(() => undefined);
24
+ return operation;
25
+ };
26
+ return {
27
+ verify: (check) => enqueue(async () => {
28
+ check?.();
29
+ await params.inspect();
30
+ check?.();
31
+ }),
32
+ apply: (mode, checks = {}) => enqueue(async () => {
33
+ // inspect() reports permission bits only; chmod ignores file-type bits,
34
+ // so tolerate raw stat modes (e.g. S_IFDIR | 0o755) by masking up front.
35
+ mode &= 0o7777;
36
+ checks.check?.();
37
+ const currentMode = await params.inspect();
38
+ checks.check?.();
39
+ if (currentMode === mode && !checks.beforeChmod && !checks.check)
40
+ return;
41
+ if (currentMode !== mode) {
42
+ await params.prepareChmod?.();
43
+ checks.check?.();
44
+ }
45
+ await checks.beforeChmod?.();
46
+ checks.check?.();
47
+ // Hooks/ancestor checks can yield; recheck the original named association.
48
+ await params.inspect();
49
+ checks.check?.();
50
+ let dispatchDeadlineFailure;
51
+ if (currentMode !== mode) {
52
+ checks.check?.();
53
+ try {
54
+ await params.chmod(mode);
55
+ }
56
+ catch (error) {
57
+ if (!params.ignoreChmodError)
58
+ throw error;
59
+ }
60
+ // Expiry cannot release the fd while post-dispatch verification is pending.
61
+ try {
62
+ checks.check?.();
63
+ }
64
+ catch (error) {
65
+ dispatchDeadlineFailure = { error };
66
+ }
67
+ await params.verifyChmod?.();
68
+ }
69
+ const finalMode = await params.inspect();
70
+ if (dispatchDeadlineFailure)
71
+ throw dispatchDeadlineFailure.error;
72
+ checks.check?.();
73
+ if (!params.ignoreChmodError && finalMode !== mode) {
74
+ throw new FsSafeError("path-mismatch", "directory final mode could not be verified");
75
+ }
76
+ }),
77
+ close() {
78
+ closing ??= pending.then(params.close);
79
+ return closing;
80
+ },
81
+ };
82
+ }
7
83
  /** Darwin descriptor inspection avoids requesting directory-content reads. */
8
84
  export function nodeDarwinDirectoryMetadataFlags() {
9
85
  if (process.platform !== "darwin" || (process.arch !== "x64" && process.arch !== "arm64")) {
@@ -66,7 +66,7 @@ export class SyncLockAcquisition {
66
66
  }
67
67
  catch (waitError) {
68
68
  // Exhausted backoff must preserve the filesystem denial diagnosis.
69
- if (waitError.code === "file_lock_timeout")
69
+ if (waitError?.code === "file_lock_timeout")
70
70
  throw error;
71
71
  throw waitError;
72
72
  }
@@ -1,19 +1,11 @@
1
1
  import type { FileLockSyncAcquireOptions } from "./file-lock-sync.js";
2
- import type { SidecarLockCompromisedInfo, SidecarLockRetryOptions, SidecarLockStaleRecovery } from "./sidecar-lock-types.js";
3
- type CapturedRootSyncAcquireOptions<TPayload extends Record<string, unknown>> = Readonly<{
4
- compromiseCheckIntervalMs?: number;
5
- lockPath?: string;
6
- onCompromised?: (info: SidecarLockCompromisedInfo) => void;
2
+ import type { SidecarLockRetryOptions } from "./sidecar-lock-types.js";
3
+ type CapturedRootSyncAcquireOptions<TPayload extends Record<string, unknown>> = Readonly<Pick<FileLockSyncAcquireOptions<TPayload>, "compromiseCheckIntervalMs" | "lockPath" | "onCompromised" | "parsePayload" | "payload" | "reentrantOwner" | "staleRecovery" | "timeoutMs"> & {
7
4
  optionsReceiver: FileLockSyncAcquireOptions<TPayload>;
8
- parsePayload?: (raw: string) => unknown;
9
- payload: () => TPayload;
10
- reentrantOwner?: string;
11
5
  retry: Readonly<SidecarLockRetryOptions>;
12
6
  shouldReclaim?: FileLockSyncAcquireOptions<TPayload>["shouldReclaim"];
13
7
  shouldRemoveStaleLock?: FileLockSyncAcquireOptions<TPayload>["shouldRemoveStaleLock"];
14
8
  staleMs: number;
15
- staleRecovery?: SidecarLockStaleRecovery;
16
- timeoutMs?: number;
17
9
  }>;
18
10
  export declare function captureRootSyncAcquireOptions<TPayload extends Record<string, unknown>>(options: FileLockSyncAcquireOptions<TPayload>): CapturedRootSyncAcquireOptions<TPayload>;
19
11
  export {};
@@ -4,11 +4,7 @@ export type DurableQueueEntryPathsLike = {
4
4
  deliveredPath: string;
5
5
  processingPath?: string;
6
6
  };
7
- export type ValidatedDurableQueueEntryPaths = Readonly<{
8
- jsonPath: string;
9
- deliveredPath: string;
10
- processingPath: string;
11
- }>;
7
+ export type ValidatedDurableQueueEntryPaths = Readonly<Required<DurableQueueEntryPathsLike>>;
12
8
  export declare function validateDurableQueueEntryPaths(paths: DurableQueueEntryPathsLike): ValidatedDurableQueueEntryPaths;
13
9
  export declare function claimDurableQueueEntry(paths: DurableQueueEntryPathsLike, options?: {
14
10
  skipUnowned?: boolean;
@@ -1,4 +1,3 @@
1
- export { DEFAULT_JSON_DURABLE_QUEUE_ENTRY_MAX_BYTES } from "./json-durable-queue-read.js";
2
1
  export type JsonDurableQueueEntryPaths = {
3
2
  jsonPath: string;
4
3
  deliveredPath: string;
@@ -8,7 +8,6 @@ import { stringifyJsonDocument } from "./json-stringify.js";
8
8
  import { replaceFileAtomicWithDirectorySync } from "./replace-file.js";
9
9
  import { assertSafePathSegment } from "./safe-path-segment.js";
10
10
  import { admitStandalonePublicationPath, assertNoWindowsPathAlias, hasWindowsPathAlias, } from "./windows-path-alias.js";
11
- export { DEFAULT_JSON_DURABLE_QUEUE_ENTRY_MAX_BYTES } from "./json-durable-queue-read.js";
12
11
  function assertSafeQueueEntryId(id) {
13
12
  assertSafePathSegment(id, { label: "queue entry id" });
14
13
  }
@@ -7,6 +7,12 @@ export type PermissionCommandFailure = {
7
7
  signal: string | null;
8
8
  stderr: string;
9
9
  };
10
+ export type PermissionFailureFields = {
11
+ error?: string;
12
+ errorDetail?: PermissionCommandFailure;
13
+ /** Original inspection failure, retained separately from serializable diagnostics. */
14
+ errorCause?: unknown;
15
+ };
10
16
  export declare function formatPermissionErrorDetail(value: string): string;
11
17
  /** Formats only caught permission-query failures without invoking user code. */
12
18
  export declare function formatCaughtPermissionFailure(error: unknown): string;
@@ -1,3 +1,4 @@
1
- export { formatOctal, formatPermissionDetail, formatPermissionRemediation, inspectPathPermissions, isGroupReadable, isGroupWritable, isWorldReadable, isWorldWritable, modeBits, safeStat, type PermissionCheck, type PermissionCheckOptions, type PermissionCommandFailure, type SafeStatResult, } from "./permissions.js";
1
+ export { formatOctal, formatPermissionDetail, formatPermissionRemediation, inspectPathPermissions, isGroupReadable, isGroupWritable, isWorldReadable, isWorldWritable, modeBits, safeStat, type PermissionCheck, type PermissionCheckOptions, type SafeStatResult, } from "./permissions.js";
2
+ export type { PermissionCommandFailure } from "./permission-exec.js";
2
3
  export { createPrivateDirectory, type CreatePrivateDirectoryOptions, } from "./private-directory.js";
3
4
  export { readOwnerAndDacl, type OwnerAndDaclResult, type WindowsAccessControlEntry, type WindowsAceFlags, } from "./owner-dacl.js";
@@ -1,4 +1,4 @@
1
- import { type PermissionCommandFailure } from "./permission-exec.js";
1
+ import { type PermissionFailureFields } from "./permission-exec.js";
2
2
  import { type PermissionCheck, type PermissionCheckOptions } from "./permissions.js";
3
3
  export type PermissionExec = (command: string, args: string[]) => Promise<{
4
4
  stdout: string;
@@ -13,16 +13,13 @@ export type WindowsAclEntry = {
13
13
  canRead: boolean;
14
14
  canWrite: boolean;
15
15
  };
16
- export type WindowsAclSummary = {
16
+ export type WindowsAclSummary = Omit<PermissionFailureFields & {
17
17
  ok: boolean;
18
18
  entries: WindowsAclEntry[];
19
19
  untrustedWorld: WindowsAclEntry[];
20
20
  untrustedGroup: WindowsAclEntry[];
21
21
  trusted: WindowsAclEntry[];
22
- error?: string;
23
- errorDetail?: PermissionCommandFailure;
24
- errorCause?: unknown;
25
- };
22
+ }, never>;
26
23
  export type WindowsUserInfoProvider = () => {
27
24
  username?: string | null;
28
25
  };
@@ -1,9 +1,6 @@
1
- import type { PermissionCommandFailure } from "./permission-exec.js";
1
+ import type { PermissionFailureFields } from "./permission-exec.js";
2
2
  import { type PermissionExec } from "./permissions-windows.js";
3
- export type { PermissionCommandFailure } from "./permission-exec.js";
4
- export { createIcaclsResetCommand, formatIcaclsResetCommand, formatWindowsAclSummary, inspectWindowsAcl, parseIcaclsOutput, resolveWindowsUserPrincipal, summarizeWindowsAcl, } from "./permissions-windows.js";
5
- export type { IcaclsResetCommandOptions, PermissionExec, WindowsAclEntry, WindowsAclSummary, WindowsUserInfoProvider, } from "./permissions-windows.js";
6
- export type PermissionCheck = {
3
+ export type PermissionCheck = Omit<PermissionFailureFields & {
7
4
  ok: boolean;
8
5
  isSymlink: boolean;
9
6
  isDir: boolean;
@@ -21,11 +18,7 @@ export type PermissionCheck = {
21
18
  /** Owner-query failure detail when Windows ownership could not be verified. */
22
19
  ownerError?: string;
23
20
  aclSummary?: string;
24
- error?: string;
25
- errorDetail?: PermissionCommandFailure;
26
- /** Original inspection failure, retained separately from serializable diagnostics. */
27
- errorCause?: unknown;
28
- };
21
+ }, never>;
29
22
  export type PermissionCheckOptions = {
30
23
  platform?: NodeJS.Platform;
31
24
  env?: NodeJS.ProcessEnv;
@@ -1,7 +1,6 @@
1
1
  import fsSync from "node:fs";
2
2
  import { formatIcaclsResetCommand, inspectWindowsPermissions, } from "./permissions-windows.js";
3
3
  import { hasWindowsPathAlias, pathForWindowsFilesystem, } from "./windows-path-alias.js";
4
- export { createIcaclsResetCommand, formatIcaclsResetCommand, formatWindowsAclSummary, inspectWindowsAcl, parseIcaclsOutput, resolveWindowsUserPrincipal, summarizeWindowsAcl, } from "./permissions-windows.js";
5
4
  function failedSafeStat(error) {
6
5
  return {
7
6
  ok: false,
@@ -18,6 +18,7 @@ export type PinnedOpenSyncFinalAdmission = (params: {
18
18
  descriptorIdentity: fs.BigIntStats;
19
19
  }) => string;
20
20
  export type PinnedOpenSyncFs = Pick<typeof fs, "constants" | "lstatSync" | "realpathSync" | "openSync" | "fstatSync" | "closeSync">;
21
+ export declare function isExpectedPathError(error: unknown): boolean;
21
22
  export declare function openPinnedFileSync(params: {
22
23
  filePath: string;
23
24
  resolvedPath?: string;
@@ -5,7 +5,7 @@ import { inspectFileIdentitySync } from "./strict-file-identity.js";
5
5
  import { resolveReadOpenFlags } from "./read-open-flags.js";
6
6
  import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
7
7
  import { realpathSync } from "./realpath.js";
8
- function isExpectedPathError(error) {
8
+ export function isExpectedPathError(error) {
9
9
  const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
10
10
  return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
11
11
  }
@@ -6,7 +6,7 @@ import { assertSyncDirectoryGuard, createAsyncDirectoryGuard, inspectDirectoryId
6
6
  import { FsSafeError } from "./errors.js";
7
7
  import { classifyNativeRenameFailure, NATIVE_RENAME_SOURCE_IDENTITY_MISMATCH, } from "./native-rename-outcome.js";
8
8
  import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
9
- import { RetainedDirectoryReplacement } from "./retained-directory-replacement.js";
9
+ import { copyOperationalCode, RetainedDirectoryReplacement } from "./retained-directory-replacement.js";
10
10
  import { assertSafePathPrefix } from "./safe-path-segment.js";
11
11
  import { admitStandalonePublicationPath, assertNoWindowsPathAlias } from "./windows-path-alias.js";
12
12
  import { serializePathWrite } from "./write-queue.js";
@@ -17,30 +17,18 @@ function assertGuardsCurrent(guards) {
17
17
  for (const guard of guards)
18
18
  assertSyncDirectoryGuard(guard);
19
19
  }
20
- function copyOperationalCode(target, source) {
21
- const code = source?.code;
22
- if (typeof code === "string") {
23
- Object.defineProperty(target, "code", { configurable: true, value: code });
24
- }
25
- }
26
- function replacementFailure(error, message, details) {
20
+ function replacementFailure(error, message, details, cause = error) {
27
21
  if (error instanceof FsSafeError) {
28
- return new FsSafeError(error.code, message, { cause: error, details });
22
+ return new FsSafeError(error.code, message, { cause, details });
29
23
  }
30
- const failure = new Error(message, { cause: error });
24
+ const failure = new Error(message, { cause });
31
25
  copyOperationalCode(failure, error);
32
26
  Object.defineProperty(failure, "details", { configurable: true, value: details });
33
27
  return failure;
34
28
  }
35
29
  function replacementAggregate(error, recoveryError, message, details) {
36
30
  const combined = new AggregateError([error, recoveryError], "directory replacement operation and recovery both failed");
37
- if (error instanceof FsSafeError) {
38
- return new FsSafeError(error.code, message, { cause: combined, details });
39
- }
40
- const failure = new Error(message, { cause: combined });
41
- copyOperationalCode(failure, error);
42
- Object.defineProperty(failure, "details", { configurable: true, value: details });
43
- return failure;
31
+ return replacementFailure(error, message, details, combined);
44
32
  }
45
33
  function isMissingDirectoryEntry(error) {
46
34
  return error?.code === "ENOENT";
@@ -24,15 +24,8 @@ export declare function copyFallbackReplace(params: {
24
24
  expectedSourceIdentity?: BigIntStats;
25
25
  sync: boolean;
26
26
  }): Promise<void>;
27
- export declare function copyFallbackReplaceSync(params: {
27
+ export declare function copyFallbackReplaceSync(params: Omit<Parameters<typeof copyFallbackReplace>[0], "fsModule"> & {
28
28
  fsModule: SyncFallbackFs;
29
- src: string;
30
- dest: string;
31
- destinationHardlinks?: ReplaceFileDestinationHardlinkPolicy;
32
- restore: ReplaceFileCopyFallbackRestorePolicy;
33
- maxRestoreBytes?: number;
34
- expectedSourceIdentity?: BigIntStats;
35
29
  fchmodSync?: (fd: number, mode: number) => void;
36
- sync: boolean;
37
30
  }): void;
38
31
  export {};
@@ -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 });
@@ -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 };