@openclaw/fs-safe 0.5.5 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/README.md +16 -10
- package/dist/advanced.d.ts +2 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -0
- package/dist/archive-crc32.d.ts +2 -0
- package/dist/archive-crc32.d.ts.map +1 -0
- package/dist/archive-crc32.js +14 -0
- package/dist/archive-kind.js +2 -2
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +7 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +24 -5
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +53 -6
- package/dist/archive-tar-pax.d.ts +8 -0
- package/dist/archive-tar-pax.d.ts.map +1 -0
- package/dist/archive-tar-pax.js +100 -0
- package/dist/archive-tar-runtime.d.ts +1 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-zip-admission.d.ts +7 -0
- package/dist/archive-zip-admission.d.ts.map +1 -0
- package/dist/archive-zip-admission.js +60 -0
- package/dist/archive-zip-count.d.ts +2 -0
- package/dist/archive-zip-count.d.ts.map +1 -0
- package/dist/archive-zip-count.js +140 -0
- package/dist/archive-zip-directory.d.ts +8 -0
- package/dist/archive-zip-directory.d.ts.map +1 -0
- package/dist/archive-zip-directory.js +224 -0
- package/dist/archive-zip-integrity.d.ts.map +1 -1
- package/dist/archive-zip-integrity.js +1 -14
- package/dist/archive-zip-names.d.ts +12 -0
- package/dist/archive-zip-names.d.ts.map +1 -0
- package/dist/archive-zip-names.js +99 -0
- package/dist/archive-zip-preflight.d.ts +1 -1
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +8 -145
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +32 -23
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +25 -19
- package/dist/file-identity.d.ts +1 -0
- package/dist/file-identity.d.ts.map +1 -1
- package/dist/file-identity.js +10 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +19 -7
- package/dist/move-path.js +2 -2
- package/dist/native-binding.d.ts +3 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts +4 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +24 -8
- package/dist/native-pinned-write-windows.d.ts +8 -0
- package/dist/native-pinned-write-windows.d.ts.map +1 -0
- package/dist/native-pinned-write-windows.js +92 -0
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +136 -127
- package/dist/native-staged-file.d.ts +24 -0
- package/dist/native-staged-file.d.ts.map +1 -0
- package/dist/native-staged-file.js +337 -0
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +4 -4
- package/dist/opened-realpath.d.ts +2 -0
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +12 -7
- package/dist/output-sibling.js +3 -3
- package/dist/owner-dacl.d.ts.map +1 -1
- package/dist/owner-dacl.js +2 -1
- package/dist/permission-exec.d.ts +19 -0
- package/dist/permission-exec.d.ts.map +1 -1
- package/dist/permission-exec.js +57 -11
- package/dist/permissions-public.d.ts +1 -1
- package/dist/permissions-public.d.ts.map +1 -1
- package/dist/permissions-windows.d.ts +3 -0
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +16 -5
- package/dist/permissions.d.ts +5 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/pinned-write.d.ts +6 -0
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +19 -29
- package/dist/private-directory.d.ts.map +1 -1
- package/dist/private-directory.js +3 -2
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +23 -15
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +2 -2
- package/dist/replace-file-descriptor.d.ts +3 -3
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +2 -2
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +84 -75
- package/dist/root-paths.d.ts +11 -14
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +36 -27
- package/dist/root-write-verification.d.ts +11 -0
- package/dist/root-write-verification.d.ts.map +1 -0
- package/dist/root-write-verification.js +91 -0
- package/dist/secret-file.d.ts +1 -6
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +43 -117
- package/dist/secret-read-async.d.ts +1 -1
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +45 -67
- package/dist/secret-read-policy.d.ts +13 -0
- package/dist/secret-read-policy.d.ts.map +1 -0
- package/dist/secret-read-policy.js +28 -0
- package/dist/secret.d.ts +2 -1
- package/dist/secret.d.ts.map +1 -1
- package/dist/secret.js +2 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +25 -16
- package/dist/sibling-temp.js +1 -1
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +4 -1
- package/dist/staged-directory.d.ts +16 -0
- package/dist/staged-directory.d.ts.map +1 -0
- package/dist/staged-directory.js +60 -0
- package/dist/staged-file-types.d.ts +56 -0
- package/dist/staged-file-types.d.ts.map +1 -0
- package/dist/staged-file-types.js +1 -0
- package/dist/staged-file.d.ts +10 -0
- package/dist/staged-file.d.ts.map +1 -0
- package/dist/staged-file.js +15 -0
- package/dist/strict-file-identity.d.ts +6 -0
- package/dist/strict-file-identity.d.ts.map +1 -0
- package/dist/strict-file-identity.js +48 -0
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +3 -3
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +9 -7
- package/dist/windows-owner.d.ts +3 -0
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +10 -2
- package/docs/advanced.md +11 -1
- package/docs/archive.md +62 -15
- package/docs/config.md +7 -0
- package/docs/contributing.md +45 -7
- package/docs/durability.md +17 -4
- package/docs/errors.md +16 -1
- package/docs/index.md +3 -1
- package/docs/install.md +15 -7
- package/docs/migrating-to-0.5.md +7 -7
- package/docs/migrating-to-0.6.md +43 -0
- package/docs/native-helper.md +15 -5
- package/docs/native.md +40 -11
- package/docs/path-scope.md +28 -2
- package/docs/permissions.md +13 -2
- package/docs/root.md +1 -1
- package/docs/secret-file.md +8 -0
- package/docs/secure-file.md +17 -1
- package/docs/security-model.md +15 -1
- package/docs/sidecar-lock.md +13 -3
- package/docs/staged-file.md +178 -0
- package/docs/testing.md +1 -1
- package/docs/writing.md +19 -0
- package/package.json +16 -9
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
package/dist/root-impl.js
CHANGED
|
@@ -5,11 +5,11 @@ import path from "node:path";
|
|
|
5
5
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
|
|
6
6
|
import { FsSafeError } from "./errors.js";
|
|
7
7
|
import { syncDirectoryBestEffort } from "./fsync.js";
|
|
8
|
-
import { sameFileIdentity } from "./file-identity.js";
|
|
8
|
+
import { sameFileIdentity, sameFileIdentityForCleanup, } from "./file-identity.js";
|
|
9
9
|
import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
|
|
10
10
|
import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
|
|
11
11
|
import { assertMutationNotDenied, mergeDenyMutationPolicies, } from "./deny-mutations.js";
|
|
12
|
-
import { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
|
|
12
|
+
import { resolveOpenedFileRealPathForFd, resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
|
|
13
13
|
import { runPinnedWriteHelper, runPinnedWriteWithRenamePolicy, } from "./pinned-write.js";
|
|
14
14
|
import { getNativeBinding } from "./native.js";
|
|
15
15
|
import { validatePinnedOperationPayload } from "./pinned-operation.js";
|
|
@@ -25,6 +25,8 @@ import { stringifyJsonDocument } from "./json-stringify.js";
|
|
|
25
25
|
import { walkRoot } from "./root-walk.js";
|
|
26
26
|
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
27
27
|
import { serializePathWrite } from "./write-queue.js";
|
|
28
|
+
import { verifyAtomicWriteResult } from "./root-write-verification.js";
|
|
29
|
+
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
28
30
|
export { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
|
|
29
31
|
function logWarn(message) {
|
|
30
32
|
if (process.env.FS_SAFE_DEBUG_WARNINGS === "1") {
|
|
@@ -74,22 +76,27 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
74
76
|
assertNoUnsafeDeviceReadPath(filePath);
|
|
75
77
|
const fsSafeTestHooks = getFsSafeTestHooks();
|
|
76
78
|
let preOpenStat;
|
|
79
|
+
let observedBeforeOpen = false;
|
|
77
80
|
// Reject directories before opening so we never surface EISDIR to callers (e.g. tool
|
|
78
81
|
// results that get sent to messaging channels). See openclaw/openclaw#31186.
|
|
79
82
|
try {
|
|
80
|
-
preOpenStat = await
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
preOpenStat = await inspectFileIdentity(async () => {
|
|
84
|
+
const stat = await fs.lstat(filePath, { bigint: true });
|
|
85
|
+
observedBeforeOpen = true;
|
|
86
|
+
if (stat.isSymbolicLink() && options?.symlinks !== "follow-within-root") {
|
|
87
|
+
throw new FsSafeError("symlink", "symlink not allowed");
|
|
88
|
+
}
|
|
89
|
+
if (!stat.isFile() && !stat.isSymbolicLink()) {
|
|
90
|
+
throw new FsSafeError("not-file", "not a file");
|
|
91
|
+
}
|
|
92
|
+
return stat;
|
|
93
|
+
});
|
|
87
94
|
}
|
|
88
95
|
catch (err) {
|
|
89
|
-
if (err instanceof FsSafeError) {
|
|
96
|
+
if (err instanceof FsSafeError || observedBeforeOpen) {
|
|
90
97
|
throw err;
|
|
91
98
|
}
|
|
92
|
-
//
|
|
99
|
+
// Only an initial lookup failure falls through; a failed re-inspection aborts.
|
|
93
100
|
}
|
|
94
101
|
if (preOpenStat) {
|
|
95
102
|
await fsSafeTestHooks?.afterPreOpenLstat?.(filePath);
|
|
@@ -127,37 +134,28 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
127
134
|
if (!stat.isFile()) {
|
|
128
135
|
throw new FsSafeError("not-file", "not a file");
|
|
129
136
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
!sameFileIdentity(stat, preOpenStat)) {
|
|
133
|
-
throw new FsSafeError("path-mismatch", "path changed before open");
|
|
134
|
-
}
|
|
137
|
+
// Keep numeric Stats for the public receipt, never for identity verification.
|
|
138
|
+
const identity = await inspectFileIdentity(() => handle.stat({ bigint: true }), preOpenStat && !preOpenStat.isSymbolicLink() ? preOpenStat : undefined);
|
|
135
139
|
if (options?.hardlinks === "reject" && stat.nlink > 1) {
|
|
136
140
|
throw hardlinkedPathNotAllowedError();
|
|
137
141
|
}
|
|
138
|
-
|
|
139
|
-
const pathStat =
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
const pathStat = await fs.lstat(filePath);
|
|
146
|
-
if (pathStat.isSymbolicLink()) {
|
|
142
|
+
await inspectFileIdentity(async () => {
|
|
143
|
+
const pathStat = options?.symlinks === "follow-within-root"
|
|
144
|
+
? await fs.stat(filePath, { bigint: true })
|
|
145
|
+
: await fs.lstat(filePath, { bigint: true });
|
|
146
|
+
if (pathStat.isSymbolicLink() && options?.symlinks !== "follow-within-root") {
|
|
147
147
|
throw new FsSafeError("symlink", "symlink not allowed");
|
|
148
148
|
}
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
return pathStat;
|
|
150
|
+
}, identity);
|
|
151
|
+
const realPath = await resolveOpenedFileRealPathForFd(handle.fd, identity, filePath);
|
|
152
|
+
await inspectFileIdentity(async () => {
|
|
153
|
+
const realStat = await fs.stat(realPath, { bigint: true });
|
|
154
|
+
if (options?.hardlinks === "reject" && realStat.nlink > 1n) {
|
|
155
|
+
throw hardlinkedPathNotAllowedError();
|
|
151
156
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const realStat = await fs.stat(realPath);
|
|
155
|
-
if (options?.hardlinks === "reject" && realStat.nlink > 1) {
|
|
156
|
-
throw hardlinkedPathNotAllowedError();
|
|
157
|
-
}
|
|
158
|
-
if (!sameFileIdentity(stat, realStat)) {
|
|
159
|
-
throw new FsSafeError("path-mismatch", "path mismatch");
|
|
160
|
-
}
|
|
157
|
+
return realStat;
|
|
158
|
+
}, identity);
|
|
161
159
|
return openResult({ handle, realPath, stat });
|
|
162
160
|
}
|
|
163
161
|
catch (err) {
|
|
@@ -473,24 +471,14 @@ async function writeTempFileForAtomicReplace(params) {
|
|
|
473
471
|
else {
|
|
474
472
|
await tempHandle.writeFile(params.data);
|
|
475
473
|
}
|
|
476
|
-
return
|
|
474
|
+
return {
|
|
475
|
+
handle: tempHandle,
|
|
476
|
+
identity: await tempHandle.stat({ bigint: true }),
|
|
477
|
+
};
|
|
477
478
|
}
|
|
478
|
-
|
|
479
|
+
catch (error) {
|
|
479
480
|
await tempHandle.close().catch(() => { });
|
|
480
|
-
|
|
481
|
-
}
|
|
482
|
-
async function verifyAtomicWriteResult(params) {
|
|
483
|
-
const opened = await openVerifiedLocalFile(params.targetPath, { hardlinks: "reject" });
|
|
484
|
-
try {
|
|
485
|
-
if (!sameFileIdentity(opened.stat, params.expectedIdentity)) {
|
|
486
|
-
throw new FsSafeError("path-mismatch", "path changed during write");
|
|
487
|
-
}
|
|
488
|
-
if (!isPathInside(params.root.rootWithSep, opened.realPath)) {
|
|
489
|
-
throw outsideWorkspaceError();
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
finally {
|
|
493
|
-
await opened.handle.close().catch(() => { });
|
|
481
|
+
throw error;
|
|
494
482
|
}
|
|
495
483
|
}
|
|
496
484
|
async function resolveGuardedWritePathInRoot(root, params) {
|
|
@@ -706,9 +694,9 @@ async function writeFileInRoot(root, params) {
|
|
|
706
694
|
});
|
|
707
695
|
}
|
|
708
696
|
async function commitPinnedWriteInRoot(root, pinned, params) {
|
|
709
|
-
let
|
|
697
|
+
let verifyingPublication = false;
|
|
710
698
|
try {
|
|
711
|
-
|
|
699
|
+
await runPinnedWriteWithRenamePolicy({
|
|
712
700
|
rootPath: pinned.rootReal,
|
|
713
701
|
relativeParentPath: pinned.relativeParentPath,
|
|
714
702
|
basename: pinned.basename,
|
|
@@ -719,9 +707,27 @@ async function commitPinnedWriteInRoot(root, pinned, params) {
|
|
|
719
707
|
overwrite: params.overwrite,
|
|
720
708
|
input: { kind: "buffer", data: params.data, encoding: params.encoding },
|
|
721
709
|
rootIdentity: root.rootIdentity,
|
|
710
|
+
verifyPublished: async (fd, expectedIdentity, parentGuard) => {
|
|
711
|
+
verifyingPublication = true;
|
|
712
|
+
try {
|
|
713
|
+
await verifyAtomicWriteResult({
|
|
714
|
+
root,
|
|
715
|
+
targetPath: pinned.targetPath,
|
|
716
|
+
fd,
|
|
717
|
+
expectedIdentity,
|
|
718
|
+
parentGuard,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
emitWriteBoundaryWarning(`post-write verification failed: ${String(error)}`);
|
|
723
|
+
throw error;
|
|
724
|
+
}
|
|
725
|
+
},
|
|
722
726
|
});
|
|
723
727
|
}
|
|
724
728
|
catch (error) {
|
|
729
|
+
if (verifyingPublication)
|
|
730
|
+
throw error;
|
|
725
731
|
const errorCode = error?.code;
|
|
726
732
|
if (errorCode === "file_lock_stale" || errorCode === "file_lock_timeout") {
|
|
727
733
|
throw error;
|
|
@@ -733,17 +739,6 @@ async function commitPinnedWriteInRoot(root, pinned, params) {
|
|
|
733
739
|
}
|
|
734
740
|
throw normalizePinnedWriteError(error);
|
|
735
741
|
}
|
|
736
|
-
try {
|
|
737
|
-
await verifyAtomicWriteResult({
|
|
738
|
-
root,
|
|
739
|
-
targetPath: pinned.targetPath,
|
|
740
|
-
expectedIdentity: identity,
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
catch (err) {
|
|
744
|
-
emitWriteBoundaryWarning(`post-write verification failed: ${String(err)}`);
|
|
745
|
-
throw err;
|
|
746
|
-
}
|
|
747
742
|
}
|
|
748
743
|
async function copyFileInRoot(root, params) {
|
|
749
744
|
assertValidRootRelativePath(params.relativePath);
|
|
@@ -807,7 +802,7 @@ async function assertCopySourcePathCurrent(source) {
|
|
|
807
802
|
async function removePathIfIdentityUnchanged(targetPath, identity) {
|
|
808
803
|
const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
|
|
809
804
|
const current = await fs.lstat(targetPath);
|
|
810
|
-
if (current.isSymbolicLink() || !
|
|
805
|
+
if (current.isSymbolicLink() || !sameFileIdentityForCleanup(current, identity)) {
|
|
811
806
|
return;
|
|
812
807
|
}
|
|
813
808
|
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
@@ -860,7 +855,7 @@ async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode,
|
|
|
860
855
|
targetPath: resolved,
|
|
861
856
|
relativeParentPath: path.posix.dirname(relativePosix) === "." ? "" : path.posix.dirname(relativePosix),
|
|
862
857
|
basename,
|
|
863
|
-
mode
|
|
858
|
+
mode,
|
|
864
859
|
};
|
|
865
860
|
}
|
|
866
861
|
async function resolvePinnedPathInRoot(root, params) {
|
|
@@ -1105,28 +1100,32 @@ async function writeFileFallback(root, params) {
|
|
|
1105
1100
|
const destinationGuard = await createAsyncDirectoryGuard(path.dirname(destinationPath));
|
|
1106
1101
|
let tempPath = null;
|
|
1107
1102
|
let unregisterTempPath = null;
|
|
1103
|
+
let writtenHandle;
|
|
1108
1104
|
try {
|
|
1109
1105
|
tempPath = buildAtomicWriteTempPath(destinationPath);
|
|
1110
1106
|
unregisterTempPath = registerTempPathForExit(tempPath);
|
|
1111
|
-
const
|
|
1107
|
+
const written = await writeTempFileForAtomicReplace({
|
|
1112
1108
|
tempPath,
|
|
1113
1109
|
data: params.data,
|
|
1114
1110
|
encoding: params.encoding,
|
|
1115
|
-
mode
|
|
1111
|
+
mode,
|
|
1116
1112
|
});
|
|
1117
|
-
|
|
1113
|
+
writtenHandle = written.handle;
|
|
1114
|
+
unregisterTempPath.setIdentity(written.identity);
|
|
1118
1115
|
const commitTempPath = tempPath;
|
|
1119
1116
|
await withAsyncDirectoryGuards([destinationGuard], async () => {
|
|
1120
1117
|
await fs.rename(commitTempPath, destinationPath);
|
|
1118
|
+
tempPath = null;
|
|
1121
1119
|
});
|
|
1122
|
-
tempPath = null;
|
|
1123
1120
|
unregisterTempPath();
|
|
1124
1121
|
unregisterTempPath = null;
|
|
1125
1122
|
try {
|
|
1126
1123
|
await verifyAtomicWriteResult({
|
|
1127
1124
|
root,
|
|
1128
1125
|
targetPath: destinationPath,
|
|
1129
|
-
expectedIdentity:
|
|
1126
|
+
expectedIdentity: written.identity,
|
|
1127
|
+
fd: written.handle.fd,
|
|
1128
|
+
parentGuard: destinationGuard,
|
|
1130
1129
|
});
|
|
1131
1130
|
}
|
|
1132
1131
|
catch (err) {
|
|
@@ -1135,6 +1134,7 @@ async function writeFileFallback(root, params) {
|
|
|
1135
1134
|
}
|
|
1136
1135
|
}
|
|
1137
1136
|
finally {
|
|
1137
|
+
await writtenHandle?.close().catch(() => undefined);
|
|
1138
1138
|
if (tempPath) {
|
|
1139
1139
|
await fs.rm(tempPath, { force: true }).catch(() => { });
|
|
1140
1140
|
}
|
|
@@ -1152,19 +1152,22 @@ async function writeMissingFileFallback(root, params) {
|
|
|
1152
1152
|
const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
|
|
1153
1153
|
let created = false;
|
|
1154
1154
|
let createdIdentity;
|
|
1155
|
+
let writtenHandle;
|
|
1156
|
+
let verifyingPublication = false;
|
|
1155
1157
|
try {
|
|
1156
1158
|
const { handle, writtenStat } = await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
1157
1159
|
const handle = await fs.open(targetPath, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600);
|
|
1158
1160
|
created = true;
|
|
1159
1161
|
try {
|
|
1160
1162
|
createdIdentity = await handle.stat();
|
|
1163
|
+
const writtenStat = await handle.stat({ bigint: true });
|
|
1161
1164
|
if (typeof params.data === "string") {
|
|
1162
1165
|
await handle.writeFile(params.data, params.encoding ?? "utf8");
|
|
1163
1166
|
}
|
|
1164
1167
|
else {
|
|
1165
1168
|
await handle.writeFile(params.data);
|
|
1166
1169
|
}
|
|
1167
|
-
return { handle, writtenStat
|
|
1170
|
+
return { handle, writtenStat };
|
|
1168
1171
|
}
|
|
1169
1172
|
catch (error) {
|
|
1170
1173
|
await handle.close().catch(() => undefined);
|
|
@@ -1176,15 +1179,20 @@ async function writeMissingFileFallback(root, params) {
|
|
|
1176
1179
|
await handle.close().catch(() => undefined);
|
|
1177
1180
|
},
|
|
1178
1181
|
});
|
|
1179
|
-
|
|
1182
|
+
writtenHandle = handle;
|
|
1183
|
+
created = false;
|
|
1184
|
+
verifyingPublication = true;
|
|
1180
1185
|
await verifyAtomicWriteResult({
|
|
1181
1186
|
root,
|
|
1182
1187
|
targetPath,
|
|
1183
1188
|
expectedIdentity: writtenStat,
|
|
1189
|
+
fd: handle.fd,
|
|
1190
|
+
parentGuard,
|
|
1184
1191
|
});
|
|
1185
|
-
created = false;
|
|
1186
1192
|
}
|
|
1187
1193
|
catch (err) {
|
|
1194
|
+
if (verifyingPublication)
|
|
1195
|
+
throw err;
|
|
1188
1196
|
if (hasNodeErrorCode(err, "EEXIST")) {
|
|
1189
1197
|
throw new FsSafeError("already-exists", "file already exists", {
|
|
1190
1198
|
cause: err instanceof Error ? err : undefined,
|
|
@@ -1193,6 +1201,7 @@ async function writeMissingFileFallback(root, params) {
|
|
|
1193
1201
|
throw err;
|
|
1194
1202
|
}
|
|
1195
1203
|
finally {
|
|
1204
|
+
await writtenHandle?.close().catch(() => undefined);
|
|
1196
1205
|
if (created && createdIdentity) {
|
|
1197
1206
|
await removePathIfIdentityUnchanged(targetPath, createdIdentity).catch(() => undefined);
|
|
1198
1207
|
}
|
package/dist/root-paths.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
1
2
|
type InvalidPathResult = {
|
|
2
3
|
ok: false;
|
|
3
4
|
error: string;
|
|
4
5
|
};
|
|
6
|
+
type DirectoryResult = {
|
|
7
|
+
ok: true;
|
|
8
|
+
path: string;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
error: string;
|
|
12
|
+
diagnostic?: FsSafeError;
|
|
13
|
+
};
|
|
5
14
|
type ResolvePathsWithinRootParams = {
|
|
6
15
|
rootDir: string;
|
|
7
16
|
requestedPaths: string[];
|
|
@@ -39,13 +48,7 @@ export type PathScope = {
|
|
|
39
48
|
}>;
|
|
40
49
|
ensureDir(requestedPath: string, options?: PathScopeResolveOptions & {
|
|
41
50
|
mode?: number;
|
|
42
|
-
}): Promise<
|
|
43
|
-
ok: true;
|
|
44
|
-
path: string;
|
|
45
|
-
} | {
|
|
46
|
-
ok: false;
|
|
47
|
-
error: string;
|
|
48
|
-
}>;
|
|
51
|
+
}): Promise<DirectoryResult>;
|
|
49
52
|
};
|
|
50
53
|
export declare function resolvePathWithinRoot(params: {
|
|
51
54
|
rootDir: string;
|
|
@@ -77,13 +80,7 @@ export declare function ensureDirectoryWithinRoot(params: {
|
|
|
77
80
|
scopeLabel: string;
|
|
78
81
|
defaultDirName?: string;
|
|
79
82
|
mode?: number;
|
|
80
|
-
}): Promise<
|
|
81
|
-
ok: true;
|
|
82
|
-
path: string;
|
|
83
|
-
} | {
|
|
84
|
-
ok: false;
|
|
85
|
-
error: string;
|
|
86
|
-
}>;
|
|
83
|
+
}): Promise<DirectoryResult>;
|
|
87
84
|
export declare function resolvePathsWithinRoot(params: ResolvePathsWithinRootParams): ResolvePathsWithinRootResult;
|
|
88
85
|
export declare function resolveExistingPathsWithinRoot(params: ResolvePathsWithinRootParams): Promise<ResolvePathsWithinRootResult>;
|
|
89
86
|
export declare function resolveStrictExistingPathsWithinRoot(params: ResolvePathsWithinRootParams): Promise<ResolvePathsWithinRootResult>;
|
package/dist/root-paths.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root-paths.d.ts","sourceRoot":"","sources":["../src/root-paths.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"root-paths.d.ts","sourceRoot":"","sources":["../src/root-paths.ts"],"names":[],"mappings":"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"}
|
package/dist/root-paths.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { formatErrorDetail } from "./error-detail.js";
|
|
3
4
|
import { FsSafeError } from "./errors.js";
|
|
4
|
-
import { isNotFoundPathError, isPathInside, isPathRelativeEscape } from "./path.js";
|
|
5
|
+
import { assertNoNulPathInput, hasNodeErrorCode, isNodeError, isNotFoundPathError, isPathInside, isPathRelativeEscape, } from "./path.js";
|
|
5
6
|
import { root as openRoot } from "./root.js";
|
|
6
7
|
function invalidPath(scopeLabel) {
|
|
7
8
|
return {
|
|
@@ -126,7 +127,7 @@ async function resolveNearestExistingPath(targetPath) {
|
|
|
126
127
|
async function assertNoSymlinkSegments(params) {
|
|
127
128
|
const relative = path.relative(params.rootDir, params.targetPath);
|
|
128
129
|
if (isPathRelativeEscape(relative)) {
|
|
129
|
-
throw new
|
|
130
|
+
throw new FsSafeError("outside-workspace", `Invalid path: must stay within ${params.scopeLabel}`);
|
|
130
131
|
}
|
|
131
132
|
let current = params.rootDir;
|
|
132
133
|
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
@@ -134,10 +135,10 @@ async function assertNoSymlinkSegments(params) {
|
|
|
134
135
|
try {
|
|
135
136
|
const stat = await fs.lstat(current);
|
|
136
137
|
if (stat.isSymbolicLink()) {
|
|
137
|
-
throw new
|
|
138
|
+
throw new FsSafeError("symlink", `Invalid path: must not traverse symlinks within ${params.scopeLabel}`);
|
|
138
139
|
}
|
|
139
140
|
if (!stat.isDirectory()) {
|
|
140
|
-
throw new
|
|
141
|
+
throw new FsSafeError("not-file", `Invalid path: existing segment must be a directory within ${params.scopeLabel}`);
|
|
141
142
|
}
|
|
142
143
|
}
|
|
143
144
|
catch (err) {
|
|
@@ -149,28 +150,29 @@ async function assertNoSymlinkSegments(params) {
|
|
|
149
150
|
}
|
|
150
151
|
}
|
|
151
152
|
export async function ensureDirectoryWithinRoot(params) {
|
|
152
|
-
const
|
|
153
|
-
rootDir: params.rootDir,
|
|
154
|
-
requestedPath: params.requestedPath,
|
|
155
|
-
scopeLabel: params.scopeLabel,
|
|
156
|
-
defaultFileName: params.defaultDirName,
|
|
157
|
-
});
|
|
158
|
-
if (!lexical.ok) {
|
|
159
|
-
return lexical;
|
|
160
|
-
}
|
|
161
|
-
const rootDir = path.resolve(params.rootDir);
|
|
162
|
-
const targetPath = path.resolve(lexical.path);
|
|
153
|
+
const scopeLabel = formatErrorDetail(params.scopeLabel.slice(0, 80));
|
|
163
154
|
try {
|
|
155
|
+
assertNoNulPathInput(params.rootDir);
|
|
156
|
+
assertNoNulPathInput(params.requestedPath);
|
|
157
|
+
if (!params.requestedPath.trim())
|
|
158
|
+
assertNoNulPathInput(params.defaultDirName ?? "");
|
|
159
|
+
const lexical = resolvePathWithinRoot({
|
|
160
|
+
...params, scopeLabel, defaultFileName: params.defaultDirName,
|
|
161
|
+
});
|
|
162
|
+
if (!lexical.ok)
|
|
163
|
+
return lexical;
|
|
164
|
+
const rootDir = path.resolve(params.rootDir);
|
|
165
|
+
const targetPath = lexical.path;
|
|
164
166
|
const rootStat = await fs.lstat(rootDir);
|
|
165
167
|
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
166
|
-
return invalidPath(
|
|
168
|
+
return invalidPath(scopeLabel);
|
|
167
169
|
}
|
|
168
|
-
await assertNoSymlinkSegments({ rootDir, targetPath, scopeLabel
|
|
170
|
+
await assertNoSymlinkSegments({ rootDir, targetPath, scopeLabel });
|
|
169
171
|
const rootReal = await fs.realpath(rootDir);
|
|
170
172
|
const nearestExistingPath = await resolveNearestExistingPath(targetPath);
|
|
171
173
|
const nearestExistingReal = await fs.realpath(nearestExistingPath);
|
|
172
174
|
if (!isPathInside(rootReal, nearestExistingReal)) {
|
|
173
|
-
return invalidPath(
|
|
175
|
+
return invalidPath(scopeLabel);
|
|
174
176
|
}
|
|
175
177
|
const relative = path.relative(rootDir, targetPath);
|
|
176
178
|
let current = rootDir;
|
|
@@ -180,7 +182,7 @@ export async function ensureDirectoryWithinRoot(params) {
|
|
|
180
182
|
try {
|
|
181
183
|
const stat = await fs.lstat(current);
|
|
182
184
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
183
|
-
return invalidPath(
|
|
185
|
+
return invalidPath(scopeLabel);
|
|
184
186
|
}
|
|
185
187
|
break;
|
|
186
188
|
}
|
|
@@ -192,10 +194,7 @@ export async function ensureDirectoryWithinRoot(params) {
|
|
|
192
194
|
await fs.mkdir(current, { mode: params.mode });
|
|
193
195
|
}
|
|
194
196
|
catch (mkdirErr) {
|
|
195
|
-
if (
|
|
196
|
-
throw mkdirErr;
|
|
197
|
-
}
|
|
198
|
-
if (mkdirErr.code === "EEXIST") {
|
|
197
|
+
if (hasNodeErrorCode(mkdirErr, "EEXIST")) {
|
|
199
198
|
continue;
|
|
200
199
|
}
|
|
201
200
|
throw mkdirErr;
|
|
@@ -204,17 +203,27 @@ export async function ensureDirectoryWithinRoot(params) {
|
|
|
204
203
|
}
|
|
205
204
|
const currentReal = await fs.realpath(current);
|
|
206
205
|
if (!isPathInside(rootReal, currentReal)) {
|
|
207
|
-
return invalidPath(
|
|
206
|
+
return invalidPath(scopeLabel);
|
|
208
207
|
}
|
|
209
208
|
}
|
|
210
209
|
const targetReal = await fs.realpath(targetPath);
|
|
211
210
|
if (!isPathInside(rootReal, targetReal)) {
|
|
212
|
-
return invalidPath(
|
|
211
|
+
return invalidPath(scopeLabel);
|
|
213
212
|
}
|
|
214
213
|
return { ok: true, path: targetPath };
|
|
215
214
|
}
|
|
216
|
-
catch {
|
|
217
|
-
|
|
215
|
+
catch (cause) {
|
|
216
|
+
if ((cause instanceof FsSafeError && cause.category === "policy") ||
|
|
217
|
+
hasNodeErrorCode(cause, "ENOTDIR")) {
|
|
218
|
+
return invalidPath(scopeLabel);
|
|
219
|
+
}
|
|
220
|
+
// Native messages include unbounded paths; display only bounded errno metadata.
|
|
221
|
+
const code = isNodeError(cause) && typeof cause.code === "string"
|
|
222
|
+
? formatErrorDetail(cause.code.slice(0, 40)) : "filesystem operation failed";
|
|
223
|
+
const syscall = isNodeError(cause) && typeof cause.syscall === "string"
|
|
224
|
+
? ` during ${formatErrorDetail(cause.syscall.slice(0, 40))}` : "";
|
|
225
|
+
const diagnostic = new FsSafeError("helper-failed", `Could not prepare ${scopeLabel}: ${code}${syscall}`, { cause });
|
|
226
|
+
return { ok: false, error: diagnostic.message, diagnostic };
|
|
218
227
|
}
|
|
219
228
|
}
|
|
220
229
|
export function resolvePathsWithinRoot(params) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type AsyncDirectoryGuard } from "./directory-guard.js";
|
|
2
|
+
import type { PublishedWriteIdentity } from "./pinned-write.js";
|
|
3
|
+
import { type RootContext } from "./root-context.js";
|
|
4
|
+
export declare function verifyAtomicWriteResult(params: {
|
|
5
|
+
root: RootContext;
|
|
6
|
+
targetPath: string;
|
|
7
|
+
fd: number;
|
|
8
|
+
expectedIdentity: PublishedWriteIdentity;
|
|
9
|
+
parentGuard: AsyncDirectoryGuard;
|
|
10
|
+
}): Promise<void>;
|
|
11
|
+
//# sourceMappingURL=root-write-verification.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root-write-verification.d.ts","sourceRoot":"","sources":["../src/root-write-verification.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAK3F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAEhE,OAAO,EAA6B,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhF,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,WAAW,EAAE,mBAAmB,CAAC;CAClC,GAAG,OAAO,CAAC,IAAI,CAAC,CA6EhB"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import fsSync, {} from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { assertAsyncDirectoryGuard } from "./directory-guard.js";
|
|
4
|
+
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { sameFileIdentity } from "./file-identity.js";
|
|
6
|
+
import { resolveOpenedFileRealPathForFd } from "./opened-realpath.js";
|
|
7
|
+
import { assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isPathInside, isSymlinkOpenError } from "./path.js";
|
|
8
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
9
|
+
import { assertRootIdentityCurrent } from "./root-context.js";
|
|
10
|
+
import { fileNotFoundError, hardlinkedPathNotAllowedError, outsideWorkspaceError } from "./root-errors.js";
|
|
11
|
+
export async function verifyAtomicWriteResult(params) {
|
|
12
|
+
let needsPathOpen = false;
|
|
13
|
+
const assertFile = (stat) => {
|
|
14
|
+
if (stat.isSymbolicLink()) {
|
|
15
|
+
throw new FsSafeError("symlink", "symlink not allowed");
|
|
16
|
+
}
|
|
17
|
+
if (!stat.isFile()) {
|
|
18
|
+
throw new FsSafeError("not-file", "not a file");
|
|
19
|
+
}
|
|
20
|
+
if (!sameFileIdentity(stat, params.expectedIdentity)) {
|
|
21
|
+
throw new FsSafeError("path-mismatch", "path changed during write");
|
|
22
|
+
}
|
|
23
|
+
if (stat.nlink > 1) {
|
|
24
|
+
throw hardlinkedPathNotAllowedError();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const assertPath = (stat) => {
|
|
28
|
+
assertFile(stat);
|
|
29
|
+
needsPathOpen ||= process.platform === "win32" && (stat.dev === 0n || stat.ino === 0n);
|
|
30
|
+
};
|
|
31
|
+
const assertDescriptor = (fd = params.fd) => {
|
|
32
|
+
const stat = fsSync.fstatSync(fd, { bigint: true });
|
|
33
|
+
// Only Windows pathname stats may have unknown identity; fd stats must match exactly.
|
|
34
|
+
if (params.expectedIdentity.dev !== stat.dev || params.expectedIdentity.ino !== stat.ino) {
|
|
35
|
+
throw new FsSafeError("path-mismatch", "descriptor changed during write");
|
|
36
|
+
}
|
|
37
|
+
assertFile(stat);
|
|
38
|
+
return stat;
|
|
39
|
+
};
|
|
40
|
+
try {
|
|
41
|
+
// This descriptor remains owned by the writer, even when final mode forbids opens.
|
|
42
|
+
const stat = assertDescriptor();
|
|
43
|
+
assertPath(await fs.lstat(params.targetPath, { bigint: true }));
|
|
44
|
+
const realPath = await resolveOpenedFileRealPathForFd(params.fd, stat, params.targetPath);
|
|
45
|
+
assertPath(await fs.stat(realPath, { bigint: true }));
|
|
46
|
+
if (!isPathInside(params.root.rootWithSep, realPath)) {
|
|
47
|
+
throw outsideWorkspaceError();
|
|
48
|
+
}
|
|
49
|
+
await assertAsyncDirectoryGuard(params.parentGuard);
|
|
50
|
+
await assertRootIdentityCurrent(params.root);
|
|
51
|
+
// Recheck after canonical resolution and directory checks, including late links.
|
|
52
|
+
assertPath(await fs.lstat(params.targetPath, { bigint: true }));
|
|
53
|
+
assertDescriptor();
|
|
54
|
+
if (needsPathOpen) {
|
|
55
|
+
// A retained fd cannot prove that an opaque Windows pathname still names it.
|
|
56
|
+
// Reopen only for publication verification: the writer's exact identity is
|
|
57
|
+
// independent proof unavailable to ordinary readers. Never read any bytes.
|
|
58
|
+
assertNoUnsafeDeviceReadPath(params.targetPath);
|
|
59
|
+
const opened = await fs.open(params.targetPath, resolveReadOpenFlags()).catch((error) => {
|
|
60
|
+
if (isSymlinkOpenError(error)) {
|
|
61
|
+
throw new FsSafeError("symlink", "symlink open blocked", { cause: error });
|
|
62
|
+
}
|
|
63
|
+
if (hasNodeErrorCode(error, "EISDIR")) {
|
|
64
|
+
throw new FsSafeError("not-file", "not a file");
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
const reopenedStat = assertDescriptor(opened.fd);
|
|
70
|
+
assertPath(await fs.lstat(params.targetPath, { bigint: true }));
|
|
71
|
+
const reopenedPath = await resolveOpenedFileRealPathForFd(opened.fd, reopenedStat, params.targetPath);
|
|
72
|
+
assertPath(await fs.stat(reopenedPath, { bigint: true }));
|
|
73
|
+
if (!isPathInside(params.root.rootWithSep, reopenedPath)) {
|
|
74
|
+
throw outsideWorkspaceError();
|
|
75
|
+
}
|
|
76
|
+
await assertAsyncDirectoryGuard(params.parentGuard);
|
|
77
|
+
await assertRootIdentityCurrent(params.root);
|
|
78
|
+
assertPath(await fs.lstat(params.targetPath, { bigint: true }));
|
|
79
|
+
assertDescriptor(opened.fd);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
await opened.close().catch(() => undefined);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (isNotFoundPathError(error))
|
|
88
|
+
throw fileNotFoundError();
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
package/dist/secret-file.d.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { type SecretFileReadOptions } from "./secret-read-policy.js";
|
|
2
2
|
export declare const PRIVATE_SECRET_DIR_MODE = 448;
|
|
3
3
|
export declare const PRIVATE_SECRET_FILE_MODE = 384;
|
|
4
|
-
export type SecretFileReadOptions = {
|
|
5
|
-
maxBytes?: number;
|
|
6
|
-
rejectSymlink?: boolean;
|
|
7
|
-
rejectHardlinks?: boolean;
|
|
8
|
-
};
|
|
9
4
|
export declare function readSecretFileSync(filePath: string, label: string, options?: SecretFileReadOptions): string;
|
|
10
5
|
export declare function tryReadSecretFileSync(filePath: string | undefined, label: string, options?: SecretFileReadOptions): string | undefined;
|
|
11
6
|
type SecretFileWriteParams = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAWA,OAAO,EAML,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAIjC,eAAO,MAAM,uBAAuB,MAAQ,CAAC;AAC7C,eAAO,MAAM,wBAAwB,MAAQ,CAAC;AAE9C,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAsER;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,GAAG,SAAS,CAUpB;AAoID,KAAK,qBAAqB,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAkEF,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKxF;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAezF"}
|