@openclaw/fs-safe 0.13.0 → 0.14.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 (59) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/dist/copy-file-input.d.ts +1 -1
  3. package/dist/copy-file-input.d.ts.map +1 -1
  4. package/dist/copy-file-input.js +2 -0
  5. package/dist/file-store-sync-directory.d.ts.map +1 -1
  6. package/dist/file-store-sync-directory.js +14 -7
  7. package/dist/guest-dispatch-python.js +1 -1
  8. package/dist/native-binding.d.ts +9 -0
  9. package/dist/native-binding.d.ts.map +1 -1
  10. package/dist/native-binding.js +8 -1
  11. package/dist/native-operations.d.ts.map +1 -1
  12. package/dist/native-operations.js +6 -4
  13. package/dist/native-parent-admission.d.ts +1 -0
  14. package/dist/native-parent-admission.d.ts.map +1 -1
  15. package/dist/native-parent-admission.js +6 -3
  16. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  17. package/dist/native-pinned-write-windows.js +8 -6
  18. package/dist/native-pinned-write.d.ts.map +1 -1
  19. package/dist/native-pinned-write.js +200 -163
  20. package/dist/native-policy-directory-observation.d.ts +12 -0
  21. package/dist/native-policy-directory-observation.d.ts.map +1 -0
  22. package/dist/native-policy-directory-observation.js +61 -0
  23. package/dist/native-staged-file.d.ts +4 -4
  24. package/dist/native-staged-file.d.ts.map +1 -1
  25. package/dist/native-staged-file.js +20 -10
  26. package/dist/native.d.ts +1 -1
  27. package/dist/native.d.ts.map +1 -1
  28. package/dist/native.js +7 -2
  29. package/dist/pinned-mutation-admission.d.ts.map +1 -1
  30. package/dist/pinned-mutation-admission.js +13 -24
  31. package/dist/pinned-mutation-observation.d.ts +7 -1
  32. package/dist/pinned-mutation-observation.d.ts.map +1 -1
  33. package/dist/pinned-mutation-observation.js +87 -5
  34. package/dist/publish-file.d.ts.map +1 -1
  35. package/dist/publish-file.js +5 -3
  36. package/dist/root-directory-creation.d.ts.map +1 -1
  37. package/dist/root-directory-creation.js +5 -4
  38. package/dist/root-impl.d.ts.map +1 -1
  39. package/dist/root-impl.js +2 -1
  40. package/dist/root-path-existing.d.ts.map +1 -1
  41. package/dist/root-path-existing.js +1 -2
  42. package/dist/root-write-compatibility.d.ts +1 -2
  43. package/dist/root-write-compatibility.d.ts.map +1 -1
  44. package/dist/root-write-compatibility.js +17 -68
  45. package/dist/root-write-complete-parent.d.ts.map +1 -1
  46. package/dist/root-write-complete-parent.js +3 -1
  47. package/dist/root-write-lock-binding.d.ts +15 -0
  48. package/dist/root-write-lock-binding.d.ts.map +1 -0
  49. package/dist/root-write-lock-binding.js +162 -0
  50. package/dist/staged-directory.d.ts +5 -2
  51. package/dist/staged-directory.d.ts.map +1 -1
  52. package/dist/staged-directory.js +37 -1
  53. package/dist/temp-workspace-admission.d.ts.map +1 -1
  54. package/dist/temp-workspace-admission.js +28 -4
  55. package/docs/guest.md +1 -1
  56. package/docs/native-helper.md +14 -1
  57. package/docs/native.md +8 -0
  58. package/docs/writing.md +6 -0
  59. package/package.json +8 -8
@@ -57,6 +57,8 @@ import { FsSafeError } from "./errors.js";
57
57
  import { runPinnedWriteWindows } from "./native-pinned-write-windows.js";
58
58
  import { openNativeParentAdmission, openNativeRootAdmission } from "./native-parent-admission.js";
59
59
  import { assertNativeStaging, writeNativeStage } from "./native-staged-file.js";
60
+ import { captureNativeFdClose } from "./native-binding.js";
61
+ import { NativePolicyDirectoryMismatch } from "./native-policy-directory-observation.js";
60
62
  import { assertPolicyStagedDirectoryCurrent, assertStagedDirectoryCurrent, describePolicyStagedDirectory, describeStagedDirectory, refreshPolicyStagedDirectoryObservation, } from "./staged-directory.js";
