@openclaw/fs-safe 0.1.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.
Files changed (260) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +390 -0
  4. package/SECURITY.md +12 -0
  5. package/dist/absolute-path.d.ts +19 -0
  6. package/dist/absolute-path.d.ts.map +1 -0
  7. package/dist/absolute-path.js +98 -0
  8. package/dist/advanced.d.ts +22 -0
  9. package/dist/advanced.d.ts.map +1 -0
  10. package/dist/advanced.js +24 -0
  11. package/dist/archive-entry.d.ts +13 -0
  12. package/dist/archive-entry.d.ts.map +1 -0
  13. package/dist/archive-entry.js +47 -0
  14. package/dist/archive-kind.d.ts +8 -0
  15. package/dist/archive-kind.d.ts.map +1 -0
  16. package/dist/archive-kind.js +58 -0
  17. package/dist/archive-limits.d.ts +40 -0
  18. package/dist/archive-limits.d.ts.map +1 -0
  19. package/dist/archive-limits.js +92 -0
  20. package/dist/archive-staging.d.ts +26 -0
  21. package/dist/archive-staging.d.ts.map +1 -0
  22. package/dist/archive-staging.js +171 -0
  23. package/dist/archive-tar.d.ts +14 -0
  24. package/dist/archive-tar.d.ts.map +1 -0
  25. package/dist/archive-tar.js +52 -0
  26. package/dist/archive-utils.d.ts +3 -0
  27. package/dist/archive-utils.d.ts.map +1 -0
  28. package/dist/archive-utils.js +26 -0
  29. package/dist/archive-zip-preflight.d.ts +7 -0
  30. package/dist/archive-zip-preflight.d.ts.map +1 -0
  31. package/dist/archive-zip-preflight.js +170 -0
  32. package/dist/archive.d.ts +24 -0
  33. package/dist/archive.d.ts.map +1 -0
  34. package/dist/archive.js +251 -0
  35. package/dist/async-lock.d.ts +2 -0
  36. package/dist/async-lock.d.ts.map +1 -0
  37. package/dist/async-lock.js +17 -0
  38. package/dist/atomic.d.ts +5 -0
  39. package/dist/atomic.d.ts.map +1 -0
  40. package/dist/atomic.js +4 -0
  41. package/dist/boundary-file-read.d.ts +44 -0
  42. package/dist/boundary-file-read.d.ts.map +1 -0
  43. package/dist/boundary-file-read.js +129 -0
  44. package/dist/boundary-path.d.ts +39 -0
  45. package/dist/boundary-path.d.ts.map +1 -0
  46. package/dist/boundary-path.js +598 -0
  47. package/dist/config.d.ts +2 -0
  48. package/dist/config.d.ts.map +1 -0
  49. package/dist/config.js +1 -0
  50. package/dist/errors.d.ts +11 -0
  51. package/dist/errors.d.ts.map +1 -0
  52. package/dist/errors.js +20 -0
  53. package/dist/file-identity.d.ts +6 -0
  54. package/dist/file-identity.d.ts.map +1 -0
  55. package/dist/file-identity.js +14 -0
  56. package/dist/file-lock.d.ts +20 -0
  57. package/dist/file-lock.d.ts.map +1 -0
  58. package/dist/file-lock.js +34 -0
  59. package/dist/file-store.d.ts +66 -0
  60. package/dist/file-store.d.ts.map +1 -0
  61. package/dist/file-store.js +436 -0
  62. package/dist/file-url.d.ts +2 -0
  63. package/dist/file-url.d.ts.map +1 -0
  64. package/dist/file-url.js +1 -0
  65. package/dist/filename.d.ts +2 -0
  66. package/dist/filename.d.ts.map +1 -0
  67. package/dist/filename.js +25 -0
  68. package/dist/fs-pinned-path-helper.d.ts +7 -0
  69. package/dist/fs-pinned-path-helper.d.ts.map +1 -0
  70. package/dist/fs-pinned-path-helper.js +182 -0
  71. package/dist/fs-pinned-write-helper.d.ts +21 -0
  72. package/dist/fs-pinned-write-helper.d.ts.map +1 -0
  73. package/dist/fs-pinned-write-helper.js +263 -0
  74. package/dist/fs.d.ts +13 -0
  75. package/dist/fs.d.ts.map +1 -0
  76. package/dist/fs.js +30 -0
  77. package/dist/hardlink-guards.d.ts +7 -0
  78. package/dist/hardlink-guards.d.ts.map +1 -0
  79. package/dist/hardlink-guards.js +30 -0
  80. package/dist/home-dir.d.ts +22 -0
  81. package/dist/home-dir.d.ts.map +1 -0
  82. package/dist/home-dir.js +107 -0
  83. package/dist/index.d.ts +4 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +3 -0
  86. package/dist/install-path.d.ts +20 -0
  87. package/dist/install-path.d.ts.map +1 -0
  88. package/dist/install-path.js +94 -0
  89. package/dist/install-safe-path.d.ts +20 -0
  90. package/dist/install-safe-path.d.ts.map +1 -0
  91. package/dist/install-safe-path.js +94 -0
  92. package/dist/json-document-store.d.ts +30 -0
  93. package/dist/json-document-store.d.ts.map +1 -0
  94. package/dist/json-document-store.js +68 -0
  95. package/dist/json-file.d.ts +3 -0
  96. package/dist/json-file.d.ts.map +1 -0
  97. package/dist/json-file.js +123 -0
  98. package/dist/json-files.d.ts +20 -0
  99. package/dist/json-files.d.ts.map +1 -0
  100. package/dist/json-files.js +153 -0
  101. package/dist/json-store.d.ts +9 -0
  102. package/dist/json-store.d.ts.map +1 -0
  103. package/dist/json-store.js +12 -0
  104. package/dist/json.d.ts +17 -0
  105. package/dist/json.d.ts.map +1 -0
  106. package/dist/json.js +198 -0
  107. package/dist/local-file-access.d.ts +8 -0
  108. package/dist/local-file-access.d.ts.map +1 -0
  109. package/dist/local-file-access.js +73 -0
  110. package/dist/local-roots.d.ts +26 -0
  111. package/dist/local-roots.d.ts.map +1 -0
  112. package/dist/local-roots.js +150 -0
  113. package/dist/mode.d.ts +2 -0
  114. package/dist/mode.d.ts.map +1 -0
  115. package/dist/mode.js +3 -0
  116. package/dist/move-path.d.ts +6 -0
  117. package/dist/move-path.d.ts.map +1 -0
  118. package/dist/move-path.js +18 -0
  119. package/dist/path-alias-guards.d.ts +19 -0
  120. package/dist/path-alias-guards.d.ts.map +1 -0
  121. package/dist/path-alias-guards.js +21 -0
  122. package/dist/path-guards.d.ts +7 -0
  123. package/dist/path-guards.d.ts.map +1 -0
  124. package/dist/path-guards.js +49 -0
  125. package/dist/path-policy.d.ts +25 -0
  126. package/dist/path-policy.d.ts.map +1 -0
  127. package/dist/path-policy.js +50 -0
  128. package/dist/path-safety.d.ts +12 -0
  129. package/dist/path-safety.d.ts.map +1 -0
  130. package/dist/path-safety.js +50 -0
  131. package/dist/path-scope.d.ts +2 -0
  132. package/dist/path-scope.d.ts.map +1 -0
  133. package/dist/path-scope.js +1 -0
  134. package/dist/path.d.ts +18 -0
  135. package/dist/path.d.ts.map +1 -0
  136. package/dist/path.js +123 -0
  137. package/dist/permissions-public.d.ts +2 -0
  138. package/dist/permissions-public.d.ts.map +1 -0
  139. package/dist/permissions-public.js +1 -0
  140. package/dist/permissions.d.ts +86 -0
  141. package/dist/permissions.d.ts.map +1 -0
  142. package/dist/permissions.js +435 -0
  143. package/dist/pinned-helper.d.ts +9 -0
  144. package/dist/pinned-helper.d.ts.map +1 -0
  145. package/dist/pinned-helper.js +19 -0
  146. package/dist/pinned-open.d.ts +24 -0
  147. package/dist/pinned-open.d.ts.map +1 -0
  148. package/dist/pinned-open.js +71 -0
  149. package/dist/pinned-path.d.ts +7 -0
  150. package/dist/pinned-path.d.ts.map +1 -0
  151. package/dist/pinned-path.js +21 -0
  152. package/dist/pinned-python-config.d.ts +9 -0
  153. package/dist/pinned-python-config.d.ts.map +1 -0
  154. package/dist/pinned-python-config.js +39 -0
  155. package/dist/pinned-python.d.ts +12 -0
  156. package/dist/pinned-python.d.ts.map +1 -0
  157. package/dist/pinned-python.js +599 -0
  158. package/dist/pinned-write.d.ts +33 -0
  159. package/dist/pinned-write.d.ts.map +1 -0
  160. package/dist/pinned-write.js +198 -0
  161. package/dist/private-file-store.d.ts +60 -0
  162. package/dist/private-file-store.d.ts.map +1 -0
  163. package/dist/private-file-store.js +233 -0
  164. package/dist/private-temp-workspace.d.ts +39 -0
  165. package/dist/private-temp-workspace.d.ts.map +1 -0
  166. package/dist/private-temp-workspace.js +185 -0
  167. package/dist/regular-file.d.ts +38 -0
  168. package/dist/regular-file.d.ts.map +1 -0
  169. package/dist/regular-file.js +239 -0
  170. package/dist/replace-directory.d.ts +7 -0
  171. package/dist/replace-directory.d.ts.map +1 -0
  172. package/dist/replace-directory.js +32 -0
  173. package/dist/replace-file.d.ts +41 -0
  174. package/dist/replace-file.d.ts.map +1 -0
  175. package/dist/replace-file.js +325 -0
  176. package/dist/root-file.d.ts +44 -0
  177. package/dist/root-file.d.ts.map +1 -0
  178. package/dist/root-file.js +129 -0
  179. package/dist/root-path.d.ts +39 -0
  180. package/dist/root-path.d.ts.map +1 -0
  181. package/dist/root-path.js +598 -0
  182. package/dist/root-paths.d.ts +92 -0
  183. package/dist/root-paths.d.ts.map +1 -0
  184. package/dist/root-paths.js +338 -0
  185. package/dist/root.d.ts +108 -0
  186. package/dist/root.d.ts.map +1 -0
  187. package/dist/root.js +1332 -0
  188. package/dist/safe-open-sync.d.ts +24 -0
  189. package/dist/safe-open-sync.d.ts.map +1 -0
  190. package/dist/safe-open-sync.js +71 -0
  191. package/dist/safe-root.d.ts +123 -0
  192. package/dist/safe-root.d.ts.map +1 -0
  193. package/dist/safe-root.js +1060 -0
  194. package/dist/secret-file.d.ts +17 -0
  195. package/dist/secret-file.d.ts.map +1 -0
  196. package/dist/secret-file.js +232 -0
  197. package/dist/secret.d.ts +2 -0
  198. package/dist/secret.d.ts.map +1 -0
  199. package/dist/secret.js +1 -0
  200. package/dist/secure-file.d.ts +32 -0
  201. package/dist/secure-file.d.ts.map +1 -0
  202. package/dist/secure-file.js +163 -0
  203. package/dist/secure-temp-dir.d.ts +27 -0
  204. package/dist/secure-temp-dir.d.ts.map +1 -0
  205. package/dist/secure-temp-dir.js +155 -0
  206. package/dist/secure-temp-workspace.d.ts +25 -0
  207. package/dist/secure-temp-workspace.d.ts.map +1 -0
  208. package/dist/secure-temp-workspace.js +136 -0
  209. package/dist/sibling-temp-file.d.ts +16 -0
  210. package/dist/sibling-temp-file.d.ts.map +1 -0
  211. package/dist/sibling-temp-file.js +73 -0
  212. package/dist/sibling-temp-write.d.ts +8 -0
  213. package/dist/sibling-temp-write.d.ts.map +1 -0
  214. package/dist/sibling-temp-write.js +40 -0
  215. package/dist/sibling-temp.d.ts +23 -0
  216. package/dist/sibling-temp.d.ts.map +1 -0
  217. package/dist/sibling-temp.js +119 -0
  218. package/dist/sidecar-lock.d.ts +50 -0
  219. package/dist/sidecar-lock.d.ts.map +1 -0
  220. package/dist/sidecar-lock.js +235 -0
  221. package/dist/store.d.ts +3 -0
  222. package/dist/store.d.ts.map +1 -0
  223. package/dist/store.js +2 -0
  224. package/dist/string-coerce.d.ts +14 -0
  225. package/dist/string-coerce.d.ts.map +1 -0
  226. package/dist/string-coerce.js +72 -0
  227. package/dist/symlink-parents.d.ts +12 -0
  228. package/dist/symlink-parents.d.ts.map +1 -0
  229. package/dist/symlink-parents.js +78 -0
  230. package/dist/temp-cleanup.d.ts +6 -0
  231. package/dist/temp-cleanup.d.ts.map +1 -0
  232. package/dist/temp-cleanup.js +42 -0
  233. package/dist/temp-target.d.ts +28 -0
  234. package/dist/temp-target.d.ts.map +1 -0
  235. package/dist/temp-target.js +84 -0
  236. package/dist/temp.d.ts +3 -0
  237. package/dist/temp.d.ts.map +1 -0
  238. package/dist/temp.js +2 -0
  239. package/dist/test-hooks.d.ts +9 -0
  240. package/dist/test-hooks.d.ts.map +1 -0
  241. package/dist/test-hooks.js +13 -0
  242. package/dist/text-atomic.d.ts +7 -0
  243. package/dist/text-atomic.d.ts.map +1 -0
  244. package/dist/text-atomic.js +13 -0
  245. package/dist/timing.d.ts +6 -0
  246. package/dist/timing.d.ts.map +1 -0
  247. package/dist/timing.js +24 -0
  248. package/dist/trash.d.ts +5 -0
  249. package/dist/trash.d.ts.map +1 -0
  250. package/dist/trash.js +127 -0
  251. package/dist/types.d.ts +23 -0
  252. package/dist/types.d.ts.map +1 -0
  253. package/dist/types.js +1 -0
  254. package/dist/walk.d.ts +26 -0
  255. package/dist/walk.d.ts.map +1 -0
  256. package/dist/walk.js +169 -0
  257. package/dist/write-queue.d.ts +2 -0
  258. package/dist/write-queue.d.ts.map +1 -0
  259. package/dist/write-queue.js +18 -0
  260. package/package.json +117 -0
