@openclaw/fs-safe 0.13.1 → 0.15.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 (256) hide show
  1. package/CHANGELOG.md +64 -1
  2. package/README.md +10 -4
  3. package/dist/advanced.d.ts +1 -1
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +1 -1
  6. package/dist/archive-merge.d.ts.map +1 -1
  7. package/dist/archive-merge.js +113 -46
  8. package/dist/archive-read.d.ts.map +1 -1
  9. package/dist/archive-read.js +4 -4
  10. package/dist/archive-zip-directory.d.ts +4 -0
  11. package/dist/archive-zip-directory.d.ts.map +1 -1
  12. package/dist/archive-zip-directory.js +2 -0
  13. package/dist/archive-zip-entry.d.ts +6 -2
  14. package/dist/archive-zip-entry.d.ts.map +1 -1
  15. package/dist/archive-zip-entry.js +23 -8
  16. package/dist/archive-zip-integrity.d.ts.map +1 -1
  17. package/dist/archive-zip-integrity.js +3 -4
  18. package/dist/archive-zip-loader.d.ts.map +1 -1
  19. package/dist/archive-zip-loader.js +107 -31
  20. package/dist/archive-zip-names.d.ts +1 -0
  21. package/dist/archive-zip-names.d.ts.map +1 -1
  22. package/dist/archive-zip-names.js +6 -0
  23. package/dist/archive.js +6 -5
  24. package/dist/bounded-read-stream.d.ts +0 -1
  25. package/dist/bounded-read-stream.d.ts.map +1 -1
  26. package/dist/bounded-read-stream.js +0 -6
  27. package/dist/copy-publication.d.ts +6 -0
  28. package/dist/copy-publication.d.ts.map +1 -1
  29. package/dist/copy-publication.js +3 -0
  30. package/dist/copy-tree-portable.d.ts.map +1 -1
  31. package/dist/copy-tree-portable.js +44 -24
  32. package/dist/copy.d.ts.map +1 -1
  33. package/dist/copy.js +29 -11
  34. package/dist/directory-mode-owner.js +5 -5
  35. package/dist/file-handle-transfer.d.ts +2 -0
  36. package/dist/file-handle-transfer.d.ts.map +1 -1
  37. package/dist/file-handle-transfer.js +57 -2
  38. package/dist/file-identity.d.ts.map +1 -1
  39. package/dist/file-identity.js +18 -4
  40. package/dist/file-lock-sync-admission.d.ts +19 -0
  41. package/dist/file-lock-sync-admission.d.ts.map +1 -0
  42. package/dist/file-lock-sync-admission.js +93 -0
  43. package/dist/file-lock-sync-root-acquire.d.ts +4 -0
  44. package/dist/file-lock-sync-root-acquire.d.ts.map +1 -0
  45. package/dist/file-lock-sync-root-acquire.js +370 -0
  46. package/dist/file-lock-sync-root-arbitration.d.ts +18 -0
  47. package/dist/file-lock-sync-root-arbitration.d.ts.map +1 -0
  48. package/dist/file-lock-sync-root-arbitration.js +66 -0
  49. package/dist/file-lock-sync-root-held.d.ts +34 -0
  50. package/dist/file-lock-sync-root-held.d.ts.map +1 -0
  51. package/dist/file-lock-sync-root-held.js +393 -0
  52. package/dist/file-lock-sync-root-io.d.ts +44 -0
  53. package/dist/file-lock-sync-root-io.d.ts.map +1 -0
  54. package/dist/file-lock-sync-root-io.js +209 -0
  55. package/dist/file-lock-sync-root-mutation.d.ts +17 -0
  56. package/dist/file-lock-sync-root-mutation.d.ts.map +1 -0
  57. package/dist/file-lock-sync-root-mutation.js +277 -0
  58. package/dist/file-lock-sync-root-options.d.ts +20 -0
  59. package/dist/file-lock-sync-root-options.d.ts.map +1 -0
  60. package/dist/file-lock-sync-root-options.js +58 -0
  61. package/dist/file-lock-sync-root-registration.d.ts +2 -0
  62. package/dist/file-lock-sync-root-registration.d.ts.map +1 -0
  63. package/dist/file-lock-sync-root-registration.js +90 -0
  64. package/dist/file-lock-sync-root.d.ts +36 -0
  65. package/dist/file-lock-sync-root.d.ts.map +1 -0
  66. package/dist/file-lock-sync-root.js +361 -0
  67. package/dist/file-lock-sync-stale-admission.d.ts +24 -0
  68. package/dist/file-lock-sync-stale-admission.d.ts.map +1 -0
  69. package/dist/file-lock-sync-stale-admission.js +205 -0
  70. package/dist/file-lock-sync.d.ts.map +1 -1
  71. package/dist/file-lock-sync.js +245 -205
  72. package/dist/file-store-prune.d.ts.map +1 -1
  73. package/dist/file-store-prune.js +5 -1
  74. package/dist/file-store-sync-directory.d.ts.map +1 -1
  75. package/dist/file-store-sync-directory.js +14 -7
  76. package/dist/file-store-sync-write.js +3 -3
  77. package/dist/file-store.d.ts.map +1 -1
  78. package/dist/file-store.js +47 -12
  79. package/dist/guest-dispatch-python.js +1 -1
  80. package/dist/json-document-store.d.ts.map +1 -1
  81. package/dist/json-document-store.js +22 -15
  82. package/dist/json-durable-queue-ownership.d.ts +0 -1
  83. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  84. package/dist/json-durable-queue-ownership.js +0 -6
  85. package/dist/native-binding.d.ts +8 -0
  86. package/dist/native-binding.d.ts.map +1 -1
  87. package/dist/native-parent-admission.d.ts +3 -2
  88. package/dist/native-parent-admission.d.ts.map +1 -1
  89. package/dist/native-parent-admission.js +24 -5
  90. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  91. package/dist/native-pinned-write-windows.js +7 -7
  92. package/dist/native-pinned-write.d.ts.map +1 -1
  93. package/dist/native-pinned-write.js +202 -172
  94. package/dist/native-policy-directory-observation.d.ts +12 -0
  95. package/dist/native-policy-directory-observation.d.ts.map +1 -0
  96. package/dist/native-policy-directory-observation.js +61 -0
  97. package/dist/native-policy-parent-windows.d.ts +14 -0
  98. package/dist/native-policy-parent-windows.d.ts.map +1 -0
  99. package/dist/native-policy-parent-windows.js +200 -0
  100. package/dist/native-rename-outcome.d.ts +4 -0
  101. package/dist/native-rename-outcome.d.ts.map +1 -0
  102. package/dist/native-rename-outcome.js +8 -0
  103. package/dist/native-staged-file.d.ts +2 -2
  104. package/dist/native-staged-file.d.ts.map +1 -1
  105. package/dist/native-staged-file.js +42 -40
  106. package/dist/output.d.ts.map +1 -1
  107. package/dist/output.js +12 -8
  108. package/dist/path-prefix.d.ts.map +1 -1
  109. package/dist/path-prefix.js +30 -8
  110. package/dist/path-suffix-aliases.d.ts +2 -0
  111. package/dist/path-suffix-aliases.d.ts.map +1 -1
  112. package/dist/path-suffix-aliases.js +25 -17
  113. package/dist/permission-exec.d.ts +2 -0
  114. package/dist/permission-exec.d.ts.map +1 -1
  115. package/dist/permission-exec.js +150 -21
  116. package/dist/permissions-windows.js +1 -1
  117. package/dist/pinned-mutation-admission.d.ts.map +1 -1
  118. package/dist/pinned-mutation-admission.js +23 -29
  119. package/dist/pinned-mutation-observation.d.ts +7 -2
  120. package/dist/pinned-mutation-observation.d.ts.map +1 -1
  121. package/dist/pinned-mutation-observation.js +81 -18
  122. package/dist/pinned-mutation-shared-route.d.ts +1 -0
  123. package/dist/pinned-mutation-shared-route.d.ts.map +1 -1
  124. package/dist/pinned-mutation-shared-route.js +1 -1
  125. package/dist/pinned-write-types.d.ts +2 -0
  126. package/dist/pinned-write-types.d.ts.map +1 -1
  127. package/dist/private-temp-workspace.d.ts.map +1 -1
  128. package/dist/private-temp-workspace.js +75 -121
  129. package/dist/regular-file.d.ts.map +1 -1
  130. package/dist/regular-file.js +1 -15
  131. package/dist/replace-directory.d.ts.map +1 -1
  132. package/dist/replace-directory.js +256 -18
  133. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  134. package/dist/replace-file-copy-fallback.js +62 -70
  135. package/dist/replace-file-copy-source.d.ts.map +1 -1
  136. package/dist/replace-file-copy-source.js +10 -12
  137. package/dist/replace-file-temp-owner.d.ts +5 -2
  138. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  139. package/dist/replace-file-temp-owner.js +65 -30
  140. package/dist/replace-file.js +6 -6
  141. package/dist/retained-directory-replacement.d.ts +26 -0
  142. package/dist/retained-directory-replacement.d.ts.map +1 -0
  143. package/dist/retained-directory-replacement.js +193 -0
  144. package/dist/root-boundary.d.ts +1 -0
  145. package/dist/root-boundary.d.ts.map +1 -1
  146. package/dist/root-boundary.js +4 -0
  147. package/dist/root-context.d.ts +0 -8
  148. package/dist/root-context.d.ts.map +1 -1
  149. package/dist/root-context.js +0 -3
  150. package/dist/root-create-input.d.ts +6 -0
  151. package/dist/root-create-input.d.ts.map +1 -1
  152. package/dist/root-create-input.js +5 -1
  153. package/dist/root-directory-creation.d.ts.map +1 -1
  154. package/dist/root-directory-creation.js +5 -4
  155. package/dist/root-directory-list.d.ts +1 -0
  156. package/dist/root-directory-list.d.ts.map +1 -1
  157. package/dist/root-directory-list.js +1 -0
  158. package/dist/root-impl.d.ts.map +1 -1
  159. package/dist/root-impl.js +20 -11
  160. package/dist/root-move-noreplace.d.ts.map +1 -1
  161. package/dist/root-move-noreplace.js +2 -2
  162. package/dist/root-path-errors.d.ts +1 -0
  163. package/dist/root-path-errors.d.ts.map +1 -1
  164. package/dist/root-path-errors.js +11 -2
  165. package/dist/root-path-existing.d.ts.map +1 -1
  166. package/dist/root-path-existing.js +12 -37
  167. package/dist/root-path.js +1 -13
  168. package/dist/root-remove.d.ts +1 -0
  169. package/dist/root-remove.d.ts.map +1 -1
  170. package/dist/root-remove.js +4 -0
  171. package/dist/root-walk.d.ts +1 -1
  172. package/dist/root-walk.d.ts.map +1 -1
  173. package/dist/root-walk.js +17 -2
  174. package/dist/root-write-admission.d.ts +0 -2
  175. package/dist/root-write-admission.d.ts.map +1 -1
  176. package/dist/root-write-admission.js +1 -15
  177. package/dist/root-write-compatibility.d.ts +1 -2
  178. package/dist/root-write-compatibility.d.ts.map +1 -1
  179. package/dist/root-write-compatibility.js +17 -68
  180. package/dist/root-write-complete-parent.d.ts.map +1 -1
  181. package/dist/root-write-complete-parent.js +10 -24
  182. package/dist/root-write-lock-binding.d.ts +15 -0
  183. package/dist/root-write-lock-binding.d.ts.map +1 -0
  184. package/dist/root-write-lock-binding.js +162 -0
  185. package/dist/root-write-verification.d.ts.map +1 -1
  186. package/dist/root-write-verification.js +29 -42
  187. package/dist/secret-file.d.ts.map +1 -1
  188. package/dist/secret-file.js +2 -24
  189. package/dist/secret-read-async.d.ts.map +1 -1
  190. package/dist/secret-read-async.js +3 -24
  191. package/dist/secret-read-policy.d.ts +6 -2
  192. package/dist/secret-read-policy.d.ts.map +1 -1
  193. package/dist/secret-read-policy.js +26 -2
  194. package/dist/sibling-temp.d.ts.map +1 -1
  195. package/dist/sibling-temp.js +23 -12
  196. package/dist/sidecar-lock-acquire.d.ts +2 -28
  197. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  198. package/dist/sidecar-lock-acquire.js +288 -199
  199. package/dist/sidecar-lock-admission-context.d.ts +19 -0
  200. package/dist/sidecar-lock-admission-context.d.ts.map +1 -0
  201. package/dist/sidecar-lock-admission-context.js +60 -0
  202. package/dist/sidecar-lock-admission-parser.d.ts +43 -0
  203. package/dist/sidecar-lock-admission-parser.d.ts.map +1 -0
  204. package/dist/sidecar-lock-admission-parser.js +113 -0
  205. package/dist/sidecar-lock-admission.d.ts +35 -0
  206. package/dist/sidecar-lock-admission.d.ts.map +1 -0
  207. package/dist/sidecar-lock-admission.js +7 -0
  208. package/dist/sidecar-lock-reclaim.d.ts +9 -4
  209. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  210. package/dist/sidecar-lock-reclaim.js +80 -25
  211. package/dist/sidecar-lock-stale-admission.d.ts +39 -0
  212. package/dist/sidecar-lock-stale-admission.d.ts.map +1 -0
  213. package/dist/sidecar-lock-stale-admission.js +232 -0
  214. package/dist/sidecar-lock-target.d.ts +8 -0
  215. package/dist/sidecar-lock-target.d.ts.map +1 -0
  216. package/dist/sidecar-lock-target.js +55 -0
  217. package/dist/sidecar-lock.d.ts.map +1 -1
  218. package/dist/sidecar-lock.js +100 -16
  219. package/dist/staged-directory.d.ts +5 -2
  220. package/dist/staged-directory.d.ts.map +1 -1
  221. package/dist/staged-directory.js +37 -1
  222. package/dist/temp-workspace-admission.d.ts.map +1 -1
  223. package/dist/temp-workspace-admission.js +28 -4
  224. package/dist/temp-workspace-descriptor.d.ts.map +1 -1
  225. package/dist/temp-workspace-descriptor.js +9 -27
  226. package/dist/temp-workspace-owner.d.ts.map +1 -1
  227. package/dist/temp-workspace-owner.js +8 -8
  228. package/dist/walk.d.ts +5 -1
  229. package/dist/walk.d.ts.map +1 -1
  230. package/dist/walk.js +19 -6
  231. package/dist/windows-owner.d.ts.map +1 -1
  232. package/dist/windows-owner.js +2 -2
  233. package/docs/advanced.md +1 -1
  234. package/docs/archive.md +36 -9
  235. package/docs/atomic.md +85 -8
  236. package/docs/copy.md +35 -0
  237. package/docs/file-store.md +19 -0
  238. package/docs/guest.md +1 -1
  239. package/docs/json-store.md +5 -0
  240. package/docs/native-helper.md +10 -3
  241. package/docs/native.md +8 -0
  242. package/docs/output.md +6 -0
  243. package/docs/path-prefix.md +10 -0
  244. package/docs/path-suffix-aliases.md +51 -6
  245. package/docs/permissions.md +13 -0
  246. package/docs/public-api.md +3 -2
  247. package/docs/root.md +22 -3
  248. package/docs/sidecar-lock.md +109 -4
  249. package/docs/staged-file.md +7 -3
  250. package/docs/temp.md +20 -3
  251. package/docs/walk.md +67 -1
  252. package/docs/writing.md +11 -1
  253. package/package.json +10 -10
  254. package/dist/darwin-acl.d.ts +0 -4
  255. package/dist/darwin-acl.d.ts.map +0 -1
  256. package/dist/darwin-acl.js +0 -24
