xploitscan 0.6.0 → 0.8.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 +79 -79
- package/dist/{api-QTNXZY4O.js → api-Z7VNGPT2.js} +2 -2
- package/dist/{chunk-47X3TUVR.js → chunk-CBDFSACC.js} +1 -8
- package/dist/{chunk-47X3TUVR.js.map → chunk-CBDFSACC.js.map} +1 -1
- package/dist/index.js +608 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- /package/dist/{api-QTNXZY4O.js.map → api-Z7VNGPT2.js.map} +0 -0
package/README.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# XploitScan
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/xploitscan)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
**Security scanner for AI-generated code.** Find vulnerabilities before attackers do.
|
|
7
|
+
|
|
8
|
+
Built for developers shipping code via Cursor, Lovable, Bolt, Replit, and Claude Code. 131 security rules. Plain-English results. Copy-paste fixes.
|
|
6
9
|
|
|
7
10
|
## Quick Start
|
|
8
11
|
|
|
@@ -10,29 +13,29 @@ Built for solo devs and non-technical founders shipping AI-generated code via Cu
|
|
|
10
13
|
npx xploitscan scan .
|
|
11
14
|
```
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
No install, no config, no account required. Your code stays 100% local.
|
|
14
17
|
|
|
15
18
|
## What It Catches
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
|
20
|
-
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
131 rules across 15+ categories:
|
|
21
|
+
|
|
22
|
+
| Category | Examples | Rules |
|
|
23
|
+
|----------|---------|-------|
|
|
24
|
+
| **Secrets** | Hardcoded API keys, .env files, OAuth secrets, Terraform state | 15+ |
|
|
25
|
+
| **Injection** | SQL, XSS, SSRF, command injection, path traversal, XXE, SSTI | 20+ |
|
|
26
|
+
| **Authentication** | Missing auth, weak JWT, insecure password reset, OAuth flaws | 15+ |
|
|
27
|
+
| **Cryptography** | Weak RSA, deprecated TLS, ECB mode, hardcoded IVs | 10+ |
|
|
28
|
+
| **Infrastructure** | Dockerfile, Kubernetes, Terraform, AWS IAM misconfigs | 10+ |
|
|
29
|
+
| **Supply Chain** | Unpinned GitHub Actions, vulnerable dependencies | 5+ |
|
|
30
|
+
| **Information Leakage** | PII in logs, unencrypted DB fields, exposed admin routes | 10+ |
|
|
31
|
+
| **Code Quality** | Console.log in production, empty catch blocks, TODO/FIXME | 10+ |
|
|
32
|
+
|
|
33
|
+
Every finding includes OWASP Top 10 and CWE compliance mappings.
|
|
31
34
|
|
|
32
35
|
## Installation
|
|
33
36
|
|
|
34
37
|
```bash
|
|
35
|
-
# Run directly (
|
|
38
|
+
# Run directly (recommended — always latest version)
|
|
36
39
|
npx xploitscan scan .
|
|
37
40
|
|
|
38
41
|
# Or install globally
|
|
@@ -44,105 +47,102 @@ xploitscan scan .
|
|
|
44
47
|
|
|
45
48
|
```bash
|
|
46
49
|
# Scan current directory
|
|
47
|
-
xploitscan scan .
|
|
50
|
+
npx xploitscan scan .
|
|
48
51
|
|
|
49
|
-
# Scan a specific
|
|
50
|
-
xploitscan scan ./
|
|
52
|
+
# Scan a specific folder
|
|
53
|
+
npx xploitscan scan ./src
|
|
51
54
|
|
|
52
|
-
#
|
|
53
|
-
xploitscan scan . --
|
|
55
|
+
# JSON output (for scripting/CI)
|
|
56
|
+
npx xploitscan scan . --format json
|
|
54
57
|
|
|
55
|
-
#
|
|
56
|
-
xploitscan scan . --format
|
|
58
|
+
# SARIF output (for GitHub Security tab)
|
|
59
|
+
npx xploitscan scan . --format sarif
|
|
57
60
|
|
|
58
|
-
#
|
|
59
|
-
xploitscan scan . --
|
|
61
|
+
# Scan only changed files vs main branch
|
|
62
|
+
npx xploitscan scan . --diff
|
|
60
63
|
|
|
61
|
-
#
|
|
62
|
-
xploitscan scan .
|
|
64
|
+
# Watch mode — re-scan on file changes
|
|
65
|
+
npx xploitscan scan . --watch
|
|
63
66
|
```
|
|
64
67
|
|
|
65
|
-
##
|
|
68
|
+
## Output Formats
|
|
66
69
|
|
|
67
|
-
|
|
70
|
+
| Format | Use Case |
|
|
71
|
+
|--------|----------|
|
|
72
|
+
| `text` | Human-readable terminal output (default) |
|
|
73
|
+
| `json` | Machine-readable JSON with all findings |
|
|
74
|
+
| `sarif` | GitHub Security tab integration |
|
|
68
75
|
|
|
69
|
-
|
|
70
|
-
export ANTHROPIC_API_KEY=sk-ant-...
|
|
71
|
-
xploitscan scan .
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
The AI analyzer understands your code in context and explains vulnerabilities in plain English with specific fix instructions.
|
|
76
|
+
## GitHub Action
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
### GitHub Actions
|
|
78
|
+
Add automated scanning to every PR:
|
|
79
79
|
|
|
80
80
|
```yaml
|
|
81
81
|
name: Security Scan
|
|
82
82
|
on: [push, pull_request]
|
|
83
83
|
|
|
84
84
|
jobs:
|
|
85
|
-
|
|
85
|
+
security:
|
|
86
86
|
runs-on: ubuntu-latest
|
|
87
87
|
steps:
|
|
88
88
|
- uses: actions/checkout@v4
|
|
89
|
-
- uses: xploitscan/action@v1
|
|
90
|
-
with:
|
|
91
|
-
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
Results appear in the GitHub Security tab.
|
|
95
89
|
|
|
96
|
-
|
|
90
|
+
- name: Run XploitScan
|
|
91
|
+
uses: bgage72590/xploitscan@main
|
|
92
|
+
with:
|
|
93
|
+
path: '.'
|
|
94
|
+
format: 'sarif'
|
|
95
|
+
fail-on: 'critical'
|
|
97
96
|
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
- name: Upload SARIF
|
|
98
|
+
if: always()
|
|
99
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
100
|
+
with:
|
|
101
|
+
sarif_file: xploitscan-results.sarif
|
|
100
102
|
```
|
|
101
103
|
|
|
102
|
-
|
|
104
|
+
Findings appear in the GitHub Security tab as code scanning alerts.
|
|
103
105
|
|
|
104
106
|
## Configuration
|
|
105
107
|
|
|
106
|
-
Create a `.xploitscanrc
|
|
108
|
+
Create a `.xploitscanrc` file in your project root:
|
|
107
109
|
|
|
108
110
|
```json
|
|
109
111
|
{
|
|
110
|
-
"
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
"rules": {
|
|
113
|
+
"include": ["VC001-VC131"],
|
|
114
|
+
"exclude": ["VC042"]
|
|
115
|
+
},
|
|
116
|
+
"format": "json",
|
|
117
|
+
"fail-on": "high",
|
|
118
|
+
"ignore": ["node_modules", "dist", ".git"]
|
|
114
119
|
}
|
|
115
120
|
```
|
|
116
121
|
|
|
117
|
-
##
|
|
122
|
+
## Web Dashboard
|
|
118
123
|
|
|
119
|
-
|
|
124
|
+
Scan via the web at [xploitscan.com](https://xploitscan.com):
|
|
120
125
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
126
|
+
- Drag-and-drop file/ZIP upload
|
|
127
|
+
- GitHub URL scanning
|
|
128
|
+
- Scan history and score trends
|
|
129
|
+
- PDF security reports
|
|
130
|
+
- SOC2/ISO27001 compliance mapping
|
|
131
|
+
- Slack and Discord webhook notifications
|
|
124
132
|
|
|
125
|
-
|
|
126
|
-
brew install gitleaks
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
XploitScan automatically uses them if available.
|
|
133
|
+
**Free**: 5 scans/day, 30 core rules. **Pro** ($29/mo): unlimited scans, all 131 rules, and all dashboard features.
|
|
130
134
|
|
|
131
|
-
##
|
|
135
|
+
## Supported Languages
|
|
132
136
|
|
|
133
|
-
|
|
134
|
-
# Log in to sync scan history
|
|
135
|
-
xploitscan auth login
|
|
137
|
+
JavaScript, TypeScript, Python, Ruby, Go, Rust, Java, PHP, Swift, Kotlin, C#, Dart, C/C++, and configuration files (Dockerfile, Terraform, Kubernetes, GitHub Actions, .env).
|
|
136
138
|
|
|
137
|
-
|
|
138
|
-
xploitscan auth whoami
|
|
139
|
-
|
|
140
|
-
# Upgrade to Pro ($29/mo) for unlimited scans
|
|
141
|
-
xploitscan upgrade
|
|
142
|
-
```
|
|
139
|
+
## Links
|
|
143
140
|
|
|
144
|
-
|
|
141
|
+
- **Website**: [xploitscan.com](https://xploitscan.com)
|
|
142
|
+
- **Documentation**: [xploitscan.com/docs](https://xploitscan.com/docs)
|
|
143
|
+
- **Changelog**: [xploitscan.com/changelog](https://xploitscan.com/changelog)
|
|
144
|
+
- **Email**: admin@xploitscan.com
|
|
145
145
|
|
|
146
146
|
## License
|
|
147
147
|
|
|
148
|
-
MIT
|
|
148
|
+
MIT -- [Cipherline LLC](https://xploitscan.com)
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
storeToken,
|
|
10
10
|
syncUser,
|
|
11
11
|
uploadScanResults
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-CBDFSACC.js";
|
|
13
13
|
export {
|
|
14
14
|
checkUsage,
|
|
15
15
|
clearToken,
|
|
@@ -21,4 +21,4 @@ export {
|
|
|
21
21
|
syncUser,
|
|
22
22
|
uploadScanResults
|
|
23
23
|
};
|
|
24
|
-
//# sourceMappingURL=api-
|
|
24
|
+
//# sourceMappingURL=api-Z7VNGPT2.js.map
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
-
}) : x)(function(x) {
|
|
5
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
-
});
|
|
8
2
|
|
|
9
3
|
// src/utils/api.ts
|
|
10
4
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "fs";
|
|
@@ -114,7 +108,6 @@ async function getCheckoutUrl() {
|
|
|
114
108
|
}
|
|
115
109
|
|
|
116
110
|
export {
|
|
117
|
-
__require,
|
|
118
111
|
getStoredToken,
|
|
119
112
|
storeToken,
|
|
120
113
|
clearToken,
|
|
@@ -125,4 +118,4 @@ export {
|
|
|
125
118
|
syncUser,
|
|
126
119
|
getCheckoutUrl
|
|
127
120
|
};
|
|
128
|
-
//# sourceMappingURL=chunk-
|
|
121
|
+
//# sourceMappingURL=chunk-CBDFSACC.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/api.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nconst CONFIG_DIR = join(homedir(), \".xploitscan\");\nconst TOKEN_FILE = join(CONFIG_DIR, \"token.json\");\n\nconst API_BASE = process.env.XPLOITSCAN_API_URL ?? \"https://api.xploitscan.com\";\n\nif (API_BASE.startsWith(\"http://\") && !API_BASE.includes(\"localhost\") && !API_BASE.includes(\"127.0.0.1\")) {\n console.warn(\"WARNING: API URL is not using HTTPS. This is insecure.\");\n}\n\ninterface TokenData {\n token: string;\n userId: string;\n email: string;\n expiresAt?: number;\n}\n\nexport function getStoredToken(): TokenData | null {\n try {\n if (!existsSync(TOKEN_FILE)) return null;\n const data = JSON.parse(readFileSync(TOKEN_FILE, \"utf-8\"));\n if (data.expiresAt && Date.now() > data.expiresAt) {\n // Token expired\n unlinkSync(TOKEN_FILE);\n return null;\n }\n return data;\n } catch {\n return null;\n }\n}\n\nexport function storeToken(data: TokenData): void {\n mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });\n writeFileSync(TOKEN_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });\n}\n\nexport function clearToken(): void {\n try {\n if (existsSync(TOKEN_FILE)) {\n unlinkSync(TOKEN_FILE);\n }\n } catch {\n // ignore\n }\n}\n\nexport function isAuthenticated(): boolean {\n return getStoredToken() !== null;\n}\n\nasync function apiRequest(\n path: string,\n options: RequestInit = {},\n): Promise<Response> {\n const token = getStoredToken();\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n ...(options.headers as Record<string, string>),\n };\n\n if (token) {\n headers.Authorization = `Bearer ${token.token}`;\n }\n\n return fetch(`${API_BASE}${path}`, {\n ...options,\n headers,\n });\n}\n\nexport async function checkUsage(): Promise<{\n allowed: boolean;\n plan: string;\n remaining: number;\n limit: number;\n}> {\n const token = getStoredToken();\n if (!token) {\n // Unauthenticated users get limited local scans\n return { allowed: true, plan: \"anonymous\", remaining: -1, limit: -1 };\n }\n\n try {\n const res = await apiRequest(\"/api/usage/check\");\n if (!res.ok) {\n // API error — allow scan to proceed locally\n return { allowed: true, plan: \"unknown\", remaining: -1, limit: -1 };\n }\n return await res.json();\n } catch {\n // Network error — allow local scan\n return { allowed: true, plan: \"offline\", remaining: -1, limit: -1 };\n }\n}\n\nexport async function incrementUsage(): Promise<void> {\n const token = getStoredToken();\n if (!token) return;\n\n try {\n await apiRequest(\"/api/usage/increment\", { method: \"POST\" });\n } catch {\n // Silent fail — don't block scan\n }\n}\n\nexport async function uploadScanResults(result: {\n directory: string;\n filesScanned: number;\n findings: unknown[];\n duration: number;\n}): Promise<void> {\n const token = getStoredToken();\n if (!token) return;\n\n try {\n await apiRequest(\"/api/scans\", {\n method: \"POST\",\n body: JSON.stringify(result),\n });\n } catch {\n // Silent fail\n }\n}\n\nexport async function syncUser(): Promise<{ plan: string; email: string } | null> {\n try {\n const res = await apiRequest(\"/api/users/sync\", { method: \"POST\" });\n if (!res.ok) return null;\n const data = await res.json();\n return data.user;\n } catch {\n return null;\n }\n}\n\nexport async function getCheckoutUrl(): Promise<string | null> {\n try {\n const res = await apiRequest(\"/api/billing/checkout\", { method: \"POST\" });\n if (!res.ok) return null;\n const data = await res.json();\n return data.url;\n } catch {\n return null;\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/utils/api.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nconst CONFIG_DIR = join(homedir(), \".xploitscan\");\nconst TOKEN_FILE = join(CONFIG_DIR, \"token.json\");\n\nconst API_BASE = process.env.XPLOITSCAN_API_URL ?? \"https://api.xploitscan.com\";\n\nif (API_BASE.startsWith(\"http://\") && !API_BASE.includes(\"localhost\") && !API_BASE.includes(\"127.0.0.1\")) {\n console.warn(\"WARNING: API URL is not using HTTPS. This is insecure.\");\n}\n\ninterface TokenData {\n token: string;\n userId: string;\n email: string;\n expiresAt?: number;\n}\n\nexport function getStoredToken(): TokenData | null {\n try {\n if (!existsSync(TOKEN_FILE)) return null;\n const data = JSON.parse(readFileSync(TOKEN_FILE, \"utf-8\"));\n if (data.expiresAt && Date.now() > data.expiresAt) {\n // Token expired\n unlinkSync(TOKEN_FILE);\n return null;\n }\n return data;\n } catch {\n return null;\n }\n}\n\nexport function storeToken(data: TokenData): void {\n mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });\n writeFileSync(TOKEN_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });\n}\n\nexport function clearToken(): void {\n try {\n if (existsSync(TOKEN_FILE)) {\n unlinkSync(TOKEN_FILE);\n }\n } catch {\n // ignore\n }\n}\n\nexport function isAuthenticated(): boolean {\n return getStoredToken() !== null;\n}\n\nasync function apiRequest(\n path: string,\n options: RequestInit = {},\n): Promise<Response> {\n const token = getStoredToken();\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n ...(options.headers as Record<string, string>),\n };\n\n if (token) {\n headers.Authorization = `Bearer ${token.token}`;\n }\n\n return fetch(`${API_BASE}${path}`, {\n ...options,\n headers,\n });\n}\n\nexport async function checkUsage(): Promise<{\n allowed: boolean;\n plan: string;\n remaining: number;\n limit: number;\n}> {\n const token = getStoredToken();\n if (!token) {\n // Unauthenticated users get limited local scans\n return { allowed: true, plan: \"anonymous\", remaining: -1, limit: -1 };\n }\n\n try {\n const res = await apiRequest(\"/api/usage/check\");\n if (!res.ok) {\n // API error — allow scan to proceed locally\n return { allowed: true, plan: \"unknown\", remaining: -1, limit: -1 };\n }\n return await res.json();\n } catch {\n // Network error — allow local scan\n return { allowed: true, plan: \"offline\", remaining: -1, limit: -1 };\n }\n}\n\nexport async function incrementUsage(): Promise<void> {\n const token = getStoredToken();\n if (!token) return;\n\n try {\n await apiRequest(\"/api/usage/increment\", { method: \"POST\" });\n } catch {\n // Silent fail — don't block scan\n }\n}\n\nexport async function uploadScanResults(result: {\n directory: string;\n filesScanned: number;\n findings: unknown[];\n duration: number;\n}): Promise<void> {\n const token = getStoredToken();\n if (!token) return;\n\n try {\n await apiRequest(\"/api/scans\", {\n method: \"POST\",\n body: JSON.stringify(result),\n });\n } catch {\n // Silent fail\n }\n}\n\nexport async function syncUser(): Promise<{ plan: string; email: string } | null> {\n try {\n const res = await apiRequest(\"/api/users/sync\", { method: \"POST\" });\n if (!res.ok) return null;\n const data = await res.json();\n return data.user;\n } catch {\n return null;\n }\n}\n\nexport async function getCheckoutUrl(): Promise<string | null> {\n try {\n const res = await apiRequest(\"/api/billing/checkout\", { method: \"POST\" });\n if (!res.ok) return null;\n const data = await res.json();\n return data.url;\n } catch {\n return null;\n }\n}\n"],"mappings":";;;AAAA,SAAS,cAAc,eAAe,WAAW,YAAY,kBAAkB;AAC/E,SAAS,YAAY;AACrB,SAAS,eAAe;AAExB,IAAM,aAAa,KAAK,QAAQ,GAAG,aAAa;AAChD,IAAM,aAAa,KAAK,YAAY,YAAY;AAEhD,IAAM,WAAW,QAAQ,IAAI,sBAAsB;AAEnD,IAAI,SAAS,WAAW,SAAS,KAAK,CAAC,SAAS,SAAS,WAAW,KAAK,CAAC,SAAS,SAAS,WAAW,GAAG;AACxG,UAAQ,KAAK,wDAAwD;AACvE;AASO,SAAS,iBAAmC;AACjD,MAAI;AACF,QAAI,CAAC,WAAW,UAAU,EAAG,QAAO;AACpC,UAAM,OAAO,KAAK,MAAM,aAAa,YAAY,OAAO,CAAC;AACzD,QAAI,KAAK,aAAa,KAAK,IAAI,IAAI,KAAK,WAAW;AAEjD,iBAAW,UAAU;AACrB,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,WAAW,MAAuB;AAChD,YAAU,YAAY,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AACtD,gBAAc,YAAY,KAAK,UAAU,MAAM,MAAM,CAAC,GAAG,EAAE,MAAM,IAAM,CAAC;AAC1E;AAEO,SAAS,aAAmB;AACjC,MAAI;AACF,QAAI,WAAW,UAAU,GAAG;AAC1B,iBAAW,UAAU;AAAA,IACvB;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAEO,SAAS,kBAA2B;AACzC,SAAO,eAAe,MAAM;AAC9B;AAEA,eAAe,WACb,MACA,UAAuB,CAAC,GACL;AACnB,QAAM,QAAQ,eAAe;AAC7B,QAAM,UAAkC;AAAA,IACtC,gBAAgB;AAAA,IAChB,GAAI,QAAQ;AAAA,EACd;AAEA,MAAI,OAAO;AACT,YAAQ,gBAAgB,UAAU,MAAM,KAAK;AAAA,EAC/C;AAEA,SAAO,MAAM,GAAG,QAAQ,GAAG,IAAI,IAAI;AAAA,IACjC,GAAG;AAAA,IACH;AAAA,EACF,CAAC;AACH;AAEA,eAAsB,aAKnB;AACD,QAAM,QAAQ,eAAe;AAC7B,MAAI,CAAC,OAAO;AAEV,WAAO,EAAE,SAAS,MAAM,MAAM,aAAa,WAAW,IAAI,OAAO,GAAG;AAAA,EACtE;AAEA,MAAI;AACF,UAAM,MAAM,MAAM,WAAW,kBAAkB;AAC/C,QAAI,CAAC,IAAI,IAAI;AAEX,aAAO,EAAE,SAAS,MAAM,MAAM,WAAW,WAAW,IAAI,OAAO,GAAG;AAAA,IACpE;AACA,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB,QAAQ;AAEN,WAAO,EAAE,SAAS,MAAM,MAAM,WAAW,WAAW,IAAI,OAAO,GAAG;AAAA,EACpE;AACF;AAEA,eAAsB,iBAAgC;AACpD,QAAM,QAAQ,eAAe;AAC7B,MAAI,CAAC,MAAO;AAEZ,MAAI;AACF,UAAM,WAAW,wBAAwB,EAAE,QAAQ,OAAO,CAAC;AAAA,EAC7D,QAAQ;AAAA,EAER;AACF;AAEA,eAAsB,kBAAkB,QAKtB;AAChB,QAAM,QAAQ,eAAe;AAC7B,MAAI,CAAC,MAAO;AAEZ,MAAI;AACF,UAAM,WAAW,cAAc;AAAA,MAC7B,QAAQ;AAAA,MACR,MAAM,KAAK,UAAU,MAAM;AAAA,IAC7B,CAAC;AAAA,EACH,QAAQ;AAAA,EAER;AACF;AAEA,eAAsB,WAA4D;AAChF,MAAI;AACF,UAAM,MAAM,MAAM,WAAW,mBAAmB,EAAE,QAAQ,OAAO,CAAC;AAClE,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,WAAO,KAAK;AAAA,EACd,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,iBAAyC;AAC7D,MAAI;AACF,UAAM,MAAM,MAAM,WAAW,yBAAyB,EAAE,QAAQ,OAAO,CAAC;AACxE,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,WAAO,KAAK;AAAA,EACd,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
|