@openclaw/fs-safe 0.4.1 → 0.4.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +4 -2
  3. package/dist/advanced.d.ts +1 -0
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +1 -0
  6. package/dist/archive-limits.d.ts.map +1 -1
  7. package/dist/archive-staging.d.ts.map +1 -1
  8. package/dist/async-lock.d.ts.map +1 -1
  9. package/dist/bounded-read.d.ts +14 -0
  10. package/dist/bounded-read.d.ts.map +1 -0
  11. package/dist/bounded-read.js +86 -0
  12. package/dist/errors.d.ts.map +1 -1
  13. package/dist/file-store.d.ts.map +1 -1
  14. package/dist/file-store.js +5 -2
  15. package/dist/json.d.ts +8 -5
  16. package/dist/json.d.ts.map +1 -1
  17. package/dist/json.js +27 -14
  18. package/dist/pinned-python.js +10 -10
  19. package/dist/pinned-write.d.ts.map +1 -1
  20. package/dist/pinned-write.js +12 -2
  21. package/dist/read-opened-file.d.ts.map +1 -1
  22. package/dist/read-opened-file.js +4 -4
  23. package/dist/regular-file.d.ts.map +1 -1
  24. package/dist/regular-file.js +11 -52
  25. package/dist/root-context.d.ts.map +1 -1
  26. package/dist/root-path.d.ts.map +1 -1
  27. package/dist/secret-file.d.ts.map +1 -1
  28. package/dist/secret-file.js +5 -2
  29. package/dist/secure-file.d.ts.map +1 -1
  30. package/dist/secure-file.js +6 -7
  31. package/dist/sidecar-lock.d.ts +5 -1
  32. package/dist/sidecar-lock.d.ts.map +1 -1
  33. package/dist/sidecar-lock.js +3 -42
  34. package/docs/advanced.md +20 -0
  35. package/docs/config.md +2 -2
  36. package/docs/index.md +1 -1
  37. package/docs/json-store.md +2 -2
  38. package/docs/json.md +13 -5
  39. package/docs/regular-file.md +26 -32
  40. package/docs/sidecar-lock.md +8 -24
  41. package/docs/writing.md +1 -1
  42. package/package.json +19 -17
@@ -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"; // default "fail-closed"
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; `staleRecovery` then decides whether acquisition fails closed or tries caller-approved removal.
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: `remove-if-unchanged`
175
+ ## Stale recovery is fail-closed
176
176
 
177
- The default `staleRecovery: "fail-closed"` never removes third-party sidecars. Use `staleRecovery: "remove-if-unchanged"` only when your app has a reliable owner-liveness policy and can prove a stale owner cannot still be writing.
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
- ```ts
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 by default. Opt-in removal rechecks the observed snapshot before unlinking.
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, use the payload and your own supervisor/process table to decide when removal is safe, then opt into `remove-if-unchanged`.
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. Remove a stale sidecar only after proving that its holder can no longer write, using the application-owned recovery guidance in [File lock](sidecar-lock.md#stale-recovery-remove-if-unchanged).
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.1",
3
+ "version": "0.4.2",
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
+ }