@openclaw/fs-safe 0.1.0 → 0.1.1
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 +7 -0
- package/README.md +3 -2
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +51 -7
- package/dist/archive.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1 - 2026-05-06
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- Preserve the caller's destination path spelling during staged archive merges so symlink-rebind checks catch alias races on macOS.
|
|
8
|
+
- Reject archive writes that gain a hardlink alias during post-write verification and clean up the destination file.
|
|
9
|
+
|
|
3
10
|
## 0.1.0 - 2026-05-06
|
|
4
11
|
|
|
5
12
|
### Features
|
package/README.md
CHANGED
|
@@ -241,8 +241,9 @@ const opened = await media.open("inbound/photo.jpg");
|
|
|
241
241
|
await media.pruneExpired({ ttlMs: 10 * 60 * 1000, recursive: true });
|
|
242
242
|
```
|
|
243
243
|
|
|
244
|
-
`tempWorkspace()`
|
|
245
|
-
single-file scratch workflows without hand-rolled path joins
|
|
244
|
+
`tempWorkspace()` exposes `write()`, `writeText()`, `writeJson()`, `copyIn()`, and `read()` for
|
|
245
|
+
single-file scratch workflows without hand-rolled path joins, plus a `store: FileStore` view of
|
|
246
|
+
the workspace dir for the richer cases (`writeStream`, `readJsonIfExists`, `store.json<T>(rel)`).
|
|
246
247
|
|
|
247
248
|
`tempFile()` is the smaller one-file temp helper. It is intentionally an
|
|
248
249
|
advanced primitive: use `tempWorkspace()` for the stable temp surface and reach
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-staging.d.ts","sourceRoot":"","sources":["../src/archive-staging.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"archive-staging.d.ts","sourceRoot":"","sources":["../src/archive-staging.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,wBAAwB,GAChC,2BAA2B,GAC3B,qBAAqB,GACrB,+BAA+B,CAAC;AAEpC,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,IAAI,EAAE,wBAAwB,CAAC;gBAEnB,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY;CAKpF;AASD,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAYnF;AA6CD,wBAAsB,wBAAwB,CAAC,MAAM,EAAE;IACrD,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBhB;AAqDD,wBAAsB,4BAA4B,CAAC,CAAC,EAAE,MAAM,EAAE;IAC5D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,GAAG,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;CACzC,GAAG,OAAO,CAAC,CAAC,CAAC,CAkBb;AAED,wBAAsB,iCAAiC,CAAC,MAAM,EAAE;IAC9D,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;CAC5B,GAAG,OAAO,CAAC,IAAI,CAAC,CAyEhB;AAED,wBAAgB,kCAAkC,CAAC,YAAY,EAAE,MAAM,GAAG,oBAAoB,CAE7F"}
|
package/dist/archive-staging.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { FsSafeError } from "./errors.js";
|
|
3
4
|
import { root } from "./root.js";
|
|
4
5
|
import { isNotFoundPathError, isPathInside } from "./path.js";
|
|
5
6
|
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
@@ -95,6 +96,32 @@ async function applyStagedEntryMode(params) {
|
|
|
95
96
|
await fs.chmod(destinationPath, params.mode).catch(() => undefined);
|
|
96
97
|
}
|
|
97
98
|
}
|
|
99
|
+
async function assertExtractedFileHasNoHardlinkAlias(params) {
|
|
100
|
+
const destinationPath = path.join(params.destinationRealDir, params.relPath);
|
|
101
|
+
await assertResolvedInsideDestination({
|
|
102
|
+
destinationRealDir: params.destinationRealDir,
|
|
103
|
+
targetPath: destinationPath,
|
|
104
|
+
originalPath: params.originalPath,
|
|
105
|
+
});
|
|
106
|
+
const stat = await fs.lstat(destinationPath);
|
|
107
|
+
if (stat.isFile() && stat.nlink > 1) {
|
|
108
|
+
throw symlinkTraversalError(params.originalPath);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function removeExtractedDestinationFile(params) {
|
|
112
|
+
const destinationPath = path.join(params.destinationRealDir, params.relPath);
|
|
113
|
+
let resolved;
|
|
114
|
+
try {
|
|
115
|
+
resolved = await fs.realpath(destinationPath);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (!isPathInside(params.destinationRealDir, resolved)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
await fs.rm(destinationPath, { force: true }).catch(() => undefined);
|
|
124
|
+
}
|
|
98
125
|
export async function withStagedArchiveDestination(params) {
|
|
99
126
|
const stagingRoot = resolveSecureTempRoot({
|
|
100
127
|
fallbackPrefix: "fs-safe-archive",
|
|
@@ -155,13 +182,30 @@ export async function mergeExtractedTreeIntoDestination(params) {
|
|
|
155
182
|
originalPath,
|
|
156
183
|
isDirectory: false,
|
|
157
184
|
});
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
185
|
+
try {
|
|
186
|
+
await targetRoot.copyIn(relPath, sourcePath, { mkdir: true });
|
|
187
|
+
await assertExtractedFileHasNoHardlinkAlias({
|
|
188
|
+
destinationRealDir: params.destinationRealDir,
|
|
189
|
+
relPath,
|
|
190
|
+
originalPath,
|
|
191
|
+
});
|
|
192
|
+
await applyStagedEntryMode({
|
|
193
|
+
destinationRealDir: params.destinationRealDir,
|
|
194
|
+
relPath,
|
|
195
|
+
mode: sourceStat.mode & 0o777,
|
|
196
|
+
originalPath,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
await removeExtractedDestinationFile({
|
|
201
|
+
destinationRealDir: params.destinationRealDir,
|
|
202
|
+
relPath,
|
|
203
|
+
});
|
|
204
|
+
if (err instanceof FsSafeError && (err.code === "hardlink" || err.code === "path-alias")) {
|
|
205
|
+
throw symlinkTraversalError(originalPath);
|
|
206
|
+
}
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
165
209
|
}
|
|
166
210
|
};
|
|
167
211
|
await walk(params.sourceDir);
|
package/dist/archive.js
CHANGED
|
@@ -168,7 +168,7 @@ async function extractZip(params) {
|
|
|
168
168
|
}
|
|
169
169
|
await mergeExtractedTreeIntoDestination({
|
|
170
170
|
sourceDir: stagingRealDir,
|
|
171
|
-
destinationDir:
|
|
171
|
+
destinationDir: params.destDir,
|
|
172
172
|
destinationRealDir,
|
|
173
173
|
});
|
|
174
174
|
},
|
|
@@ -223,7 +223,7 @@ export async function extractArchive(params) {
|
|
|
223
223
|
});
|
|
224
224
|
await mergeExtractedTreeIntoDestination({
|
|
225
225
|
sourceDir: stagingDir,
|
|
226
|
-
destinationDir:
|
|
226
|
+
destinationDir: params.destDir,
|
|
227
227
|
destinationRealDir,
|
|
228
228
|
});
|
|
229
229
|
},
|