@openclaw/fs-safe 0.5.6 → 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 (289) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +23 -10
  3. package/dist/advanced.d.ts +2 -1
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +1 -0
  6. package/dist/archive-crc32.d.ts +2 -0
  7. package/dist/archive-crc32.d.ts.map +1 -0
  8. package/dist/archive-crc32.js +14 -0
  9. package/dist/archive-deadline.d.ts +3 -0
  10. package/dist/archive-deadline.d.ts.map +1 -1
  11. package/dist/archive-deadline.js +44 -8
  12. package/dist/archive-entry.d.ts.map +1 -1
  13. package/dist/archive-entry.js +1 -0
  14. package/dist/archive-errors.d.ts +1 -0
  15. package/dist/archive-errors.d.ts.map +1 -1
  16. package/dist/archive-errors.js +3 -0
  17. package/dist/archive-input.d.ts.map +1 -1
  18. package/dist/archive-input.js +26 -16
  19. package/dist/archive-kind.js +2 -2
  20. package/dist/archive-limits.d.ts +11 -3
  21. package/dist/archive-limits.d.ts.map +1 -1
  22. package/dist/archive-limits.js +24 -0
  23. package/dist/archive-native.d.ts +3 -2
  24. package/dist/archive-native.d.ts.map +1 -1
  25. package/dist/archive-native.js +31 -9
  26. package/dist/archive-policy.d.ts +2 -0
  27. package/dist/archive-policy.d.ts.map +1 -1
  28. package/dist/archive-policy.js +9 -1
  29. package/dist/archive-read.d.ts.map +1 -1
  30. package/dist/archive-read.js +88 -41
  31. package/dist/archive-staging.d.ts +3 -0
  32. package/dist/archive-staging.d.ts.map +1 -1
  33. package/dist/archive-staging.js +91 -43
  34. package/dist/archive-tar-admission.d.ts +7 -0
  35. package/dist/archive-tar-admission.d.ts.map +1 -0
  36. package/dist/archive-tar-admission.js +43 -0
  37. package/dist/archive-tar-gnu.d.ts +2 -0
  38. package/dist/archive-tar-gnu.d.ts.map +1 -0
  39. package/dist/archive-tar-gnu.js +20 -0
  40. package/dist/archive-tar-header.d.ts +8 -0
  41. package/dist/archive-tar-header.d.ts.map +1 -0
  42. package/dist/archive-tar-header.js +47 -0
  43. package/dist/archive-tar-meta.d.ts +29 -1
  44. package/dist/archive-tar-meta.d.ts.map +1 -1
  45. package/dist/archive-tar-meta.js +135 -19
  46. package/dist/archive-tar-pax.d.ts +8 -0
  47. package/dist/archive-tar-pax.d.ts.map +1 -0
  48. package/dist/archive-tar-pax.js +100 -0
  49. package/dist/archive-tar-runtime.d.ts +4 -0
  50. package/dist/archive-tar-runtime.d.ts.map +1 -1
  51. package/dist/archive-tar-runtime.js +3 -0
  52. package/dist/archive-tar.d.ts.map +1 -1
  53. package/dist/archive-tar.js +8 -2
  54. package/dist/archive-zip-admission.d.ts +7 -0
  55. package/dist/archive-zip-admission.d.ts.map +1 -0
  56. package/dist/archive-zip-admission.js +60 -0
  57. package/dist/archive-zip-count.d.ts +2 -0
  58. package/dist/archive-zip-count.d.ts.map +1 -0
  59. package/dist/archive-zip-count.js +140 -0
  60. package/dist/archive-zip-directory.d.ts +8 -0
  61. package/dist/archive-zip-directory.d.ts.map +1 -0
  62. package/dist/archive-zip-directory.js +224 -0
  63. package/dist/archive-zip-integrity.d.ts.map +1 -1
  64. package/dist/archive-zip-integrity.js +1 -14
  65. package/dist/archive-zip-names.d.ts +12 -0
  66. package/dist/archive-zip-names.d.ts.map +1 -0
  67. package/dist/archive-zip-names.js +99 -0
  68. package/dist/archive-zip-preflight.d.ts +1 -1
  69. package/dist/archive-zip-preflight.d.ts.map +1 -1
  70. package/dist/archive-zip-preflight.js +8 -145
  71. package/dist/archive.d.ts.map +1 -1
  72. package/dist/archive.js +63 -42
  73. package/dist/atomic.d.ts +1 -1
  74. package/dist/atomic.d.ts.map +1 -1
  75. package/dist/bounded-read-stream.d.ts.map +1 -1
  76. package/dist/bounded-read-stream.js +2 -4
  77. package/dist/bounded-read.d.ts.map +1 -1
  78. package/dist/bounded-read.js +3 -10
  79. package/dist/byte-budget.d.ts +5 -0
  80. package/dist/byte-budget.d.ts.map +1 -0
  81. package/dist/byte-budget.js +9 -0
  82. package/dist/file-hash.d.ts.map +1 -1
  83. package/dist/file-hash.js +25 -19
  84. package/dist/file-lock-sync.d.ts.map +1 -1
  85. package/dist/file-lock-sync.js +26 -9
  86. package/dist/file-store-boundary.d.ts.map +1 -1
  87. package/dist/file-store-boundary.js +9 -5
  88. package/dist/file-store-limit.d.ts +2 -0
  89. package/dist/file-store-limit.d.ts.map +1 -0
  90. package/dist/file-store-limit.js +8 -0
  91. package/dist/file-store-sync-write.d.ts.map +1 -1
  92. package/dist/file-store-sync-write.js +38 -8
  93. package/dist/file-store.d.ts.map +1 -1
  94. package/dist/file-store.js +26 -26
  95. package/dist/json-durable-queue-directory.d.ts +2 -0
  96. package/dist/json-durable-queue-directory.d.ts.map +1 -0
  97. package/dist/json-durable-queue-directory.js +20 -0
  98. package/dist/json-durable-queue-ownership.d.ts +14 -0
  99. package/dist/json-durable-queue-ownership.d.ts.map +1 -0
  100. package/dist/json-durable-queue-ownership.js +168 -0
  101. package/dist/json-durable-queue-retirement.d.ts +9 -0
  102. package/dist/json-durable-queue-retirement.d.ts.map +1 -0
  103. package/dist/json-durable-queue-retirement.js +126 -0
  104. package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
  105. package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
  106. package/dist/json-durable-queue-transfer-lock.js +19 -0
  107. package/dist/json-durable-queue.d.ts +1 -0
  108. package/dist/json-durable-queue.d.ts.map +1 -1
  109. package/dist/json-durable-queue.js +90 -57
  110. package/dist/json.d.ts.map +1 -1
  111. package/dist/json.js +27 -8
  112. package/dist/local-roots.d.ts.map +1 -1
  113. package/dist/local-roots.js +4 -2
  114. package/dist/native-binding.d.ts +15 -3
  115. package/dist/native-binding.d.ts.map +1 -1
  116. package/dist/native-operations.d.ts +4 -1
  117. package/dist/native-operations.d.ts.map +1 -1
  118. package/dist/native-operations.js +22 -6
  119. package/dist/native-pinned-write-windows.d.ts +8 -0
  120. package/dist/native-pinned-write-windows.d.ts.map +1 -0
  121. package/dist/native-pinned-write-windows.js +92 -0
  122. package/dist/native-pinned-write.d.ts.map +1 -1
  123. package/dist/native-pinned-write.js +136 -127
  124. package/dist/native-staged-file.d.ts +24 -0
  125. package/dist/native-staged-file.d.ts.map +1 -0
  126. package/dist/native-staged-file.js +337 -0
  127. package/dist/native.d.ts.map +1 -1
  128. package/dist/native.js +4 -4
  129. package/dist/opened-realpath.d.ts +2 -0
  130. package/dist/opened-realpath.d.ts.map +1 -1
  131. package/dist/opened-realpath.js +12 -7
  132. package/dist/output-sibling.d.ts.map +1 -1
  133. package/dist/output-sibling.js +11 -110
  134. package/dist/output.d.ts.map +1 -1
  135. package/dist/output.js +4 -2
  136. package/dist/owner-dacl.d.ts.map +1 -1
  137. package/dist/owner-dacl.js +2 -1
  138. package/dist/permission-exec.d.ts +19 -0
  139. package/dist/permission-exec.d.ts.map +1 -1
  140. package/dist/permission-exec.js +57 -11
  141. package/dist/permissions-public.d.ts +1 -1
  142. package/dist/permissions-public.d.ts.map +1 -1
  143. package/dist/permissions-windows.d.ts +3 -0
  144. package/dist/permissions-windows.d.ts.map +1 -1
  145. package/dist/permissions-windows.js +16 -5
  146. package/dist/permissions.d.ts +5 -0
  147. package/dist/permissions.d.ts.map +1 -1
  148. package/dist/pinned-open.d.ts.map +1 -1
  149. package/dist/pinned-open.js +27 -48
  150. package/dist/pinned-write.d.ts +6 -0
  151. package/dist/pinned-write.d.ts.map +1 -1
  152. package/dist/pinned-write.js +26 -37
  153. package/dist/private-directory.d.ts.map +1 -1
  154. package/dist/private-directory.js +3 -2
  155. package/dist/private-temp-workspace.d.ts +3 -1
  156. package/dist/private-temp-workspace.d.ts.map +1 -1
  157. package/dist/private-temp-workspace.js +81 -56
  158. package/dist/publish-file.d.ts.map +1 -1
  159. package/dist/publish-file.js +2 -4
  160. package/dist/read-opened-file.d.ts.map +1 -1
  161. package/dist/read-opened-file.js +6 -4
  162. package/dist/regular-file.d.ts.map +1 -1
  163. package/dist/regular-file.js +136 -82
  164. package/dist/replace-file-copy-fallback.d.ts +3 -1
  165. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  166. package/dist/replace-file-copy-fallback.js +21 -36
  167. package/dist/replace-file-copy-source.d.ts +21 -0
  168. package/dist/replace-file-copy-source.d.ts.map +1 -0
  169. package/dist/replace-file-copy-source.js +112 -0
  170. package/dist/replace-file-descriptor.d.ts +13 -3
  171. package/dist/replace-file-descriptor.d.ts.map +1 -1
  172. package/dist/replace-file-descriptor.js +32 -7
  173. package/dist/replace-file-rename-policy.d.ts +7 -0
  174. package/dist/replace-file-rename-policy.d.ts.map +1 -0
  175. package/dist/replace-file-rename-policy.js +30 -0
  176. package/dist/replace-file-temp-owner.d.ts +46 -0
  177. package/dist/replace-file-temp-owner.d.ts.map +1 -0
  178. package/dist/replace-file-temp-owner.js +346 -0
  179. package/dist/replace-file.d.ts +6 -1
  180. package/dist/replace-file.d.ts.map +1 -1
  181. package/dist/replace-file.js +72 -58
  182. package/dist/root-impl.d.ts.map +1 -1
  183. package/dist/root-impl.js +118 -98
  184. package/dist/root-paths.d.ts +11 -14
  185. package/dist/root-paths.d.ts.map +1 -1
  186. package/dist/root-paths.js +36 -27
  187. package/dist/root-write-verification.d.ts +11 -0
  188. package/dist/root-write-verification.d.ts.map +1 -0
  189. package/dist/root-write-verification.js +91 -0
  190. package/dist/secret-file.d.ts +1 -6
  191. package/dist/secret-file.d.ts.map +1 -1
  192. package/dist/secret-file.js +49 -120
  193. package/dist/secret-read-async.d.ts +1 -1
  194. package/dist/secret-read-async.d.ts.map +1 -1
  195. package/dist/secret-read-async.js +51 -72
  196. package/dist/secret-read-policy.d.ts +13 -0
  197. package/dist/secret-read-policy.d.ts.map +1 -0
  198. package/dist/secret-read-policy.js +28 -0
  199. package/dist/secret.d.ts +2 -1
  200. package/dist/secret.d.ts.map +1 -1
  201. package/dist/secret.js +2 -1
  202. package/dist/secure-file.d.ts.map +1 -1
  203. package/dist/secure-file.js +42 -29
  204. package/dist/sibling-staged-file.d.ts +15 -0
  205. package/dist/sibling-staged-file.d.ts.map +1 -0
  206. package/dist/sibling-staged-file.js +148 -0
  207. package/dist/sibling-temp.d.ts +3 -0
  208. package/dist/sibling-temp.d.ts.map +1 -1
  209. package/dist/sibling-temp.js +30 -74
  210. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  211. package/dist/sidecar-lock-acquire.js +48 -27
  212. package/dist/sidecar-lock-handle.d.ts +6 -2
  213. package/dist/sidecar-lock-handle.d.ts.map +1 -1
  214. package/dist/sidecar-lock-handle.js +17 -3
  215. package/dist/sidecar-lock-policy.d.ts +2 -0
  216. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  217. package/dist/sidecar-lock-policy.js +29 -0
  218. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  219. package/dist/sidecar-lock-reclaim.js +29 -7
  220. package/dist/sidecar-lock.d.ts.map +1 -1
  221. package/dist/sidecar-lock.js +28 -16
  222. package/dist/staged-directory.d.ts +16 -0
  223. package/dist/staged-directory.d.ts.map +1 -0
  224. package/dist/staged-directory.js +60 -0
  225. package/dist/staged-file-types.d.ts +56 -0
  226. package/dist/staged-file-types.d.ts.map +1 -0
  227. package/dist/staged-file-types.js +1 -0
  228. package/dist/staged-file.d.ts +10 -0
  229. package/dist/staged-file.d.ts.map +1 -0
  230. package/dist/staged-file.js +15 -0
  231. package/dist/strict-file-identity.d.ts +6 -0
  232. package/dist/strict-file-identity.d.ts.map +1 -0
  233. package/dist/strict-file-identity.js +48 -0
  234. package/dist/suppressed-error.d.ts +6 -0
  235. package/dist/suppressed-error.d.ts.map +1 -0
  236. package/dist/suppressed-error.js +15 -0
  237. package/dist/temp-cleanup.d.ts +2 -0
  238. package/dist/temp-cleanup.d.ts.map +1 -1
  239. package/dist/temp-cleanup.js +25 -10
  240. package/dist/temp-workspace-owner.d.ts +23 -0
  241. package/dist/temp-workspace-owner.d.ts.map +1 -0
  242. package/dist/temp-workspace-owner.js +320 -0
  243. package/dist/temp.d.ts +1 -1
  244. package/dist/temp.d.ts.map +1 -1
  245. package/dist/test-hooks.d.ts +5 -0
  246. package/dist/test-hooks.d.ts.map +1 -1
  247. package/dist/windows-owner.d.ts +3 -0
  248. package/dist/windows-owner.d.ts.map +1 -1
  249. package/dist/windows-owner.js +10 -2
  250. package/docs/advanced.md +19 -2
  251. package/docs/archive.md +250 -35
  252. package/docs/atomic.md +11 -2
  253. package/docs/config.md +7 -0
  254. package/docs/contributing.md +45 -7
  255. package/docs/durability.md +18 -5
  256. package/docs/errors.md +16 -1
  257. package/docs/file-store.md +2 -0
  258. package/docs/index.md +3 -1
  259. package/docs/install.md +20 -8
  260. package/docs/json.md +8 -4
  261. package/docs/migrating-to-0.5.md +7 -7
  262. package/docs/migrating-to-0.6.md +43 -0
  263. package/docs/native-helper.md +30 -8
  264. package/docs/native.md +73 -17
  265. package/docs/output.md +10 -0
  266. package/docs/path-scope.md +28 -2
  267. package/docs/permissions.md +13 -2
  268. package/docs/public-api.md +6 -3
  269. package/docs/quickstart.md +1 -1
  270. package/docs/reading.md +1 -1
  271. package/docs/regular-file.md +9 -2
  272. package/docs/root.md +3 -1
  273. package/docs/secret-file.md +12 -0
  274. package/docs/secure-file.md +21 -3
  275. package/docs/security-model.md +33 -1
  276. package/docs/sidecar-lock.md +15 -1
  277. package/docs/staged-file.md +178 -0
  278. package/docs/store.md +13 -0
  279. package/docs/temp.md +128 -14
  280. package/docs/testing.md +1 -1
  281. package/docs/writing.md +19 -0
  282. package/package.json +16 -9
  283. package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
  284. package/dist/native/darwin-x64/fs-safe-native.node +0 -0
  285. package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
  286. package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
  287. package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
  288. package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
  289. package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
