@openclaw/openshell-sandbox 2026.10.1-beta.2 → 2026.10.5-beta.1

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 (126) hide show
  1. package/dist/index.js +69 -111
  2. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +91 -0
  3. package/node_modules/@openclaw/fs-safe/README.md +1 -1
  4. package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +1 -0
  5. package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
  6. package/node_modules/@openclaw/fs-safe/dist/archive-durability.js +1 -1
  7. package/node_modules/@openclaw/fs-safe/dist/archive-entry.js +7 -3
  8. package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +4 -4
  9. package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
  10. package/node_modules/@openclaw/fs-safe/dist/archive-read.js +5 -4
  11. package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -1
  12. package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +22 -38
  13. package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.d.ts +17 -0
  14. package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +117 -0
  15. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +38 -0
  16. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +195 -0
  17. package/node_modules/@openclaw/fs-safe/dist/create.d.ts +1 -0
  18. package/node_modules/@openclaw/fs-safe/dist/create.js +4 -1
  19. package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +3 -1
  20. package/node_modules/@openclaw/fs-safe/dist/errors.d.ts +2 -0
  21. package/node_modules/@openclaw/fs-safe/dist/errors.js +5 -0
  22. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-acquire.d.ts +3 -0
  23. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-acquire.js +57 -11
  24. package/node_modules/@openclaw/fs-safe/dist/guarded-mutation.js +5 -5
  25. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +52 -5
  26. package/node_modules/@openclaw/fs-safe/dist/guest.js +1 -1
  27. package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -1
  28. package/node_modules/@openclaw/fs-safe/dist/index.js +1 -1
  29. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +4 -0
  30. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +3 -0
  31. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +54 -0
  32. package/node_modules/@openclaw/fs-safe/dist/native-noreplace.d.ts +8 -0
  33. package/node_modules/@openclaw/fs-safe/dist/native-noreplace.js +55 -0
  34. package/node_modules/@openclaw/fs-safe/dist/native-operations.d.ts +0 -1
  35. package/node_modules/@openclaw/fs-safe/dist/native-operations.js +22 -26
  36. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +5 -1
  37. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +1 -1
  38. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +12 -2
  39. package/node_modules/@openclaw/fs-safe/dist/native-rename-outcome.js +3 -1
  40. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.d.ts +1 -1
  41. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +52 -6
  42. package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +2 -1
  43. package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.d.ts +2 -0
  44. package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +17 -0
  45. package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +6 -1
  46. package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +1 -1
  47. package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.js +2 -2
  48. package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.d.ts +5 -0
  49. package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.js +1 -0
  50. package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +2 -0
  51. package/node_modules/@openclaw/fs-safe/dist/publish-file.js +35 -2
  52. package/node_modules/@openclaw/fs-safe/dist/replace-directory.js +6 -5
  53. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +1 -4
  54. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +6 -0
  55. package/node_modules/@openclaw/fs-safe/dist/root-create-input.d.ts +15 -1
  56. package/node_modules/@openclaw/fs-safe/dist/root-create-input.js +4 -0
  57. package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +1 -1
  58. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +0 -1
  59. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +6 -12
  60. package/node_modules/@openclaw/fs-safe/dist/root-errors.js +25 -4
  61. package/node_modules/@openclaw/fs-safe/dist/root-impl.js +18 -18
  62. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +4 -2
  63. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +13 -17
  64. package/node_modules/@openclaw/fs-safe/dist/root-paths.js +1 -1
  65. package/node_modules/@openclaw/fs-safe/dist/root-walk.js +4 -4
  66. package/node_modules/@openclaw/fs-safe/dist/root-write-verification.d.ts +10 -1
  67. package/node_modules/@openclaw/fs-safe/dist/root-write-verification.js +20 -0
  68. package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.js +19 -7
  69. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +18 -3
  70. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +25 -3
  71. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-admission.d.ts +2 -0
  72. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.d.ts +14 -0
  73. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +187 -0
  74. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock.js +15 -1
  75. package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.js +4 -1
  76. package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.d.ts +10 -0
  77. package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.js +29 -0
  78. package/node_modules/@openclaw/fs-safe/dist/temp-target.js +1 -1
  79. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-admission.js +6 -6
  80. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +4 -4
  81. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +17 -16
  82. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +20 -28
  83. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.d.ts +1 -0
  84. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +27 -4
  85. package/node_modules/@openclaw/fs-safe/dist/walk.js +2 -13
  86. package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +2 -2
  87. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +3 -1
  88. package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +3 -9
  89. package/node_modules/@openclaw/fs-safe/dist/watch.js +2 -3
  90. package/node_modules/@openclaw/fs-safe/docs/advanced.md +1 -0
  91. package/node_modules/@openclaw/fs-safe/docs/archive.md +5 -1
  92. package/node_modules/@openclaw/fs-safe/docs/atomic.md +11 -3
  93. package/node_modules/@openclaw/fs-safe/docs/contributing.md +1 -1
  94. package/node_modules/@openclaw/fs-safe/docs/copy.md +109 -1
  95. package/node_modules/@openclaw/fs-safe/docs/durability.md +50 -5
  96. package/node_modules/@openclaw/fs-safe/docs/errors.md +37 -0
  97. package/node_modules/@openclaw/fs-safe/docs/guest.md +21 -4
  98. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +5 -3
  99. package/node_modules/@openclaw/fs-safe/docs/native.md +39 -6
  100. package/node_modules/@openclaw/fs-safe/docs/path-scope.md +4 -0
  101. package/node_modules/@openclaw/fs-safe/docs/public-api.md +3 -1
  102. package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +46 -8
  103. package/node_modules/@openclaw/fs-safe/docs/staged-file.md +10 -3
  104. package/node_modules/@openclaw/fs-safe/docs/temp.md +12 -0
  105. package/node_modules/@openclaw/fs-safe/docs/watch.md +6 -3
  106. package/node_modules/@openclaw/fs-safe/docs/writing.md +40 -11
  107. package/node_modules/@openclaw/fs-safe/package.json +16 -16
  108. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  109. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  110. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  111. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  112. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  113. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  114. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  115. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  116. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  117. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  118. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  119. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  120. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
  121. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
  122. package/package.json +5 -5
  123. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.d.ts +0 -17
  124. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.js +0 -66
  125. package/node_modules/@openclaw/fs-safe/dist/root-move-preflight.d.ts +0 -7
  126. package/node_modules/@openclaw/fs-safe/dist/root-move-preflight.js +0 -16
