@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.
Files changed (108) hide show
  1. package/README.md +236 -33
  2. package/lib/controls/address.d.mts +142 -0
  3. package/lib/controls/address.d.mts.map +1 -0
  4. package/lib/controls/address.mjs +533 -0
  5. package/lib/controls/address.mjs.map +1 -0
  6. package/lib/controls/csrf.d.mts +91 -0
  7. package/lib/controls/csrf.d.mts.map +1 -0
  8. package/lib/controls/csrf.mjs +200 -0
  9. package/lib/controls/csrf.mjs.map +1 -0
  10. package/lib/controls/index.d.mts +8 -0
  11. package/lib/controls/index.mjs +8 -0
  12. package/lib/controls/origin.d.mts +95 -0
  13. package/lib/controls/origin.d.mts.map +1 -0
  14. package/lib/controls/origin.mjs +156 -0
  15. package/lib/controls/origin.mjs.map +1 -0
  16. package/lib/controls/payload.d.mts +84 -0
  17. package/lib/controls/payload.d.mts.map +1 -0
  18. package/lib/controls/payload.mjs +147 -0
  19. package/lib/controls/payload.mjs.map +1 -0
  20. package/lib/controls/query.d.mts +157 -0
  21. package/lib/controls/query.d.mts.map +1 -0
  22. package/lib/controls/query.mjs +368 -0
  23. package/lib/controls/query.mjs.map +1 -0
  24. package/lib/controls/redirect.d.mts +92 -0
  25. package/lib/controls/redirect.d.mts.map +1 -0
  26. package/lib/controls/redirect.mjs +110 -0
  27. package/lib/controls/redirect.mjs.map +1 -0
  28. package/lib/controls/upload.d.mts +108 -0
  29. package/lib/controls/upload.d.mts.map +1 -0
  30. package/lib/controls/upload.mjs +374 -0
  31. package/lib/controls/upload.mjs.map +1 -0
  32. package/lib/crypto.d.mts +18 -5
  33. package/lib/crypto.d.mts.map +1 -1
  34. package/lib/crypto.mjs +35 -24
  35. package/lib/crypto.mjs.map +1 -1
  36. package/lib/hash.d.mts +83 -0
  37. package/lib/hash.d.mts.map +1 -0
  38. package/lib/hash.mjs +111 -0
  39. package/lib/hash.mjs.map +1 -0
  40. package/lib/index.d.mts +18 -2
  41. package/lib/index.mjs +20 -2
  42. package/lib/paths.d.mts +92 -0
  43. package/lib/paths.d.mts.map +1 -0
  44. package/lib/paths.mjs +140 -0
  45. package/lib/paths.mjs.map +1 -0
  46. package/lib/sanitize.d.mts +137 -35
  47. package/lib/sanitize.d.mts.map +1 -1
  48. package/lib/sanitize.mjs +170 -46
  49. package/lib/sanitize.mjs.map +1 -1
  50. package/lib/threats/capec.generated.d.mts +59 -0
  51. package/lib/threats/capec.generated.d.mts.map +1 -0
  52. package/lib/threats/capec.generated.mjs +644 -0
  53. package/lib/threats/capec.generated.mjs.map +1 -0
  54. package/lib/threats/engine.d.mts +94 -0
  55. package/lib/threats/engine.d.mts.map +1 -0
  56. package/lib/threats/engine.mjs +167 -0
  57. package/lib/threats/engine.mjs.map +1 -0
  58. package/lib/threats/index.d.mts +11 -0
  59. package/lib/threats/index.mjs +11 -0
  60. package/lib/threats/rules/datastore.d.mts +13 -0
  61. package/lib/threats/rules/datastore.d.mts.map +1 -0
  62. package/lib/threats/rules/datastore.mjs +366 -0
  63. package/lib/threats/rules/datastore.mjs.map +1 -0
  64. package/lib/threats/rules/index.d.mts +54 -0
  65. package/lib/threats/rules/index.d.mts.map +1 -0
  66. package/lib/threats/rules/index.mjs +121 -0
  67. package/lib/threats/rules/index.mjs.map +1 -0
  68. package/lib/threats/rules/markup.d.mts +28 -0
  69. package/lib/threats/rules/markup.d.mts.map +1 -0
  70. package/lib/threats/rules/markup.mjs +373 -0
  71. package/lib/threats/rules/markup.mjs.map +1 -0
  72. package/lib/threats/rules/protocol.d.mts +49 -0
  73. package/lib/threats/rules/protocol.d.mts.map +1 -0
  74. package/lib/threats/rules/protocol.mjs +175 -0
  75. package/lib/threats/rules/protocol.mjs.map +1 -0
  76. package/lib/threats/rules/system.d.mts +19 -0
  77. package/lib/threats/rules/system.d.mts.map +1 -0
  78. package/lib/threats/rules/system.mjs +455 -0
  79. package/lib/threats/rules/system.mjs.map +1 -0
  80. package/lib/threats/rules/web.d.mts +26 -0
  81. package/lib/threats/rules/web.d.mts.map +1 -0
  82. package/lib/threats/rules/web.mjs +412 -0
  83. package/lib/threats/rules/web.mjs.map +1 -0
  84. package/lib/threats/scoring.d.mts +59 -0
  85. package/lib/threats/scoring.d.mts.map +1 -0
  86. package/lib/threats/scoring.mjs +111 -0
  87. package/lib/threats/scoring.mjs.map +1 -0
  88. package/lib/threats/types.d.mts +245 -0
  89. package/lib/threats/types.d.mts.map +1 -0
  90. package/lib/threats/types.mjs +52 -0
  91. package/lib/threats/types.mjs.map +1 -0
  92. package/lib/threats/variants.d.mts +57 -0
  93. package/lib/threats/variants.d.mts.map +1 -0
  94. package/lib/threats/variants.mjs +144 -0
  95. package/lib/threats/variants.mjs.map +1 -0
  96. package/lib/unicode/confusables.d.mts +82 -0
  97. package/lib/unicode/confusables.d.mts.map +1 -0
  98. package/lib/unicode/confusables.mjs +954 -0
  99. package/lib/unicode/confusables.mjs.map +1 -0
  100. package/lib/unicode/index.d.mts +126 -0
  101. package/lib/unicode/index.d.mts.map +1 -0
  102. package/lib/unicode/index.mjs +288 -0
  103. package/lib/unicode/index.mjs.map +1 -0
  104. package/lib/validators.d.mts +341 -164
  105. package/lib/validators.d.mts.map +1 -1
  106. package/lib/validators.mjs +519 -338
  107. package/lib/validators.mjs.map +1 -1
  108. 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"}
