@openclaw/fs-safe 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +2 -2
- package/dist/advanced.d.ts +2 -2
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +2 -2
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +5 -0
- package/dist/archive-gzip-tail.d.ts +18 -0
- package/dist/archive-gzip-tail.d.ts.map +1 -0
- package/dist/archive-gzip-tail.js +74 -0
- package/dist/archive-kind.d.ts +1 -0
- package/dist/archive-kind.d.ts.map +1 -1
- package/dist/archive-kind.js +6 -0
- package/dist/archive-limits.d.ts +0 -1
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +0 -3
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +5 -8
- package/dist/archive-parser-errors.d.ts +2 -0
- package/dist/archive-parser-errors.d.ts.map +1 -0
- package/dist/archive-parser-errors.js +12 -0
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +25 -73
- package/dist/archive-tar-stream.d.ts +18 -0
- package/dist/archive-tar-stream.d.ts.map +1 -0
- package/dist/archive-tar-stream.js +95 -0
- package/dist/archive-tar-wasm.d.ts +18 -0
- package/dist/archive-tar-wasm.d.ts.map +1 -0
- package/dist/archive-tar-wasm.js +99 -0
- package/dist/archive-tar.d.ts +0 -1
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +0 -22
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +44 -114
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/atomic.js +1 -1
- package/dist/bounded-read-stream.d.ts +1 -5
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +5 -13
- package/dist/byte-budget.d.ts +0 -1
- package/dist/byte-budget.d.ts.map +1 -1
- package/dist/byte-budget.js +1 -1
- package/dist/device-path.d.ts +1 -0
- package/dist/device-path.d.ts.map +1 -1
- package/dist/device-path.js +5 -2
- package/dist/directory-mode-owner.d.ts.map +1 -1
- package/dist/directory-mode-owner.js +5 -0
- package/dist/error-detail.d.ts +1 -0
- package/dist/error-detail.d.ts.map +1 -1
- package/dist/error-detail.js +6 -0
- package/dist/file-store-boundary.d.ts +1 -1
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +2 -13
- package/dist/file-store.js +1 -1
- package/dist/file-sync.d.ts +5 -0
- package/dist/file-sync.d.ts.map +1 -0
- package/dist/file-sync.js +19 -0
- package/dist/guarded-mutation.d.ts +3 -0
- package/dist/guarded-mutation.d.ts.map +1 -1
- package/dist/guarded-mutation.js +5 -1
- package/dist/home-dir.d.ts +0 -10
- package/dist/home-dir.d.ts.map +1 -1
- package/dist/home-dir.js +0 -28
- package/dist/json-durable-queue-ownership.d.ts +1 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +139 -18
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +5 -10
- package/dist/json-store.d.ts.map +1 -1
- package/dist/json-store.js +1 -1
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +2 -3
- package/dist/move-path-cleanup.d.ts +1 -1
- package/dist/move-path-cleanup.d.ts.map +1 -1
- package/dist/move-path-cleanup.js +4 -2
- package/dist/move-path.d.ts +9 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +53 -16
- package/dist/native-operations.d.ts +0 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +0 -10
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +5 -4
- package/dist/native-staged-file.d.ts +8 -1
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +30 -9
- package/dist/opened-realpath.d.ts +12 -1
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +8 -7
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +26 -2
- package/dist/path-policy.js +1 -1
- package/dist/permissions.d.ts +1 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +3 -0
- package/dist/pinned-write.d.ts +1 -2
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +2 -11
- package/dist/private-temp-workspace.js +1 -1
- package/dist/publish-file.js +2 -2
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +2 -7
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +5 -17
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +2 -7
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +21 -8
- package/dist/root-errors.d.ts +1 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +9 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +17 -28
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +12 -33
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +49 -94
- package/dist/root-write-mode.d.ts +6 -0
- package/dist/root-write-mode.d.ts.map +1 -0
- package/dist/root-write-mode.js +60 -0
- package/dist/root-write-verification.js +2 -2
- package/dist/safe-path-segment.d.ts +1 -0
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +1 -1
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +4 -9
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +5 -10
- package/dist/secret-read-policy.d.ts +4 -0
- package/dist/secret-read-policy.d.ts.map +1 -1
- package/dist/secret-read-policy.js +12 -0
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +2 -7
- package/dist/sidecar-lock-policy.d.ts +0 -1
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +0 -4
- package/dist/string-coerce.d.ts +0 -9
- package/dist/string-coerce.d.ts.map +1 -1
- package/dist/string-coerce.js +0 -56
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +1 -12
- package/docs/archive.md +66 -61
- package/docs/atomic.md +53 -19
- package/docs/contributing.md +35 -1
- package/docs/install.md +1 -1
- package/docs/native-helper.md +5 -0
- package/docs/native.md +16 -10
- package/docs/secret-file.md +5 -0
- package/docs/staged-file.md +6 -2
- package/docs/writing.md +5 -0
- package/package.json +19 -16
- package/dist/archive-tar-admission.d.ts +0 -7
- package/dist/archive-tar-admission.d.ts.map +0 -1
- package/dist/archive-tar-admission.js +0 -43
- package/dist/archive-tar-gnu.d.ts +0 -2
- package/dist/archive-tar-gnu.d.ts.map +0 -1
- package/dist/archive-tar-gnu.js +0 -20
- package/dist/archive-tar-header.d.ts +0 -8
- package/dist/archive-tar-header.d.ts.map +0 -1
- package/dist/archive-tar-header.js +0 -47
- package/dist/archive-tar-meta.d.ts +0 -34
- package/dist/archive-tar-meta.d.ts.map +0 -1
- package/dist/archive-tar-meta.js +0 -277
- package/dist/archive-tar-pax.d.ts +0 -8
- package/dist/archive-tar-pax.d.ts.map +0 -1
- package/dist/archive-tar-pax.js +0 -100
- package/dist/archive-tar-runtime.d.ts +0 -49
- package/dist/archive-tar-runtime.d.ts.map +0 -1
- package/dist/archive-tar-runtime.js +0 -22
- package/dist/fsync.d.ts +0 -2
- package/dist/fsync.d.ts.map +0 -1
- package/dist/fsync.js +0 -1
- package/dist/json-durable-queue-retirement.d.ts +0 -9
- package/dist/json-durable-queue-retirement.d.ts.map +0 -1
- package/dist/json-durable-queue-retirement.js +0 -126
- package/dist/json-durable-queue-transfer-lock.d.ts +0 -2
- package/dist/json-durable-queue-transfer-lock.d.ts.map +0 -1
- package/dist/json-durable-queue-transfer-lock.js +0 -19
- package/dist/mode.d.ts +0 -2
- package/dist/mode.d.ts.map +0 -1
- package/dist/mode.js +0 -3
- package/dist/output-sibling.d.ts +0 -8
- package/dist/output-sibling.d.ts.map +0 -1
- package/dist/output-sibling.js +0 -30
- package/dist/read-error.d.ts +0 -2
- package/dist/read-error.d.ts.map +0 -1
- package/dist/read-error.js +0 -11
- package/dist/short-path.d.ts +0 -2
- package/dist/short-path.d.ts.map +0 -1
- package/dist/short-path.js +0 -7
- package/dist/staged-file.d.ts +0 -10
- package/dist/staged-file.d.ts.map +0 -1
- package/dist/staged-file.js +0 -15
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { readFileHandleBounded } from "./bounded-read.js";
|
|
4
|
-
import {
|
|
4
|
+
import { assertNoUnsafeDeviceReadPath } from "./device-path.js";
|
|
5
5
|
import { FsSafeError } from "./errors.js";
|
|
6
|
-
import { resolveHomeRelativePath } from "./home-dir.js";
|
|
7
6
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
8
7
|
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
9
|
-
import { assertSecretFilePreview,
|
|
8
|
+
import { assertSecretFilePreview, resolveSecretReadPolicy, secretPathErrorCode, secretReadError, trimSecretFileContent, } from "./secret-read-policy.js";
|
|
10
9
|
export async function readSecretFile(filePath, label, options = {}) {
|
|
11
|
-
const resolvedPath =
|
|
12
|
-
if (!resolvedPath) {
|
|
13
|
-
throw new FsSafeError("invalid-path", `${label} file path is empty.`, { cause: undefined });
|
|
14
|
-
}
|
|
15
|
-
const maxBytes = normalizeMaxBytes(options.maxBytes, {
|
|
16
|
-
defaultValue: DEFAULT_SECRET_FILE_MAX_BYTES,
|
|
17
|
-
});
|
|
10
|
+
const { resolvedPath, maxBytes } = resolveSecretReadPolicy(filePath, label, options);
|
|
18
11
|
async function inspectInput(symlinkMessage) {
|
|
19
12
|
const stat = options.rejectSymlink
|
|
20
13
|
? await fs.lstat(resolvedPath, { bigint: true })
|
|
@@ -26,6 +19,7 @@ export async function readSecretFile(filePath, label, options = {}) {
|
|
|
26
19
|
}
|
|
27
20
|
let previewStat;
|
|
28
21
|
try {
|
|
22
|
+
assertNoUnsafeDeviceReadPath(resolvedPath);
|
|
29
23
|
previewStat = await inspectFileIdentity(() => inspectInput(`${label} file at ${resolvedPath} must not be a symlink.`));
|
|
30
24
|
}
|
|
31
25
|
catch (error) {
|
|
@@ -36,6 +30,7 @@ export async function readSecretFile(filePath, label, options = {}) {
|
|
|
36
30
|
let raw;
|
|
37
31
|
try {
|
|
38
32
|
const realPath = await fs.realpath(resolvedPath);
|
|
33
|
+
assertNoUnsafeDeviceReadPath(realPath);
|
|
39
34
|
handle = await fs.open(realPath, resolveReadOpenFlags());
|
|
40
35
|
const openedHandle = handle;
|
|
41
36
|
const openedStat = await inspectFileIdentity(async () => {
|
|
@@ -10,4 +10,8 @@ export declare function secretPathErrorCode(error: unknown): FsSafeErrorCode;
|
|
|
10
10
|
export declare function secretReadError(code: FsSafeErrorCode, action: "inspect" | "read", label: string, resolvedPath: string, error: unknown): FsSafeError;
|
|
11
11
|
export declare function assertSecretFilePreview(stat: BigIntStats, label: string, resolvedPath: string, maxBytes: number, rejectHardlinks: boolean): void;
|
|
12
12
|
export declare function trimSecretFileContent(raw: string, label: string, resolvedPath: string): string;
|
|
13
|
+
export declare function resolveSecretReadPolicy(filePath: string, label: string, options: SecretFileReadOptions): {
|
|
14
|
+
resolvedPath: string;
|
|
15
|
+
maxBytes: number;
|
|
16
|
+
};
|
|
13
17
|
//# sourceMappingURL=secret-read-policy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-read-policy.d.ts","sourceRoot":"","sources":["../src/secret-read-policy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"secret-read-policy.d.ts","sourceRoot":"","sources":["../src/secret-read-policy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAEhE,eAAO,MAAM,6BAA6B,QAAY,CAAC;AAEvD,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAGnE;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,SAAS,GAAG,MAAM,EAC1B,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,OAAO,GACb,WAAW,CAGb;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,OAAO,GACvB,IAAI,CAUN;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAM9F;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG;IACxG,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB,CASA"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
2
|
+
import { resolveHomeRelativePath } from "./home-dir.js";
|
|
1
3
|
import { FsSafeError } from "./errors.js";
|
|
2
4
|
export const DEFAULT_SECRET_FILE_MAX_BYTES = 16 * 1024;
|
|
3
5
|
export function secretPathErrorCode(error) {
|
|
@@ -26,3 +28,13 @@ export function trimSecretFileContent(raw, label, resolvedPath) {
|
|
|
26
28
|
}
|
|
27
29
|
return secret;
|
|
28
30
|
}
|
|
31
|
+
export function resolveSecretReadPolicy(filePath, label, options) {
|
|
32
|
+
const resolvedPath = resolveHomeRelativePath(filePath.trim());
|
|
33
|
+
if (!resolvedPath) {
|
|
34
|
+
throw new FsSafeError("invalid-path", `${label} file path is empty.`, { cause: undefined });
|
|
35
|
+
}
|
|
36
|
+
const maxBytes = normalizeMaxBytes(options.maxBytes, {
|
|
37
|
+
defaultValue: DEFAULT_SECRET_FILE_MAX_BYTES,
|
|
38
|
+
});
|
|
39
|
+
return { resolvedPath, maxBytes };
|
|
40
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sibling-staged-file.d.ts","sourceRoot":"","sources":["../src/sibling-staged-file.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sibling-staged-file.d.ts","sourceRoot":"","sources":["../src/sibling-staged-file.ts"],"names":[],"mappings":"AAmCA,wBAAsB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,gBAAgB,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;CACxB,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,CAAA;CAAE,CAAC,CAyG3C"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { syncFileBestEffort } from "./file-sync.js";
|
|
1
2
|
import fsSync, {} from "node:fs";
|
|
2
3
|
import fs, {} from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
@@ -92,13 +93,7 @@ export async function writeCallbackSibling(params) {
|
|
|
92
93
|
}
|
|
93
94
|
if (params.syncTempFile) {
|
|
94
95
|
await assertCurrent(params.tempPath);
|
|
95
|
-
|
|
96
|
-
await handle.sync();
|
|
97
|
-
}
|
|
98
|
-
catch (error) {
|
|
99
|
-
if (error?.code !== "EPERM")
|
|
100
|
-
throw error;
|
|
101
|
-
}
|
|
96
|
+
await syncFileBestEffort(handle);
|
|
102
97
|
}
|
|
103
98
|
await assertCurrent(params.tempPath);
|
|
104
99
|
await fs.rename(params.tempPath, filePath);
|
|
@@ -4,7 +4,6 @@ export declare function validateSidecarLockTimeoutMs(timeoutMs: number | undefin
|
|
|
4
4
|
export declare function computeSidecarLockDelayMs(retry: SidecarLockRetryOptions, attempt: number): number;
|
|
5
5
|
export declare const maxTransientLockDenials = 8;
|
|
6
6
|
export declare function isTransientLockFileDenial(error: unknown, lockPath: string): boolean;
|
|
7
|
-
export declare function sidecarLockPayloadIsStale(payload: unknown, staleMs: number, nowMs: number): boolean;
|
|
8
7
|
export declare function sidecarLockPayloadCreatedAtMs(payload: unknown): number | null;
|
|
9
8
|
export declare function defaultSidecarLockShouldReclaim(params: {
|
|
10
9
|
lockPath: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock-policy.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAQvE,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAkBpF;AAED,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAGhF;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,uBAAuB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAQjG;AASD,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGnF;AAED,wBAAgB,
|
|
1
|
+
{"version":3,"file":"sidecar-lock-policy.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAQvE,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAkBpF;AAED,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAGhF;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,uBAAuB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAQjG;AASD,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGnF;AAED,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAU7E;AAED,wBAAsB,+BAA+B,CAAC,MAAM,EAAE;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,OAAO,CAAC,CAQnB"}
|
|
@@ -48,10 +48,6 @@ export function isTransientLockFileDenial(error, lockPath) {
|
|
|
48
48
|
const denial = error;
|
|
49
49
|
return process.platform === "win32" && denial?.code === "EPERM" && denial.path === lockPath;
|
|
50
50
|
}
|
|
51
|
-
export function sidecarLockPayloadIsStale(payload, staleMs, nowMs) {
|
|
52
|
-
const createdAtMs = sidecarLockPayloadCreatedAtMs(payload);
|
|
53
|
-
return createdAtMs !== null && nowMs - createdAtMs > staleMs;
|
|
54
|
-
}
|
|
55
51
|
export function sidecarLockPayloadCreatedAtMs(payload) {
|
|
56
52
|
const createdAt = payload &&
|
|
57
53
|
typeof payload === "object" &&
|
package/dist/string-coerce.d.ts
CHANGED
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
export declare function readStringValue(value: unknown): string | undefined;
|
|
2
1
|
export declare function normalizeNullableString(value: unknown): string | null;
|
|
3
2
|
export declare function normalizeOptionalString(value: unknown): string | undefined;
|
|
4
|
-
export declare function normalizeStringifiedOptionalString(value: unknown): string | undefined;
|
|
5
3
|
export declare function normalizeOptionalLowercaseString(value: unknown): string | undefined;
|
|
6
4
|
export declare function normalizeLowercaseStringOrEmpty(value: unknown): string;
|
|
7
|
-
export declare function normalizeFastMode(raw?: string | boolean | null): boolean | undefined;
|
|
8
|
-
export declare function lowercasePreservingWhitespace(value: string): string;
|
|
9
|
-
export declare function localeLowercasePreservingWhitespace(value: string): string;
|
|
10
|
-
export declare function resolvePrimaryStringValue(value: unknown): string | undefined;
|
|
11
|
-
export declare function normalizeOptionalThreadValue(value: unknown): string | number | undefined;
|
|
12
|
-
export declare function normalizeOptionalStringifiedId(value: unknown): string | undefined;
|
|
13
|
-
export declare function hasNonEmptyString(value: unknown): value is string;
|
|
14
5
|
//# sourceMappingURL=string-coerce.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"string-coerce.d.ts","sourceRoot":"","sources":["../src/string-coerce.ts"],"names":[],"mappings":"AAAA,wBAAgB,
|
|
1
|
+
{"version":3,"file":"string-coerce.d.ts","sourceRoot":"","sources":["../src/string-coerce.ts"],"names":[],"mappings":"AAAA,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAMrE;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAE1E;AAED,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAEnF;AAED,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEtE"}
|
package/dist/string-coerce.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
export function readStringValue(value) {
|
|
2
|
-
return typeof value === "string" ? value : undefined;
|
|
3
|
-
}
|
|
4
1
|
export function normalizeNullableString(value) {
|
|
5
2
|
if (typeof value !== "string") {
|
|
6
3
|
return null;
|
|
@@ -11,62 +8,9 @@ export function normalizeNullableString(value) {
|
|
|
11
8
|
export function normalizeOptionalString(value) {
|
|
12
9
|
return normalizeNullableString(value) ?? undefined;
|
|
13
10
|
}
|
|
14
|
-
export function normalizeStringifiedOptionalString(value) {
|
|
15
|
-
if (typeof value === "string") {
|
|
16
|
-
return normalizeOptionalString(value);
|
|
17
|
-
}
|
|
18
|
-
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
19
|
-
return normalizeOptionalString(String(value));
|
|
20
|
-
}
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
23
11
|
export function normalizeOptionalLowercaseString(value) {
|
|
24
12
|
return normalizeOptionalString(value)?.toLowerCase();
|
|
25
13
|
}
|
|
26
14
|
export function normalizeLowercaseStringOrEmpty(value) {
|
|
27
15
|
return normalizeOptionalLowercaseString(value) ?? "";
|
|
28
16
|
}
|
|
29
|
-
export function normalizeFastMode(raw) {
|
|
30
|
-
if (typeof raw === "boolean") {
|
|
31
|
-
return raw;
|
|
32
|
-
}
|
|
33
|
-
if (!raw) {
|
|
34
|
-
return undefined;
|
|
35
|
-
}
|
|
36
|
-
const key = normalizeLowercaseStringOrEmpty(raw);
|
|
37
|
-
if (["off", "false", "no", "0", "disable", "disabled", "normal"].includes(key)) {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
if (["on", "true", "yes", "1", "enable", "enabled", "fast"].includes(key)) {
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
export function lowercasePreservingWhitespace(value) {
|
|
46
|
-
return value.toLowerCase();
|
|
47
|
-
}
|
|
48
|
-
export function localeLowercasePreservingWhitespace(value) {
|
|
49
|
-
return value.toLocaleLowerCase();
|
|
50
|
-
}
|
|
51
|
-
export function resolvePrimaryStringValue(value) {
|
|
52
|
-
if (typeof value === "string") {
|
|
53
|
-
return normalizeOptionalString(value);
|
|
54
|
-
}
|
|
55
|
-
if (!value || typeof value !== "object") {
|
|
56
|
-
return undefined;
|
|
57
|
-
}
|
|
58
|
-
return normalizeOptionalString(value.primary);
|
|
59
|
-
}
|
|
60
|
-
export function normalizeOptionalThreadValue(value) {
|
|
61
|
-
if (typeof value === "number") {
|
|
62
|
-
return Number.isFinite(value) ? Math.trunc(value) : undefined;
|
|
63
|
-
}
|
|
64
|
-
return normalizeOptionalString(value);
|
|
65
|
-
}
|
|
66
|
-
export function normalizeOptionalStringifiedId(value) {
|
|
67
|
-
const normalized = normalizeOptionalThreadValue(value);
|
|
68
|
-
return normalized == null ? undefined : String(normalized);
|
|
69
|
-
}
|
|
70
|
-
export function hasNonEmptyString(value) {
|
|
71
|
-
return normalizeOptionalString(value) !== undefined;
|
|
72
|
-
}
|
|
@@ -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;
|
|
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;AA8DF,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
|
@@ -2,7 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
5
|
-
import { assertSafePathSegment, sanitizeSafePathSegment } from "./safe-path-segment.js";
|
|
5
|
+
import { assertSafePathSegment, sanitizeSafePathSegment, trimHyphenEdges } from "./safe-path-segment.js";
|
|
6
6
|
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
7
7
|
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
8
8
|
const HYPHEN_CHAR_CODE = 0x2d;
|
|
@@ -14,17 +14,6 @@ const UPPERCASE_Z_CHAR_CODE = 0x5a;
|
|
|
14
14
|
const UNDERSCORE_CHAR_CODE = 0x5f;
|
|
15
15
|
const LOWERCASE_A_CHAR_CODE = 0x61;
|
|
16
16
|
const LOWERCASE_Z_CHAR_CODE = 0x7a;
|
|
17
|
-
function trimHyphenEdges(value) {
|
|
18
|
-
let start = 0;
|
|
19
|
-
let end = value.length;
|
|
20
|
-
while (start < end && value.charCodeAt(start) === HYPHEN_CHAR_CODE) {
|
|
21
|
-
start += 1;
|
|
22
|
-
}
|
|
23
|
-
while (end > start && value.charCodeAt(end - 1) === HYPHEN_CHAR_CODE) {
|
|
24
|
-
end -= 1;
|
|
25
|
-
}
|
|
26
|
-
return start === 0 && end === value.length ? value : value.slice(start, end);
|
|
27
|
-
}
|
|
28
17
|
function isExtensionCharCode(charCode) {
|
|
29
18
|
return ((charCode >= NUMBER_ZERO_CHAR_CODE && charCode <= NUMBER_NINE_CHAR_CODE) ||
|
|
30
19
|
(charCode >= UPPERCASE_A_CHAR_CODE && charCode <= UPPERCASE_Z_CHAR_CODE) ||
|
package/docs/archive.md
CHANGED
|
@@ -2,15 +2,12 @@
|
|
|
2
2
|
|
|
3
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
|
-
|
|
6
|
-
|
|
7
|
-
that
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
(`--no-optional`, `--omit=optional`, or equivalent). If an archive helper throws
|
|
12
|
-
that an optional archive dependency is not installed, install `jszip` and/or
|
|
13
|
-
`tar` explicitly in the consuming package.
|
|
5
|
+
TAR admission uses one Rust core compiled into both the native binding and a
|
|
6
|
+
bundled, import-free WebAssembly module. The guarded JavaScript fallback uses
|
|
7
|
+
that module for TAR/gzip and optional `jszip` for ZIP. TAR needs no optional
|
|
8
|
+
parser dependency, runtime download, install script, or consumer Rust toolchain.
|
|
9
|
+
Installs omitting optional dependencies can import every public subpath and use
|
|
10
|
+
TAR/gzip in `auto` or `off`; ZIP fallback still requires `jszip`.
|
|
14
11
|
|
|
15
12
|
```ts
|
|
16
13
|
import { extractArchive, resolveArchiveKind } from "@openclaw/fs-safe/archive";
|
|
@@ -68,12 +65,12 @@ directories; ZIP UNIX creator records with zero attributes are explicit zero,
|
|
|
68
65
|
while non-UNIX ZIP records use the absent-metadata defaults.
|
|
69
66
|
|
|
70
67
|
TAR mode fields containing only NUL/ASCII-space padding use absent defaults.
|
|
71
|
-
|
|
72
|
-
within
|
|
73
|
-
Malformed or unsupported mode
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
68
|
+
Both backends recognize GNU binary modes, including signed values,
|
|
69
|
+
within JavaScript's safe-integer range before masking permission bits.
|
|
70
|
+
Malformed or unsupported mode fields consistently fall back to zero, matching
|
|
71
|
+
the former native behavior. This replaces JavaScript's decoder-dependent octal
|
|
72
|
+
prefix parsing, defaulting, or rejection for malformed fields. Ordinary octal,
|
|
73
|
+
absent, explicit zero, and supported GNU binary fields retain their behavior.
|
|
77
74
|
|
|
78
75
|
Final modes remain separate from private working staging permissions: files
|
|
79
76
|
stay `0o600` and directories `0o700` until publication. Files receive their final
|
|
@@ -112,8 +109,8 @@ normalizing separators. For example, `./pkg/hello.txt` with
|
|
|
112
109
|
`stripComponents: 1` extracts to `hello.txt` on both backends. Entries with no
|
|
113
110
|
remaining components are skipped before the filter callback, but still count
|
|
114
111
|
toward `maxEntries` and undergo traversal validation. JavaScript TAR extraction
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
copies the admitted payload range to this accepted output path, so depth checks,
|
|
113
|
+
collision checks, writes, and mode application agree.
|
|
117
114
|
|
|
118
115
|
An `entryFilter` sees the validated **canonical effective archive path before
|
|
119
116
|
stripping**, entry kind, and declared size. On every JavaScript and native
|
|
@@ -127,6 +124,12 @@ Unicode Path names use the same canonicalization.
|
|
|
127
124
|
Raw paths undergo traversal, absolute/drive-path, and NUL validation **before**
|
|
128
125
|
canonicalization; normalization cannot turn an unsafe path into an accepted
|
|
129
126
|
one. Stripping and output collision checks use this same canonical identity.
|
|
127
|
+
On Windows, archive admission also rejects reserved device segments such as
|
|
128
|
+
`NUL`, `CON.txt`, and `nul .txt` with `ArchiveSecurityError("entry-path")`,
|
|
129
|
+
before extraction or bounded member reads. Ignored trailing spaces in the stem
|
|
130
|
+
before an extension do not bypass this check. Ordinary members with these names
|
|
131
|
+
remain valid on POSIX; this is a host-specific device guard, not a portable-name
|
|
132
|
+
restriction.
|
|
130
133
|
Filters that compare exact strings should use canonical pre-strip paths,
|
|
131
134
|
including directory names without a trailing `/`.
|
|
132
135
|
Returning `"skip"` rejects the whole archive unless `onFiltered` is
|
|
@@ -162,11 +165,10 @@ If skipping was not explicitly part of the restore contract, omit
|
|
|
162
165
|
`onFiltered`; the first `"skip"` then rejects the complete archive with
|
|
163
166
|
`ArchiveSecurityError("entry-filtered")`.
|
|
164
167
|
|
|
165
|
-
Both TAR
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
instead of leaving a paused parser to drain indefinitely.
|
|
168
|
+
Both TAR routes finish bounded admission before TypeScript policy evaluation,
|
|
169
|
+
so a rejected plan never starts extraction. The JavaScript path owns its input,
|
|
170
|
+
decoder, and WASM parser streams, joining their teardown on validation, write,
|
|
171
|
+
or timeout failure.
|
|
170
172
|
|
|
171
173
|
TAR character devices, block devices, and FIFOs are presented to the filter as
|
|
172
174
|
`kind: "other"`. Accepted entries of these types reject with
|
|
@@ -183,8 +185,7 @@ and output collision checks in physical order. Each remaining record reaches
|
|
|
183
185
|
`entryFilter` once with its canonical pre-strip path, `kind: "other"`, and
|
|
184
186
|
declared effective size. A filter skip rejects with `"entry-filtered"` unless
|
|
185
187
|
`onFiltered: "skip-entry"` is explicit. Accepted unsupported records are safely
|
|
186
|
-
omitted and do not consume output payload budgets.
|
|
187
|
-
underlying TAR parser suppresses the record. GNU long names describe one such
|
|
188
|
+
omitted and do not consume output payload budgets. The shared core admits these records explicitly. GNU long names describe one such
|
|
188
189
|
record and are then cleared; local PAX on unsupported types and GNU sparse
|
|
189
190
|
`S` records retain their existing fail-closed format policy.
|
|
190
191
|
|
|
@@ -219,7 +220,7 @@ type ArchiveExtractLimits = {
|
|
|
219
220
|
};
|
|
220
221
|
```
|
|
221
222
|
|
|
222
|
-
Defaults exist for each (`DEFAULT_MAX_ARCHIVE_BYTES_ZIP`, `DEFAULT_MAX_ENTRIES`, `DEFAULT_MAX_EXTRACTED_BYTES`, `DEFAULT_MAX_ENTRY_BYTES`, `DEFAULT_MAX_META_ENTRY_BYTES`, `DEFAULT_MAX_ENTRY_PATH_COMPONENTS`). An explicit zero remains zero rather than selecting the default. `maxEntries` counts every archive entry, including entries removed by `stripComponents` or an explicit filter. The path-component default is 256. It is evaluated after `stripComponents` and before TypeScript accepts an entry for either JavaScript or native extraction, so rejected entries cannot cause implicit parent-directory creation. The 1 MiB metadata default
|
|
223
|
+
Defaults exist for each (`DEFAULT_MAX_ARCHIVE_BYTES_ZIP`, `DEFAULT_MAX_ENTRIES`, `DEFAULT_MAX_EXTRACTED_BYTES`, `DEFAULT_MAX_ENTRY_BYTES`, `DEFAULT_MAX_META_ENTRY_BYTES`, `DEFAULT_MAX_ENTRY_PATH_COMPONENTS`). An explicit zero remains zero rather than selecting the default. `maxEntries` counts every archive entry, including entries removed by `stripComponents` or an explicit filter. The path-component default is 256. It is evaluated after `stripComponents` and before TypeScript accepts an entry for either JavaScript or native extraction, so rejected entries cannot cause implicit parent-directory creation. The same resolved 1 MiB metadata default applies to the native and WASM core.
|
|
223
224
|
|
|
224
225
|
A limit violation throws `ArchiveLimitError`. Its constant and string code are:
|
|
225
226
|
|
|
@@ -262,17 +263,17 @@ codes remain `"destination-not-directory"`, `"destination-symlink"`, and
|
|
|
262
263
|
- **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).
|
|
263
264
|
- **Zip bombs:** `maxExtractedBytes` and `maxEntryBytes` apply to *post-decompression* bytes, so highly-compressed payloads hit the cap before they exhaust disk.
|
|
264
265
|
- **Corrupt ZIP payloads:** streamed output must match both the central-directory CRC and declared uncompressed size before it can leave private staging.
|
|
265
|
-
- **
|
|
266
|
+
- **Gzip container integrity:** every concatenated gzip member must have a complete valid header, body, CRC32, and ISIZE trailer. A completed member may be followed by all-zero compressed-container padding (including system-tar stdout padding), bounded by the original archive-byte limit. The padding must remain zero through physical EOF; nonzero bytes or another member after padding reject. Truncation and corruption reject before publication or selected bytes return on both backends. Compressed padding is separate from decoded TAR EOF and does not bypass its checks.
|
|
266
267
|
- **Slow-loris archives:** `timeoutMs` is a hard wall-clock budget for non-mutating work. Extraction is aborted on overrun; if a destination mutation is already in flight, that mutation and rollback are joined before rejection so archive-controlled publication cannot continue afterward.
|
|
267
|
-
- **Metadata bombs:** a streaming pass-through reader rejects oversized PAX, GNU long-name, and GNU long-link bodies before
|
|
268
|
+
- **Metadata bombs:** a streaming pass-through reader rejects oversized PAX, GNU long-name, and GNU long-link bodies before buffering their bodies. 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.
|
|
268
269
|
|
|
269
270
|
### Raw TAR framing
|
|
270
271
|
|
|
271
272
|
Extraction and bounded reads admit the complete decoded TAR stream through the
|
|
272
|
-
|
|
273
|
+
shared Rust core. This applies to plain TAR,
|
|
273
274
|
gzip, and native-supported zstd/bzip2, without changing native-mode availability
|
|
274
|
-
or fallback policy. The
|
|
275
|
-
framing rules
|
|
275
|
+
or fallback policy. The native and WASM builds enforce the same
|
|
276
|
+
framing rules:
|
|
276
277
|
|
|
277
278
|
- Every nonzero header must have a valid unsigned octal checksum, delimited
|
|
278
279
|
within its field. Checksum validation precedes metadata allocation and member
|
|
@@ -301,22 +302,28 @@ Missing linknames on links and nonempty linknames on non-links still use the
|
|
|
301
302
|
format error. PAX `x` and GNU long-name/long-link `L`/`K` payloads retain their
|
|
302
303
|
existing support and metadata limits; the zero-body rule is not applied to all
|
|
303
304
|
non-regular types.
|
|
304
|
-
PAX effective sizes
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
field's NUL terminator reject
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
305
|
+
PAX effective sizes determine regular-member framing. Admission preserves input
|
|
306
|
+
bytes and emits an ordered manifest with exact effective names, types, modes,
|
|
307
|
+
sizes, and decoded payload offsets. TypeScript owns filtering, stripping,
|
|
308
|
+
collisions, permissions, and accepted-output limits. Executors replay admitted
|
|
309
|
+
ranges from the immutable staged input; no second TAR parser interprets PAX,
|
|
310
|
+
GNU names, or payload lengths. Native writes remain descriptor-relative;
|
|
311
|
+
JavaScript writes use the shared guarded private staging and pinned-write helpers.
|
|
312
|
+
|
|
313
|
+
Original member names and USTAR prefixes are validated even when overridden.
|
|
314
|
+
Non-padding bytes after a fixed path field's NUL terminator reject. The core
|
|
315
|
+
enforces the 255-byte component ceiling under NFC and NFD, including Hangul
|
|
316
|
+
expansion. Every replay drains and validates physical EOF before publication or
|
|
317
|
+
returning selected bytes. Unrequested, filtered, and stripped members cannot
|
|
318
|
+
bypass validation. Decompression remains streaming; no complete decoded archive
|
|
319
|
+
is retained in memory or written to a decoded spool.
|
|
320
|
+
|
|
321
|
+
The WASM transport has a fixed 64 KiB input buffer, one pending member event,
|
|
322
|
+
and a 256 MiB maximum linear memory per isolated parser instance. Metadata is
|
|
323
|
+
bounded before allocation; allocation failure rejects. Stream backpressure
|
|
324
|
+
bounds queued chunks, and completion/error destroys the instance's parser
|
|
325
|
+
state. The manifest retains the existing charged budget below; linear memory
|
|
326
|
+
is an additional execution resource bound, not a new public limit option.
|
|
320
327
|
|
|
321
328
|
The raw meter enforces `maxEntries` before consuming each logical member's body,
|
|
322
329
|
including members later skipped by filtering or stripping. PAX/GNU metadata
|
|
@@ -343,9 +350,7 @@ archive overhead with safe addition. Ordinary limits, including
|
|
|
343
350
|
zero and the existing defaulting/rounding rules, retain their behavior.
|
|
344
351
|
There is no new public option. This is an absolute decoded admission
|
|
345
352
|
cap, not a decompression-ratio policy; bounded stream/codec read-ahead remains.
|
|
346
|
-
|
|
347
|
-
independent ratio threshold so it cannot reject data that the native backend
|
|
348
|
-
accepts within the same absolute limits.
|
|
353
|
+
There is no independent TAR parser decompression-ratio threshold.
|
|
349
354
|
|
|
350
355
|
### Bounded local PAX support
|
|
351
356
|
|
|
@@ -359,13 +364,14 @@ permits link creation. Effective sizes drive framing, filters, and the existing
|
|
|
359
364
|
budgets; `maxEntries` still counts members, not their metadata headers.
|
|
360
365
|
|
|
361
366
|
Records must have exact byte lengths, ASCII keys, a final newline, and no
|
|
362
|
-
duplicate keys
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
367
|
+
duplicate keys or unconsumed bytes. `path` and `linkpath` must be nonempty strict
|
|
368
|
+
UTF-8 without NUL. Unicode, a leading BOM, numeric-looking names, and embedded
|
|
369
|
+
newlines preserve their exact spelling; newlines inside a byte-counted value
|
|
370
|
+
are data. Windows filesystem filename restrictions still apply during creation.
|
|
371
|
+
Ownership names retain the existing nonempty printable-ASCII contract. Raw name,
|
|
372
|
+
USTAR prefix, and link fields still require strict UTF-8 and NUL padding even
|
|
373
|
+
when metadata overrides them. Raw link targets must be present only on links.
|
|
374
|
+
`size`, `uid`, and `gid`
|
|
369
375
|
must be canonical unsigned decimal safe integers (zero is valid; signs, leading
|
|
370
376
|
zeros, fractions, and exponents are not). Padded member sizes must also fit the
|
|
371
377
|
safe integer range. Raw and effective directory/link sizes must both be zero;
|
|
@@ -378,8 +384,7 @@ with optional fractional digits, within JavaScript's Date range), `uid`, `gid`,
|
|
|
378
384
|
destination. `LIBARCHIVE.xattr.*` and `SCHILY.xattr.*` with nonempty ASCII
|
|
379
385
|
alphanumeric/dot/underscore/hyphen suffixes are also accepted as inert metadata,
|
|
380
386
|
never restored as extended attributes. Their values are byte-counted and may
|
|
381
|
-
contain NUL
|
|
382
|
-
newlines are rejected because they can disrupt downstream record parsing.
|
|
387
|
+
contain NUL, non-UTF8 bytes, or newlines, including macOS provenance metadata.
|
|
383
388
|
|
|
384
389
|
Global `g`, old `X`, old GNU `N`, empty/dangling/repeated local headers, mixed
|
|
385
390
|
PAX/GNU extension chains, unknown keys, charset declarations, ACL extensions,
|
|
@@ -393,11 +398,11 @@ chains without introducing a new limit or changing defaults.
|
|
|
393
398
|
|
|
394
399
|
### Bounded GNU long names and links
|
|
395
400
|
|
|
396
|
-
|
|
397
|
-
`maxMetaEntryBytes
|
|
401
|
+
The shared core buffers GNU long-name `L` and long-link `K` bodies within
|
|
402
|
+
`maxMetaEntryBytes`. A body must contain a nonempty
|
|
398
403
|
UTF-8 name, with either no NUL or exactly one terminal NUL. Embedded NULs,
|
|
399
404
|
additional terminal NULs, bytes after a NUL, and invalid UTF-8 reject with
|
|
400
|
-
`ArchiveFormatError("archive-header-invalid")`. The
|
|
405
|
+
`ArchiveFormatError("archive-header-invalid")`. The core preserves original
|
|
401
406
|
archive bytes, including the optional terminator and block padding.
|
|
402
407
|
|
|
403
408
|
One logical member may have at most one `L` and one `K`, in either order.
|
package/docs/atomic.md
CHANGED
|
@@ -173,7 +173,8 @@ metadata where lower latency matters more than crash-durability.
|
|
|
173
173
|
|
|
174
174
|
## `movePathWithCopyFallback`
|
|
175
175
|
|
|
176
|
-
Rename a path. If the rename fails with `EXDEV` (cross-device),
|
|
176
|
+
Rename a path. If the rename fails with `EXDEV` (cross-device), or `EPERM` on
|
|
177
|
+
Windows, fall back to
|
|
177
178
|
copying into a staged sibling path, renaming that staged path into place, and
|
|
178
179
|
then removing only the source entries that were copied. The fallback avoids
|
|
179
180
|
buffering regular files into memory and does not tighten the destination parent
|
|
@@ -215,39 +216,72 @@ identity becomes the next cleanup receipt. This accounts for the operation's
|
|
|
215
216
|
own link-count and ctime changes without suppressing unexpected external
|
|
216
217
|
mutations.
|
|
217
218
|
|
|
218
|
-
###
|
|
219
|
+
### Mutation authority and publication receipts
|
|
219
220
|
|
|
220
|
-
Pass `
|
|
221
|
-
caller-owned authorization. The helper captures
|
|
222
|
-
runs it synchronously after asynchronous preparation and
|
|
223
|
-
immediately before each rename
|
|
221
|
+
Pass `assertBeforeMutation` when a move depends on a revocable lease or another
|
|
222
|
+
caller-owned authorization. The helper captures the callback when called and
|
|
223
|
+
runs it synchronously after asynchronous preparation and identity checks,
|
|
224
|
+
immediately before each rename, source-file or source-symlink unlink, and
|
|
225
|
+
source-directory removal. `assertBeforeRename` remains available for callers
|
|
226
|
+
that only guard publication. When both are set, the rename check runs first,
|
|
227
|
+
then the mutation check, without yielding before dispatch.
|
|
224
228
|
|
|
225
229
|
```ts
|
|
230
|
+
type MovePathPublicationReceipt = Readonly<{
|
|
231
|
+
path: string;
|
|
232
|
+
dev: bigint;
|
|
233
|
+
ino: bigint;
|
|
234
|
+
}>;
|
|
235
|
+
|
|
226
236
|
type MovePathWithCopyFallbackOptions = {
|
|
227
237
|
from: string;
|
|
228
238
|
sourceHardlinks?: "allow" | "reject";
|
|
229
239
|
to: string;
|
|
230
240
|
assertBeforeRename?: () => void;
|
|
241
|
+
assertBeforeMutation?: () => void;
|
|
242
|
+
onDestinationPublished?: (receipt: MovePathPublicationReceipt) => void;
|
|
231
243
|
};
|
|
232
244
|
```
|
|
233
245
|
|
|
234
|
-
Throw to refuse
|
|
235
|
-
with `EXDEV` or `EPERM` codes; an authorization failure never starts a
|
|
236
|
-
fallback. A genuine rename failure may still require a second
|
|
246
|
+
Throw to refuse the next mutation. The original error is propagated, including
|
|
247
|
+
errors with `EXDEV` or `EPERM` codes; an authorization failure never starts a
|
|
248
|
+
copy fallback. A genuine rename failure may still require a second authority
|
|
237
249
|
check before publishing the staged copy.
|
|
238
250
|
|
|
239
|
-
|
|
240
|
-
or any other value
|
|
241
|
-
results are consumed
|
|
242
|
-
|
|
243
|
-
|
|
251
|
+
All three callbacks must return `undefined` synchronously. Returning a Promise,
|
|
252
|
+
thenable, or any other value fails with a `TypeError`; rejected asynchronous
|
|
253
|
+
results are consumed. Perform asynchronous policy checks before calling the
|
|
254
|
+
helper and use the authority callback to recheck the current owner at each
|
|
255
|
+
mutation boundary. All callbacks are captured before the first await.
|
|
256
|
+
|
|
257
|
+
`onDestinationPublished` runs exactly once after a successful rename resolves,
|
|
258
|
+
before awaited post-rename directory checks or source cleanup. It receives a
|
|
259
|
+
frozen receipt with the resolved absolute destination path and the exact bigint
|
|
260
|
+
device/inode identity observed on the rename source before dispatch: the
|
|
261
|
+
original source for a direct rename, or the staged copy for fallback. Failed
|
|
262
|
+
rename attempts never emit receipts. The return contract remains `Promise<void>`.
|
|
263
|
+
|
|
264
|
+
The receipt records an observed identity, not authorization, durable storage,
|
|
265
|
+
or an atomic guarantee against concurrent pathname replacement. Before recovery,
|
|
266
|
+
recheck caller authority and compare the retained identity with a fresh bigint
|
|
267
|
+
stat of the destination. In particular, unknown Windows device/inode values
|
|
268
|
+
must not be treated as proof of ownership. Do not derive ownership from a new
|
|
269
|
+
post-failure snapshot alone.
|
|
244
270
|
|
|
245
271
|
A refused rename leaves the source and destination unchanged; any private
|
|
246
|
-
staged copy follows
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
272
|
+
staged copy follows normal cleanup. If publication has already happened, a
|
|
273
|
+
callback error or later verification failure preserves the published destination
|
|
274
|
+
and stops further source cleanup. Revocation during cleanup leaves all
|
|
275
|
+
as-yet-unremoved source entries intact; entries already removed remain removed.
|
|
276
|
+
The caller retains the receipt even if the move later rejects and owns recovery
|
|
277
|
+
from this partial-move state. An observer that throws must retain its receipt
|
|
278
|
+
before throwing if recovery needs it.
|
|
279
|
+
|
|
280
|
+
These callbacks do not cancel already-dispatched operations or make an external
|
|
281
|
+
lease store atomic with the filesystem. `assertBeforeMutation` guards renames
|
|
282
|
+
and removal of copied source entries; private staging creation, writes, and
|
|
283
|
+
failed-staging cleanup remain owned by the helper. Omitting the new callbacks
|
|
284
|
+
preserves the existing move and source-identity behavior.
|
|
251
285
|
|
|
252
286
|
## Difference from `root()`
|
|
253
287
|
|