@openclaw/fs-safe 0.5.6 → 0.6.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.
- package/CHANGELOG.md +39 -0
- package/README.md +16 -10
- package/dist/advanced.d.ts +2 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -0
- package/dist/archive-crc32.d.ts +2 -0
- package/dist/archive-crc32.d.ts.map +1 -0
- package/dist/archive-crc32.js +14 -0
- package/dist/archive-kind.js +2 -2
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +7 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +24 -5
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +53 -6
- package/dist/archive-tar-pax.d.ts +8 -0
- package/dist/archive-tar-pax.d.ts.map +1 -0
- package/dist/archive-tar-pax.js +100 -0
- package/dist/archive-tar-runtime.d.ts +1 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-zip-admission.d.ts +7 -0
- package/dist/archive-zip-admission.d.ts.map +1 -0
- package/dist/archive-zip-admission.js +60 -0
- package/dist/archive-zip-count.d.ts +2 -0
- package/dist/archive-zip-count.d.ts.map +1 -0
- package/dist/archive-zip-count.js +140 -0
- package/dist/archive-zip-directory.d.ts +8 -0
- package/dist/archive-zip-directory.d.ts.map +1 -0
- package/dist/archive-zip-directory.js +224 -0
- package/dist/archive-zip-integrity.d.ts.map +1 -1
- package/dist/archive-zip-integrity.js +1 -14
- package/dist/archive-zip-names.d.ts +12 -0
- package/dist/archive-zip-names.d.ts.map +1 -0
- package/dist/archive-zip-names.js +99 -0
- package/dist/archive-zip-preflight.d.ts +1 -1
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +8 -145
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +32 -23
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +25 -19
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +19 -7
- package/dist/native-binding.d.ts +3 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts +4 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +22 -6
- package/dist/native-pinned-write-windows.d.ts +8 -0
- package/dist/native-pinned-write-windows.d.ts.map +1 -0
- package/dist/native-pinned-write-windows.js +92 -0
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +136 -127
- package/dist/native-staged-file.d.ts +24 -0
- package/dist/native-staged-file.d.ts.map +1 -0
- package/dist/native-staged-file.js +337 -0
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +4 -4
- package/dist/opened-realpath.d.ts +2 -0
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +12 -7
- package/dist/owner-dacl.d.ts.map +1 -1
- package/dist/owner-dacl.js +2 -1
- package/dist/permission-exec.d.ts +19 -0
- package/dist/permission-exec.d.ts.map +1 -1
- package/dist/permission-exec.js +57 -11
- package/dist/permissions-public.d.ts +1 -1
- package/dist/permissions-public.d.ts.map +1 -1
- package/dist/permissions-windows.d.ts +3 -0
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +16 -5
- package/dist/permissions.d.ts +5 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/pinned-write.d.ts +6 -0
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +19 -29
- package/dist/private-directory.d.ts.map +1 -1
- package/dist/private-directory.js +3 -2
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +78 -72
- package/dist/root-paths.d.ts +11 -14
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +36 -27
- package/dist/root-write-verification.d.ts +11 -0
- package/dist/root-write-verification.d.ts.map +1 -0
- package/dist/root-write-verification.js +91 -0
- package/dist/secret-file.d.ts +1 -6
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +43 -117
- package/dist/secret-read-async.d.ts +1 -1
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +45 -67
- package/dist/secret-read-policy.d.ts +13 -0
- package/dist/secret-read-policy.d.ts.map +1 -0
- package/dist/secret-read-policy.js +28 -0
- package/dist/secret.d.ts +2 -1
- package/dist/secret.d.ts.map +1 -1
- package/dist/secret.js +2 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +25 -16
- package/dist/staged-directory.d.ts +16 -0
- package/dist/staged-directory.d.ts.map +1 -0
- package/dist/staged-directory.js +60 -0
- package/dist/staged-file-types.d.ts +56 -0
- package/dist/staged-file-types.d.ts.map +1 -0
- package/dist/staged-file-types.js +1 -0
- package/dist/staged-file.d.ts +10 -0
- package/dist/staged-file.d.ts.map +1 -0
- package/dist/staged-file.js +15 -0
- package/dist/strict-file-identity.d.ts +6 -0
- package/dist/strict-file-identity.d.ts.map +1 -0
- package/dist/strict-file-identity.js +48 -0
- package/dist/windows-owner.d.ts +3 -0
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +10 -2
- package/docs/advanced.md +11 -1
- package/docs/archive.md +62 -15
- package/docs/config.md +7 -0
- package/docs/contributing.md +45 -7
- package/docs/durability.md +17 -4
- package/docs/errors.md +16 -1
- package/docs/index.md +3 -1
- package/docs/install.md +15 -7
- package/docs/migrating-to-0.5.md +7 -7
- package/docs/migrating-to-0.6.md +43 -0
- package/docs/native-helper.md +15 -5
- package/docs/native.md +40 -11
- package/docs/path-scope.md +28 -2
- package/docs/permissions.md +13 -2
- package/docs/root.md +1 -1
- package/docs/secret-file.md +8 -0
- package/docs/secure-file.md +17 -1
- package/docs/security-model.md +15 -1
- package/docs/sidecar-lock.md +11 -1
- package/docs/staged-file.md +178 -0
- package/docs/testing.md +1 -1
- package/docs/writing.md +19 -0
- package/package.json +15 -8
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
package/docs/security-model.md
CHANGED
|
@@ -49,10 +49,16 @@ Every path is resolved against the canonicalized real path of the root, then che
|
|
|
49
49
|
|
|
50
50
|
### Symlinks (read side)
|
|
51
51
|
|
|
52
|
-
`open()` and `read()` use `fs.open` with `O_NOFOLLOW` on POSIX where available. The library then `fstat`s the open fd and `realpath`s the original input, asserting the two refer to the same inode
|
|
52
|
+
`open()` and `read()` use `fs.open` with `O_NOFOLLOW` on POSIX where available. The library then `fstat`s the open fd and `realpath`s the original input, asserting the two refer to the same inode. A symlink swap that happens between resolve and open will fail at the identity check rather than silently following the new target.
|
|
53
53
|
|
|
54
54
|
Per-call `symlinks: "follow-within-root"` allows symlinks whose final target is still inside the root. The default is `"reject"`.
|
|
55
55
|
|
|
56
|
+
Guarded root reads compare lossless bigint identities from before open, the opened
|
|
57
|
+
descriptor, the input path, and the canonical target; numeric public `Stats`
|
|
58
|
+
receipts are not used as identity evidence. Unknown Windows device/inode values
|
|
59
|
+
receive one re-inspection without reopening the file. A definite mismatch or
|
|
60
|
+
persistent unknown identity rejects with `path-mismatch` before reading bytes.
|
|
61
|
+
|
|
56
62
|
### Symlinks (write side)
|
|
57
63
|
|
|
58
64
|
With the native binding loaded, `write()`, `create()`, and `copyIn()` use a
|
|
@@ -70,6 +76,14 @@ parent symlink. A same-privilege peer with write access to the parent can
|
|
|
70
76
|
therefore cause an out-of-root side effect before the operation throws. Use
|
|
71
77
|
native `require` mode when concurrent hostile mutation is in scope.
|
|
72
78
|
|
|
79
|
+
The separate [retained-directory staging lifecycle](staged-file.md) keeps abort
|
|
80
|
+
cleanup anchored to the original directory after a parent or ancestor move.
|
|
81
|
+
It preserves observed substituted temporary entries and never cleans a recorded
|
|
82
|
+
publication. Directory anchoring is not expected-inode/CAS replacement, and
|
|
83
|
+
identity-check-then-unlink is not atomic conditional unlink. The guarantee
|
|
84
|
+
requires the temporary name to remain owned and removal to remain permitted;
|
|
85
|
+
application authorization and cooperative coordination remain necessary.
|
|
86
|
+
|
|
73
87
|
### Hardlink aliasing
|
|
74
88
|
|
|
75
89
|
When `hardlinks: "reject"` is set, reads stat the target and refuse if `nlink > 1`, on the conservative assumption that a hardlinked file might alias an out-of-tree inode. This is defense-in-depth: the link count check is best-effort and platform-dependent. Treat it as a tripwire, not authorization.
|
package/docs/sidecar-lock.md
CHANGED
|
@@ -176,7 +176,17 @@ detection, not revocation of work already in progress.
|
|
|
176
176
|
retry, payload parsing, stale policy, owner-scoped reentrancy, guarded
|
|
177
177
|
identity-conditioned reclaim, verification, and compromise monitoring. They do
|
|
178
178
|
not use the async manager queue or support async callbacks. Retry waits block
|
|
179
|
-
the calling thread; use the async API in request-serving code.
|
|
179
|
+
the calling thread; use the async API in request-serving code. The sync
|
|
180
|
+
compromise interval treats a thrown verification I/O error as a lost lock and
|
|
181
|
+
invokes `onCompromised` once, matching the asynchronous `.catch(() => false)`
|
|
182
|
+
contract. An explicit `verifyStillHeld()` call still propagates that I/O error.
|
|
183
|
+
|
|
184
|
+
Both synchronous helpers consume the [process-wide lock defaults](config.md#configurefssafelocks-config).
|
|
185
|
+
Per-call options take precedence, including zero values; a per-call `retry`
|
|
186
|
+
object replaces the entire configured retry object. A configured
|
|
187
|
+
`staleRecovery: "remove-if-unchanged"` still needs per-call
|
|
188
|
+
`shouldRemoveStaleLock` approval, and per-call `staleRecovery: "fail-closed"`
|
|
189
|
+
disables recovery even when the process configuration opts in.
|
|
180
190
|
|
|
181
191
|
Always release in a `finally`:
|
|
182
192
|
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Retained-directory staging
|
|
3
|
+
description: "Prepare and publish one file while retaining its original directory for abort cleanup."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retained-directory staging
|
|
7
|
+
|
|
8
|
+
`stageFileInDirectory()` from `@openclaw/fs-safe/advanced` prepares one file in
|
|
9
|
+
an existing directory and keeps that directory open until cleanup or disposal.
|
|
10
|
+
It requires the native binding on Linux or macOS, even in native mode `auto`.
|
|
11
|
+
Native off, an unavailable binding, or a binding without the required mechanism
|
|
12
|
+
fails before file creation. Windows is explicitly unsupported for this API.
|
|
13
|
+
Existing root, output, temp, lock, and durability APIs retain their documented
|
|
14
|
+
platform and fallback contracts.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import type { DirectoryReceipt } from "@openclaw/fs-safe/durability";
|
|
18
|
+
import {
|
|
19
|
+
stageFileInDirectory,
|
|
20
|
+
type PublishedFileReceipt,
|
|
21
|
+
} from "@openclaw/fs-safe/advanced";
|
|
22
|
+
|
|
23
|
+
// Call while holding the application's cooperative writer lock. The caller
|
|
24
|
+
// supplies its authorization, manager, snapshot, and fingerprint checks.
|
|
25
|
+
export async function publishReviewedUpdate(options: {
|
|
26
|
+
directory: DirectoryReceipt;
|
|
27
|
+
basename: string;
|
|
28
|
+
bytes: Uint8Array;
|
|
29
|
+
assertAuthorizedAndCurrent(): Promise<void>;
|
|
30
|
+
}): Promise<PublishedFileReceipt> {
|
|
31
|
+
await options.assertAuthorizedAndCurrent();
|
|
32
|
+
await using staged = await stageFileInDirectory({
|
|
33
|
+
directory: options.directory,
|
|
34
|
+
content: options.bytes,
|
|
35
|
+
});
|
|
36
|
+
await options.assertAuthorizedAndCurrent();
|
|
37
|
+
await staged.assertCurrent();
|
|
38
|
+
return await staged.publish(options.basename, { overwrite: true });
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`await using` always disposes, including on exceptions. If both the body and
|
|
43
|
+
disposal fail, JavaScript preserves both errors in a `SuppressedError`. A
|
|
44
|
+
successful return means publication was observed and verified, not that a
|
|
45
|
+
separate application transaction committed. Application rollback and recovery
|
|
46
|
+
remain with the caller.
|
|
47
|
+
|
|
48
|
+
## API
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
function stageFileInDirectory(options: {
|
|
52
|
+
directory: string | DirectoryReceipt;
|
|
53
|
+
content: string | Uint8Array;
|
|
54
|
+
mode?: number;
|
|
55
|
+
}): Promise<StagedFile>;
|
|
56
|
+
|
|
57
|
+
interface StagedFile extends AsyncDisposable {
|
|
58
|
+
readonly receipt: StagedFileReceipt;
|
|
59
|
+
assertCurrent(): Promise<void>;
|
|
60
|
+
publish(basename: string, options: { overwrite: boolean }): Promise<PublishedFileReceipt>;
|
|
61
|
+
cleanup(): Promise<StagedFileCleanupReceipt>;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Strings are UTF-8. `mode` is the requested **published** mode and defaults to
|
|
66
|
+
`0600`; exact final modes, including `000`, are supported. The unpublished file
|
|
67
|
+
stays at `0600` throughout preparation and any awaited application checks.
|
|
68
|
+
After rename succeeds and the published entry passes identity validation, the
|
|
69
|
+
owner applies the requested mode through its retained file descriptor and
|
|
70
|
+
synchronizes the file. No parent is created or chmodded.
|
|
71
|
+
Creation uses an exclusive, no-follow, close-on-exec open of a generated direct
|
|
72
|
+
child name. Writes use that descriptor. Inspection uses non-following metadata
|
|
73
|
+
operations, never a potentially blocking reopen of the leaf.
|
|
74
|
+
|
|
75
|
+
A supplied directory receipt must still match at admission. Its numeric
|
|
76
|
+
identity must be exactly representable; ambiguous identity fails closed.
|
|
77
|
+
Returned receipts are frozen descriptive snapshots, not mutable authority.
|
|
78
|
+
Changing a supplied receipt after admission cannot retarget the lifecycle.
|
|
79
|
+
|
|
80
|
+
`StagedFileReceipt` contains `directory` (`path`, `realPath`, and exact bigint
|
|
81
|
+
`identity.dev`/`identity.ino`), `temporaryBasename`, and the prepared file's
|
|
82
|
+
`identity` (`dev`, `ino`, `size`, `nlink`, `mtimeNs`, `ctimeNs` as bigint;
|
|
83
|
+
`mode`, `uid`, `gid` as numbers). This is a preparation-time snapshot of the
|
|
84
|
+
private stage: its mode is `0600`. Publication does not refresh this snapshot;
|
|
85
|
+
neither its mode nor its timestamps are a final-file fingerprint. No raw
|
|
86
|
+
descriptors are exposed.
|
|
87
|
+
|
|
88
|
+
`assertCurrent()` verifies the original pathname's directory identity and the
|
|
89
|
+
staged name against the retained file. A failed check does not disable cleanup.
|
|
90
|
+
After successful publication there is no staged name to check, so further
|
|
91
|
+
checks or publication reject. Cleanup closes the lifecycle; later checks and
|
|
92
|
+
publication reject before descriptor use. Rejected publication still carries its
|
|
93
|
+
phase, cause, and recorded publication outcome after closure. Concurrent calls
|
|
94
|
+
are serialized in invocation order, including cleanup and disposal. Repeated
|
|
95
|
+
cleanup returns the recorded outcome, or repeats the recorded error, without
|
|
96
|
+
touching descriptors.
|
|
97
|
+
|
|
98
|
+
## Publication and failure evidence
|
|
99
|
+
|
|
100
|
+
`publish()` requires an explicit boolean `overwrite` and one direct-child
|
|
101
|
+
basename. Empty, dot, dotdot, separators, absolute paths, NUL, control characters,
|
|
102
|
+
drive-relative spellings, and the stage's own name are rejected.
|
|
103
|
+
|
|
104
|
+
With `overwrite: false`, publication is genuine kernel no-replace rename; a
|
|
105
|
+
collision leaves both names unchanged and raises `FsSafeError("already-exists")`.
|
|
106
|
+
The stage may then be cleaned or published under another name. With
|
|
107
|
+
`overwrite: true`, publication is plain atomic replacement. Neither route
|
|
108
|
+
copies. Both source and destination resolve through the retained original
|
|
109
|
+
parent, with checks immediately before rename and after publication.
|
|
110
|
+
|
|
111
|
+
`PublishedFileReceipt` has `status: "published"`, `staged`, `basename`, and
|
|
112
|
+
`overwrite`. Its `staged` field retains the private preparation snapshot, not
|
|
113
|
+
final metadata. Errors from publication carry typed `StagedFileFailureDetails`
|
|
114
|
+
in `FsSafeError.details`, including `phase` and `publication`. Publication is
|
|
115
|
+
`not-published`, `published` (with its receipt), or `indeterminate` (with the
|
|
116
|
+
attempted basename and overwrite policy) if a rename error cannot establish
|
|
117
|
+
whether it committed. The underlying error remains in `cause`.
|
|
118
|
+
|
|
119
|
+
A parent move after the final pre-check cannot divert the rename to a
|
|
120
|
+
replacement parent. It can publish inside the moved original and then fail
|
|
121
|
+
post-validation. Successful rename is recorded before those checks; cleanup
|
|
122
|
+
never deletes or rolls back a published final name. Indeterminate publication
|
|
123
|
+
also preserves names for caller-directed recovery.
|
|
124
|
+
|
|
125
|
+
The same rule applies if applying the published mode, synchronizing, or a later
|
|
126
|
+
check fails: the error reports `published`, and cleanup preserves the final
|
|
127
|
+
name. The file may still have mode `0600` or may already have the requested
|
|
128
|
+
mode, depending on which operation failed. A `published` failure receipt records
|
|
129
|
+
rename success, not successful permission finalization.
|
|
130
|
+
|
|
131
|
+
## Cleanup guarantee and limits
|
|
132
|
+
|
|
133
|
+
If the temporary basename still names the object created by this lifecycle and
|
|
134
|
+
filesystem removal remains permitted, moving or replacing the actual parent
|
|
135
|
+
(or an ancestor) does not strand the unpublished temp. Cleanup resolves through
|
|
136
|
+
the retained original directory, even after `assertCurrent()` rejects drift.
|
|
137
|
+
Same-name sentinels in the replacement parent are not touched.
|
|
138
|
+
|
|
139
|
+
`StagedFileCleanupReceipt` records `temporaryBasename`, `publication`,
|
|
140
|
+
`resources` (`closed` or `close-failed`), and `status`:
|
|
141
|
+
|
|
142
|
+
| Status | Meaning |
|
|
143
|
+
|---|---|
|
|
144
|
+
| `removed` | The owned recorded name was unlinked through the retained directory. |
|
|
145
|
+
| `name-absent` | That name was absent in the original directory; this does not prove the inode has no other names. |
|
|
146
|
+
| `preserved` | An observed substitute or an indeterminate publication was left alone. |
|
|
147
|
+
| `failed` | Inspection or removal failed. |
|
|
148
|
+
| `not-needed` | Publication was recorded, or preparation failed before creating a file. |
|
|
149
|
+
|
|
150
|
+
Explicit cleanup returns preservation outcomes. Removal or close failures throw
|
|
151
|
+
an `FsSafeError` with the receipt in `details.cleanup` and underlying errors in
|
|
152
|
+
`cause`. Disposal also throws for preservation, so ignoring its return value
|
|
153
|
+
cannot hide incomplete cleanup. Setup failures preserve the original error;
|
|
154
|
+
when cleanup also fails, an aggregate cause retains both failures. Descriptors
|
|
155
|
+
are closed on every cleanup outcome, with no retry through recycled numbers.
|
|
156
|
+
|
|
157
|
+
This is **directory-relative targeting**, not expected-destination-inode/CAS
|
|
158
|
+
publication. Checking an identity before rename does not make rename CAS.
|
|
159
|
+
Likewise, the identity check followed by `unlinkat` is not an atomic conditional
|
|
160
|
+
unlink: an adversary can replace the leaf in that final syscall gap. Observed
|
|
161
|
+
substitutions are preserved, but this API does not guarantee recovery after
|
|
162
|
+
arbitrary child renames, permission revocation, I/O failure, or process death.
|
|
163
|
+
Keep application authorization, manager checks, snapshots, fingerprints,
|
|
164
|
+
cooperative locks, and conditional rollback.
|
|
165
|
+
|
|
166
|
+
Namespace cleanup is not crash durability. Staging retains the native writer's
|
|
167
|
+
file synchronization behavior, and publication synchronizes its directory
|
|
168
|
+
(with the existing `EPERM` exception); cleanup does not promise a durable unlink.
|
|
169
|
+
No successful receipt promises survival across a crash. See [Directory
|
|
170
|
+
durability](durability.md) when an application needs a separate durability proof.
|
|
171
|
+
`pinDirectory().assertCurrent()` and `.sync()` still require a current pathname;
|
|
172
|
+
they do not gain this cleanup authority.
|
|
173
|
+
|
|
174
|
+
## Related pages
|
|
175
|
+
|
|
176
|
+
- [Advanced composition](advanced.md)
|
|
177
|
+
- [Native architecture](native.md)
|
|
178
|
+
- [Security model](security-model.md)
|
package/docs/testing.md
CHANGED
|
@@ -17,7 +17,7 @@ The double-underscore prefix is a deliberate "hands off" signal: production code
|
|
|
17
17
|
## When to reach for hooks
|
|
18
18
|
|
|
19
19
|
- Reproduce a TOCTOU race deterministically: simulate a symlink swap between resolve and open, or between write and rename.
|
|
20
|
-
- Force guarded JavaScript behavior without removing
|
|
20
|
+
- Force guarded JavaScript behavior without removing platform packages from your runners.
|
|
21
21
|
- Inject latency to test cancellation/timeout paths.
|
|
22
22
|
|
|
23
23
|
If you don't need to inject a race, you don't need hooks — most tests should drive the library through normal calls and assert on observable behavior.
|
package/docs/writing.md
CHANGED
|
@@ -33,6 +33,10 @@ old-or-new guarantee does not apply to `append()` or `openWritable()`, which
|
|
|
33
33
|
write in place, or to lower-level atomic helpers when their explicitly
|
|
34
34
|
non-atomic permission-error copy fallback is enabled.
|
|
35
35
|
|
|
36
|
+
Post-publication verification can still reject after a complete replacement has
|
|
37
|
+
been committed. Rejection does not promise that a successful rename was rolled
|
|
38
|
+
back; the published file or a raced replacement may remain at the destination.
|
|
39
|
+
|
|
36
40
|
## Denying mutations
|
|
37
41
|
|
|
38
42
|
All mutation verbs accept `denyMutations?: DenyMutationPolicy`, either as a root default or per-call option:
|
|
@@ -64,6 +68,21 @@ await fs.write("notes/today.txt", "hello\n", { encoding: "utf8" });
|
|
|
64
68
|
|
|
65
69
|
`data` accepts `string | Buffer`. `options` are `{ denyMutations?: DenyMutationPolicy; encoding?: BufferEncoding; mkdir?: boolean; mode?: number; overwrite?: boolean; renameIdentity?: RenameIdentityPolicy }`. `mode` sets the file's POSIX mode. If neither the call nor `RootDefaults` supplies it, a replacement preserves the existing file mode and a new file uses `0o600`. `mkdir` and `overwrite` both default to `true`; set `overwrite: false` for the same no-clobber behavior as `create()`.
|
|
66
70
|
|
|
71
|
+
POSIX modes without read permission, including `0o000` and `0o200`, succeed:
|
|
72
|
+
final verification uses a descriptor retained by the writer rather than reopening
|
|
73
|
+
the published file. The requested mode is not relaxed for verification.
|
|
74
|
+
Publication verification compares exact bigint descriptor and pathname identities,
|
|
75
|
+
including large file indexes that cannot be represented by a JavaScript number.
|
|
76
|
+
Later reads still obey OS permissions, and access checks on a pre-existing
|
|
77
|
+
destination are unchanged. The explicit FUSE compatibility policy still requires
|
|
78
|
+
a readable destination to prove matching content when rename changes its identity.
|
|
79
|
+
|
|
80
|
+
When Windows cannot report a pathname's identity, the publication verifier reopens
|
|
81
|
+
the name and compares that descriptor's exact identity with the original retained
|
|
82
|
+
file, without reading bytes. It also rechecks links and root/parent containment.
|
|
83
|
+
This write-specific proof does not relax ordinary reads: unknown pathname metadata
|
|
84
|
+
alone is never proof that the name still refers to the expected file.
|
|
85
|
+
|
|
67
86
|
### `fs.create(rel, data, options?)`
|
|
68
87
|
|
|
69
88
|
Don't-clobber variant of `write()`. Throws `already-exists` if the target is there.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/fs-safe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"filesystem",
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
"dist/**/*.js",
|
|
28
28
|
"dist/**/*.d.ts",
|
|
29
29
|
"dist/**/*.d.ts.map",
|
|
30
|
-
"dist/native/**/*.node",
|
|
31
30
|
"docs/**/*.md",
|
|
32
31
|
"docs/assets/readme-banner.jpg",
|
|
33
32
|
"README.md",
|
|
@@ -139,6 +138,7 @@
|
|
|
139
138
|
"native:test": "cargo test --manifest-path native/Cargo.toml",
|
|
140
139
|
"pack:check": "pnpm build && node scripts/check-pack.mjs",
|
|
141
140
|
"public-api:update": "pnpm build && node scripts/check-pack.mjs --update-public-api",
|
|
141
|
+
"package:collect": "node scripts/check-release-packages.mjs --output release-artifacts",
|
|
142
142
|
"package:smoke": "node scripts/check-release-packages.mjs --allow-host-only --output release-artifacts",
|
|
143
143
|
"check:changed": "pnpm run check",
|
|
144
144
|
"release:notes": "node scripts/release-notes.mjs",
|
|
@@ -149,25 +149,32 @@
|
|
|
149
149
|
"crabbox:warmup": "crabbox warmup"
|
|
150
150
|
},
|
|
151
151
|
"optionalDependencies": {
|
|
152
|
+
"@openclaw/fs-safe-darwin-arm64": "0.6.0",
|
|
153
|
+
"@openclaw/fs-safe-darwin-x64": "0.6.0",
|
|
154
|
+
"@openclaw/fs-safe-linux-arm64-gnu": "0.6.0",
|
|
155
|
+
"@openclaw/fs-safe-linux-arm64-musl": "0.6.0",
|
|
156
|
+
"@openclaw/fs-safe-linux-x64-gnu": "0.6.0",
|
|
157
|
+
"@openclaw/fs-safe-linux-x64-musl": "0.6.0",
|
|
158
|
+
"@openclaw/fs-safe-win32-x64-msvc": "0.6.0",
|
|
152
159
|
"jszip": "^3.10.1",
|
|
153
160
|
"tar": "7.5.22"
|
|
154
161
|
},
|
|
155
162
|
"devDependencies": {
|
|
156
|
-
"@emnapi/runtime": "2.0.0-alpha.
|
|
163
|
+
"@emnapi/runtime": "2.0.0-alpha.4",
|
|
157
164
|
"@napi-rs/cli": "3.8.6",
|
|
158
|
-
"@types/node": "^26.
|
|
159
|
-
"@vitest/coverage-v8": "4.1.
|
|
165
|
+
"@types/node": "^26.3.0",
|
|
166
|
+
"@vitest/coverage-v8": "4.1.11",
|
|
160
167
|
"fast-check": "^4.9.0",
|
|
161
168
|
"istanbul-lib-coverage": "3.2.2",
|
|
162
169
|
"istanbul-lib-report": "3.0.1",
|
|
163
170
|
"istanbul-reports": "3.2.0",
|
|
164
171
|
"sigstore": "5.0.0",
|
|
165
172
|
"typescript": "^7.0.2",
|
|
166
|
-
"vite": "8.2.
|
|
167
|
-
"vitest": "^4.1.
|
|
173
|
+
"vite": "8.2.2",
|
|
174
|
+
"vitest": "^4.1.11"
|
|
168
175
|
},
|
|
169
176
|
"engines": {
|
|
170
177
|
"node": ">=22"
|
|
171
178
|
},
|
|
172
|
-
"packageManager": "pnpm@
|
|
179
|
+
"packageManager": "pnpm@11.24.0"
|
|
173
180
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|