@@ -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"}
@@ -1,3 +1,4 @@
1
+ import { type PermissionCommandFailure } from "./permission-exec.js";
1
2
  export type WindowsOwnerExec = (command: string, args: string[]) => Promise<{
2
3
  stdout: string;
3
4
  stderr: string;
@@ -10,6 +11,8 @@ export type WindowsOwnerSummary = {
10
11
  remote?: boolean;
11
12
  trusted?: boolean;
12
13
  error?: string;
14
+ errorDetail?: PermissionCommandFailure;
15
+ errorCause?: unknown;
13
16
  };
14
17
  export declare function resolveWindowsPrincipalSids(params: {
15
18
  principals: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"windows-owner.d.ts","sourceRoot":"","sources":["../src/windows-owner.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,gBAAgB,GAAG,CAC7B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,KACX,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEjD,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA4DF,wBAAsB,2BAA2B,CAAC,MAAM,EAAE;IACxD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,EAAE,gBAAgB,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAyBlC;AAED,wBAAsB,4BAA4B,CAAC,MAAM,EAAE;IACzD,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,EAAE,gBAAgB,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAWzB;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,EAAE,gBAAgB,CAAC;CACxB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA2C/B"}
1
+ {"version":3,"file":"windows-owner.d.ts","sourceRoot":"","sources":["../src/windows-owner.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,sBAAsB,CAAC;AAG9B,MAAM,MAAM,gBAAgB,GAAG,CAC7B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,KACX,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEjD,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,wBAAwB,CAAC;IACvC,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AA4DF,wBAAsB,2BAA2B,CAAC,MAAM,EAAE;IACxD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,EAAE,gBAAgB,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAyBlC;AAED,wBAAsB,4BAA4B,CAAC,MAAM,EAAE;IACzD,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,EAAE,gBAAgB,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAWzB;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,EAAE,gBAAgB,CAAC;CACxB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAkD/B"}
@@ -1,3 +1,4 @@
1
+ import { formatPermissionErrorDetail, getPermissionCommandFailure, } from "./permission-exec.js";
1
2
  import { resolveWindowsSystemCommand } from "./windows-command.js";
2
3
  const SID_RE = /^\*?s-\d+-\d+(-\d+)+$/i;
3
4
  const TRUSTED_OWNER_SIDS = new Set(["s-1-5-18", "s-1-5-32-544"]);
@@ -83,8 +84,11 @@ export async function resolveWindowsCurrentUserSid(params) {
83
84
  }
84
85
  }
85
86
  export async function inspectWindowsOwner(params) {
87
+ let command = "";
88
+ let startedAt = performance.now();
86
89
  try {
87
- const command = resolveWindowsSystemCommand(String.raw `WindowsPowerShell\v1.0\powershell.exe`, params.env);
90
+ command = resolveWindowsSystemCommand(String.raw `WindowsPowerShell\v1.0\powershell.exe`, params.env);
91
+ startedAt = performance.now();
88
92
  const { stdout } = await params.exec(command, [
89
93
  "-NoLogo",
90
94
  "-NoProfile",
@@ -113,6 +117,10 @@ export async function inspectWindowsOwner(params) {
113
117
  };
114
118
  }
115
119
  catch (err) {
116
- return { error: String(err) };
120
+ return {
121
+ error: formatPermissionErrorDetail(String(err)),
122
+ errorDetail: getPermissionCommandFailure(err, command, performance.now() - startedAt),
123
+ errorCause: err,
124
+ };
117
125
  }
118
126
  }
package/docs/advanced.md CHANGED
@@ -26,13 +26,22 @@ The exports group into a handful of themes. Each documented helper has its own p
26
26
  | Export | Page | Notes |
27
27
  |---|---|---|
28
28
  | `pathScope`, `PathScope`, `PathScopeOptions`, `PathScopeResolveOptions` | [path-scope.md](path-scope.md) | Absolute-path boundary helper with `Result`-shaped returns. |
29
- | `ensureDirectoryWithinRoot` | | Create a directory while enforcing the root boundary. |
29
+ | `ensureDirectoryWithinRoot` | [path-scope.md](path-scope.md#ensuredir-rel-options) | Create a directory while enforcing the root boundary; same result contract as `pathScope().ensureDir()`. |
30
30
  | `resolvePathWithinRoot`, `resolvePathsWithinRoot` | – | Resolve one or many relative paths against a trusted root. |
31
31
  | `resolveExistingPathsWithinRoot`, `resolveStrictExistingPathsWithinRoot` | – | Same, but require the targets to exist. |
32
32
  | `resolveWritablePathWithinRoot` | – | Resolve a write target inside a root. |
33
33
  | `resolveRootPath`, `resolveRootPathSync`, `ResolvedRootPath`, `ROOT_PATH_ALIAS_POLICIES`, `RootPathAliasPolicy` | – | Resolve a root directory honoring alias policy. |
34
34
  | `resolvePathViaExistingAncestorSync` | – | Walk to an existing ancestor for paths whose tail does not yet exist. |
35
35
 
36
+ `ensureDirectoryWithinRoot({ rootDir, requestedPath, scopeLabel, defaultDirName?, mode? })`
37
+ returns `{ ok: true, path }` or `{ ok: false, error: string, diagnostic?: FsSafeError }`.
38
+ It does not throw filesystem failures: operational failures carry a
39
+ `helper-failed` diagnostic with the original `cause` and a bounded, escaped
40
+ message naming the native code/syscall when available. Policy failures omit
41
+ `diagnostic`. Missing parents already created before a failure remain in place.
42
+ The diagnostic contract is specific to directory preparation, not the other
43
+ root-path result helpers.
44
+
36
45
  ### Absolute path helpers
37
46
 
38
47
  | Export | Page | Notes |
@@ -63,6 +72,13 @@ Operational filesystem failures such as permissions or I/O errors are rethrown.
63
72
  | `assertNoSymlinkParents`, `assertNoSymlinkParentsSync`, `AssertNoSymlinkParentsOptions` | – | Reject paths whose ancestor chain contains symlinks. |
64
73
  | `assertNoHardlinkedFinalPath`, `assertNoPathAliasEscape`, `PATH_ALIAS_POLICIES`, `PathAliasPolicy` | – | Hardlink/alias defense building blocks. |
65
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
+
66
82
  The bounded descriptor helpers start at the descriptor's current offset and
67
83
  leave ownership with the caller. They are intended for the second half of a
68
84
  safe read: first open and validate the path using the boundary appropriate to
@@ -107,8 +123,9 @@ component is followed by another segment, both helpers throw
107
123
 
108
124
  | Export | Page | Notes |
109
125
  |---|---|---|
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. |
110
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. |
111
- | `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. |
112
129
 
113
130
  ### Permissions
114
131