@@ -0,0 +1,47 @@
1
+ import path from "node:path";
2
+ import { resolveSafeBaseDir } from "./path.js";
3
+ export function isWindowsDrivePath(value) {
4
+ return /^[a-zA-Z]:[\\/]/.test(value);
5
+ }
6
+ export function normalizeArchiveEntryPath(raw) {
7
+ return raw.replaceAll("\\", "/");
8
+ }
9
+ export function validateArchiveEntryPath(entryPath, params) {
10
+ if (!entryPath || entryPath === "." || entryPath === "./") {
11
+ return;
12
+ }
13
+ if (isWindowsDrivePath(entryPath)) {
14
+ throw new Error(`archive entry uses a drive path: ${entryPath}`);
15
+ }
16
+ const normalized = path.posix.normalize(normalizeArchiveEntryPath(entryPath));
17
+ const escapeLabel = params?.escapeLabel ?? "destination";
18
+ if (normalized === ".." || normalized.startsWith("../")) {
19
+ throw new Error(`archive entry escapes ${escapeLabel}: ${entryPath}`);
20
+ }
21
+ if (path.posix.isAbsolute(normalized) || normalized.startsWith("//")) {
22
+ throw new Error(`archive entry is absolute: ${entryPath}`);
23
+ }
24
+ }
25
+ export function stripArchivePath(entryPath, stripComponents) {
26
+ const raw = normalizeArchiveEntryPath(entryPath);
27
+ if (!raw || raw === "." || raw === "./") {
28
+ return null;
29
+ }
30
+ const parts = raw.split("/").filter((part) => part.length > 0 && part !== ".");
31
+ const strip = Math.max(0, Math.floor(stripComponents));
32
+ const stripped = strip === 0 ? parts.join("/") : parts.slice(strip).join("/");
33
+ const result = path.posix.normalize(stripped);
34
+ if (!result || result === "." || result === "./") {
35
+ return null;
36
+ }
37
+ return result;
38
+ }
39
+ export function resolveArchiveOutputPath(params) {
40
+ const safeBase = resolveSafeBaseDir(params.rootDir);
41
+ const outPath = path.resolve(params.rootDir, params.relPath);
42
+ const escapeLabel = params.escapeLabel ?? "destination";
43
+ if (!outPath.startsWith(safeBase)) {
44
+ throw new Error(`archive entry escapes ${escapeLabel}: ${params.originalPath}`);
45
+ }
46
+ return outPath;
47
+ }
@@ -0,0 +1,8 @@
1
+ export type ArchiveKind = "tar" | "zip";
2
+ export declare function resolveArchiveKind(filePath: string): ArchiveKind | null;
3
+ type ResolvePackedRootDirOptions = {
4
+ rootMarkers?: string[];
5
+ };
6
+ export declare function resolvePackedRootDir(extractDir: string, options?: ResolvePackedRootDirOptions): Promise<string>;
7
+ export {};
8
+ //# sourceMappingURL=archive-kind.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-kind.d.ts","sourceRoot":"","sources":["../src/archive-kind.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,KAAK,CAAC;AAIxC,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CASvE;AAED,KAAK,2BAA2B,GAAG;IACjC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AAkBF,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,2BAA2B,GACpC,OAAO,CAAC,MAAM,CAAC,CA4BjB"}
@@ -0,0 +1,58 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { normalizeLowercaseStringOrEmpty } from "./string-coerce.js";
4
+ const TAR_SUFFIXES = [".tgz", ".tar.gz", ".tar"];
5
+ export function resolveArchiveKind(filePath) {
6
+ const lower = normalizeLowercaseStringOrEmpty(filePath);
7
+ if (lower.endsWith(".zip")) {
8
+ return "zip";
9
+ }
10
+ if (TAR_SUFFIXES.some((suffix) => lower.endsWith(suffix))) {
11
+ return "tar";
12
+ }
13
+ return null;
14
+ }
15
+ async function hasPackedRootMarker(extractDir, rootMarkers) {
16
+ for (const marker of rootMarkers) {
17
+ const trimmed = marker.trim();
18
+ if (!trimmed) {
19
+ continue;
20
+ }
21
+ try {
22
+ await fs.stat(path.join(extractDir, trimmed));
23
+ return true;
24
+ }
25
+ catch {
26
+ // ignore
27
+ }
28
+ }
29
+ return false;
30
+ }
31
+ export async function resolvePackedRootDir(extractDir, options) {
32
+ const direct = path.join(extractDir, "package");
33
+ try {
34
+ const stat = await fs.stat(direct);
35
+ if (stat.isDirectory()) {
36
+ return direct;
37
+ }
38
+ }
39
+ catch {
40
+ // ignore
41
+ }
42
+ if ((options?.rootMarkers?.length ?? 0) > 0) {
43
+ const hasMarker = await hasPackedRootMarker(extractDir, options?.rootMarkers ?? []);
44
+ if (hasMarker) {
45
+ return extractDir;
46
+ }
47
+ }
48
+ const entries = await fs.readdir(extractDir, { withFileTypes: true });
49
+ const dirs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
50
+ if (dirs.length !== 1) {
51
+ throw new Error(`unexpected archive layout (dirs: ${dirs.join(", ")})`);
52
+ }
53
+ const onlyDir = dirs[0];
54
+ if (!onlyDir) {
55
+ throw new Error("unexpected archive layout (no package dir found)");
56
+ }
57
+ return path.join(extractDir, onlyDir);
58
+ }
@@ -0,0 +1,40 @@
1
+ import { Transform } from "node:stream";
2
+ export type ArchiveExtractLimits = {
3
+ /**
4
+ * Max archive file bytes (compressed).
5
+ */
6
+ maxArchiveBytes?: number;
7
+ /** Max number of extracted entries (files + dirs). */
8
+ maxEntries?: number;
9
+ /** Max extracted bytes (sum of all files). */
10
+ maxExtractedBytes?: number;
11
+ /** Max extracted bytes for a single file entry. */
12
+ maxEntryBytes?: number;
13
+ };
14
+ export declare const DEFAULT_MAX_ARCHIVE_BYTES_ZIP: number;
15
+ export declare const DEFAULT_MAX_ENTRIES = 50000;
16
+ export declare const DEFAULT_MAX_EXTRACTED_BYTES: number;
17
+ export declare const DEFAULT_MAX_ENTRY_BYTES: number;
18
+ export declare const ARCHIVE_LIMIT_ERROR_CODE: {
19
+ readonly ARCHIVE_SIZE_EXCEEDS_LIMIT: "archive-size-exceeds-limit";
20
+ readonly ENTRY_COUNT_EXCEEDS_LIMIT: "archive-entry-count-exceeds-limit";
21
+ readonly ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT: "archive-entry-extracted-size-exceeds-limit";
22
+ readonly EXTRACTED_SIZE_EXCEEDS_LIMIT: "archive-extracted-size-exceeds-limit";
23
+ };
24
+ export type ArchiveLimitErrorCode = (typeof ARCHIVE_LIMIT_ERROR_CODE)[keyof typeof ARCHIVE_LIMIT_ERROR_CODE];
25
+ export declare class ArchiveLimitError extends Error {
26
+ readonly code: ArchiveLimitErrorCode;
27
+ constructor(code: ArchiveLimitErrorCode);
28
+ }
29
+ export type ResolvedArchiveExtractLimits = Required<ArchiveExtractLimits>;
30
+ export declare function resolveExtractLimits(limits?: ArchiveExtractLimits): ResolvedArchiveExtractLimits;
31
+ export declare function assertArchiveEntryCountWithinLimit(entryCount: number, limits: ResolvedArchiveExtractLimits): void;
32
+ export declare function createByteBudgetTracker(limits: ResolvedArchiveExtractLimits): {
33
+ startEntry: () => void;
34
+ addBytes: (bytes: number) => void;
35
+ addEntrySize: (size: number) => void;
36
+ };
37
+ export declare function createExtractBudgetTransform(params: {
38
+ onChunkBytes: (bytes: number) => void;
39
+ }): Transform;
40
+ //# sourceMappingURL=archive-limits.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-limits.d.ts","sourceRoot":"","sources":["../src/archive-limits.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,MAAM,oBAAoB,GAAG;IACjC;;OAEG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mDAAmD;IACnD,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,eAAO,MAAM,6BAA6B,QAAoB,CAAC;AAC/D,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAC1C,eAAO,MAAM,2BAA2B,QAAoB,CAAC;AAC7D,eAAO,MAAM,uBAAuB,QAAoB,CAAC;AAEzD,eAAO,MAAM,wBAAwB;;;;;CAK3B,CAAC;AAEX,MAAM,MAAM,qBAAqB,GAC/B,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,OAAO,wBAAwB,CAAC,CAAC;AAU3E,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;gBAEzB,IAAI,EAAE,qBAAqB;CAKxC;AAED,MAAM,MAAM,4BAA4B,GAAG,QAAQ,CAAC,oBAAoB,CAAC,CAAC;AAU1E,wBAAgB,oBAAoB,CAClC,MAAM,CAAC,EAAE,oBAAoB,GAC5B,4BAA4B,CAQ9B;AAED,wBAAgB,kCAAkC,CAChD,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,4BAA4B,QAKrC;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,4BAA4B,GAAG;IAC7E,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC,CAiCA;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE;IACnD,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC,GAAG,SAAS,CAYZ"}
@@ -0,0 +1,92 @@
1
+ import { Transform } from "node:stream";
2
+ export const DEFAULT_MAX_ARCHIVE_BYTES_ZIP = 256 * 1024 * 1024;
3
+ export const DEFAULT_MAX_ENTRIES = 50_000;
4
+ export const DEFAULT_MAX_EXTRACTED_BYTES = 512 * 1024 * 1024;
5
+ export const DEFAULT_MAX_ENTRY_BYTES = 256 * 1024 * 1024;
6
+ export const ARCHIVE_LIMIT_ERROR_CODE = {
7
+ ARCHIVE_SIZE_EXCEEDS_LIMIT: "archive-size-exceeds-limit",
8
+ ENTRY_COUNT_EXCEEDS_LIMIT: "archive-entry-count-exceeds-limit",
9
+ ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT: "archive-entry-extracted-size-exceeds-limit",
10
+ EXTRACTED_SIZE_EXCEEDS_LIMIT: "archive-extracted-size-exceeds-limit",
11
+ };
12
+ const ARCHIVE_LIMIT_ERROR_MESSAGE = {
13
+ [ARCHIVE_LIMIT_ERROR_CODE.ARCHIVE_SIZE_EXCEEDS_LIMIT]: "archive size exceeds limit",
14
+ [ARCHIVE_LIMIT_ERROR_CODE.ENTRY_COUNT_EXCEEDS_LIMIT]: "archive entry count exceeds limit",
15
+ [ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT]: "archive entry extracted size exceeds limit",
16
+ [ARCHIVE_LIMIT_ERROR_CODE.EXTRACTED_SIZE_EXCEEDS_LIMIT]: "archive extracted size exceeds limit",
17
+ };
18
+ export class ArchiveLimitError extends Error {
19
+ code;
20
+ constructor(code) {
21
+ super(ARCHIVE_LIMIT_ERROR_MESSAGE[code]);
22
+ this.name = "ArchiveLimitError";
23
+ this.code = code;
24
+ }
25
+ }
26
+ function clampLimit(value) {
27
+ if (typeof value !== "number" || !Number.isFinite(value)) {
28
+ return undefined;
29
+ }
30
+ const v = Math.floor(value);
31
+ return v > 0 ? v : undefined;
32
+ }
33
+ export function resolveExtractLimits(limits) {
34
+ // Defaults: defensive, but should not break normal installs.
35
+ return {
36
+ maxArchiveBytes: clampLimit(limits?.maxArchiveBytes) ?? DEFAULT_MAX_ARCHIVE_BYTES_ZIP,
37
+ maxEntries: clampLimit(limits?.maxEntries) ?? DEFAULT_MAX_ENTRIES,
38
+ maxExtractedBytes: clampLimit(limits?.maxExtractedBytes) ?? DEFAULT_MAX_EXTRACTED_BYTES,
39
+ maxEntryBytes: clampLimit(limits?.maxEntryBytes) ?? DEFAULT_MAX_ENTRY_BYTES,
40
+ };
41
+ }
42
+ export function assertArchiveEntryCountWithinLimit(entryCount, limits) {
43
+ if (entryCount > limits.maxEntries) {
44
+ throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_COUNT_EXCEEDS_LIMIT);
45
+ }
46
+ }
47
+ export function createByteBudgetTracker(limits) {
48
+ let entryBytes = 0;
49
+ let extractedBytes = 0;
50
+ const addBytes = (bytes) => {
51
+ const b = Math.max(0, Math.floor(bytes));
52
+ if (b === 0) {
53
+ return;
54
+ }
55
+ entryBytes += b;
56
+ if (entryBytes > limits.maxEntryBytes) {
57
+ throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
58
+ }
59
+ extractedBytes += b;
60
+ if (extractedBytes > limits.maxExtractedBytes) {
61
+ throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.EXTRACTED_SIZE_EXCEEDS_LIMIT);
62
+ }
63
+ };
64
+ return {
65
+ startEntry() {
66
+ entryBytes = 0;
67
+ },
68
+ addBytes,
69
+ addEntrySize(size) {
70
+ const s = Math.max(0, Math.floor(size));
71
+ if (s > limits.maxEntryBytes) {
72
+ throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
73
+ }
74
+ // Tar budgets are based on the header-declared size.
75
+ addBytes(s);
76
+ },
77
+ };
78
+ }
79
+ export function createExtractBudgetTransform(params) {
80
+ return new Transform({
81
+ transform(chunk, _encoding, callback) {
82
+ try {
83
+ const buf = chunk instanceof Buffer ? chunk : Buffer.from(chunk);
84
+ params.onChunkBytes(buf.byteLength);
85
+ callback(null, buf);
86
+ }
87
+ catch (err) {
88
+ callback(err instanceof Error ? err : new Error(String(err)));
89
+ }
90
+ },
91
+ });
92
+ }
@@ -0,0 +1,26 @@
1
+ export type ArchiveSecurityErrorCode = "destination-not-directory" | "destination-symlink" | "destination-symlink-traversal";
2
+ export declare class ArchiveSecurityError extends Error {
3
+ code: ArchiveSecurityErrorCode;
4
+ constructor(code: ArchiveSecurityErrorCode, message: string, options?: ErrorOptions);
5
+ }
6
+ export declare function prepareArchiveDestinationDir(destDir: string): Promise<string>;
7
+ export declare function prepareArchiveOutputPath(params: {
8
+ destinationDir: string;
9
+ destinationRealDir: string;
10
+ relPath: string;
11
+ outPath: string;
12
+ originalPath: string;
13
+ isDirectory: boolean;
14
+ }): Promise<void>;
15
+ export declare function withStagedArchiveDestination<T>(params: {
16
+ destinationRealDir: string;
17
+ stagingDirPrefix?: string;
18
+ run: (stagingDir: string) => Promise<T>;
19
+ }): Promise<T>;
20
+ export declare function mergeExtractedTreeIntoDestination(params: {
21
+ sourceDir: string;
22
+ destinationDir: string;
23
+ destinationRealDir: string;
24
+ }): Promise<void>;
25
+ export declare function createArchiveSymlinkTraversalError(originalPath: string): ArchiveSecurityError;
26
+ //# sourceMappingURL=archive-staging.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-staging.d.ts","sourceRoot":"","sources":["../src/archive-staging.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,wBAAwB,GAChC,2BAA2B,GAC3B,qBAAqB,GACrB,+BAA+B,CAAC;AAEpC,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,IAAI,EAAE,wBAAwB,CAAC;gBAEnB,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY;CAKpF;AASD,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAYnF;AA6CD,wBAAsB,wBAAwB,CAAC,MAAM,EAAE;IACrD,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBhB;AAmBD,wBAAsB,4BAA4B,CAAC,CAAC,EAAE,MAAM,EAAE;IAC5D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,GAAG,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;CACzC,GAAG,OAAO,CAAC,CAAC,CAAC,CAkBb;AAED,wBAAsB,iCAAiC,CAAC,MAAM,EAAE;IAC9D,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;CAC5B,GAAG,OAAO,CAAC,IAAI,CAAC,CAyDhB;AAED,wBAAgB,kCAAkC,CAAC,YAAY,EAAE,MAAM,GAAG,oBAAoB,CAE7F"}
@@ -0,0 +1,171 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { root } from "./root.js";
4
+ import { isNotFoundPathError, isPathInside } from "./path.js";
5
+ import { resolveSecureTempRoot } from "./secure-temp-dir.js";
6
+ const ERROR_ARCHIVE_ENTRY_TRAVERSES_SYMLINK = "archive entry traverses symlink in destination";
7
+ const ARCHIVE_STAGING_MODE = 0o700;
8
+ export class ArchiveSecurityError extends Error {
9
+ code;
10
+ constructor(code, message, options) {
11
+ super(message, options);
12
+ this.code = code;
13
+ this.name = "ArchiveSecurityError";
14
+ }
15
+ }
16
+ function symlinkTraversalError(originalPath) {
17
+ return new ArchiveSecurityError("destination-symlink-traversal", `${ERROR_ARCHIVE_ENTRY_TRAVERSES_SYMLINK}: ${originalPath}`);
18
+ }
19
+ export async function prepareArchiveDestinationDir(destDir) {
20
+ const stat = await fs.lstat(destDir);
21
+ if (stat.isSymbolicLink()) {
22
+ throw new ArchiveSecurityError("destination-symlink", "archive destination is a symlink");
23
+ }
24
+ if (!stat.isDirectory()) {
25
+ throw new ArchiveSecurityError("destination-not-directory", "archive destination is not a directory");
26
+ }
27
+ return await fs.realpath(destDir);
28
+ }
29
+ async function assertNoSymlinkTraversal(params) {
30
+ const parts = params.relPath.split(/[\\/]+/).filter(Boolean);
31
+ let current = path.resolve(params.rootDir);
32
+ for (const part of parts) {
33
+ current = path.join(current, part);
34
+ let stat;
35
+ try {
36
+ stat = await fs.lstat(current);
37
+ }
38
+ catch (err) {
39
+ if (isNotFoundPathError(err)) {
40
+ continue;
41
+ }
42
+ throw err;
43
+ }
44
+ if (stat.isSymbolicLink()) {
45
+ throw symlinkTraversalError(params.originalPath);
46
+ }
47
+ }
48
+ }
49
+ async function assertResolvedInsideDestination(params) {
50
+ let resolved;
51
+ try {
52
+ resolved = await fs.realpath(params.targetPath);
53
+ }
54
+ catch (err) {
55
+ if (isNotFoundPathError(err)) {
56
+ return;
57
+ }
58
+ throw err;
59
+ }
60
+ if (!isPathInside(params.destinationRealDir, resolved)) {
61
+ throw symlinkTraversalError(params.originalPath);
62
+ }
63
+ }
64
+ export async function prepareArchiveOutputPath(params) {
65
+ await assertNoSymlinkTraversal({
66
+ rootDir: params.destinationDir,
67
+ relPath: params.relPath,
68
+ originalPath: params.originalPath,
69
+ });
70
+ if (params.isDirectory) {
71
+ await fs.mkdir(params.outPath, { recursive: true });
72
+ await assertResolvedInsideDestination({
73
+ destinationRealDir: params.destinationRealDir,
74
+ targetPath: params.outPath,
75
+ originalPath: params.originalPath,
76
+ });
77
+ return;
78
+ }
79
+ const parentDir = path.dirname(params.outPath);
80
+ await fs.mkdir(parentDir, { recursive: true });
81
+ await assertResolvedInsideDestination({
82
+ destinationRealDir: params.destinationRealDir,
83
+ targetPath: parentDir,
84
+ originalPath: params.originalPath,
85
+ });
86
+ }
87
+ async function applyStagedEntryMode(params) {
88
+ const destinationPath = path.join(params.destinationRealDir, params.relPath);
89
+ await assertResolvedInsideDestination({
90
+ destinationRealDir: params.destinationRealDir,
91
+ targetPath: destinationPath,
92
+ originalPath: params.originalPath,
93
+ });
94
+ if (params.mode !== 0) {
95
+ await fs.chmod(destinationPath, params.mode).catch(() => undefined);
96
+ }
97
+ }
98
+ export async function withStagedArchiveDestination(params) {
99
+ const stagingRoot = resolveSecureTempRoot({
100
+ fallbackPrefix: "fs-safe-archive",
101
+ unsafeFallbackLabel: "archive staging temp dir",
102
+ warn: () => undefined,
103
+ });
104
+ if (isPathInside(params.destinationRealDir, stagingRoot)) {
105
+ throw new Error(`archive staging root must be outside destination: ${stagingRoot}`);
106
+ }
107
+ const stagingDir = await fs.mkdtemp(path.join(stagingRoot, params.stagingDirPrefix ?? "fs-safe-archive-"));
108
+ try {
109
+ await fs.chmod(stagingDir, ARCHIVE_STAGING_MODE).catch(() => undefined);
110
+ return await params.run(stagingDir);
111
+ }
112
+ finally {
113
+ await fs.rm(stagingDir, { recursive: true, force: true }).catch(() => undefined);
114
+ }
115
+ }
116
+ export async function mergeExtractedTreeIntoDestination(params) {
117
+ const targetRoot = await root(params.destinationRealDir);
118
+ const walk = async (currentSourceDir) => {
119
+ const entries = await fs.readdir(currentSourceDir, { withFileTypes: true });
120
+ for (const entry of entries) {
121
+ const sourcePath = path.join(currentSourceDir, entry.name);
122
+ const relPath = path.relative(params.sourceDir, sourcePath);
123
+ const originalPath = relPath.split(path.sep).join("/");
124
+ const destinationPath = path.join(params.destinationDir, relPath);
125
+ const sourceStat = await fs.lstat(sourcePath);
126
+ if (sourceStat.isSymbolicLink()) {
127
+ throw symlinkTraversalError(originalPath);
128
+ }
129
+ if (sourceStat.isDirectory()) {
130
+ await prepareArchiveOutputPath({
131
+ destinationDir: params.destinationDir,
132
+ destinationRealDir: params.destinationRealDir,
133
+ relPath,
134
+ outPath: destinationPath,
135
+ originalPath,
136
+ isDirectory: true,
137
+ });
138
+ await walk(sourcePath);
139
+ await applyStagedEntryMode({
140
+ destinationRealDir: params.destinationRealDir,
141
+ relPath,
142
+ mode: sourceStat.mode & 0o777,
143
+ originalPath,
144
+ });
145
+ continue;
146
+ }
147
+ if (!sourceStat.isFile()) {
148
+ throw new Error(`archive staging contains unsupported entry: ${originalPath}`);
149
+ }
150
+ await prepareArchiveOutputPath({
151
+ destinationDir: params.destinationDir,
152
+ destinationRealDir: params.destinationRealDir,
153
+ relPath,
154
+ outPath: destinationPath,
155
+ originalPath,
156
+ isDirectory: false,
157
+ });
158
+ await targetRoot.copyIn(relPath, sourcePath, { mkdir: true });
159
+ await applyStagedEntryMode({
160
+ destinationRealDir: params.destinationRealDir,
161
+ relPath,
162
+ mode: sourceStat.mode & 0o777,
163
+ originalPath,
164
+ });
165
+ }
166
+ };
167
+ await walk(params.sourceDir);
168
+ }
169
+ export function createArchiveSymlinkTraversalError(originalPath) {
170
+ return symlinkTraversalError(originalPath);
171
+ }
@@ -0,0 +1,14 @@
1
+ import { type ArchiveExtractLimits } from "./archive-limits.js";
2
+ export type TarEntryInfo = {
3
+ path: string;
4
+ type: string;
5
+ size: number;
6
+ };
7
+ export declare function readTarEntryInfo(entry: unknown): TarEntryInfo;
8
+ export declare function createTarEntryPreflightChecker(params: {
9
+ rootDir: string;
10
+ stripComponents?: number;
11
+ limits?: ArchiveExtractLimits;
12
+ escapeLabel?: string;
13
+ }): (entry: TarEntryInfo) => void;
14
+ //# sourceMappingURL=archive-tar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-tar.d.ts","sourceRoot":"","sources":["../src/archive-tar.ts"],"names":[],"mappings":"AAKA,OAAO,EAIL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAE7B,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAWxE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,CAkB7D;AAED,wBAAgB,8BAA8B,CAAC,MAAM,EAAE;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CA6BhC"}
@@ -0,0 +1,52 @@
1
+ import { resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
2
+ import { assertArchiveEntryCountWithinLimit, createByteBudgetTracker, resolveExtractLimits, } from "./archive-limits.js";
3
+ const BLOCKED_TAR_ENTRY_TYPES = new Set([
4
+ "SymbolicLink",
5
+ "Link",
6
+ "BlockDevice",
7
+ "CharacterDevice",
8
+ "FIFO",
9
+ "Socket",
10
+ ]);
11
+ export function readTarEntryInfo(entry) {
12
+ const p = typeof entry === "object" && entry !== null && "path" in entry
13
+ ? String(entry.path)
14
+ : "";
15
+ const t = typeof entry === "object" && entry !== null && "type" in entry
16
+ ? String(entry.type)
17
+ : "";
18
+ const s = typeof entry === "object" &&
19
+ entry !== null &&
20
+ "size" in entry &&
21
+ typeof entry.size === "number" &&
22
+ Number.isFinite(entry.size)
23
+ ? Math.max(0, Math.floor(entry.size))
24
+ : 0;
25
+ return { path: p, type: t, size: s };
26
+ }
27
+ export function createTarEntryPreflightChecker(params) {
28
+ const strip = Math.max(0, Math.floor(params.stripComponents ?? 0));
29
+ const limits = resolveExtractLimits(params.limits);
30
+ let entryCount = 0;
31
+ const budget = createByteBudgetTracker(limits);
32
+ return (entry) => {
33
+ validateArchiveEntryPath(entry.path, { escapeLabel: params.escapeLabel });
34
+ const relPath = stripArchivePath(entry.path, strip);
35
+ if (!relPath) {
36
+ return;
37
+ }
38
+ validateArchiveEntryPath(relPath, { escapeLabel: params.escapeLabel });
39
+ resolveArchiveOutputPath({
40
+ rootDir: params.rootDir,
41
+ relPath,
42
+ originalPath: entry.path,
43
+ escapeLabel: params.escapeLabel,
44
+ });
45
+ if (BLOCKED_TAR_ENTRY_TYPES.has(entry.type)) {
46
+ throw new Error(`tar entry is a link: ${entry.path}`);
47
+ }
48
+ entryCount += 1;
49
+ assertArchiveEntryCountWithinLimit(entryCount, limits);
50
+ budget.addEntrySize(entry.size);
51
+ };
52
+ }
@@ -0,0 +1,3 @@
1
+ export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T>;
2
+ export declare function fileExists(filePath: string): Promise<boolean>;
3
+ //# sourceMappingURL=archive-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-utils.d.ts","sourceRoot":"","sources":["../src/archive-utils.ts"],"names":[],"mappings":"AAEA,wBAAsB,WAAW,CAAC,CAAC,EACjC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,CAAC,CAAC,CAiBZ;AAED,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOnE"}
@@ -0,0 +1,26 @@
1
+ import fs from "node:fs/promises";
2
+ export async function withTimeout(promise, timeoutMs, label) {
3
+ let timeoutId;
4
+ try {
5
+ return await Promise.race([
6
+ promise,
7
+ new Promise((_, reject) => {
8
+ timeoutId = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
9
+ }),
10
+ ]);
11
+ }
12
+ finally {
13
+ if (timeoutId) {
14
+ clearTimeout(timeoutId);
15
+ }
16
+ }
17
+ }
18
+ export async function fileExists(filePath) {
19
+ try {
20
+ await fs.stat(filePath);
21
+ return true;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
@@ -0,0 +1,7 @@
1
+ import { type ArchiveExtractLimits } from "./archive-limits.js";
2
+ export type ZipArchiveWithFiles = {
3
+ files: Record<string, unknown>;
4
+ };
5
+ export declare function readZipCentralDirectoryEntryCount(buffer: Buffer | Uint8Array): number | null;
6
+ export declare function loadZipArchiveWithPreflight(buffer: Buffer | Uint8Array, limits?: ArchiveExtractLimits): Promise<ZipArchiveWithFiles>;
7
+ //# sourceMappingURL=archive-zip-preflight.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-zip-preflight.d.ts","sourceRoot":"","sources":["../src/archive-zip-preflight.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAE7B,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,CAAC;AAiLF,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,GAAG,IAAI,CAU5F;AAED,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,MAAM,GAAG,UAAU,EAC3B,MAAM,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,mBAAmB,CAAC,CAW9B"}