@openclaw/fs-safe 0.6.0 → 0.7.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 (202) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +7 -0
  3. package/dist/archive-deadline.d.ts +3 -0
  4. package/dist/archive-deadline.d.ts.map +1 -1
  5. package/dist/archive-deadline.js +44 -8
  6. package/dist/archive-entry.d.ts.map +1 -1
  7. package/dist/archive-entry.js +1 -0
  8. package/dist/archive-errors.d.ts +1 -0
  9. package/dist/archive-errors.d.ts.map +1 -1
  10. package/dist/archive-errors.js +3 -0
  11. package/dist/archive-input.d.ts.map +1 -1
  12. package/dist/archive-input.js +26 -16
  13. package/dist/archive-limits.d.ts +11 -3
  14. package/dist/archive-limits.d.ts.map +1 -1
  15. package/dist/archive-limits.js +24 -0
  16. package/dist/archive-native.d.ts +3 -2
  17. package/dist/archive-native.d.ts.map +1 -1
  18. package/dist/archive-native.js +24 -9
  19. package/dist/archive-policy.d.ts +2 -0
  20. package/dist/archive-policy.d.ts.map +1 -1
  21. package/dist/archive-policy.js +9 -1
  22. package/dist/archive-read.d.ts.map +1 -1
  23. package/dist/archive-read.js +69 -41
  24. package/dist/archive-staging.d.ts +3 -0
  25. package/dist/archive-staging.d.ts.map +1 -1
  26. package/dist/archive-staging.js +91 -43
  27. package/dist/archive-tar-admission.d.ts +7 -0
  28. package/dist/archive-tar-admission.d.ts.map +1 -0
  29. package/dist/archive-tar-admission.js +43 -0
  30. package/dist/archive-tar-gnu.d.ts +2 -0
  31. package/dist/archive-tar-gnu.d.ts.map +1 -0
  32. package/dist/archive-tar-gnu.js +20 -0
  33. package/dist/archive-tar-header.d.ts +8 -0
  34. package/dist/archive-tar-header.d.ts.map +1 -0
  35. package/dist/archive-tar-header.js +47 -0
  36. package/dist/archive-tar-meta.d.ts +29 -1
  37. package/dist/archive-tar-meta.d.ts.map +1 -1
  38. package/dist/archive-tar-meta.js +104 -35
  39. package/dist/archive-tar-runtime.d.ts +3 -0
  40. package/dist/archive-tar-runtime.d.ts.map +1 -1
  41. package/dist/archive-tar-runtime.js +3 -0
  42. package/dist/archive-tar.d.ts.map +1 -1
  43. package/dist/archive-tar.js +8 -2
  44. package/dist/archive-zip-names.js +2 -2
  45. package/dist/archive.d.ts.map +1 -1
  46. package/dist/archive.js +38 -27
  47. package/dist/atomic.d.ts +1 -1
  48. package/dist/atomic.d.ts.map +1 -1
  49. package/dist/bounded-read-stream.d.ts.map +1 -1
  50. package/dist/bounded-read-stream.js +2 -4
  51. package/dist/bounded-read.d.ts.map +1 -1
  52. package/dist/bounded-read.js +3 -10
  53. package/dist/byte-budget.d.ts +5 -0
  54. package/dist/byte-budget.d.ts.map +1 -0
  55. package/dist/byte-budget.js +9 -0
  56. package/dist/directory-guard.d.ts +2 -1
  57. package/dist/directory-guard.d.ts.map +1 -1
  58. package/dist/directory-guard.js +10 -0
  59. package/dist/file-lock-sync.d.ts.map +1 -1
  60. package/dist/file-lock-sync.js +81 -30
  61. package/dist/file-observation.d.ts +9 -0
  62. package/dist/file-observation.d.ts.map +1 -0
  63. package/dist/file-observation.js +22 -0
  64. package/dist/file-store-boundary.d.ts.map +1 -1
  65. package/dist/file-store-boundary.js +9 -5
  66. package/dist/file-store-limit.d.ts +2 -0
  67. package/dist/file-store-limit.d.ts.map +1 -0
  68. package/dist/file-store-limit.js +8 -0
  69. package/dist/file-store-sync-write.d.ts.map +1 -1
  70. package/dist/file-store-sync-write.js +38 -8
  71. package/dist/file-store.d.ts.map +1 -1
  72. package/dist/file-store.js +26 -26
  73. package/dist/json-durable-queue-directory.d.ts +2 -0
  74. package/dist/json-durable-queue-directory.d.ts.map +1 -0
  75. package/dist/json-durable-queue-directory.js +20 -0
  76. package/dist/json-durable-queue-ownership.d.ts +14 -0
  77. package/dist/json-durable-queue-ownership.d.ts.map +1 -0
  78. package/dist/json-durable-queue-ownership.js +168 -0
  79. package/dist/json-durable-queue-retirement.d.ts +9 -0
  80. package/dist/json-durable-queue-retirement.d.ts.map +1 -0
  81. package/dist/json-durable-queue-retirement.js +126 -0
  82. package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
  83. package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
  84. package/dist/json-durable-queue-transfer-lock.js +19 -0
  85. package/dist/json-durable-queue.d.ts +1 -0
  86. package/dist/json-durable-queue.d.ts.map +1 -1
  87. package/dist/json-durable-queue.js +90 -57
  88. package/dist/json.d.ts.map +1 -1
  89. package/dist/json.js +32 -13
  90. package/dist/local-roots.d.ts.map +1 -1
  91. package/dist/local-roots.js +4 -2
  92. package/dist/native-binding.d.ts +12 -3
  93. package/dist/native-binding.d.ts.map +1 -1
  94. package/dist/native-pinned-write-windows.js +1 -1
  95. package/dist/opened-file-failure.d.ts +7 -0
  96. package/dist/opened-file-failure.d.ts.map +1 -0
  97. package/dist/opened-file-failure.js +41 -0
  98. package/dist/opened-realpath.d.ts.map +1 -1
  99. package/dist/opened-realpath.js +8 -2
  100. package/dist/output-sibling.d.ts.map +1 -1
  101. package/dist/output-sibling.js +11 -110
  102. package/dist/output.d.ts.map +1 -1
  103. package/dist/output.js +4 -2
  104. package/dist/pinned-open.d.ts.map +1 -1
  105. package/dist/pinned-open.js +27 -48
  106. package/dist/pinned-write.d.ts.map +1 -1
  107. package/dist/pinned-write.js +10 -10
  108. package/dist/private-temp-workspace.d.ts +3 -1
  109. package/dist/private-temp-workspace.d.ts.map +1 -1
  110. package/dist/private-temp-workspace.js +81 -56
  111. package/dist/publish-file.d.ts.map +1 -1
  112. package/dist/publish-file.js +2 -4
  113. package/dist/read-opened-file.d.ts.map +1 -1
  114. package/dist/read-opened-file.js +6 -4
  115. package/dist/regular-file.d.ts.map +1 -1
  116. package/dist/regular-file.js +136 -82
  117. package/dist/replace-file-copy-fallback.d.ts +3 -1
  118. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  119. package/dist/replace-file-copy-fallback.js +21 -36
  120. package/dist/replace-file-copy-source.d.ts +21 -0
  121. package/dist/replace-file-copy-source.d.ts.map +1 -0
  122. package/dist/replace-file-copy-source.js +112 -0
  123. package/dist/replace-file-descriptor.d.ts +13 -3
  124. package/dist/replace-file-descriptor.d.ts.map +1 -1
  125. package/dist/replace-file-descriptor.js +32 -7
  126. package/dist/replace-file-rename-policy.d.ts +7 -0
  127. package/dist/replace-file-rename-policy.d.ts.map +1 -0
  128. package/dist/replace-file-rename-policy.js +30 -0
  129. package/dist/replace-file-temp-owner.d.ts +46 -0
  130. package/dist/replace-file-temp-owner.d.ts.map +1 -0
  131. package/dist/replace-file-temp-owner.js +346 -0
  132. package/dist/replace-file.d.ts +6 -1
  133. package/dist/replace-file.d.ts.map +1 -1
  134. package/dist/replace-file.js +72 -58
  135. package/dist/root-impl.d.ts.map +1 -1
  136. package/dist/root-impl.js +113 -82
  137. package/dist/secret-file.d.ts.map +1 -1
  138. package/dist/secret-file.js +6 -3
  139. package/dist/secret-read-async.d.ts.map +1 -1
  140. package/dist/secret-read-async.js +6 -5
  141. package/dist/secure-file.d.ts.map +1 -1
  142. package/dist/secure-file.js +17 -13
  143. package/dist/sibling-staged-file.d.ts +15 -0
  144. package/dist/sibling-staged-file.d.ts.map +1 -0
  145. package/dist/sibling-staged-file.js +148 -0
  146. package/dist/sibling-temp.d.ts +3 -0
  147. package/dist/sibling-temp.d.ts.map +1 -1
  148. package/dist/sibling-temp.js +30 -74
  149. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  150. package/dist/sidecar-lock-acquire.js +91 -37
  151. package/dist/sidecar-lock-handle.d.ts +6 -2
  152. package/dist/sidecar-lock-handle.d.ts.map +1 -1
  153. package/dist/sidecar-lock-handle.js +17 -3
  154. package/dist/sidecar-lock-policy.d.ts +2 -0
  155. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  156. package/dist/sidecar-lock-policy.js +29 -0
  157. package/dist/sidecar-lock-reclaim.d.ts +16 -5
  158. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  159. package/dist/sidecar-lock-reclaim.js +60 -28
  160. package/dist/sidecar-lock-root.d.ts +3 -0
  161. package/dist/sidecar-lock-root.d.ts.map +1 -0
  162. package/dist/sidecar-lock-root.js +76 -0
  163. package/dist/sidecar-lock.d.ts.map +1 -1
  164. package/dist/sidecar-lock.js +28 -16
  165. package/dist/strict-file-identity.d.ts.map +1 -1
  166. package/dist/strict-file-identity.js +4 -1
  167. package/dist/suppressed-error.d.ts +6 -0
  168. package/dist/suppressed-error.d.ts.map +1 -0
  169. package/dist/suppressed-error.js +15 -0
  170. package/dist/temp-cleanup.d.ts +2 -0
  171. package/dist/temp-cleanup.d.ts.map +1 -1
  172. package/dist/temp-cleanup.js +25 -10
  173. package/dist/temp-workspace-owner.d.ts +23 -0
  174. package/dist/temp-workspace-owner.d.ts.map +1 -0
  175. package/dist/temp-workspace-owner.js +320 -0
  176. package/dist/temp.d.ts +1 -1
  177. package/dist/temp.d.ts.map +1 -1
  178. package/dist/test-hooks.d.ts +5 -0
  179. package/dist/test-hooks.d.ts.map +1 -1
  180. package/docs/advanced.md +8 -1
  181. package/docs/archive.md +202 -26
  182. package/docs/atomic.md +11 -2
  183. package/docs/durability.md +1 -1
  184. package/docs/file-store.md +2 -0
  185. package/docs/install.md +8 -4
  186. package/docs/json.md +11 -4
  187. package/docs/native-helper.md +15 -3
  188. package/docs/native.md +33 -6
  189. package/docs/output.md +16 -3
  190. package/docs/public-api.md +6 -3
  191. package/docs/quickstart.md +1 -1
  192. package/docs/reading.md +1 -1
  193. package/docs/regular-file.md +9 -2
  194. package/docs/root.md +2 -0
  195. package/docs/secret-file.md +5 -1
  196. package/docs/secure-file.md +4 -2
  197. package/docs/security-model.md +18 -0
  198. package/docs/sidecar-lock.md +60 -4
  199. package/docs/store.md +13 -0
  200. package/docs/temp.md +128 -14
  201. package/docs/writing.md +6 -0
  202. package/package.json +10 -10