@@ -0,0 +1,117 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { assertSyncDirectoryGuard, captureDirectoryGuard, inspectDirectoryIdentitySync } from "./directory-guard.js";
4
+ import { FsSafeError } from "./errors.js";
5
+ import { sameFileIdentity } from "./file-identity.js";
6
+ import { realpathSync } from "./realpath.js";
7
+ import { resolveRootPathSync, ROOT_PATH_ALIAS_POLICIES } from "./root-path.js";
8
+ function admitUncachedCopyPath(input, create = false, admittedRoot) {
9
+ if (!path.isAbsolute(input.rootPath) || !path.isAbsolute(input.absolutePath)) {
10
+ throw new FsSafeError("invalid-path", "copy roots and paths must be absolute");
11
+ }
12
+ const root = admittedRoot ?? captureDirectoryGuard(realpathSync(input.rootPath), "native", { bigint: true });
13
+ // Canonical POSIX callers already own a freshly checked root. Keep the
14
+ // resolver's lexical/symlink walk, but do not rediscover that root each time.
15
+ const rootCanonicalPath = admittedRoot?.realPath;
16
+ const resolve = () => {
17
+ const parent = create ? resolveRootPathSync({
18
+ ...input, rootCanonicalPath, absolutePath: path.dirname(input.absolutePath), boundaryLabel: "copy parent",
19
+ rejectSymlinks: true, rejectFinalSymlink: true,
20
+ }) : undefined;
21
+ const selected = resolveRootPathSync({
22
+ ...input, rootCanonicalPath, boundaryLabel: "copy", rejectSymlinks: !create, rejectFinalSymlink: !create,
23
+ policy: create ? ROOT_PATH_ALIAS_POLICIES.unlinkTarget : undefined,
24
+ });
25
+ if (parent && path.dirname(selected.canonicalPath) !== parent.canonicalPath) {
26
+ throw new FsSafeError("path-mismatch", "copy destination parent changed");
27
+ }
28
+ return selected;
29
+ };
30
+ const selected = resolve();
31
+ const parent = captureDirectoryGuard(path.dirname(selected.canonicalPath), "native", { bigint: true });
32
+ const assertCurrent = () => {
33
+ assertSyncDirectoryGuard(root);
34
+ assertSyncDirectoryGuard(parent);
35
+ const current = resolve();
36
+ if (current.rootCanonicalPath !== root.realPath || current.canonicalPath !== selected.canonicalPath) {
37
+ throw new FsSafeError("path-mismatch", "copy path changed during operation");
38
+ }
39
+ assertSyncDirectoryGuard(root);
40
+ assertSyncDirectoryGuard(parent);
41
+ };
42
+ assertCurrent();
43
+ return { path: selected.canonicalPath, parent, assertCurrent, rootCanonicalPath: undefined };
44
+ }
45
+ /** Metadata custody lasts only for the owning copy or explicit batch. */
46
+ export function createCopyPathAdmissionCache() {
47
+ const roots = new Map();
48
+ const parents = new Map();
49
+ const directories = new Map();
50
+ const assertParent = ({ root, parent, chain }) => {
51
+ assertSyncDirectoryGuard(root);
52
+ for (const guard of chain) {
53
+ inspectDirectoryIdentitySync(guard.dir, { dev: BigInt(guard.stat.dev), ino: BigInt(guard.stat.ino) });
54
+ }
55
+ assertSyncDirectoryGuard(parent);
56
+ assertSyncDirectoryGuard(root);
57
+ };
58
+ const admit = (input, create = false) => {
59
+ // Keep the public resolver for Windows aliases and noncanonical spellings,
60
+ // including raw dot segments whose traversal must not be normalized away.
61
+ if (process.platform === "win32" || !path.isAbsolute(input.rootPath) ||
62
+ !path.isAbsolute(input.absolutePath) || input.rootPath.includes("\0") || input.absolutePath.includes("\0") ||
63
+ path.resolve(input.rootPath) !== input.rootPath || path.resolve(input.absolutePath) !== input.absolutePath ||
64
+ !input.absolutePath.startsWith(input.rootPath.endsWith(path.sep) ? input.rootPath : `${input.rootPath}${path.sep}`)) {
65
+ return admitUncachedCopyPath(input, create);
66
+ }
67
+ let root = roots.get(input.rootPath);
68
+ if (!root) {
69
+ const canonical = realpathSync(input.rootPath);
70
+ if (canonical !== input.rootPath)
71
+ return admitUncachedCopyPath(input, create);
72
+ root = captureDirectoryGuard(canonical, "native", { bigint: true });
73
+ roots.set(input.rootPath, root);
74
+ }
75
+ assertSyncDirectoryGuard(root);
76
+ const parentPath = path.dirname(input.absolutePath);
77
+ let byParent = parents.get(input.rootPath);
78
+ let admitted = byParent?.get(parentPath);
79
+ if (!admitted) {
80
+ // The first file in each parent retains the existing lexical admission.
81
+ const selected = admitUncachedCopyPath(input, create, root);
82
+ const chain = [];
83
+ let cursor = input.rootPath;
84
+ const relative = path.relative(input.rootPath, parentPath);
85
+ for (const segment of relative ? relative.split(path.sep) : []) {
86
+ cursor = path.join(cursor, segment);
87
+ let guard = directories.get(cursor);
88
+ if (!guard) {
89
+ guard = captureDirectoryGuard(cursor, "native", { bigint: true });
90
+ directories.set(cursor, guard);
91
+ }
92
+ chain.push(guard);
93
+ }
94
+ admitted = { root, parent: chain.at(-1) ?? root, chain };
95
+ assertParent(admitted);
96
+ if (selected.path !== input.absolutePath || selected.parent.realPath !== parentPath ||
97
+ !sameFileIdentity(selected.parent.stat, admitted.parent.stat)) {
98
+ throw new FsSafeError("path-mismatch", "copy path changed during admission");
99
+ }
100
+ byParent ??= new Map();
101
+ byParent.set(parentPath, admitted);
102
+ parents.set(input.rootPath, byParent);
103
+ }
104
+ const selected = admitted;
105
+ const assertCurrent = () => {
106
+ assertParent(selected);
107
+ // No file observation is cached. Preserve the resolver's symlink errors.
108
+ if (!create && fs.lstatSync(input.absolutePath, { throwIfNoEntry: false })?.isSymbolicLink()) {
109
+ admitUncachedCopyPath(input, create);
110
+ throw new FsSafeError("symlink", "symlink not allowed");
111
+ }
112
+ };
113
+ assertCurrent();
114
+ return { path: input.absolutePath, parent: selected.parent, assertCurrent, rootCanonicalPath: selected.root.realPath };
115
+ };
116
+ return { admit, clear() { roots.clear(); parents.clear(); directories.clear(); } };
117
+ }
@@ -0,0 +1,38 @@
1
+ import { type CopyPath } from "./copy-path-admission.js";
2
+ import { type CopyCloneMode } from "./copy-policy.js";
3
+ import { type OwnedFileDescriptorSync } from "./create-owned-file.js";
4
+ export type CopyRootFileSyncOptions = {
5
+ source: CopyPath;
6
+ destination: CopyPath;
7
+ expectedSourceIdentity?: {
8
+ dev: bigint;
9
+ ino: bigint;
10
+ };
11
+ clone?: CopyCloneMode;
12
+ maxBytes?: number;
13
+ mode?: number;
14
+ preserveSourceMode?: boolean;
15
+ sourceHardlinks?: "reject" | "allow";
16
+ };
17
+ export type CopiedRootFileSync = OwnedFileDescriptorSync & Readonly<{
18
+ path: string;
19
+ bytes: number;
20
+ method: "clone" | "copy-file-range" | "copy";
21
+ identity: Readonly<{
22
+ dev: bigint;
23
+ ino: bigint;
24
+ }>;
25
+ sourceIdentity: Readonly<{
26
+ dev: bigint;
27
+ ino: bigint;
28
+ }>;
29
+ }>;
30
+ /** Exclusively copies an admitted source and transfers the owned destination fd. */
31
+ export declare function copyRootFileSync(options: CopyRootFileSyncOptions): CopiedRootFileSync;
32
+ export type RootFileCopyBatchSync = {
33
+ copyFile(options: CopyRootFileSyncOptions): CopiedRootFileSync;
34
+ close(): void;
35
+ [Symbol.dispose](): void;
36
+ };
37
+ /** Reuses directory admissions while retaining fresh checks for every copy. */
38
+ export declare function createRootFileCopyBatchSync(): RootFileCopyBatchSync;
@@ -0,0 +1,195 @@
1
+ import fs, {} from "node:fs";
2
+ import path from "node:path";
3
+ import { normalizeMaxBytes } from "./byte-budget.js";
4
+ import { createCopyPathAdmissionCache } from "./copy-path-admission.js";
5
+ import { resolveCopyCloneMode } from "./copy-policy.js";
6
+ import { createFileWithAdmissionSync } from "./create.js";
7
+ import { ownFileDescriptorSync } from "./create-owned-file.js";
8
+ import { creationAdmissionFromParent, removeRecordedCreationFileSync } from "./creation-boundary.js";
9
+ import { FsSafeError } from "./errors.js";
10
+ import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
11
+ import { copyFileDescriptorSync } from "./file-handle-transfer.js";
12
+ import { captureNativeFdClose } from "./native-binding.js";
13
+ import { getNativeBinding } from "./native.js";
14
+ import { openRootFileSync } from "./root-file.js";
15
+ import { inspectOpenedPathIdentitySync } from "./root-read-admission.js";
16
+ import { openStagedDirectory } from "./staged-directory.js";
17
+ import { inspectFileIdentitySync } from "./strict-file-identity.js";
18
+ function copyError(error) {
19
+ if (error instanceof FsSafeError)
20
+ return error;
21
+ const code = error?.code;
22
+ return new FsSafeError(code === "EEXIST" ? "already-exists" : code === "ENOENT" ? "not-found" :
23
+ code === "too-large" ? "too-large" :
24
+ ["ENOTSUP", "EOPNOTSUPP", "ENOSYS", "EXDEV", "EINVAL"].includes(code ?? "")
25
+ ? "unsupported-platform" : "helper-failed", "guarded synchronous file copy failed", { cause: error });
26
+ }
27
+ /** Exclusively copies an admitted source and transfers the owned destination fd. */
28
+ export function copyRootFileSync(options) {
29
+ return copyRootFileWithAdmissionSync(options, createCopyPathAdmissionCache());
30
+ }
31
+ /** Reuses directory admissions while retaining fresh checks for every copy. */
32
+ export function createRootFileCopyBatchSync() {
33
+ const cache = createCopyPathAdmissionCache();
34
+ let closed = false;
35
+ const close = () => { closed = true; cache.clear(); };
36
+ return Object.freeze({
37
+ copyFile(options) {
38
+ if (closed)
39
+ throw new FsSafeError("invalid-path", "copy batch is closed");
40
+ return copyRootFileWithAdmissionSync(options, cache);
41
+ },
42
+ close,
43
+ [Symbol.dispose]: close,
44
+ });
45
+ }
46
+ function copyRootFileWithAdmissionSync(options, cache) {
47
+ // Snapshot caller options before the first filesystem observation.
48
+ const sourceInput = options.source;
49
+ const sourcePath = { rootPath: sourceInput.rootPath, absolutePath: sourceInput.absolutePath };
50
+ const targetInput = options.destination;
51
+ const targetPath = { rootPath: targetInput.rootPath, absolutePath: targetInput.absolutePath };
52
+ const expectedInput = options.expectedSourceIdentity;
53
+ const expectedSourceIdentity = expectedInput === undefined ? undefined : { dev: expectedInput.dev, ino: expectedInput.ino };
54
+ const clone = resolveCopyCloneMode(options.clone, "never");
55
+ const maxBytes = normalizeMaxBytes(options.maxBytes);
56
+ const mode = options.mode;
57
+ const preserveSourceMode = options.preserveSourceMode;
58
+ const sourceHardlinks = options.sourceHardlinks ?? "reject";
59
+ if (mode !== undefined && (!Number.isInteger(mode) || mode < 0 || mode > 0o777)) {
60
+ throw new FsSafeError("invalid-path", "copy mode must be an integer from 0 to 0o777");
61
+ }
62
+ if (sourceHardlinks !== "allow" && sourceHardlinks !== "reject") {
63
+ throw new FsSafeError("invalid-path", "invalid copy source hardlink policy");
64
+ }
65
+ let sourceOwner;
66
+ let targetOwner;
67
+ let parentOwner;
68
+ let parentOpen = false;
69
+ let target;
70
+ let identity;
71
+ const native = getNativeBinding();
72
+ const nativeCopy = process.platform !== "win32" && native?.copyFileExclusiveSync && native.removeStagedFile
73
+ ? native.copyFileExclusiveSync.bind(native) : undefined;
74
+ try {
75
+ const source = cache.admit(sourcePath);
76
+ target = cache.admit(targetPath, true);
77
+ // Preserve exclusive-create collision semantics for dangling Windows leaves.
78
+ assertExclusiveCreateLeaf(targetPath.absolutePath);
79
+ if (fs.lstatSync(target.path, { throwIfNoEntry: false })) {
80
+ throw new FsSafeError("already-exists", "copy destination already exists");
81
+ }
82
+ const opened = openRootFileSync({
83
+ ...sourcePath, rootRealPath: source.rootCanonicalPath, boundaryLabel: "copy source", maxBytes,
84
+ rejectHardlinks: sourceHardlinks !== "allow",
85
+ });
86
+ if (!opened.ok)
87
+ throw opened.error ?? new FsSafeError("helper-failed", "copy source admission failed");
88
+ sourceOwner = ownFileDescriptorSync(opened.fd);
89
+ const sourceIdentity = inspectFileIdentitySync(() => fs.fstatSync(opened.fd, { bigint: true }), expectedSourceIdentity);
90
+ const verifySource = () => {
91
+ source.assertCurrent();
92
+ for (const inspect of [
93
+ () => fs.fstatSync(opened.fd, { bigint: true }),
94
+ () => inspectOpenedPathIdentitySync(source.path, undefined),
95
+ ]) {
96
+ const current = inspectFileIdentitySync(inspect, sourceIdentity);
97
+ if (!current.isFile())
98
+ throw new FsSafeError("not-file", "copy source must be a regular file");
99
+ if (sourceHardlinks !== "allow" && current.nlink > 1n) {
100
+ throw new FsSafeError("hardlink", "copy source must not be hardlinked");
101
+ }
102
+ }
103
+ };
104
+ const selectedMode = mode ?? (preserveSourceMode ? Number(sourceIdentity.mode & 511n) : 0o600 & ~process.umask());
105
+ let copied;
106
+ verifySource();
107
+ target.assertCurrent();
108
+ if (nativeCopy) {
109
+ const closeNative = captureNativeFdClose(native);
110
+ const parent = openStagedDirectory(target.parent.realPath);
111
+ parentOwner = ownFileDescriptorSync(parent.fd);
112
+ parentOpen = true;
113
+ inspectFileIdentitySync(() => fs.fstatSync(parent.fd, { bigint: true }), { dev: BigInt(target.parent.stat.dev), ino: BigInt(target.parent.stat.ino) });
114
+ target.assertCurrent();
115
+ copied = nativeCopy(opened.fd, parent.fd, path.basename(target.path), clone, maxBytes !== undefined && Number.isFinite(maxBytes) ? maxBytes : undefined);
116
+ targetOwner = ownFileDescriptorSync(copied.fd, closeNative);
117
+ }
118
+ else {
119
+ if (clone === "always")
120
+ throw new FsSafeError("unsupported-platform", "synchronous native file cloning is unavailable");
121
+ targetOwner = createFileWithAdmissionSync(target.path, { mode: 0o600, assertBeforeMutation: target.assertCurrent }, creationAdmissionFromParent(target.parent));
122
+ }
123
+ identity = inspectFileIdentitySync(() => fs.fstatSync(targetOwner.fd, { bigint: true }));
124
+ const verifyTarget = () => {
125
+ target.assertCurrent();
126
+ const current = inspectFileIdentitySync(() => fs.fstatSync(targetOwner.fd, { bigint: true }), identity);
127
+ const named = inspectFileIdentitySync(() => inspectOpenedPathIdentitySync(target.path, undefined), identity);
128
+ if (!current.isFile() || current.nlink !== 1n || !named.isFile() || named.nlink !== 1n) {
129
+ throw new FsSafeError("path-mismatch", "copy destination changed");
130
+ }
131
+ return current;
132
+ };
133
+ verifyTarget();
134
+ if (copied?.errorCode) {
135
+ throw copyError(Object.assign(new Error(copied.errorMessage), { code: copied.errorCode }));
136
+ }
137
+ const method = copied ? copied.method : "copy";
138
+ if (method !== "clone" && method !== "copy-file-range" && method !== "copy") {
139
+ throw new FsSafeError("helper-failed", "native copy returned an unknown method");
140
+ }
141
+ if (!copied) {
142
+ copyFileDescriptorSync(opened.fd, targetOwner.fd, { maxBytes });
143
+ }
144
+ verifySource();
145
+ const completed = verifyTarget();
146
+ if (maxBytes !== undefined && Number.isFinite(maxBytes) && completed.size > BigInt(maxBytes)) {
147
+ throw new FsSafeError("too-large", "copy exceeds maxBytes");
148
+ }
149
+ fs.fchmodSync(targetOwner.fd, selectedMode);
150
+ verifySource();
151
+ verifyTarget();
152
+ sourceOwner.close();
153
+ parentOpen = false;
154
+ parentOwner?.close();
155
+ return Object.freeze({
156
+ ...targetOwner, path: target.path, bytes: Number(completed.size), method,
157
+ identity: Object.freeze({ dev: identity.dev, ino: identity.ino }),
158
+ sourceIdentity: Object.freeze({ dev: sourceIdentity.dev, ino: sourceIdentity.ino }),
159
+ });
160
+ }
161
+ catch (error) {
162
+ const primary = copyError(error);
163
+ const cleanup = [];
164
+ if (targetOwner && target) {
165
+ try {
166
+ if (parentOpen) {
167
+ const removed = native.removeStagedFile(parentOwner.fd, path.basename(target.path), targetOwner.fd);
168
+ if (removed === "preserved")
169
+ throw new FsSafeError("path-mismatch", "copy cleanup preserved a replacement");
170
+ }
171
+ else if (identity) {
172
+ removeRecordedCreationFileSync(target.path, identity, target.assertCurrent);
173
+ }
174
+ }
175
+ catch (failure) {
176
+ cleanup.push(failure);
177
+ }
178
+ }
179
+ for (const owner of [targetOwner, sourceOwner, parentOwner]) {
180
+ try {
181
+ owner?.close();
182
+ }
183
+ catch (failure) {
184
+ cleanup.push(failure);
185
+ }
186
+ }
187
+ if (cleanup.length) {
188
+ throw new FsSafeError(primary.code, primary.message, {
189
+ cause: new AggregateError([primary, ...cleanup], "copy and cleanup failed"),
190
+ details: { path: target?.path, cleanup: "failed" },
191
+ });
192
+ }
193
+ throw primary;
194
+ }
195
+ }
@@ -4,6 +4,7 @@ import { type CreationPermissions, type CreationParentIdentity } from "./creatio
4
4
  export type { OwnedFileDescriptorSync } from "./create-owned-file.js";
