@openclaw/fs-safe 0.11.0 → 0.12.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 +58 -0
- package/README.md +4 -2
- package/dist/advanced.d.ts +1 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -1
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +4 -3
- package/dist/archive-gzip-tail.d.ts.map +1 -1
- package/dist/archive-gzip-tail.js +13 -9
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +37 -33
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +17 -5
- package/dist/bounded-read.js +2 -2
- package/dist/device-path.d.ts.map +1 -1
- package/dist/device-path.js +5 -3
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +9 -2
- package/dist/file-store-prune.d.ts.map +1 -1
- package/dist/file-store-prune.js +9 -1
- package/dist/filename.d.ts +1 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +30 -13
- package/dist/guarded-mkdir.d.ts +2 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +50 -15
- package/dist/guest.d.ts.map +1 -1
- package/dist/guest.js +12 -4
- package/dist/install-path.d.ts +6 -0
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +13 -0
- package/dist/json-durable-queue-ownership.d.ts +2 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +47 -3
- package/dist/json-durable-queue-read.d.ts +6 -0
- package/dist/json-durable-queue-read.d.ts.map +1 -0
- package/dist/json-durable-queue-read.js +59 -0
- package/dist/json-durable-queue.d.ts +1 -1
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +37 -80
- package/dist/native-binding.d.ts +7 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/path-scope-lexical.d.ts +14 -0
- package/dist/path-scope-lexical.d.ts.map +1 -0
- package/dist/path-scope-lexical.js +27 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +20 -2
- package/dist/pinned-write.js +1 -0
- package/dist/root-boundary.d.ts +25 -0
- package/dist/root-boundary.d.ts.map +1 -0
- package/dist/root-boundary.js +177 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +32 -5
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +10 -2
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +53 -34
- package/dist/root-move-preflight.d.ts +8 -0
- package/dist/root-move-preflight.d.ts.map +1 -0
- package/dist/root-move-preflight.js +16 -0
- package/dist/root-path-existing.d.ts +2 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +10 -2
- package/dist/root-path.d.ts +2 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +54 -26
- package/dist/root-paths.d.ts +2 -6
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +14 -26
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +2 -1
- package/dist/root-write-mode.d.ts +2 -0
- package/dist/root-write-mode.d.ts.map +1 -1
- package/dist/root-write-mode.js +20 -7
- package/dist/root-write-verification.d.ts.map +1 -1
- package/dist/root-write-verification.js +12 -3
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +3 -1
- package/dist/secure-file-windows.d.ts +10 -0
- package/dist/secure-file-windows.d.ts.map +1 -0
- package/dist/secure-file-windows.js +186 -0
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +6 -3
- package/dist/sibling-staged-file.d.ts +1 -0
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +7 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +2 -1
- package/dist/sidecar-lock-acquire.js +1 -1
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +2 -3
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +3 -1
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +11 -10
- package/docs/advanced.md +3 -1
- package/docs/archive.md +6 -0
- package/docs/contributing.md +4 -1
- package/docs/copy.md +1 -1
- package/docs/durability.md +4 -2
- package/docs/file-store.md +6 -0
- package/docs/filename.md +9 -2
- package/docs/guest.md +5 -0
- package/docs/install-path.md +59 -13
- package/docs/install.md +4 -1
- package/docs/native-helper.md +4 -2
- package/docs/native.md +14 -2
- package/docs/path.md +1 -1
- package/docs/permissions.md +26 -1
- package/docs/public-api.md +5 -0
- package/docs/root.md +3 -1
- package/docs/secure-file.md +15 -14
- package/docs/security-model.md +1 -1
- package/docs/store.md +22 -1
- package/docs/temp.md +4 -0
- package/package.json +8 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root-write-mode.d.ts","sourceRoot":"","sources":["../src/root-write-mode.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"root-write-mode.d.ts","sourceRoot":"","sources":["../src/root-write-mode.ts"],"names":[],"mappings":"AAQA,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAGpF,wBAAsB,sBAAsB,CAAC,MAAM,EAAE;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,oBAAoB,CAAC;IACpC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CAkDlB"}
|
package/dist/root-write-mode.js
CHANGED
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
|
-
import { isNotFoundPathError
|
|
4
|
+
import { isNotFoundPathError } from "./path.js";
|
|
5
5
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
6
6
|
import { realpathSync } from "./realpath.js";
|
|
7
7
|
import { hardlinkedPathNotAllowedError, outsideWorkspaceError } from "./root-errors.js";
|
|
8
8
|
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
9
|
+
import { admitPathInsideRoot } from "./root-boundary.js";
|
|
9
10
|
// The caller has already resolved and guarded this write target.
|
|
10
11
|
export async function inheritWriteTargetMode(params) {
|
|
11
12
|
try {
|
|
12
|
-
const
|
|
13
|
+
const admittedTarget = admitPathInsideRoot({
|
|
14
|
+
rootPath: params.rootWithSep,
|
|
15
|
+
candidatePath: params.targetPath,
|
|
16
|
+
rootIdentity: params.rootIdentity,
|
|
17
|
+
});
|
|
18
|
+
if (!admittedTarget)
|
|
19
|
+
throw outsideWorkspaceError();
|
|
20
|
+
const targetPath = admittedTarget.path;
|
|
21
|
+
const existing = await inspectFileIdentity(() => fsSync.lstatSync(targetPath, { bigint: true }));
|
|
13
22
|
if (existing.isSymbolicLink())
|
|
14
23
|
throw new FsSafeError("path-alias", "path alias escape blocked");
|
|
15
24
|
if (!existing.isFile())
|
|
16
25
|
throw new FsSafeError("not-file", "not a file");
|
|
17
26
|
if (existing.nlink > 1n)
|
|
18
27
|
throw hardlinkedPathNotAllowedError();
|
|
19
|
-
if (!isPathInside(params.rootWithSep, params.targetPath))
|
|
20
|
-
throw outsideWorkspaceError();
|
|
21
28
|
// Preserve read-open admission of the pre-existing destination. access(2)
|
|
22
29
|
// is not equivalent: it ignores ACLs on Windows and capabilities on Linux.
|
|
23
|
-
const handle = await fs.open(
|
|
30
|
+
const handle = await fs.open(targetPath, resolveReadOpenFlags());
|
|
24
31
|
try {
|
|
25
32
|
// Bind admission to the inode whose metadata is inherited.
|
|
26
33
|
await inspectFileIdentity(() => fsSync.fstatSync(handle.fd, { bigint: true }), existing);
|
|
@@ -31,9 +38,15 @@ export async function inheritWriteTargetMode(params) {
|
|
|
31
38
|
try {
|
|
32
39
|
// A parent can change after guarded resolution. Do not inherit metadata
|
|
33
40
|
// from an outside inode, even if the parent is restored before publication.
|
|
34
|
-
const
|
|
35
|
-
|
|
41
|
+
const observedRealPath = realpathSync.native(targetPath);
|
|
42
|
+
const admittedRealPath = admitPathInsideRoot({
|
|
43
|
+
rootPath: params.rootWithSep,
|
|
44
|
+
candidatePath: observedRealPath,
|
|
45
|
+
rootIdentity: params.rootIdentity,
|
|
46
|
+
});
|
|
47
|
+
if (!admittedRealPath)
|
|
36
48
|
throw outsideWorkspaceError();
|
|
49
|
+
const realPath = admittedRealPath.path;
|
|
37
50
|
await inspectFileIdentity(async () => {
|
|
38
51
|
const current = fsSync.statSync(realPath, { bigint: true });
|
|
39
52
|
if (!current.isFile())
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root-write-verification.d.ts","sourceRoot":"","sources":["../src/root-write-verification.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAK9F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAEhE,OAAO,EAA6B,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"root-write-verification.d.ts","sourceRoot":"","sources":["../src/root-write-verification.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAK9F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAEhE,OAAO,EAA6B,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIhF,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,gBAAgB,EAAE,sBAAsB,CAAC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,sBAAsB,CAAC;CACrC,GAAG,OAAO,CAAC,IAAI,CAAC,CA6FhB"}
|
|
@@ -4,10 +4,11 @@ import { assertAsyncDirectoryGuard } from "./directory-guard.js";
|
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
5
|
import { sameFileIdentity } from "./file-identity.js";
|
|
6
6
|
import { resolveOpenedFileRealPathForFd } from "./opened-realpath.js";
|
|
7
|
-
import { assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError,
|
|
7
|
+
import { assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isSymlinkOpenError } from "./path.js";
|
|
8
8
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
9
9
|
import { assertRootIdentityCurrent } from "./root-context.js";
|
|
10
10
|
import { fileNotFoundError, hardlinkedPathNotAllowedError, outsideWorkspaceError } from "./root-errors.js";
|
|
11
|
+
import { admitPathInsideRoot } from "./root-boundary.js";
|
|
11
12
|
export async function verifyAtomicWriteResult(params) {
|
|
12
13
|
let needsPathOpen = false;
|
|
13
14
|
const assertFile = (stat) => {
|
|
@@ -49,7 +50,11 @@ export async function verifyAtomicWriteResult(params) {
|
|
|
49
50
|
assertPath(fsSync.lstatSync(params.targetPath, { bigint: true }));
|
|
50
51
|
const { realPath } = await resolveOpenedFileRealPathForFd(params.fd, stat, params.targetPath);
|
|
51
52
|
assertPath(fsSync.statSync(realPath, { bigint: true }));
|
|
52
|
-
if (!
|
|
53
|
+
if (!admitPathInsideRoot({
|
|
54
|
+
rootPath: params.root.rootReal,
|
|
55
|
+
candidatePath: realPath,
|
|
56
|
+
rootIdentity: params.root.rootIdentity,
|
|
57
|
+
})) {
|
|
53
58
|
throw outsideWorkspaceError();
|
|
54
59
|
}
|
|
55
60
|
await assertAsyncDirectoryGuard(params.parentGuard);
|
|
@@ -76,7 +81,11 @@ export async function verifyAtomicWriteResult(params) {
|
|
|
76
81
|
assertPath(fsSync.lstatSync(params.targetPath, { bigint: true }));
|
|
77
82
|
const { realPath: reopenedPath } = await resolveOpenedFileRealPathForFd(opened.fd, reopenedStat, params.targetPath);
|
|
78
83
|
assertPath(fsSync.statSync(reopenedPath, { bigint: true }));
|
|
79
|
-
if (!
|
|
84
|
+
if (!admitPathInsideRoot({
|
|
85
|
+
rootPath: params.root.rootReal,
|
|
86
|
+
candidatePath: reopenedPath,
|
|
87
|
+
rootIdentity: params.root.rootIdentity,
|
|
88
|
+
})) {
|
|
80
89
|
throw outsideWorkspaceError();
|
|
81
90
|
}
|
|
82
91
|
await assertAsyncDirectoryGuard(params.parentGuard);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-path-segment.d.ts","sourceRoot":"","sources":["../src/safe-path-segment.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"safe-path-segment.d.ts","sourceRoot":"","sources":["../src/safe-path-segment.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,sBAAsB,GAAG;IACnC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKtF;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUrD;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAaT;AAED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAUR;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAWR;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAeR"}
|
|
@@ -4,12 +4,14 @@ const SAFE_DOT_PREFIX_PATH_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
|
4
4
|
// Windows treats "C:name" as relative to the drive's current directory even
|
|
5
5
|
// though path.win32.isAbsolute() reports false.
|
|
6
6
|
const DRIVE_RELATIVE_PREFIX = /^[A-Za-z]:(?![\\/])/;
|
|
7
|
+
// A forward slash ends the segment, so "C:/file" still contains the "C:" segment.
|
|
8
|
+
const DRIVE_RELATIVE_SEGMENT = /(?:^|\/)[A-Za-z]:(?!\\)/;
|
|
7
9
|
const HYPHEN_CHAR_CODE = 0x2d;
|
|
8
10
|
export function isDriveRelativePath(value) {
|
|
9
11
|
return DRIVE_RELATIVE_PREFIX.test(value);
|
|
10
12
|
}
|
|
11
13
|
export function assertNoDriveRelativePathSegments(value, label) {
|
|
12
|
-
if (
|
|
14
|
+
if (DRIVE_RELATIVE_SEGMENT.test(value)) {
|
|
13
15
|
throw new FsSafeError("invalid-path", `${label} must not contain a drive letter`);
|
|
14
16
|
}
|
|
15
17
|
return value;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BigIntStats, Stats } from "node:fs";
|
|
2
|
+
import type { PermissionCheck } from "./permissions.js";
|
|
3
|
+
type ExactIdentity = Pick<BigIntStats, "dev" | "ino">;
|
|
4
|
+
export declare function inspectSecureWindowsDescriptor(params: {
|
|
5
|
+
fd: number;
|
|
6
|
+
identity: ExactIdentity;
|
|
7
|
+
stat: Stats;
|
|
8
|
+
}): PermissionCheck;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=secure-file-windows.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secure-file-windows.d.ts","sourceRoot":"","sources":["../src/secure-file-windows.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AASlD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AA4BxD,KAAK,aAAa,GAAG,IAAI,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC;AAuHtD,wBAAgB,8BAA8B,CAAC,MAAM,EAAE;IACrD,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,aAAa,CAAC;IACxB,IAAI,EAAE,KAAK,CAAC;CACb,GAAG,eAAe,CA0ClB"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
import { recordFileObservationFailure } from "./file-observation.js";
|
|
3
|
+
import { getNativeBinding } from "./native.js";
|
|
4
|
+
const IDENTITY_RE = /^([0-9a-f]{8}):([0-9a-f]{16})$/;
|
|
5
|
+
const SID_RE = /^s-\d+-\d+(?:-\d+)+$/;
|
|
6
|
+
const OWNER_CLASSES = new Set(["current-user", "system", "administrators", "foreign"]);
|
|
7
|
+
const TRUSTED_OWNER_CLASSES = new Set(["current-user", "system", "administrators"]);
|
|
8
|
+
const SYSTEM_SID = "s-1-5-18";
|
|
9
|
+
const ADMINISTRATORS_SID = "s-1-5-32-544";
|
|
10
|
+
const WORLD_SIDS = new Set([
|
|
11
|
+
"s-1-1-0", "s-1-5-11", "s-1-5-32-545", "s-1-5-7",
|
|
12
|
+
"s-1-5-32-546", "s-1-5-4", "s-1-5-2",
|
|
13
|
+
]);
|
|
14
|
+
const GENERIC_READ = 0x8000_0000;
|
|
15
|
+
const GENERIC_WRITE = 0x4000_0000;
|
|
16
|
+
const GENERIC_ALL = 0x1000_0000;
|
|
17
|
+
const DELETE_ACCESS = 0x0001_0000;
|
|
18
|
+
const WRITE_DAC = 0x0004_0000;
|
|
19
|
+
const WRITE_OWNER = 0x0008_0000;
|
|
20
|
+
const FILE_READ_DATA = 0x0000_0001;
|
|
21
|
+
const FILE_WRITE_DATA = 0x0000_0002;
|
|
22
|
+
const FILE_APPEND_DATA = 0x0000_0004;
|
|
23
|
+
const FILE_READ_EA = 0x0000_0008;
|
|
24
|
+
const FILE_WRITE_EA = 0x0000_0010;
|
|
25
|
+
const FILE_DELETE_CHILD = 0x0000_0040;
|
|
26
|
+
const FILE_READ_ATTRIBUTES = 0x0000_0080;
|
|
27
|
+
const FILE_WRITE_ATTRIBUTES = 0x0000_0100;
|
|
28
|
+
function permissionUnverified(message, cause) {
|
|
29
|
+
throw new FsSafeError("permission-unverified", message, cause === undefined ? {} : { cause });
|
|
30
|
+
}
|
|
31
|
+
function identityMismatch() {
|
|
32
|
+
const error = new FsSafeError("path-mismatch", "file identity changed or could not be verified");
|
|
33
|
+
recordFileObservationFailure(error, "identity");
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
function isRecord(value) {
|
|
37
|
+
return typeof value === "object" && value !== null;
|
|
38
|
+
}
|
|
39
|
+
function isUint32(value) {
|
|
40
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 0xffff_ffff;
|
|
41
|
+
}
|
|
42
|
+
function parseIdentity(value) {
|
|
43
|
+
if (typeof value !== "string")
|
|
44
|
+
identityMismatch();
|
|
45
|
+
const match = IDENTITY_RE.exec(value);
|
|
46
|
+
if (!match)
|
|
47
|
+
identityMismatch();
|
|
48
|
+
return {
|
|
49
|
+
dev: BigInt(`0x${match[1]}`),
|
|
50
|
+
ino: BigInt(`0x${match[2]}`),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function validateFlags(value) {
|
|
54
|
+
if (!isRecord(value))
|
|
55
|
+
return false;
|
|
56
|
+
const raw = value.raw;
|
|
57
|
+
if (!isUint32(raw) || (raw & ~0xdf) !== 0)
|
|
58
|
+
return false;
|
|
59
|
+
const fields = [
|
|
60
|
+
["objectInherit", 0x01], ["containerInherit", 0x02],
|
|
61
|
+
["noPropagateInherit", 0x04], ["inheritOnly", 0x08], ["inherited", 0x10],
|
|
62
|
+
["successfulAccess", 0x40], ["failedAccess", 0x80],
|
|
63
|
+
];
|
|
64
|
+
return fields.every(([field, bit]) => typeof value[field] === "boolean" && value[field] === ((raw & bit) !== 0));
|
|
65
|
+
}
|
|
66
|
+
function validateAce(value) {
|
|
67
|
+
return isRecord(value) && typeof value.sid === "string" && SID_RE.test(value.sid) &&
|
|
68
|
+
isUint32(value.mask) && (value.aceType === "allow" || value.aceType === "deny") &&
|
|
69
|
+
validateFlags(value.flags);
|
|
70
|
+
}
|
|
71
|
+
function canRead(mask) {
|
|
72
|
+
return (mask & (GENERIC_ALL | GENERIC_READ | FILE_READ_DATA | FILE_READ_EA |
|
|
73
|
+
FILE_READ_ATTRIBUTES)) !== 0;
|
|
74
|
+
}
|
|
75
|
+
function canWrite(mask) {
|
|
76
|
+
return (mask & (GENERIC_ALL | GENERIC_WRITE | FILE_WRITE_DATA | FILE_APPEND_DATA |
|
|
77
|
+
FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES | FILE_DELETE_CHILD | DELETE_ACCESS |
|
|
78
|
+
WRITE_DAC | WRITE_OWNER)) !== 0;
|
|
79
|
+
}
|
|
80
|
+
function expectedOwnerClass(facts) {
|
|
81
|
+
if (facts.ownerSid === facts.currentUserSid)
|
|
82
|
+
return "current-user";
|
|
83
|
+
if (facts.ownerSid === SYSTEM_SID)
|
|
84
|
+
return "system";
|
|
85
|
+
if (facts.ownerSid === ADMINISTRATORS_SID)
|
|
86
|
+
return "administrators";
|
|
87
|
+
return "foreign";
|
|
88
|
+
}
|
|
89
|
+
function validateSecurity(value) {
|
|
90
|
+
if (!isRecord(value))
|
|
91
|
+
permissionUnverified("Windows descriptor ACL facts were malformed");
|
|
92
|
+
const facts = value;
|
|
93
|
+
const booleans = [
|
|
94
|
+
"worldWritable", "groupWritable", "worldReadable", "groupReadable",
|
|
95
|
+
"fallbackRequired", "daclPresent", "isLocal", "aceListComplete",
|
|
96
|
+
];
|
|
97
|
+
if (!booleans.every((field) => typeof facts[field] === "boolean") ||
|
|
98
|
+
typeof facts.ownerSid !== "string" || !SID_RE.test(facts.ownerSid) ||
|
|
99
|
+
typeof facts.currentUserSid !== "string" || !SID_RE.test(facts.currentUserSid) ||
|
|
100
|
+
typeof facts.ownerClass !== "string" || !OWNER_CLASSES.has(facts.ownerClass) ||
|
|
101
|
+
!Array.isArray(facts.unsupportedAceTypes) ||
|
|
102
|
+
!facts.unsupportedAceTypes.every(isUint32) ||
|
|
103
|
+
!Array.isArray(facts.aces) || !facts.aces.every(validateAce)) {
|
|
104
|
+
permissionUnverified("Windows descriptor ACL facts were malformed");
|
|
105
|
+
}
|
|
106
|
+
if (facts.ownerClass !== expectedOwnerClass(facts)) {
|
|
107
|
+
permissionUnverified("Windows descriptor owner facts were inconsistent");
|
|
108
|
+
}
|
|
109
|
+
if (facts.fallbackRequired || !facts.isLocal || !facts.aceListComplete ||
|
|
110
|
+
facts.unsupportedAceTypes.length !== 0) {
|
|
111
|
+
permissionUnverified("Windows descriptor ACL facts were incomplete or unsupported");
|
|
112
|
+
}
|
|
113
|
+
if (!facts.daclPresent && facts.aces.length !== 0) {
|
|
114
|
+
permissionUnverified("Windows descriptor DACL facts were inconsistent");
|
|
115
|
+
}
|
|
116
|
+
return facts;
|
|
117
|
+
}
|
|
118
|
+
function validatePermissionSummary(facts) {
|
|
119
|
+
let worldReadable = !facts.daclPresent;
|
|
120
|
+
let worldWritable = !facts.daclPresent;
|
|
121
|
+
let groupReadable = false;
|
|
122
|
+
let groupWritable = false;
|
|
123
|
+
const trustedSids = new Set([facts.currentUserSid, SYSTEM_SID, ADMINISTRATORS_SID]);
|
|
124
|
+
for (const ace of facts.aces) {
|
|
125
|
+
if (ace.aceType !== "allow" || ace.flags.inheritOnly || trustedSids.has(ace.sid))
|
|
126
|
+
continue;
|
|
127
|
+
const world = WORLD_SIDS.has(ace.sid);
|
|
128
|
+
if (world) {
|
|
129
|
+
worldReadable ||= canRead(ace.mask);
|
|
130
|
+
worldWritable ||= canWrite(ace.mask);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
groupReadable ||= canRead(ace.mask);
|
|
134
|
+
groupWritable ||= canWrite(ace.mask);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (facts.worldReadable !== worldReadable || facts.worldWritable !== worldWritable ||
|
|
138
|
+
facts.groupReadable !== groupReadable || facts.groupWritable !== groupWritable) {
|
|
139
|
+
permissionUnverified("Windows descriptor ACL summary was inconsistent");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export function inspectSecureWindowsDescriptor(params) {
|
|
143
|
+
let native;
|
|
144
|
+
try {
|
|
145
|
+
native = getNativeBinding();
|
|
146
|
+
}
|
|
147
|
+
catch (cause) {
|
|
148
|
+
permissionUnverified("Windows descriptor ACL verification requires the matching native helper", cause);
|
|
149
|
+
}
|
|
150
|
+
const inspect = native?.inspectWindowsSecureFileHandle;
|
|
151
|
+
if (typeof inspect !== "function") {
|
|
152
|
+
permissionUnverified("Windows descriptor ACL verification requires an up-to-date native helper");
|
|
153
|
+
}
|
|
154
|
+
let result;
|
|
155
|
+
try {
|
|
156
|
+
result = inspect.call(native, params.fd);
|
|
157
|
+
}
|
|
158
|
+
catch (cause) {
|
|
159
|
+
permissionUnverified("Windows descriptor ACL verification failed", cause);
|
|
160
|
+
}
|
|
161
|
+
if (!isRecord(result))
|
|
162
|
+
permissionUnverified("Windows descriptor ACL facts were malformed");
|
|
163
|
+
const observed = parseIdentity(result.identity);
|
|
164
|
+
if (observed.dev !== params.identity.dev || observed.ino !== params.identity.ino) {
|
|
165
|
+
identityMismatch();
|
|
166
|
+
}
|
|
167
|
+
const facts = validateSecurity(result.security);
|
|
168
|
+
validatePermissionSummary(facts);
|
|
169
|
+
return {
|
|
170
|
+
ok: true,
|
|
171
|
+
isSymlink: false,
|
|
172
|
+
isDir: params.stat.isDirectory(),
|
|
173
|
+
mode: typeof params.stat.mode === "number" ? params.stat.mode : null,
|
|
174
|
+
bits: null,
|
|
175
|
+
source: "windows-acl",
|
|
176
|
+
worldWritable: facts.worldWritable,
|
|
177
|
+
groupWritable: facts.groupWritable,
|
|
178
|
+
worldReadable: facts.worldReadable,
|
|
179
|
+
groupReadable: facts.groupReadable,
|
|
180
|
+
ownerSid: facts.ownerSid,
|
|
181
|
+
ownerTrusted: TRUSTED_OWNER_CLASSES.has(facts.ownerClass),
|
|
182
|
+
aclSummary: `native descriptor owner=${facts.ownerClass} world=` +
|
|
183
|
+
`${facts.worldReadable ? "r" : "-"}${facts.worldWritable ? "w" : "-"} ` +
|
|
184
|
+
`group=${facts.groupReadable ? "r" : "-"}${facts.groupWritable ? "w" : "-"}`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secure-file.d.ts","sourceRoot":"","sources":["../src/secure-file.ts"],"names":[],"mappings":"AAAA,OAAe,EAAoB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAc/D,OAAO,EAOL,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC5B,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"secure-file.d.ts","sourceRoot":"","sources":["../src/secure-file.ts"],"names":[],"mappings":"AAAA,OAAe,EAAoB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAc/D,OAAO,EAOL,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC5B,MAAM,kBAAkB,CAAC;AAK1B,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,WAAW,CAAC,EAAE,2BAA2B,CAAC;IAC1C,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,EAAE,CAAC,EAAE,mBAAmB,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,sBAAsB,CAAC;AAE7D,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;IACZ,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B,CAAC;AAuNF,wBAAsB,cAAc,CAClC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CA6B/B"}
|
package/dist/secure-file.js
CHANGED
|
@@ -12,6 +12,7 @@ import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
|
12
12
|
import { realpathSync } from "./realpath.js";
|
|
13
13
|
import { inspectPathPermissions, isGroupReadable, isGroupWritable, isWorldReadable, isWorldWritable, modeBits, } from "./permissions.js";
|
|
14
14
|
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
15
|
+
import { inspectSecureWindowsDescriptor } from "./secure-file-windows.js";
|
|
15
16
|
import { scheduleTimeout } from "./timing.js";
|
|
16
17
|
function isAbsolutePathname(value) {
|
|
17
18
|
return (path.isAbsolute(value) ||
|
|
@@ -128,13 +129,15 @@ function inspectOpenedPermissions(stat, platform) {
|
|
|
128
129
|
groupReadable: isGroupReadable(bits),
|
|
129
130
|
};
|
|
130
131
|
}
|
|
131
|
-
async function assertSecurePermissions(options, stat, realPath) {
|
|
132
|
+
async function assertSecurePermissions(options, stat, realPath, identity, fd) {
|
|
132
133
|
if (options.permissions?.allowInsecure) {
|
|
133
134
|
return undefined;
|
|
134
135
|
}
|
|
135
136
|
const platform = options.inject?.platform ?? process.platform;
|
|
136
137
|
const permissions = platform === "win32"
|
|
137
|
-
?
|
|
138
|
+
? process.platform === "win32"
|
|
139
|
+
? inspectSecureWindowsDescriptor({ fd, identity, stat })
|
|
140
|
+
: await inspectPathPermissions(realPath, options.inject)
|
|
138
141
|
: inspectOpenedPermissions(stat, platform);
|
|
139
142
|
const reason = permissions.error ? `: ${formatPermissionErrorDetail(permissions.error)}` : "";
|
|
140
143
|
const diagnostics = {
|
|
@@ -194,7 +197,7 @@ export async function readSecureFile(options) {
|
|
|
194
197
|
const opened = await openSecureHandle(options, maxBytes);
|
|
195
198
|
try {
|
|
196
199
|
await assertTrustedDirs(options, opened.realPath);
|
|
197
|
-
const permissions = await assertSecurePermissions(options, opened.pathStat, opened.realPath);
|
|
200
|
+
const permissions = await assertSecurePermissions(options, opened.pathStat, opened.realPath, opened.identity, opened.handle.fd);
|
|
198
201
|
const buffer = await readHandleWithTimeout(opened.handle, options.io?.timeoutMs, maxBytes);
|
|
199
202
|
const finalIdentity = await inspectFileIdentity(() => fsSync.fstatSync(opened.handle.fd, { bigint: true }), opened.identity);
|
|
200
203
|
if (!finalIdentity.isFile()) {
|
|
@@ -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":"AAuCA,wBAAgB,gCAAgC,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIvE;AAmCD,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,iBAAiB,CAAC,EAAE,mBAAmB,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,CAgH3C"}
|
|
@@ -4,6 +4,7 @@ import fs, {} from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { assertAsyncDirectoryGuard, assertDirectoryIdentitySync, createAsyncDirectoryGuard, } from "./directory-guard.js";
|
|
6
6
|
import { syncDirectoryBestEffort } from "./directory-durability.js";
|
|
7
|
+
import { isWindowsReservedDeviceName } from "./device-path.js";
|
|
7
8
|
import { FsSafeError } from "./errors.js";
|
|
8
9
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
9
10
|
import { root } from "./root.js";
|
|
@@ -29,6 +30,11 @@ async function inspectStage(inspect, expected) {
|
|
|
29
30
|
return stat;
|
|
30
31
|
}, expected);
|
|
31
32
|
}
|
|
33
|
+
export function assertCallbackTempPathDeviceSafe(tempPath) {
|
|
34
|
+
if (isWindowsReservedDeviceName(tempPath)) {
|
|
35
|
+
throw new FsSafeError("invalid-path", "callback temp path uses a reserved Windows device name");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
32
38
|
// Own the workspace before the producer can leave partial output. The finished
|
|
33
39
|
// file still enters the ordinary sibling admission and publication lifecycle.
|
|
34
40
|
async function writeIsolatedProducer(params) {
|
|
@@ -59,6 +65,7 @@ async function writeIsolatedProducer(params) {
|
|
|
59
65
|
// Keep one descriptor and one exact identity through mode, sync, rename and cleanup.
|
|
60
66
|
// Read/write access is needed only when the caller requests file synchronization.
|
|
61
67
|
export async function writeCallbackSibling(params) {
|
|
68
|
+
assertCallbackTempPathDeviceSafe(params.tempPath);
|
|
62
69
|
const parent = path.dirname(params.tempPath);
|
|
63
70
|
const guard = await createAsyncDirectoryGuard(parent, { bigint: true });
|
|
64
71
|
const assertParent = () => assertAsyncDirectoryGuard(guard);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sibling-temp.d.ts","sourceRoot":"","sources":["../src/sibling-temp.ts"],"names":[],"mappings":"AAeA,MAAM,MAAM,2BAA2B,CAAC,CAAC,IAAI;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5C,oEAAoE;IACpE,iBAAiB,CAAC,EAAE,mBAAmB,CAAC;IACxC,gBAAgB,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uGAAuG;IACvG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,yEAAyE;IACzE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,0BAA0B,CAAC,CAAC,IAAI;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AASF,wBAAsB,oBAAoB,CAAC,CAAC,EAC1C,OAAO,EAAE,2BAA2B,CAAC,CAAC,CAAC,GACtC,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAqBxC;AAwBD,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"sibling-temp.d.ts","sourceRoot":"","sources":["../src/sibling-temp.ts"],"names":[],"mappings":"AAeA,MAAM,MAAM,2BAA2B,CAAC,CAAC,IAAI;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5C,oEAAoE;IACpE,iBAAiB,CAAC,EAAE,mBAAmB,CAAC;IACxC,gBAAgB,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uGAAuG;IACvG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,yEAAyE;IACzE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,0BAA0B,CAAC,CAAC,IAAI;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AASF,wBAAsB,oBAAoB,CAAC,CAAC,EAC1C,OAAO,EAAE,2BAA2B,CAAC,CAAC,CAAC,GACtC,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAqBxC;AAwBD,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8ChB"}
|
package/dist/sibling-temp.js
CHANGED
|
@@ -9,7 +9,7 @@ import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
|
9
9
|
import { root } from "./root.js";
|
|
10
10
|
import { assertSafePathPrefix } from "./safe-path-segment.js";
|
|
11
11
|
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
12
|
-
import { writeCallbackSibling } from "./sibling-staged-file.js";
|
|
12
|
+
import { assertCallbackTempPathDeviceSafe, writeCallbackSibling } from "./sibling-staged-file.js";
|
|
13
13
|
import { tempFile } from "./temp-target.js";
|
|
14
14
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
15
15
|
function buildTempPath(dir, tempPrefix) {
|
|
@@ -93,6 +93,7 @@ export async function writeViaSiblingTempPath(params) {
|
|
|
93
93
|
fallbackFileName: params.fallbackFileName ?? "output.bin",
|
|
94
94
|
tempPrefix: params.tempPrefix ?? ".fs-safe-output-",
|
|
95
95
|
});
|
|
96
|
+
assertCallbackTempPathDeviceSafe(tempPath);
|
|
96
97
|
await getFsSafeTestHooks()?.beforeSiblingTempWrite?.(tempPath);
|
|
97
98
|
await params.writeTemp(tempPath);
|
|
98
99
|
await assertAsyncDirectoryGuard(rootGuard);
|
|
@@ -58,7 +58,7 @@ export async function acquireSidecarLock(options, context) {
|
|
|
58
58
|
options.reentrantOwner !== undefined &&
|
|
59
59
|
held.reentrantOwner !== undefined &&
|
|
60
60
|
options.reentrantOwner === held.reentrantOwner) {
|
|
61
|
-
held.refCount
|
|
61
|
+
held.refCount = (held.refCount ?? 1) + 1;
|
|
62
62
|
// Retention is monotonic: any same-owner request to keep the sidecar on
|
|
63
63
|
// exit upgrades the held lock; a later default acquisition never revokes it.
|
|
64
64
|
if (options.retainOnExit === true) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock.d.ts","sourceRoot":"","sources":["../src/sidecar-lock.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACV,yBAAyB,EACzB,iBAAiB,EACjB,oBAAoB,EACpB,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAC1E,YAAY,EACV,yBAAyB,EACzB,0BAA0B,EAC1B,iBAAiB,EACjB,oBAAoB,EACpB,uBAAuB,EACvB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"sidecar-lock.d.ts","sourceRoot":"","sources":["../src/sidecar-lock.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACV,yBAAyB,EACzB,iBAAiB,EACjB,oBAAoB,EACpB,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAC1E,YAAY,EACV,yBAAyB,EACzB,0BAA0B,EAC1B,iBAAiB,EACjB,oBAAoB,EACpB,uBAAuB,EACvB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,yBAAyB,CAAC;AAmIjC,0FAA0F;AAC1F,wBAAgB,uBAAuB,IAAI,OAAO,CAMjD;AAyFD,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM;cAU3B,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACpD,yBAAyB,CAAC,QAAQ,CAAC,KAC3C,OAAO,CAAC,iBAAiB,CAAC;eAkBL,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACxD,yBAAyB,CAAC,QAAQ,CAAC,MACxC,MAAM,OAAO,CAAC,CAAC,CAAC,KACnB,OAAO,CAAC,CAAC,CAAC;iBAqBW,OAAO,CAAC,IAAI,CAAC;iBAQnB,IAAI;uBAIE,oBAAoB,EAAE;EAW/C;AAED,wBAAsB,eAAe,CAAC,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,sBAAsB,CAAC,QAAQ,CAAC,EACzC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAMZ"}
|
package/dist/sidecar-lock.js
CHANGED
|
@@ -37,9 +37,6 @@ function resolveManagerState(key) {
|
|
|
37
37
|
// Backfill state created by fs-safe versions that predate reclaim guards.
|
|
38
38
|
state.reclaimCleanupRegistered ??= false;
|
|
39
39
|
state.reclaimGuards ??= new Set();
|
|
40
|
-
for (const held of state.held.values()) {
|
|
41
|
-
held.refCount ??= 1;
|
|
42
|
-
}
|
|
43
40
|
}
|
|
44
41
|
return state;
|
|
45
42
|
}
|
|
@@ -163,6 +160,8 @@ async function releaseHeldLock(state, normalizedTargetPath, held, options = {})
|
|
|
163
160
|
await held.releasePromise;
|
|
164
161
|
return true;
|
|
165
162
|
}
|
|
163
|
+
// Older package copies can add holders after this manager was constructed.
|
|
164
|
+
held.refCount ??= 1;
|
|
166
165
|
if (options.force) {
|
|
167
166
|
held.refCount = 0;
|
|
168
167
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"temp-target.d.ts","sourceRoot":"","sources":["../src/temp-target.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"temp-target.d.ts","sourceRoot":"","sources":["../src/temp-target.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,SAAS,CAAC;AAS7B,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;AAEF,KAAK,eAAe,GAAG;IACrB,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,CAAC;AA8DF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAK7D;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;AAyCD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC;IAC1E,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC;CAC7D,CAAC,CA2BD;AAED,wBAAsB,QAAQ,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,QAAQ,CAAC,CAEzE;AAED,wBAAsB,YAAY,CAAC,CAAC,EAClC,MAAM,EAAE,eAAe,EACvB,EAAE,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAClC,OAAO,CAAC,CAAC,CAAC,CAOZ"}
|
package/dist/temp-target.js
CHANGED
|
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fsSync from "node:fs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { suffixWindowsReservedDeviceName } from "./filename.js";
|
|
5
6
|
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
6
7
|
import { assertSafePathSegment, sanitizeSafePathSegment, trimHyphenEdges } from "./safe-path-segment.js";
|
|
7
8
|
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
@@ -57,9 +58,10 @@ function sanitizeExtension(extension) {
|
|
|
57
58
|
return token ? `.${token}` : "";
|
|
58
59
|
}
|
|
59
60
|
export function sanitizeTempFileName(fileName) {
|
|
60
|
-
|
|
61
|
+
const sanitized = sanitizeSafePathSegment(path.basename(fileName), "download.bin", {
|
|
61
62
|
allowDotPrefix: true,
|
|
62
63
|
});
|
|
64
|
+
return suffixWindowsReservedDeviceName(sanitized);
|
|
63
65
|
}
|
|
64
66
|
export function buildRandomTempFilePath(params) {
|
|
65
67
|
const rootDir = resolveTempRoot(params.rootDir);
|
package/dist/walk.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;AAK7B,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AACvE,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE9D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;IACjD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;IAGnB,UAAU,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACrC,CAAC;AAEF,KAAK,+BAA+B,GAAG,mBAAmB,GAAG;IAC3D,UAAU,EAAE,oBAAoB,EAAE,CAAC;CACpC,CAAC;
|
|
1
|
+
{"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;AAK7B,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AACvE,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE9D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;IACjD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;IAGnB,UAAU,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACrC,CAAC;AAEF,KAAK,+BAA+B,GAAG,mBAAmB,GAAG;IAC3D,UAAU,EAAE,oBAAoB,EAAE,CAAC;CACpC,CAAC;AA+EF,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,+BAA+B,CA0DjC;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,+BAA+B,CAAC,CA0D1C"}
|
package/dist/walk.js
CHANGED
|
@@ -29,11 +29,10 @@ function shouldStop(result, options) {
|
|
|
29
29
|
}
|
|
30
30
|
function buildEntry(params) {
|
|
31
31
|
const fullPath = params.fullPath;
|
|
32
|
-
const relativePath = path.relative(params.rootDir, fullPath) || params.dirent.name;
|
|
33
32
|
return {
|
|
34
33
|
name: params.dirent.name,
|
|
35
34
|
path: fullPath,
|
|
36
|
-
relativePath,
|
|
35
|
+
relativePath: params.relativePath,
|
|
37
36
|
depth: params.depth,
|
|
38
37
|
kind: params.kind ?? kindForDirent(params.dirent),
|
|
39
38
|
dirent: params.dirent,
|
|
@@ -79,7 +78,7 @@ export function walkDirectorySync(rootDir, options = {}) {
|
|
|
79
78
|
failedDirs: [],
|
|
80
79
|
};
|
|
81
80
|
const visitedDirs = new Set();
|
|
82
|
-
function visit(dir, depth) {
|
|
81
|
+
function visit(dir, relativeDir, depth) {
|
|
83
82
|
if (options.maxDepth !== undefined && depth > options.maxDepth)
|
|
84
83
|
return;
|
|
85
84
|
let realDir;
|
|
@@ -111,20 +110,21 @@ export function walkDirectorySync(rootDir, options = {}) {
|
|
|
111
110
|
const kind = resolveKind(fullPath, dirent, symlinks);
|
|
112
111
|
if (!kind)
|
|
113
112
|
continue;
|
|
114
|
-
const
|
|
113
|
+
const relativePath = relativeDir ? `${relativeDir}${path.sep}${dirent.name}` : dirent.name;
|
|
114
|
+
const entry = buildEntry({ relativePath, fullPath, dirent, depth, kind });
|
|
115
115
|
if (options.include?.(entry) ?? true) {
|
|
116
116
|
result.entries.push(entry);
|
|
117
117
|
}
|
|
118
118
|
if (kind === "directory" &&
|
|
119
119
|
(options.maxDepth === undefined || depth < options.maxDepth) &&
|
|
120
120
|
(options.descend?.(entry) ?? true)) {
|
|
121
|
-
visit(fullPath, depth + 1);
|
|
121
|
+
visit(fullPath, relativePath, depth + 1);
|
|
122
122
|
if (result.truncated)
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
-
visit(root, 1);
|
|
127
|
+
visit(root, "", 1);
|
|
128
128
|
return result;
|
|
129
129
|
}
|
|
130
130
|
export async function walkDirectory(rootDir, options = {}) {
|
|
@@ -138,7 +138,7 @@ export async function walkDirectory(rootDir, options = {}) {
|
|
|
138
138
|
failedDirs: [],
|
|
139
139
|
};
|
|
140
140
|
const visitedDirs = new Set();
|
|
141
|
-
async function visit(dir, depth) {
|
|
141
|
+
async function visit(dir, relativeDir, depth) {
|
|
142
142
|
if (options.maxDepth !== undefined && depth > options.maxDepth)
|
|
143
143
|
return;
|
|
144
144
|
let realDir;
|
|
@@ -170,19 +170,20 @@ export async function walkDirectory(rootDir, options = {}) {
|
|
|
170
170
|
const kind = resolveKind(fullPath, dirent, symlinks);
|
|
171
171
|
if (!kind)
|
|
172
172
|
continue;
|
|
173
|
-
const
|
|
173
|
+
const relativePath = relativeDir ? `${relativeDir}${path.sep}${dirent.name}` : dirent.name;
|
|
174
|
+
const entry = buildEntry({ relativePath, fullPath, dirent, depth, kind });
|
|
174
175
|
if (options.include?.(entry) ?? true) {
|
|
175
176
|
result.entries.push(entry);
|
|
176
177
|
}
|
|
177
178
|
if (kind === "directory" &&
|
|
178
179
|
(options.maxDepth === undefined || depth < options.maxDepth) &&
|
|
179
180
|
(options.descend?.(entry) ?? true)) {
|
|
180
|
-
await visit(fullPath, depth + 1);
|
|
181
|
+
await visit(fullPath, relativePath, depth + 1);
|
|
181
182
|
if (result.truncated)
|
|
182
183
|
return;
|
|
183
184
|
}
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
|
-
await visit(root, 1);
|
|
187
|
+
await visit(root, "", 1);
|
|
187
188
|
return result;
|
|
188
189
|
}
|
package/docs/advanced.md
CHANGED
|
@@ -96,6 +96,8 @@ byte limit. Continuation buffers grow only after filling with actual bytes,
|
|
|
96
96
|
up to the byte budget plus its probe; file-size hints cannot force that growth.
|
|
97
97
|
Unknown-size inputs start with at most 64 KiB. This avoids per-chunk copies and
|
|
98
98
|
a final concatenation when a file exceeds the initial allocation.
|
|
99
|
+
Regular files that report a size of zero, such as virtual files, continue through
|
|
100
|
+
positive short reads until actual EOF or byte-limit overflow.
|
|
99
101
|
|
|
100
102
|
The bounded descriptor helpers start at the descriptor's current offset and
|
|
101
103
|
leave ownership with the caller. They are intended for the second half of a
|
|
@@ -133,7 +135,7 @@ component is followed by another segment, both helpers throw
|
|
|
133
135
|
|
|
134
136
|
| Export | Page | Notes |
|
|
135
137
|
|---|---|---|
|
|
136
|
-
| `safeDirName`, `safePathSegmentHashed`, `resolveSafeInstallDir`, `assertCanonicalPathWithinBase` | [install-path.md](install-path.md) | Build install-target directories
|
|
138
|
+
| `safeDirName`, `safePathSegmentHashed`, `safePathSegmentHashedV2`, `resolveSafeInstallDir`, `assertCanonicalPathWithinBase` | [install-path.md](install-path.md) | Build install-target directories; use V2 for untrusted identifier mappings. |
|
|
137
139
|
| `sanitizeUntrustedFileName` | [filename.md](filename.md) | Coerce an untrusted string into a safe filename. |
|
|
138
140
|
| `resolveHomeRelativePath` | – | Expand a leading `~` before resolving `.` and `..`; tildes inside relative paths stay literal. |
|
|
139
141
|
|
package/docs/archive.md
CHANGED
|
@@ -141,6 +141,12 @@ between native and JavaScript paths rather than reimplementing it in Rust.
|
|
|
141
141
|
|
|
142
142
|
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. Native ZIP extraction groups nearby metadata reads into at most two 4 KiB read-ahead buffers per admission pass; larger records retain separately bounded reads. Buffered record views remain stable across eviction, and cached work periodically yields for deadline checks.
|
|
143
143
|
|
|
144
|
+
Within one ZIP entry, identical local and central name bytes reuse the same
|
|
145
|
+
decoded validation. Unicode Path admission is shared only when both the raw names
|
|
146
|
+
and the complete Unicode fields match; different fields still verify their own
|
|
147
|
+
CRC and interpretation. Shared backing memory is checked independently. Decoded
|
|
148
|
+
name validation is not reused across entries or archives.
|
|
149
|
+
|
|
144
150
|
`stripComponents` removes leading nonempty, non-`.` path components after
|
|
145
151
|
normalizing separators. For example, `./pkg/hello.txt` with
|
|
146
152
|
`stripComponents: 1` extracts to `hello.txt` on both backends. Entries with no
|