@@ -1,19 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import fsSync from "node:fs";
3
- import fs, {} from "node:fs/promises";
4
2
  import path from "node:path";
5
- import { createAsyncDirectoryGuard } from "./directory-guard.js";
6
- import { syncDirectoryBestEffort } from "./directory-durability.js";
7
- import { FsSafeError } from "./errors.js";
8
- import { sameFileIdentity, sameFileIdentityForCleanup } from "./file-identity.js";
9
- import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
10
3
  import { sanitizeUntrustedFileName } from "./filename.js";
11
- import { registerTempPathForExit } from "./temp-cleanup.js";
12
- import { serializePathWrite } from "./write-queue.js";
13
- const OPEN_READ_WRITE_NOFOLLOW = fsSync.constants.O_RDWR |
14
- (process.platform !== "win32" && typeof fsSync.constants.O_NOFOLLOW === "number"
15
- ? fsSync.constants.O_NOFOLLOW
16
- : 0);
4
+ import { writeCallbackSibling } from "./sibling-staged-file.js";
17
5
  function safeFallbackFileName(fallbackFileName) {
18
6
  return sanitizeUntrustedFileName(fallbackFileName ?? "output.bin", "output.bin");
19
7
  }
@@ -21,103 +9,16 @@ function buildSiblingTempPath(targetPath, fallbackFileName) {
21
9
  const safeTail = sanitizeUntrustedFileName(path.basename(targetPath), safeFallbackFileName(fallbackFileName));
22
10
  return path.join(path.dirname(targetPath), `.fs-safe-output-${process.pid}-${randomUUID()}-${safeTail}.part`);
23
11
  }
