@openclaw/fs-safe 0.4.1 → 0.4.3
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 +21 -0
- package/README.md +4 -2
- package/dist/advanced.d.ts +1 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -0
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/async-lock.d.ts.map +1 -1
- package/dist/bounded-read.d.ts +14 -0
- package/dist/bounded-read.d.ts.map +1 -0
- package/dist/bounded-read.js +86 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +5 -2
- package/dist/json.d.ts +8 -5
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +27 -14
- package/dist/pinned-python.js +10 -10
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +12 -2
- package/dist/read-opened-file.d.ts.map +1 -1
- package/dist/read-opened-file.js +4 -4
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +37 -49
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-path.d.ts.map +1 -1
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +5 -2
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +6 -7
- package/dist/sidecar-lock.d.ts +5 -1
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +3 -42
- package/docs/advanced.md +20 -0
- package/docs/config.md +2 -2
- package/docs/index.md +1 -1
- package/docs/json-store.md +2 -2
- package/docs/json.md +13 -5
- package/docs/regular-file.md +26 -32
- package/docs/sidecar-lock.md +8 -24
- package/docs/writing.md +1 -1
- package/package.json +19 -17
package/docs/sidecar-lock.md
CHANGED
|
@@ -51,7 +51,7 @@ type FileLockAcquireOptions<TPayload extends Record<string, unknown>> = {
|
|
|
51
51
|
staleMs?: number; // default 30_000
|
|
52
52
|
timeoutMs?: number; // overall acquire deadline; default unbounded
|
|
53
53
|
retry?: FileLockRetryOptions;
|
|
54
|
-
staleRecovery?: "fail-closed" | "remove-if-unchanged"; //
|
|
54
|
+
staleRecovery?: "fail-closed" | "remove-if-unchanged"; // legacy value also fails closed
|
|
55
55
|
allowReentrant?: boolean; // if this process already holds it, increment a count instead of failing
|
|
56
56
|
payload: () => TPayload | Promise<TPayload>;
|
|
57
57
|
shouldReclaim?: (params: {
|
|
@@ -62,7 +62,7 @@ type FileLockAcquireOptions<TPayload extends Record<string, unknown>> = {
|
|
|
62
62
|
nowMs: number;
|
|
63
63
|
heldByThisProcess: boolean;
|
|
64
64
|
}) => boolean | Promise<boolean>;
|
|
65
|
-
shouldRemoveStaleLock?: (snapshot: {
|
|
65
|
+
shouldRemoveStaleLock?: (snapshot: { // deprecated; retained but not invoked
|
|
66
66
|
lockPath: string;
|
|
67
67
|
normalizedTargetPath: string;
|
|
68
68
|
raw: string;
|
|
@@ -170,40 +170,24 @@ const handle = await acquireFileLock(targetPath, {
|
|
|
170
170
|
});
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
-
`heldByThisProcess` is true when this manager already holds the lock (relevant for the reentrant case). A `true` result marks the observed sidecar as stale
|
|
173
|
+
`heldByThisProcess` is true when this manager already holds the lock (relevant for the reentrant case). A `true` result marks the observed sidecar as stale and acquisition throws an error with code `file_lock_stale`.
|
|
174
174
|
|
|
175
|
-
## Stale recovery
|
|
175
|
+
## Stale recovery is fail-closed
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
fs-safe never removes a stale third-party sidecar during acquisition. Checking a pathname's content and identity before unlinking it is not atomic: another process can replace the lock between the final check and the unlink. Every stale result therefore fails closed with error code `file_lock_stale`.
|
|
178
178
|
|
|
179
|
-
|
|
180
|
-
const handle = await acquireFileLock(targetPath, {
|
|
181
|
-
staleMs: 60_000,
|
|
182
|
-
staleRecovery: "remove-if-unchanged",
|
|
183
|
-
payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
|
|
184
|
-
shouldReclaim: ({ payload }) => {
|
|
185
|
-
const pid = Number(payload?.pid);
|
|
186
|
-
return Number.isInteger(pid) && pid > 0 && ownerIsDefinitelyDead(pid);
|
|
187
|
-
},
|
|
188
|
-
shouldRemoveStaleLock: ({ payload }) => {
|
|
189
|
-
const pid = Number(payload?.pid);
|
|
190
|
-
return Number.isInteger(pid) && pid > 0 && ownerIsDefinitelyDead(pid);
|
|
191
|
-
},
|
|
192
|
-
});
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
`shouldRemoveStaleLock` receives the exact lock snapshot that `fs-safe` inspected. `fs-safe` re-reads the sidecar and removes it only if the raw content and file identity are unchanged. If the callback is missing, returns false, or the file changed, acquisition fails closed or keeps retrying according to the normal retry policy.
|
|
179
|
+
The `"remove-if-unchanged"` value and `shouldRemoveStaleLock` callback remain accepted as deprecated compatibility inputs. They are ignored and the callback is not invoked. To recover, stop or otherwise exclude every process that can acquire the lock, remove the confirmed stale sidecar under that external authority, and retry acquisition.
|
|
196
180
|
|
|
197
181
|
## What sidecar locks defend against
|
|
198
182
|
|
|
199
183
|
- **Two processes writing the same file at once.** `acquire` serializes the critical section.
|
|
200
|
-
- **Accidentally deleting a fresh lock during stale recovery.** Stale third-party locks fail closed
|
|
184
|
+
- **Accidentally deleting a fresh lock during stale recovery.** Stale third-party locks always fail closed and are never unlinked during acquisition.
|
|
201
185
|
- **Race between simultaneous acquire attempts.** `O_CREAT | O_EXCL` ensures one wins.
|
|
202
186
|
|
|
203
187
|
## What they do **not** defend against
|
|
204
188
|
|
|
205
189
|
- **Misbehaving holders that ignore the lock.** Locks are advisory — only callers that go through `acquire` are bound.
|
|
206
|
-
- **Automatic stale lock deletion.** If a process crashes,
|
|
190
|
+
- **Automatic stale lock deletion.** If a process crashes, recover only under external authority that excludes every competing lock acquirer.
|
|
207
191
|
- **Multi-host coordination over network filesystems.** Behavior depends on the underlying filesystem's `O_EXCL` semantics; treat as best-effort.
|
|
208
192
|
|
|
209
193
|
## Common patterns
|
package/docs/writing.md
CHANGED
|
@@ -250,7 +250,7 @@ await fs.write("state.json", body); // succeeds on rclone FUSE
|
|
|
250
250
|
|
|
251
251
|
**Security note.** `verify-content-with-lock` proves that the bytes observed after rename match the requested write and prevents *cooperating* writers from interleaving. It does **not** prove that the destination still names the temp-file object, retain the Python helper's fd-relative parent pinning, or stop a same-UID process that ignores the advisory lock. Do not use this option on directories writable by untrusted same-UID processes. Strict identity verification remains the default.
|
|
252
252
|
|
|
253
|
-
Lock recovery is fail-closed. If a process crashes and leaves the root-level `.fs-safe-write-<sha256>.lock`, a later write reports the stale lock instead of deleting it based on a host-local PID.
|
|
253
|
+
Lock recovery is fail-closed. If a process crashes and leaves the root-level `.fs-safe-write-<sha256>.lock`, a later write reports the stale lock instead of deleting it based on a host-local PID. Recover only under external authority that excludes every competing writer; see [File lock](sidecar-lock.md#stale-recovery-is-fail-closed).
|
|
254
254
|
|
|
255
255
|
## See also
|
|
256
256
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/fs-safe",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -97,25 +97,12 @@
|
|
|
97
97
|
"default": "./dist/test-hooks.js"
|
|
98
98
|
}
|
|
99
99
|
},
|
|
100
|
-
"optionalDependencies": {
|
|
101
|
-
"jszip": "^3.10.1",
|
|
102
|
-
"tar": "7.5.19"
|
|
103
|
-
},
|
|
104
|
-
"devDependencies": {
|
|
105
|
-
"@types/node": "^22.20.0",
|
|
106
|
-
"@vitest/coverage-v8": "4.1.9",
|
|
107
|
-
"typescript": "^5.9.3",
|
|
108
|
-
"vite": "7.3.5",
|
|
109
|
-
"vitest": "^4.1.9"
|
|
110
|
-
},
|
|
111
|
-
"engines": {
|
|
112
|
-
"node": ">=22"
|
|
113
|
-
},
|
|
114
100
|
"scripts": {
|
|
115
101
|
"benchmark": "node scripts/benchmark.mjs",
|
|
116
102
|
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
|
|
117
103
|
"lint:file-size": "node scripts/check-file-size.mjs",
|
|
118
104
|
"lint:fs-boundary": "node scripts/check-fs-boundary-primitives.mjs",
|
|
105
|
+
"prepack": "node scripts/prepack-build.mjs",
|
|
119
106
|
"test": "vitest run",
|
|
120
107
|
"test:coverage": "vitest run --coverage",
|
|
121
108
|
"test:security": "vitest run test/fs-safe.test.ts test/read-boundary-bypass.test.ts test/write-boundary-bypass.test.ts test/additional-boundary-bypass.test.ts test/adversarial-boundary-payloads.test.ts",
|
|
@@ -127,5 +114,20 @@
|
|
|
127
114
|
"crabbox:run": "crabbox run",
|
|
128
115
|
"crabbox:stop": "crabbox stop",
|
|
129
116
|
"crabbox:warmup": "crabbox warmup"
|
|
130
|
-
}
|
|
131
|
-
|
|
117
|
+
},
|
|
118
|
+
"optionalDependencies": {
|
|
119
|
+
"jszip": "^3.10.1",
|
|
120
|
+
"tar": "7.5.20"
|
|
121
|
+
},
|
|
122
|
+
"devDependencies": {
|
|
123
|
+
"@types/node": "^26.1.1",
|
|
124
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
125
|
+
"typescript": "^7.0.2",
|
|
126
|
+
"vite": "8.1.5",
|
|
127
|
+
"vitest": "^4.1.10"
|
|
128
|
+
},
|
|
129
|
+
"engines": {
|
|
130
|
+
"node": ">=22"
|
|
131
|
+
},
|
|
132
|
+
"packageManager": "pnpm@10.34.5"
|
|
133
|
+
}
|