@openclaw/fs-safe 0.6.0 → 0.7.0

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 (183) hide show
  1. package/CHANGELOG.md +50 -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 -26
  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/file-lock-sync.d.ts.map +1 -1
  57. package/dist/file-lock-sync.js +10 -5
  58. package/dist/file-store-boundary.d.ts.map +1 -1
  59. package/dist/file-store-boundary.js +9 -5
  60. package/dist/file-store-limit.d.ts +2 -0
  61. package/dist/file-store-limit.d.ts.map +1 -0
  62. package/dist/file-store-limit.js +8 -0
  63. package/dist/file-store-sync-write.d.ts.map +1 -1
  64. package/dist/file-store-sync-write.js +38 -8
  65. package/dist/file-store.d.ts.map +1 -1
  66. package/dist/file-store.js +26 -26
  67. package/dist/json-durable-queue-directory.d.ts +2 -0
  68. package/dist/json-durable-queue-directory.d.ts.map +1 -0
  69. package/dist/json-durable-queue-directory.js +20 -0
  70. package/dist/json-durable-queue-ownership.d.ts +14 -0
  71. package/dist/json-durable-queue-ownership.d.ts.map +1 -0
  72. package/dist/json-durable-queue-ownership.js +168 -0
  73. package/dist/json-durable-queue-retirement.d.ts +9 -0
  74. package/dist/json-durable-queue-retirement.d.ts.map +1 -0
  75. package/dist/json-durable-queue-retirement.js +126 -0
  76. package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
  77. package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
  78. package/dist/json-durable-queue-transfer-lock.js +19 -0
  79. package/dist/json-durable-queue.d.ts +1 -0
  80. package/dist/json-durable-queue.d.ts.map +1 -1
  81. package/dist/json-durable-queue.js +90 -57
  82. package/dist/json.d.ts.map +1 -1
  83. package/dist/json.js +27 -8
  84. package/dist/local-roots.d.ts.map +1 -1
  85. package/dist/local-roots.js +4 -2
  86. package/dist/native-binding.d.ts +12 -3
  87. package/dist/native-binding.d.ts.map +1 -1
  88. package/dist/output-sibling.d.ts.map +1 -1
  89. package/dist/output-sibling.js +11 -110
  90. package/dist/output.d.ts.map +1 -1
  91. package/dist/output.js +4 -2
  92. package/dist/pinned-open.d.ts.map +1 -1
  93. package/dist/pinned-open.js +27 -48
  94. package/dist/pinned-write.d.ts.map +1 -1
  95. package/dist/pinned-write.js +8 -9
  96. package/dist/private-temp-workspace.d.ts +3 -1
  97. package/dist/private-temp-workspace.d.ts.map +1 -1
  98. package/dist/private-temp-workspace.js +81 -56
  99. package/dist/publish-file.d.ts.map +1 -1
  100. package/dist/publish-file.js +2 -4
  101. package/dist/read-opened-file.d.ts.map +1 -1
  102. package/dist/read-opened-file.js +6 -4
  103. package/dist/regular-file.d.ts.map +1 -1
  104. package/dist/regular-file.js +136 -82
  105. package/dist/replace-file-copy-fallback.d.ts +3 -1
  106. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  107. package/dist/replace-file-copy-fallback.js +21 -36
  108. package/dist/replace-file-copy-source.d.ts +21 -0
  109. package/dist/replace-file-copy-source.d.ts.map +1 -0
  110. package/dist/replace-file-copy-source.js +112 -0
  111. package/dist/replace-file-descriptor.d.ts +13 -3
  112. package/dist/replace-file-descriptor.d.ts.map +1 -1
  113. package/dist/replace-file-descriptor.js +32 -7
  114. package/dist/replace-file-rename-policy.d.ts +7 -0
  115. package/dist/replace-file-rename-policy.d.ts.map +1 -0
  116. package/dist/replace-file-rename-policy.js +30 -0
  117. package/dist/replace-file-temp-owner.d.ts +46 -0
  118. package/dist/replace-file-temp-owner.d.ts.map +1 -0
  119. package/dist/replace-file-temp-owner.js +346 -0
  120. package/dist/replace-file.d.ts +6 -1
  121. package/dist/replace-file.d.ts.map +1 -1
  122. package/dist/replace-file.js +72 -58
  123. package/dist/root-impl.d.ts.map +1 -1
  124. package/dist/root-impl.js +40 -26
  125. package/dist/secret-file.d.ts.map +1 -1
  126. package/dist/secret-file.js +6 -3
  127. package/dist/secret-read-async.d.ts.map +1 -1
  128. package/dist/secret-read-async.js +6 -5
  129. package/dist/secure-file.d.ts.map +1 -1
  130. package/dist/secure-file.js +17 -13
  131. package/dist/sibling-staged-file.d.ts +15 -0
  132. package/dist/sibling-staged-file.d.ts.map +1 -0
  133. package/dist/sibling-staged-file.js +148 -0
  134. package/dist/sibling-temp.d.ts +3 -0
  135. package/dist/sibling-temp.d.ts.map +1 -1
  136. package/dist/sibling-temp.js +30 -74
  137. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  138. package/dist/sidecar-lock-acquire.js +48 -27
  139. package/dist/sidecar-lock-handle.d.ts +6 -2
  140. package/dist/sidecar-lock-handle.d.ts.map +1 -1
  141. package/dist/sidecar-lock-handle.js +17 -3
  142. package/dist/sidecar-lock-policy.d.ts +2 -0
  143. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  144. package/dist/sidecar-lock-policy.js +29 -0
  145. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  146. package/dist/sidecar-lock-reclaim.js +29 -7
  147. package/dist/sidecar-lock.d.ts.map +1 -1
  148. package/dist/sidecar-lock.js +28 -16
  149. package/dist/suppressed-error.d.ts +6 -0
  150. package/dist/suppressed-error.d.ts.map +1 -0
  151. package/dist/suppressed-error.js +15 -0
  152. package/dist/temp-cleanup.d.ts +2 -0
  153. package/dist/temp-cleanup.d.ts.map +1 -1
  154. package/dist/temp-cleanup.js +25 -10
  155. package/dist/temp-workspace-owner.d.ts +23 -0
  156. package/dist/temp-workspace-owner.d.ts.map +1 -0
  157. package/dist/temp-workspace-owner.js +320 -0
  158. package/dist/temp.d.ts +1 -1
  159. package/dist/temp.d.ts.map +1 -1
  160. package/dist/test-hooks.d.ts +5 -0
  161. package/dist/test-hooks.d.ts.map +1 -1
  162. package/docs/advanced.md +8 -1
  163. package/docs/archive.md +194 -26
  164. package/docs/atomic.md +11 -2
  165. package/docs/durability.md +1 -1
  166. package/docs/file-store.md +2 -0
  167. package/docs/install.md +8 -4
  168. package/docs/json.md +8 -4
  169. package/docs/native-helper.md +15 -3
  170. package/docs/native.md +33 -6
  171. package/docs/output.md +10 -0
  172. package/docs/public-api.md +6 -3
  173. package/docs/quickstart.md +1 -1
  174. package/docs/reading.md +1 -1
  175. package/docs/regular-file.md +9 -2
  176. package/docs/root.md +2 -0
  177. package/docs/secret-file.md +5 -1
  178. package/docs/secure-file.md +4 -2
  179. package/docs/security-model.md +18 -0
  180. package/docs/sidecar-lock.md +4 -0
  181. package/docs/store.md +13 -0
  182. package/docs/temp.md +128 -14
  183. package/package.json +10 -10
