@openclaw/fs-safe 0.18.1 → 0.18.2
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 +8 -0
- package/dist/advanced.d.ts +2 -1
- package/dist/advanced.js +1 -1
- package/dist/archive-merge.js +0 -1
- package/dist/archive-staging.d.ts +1 -2
- package/dist/archive-staging.js +0 -2
- package/dist/archive-zip-preflight.d.ts +0 -2
- package/dist/archive-zip-preflight.js +0 -1
- package/dist/archive.d.ts +6 -3
- package/dist/archive.js +5 -3
- package/dist/directory-guard.d.ts +0 -1
- package/dist/directory-guard.js +0 -3
- package/dist/directory-mode-node.d.ts +20 -2
- package/dist/directory-mode-node.js +77 -1
- package/dist/file-lock-sync-acquisition.js +1 -1
- package/dist/file-lock-sync-root-options.d.ts +2 -10
- package/dist/json-durable-queue-ownership.d.ts +1 -5
- package/dist/json-durable-queue.d.ts +0 -1
- package/dist/json-durable-queue.js +0 -1
- package/dist/permission-exec.d.ts +6 -0
- package/dist/permissions-public.d.ts +2 -1
- package/dist/permissions-windows.d.ts +3 -6
- package/dist/permissions.d.ts +3 -10
- package/dist/permissions.js +0 -1
- package/dist/pinned-open.d.ts +1 -0
- package/dist/pinned-open.js +1 -1
- package/dist/replace-directory.js +5 -17
- package/dist/replace-file-copy-fallback.d.ts +1 -8
- package/dist/replace-file-descriptor.d.ts +3 -11
- package/dist/replace-file-descriptor.js +1 -1
- package/dist/retained-directory-replacement.d.ts +1 -0
- package/dist/retained-directory-replacement.js +1 -1
- package/dist/root-file-final-admission.d.ts +1 -1
- package/dist/root-file-final-admission.js +1 -6
- package/dist/root-file.js +2 -5
- package/dist/secret-file.js +11 -16
- package/dist/secret-read-policy.js +1 -1
- package/dist/secure-file-windows.js +4 -9
- package/dist/secure-temp-dir.js +2 -7
- package/dist/sidecar-lock-acquire.js +1 -1
- package/dist/sidecar-lock-reclaim.js +5 -6
- package/dist/stat-observation.js +3 -10
- package/dist/strict-file-identity.d.ts +2 -0
- package/dist/strict-file-identity.js +6 -6
- package/dist/temp-target.js +3 -8
- package/dist/temp-workspace-admission.js +6 -12
- package/dist/temp-workspace-child-admission.js +2 -8
- package/dist/temp-workspace-descriptor.js +1 -2
- package/dist/windows-owner.d.ts +3 -6
- package/dist/windows-security-bridge.cs +0 -2
- package/docs/secret-file.md +9 -0
- package/docs/sidecar-lock.md +2 -0
- package/package.json +8 -8
- package/dist/directory-mode-owner.d.ts +0 -20
- package/dist/directory-mode-owner.js +0 -78
package/dist/secret-file.js
CHANGED
|
@@ -5,8 +5,7 @@ import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
|
5
5
|
import { canonicalPathFromExistingAncestor } from "./absolute-path.js";
|
|
6
6
|
import { readFileDescriptorBoundedSync } from "./bounded-read.js";
|
|
7
7
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
|
|
8
|
-
import { pinNodeDirectoryForMode } from "./directory-mode-node.js";
|
|
9
|
-
import { assertOwnedDirectory } from "./directory-mode-owner.js";
|
|
8
|
+
import { pinNodeDirectoryForMode, assertOwnedDirectory } from "./directory-mode-node.js";
|
|
10
9
|
import { FsSafeError } from "./errors.js";
|
|
11
10
|
import { openPinnedFileSync } from "./pinned-open.js";
|
|
12
11
|
import { runPinnedWriteHelper } from "./pinned-write.js";
|
|
@@ -199,7 +198,11 @@ async function ensurePrivateDirectory(rootDir, targetDir, mode) {
|
|
|
199
198
|
}
|
|
200
199
|
return { rootGuard, parentGuard: targetGuard };
|
|
201
200
|
}
|
|
202
|
-
function snapshotSecretFileWriteParams(params
|
|
201
|
+
function snapshotSecretFileWriteParams(params) {
|
|
202
|
+
const rootDir = params.rootDir;
|
|
203
|
+
const filePath = params.filePath;
|
|
204
|
+
assertNoWindowsPathAlias(rootDir, "filesystem", "private secret root uses a Windows filesystem namespace alias");
|
|
205
|
+
assertNoWindowsPathAlias(filePath, "filesystem", "private secret path uses a Windows filesystem namespace alias");
|
|
203
206
|
return {
|
|
204
207
|
rootDir,
|
|
205
208
|
filePath,
|
|
@@ -305,31 +308,23 @@ async function materializeSecretFileAtomic(params, createOnly) {
|
|
|
305
308
|
});
|
|
306
309
|
}
|
|
307
310
|
export async function writeSecretFileAtomic(params) {
|
|
308
|
-
const
|
|
309
|
-
const
|
|
310
|
-
assertNoWindowsPathAlias(rootDir, "filesystem", "private secret root uses a Windows filesystem namespace alias");
|
|
311
|
-
assertNoWindowsPathAlias(filePath, "filesystem", "private secret path uses a Windows filesystem namespace alias");
|
|
312
|
-
const ownedParams = snapshotSecretFileWriteParams(params, rootDir, filePath);
|
|
313
|
-
const canonicalPath = await secretFileWriteQueueKey(filePath);
|
|
311
|
+
const ownedParams = snapshotSecretFileWriteParams(params);
|
|
312
|
+
const canonicalPath = await secretFileWriteQueueKey(ownedParams.filePath);
|
|
314
313
|
await serializePathWrite(canonicalPath, async () => {
|
|
315
314
|
await materializeSecretFileAtomic(ownedParams, false);
|
|
316
315
|
});
|
|
317
316
|
}
|
|
318
317
|
export async function createSecretFileAtomic(params) {
|
|
319
318
|
try {
|
|
320
|
-
const
|
|
321
|
-
const
|
|
322
|
-
assertNoWindowsPathAlias(rootDir, "filesystem", "private secret root uses a Windows filesystem namespace alias");
|
|
323
|
-
assertNoWindowsPathAlias(filePath, "filesystem", "private secret path uses a Windows filesystem namespace alias");
|
|
324
|
-
const ownedParams = snapshotSecretFileWriteParams(params, rootDir, filePath);
|
|
325
|
-
const canonicalPath = await secretFileWriteQueueKey(filePath);
|
|
319
|
+
const ownedParams = snapshotSecretFileWriteParams(params);
|
|
320
|
+
const canonicalPath = await secretFileWriteQueueKey(ownedParams.filePath);
|
|
326
321
|
await serializePathWrite(canonicalPath, async () => {
|
|
327
322
|
await materializeSecretFileAtomic(ownedParams, true);
|
|
328
323
|
});
|
|
329
324
|
}
|
|
330
325
|
catch (error) {
|
|
331
326
|
if ((error instanceof FsSafeError && error.code === "already-exists") ||
|
|
332
|
-
error
|
|
327
|
+
error?.code === "EEXIST") {
|
|
333
328
|
throw new FsSafeError("secret-exists", "Private secret file already exists.", { cause: error });
|
|
334
329
|
}
|
|
335
330
|
throw error;
|
|
@@ -7,7 +7,7 @@ import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
|
7
7
|
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
8
8
|
export const DEFAULT_SECRET_FILE_MAX_BYTES = 16 * 1024;
|
|
9
9
|
export function secretPathErrorCode(error) {
|
|
10
|
-
const code = error
|
|
10
|
+
const code = error?.code;
|
|
11
11
|
return code === "ENOENT" || code === "ENOTDIR" ? "not-found" : "invalid-path";
|
|
12
12
|
}
|
|
13
13
|
export function secretReadError(code, action, label, resolvedPath, error) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FsSafeError } from "./errors.js";
|
|
2
|
-
import {
|
|
2
|
+
import { fileIdentityMismatchError } from "./strict-file-identity.js";
|
|
3
3
|
import { getNativeBinding } from "./native.js";
|
|
4
4
|
import { getFsSafeNativeConfig } from "./native-config.js";
|
|
5
5
|
import { warnNativeFallback } from "./native-fallback-warning.js";
|
|
@@ -10,20 +10,15 @@ const TRUSTED_OWNER_CLASSES = new Set(["current-user", "system", "administrators
|
|
|
10
10
|
function permissionUnverified(message, cause) {
|
|
11
11
|
throw new FsSafeError("permission-unverified", message, cause === undefined ? {} : { cause });
|
|
12
12
|
}
|
|
13
|
-
function identityMismatch() {
|
|
14
|
-
const error = new FsSafeError("path-mismatch", "file identity changed or could not be verified");
|
|
15
|
-
recordFileObservationFailure(error, "identity");
|
|
16
|
-
throw error;
|
|
17
|
-
}
|
|
18
13
|
function isRecord(value) {
|
|
19
14
|
return typeof value === "object" && value !== null;
|
|
20
15
|
}
|
|
21
16
|
function parseIdentity(value) {
|
|
22
17
|
if (typeof value !== "string")
|
|
23
|
-
|
|
18
|
+
throw fileIdentityMismatchError();
|
|
24
19
|
const match = IDENTITY_RE.exec(value);
|
|
25
20
|
if (!match)
|
|
26
|
-
|
|
21
|
+
throw fileIdentityMismatchError();
|
|
27
22
|
return {
|
|
28
23
|
dev: BigInt(`0x${match[1]}`),
|
|
29
24
|
ino: BigInt(`0x${match[2]}`),
|
|
@@ -34,7 +29,7 @@ function inspectDescriptorResult(params, result, mechanism) {
|
|
|
34
29
|
permissionUnverified("Windows descriptor ACL facts were malformed");
|
|
35
30
|
const observed = parseIdentity(result.identity);
|
|
36
31
|
if (observed.dev !== params.identity.dev || observed.ino !== params.identity.ino) {
|
|
37
|
-
|
|
32
|
+
throw fileIdentityMismatchError();
|
|
38
33
|
}
|
|
39
34
|
const facts = validateSecureWindowsSecurityFacts(result.security);
|
|
40
35
|
return {
|
package/dist/secure-temp-dir.js
CHANGED
|
@@ -3,15 +3,10 @@ import { tmpdir as getOsTmpDir } from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { directoryEntryPath } from "./directory-entry-path.js";
|
|
5
5
|
import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
6
|
+
import { hasNodeErrorCode } from "./path.js";
|
|
6
7
|
import { assertSafePathSegment } from "./safe-path-segment.js";
|
|
7
8
|
import { assertNoWindowsPathAlias, pathForWindowsFilesystem } from "./windows-path-alias.js";
|
|
8
9
|
import { captureSecureTempRepairAdapter, repairSecureTempDirectory, secureTempDirectoryReceipt, } from "./secure-temp-repair.js";
|
|
9
|
-
function isNodeErrorWithCode(err, code) {
|
|
10
|
-
return (typeof err === "object" &&
|
|
11
|
-
err !== null &&
|
|
12
|
-
"code" in err &&
|
|
13
|
-
err.code === code);
|
|
14
|
-
}
|
|
15
10
|
export function resolveSecureTempRoot(options) {
|
|
16
11
|
const { fallbackPrefix: prefix, accessSync: suppliedAccess, lstatSync: suppliedLstat, mkdirSync: suppliedMkdir, descriptor: suppliedDescriptor, getuid: suppliedGetuid, tmpdir: suppliedTmpdir, platform: suppliedPlatform, preferredDir, skipPreferredOnWindows, warn: suppliedWarn, warningPrefix: suppliedWarningPrefix, unsafeFallbackLabel: suppliedFallbackLabel, } = options;
|
|
17
12
|
const platform = suppliedPlatform ?? process.platform;
|
|
@@ -87,7 +82,7 @@ export function resolveSecureTempRoot(options) {
|
|
|
87
82
|
candidate = lstatSync(candidatePath);
|
|
88
83
|
}
|
|
89
84
|
catch (error) {
|
|
90
|
-
return { kind:
|
|
85
|
+
return { kind: hasNodeErrorCode(error, "ENOENT") ? "missing" : "invalid", error };
|
|
91
86
|
}
|
|
92
87
|
let receipt;
|
|
93
88
|
try {
|
|
@@ -102,7 +102,7 @@ export async function acquireSidecarLock(options, context) {
|
|
|
102
102
|
await waitForRetry();
|
|
103
103
|
}
|
|
104
104
|
catch (waitError) {
|
|
105
|
-
if (waitError
|
|
105
|
+
if (waitError?.code === "file_lock_timeout")
|
|
106
106
|
throw denial;
|
|
107
107
|
throw waitError;
|
|
108
108
|
}
|
|
@@ -357,13 +357,12 @@ export async function removeStaleSidecarLockIfAllowed(params) {
|
|
|
357
357
|
if (params.assertGuardHeld)
|
|
358
358
|
await params.assertGuardHeld();
|
|
359
359
|
params.assertAuthorized?.();
|
|
360
|
+
if (params.lockRoot) {
|
|
361
|
+
await params.lockRoot.remove(relativeSidecarLockPath(params.lockRoot, params.lockPath), { assertBeforeMutation: params.assertAuthorized });
|
|
362
|
+
return "removed";
|
|
363
|
+
}
|
|
360
364
|
try {
|
|
361
|
-
|
|
362
|
-
await params.lockRoot.remove(relativeSidecarLockPath(params.lockRoot, params.lockPath), { assertBeforeMutation: params.assertAuthorized });
|
|
363
|
-
}
|
|
364
|
-
else {
|
|
365
|
-
await fs.rm(params.lockPath);
|
|
366
|
-
}
|
|
365
|
+
await fs.rm(params.lockPath);
|
|
367
366
|
return "removed";
|
|
368
367
|
}
|
|
369
368
|
catch (err) {
|
package/dist/stat-observation.js
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { recordFileObservationFailure } from "./file-observation.js";
|
|
3
|
-
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
4
|
-
function identityMismatch() {
|
|
5
|
-
const error = new FsSafeError("path-mismatch", "file identity changed or could not be verified");
|
|
6
|
-
recordFileObservationFailure(error, "identity");
|
|
7
|
-
throw error;
|
|
8
|
-
}
|
|
1
|
+
import { fileIdentityMismatchError, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
9
2
|
function safeNumber(value) {
|
|
10
3
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
11
4
|
}
|
|
@@ -35,7 +28,7 @@ function observeStatSync(inspect, expected, initial, platform = process.platform
|
|
|
35
28
|
const ino = numericIno ? BigInt(rawIno) : typeof rawIno === "bigint" ? rawIno : undefined;
|
|
36
29
|
if (expected && ((dev !== undefined && dev !== expected.dev) ||
|
|
37
30
|
(ino !== undefined && ino !== expected.ino)))
|
|
38
|
-
|
|
31
|
+
throw fileIdentityMismatchError();
|
|
39
32
|
if (numericDev && numericIno) {
|
|
40
33
|
return (captureIdentity
|
|
41
34
|
? { stat, identity: expected ?? { dev: dev, ino: ino } }
|
|
@@ -47,7 +40,7 @@ function observeStatSync(inspect, expected, initial, platform = process.platform
|
|
|
47
40
|
const current = inspect(true);
|
|
48
41
|
if ((dev !== undefined && current.dev !== dev) ||
|
|
49
42
|
(ino !== undefined && current.ino !== ino))
|
|
50
|
-
|
|
43
|
+
throw fileIdentityMismatchError();
|
|
51
44
|
return current;
|
|
52
45
|
}, expected, platform);
|
|
53
46
|
return (captureIdentity
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { BigIntStats } from "node:fs";
|
|
2
|
+
import { FsSafeError } from "./errors.js";
|
|
2
3
|
type ExactFileIdentity = Pick<BigIntStats, "dev" | "ino">;
|
|
4
|
+
export declare function fileIdentityMismatchError(): FsSafeError;
|
|
3
5
|
export declare function inspectFileIdentity<T extends ExactFileIdentity>(inspect: () => T | Promise<T>, expected?: ExactFileIdentity, platform?: NodeJS.Platform): Promise<T>;
|
|
4
6
|
export declare function inspectFileIdentitySync<T extends ExactFileIdentity>(inspect: () => T, expected?: ExactFileIdentity, platform?: NodeJS.Platform, mismatch?: () => Error): T;
|
|
5
7
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FsSafeError } from "./errors.js";
|
|
2
2
|
import { recordFileObservationFailure } from "./file-observation.js";
|
|
3
|
-
function
|
|
3
|
+
export function fileIdentityMismatchError() {
|
|
4
4
|
const error = new FsSafeError("path-mismatch", "file identity changed or could not be verified");
|
|
5
5
|
recordFileObservationFailure(error, "identity");
|
|
6
6
|
return error;
|
|
@@ -13,20 +13,20 @@ function identityCheck(expected, platform) {
|
|
|
13
13
|
const value = stat[field];
|
|
14
14
|
// Numeric receipts cannot recover identity bits already lost to rounding.
|
|
15
15
|
if (typeof value !== "bigint")
|
|
16
|
-
throw
|
|
16
|
+
throw fileIdentityMismatchError();
|
|
17
17
|
if (platform === "win32" && value === 0n) {
|
|
18
18
|
complete = false;
|
|
19
19
|
}
|
|
20
20
|
else {
|
|
21
21
|
if (known[field] !== undefined && known[field] !== value)
|
|
22
|
-
throw
|
|
22
|
+
throw fileIdentityMismatchError();
|
|
23
23
|
known[field] = value;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
return complete;
|
|
27
27
|
};
|
|
28
28
|
if (expected && !check(expected))
|
|
29
|
-
throw
|
|
29
|
+
throw fileIdentityMismatchError();
|
|
30
30
|
return check;
|
|
31
31
|
}
|
|
32
32
|
// Retry only unknown Windows identities, retaining every known component so a
|
|
@@ -38,9 +38,9 @@ export async function inspectFileIdentity(inspect, expected, platform = process.
|
|
|
38
38
|
if (check(stat))
|
|
39
39
|
return stat;
|
|
40
40
|
}
|
|
41
|
-
throw
|
|
41
|
+
throw fileIdentityMismatchError();
|
|
42
42
|
}
|
|
43
|
-
export function inspectFileIdentitySync(inspect, expected, platform = process.platform, mismatch =
|
|
43
|
+
export function inspectFileIdentitySync(inspect, expected, platform = process.platform, mismatch = fileIdentityMismatchError) {
|
|
44
44
|
let knownDev;
|
|
45
45
|
let knownIno;
|
|
46
46
|
if (expected) {
|
package/dist/temp-target.js
CHANGED
|
@@ -6,6 +6,7 @@ import { suffixWindowsReservedDeviceName } from "./filename.js";
|
|
|
6
6
|
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
7
7
|
import { assertSafePathSegment, sanitizeSafePathSegment, trimHyphenEdges } from "./safe-path-segment.js";
|
|
8
8
|
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
9
|
+
import { hasNodeErrorCode } from "./path.js";
|
|
9
10
|
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
10
11
|
import { assertNoWindowsPathAlias, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
|
|
11
12
|
const HYPHEN_CHAR_CODE = 0x2d;
|
|
@@ -76,12 +77,6 @@ export function buildRandomTempFilePath(params) {
|
|
|
76
77
|
assertNoWindowsPathAlias(filePath, "filesystem", "temp file path uses a Windows filesystem namespace alias");
|
|
77
78
|
return filePath;
|
|
78
79
|
}
|
|
79
|
-
function isNodeErrorWithCode(err, code) {
|
|
80
|
-
return (typeof err === "object" &&
|
|
81
|
-
err !== null &&
|
|
82
|
-
"code" in err &&
|
|
83
|
-
err.code === code);
|
|
84
|
-
}
|
|
85
80
|
async function cleanupTempDir(dir, identity, onCleanupError) {
|
|
86
81
|
try {
|
|
87
82
|
const current = fsSync.lstatSync(dir, { bigint: true });
|
|
@@ -91,7 +86,7 @@ async function cleanupTempDir(dir, identity, onCleanupError) {
|
|
|
91
86
|
await fs.rm(dir, { recursive: true, force: true });
|
|
92
87
|
}
|
|
93
88
|
catch (err) {
|
|
94
|
-
if (!
|
|
89
|
+
if (!hasNodeErrorCode(err, "ENOENT")) {
|
|
95
90
|
onCleanupError?.(err);
|
|
96
91
|
}
|
|
97
92
|
}
|
|
@@ -174,7 +169,7 @@ export async function createOwnedTempFile(params) {
|
|
|
174
169
|
await owner.cleanup();
|
|
175
170
|
}
|
|
176
171
|
catch (err) {
|
|
177
|
-
if (!
|
|
172
|
+
if (!hasNodeErrorCode(err, "ENOENT")) {
|
|
178
173
|
params.onCleanupError?.(err);
|
|
179
174
|
}
|
|
180
175
|
}
|
|
@@ -4,10 +4,9 @@ import path from "node:path";
|
|
|
4
4
|
import { inspectDirectoryIdentitySync, observeDirectoryIdentitySync, } from "./directory-guard.js";
|
|
5
5
|
import { admitTempWorkspaceChild, admitTempWorkspaceChildSync, assertTrustedTempWorkspaceDirectory, inspectTempWorkspaceDescriptorIdentitySync, projectTempWorkspaceNumericIdentity, TEMP_WORKSPACE_NUMERIC_IDENTITY_REPLAY, } from "./temp-workspace-child-admission.js";
|
|
6
6
|
import { FsSafeError } from "./errors.js";
|
|
7
|
-
import { recordFileObservationFailure } from "./file-observation.js";
|
|
8
7
|
import { realpathSync } from "./realpath.js";
|
|
9
8
|
import { assertNoWindowsPathAlias, pathForWindowsFilesystem, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
|
|
10
|
-
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
9
|
+
import { fileIdentityMismatchError, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
11
10
|
const WINDOWS = process.platform === "win32";
|
|
12
11
|
function effectiveOwner() {
|
|
13
12
|
// Windows mode/uid fields do not describe ACL authority. The supplied root's
|
|
@@ -72,11 +71,6 @@ function assertCanonicalRoot(entry) {
|
|
|
72
71
|
throw new FsSafeError("path-mismatch", "temp workspace root ancestry changed");
|
|
73
72
|
}
|
|
74
73
|
}
|
|
75
|
-
function identityMismatch() {
|
|
76
|
-
const error = new FsSafeError("path-mismatch", "file identity changed or could not be verified");
|
|
77
|
-
recordFileObservationFailure(error, "identity");
|
|
78
|
-
throw error;
|
|
79
|
-
}
|
|
80
74
|
function inspectSnapshotIdentity(entry) {
|
|
81
75
|
const expected = entry.numericIdentity;
|
|
82
76
|
if (!expected) {
|
|
@@ -87,21 +81,21 @@ function inspectSnapshotIdentity(entry) {
|
|
|
87
81
|
const devKnown = Number.isSafeInteger(stat.dev) && stat.dev >= 0 && (!WINDOWS || stat.dev !== 0);
|
|
88
82
|
if (devKnown) {
|
|
89
83
|
if (stat.dev !== expected.dev)
|
|
90
|
-
|
|
84
|
+
throw fileIdentityMismatchError();
|
|
91
85
|
}
|
|
92
86
|
else if (WINDOWS)
|
|
93
87
|
requiresExactRetry = true;
|
|
94
88
|
else
|
|
95
|
-
|
|
89
|
+
throw fileIdentityMismatchError();
|
|
96
90
|
const inoKnown = Number.isSafeInteger(stat.ino) && stat.ino >= 0 && (!WINDOWS || stat.ino !== 0);
|
|
97
91
|
if (inoKnown) {
|
|
98
92
|
if (stat.ino !== expected.ino)
|
|
99
|
-
|
|
93
|
+
throw fileIdentityMismatchError();
|
|
100
94
|
}
|
|
101
95
|
else if (WINDOWS)
|
|
102
96
|
requiresExactRetry = true;
|
|
103
97
|
else
|
|
104
|
-
|
|
98
|
+
throw fileIdentityMismatchError();
|
|
105
99
|
if (!requiresExactRetry)
|
|
106
100
|
return stat;
|
|
107
101
|
// Read exact identity only once. The strict helper may re-check this constant
|
|
@@ -157,7 +151,7 @@ function canonicalAncestry(root) {
|
|
|
157
151
|
}
|
|
158
152
|
function exactIdentityMatches(current, expected) {
|
|
159
153
|
if (current.dev !== expected.dev || current.ino !== expected.ino)
|
|
160
|
-
|
|
154
|
+
throw fileIdentityMismatchError();
|
|
161
155
|
}
|
|
162
156
|
function associateTempWorkspaceRoot(entry, ownerUid, descriptorFd) {
|
|
163
157
|
const stat = inspectTempWorkspaceDescriptorIdentitySync(descriptorFd, entry.identity, entry.numericIdentity);
|
|
@@ -2,8 +2,7 @@ import fsSync, {} from "node:fs";
|
|
|
2
2
|
import { inspectDirectoryIdentitySync, observeDirectoryIdentitySync } from "./directory-guard.js";
|
|
3
3
|
import { pinNodeDirectoryForMode, pinNodeDirectoryForModeSync } from "./directory-mode-node.js";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
|
-
import {
|
|
6
|
-
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
5
|
+
import { fileIdentityMismatchError, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
7
6
|
export const TEMP_WORKSPACE_NUMERIC_IDENTITY_REPLAY = process.platform === "linux" || process.platform === "darwin";
|
|
8
7
|
export function projectTempWorkspaceNumericIdentity(identity) {
|
|
9
8
|
const dev = Number(identity.dev);
|
|
@@ -14,15 +13,10 @@ export function projectTempWorkspaceNumericIdentity(identity) {
|
|
|
14
13
|
}
|
|
15
14
|
return Object.freeze({ dev, ino });
|
|
16
15
|
}
|
|
17
|
-
function identityMismatch() {
|
|
18
|
-
const error = new FsSafeError("path-mismatch", "file identity changed or could not be verified");
|
|
19
|
-
recordFileObservationFailure(error, "identity");
|
|
20
|
-
throw error;
|
|
21
|
-
}
|
|
22
16
|
function inspectNumericIdentity(current, expected) {
|
|
23
17
|
if (!Number.isSafeInteger(current.dev) || current.dev < 0 || current.dev !== expected.dev ||
|
|
24
18
|
!Number.isSafeInteger(current.ino) || current.ino < 0 || current.ino !== expected.ino) {
|
|
25
|
-
|
|
19
|
+
throw fileIdentityMismatchError();
|
|
26
20
|
}
|
|
27
21
|
return current;
|
|
28
22
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
-
import { nodeDirectorySearchOnlyFlags } from "./directory-mode-node.js";
|
|
4
|
-
import { assertOwnedDirectory } from "./directory-mode-owner.js";
|
|
3
|
+
import { nodeDirectorySearchOnlyFlags, assertOwnedDirectory } from "./directory-mode-node.js";
|
|
5
4
|
import { FsSafeError } from "./errors.js";
|
|
6
5
|
import { assertNoWindowsPathAlias, pathForWindowsFilesystem, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
|
|
7
6
|
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
package/dist/windows-owner.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type PermissionFailureFields } from "./permission-exec.js";
|
|
2
2
|
export type WindowsOwnerExec = (command: string, args: string[]) => Promise<{
|
|
3
3
|
stdout: string;
|
|
4
4
|
stderr: string;
|
|
5
5
|
}>;
|
|
6
|
-
export type WindowsOwnerSummary = {
|
|
6
|
+
export type WindowsOwnerSummary = Omit<PermissionFailureFields & {
|
|
7
7
|
sid?: string;
|
|
8
8
|
currentUserSid?: string;
|
|
9
9
|
daclPresent?: boolean;
|
|
@@ -11,10 +11,7 @@ export type WindowsOwnerSummary = {
|
|
|
11
11
|
aclError?: string;
|
|
12
12
|
remote?: boolean;
|
|
13
13
|
trusted?: boolean;
|
|
14
|
-
|
|
15
|
-
errorDetail?: PermissionCommandFailure;
|
|
16
|
-
errorCause?: unknown;
|
|
17
|
-
};
|
|
14
|
+
}, never>;
|
|
18
15
|
export type WindowsOwnerAce = {
|
|
19
16
|
sid: string;
|
|
20
17
|
mask: number;
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
// descriptor never passes through CommonSecurityDescriptor's ACE normalization.
|
|
3
3
|
using System;
|
|
4
4
|
using System.Collections.Generic;
|
|
5
|
-
using System.ComponentModel;
|
|
6
|
-
using System.IO;
|
|
7
5
|
using System.Runtime.InteropServices;
|
|
8
6
|
using System.Security.AccessControl;
|
|
9
7
|
using System.Security.Principal;
|
package/docs/secret-file.md
CHANGED
|
@@ -113,6 +113,10 @@ If an already validated descriptor fails while reading, both readers throw an
|
|
|
113
113
|
operational `FsSafeError` with `code: "read-failed"`; inspect `cause` for the
|
|
114
114
|
underlying Node filesystem code such as `EIO`.
|
|
115
115
|
|
|
116
|
+
Caught `null` or `undefined` inspection and read failures are reported as
|
|
117
|
+
structured errors with an `Error` cause carrying `"null"` or `"undefined"`,
|
|
118
|
+
instead of an internal `TypeError` while inspecting the thrown value.
|
|
119
|
+
|
|
116
120
|
A synchronous reader closes its descriptor once. A close failure preserves an
|
|
117
121
|
earlier read or identity-validation error; after a successful read, the close
|
|
118
122
|
failure is reported before trimming or rejecting empty content.
|
|
@@ -137,6 +141,11 @@ startWebhookVerifier(signingKey);
|
|
|
137
141
|
|
|
138
142
|
Async. Creates the parent directory at `dirMode` (default `0o700`) if missing, writes content to a sibling temp file, finalizes `mode` (default `0o600`) through an owned descriptor after content writes, and atomically renames over the destination. Publication verification checks the final file identity and mode.
|
|
139
143
|
|
|
144
|
+
Both secret writers capture top-level parameter values when called, before
|
|
145
|
+
asynchronous filesystem preparation; a supplied byte buffer is captured by
|
|
146
|
+
reference. A parameter getter throwing `null` or `undefined` rejects with that
|
|
147
|
+
same value before filesystem inspection.
|
|
148
|
+
|
|
140
149
|
On POSIX, both native and JavaScript writers verify actual `0o600` permission
|
|
141
150
|
bits through the retained descriptor before writing content. A filesystem that
|
|
142
151
|
reports successful chmod without enforcing those bits fails with
|
package/docs/sidecar-lock.md
CHANGED
|
@@ -267,6 +267,8 @@ Pass `lockRoot` to place sidecar create, read, verification, and removal behind
|
|
|
267
267
|
an existing `Root` capability. `lockPath` must resolve inside that root.
|
|
268
268
|
Identity-conditioned removal remains the only release and reclaim deletion
|
|
269
269
|
path.
|
|
270
|
+
Root mutation refusals during stale removal propagate unchanged, including
|
|
271
|
+
`null`, `undefined`, and errors carrying `ENOENT`; they do not become missing-sidecar retries.
|
|
270
272
|
|
|
271
273
|
Async Root-backed acquisition normalizes the target's parent without creating
|
|
272
274
|
it, checking the retained Root before and after normalization. A deleted or
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/fs-safe",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.2",
|
|
4
4
|
"description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"filesystem",
|
|
@@ -169,13 +169,13 @@
|
|
|
169
169
|
"archive:producer-smoke": "node scripts/archive-producer-smoke.mjs"
|
|
170
170
|
},
|
|
171
171
|
"optionalDependencies": {
|
|
172
|
-
"@openclaw/fs-safe-darwin-arm64": "0.18.
|
|
173
|
-
"@openclaw/fs-safe-darwin-x64": "0.18.
|
|
174
|
-
"@openclaw/fs-safe-linux-arm64-gnu": "0.18.
|
|
175
|
-
"@openclaw/fs-safe-linux-arm64-musl": "0.18.
|
|
176
|
-
"@openclaw/fs-safe-linux-x64-gnu": "0.18.
|
|
177
|
-
"@openclaw/fs-safe-linux-x64-musl": "0.18.
|
|
178
|
-
"@openclaw/fs-safe-win32-x64-msvc": "0.18.
|
|
172
|
+
"@openclaw/fs-safe-darwin-arm64": "0.18.2",
|
|
173
|
+
"@openclaw/fs-safe-darwin-x64": "0.18.2",
|
|
174
|
+
"@openclaw/fs-safe-linux-arm64-gnu": "0.18.2",
|
|
175
|
+
"@openclaw/fs-safe-linux-arm64-musl": "0.18.2",
|
|
176
|
+
"@openclaw/fs-safe-linux-x64-gnu": "0.18.2",
|
|
177
|
+
"@openclaw/fs-safe-linux-x64-musl": "0.18.2",
|
|
178
|
+
"@openclaw/fs-safe-win32-x64-msvc": "0.18.2",
|
|
179
179
|
"jszip": "^3.10.2"
|
|
180
180
|
},
|
|
181
181
|
"devDependencies": {
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { BigIntStats, Stats } from "node:fs";
|
|
2
|
-
export type DirectoryModeChecks = {
|
|
3
|
-
check?: () => void;
|
|
4
|
-
beforeChmod?: () => Promise<void>;
|
|
5
|
-
};
|
|
6
|
-
export type DirectoryModeOwner = {
|
|
7
|
-
verify(check?: () => void): Promise<void>;
|
|
8
|
-
apply(mode: number, checks?: DirectoryModeChecks): Promise<void>;
|
|
9
|
-
close(): Promise<void>;
|
|
10
|
-
};
|
|
11
|
-
export declare function assertOwnedDirectory(expected: Stats | BigIntStats, actual: Stats | BigIntStats): void;
|
|
12
|
-
/** Serializes use and close: even a queued path-based fd operation retains its descriptor. */
|
|
13
|
-
export declare function ownDirectoryMode(params: {
|
|
14
|
-
inspect: () => Promise<number>;
|
|
15
|
-
chmod: (mode: number) => Promise<void>;
|
|
16
|
-
prepareChmod?: () => Promise<void>;
|
|
17
|
-
verifyChmod?: () => Promise<void>;
|
|
18
|
-
close: () => Promise<void>;
|
|
19
|
-
ignoreChmodError?: boolean;
|
|
20
|
-
}): DirectoryModeOwner;
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { FsSafeError } from "./errors.js";
|
|
2
|
-
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
3
|
-
export function assertOwnedDirectory(expected, actual) {
|
|
4
|
-
if (actual.isSymbolicLink() || !actual.isDirectory()) {
|
|
5
|
-
throw new FsSafeError("not-file", "directory mode target must be a real directory");
|
|
6
|
-
}
|
|
7
|
-
if (!sameFileIdentityForCleanup(expected, actual)) {
|
|
8
|
-
throw new FsSafeError("path-mismatch", "directory changed before its mode could be applied");
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
/** Serializes use and close: even a queued path-based fd operation retains its descriptor. */
|
|
12
|
-
export function ownDirectoryMode(params) {
|
|
13
|
-
let pending = Promise.resolve();
|
|
14
|
-
let closing;
|
|
15
|
-
const enqueue = (run) => {
|
|
16
|
-
if (closing)
|
|
17
|
-
return Promise.reject(new FsSafeError("path-mismatch", "directory mode owner is closed"));
|
|
18
|
-
const operation = pending.then(run);
|
|
19
|
-
pending = operation.catch(() => undefined);
|
|
20
|
-
return operation;
|
|
21
|
-
};
|
|
22
|
-
return {
|
|
23
|
-
verify: (check) => enqueue(async () => {
|
|
24
|
-
check?.();
|
|
25
|
-
await params.inspect();
|
|
26
|
-
check?.();
|
|
27
|
-
}),
|
|
28
|
-
apply: (mode, checks = {}) => enqueue(async () => {
|
|
29
|
-
// inspect() reports permission bits only; chmod ignores file-type bits,
|
|
30
|
-
// so tolerate raw stat modes (e.g. S_IFDIR | 0o755) by masking up front.
|
|
31
|
-
mode &= 0o7777;
|
|
32
|
-
checks.check?.();
|
|
33
|
-
const currentMode = await params.inspect();
|
|
34
|
-
checks.check?.();
|
|
35
|
-
if (currentMode === mode && !checks.beforeChmod && !checks.check)
|
|
36
|
-
return;
|
|
37
|
-
if (currentMode !== mode) {
|
|
38
|
-
await params.prepareChmod?.();
|
|
39
|
-
checks.check?.();
|
|
40
|
-
}
|
|
41
|
-
await checks.beforeChmod?.();
|
|
42
|
-
checks.check?.();
|
|
43
|
-
// Hooks/ancestor checks can yield; recheck the original named association.
|
|
44
|
-
await params.inspect();
|
|
45
|
-
checks.check?.();
|
|
46
|
-
let dispatchDeadlineFailure;
|
|
47
|
-
if (currentMode !== mode) {
|
|
48
|
-
checks.check?.();
|
|
49
|
-
try {
|
|
50
|
-
await params.chmod(mode);
|
|
51
|
-
}
|
|
52
|
-
catch (error) {
|
|
53
|
-
if (!params.ignoreChmodError)
|
|
54
|
-
throw error;
|
|
55
|
-
}
|
|
56
|
-
// Expiry cannot release the fd while post-dispatch verification is pending.
|
|
57
|
-
try {
|
|
58
|
-
checks.check?.();
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
dispatchDeadlineFailure = { error };
|
|
62
|
-
}
|
|
63
|
-
await params.verifyChmod?.();
|
|
64
|
-
}
|
|
65
|
-
const finalMode = await params.inspect();
|
|
66
|
-
if (dispatchDeadlineFailure)
|
|
67
|
-
throw dispatchDeadlineFailure.error;
|
|
68
|
-
checks.check?.();
|
|
69
|
-
if (!params.ignoreChmodError && finalMode !== mode) {
|
|
70
|
-
throw new FsSafeError("path-mismatch", "directory final mode could not be verified");
|
|
71
|
-
}
|
|
72
|
-
}),
|
|
73
|
-
close() {
|
|
74
|
-
closing ??= pending.then(params.close);
|
|
75
|
-
return closing;
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
}
|