@openclaw/openshell-sandbox 2026.10.6 → 2026.10.8

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 (123) hide show
  1. package/dist/.source-module-urls/fcca51e67b787234d4169b8d7f34d9367d49b71edb1b23c6d9a9ede01de9ef51.json +1 -0
  2. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +48 -0
  3. package/node_modules/@openclaw/fs-safe/README.md +14 -4
  4. package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +28 -3
  5. package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
  6. package/node_modules/@openclaw/fs-safe/dist/archive-read.js +11 -5
  7. package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.js +4 -2
  8. package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -2
  9. package/node_modules/@openclaw/fs-safe/dist/clone-metadata.js +1 -1
  10. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +2 -0
  11. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +4 -3
  12. package/node_modules/@openclaw/fs-safe/dist/copy-link.d.ts +26 -0
  13. package/node_modules/@openclaw/fs-safe/dist/copy-link.js +169 -0
  14. package/node_modules/@openclaw/fs-safe/dist/copy-metadata.d.ts +9 -0
  15. package/node_modules/@openclaw/fs-safe/dist/copy-metadata.js +39 -0
  16. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +1 -1
  17. package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +3 -6
  18. package/node_modules/@openclaw/fs-safe/dist/copy.js +3 -3
  19. package/node_modules/@openclaw/fs-safe/dist/darwin-acl.d.ts +12 -0
  20. package/node_modules/@openclaw/fs-safe/dist/darwin-acl.js +29 -0
  21. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.d.ts +3 -0
  22. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.js +27 -2
  23. package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +1 -1
  24. package/node_modules/@openclaw/fs-safe/dist/file-handle-transfer.js +4 -2
  25. package/node_modules/@openclaw/fs-safe/dist/file-hash.js +4 -3
  26. package/node_modules/@openclaw/fs-safe/dist/file-lock.d.ts +1 -0
  27. package/node_modules/@openclaw/fs-safe/dist/file-lock.js +1 -0
  28. package/node_modules/@openclaw/fs-safe/dist/file-write-lease.d.ts +9 -0
  29. package/node_modules/@openclaw/fs-safe/dist/file-write-lease.js +42 -0
  30. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +3 -0
  31. package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.d.ts +1 -0
  32. package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.js +5 -0
  33. package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-ownership.js +16 -3
  34. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +57 -18
  35. package/node_modules/@openclaw/fs-safe/dist/native-directory-observation.js +1 -1
  36. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +1 -1
  37. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +2 -0
  38. package/node_modules/@openclaw/fs-safe/dist/native-operations.js +1 -1
  39. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +1 -1
  40. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +2 -0
  41. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.d.ts +1 -1
  42. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +19 -1
  43. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +4 -1
  44. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +18 -0
  45. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +1 -1
  46. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +3 -0
  47. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.d.ts +1 -1
  48. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +3 -0
  49. package/node_modules/@openclaw/fs-safe/dist/native.d.ts +5 -3
  50. package/node_modules/@openclaw/fs-safe/dist/native.js +25 -6
  51. package/node_modules/@openclaw/fs-safe/dist/opened-realpath.js +9 -1
  52. package/node_modules/@openclaw/fs-safe/dist/overwrite-file-handle.js +4 -1
  53. package/node_modules/@openclaw/fs-safe/dist/permissions-public.d.ts +1 -0
  54. package/node_modules/@openclaw/fs-safe/dist/permissions-public.js +1 -0
  55. package/node_modules/@openclaw/fs-safe/dist/permissions-windows.js +36 -47
  56. package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +2 -0
  57. package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +3 -0
  58. package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +2 -1
  59. package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +5 -3
  60. package/node_modules/@openclaw/fs-safe/dist/pipe.d.ts +9 -0
  61. package/node_modules/@openclaw/fs-safe/dist/pipe.js +26 -0
  62. package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +2 -5
  63. package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +92 -10
  64. package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +1 -1
  65. package/node_modules/@openclaw/fs-safe/dist/publish-file.js +12 -11
  66. package/node_modules/@openclaw/fs-safe/dist/root-copy.d.ts +18 -0
  67. package/node_modules/@openclaw/fs-safe/dist/root-copy.js +128 -0
  68. package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +12 -4
  69. package/node_modules/@openclaw/fs-safe/dist/root-impl.js +6 -92
  70. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +1 -1
  71. package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -0
  72. package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +8 -1
  73. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +1 -1
  74. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +1 -1
  75. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-target.js +8 -1
  76. package/node_modules/@openclaw/fs-safe/dist/test-hooks.d.ts +1 -0
  77. package/node_modules/@openclaw/fs-safe/dist/test-hooks.js +1 -0
  78. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +2 -2
  79. package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.cs +11 -5
  80. package/node_modules/@openclaw/fs-safe/dist/windows-security-command.js +9 -1
  81. package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.d.ts +16 -0
  82. package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.js +51 -0
  83. package/node_modules/@openclaw/fs-safe/docs/archive.md +16 -0
  84. package/node_modules/@openclaw/fs-safe/docs/contributing.md +35 -5
  85. package/node_modules/@openclaw/fs-safe/docs/copy.md +22 -0
  86. package/node_modules/@openclaw/fs-safe/docs/durability.md +4 -2
  87. package/node_modules/@openclaw/fs-safe/docs/errors.md +8 -0
  88. package/node_modules/@openclaw/fs-safe/docs/guest.md +2 -0
  89. package/node_modules/@openclaw/fs-safe/docs/install.md +13 -5
  90. package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +5 -5
  91. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +15 -5
  92. package/node_modules/@openclaw/fs-safe/docs/native.md +51 -2
  93. package/node_modules/@openclaw/fs-safe/docs/pipe.md +81 -0
  94. package/node_modules/@openclaw/fs-safe/docs/platform-capabilities.md +63 -0
  95. package/node_modules/@openclaw/fs-safe/docs/quickstart.md +23 -3
  96. package/node_modules/@openclaw/fs-safe/docs/reading.md +7 -1
  97. package/node_modules/@openclaw/fs-safe/docs/secret-file.md +5 -5
  98. package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +31 -13
  99. package/node_modules/@openclaw/fs-safe/docs/store.md +11 -0
  100. package/node_modules/@openclaw/fs-safe/docs/types.md +45 -80
  101. package/node_modules/@openclaw/fs-safe/docs/writing.md +121 -14
  102. package/node_modules/@openclaw/fs-safe/package.json +18 -10
  103. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  104. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  105. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  106. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  107. package/node_modules/@openclaw/fs-safe-freebsd-arm64/fs-safe-native.node +0 -0
  108. package/node_modules/@openclaw/fs-safe-freebsd-arm64/package.json +29 -0
  109. package/node_modules/@openclaw/fs-safe-freebsd-x64/fs-safe-native.node +0 -0
  110. package/node_modules/@openclaw/fs-safe-freebsd-x64/package.json +29 -0
  111. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  112. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  113. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  114. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  115. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  116. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  117. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  118. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  119. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/fs-safe-native.node +0 -0
  120. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/package.json +29 -0
  121. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
  122. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
  123. package/package.json +5 -5
