@openclaw/openshell-sandbox 2026.10.5 → 2026.10.7

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/index.js +16 -18
  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
@@ -2,7 +2,10 @@ import fs, {} from "node:fs";
2
2
  import path from "node:path";
3
3
  import { assertSyncDirectoryGuard } from "./directory-guard.js";
4
4
  import { FsSafeError } from "./errors.js";
5
+ import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
5
6
  import { sameFileIdentityForCleanup } from "./file-identity.js";
7
+ import { hasErrorCode } from "./file-cleanup.js";
8
+ import { isHardlinkFallbackError } from "./hardlink-fallback.js";
6
9
  // Node has no portable no-replace rename. Publish the completed stage by link,
7
10
  // then remove its verified temporary name without yielding.
8
11
  export function publishCopyStage(params) {
@@ -27,7 +30,16 @@ export function publishCopyStage(params) {
27
30
  throw new FsSafeError("already-exists", "destination already exists");
28
31
  }
29
32
  params.onPublicationAttempt?.();
30
- fs.linkSync(temporaryPath, targetPath);
33
+ let renamed = false;
34
+ try {
35
+ fs.linkSync(temporaryPath, targetPath);
36
+ }
37
+ catch (error) {
38
+ if (!isHardlinkFallbackError(error))
39
+ throw error;
40
+ publishThroughPlaceholder(params, assertCurrent);
41
+ renamed = true;
42
+ }
31
43
  let observerRejected = false;
32
44
  let observerError;
33
45
  try {
@@ -38,16 +50,29 @@ export function publishCopyStage(params) {
38
50
  observerError = error;
39
51
  }
40
52
  try {
41
- const parent = fs.lstatSync(path.dirname(temporaryPath), { bigint: true });
42
- const current = fs.lstatSync(temporaryPath, { bigint: true });
43
- if (parent.isSymbolicLink() || !parent.isDirectory() ||
44
- !sameFileIdentityForCleanup(parent, params.parentGuard.stat) ||
45
- current.isSymbolicLink() || !current.isFile() ||
46
- !sameFileIdentityForCleanup(current, identity)) {
47
- throw new FsSafeError("path-mismatch", "published copy temporary name changed before cleanup");
53
+ if (renamed) {
54
+ assertSyncDirectoryGuard(params.parentGuard);
55
+ if (fs.lstatSync(temporaryPath, { throwIfNoEntry: false })) {
56
+ throw new FsSafeError("path-mismatch", "renamed copy stage still exists");
57
+ }
58
+ const target = fs.lstatSync(targetPath, { bigint: true });
59
+ if (!target.isFile() || target.isSymbolicLink() || target.nlink !== 1n ||
60
+ !sameFileIdentityForCleanup(target, identity)) {
61
+ throw new FsSafeError("path-mismatch", "renamed copy target changed after publication");
62
+ }
63
+ }
64
+ else {
65
+ const parent = fs.lstatSync(path.dirname(temporaryPath), { bigint: true });
66
+ const current = fs.lstatSync(temporaryPath, { bigint: true });
67
+ if (parent.isSymbolicLink() || !parent.isDirectory() ||
68
+ !sameFileIdentityForCleanup(parent, params.parentGuard.stat) ||
69
+ current.isSymbolicLink() || !current.isFile() ||
70
+ !sameFileIdentityForCleanup(current, identity)) {
71
+ throw new FsSafeError("path-mismatch", "published copy temporary name changed before cleanup");
72
+ }
73
+ // This removes the stage's name, never the published destination or source.
74
+ fs.unlinkSync(temporaryPath);
48
75
  }
49
- // This removes the stage's name, never the published destination or source.
50
- fs.unlinkSync(temporaryPath);
51
76
  }
52
77
  catch (error) {
53
78
  throw new FsSafeError("helper-failed", "published copy temporary cleanup failed", {
@@ -58,3 +83,60 @@ export function publishCopyStage(params) {
58
83
  if (observerRejected)
59
84
  throw observerError;
60
85
  }
86
+ function isEmptyPlaceholder(current, identity) {
87
+ return current.isFile() && !current.isSymbolicLink() && current.nlink === 1n &&
88
+ current.size === 0n && sameFileIdentityForCleanup(current, identity);
89
+ }
90
+ function publishThroughPlaceholder(params, assertCurrent) {
91
+ const { targetPath, temporaryPath } = params;
92
+ // An empty placeholder can be observed before the completed stage atomically replaces it.
93
+ // The stage proved directory write access; a real permission denial still fails O_EXCL.
94
+ assertCurrent();
95
+ let fd;
96
+ try {
97
+ assertExclusiveCreateLeaf(targetPath);
98
+ fd = fs.openSync(targetPath, fs.constants.O_WRONLY | fs.constants.O_CREAT |
99
+ fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
100
+ }
101
+ catch (error) {
102
+ if (hasErrorCode(error, "EEXIST")) {
103
+ throw new FsSafeError("already-exists", "destination already exists", { cause: error });
104
+ }
105
+ throw error;
106
+ }
107
+ let placeholder;
108
+ try {
109
+ try {
110
+ placeholder = fs.fstatSync(fd, { bigint: true });
111
+ }
112
+ finally {
113
+ fs.closeSync(fd);
114
+ }
115
+ assertCurrent();
116
+ const current = fs.lstatSync(targetPath, { bigint: true, throwIfNoEntry: false });
117
+ if (!current || !isEmptyPlaceholder(current, placeholder)) {
118
+ throw new FsSafeError("path-mismatch", "exclusive copy placeholder changed before publication");
119
+ }
120
+ fs.renameSync(temporaryPath, targetPath);
121
+ }
122
+ catch (error) {
123
+ try {
124
+ assertSyncDirectoryGuard(params.parentGuard);
125
+ const current = fs.lstatSync(targetPath, { bigint: true, throwIfNoEntry: false });
126
+ if (current) {
127
+ if (!placeholder)
128
+ throw new FsSafeError("path-mismatch", "copy placeholder identity unavailable for cleanup");
129
+ if (isEmptyPlaceholder(current, placeholder))
130
+ fs.unlinkSync(targetPath);
131
+ }
132
+ }
133
+ catch (cleanupError) {
134
+ throw new FsSafeError("helper-failed", "unpublished copy placeholder cleanup failed", {
135
+ cause: new AggregateError([error, cleanupError], "publication and cleanup failed"),
136
+ details: { publication: "not-published", path: targetPath, cleanup: "failed",
137
+ ...(placeholder ? { dev: placeholder.dev, ino: placeholder.ino } : {}) },
138
+ });
139
+ }
140
+ throw error;
141
+ }
142
+ }
@@ -2,6 +2,7 @@ import { type BigIntStats, type Stats } from "node:fs";
2
2
  import { type DirectoryReceipt, type DirectorySyncOutcome } from "./directory-durability.js";
3
3
  import { type FileIdentityStat } from "./file-identity.js";
4
4
  import { type PublishFileExclusiveSyncFailurePolicy } from "./publish-file-failure.js";
5
+ export { isHardlinkFallbackError } from "./hardlink-fallback.js";
5
6
  export type PublishFileExclusiveStrategy = "link-or-copy" | "link-required" | "rename-noreplace";
6
7
  export type PublishFileExclusiveResult = {
7
8
  method: "hardlink" | "exclusive-copy" | "rename-noreplace";
@@ -10,7 +11,6 @@ export type PublishFileExclusiveResult = {
10
11
  identity: Stats;
11
12
  directorySync: DirectorySyncOutcome;
12
13
  };
13
- export declare function isHardlinkFallbackError(error: unknown): boolean;
14
14
  export declare function publishFileExclusive(params: {
15
15
  sourcePath: string;
16
16
  targetPath: string;
@@ -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";