5
5
  export type CreateFileOptions = CreateDirectoryOptions;
6
6
  export declare function createFileSync(targetPath: string, options?: CreateFileOptions): OwnedFileDescriptorSync;
7
+ export declare function createFileWithAdmissionSync(targetPath: string, options?: CreateFileOptions, admission?: CreationAdmission): OwnedFileDescriptorSync;
7
8
  /** Internal async writer; Windows preparation uses the nonblocking stage owner. */
8
9
  export declare function createFileHandle(targetPath: string, options?: CreateFileOptions, admission?: CreationAdmission): Promise<FileHandle>;
9
10
  export type CreateDirectoryOptions = CreationPermissions & {
@@ -108,9 +108,12 @@ function createPrivateWindowsFile(selected, options, mode) {
108
108
  }
109
109
  }
110
110
  export function createFileSync(targetPath, options = {}) {
111
+ return createFileWithAdmissionSync(targetPath, options);
112
+ }
113
+ export function createFileWithAdmissionSync(targetPath, options = {}, admission = {}) {
111
114
  const permissions = resolveCreationPermissions(options, false);
112
115
  const assertion = options.assertBeforeMutation;
113
- const selected = prepareCreationPath(targetPath);
116
+ const selected = prepareCreationPath(targetPath, admission.expectedParentIdentity);
114
117
  if (permissions.private && process.platform === "win32") {
115
118
  assertPrivateFileCreationAvailable();
116
119
  return createPrivateWindowsFile(selected, { ...permissions, assertBeforeMutation: assertion }, permissions.mode);
@@ -2,6 +2,7 @@ import fs, {} from "node:fs";
2
2
  import path from "node:path";
3
3
  import { assertDirectoryIdentitySync } from "./directory-guard.js";
4
4
  import { FsSafeError } from "./errors.js";
5
+ import { noReplaceUnavailable } from "./native-noreplace.js";
5
6
  import { assertSynchronousCallbackResult } from "./mutation-authority.js";
6
7
  import { captureNativeFdClose } from "./native-binding.js";
7
8
  import { requireNativeBinding } from "./native.js";
@@ -120,7 +121,8 @@ class Publication {
120
121
  }
121
122
  transition = native.outcome; // Capture BEFORE diagnostics, observations or close.
122
123
  if (native.errorCode || transition !== "committed") {
123
- throw Object.assign(new Error(native.errorMessage ?? "entry publication did not commit"), { code: native.errorCode ?? "helper-failed" });
124
+ const error = Object.assign(new Error(native.errorMessage ?? "entry publication did not commit"), { code: native.errorCode ?? "helper-failed" });
125
+ throw noReplaceUnavailable(error, "retained entry publication") ?? error;
124
126
  }
125
127
  phase = "postcheck";
126
128
  verification = "failed";
@@ -12,4 +12,6 @@ export declare class FsSafeError extends Error {
12
12
  details?: FsSafeErrorDetails;
13
13
  });
14
14
  }
