@openclaw/fs-safe 0.5.5 → 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 +49 -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-identity.d.ts +1 -0
- package/dist/file-identity.d.ts.map +1 -1
- package/dist/file-identity.js +10 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +19 -7
- package/dist/move-path.js +2 -2
- 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 +24 -8
- 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/output-sibling.js +3 -3
- 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/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +23 -15
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +2 -2
- package/dist/replace-file-descriptor.d.ts +3 -3
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +2 -2
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +84 -75
- 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/sibling-temp.js +1 -1
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +4 -1
- 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/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +3 -3
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +9 -7
- 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 +13 -3
- package/docs/staged-file.md +178 -0
- package/docs/testing.md +1 -1
- package/docs/writing.md +19 -0
- package/package.json +16 -9
- 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
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type StagedFileReceipt = Readonly<{
|
|
2
|
+
directory: Readonly<{
|
|
3
|
+
path: string;
|
|
4
|
+
realPath: string;
|
|
5
|
+
identity: Readonly<{
|
|
6
|
+
dev: bigint;
|
|
7
|
+
ino: bigint;
|
|
8
|
+
}>;
|
|
9
|
+
}>;
|
|
10
|
+
temporaryBasename: string;
|
|
11
|
+
/** Preparation-time metadata of the private stage, not a final-file fingerprint. */
|
|
12
|
+
identity: Readonly<{
|
|
13
|
+
dev: bigint;
|
|
14
|
+
ino: bigint;
|
|
15
|
+
mode: number;
|
|
16
|
+
nlink: bigint;
|
|
17
|
+
size: bigint;
|
|
18
|
+
uid: number;
|
|
19
|
+
gid: number;
|
|
20
|
+
mtimeNs: bigint;
|
|
21
|
+
ctimeNs: bigint;
|
|
22
|
+
}>;
|
|
23
|
+
}>;
|
|
24
|
+
export type PublishedFileReceipt = Readonly<{
|
|
25
|
+
status: "published";
|
|
26
|
+
staged: StagedFileReceipt;
|
|
27
|
+
basename: string;
|
|
28
|
+
overwrite: boolean;
|
|
29
|
+
}>;
|
|
30
|
+
export type StagedFilePublication = Readonly<{
|
|
31
|
+
status: "not-published";
|
|
32
|
+
}> | PublishedFileReceipt | Readonly<{
|
|
33
|
+
status: "indeterminate";
|
|
34
|
+
basename: string;
|
|
35
|
+
overwrite: boolean;
|
|
36
|
+
}>;
|
|
37
|
+
export type StagedFileCleanupReceipt = Readonly<{
|
|
38
|
+
temporaryBasename: string;
|
|
39
|
+
publication: StagedFilePublication;
|
|
40
|
+
status: "removed" | "name-absent" | "preserved" | "failed" | "not-needed";
|
|
41
|
+
resources: "closed" | "close-failed";
|
|
42
|
+
}>;
|
|
43
|
+
export type StagedFileFailureDetails = Readonly<{
|
|
44
|
+
phase: "prepare" | "publish" | "cleanup";
|
|
45
|
+
publication: StagedFilePublication;
|
|
46
|
+
cleanup?: StagedFileCleanupReceipt;
|
|
47
|
+
}>;
|
|
48
|
+
export interface StagedFile extends AsyncDisposable {
|
|
49
|
+
readonly receipt: StagedFileReceipt;
|
|
50
|
+
assertCurrent(): Promise<void>;
|
|
51
|
+
publish(basename: string, options: {
|
|
52
|
+
overwrite: boolean;
|
|
53
|
+
}): Promise<PublishedFileReceipt>;
|
|
54
|
+
cleanup(): Promise<StagedFileCleanupReceipt>;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=staged-file-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staged-file-types.d.ts","sourceRoot":"","sources":["../src/staged-file-types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,SAAS,EAAE,QAAQ,CAAC;QAClB,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,QAAQ,CAAC;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAClD,CAAC,CAAC;IACH,iBAAiB,EAAE,MAAM,CAAC;IAC1B,oFAAoF;IACpF,QAAQ,EAAE,QAAQ,CAAC;QACjB,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC,CAAC;CACJ,CAAC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C,MAAM,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;CACpB,CAAC,CAAC;AAEH,MAAM,MAAM,qBAAqB,GAC7B,QAAQ,CAAC;IAAE,MAAM,EAAE,eAAe,CAAA;CAAE,CAAC,GACrC,oBAAoB,GACpB,QAAQ,CAAC;IAAE,MAAM,EAAE,eAAe,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAAC,CAAC;AAEhF,MAAM,MAAM,wBAAwB,GAAG,QAAQ,CAAC;IAC9C,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,qBAAqB,CAAC;IACnC,MAAM,EAAE,SAAS,GAAG,aAAa,GAAG,WAAW,GAAG,QAAQ,GAAG,YAAY,CAAC;IAC1E,SAAS,EAAE,QAAQ,GAAG,cAAc,CAAC;CACtC,CAAC,CAAC;AAEH,MAAM,MAAM,wBAAwB,GAAG,QAAQ,CAAC;IAC9C,KAAK,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;IACzC,WAAW,EAAE,qBAAqB,CAAC;IACnC,OAAO,CAAC,EAAE,wBAAwB,CAAC;CACpC,CAAC,CAAC;AAEH,MAAM,WAAW,UAAW,SAAQ,eAAe;IACjD,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC1F,OAAO,IAAI,OAAO,CAAC,wBAAwB,CAAC,CAAC;CAC9C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DirectoryReceipt } from "./directory-durability.js";
|
|
2
|
+
import type { StagedFile } from "./staged-file-types.js";
|
|
3
|
+
export type { PublishedFileReceipt, StagedFile, StagedFileCleanupReceipt, StagedFileFailureDetails, StagedFilePublication, StagedFileReceipt, } from "./staged-file-types.js";
|
|
4
|
+
export declare function stageFileInDirectory(options: {
|
|
5
|
+
directory: string | DirectoryReceipt;
|
|
6
|
+
content: string | Uint8Array;
|
|
7
|
+
/** Published mode; the unpublished stage stays at 0600. Defaults to 0600. */
|
|
8
|
+
mode?: number;
|
|
9
|
+
}): Promise<StagedFile>;
|
|
10
|
+
//# sourceMappingURL=staged-file.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staged-file.d.ts","sourceRoot":"","sources":["../src/staged-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAKlE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD,YAAY,EACV,oBAAoB,EAAE,UAAU,EAAE,wBAAwB,EAAE,wBAAwB,EACpF,qBAAqB,EAAE,iBAAiB,GACzC,MAAM,wBAAwB,CAAC;AAEhC,wBAAsB,oBAAoB,CAAC,OAAO,EAAE;IAClD,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAAC;IACrC,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,UAAU,CAAC,CAUtB"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
import { assertNativeStaging, createNativeStage } from "./native-staged-file.js";
|
|
3
|
+
import { requireNativeBinding } from "./native.js";
|
|
4
|
+
import { openStagedDirectory } from "./staged-directory.js";
|
|
5
|
+
export async function stageFileInDirectory(options) {
|
|
6
|
+
if (process.platform !== "linux" && process.platform !== "darwin") {
|
|
7
|
+
throw new FsSafeError("unsupported-platform", "retained-directory staging requires Linux or macOS");
|
|
8
|
+
}
|
|
9
|
+
const binding = requireNativeBinding();
|
|
10
|
+
assertNativeStaging(binding);
|
|
11
|
+
const input = { kind: "buffer", data: Buffer.from(options.content) };
|
|
12
|
+
const mode = options.mode ?? 0o600;
|
|
13
|
+
const parent = openStagedDirectory(options.directory);
|
|
14
|
+
return await createNativeStage(binding, parent.fd, parent.receipt, input, mode);
|
|
15
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BigIntStats } from "node:fs";
|
|
2
|
+
type ExactFileIdentity = Pick<BigIntStats, "dev" | "ino">;
|
|
3
|
+
export declare function inspectFileIdentity<T extends ExactFileIdentity>(inspect: () => Promise<T>, expected?: ExactFileIdentity, platform?: NodeJS.Platform): Promise<T>;
|
|
4
|
+
export declare function inspectFileIdentitySync<T extends ExactFileIdentity>(inspect: () => T, expected?: ExactFileIdentity, platform?: NodeJS.Platform): T;
|
|
5
|
+
export {};
|
|
6
|
+
//# sourceMappingURL=strict-file-identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strict-file-identity.d.ts","sourceRoot":"","sources":["../src/strict-file-identity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAG3C,KAAK,iBAAiB,GAAG,IAAI,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC;AA6B1D,wBAAsB,mBAAmB,CAAC,CAAC,SAAS,iBAAiB,EACnE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACzB,QAAQ,CAAC,EAAE,iBAAiB,EAC5B,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,OAAO,CAAC,CAAC,CAAC,CAOZ;AAED,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,iBAAiB,EACjE,OAAO,EAAE,MAAM,CAAC,EAChB,QAAQ,CAAC,EAAE,iBAAiB,EAC5B,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,CAAC,CAOH"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
function identityMismatch() {
|
|
3
|
+
return new FsSafeError("path-mismatch", "file identity changed or could not be verified");
|
|
4
|
+
}
|
|
5
|
+
function identityCheck(expected, platform) {
|
|
6
|
+
const known = {};
|
|
7
|
+
const check = (stat) => {
|
|
8
|
+
let complete = true;
|
|
9
|
+
for (const field of ["dev", "ino"]) {
|
|
10
|
+
const value = stat[field];
|
|
11
|
+
// Numeric receipts cannot recover identity bits already lost to rounding.
|
|
12
|
+
if (typeof value !== "bigint")
|
|
13
|
+
throw identityMismatch();
|
|
14
|
+
if (platform === "win32" && value === 0n) {
|
|
15
|
+
complete = false;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
if (known[field] !== undefined && known[field] !== value)
|
|
19
|
+
throw identityMismatch();
|
|
20
|
+
known[field] = value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return complete;
|
|
24
|
+
};
|
|
25
|
+
if (expected && !check(expected))
|
|
26
|
+
throw identityMismatch();
|
|
27
|
+
return check;
|
|
28
|
+
}
|
|
29
|
+
// Retry only unknown Windows identities, retaining every known component so a
|
|
30
|
+
// later observation cannot erase a definite mismatch. Never reopen the file.
|
|
31
|
+
export async function inspectFileIdentity(inspect, expected, platform = process.platform) {
|
|
32
|
+
const check = identityCheck(expected, platform);
|
|
33
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
34
|
+
const stat = await inspect();
|
|
35
|
+
if (check(stat))
|
|
36
|
+
return stat;
|
|
37
|
+
}
|
|
38
|
+
throw identityMismatch();
|
|
39
|
+
}
|
|
40
|
+
export function inspectFileIdentitySync(inspect, expected, platform = process.platform) {
|
|
41
|
+
const check = identityCheck(expected, platform);
|
|
42
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
43
|
+
const stat = inspect();
|
|
44
|
+
if (check(stat))
|
|
45
|
+
return stat;
|
|
46
|
+
}
|
|
47
|
+
throw identityMismatch();
|
|
48
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"temp-cleanup.d.ts","sourceRoot":"","sources":["../src/temp-cleanup.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"temp-cleanup.d.ts","sourceRoot":"","sources":["../src/temp-cleanup.ts"],"names":[],"mappings":"AACA,OAAO,EAA8B,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEvF,MAAM,MAAM,uBAAuB,GAAG,gBAAgB,CAAC;AAEvD,MAAM,MAAM,oBAAoB,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG;IAChD,WAAW,CAAC,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAC/C,CAAC;AAsCF,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAA;CAAE,GAC7D,oBAAoB,CAyBtB;AAED,wBAAgB,mCAAmC,IAAI,IAAI,CAE1D;AAED,wBAAgB,kCAAkC,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAYzE"}
|
package/dist/temp-cleanup.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
3
3
|
const tempCleanupEntries = new Map();
|
|
4
4
|
let cleanupRegistered = false;
|
|
5
5
|
function pathStillMatchesReceipt(entry) {
|
|
@@ -7,7 +7,7 @@ function pathStillMatchesReceipt(entry) {
|
|
|
7
7
|
return false;
|
|
8
8
|
}
|
|
9
9
|
try {
|
|
10
|
-
return
|
|
10
|
+
return sameFileIdentityForCleanup(fsSync.lstatSync(entry.path, { bigint: true }), entry.identity);
|
|
11
11
|
}
|
|
12
12
|
catch (error) {
|
|
13
13
|
return error.code === "ENOENT";
|
|
@@ -38,7 +38,7 @@ export function registerTempPathForExit(tempPath, options) {
|
|
|
38
38
|
};
|
|
39
39
|
if (!entry.identity) {
|
|
40
40
|
try {
|
|
41
|
-
entry.identity = fsSync.lstatSync(tempPath);
|
|
41
|
+
entry.identity = fsSync.lstatSync(tempPath, { bigint: true });
|
|
42
42
|
}
|
|
43
43
|
catch {
|
|
44
44
|
// Callers that register before creation set the identity after opening.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"temp-target.d.ts","sourceRoot":"","sources":["../src/temp-target.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AA0EF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAaT;AAsCD,wBAAsB,QAAQ,CAAC,MAAM,EAAE;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,GAAG,OAAO,CAAC,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"temp-target.d.ts","sourceRoot":"","sources":["../src/temp-target.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AA0EF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAaT;AAsCD,wBAAsB,QAAQ,CAAC,MAAM,EAAE;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,GAAG,OAAO,CAAC,QAAQ,CAAC,CAwBpB;AAED,wBAAsB,YAAY,CAAC,CAAC,EAClC,MAAM,EAAE;IACN,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,EACD,EAAE,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAClC,OAAO,CAAC,CAAC,CAAC,CAOZ"}
|
package/dist/temp-target.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
5
5
|
import { assertSafePathSegment, sanitizeSafePathSegment } from "./safe-path-segment.js";
|
|
6
6
|
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
7
7
|
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
@@ -92,16 +92,16 @@ function isNodeErrorWithCode(err, code) {
|
|
|
92
92
|
}
|
|
93
93
|
async function cleanupTempDir(dir, identity, onCleanupError) {
|
|
94
94
|
try {
|
|
95
|
-
const current = await lstat(dir).catch((error) => {
|
|
95
|
+
const current = await fs.lstat(dir, { bigint: true }).catch((error) => {
|
|
96
96
|
if (isNodeErrorWithCode(error, "ENOENT")) {
|
|
97
97
|
return undefined;
|
|
98
98
|
}
|
|
99
99
|
throw error;
|
|
100
100
|
});
|
|
101
|
-
if (!current || !
|
|
101
|
+
if (!current || !sameFileIdentityForCleanup(current, identity)) {
|
|
102
102
|
return;
|
|
103
103
|
}
|
|
104
|
-
await rm(dir, { recursive: true, force: true });
|
|
104
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
105
105
|
}
|
|
106
106
|
catch (err) {
|
|
107
107
|
if (!isNodeErrorWithCode(err, "ENOENT")) {
|
|
@@ -115,8 +115,10 @@ function resolveTempRoot(rootDir) {
|
|
|
115
115
|
export async function tempFile(params) {
|
|
116
116
|
const rootDir = resolveTempRoot(params.rootDir);
|
|
117
117
|
const prefix = `${sanitizePrefix(params.prefix)}-`;
|
|
118
|
-
const dir = await mkdtemp(path.join(rootDir, prefix));
|
|
119
|
-
|
|
118
|
+
const dir = await fs.mkdtemp(path.join(rootDir, prefix));
|
|
119
|
+
// Windows file indexes can exceed Number.MAX_SAFE_INTEGER. Cleanup receipts
|
|
120
|
+
// must retain the exact identity or adjacent directories can compare equal.
|
|
121
|
+
const identity = await fs.lstat(dir, { bigint: true });
|
|
120
122
|
const unregisterTempDir = registerTempPathForExit(dir, { recursive: true, identity });
|
|
121
123
|
const file = (fileName) => path.join(dir, sanitizeTempFileName(fileName ?? params.fileName ?? "download.bin"));
|
|
122
124
|
const cleanup = async () => {
|
package/dist/windows-owner.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PermissionCommandFailure } from "./permission-exec.js";
|
|
1
2
|
export type WindowsOwnerExec = (command: string, args: string[]) => Promise<{
|
|
2
3
|
stdout: string;
|
|
3
4
|
stderr: string;
|
|
@@ -10,6 +11,8 @@ export type WindowsOwnerSummary = {
|
|
|
10
11
|
remote?: boolean;
|
|
11
12
|
trusted?: boolean;
|
|
12
13
|
error?: string;
|
|
14
|
+
errorDetail?: PermissionCommandFailure;
|
|
15
|
+
errorCause?: unknown;
|
|
13
16
|
};
|
|
14
17
|
export declare function resolveWindowsPrincipalSids(params: {
|
|
15
18
|
principals: string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"windows-owner.d.ts","sourceRoot":"","sources":["../src/windows-owner.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"windows-owner.d.ts","sourceRoot":"","sources":["../src/windows-owner.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,sBAAsB,CAAC;AAG9B,MAAM,MAAM,gBAAgB,GAAG,CAC7B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,KACX,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEjD,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,wBAAwB,CAAC;IACvC,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AA4DF,wBAAsB,2BAA2B,CAAC,MAAM,EAAE;IACxD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,EAAE,gBAAgB,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAyBlC;AAED,wBAAsB,4BAA4B,CAAC,MAAM,EAAE;IACzD,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,EAAE,gBAAgB,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAWzB;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,EAAE,gBAAgB,CAAC;CACxB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAkD/B"}
|
package/dist/windows-owner.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatPermissionErrorDetail, getPermissionCommandFailure, } from "./permission-exec.js";
|
|
1
2
|
import { resolveWindowsSystemCommand } from "./windows-command.js";
|
|
2
3
|
const SID_RE = /^\*?s-\d+-\d+(-\d+)+$/i;
|
|
3
4
|
const TRUSTED_OWNER_SIDS = new Set(["s-1-5-18", "s-1-5-32-544"]);
|
|
@@ -83,8 +84,11 @@ export async function resolveWindowsCurrentUserSid(params) {
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
export async function inspectWindowsOwner(params) {
|
|
87
|
+
let command = "";
|
|
88
|
+
let startedAt = performance.now();
|
|
86
89
|
try {
|
|
87
|
-
|
|
90
|
+
command = resolveWindowsSystemCommand(String.raw `WindowsPowerShell\v1.0\powershell.exe`, params.env);
|
|
91
|
+
startedAt = performance.now();
|
|
88
92
|
const { stdout } = await params.exec(command, [
|
|
89
93
|
"-NoLogo",
|
|
90
94
|
"-NoProfile",
|
|
@@ -113,6 +117,10 @@ export async function inspectWindowsOwner(params) {
|
|
|
113
117
|
};
|
|
114
118
|
}
|
|
115
119
|
catch (err) {
|
|
116
|
-
return {
|
|
120
|
+
return {
|
|
121
|
+
error: formatPermissionErrorDetail(String(err)),
|
|
122
|
+
errorDetail: getPermissionCommandFailure(err, command, performance.now() - startedAt),
|
|
123
|
+
errorCause: err,
|
|
124
|
+
};
|
|
117
125
|
}
|
|
118
126
|
}
|
package/docs/advanced.md
CHANGED
|
@@ -26,13 +26,22 @@ The exports group into a handful of themes. Each documented helper has its own p
|
|
|
26
26
|
| Export | Page | Notes |
|
|
27
27
|
|---|---|---|
|
|
28
28
|
| `pathScope`, `PathScope`, `PathScopeOptions`, `PathScopeResolveOptions` | [path-scope.md](path-scope.md) | Absolute-path boundary helper with `Result`-shaped returns. |
|
|
29
|
-
| `ensureDirectoryWithinRoot` |
|
|
29
|
+
| `ensureDirectoryWithinRoot` | [path-scope.md](path-scope.md#ensuredir-rel-options) | Create a directory while enforcing the root boundary; same result contract as `pathScope().ensureDir()`. |
|
|
30
30
|
| `resolvePathWithinRoot`, `resolvePathsWithinRoot` | – | Resolve one or many relative paths against a trusted root. |
|
|
31
31
|
| `resolveExistingPathsWithinRoot`, `resolveStrictExistingPathsWithinRoot` | – | Same, but require the targets to exist. |
|
|
32
32
|
| `resolveWritablePathWithinRoot` | – | Resolve a write target inside a root. |
|
|
33
33
|
| `resolveRootPath`, `resolveRootPathSync`, `ResolvedRootPath`, `ROOT_PATH_ALIAS_POLICIES`, `RootPathAliasPolicy` | – | Resolve a root directory honoring alias policy. |
|
|
34
34
|
| `resolvePathViaExistingAncestorSync` | – | Walk to an existing ancestor for paths whose tail does not yet exist. |
|
|
35
35
|
|
|
36
|
+
`ensureDirectoryWithinRoot({ rootDir, requestedPath, scopeLabel, defaultDirName?, mode? })`
|
|
37
|
+
returns `{ ok: true, path }` or `{ ok: false, error: string, diagnostic?: FsSafeError }`.
|
|
38
|
+
It does not throw filesystem failures: operational failures carry a
|
|
39
|
+
`helper-failed` diagnostic with the original `cause` and a bounded, escaped
|
|
40
|
+
message naming the native code/syscall when available. Policy failures omit
|
|
41
|
+
`diagnostic`. Missing parents already created before a failure remain in place.
|
|
42
|
+
The diagnostic contract is specific to directory preparation, not the other
|
|
43
|
+
root-path result helpers.
|
|
44
|
+
|
|
36
45
|
### Absolute path helpers
|
|
37
46
|
|
|
38
47
|
| Export | Page | Notes |
|
|
@@ -107,6 +116,7 @@ component is followed by another segment, both helpers throw
|
|
|
107
116
|
|
|
108
117
|
| Export | Page | Notes |
|
|
109
118
|
|---|---|---|
|
|
119
|
+
| `stageFileInDirectory`, `StagedFile`, `StagedFileReceipt`, `PublishedFileReceipt`, `StagedFilePublication`, `StagedFileCleanupReceipt`, `StagedFileFailureDetails` | [staged-file.md](staged-file.md) | Native-required Linux/macOS lifecycle retaining the original directory for abort cleanup. |
|
|
110
120
|
| `tempFile`, `withTempFile`, `TempFile`, `buildRandomTempFilePath`, `sanitizeTempFileName` | [temp.md](temp.md) | One-file temp primitive; prefer `tempWorkspace` from `@openclaw/fs-safe/temp` for the stable surface. |
|
|
111
121
|
| `writeSiblingTempFile`, `writeViaSiblingTempPath`, `WriteSiblingTempFileOptions`, `WriteSiblingTempFileResult` | – | Sibling-temp write building block used by `replaceFileAtomic`. |
|
|
112
122
|
|
package/docs/archive.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Archive extraction
|
|
2
2
|
|
|
3
|
-
`@openclaw/fs-safe/archive` extracts ZIP and TAR archives behind one API, with traversal checks, blocked-link-type rejection, and entry-count and byte budgets. When the
|
|
3
|
+
`@openclaw/fs-safe/archive` extracts ZIP and TAR archives behind one API, with traversal checks, blocked-link-type rejection, and entry-count and byte budgets. When the native binding is available for the current platform, Rust streams ZIP, TAR, gzip, zstd, and bzip2 while TypeScript remains the sole policy owner; every accepted output is created fd-relative in a private staging root. Extraction then merges through the same safe-open boundary used by direct writes — a symlinked entry can't trick the merge into following an out-of-tree path.
|
|
4
4
|
|
|
5
5
|
The guarded JavaScript fallback uses optional runtime dependencies: `jszip` for
|
|
6
6
|
ZIP and `tar` for TAR/gzip. The native path does not use those packages. Installs
|
|
@@ -72,8 +72,19 @@ accepted-entry plan back to Rust. Rust only performs decompression and the
|
|
|
72
72
|
fd-relative `mkdirBeneath`/exclusive-open writes. This keeps policy identical
|
|
73
73
|
between native and JavaScript paths rather than reimplementing it in Rust.
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
ZIP extraction and bounded reads admit every physical central-directory record and its referenced local header before either decoder can normalize or collapse names. Raw names and valid Unicode Path names must pass traversal checks before stripping, filtering, or selecting a requested member; duplicate or colliding names reject with `entry-path`, even in unrelated or skipped members. Materially conflicting local/central or Unicode interpretations, malformed critical metadata, and ambiguous framing reject with `ArchiveFormatError`. Harmless separator and dot-component equivalence is allowed only after validation. Ordinary legacy filename decoding remains backend-selected.
|
|
76
|
+
|
|
77
|
+
`stripComponents` removes leading nonempty, non-`.` path components after
|
|
78
|
+
normalizing separators. For example, `./pkg/hello.txt` with
|
|
79
|
+
`stripComponents: 1` extracts to `hello.txt` on both backends. Entries with no
|
|
80
|
+
remaining components are skipped before the filter callback, but still count
|
|
81
|
+
toward `maxEntries` and undergo traversal validation. JavaScript TAR extraction
|
|
82
|
+
passes node-tar this accepted output path with its own stripping disabled, so
|
|
83
|
+
depth checks, collision checks, writes, and mode application agree.
|
|
84
|
+
|
|
85
|
+
An `entryFilter` sees the validated effective archive path **before stripping**
|
|
86
|
+
(including a local PAX `path` override), entry kind, and declared size.
|
|
87
|
+
Returning `"skip"` rejects the whole archive unless `onFiltered` is
|
|
77
88
|
explicitly `"skip-entry"`. Path traversal and archive-wide entry-count checks
|
|
78
89
|
still apply to skipped entries.
|
|
79
90
|
|
|
@@ -149,22 +160,58 @@ codes remain `"destination-not-directory"`, `"destination-symlink"`, and
|
|
|
149
160
|
## What it defends against
|
|
150
161
|
|
|
151
162
|
- **Path traversal:** entries with `..`, absolute paths, NUL bytes, or Windows drive-relative segments such as `C:secret` and `nested/C:secret` are rejected (`ArchiveSecurityError`). On Windows, path segments containing `:` are also rejected as alternate data stream names before either backend writes to the filesystem.
|
|
152
|
-
- **Symlink/hardlink entries:** rejected by default. Some archives ship symlink/hardlink entries that point outside the destination once resolved; `extractArchive` does not follow them.
|
|
163
|
+
- **Symlink/hardlink entries:** rejected by default, including ZIP entries whose Unix mode says symlink while their name ends in a slash or their DOS directory bit is set. An explicit `entryFilter` with `onFiltered: "skip-entry"` can omit these entries. Some archives ship symlink/hardlink entries that point outside the destination once resolved; `extractArchive` does not follow them.
|
|
153
164
|
- **Ambiguous output names:** duplicate names and distinct names that collide after `stripComponents`, case normalization, or Unicode normalization are rejected instead of relying on backend- or volume-specific overwrite order.
|
|
154
165
|
- **TOCTOU during merge:** extraction first writes to a private temp dir, then merges into `destDir` using the same boundary checks as `root().write()`. Destination symlink swaps are checked with the selected platform mechanism; non-Linux routes retain the best-effort race window documented in the [security model](security-model.md#containment-guarantees-by-platform).
|
|
155
166
|
- **Zip bombs:** `maxExtractedBytes` and `maxEntryBytes` apply to *post-decompression* bytes, so highly-compressed payloads hit the cap before they exhaust disk.
|
|
156
167
|
- **Corrupt ZIP payloads:** streamed output must match both the central-directory CRC and declared uncompressed size before it can leave private staging.
|
|
157
168
|
- **Slow-loris archives:** `timeoutMs` is a hard wall-clock budget. Extraction is aborted on overrun.
|
|
158
|
-
- **Metadata bombs:** a
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
169
|
+
- **Metadata bombs:** a streaming pass-through reader rejects oversized PAX, GNU long-name, and GNU long-link bodies before either TAR implementation buffers them. It understands octal and base-256 fixed sizes and validates bounded local PAX bodies before using their size overrides for member framing. Original archive bytes remain unchanged.
|
|
170
|
+
|
|
171
|
+
### Bounded local PAX support
|
|
172
|
+
|
|
173
|
+
Extraction and single-entry reads accept one nonempty local POSIX `x` header
|
|
174
|
+
(USTAR or GNU header format) immediately before one regular/contiguous file,
|
|
175
|
+
directory, symlink, or hardlink. `path`, `linkpath`, and `size` override that
|
|
176
|
+
member only. Effective paths still pass traversal validation before stripping,
|
|
177
|
+
then the output paths pass depth and collision checks. The filter receives the
|
|
178
|
+
effective pre-strip path, followed by link policy checks. PAX never permits link
|
|
179
|
+
creation. Effective sizes drive framing, filters, and the existing output-byte
|
|
180
|
+
budgets; `maxEntries` still counts members, not their metadata headers.
|
|
181
|
+
|
|
182
|
+
Records must have exact byte lengths, ASCII keys, a final newline, and no
|
|
183
|
+
duplicate keys, embedded newlines, or unconsumed bytes. Structural `path` and
|
|
184
|
+
`linkpath` values and ownership names must be nonempty printable ASCII. A PAX
|
|
185
|
+
member's raw name, USTAR prefix, and raw link target must also be printable
|
|
186
|
+
ASCII; raw link targets must be present only on links, even when overridden.
|
|
187
|
+
Unicode
|
|
188
|
+
PAX structural text is deliberately unsupported because the underlying parsers
|
|
189
|
+
do not agree when UTF-8 is split across input chunks. `size`, `uid`, and `gid`
|
|
190
|
+
must be canonical unsigned decimal safe integers (zero is valid; signs, leading
|
|
191
|
+
zeros, fractions, and exponents are not). Padded member sizes must also fit the
|
|
192
|
+
safe integer range. Raw and effective directory/link sizes must both be zero;
|
|
193
|
+
non-directory paths ending with a separator and `linkpath` on non-links are
|
|
194
|
+
rejected rather than allowing parser-specific type or framing changes.
|
|
195
|
+
|
|
196
|
+
The descriptive allowlist is `mtime`, `atime`, `ctime` (signed decimal seconds
|
|
197
|
+
with optional fractional digits, within JavaScript's Date range), `uid`, `gid`,
|
|
198
|
+
`uname`, and `gname`. These attributes are accepted but not restored to the
|
|
199
|
+
destination. `LIBARCHIVE.xattr.*` and `SCHILY.xattr.*` with nonempty ASCII
|
|
200
|
+
alphanumeric/dot/underscore/hyphen suffixes are also accepted as inert metadata,
|
|
201
|
+
never restored as extended attributes. Their values are byte-counted and may
|
|
202
|
+
contain NUL or non-UTF8 bytes, including macOS provenance metadata; embedded
|
|
203
|
+
newlines are rejected because they can disrupt downstream record parsing.
|
|
204
|
+
|
|
205
|
+
Global `g`, old `X`, old GNU `N`, empty/dangling/repeated local headers, mixed
|
|
206
|
+
PAX/GNU extension chains, unknown keys, charset declarations, ACL extensions,
|
|
207
|
+
and all sparse extensions (including `GNU.sparse.*`, `SCHILY.filetype`,
|
|
208
|
+
`SCHILY.realsize`, and `SCHILY.size`) fail closed with
|
|
209
|
+
`ArchiveFormatError("archive-header-invalid")`. Standalone GNU long-name `L`
|
|
210
|
+
and long-link `K` support is unchanged. GNU sparse extension blocks are still
|
|
211
|
+
metered in 512-byte units before rejection, preserving metadata-limit errors
|
|
212
|
+
for excessive chains. The per-body `maxMetaEntryBytes` limit bounds PAX storage
|
|
213
|
+
and duplicate-key state; one local header per member prevents local metadata
|
|
214
|
+
chains without introducing a new limit or changing defaults.
|
|
168
215
|
|
|
169
216
|
## `resolveArchiveKind`
|
|
170
217
|
|
|
@@ -257,7 +304,7 @@ import {
|
|
|
257
304
|
|
|
258
305
|
- `validateArchiveEntryPath(raw, opts)` — throws `ArchiveSecurityError` for `..`, absolute, NUL-containing, drive-relative, or otherwise unsafe entry paths, including alternate data stream names on Windows.
|
|
259
306
|
- `normalizeArchiveEntryPath(raw)` — converts backslashes in the entry path to forward slashes.
|
|
260
|
-
- `stripArchivePath(entryPath, n)` — strip the leading N
|
|
307
|
+
- `stripArchivePath(entryPath, n)` — normalize separators, drop empty and `.` components, then strip the leading N components, returning `null` if none remain.
|
|
261
308
|
- `resolveArchiveOutputPath({ destDir, entryPath })` — combines the entry path with the destination, after validation.
|
|
262
309
|
- `isWindowsDrivePath(value)` — detects drive-relative segments such as `C:secret` or `nested/C:secret` that should be rejected.
|
|
263
310
|
|
package/docs/config.md
CHANGED
|
@@ -78,6 +78,13 @@ configureFsSafeLocks({
|
|
|
78
78
|
});
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
+
These defaults apply to both `acquireFileLock()` / `withFileLock()` and
|
|
82
|
+
`acquireFileLockSync()` / `withFileLockSync()`. Each acquisition resolves
|
|
83
|
+
`retry`, `staleMs`, `staleRecovery`, and `timeoutMs` from the per-call option
|
|
84
|
+
first, then the process configuration, then the package default. Explicit zero
|
|
85
|
+
values are preserved. A per-call `retry` object replaces the configured object
|
|
86
|
+
as a whole; omitted retry fields use package defaults, not configured fields.
|
|
87
|
+
|
|
81
88
|
Individual lock calls can override any default. Switching the global stale
|
|
82
89
|
recovery mode does not provide the application-owned liveness proof required
|
|
83
90
|
by `shouldRemoveStaleLock`.
|
package/docs/contributing.md
CHANGED
|
@@ -46,23 +46,55 @@ pnpm check
|
|
|
46
46
|
This runs the filesystem boundary checks, build, tests, and package
|
|
47
47
|
tarball/import validation.
|
|
48
48
|
|
|
49
|
+
### Native consumer installs
|
|
50
|
+
|
|
51
|
+
After `pnpm build` and a fresh `pnpm native:build`, run `pnpm package:smoke`.
|
|
52
|
+
It packs the real root and host binding, then runs root-only npm and the
|
|
53
|
+
declared pnpm version against a disposable loopback registry. The root's exact
|
|
54
|
+
optional dependencies stay unchanged. Each consumer lives outside the workspace
|
|
55
|
+
with isolated configuration, caches, and stores; the registry never proxies to
|
|
56
|
+
the Internet. The smoke verifies root integrity, consumer-local resolution,
|
|
57
|
+
OS/CPU/libc selection, a native-required SHA-256 operation, and fresh-process
|
|
58
|
+
`auto`/`off` fallbacks and `require` failures for missing bindings and omitted
|
|
59
|
+
optionals. Omitted-optionals installs also verify that all public subpaths can
|
|
60
|
+
be imported, without implying every operation remains available.
|
|
61
|
+
|
|
62
|
+
Host-only smoke supplies the six foreign packages using their unchanged real
|
|
63
|
+
manifests and clearly marked synthetic, non-executable payloads. Every foreign
|
|
64
|
+
metadata/tarball endpoint is checked before installation, so a missing fixture
|
|
65
|
+
cannot masquerade as successful platform filtering. These temporary fixtures
|
|
66
|
+
never enter `packages/`, release artifacts, or the publish manifest. They prove
|
|
67
|
+
installer filtering, not foreign native compilation or execution. Full release
|
|
68
|
+
collection uses the actual seven collected native tarballs instead. Run it with
|
|
69
|
+
`pnpm package:collect` after assembling all seven real bindings; missing targets
|
|
70
|
+
fail collection. `pnpm package:collect --allow-host-only` exercises the same
|
|
71
|
+
lifecycle boundary locally but proves only the host. Both collection commands
|
|
72
|
+
require the pnpm lifecycle CLI path; direct `node` invocation is unsupported. Archive
|
|
73
|
+
codecs and their dependencies are packed from the installed dependency graph.
|
|
74
|
+
|
|
75
|
+
PR CI builds and executes four host targets: Linux x64 glibc, Linux x64 musl
|
|
76
|
+
(Alpine), macOS arm64, and Windows x64. The root-only smoke runs on each. The
|
|
77
|
+
seven-target source build matrix runs on release tags; packaging all seven is
|
|
78
|
+
not execution proof for every architecture. The smoke writes manager versions,
|
|
79
|
+
cases, and synthetic-fixture scope to `release-artifacts/consumer-proof.json`.
|
|
80
|
+
|
|
49
81
|
## Docs
|
|
50
82
|
|
|
51
|
-
The docs site is rendered from `docs
|
|
83
|
+
The docs site is rendered recursively from Markdown files under `docs/` by `scripts/build-docs-site.mjs`. Build locally to preview:
|
|
52
84
|
|
|
53
85
|
```bash
|
|
54
|
-
|
|
86
|
+
pnpm docs:site
|
|
55
87
|
open dist/docs-site/index.html
|
|
56
88
|
```
|
|
57
89
|
|
|
58
|
-
The build validates internal links and embedded anchors. Broken links fail the build — fix them before pushing.
|
|
90
|
+
The build validates internal links and embedded anchors. Broken links fail the build — fix them before pushing. Navigation must list every non-excluded Markdown page exactly once: missing, nonexistent, and duplicate entries fail before the build replaces existing site output. The builder and navigation tests share discovery and validation in `scripts/docs-site-navigation.mjs`.
|
|
59
91
|
|
|
60
92
|
Adding a new doc page:
|
|
61
93
|
|
|
62
94
|
1. Create `docs/<page>.md`. Use a leading `# Title` heading.
|
|
63
|
-
2. Add the page to the appropriate section in `scripts/
|
|
95
|
+
2. Add the page to the appropriate section in `scripts/docs-site-navigation.mjs` (`sections` array near the top). Nested pages use slash-separated paths relative to `docs/`, such as `guides/example.md`.
|
|
64
96
|
3. Cross-link from `docs/index.md` if it's a major surface.
|
|
65
|
-
4.
|
|
97
|
+
4. Run `pnpm test test/docs-site-navigation.test.ts` and re-run the local build.
|
|
66
98
|
|
|
67
99
|
Internal links use relative `*.md` paths — the builder rewrites them to the rendered HTML. Code fences support GitHub-flavored markdown.
|
|
68
100
|
|
|
@@ -79,8 +111,14 @@ Small, focused PRs land faster. The general shape:
|
|
|
79
111
|
|
|
80
112
|
Maintainers publish from a protected `vX.Y.Z` tag on `main` through
|
|
81
113
|
`.github/workflows/release.yml`. The workflow requires the package version and a
|
|
82
|
-
dated `CHANGELOG.md` section to match the tag
|
|
83
|
-
|
|
114
|
+
dated `CHANGELOG.md` section to match the tag. It builds and publishes all seven
|
|
115
|
+
platform packages before publishing `@openclaw/fs-safe`, verifies every registry
|
|
116
|
+
artifact and provenance statement, and then creates the GitHub release.
|
|
117
|
+
|
|
118
|
+
Each package needs its own npm trusted-publisher configuration for
|
|
119
|
+
`openclaw/fs-safe` and `release.yml`. A new platform package must be created and
|
|
120
|
+
configured on npm before the first tag that references it; npm trust is
|
|
121
|
+
package-specific and cannot be bootstrapped by the tag workflow itself.
|
|
84
122
|
|
|
85
123
|
External contributors do not need to do anything beyond getting the pull
|
|
86
124
|
request merged. Maintainers must not publish locally or add npm automation
|
package/docs/durability.md
CHANGED
|
@@ -63,6 +63,12 @@ into proof for a different directory.
|
|
|
63
63
|
|
|
64
64
|
Call `close()` in `finally`. Closing is idempotent; using a closed pin fails.
|
|
65
65
|
|
|
66
|
+
These checks intentionally reject a moved or replaced pathname. For one file's
|
|
67
|
+
abort cleanup through its original directory after a move, use the separate
|
|
68
|
+
[retained-directory staging lifecycle](staged-file.md). Its cleanup authority
|
|
69
|
+
does not weaken `pinDirectory().assertCurrent()` or `.sync()`, and namespace
|
|
70
|
+
cleanup is not proof of crash durability.
|
|
71
|
+
|
|
66
72
|
## Durable directory creation
|
|
67
73
|
|
|
68
74
|
`ensureDurableDirectory()` finds and pins the nearest existing ancestor,
|
|
@@ -204,10 +210,17 @@ try {
|
|
|
204
210
|
The result is `{ bytes, digest }`, where `digest` is lowercase hexadecimal.
|
|
205
211
|
The handle overload never closes the caller's descriptor and uses positioned
|
|
206
212
|
reads, so it does not alter the descriptor's current offset. The path overload
|
|
207
|
-
rejects symbolic links and non-regular files,
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
213
|
+
rejects symbolic links and non-regular files, compares lossless bigint identities
|
|
214
|
+
from the pre-open pathname inspection to the opened descriptor and from that
|
|
215
|
+
descriptor to the current pathname, and closes its own handle. All identity
|
|
216
|
+
checks complete before any JavaScript or native hashing. Each inspection allows
|
|
217
|
+
one bounded retry for unknown Windows identity components, retaining known
|
|
218
|
+
components and rejecting known differences immediately. Persistent unknown
|
|
219
|
+
identity fails closed with `path-mismatch`, even for benign files: this trades
|
|
220
|
+
availability for verifiable identity. Retries inspect the same descriptor or
|
|
221
|
+
pathname without reopening the file and repeat the symlink and file-type checks.
|
|
222
|
+
POSIX opens are nonblocking, so a raced FIFO or device is rejected after
|
|
223
|
+
descriptor inspection rather than waiting for a writer.
|
|
211
224
|
|
|
212
225
|
When the optional binding is active, hashing runs as an async native task and
|
|
213
226
|
does not occupy the JavaScript event loop with digest updates. With native mode
|
package/docs/errors.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Every failure that's the library's job to surface lands as an `FsSafeError` with a closed `code` union you can branch on. Catch by code, not by message text — messages may change, codes will not.
|
|
4
4
|
|
|
5
|
+
Result-based APIs are an exception to throwing: [`pathScope().ensureDir()`](path-scope.md#ensuredir-rel-options)
|
|
6
|
+
and `ensureDirectoryWithinRoot()` return operational failures as
|
|
7
|
+
`{ ok: false, error: string, diagnostic: FsSafeError }`. The diagnostic uses
|
|
8
|
+
`helper-failed` / `operational`, retains the exact native error in `cause`, and
|
|
9
|
+
has the same message as `error`. Its display text names the native code/syscall
|
|
10
|
+
when available but omits raw paths and native messages. Policy failures keep
|
|
11
|
+
the string-only result without `diagnostic`; other `pathScope` methods do not
|
|
12
|
+
gain this field. Directory preparation can partially complete before failing.
|
|
13
|
+
|
|
5
14
|
Path and archive-entry details embedded in diagnostics escape control characters
|
|
6
15
|
as `\\uXXXX` sequences. This keeps attacker-controlled names on one log line;
|
|
7
16
|
the escaped message is for diagnosis, not for reconstructing the original path.
|
|
@@ -105,7 +114,7 @@ type FsSafeErrorCode =
|
|
|
105
114
|
| `device-path` | A read/open target is a known unsafe device or process-fd path. | `/dev/zero`, `/dev/random`, `/dev/stdin`, `/dev/fd/*`, `/proc/*/fd/*`, or a Windows reserved device name. |
|
|
106
115
|
| `hardlink` | Read or copy with `hardlinks: "reject"` saw `nlink > 1`. | File is hardlinked — possibly an alias of an out-of-tree inode. |
|
|
107
116
|
| `helper-failed` | A native mechanism or multi-step operational helper failed. | Inspect `cause` and any operation-specific `details`; retrying may be unsafe if the operation partially completed. |
|
|
108
|
-
| `helper-unavailable` | Required native binding could not be loaded. | Unsupported platform, missing/incompatible
|
|
117
|
+
| `helper-unavailable` | Required native binding could not be loaded. | Unsupported platform, omitted/missing/incompatible platform package, or `FS_SAFE_NATIVE_MODE=require`. `auto` falls back where possible; `require` fails closed. |
|
|
109
118
|
| `insecure-permissions` | A secure file or path permission check found a mode/ACL that allows broader access than requested. | File or directory is group/world writable/readable; Windows ACL grants broad read. |
|
|
110
119
|
| `invalid-path` | Input was empty, contained NUL, was an unparseable URL, or otherwise unusable. | Caller didn't validate input; input was a network path on Windows, a drive-relative segment in a portable relative path or store key, or a leading drive-relative spelling such as `C:name` used as a Root destination. Existing-object Root lookups retain legal POSIX drive-like names. |
|
|
111
120
|
| `not-empty` | `remove()` on a non-empty directory. | Use `replaceDirectoryAtomic` or remove children first. |
|
|
@@ -125,6 +134,12 @@ type FsSafeErrorCode =
|
|
|
125
134
|
| `too-large` | A read or bounded walk exceeded its configured budget. | Caller gave a too-permissive file or traversal limit. |
|
|
126
135
|
| `unsupported-platform` | Reserved compatibility code for a platform-specific operation. | No current public helper emits this `FsSafeError` code. Platform-specific APIs currently return a typed unsupported result or use `helper-unavailable`; keep the union member when exhaustively switching across supported package versions. |
|
|
127
136
|
|
|
137
|
+
Pathname `sha256File()` also reports `path-mismatch` when pre-open, descriptor,
|
|
138
|
+
or current-path identity remains unknown after one bounded Windows retry, even
|
|
139
|
+
if the file is benign. It never reopens to recover identity. Preview symlinks
|
|
140
|
+
report `symlink`, preview or descriptor non-files report `not-file`, and a
|
|
141
|
+
current-path symlink or non-file reports `path-mismatch`.
|
|
142
|
+
|
|
128
143
|
## Branching
|
|
129
144
|
|
|
130
145
|
```ts
|