@@ -55,9 +55,11 @@ import path from "node:path";
55
55
  import { inspectDirectoryIdentity } from "./directory-guard.js";
56
56
  import { FsSafeError } from "./errors.js";
57
57
  import { runPinnedWriteWindows } from "./native-pinned-write-windows.js";
58
+ import { capturePolicyAwareWindowsParent } from "./native-policy-parent-windows.js";
58
59
  import { openNativeParentAdmission, openNativeRootAdmission } from "./native-parent-admission.js";
59
60
  import { assertNativeStaging, writeNativeStage } from "./native-staged-file.js";
60
61
  import { captureNativeFdClose } from "./native-binding.js";
62
+ import { NativePolicyDirectoryMismatch } from "./native-policy-directory-observation.js";
61
63
  import { assertPolicyStagedDirectoryCurrent, assertStagedDirectoryCurrent, describePolicyStagedDirectory, describeStagedDirectory, refreshPolicyStagedDirectoryObservation, } from "./staged-directory.js";
62
64
  import { inspectFileIdentitySync } from "./strict-file-identity.js";
63
65
  import { realpathSync } from "./realpath.js";
@@ -118,8 +120,8 @@ async function describePosixParent(parentFd, pathname) {
118
120
  observation: checkedMutationDirectory(parentPath, directory.realPath, parentPathStat),
119
121
  };
