@resq-systems/security 1.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 ADDED
@@ -0,0 +1,280 @@
1
+ <!--
2
+ Copyright 2026 ResQ Systems, Inc.
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ -->
16
+
17
+ # @resq-systems/security
18
+
19
+ [![npm](https://img.shields.io/npm/v/%40resq-systems%2Fsecurity?style=flat-square)](https://www.npmjs.com/package/@resq-systems/security)
20
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue?style=flat-square)](../../LICENSE.md)
21
+
22
+ > Encryption, threat detection, input validation, PII sanitization, and Effect Schema validators.
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ bun add @resq-systems/security effect
28
+ ```
29
+
30
+ Peer dependency: `effect`. Uses Node.js `crypto` module for encryption.
31
+
32
+ ## Quick Start
33
+
34
+ ```ts
35
+ import { encryptData, decryptData, isSafeInput, escapeHtml, redactPII } from "@resq-systems/security";
36
+
37
+ // Encrypt/decrypt
38
+ const encrypted = await encryptData("sensitive", "my-secret-key");
39
+ const decrypted = await decryptData(encrypted, "my-secret-key");
40
+
41
+ // Validate input
42
+ if (!isSafeInput(userInput)) {
43
+ return new Response("Invalid input", { status: 400 });
44
+ }
45
+
46
+ // Sanitize for display
47
+ const safe = escapeHtml('<script>alert("xss")</script>');
48
+
49
+ // Redact PII for logging
50
+ const clean = redactPII("Email: john@example.com, SSN: 123-45-6789");
51
+ // "Email: [EMAIL], SSN: [SSN]"
52
+ ```
53
+
54
+ ## API Reference
55
+
56
+ ### Encryption (`crypto.ts`)
57
+
58
+ #### `encryptData(plaintext, encryptionKey): Promise<string>`
59
+
60
+ Encrypts data using AES-256-GCM with scrypt key derivation.
61
+
62
+ - **plaintext** (`string`) -- data to encrypt.
63
+ - **encryptionKey** (`string`) -- encryption key/password.
64
+ - Returns base64 string containing `salt:iv:authTag:ciphertext`.
65
+
66
+ #### `decryptData(encryptedData, encryptionKey): Promise<string>`
67
+
68
+ Decrypts data produced by `encryptData`.
69
+
70
+ - **encryptedData** (`string`) -- base64-encoded encrypted data.
71
+ - **encryptionKey** (`string`) -- same key used for encryption.
72
+ - Returns the original plaintext string.
73
+
74
+ #### `hashData(data): string`
75
+
76
+ Hashes data using SHA-256. Non-reversible.
77
+
78
+ - Returns hex-encoded hash string.
79
+
80
+ #### `generateSecureToken(length?): string`
81
+
82
+ Generates a cryptographically secure random token.
83
+
84
+ - **length** (`number`, default `32`) -- byte length.
85
+ - Returns hex string (2x the byte length).
86
+
87
+ #### `maskPII(data): string`
88
+
89
+ Masks a string, showing first 2 and last 2 characters (e.g. `"Al****ce"`). Returns `"****"` for strings <= 4 chars.
90
+
91
+ #### `maskEmail(email): string`
92
+
93
+ Masks email local part (e.g. `"j***n@example.com"`).
94
+
95
+ #### `sanitizeForLogging(obj, sensitiveFields?): Partial<T>`
96
+
97
+ Recursively redacts sensitive fields from an object for safe logging.
98
+
99
+ - **sensitiveFields** (`string[]`, default: `["password", "passwordHash", "token", "secret", "twoFactorSecret", "apiKey"]`)
100
+ - Email fields are automatically masked.
101
+
102
+ ```ts
103
+ sanitizeForLogging({ password: "secret", email: "john@example.com", name: "John" });
104
+ // { password: "[REDACTED]", email: "j***n@example.com", name: "John" }
105
+ ```
106
+
107
+ ### Threat Detection (`validators.ts`)
108
+
109
+ #### `detectThreatPatterns(input, config?): ThreatDetectionResult`
110
+
111
+ Runs all configured detectors on input.
112
+
113
+ - Returns `{ isSafe: boolean, threats: ThreatFinding[] }`.
114
+
115
+ | Config Option | Type | Default | Description |
116
+ |---------------|------|---------|-------------|
117
+ | `checkXSS` | `boolean` | `true` | Detect script injection, event handlers |
118
+ | `checkSQLInjection` | `boolean` | `true` | Detect UNION, DROP, stacked queries |
119
+ | `checkNoSQLInjection` | `boolean` | `true` | Detect MongoDB operators |
120
+ | `checkCommandInjection` | `boolean` | `false` | Detect shell commands (can cause false positives) |
121
+ | `checkPathTraversal` | `boolean` | `true` | Detect `../`, `%2e%2e`, null bytes |
122
+ | `checkHomoglyphs` | `boolean` | `true` | Detect Unicode lookalike characters |
123
+
124
+ #### `isSafeInput(input, config?): boolean`
125
+
126
+ Quick check returning `true` if no threats are detected.
127
+
128
+ #### Individual Detectors
129
+
130
+ Each returns `ThreatFinding[]`:
131
+
132
+ | Function | Detects |
133
+ |----------|---------|
134
+ | `containsXSSPatterns(input)` | Script tags, event handlers, `javascript:` URIs, `eval()` |
135
+ | `containsSQLInjection(input)` | UNION SELECT, DROP TABLE, `1=1`, SLEEP, stacked queries |
136
+ | `containsNoSQLInjection(input)` | MongoDB operators (`$gt`, `$where`, `$function`) |
137
+ | `containsCommandInjection(input)` | Command substitution, piped shells |
138
+ | `containsPathTraversal(input)` | Directory traversal, null bytes, sensitive paths |
139
+ | `containsHomoglyphs(input)` | Cyrillic/Greek lookalike characters |
140
+
141
+ #### `sanitizeForDisplay(input): string`
142
+
143
+ Escapes HTML entities (`<`, `>`, `&`, `"`, `'`, `/`) for safe rendering.
144
+
145
+ #### `normalizeUnicode(input): string`
146
+
147
+ Normalizes to NFC form and replaces known homoglyphs with ASCII equivalents.
148
+
149
+ #### `validateSafeText(input): boolean`
150
+
151
+ Validates text is safe from all attack patterns. For use as a schema refinement.
152
+
153
+ #### `validateSafeName(input): boolean`
154
+
155
+ Validates a name field -- allows international characters but blocks injection patterns.
156
+
157
+ #### `validateSafeEmail(input): boolean`
158
+
159
+ Validates email format and checks for injection patterns.
160
+
161
+ #### `getThreatErrorMessage(result): string`
162
+
163
+ Returns a human-readable error message for a threat detection result.
164
+
165
+ ### Sanitization (`sanitize.ts`)
166
+
167
+ #### `escapeHtml(text): string`
168
+
169
+ Escapes `&`, `<`, `>`, `"`, `'` to HTML entities.
170
+
171
+ ```ts
172
+ escapeHtml('<img onerror="alert(1)">'); // "&lt;img onerror=&quot;alert(1)&quot;&gt;"
173
+ ```
174
+
175
+ #### `sanitizeUrl(url, allowedProtocols?): string`
176
+
177
+ Validates and returns a safe URL; returns empty string if unsafe.
178
+
179
+ - **allowedProtocols** (`string[]`, default: `["http:", "https:", "mailto:"]`).
180
+ - Blocks `javascript:`, `data:` URIs.
181
+
182
+ #### `sanitizeUrlEffect(url, allowedProtocols?): Exit<string, unknown>`
183
+
184
+ Effect-based version returning an `Exit`.
185
+
186
+ #### `validateUserInput(input, maxLength?, allowHtml?): string`
187
+
188
+ Strips HTML tags, normalizes whitespace, removes dangerous patterns, and truncates.
189
+
190
+ - **maxLength** (`number`, default `500`).
191
+ - **allowHtml** (`boolean`, default `false`).
192
+
193
+ #### `validateUserInputEffect(input, options?): Exit<string, unknown>`
194
+
195
+ Effect-based version with full options.
196
+
197
+ | Option | Type | Default | Description |
198
+ |--------|------|---------|-------------|
199
+ | `maxLength` | `number` | `500` | Max output length |
200
+ | `allowHtml` | `boolean` | `false` | Preserve HTML tags |
201
+ | `allowNewlines` | `boolean` | `false` | Preserve newlines |
202
+ | `trimWhitespace` | `boolean` | `true` | Trim leading/trailing whitespace |
203
+
204
+ #### `sanitizeJson<T>(jsonString): T | null`
205
+
206
+ Safely parses JSON with prototype pollution protection. Removes `__proto__`, `constructor`, `prototype` keys.
207
+
208
+ #### `parseJsonWithSchema<A>(jsonString, schema): Option<A>`
209
+
210
+ Parses JSON with Effect Schema validation and prototype pollution protection. Returns `Option.some(value)` or `Option.none()`.
211
+
212
+ ```ts
213
+ const UserSchema = Schema.Struct({ name: Schema.String, age: Schema.Number });
214
+ const user = parseJsonWithSchema('{"name":"John","age":30}', UserSchema);
215
+ // Option.some({ name: "John", age: 30 })
216
+ ```
217
+
218
+ #### `stripAnsi(text): string`
219
+
220
+ Removes ANSI escape codes from strings.
221
+
222
+ #### `redactPII(text, options?): string`
223
+
224
+ Replaces PII patterns with redaction markers.
225
+
226
+ | Option | Type | Default | Marker |
227
+ |--------|------|---------|--------|
228
+ | `redactEmails` | `boolean` | `true` | `[EMAIL]` |
229
+ | `redactPhones` | `boolean` | `true` | `[PHONE]` |
230
+ | `redactSSN` | `boolean` | `true` | `[SSN]` |
231
+ | `redactCreditCards` | `boolean` | `true` | `[CREDIT_CARD]` |
232
+ | `redactIPs` | `boolean` | `true` | `[IP_ADDRESS]` |
233
+ | `customPatterns` | `Array<{ pattern, replacement }>` | `[]` | custom |
234
+
235
+ #### `safeStringify(obj, sensitiveKeys?, indent?): string`
236
+
237
+ JSON.stringify with automatic redaction of sensitive keys.
238
+
239
+ - **sensitiveKeys** (`string[]`, default: `["password", "token", "apiKey", "secret", "authorization", "cookie", "ssn", "creditCard"]`).
240
+
241
+ ### Validation Helpers
242
+
243
+ | Function | Description |
244
+ |----------|-------------|
245
+ | `isValidEmail(email)` | Validates email format |
246
+ | `isValidPhone(phone)` | Validates US phone format |
247
+ | `isValidSSN(ssn)` | Validates US SSN format |
248
+ | `isValidUrl(url)` | Validates safe URL |
249
+
250
+ ### Effect Schemas
251
+
252
+ Exported for runtime validation: `SafeUrlSchema`, `EmailSchema`, `PhoneNumberSchema`, `SSNSchema`, `CreditCardSchema`, `IPv4Schema`, `SanitizedStringSchema`, `UrlProtocolSchema`, `PIIRedactionOptionsSchema`, `UserInputOptionsSchema`.
253
+
254
+ ### Types
255
+
256
+ Exported types: `ThreatDetectionResult`, `ThreatFinding`, `ThreatType`, `ThreatDetectionConfig`, `PIIRedactionOptions`, `UserInputOptions`, `SafeUrl`, `Email`, `PhoneNumber`, `SSN`, `CreditCard`, `IPv4`, `UrlProtocol`.
257
+
258
+ ## Prerequisites
259
+
260
+ - **Runtime**: Bun 1.1+ or Node.js 20+
261
+ - **Peer Dependencies**: `effect` (v4.0.0-beta.93+)
262
+
263
+ ## Configuration
264
+
265
+ - **Crypto Key**: Ensure `ENCRYPTION_KEY` is set for cryptographic modules (must be a valid hex string of proper bit length).
266
+
267
+ ## Testing
268
+
269
+ ```sh
270
+ bun --filter @resq-systems/security test
271
+ ```
272
+
273
+ ## Troubleshooting
274
+
275
+ - **Strict JSON parsing error**: Stricter `ts-reset` settings type `JSON.parse` as `unknown`. Use the provided `sanitizeJson` / `parseJsonWithSchema` wrapper helpers to cast to safe formats.
276
+
277
+
278
+ ## License
279
+
280
+ Apache-2.0
@@ -0,0 +1,195 @@
1
+ import { Brand, PositiveInt } from "@resq-systems/types";
2
+
3
+ //#region src/crypto.d.ts
4
+ /**
5
+ * Base64 AES-256-GCM payload produced by {@link encryptData} — the
6
+ * `salt | iv | authTag | ciphertext` envelope. Only {@link decryptData}
7
+ * should consume a value of this type; read one back from storage through
8
+ * {@link toCiphertext}.
9
+ */
10
+ type Ciphertext = Brand<string, "Ciphertext">;
11
+ /**
12
+ * A secret accepted by {@link encryptData}/{@link decryptData} as the
13
+ * scrypt password. Mint one at the boundary where the secret enters the
14
+ * process (typically from `process.env`) via {@link toEncryptionKey}.
15
+ */
16
+ type EncryptionKey = Brand<string, "EncryptionKey">;
17
+ /** Cryptographically random hex token minted by {@link generateSecureToken}. */
18
+ type SecureToken = Brand<string, "SecureToken">;
19
+ /** Lowercase 64-char SHA-256 hex digest produced by {@link hashData}. */
20
+ type Sha256Hex = Brand<string, "Sha256Hex">;
21
+ /** A PII string masked for safe logging by {@link maskPII}/{@link maskEmail}. */
22
+ type Masked = Brand<string, "Masked">;
23
+ /** Type guard: `true` when `value` is a usable {@link EncryptionKey}. */
24
+ declare const isEncryptionKey: (value: string) => value is Brand<string, "EncryptionKey">;
25
+ /** Assert `value` is a non-empty secret and brand it, throwing otherwise. */
26
+ declare const toEncryptionKey: (value: string) => Brand<string, "EncryptionKey">;
27
+ /** Return `value` branded as an {@link EncryptionKey}, or `null` when empty. */
28
+ declare const coerceEncryptionKey: (value: string) => Brand<string, "EncryptionKey">;
29
+ /** Brand `value` as an {@link EncryptionKey} without checking. */
30
+ declare const unsafeEncryptionKey: (value: string) => Brand<string, "EncryptionKey">;
31
+ /** Type guard: `true` when `value` is a well-formed {@link Ciphertext} envelope. */
32
+ declare const isCiphertext: (value: string) => value is Brand<string, "Ciphertext">;
33
+ /** Assert `value` is a well-formed envelope and brand it, throwing otherwise. */
34
+ declare const toCiphertext: (value: string) => Brand<string, "Ciphertext">;
35
+ /** Return `value` branded as a {@link Ciphertext}, or `null` when malformed. */
36
+ declare const coerceCiphertext: (value: string) => Brand<string, "Ciphertext">;
37
+ /** Brand `value` as a {@link Ciphertext} without checking. */
38
+ declare const unsafeCiphertext: (value: string) => Brand<string, "Ciphertext">;
39
+ /**
40
+ * Encrypt a UTF-8 string with AES-256-GCM authenticated encryption.
41
+ *
42
+ * Each call generates a fresh random salt and IV — the same plaintext
43
+ * encrypted twice with the same `encryptionKey` produces different
44
+ * ciphertexts, which is the property you want for at-rest encryption.
45
+ *
46
+ * Output layout (base64-encoded): `salt(32) | iv(16) | authTag(16) | ciphertext(*)`.
47
+ * The companion {@link decryptData} understands this layout.
48
+ *
49
+ * @param plaintext - UTF-8 string to encrypt.
50
+ * @param encryptionKey - Caller-supplied secret. Treated as a password
51
+ * and stretched into a 256-bit AES key via scrypt; can be any length,
52
+ * though a high-entropy secret (≥ 32 bytes) is strongly preferred.
53
+ *
54
+ * @returns A self-contained base64 string. Store or transmit verbatim;
55
+ * the salt/IV are recovered on decryption.
56
+ *
57
+ * @throws From the underlying Node crypto primitives if `encryptionKey`
58
+ * is empty or scrypt fails.
59
+ *
60
+ * @compliance NIST 800-53 SC-28 (Protection of Information at Rest),
61
+ * SC-13 (Cryptographic Protection).
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * const ct = await encryptData("user@example.com", process.env.PII_KEY!);
66
+ * await db.users.update(id, { email: ct });
67
+ * ```
68
+ */
69
+ declare function encryptData(plaintext: string, encryptionKey: EncryptionKey): Promise<Ciphertext>;
70
+ /**
71
+ * Reverse {@link encryptData}. Verifies the GCM authentication tag
72
+ * before returning plaintext — tampered ciphertexts throw.
73
+ *
74
+ * @param encryptedData - Base64 string produced by {@link encryptData}.
75
+ * @param encryptionKey - Same key/password used to encrypt. Wrong keys
76
+ * throw an "Unsupported state or unable to authenticate data" error
77
+ * from Node — the authenticated tag failure is indistinguishable from
78
+ * tampering, by design.
79
+ *
80
+ * @returns The original UTF-8 plaintext.
81
+ *
82
+ * @throws Error if the tag does not verify (wrong key, modified
83
+ * ciphertext, truncated payload). Catch this and treat it as a
84
+ * security event, not a recoverable error.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * const plaintext = await decryptData(stored, process.env.PII_KEY!);
89
+ * ```
90
+ */
91
+ declare function decryptData(encryptedData: Ciphertext, encryptionKey: EncryptionKey): Promise<string>;
92
+ /**
93
+ * Compute a SHA-256 digest of a UTF-8 string and return it as lowercase
94
+ * hex.
95
+ *
96
+ * **Not for password storage.** SHA-256 is fast by design — use a
97
+ * deliberately slow KDF (`bcrypt`, `argon2`, or `scrypt`) for
98
+ * password-equivalent material. This helper is intended for
99
+ * non-reversible identifiers, content hashes, and idempotency keys.
100
+ *
101
+ * @param data - UTF-8 input.
102
+ * @returns 64-character lowercase hex digest.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * hashData("hello"); // → "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
107
+ * ```
108
+ */
109
+ declare function hashData(data: string): Sha256Hex;
110
+ /**
111
+ * Generate a cryptographically random hex token suitable for session
112
+ * IDs, password-reset tokens, CSRF tokens, and similar single-use
113
+ * secrets.
114
+ *
115
+ * @param length - Number of random *bytes* to draw as a {@link PositiveInt}
116
+ * (the returned hex string is twice as long). Default `32` ⇒ 64-char hex
117
+ * / 256 bits of entropy. Construct non-default lengths with `toPositiveInt`
118
+ * so zero-byte and negative lengths are unrepresentable.
119
+ * @returns A {@link SecureToken}: lowercase hex string of length `length * 2`.
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * import { toPositiveInt } from "@resq-systems/types";
124
+ * generateSecureToken(); // 64-char hex (256-bit entropy)
125
+ * generateSecureToken(toPositiveInt(16)); // 32-char hex (128-bit entropy)
126
+ * ```
127
+ */
128
+ declare function generateSecureToken(length?: PositiveInt): SecureToken;
129
+ /**
130
+ * Mask an arbitrary PII string for safe logging — keeps the first two
131
+ * and last two characters and replaces everything in between with
132
+ * asterisks. Strings of length ≤ 4 are fully masked as `"****"`.
133
+ *
134
+ * @param data - Raw PII string.
135
+ * @returns Masked representation safe for logs.
136
+ *
137
+ * @example
138
+ * ```ts
139
+ * maskPII("4242424242424242"); // → "42************42"
140
+ * maskPII("AB12"); // → "****"
141
+ * ```
142
+ */
143
+ declare function maskPII(data: string): Masked;
144
+ /**
145
+ * Mask an email address while preserving the domain — useful for
146
+ * deduplication and support workflows where the domain is non-PII but
147
+ * the local part identifies the user.
148
+ *
149
+ * @param email - Full email. Falls back to {@link maskPII} if the input
150
+ * does not contain a valid `local@domain` shape.
151
+ * @returns Masked email; e.g. `"j*****e@example.com"`.
152
+ *
153
+ * @example
154
+ * ```ts
155
+ * maskEmail("jane@example.com"); // → "j**e@example.com"
156
+ * maskEmail("ab@example.com"); // → "**@example.com"
157
+ * maskEmail("not-an-email"); // → "no********il" (maskPII fallback)
158
+ * ```
159
+ */
160
+ declare function maskEmail(email: string): Masked;
161
+ /**
162
+ * Recursively shallow-copy an object, replacing any field whose key
163
+ * contains a sensitive substring (case-insensitive) with `[REDACTED]`,
164
+ * and masking string fields whose key contains `"email"` via
165
+ * {@link maskEmail}.
166
+ *
167
+ * Designed for log structures — preserves shape so log queries continue
168
+ * to work, but ensures secrets and identifiers don't leak. Use as a
169
+ * defensive layer **before** writing structured log lines.
170
+ *
171
+ * @param obj - Object to sanitize. Original is not mutated.
172
+ * @param sensitiveFields - Substring allow-list. Defaults to
173
+ * `["password", "passwordHash", "token", "secret",
174
+ * "twoFactorSecret", "apiKey"]`. Substrings match anywhere in the
175
+ * key, e.g. `"token"` matches `"refreshToken"` and `"id_token"`.
176
+ *
177
+ * @returns A new object with sensitive fields redacted and emails
178
+ * masked. Nested objects are recursed; arrays and primitives pass
179
+ * through unchanged.
180
+ *
181
+ * @example
182
+ * ```ts
183
+ * sanitizeForLogging({
184
+ * id: 1,
185
+ * email: "u@x.com",
186
+ * apiKey: "sk-...",
187
+ * nested: { token: "..." },
188
+ * });
189
+ * // → { id: 1, email: "u@x.com" (masked), apiKey: "[REDACTED]", nested: { token: "[REDACTED]" } }
190
+ * ```
191
+ */
192
+ declare function sanitizeForLogging(obj: Record<string, unknown>, sensitiveFields?: string[]): Record<string, unknown>;
193
+ //#endregion
194
+ export { Ciphertext, EncryptionKey, Masked, SecureToken, Sha256Hex, coerceCiphertext, coerceEncryptionKey, decryptData, encryptData, generateSecureToken, hashData, isCiphertext, isEncryptionKey, maskEmail, maskPII, sanitizeForLogging, toCiphertext, toEncryptionKey, unsafeCiphertext, unsafeEncryptionKey };
195
+ //# sourceMappingURL=crypto.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.mts","names":[],"sources":["../src/crypto.ts"],"mappings":";;;;AA8FA;;;;;KAjCY,UAAA,GAAa,KAAA;;;;AAmCzB;;KA5BY,aAAA,GAAgB,KAAA;;KAGhB,WAAA,GAAc,KAAA;AA2B1B;AAAA,KAxBY,SAAA,GAAY,KAAA;;KAGZ,MAAA,GAAS,KAAA;;cAiBR,eAAA,GAAe,KAAA,aAAA,KAAA,IAAA,KAAA;;cAEf,eAAA,GAAe,KAAA,aAAA,KAAA;;cAEf,mBAAA,GAAmB,KAAA,aAAA,KAAA;AAgBhC;AAAA,cAda,mBAAA,GAAmB,KAAA,aAAA,KAAA;;cAcnB,YAAA,GAAY,KAAA,aAAA,KAAA,IAAA,KAAA;;cAEZ,YAAA,GAAY,KAAA,aAAA,KAAA;;cAEZ,gBAAA,GAAgB,KAAA,aAAA,KAAA;;cAEhB,gBAAA,GAAgB,KAAA,aAAA,KAAA;;;;;AAF7B;;;;;AAEA;;;;;AA8CA;;;;;;;;;;;;;;;AAqCA;iBArCsB,WAAA,CACrB,SAAA,UACA,aAAA,EAAe,aAAA,GACb,OAAA,CAAQ,UAAA;;;;;;;;;;;;;;AA0EX;;;;;AAsBA;;;iBA9DsB,WAAA,CACrB,aAAA,EAAe,UAAA,EACf,aAAA,EAAe,aAAA,GACb,OAAA;;;;;;AA6EH;;;;;AAyBA;;;;;AA2CA;;iBA5GgB,QAAA,CAAS,IAAA,WAAe,SAAA;;;;;;;;;;;;;;;;;;;iBAsBxB,mBAAA,CAAoB,MAAA,GAAQ,WAAA,GAAkC,WAAA;;;;;;;;;;;;;;;iBAkB9D,OAAA,CAAQ,IAAA,WAAe,MAAA;;;;;;;;;;;;;;;;;iBAyBvB,SAAA,CAAU,KAAA,WAAgB,MAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2C1B,kBAAA,CACf,GAAA,EAAK,MAAA,mBACL,eAAA,cAQE,MAAA"}