@resq-systems/security 1.0.4 → 2.0.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/README.md +236 -33
- package/lib/controls/address.d.mts +142 -0
- package/lib/controls/address.d.mts.map +1 -0
- package/lib/controls/address.mjs +533 -0
- package/lib/controls/address.mjs.map +1 -0
- package/lib/controls/csrf.d.mts +91 -0
- package/lib/controls/csrf.d.mts.map +1 -0
- package/lib/controls/csrf.mjs +200 -0
- package/lib/controls/csrf.mjs.map +1 -0
- package/lib/controls/index.d.mts +8 -0
- package/lib/controls/index.mjs +8 -0
- package/lib/controls/origin.d.mts +95 -0
- package/lib/controls/origin.d.mts.map +1 -0
- package/lib/controls/origin.mjs +156 -0
- package/lib/controls/origin.mjs.map +1 -0
- package/lib/controls/payload.d.mts +84 -0
- package/lib/controls/payload.d.mts.map +1 -0
- package/lib/controls/payload.mjs +147 -0
- package/lib/controls/payload.mjs.map +1 -0
- package/lib/controls/query.d.mts +157 -0
- package/lib/controls/query.d.mts.map +1 -0
- package/lib/controls/query.mjs +368 -0
- package/lib/controls/query.mjs.map +1 -0
- package/lib/controls/redirect.d.mts +92 -0
- package/lib/controls/redirect.d.mts.map +1 -0
- package/lib/controls/redirect.mjs +110 -0
- package/lib/controls/redirect.mjs.map +1 -0
- package/lib/controls/upload.d.mts +108 -0
- package/lib/controls/upload.d.mts.map +1 -0
- package/lib/controls/upload.mjs +374 -0
- package/lib/controls/upload.mjs.map +1 -0
- package/lib/crypto.d.mts +18 -5
- package/lib/crypto.d.mts.map +1 -1
- package/lib/crypto.mjs +35 -24
- package/lib/crypto.mjs.map +1 -1
- package/lib/hash.d.mts +83 -0
- package/lib/hash.d.mts.map +1 -0
- package/lib/hash.mjs +111 -0
- package/lib/hash.mjs.map +1 -0
- package/lib/index.d.mts +18 -2
- package/lib/index.mjs +20 -2
- package/lib/paths.d.mts +92 -0
- package/lib/paths.d.mts.map +1 -0
- package/lib/paths.mjs +140 -0
- package/lib/paths.mjs.map +1 -0
- package/lib/sanitize.d.mts +137 -35
- package/lib/sanitize.d.mts.map +1 -1
- package/lib/sanitize.mjs +170 -46
- package/lib/sanitize.mjs.map +1 -1
- package/lib/threats/capec.generated.d.mts +59 -0
- package/lib/threats/capec.generated.d.mts.map +1 -0
- package/lib/threats/capec.generated.mjs +644 -0
- package/lib/threats/capec.generated.mjs.map +1 -0
- package/lib/threats/engine.d.mts +94 -0
- package/lib/threats/engine.d.mts.map +1 -0
- package/lib/threats/engine.mjs +167 -0
- package/lib/threats/engine.mjs.map +1 -0
- package/lib/threats/index.d.mts +11 -0
- package/lib/threats/index.mjs +11 -0
- package/lib/threats/rules/datastore.d.mts +13 -0
- package/lib/threats/rules/datastore.d.mts.map +1 -0
- package/lib/threats/rules/datastore.mjs +366 -0
- package/lib/threats/rules/datastore.mjs.map +1 -0
- package/lib/threats/rules/index.d.mts +54 -0
- package/lib/threats/rules/index.d.mts.map +1 -0
- package/lib/threats/rules/index.mjs +121 -0
- package/lib/threats/rules/index.mjs.map +1 -0
- package/lib/threats/rules/markup.d.mts +28 -0
- package/lib/threats/rules/markup.d.mts.map +1 -0
- package/lib/threats/rules/markup.mjs +373 -0
- package/lib/threats/rules/markup.mjs.map +1 -0
- package/lib/threats/rules/protocol.d.mts +49 -0
- package/lib/threats/rules/protocol.d.mts.map +1 -0
- package/lib/threats/rules/protocol.mjs +175 -0
- package/lib/threats/rules/protocol.mjs.map +1 -0
- package/lib/threats/rules/system.d.mts +19 -0
- package/lib/threats/rules/system.d.mts.map +1 -0
- package/lib/threats/rules/system.mjs +455 -0
- package/lib/threats/rules/system.mjs.map +1 -0
- package/lib/threats/rules/web.d.mts +26 -0
- package/lib/threats/rules/web.d.mts.map +1 -0
- package/lib/threats/rules/web.mjs +412 -0
- package/lib/threats/rules/web.mjs.map +1 -0
- package/lib/threats/scoring.d.mts +59 -0
- package/lib/threats/scoring.d.mts.map +1 -0
- package/lib/threats/scoring.mjs +111 -0
- package/lib/threats/scoring.mjs.map +1 -0
- package/lib/threats/types.d.mts +245 -0
- package/lib/threats/types.d.mts.map +1 -0
- package/lib/threats/types.mjs +52 -0
- package/lib/threats/types.mjs.map +1 -0
- package/lib/threats/variants.d.mts +57 -0
- package/lib/threats/variants.d.mts.map +1 -0
- package/lib/threats/variants.mjs +144 -0
- package/lib/threats/variants.mjs.map +1 -0
- package/lib/unicode/confusables.d.mts +82 -0
- package/lib/unicode/confusables.d.mts.map +1 -0
- package/lib/unicode/confusables.mjs +954 -0
- package/lib/unicode/confusables.mjs.map +1 -0
- package/lib/unicode/index.d.mts +126 -0
- package/lib/unicode/index.d.mts.map +1 -0
- package/lib/unicode/index.mjs +288 -0
- package/lib/unicode/index.mjs.map +1 -0
- package/lib/validators.d.mts +341 -164
- package/lib/validators.d.mts.map +1 -1
- package/lib/validators.mjs +519 -338
- package/lib/validators.mjs.map +1 -1
- package/package.json +35 -8
package/lib/paths.mjs
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
//#region src/paths.ts
|
|
3
|
+
/**
|
|
4
|
+
* Copyright 2026 ResQ Systems, Inc.
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* @fileoverview Path containment — the *prevention* half of CWE-22, as distinct from
|
|
20
|
+
* the detection rules in `@resq-systems/security/threats`.
|
|
21
|
+
*
|
|
22
|
+
* Detecting `../` is not the control. CWE-22 describes the failure to keep a
|
|
23
|
+
* constructed path inside its restricted directory, so the control is to resolve the
|
|
24
|
+
* candidate against the base and verify the result still lives beneath it. That check
|
|
25
|
+
* holds for inputs no signature catches: an absolute path, a Windows drive-relative
|
|
26
|
+
* path, an alternate separator, or a name whose `..` only appears after normalization.
|
|
27
|
+
*
|
|
28
|
+
* **Node-only.** This module statically imports `node:path` and is therefore published
|
|
29
|
+
* exclusively under the `@resq-systems/security/paths` subpath, never through the
|
|
30
|
+
* package root, so browser bundles of the root barrel stay free of Node builtins.
|
|
31
|
+
*
|
|
32
|
+
* @module @resq-systems/security/paths
|
|
33
|
+
*/
|
|
34
|
+
/** NUL truncates paths in some syscall layers, so it is rejected before resolution. */
|
|
35
|
+
const NUL = "\0";
|
|
36
|
+
/**
|
|
37
|
+
* Resolve an untrusted path against a base directory and verify containment.
|
|
38
|
+
*
|
|
39
|
+
* The candidate is resolved against the base — which also normalizes `.`, `..`, and
|
|
40
|
+
* duplicate separators — and accepted only if the result is the base or sits beneath
|
|
41
|
+
* it. An absolute `untrustedPath` overrides the base under `path.resolve` semantics,
|
|
42
|
+
* so it too is caught by the containment check rather than silently trusted.
|
|
43
|
+
*
|
|
44
|
+
* **This function performs no filesystem I/O**, so it cannot see symlinks. A path that
|
|
45
|
+
* is textually contained may still resolve on disk to a target outside the base. When
|
|
46
|
+
* the target may exist and may be a link, follow up with `fs.promises.realpath` on
|
|
47
|
+
* both the base and the result and re-run {@link isPathContained} on those.
|
|
48
|
+
*
|
|
49
|
+
* @param baseDirectory - Directory the result must stay within. Resolved against the
|
|
50
|
+
* process working directory when relative.
|
|
51
|
+
* @param untrustedPath - Candidate path from an untrusted source.
|
|
52
|
+
* @param options - See {@link ContainmentOptions}.
|
|
53
|
+
* @returns The resolved absolute path when contained, otherwise `null`. Also `null`
|
|
54
|
+
* for non-string arguments, an empty base, or a NUL byte in either argument.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* const base = "/srv/uploads";
|
|
59
|
+
*
|
|
60
|
+
* resolveContainedPath(base, "avatar.png"); // "/srv/uploads/avatar.png"
|
|
61
|
+
* resolveContainedPath(base, "nested/a.txt"); // "/srv/uploads/nested/a.txt"
|
|
62
|
+
* resolveContainedPath(base, "../../etc/passwd"); // null
|
|
63
|
+
* resolveContainedPath(base, "/etc/passwd"); // null
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
function resolveContainedPath(baseDirectory, untrustedPath, options = {}) {
|
|
67
|
+
if (typeof baseDirectory !== "string" || typeof untrustedPath !== "string") return null;
|
|
68
|
+
if (baseDirectory.length === 0) return null;
|
|
69
|
+
if (untrustedPath.includes(NUL) || baseDirectory.includes(NUL)) return null;
|
|
70
|
+
const { allowBaseItself = true } = options;
|
|
71
|
+
const base = path.resolve(baseDirectory);
|
|
72
|
+
const candidate = path.resolve(base, untrustedPath);
|
|
73
|
+
const relative = path.relative(base, candidate);
|
|
74
|
+
if (relative === "") return allowBaseItself ? candidate : null;
|
|
75
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return null;
|
|
76
|
+
return candidate;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Boolean form of {@link resolveContainedPath}.
|
|
80
|
+
*
|
|
81
|
+
* @param baseDirectory - Directory the candidate must stay within.
|
|
82
|
+
* @param candidatePath - Path to test.
|
|
83
|
+
* @param options - See {@link ContainmentOptions}.
|
|
84
|
+
* @returns `true` when the candidate resolves inside the base.
|
|
85
|
+
*/
|
|
86
|
+
function isPathContained(baseDirectory, candidatePath, options) {
|
|
87
|
+
return resolveContainedPath(baseDirectory, candidatePath, options) !== null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Characters invalid in a Windows path segment, plus both separators and the C0
|
|
91
|
+
* control range.
|
|
92
|
+
*/
|
|
93
|
+
const UNSAFE_FILENAME_CHARS = /[<>:"/\\|?*\u0000-\u001f\u007f]/g;
|
|
94
|
+
/**
|
|
95
|
+
* Windows device names, reserved regardless of extension — opening `CON.txt` still
|
|
96
|
+
* targets the console device.
|
|
97
|
+
*/
|
|
98
|
+
const RESERVED_WINDOWS_NAMES = /^(?:CON|PRN|AUX|NUL|COM\d|LPT\d)(?:\.|$)/i;
|
|
99
|
+
/** Longest filename accepted on common filesystems. */
|
|
100
|
+
const MAX_FILENAME_LENGTH = 255;
|
|
101
|
+
/** Longest extension preserved when truncating an over-long filename. */
|
|
102
|
+
const MAX_PRESERVED_EXTENSION = 16;
|
|
103
|
+
/**
|
|
104
|
+
* Reduce an untrusted string to a single safe path segment.
|
|
105
|
+
*
|
|
106
|
+
* Strips directory separators, control characters, and Windows-reserved characters;
|
|
107
|
+
* removes leading dots so the result cannot become `.`/`..` or a hidden file; trims
|
|
108
|
+
* the trailing dots and spaces Windows silently drops (which is how `evil.php.`
|
|
109
|
+
* becomes `evil.php` after the fact); and refuses reserved device names.
|
|
110
|
+
*
|
|
111
|
+
* The output is a *segment*, never a path. Join it onto a base directory yourself and
|
|
112
|
+
* confirm the result with {@link resolveContainedPath} — that remains the containment
|
|
113
|
+
* control; this is only hygiene.
|
|
114
|
+
*
|
|
115
|
+
* @param filename - Untrusted filename, typically from a multipart upload.
|
|
116
|
+
* @param fallback - Returned when nothing usable survives. Defaults to `"file"`.
|
|
117
|
+
* @returns A safe single path segment.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* sanitizeFilename("../../etc/passwd"); // "etcpasswd"
|
|
122
|
+
* sanitizeFilename("report.pdf."); // "report.pdf"
|
|
123
|
+
* sanitizeFilename("CON.txt"); // "file"
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
function sanitizeFilename(filename, fallback = "file") {
|
|
127
|
+
if (typeof filename !== "string" || filename.length === 0) return fallback;
|
|
128
|
+
let safe = filename.normalize("NFC").replace(UNSAFE_FILENAME_CHARS, "").replace(/^\.+/, "").replace(/[. ]+$/, "").trim();
|
|
129
|
+
if (safe.length === 0) return fallback;
|
|
130
|
+
if (RESERVED_WINDOWS_NAMES.test(safe)) return fallback;
|
|
131
|
+
if (safe.length > MAX_FILENAME_LENGTH) {
|
|
132
|
+
const extension = path.extname(safe).slice(0, MAX_PRESERVED_EXTENSION);
|
|
133
|
+
safe = safe.slice(0, MAX_FILENAME_LENGTH - extension.length) + extension;
|
|
134
|
+
}
|
|
135
|
+
return safe;
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
export { isPathContained, resolveContainedPath, sanitizeFilename };
|
|
139
|
+
|
|
140
|
+
//# sourceMappingURL=paths.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.mjs","names":[],"sources":["../src/paths.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview Path containment — the *prevention* half of CWE-22, as distinct from\n * the detection rules in `@resq-systems/security/threats`.\n *\n * Detecting `../` is not the control. CWE-22 describes the failure to keep a\n * constructed path inside its restricted directory, so the control is to resolve the\n * candidate against the base and verify the result still lives beneath it. That check\n * holds for inputs no signature catches: an absolute path, a Windows drive-relative\n * path, an alternate separator, or a name whose `..` only appears after normalization.\n *\n * **Node-only.** This module statically imports `node:path` and is therefore published\n * exclusively under the `@resq-systems/security/paths` subpath, never through the\n * package root, so browser bundles of the root barrel stay free of Node builtins.\n *\n * @module @resq-systems/security/paths\n */\n\nimport path from \"node:path\";\n\n//#region Containment\n\n/** Options for {@link resolveContainedPath}. */\nexport interface ContainmentOptions {\n\t/**\n\t * Treat the base directory itself as an acceptable result. Defaults to `true`.\n\t * Pass `false` when the caller requires a path strictly *inside* the base — an\n\t * upload target, say, rather than a listing root.\n\t */\n\treadonly allowBaseItself?: boolean;\n}\n\n/** NUL truncates paths in some syscall layers, so it is rejected before resolution. */\nconst NUL = \"\\u0000\";\n\n/**\n * Resolve an untrusted path against a base directory and verify containment.\n *\n * The candidate is resolved against the base — which also normalizes `.`, `..`, and\n * duplicate separators — and accepted only if the result is the base or sits beneath\n * it. An absolute `untrustedPath` overrides the base under `path.resolve` semantics,\n * so it too is caught by the containment check rather than silently trusted.\n *\n * **This function performs no filesystem I/O**, so it cannot see symlinks. A path that\n * is textually contained may still resolve on disk to a target outside the base. When\n * the target may exist and may be a link, follow up with `fs.promises.realpath` on\n * both the base and the result and re-run {@link isPathContained} on those.\n *\n * @param baseDirectory - Directory the result must stay within. Resolved against the\n * process working directory when relative.\n * @param untrustedPath - Candidate path from an untrusted source.\n * @param options - See {@link ContainmentOptions}.\n * @returns The resolved absolute path when contained, otherwise `null`. Also `null`\n * for non-string arguments, an empty base, or a NUL byte in either argument.\n *\n * @example\n * ```ts\n * const base = \"/srv/uploads\";\n *\n * resolveContainedPath(base, \"avatar.png\"); // \"/srv/uploads/avatar.png\"\n * resolveContainedPath(base, \"nested/a.txt\"); // \"/srv/uploads/nested/a.txt\"\n * resolveContainedPath(base, \"../../etc/passwd\"); // null\n * resolveContainedPath(base, \"/etc/passwd\"); // null\n * ```\n */\nexport function resolveContainedPath(\n\tbaseDirectory: string,\n\tuntrustedPath: string,\n\toptions: ContainmentOptions = {},\n): string | null {\n\tif (typeof baseDirectory !== \"string\" || typeof untrustedPath !== \"string\") {\n\t\treturn null;\n\t}\n\tif (baseDirectory.length === 0) return null;\n\n\t// `a.txt\\0.png` can pass an extension check and then open `a.txt`.\n\tif (untrustedPath.includes(NUL) || baseDirectory.includes(NUL)) {\n\t\treturn null;\n\t}\n\n\tconst { allowBaseItself = true } = options;\n\n\tconst base = path.resolve(baseDirectory);\n\tconst candidate = path.resolve(base, untrustedPath);\n\tconst relative = path.relative(base, candidate);\n\n\tif (relative === \"\") {\n\t\treturn allowBaseItself ? candidate : null;\n\t}\n\n\t// `path.relative` yields a `..`-prefixed path when the candidate escapes, and an\n\t// absolute path when the two sit on different Windows drives.\n\tif (relative === \"..\" || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {\n\t\treturn null;\n\t}\n\n\treturn candidate;\n}\n\n/**\n * Boolean form of {@link resolveContainedPath}.\n *\n * @param baseDirectory - Directory the candidate must stay within.\n * @param candidatePath - Path to test.\n * @param options - See {@link ContainmentOptions}.\n * @returns `true` when the candidate resolves inside the base.\n */\nexport function isPathContained(\n\tbaseDirectory: string,\n\tcandidatePath: string,\n\toptions?: ContainmentOptions,\n): boolean {\n\treturn resolveContainedPath(baseDirectory, candidatePath, options) !== null;\n}\n\n//#endregion\n\n//#region Filename sanitization\n\n/**\n * Characters invalid in a Windows path segment, plus both separators and the C0\n * control range.\n */\n// biome-ignore lint/suspicious/noControlCharactersInRegex: control characters are invalid in filenames and must be stripped\nconst UNSAFE_FILENAME_CHARS = /[<>:\"/\\\\|?*\\u0000-\\u001f\\u007f]/g;\n\n/**\n * Windows device names, reserved regardless of extension — opening `CON.txt` still\n * targets the console device.\n */\nconst RESERVED_WINDOWS_NAMES = /^(?:CON|PRN|AUX|NUL|COM\\d|LPT\\d)(?:\\.|$)/i;\n\n/** Longest filename accepted on common filesystems. */\nconst MAX_FILENAME_LENGTH = 255;\n\n/** Longest extension preserved when truncating an over-long filename. */\nconst MAX_PRESERVED_EXTENSION = 16;\n\n/**\n * Reduce an untrusted string to a single safe path segment.\n *\n * Strips directory separators, control characters, and Windows-reserved characters;\n * removes leading dots so the result cannot become `.`/`..` or a hidden file; trims\n * the trailing dots and spaces Windows silently drops (which is how `evil.php.`\n * becomes `evil.php` after the fact); and refuses reserved device names.\n *\n * The output is a *segment*, never a path. Join it onto a base directory yourself and\n * confirm the result with {@link resolveContainedPath} — that remains the containment\n * control; this is only hygiene.\n *\n * @param filename - Untrusted filename, typically from a multipart upload.\n * @param fallback - Returned when nothing usable survives. Defaults to `\"file\"`.\n * @returns A safe single path segment.\n *\n * @example\n * ```ts\n * sanitizeFilename(\"../../etc/passwd\"); // \"etcpasswd\"\n * sanitizeFilename(\"report.pdf.\"); // \"report.pdf\"\n * sanitizeFilename(\"CON.txt\"); // \"file\"\n * ```\n */\nexport function sanitizeFilename(filename: string, fallback = \"file\"): string {\n\tif (typeof filename !== \"string\" || filename.length === 0) return fallback;\n\n\tlet safe = filename\n\t\t.normalize(\"NFC\")\n\t\t.replace(UNSAFE_FILENAME_CHARS, \"\")\n\t\t.replace(/^\\.+/, \"\")\n\t\t.replace(/[. ]+$/, \"\")\n\t\t.trim();\n\n\tif (safe.length === 0) return fallback;\n\tif (RESERVED_WINDOWS_NAMES.test(safe)) return fallback;\n\n\tif (safe.length > MAX_FILENAME_LENGTH) {\n\t\tconst extension = path.extname(safe).slice(0, MAX_PRESERVED_EXTENSION);\n\t\tsafe = safe.slice(0, MAX_FILENAME_LENGTH - extension.length) + extension;\n\t}\n\n\treturn safe;\n}\n\n//#endregion\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCZ,SAAgB,qBACf,eACA,eACA,UAA8B,CAAC,GACf;CAChB,IAAI,OAAO,kBAAkB,YAAY,OAAO,kBAAkB,UACjE,OAAO;CAER,IAAI,cAAc,WAAW,GAAG,OAAO;CAGvC,IAAI,cAAc,SAAS,GAAG,KAAK,cAAc,SAAS,GAAG,GAC5D,OAAO;CAGR,MAAM,EAAE,kBAAkB,SAAS;CAEnC,MAAM,OAAO,KAAK,QAAQ,aAAa;CACvC,MAAM,YAAY,KAAK,QAAQ,MAAM,aAAa;CAClD,MAAM,WAAW,KAAK,SAAS,MAAM,SAAS;CAE9C,IAAI,aAAa,IAChB,OAAO,kBAAkB,YAAY;CAKtC,IAAI,aAAa,QAAQ,SAAS,WAAW,KAAK,KAAK,KAAK,KAAK,KAAK,WAAW,QAAQ,GACxF,OAAO;CAGR,OAAO;AACR;;;;;;;;;AAUA,SAAgB,gBACf,eACA,eACA,SACU;CACV,OAAO,qBAAqB,eAAe,eAAe,OAAO,MAAM;AACxE;;;;;AAWA,MAAM,wBAAwB;;;;;AAM9B,MAAM,yBAAyB;;AAG/B,MAAM,sBAAsB;;AAG5B,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;AAyBhC,SAAgB,iBAAiB,UAAkB,WAAW,QAAgB;CAC7E,IAAI,OAAO,aAAa,YAAY,SAAS,WAAW,GAAG,OAAO;CAElE,IAAI,OAAO,SACT,UAAU,KAAK,CAAC,CAChB,QAAQ,uBAAuB,EAAE,CAAC,CAClC,QAAQ,QAAQ,EAAE,CAAC,CACnB,QAAQ,UAAU,EAAE,CAAC,CACrB,KAAK;CAEP,IAAI,KAAK,WAAW,GAAG,OAAO;CAC9B,IAAI,uBAAuB,KAAK,IAAI,GAAG,OAAO;CAE9C,IAAI,KAAK,SAAS,qBAAqB;EACtC,MAAM,YAAY,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,uBAAuB;EACrE,OAAO,KAAK,MAAM,GAAG,sBAAsB,UAAU,MAAM,IAAI;CAChE;CAEA,OAAO;AACR"}
|
package/lib/sanitize.d.mts
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { Brand } from "@resq-systems/types";
|
|
2
2
|
import { Exit, Option, Schema } from "effect";
|
|
3
3
|
import { Config } from "dompurify";
|
|
4
|
-
|
|
5
4
|
//#region src/sanitize.d.ts
|
|
6
5
|
/**
|
|
7
|
-
* A Schema
|
|
6
|
+
* A Schema whose decoding services are constrained to `never`, allowing synchronous
|
|
7
|
+
* decoding without an Effect runtime.
|
|
8
8
|
*/
|
|
9
9
|
type SyncSchema<T> = Schema.Codec<T, unknown, never>;
|
|
10
10
|
/**
|
|
11
|
-
* Schema
|
|
11
|
+
* Schema constraining a URL protocol to the recognized safe set.
|
|
12
12
|
* @compliance NIST 800-53 SI-10 (Information Input Validation)
|
|
13
13
|
*/
|
|
14
14
|
declare const UrlProtocolSchema: Schema.Literals<readonly ["http:", "https:", "mailto:", "tel:", "ftp:"]>;
|
|
15
|
+
/** One of the protocols accepted by {@link UrlProtocolSchema}. */
|
|
15
16
|
type UrlProtocol = typeof UrlProtocolSchema.Type;
|
|
16
17
|
/**
|
|
17
|
-
* Schema for
|
|
18
|
+
* Schema for the per-category toggles that drive {@link redactPII}.
|
|
18
19
|
* @compliance NIST 800-53 AU-3 (Content of Audit Records)
|
|
19
20
|
*/
|
|
20
21
|
declare const PIIRedactionOptionsSchema: Schema.Struct<{
|
|
@@ -25,9 +26,10 @@ declare const PIIRedactionOptionsSchema: Schema.Struct<{
|
|
|
25
26
|
readonly redactIPs: Schema.optional<Schema.Boolean>;
|
|
26
27
|
readonly redactDates: Schema.optional<Schema.Boolean>;
|
|
27
28
|
}>;
|
|
29
|
+
/** Decoded options accepted by {@link redactPIIEffect} / {@link redactPII}. */
|
|
28
30
|
type PIIRedactionOptions = typeof PIIRedactionOptionsSchema.Type;
|
|
29
31
|
/**
|
|
30
|
-
* Schema for
|
|
32
|
+
* Schema for the options controlling {@link validateUserInputEffect}.
|
|
31
33
|
* @compliance NIST 800-53 SI-10 (Information Input Validation)
|
|
32
34
|
*/
|
|
33
35
|
declare const UserInputOptionsSchema: Schema.Struct<{
|
|
@@ -36,19 +38,34 @@ declare const UserInputOptionsSchema: Schema.Struct<{
|
|
|
36
38
|
readonly allowNewlines: Schema.optional<Schema.Boolean>;
|
|
37
39
|
readonly trimWhitespace: Schema.optional<Schema.Boolean>;
|
|
38
40
|
}>;
|
|
41
|
+
/** Decoded options accepted by {@link validateUserInputEffect}. */
|
|
39
42
|
type UserInputOptions = typeof UserInputOptionsSchema.Type;
|
|
43
|
+
declare const SafeUrlSchema: Schema.String;
|
|
40
44
|
/**
|
|
41
|
-
*
|
|
42
|
-
* @
|
|
45
|
+
* A URL string vouched safe against scheme-based injection. Mint one by
|
|
46
|
+
* narrowing through the {@link isValidUrl} type guard (backed by
|
|
47
|
+
* {@link SafeUrlSchema}); the brand guarantees the value is either a
|
|
48
|
+
* root-relative path or an absolute URL restricted to `http:`/`https:`/
|
|
49
|
+
* `mailto:`. An authority-opening reference (`//host`, `///host`, `/\host`) is
|
|
50
|
+
* rejected, since resolving one against a base yields a different host.
|
|
51
|
+
*
|
|
52
|
+
* It does **not** guarantee the host is reachable or trusted — for that, validate the
|
|
53
|
+
* resolved origin with `isAllowedOrigin` from `@resq-systems/security/controls`.
|
|
43
54
|
*/
|
|
44
|
-
declare const SafeUrlSchema: Schema.String;
|
|
45
|
-
/** A string that has passed {@link isValidUrl} — a validated, injection-safe URL. */
|
|
46
55
|
type SafeUrl = Brand<string, "SafeUrl">;
|
|
47
56
|
/**
|
|
48
|
-
* Schema for sanitized HTML-safe string
|
|
57
|
+
* Schema for a sanitized HTML-safe string — validates the value is a string; the
|
|
58
|
+
* actual escaping is applied at runtime by the sanitization helpers.
|
|
49
59
|
* @compliance NIST 800-53 SI-10 (Information Input Validation)
|
|
50
60
|
*/
|
|
51
61
|
declare const SanitizedStringSchema: Schema.String;
|
|
62
|
+
/**
|
|
63
|
+
* A string carrying the {@link SanitizedStringSchema} contract. The schema is
|
|
64
|
+
* `S.String` alone, so decoding asserts only that the value is a string —
|
|
65
|
+
* the actual escaping is applied separately by the sanitization helpers
|
|
66
|
+
* (e.g. {@link escapeHtml}). The type name signals intent, not a proof of
|
|
67
|
+
* escaping.
|
|
68
|
+
*/
|
|
52
69
|
type SanitizedString = typeof SanitizedStringSchema.Type;
|
|
53
70
|
/**
|
|
54
71
|
* Schema for email address validation.
|
|
@@ -59,34 +76,60 @@ type SanitizedString = typeof SanitizedStringSchema.Type;
|
|
|
59
76
|
* @compliance NIST 800-53 SI-10 (Information Input Validation)
|
|
60
77
|
*/
|
|
61
78
|
declare const EmailSchema: Schema.String;
|
|
62
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* An email address that matches {@link EmailSchema}. Mint one by narrowing
|
|
81
|
+
* through the {@link isValidEmail} type guard. The brand guarantees only
|
|
82
|
+
* syntactic well-formedness (including IDN/Punycode TLDs) — not that the
|
|
83
|
+
* mailbox exists or is deliverable.
|
|
84
|
+
*/
|
|
63
85
|
type Email = Brand<string, "Email">;
|
|
64
86
|
/**
|
|
65
|
-
* Schema for phone number validation (US format)
|
|
87
|
+
* Schema for phone number validation (US format).
|
|
66
88
|
* @compliance NIST 800-53 SI-10 (Information Input Validation)
|
|
67
89
|
*/
|
|
68
90
|
declare const PhoneNumberSchema: Schema.String;
|
|
69
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* A US-format phone number matching {@link PhoneNumberSchema}. Mint one by
|
|
93
|
+
* narrowing through the {@link isValidPhone} type guard. The brand asserts
|
|
94
|
+
* the digit/separator shape only; it neither normalizes formatting nor
|
|
95
|
+
* confirms the number is assigned.
|
|
96
|
+
*/
|
|
70
97
|
type PhoneNumber = Brand<string, "PhoneNumber">;
|
|
71
98
|
/**
|
|
72
|
-
* Schema for SSN validation (US format)
|
|
99
|
+
* Schema for SSN validation (US format).
|
|
73
100
|
* @compliance NIST 800-53 SI-10 (Information Input Validation)
|
|
74
101
|
*/
|
|
75
102
|
declare const SSNSchema: Schema.String;
|
|
76
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* A US Social Security Number matching {@link SSNSchema}. Mint one by
|
|
105
|
+
* narrowing through the {@link isValidSSN} type guard. The brand asserts
|
|
106
|
+
* the `NNN-NN-NNNN` shape only — it does not validate area/group ranges or
|
|
107
|
+
* confirm the number was ever issued. Treat any value as sensitive PII.
|
|
108
|
+
*/
|
|
77
109
|
type SSN = Brand<string, "SSN">;
|
|
78
110
|
/**
|
|
79
|
-
* Schema for credit card number validation
|
|
111
|
+
* Schema for credit card number validation.
|
|
80
112
|
* @compliance NIST 800-53 SI-10 (Information Input Validation)
|
|
81
113
|
*/
|
|
82
114
|
declare const CreditCardSchema: Schema.String;
|
|
83
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* A card number matching the {@link CreditCardSchema} pattern (13–16 digits
|
|
117
|
+
* with optional group separators). No exported type guard mints this brand;
|
|
118
|
+
* decode {@link CreditCardSchema} directly at the boundary. The pattern is a
|
|
119
|
+
* shape check only — it performs **no** Luhn checksum and does not identify
|
|
120
|
+
* the issuer. Treat any value as sensitive PII.
|
|
121
|
+
*/
|
|
84
122
|
type CreditCard = Brand<string, "CreditCard">;
|
|
85
123
|
/**
|
|
86
|
-
* Schema for IPv4 address validation
|
|
124
|
+
* Schema for IPv4 address validation.
|
|
87
125
|
*/
|
|
88
126
|
declare const IPv4Schema: Schema.String;
|
|
89
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* A dotted-quad string matching {@link IPv4Schema}. No exported type guard
|
|
129
|
+
* mints this brand; decode {@link IPv4Schema} directly. The pattern checks
|
|
130
|
+
* four dot-separated groups of 1–3 digits only — it does **not** bound each
|
|
131
|
+
* octet to `0–255`, so `999.0.0.1` still matches.
|
|
132
|
+
*/
|
|
90
133
|
type IPv4 = Brand<string, "IPv4">;
|
|
91
134
|
/**
|
|
92
135
|
* Escapes special HTML characters in a string to their corresponding HTML entities,
|
|
@@ -107,9 +150,16 @@ declare const escapeHtml: (text: string) => string;
|
|
|
107
150
|
* Validates and sanitizes a user-supplied URL using Effect Schema.
|
|
108
151
|
* Returns an Exit with the sanitized URL or an error.
|
|
109
152
|
*
|
|
153
|
+
* Pure and total — failure is encoded as a resolved {@link Exit.Exit} failure
|
|
154
|
+
* (an `Exit.fail` carrying a {@link S.SchemaError}), never a thrown exception.
|
|
155
|
+
*
|
|
110
156
|
* @param url - The URL to be validated and sanitized.
|
|
111
|
-
* @param allowedProtocols -
|
|
112
|
-
*
|
|
157
|
+
* @param allowedProtocols - Allowed URL protocols; a root-relative path (`/foo`) is
|
|
158
|
+
* always accepted regardless of this list. An authority-opening reference — `//host`,
|
|
159
|
+
* `///host`, or `/\host` — is always **rejected**, because it names a different host
|
|
160
|
+
* once resolved and would otherwise bypass this list entirely.
|
|
161
|
+
* @returns An {@link Exit.Exit}: success carries the accepted URL string,
|
|
162
|
+
* failure carries a {@link S.SchemaError}.
|
|
113
163
|
* @compliance NIST 800-53 SI-10 (Information Input Validation)
|
|
114
164
|
*
|
|
115
165
|
* @example
|
|
@@ -146,17 +196,32 @@ declare const sanitizeUrl: (url: string, allowedProtocols?: readonly UrlProtocol
|
|
|
146
196
|
* NOTE: Server-side HTML sanitization requires `jsdom` to be installed in the consuming application
|
|
147
197
|
* environment; otherwise, it will fall back to escaping HTML characters.
|
|
148
198
|
*
|
|
199
|
+
* On the first server-side call this lazily resolves `node:module` and
|
|
200
|
+
* `require`s `jsdom` to build a DOMPurify instance; the instance is cached at
|
|
201
|
+
* module scope, so subsequent calls incur no further module loading. Returns
|
|
202
|
+
* `""` for non-string or empty input and never throws — any loader failure is
|
|
203
|
+
* swallowed and downgraded to {@link escapeHtml}.
|
|
204
|
+
*
|
|
149
205
|
* @param html - The HTML string to sanitize.
|
|
150
206
|
* @param options - Optional DOMPurify configuration.
|
|
151
|
-
* @returns The sanitized HTML string
|
|
207
|
+
* @returns The sanitized HTML string, or the escaped string when no DOM is
|
|
208
|
+
* available.
|
|
152
209
|
*/
|
|
153
210
|
declare const sanitizeHtml: (html: string, options?: Config) => string;
|
|
154
211
|
/**
|
|
155
212
|
* Validates user input using Effect Schema and returns an Exit.
|
|
156
213
|
*
|
|
214
|
+
* Strips HTML (unless `allowHtml`), collapses whitespace, and repeatedly
|
|
215
|
+
* removes dangerous URI schemes and inline handlers until the string
|
|
216
|
+
* stabilizes — the fixed-point loop defeats nested-payload bypasses such as
|
|
217
|
+
* `javascrjavascript:ipt:`. Failure is a resolved {@link Exit.Exit} failure,
|
|
218
|
+
* never a throw; the only failure path is a non-string reaching `S.String`.
|
|
219
|
+
*
|
|
157
220
|
* @param input - User input to validate and sanitize.
|
|
158
|
-
* @param options - Validation options.
|
|
159
|
-
*
|
|
221
|
+
* @param options - Validation options. Defaults: `maxLength` 500, `allowHtml`
|
|
222
|
+
* false, `allowNewlines` false, `trimWhitespace` true.
|
|
223
|
+
* @returns An {@link Exit.Exit}: success carries the sanitized string truncated
|
|
224
|
+
* to `maxLength`; failure carries a {@link S.SchemaError}.
|
|
160
225
|
* @compliance NIST 800-53 SI-10 (Information Input Validation)
|
|
161
226
|
*/
|
|
162
227
|
declare const validateUserInputEffect: (input: string, options?: UserInputOptions) => Exit.Exit<string, Schema.SchemaError>;
|
|
@@ -180,10 +245,19 @@ declare const validateUserInput: (input: string, maxLength?: number, allowHtml?:
|
|
|
180
245
|
/**
|
|
181
246
|
* Safely parses JSON with Effect Schema validation and prototype pollution protection.
|
|
182
247
|
*
|
|
183
|
-
*
|
|
248
|
+
* Never throws: malformed JSON, a non-string argument, and schema-validation
|
|
249
|
+
* failure all resolve to {@link Option.none} rather than a thrown error, so the
|
|
250
|
+
* failure channel is the `Option` itself. As a side effect the parsed value is
|
|
251
|
+
* stripped of prototype-pollution keys in place before validation (the value is
|
|
252
|
+
* freshly created by `JSON.parse`, so no caller state is mutated).
|
|
253
|
+
*
|
|
254
|
+
* @template A - The decoded value type the `schema` produces on success; the
|
|
255
|
+
* returned `Option` carries this type.
|
|
184
256
|
* @param jsonString - The JSON string to parse.
|
|
185
|
-
* @param schema - Effect Schema to validate against
|
|
186
|
-
* @
|
|
257
|
+
* @param schema - Effect Schema to validate against; its decode must require no
|
|
258
|
+
* services ({@link SyncSchema}) so parsing stays synchronous.
|
|
259
|
+
* @returns {@link Option.some} with the parsed, validated value, or
|
|
260
|
+
* {@link Option.none} on any parse or validation failure.
|
|
187
261
|
* @compliance NIST 800-53 SI-10 (Information Input Validation)
|
|
188
262
|
*
|
|
189
263
|
* @example
|
|
@@ -216,8 +290,17 @@ declare const parseJsonWithSchema: <A>(jsonString: string, schema: SyncSchema<A>
|
|
|
216
290
|
*/
|
|
217
291
|
declare const sanitizeJson: (jsonString: string) => unknown;
|
|
218
292
|
/**
|
|
219
|
-
* Strips ANSI escape
|
|
220
|
-
*
|
|
293
|
+
* Strips ANSI escape sequences from a string.
|
|
294
|
+
*
|
|
295
|
+
* Removes CSI sequences (colour, cursor movement, screen and line erasure, mode
|
|
296
|
+
* switches), OSC sequences (window title and similar), two-character escapes, and
|
|
297
|
+
* any bare ESC left over. This is the control named by `LOG-ANSI-ESCAPE-001`: a
|
|
298
|
+
* terminal-backed log sink treats these as commands, so an attacker who lands them
|
|
299
|
+
* in a log can scroll earlier entries away or overwrite them (CWE-117).
|
|
300
|
+
*
|
|
301
|
+
* Removal is destructive by design — the sequence goes, and any text it carried
|
|
302
|
+
* goes with it. Where the record matters more than the rendering, escape the
|
|
303
|
+
* characters instead of deleting them.
|
|
221
304
|
*
|
|
222
305
|
* @param text - The text potentially containing ANSI codes.
|
|
223
306
|
* @returns The text with ANSI codes removed.
|
|
@@ -231,9 +314,15 @@ declare const stripAnsi: (text: string) => string;
|
|
|
231
314
|
/**
|
|
232
315
|
* Redacts PII from text using Effect Schema validated options.
|
|
233
316
|
*
|
|
317
|
+
* Total — never throws. Failure to decode `options` is returned as a resolved
|
|
318
|
+
* {@link Exit.Exit} failure carrying the {@link S.SchemaError}. By default
|
|
319
|
+
* dates are **not** redacted (`redactDates` defaults to `false`); every other
|
|
320
|
+
* category defaults to `true`.
|
|
321
|
+
*
|
|
234
322
|
* @param text - The text to redact PII from.
|
|
235
323
|
* @param options - Configuration options for redaction.
|
|
236
|
-
* @returns Exit
|
|
324
|
+
* @returns An {@link Exit.Exit}: success carries the redacted text, failure
|
|
325
|
+
* carries a {@link S.SchemaError} from invalid `options`.
|
|
237
326
|
* @compliance NIST 800-53 AU-3 (Content of Audit Records)
|
|
238
327
|
*/
|
|
239
328
|
declare const redactPIIEffect: (text: string, options?: PIIRedactionOptions) => Exit.Exit<string, Schema.SchemaError>;
|
|
@@ -241,9 +330,15 @@ declare const redactPIIEffect: (text: string, options?: PIIRedactionOptions) =>
|
|
|
241
330
|
* Redacts common PII patterns in a string for safe logging.
|
|
242
331
|
* Detects and masks SSNs, credit cards, emails, phone numbers, etc.
|
|
243
332
|
*
|
|
244
|
-
* @param text - The text to redact PII from.
|
|
245
|
-
* @param options - Configuration options for redaction
|
|
246
|
-
*
|
|
333
|
+
* @param text - The text to redact PII from. Non-string input yields `""`.
|
|
334
|
+
* @param options - Configuration options for redaction, plus optional
|
|
335
|
+
* `customPatterns` applied after the built-ins. Each `pattern` **must** be a
|
|
336
|
+
* global (`/g`) RegExp.
|
|
337
|
+
* @returns The text with PII patterns replaced with redaction markers, or `""`
|
|
338
|
+
* for non-string input. If the built-in options fail schema validation the
|
|
339
|
+
* original `text` is returned unredacted rather than throwing.
|
|
340
|
+
* @throws {TypeError} If any `customPatterns` entry uses a non-global RegExp —
|
|
341
|
+
* `String.prototype.replaceAll` rejects non-global patterns.
|
|
247
342
|
* @compliance NIST 800-53 AU-3 (Content of Audit Records)
|
|
248
343
|
*
|
|
249
344
|
* @example
|
|
@@ -265,10 +360,17 @@ declare const redactPII: (text: string, options?: PIIRedactionOptions & {
|
|
|
265
360
|
* Creates a safe string representation of an object for logging,
|
|
266
361
|
* automatically redacting sensitive fields.
|
|
267
362
|
*
|
|
363
|
+
* Never throws: any serialization failure — a circular reference, a `BigInt`
|
|
364
|
+
* value, a throwing `toJSON` — is caught and returned as the sentinel string
|
|
365
|
+
* `"[Unable to stringify object]"`. Key matching is case-insensitive and
|
|
366
|
+
* compares the full key name (not substrings), so `apiKey` matches only the
|
|
367
|
+
* literal `"apiKey"`, not `"apiKeyId"`.
|
|
368
|
+
*
|
|
268
369
|
* @param obj - The object to stringify.
|
|
269
|
-
* @param sensitiveKeys -
|
|
370
|
+
* @param sensitiveKeys - Key names to redact, compared case-insensitively.
|
|
270
371
|
* @param indent - JSON indentation (default: 2).
|
|
271
|
-
* @returns A JSON string with sensitive values redacted
|
|
372
|
+
* @returns A JSON string with sensitive values redacted, or the sentinel
|
|
373
|
+
* `"[Unable to stringify object]"` when serialization fails.
|
|
272
374
|
* @compliance NIST 800-53 AU-3 (Content of Audit Records)
|
|
273
375
|
*
|
|
274
376
|
* @example
|
package/lib/sanitize.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sanitize.d.mts","names":[],"sources":["../src/sanitize.ts"],"mappings":";;;;;;;;
|
|
1
|
+
{"version":3,"file":"sanitize.d.mts","names":[],"sources":["../src/sanitize.ts"],"mappings":";;;;;;;;KAoCK,WAAW,KAAK,OAAE,MAAM;;;;;cAShB,mBAAiB,OAAA;;KAElB,qBAAqB,kBAAkB;;;;;cAMtC,2BAAyB,OAAA;;;;;;;;;KAS1B,6BAA6B,0BAA0B;;;;;cAMtD,wBAAsB,OAAA;;;;;;;KAOvB,0BAA0B,uBAAuB;cAuBhD,eAAa,OAAA;;;;;;;;;;;;KA4Bd,UAAU;;;;;;cAOT,uBAAqB,OAAA;;;;;;;;KAQtB,yBAAyB,sBAAsB;;;;;;;;;cAU9C,aAAW,OAAA;;;;;;;KASZ,QAAQ;;;;;cAMP,mBAAiB,OAAA;;;;;;;KASlB,cAAc;;;;;cAMb,WAAS,OAAA;;;;;;;KAOV,MAAM;;;;;cAML,kBAAgB,OAAA;;;;;;;;KAUjB,aAAa;;;;cAKZ,YAAU,OAAA;;;;;;;KAOX,OAAO;;;;;;;;;;;;;;;cAmBN,aAAc;;;;;;;;;;;;;;;;;;;;;;;;;;cAsCd,oBACZ,aACA,4BAA2B,kBACzB,KAAK,aAAa,OAAE;;;;;;;;;;;;;;;;cA+CV,cACZ,aACA,4BAA2B;;;;;;;;;;;;;;;;;;;;cAyGf,eAAgB,cAAc,UAAU;;;;;;;;;;;;;;;;;cA6BxC,0BACZ,eACA,UAAS,qBACP,KAAK,aAAa,OAAE;;;;;;;;;;;;;;;;;cA+DV,oBAAqB,eAAe,oBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;cAmFrD,sBAAuB,GACnC,oBACA,QAAQ,WAAW,OACjB,OAAO,OAAO;;;;;;;;;;;;;;;;;;;;;cA0CJ,eAAgB;;;;;;;;;;;;;;;;;;;;;;cA0ChB,YAAa;;;;;;;;;;;;;;;cAoEb,kBACZ,cACA,UAAS,wBACP,KAAK,aAAa,OAAE;;;;;;;;;;;;;;;;;;;;;;;;;cAuEV,YACZ,cACA,UAAS;EACR,iBAAiB;IAAQ,SAAS;IAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;cA0D/B,gBACZ,cACA,0BAUA;;;;;;;;;;cA+BY,eAAgB,kBAAgB,SAAS;;;;;;;;;cAYzC,eAAgB,kBAAgB,SAAS;;;;;;;;;cAYzC,aAAc,gBAAc,OAAO;;;;;;;;;cAYnC,aAAc,gBAAc,OAAO"}
|