24
- function assertStagedFile(pathname, opened, maxBytes) {
25
- if (pathname.isSymbolicLink() || !pathname.isFile() || !opened.isFile()) {
26
- throw new FsSafeError("not-file", "sibling-staged output must be a regular file");
27
- }
28
- if (!sameFileIdentity(pathname, opened)) {
29
- throw new FsSafeError("path-mismatch", "sibling-staged output changed while opening");
30
- }
31
- if (opened.nlink > 1) {
32
- throw new FsSafeError("hardlink", "hardlinked sibling-staged output not allowed");
33
- }
34
- if (maxBytes !== undefined && opened.size > maxBytes) {
35
- throw new FsSafeError("too-large", `sibling-staged output exceeds maxBytes (${maxBytes})`);
36
- }
37
- }
38
- async function openPinnedStagedFile(tempPath, maxBytes) {
39
- const preview = await fs.lstat(tempPath);
40
- if (preview.isSymbolicLink()) {
41
- throw new FsSafeError("symlink", "symlink sibling-staged output not allowed");
42
- }
43
- const handle = await fs.open(tempPath, OPEN_READ_WRITE_NOFOLLOW);
44
- try {
45
- const identity = await handle.stat();
46
- assertStagedFile(await fs.lstat(tempPath), identity, maxBytes);
47
- return { handle, identity };
48
- }
49
- catch (error) {
50
- await handle.close().catch(() => undefined);
51
- throw error;
52
- }
53
- }
54
- async function syncFile(handle) {
55
- try {
56
- await handle.sync();
57
- }
58
- catch (error) {
59
- if (error.code !== "EPERM")
60
- throw error;
61
- }
62
- }
63
- async function cleanupTempPath(tempPath, identity) {
64
- try {
65
- const current = await fs.lstat(tempPath);
66
- if (!identity || (!current.isSymbolicLink() && sameFileIdentityForCleanup(current, identity))) {
67
- await fs.rm(tempPath, { force: true });
68
- }
69
- return true;
70
- }
71
- catch (error) {
72
- return error.code === "ENOENT";
73
- }
74
- }
75
12
  export async function writeExternalFileViaSibling(params) {
76
13
  const finalPath = path.resolve(params.finalPath);
77
- const parentPath = path.dirname(finalPath);
78
- const parentGuard = await createAsyncDirectoryGuard(parentPath);
79
- const tempPath = buildSiblingTempPath(finalPath, params.fallbackFileName);
80
- const unregisterTempPath = registerTempPathForExit(tempPath);
81
- let stagedIdentity;
82
- let renamed = false;
83
- try {
84
- const result = await params.write(tempPath);
85
- const pinned = await openPinnedStagedFile(tempPath, params.maxBytes);
86
- stagedIdentity = pinned.identity;
87
- unregisterTempPath.setIdentity(await pinned.handle.stat({ bigint: true }));
88
- try {
89
- if (params.mode !== undefined) {
90
- await pinned.handle.chmod(params.mode);
91
- }
92
- await syncFile(pinned.handle);
93
- await serializePathWrite(finalPath, async () => {
94
- await withAsyncDirectoryGuards([parentGuard], async () => {
95
- const currentIdentity = await pinned.handle.stat();
96
- assertStagedFile(await fs.lstat(tempPath), currentIdentity, params.maxBytes);
97
- if (!sameFileIdentity(currentIdentity, stagedIdentity)) {
98
- throw new FsSafeError("path-mismatch", "sibling-staged output changed before rename");
99
- }
100
- await fs.rename(tempPath, finalPath);
101
- renamed = true;
102
- const published = await fs.lstat(finalPath);
103
- if (published.isSymbolicLink() || !sameFileIdentity(published, currentIdentity)) {
104
- throw new FsSafeError("path-mismatch", "sibling-staged output changed during rename");
105
- }
106
- });
107
- await syncDirectoryBestEffort(parentPath);
108
- });
109
- }
110
- finally {
111
- await pinned.handle.close().catch(() => undefined);
112
- }
113
- return result;
114
- }
115
- finally {
116
- let cleanupComplete = renamed;
117
- if (!renamed) {
118
- cleanupComplete = await cleanupTempPath(tempPath, stagedIdentity);
119
- }
120
- if (cleanupComplete)
121
- unregisterTempPath();
122
- }
14
+ const { result } = await writeCallbackSibling({
15
+ tempPath: buildSiblingTempPath(finalPath, params.fallbackFileName),
16
+ write: params.write,
17
+ resolveFinalPath: () => finalPath,
18
+ mode: params.mode,
19
+ maxBytes: params.maxBytes,
20
+ syncTempFile: true,
21
+ syncParentDir: true,
22
+ });
23
+ return result;
123
24
  }
