@openclaw/fs-safe 0.10.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 +114 -0
- package/LICENSE +1 -0
- package/README.md +39 -6
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +3 -2
- package/dist/advanced.d.ts +5 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +5 -1
- package/dist/archive-durability.d.ts +6 -6
- package/dist/archive-durability.d.ts.map +1 -1
- package/dist/archive-durability.js +1 -1
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +8 -8
- package/dist/archive-gzip-tail.d.ts +1 -0
- package/dist/archive-gzip-tail.d.ts.map +1 -1
- package/dist/archive-gzip-tail.js +16 -9
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +4 -2
- package/dist/archive-merge.d.ts +5 -1
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +15 -12
- package/dist/archive-native.js +4 -4
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +54 -42
- package/dist/archive-staging.d.ts +6 -3
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +42 -22
- package/dist/archive-tar-stream.d.ts.map +1 -1
- package/dist/archive-tar-stream.js +7 -6
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +16 -13
- package/dist/archive-zip-admission.d.ts +1 -1
- package/dist/archive-zip-admission.d.ts.map +1 -1
- package/dist/archive-zip-admission.js +48 -12
- package/dist/archive-zip-loader.d.ts +6 -0
- package/dist/archive-zip-loader.d.ts.map +1 -0
- package/dist/archive-zip-loader.js +38 -0
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +26 -9
- package/dist/archive-zip-preflight.d.ts +2 -3
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +2 -34
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +12 -10
- package/dist/bounded-read.d.ts +5 -0
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +18 -11
- package/dist/copy-file-input.d.ts +0 -1
- package/dist/copy-file-input.d.ts.map +1 -1
- package/dist/copy-file-input.js +4 -26
- package/dist/copy-tree-portable.d.ts.map +1 -1
- package/dist/copy-tree-portable.js +57 -26
- package/dist/copy.d.ts +1 -1
- package/dist/copy.d.ts.map +1 -1
- package/dist/copy.js +3 -1
- package/dist/device-path.d.ts.map +1 -1
- package/dist/device-path.js +5 -3
- package/dist/directory-durability.d.ts.map +1 -1
- package/dist/directory-durability.js +5 -4
- package/dist/directory-guard.d.ts +11 -1
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +53 -11
- package/dist/durability.d.ts +1 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +1 -1
- package/dist/file-handle-transfer.d.ts +14 -0
- package/dist/file-handle-transfer.d.ts.map +1 -0
- package/dist/file-handle-transfer.js +64 -0
- package/dist/file-hash.d.ts +3 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +99 -32
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +8 -4
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +7 -5
- package/dist/file-store-path.d.ts +3 -0
- package/dist/file-store-path.d.ts.map +1 -0
- package/dist/file-store-path.js +27 -0
- package/dist/file-store-prune.d.ts.map +1 -1
- package/dist/file-store-prune.js +15 -5
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +56 -44
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +2 -18
- package/dist/filename.d.ts +1 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +32 -14
- package/dist/guarded-mkdir.d.ts +2 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +52 -16
- package/dist/guest-dispatch-python.d.ts +2 -0
- package/dist/guest-dispatch-python.d.ts.map +1 -0
- package/dist/guest-dispatch-python.js +117 -0
- package/dist/guest-native-python.d.ts +4 -0
- package/dist/guest-native-python.d.ts.map +1 -0
- package/dist/guest-native-python.js +135 -0
- package/dist/guest.d.ts +9 -0
- package/dist/guest.d.ts.map +1 -0
- package/dist/guest.js +421 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/install-path.d.ts +6 -0
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +16 -2
- package/dist/json-durable-queue-directory.js +3 -3
- 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 +43 -84
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +2 -1
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +2 -1
- package/dist/move-path-stage.d.ts.map +1 -1
- package/dist/move-path-stage.js +2 -1
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +4 -3
- package/dist/mutation-authority.d.ts +1 -0
- package/dist/mutation-authority.d.ts.map +1 -1
- package/dist/mutation-authority.js +4 -4
- package/dist/native-binding.d.ts +14 -2
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +3 -2
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +2 -1
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +5 -4
- package/dist/output.d.ts +2 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +2 -0
- package/dist/overwrite-file-handle.d.ts +8 -0
- package/dist/overwrite-file-handle.d.ts.map +1 -0
- package/dist/overwrite-file-handle.js +42 -0
- package/dist/path-case.d.ts +7 -0
- package/dist/path-case.d.ts.map +1 -0
- package/dist/path-case.js +136 -0
- 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 +22 -3
- package/dist/permissions-windows.d.ts +1 -1
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +48 -6
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +3 -1
- package/dist/pinned-write.d.ts +2 -2
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +3 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +6 -4
- package/dist/realpath.d.ts +4 -0
- package/dist/realpath.d.ts.map +1 -0
- package/dist/realpath.js +43 -0
- package/dist/recursive-mkdir-path.d.ts +3 -0
- package/dist/recursive-mkdir-path.d.ts.map +1 -0
- package/dist/recursive-mkdir-path.js +8 -0
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +2 -1
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +23 -31
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +7 -12
- package/dist/replace-file-mode.d.ts +3 -0
- package/dist/replace-file-mode.d.ts.map +1 -0
- package/dist/replace-file-mode.js +10 -0
- package/dist/replace-file.d.ts +1 -0
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +14 -8
- 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 +40 -13
- package/dist/root-create-input.d.ts +10 -0
- package/dist/root-create-input.d.ts.map +1 -0
- package/dist/root-create-input.js +80 -0
- package/dist/root-directory-list.d.ts +3 -1
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +31 -5
- package/dist/root-entries.d.ts +11 -0
- package/dist/root-entries.d.ts.map +1 -0
- package/dist/root-entries.js +61 -0
- package/dist/root-errors.d.ts +5 -5
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +13 -12
- package/dist/root-impl.d.ts +13 -3
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +93 -70
- 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-options.d.ts +12 -1
- package/dist/root-options.d.ts.map +1 -1
- 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 +14 -5
- package/dist/root-path-symlink.d.ts.map +1 -1
- package/dist/root-path-symlink.js +3 -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 +24 -32
- package/dist/root-remove.d.ts +5 -0
- package/dist/root-remove.d.ts.map +1 -0
- package/dist/root-remove.js +286 -0
- package/dist/root-symlink-policy.d.ts +2 -1
- package/dist/root-symlink-policy.d.ts.map +1 -1
- package/dist/root-symlink-policy.js +2 -2
- 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 +21 -7
- package/dist/root-write-verification.d.ts.map +1 -1
- package/dist/root-write-verification.js +12 -3
- package/dist/root.d.ts +2 -1
- package/dist/root.d.ts.map +1 -1
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +3 -1
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +2 -1
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +2 -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 +27 -7
- package/dist/secure-temp-dir.d.ts.map +1 -1
- package/dist/secure-temp-dir.js +2 -1
- package/dist/sibling-staged-file.d.ts +2 -0
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +49 -8
- package/dist/sibling-temp.d.ts +2 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +8 -5
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +16 -5
- package/dist/sidecar-lock-policy.d.ts +2 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +17 -0
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +2 -3
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +4 -3
- package/dist/temp-target.d.ts +14 -12
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +15 -7
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +7 -5
- package/dist/unicode-path.d.ts +3 -0
- package/dist/unicode-path.d.ts.map +1 -0
- package/dist/unicode-path.js +13 -0
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +14 -12
- package/dist/write-file-handle.d.ts +1 -0
- package/dist/write-file-handle.d.ts.map +1 -1
- package/dist/write-file-handle.js +3 -2
- package/docs/advanced.md +7 -1
- package/docs/archive.md +31 -5
- package/docs/atomic.md +17 -1
- package/docs/config.md +1 -0
- package/docs/contributing.md +33 -2
- package/docs/copy.md +75 -6
- package/docs/directory-identity.md +85 -0
- package/docs/durability.md +40 -3
- package/docs/entries.md +109 -0
- package/docs/errors.md +3 -3
- package/docs/file-store.md +21 -0
- package/docs/filename.md +9 -2
- package/docs/guest.md +146 -0
- package/docs/in-place-write.md +81 -0
- package/docs/index.md +2 -0
- package/docs/install-path.md +59 -13
- package/docs/install.md +34 -0
- package/docs/native-helper.md +14 -5
- package/docs/native.md +18 -2
- package/docs/output.md +32 -6
- package/docs/path-case.md +64 -0
- package/docs/path-scope.md +1 -1
- package/docs/path.md +1 -1
- package/docs/permissions.md +37 -3
- package/docs/public-api.md +36 -2
- package/docs/root.md +33 -3
- package/docs/secure-file.md +17 -14
- package/docs/security-model.md +1 -1
- package/docs/sidecar-lock.md +12 -3
- package/docs/store.md +22 -1
- package/docs/temp.md +39 -6
- package/docs/types.md +1 -1
- package/docs/writing.md +153 -3
- package/package.json +14 -8
|
@@ -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,
|
|
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
|
@@ -9,8 +9,10 @@ import { isWindowsDriveLetterPath, isWindowsNetworkPath } from "./local-file-acc
|
|
|
9
9
|
import { isPathInside, isSymlinkOpenError } from "./path.js";
|
|
10
10
|
import { formatPermissionErrorDetail } from "./permission-exec.js";
|
|
11
11
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
12
|
+
import { realpathSync } from "./realpath.js";
|
|
12
13
|
import { inspectPathPermissions, isGroupReadable, isGroupWritable, isWorldReadable, isWorldWritable, modeBits, } from "./permissions.js";
|
|
13
14
|
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
15
|
+
import { inspectSecureWindowsDescriptor } from "./secure-file-windows.js";
|
|
14
16
|
import { scheduleTimeout } from "./timing.js";
|
|
15
17
|
function isAbsolutePathname(value) {
|
|
16
18
|
return (path.isAbsolute(value) ||
|
|
@@ -20,6 +22,11 @@ function isAbsolutePathname(value) {
|
|
|
20
22
|
function label(options) {
|
|
21
23
|
return options.label ?? "Secure file";
|
|
22
24
|
}
|
|
25
|
+
function assertNotHardlinked(options, stat) {
|
|
26
|
+
if (stat.nlink > 1n) {
|
|
27
|
+
throw new FsSafeError("hardlink", `${label(options)} must not be hardlinked: ${options.filePath}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
23
30
|
async function openSecureHandle(options, maxBytes) {
|
|
24
31
|
assertNoUnsafeDeviceReadPath(options.filePath);
|
|
25
32
|
if (isWindowsNetworkPath(options.filePath, "win32") && !options.trust?.allowNetworkPath) {
|
|
@@ -63,6 +70,7 @@ async function openSecureHandle(options, maxBytes) {
|
|
|
63
70
|
throw new FsSafeError("not-file", `${label(options)} must be a file: ${options.filePath}`);
|
|
64
71
|
}
|
|
65
72
|
const openedIdentity = await inspectFileIdentity(() => fsSync.fstatSync(handle.fd, { bigint: true }));
|
|
73
|
+
assertNotHardlinked(options, openedIdentity);
|
|
66
74
|
await inspectFileIdentity(async () => {
|
|
67
75
|
const pathStat = options.trust?.allowSymlink
|
|
68
76
|
? fsSync.statSync(options.filePath, { bigint: true })
|
|
@@ -70,14 +78,19 @@ async function openSecureHandle(options, maxBytes) {
|
|
|
70
78
|
if (!options.trust?.allowSymlink && pathStat.isSymbolicLink()) {
|
|
71
79
|
throw new FsSafeError("symlink", `${label(options)} must not be a symlink: ${options.filePath}`);
|
|
72
80
|
}
|
|
81
|
+
assertNotHardlinked(options, pathStat);
|
|
73
82
|
return pathStat;
|
|
74
83
|
}, openedIdentity);
|
|
75
|
-
const realPath =
|
|
76
|
-
await inspectFileIdentity(() =>
|
|
84
|
+
const realPath = realpathSync.native(options.filePath);
|
|
85
|
+
await inspectFileIdentity(() => {
|
|
86
|
+
const realPathStat = fsSync.statSync(realPath, { bigint: true });
|
|
87
|
+
assertNotHardlinked(options, realPathStat);
|
|
88
|
+
return realPathStat;
|
|
89
|
+
}, openedIdentity);
|
|
77
90
|
if (maxBytes !== undefined && openedStat.size > maxBytes) {
|
|
78
91
|
throw new FsSafeError("too-large", `${label(options)} exceeded maxBytes (${maxBytes}).`);
|
|
79
92
|
}
|
|
80
|
-
return { handle, pathStat: openedStat, realPath };
|
|
93
|
+
return { handle, identity: openedIdentity, pathStat: openedStat, realPath };
|
|
81
94
|
}
|
|
82
95
|
catch (err) {
|
|
83
96
|
await handle.close().catch(() => undefined);
|
|
@@ -91,7 +104,7 @@ async function assertTrustedDirs(options, realPath) {
|
|
|
91
104
|
const trusted = await Promise.all(options.trust.trustedDirs.map(async (dir) => {
|
|
92
105
|
const resolved = path.resolve(dir);
|
|
93
106
|
try {
|
|
94
|
-
return
|
|
107
|
+
return realpathSync.native(resolved);
|
|
95
108
|
}
|
|
96
109
|
catch {
|
|
97
110
|
return resolved;
|
|
@@ -116,13 +129,15 @@ function inspectOpenedPermissions(stat, platform) {
|
|
|
116
129
|
groupReadable: isGroupReadable(bits),
|
|
117
130
|
};
|
|
118
131
|
}
|
|
119
|
-
async function assertSecurePermissions(options, stat, realPath) {
|
|
132
|
+
async function assertSecurePermissions(options, stat, realPath, identity, fd) {
|
|
120
133
|
if (options.permissions?.allowInsecure) {
|
|
121
134
|
return undefined;
|
|
122
135
|
}
|
|
123
136
|
const platform = options.inject?.platform ?? process.platform;
|
|
124
137
|
const permissions = platform === "win32"
|
|
125
|
-
?
|
|
138
|
+
? process.platform === "win32"
|
|
139
|
+
? inspectSecureWindowsDescriptor({ fd, identity, stat })
|
|
140
|
+
: await inspectPathPermissions(realPath, options.inject)
|
|
126
141
|
: inspectOpenedPermissions(stat, platform);
|
|
127
142
|
const reason = permissions.error ? `: ${formatPermissionErrorDetail(permissions.error)}` : "";
|
|
128
143
|
const diagnostics = {
|
|
@@ -182,8 +197,13 @@ export async function readSecureFile(options) {
|
|
|
182
197
|
const opened = await openSecureHandle(options, maxBytes);
|
|
183
198
|
try {
|
|
184
199
|
await assertTrustedDirs(options, opened.realPath);
|
|
185
|
-
const permissions = await assertSecurePermissions(options, opened.pathStat, opened.realPath);
|
|
200
|
+
const permissions = await assertSecurePermissions(options, opened.pathStat, opened.realPath, opened.identity, opened.handle.fd);
|
|
186
201
|
const buffer = await readHandleWithTimeout(opened.handle, options.io?.timeoutMs, maxBytes);
|
|
202
|
+
const finalIdentity = await inspectFileIdentity(() => fsSync.fstatSync(opened.handle.fd, { bigint: true }), opened.identity);
|
|
203
|
+
if (!finalIdentity.isFile()) {
|
|
204
|
+
throw new FsSafeError("not-file", `${label(options)} must remain a file: ${options.filePath}`);
|
|
205
|
+
}
|
|
206
|
+
assertNotHardlinked(options, finalIdentity);
|
|
187
207
|
return { buffer, realPath: opened.realPath, stat: opened.pathStat, permissions };
|
|
188
208
|
}
|
|
189
209
|
finally {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secure-temp-dir.d.ts","sourceRoot":"","sources":["../src/secure-temp-dir.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"secure-temp-dir.d.ts","sourceRoot":"","sources":["../src/secure-temp-dir.ts"],"names":[],"mappings":"AAQA,KAAK,aAAa,GAAG;IACnB,WAAW,IAAI,OAAO,CAAC;IACvB,cAAc,IAAI,OAAO,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IACzC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAClC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,aAAa,CAAC;IAC5C,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAWF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,MAAM,CA+JnF"}
|
package/dist/secure-temp-dir.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { tmpdir as getOsTmpDir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
4
5
|
import { assertSafePathSegment } from "./safe-path-segment.js";
|
|
5
6
|
function isNodeErrorWithCode(err, code) {
|
|
6
7
|
return (typeof err === "object" &&
|
|
@@ -17,7 +18,7 @@ export function resolveSecureTempRoot(options) {
|
|
|
17
18
|
const accessSync = options.accessSync ?? fs.accessSync;
|
|
18
19
|
const chmodSync = options.chmodSync ?? fs.chmodSync;
|
|
19
20
|
const lstatSync = options.lstatSync ?? fs.lstatSync;
|
|
20
|
-
const mkdirSync = options.mkdirSync ?? fs.mkdirSync;
|
|
21
|
+
const mkdirSync = options.mkdirSync ?? ((directory, mkdirOptions) => fs.mkdirSync(recursiveMkdirPath(directory), mkdirOptions));
|
|
21
22
|
const warn = options.warn ?? ((message) => console.warn(message));
|
|
22
23
|
const warningPrefix = options.warningPrefix ?? "[fs-safe]";
|
|
23
24
|
const unsafeFallbackLabel = options.unsafeFallbackLabel ?? "secure temp dir";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
export declare function assertCallbackTempPathDeviceSafe(tempPath: string): void;
|
|
1
2
|
export declare function writeCallbackSibling<T>(params: {
|
|
2
3
|
tempPath: string;
|
|
3
4
|
write: (tempPath: string) => Promise<T>;
|
|
5
|
+
producerIsolation?: "private-directory";
|
|
4
6
|
resolveFinalPath: (result: T) => string;
|
|
5
7
|
mode?: number;
|
|
6
8
|
/** Preserve the caller's historical best-effort mode behavior. */
|
|
@@ -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"}
|
|
@@ -2,12 +2,15 @@ import { syncFileBestEffort } from "./file-sync.js";
|
|
|
2
2
|
import fsSync, {} from "node:fs";
|
|
3
3
|
import fs, {} from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard } from "./directory-guard.js";
|
|
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";
|
|
10
|
+
import { root } from "./root.js";
|
|
9
11
|
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
10
12
|
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
13
|
+
import { createOwnedTempFile } from "./temp-target.js";
|
|
11
14
|
import { serializePathWrite } from "./write-queue.js";
|
|
12
15
|
function assertRegularFile(stat) {
|
|
13
16
|
if (stat.isSymbolicLink()) {
|
|
@@ -27,17 +30,45 @@ async function inspectStage(inspect, expected) {
|
|
|
27
30
|
return stat;
|
|
28
31
|
}, expected);
|
|
29
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
|
+
}
|
|
38
|
+
// Own the workspace before the producer can leave partial output. The finished
|
|
39
|
+
// file still enters the ordinary sibling admission and publication lifecycle.
|
|
40
|
+
async function writeIsolatedProducer(params) {
|
|
41
|
+
const targetRoot = await root(path.dirname(params.tempPath));
|
|
42
|
+
const { target, identity } = await createOwnedTempFile({
|
|
43
|
+
rootDir: targetRoot.rootReal,
|
|
44
|
+
prefix: "fs-safe-output",
|
|
45
|
+
fileName: path.basename(params.tempPath),
|
|
46
|
+
});
|
|
47
|
+
const assertCurrent = () => {
|
|
48
|
+
params.assertParent();
|
|
49
|
+
assertDirectoryIdentitySync(target.dir, { ...identity, realPath: target.dir });
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
assertCurrent();
|
|
53
|
+
const result = await params.write(target.path);
|
|
54
|
+
assertCurrent();
|
|
55
|
+
await targetRoot.move(path.relative(targetRoot.rootReal, target.path), path.basename(params.tempPath), {
|
|
56
|
+
assertBeforeMutation: assertCurrent,
|
|
57
|
+
});
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
await target.cleanup();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
30
64
|
// Callback paths are not owned until all three admission observations agree.
|
|
31
65
|
// Keep one descriptor and one exact identity through mode, sync, rename and cleanup.
|
|
32
66
|
// Read/write access is needed only when the caller requests file synchronization.
|
|
33
67
|
export async function writeCallbackSibling(params) {
|
|
68
|
+
assertCallbackTempPathDeviceSafe(params.tempPath);
|
|
34
69
|
const parent = path.dirname(params.tempPath);
|
|
35
|
-
const guard = await createAsyncDirectoryGuard(parent);
|
|
36
|
-
const
|
|
37
|
-
const assertParent = async () => {
|
|
38
|
-
await assertAsyncDirectoryGuard(guard);
|
|
39
|
-
await inspectFileIdentity(() => fsSync.lstatSync(parent, { bigint: true }), parentIdentity);
|
|
40
|
-
};
|
|
70
|
+
const guard = await createAsyncDirectoryGuard(parent, { bigint: true });
|
|
71
|
+
const assertParent = () => assertAsyncDirectoryGuard(guard);
|
|
41
72
|
let handle;
|
|
42
73
|
let identity;
|
|
43
74
|
let unregister;
|
|
@@ -54,7 +85,17 @@ export async function writeCallbackSibling(params) {
|
|
|
54
85
|
}
|
|
55
86
|
};
|
|
56
87
|
try {
|
|
57
|
-
const result =
|
|
88
|
+
const result = params.producerIsolation === "private-directory"
|
|
89
|
+
? await writeIsolatedProducer({
|
|
90
|
+
tempPath: params.tempPath,
|
|
91
|
+
write: params.write,
|
|
92
|
+
assertParent: () => assertDirectoryIdentitySync(parent, {
|
|
93
|
+
dev: guard.stat.dev,
|
|
94
|
+
ino: guard.stat.ino,
|
|
95
|
+
realPath: guard.realPath,
|
|
96
|
+
}),
|
|
97
|
+
})
|
|
98
|
+
: await params.write(params.tempPath);
|
|
58
99
|
await assertParent();
|
|
59
100
|
const before = await inspectPath(params.tempPath);
|
|
60
101
|
try {
|
package/dist/sibling-temp.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export type WriteSiblingTempFileOptions<T> = {
|
|
2
2
|
dir: string;
|
|
3
3
|
writeTemp: (tempPath: string) => Promise<T>;
|
|
4
|
+
/** Own a private sibling workspace before invoking the producer. */
|
|
5
|
+
producerIsolation?: "private-directory";
|
|
4
6
|
resolveFinalPath: (result: T) => string;
|
|
5
7
|
tempPrefix?: string;
|
|
6
8
|
dirMode?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sibling-temp.d.ts","sourceRoot":"","sources":["../src/sibling-temp.ts"],"names":[],"mappings":"
|
|
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
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import crypto, { randomUUID } from "node:crypto";
|
|
2
|
-
import fsSync from "node:fs";
|
|
3
2
|
import fs from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard } from "./directory-guard.js";
|
|
6
5
|
import { fitFileNameToPortableComponent, sanitizeUntrustedFileName } from "./filename.js";
|
|
7
6
|
import { applyDirectoryMode } from "./replace-file-descriptor.js";
|
|
7
|
+
import { realpathSync } from "./realpath.js";
|
|
8
|
+
import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
8
9
|
import { root } from "./root.js";
|
|
9
10
|
import { assertSafePathPrefix } from "./safe-path-segment.js";
|
|
10
11
|
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
11
|
-
import { writeCallbackSibling } from "./sibling-staged-file.js";
|
|
12
|
+
import { assertCallbackTempPathDeviceSafe, writeCallbackSibling } from "./sibling-staged-file.js";
|
|
12
13
|
import { tempFile } from "./temp-target.js";
|
|
13
14
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
14
15
|
function buildTempPath(dir, tempPrefix) {
|
|
@@ -19,7 +20,7 @@ function buildTempPath(dir, tempPrefix) {
|
|
|
19
20
|
}
|
|
20
21
|
export async function writeSiblingTempFile(options) {
|
|
21
22
|
const dir = path.resolve(options.dir);
|
|
22
|
-
await fs.mkdir(dir, { recursive: true, mode: options.dirMode ?? 0o700 });
|
|
23
|
+
await fs.mkdir(recursiveMkdirPath(dir), { recursive: true, mode: options.dirMode ?? 0o700 });
|
|
23
24
|
if (options.chmodDir !== false) {
|
|
24
25
|
await applyDirectoryMode({
|
|
25
26
|
fsModule: fs,
|
|
@@ -31,6 +32,7 @@ export async function writeSiblingTempFile(options) {
|
|
|
31
32
|
return await writeCallbackSibling({
|
|
32
33
|
tempPath: buildTempPath(dir, options.tempPrefix),
|
|
33
34
|
write: options.writeTemp,
|
|
35
|
+
producerIsolation: options.producerIsolation,
|
|
34
36
|
resolveFinalPath: options.resolveFinalPath,
|
|
35
37
|
mode: options.mode,
|
|
36
38
|
ignoreModeError: true,
|
|
@@ -55,7 +57,7 @@ function buildSiblingTempPath(params) {
|
|
|
55
57
|
export async function writeViaSiblingTempPath(params) {
|
|
56
58
|
let rootDir;
|
|
57
59
|
try {
|
|
58
|
-
rootDir =
|
|
60
|
+
rootDir = realpathSync.native(path.resolve(params.rootDir));
|
|
59
61
|
}
|
|
60
62
|
catch {
|
|
61
63
|
rootDir = path.resolve(params.rootDir);
|
|
@@ -63,7 +65,7 @@ export async function writeViaSiblingTempPath(params) {
|
|
|
63
65
|
const requestedTargetPath = path.resolve(params.targetPath);
|
|
64
66
|
let targetPath;
|
|
65
67
|
try {
|
|
66
|
-
const realDir =
|
|
68
|
+
const realDir = realpathSync.native(path.dirname(requestedTargetPath));
|
|
67
69
|
targetPath = path.join(realDir, path.basename(requestedTargetPath));
|
|
68
70
|
}
|
|
69
71
|
catch {
|
|
@@ -91,6 +93,7 @@ export async function writeViaSiblingTempPath(params) {
|
|
|
91
93
|
fallbackFileName: params.fallbackFileName ?? "output.bin",
|
|
92
94
|
tempPrefix: params.tempPrefix ?? ".fs-safe-output-",
|
|
93
95
|
});
|
|
96
|
+
assertCallbackTempPathDeviceSafe(tempPath);
|
|
94
97
|
await getFsSafeTestHooks()?.beforeSiblingTempWrite?.(tempPath);
|
|
95
98
|
await params.writeTemp(tempPath);
|
|
96
99
|
await assertAsyncDirectoryGuard(rootGuard);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock-acquire.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-acquire.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sidecar-lock-acquire.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-acquire.ts"],"names":[],"mappings":"AAUA,OAAO,EAA6B,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1F,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAW3C,OAAO,EAWL,KAAK,mBAAmB,EACzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAI5F,KAAK,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,EAAE,IAAI,GAAG,OAAO,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC;AAEvF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,cAAc,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,eAAe,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;CAClC,CAAC;AAEF,KAAK,6BAA6B,GAAG;IACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACnC,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,2BAA2B,IAAI,IAAI,CAAC;IACpC,iBAAiB,CAAC,oBAAoB,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,iBAAiB,CAAC;IAC1F,eAAe,CACb,oBAAoB,EAAE,MAAM,EAC5B,IAAI,EAAE,eAAe,EACrB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAC5B,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB,CAAC;AAqBF,wBAAsB,kBAAkB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/E,OAAO,EAAE,yBAAyB,CAAC,QAAQ,CAAC,EAC5C,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,iBAAiB,CAAC,CA6T5B"}
|
|
@@ -5,9 +5,11 @@ import { canonicalPathFromExistingAncestor } from "./absolute-path.js";
|
|
|
5
5
|
import { FsSafeError } from "./errors.js";
|
|
6
6
|
import { fileObservation } from "./file-observation.js";
|
|
7
7
|
import { readFileHandleBounded } from "./bounded-read.js";
|
|
8
|
+
import { realpathSync } from "./realpath.js";
|
|
9
|
+
import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
8
10
|
import { openSidecarRoot } from "./sidecar-lock-root.js";
|
|
9
11
|
import { createNativeExclusiveFile } from "./native-operations.js";
|
|
10
|
-
import { computeSidecarLockDelayMs, defaultSidecarLockShouldReclaim, isTransientLockFileDenial, maxTransientLockDenials, validateSidecarLockRetryOptions, validateSidecarLockTimeoutMs, } from "./sidecar-lock-policy.js";
|
|
12
|
+
import { computeSidecarLockDelayMs, defaultSidecarLockShouldReclaim, isTransientLockFileDenial, maxTransientLockDenials, validateSidecarLockStaleMs, validateSidecarLockRetryOptions, validateSidecarLockCompromiseCheckIntervalMs, validateSidecarLockTimeoutMs, } from "./sidecar-lock-policy.js";
|
|
11
13
|
import { readSidecarLockRawSnapshot, parseSidecarLockSnapshot, relativeSidecarLockPath, releaseSidecarReclaimGuard, removeSidecarLockIfUnchanged, removeStaleSidecarLockIfAllowed, serializeSidecarLockPayload, sidecarLockSnapshotStillPresent, sidecarReclaimGuardExists, tryAcquireSidecarReclaimGuard, } from "./sidecar-lock-reclaim.js";
|
|
12
14
|
import { createSuppressedError } from "./suppressed-error.js";
|
|
13
15
|
import { sleep } from "./timing.js";
|
|
@@ -22,9 +24,9 @@ async function resolveNormalizedTargetPath(targetPath, lockRoot) {
|
|
|
22
24
|
await lockRoot.resolve(".");
|
|
23
25
|
return path.join(parent, path.basename(resolved));
|
|
24
26
|
}
|
|
25
|
-
await fs.mkdir(dir, { recursive: true });
|
|
27
|
+
await fs.mkdir(recursiveMkdirPath(dir), { recursive: true });
|
|
26
28
|
try {
|
|
27
|
-
return path.join(
|
|
29
|
+
return path.join(realpathSync.native(dir), path.basename(resolved));
|
|
28
30
|
}
|
|
29
31
|
catch {
|
|
30
32
|
return resolved;
|
|
@@ -34,6 +36,8 @@ export async function acquireSidecarLock(options, context) {
|
|
|
34
36
|
const retry = options.retry ?? {};
|
|
35
37
|
validateSidecarLockRetryOptions(retry);
|
|
36
38
|
validateSidecarLockTimeoutMs(options.timeoutMs);
|
|
39
|
+
validateSidecarLockStaleMs(options.staleMs);
|
|
40
|
+
validateSidecarLockCompromiseCheckIntervalMs(options.compromiseCheckIntervalMs);
|
|
37
41
|
context.ensureExitCleanupRegistered();
|
|
38
42
|
const normalizedTargetPath = await resolveNormalizedTargetPath(options.targetPath, options.lockRoot);
|
|
39
43
|
const lockPath = options.lockPath ?? `${normalizedTargetPath}.lock`;
|
|
@@ -54,7 +58,7 @@ export async function acquireSidecarLock(options, context) {
|
|
|
54
58
|
options.reentrantOwner !== undefined &&
|
|
55
59
|
held.reentrantOwner !== undefined &&
|
|
56
60
|
options.reentrantOwner === held.reentrantOwner) {
|
|
57
|
-
held.refCount
|
|
61
|
+
held.refCount = (held.refCount ?? 1) + 1;
|
|
58
62
|
// Retention is monotonic: any same-owner request to keep the sidecar on
|
|
59
63
|
// exit upgrades the held lock; a later default acquisition never revokes it.
|
|
60
64
|
if (options.retainOnExit === true) {
|
|
@@ -153,7 +157,7 @@ export async function acquireSidecarLock(options, context) {
|
|
|
153
157
|
try {
|
|
154
158
|
handle =
|
|
155
159
|
(await createNativeExclusiveFile(lockPath, 0o600)) ??
|
|
156
|
-
(await fs.open(lockPath, "wx"));
|
|
160
|
+
(await fs.open(lockPath, "wx", 0o600));
|
|
157
161
|
}
|
|
158
162
|
catch (createError) {
|
|
159
163
|
lockFileCreateDenied = isTransientLockFileDenial(createError, lockPath);
|
|
@@ -194,7 +198,11 @@ export async function acquireSidecarLock(options, context) {
|
|
|
194
198
|
const returnedHandle = context.handleForHeldLock(normalizedTargetPath, createdHeld);
|
|
195
199
|
const interval = options.compromiseCheckIntervalMs;
|
|
196
200
|
if (options.onCompromised && interval !== undefined && interval > 0) {
|
|
201
|
+
let compromiseCheckInFlight = false;
|
|
197
202
|
createdHeld.compromiseTimer = setInterval(() => {
|
|
203
|
+
if (compromiseCheckInFlight)
|
|
204
|
+
return;
|
|
205
|
+
compromiseCheckInFlight = true;
|
|
198
206
|
void returnedHandle
|
|
199
207
|
.verifyStillHeld()
|
|
200
208
|
.catch(() => false)
|
|
@@ -204,6 +212,9 @@ export async function acquireSidecarLock(options, context) {
|
|
|
204
212
|
createdHeld.compromiseTimer = undefined;
|
|
205
213
|
options.onCompromised?.({ lockPath, normalizedTargetPath });
|
|
206
214
|
}
|
|
215
|
+
})
|
|
216
|
+
.finally(() => {
|
|
217
|
+
compromiseCheckInFlight = false;
|
|
207
218
|
});
|
|
208
219
|
}, interval);
|
|
209
220
|
createdHeld.compromiseTimer.unref();
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { SidecarLockRetryOptions } from "./sidecar-lock-types.js";
|
|
2
2
|
export declare function validateSidecarLockRetryOptions(retry: SidecarLockRetryOptions): void;
|
|
3
3
|
export declare function validateSidecarLockTimeoutMs(timeoutMs: number | undefined): void;
|
|
4
|
+
export declare function validateSidecarLockStaleMs(staleMs: number | undefined): void;
|
|
5
|
+
export declare function validateSidecarLockCompromiseCheckIntervalMs(intervalMs: number | undefined): void;
|
|
4
6
|
export declare function computeSidecarLockDelayMs(retry: SidecarLockRetryOptions, attempt: number): number;
|
|
5
7
|
export declare const maxTransientLockDenials = 8;
|
|
6
8
|
export declare function isTransientLockFileDenial(error: unknown, lockPath: string): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock-policy.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"sidecar-lock-policy.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAUvE,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAkBpF;AAED,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAGhF;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAO5E;AAED,wBAAgB,4CAA4C,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAOjG;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,uBAAuB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAUjG;AASD,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGnF;AAED,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAU7E;AAED,wBAAsB,+BAA+B,CAAC,MAAM,EAAE;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,OAAO,CAAC,CAQnB"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
|
+
const MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
2
3
|
function assertFiniteNonNegative(value, label) {
|
|
3
4
|
if (!Number.isFinite(value) || value < 0) {
|
|
4
5
|
throw new RangeError(`${label} must be a finite non-negative number`);
|
|
@@ -27,6 +28,22 @@ export function validateSidecarLockTimeoutMs(timeoutMs) {
|
|
|
27
28
|
return;
|
|
28
29
|
assertFiniteNonNegative(timeoutMs, "lock timeoutMs");
|
|
29
30
|
}
|
|
31
|
+
export function validateSidecarLockStaleMs(staleMs) {
|
|
32
|
+
// The lower-level sidecar manager historically treats an omitted threshold
|
|
33
|
+
// as disabling age-based reclamation. Its public type requires a number, but
|
|
34
|
+
// retain that fail-closed runtime behavior for JavaScript callers and older
|
|
35
|
+
// consumers.
|
|
36
|
+
if (staleMs === undefined || staleMs === Number.POSITIVE_INFINITY)
|
|
37
|
+
return;
|
|
38
|
+
assertFiniteNonNegative(staleMs, "lock staleMs");
|
|
39
|
+
}
|
|
40
|
+
export function validateSidecarLockCompromiseCheckIntervalMs(intervalMs) {
|
|
41
|
+
if (intervalMs === undefined || intervalMs === 0)
|
|
42
|
+
return;
|
|
43
|
+
if (!Number.isFinite(intervalMs) || intervalMs < 1 || intervalMs > MAX_TIMER_DELAY_MS) {
|
|
44
|
+
throw new RangeError(`lock compromiseCheckIntervalMs must be 0 or a finite number between 1 and ${MAX_TIMER_DELAY_MS}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
30
47
|
export function computeSidecarLockDelayMs(retry, attempt) {
|
|
31
48
|
validateSidecarLockRetryOptions(retry);
|
|
32
49
|
const minTimeout = retry.minTimeout ?? 50;
|
|
@@ -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"}
|