61
63
  import { inspectFileIdentitySync } from "./strict-file-identity.js";
62
64
  import { realpathSync } from "./realpath.js";
@@ -117,8 +119,8 @@ async function describePosixParent(parentFd, pathname) {
117
119
  observation: checkedMutationDirectory(parentPath, directory.realPath, parentPathStat),
118
120
  };
119
121
  }
120
- function describePolicyPosixParent(parentFd, pathname) {
121
- const policyDirectory = describePolicyStagedDirectory(parentFd, pathname);
122
+ function describePolicyPosixParent(binding, parentFd, pathname) {
123
+ const policyDirectory = describePolicyStagedDirectory(parentFd, pathname, binding);
122
124
  return {
123
125
  parentFd,
124
126
  parentPath: policyDirectory.directory.realPath,
@@ -130,11 +132,12 @@ function describePolicyPosixParent(parentFd, pathname) {
130
132
  }
131
133
  function policyParentCaptureDeopt(error) {
132
134
  return error instanceof FsSafeError &&
135
+ !(error instanceof NativePolicyDirectoryMismatch) &&
133
136
  (error.code === "path-mismatch" || error.code === "not-file");
134
137
  }
135
- function tryDescribePolicyPosixParent(parentFd, pathname) {
138
+ function tryDescribePolicyPosixParent(binding, parentFd, pathname) {
136
139
  try {
137
- return describePolicyPosixParent(parentFd, pathname);
140
+ return describePolicyPosixParent(binding, parentFd, pathname);
138
141
  }
139
142
  catch (error) {
140
143
  if (!policyParentCaptureDeopt(error))
@@ -148,183 +151,217 @@ function assertPosixParentCurrent(parent) {
148
151
  : assertStagedDirectoryCurrent(parent.directory);
149
152
  }
150
153
  async function capturePolicyAwarePosixParent(binding, params, rootFd, directoryFlags) {
151
- const segments = relativeParentSegments(params.relativeParentPath);
152
- const parentSpelling = segments.length
153
- ? path.join(params.rootPath, ...segments)
154
- : params.rootPath;
155
- const segmentRoute = createPathSegmentRoute(segments);
156
- const initialTarget = prospectiveTargetPath(params.rootPath, segmentRoute, 0, params.basename);
157
- let retainedTargetPath = params.mutationAdmission?.beginParentWalk?.();
158
- if (retainedTargetPath && !sameAbsolutePath(retainedTargetPath, initialTarget)) {
159
- retainedTargetPath = undefined;
160
- }
161
- // Preserve the one-open hot path when the complete parent still exists.
162
- // Policy is attached only after the opened descriptor is associated with its
163
- // current canonical pathname, so a contained redirect cannot retain the old
164
- // preflight authorization.
165
- let completeParentFd;
154
+ const env_1 = { stack: [], error: void 0, hasError: false };
166
155
  try {
167
- completeParentFd = binding.openBeneath(rootFd, params.relativeParentPath, directoryFlags).fd;
168
- }
169
- catch (error) {
170
- if (error?.code !== "ENOENT" || !params.mkdir)
171
- throw error;
172
- }
173
- if (completeParentFd !== undefined) {
174
- const parentFd = completeParentFd;
156
+ const closeFd = captureNativeFdClose(binding);
157
+ const observationDisposers = new Map();
158
+ const disposeObservation = (fd) => {
159
+ const dispose = observationDisposers.get(fd);
160
+ observationDisposers.delete(fd);
161
+ dispose?.();
162
+ };
163
+ const observationScope = __addDisposableResource(env_1, {
164
+ [Symbol.dispose]() { for (const dispose of observationDisposers.values())
165
+ dispose(); },
166
+ }, false);
167
+ const capturePolicyParent = (fd, pathname) => {
168
+ const captured = tryDescribePolicyPosixParent(binding, fd, pathname);
169
+ const dispose = captured?.policyDirectory?.disposeObservation;
170
+ if (dispose)
171
+ observationDisposers.set(fd, dispose);
172
+ return captured;
173
+ };
174
+ const segments = relativeParentSegments(params.relativeParentPath);
175
+ const parentSpelling = segments.length
176
+ ? path.join(params.rootPath, ...segments)
177
+ : params.rootPath;
178
+ const segmentRoute = createPathSegmentRoute(segments);
179
+ const initialTarget = prospectiveTargetPath(params.rootPath, segmentRoute, 0, params.basename);
180
+ let retainedTargetPath = params.mutationAdmission?.beginParentWalk?.();
181
+ if (retainedTargetPath && !sameAbsolutePath(retainedTargetPath, initialTarget)) {
182
+ retainedTargetPath = undefined;
183
+ }
184
+ // Preserve the one-open hot path when the complete parent still exists.
185
+ // Policy is attached only after the opened descriptor is associated with its
186
+ // current canonical pathname, so a contained redirect cannot retain the old
187
+ // preflight authorization.
188
+ let completeParentFd;
175
189
  try {
176
- let admitted = retainedTargetPath
177
- ? tryDescribePolicyPosixParent(parentFd, parentSpelling)
178
- : undefined;
179
- if (!admitted) {
180
- retainedTargetPath = undefined;
181
- admitted = await describePosixParent(parentFd, parentSpelling);
182
- }
183
- const targetPath = path.join(admitted.parentPath, params.basename);
184
- await authorizePinnedMutation(params, {
185
- targetPath,
186
- mutationPath: targetPath,
187
- phase: "parent",
188
- });
189
- assertPosixParentCurrent(admitted);
190
- return admitted;
190
+ completeParentFd = binding.openBeneath(rootFd, params.relativeParentPath, directoryFlags).fd;
191
191
  }
192
192
  catch (error) {
193
- fsSync.closeSync(parentFd);
194
- throw error;
193
+ if (error?.code !== "ENOENT" || !params.mkdir)
194
+ throw error;
195
195
  }
196
- }
197
- // The canonical preflight spelling has no intentional symlinks in its
198
- // existing prefix. Walk missing-parent cases one direct child at a time so
199
- // every existing/opened object is authorized and every mkdir is authorized
200
- // before dispatch. O_NOFOLLOW makes a concurrently introduced link fail
201
- // before the next component can be created through it.
202
- const secureDirectoryFlags = directoryFlags | (fsSync.constants.O_NOFOLLOW ?? 0);
203
- let currentFd = rootFd;
204
- let currentOwnedFd;
205
- let currentPath = params.rootPath;
206
- let current = retainedTargetPath
207
- ? tryDescribePolicyPosixParent(rootFd, currentPath)
208
- : undefined;
209
- if (!current) {
210
- retainedTargetPath = undefined;
211
- current = await describePosixParent(rootFd, currentPath);
212
- }
213
- try {
214
- await authorizePinnedMutation(params, {
215
- targetPath: retainedTargetPath ?? initialTarget,
216
- mutationPath: retainedTargetPath ?? initialTarget,
217
- phase: "parent",
218
- });
219
- assertPosixParentCurrent(current);
220
- for (let index = 0; index < segments.length; index += 1) {
221
- const segment = segments[index];
222
- const childPath = path.join(currentPath, segment);
223
- let childFd;
224
- let createdByMkdir = false;
225
- let createReceipt;
196
+ if (completeParentFd !== undefined) {
197
+ const parentFd = completeParentFd;
226
198
  try {
227
- childFd = binding.openBeneath(currentFd, segment, secureDirectoryFlags).fd;
228
- }
229
- catch (openError) {
230
- if (!isNotFoundPathError(openError)) {
231
- throw normalizePolicyParentOpenError(openError, params);
199
+ let admitted = retainedTargetPath
200
+ ? capturePolicyParent(parentFd, parentSpelling)
201
+ : undefined;
202
+ if (!admitted) {
203
+ retainedTargetPath = undefined;
204
+ admitted = await describePosixParent(parentFd, parentSpelling);
232
205
  }
233
- const targetPath = retainedTargetPath ?? prospectiveTargetPath(currentPath, segmentRoute, index, params.basename);
234
- const request = Object.freeze({
206
+ const targetPath = path.join(admitted.parentPath, params.basename);
207
+ await authorizePinnedMutation(params, {
235
208
  targetPath,
236
- mutationPath: childPath,
237
- phase: "parent-create",
209
+ mutationPath: targetPath,
210
+ phase: "parent",
238
211
  });
239
- createReceipt = params.mutationAdmission?.tryAuthorizeAtParent?.(request, current.observation);
240
- if (!createReceipt) {
241
- createReceipt = await authorizePinnedMutation(params, request);
242
- assertPosixParentCurrent(current);
243
- }
244
- params.assertBeforeMutation?.();
245
- // Do not carry pathname freshness across the live authority callback.
246
- assertPosixParentCurrent(current);
247
- createdByMkdir = mkdirPolicyChild(binding, currentFd, segment, 0o777);
212
+ assertPosixParentCurrent(admitted);
213
+ return admitted;
214
+ }
215
+ catch (error) {
216
+ disposeObservation(parentFd);
217
+ closeFd(parentFd);
218
+ throw error;
219
+ }
220
+ }
221
+ // The canonical preflight spelling has no intentional symlinks in its
222
+ // existing prefix. Walk missing-parent cases one direct child at a time so
223
+ // every existing/opened object is authorized and every mkdir is authorized
224
+ // before dispatch. O_NOFOLLOW makes a concurrently introduced link fail
225
+ // before the next component can be created through it.
226
+ const secureDirectoryFlags = directoryFlags | (fsSync.constants.O_NOFOLLOW ?? 0);
227
+ let currentFd = rootFd;
228
+ let currentOwnedFd;
229
+ let currentPath = params.rootPath;
230
+ let current = retainedTargetPath
231
+ ? capturePolicyParent(rootFd, currentPath)
232
+ : undefined;
233
+ if (!current) {
234
+ retainedTargetPath = undefined;
235
+ current = await describePosixParent(rootFd, currentPath);
236
+ }
237
+ try {
238
+ await authorizePinnedMutation(params, {
239
+ targetPath: retainedTargetPath ?? initialTarget,
240
+ mutationPath: retainedTargetPath ?? initialTarget,
241
+ phase: "parent",
242
+ });
243
+ assertPosixParentCurrent(current);
244
+ for (let index = 0; index < segments.length; index += 1) {
245
+ const segment = segments[index];
246
+ const childPath = path.join(currentPath, segment);
247
+ let childFd;
248
+ let createdByMkdir = false;
249
+ let createReceipt;
248
250
  try {
249
251
  childFd = binding.openBeneath(currentFd, segment, secureDirectoryFlags).fd;
250
252
  }
251
- catch (createdOpenError) {
252
- throw normalizePolicyParentOpenError(createdOpenError, params);
253
- }
254
- }
255
- let child;
256
- try {
257
- const acceleratedChild = retainedTargetPath
258
- ? tryDescribePolicyPosixParent(childFd, childPath)
259
- : undefined;
260
- if (acceleratedChild) {
261
- child = acceleratedChild;
262
- }
263
- else {
264
- retainedTargetPath = undefined;
265
- child = await describePosixParent(childFd, childPath);
266
- }
267
- if (!sameAbsolutePath(child.parentPath, childPath))
268
- retainedTargetPath = undefined;
269
- let childAuthorization;
270
- if (createdByMkdir && createReceipt && params.mutationAdmission?.advanceCreatedDirectory) {
271
- let evidence;
253
+ catch (openError) {
254
+ if (!isNotFoundPathError(openError)) {
255
+ throw normalizePolicyParentOpenError(openError, params);
256
+ }
257
+ const targetPath = retainedTargetPath ?? prospectiveTargetPath(currentPath, segmentRoute, index, params.basename);
258
+ const request = Object.freeze({
259
+ targetPath,
260
+ mutationPath: childPath,
261
+ phase: "parent-create",
262
+ });
263
+ createReceipt = params.mutationAdmission?.tryAuthorizeAtParent?.(request, current.observation);
264
+ if (!createReceipt) {
265
+ createReceipt = await authorizePinnedMutation(params, request);
266
+ assertPosixParentCurrent(current);
267
+ }
268
+ params.assertBeforeMutation?.();
269
+ // Do not carry pathname freshness across the live authority callback.
270
+ assertPosixParentCurrent(current);
271
+ createdByMkdir = mkdirPolicyChild(binding, currentFd, segment, 0o777);
272
272
  try {
273
- const parent = current.policyDirectory
274
- ? refreshPolicyStagedDirectoryObservation(current.policyDirectory)
275
- : checkedMutationDirectory(currentPath, current.directory.realPath, assertStagedDirectoryCurrent(current.directory));
276
- evidence = Object.freeze({
277
- admission: createReceipt,
278
- parent,
279
- child: child.observation,
280
- });
273
+ childFd = binding.openBeneath(currentFd, segment, secureDirectoryFlags).fd;
281
274
  }
282
- catch {
283
- // Leave failed evidence to the full ordered admission below.
275
+ catch (createdOpenError) {
276
+ throw normalizePolicyParentOpenError(createdOpenError, params);
284
277
  }
285
- if (evidence) {
286
- childAuthorization = params.mutationAdmission.advanceCreatedDirectory(evidence);
278
+ }
279
+ let child;
280
+ try {
281
+ const acceleratedChild = retainedTargetPath
282
+ ? capturePolicyParent(childFd, childPath)
283
+ : undefined;
284
+ if (acceleratedChild) {
285
+ child = acceleratedChild;
286
+ }
287
+ else {
288
+ retainedTargetPath = undefined;
289
+ child = await describePosixParent(childFd, childPath);
290
+ }
291
+ if (!sameAbsolutePath(child.parentPath, childPath))
292
+ retainedTargetPath = undefined;
293
+ let childAuthorization;
294
+ if (createdByMkdir && createReceipt && params.mutationAdmission?.advanceCreatedDirectory) {
295
+ let evidence;
296
+ try {
297
+ const parent = current.policyDirectory
298
+ ? refreshPolicyStagedDirectoryObservation(current.policyDirectory)
299
+ : checkedMutationDirectory(currentPath, current.directory.realPath, assertStagedDirectoryCurrent(current.directory));
300
+ evidence = Object.freeze({
301
+ admission: createReceipt,
302
+ parent,
303
+ child: child.observation,
304
+ });
305
+ }
306
+ catch {
307
+ // Leave failed evidence to the full ordered admission below.
308
+ }
309
+ if (evidence) {
310
+ childAuthorization = params.mutationAdmission.advanceCreatedDirectory(evidence);
311
+ }
312
+ }
313
+ const targetPath = retainedTargetPath ?? prospectiveTargetPath(child.parentPath, segmentRoute, index + 1, params.basename);
314
+ const request = Object.freeze({
315
+ targetPath,
316
+ mutationPath: targetPath,
317
+ phase: "parent",
318
+ });
319
+ childAuthorization ??= params.mutationAdmission?.tryAuthorizeAtParent?.(request, child.observation);
320
+ if (!childAuthorization) {
321
+ await authorizePinnedMutation(params, request);
322
+ assertPosixParentCurrent(child);
287
323
  }
288
324
  }
289
- const targetPath = retainedTargetPath ?? prospectiveTargetPath(child.parentPath, segmentRoute, index + 1, params.basename);
290
- const request = Object.freeze({
291
- targetPath,
292
- mutationPath: targetPath,
293
- phase: "parent",
294
- });
295
- childAuthorization ??= params.mutationAdmission?.tryAuthorizeAtParent?.(request, child.observation);
296
- if (!childAuthorization) {
297
- await authorizePinnedMutation(params, request);
298
- assertPosixParentCurrent(child);
325
+ catch (error) {
326
+ disposeObservation(childFd);
327
+ closeFd(childFd);
328
+ throw error;
329
+ }
330
+ const previousOwnedFd = currentOwnedFd;
331
+ disposeObservation(current.parentFd);
332
+ currentOwnedFd = child.parentFd;
333
+ currentFd = child.parentFd;
334
+ currentPath = child.parentPath;
335
+ current = child;
336
+ if (previousOwnedFd !== undefined)
337
+ closeFd(previousOwnedFd);
338
+ if (index === segments.length - 1) {
339
+ currentOwnedFd = undefined;
340
+ return child;
299
341
  }
300
342
  }
301
- catch (error) {
302
- fsSync.closeSync(childFd);
303
- throw error;
304
- }
305
- const previousOwnedFd = currentOwnedFd;
306
- currentOwnedFd = child.parentFd;
307
- currentFd = child.parentFd;
308
- currentPath = child.parentPath;
309
- current = child;
310
- if (previousOwnedFd !== undefined)
311
- fsSync.closeSync(previousOwnedFd);
312
- if (index === segments.length - 1) {
313
- currentOwnedFd = undefined;
314
- return child;
343
+ // Empty relative parents are handled by the complete-parent fast path.
344
+ throw new FsSafeError("path-mismatch", "native write parent admission did not complete");
345
+ }
346
+ finally {
347
+ if (currentOwnedFd !== undefined) {
348
+ disposeObservation(currentOwnedFd);
349
+ closeFd(currentOwnedFd);
315
350
  }
316
351
  }
317
- // Empty relative parents are handled by the complete-parent fast path.
318
- throw new FsSafeError("path-mismatch", "native write parent admission did not complete");
352
+ }
353
+ catch (e_1) {
354
+ env_1.error = e_1;
355
+ env_1.hasError = true;
319
356
  }
320
357
  finally {
321
- if (currentOwnedFd !== undefined)
322
- fsSync.closeSync(currentOwnedFd);
358
+ __disposeResources(env_1);
323
359
  }
324
360
  }
325
361
  export async function runPinnedWriteNative(binding, params) {
326
- const env_1 = { stack: [], error: void 0, hasError: false };
362
+ const env_2 = { stack: [], error: void 0, hasError: false };
327
363
  try {
364
+ const closeFd = captureNativeFdClose(binding);
328
365
  const windows = process.platform === "win32";
329
366
  if (!windows) {
330
367
  assertNativeStaging(binding);
@@ -336,15 +373,15 @@ export async function runPinnedWriteNative(binding, params) {
336
373
  operation: "native write",
337
374
  });
338
375
  const root = rootAdmission.root;
339
- const posixRoot = __addDisposableResource(env_1, windows ? undefined : root, true);
376
+ const posixRoot = __addDisposableResource(env_2, windows ? undefined : root, true);
340
377
  let parentFd;
341
378
  let windowsOwnsDirectories = false;
342
379
  // Until stage construction takes ownership, even admission failures must close
343
380
  // the raw POSIX parent. Disposal preserves both admission and close failures.
344
- const parentGuard = __addDisposableResource(env_1, {
381
+ const parentGuard = __addDisposableResource(env_2, {
345
382
  [Symbol.dispose]() {
346
383
  if (!windows && parentFd !== undefined) {
347
- fsSync.closeSync(parentFd);
384
+ closeFd(parentFd);
348
385
  }
349
386
  },
350
387
  }, false);
@@ -400,7 +437,7 @@ export async function runPinnedWriteNative(binding, params) {
400
437
  }
401
438
  const ownedParent = parentFd;
402
439
  parentFd = undefined;
403
- return await writeNativeStage(binding, ownedParent, directory, params, verificationGuard);
440
+ return await writeNativeStage(binding, ownedParent, closeFd, directory, params, verificationGuard);
404
441
  }
405
442
  finally {
406
443
  if (windows && !windowsOwnsDirectories) {
@@ -410,7 +447,7 @@ export async function runPinnedWriteNative(binding, params) {
410
447
  // a close attempt. In particular, a parent close failure must not skip
411
448
  // the root FileHandle close.
412
449
  try {
413
- fsSync.closeSync(parentFd);
450
+ closeFd(parentFd);
414
451
  }
415
452
  catch {
416
453
  // Best effort while propagating the operation failure.
@@ -420,12 +457,12 @@ export async function runPinnedWriteNative(binding, params) {
420
457
  }
421
458
  }
422
459
  }
423
- catch (e_1) {
424
- env_1.error = e_1;
425
- env_1.hasError = true;
460
+ catch (e_2) {
461
+ env_2.error = e_2;
462
+ env_2.hasError = true;
426
463
  }
427
464
  finally {
428
- const result_1 = __disposeResources(env_1);
465
+ const result_1 = __disposeResources(env_2);
429
466
  if (result_1)
430
467
  await result_1;
431
468
  }
@@ -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
+ }
@@ -1,16 +1,16 @@
1
1
  import type { DirectoryReceipt } from "./directory-durability.js";
2
2
  import type { AnyAsyncDirectoryGuard } from "./directory-guard.js";
3
3
  import type { FileIdentityStat } from "./file-identity.js";
4
- import type { NativeBinding } from "./native-binding.js";
4
+ import { type NativeBinding } from "./native-binding.js";
5
5
  import type { PinnedWriteInput, PinnedWriteParams } from "./pinned-write.js";
6
6
  import type { PublishedFileReceipt, StagedFile, StagedFileCleanupReceipt, StagedFileReceipt } from "./staged-file-types.js";
7
7
  export type NativeStagingBinding = NativeBinding & Required<Pick<NativeBinding, "createStagedFile" | "stagedFileMatches" | "removeStagedFile">>;
8
8
  export declare function assertNativeStaging(binding: NativeBinding): asserts binding is NativeStagingBinding;
9
9
  declare class NativeStagedFile implements StagedFile {
10
10
  #private;
11
- constructor(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], portableNames: boolean, publishedMode: number, sync: boolean, assertBeforeMutation?: () => void);
12
- static create(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], input: PinnedWriteInput, mode: number, maxBytes?: number, portableNames?: boolean, sync?: boolean, assertBeforeMutation?: () => void): Promise<NativeStagedFile>;
13
- static write(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], params: PinnedWriteParams, parentGuard: AnyAsyncDirectoryGuard): Promise<FileIdentityStat>;
11
+ constructor(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], portableNames: boolean, publishedMode: number, sync: boolean, assertBeforeMutation?: () => void);
12
+ static create(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], input: PinnedWriteInput, mode: number, maxBytes?: number, portableNames?: boolean, sync?: boolean, assertBeforeMutation?: () => void): Promise<NativeStagedFile>;
13
+ static write(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], params: PinnedWriteParams, parentGuard: AnyAsyncDirectoryGuard): Promise<FileIdentityStat>;
14
14
  get receipt(): StagedFileReceipt;
15
15
  assertCurrent(): Promise<void>;
16
16
  publish(basename: string, options: {
@@ -1 +1 @@
1
- {"version":3,"file":"native-staged-file.d.ts","sourceRoot":"","sources":["../src/native-staged-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAMlE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAGnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAG7E,OAAO,KAAK,EACV,oBAAoB,EACpB,UAAU,EACV,wBAAwB,EAGxB,iBAAiB,EAClB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,MAAM,oBAAoB,GAAG,aAAa,GAAG,QAAQ,CAAC,IAAI,CAC9D,aAAa,EACb,kBAAkB,GAAG,mBAAmB,GAAG,kBAAkB,CAC9D,CAAC,CAAC;AAEH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,IAAI,oBAAoB,CAUnG;AA6BD,cAAM,gBAAiB,YAAW,UAAU;;IAa1C,YACE,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,aAAa,EAAE,OAAO,EACtB,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,OAAO,EACb,oBAAoB,CAAC,EAAE,MAAM,IAAI,EASlC;IAGD,OAAa,MAAM,CACjB,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,MAAM,EAEjB,aAAa,UAAO,EACpB,IAAI,UAAO,EACX,oBAAoB,CAAC,EAAE,MAAM,IAAI,GAChC,OAAO,CAAC,gBAAgB,CAAC,CAc3B;IAED,OAAa,KAAK,CAChB,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,MAAM,EAAE,iBAAiB,EACzB,WAAW,EAAE,sBAAsB,GAClC,OAAO,CAAC,gBAAgB,CAAC,CAc3B;IAED,IAAI,OAAO,IAAI,iBAAiB,CAK/B;IA8FK,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnC;IAEK,OAAO,CACX,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,EAC/B,WAAW,CAAC,EAAE,iBAAiB,CAAC,aAAa,CAAC,GAC7C,OAAO,CAAC,oBAAoB,CAAC,CAyD/B;IA6CK,OAAO,IAAI,OAAO,CAAC,wBAAwB,CAAC,CAMjD;IAEK,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAO3C;CACF;AAED,eAAO,MAAM,iBAAiB,EAAE,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,UAAU,CAClF,CAAC;AAC1B,eAAO,MAAM,gBAAgB,+BAAyB,CAAC;AAEvD,YAAY,EACV,oBAAoB,EAAE,UAAU,EAAE,wBAAwB,EAAE,wBAAwB,EACpF,qBAAqB,EAAE,iBAAiB,GACzC,MAAM,wBAAwB,CAAC;AAEhC,wBAAsB,oBAAoB,CAAC,OAAO,EAAE;IAClD,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAAC;IACrC,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,UAAU,CAAC,CAUtB"}
1
+ {"version":3,"file":"native-staged-file.d.ts","sourceRoot":"","sources":["../src/native-staged-file.ts"],"names":[],"mappings":"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"}