@@ -1 +1 @@
1
- {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,wBAAwB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAClC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AAyDF,wBAAsB,2BAA2B,CAAC,CAAC,GAAG,IAAI,EACxD,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,CA+CrC"}
1
+ {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,wBAAwB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAClC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AAyDF,wBAAsB,2BAA2B,CAAC,CAAC,GAAG,IAAI,EACxD,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,CAgDrC"}
package/dist/output.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { normalizeMaxBytes } from "./byte-budget.js";
2
3
  import { FsSafeError } from "./errors.js";
3
4
  import { sanitizeUntrustedFileName } from "./filename.js";
4
5
  import { isPathInside } from "./path.js";
@@ -48,6 +49,7 @@ function assertFileTargetPath(targetPath) {
48
49
  }
49
50
  }
50
51
  export async function writeExternalFileWithinRoot(options) {
52
+ const maxBytes = normalizeMaxBytes(options.maxBytes);
51
53
  const targetRoot = await root(options.rootDir);
52
54
  const requestedTargetPath = options.path;
53
55
  if (requestedTargetPath.length === 0) {
@@ -72,7 +74,7 @@ export async function writeExternalFileWithinRoot(options) {
72
74
  finalPath: siblingFinalPath,
73
75
  write: options.write,
74
76
  fallbackFileName: options.fallbackFileName,
75
- maxBytes: options.maxBytes,
77
+ maxBytes,
76
78
  mode: options.mode,
77
79
  });
78
80
  return { path: siblingFinalPath, result };
@@ -84,7 +86,7 @@ export async function writeExternalFileWithinRoot(options) {
84
86
  try {
85
87
  const result = await options.write(staged.path);
86
88
  await targetRoot.copyIn(targetPath, staged.path, {
87
- maxBytes: options.maxBytes,
89
+ maxBytes,
88
90
  mode: options.mode,
89
91
  mkdir: true,
90
92
  sourceHardlinks: "reject",
@@ -1 +1 @@
1
- {"version":3,"file":"pinned-open.d.ts","sourceRoot":"","sources":["../src/pinned-open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAMzB,MAAM,MAAM,2BAA2B,GAAG,MAAM,GAAG,YAAY,GAAG,IAAI,CAAC;AAEvE,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAA;CAAE,GACtD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,2BAA2B,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAExE,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW,CAAC;AAE7D,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,OAAO,EAAE,EACT,WAAW,GAAG,WAAW,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CACpF,CAAC;AAYF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,yBAAyB,CAAC;IACxC,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB,GAAG,oBAAoB,CA+EvB"}
1
+ {"version":3,"file":"pinned-open.d.ts","sourceRoot":"","sources":["../src/pinned-open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAMzB,MAAM,MAAM,2BAA2B,GAAG,MAAM,GAAG,YAAY,GAAG,IAAI,CAAC;AAEvE,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAA;CAAE,GACtD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,2BAA2B,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAExE,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW,CAAC;AAE7D,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,OAAO,EAAE,EACT,WAAW,GAAG,WAAW,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CACpF,CAAC;AAQF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,yBAAyB,CAAC;IACxC,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB,GAAG,oBAAoB,CAoDvB"}
@@ -1,15 +1,12 @@
1
1
  import fs from "node:fs";
2
2
  import { isUnsafeDeviceReadPath } from "./device-path.js";
3
3
  import { FsSafeError } from "./errors.js";
4
- import { sameFileIdentity as hasSameFileIdentity } from "./file-identity.js";
4
+ import { inspectFileIdentitySync } from "./strict-file-identity.js";
5
5
  import { resolveReadOpenFlags } from "./read-open-flags.js";
6
6
  function isExpectedPathError(error) {
7
7
  const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
8
8
  return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
9
9
  }
10
- function sameFileIdentity(left, right) {
11
- return hasSameFileIdentity(left, right);
12
- }
13
10
  export function openPinnedFileSync(params) {
14
11
  const ioFs = params.ioFs ?? fs;
15
12
  const allowedType = params.allowedType ?? "file";
@@ -29,53 +26,30 @@ export function openPinnedFileSync(params) {
29
26
  if (isUnsafeDeviceReadPath(realPath)) {
30
27
  return { ok: false, reason: "validation" };
31
28
  }
32
- const preOpenStat = ioFs.lstatSync(realPath);
33
- if (!isAllowedType(preOpenStat, allowedType)) {
34
- return {
35
- ok: false,
36
- reason: "validation",
37
- error: new FsSafeError("not-file", "path does not have the required file type"),
38
- };
39
- }
40
- if (params.rejectHardlinks && preOpenStat.isFile() && preOpenStat.nlink > 1) {
41
- return {
42
- ok: false,
43
- reason: "validation",
44
- error: new FsSafeError("hardlink", "path must not be hardlinked"),
45
- };
46
- }
47
- if (params.maxBytes !== undefined &&
48
- preOpenStat.isFile() &&
49
- preOpenStat.size > params.maxBytes) {
50
- return { ok: false, reason: "validation" };
51
- }
29
+ const preOpenStat = inspectFileIdentitySync(() => {
30
+ const stat = ioFs.lstatSync(realPath, { bigint: true });
31
+ assertAllowedStat(stat, allowedType, params);
32
+ return stat;
33
+ });
52
34
  fd = ioFs.openSync(realPath, openReadFlags);
53
35
  const openedStat = ioFs.fstatSync(fd);
54
- if (!isAllowedType(openedStat, allowedType)) {
55
- return {
56
- ok: false,
57
- reason: "validation",
58
- error: new FsSafeError("not-file", "path does not have the required file type"),
59
- };
60
- }
61
- if (params.rejectHardlinks && openedStat.isFile() && openedStat.nlink > 1) {
62
- return {
63
- ok: false,
64
- reason: "validation",
65
- error: new FsSafeError("hardlink", "path must not be hardlinked"),
66
- };
67
- }
68
- if (params.maxBytes !== undefined && openedStat.isFile() && openedStat.size > params.maxBytes) {
69
- return { ok: false, reason: "validation" };
70
- }
71
- if (!sameFileIdentity(preOpenStat, openedStat)) {
72
- return { ok: false, reason: "validation" };
73
- }
36
+ const identity = inspectFileIdentitySync(() => {
37
+ const stat = ioFs.fstatSync(fd, { bigint: true });
38
+ assertAllowedStat(stat, allowedType, params);
39
+ return stat;
40
+ }, preOpenStat);
41
+ inspectFileIdentitySync(() => {
42
+ const stat = ioFs.lstatSync(realPath, { bigint: true });
43
+ assertAllowedStat(stat, allowedType, params);
44
+ return stat;
45
+ }, identity);
74
46
  const opened = { ok: true, path: realPath, fd, stat: openedStat };
75
47
  fd = null;
76
48
  return opened;
77
49
  }
78
50
  catch (error) {
51
+ if (error instanceof FsSafeError)
52
+ return { ok: false, reason: "validation", error };
79
53
  if (isExpectedPathError(error)) {
80
54
  return { ok: false, reason: "path", error };
81
55
  }
@@ -87,9 +61,14 @@ export function openPinnedFileSync(params) {
87
61
  }
88
62
  }
89
63
  }
90
- function isAllowedType(stat, allowedType) {
91
- if (allowedType === "directory") {
92
- return stat.isDirectory();
64
+ function assertAllowedStat(stat, allowedType, params) {
65
+ if (stat.isSymbolicLink() || !(allowedType === "directory" ? stat.isDirectory() : stat.isFile())) {
66
+ throw new FsSafeError("not-file", "path does not have the required file type");
67
+ }
68
+ if (params.rejectHardlinks && stat.isFile() && stat.nlink > 1n) {
69
+ throw new FsSafeError("hardlink", "path must not be hardlinked");
70
+ }
71
+ if (params.maxBytes !== undefined && stat.isFile() && stat.size > params.maxBytes) {
72
+ throw new FsSafeError("too-large", "file exceeds byte limit");
93
73
  }
94
- return stat.isFile();
95
74
  }
@@ -1 +1 @@
1
- {"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAkE,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAGhI,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAU3D,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,cAAc,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAA;CAAE,CAAC;AAgEzC,MAAM,MAAM,4BAA4B,GAAG,OAAO,GAAG,gBAAgB,CAAC;AAEtE,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,0BAA0B,CAAC;AAEzE,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE5E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC;IACxB,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC,wBAAwB,CAAC,EAAE,4BAA4B,CAAC;IAExD,eAAe,CAAC,EAAE,CAChB,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,sBAAsB,EAChC,WAAW,EAAE,mBAAmB,KAC7B,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB,CAAC;AAEF,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAe/F;AAED,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,iBAAiB,GAAG;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,oBAAoB,CAAC;CACvC,GACA,OAAO,CAAC,gBAAgB,CAAC,CA2B3B"}
1
+ {"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAkE,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAGhI,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAW3D,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,cAAc,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAA;CAAE,CAAC;AAgEzC,MAAM,MAAM,4BAA4B,GAAG,OAAO,GAAG,gBAAgB,CAAC;AAEtE,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,0BAA0B,CAAC;AAEzE,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE5E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC;IACxB,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC,wBAAwB,CAAC,EAAE,4BAA4B,CAAC;IAExD,eAAe,CAAC,EAAE,CAChB,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,sBAAsB,EAChC,WAAW,EAAE,mBAAmB,KAC7B,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB,CAAC;AAEF,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAgB/F;AAED,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,iBAAiB,GAAG;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,oBAAoB,CAAC;CACvC,GACA,OAAO,CAAC,gBAAgB,CAAC,CA2B3B"}
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import fsSync from "node:fs";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
+ import { normalizeMaxBytes } from "./byte-budget.js";
5
6
  import { createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
6
7
  import { FsSafeError } from "./errors.js";
7
8
  import { syncDirectoryBestEffort } from "./fsync.js";
@@ -11,6 +12,7 @@ import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
11
12
  import { runPinnedWriteNative } from "./native-pinned-write.js";
12
13
  import { getNativeBinding } from "./native.js";
13
14
  import { validatePinnedOperationPayload } from "./pinned-operation.js";
15
+ import { resolveReadOpenFlags } from "./read-open-flags.js";
14
16
  import { withSidecarLock } from "./sidecar-lock.js";
15
17
  import { getFsSafeTestHooks } from "./test-hooks.js";
16
18
  function byteLength(input, encoding) {
@@ -59,20 +61,21 @@ async function writeStreamToHandle(stream, handle, maxBytes) {
59
61
  }
60
62
  }
61
63
  export async function runPinnedWriteHelper(params) {
64
+ const normalizedParams = { ...params, maxBytes: normalizeMaxBytes(params.maxBytes) };
62
65
  assertSafeBasename(params.basename);
63
66
  validatePinnedOperationPayload({
64
67
  relativeParentPath: params.relativeParentPath,
65
68
  });
66
69
  // The explicit compatibility policy uses the guarded Node fallback, where
67
70
  // content verification can replace the strict post-rename inode check.
68
- if (params.onRenameIdentityMismatch === "verify-content") {
69
- return await runPinnedWriteFallback(params);
71
+ if (normalizedParams.onRenameIdentityMismatch === "verify-content") {
72
+ return await runPinnedWriteFallback(normalizedParams);
70
73
  }
71
74
  const native = getNativeBinding();
72
75
  if (native) {
73
- return await runPinnedWriteNative(native, params);
76
+ return await runPinnedWriteNative(native, normalizedParams);
74
77
  }
75
- return await runPinnedWriteFallback(params);
78
+ return await runPinnedWriteFallback(normalizedParams);
76
79
  }
77
80
  export async function runPinnedWriteWithRenamePolicy(params) {
78
81
  const { targetPath, renameIdentity, ...writeParams } = params;
@@ -153,7 +156,8 @@ async function runPinnedWriteFallback(params) {
153
156
  }
154
157
  }
155
158
  }
156
- const tempPath = path.join(parentPath, `.${params.basename}.${randomUUID()}.fallback.tmp`);
159
+ // Private staging must not consume the destination basename's filename budget.
160
+ const tempPath = path.join(parentPath, `.fs-safe-${randomUUID()}.tmp`);
157
161
  const tempFlags = fsSync.constants.O_WRONLY |
158
162
  fsSync.constants.O_CREAT |
159
163
  fsSync.constants.O_EXCL |
@@ -209,11 +213,7 @@ async function runPinnedWriteFallback(params) {
209
213
  throw new FsSafeError("path-mismatch", "fallback target changed during write");
210
214
  }
211
215
  const expectedHash = sha256Hex(params.input.data, params.input.encoding);
212
- const readFlags = fsSync.constants.O_RDONLY |
213
- (process.platform !== "win32" && "O_NOFOLLOW" in fsSync.constants
214
- ? fsSync.constants.O_NOFOLLOW
215
- : 0);
216
- readHandle = await fs.open(targetPath, readFlags);
216
+ readHandle = await fs.open(targetPath, resolveReadOpenFlags());
217
217
  const readHandleStat = await readHandle.stat({ bigint: true });
218
218
  const actualHash = sha256Hex(await readHandle.readFile());
219
219
  if (actualHash !== expectedHash) {
@@ -1,11 +1,13 @@
1
1
  import { type FileStore, type FileStoreSync } from "./file-store.js";
2
2
  import { type TempPathIdentityReceipt } from "./temp-cleanup.js";
3
- export type TempWorkspaceCleanupResult = "removed" | "missing" | "identity-mismatch";
3
+ import { type TempWorkspaceCleanupResult, type TempWorkspaceCleanupSafety } from "./temp-workspace-owner.js";
4
+ export type { TempWorkspaceCleanupResult, TempWorkspaceCleanupSafety, } from "./temp-workspace-owner.js";
4
5
  export type TempWorkspaceOptions = {
5
6
  rootDir: string;
6
7
  prefix: string;
7
8
  dirMode?: number;
8
9
  mode?: number;
10
+ cleanupSafety?: TempWorkspaceCleanupSafety;
9
11
  };
10
12
  export type TempWorkspace = {
11
13
  dir: string;
@@ -1 +1 @@
1
- {"version":3,"file":"private-temp-workspace.d.ts","sourceRoot":"","sources":["../src/private-temp-workspace.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAUzB,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,SAAS,GAAG,mBAAmB,CAAC;AAErF,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,uBAAuB,CAAC;IAClC,KAAK,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3D,SAAS,CACP,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,OAAO,EACb,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GACtC,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9D,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,OAAO,IAAI,OAAO,CAAC,0BAA0B,CAAC,CAAC;IAC/C,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,uBAAuB,CAAC;IAClC,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;IAC3D,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAClD,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,MAAM,CAAC;IAC5F,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,OAAO,IAAI,0BAA0B,CAAC;IACtC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;CAC1B,CAAC;AA+KF,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,aAAa,CAAC,CAExB;AAED,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAC5C,OAAO,CAAC,CAAC,CAAC,CAUZ;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,oBAAoB,GAC5B,iBAAiB,CA8EnB;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EACrC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,CAAC,GACvC,CAAC,CAUH"}
1
+ {"version":3,"file":"private-temp-workspace.d.ts","sourceRoot":"","sources":["../src/private-temp-workspace.ts"],"names":[],"mappings":"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"}
@@ -4,11 +4,18 @@ import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { fileStore, fileStoreSync, } from "./file-store.js";
6
6
  import { FsSafeError } from "./errors.js";
7
- import { sameFileIdentityForCleanup } from "./file-identity.js";
8
7
  import { isNotFoundPathError } from "./path.js";
9
8
  import { throwFsSafeReadError } from "./read-error.js";
10
9
  import { matchRootFileOpenFailure, openRootFileSync, } from "./root-file.js";
11
10
  import { registerTempPathForExit, } from "./temp-cleanup.js";
11
+ import { TempWorkspaceCleanupCapability, TempWorkspaceCleanupOwner, } from "./temp-workspace-owner.js";
12
+ function resolveTempWorkspaceCleanupSafety(value) {
13
+ if (value === undefined || value === "compatible")
14
+ return "compatible";
15
+ if (value === "require-bounded")
16
+ return value;
17
+ throw new TypeError("cleanupSafety must be compatible or require-bounded");
18
+ }
12
19
  function sanitizeTempPrefix(prefix) {
13
20
  const sanitized = prefix.trim().replace(/[^a-zA-Z0-9._-]/g, "-");
14
21
  if (!sanitized || sanitized === "." || sanitized === "..") {
@@ -79,52 +86,47 @@ function ensurePrivateDirectorySync(dir, mode) {
79
86
  // Best-effort on platforms that do not enforce POSIX modes.
80
87
  }
81
88
  }
82
- async function cleanupWorkspace(dir, identity) {
83
- let current;
84
- try {
85
- current = await fs.lstat(dir, { bigint: true });
86
- }
87
- catch (error) {
88
- return error.code === "ENOENT" ? "missing" : "identity-mismatch";
89
- }
90
- if (!sameFileIdentityForCleanup(current, identity)) {
91
- return "identity-mismatch";
92
- }
93
- await fs.rm(dir, { recursive: true, force: true });
94
- return "removed";
95
- }
96
- function cleanupWorkspaceSync(dir, identity) {
97
- let current;
98
- try {
99
- current = fsSync.lstatSync(dir, { bigint: true });
100
- }
101
- catch (error) {
102
- return error.code === "ENOENT" ? "missing" : "identity-mismatch";
103
- }
104
- if (!sameFileIdentityForCleanup(current, identity)) {
105
- return "identity-mismatch";
106
- }
107
- fsSync.rmSync(dir, { recursive: true, force: true });
108
- return "removed";
109
- }
110
89
  async function createTempWorkspace(options) {
111
90
  const dirMode = options.dirMode ?? 0o700;
112
91
  const mode = options.mode ?? 0o600;
92
+ const cleanupSafety = resolveTempWorkspaceCleanupSafety(options.cleanupSafety);
113
93
  const requestedRoot = path.resolve(options.rootDir);
114
94
  const root = await fs.realpath(requestedRoot).catch(() => requestedRoot);
115
95
  await ensurePrivateDirectory(root, dirMode);
116
- const dir = await fs.mkdtemp(path.join(root, sanitizeTempPrefix(options.prefix)));
117
- await fs.chmod(dir, dirMode).catch(() => undefined);
118
- const stat = await fs.lstat(dir, { bigint: true });
119
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
120
- throw new Error(`Temp workspace must be a directory: ${dir}`);
96
+ const capability = new TempWorkspaceCleanupCapability(root, cleanupSafety);
97
+ let dir;
98
+ let stat;
99
+ let cleanupOwner;
100
+ let unregisterTempDir;
101
+ try {
102
+ dir = await fs.mkdtemp(path.join(root, sanitizeTempPrefix(options.prefix)));
103
+ await fs.chmod(dir, dirMode).catch(() => undefined);
104
+ stat = await fs.lstat(dir, { bigint: true });
105
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
106
+ throw new Error(`Temp workspace must be a directory: ${dir}`);
107
+ }
108
+ if (capability.parent)
109
+ capability.assertCurrent();
110
+ cleanupOwner = new TempWorkspaceCleanupOwner(dir, stat, capability);
111
+ unregisterTempDir = registerTempPathForExit(dir, {
112
+ cleanupSync: () => cleanupOwner.cleanupSync(),
113
+ });
121
114
  }
115
+ catch (error) {
116
+ try {
117
+ if (cleanupOwner)
118
+ cleanupOwner.cleanupSync();
119
+ else
120
+ capability.close();
121
+ }
122
+ catch (cleanupError) {
123
+ throw new AggregateError([error, cleanupError], "temp workspace creation and cleanup both failed");
124
+ }
125
+ throw error;
126
+ }
127
+ const owner = cleanupOwner;
122
128
  const identity = { dev: Number(stat.dev), ino: Number(stat.ino) };
123
- const cleanupIdentity = { dev: stat.dev, ino: stat.ino };
124
- const unregisterTempDir = registerTempPathForExit(dir, {
125
- recursive: true,
126
- identity: cleanupIdentity,
127
- });
129
+ // Once registered, even a store-construction failure remains exit-cleanable.
128
130
  const store = fileStore({ rootDir: dir, private: true, dirMode, mode });
129
131
  return {
130
132
  dir,
@@ -148,7 +150,7 @@ async function createTempWorkspace(options) {
148
150
  },
149
151
  cleanup: async () => {
150
152
  try {
151
- return await cleanupWorkspace(dir, cleanupIdentity);
153
+ return await owner.cleanup();
152
154
  }
153
155
  finally {
154
156
  unregisterTempDir();
@@ -156,7 +158,7 @@ async function createTempWorkspace(options) {
156
158
  },
157
159
  [Symbol.asyncDispose]: async () => {
158
160
  try {
159
- await cleanupWorkspace(dir, cleanupIdentity);
161
+ await owner.cleanup();
160
162
  }
161
163
  finally {
162
164
  unregisterTempDir();
@@ -182,6 +184,7 @@ export async function withTempWorkspace(options, run) {
182
184
  export function tempWorkspaceSync(options) {
183
185
  const dirMode = options.dirMode ?? 0o700;
184
186
  const mode = options.mode ?? 0o600;
187
+ const cleanupSafety = resolveTempWorkspaceCleanupSafety(options.cleanupSafety);
185
188
  const requestedRoot = path.resolve(options.rootDir);
186
189
  let root = requestedRoot;
187
190
  try {
@@ -191,23 +194,45 @@ export function tempWorkspaceSync(options) {
191
194
  root = requestedRoot;
192
195
  }
193
196
  ensurePrivateDirectorySync(root, dirMode);
194
- const dir = fsSync.mkdtempSync(path.join(root, sanitizeTempPrefix(options.prefix)));
197
+ const capability = new TempWorkspaceCleanupCapability(root, cleanupSafety);
198
+ let dir;
199
+ let stat;
200
+ let cleanupOwner;
201
+ let unregisterTempDir;
195
202
  try {
196
- fsSync.chmodSync(dir, dirMode);
203
+ dir = fsSync.mkdtempSync(path.join(root, sanitizeTempPrefix(options.prefix)));
204
+ try {
205
+ fsSync.chmodSync(dir, dirMode);
206
+ }
207
+ catch {
208
+ // Best-effort on platforms that do not enforce POSIX modes.
209
+ }
210
+ stat = fsSync.lstatSync(dir, { bigint: true });
211
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
212
+ throw new Error(`Temp workspace must be a directory: ${dir}`);
213
+ }
214
+ if (capability.parent)
215
+ capability.assertCurrent();
216
+ cleanupOwner = new TempWorkspaceCleanupOwner(dir, stat, capability);
217
+ unregisterTempDir = registerTempPathForExit(dir, {
218
+ cleanupSync: () => cleanupOwner.cleanupSync(),
219
+ });
197
220
  }
198
- catch {
199
- // Best-effort on platforms that do not enforce POSIX modes.
200
- }
201
- const stat = fsSync.lstatSync(dir, { bigint: true });
202
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
203
- throw new Error(`Temp workspace must be a directory: ${dir}`);
221
+ catch (error) {
222
+ try {
223
+ if (cleanupOwner)
224
+ cleanupOwner.cleanupSync();
225
+ else
226
+ capability.close();
227
+ }
228
+ catch (cleanupError) {
229
+ throw new AggregateError([error, cleanupError], "temp workspace creation and cleanup both failed");
230
+ }
231
+ throw error;
204
232
  }
233
+ const owner = cleanupOwner;
205
234
  const identity = { dev: Number(stat.dev), ino: Number(stat.ino) };
206
- const cleanupIdentity = { dev: stat.dev, ino: stat.ino };
207
- const unregisterTempDir = registerTempPathForExit(dir, {
208
- recursive: true,
209
- identity: cleanupIdentity,
210
- });
235
+ // Once registered, even a store-construction failure remains exit-cleanable.
211
236
  const store = fileStoreSync({ rootDir: dir, private: true, dirMode, mode });
212
237
  return {
213
238
  dir,
@@ -244,7 +269,7 @@ export function tempWorkspaceSync(options) {
244
269
  },
245
270
  cleanup: () => {
246
271
  try {
247
- return cleanupWorkspaceSync(dir, cleanupIdentity);
272
+ return owner.cleanupSync();
248
273
  }
249
274
  finally {
250
275
  unregisterTempDir();
@@ -252,7 +277,7 @@ export function tempWorkspaceSync(options) {
252
277
  },
253
278
  [Symbol.dispose]: () => {
254
279
  try {
255
- cleanupWorkspaceSync(dir, cleanupIdentity);
280
+ owner.cleanupSync();
256
281
  }
257
282
  finally {
258
283
  unregisterTempDir();
@@ -1 +1 @@
1
- {"version":3,"file":"publish-file.d.ts","sourceRoot":"","sources":["../src/publish-file.ts"],"names":[],"mappings":"AACA,OAAe,EAAoB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAI/D,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EAC1B,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAML,KAAK,qCAAqC,EAC3C,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EACV,2BAA2B,EAC3B,wCAAwC,EACxC,kCAAkC,EAClC,gCAAgC,EAChC,qCAAqC,GACtC,MAAM,2BAA2B,CAAC;AAEnC,MAAM,MAAM,4BAA4B,GACpC,cAAc,GACd,eAAe,GACf,kBAAkB,CAAC;AAEvB,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;IAC3D,QAAQ,EAAE,KAAK,CAAC;IAChB,aAAa,EAAE,oBAAoB,CAAC;CACrC,CAAC;AAkBF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE/D;AA2ND,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,sBAAsB,CAAC,EAAE,gBAAgB,CAAC;IAC1C,QAAQ,EAAE,4BAA4B,CAAC;IACvC,aAAa,CAAC,EAAE,qCAAqC,CAAC;IACtD,aAAa,CAAC,EAAE,gBAAgB,CAAC;CAClC,GAAG,OAAO,CAAC,0BAA0B,CAAC,CA4MtC"}
1
+ {"version":3,"file":"publish-file.d.ts","sourceRoot":"","sources":["../src/publish-file.ts"],"names":[],"mappings":"AACA,OAAe,EAAoB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAI/D,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EAC1B,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EAML,KAAK,qCAAqC,EAC3C,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EACV,2BAA2B,EAC3B,wCAAwC,EACxC,kCAAkC,EAClC,gCAAgC,EAChC,qCAAqC,GACtC,MAAM,2BAA2B,CAAC;AAEnC,MAAM,MAAM,4BAA4B,GACpC,cAAc,GACd,eAAe,GACf,kBAAkB,CAAC;AAEvB,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;IAC3D,QAAQ,EAAE,KAAK,CAAC;IAChB,aAAa,EAAE,oBAAoB,CAAC;CACrC,CAAC;AAkBF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE/D;AAsND,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,sBAAsB,CAAC,EAAE,gBAAgB,CAAC;IAC1C,QAAQ,EAAE,4BAA4B,CAAC;IACvC,aAAa,CAAC,EAAE,qCAAqC,CAAC;IACtD,aAAa,CAAC,EAAE,gBAAgB,CAAC;CAClC,GAAG,OAAO,CAAC,0BAA0B,CAAC,CA4MtC"}
@@ -8,6 +8,7 @@ import { hashFileHandle } from "./file-hash.js";
8
8
  import { sameFileIdentity, sameFileIdentityForCleanup, } from "./file-identity.js";
9
9
  import { syncNativeFileBestEffort } from "./native-operations.js";
10
10
  import { getNativeBinding, requireNativeBinding } from "./native.js";
11
+ import { resolveReadOpenFlags } from "./read-open-flags.js";
11
12
  import { directorySyncFailure, publicationFailure, rememberCreatedTarget, } from "./publish-file-failure.js";
12
13
  import { getFsSafeTestHooks } from "./test-hooks.js";
13
14
  const HARDLINK_FALLBACK_CODES = new Set([
@@ -29,10 +30,7 @@ export function isHardlinkFallbackError(error) {
29
30
  return HARDLINK_FALLBACK_CODES.has(error?.code ?? "");
30
31
  }
31
32
  function sourceOpenFlags() {
32
- return (fsSync.constants.O_RDONLY |
33
- (process.platform !== "win32" && typeof fsSync.constants.O_NOFOLLOW === "number"
34
- ? fsSync.constants.O_NOFOLLOW
35
- : 0));
33
+ return resolveReadOpenFlags();
36
34
  }
37
35
  function directoryOpenFlags() {
38
36
  return (fsSync.constants.O_RDONLY |
@@ -1 +1 @@
1
- {"version":3,"file":"read-opened-file.d.ts","sourceRoot":"","sources":["../src/read-opened-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAI7D,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,oBAAoB,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;CACb,CAAC;AAEF,KAAK,UAAU,GAAG;IAChB,MAAM,EAAE,UAAU,CAAC;IACnB,WAAW,EAAE,oBAAoB,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;CACb,CAAC;AAEF,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,UAAU,CAAC,CAiBtB"}
1
+ {"version":3,"file":"read-opened-file.d.ts","sourceRoot":"","sources":["../src/read-opened-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAI7D,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,oBAAoB,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;CACb,CAAC;AAEF,KAAK,UAAU,GAAG;IAChB,MAAM,EAAE,UAAU,CAAC;IACnB,WAAW,EAAE,oBAAoB,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;CACb,CAAC;AAEF,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,UAAU,CAAC,CAkBtB"}