@@ -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 with DecodingServices constrained to `never`, allowing synchronous decoding.
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 for URL protocol validation
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 PII redaction options
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 user input validation options
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
- * Schema for safe URL - validates URL format and protocol
42
- * @compliance NIST 800-53 SI-10 (Information Input Validation)
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 (validates as string; escaping done at runtime)
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
- /** A string that has passed {@link isValidEmail}. */
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
- /** A string that has passed {@link isValidPhone} (US format). */
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
- /** A string that has passed {@link isValidSSN} (US format). */
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
- /** A string matching the {@link CreditCardSchema} pattern. */
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
- /** A string matching the {@link IPv4Schema} dotted-quad pattern. */
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 - Array of allowed URL protocols.
112
- * @returns Exit containing the sanitized URL or an error.
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
- * @returns Exit containing sanitized input or error.
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
- * @template A - The expected schema type
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
- * @returns Option containing the parsed and validated object.
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 codes from a string.
220
- * Useful for cleaning terminal output before logging to files.
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 containing redacted text or error.
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
- * @returns The text with PII patterns replaced with redaction markers.
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 - Array of key names to redact.
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
@@ -1 +1 @@
1
- {"version":3,"file":"sanitize.d.mts","names":[],"sources":["../src/sanitize.ts"],"mappings":";;;;;;;;KAiCK,UAAA,MAAgB,MAAA,CAAE,KAAA,CAAM,CAAA;;;;;cAUhB,iBAAA,EAAiB,MAAA,CAAA,QAAA;AAAA,KAClB,WAAA,UAAqB,iBAAA,CAAkB,IAAA;;;;;cAMtC,yBAAA,EAAyB,MAAA,CAAA,MAAA;EAAA;;;;;;;KAQ1B,mBAAA,UAA6B,yBAAA,CAA0B,IAAA;;;;;cAMtD,sBAAA,EAAsB,MAAA,CAAA,MAAA;EAAA;;;;;KAMvB,gBAAA,UAA0B,sBAAA,CAAuB,IAAA;;;;;cAMhD,aAAA,EAAa,MAAA,CAAA,MAAA;;KAiBd,OAAA,GAAU,KAAA;;;;;cAMT,qBAAA,EAAqB,MAAA,CAAA,MAAA;AAAA,KACtB,eAAA,UAAyB,qBAAA,CAAsB,IAAA;;;;;AA1C3D;;;;cAoDa,WAAA,EAAW,MAAA,CAAA,MAAA;AA9CxB;AAAA,KAkDY,KAAA,GAAQ,KAAA;;;;;cAMP,iBAAA,EAAiB,MAAA,CAAA,MAAA;;KAIlB,WAAA,GAAc,KAAA;;;;;cAMb,SAAA,EAAS,MAAA,CAAA,MAAA;;KAEV,GAAA,GAAM,KAAA;;;;;cAML,gBAAA,EAAgB,MAAA,CAAA,MAAA;;KAIjB,UAAA,GAAa,KAAA;;;;cAKZ,UAAA,EAAU,MAAA,CAAA,MAAA;;KAEX,IAAA,GAAO,KAAA;;;;;;;;;;AA/EnB;;;;;cAmGa,UAAA,GAAc,IAAA;;;;;AA5E3B;;;;;AAMA;;;;;AACA;;;;cAoGa,iBAAA,GACZ,GAAA,UACA,gBAAA,YAA2B,WAAA,OACzB,IAAA,CAAK,IAAA,SAAa,MAAA,CAAE,WAAA;AA7FvB;;;;;AAIA;;;;;AAMA;;;;;AAVA,cAyIa,WAAA,GACZ,GAAA,UACA,gBAAA,YAA2B,WAAA;;;;;AAvH5B;;;;;AAEA;;;cA+Ka,YAAA,GAAgB,IAAA,UAAc,OAAA,GAAU,MAAA;;AAzKrD;;;;;AAIA;;cA0La,uBAAA,GACZ,KAAA,UACA,OAAA,GAAS,gBAAA,KACP,IAAA,CAAK,IAAA,SAAa,MAAA,CAAE,WAAA;;;AAxLvB;;;;;AAEA;;;;;AAoBA;;;;cAiOa,iBAAA,GAAqB,KAAA,UAAe,SAAA,WAAiB,SAAA;AAlMlE;;;;;;;;;;;;;;;;AAAA,cAuPa,mBAAA,MACZ,UAAA,UACA,MAAA,EAAQ,UAAA,CAAW,CAAA,MACjB,MAAA,CAAO,MAAA,CAAO,CAAA;AA3MjB;;;;;;;;;AA4DA;;;;;;;;;AAqBA;;AAjFA,cAqPa,YAAA,GAAgB,UAAA;;;;;;;;;;;;;cAiChB,SAAA,GAAa,IAAA;;AAnI1B;;;;;;;cAkLa,eAAA,GACZ,IAAA,UACA,OAAA,GAAS,mBAAA,KACP,IAAA,CAAK,IAAA,SAAa,MAAA,CAAE,WAAA;;AAhIvB;;;;;;;;;;;;;;;;;cAiMa,SAAA,GACZ,IAAA,UACA,OAAA,GAAS,mBAAA;EACR,cAAA,GAAiB,KAAA;IAAQ,OAAA,EAAS,MAAA;IAAQ,WAAA;EAAA;AAAA;;AAtH5C;;;;;AA+CA;;;;;;;;;;cA0Ha,aAAA,GACZ,GAAA,WACA,aAAA,aAUA,MAAA;;;;;;;AAlED;;;cAiGa,YAAA,GAAgB,KAAA,aAAgB,KAAA,IAAS,KAAA;;;;;;;;;cAYzC,YAAA,GAAgB,KAAA,aAAgB,KAAA,IAAS,WAAA;;;;;;AAvDtD;;;cAmEa,UAAA,GAAc,GAAA,aAAc,GAAA,IAAO,GAAA;;;;;;AAxBhD;;;cAoCa,UAAA,GAAc,GAAA,aAAc,GAAA,IAAO,OAAA"}
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"}