@openclaw/fs-safe 0.7.1 → 0.7.2

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.2 - 2026-09-01
4
+
5
+ - Add `movePathWithCopyFallback({ assertBeforeRename })` to synchronously recheck caller-owned authorization immediately before direct or staged rename, preserving refusal errors and rejecting asynchronous callbacks without publishing the move.
6
+
3
7
  ## 0.7.1 - 2026-09-01
4
8
 
5
9
  ### Highlights
@@ -7,6 +7,7 @@ export declare function withSyncDirectoryGuards<T>(guards: readonly SyncDirector
7
7
  verifyAfter?: boolean;
8
8
  }): T;
9
9
  export declare function guardedRename(params: {
10
+ assertBeforeRename?: () => void;
10
11
  from: string;
11
12
  to: string;
12
13
  targetRoot?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"guarded-mutation.d.ts","sourceRoot":"","sources":["../src/guarded-mutation.ts"],"names":[],"mappings":"AAGA,OAAO,EAOL,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACxB,MAAM,sBAAsB,CAAC;AAE9B,wBAAsB,wBAAwB,CAAC,CAAC,EAC9C,MAAM,EAAE,SAAS,mBAAmB,EAAE,EACtC,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACxB,OAAO,GAAE;IACP,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACrE,GACL,OAAO,CAAC,CAAC,CAAC,CAyBZ;AAED,wBAAgB,uBAAuB,CAAC,CAAC,EACvC,MAAM,EAAE,SAAS,kBAAkB,EAAE,EACrC,MAAM,EAAE,MAAM,CAAC,EACf,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,GACtC,CAAC,CAWH;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhB;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GAAG,IAAI,CAUP;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhB;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GAAG,IAAI,CAWP"}
1
+ {"version":3,"file":"guarded-mutation.d.ts","sourceRoot":"","sources":["../src/guarded-mutation.ts"],"names":[],"mappings":"AAGA,OAAO,EAOL,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACxB,MAAM,sBAAsB,CAAC;AAE9B,wBAAsB,wBAAwB,CAAC,CAAC,EAC9C,MAAM,EAAE,SAAS,mBAAmB,EAAE,EACtC,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACxB,OAAO,GAAE;IACP,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACrE,GACL,OAAO,CAAC,CAAC,CAAC,CAyBZ;AAED,wBAAgB,uBAAuB,CAAC,CAAC,EACvC,MAAM,EAAE,SAAS,kBAAkB,EAAE,EACrC,MAAM,EAAE,MAAM,CAAC,EACf,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,GACtC,CAAC,CAWH;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CAchB;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GAAG,IAAI,CAUP;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhB;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GAAG,IAAI,CAWP"}
@@ -48,6 +48,8 @@ export async function guardedRename(params) {
48
48
  ? await createNearestExistingDirectoryGuard(params.targetRoot, path.dirname(params.to))
49
49
  : await createAsyncDirectoryGuard(path.dirname(params.to));
50
50
  await withAsyncDirectoryGuards([sourceGuard, targetGuard], async () => {
51
+ // Authority must survive all awaited guards; do not yield before rename dispatch.
52
+ params.assertBeforeRename?.();
51
53
  await fs.rename(params.from, params.to);
52
54
  }, { verifyAfter: params.verifyAfter });
53
55
  }
@@ -1,4 +1,6 @@
1
1
  export type MovePathWithCopyFallbackOptions = {
2
+ /** Rechecks caller authority synchronously immediately before each rename dispatch. */
3
+ assertBeforeRename?: () => void;
2
4
  from: string;
3
5
  sourceHardlinks?: "allow" | "reject";
4
6
  to: string;
@@ -1 +1 @@
1
- {"version":3,"file":"move-path.d.ts","sourceRoot":"","sources":["../src/move-path.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,+BAA+B,GAAG;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IACrC,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,KAAK,sBAAsB,GAAG,cAAc,GAAG,uBAAuB,CAAC;AAEvE,wBAAgB,oCAAoC,CAClD,KAAK,EAAE,OAAO,EACd,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,sBAAsB,GAAG,SAAS,CASpC;AAiVD,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,+BAA+B,GACvC,OAAO,CAAC,IAAI,CAAC,CA2Df"}
1
+ {"version":3,"file":"move-path.d.ts","sourceRoot":"","sources":["../src/move-path.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,+BAA+B,GAAG;IAC5C,uFAAuF;IACvF,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IACrC,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,KAAK,sBAAsB,GAAG,cAAc,GAAG,uBAAuB,CAAC;AAEvE,wBAAgB,oCAAoC,CAClD,KAAK,EAAE,OAAO,EACd,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,sBAAsB,GAAG,SAAS,CASpC;AAiVD,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,+BAA+B,GACvC,OAAO,CAAC,IAAI,CAAC,CA4Ef"}
package/dist/move-path.js CHANGED
@@ -267,6 +267,23 @@ async function cleanupCopiedEntry(sourcePath, manifest) {
267
267
  return "removed";
268
268
  }
269
269
  export async function movePathWithCopyFallback(options) {
270
+ // Keep the initiating owner's callback across preparation and copy fallback.
271
+ const callerAssert = options.assertBeforeRename;
272
+ let assertionRejected = false;
273
+ const assertBeforeRename = () => {
274
+ try {
275
+ const returned = callerAssert?.();
276
+ if (returned !== undefined) {
277
+ // TypeScript permits async functions for () => void; they cannot authorize a rename.
278
+ void Promise.resolve(returned).catch(() => { });
279
+ throw new TypeError("assertBeforeRename must return undefined synchronously");
280
+ }
281
+ }
282
+ catch (error) {
283
+ assertionRejected = true;
284
+ throw error;
285
+ }
286
+ };
270
287
  const sourcePath = path.resolve(options.from);
271
288
  const targetPath = path.resolve(options.to);
272
289
  const rejectHardlinks = options.sourceHardlinks === "reject";
@@ -275,11 +292,12 @@ export async function movePathWithCopyFallback(options) {
275
292
  }
276
293
  if (!rejectHardlinks) {
277
294
  try {
278
- await guardedRename({ from: sourcePath, to: targetPath });
295
+ await guardedRename({ from: sourcePath, to: targetPath, assertBeforeRename });
279
296
  return;
280
297
  }
281
298
  catch (error) {
282
- if (!moveCopyFallbackReasonForRenameError(error)) {
299
+ // An owner's EXDEV/EPERM refusal is not permission to copy instead.
300
+ if (assertionRejected || !moveCopyFallbackReasonForRenameError(error)) {
283
301
  throw error;
284
302
  }
285
303
  }
@@ -301,7 +319,7 @@ export async function movePathWithCopyFallback(options) {
301
319
  });
302
320
  unregisterStaged.setIdentity(await fs.lstat(staged, { bigint: true }));
303
321
  await assertCopyDestinationOutsideSource(sourcePath, targetPath);
304
- await guardedRename({ from: staged, to: targetPath });
322
+ await guardedRename({ from: staged, to: targetPath, assertBeforeRename });
305
323
  stagedCommitted = true;
306
324
  unregisterStaged();
307
325
  const cleanupResult = await cleanupCopiedEntry(sourcePath, manifest);
package/docs/atomic.md CHANGED
@@ -209,6 +209,40 @@ throws `ESTALE` before commit when possible. If the destination has already
209
209
  been committed, cleanup still preserves the changed source entries and throws
210
210
  `ESTALE`.
211
211
 
212
+ ### Final rename authorization
213
+
214
+ Pass `assertBeforeRename` when a move depends on a revocable lease or another
215
+ caller-owned authorization. The helper captures this callback when called and
216
+ runs it synchronously after asynchronous preparation and directory checks,
217
+ immediately before each rename is dispatched:
218
+
219
+ ```ts
220
+ type MovePathWithCopyFallbackOptions = {
221
+ from: string;
222
+ sourceHardlinks?: "allow" | "reject";
223
+ to: string;
224
+ assertBeforeRename?: () => void;
225
+ };
226
+ ```
227
+
228
+ Throw to refuse publication. The original error is propagated, including errors
229
+ with `EXDEV` or `EPERM` codes; an authorization failure never starts a copy
230
+ fallback. A genuine rename failure may still require a second authorization
231
+ check before publishing the staged copy.
232
+
233
+ The callback must return `undefined` synchronously. Returning a Promise, thenable,
234
+ or any other value refuses the rename with a `TypeError`; rejected asynchronous
235
+ results are consumed without authorizing the operation. Perform asynchronous
236
+ policy checks before calling the helper and use this callback to recheck the
237
+ current owner at the mutation boundary.
238
+
239
+ A refused rename leaves the source and destination unchanged; any private
240
+ staged copy follows the helper's normal cleanup. The check does not cancel an
241
+ already-dispatched rename or make an external lease store atomic with the
242
+ filesystem. Cleanup after a successful move retains the existing source-identity
243
+ checks. Omitting the
244
+ callback preserves the usual move behavior.
245
+
212
246
  ## Difference from `root()`
213
247
 
214
248
  | `Root` methods | `atomic` helpers |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
5
5
  "keywords": [
6
6
  "filesystem",
@@ -149,13 +149,13 @@
149
149
  "crabbox:warmup": "crabbox warmup"
150
150
  },
151
151
  "optionalDependencies": {
152
- "@openclaw/fs-safe-darwin-arm64": "0.7.1",
153
- "@openclaw/fs-safe-darwin-x64": "0.7.1",
154
- "@openclaw/fs-safe-linux-arm64-gnu": "0.7.1",
155
- "@openclaw/fs-safe-linux-arm64-musl": "0.7.1",
156
- "@openclaw/fs-safe-linux-x64-gnu": "0.7.1",
157
- "@openclaw/fs-safe-linux-x64-musl": "0.7.1",
158
- "@openclaw/fs-safe-win32-x64-msvc": "0.7.1",
152
+ "@openclaw/fs-safe-darwin-arm64": "0.7.2",
153
+ "@openclaw/fs-safe-darwin-x64": "0.7.2",
154
+ "@openclaw/fs-safe-linux-arm64-gnu": "0.7.2",
155
+ "@openclaw/fs-safe-linux-arm64-musl": "0.7.2",
156
+ "@openclaw/fs-safe-linux-x64-gnu": "0.7.2",
157
+ "@openclaw/fs-safe-linux-x64-musl": "0.7.2",
158
+ "@openclaw/fs-safe-win32-x64-msvc": "0.7.2",
159
159
  "jszip": "^3.10.1",
160
160
  "tar": "7.5.22"
161
161
  },