15
+ /** A rejected no-replace rename capability, not a generic missing native helper. */
16
+ export declare function isNoReplaceUnsupported(error: unknown): error is FsSafeError;
15
17
  export {};
@@ -25,3 +25,8 @@ export class FsSafeError extends Error {
25
25
  this.details = options.details;
26
26
  }
27
27
  }
28
+ /** A rejected no-replace rename capability, not a generic missing native helper. */
29
+ export function isNoReplaceUnsupported(error) {
30
+ return error instanceof FsSafeError && error.code === "helper-unavailable" &&
31
+ error.details?.capability === "rename-noreplace";
32
+ }
@@ -1,3 +1,6 @@
1
1
  import type { FileLockSyncAcquireOptions, FileLockSyncHandle } from "./file-lock-sync.js";
2
2
  import type { Root } from "./root-impl.js";
3
+ import { type FileLockSyncRootPath } from "./file-lock-sync-root.js";
4
+ import { type FileLockSyncRootFileReceipt } from "./file-lock-sync-root-io.js";
5
+ export declare function cleanupCreatedRootSyncLock(lockRootPath: FileLockSyncRootPath, fd: number, receipt: FileLockSyncRootFileReceipt, timer?: NodeJS.Timeout): void;
3
6
  export declare function acquireFileLockSyncWithRoot<TPayload extends Record<string, unknown>>(targetPath: string, inputOptions: FileLockSyncAcquireOptions<TPayload>, lockRoot: Root): FileLockSyncHandle;