120
122
  }
121
- function describePolicyPosixParent(parentFd, pathname) {
122
- const policyDirectory = describePolicyStagedDirectory(parentFd, pathname);
123
+ function describePolicyPosixParent(binding, parentFd, pathname) {
124
+ const policyDirectory = describePolicyStagedDirectory(parentFd, pathname, binding);
123
125
  return {
124
126
  parentFd,
125
127
  parentPath: policyDirectory.directory.realPath,
@@ -131,11 +133,12 @@ function describePolicyPosixParent(parentFd, pathname) {
131
133
  }
132
134
  function policyParentCaptureDeopt(error) {
133
135
  return error instanceof FsSafeError &&
136
+ !(error instanceof NativePolicyDirectoryMismatch) &&
134
137
  (error.code === "path-mismatch" || error.code === "not-file");
135
138
  }
136
- function tryDescribePolicyPosixParent(parentFd, pathname) {
139
+ function tryDescribePolicyPosixParent(binding, parentFd, pathname) {
137
140
  try {
138
- return describePolicyPosixParent(parentFd, pathname);
141
+ return describePolicyPosixParent(binding, parentFd, pathname);
139
142
  }
140
143
  catch (error) {
141
144
  if (!policyParentCaptureDeopt(error))
@@ -149,183 +152,215 @@ function assertPosixParentCurrent(parent) {
149
152
  : assertStagedDirectoryCurrent(parent.directory);
150
153
  }
151
154
  async function capturePolicyAwarePosixParent(binding, params, rootFd, directoryFlags) {
152
- const closeFd = captureNativeFdClose(binding);
153
- const segments = relativeParentSegments(params.relativeParentPath);
154
- const parentSpelling = segments.length
155
- ? path.join(params.rootPath, ...segments)
156
- : params.rootPath;
157
- const segmentRoute = createPathSegmentRoute(segments);
158
- const initialTarget = prospectiveTargetPath(params.rootPath, segmentRoute, 0, params.basename);
159
- let retainedTargetPath = params.mutationAdmission?.beginParentWalk?.();
160
- if (retainedTargetPath && !sameAbsolutePath(retainedTargetPath, initialTarget)) {
161
- retainedTargetPath = undefined;
162
- }
163
- // Preserve the one-open hot path when the complete parent still exists.
164
- // Policy is attached only after the opened descriptor is associated with its
165
- // current canonical pathname, so a contained redirect cannot retain the old
166
- // preflight authorization.
167
- let completeParentFd;
155
+ const env_1 = { stack: [], error: void 0, hasError: false };
168
156
  try {
169
- completeParentFd = binding.openBeneath(rootFd, params.relativeParentPath, directoryFlags).fd;
170
- }
171
- catch (error) {
172
- if (error?.code !== "ENOENT" || !params.mkdir)
173
- throw error;
174
- }
175
- if (completeParentFd !== undefined) {
176
- const parentFd = completeParentFd;
157
+ const closeFd = captureNativeFdClose(binding);
158
+ const observationDisposers = new Map();
159
+ const disposeObservation = (fd) => {
160
+ const dispose = observationDisposers.get(fd);
161
+ observationDisposers.delete(fd);
162
+ dispose?.();
163
+ };
164
+ const observationScope = __addDisposableResource(env_1, {
165
+ [Symbol.dispose]() { for (const dispose of observationDisposers.values())
166
+ dispose(); },
167
+ }, false);
168
+ const capturePolicyParent = (fd, pathname) => {
169
+ const captured = tryDescribePolicyPosixParent(binding, fd, pathname);
170
+ const dispose = captured?.policyDirectory?.disposeObservation;
171
+ if (dispose)
172
+ observationDisposers.set(fd, dispose);
173
+ return captured;
174
+ };
175
+ const segments = relativeParentSegments(params.relativeParentPath);
176
+ const parentSpelling = segments.length
177
+ ? path.join(params.rootPath, ...segments)
178
+ : params.rootPath;
179
+ const segmentRoute = createPathSegmentRoute(segments);
180
+ const initialTarget = prospectiveTargetPath(params.rootPath, segmentRoute, 0, params.basename);
181
+ let retainedTargetPath = params.mutationAdmission?.beginParentWalk?.();
182
+ if (retainedTargetPath && !sameAbsolutePath(retainedTargetPath, initialTarget)) {
183
+ retainedTargetPath = undefined;
184
+ }
185
+ // Preserve the one-open hot path when the complete parent still exists.
186
+ // Policy is attached only after the opened descriptor is associated with its
187
+ // current canonical pathname, so a contained redirect cannot retain the old
188
+ // preflight authorization.
189
+ let completeParentFd;
177
190
  try {
178
- let admitted = retainedTargetPath
179
- ? tryDescribePolicyPosixParent(parentFd, parentSpelling)
180
- : undefined;
181
- if (!admitted) {
182
- retainedTargetPath = undefined;
183
- admitted = await describePosixParent(parentFd, parentSpelling);
184
- }
185
- const targetPath = path.join(admitted.parentPath, params.basename);
186
- await authorizePinnedMutation(params, {
187
- targetPath,
188
- mutationPath: targetPath,
189
- phase: "parent",
190
- });
191
- assertPosixParentCurrent(admitted);
192
- return admitted;
191
+ completeParentFd = binding.openBeneath(rootFd, params.relativeParentPath, directoryFlags).fd;
193
192
  }
194
193
  catch (error) {
195
- closeFd(parentFd);
196
- throw error;
194
+ if (error?.code !== "ENOENT" || !params.mkdir)
195
+ throw error;
197
196
  }
198
- }
199
- // The canonical preflight spelling has no intentional symlinks in its
200
- // existing prefix. Walk missing-parent cases one direct child at a time so
201
- // every existing/opened object is authorized and every mkdir is authorized
202
- // before dispatch. O_NOFOLLOW makes a concurrently introduced link fail
203
- // before the next component can be created through it.
204
- const secureDirectoryFlags = directoryFlags | (fsSync.constants.O_NOFOLLOW ?? 0);
205
- let currentFd = rootFd;
206
- let currentOwnedFd;
207
- let currentPath = params.rootPath;
208
- let current = retainedTargetPath
209
- ? tryDescribePolicyPosixParent(rootFd, currentPath)
210
- : undefined;
211
- if (!current) {
212
- retainedTargetPath = undefined;
213
- current = await describePosixParent(rootFd, currentPath);
214
- }
215
- try {
216
- await authorizePinnedMutation(params, {
217
- targetPath: retainedTargetPath ?? initialTarget,
218
- mutationPath: retainedTargetPath ?? initialTarget,
219
- phase: "parent",
220
- });
221
- assertPosixParentCurrent(current);
222
- for (let index = 0; index < segments.length; index += 1) {
223
- const segment = segments[index];
224
- const childPath = path.join(currentPath, segment);
225
- let childFd;
226
- let createdByMkdir = false;
227
- let createReceipt;
197
+ if (completeParentFd !== undefined) {
198
+ const parentFd = completeParentFd;
228
199
  try {
229
- childFd = binding.openBeneath(currentFd, segment, secureDirectoryFlags).fd;
230
- }
231
- catch (openError) {
232
- if (!isNotFoundPathError(openError)) {
233
- throw normalizePolicyParentOpenError(openError, params);
200
+ let admitted = retainedTargetPath
201
+ ? capturePolicyParent(parentFd, parentSpelling)
202
+ : undefined;
203
+ if (!admitted) {
204
+ retainedTargetPath = undefined;
205
+ admitted = await describePosixParent(parentFd, parentSpelling);
234
206
  }
235
- const targetPath = retainedTargetPath ?? prospectiveTargetPath(currentPath, segmentRoute, index, params.basename);
236
- const request = Object.freeze({
207
+ const targetPath = path.join(admitted.parentPath, params.basename);
208
+ await authorizePinnedMutation(params, {
237
209
  targetPath,
238
- mutationPath: childPath,
239
- phase: "parent-create",
210
+ mutationPath: targetPath,
211
+ phase: "parent",
240
212
  });
241
- createReceipt = params.mutationAdmission?.tryAuthorizeAtParent?.(request, current.observation);
242
- if (!createReceipt) {
243
- createReceipt = await authorizePinnedMutation(params, request);
244
- assertPosixParentCurrent(current);
245
- }
246
- params.assertBeforeMutation?.();
247
- // Do not carry pathname freshness across the live authority callback.
248
- assertPosixParentCurrent(current);
249
- createdByMkdir = mkdirPolicyChild(binding, currentFd, segment, 0o777);
213
+ assertPosixParentCurrent(admitted);
214
+ return admitted;
215
+ }
216
+ catch (error) {
217
+ disposeObservation(parentFd);
218
+ closeFd(parentFd);
219
+ throw error;
220
+ }
221
+ }
222
+ // The canonical preflight spelling has no intentional symlinks in its
223
+ // existing prefix. Walk missing-parent cases one direct child at a time so
224
+ // every existing/opened object is authorized and every mkdir is authorized
225
+ // before dispatch. O_NOFOLLOW makes a concurrently introduced link fail
226
+ // before the next component can be created through it.
227
+ const secureDirectoryFlags = directoryFlags | (fsSync.constants.O_NOFOLLOW ?? 0);
228
+ let currentFd = rootFd;
229
+ let currentOwnedFd;
230
+ let currentPath = params.rootPath;
231
+ let current = retainedTargetPath
232
+ ? capturePolicyParent(rootFd, currentPath)
233
+ : undefined;
234
+ if (!current) {
235
+ retainedTargetPath = undefined;
236
+ current = await describePosixParent(rootFd, currentPath);
237
+ }
238
+ try {
239
+ await authorizePinnedMutation(params, {
240
+ targetPath: retainedTargetPath ?? initialTarget,
241
+ mutationPath: retainedTargetPath ?? initialTarget,
242
+ phase: "parent",
243
+ });
244
+ assertPosixParentCurrent(current);
245
+ for (let index = 0; index < segments.length; index += 1) {
246
+ const segment = segments[index];
247
+ const childPath = path.join(currentPath, segment);
248
+ let childFd;
249
+ let createdByMkdir = false;
250
+ let createReceipt;
250
251
  try {
251
252
  childFd = binding.openBeneath(currentFd, segment, secureDirectoryFlags).fd;
252
253
  }
253
- catch (createdOpenError) {
254
- throw normalizePolicyParentOpenError(createdOpenError, params);
255
- }
256
- }
257
- let child;
258
- try {
259
- const acceleratedChild = retainedTargetPath
260
- ? tryDescribePolicyPosixParent(childFd, childPath)
261
- : undefined;
262
- if (acceleratedChild) {
263
- child = acceleratedChild;
264
- }
265
- else {
266
- retainedTargetPath = undefined;
267
- child = await describePosixParent(childFd, childPath);
268
- }
269
- if (!sameAbsolutePath(child.parentPath, childPath))
270
- retainedTargetPath = undefined;
271
- let childAuthorization;
272
- if (createdByMkdir && createReceipt && params.mutationAdmission?.advanceCreatedDirectory) {
273
- let evidence;
254
+ catch (openError) {
255
+ if (!isNotFoundPathError(openError)) {
256
+ throw normalizePolicyParentOpenError(openError, params);
257
+ }
258
+ const targetPath = retainedTargetPath ?? prospectiveTargetPath(currentPath, segmentRoute, index, params.basename);
259
+ const request = Object.freeze({
260
+ targetPath,
261
+ mutationPath: childPath,
262
+ phase: "parent-create",
263
+ });
264
+ createReceipt = params.mutationAdmission?.tryAuthorizeAtParent?.(request, current.observation);
265
+ if (!createReceipt) {
266
+ createReceipt = await authorizePinnedMutation(params, request);
267
+ assertPosixParentCurrent(current);
268
+ }
269
+ params.assertBeforeMutation?.();
270
+ // Do not carry pathname freshness across the live authority callback.
271
+ assertPosixParentCurrent(current);
272
+ createdByMkdir = mkdirPolicyChild(binding, currentFd, segment, 0o777);
274
273
  try {
275
- const parent = current.policyDirectory
276
- ? refreshPolicyStagedDirectoryObservation(current.policyDirectory)
277
- : checkedMutationDirectory(currentPath, current.directory.realPath, assertStagedDirectoryCurrent(current.directory));
278
- evidence = Object.freeze({
279
- admission: createReceipt,
280
- parent,
281
- child: child.observation,
282
- });
274
+ childFd = binding.openBeneath(currentFd, segment, secureDirectoryFlags).fd;
283
275
  }
284
- catch {
285
- // Leave failed evidence to the full ordered admission below.
276
+ catch (createdOpenError) {
277
+ throw normalizePolicyParentOpenError(createdOpenError, params);
278
+ }
279
+ }
280
+ let child;
281
+ try {
282
+ const acceleratedChild = retainedTargetPath
283
+ ? capturePolicyParent(childFd, childPath)
284
+ : undefined;
285
+ if (acceleratedChild) {
286
+ child = acceleratedChild;
286
287
  }
287
- if (evidence) {
288
- childAuthorization = params.mutationAdmission.advanceCreatedDirectory(evidence);
288
+ else {
289
+ retainedTargetPath = undefined;
290
+ child = await describePosixParent(childFd, childPath);
291
+ }
292
+ if (!sameAbsolutePath(child.parentPath, childPath))
293
+ retainedTargetPath = undefined;
294
+ let childAuthorization;
295
+ if (createdByMkdir && createReceipt && params.mutationAdmission?.advanceCreatedDirectory) {
296
+ let evidence;
297
+ try {
298
+ const parent = current.policyDirectory
299
+ ? refreshPolicyStagedDirectoryObservation(current.policyDirectory)
300
+ : checkedMutationDirectory(currentPath, current.directory.realPath, assertStagedDirectoryCurrent(current.directory));
301
+ evidence = Object.freeze({
302
+ admission: createReceipt,
303
+ parent,
304
+ child: child.observation,
305
+ });
306
+ }
307
+ catch {
308
+ // Leave failed evidence to the full ordered admission below.
309
+ }
310
+ if (evidence) {
311
+ childAuthorization = params.mutationAdmission.advanceCreatedDirectory(evidence);
312
+ }
313
+ }
314
+ const targetPath = retainedTargetPath ?? prospectiveTargetPath(child.parentPath, segmentRoute, index + 1, params.basename);
315
+ const request = Object.freeze({
316
+ targetPath,
317
+ mutationPath: targetPath,
318
+ phase: "parent",
319
+ });
320
+ childAuthorization ??= params.mutationAdmission?.tryAuthorizeAtParent?.(request, child.observation);
321
+ if (!childAuthorization) {
322
+ await authorizePinnedMutation(params, request);
323
+ assertPosixParentCurrent(child);
289
324
  }
290
325
  }
291
- const targetPath = retainedTargetPath ?? prospectiveTargetPath(child.parentPath, segmentRoute, index + 1, params.basename);
292
- const request = Object.freeze({
293
- targetPath,
294
- mutationPath: targetPath,
295
- phase: "parent",
296
- });
297
- childAuthorization ??= params.mutationAdmission?.tryAuthorizeAtParent?.(request, child.observation);
298
- if (!childAuthorization) {
299
- await authorizePinnedMutation(params, request);
300
- assertPosixParentCurrent(child);
326
+ catch (error) {
327
+ disposeObservation(childFd);
328
+ closeFd(childFd);
329
+ throw error;
330
+ }
331
+ const previousOwnedFd = currentOwnedFd;
332
+ disposeObservation(current.parentFd);
333
+ currentOwnedFd = child.parentFd;
334
+ currentFd = child.parentFd;
335
+ currentPath = child.parentPath;
336
+ current = child;
337
+ if (previousOwnedFd !== undefined)
338
+ closeFd(previousOwnedFd);
339
+ if (index === segments.length - 1) {
340
+ currentOwnedFd = undefined;
341
+ return child;
301
342
  }
302
343
  }
303
- catch (error) {
304
- closeFd(childFd);
305
- throw error;
306
- }
307
- const previousOwnedFd = currentOwnedFd;
308
- currentOwnedFd = child.parentFd;
309
- currentFd = child.parentFd;
310
- currentPath = child.parentPath;
311
- current = child;
312
- if (previousOwnedFd !== undefined)
313
- closeFd(previousOwnedFd);
314
- if (index === segments.length - 1) {
315
- currentOwnedFd = undefined;
316
- return child;
344
+ // Empty relative parents are handled by the complete-parent fast path.
345
+ throw new FsSafeError("path-mismatch", "native write parent admission did not complete");
346
+ }
347
+ finally {
348
+ if (currentOwnedFd !== undefined) {
349
+ disposeObservation(currentOwnedFd);
350
+ closeFd(currentOwnedFd);
317
351
  }
318
352
  }
319
- // Empty relative parents are handled by the complete-parent fast path.
320
- throw new FsSafeError("path-mismatch", "native write parent admission did not complete");
353
+ }
354
+ catch (e_1) {
355
+ env_1.error = e_1;
356
+ env_1.hasError = true;
321
357
  }
322
358
  finally {
323
- if (currentOwnedFd !== undefined)
324
- closeFd(currentOwnedFd);
359
+ __disposeResources(env_1);
325
360
  }
326
361
  }
327
362
  export async function runPinnedWriteNative(binding, params) {
328
- const env_1 = { stack: [], error: void 0, hasError: false };
363
+ const env_2 = { stack: [], error: void 0, hasError: false };
329
364
  try {
330
365
  const closeFd = captureNativeFdClose(binding);
331
366
  const windows = process.platform === "win32";
@@ -339,12 +374,12 @@ export async function runPinnedWriteNative(binding, params) {
339
374
  operation: "native write",
340
375
  });
341
376
  const root = rootAdmission.root;
342
- const posixRoot = __addDisposableResource(env_1, windows ? undefined : root, true);
377
+ const posixRoot = __addDisposableResource(env_2, windows ? undefined : root, true);
343
378
  let parentFd;
344
379
  let windowsOwnsDirectories = false;
345
380
  // Until stage construction takes ownership, even admission failures must close
346
381
  // the raw POSIX parent. Disposal preserves both admission and close failures.
347
- const parentGuard = __addDisposableResource(env_1, {
382
+ const parentGuard = __addDisposableResource(env_2, {
348
383
  [Symbol.dispose]() {
349
384
  if (!windows && parentFd !== undefined) {
350
385
  closeFd(parentFd);
@@ -355,14 +390,18 @@ export async function runPinnedWriteNative(binding, params) {
355
390
  let parentPath;
356
391
  let directory;
357
392
  let parentPathStat;
358
- let policyParentAdmitted = false;
359
393
  if (!windows && params.mutationAdmission) {
360
394
  const admitted = await capturePolicyAwarePosixParent(binding, params, root.fd, directoryFlags);
361
395
  parentFd = admitted.parentFd;
362
396
  parentPath = admitted.parentPath;
363
397
  directory = admitted.directory;
364
398
  parentPathStat = admitted.parentPathStat;
365
- policyParentAdmitted = true;
399
+ }
400
+ else if (windows && params.mutationAdmission) {
401
+ const admitted = await capturePolicyAwareWindowsParent(binding, params, rootAdmission);
402
+ parentFd = admitted.fd;
403
+ parentPath = admitted.guard.realPath;
404
+ parentPathStat = admitted.guard.stat;
366
405
  }
367
406
  else {
368
407
  if (params.mkdir) {
@@ -375,15 +414,6 @@ export async function runPinnedWriteNative(binding, params) {
375
414
  directory = admitted.stagedDirectory;
376
415
  parentPathStat = admitted.guard.stat;
377
416
  }
378
- if (!policyParentAdmitted && params.mutationAdmission) {
379
- const targetPath = path.join(parentPath, params.basename);
380
- await authorizePinnedMutation(params, {
381
- targetPath,
382
- mutationPath: targetPath,
383
- phase: "parent",
384
- });
385
- await inspectDirectoryIdentity(parentPath, inspectFileIdentitySync(() => fsSync.fstatSync(parentFd, { bigint: true })));
386
- }
387
417
  const verificationGuard = { dir: parentPath, realPath: parentPath, stat: parentPathStat };
388
418
  if (params.overwrite === false) {
389
419
  try {
@@ -423,12 +453,12 @@ export async function runPinnedWriteNative(binding, params) {
423
453
  }
424
454
  }
425
455
  }
426
- catch (e_1) {
427
- env_1.error = e_1;
428
- env_1.hasError = true;
456
+ catch (e_2) {
457
+ env_2.error = e_2;
458
+ env_2.hasError = true;
429
459
  }
430
460
  finally {
431
- const result_1 = __disposeResources(env_1);
461
+ const result_1 = __disposeResources(env_2);
432
462
  if (result_1)
433
463
  await result_1;
434
464
  }
@@ -0,0 +1,12 @@
1
+ import { FsSafeError } from "./errors.js";
2
+ import type { NativeBinding } from "./native-binding.js";
3
+ import type { MutationDirectoryObserver } from "./pinned-mutation-observation.js";
4
+ export declare class NativePolicyDirectoryMismatch extends FsSafeError {
5
+ constructor(cause?: Error);
6
+ }
7
+ export type NativePolicyDirectoryObserver = MutationDirectoryObserver & {
8
+ dispose(): void;
9
+ };
10
+ /** Fresh name/metadata/canonical facts from a caller-owned POSIX descriptor. */
11
+ export declare function nativePolicyDirectoryObserver(binding: NativeBinding | undefined, fd: number, pathname: string): NativePolicyDirectoryObserver | undefined;
12
+ //# sourceMappingURL=native-policy-directory-observation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-policy-directory-observation.d.ts","sourceRoot":"","sources":["../src/native-policy-directory-observation.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,kCAAkC,CAAC;AAElF,qBAAa,6BAA8B,SAAQ,WAAW;IAC5D,YAAY,KAAK,CAAC,EAAE,KAAK,EAExB;CACF;AAED,MAAM,MAAM,6BAA6B,GAAG,yBAAyB,GAAG;IAAE,OAAO,IAAI,IAAI,CAAA;CAAE,CAAC;AAE5F,gFAAgF;AAChF,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,aAAa,GAAG,SAAS,EAClC,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,MAAM,GACf,6BAA6B,GAAG,SAAS,CA2C3C"}
@@ -0,0 +1,61 @@
1
+ import path from "node:path";
2
+ import { FsSafeError } from "./errors.js";
3
+ import { getFsSafeNativeConfig } from "./native-config.js";
4
+ export class NativePolicyDirectoryMismatch extends FsSafeError {
5
+ constructor(cause) {
6
+ super("path-mismatch", "staging directory pathname changed", { cause });
7
+ }
8
+ }
9
+ /** Fresh name/metadata/canonical facts from a caller-owned POSIX descriptor. */
10
+ export function nativePolicyDirectoryObserver(binding, fd, pathname) {
11
+ if (!binding || process.platform === "win32" || process.versions.bun)
12
+ return undefined;
13
+ const observe = binding.observeDirectoryFd;
14
+ if (typeof observe !== "function")
15
+ return undefined;
16
+ const mode = getFsSafeNativeConfig().mode;
17
+ let disposed = false;
18
+ const inspect = () => {
19
+ if (disposed)
20
+ throw new NativePolicyDirectoryMismatch();
21
+ if (getFsSafeNativeConfig().mode !== mode)
22
+ return undefined;
23
+ let observed;
24
+ try {
25
+ observed = observe.call(binding, fd, pathname);
26
+ }
27
+ catch (error) {
28
+ if (error?.code === "OBSERVATION_UNAVAILABLE")
29
+ return undefined;
30
+ if (error?.code === "OBSERVATION_REDIRECTED") {
31
+ // A contained redirect requires ordinary selected-object admission;
32
+ // it has not yet supplied inconsistent descriptor/name evidence.
33
+ throw new FsSafeError("path-mismatch", "staging directory pathname redirected");
34
+ }
35
+ if (error?.code === "path-mismatch") {
36
+ throw new NativePolicyDirectoryMismatch(error instanceof Error ? error : undefined);
37
+ }
38
+ throw error;
39
+ }
40
+ if (getFsSafeNativeConfig().mode !== mode)
41
+ return undefined;
42
+ if (!observed || typeof observed.dev !== "bigint" || observed.dev < 0n || observed.dev > 0xffffffffffffffffn ||
43
+ typeof observed.ino !== "bigint" || observed.ino < 0n || observed.ino > 0xffffffffffffffffn ||
44
+ typeof observed.mode !== "bigint" || observed.mode < 0n || observed.mode > 0xffffffffn ||
45
+ typeof observed.nlink !== "bigint" || observed.nlink < 0n || observed.nlink > 0xffffffffffffffffn ||
46
+ typeof observed.realPath !== "string" || observed.realPath.includes("\0") ||
47
+ !path.isAbsolute(observed.realPath))
48
+ return undefined;
49
+ if ((observed.mode & 61440n) !== 16384n || observed.nlink === 0n ||
50
+ observed.realPath !== pathname) {
51
+ throw new NativePolicyDirectoryMismatch();
52
+ }
53
+ return Object.freeze({
54
+ canonicalPath: observed.realPath,
55
+ identity: Object.freeze({
56
+ dev: observed.dev, ino: observed.ino, mode: observed.mode, nlink: observed.nlink,
57
+ }),
58
+ });
59
+ };
60
+ return Object.assign(inspect, { dispose() { disposed = true; } });
61
+ }
@@ -0,0 +1,14 @@
1
+ import { type BigIntStats } from "node:fs";
2
+ import { type AsyncDirectoryGuard } from "./directory-guard.js";
3
+ import { type NativeRootAdmission } from "./native-parent-admission.js";
4
+ import type { NativeBinding } from "./native.js";
5
+ import type { PinnedWriteParams } from "./pinned-write.js";
6
+ import { type MutationDirectoryObservation } from "./pinned-mutation-observation.js";
7
+ type PolicyParent = {
8
+ fd: number;
9
+ guard: AsyncDirectoryGuard<BigIntStats>;
10
+ observation: MutationDirectoryObservation;
11
+ };
12
+ export declare function capturePolicyAwareWindowsParent(binding: NativeBinding, params: PinnedWriteParams, rootAdmission: NativeRootAdmission): Promise<PolicyParent>;
13
+ export {};
14
+ //# sourceMappingURL=native-policy-parent-windows.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-policy-parent-windows.d.ts","sourceRoot":"","sources":["../src/native-policy-parent-windows.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAEnD,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAA6B,KAAK,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAKjD,OAAO,KAAK,EAAE,iBAAiB,EAAkG,MAAM,mBAAmB,CAAC;AAC3J,OAAO,EAA4B,KAAK,4BAA4B,EAAE,MAAM,kCAAkC,CAAC;AAI/G,KAAK,YAAY,GAAG;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC;IACxC,WAAW,EAAE,4BAA4B,CAAC;CAC3C,CAAC;AAqBF,wBAAsB,+BAA+B,CACnD,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,iBAAiB,EACzB,aAAa,EAAE,mBAAmB,GACjC,OAAO,CAAC,YAAY,CAAC,CA2KvB"}