@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 +4 -0
- package/dist/guarded-mutation.d.ts +1 -0
- package/dist/guarded-mutation.d.ts.map +1 -1
- package/dist/guarded-mutation.js +2 -0
- package/dist/move-path.d.ts +2 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +21 -3
- package/docs/atomic.md +34 -0
- package/package.json +8 -8
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
|
|
@@ -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,
|
|
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"}
|
package/dist/guarded-mutation.js
CHANGED
|
@@ -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
|
}
|
package/dist/move-path.d.ts
CHANGED
package/dist/move-path.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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.
|
|
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.
|
|
153
|
-
"@openclaw/fs-safe-darwin-x64": "0.7.
|
|
154
|
-
"@openclaw/fs-safe-linux-arm64-gnu": "0.7.
|
|
155
|
-
"@openclaw/fs-safe-linux-arm64-musl": "0.7.
|
|
156
|
-
"@openclaw/fs-safe-linux-x64-gnu": "0.7.
|
|
157
|
-
"@openclaw/fs-safe-linux-x64-musl": "0.7.
|
|
158
|
-
"@openclaw/fs-safe-win32-x64-msvc": "0.7.
|
|
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
|
},
|