@@ -10,10 +10,40 @@ import { SyncLockAcquisition } from "./file-lock-sync-acquisition.js";
10
10
  import { assertSynchronousCallbackResult } from "./mutation-authority.js";
11
11
  import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
12
12
  import { admitFileLockSyncRootPath, assertFileLockSyncRootPathsCurrent, captureFileLockSyncRootAuthority, normalizeFileLockSyncTargetWithRoot, } from "./file-lock-sync-root.js";
13
- import { cleanupCreatedRootSyncLock, FileLockSyncRootArbitrationCollision, tryReuseCurrentRootSyncHeldLock, } from "./file-lock-sync-root-arbitration.js";
14
13
  import { fileLockSyncRootSnapshotStillCurrent, } from "./file-lock-sync-root-io.js";
15
14
  import { createFileLockSyncRootDirectory, createFileLockSyncRootFile, fileLockSyncRootGuardExists, fileLockSyncRootDirectoryReceiptStillCurrent, refreshFileLockSyncRootFileReceipt, removeFileLockSyncRootDirectory, removeFileLockSyncRootFile, } from "./file-lock-sync-root-mutation.js";
16
- import { createRootSyncHeldLockHandle, ensureRootSyncExitCleanupRegistered, getRootSyncHeldLocks, readRootSidecarSnapshotSync, } from "./file-lock-sync-root-held.js";
15
+ import { createRootSyncHeldLockHandle, ensureRootSyncExitCleanupRegistered, getRootSyncHeldLocks, readRootSidecarSnapshotSync, verifyRootSyncHeldLock, } from "./file-lock-sync-root-held.js";
16
+ class FileLockSyncRootArbitrationCollision extends FsSafeError {
17
+ constructor() {
18
+ super("path-mismatch", "file lock arbitration changed during local creation");
19
+ }
20
+ }
21
+ export function cleanupCreatedRootSyncLock(lockRootPath, fd, receipt, timer) {
22
+ let timerCleanupFailed = false;
23
+ let timerCleanupError;
24
+ try {
25
+ if (timer)
26
+ clearInterval(timer);
27
+ }
28
+ catch (error) {
29
+ timerCleanupFailed = true;
30
+ timerCleanupError = error;
31
+ }
32
+ try {
33
+ fs.closeSync(fd);
34
+ if (!removeFileLockSyncRootFile(lockRootPath, receipt)) {
35
+ throw new FsSafeError("path-mismatch", "created sidecar lock changed before cleanup");
36
+ }
37
+ }
38
+ catch (fileCleanupError) {
39
+ if (timerCleanupFailed) {
40
+ throw createSuppressedError(timerCleanupError, fileCleanupError, "unpublished lock timer and file cleanup both failed");
41
+ }
42
+ throw fileCleanupError;
43
+ }
44
+ if (timerCleanupFailed)
45
+ throw timerCleanupError;
46
+ }
17
47
  export function acquireFileLockSyncWithRoot(targetPath, inputOptions, lockRoot) {
18
48
  // The public dispatcher already read lockRoot exactly once. Validate and
19
49
  // snapshot that genuine Root before any remaining option or retry getter can
@@ -37,23 +67,39 @@ export function acquireFileLockSyncWithRoot(targetPath, inputOptions, lockRoot)
37
67
  const heldLocks = getRootSyncHeldLocks();
38
68
  const currentTargetHolder = () => heldLocks.get(normalizedTargetPath) ??
39
69
  foreignSyncHeldLock("root", normalizedTargetPath);
40
- const arbitration = Object.freeze({
41
- authority,
42
- heldLocks,
43
- lockRootPath,
44
- normalizedTargetPath,
45
- reentrantOwner: options.reentrantOwner,
46
- });
70
+ const tryReuseCurrentHeld = () => {
71
+ const held = heldLocks.get(normalizedTargetPath);
72
+ const reusable = held && !foreignSyncHeldLock("root", normalizedTargetPath) &&
73
+ options.reentrantOwner !== undefined && held.reentrantOwner !== undefined &&
74
+ options.reentrantOwner === held.reentrantOwner &&
75
+ authority.adapter === held.rootAuthority.adapter &&
76
+ lockRootPath.relativePath === held.rootPath.relativePath &&
77
+ path.relative(lockRootPath.path, held.rootPath.path) === "" &&
78
+ held.releaseState !== "released";
79
+ if (!reusable)
80
+ return undefined;
81
+ if (!verifyRootSyncHeldLock(held)) {
82
+ throw new FsSafeError("path-mismatch", "held sidecar lock changed before reentrant reuse");
83
+ }
84
+ // The parser can reenter acquisition; recheck the exact held entry before reuse.
85
+ if (heldLocks.get(normalizedTargetPath) !== held || held.releaseState === "released" ||
86
+ foreignSyncHeldLock("root", normalizedTargetPath)) {
87
+ throw new FsSafeError("path-mismatch", "held sidecar lock changed during reentrant reuse");
88
+ }
89
+ held.refCount += 1;
90
+ held.revision += 1;
91
+ return createRootSyncHeldLockHandle(held);
92
+ };
47
93
  if (heldLocks.has(normalizedTargetPath) && !foreignSyncHeldLock("root", normalizedTargetPath)) {
48
94
  assertFileLockSyncRootPathsCurrent(guardedPaths);
49
- const initiallyReusable = tryReuseCurrentRootSyncHeldLock(arbitration);
95
+ const initiallyReusable = tryReuseCurrentHeld();
50
96
  if (initiallyReusable)
51
97
  return initiallyReusable;
52
98
  }
53
99
  const acquisition = new SyncLockAcquisition(lockPath, normalizedTargetPath, options.retry, options.timeoutMs);
54
100
  let ownedReclaimGuard;
55
101
  let reclaimCleanupAttempted = false;
56
- const reuseCurrentHeld = () => ownedReclaimGuard ? undefined : tryReuseCurrentRootSyncHeldLock(arbitration);
102
+ const reuseCurrentHeld = () => ownedReclaimGuard ? undefined : tryReuseCurrentHeld();
57
103
  const releaseReclaimGuard = () => {
58
104
  const receipt = ownedReclaimGuard;
59
105
  if (!receipt)
@@ -1,16 +1,16 @@
1
1
  import fsSync, {} from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { assertAsyncDirectoryGuard, assertSyncDirectoryGuard, createAsyncDirectoryGuard, createSyncDirectoryGuard, } from "./directory-guard.js";
4
+ import { assertDirectoryGuard, assertSyncDirectoryGuard, createAsyncDirectoryGuard, createSyncDirectoryGuard, } from "./directory-guard.js";
5
5
  export async function withAsyncDirectoryGuards(guards, mutate, options = {}) {
6
6
  for (const guard of guards) {
7
- await assertAsyncDirectoryGuard(guard);
7
+ assertDirectoryGuard(guard, "native");
8
8
  }
9
9
  const result = await mutate();
10
10
  if (options.verifyAfter !== false) {
11
11
  try {
12
12
  for (const guard of guards) {
13
- await assertAsyncDirectoryGuard(guard);
13
+ assertDirectoryGuard(guard, "native");
14
14
  }
15
15
  }
16
16
  catch (error) {
@@ -46,11 +46,11 @@ export async function guardedRename(params) {
46
46
  const sourceGuard = await createAsyncDirectoryGuard(path.dirname(params.from));
47
47
  const targetGuard = await createAsyncDirectoryGuard(path.dirname(params.to));
48
48
  await withAsyncDirectoryGuards([sourceGuard, targetGuard], async () => {
49
+ // Authority must survive all awaited guards; do not yield before rename dispatch.
50
+ params.assertBeforeRename?.();
49
51
  if (params.onSourceInspected) {
50
52
  params.onSourceInspected(fsSync.lstatSync(params.from, { bigint: true }));
51
53
  }
52
- // Authority must survive all awaited guards; do not yield before rename dispatch.
53
- params.assertBeforeRename?.();
54
54
  await fs.rename(params.from, params.to);
55
55
  params.onRenamed?.();
56
56
  });