@@ -7,18 +7,32 @@ function pathStillMatchesReceipt(entry) {
7
7
  return false;
8
8
  }
9
9
  try {
10
- return sameFileIdentityForCleanup(fsSync.lstatSync(entry.path, { bigint: true }), entry.identity);
10
+ const current = fsSync.lstatSync(entry.path, { bigint: true });
11
+ return (!entry.singleLinkFile || (current.isFile() && current.nlink === 1n)) &&
12
+ sameFileIdentityForCleanup(current, entry.identity);
11
13
  }
12
14
  catch (error) {
13
- return error.code === "ENOENT";
15
+ return !entry.singleLinkFile && error.code === "ENOENT";
16
+ }
17
+ }
18
+ function removeRegisteredPathSync(entry) {
19
+ if (entry.singleLinkFile)
20
+ fsSync.unlinkSync(entry.path);
21
+ else
22
+ fsSync.rmSync(entry.path, { force: true, recursive: entry.recursive });
23
+ }
24
+ function cleanupEntrySync(entry) {
25
+ if (entry.cleanupSync) {
26
+ entry.cleanupSync();
27
+ }
28
+ else if (pathStillMatchesReceipt(entry)) {
29
+ removeRegisteredPathSync(entry);
14
30
  }
15
31
  }
16
32
  function cleanupRegisteredTempPathsSync() {
17
33
  for (const entry of tempCleanupEntries.values()) {
18
34
  try {
19
- if (pathStillMatchesReceipt(entry)) {
20
- fsSync.rmSync(entry.path, { force: true, recursive: entry.recursive });
21
- }
35
+ cleanupEntrySync(entry);
22
36
  }
23
37
  catch {
24
38
  // Process-exit cleanup is best-effort.
@@ -35,8 +49,10 @@ export function registerTempPathForExit(tempPath, options) {
35
49
  path: tempPath,
36
50
  recursive: options?.recursive === true,
37
51
  identity: options?.identity,
52
+ singleLinkFile: options?.singleLinkFile,
53
+ cleanupSync: options?.cleanupSync,
38
54
  };
39
- if (!entry.identity) {
55
+ if (!entry.identity && !entry.cleanupSync) {
40
56
  try {
41
57
  entry.identity = fsSync.lstatSync(tempPath, { bigint: true });
42
58
  }
@@ -46,7 +62,8 @@ export function registerTempPathForExit(tempPath, options) {
46
62
  }
47
63
  tempCleanupEntries.set(tempPath, entry);
48
64
  const unregister = (() => {
49
- tempCleanupEntries.delete(tempPath);
65
+ if (tempCleanupEntries.get(tempPath) === entry)
66
+ tempCleanupEntries.delete(tempPath);
50
67
  });
51
68
  unregister.setIdentity = (identity) => {
52
69
  entry.identity = identity;
@@ -62,9 +79,7 @@ export function __cleanupRegisteredTempPathForTest(tempPath) {
62
79
  return;
63
80
  }
64
81
  try {
65
- if (pathStillMatchesReceipt(entry)) {
66
- fsSync.rmSync(entry.path, { force: true, recursive: entry.recursive });
67
- }
82
+ cleanupEntrySync(entry);
68
83
  }
69
84
  finally {
70
85
  tempCleanupEntries.delete(tempPath);
@@ -0,0 +1,23 @@
1
+ import { type FileIdentityStat } from "./file-identity.js";
2
+ import { type NativeBinding } from "./native.js";
3
+ import { openStagedDirectory } from "./staged-directory.js";
4
+ export type TempWorkspaceCleanupResult = "removed" | "missing" | "identity-mismatch" | "indeterminate";
5
+ export type TempWorkspaceCleanupSafety = "compatible" | "require-bounded";
6
+ type RetainedDirectory = ReturnType<typeof openStagedDirectory>;
7
+ export declare class TempWorkspaceCleanupCapability {
8
+ #private;
9
+ readonly binding: NativeBinding | undefined;
10
+ readonly parent: RetainedDirectory | undefined;
11
+ constructor(root: string, safety: TempWorkspaceCleanupSafety);
12
+ get canRemoveOwnedTree(): boolean;
13
+ assertCurrent(): void;
14
+ close(): void;
15
+ }
16
+ export declare class TempWorkspaceCleanupOwner {
17
+ #private;
18
+ constructor(dir: string, identity: FileIdentityStat, capability: TempWorkspaceCleanupCapability);
19
+ cleanup(): Promise<TempWorkspaceCleanupResult>;
20
+ cleanupSync(): TempWorkspaceCleanupResult;
21
+ }
22
+ export {};
23
+ //# sourceMappingURL=temp-workspace-owner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"temp-workspace-owner.d.ts","sourceRoot":"","sources":["../src/temp-workspace-owner.ts"],"names":[],"mappings":"AAMA,OAAO,EAA8B,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEvF,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAEnE,OAAO,EAAgC,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAG1F,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,SAAS,GAAG,mBAAmB,GAAG,eAAe,CAAC;AACvG,MAAM,MAAM,0BAA0B,GAAG,YAAY,GAAG,iBAAiB,CAAC;AAE1E,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAoBhE,qBAAa,8BAA8B;;IACzC,QAAQ,CAAC,OAAO,EAAE,aAAa,GAAG,SAAS,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAI/C,YAAY,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,0BAA0B,EAiC3D;IAED,IAAI,kBAAkB,IAAI,OAAO,CAEhC;IAED,aAAa,IAAI,IAAI,CASpB;IAED,KAAK,IAAI,IAAI,CAIZ;CACF;AAED,qBAAa,yBAAyB;;IAWpC,YAAY,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB,EAAE,UAAU,EAAE,8BAA8B,EAe9F;IAuLD,OAAO,IAAI,OAAO,CAAC,0BAA0B,CAAC,CAM7C;IAED,WAAW,IAAI,0BAA0B,CAexC;CACF"}
@@ -0,0 +1,320 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fsSync from "node:fs";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { createAsyncDirectoryGuard, createSyncDirectoryGuard } from "./directory-guard.js";
6
+ import { FsSafeError } from "./errors.js";
7
+ import { sameFileIdentityForCleanup } from "./file-identity.js";
8
+ import { withAsyncDirectoryGuards, withSyncDirectoryGuards } from "./guarded-mutation.js";
9
+ import { getNativeBinding } from "./native.js";
10
+ import { assertStagedDirectoryCurrent, openStagedDirectory } from "./staged-directory.js";
11
+ import { getFsSafeTestHooks } from "./test-hooks.js";
12
+ function isNativeCleanupBinding(binding) {
13
+ return typeof binding?.renameNoReplace === "function" &&
14
+ typeof binding.removeOwnedTree === "function" &&
15
+ typeof binding.removeOwnedTreeSync === "function" &&
16
+ typeof binding.ownedTreeRemovalAvailable === "function";
17
+ }
18
+ function nativeRemovalError(result) {
19
+ if (!result.errorCode)
20
+ return undefined;
21
+ return Object.assign(new Error(result.errorMessage ?? "native owned-tree cleanup failed"), {
22
+ code: result.errorCode,
23
+ });
24
+ }
25
+ export class TempWorkspaceCleanupCapability {
26
+ binding;
27
+ parent;
28
+ #ownedTreeRemovalAvailable;
29
+ #closed = false;
30
+ constructor(root, safety) {
31
+ let binding;
32
+ try {
33
+ binding = getNativeBinding();
34
+ }
35
+ catch (error) {
36
+ if (safety === "require-bounded")
37
+ throw error;
38
+ }
39
+ this.binding = binding;
40
+ let parent;
41
+ try {
42
+ parent = openStagedDirectory(root);
43
+ assertStagedDirectoryCurrent(parent.receipt);
44
+ }
45
+ catch {
46
+ if (parent)
47
+ fsSync.closeSync(parent.fd);
48
+ parent = undefined;
49
+ }
50
+ this.parent = parent;
51
+ let available = false;
52
+ if (parent && isNativeCleanupBinding(binding)) {
53
+ try {
54
+ available = binding.ownedTreeRemovalAvailable(parent.fd) === true;
55
+ }
56
+ catch {
57
+ // Runtime denial must select fallback or reject before child creation.
58
+ }
59
+ }
60
+ this.#ownedTreeRemovalAvailable = available;
61
+ if (safety === "require-bounded" && !this.canRemoveOwnedTree) {
62
+ this.close();
63
+ throw new FsSafeError("helper-unavailable", "temp workspace owned-tree cleanup is unavailable");
64
+ }
65
+ }
66
+ get canRemoveOwnedTree() {
67
+ return !this.#closed && this.#ownedTreeRemovalAvailable;
68
+ }
69
+ assertCurrent() {
70
+ if (this.#closed || !this.parent) {
71
+ throw new FsSafeError("path-mismatch", "temp workspace cleanup parent is unavailable");
72
+ }
73
+ assertStagedDirectoryCurrent(this.parent.receipt);
74
+ const current = fsSync.fstatSync(this.parent.fd, { bigint: true });
75
+ if (!sameFileIdentityForCleanup(current, this.parent.receipt.identity)) {
76
+ throw new FsSafeError("path-mismatch", "temp workspace cleanup parent changed");
77
+ }
78
+ }
79
+ close() {
80
+ if (this.#closed)
81
+ return;
82
+ this.#closed = true;
83
+ if (this.parent)
84
+ fsSync.closeSync(this.parent.fd);
85
+ }
86
+ }
87
+ export class TempWorkspaceCleanupOwner {
88
+ #dir;
89
+ #identity;
90
+ #capability;
91
+ #directory;
92
+ #closed = false;
93
+ #running = false;
94
+ #exitInterrupted = false;
95
+ #result;
96
+ #pending;
97
+ constructor(dir, identity, capability) {
98
+ this.#dir = dir;
99
+ this.#identity = { dev: identity.dev, ino: identity.ino };
100
+ this.#capability = capability;
101
+ let directory;
102
+ if (capability.canRemoveOwnedTree) {
103
+ directory = openStagedDirectory(dir);
104
+ const current = fsSync.fstatSync(directory.fd, { bigint: true });
105
+ if (!sameFileIdentityForCleanup(current, this.#identity)) {
106
+ fsSync.closeSync(directory.fd);
107
+ capability.close();
108
+ throw new FsSafeError("path-mismatch", "temp workspace changed while retaining cleanup authority");
109
+ }
110
+ }
111
+ this.#directory = directory;
112
+ }
113
+ #repeat() {
114
+ return this.#result === "removed" ? "missing" : this.#result;
115
+ }
116
+ #close() {
117
+ if (this.#closed)
118
+ return;
119
+ this.#closed = true;
120
+ const errors = [];
121
+ if (this.#directory) {
122
+ try {
123
+ fsSync.closeSync(this.#directory.fd);
124
+ }
125
+ catch (error) {
126
+ errors.push(error);
127
+ }
128
+ }
129
+ try {
130
+ this.#capability.close();
131
+ }
132
+ catch (error) {
133
+ errors.push(error);
134
+ }
135
+ if (errors.length === 1)
136
+ throw errors[0];
137
+ if (errors.length > 1)
138
+ throw new AggregateError(errors, "temp workspace cleanup descriptor close failed");
139
+ }
140
+ #finish(result) {
141
+ this.#result ??= this.#exitInterrupted ? "indeterminate" : result;
142
+ try {
143
+ this.#close();
144
+ }
145
+ catch (error) {
146
+ this.#result = "indeterminate";
147
+ throw error;
148
+ }
149
+ return this.#result;
150
+ }
151
+ #fallbackResult() {
152
+ try {
153
+ const current = fsSync.lstatSync(this.#dir, { bigint: true });
154
+ return current.isDirectory() && !current.isSymbolicLink() &&
155
+ sameFileIdentityForCleanup(current, this.#identity)
156
+ ? "indeterminate"
157
+ : "identity-mismatch";
158
+ }
159
+ catch (error) {
160
+ return error.code === "ENOENT" ? "missing" : "indeterminate";
161
+ }
162
+ }
163
+ #prepare() {
164
+ const parent = this.#capability.parent;
165
+ if (!parent)
166
+ return this.#fallbackResult();
167
+ try {
168
+ this.#capability.assertCurrent();
169
+ let current;
170
+ try {
171
+ current = fsSync.lstatSync(this.#dir, { bigint: true });
172
+ }
173
+ catch (error) {
174
+ this.#capability.assertCurrent();
175
+ return error.code === "ENOENT" ? "missing" : "indeterminate";
176
+ }
177
+ this.#capability.assertCurrent();
178
+ if (!current.isDirectory() || current.isSymbolicLink() ||
179
+ !sameFileIdentityForCleanup(current, this.#identity)) {
180
+ return "identity-mismatch";
181
+ }
182
+ const name = `.fs-safe-workspace-cleanup-${randomUUID()}`;
183
+ const quarantinePath = path.join(parent.receipt.path, name);
184
+ const nativeRemoval = this.#capability.canRemoveOwnedTree && this.#directory !== undefined;
185
+ if (nativeRemoval) {
186
+ this.#capability.binding.renameNoReplace(parent.fd, path.basename(this.#dir), parent.fd, name);
187
+ }
188
+ else {
189
+ const guard = createSyncDirectoryGuard(parent.receipt.path);
190
+ withSyncDirectoryGuards([guard], () => {
191
+ this.#capability.assertCurrent();
192
+ fsSync.renameSync(this.#dir, quarantinePath);
193
+ });
194
+ }
195
+ this.#capability.assertCurrent();
196
+ const quarantined = fsSync.lstatSync(quarantinePath, { bigint: true });
197
+ this.#capability.assertCurrent();
198
+ if (!quarantined.isDirectory() || quarantined.isSymbolicLink() ||
199
+ !sameFileIdentityForCleanup(quarantined, this.#identity)) {
200
+ return "indeterminate";
201
+ }
202
+ return { name, path: quarantinePath, nativeRemoval };
203
+ }
204
+ catch {
205
+ // A failed rename can still have committed on a remote filesystem.
206
+ return "indeterminate";
207
+ }
208
+ }
209
+ #assertQuarantine(quarantine) {
210
+ const current = fsSync.lstatSync(quarantine.path, { bigint: true });
211
+ if (!current.isDirectory() || current.isSymbolicLink() ||
212
+ !sameFileIdentityForCleanup(current, this.#identity)) {
213
+ throw new FsSafeError("path-mismatch", "temp workspace quarantine changed");
214
+ }
215
+ this.#capability.assertCurrent();
216
+ }
217
+ #mapRemoval(result) {
218
+ const error = nativeRemovalError(result);
219
+ if (error) {
220
+ if (error.code === "path-mismatch")
221
+ return "indeterminate";
222
+ throw error;
223
+ }
224
+ return result.outcome === "removed" ? "removed" : "indeterminate";
225
+ }
226
+ async #remove(quarantine) {
227
+ if (quarantine.nativeRemoval) {
228
+ const beforeNativeRemoval = getFsSafeTestHooks()?.beforeTempWorkspaceNativeRemoval;
229
+ if (beforeNativeRemoval)
230
+ await beforeNativeRemoval(quarantine.path);
231
+ return this.#mapRemoval(await this.#capability.binding.removeOwnedTree(this.#capability.parent.fd, quarantine.name, this.#directory.fd));
232
+ }
233
+ let removalError;
234
+ try {
235
+ const guard = await createAsyncDirectoryGuard(this.#capability.parent.receipt.path);
236
+ await withAsyncDirectoryGuards([guard], async () => {
237
+ this.#assertQuarantine(quarantine);
238
+ try {
239
+ await fs.rm(quarantine.path, { recursive: true, force: true });
240
+ }
241
+ catch (error) {
242
+ removalError = error;
243
+ throw error;
244
+ }
245
+ });
246
+ return "removed";
247
+ }
248
+ catch (error) {
249
+ if (error === removalError)
250
+ throw error;
251
+ return "indeterminate";
252
+ }
253
+ }
254
+ #removeSync(quarantine) {
255
+ if (quarantine.nativeRemoval) {
256
+ getFsSafeTestHooks()?.beforeTempWorkspaceNativeRemovalSync?.(quarantine.path);
257
+ return this.#mapRemoval(this.#capability.binding.removeOwnedTreeSync(this.#capability.parent.fd, quarantine.name, this.#directory.fd));
258
+ }
259
+ let removalError;
260
+ try {
261
+ const guard = createSyncDirectoryGuard(this.#capability.parent.receipt.path);
262
+ withSyncDirectoryGuards([guard], () => {
263
+ this.#assertQuarantine(quarantine);
264
+ try {
265
+ fsSync.rmSync(quarantine.path, { recursive: true, force: true });
266
+ }
267
+ catch (error) {
268
+ removalError = error;
269
+ throw error;
270
+ }
271
+ });
272
+ return "removed";
273
+ }
274
+ catch (error) {
275
+ if (error === removalError)
276
+ throw error;
277
+ return "indeterminate";
278
+ }
279
+ }
280
+ async #run() {
281
+ let result = "indeterminate";
282
+ try {
283
+ const prepared = this.#prepare();
284
+ result = typeof prepared === "string" ? prepared : await this.#remove(prepared);
285
+ return this.#finish(result);
286
+ }
287
+ finally {
288
+ if (!this.#closed)
289
+ this.#finish(result);
290
+ }
291
+ }
292
+ cleanup() {
293
+ if (this.#result)
294
+ return Promise.resolve(this.#repeat());
295
+ if (this.#pending)
296
+ return this.#pending.then(() => this.#repeat());
297
+ this.#running = true;
298
+ this.#pending = this.#run();
299
+ return this.#pending;
300
+ }
301
+ cleanupSync() {
302
+ if (this.#result)
303
+ return this.#repeat();
304
+ if (this.#running) {
305
+ this.#exitInterrupted = true;
306
+ return "indeterminate";
307
+ }
308
+ this.#running = true;
309
+ let result = "indeterminate";
310
+ try {
311
+ const prepared = this.#prepare();
312
+ result = typeof prepared === "string" ? prepared : this.#removeSync(prepared);
313
+ return this.#finish(result);
314
+ }
315
+ finally {
316
+ if (!this.#closed)
317
+ this.#finish(result);
318
+ }
319
+ }
320
+ }
package/dist/temp.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { tempWorkspace, type TempWorkspace, type TempWorkspaceOptions, type TempWorkspaceCleanupResult, tempWorkspaceSync, type TempWorkspaceSync, withTempWorkspace, withTempWorkspaceSync, } from "./private-temp-workspace.js";
1
+ export { tempWorkspace, type TempWorkspace, type TempWorkspaceOptions, type TempWorkspaceCleanupResult, type TempWorkspaceCleanupSafety, tempWorkspaceSync, type TempWorkspaceSync, withTempWorkspace, withTempWorkspaceSync, } from "./private-temp-workspace.js";
2
2
  export type { TempPathIdentityReceipt } from "./temp-cleanup.js";
3
3
  export { resolveSecureTempRoot, type ResolveSecureTempRootOptions } from "./secure-temp-dir.js";
4
4
  //# sourceMappingURL=temp.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"temp.d.ts","sourceRoot":"","sources":["../src/temp.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,KAAK,0BAA0B,EAC/B,iBAAiB,EACjB,KAAK,iBAAiB,EACtB,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,6BAA6B,CAAC;AACrC,YAAY,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAE,qBAAqB,EAAE,KAAK,4BAA4B,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"temp.d.ts","sourceRoot":"","sources":["../src/temp.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,0BAA0B,EAC/B,iBAAiB,EACjB,KAAK,iBAAiB,EACtB,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,6BAA6B,CAAC;AACrC,YAAY,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAE,qBAAqB,EAAE,KAAK,4BAA4B,EAAE,MAAM,sBAAsB,CAAC"}
@@ -4,6 +4,7 @@ export type FsSafeTestHooks = {
4
4
  afterPreOpenLstat?: (filePath: string) => Promise<void> | void;
5
5
  beforeOpen?: (filePath: string, flags: number) => Promise<void> | void;
6
6
  afterOpen?: (filePath: string, handle: FileHandle) => Promise<void> | void;
7
+ afterOpenedPathIdentityCheck?: (filePath: string, handle: FileHandle) => Promise<void> | void;
7
8
  beforeArchiveOutputMutation?: (operation: "mkdir" | "chmod", targetPath: string) => Promise<void> | void;
8
9
  beforeFileStorePruneDescend?: (dirPath: string) => Promise<void> | void;
9
10
  beforeFileStoreSyncPrivateWrite?: (filePath: string) => void;
@@ -11,6 +12,10 @@ export type FsSafeTestHooks = {
11
12
  afterPinnedWriteFallbackRename?: (targetPath: string) => Promise<void> | void;
12
13
  beforeSiblingTempWrite?: (tempPath: string) => Promise<void> | void;
13
14
  beforeSidecarLockSnapshotOpen?: (lockPath: string) => Promise<void> | void;
15
+ beforeRegularFileAppendOpen?: (filePath: string) => Promise<void> | void;
16
+ beforeRegularFileAppendOpenSync?: (filePath: string) => void;
17
+ beforeTempWorkspaceNativeRemoval?: (quarantinePath: string) => Promise<void> | void;
18
+ beforeTempWorkspaceNativeRemovalSync?: (quarantinePath: string) => void;
14
19
  beforeTrashMove?: (targetPath: string, destPath: string) => void;
15
20
  afterPublishTargetCreated?: (method: "hardlink" | "exclusive-copy" | "rename-noreplace", targetPath: string, identity: FileIdentityStat) => Promise<void> | void;
16
21
  beforePublishDirectorySync?: (method: "hardlink" | "exclusive-copy" | "rename-noreplace", targetPath: string, identity: FileIdentityStat) => Promise<void> | void;
@@ -1 +1 @@
1
- {"version":3,"file":"test-hooks.d.ts","sourceRoot":"","sources":["../src/test-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,MAAM,MAAM,eAAe,GAAG;IAC5B,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC/D,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvE,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3E,2BAA2B,CAAC,EAAE,CAC5B,SAAS,EAAE,OAAO,GAAG,OAAO,EAC5B,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,2BAA2B,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACxE,+BAA+B,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D,0BAA0B,CAAC,EAAE,CAC3B,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,EACtC,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,8BAA8B,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9E,sBAAsB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACpE,6BAA6B,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3E,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACjE,yBAAyB,CAAC,EAAE,CAC1B,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,kBAAkB,EAC1D,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,gBAAgB,KACvB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,0BAA0B,CAAC,EAAE,CAC3B,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,kBAAkB,EAC1D,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,gBAAgB,KACvB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC3B,CAAC;AAQF,wBAAgB,kBAAkB,IAAI,eAAe,GAAG,SAAS,CAEhE;AAED,wBAAgB,2BAA2B,CAAC,KAAK,CAAC,EAAE,eAAe,GAAG,IAAI,CAKzE"}
1
+ {"version":3,"file":"test-hooks.d.ts","sourceRoot":"","sources":["../src/test-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,MAAM,MAAM,eAAe,GAAG;IAC5B,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC/D,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvE,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3E,4BAA4B,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9F,2BAA2B,CAAC,EAAE,CAC5B,SAAS,EAAE,OAAO,GAAG,OAAO,EAC5B,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,2BAA2B,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACxE,+BAA+B,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D,0BAA0B,CAAC,EAAE,CAC3B,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,EACtC,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,8BAA8B,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9E,sBAAsB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACpE,6BAA6B,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3E,2BAA2B,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACzE,+BAA+B,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D,gCAAgC,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACpF,oCAAoC,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IACxE,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACjE,yBAAyB,CAAC,EAAE,CAC1B,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,kBAAkB,EAC1D,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,gBAAgB,KACvB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,0BAA0B,CAAC,EAAE,CAC3B,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,kBAAkB,EAC1D,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,gBAAgB,KACvB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC3B,CAAC;AAQF,wBAAgB,kBAAkB,IAAI,eAAe,GAAG,SAAS,CAEhE;AAED,wBAAgB,2BAA2B,CAAC,KAAK,CAAC,EAAE,eAAe,GAAG,IAAI,CAKzE"}
package/docs/advanced.md CHANGED
@@ -72,6 +72,13 @@ Operational filesystem failures such as permissions or I/O errors are rethrown.
72
72
  | `assertNoSymlinkParents`, `assertNoSymlinkParentsSync`, `AssertNoSymlinkParentsOptions` | – | Reject paths whose ancestor chain contains symlinks. |
73
73
  | `assertNoHardlinkedFinalPath`, `assertNoPathAliasEscape`, `PATH_ALIAS_POLICIES`, `PathAliasPolicy` | – | Hardlink/alias defense building blocks. |
74
74
 
75
+ `openRootFile()` and `openRootFileSync()` compare exact bigint identities before
76
+ open, on the retained descriptor, and on the current resolved path. Their `stat`
77
+ receipts remain numeric. Custom `ioFs` adapters must honor `{ bigint: true }` for
78
+ `lstatSync` and `fstatSync`; numeric identity responses fail validation. Unknown
79
+ Windows identities receive one re-inspection without reopening, then fail
80
+ validation if still unknown.
81
+
75
82
  The bounded descriptor helpers start at the descriptor's current offset and
76
83
  leave ownership with the caller. They are intended for the second half of a
77
84
  safe read: first open and validate the path using the boundary appropriate to
@@ -118,7 +125,7 @@ component is followed by another segment, both helpers throw
118
125
  |---|---|---|
119
126
  | `stageFileInDirectory`, `StagedFile`, `StagedFileReceipt`, `PublishedFileReceipt`, `StagedFilePublication`, `StagedFileCleanupReceipt`, `StagedFileFailureDetails` | [staged-file.md](staged-file.md) | Native-required Linux/macOS lifecycle retaining the original directory for abort cleanup. |
120
127
  | `tempFile`, `withTempFile`, `TempFile`, `buildRandomTempFilePath`, `sanitizeTempFileName` | [temp.md](temp.md) | One-file temp primitive; prefer `tempWorkspace` from `@openclaw/fs-safe/temp` for the stable surface. |
121
- | `writeSiblingTempFile`, `writeViaSiblingTempPath`, `WriteSiblingTempFileOptions`, `WriteSiblingTempFileResult` | – | Sibling-temp write building block used by `replaceFileAtomic`. |
128
+ | `writeSiblingTempFile`, `writeViaSiblingTempPath`, `WriteSiblingTempFileOptions`, `WriteSiblingTempFileResult` | – | Callback-produced file staging: verified sibling publication or private-workspace copy through a root. |
122
129
 
123
130
  ### Permissions
124
131