@@ -11,7 +11,7 @@ import { ownDirectoryReceipt } from "./directory-receipt.js";
11
11
  import { hashFileHandle } from "./file-hash.js";
12
12
  import { sameFileIdentity, sameFileIdentityForCleanup, } from "./file-identity.js";
13
13
  import { syncFileBestEffortSync } from "./file-sync.js";
14
- import { getNativeBinding, requireNativeBinding } from "./native.js";
14
+ import { getNativeBinding, requireNativeBinding, selectNativeBinding } from "./native.js";
15
15
  import { captureNativeFdClose } from "./native-binding.js";
16
16
  import { resolveReadOpenFlags } from "./read-open-flags.js";
17
17
  import { directorySyncFailure, publicationFailure, rememberCreatedTarget, } from "./publish-file-failure.js";
@@ -19,11 +19,9 @@ import { admitStandalonePublicationPath, assertNoWindowsPathAlias } from "./wind
19
19
  import { getFsSafeTestHooks } from "./test-hooks.js";
20
20
  import { hasErrorCode } from "./file-cleanup.js";
21
21
  import { writeAllToFile } from "./write-file-handle.js";
22
- const HARDLINK_FALLBACK_CODES = new Set(["EPERM", "EXDEV", "ENOTSUP", "EOPNOTSUPP", "ENOSYS"]);
22
+ import { isHardlinkFallbackError } from "./hardlink-fallback.js";
23
+ export { isHardlinkFallbackError } from "./hardlink-fallback.js";
23
24
  const NATIVE_COPY_FALLBACK_CODES = new Set(["EINVAL", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EPERM", "EXDEV"]);
24
- export function isHardlinkFallbackError(error) {
25
- return HARDLINK_FALLBACK_CODES.has(error?.code ?? "");
26
- }
27
25
  function directoryOpenFlags() {
28
26
  return (fsSync.constants.O_RDONLY |
29
27
  (typeof fsSync.constants.O_DIRECTORY === "number" ? fsSync.constants.O_DIRECTORY : 0));
@@ -78,13 +76,16 @@ async function copyPinnedSource(params) {
78
76
  if (params.native && params.targetNativeParent) {
79
77
  const closeFd = captureNativeFdClose(params.native);
80
78
  for (const method of ["clone", "copy-file-range"]) {
79
+ const native = selectNativeBinding(params.native, method === "clone" ? "cloneFileExclusive" : "copyFileRangeExclusive");
80
+ if (!native)
81
+ continue;
81
82
  let nativeFd;
82
83
  try {
83
84
  if (method === "clone") {
84
- nativeFd = params.native.cloneFileExclusive(params.source.fd, params.targetNativeParent.handle.fd, params.targetNativeParent.basename);
85
+ nativeFd = native.cloneFileExclusive(params.source.fd, params.targetNativeParent.handle.fd, params.targetNativeParent.basename);
85
86
  }
86
87
  else {
87
- const copied = await params.native.copyFileRangeExclusive(params.source.fd, params.targetNativeParent.handle.fd, params.targetNativeParent.basename);
88
+ const copied = await native.copyFileRangeExclusive(params.source.fd, params.targetNativeParent.handle.fd, params.targetNativeParent.basename);
88
89
  if (copied.errorCode) {
89
90
  throw Object.assign(new Error(copied.errorMessage ?? "native copy failed"), {
90
91
  code: copied.errorCode,
@@ -263,14 +264,14 @@ export async function publishFileExclusive(params) {
263
264
  await parent.assertCurrent();
264
265
  assertSourceCurrent();
265
266
  const native = strategy === "rename-noreplace"
266
- ? requireNativeBinding()
267
- : getNativeBinding();
267
+ ? requireNativeBinding("renameNoReplace")
268
+ : getNativeBinding("linkBeneath");
268
269
  if (native) {
269
270
  sourceNativeParent = await openNativeParent(sourcePath);
270
271
  targetNativeParent = await openNativeParent(targetPath);
271
272
  }
272
273
  if (strategy === "rename-noreplace") {
273
- const binding = requireNativeBinding();
274
+ const binding = requireNativeBinding("renameNoReplace");
274
275
  try {
275
276
  const fallback = nativeMoveNoReplace(binding, [
276
277
  sourceNativeParent.handle.fd, sourceNativeParent.basename,
@@ -313,7 +314,7 @@ export async function publishFileExclusive(params) {
313
314
  return await finishPublication("rename-noreplace", source, sourceExactIdentity);
314
315
  }
315
316
  try {
316
- if (native) {
317
+ if (native?.linkBeneath) {
317
318
  native.linkBeneath(sourceNativeParent.handle.fd, sourceNativeParent.basename, targetNativeParent.handle.fd, targetNativeParent.basename);
318
319
  }
319
320
  else {
@@ -0,0 +1,18 @@
1
+ import { type BigIntStats } from "node:fs";
2
+ import type { OpenResult } from "./root-impl.js";
3
+ import type { RootContext } from "./root-context.js";
4
+ import type { RootCopyOptions, RootCopySource, HardlinkPolicy } from "./root-options.js";
5
+ import { onCopyPublication, onCopySourceAdmission, type CopyPublicationOptions } from "./copy-publication.js";
6
+ type RootCopyParams = RootCopyOptions & {
7
+ source: RootCopySource;
8
+ relativePath: string;
9
+ verifyPublished?: CopyPublicationOptions[typeof onCopyPublication];
10
+ admitSource?: CopyPublicationOptions[typeof onCopySourceAdmission];
11
+ };
12
+ export declare function copyFileInRoot(root: RootContext, params: RootCopyParams, openVerifiedLocalFile: (path: string, options: {
13
+ hardlinks?: HardlinkPolicy;
14
+ }) => Promise<{
15
+ opened: OpenResult;
16
+ identity: BigIntStats;
17
+ }>): Promise<void>;
18
+ export {};
@@ -0,0 +1,128 @@
1
+ import fsSync, {} from "node:fs";
2
+ import { assertCopySourceCurrent, resolveFileCopyCloneMode } from "./copy-file-input.js";
3
+ import { resolveCopyCloneMode } from "./copy-policy.js";
4
+ import { captureCopyMetadata } from "./copy-metadata.js";
5
+ import { admitCopyLink, assertCopyLinkAvailable } from "./copy-link.js";
6
+ import { assertAbsolutePathInput } from "./absolute-path.js";
7
+ import { assertNoNulPathInput } from "./path.js";
8
+ import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
9
+ import { inspectFileIdentity } from "./strict-file-identity.js";
10
+ import { MutationAuthorityError } from "./mutation-authority.js";
11
+ import { serializePathWrite } from "./write-queue.js";
12
+ import { rootWriteQueueKey } from "./root-create-native.js";
13
+ import { resolvePinnedWriteTargetInRoot } from "./root-write-admission.js";
14
+ import { createCopyPublicationObserver, onCopyPublication, onCopySourceAdmission } from "./copy-publication.js";
15
+ import { isAlreadyExistsError, normalizePinnedWriteError } from "./root-errors.js";
16
+ import { runPinnedWriteHelper } from "./pinned-write.js";
17
+ import { FsSafeError } from "./errors.js";
18
+ export async function copyFileInRoot(root, params, openVerifiedLocalFile) {
19
+ params.signal?.throwIfAborted();
20
+ resolveCopyCloneMode(params.clone, "never");
21
+ if (params.sourceSymlinks !== undefined && params.sourceSymlinks !== "reject" && params.sourceSymlinks !== "copy-link") {
22
+ throw new FsSafeError("invalid-path", "sourceSymlinks must be reject or copy-link");
23
+ }
24
+ if (params.sourceSymlinks === "copy-link") {
25
+ if (typeof params.source !== "string") {
26
+ throw new FsSafeError("invalid-path", "sourceSymlinks: copy-link requires an absolute-path string source; open/stat capabilities cannot observe links without following them");
27
+ }
28
+ const link = await admitCopyLink(assertAbsolutePathInput(params.source), params.preserveMetadata === true, params.signal)
29
+ .catch(error => { throw normalizePinnedWriteError(error); });
30
+ if (link) {
31
+ if (params.overwrite === true)
32
+ throw new FsSafeError("invalid-path", "copy-link cannot overwrite an existing destination; omit overwrite or use false");
33
+ assertCopyLinkAvailable();
34
+ const mode = params.mode ?? (params.preserveSourceMode ? Number(link.identity.mode & 4095n) : undefined);
35
+ await copyInputInRoot(root, { ...params, overwrite: false }, link, mode);
36
+ return;
37
+ }
38
+ }
39
+ const clone = resolveFileCopyCloneMode(params.clone);
40
+ let source;
41
+ let sourceIdentity;
42
+ if (typeof params.source === "string") {
43
+ assertNoNulPathInput(params.source, "source path contains a NUL byte");
44
+ assertNoWindowsPathAlias(params.source, "filesystem", "source path uses a Windows filesystem namespace alias");
45
+ ({ opened: source, identity: sourceIdentity } = await openVerifiedLocalFile(params.source, {
46
+ hardlinks: params.sourceHardlinks,
47
+ }));
48
+ }
49
+ else {
50
+ source = await params.source.root.open(params.source.relativePath, params.sourceHardlinks === undefined ? undefined : { hardlinks: params.sourceHardlinks });
51
+ try {
52
+ sourceIdentity = await inspectFileIdentity(() => fsSync.fstatSync(source.handle.fd, { bigint: true }));
53
+ }
54
+ catch (error) {
55
+ await source.handle.close().catch(() => undefined);
56
+ throw error;
57
+ }
58
+ }
59
+ try {
60
+ if (params.maxBytes !== undefined && source.stat.size > params.maxBytes) {
61
+ throw new FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${source.stat.size})`);
62
+ }
63
+ let metadata;
64
+ try {
65
+ metadata = params.preserveMetadata ? captureCopyMetadata(source.handle.fd, sourceIdentity) : undefined;
66
+ }
67
+ catch (error) {
68
+ throw normalizePinnedWriteError(error);
69
+ }
70
+ const sourceAdmission = params.admitSource?.(sourceIdentity, source.realPath);
71
+ const verifySource = async () => {
72
+ params.signal?.throwIfAborted();
73
+ try {
74
+ sourceAdmission?.verify();
75
+ }
76
+ catch (error) {
77
+ throw new MutationAuthorityError(error);
78
+ }
79
+ if (typeof params.source !== "string")
80
+ await params.source.root.stat(".");
81
+ await assertCopySourceCurrent(source, sourceIdentity);
82
+ };
83
+ const mode = sourceAdmission?.mode ?? params.mode ?? (params.preserveSourceMode ? Number(sourceIdentity.mode & 4095n) : undefined);
84
+ await copyInputInRoot(root, params, {
85
+ kind: "file", handle: source.handle, size: source.stat.size, clone,
86
+ signal: params.signal, metadata, verifySource,
87
+ }, mode, () => assertCopySourceCurrent(source, sourceIdentity));
88
+ }
89
+ finally {
90
+ await source.handle.close().catch(() => { });
91
+ }
92
+ }
93
+ async function copyInputInRoot(root, params, input, mode, verifyAdmission = input.verifySource) {
94
+ if (mode !== undefined && (!Number.isInteger(mode) || mode < 0 || mode > 0o7777)) {
95
+ throw new FsSafeError("invalid-path", "invalid copy mode");
96
+ }
97
+ await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
98
+ const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, mode, params.denyMutations, params.overwrite !== false, params.mutationSymlinks);
99
+ await serializePathWrite(pinned.targetPath, async () => {
100
+ await verifyAdmission();
101
+ const observer = createCopyPublicationObserver(pinned.targetPath, params.onDestinationPublished);
102
+ try {
103
+ await runPinnedWriteHelper({
104
+ rootPath: pinned.rootReal, relativeParentPath: pinned.relativeParentPath, basename: pinned.basename,
105
+ mkdir: params.mkdir !== false, mode: pinned.mode, overwrite: params.overwrite !== false,
106
+ rejectFinalSymlink: params.mutationSymlinks !== undefined, maxBytes: params.maxBytes,
107
+ sync: params.durable !== false,
108
+ assertBeforeMutation: params.signal || params.assertBeforeMutation ? () => {
109
+ if (params.signal?.aborted)
110
+ throw new MutationAuthorityError(params.signal.reason);
111
+ params.assertBeforeMutation?.();
112
+ } : undefined,
113
+ verifyPublished: params.verifyPublished, onPublished: observer.onPublished, input,
114
+ rootIdentity: root.rootIdentity, mutationAdmission: pinned.mutationAdmission,
115
+ });
116
+ }
117
+ catch (error) {
118
+ observer.rethrowObserverFailure(error);
119
+ if (params.signal?.aborted && error === params.signal.reason)
120
+ throw error;
121
+ if (isAlreadyExistsError(error))
122
+ throw new FsSafeError("already-exists", "copy destination already exists", { cause: error });
123
+ throw normalizePinnedWriteError(error);
124
+ }
125
+ await input.verifySource();
126
+ });
127
+ });
128
+ }
@@ -63,10 +63,11 @@ import { getNativeBinding } from "./native.js";
63
63
  import { captureNativeFdClose } from "./native-binding.js";
64
64
  import { openNativeRootAdmission } from "./native-parent-admission.js";
65
65
  import { capturePolicyAwareNativeParent } from "./native-policy-parent.js";
66
- import { isNotFoundPathError, isPathInside } from "./path.js";
66
+ import { hasNodeErrorCode, isNotFoundPathError, isPathInside } from "./path.js";
67
67
  import { preparePinnedWriteMutationAdmission } from "./pinned-mutation-admission.js";
68
68
  import { resolvePathViaExistingAncestor } from "./root-path-existing.js";
69
69
  import { admitPathInsideRoot } from "./root-boundary.js";
70
+ import { directoryComponentNotDirectoryError } from "./root-errors.js";
70
71
  import { assertRootIdentityCurrentSync } from "./root-context.js";
71
72
  import { inspectFileIdentitySync } from "./strict-file-identity.js";
72
73
  import { getFsSafeTestHooks } from "./test-hooks.js";
@@ -81,7 +82,7 @@ function unavailable() {
81
82
  throw new FsSafeError("helper-unavailable", "native confined creation is unavailable");
82
83
  }
83
84
  async function withNativeDirectory(params, operation, discard) {
84
- const binding = getNativeBinding();
85
+ const binding = getNativeBinding("openBeneath", "mkdirChildBeneath");
85
86
  if (!binding?.openBeneath || !binding.mkdirChildBeneath || (params.private && process.platform === "win32"))
86
87
  return unavailable();
87
88
  const directoryTarget = params.directory === params.target;
@@ -232,10 +233,17 @@ export async function tryMkdirRootNative(params) {
232
233
  // and uses handle-relative NtCreateFile with its protected security descriptor.
233
234
  if (params.private && process.platform === "win32")
234
235
  return false;
235
- return await withNativeDirectory(params, async (_binding, _fd, assertCurrent) => { assertCurrent(); return true; });
236
+ try {
237
+ return await withNativeDirectory(params, async (_binding, _fd, assertCurrent) => { assertCurrent(); return true; });
238
+ }
239
+ catch (error) {
240
+ if (hasNodeErrorCode(error, "ENOTDIR"))
241
+ throw directoryComponentNotDirectoryError(error);
242
+ throw error;
243
+ }
236
244
  }
237
245
  export async function tryOpenCreateRootNative(params) {
238
- const binding = getNativeBinding();
246
+ const binding = getNativeBinding("openBeneath", "mkdirChildBeneath", "removeStagedFile", "openCreateBeneath");
239
247
  if (!binding?.removeStagedFile || !binding.openCreateBeneath)
240
248
  return unavailable();
241
249
  const access = (params.existingFlags & fs.constants.O_RDWR) ? 0o600 : 0o200;
@@ -55,7 +55,7 @@ import fsSync, { constants as fsConstants } from "node:fs";
55
55
  import fs from "node:fs/promises";
56
56
  import path from "node:path";
57
57
  import { normalizeMaxBytes } from "./byte-budget.js";
58
- import { assertCopySourceCurrent, resolveFileCopyCloneMode } from "./copy-file-input.js";
58
+ import { copyFileInRoot } from "./root-copy.js";
59
59
  import { assertPrivateFileCreationAvailable, resolveCreationPermissions } from "./creation-boundary.js";
60
60
  import { assertAsyncDirectoryGuard, assertSyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
61
61
  import { FsSafeError } from "./errors.js";
@@ -68,6 +68,7 @@ import { openedPathResolutionError, recordExclusiveCreateFailure, recordOpenedFi
68
68
  import { runPinnedWriteHelper, runPinnedWriteWithRenamePolicy } from "./pinned-write.js";
69
69
  import { preparePinnedWriteMutationAdmission, snapshotPinnedMutationPolicy } from "./pinned-mutation-admission.js";
70
70
  import { getNativeBinding } from "./native.js";
71
+ import { getPinnedWriteNativeBinding } from "./native-pinned-write.js";
71
72
  import { isFsSafeNativeRequired } from "./native-config.js";
72
73
  import { validatePinnedRelativePath } from "./pinned-operation.js";
73
74
  import { PATH_ALIAS_POLICIES } from "./path-policy.js";
@@ -104,7 +105,7 @@ import { assertRootFallbackWritePath } from "./root-write-lock-binding.js";
104
105
  import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
105
106
  import { admitMoveSourceStat, movePathNative } from "./root-move-noreplace.js";
106
107
  import { admitRootReadHandle, inspectOpenedPathIdentitySync } from "./root-read-admission.js";
107
- import { createCopyPublicationObserver, onCopyPublication, onCopySourceAdmission } from "./copy-publication.js";
108
+ import { onCopyPublication, onCopySourceAdmission } from "./copy-publication.js";
108
109
  import { writeAllToFile } from "./write-file-handle.js";
109
110
  import { createInputOptions, rethrowCreateInputError, rootWriteInput, retainSidecarCreated } from "./root-create-input.js";
110
111
  import { assertFinalSymlinkRejected, mutationSymlinkResolution, readSymlinkResolution } from "./root-symlink-policy.js";
@@ -113,7 +114,7 @@ import { assertNoWindowsPathAlias, resolvePathPreservingWindowsRoot } from "./wi
113
114
  import { resolvePinnedObservedPathInRoot } from "./root-observed-path.js";
114
115
  import { registerFileLockSyncRootAdapter } from "./file-lock-sync-root.js";
115
116
  import { mergeReadOptions, readDefaults, } from "./root-options.js";
116
- import { composeMutationAssertions, MutationAuthorityError, rethrowMutationAuthorityError } from "./mutation-authority.js";
117
+ import { composeMutationAssertions, rethrowMutationAuthorityError } from "./mutation-authority.js";
117
118
  export { DEFAULT_ROOT_MAX_BYTES } from "./root-options.js";
118
119
  export { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
119
120
  function logWarn(message) {
@@ -355,7 +356,7 @@ export class RootHandle {
355
356
  durable: options.durable ?? this.defaults.durable ?? true,
356
357
  verifyPublished: options[onCopyPublication],
357
358
  admitSource: options[onCopySourceAdmission],
358
- }).catch(rethrowMutationAuthorityError);
359
+ }, openVerifiedLocalFile).catch(rethrowMutationAuthorityError);
359
360
  }
360
361
  async exists(relativePath) {
361
362
  try {
@@ -897,7 +898,7 @@ async function writeFileInRoot(root, params) {
897
898
  const input = rootWriteInput(params);
898
899
  await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
899
900
  if (!params.private && input.kind === "buffer" && !input.stageBeforePublish && process.platform === "win32" &&
900
- (params.renameIdentity === "verify-content-with-lock" || !getNativeBinding())) {
901
+ (params.renameIdentity === "verify-content-with-lock" || !getPinnedWriteNativeBinding({ input }))) {
901
902
  await writeFileFallback(root, { ...params, data: input.data });
902
903
  return;
903
904
  }
@@ -965,93 +966,6 @@ async function commitPinnedWriteInRoot(root, pinned, params, input) {
965
966
  throw normalizePinnedWriteError(error);
966
967
  }
967
968
  }
968
- async function copyFileInRoot(root, params) {
969
- params.signal?.throwIfAborted();
970
- const clone = resolveFileCopyCloneMode(params.clone);
971
- let source;
972
- let sourceIdentity;
973
- if (typeof params.source === "string") {
974
- assertNoNulPathInput(params.source, "source path contains a NUL byte");
975
- assertNoWindowsPathAlias(params.source, "filesystem", "source path uses a Windows filesystem namespace alias");
976
- ({ opened: source, identity: sourceIdentity } = await openVerifiedLocalFile(params.source, {
977
- hardlinks: params.sourceHardlinks,
978
- }));
979
- }
980
- else {
981
- source = await params.source.root.open(params.source.relativePath, params.sourceHardlinks === undefined ? undefined : { hardlinks: params.sourceHardlinks });
982
- try {
983
- sourceIdentity = await inspectFileIdentity(() => fsSync.fstatSync(source.handle.fd, { bigint: true }));
984
- }
985
- catch (error) {
986
- await source.handle.close().catch(() => undefined);
987
- throw error;
988
- }
989
- }
990
- if (params.maxBytes !== undefined && source.stat.size > params.maxBytes) {
991
- await source.handle.close().catch(() => { });
992
- throw new FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${source.stat.size})`);
993
- }
994
- try {
995
- const sourceAdmission = params.admitSource?.(sourceIdentity, source.realPath);
996
- const mode = sourceAdmission?.mode ?? params.mode;
997
- await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
998
- const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, mode ?? (params.preserveSourceMode ? Number(sourceIdentity.mode & 4095n) : undefined), params.denyMutations, params.overwrite !== false, params.mutationSymlinks);
999
- await serializePathWrite(pinned.targetPath, async () => {
1000
- await assertCopySourceCurrent(source, sourceIdentity);
1001
- const verifySource = async () => {
1002
- params.signal?.throwIfAborted();
1003
- try {
1004
- sourceAdmission?.verify();
1005
- }
1006
- catch (error) {
1007
- throw new MutationAuthorityError(error);
1008
- }
1009
- if (typeof params.source !== "string") {
1010
- await params.source.root.stat(".");
1011
- }
1012
- await assertCopySourceCurrent(source, sourceIdentity);
1013
- };
1014
- const observer = createCopyPublicationObserver(pinned.targetPath, params.onDestinationPublished);
1015
- try {
1016
- await runPinnedWriteHelper({
1017
- rootPath: pinned.rootReal,
1018
- relativeParentPath: pinned.relativeParentPath,
1019
- basename: pinned.basename,
1020
- mkdir: params.mkdir !== false,
1021
- mode: pinned.mode,
1022
- overwrite: params.overwrite !== false,
1023
- rejectFinalSymlink: params.mutationSymlinks !== undefined,
1024
- maxBytes: params.maxBytes,
1025
- sync: params.durable !== false,
1026
- assertBeforeMutation: params.signal || params.assertBeforeMutation ? () => {
1027
- if (params.signal?.aborted)
1028
- throw new MutationAuthorityError(params.signal.reason);
1029
- params.assertBeforeMutation?.();
1030
- } : undefined,
1031
- verifyPublished: params.verifyPublished,
1032
- onPublished: observer.onPublished,
1033
- input: { kind: "file", handle: source.handle, size: source.stat.size, clone, signal: params.signal, verifySource },
1034
- rootIdentity: root.rootIdentity,
1035
- mutationAdmission: pinned.mutationAdmission,
1036
- });
1037
- }
1038
- catch (error) {
1039
- observer.rethrowObserverFailure(error);
1040
- if (params.signal?.aborted && error === params.signal.reason)
1041
- throw error;
1042
- if (isAlreadyExistsError(error)) {
1043
- throw new FsSafeError("already-exists", "copy destination already exists", { cause: error });
1044
- }
1045
- throw normalizePinnedWriteError(error);
1046
- }
1047
- await verifySource();
1048
- });
1049
- });
1050
- }
1051
- finally {
1052
- await source.handle.close().catch(() => { });
1053
- }
1054
- }
1055
969
  async function resolvePinnedPathInRoot(root, params) {
1056
970
  const resolved = await resolvePinnedRootPathInRoot(root, {
1057
971
  relativePath: params.relativePath,
@@ -80,7 +80,7 @@ export async function movePathNative(root, params, paths, overwrite = false) {
80
80
  throw error;
81
81
  // Advisory fast rejection only. renameNoReplace owns the collision decision.
82
82
  }
83
- const binding = requireNativeBinding();
83
+ const binding = requireNativeBinding("openBeneath", overwrite ? "renameReplaceWithIdentity" : "renameNoReplace");
84
84
  if (typeof (overwrite ? binding.renameReplaceWithIdentity : binding.renameNoReplace) !== "function") {
85
85
  throw new FsSafeError("helper-unavailable", "native move is unavailable");
86
86
  }
@@ -38,6 +38,8 @@ export type RootCopyOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyM
38
38
  clone?: CopyCloneMode;
39
39
  signal?: AbortSignal;
40
40
  preserveSourceMode?: boolean;
41
+ preserveMetadata?: boolean;
42
+ sourceSymlinks?: "reject" | "copy-link";
41
43
  onDestinationPublished?: (receipt: RootCopyPublicationReceipt) => void;
42
44
  };
43
45
  export type RootCopySource = string | {
@@ -29,7 +29,7 @@ function normalize(error, details) {
29
29
  return normalizeRemovePathError(error, details);
30
30
  }
31
31
  export async function removePathInRootNative(root, target, options, receipts) {
32
- const binding = getNativeBinding();
32
+ const binding = getNativeBinding("openBeneath", "rootRemovalStat", "rootRemovalUnlink");
33
33
  if (!binding?.rootRemovalStat || !binding.rootRemovalUnlink) {
34
34
  unavailable();
35
35
  }
@@ -39,6 +39,13 @@ export async function removePathInRootNative(root, target, options, receipts) {
39
39
  const boundary = await captureNonrecursiveRemovalAdmission(root, target, options, receipts);
40
40
  if (!boundary)
41
41
  return;
42
+ if (options.recursive && options.maxEntries === 0) {
43
+ options.signal?.throwIfAborted();
44
+ boundary.assertCurrent();
45
+ throw new FsSafeError("too-large", "recursive removal budget exceeded", {
46
+ details: { operation: "remove", phase: "inspect", relativePath: "" },
47
+ });
48
+ }
42
49
  const rootAdmission = await openNativeRootAdmission(binding, {
43
50
  rootPath: root.rootReal, rootIdentity: root.rootIdentity, operation: "remove", reportCloseErrors: true, searchOnly: true,
44
51
  }).catch(error => { throw normalizeRemoveGuardError(error); });
@@ -75,7 +75,7 @@ async function writeIsolatedProducer(params) {
75
75
  const targetRoot = rootFromDirectoryGuard(parentGuard);
76
76
  // Select the handoff before invoking the producer. Missing required native
77
77
  // support must not leave producer output behind as a discovery side effect.
78
- const native = getNativeBinding();
78
+ const native = getNativeBinding("openBeneath", "renameNoReplace");
79
79
  const cleanupErrors = [];
80
80
  const { target, identity } = await createOwnedTempFile({
81
81
  rootDir: targetRoot.rootReal,
@@ -99,7 +99,7 @@ export function retainCreatedSidecar(lockPath, raw, createdFd, guard, nativePare
99
99
  }
100
100
  } };
101
101
  }
102
- if (!binding.stagedFileMatches || !binding.removeStagedFile)
102
+ if (!binding.openBeneath || !binding.stagedFileMatches || !binding.removeStagedFile)
103
103
  return undefined;
104
104
  const closeFile = captureNativeFdClose(binding);
105
105
  const parentFd = binding.openBeneath(nativeParent.fd, "", nativeOpenFlags(fs.constants.O_RDONLY | fs.constants.O_DIRECTORY)).fd;
@@ -1,3 +1,4 @@
1
+ import fsSync from "node:fs";
1
2
  import fs from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { canonicalPathFromExistingAncestor } from "./absolute-path.js";
@@ -23,7 +24,13 @@ export async function resolveSidecarTargetPath(resolved, lockRoot) {
23
24
  assertNoWindowsPathAlias(normalized);
24
25
  return normalized;
25
26
  }
26
- await fs.mkdir(recursiveMkdirPath(dir), { recursive: true });
27
+ let parentExists = false;
28
+ try {
29
+ parentExists = fsSync.statSync(dir).isDirectory();
30
+ }
31
+ catch { /* Preserve mkdir errors below. */ }
32
+ if (!parentExists)
33
+ await fs.mkdir(recursiveMkdirPath(dir), { recursive: true });
27
34
  let parent;
28
35
  try {
29
36
  parent = realpathSync.native(dir);
@@ -1,5 +1,6 @@
1
1
  import type { FileHandle } from "node:fs/promises";
2
2
  import type { FileIdentityStat } from "./file-identity.js";
3
+ export { holdWindowsSharingLock, setWindowsFileAttributes, readWindowsFileExtents } from "./windows-test-hooks.js";
3
4
  export type FsSafeTestHooks = {
4
5
  beforeWatchRegistration?: (path: string) => void | Promise<void>;
5
6
  afterWatchRegistration?: (path: string) => void | Promise<void>;
@@ -1,3 +1,4 @@
1
+ export { holdWindowsSharingLock, setWindowsFileAttributes, readWindowsFileExtents } from "./windows-test-hooks.js";
1
2
  let fsSafeTestHooks;
2
3
  function allowFsSafeTestHooks() {
3
4
  return process.env.NODE_ENV === "test" || process.env.VITEST === "true";
@@ -5,9 +5,9 @@ import { watchStreamPaths } from "./watch-stream.js";
5
5
  export function watchBinding(mode) {
6
6
  if (mode === "poll")
7
7
  return;
8
- const binding = getNativeBinding(); // Preserves require + missing-addon failure.
8
+ const binding = getNativeBinding("watchRegister", "watchAdd", "watchUnregister", ...(process.platform === "darwin" ? ["watchConfigure", "watchEntries"] : []));
9
9
  // Bun TSFN teardown remains unqualified; guarded native scans remain usable.
10
- if (binding?.watchRegister && (process.platform !== "darwin" || (binding.watchConfigure && binding.watchEntries)) && !process.versions.bun && !process.versions.deno && ["linux", "darwin", "win32"].includes(process.platform))
10
+ if (binding && !process.versions.bun && !process.versions.deno && ["linux", "darwin", "win32"].includes(process.platform))
11
11
  return binding;
12
12
  if (mode === "events" || getFsSafeNativeConfig().mode === "require") {
13
13
  throw new FsSafeError("helper-unavailable", "native watch events are unavailable", { details: { operation: "watch" } });
@@ -41,18 +41,19 @@ public static partial class FsSafeWindowsBridge {
41
41
 
42
42
  sealed class Failure : Exception {
43
43
  public readonly string Code;
44
- public Failure(string code, string message) : base(message) { Code=code; }
44
+ public readonly uint? Errno;
45
+ public Failure(string code, string message, uint? errno=null) : base(message) { Code=code; Errno=errno; }
45
46
  }
46
47
  static Failure OsFailure(uint error, string operation, bool identity=false, bool fullIdentity=false) {
47
48
  // Native security calls retain EACCES; shared handle identity calls use EPERM.
48
49
  string code=error==5 ? (identity ? "EPERM" : "EACCES") : error==80 || error==183 ? "EEXIST" : error==2 || error==3 ? "ENOENT" :
49
50
  identity && (error==112 || error==39) ? "ENOSPC" : identity && (error==32 || error==33) ? "EBUSY" :
50
51
  fullIdentity && (error==1 || error==50 || error==87) ? "ENOTSUP" : "EIO";
51
- return new Failure(code, operation+" failed with Windows error "+error);
52
+ return new Failure(code, operation+" failed with Windows error "+error, error);
52
53
  }
53
54
  static Failure CreateFailure(uint error) {
54
55
  var failure=OsFailure(error,"create private directory",true);
55
- return failure.Code=="EPERM" ? new Failure("EACCES",failure.Message) : failure;
56
+ return failure.Code=="EPERM" ? new Failure("EACCES",failure.Message,failure.Errno) : failure;
56
57
  }
57
58
  static void Require(bool condition, string code, string message) {
58
59
  if (!condition) throw new Failure(code,message);
@@ -305,7 +306,8 @@ public static partial class FsSafeWindowsBridge {
305
306
  var failure=primary as Failure;
306
307
  throw new Failure(failure==null ? "EIO" : failure.Code,
307
308
  (failure==null ? "private directory validation failed" : failure.Message)+
308
- "; owned-handle cleanup failed with Windows error "+Marshal.GetLastWin32Error());
309
+ "; owned-handle cleanup failed with Windows error "+Marshal.GetLastWin32Error(),
310
+ failure==null ? null : failure.Errno);
309
311
  }
310
312
  throw;
311
313
  }
@@ -330,7 +332,11 @@ public static partial class FsSafeWindowsBridge {
330
332
  } else if(operation=="path") result=InspectPath(path);
331
333
  else throw new Failure("EINVAL","unknown Windows security operation");
332
334
  return Row("ok",true,"result",result);
333
- } catch(Failure error) { return Row("ok",false,"code",error.Code,"message",error.Message); }
335
+ } catch(Failure error) {
336
+ var reply=Row("ok",false,"code",error.Code,"message",error.Message);
337
+ if(error.Errno.HasValue) reply.Add("errno",error.Errno.Value);
338
+ return reply;
339
+ }
334
340
  catch(Exception) { return Row("ok",false,"code","EIO","message","Windows security descriptor processing failed"); }
335
341
  }
336
342
  }
@@ -85,7 +85,15 @@ function parseReplyValue(stdout, operation) {
85
85
  if (typeof response.code !== "string" || !codes.has(response.code) || typeof response.message !== "string") {
86
86
  unverified("Windows security command returned an invalid failure");
87
87
  }
88
- throw Object.assign(new Error(response.message), { code: response.code });
88
+ if (response.errno !== undefined &&
89
+ (typeof response.errno !== "number" || !Number.isInteger(response.errno) || response.errno < 0 || response.errno > 0xffff_ffff)) {
90
+ unverified("Windows security command returned an invalid native error number");
91
+ }
92
+ const failure = Object.assign(new Error(response.message), { code: response.code });
93
+ if (response.errno !== undefined) {
94
+ Object.defineProperty(failure, "errno", { value: response.errno, enumerable: true, configurable: true, writable: true });
95
+ }
96
+ throw failure;
89
97
  }
90
98
  if (operation === "create" && (!record(response.result) || response.result.created !== true || !isFullIdentity(response.result.identity))) {
91
99
  unverified("Windows security command did not verify private-directory creation");
@@ -0,0 +1,16 @@
1
+ /** Test-only handle that omits delete sharing. */
2
+ export declare function holdWindowsSharingLock(filePath: string): Disposable & {
3
+ close(): void;
4
+ };
5
+ /** Test-only attribute updates preserve unspecified bits on the opened object. */
6
+ export declare function setWindowsFileAttributes(filePath: string, attrs: {
7
+ readOnly?: boolean;
8
+ hidden?: boolean;
9
+ system?: boolean;
10
+ }): void;
11
+ /** Test-only physical mapping reader; lcn -1 represents sparse clusters. */
12
+ export declare function readWindowsFileExtents(filePath: string): {
13
+ vcn: bigint;
14
+ lcn: bigint;
15
+ clusters: bigint;
16
+ }[];