xploitscan 0.4.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 +148 -0
- package/dist/api-QTNXZY4O.js +24 -0
- package/dist/api-QTNXZY4O.js.map +1 -0
- package/dist/chunk-47X3TUVR.js +128 -0
- package/dist/chunk-47X3TUVR.js.map +1 -0
- package/dist/index.js +5266 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# xploitscan
|
|
2
|
+
|
|
3
|
+
AI security scanner for vibe-coded apps. Find vulnerabilities before attackers do.
|
|
4
|
+
|
|
5
|
+
Built for solo devs and non-technical founders shipping AI-generated code via Cursor, Lovable, Bolt, Replit, and Claude Code.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx xploitscan scan .
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
That's it. No config needed.
|
|
14
|
+
|
|
15
|
+
## What It Catches
|
|
16
|
+
|
|
17
|
+
| Rule | Vulnerability | Severity |
|
|
18
|
+
|------|--------------|----------|
|
|
19
|
+
| VC001 | Hardcoded API keys & secrets (AWS, Stripe, OpenAI, Supabase, DB URLs) | Critical |
|
|
20
|
+
| VC002 | .env files with secrets committed to git | High |
|
|
21
|
+
| VC003 | API routes missing authentication | High |
|
|
22
|
+
| VC004 | Supabase service_role key in client code / RLS bypass | Critical |
|
|
23
|
+
| VC005 | Stripe webhooks without signature verification | Critical |
|
|
24
|
+
| VC006 | SQL injection via string interpolation | Critical |
|
|
25
|
+
| VC007 | XSS (dangerouslySetInnerHTML, innerHTML, v-html) | High |
|
|
26
|
+
| VC008 | Server without rate limiting | Medium |
|
|
27
|
+
| VC009 | Wildcard CORS configuration | Medium |
|
|
28
|
+
| VC010 | Client-side only authorization checks | High |
|
|
29
|
+
|
|
30
|
+
Plus **AI-powered contextual analysis** that catches issues static rules miss.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Run directly (no install)
|
|
36
|
+
npx xploitscan scan .
|
|
37
|
+
|
|
38
|
+
# Or install globally
|
|
39
|
+
npm install -g xploitscan
|
|
40
|
+
xploitscan scan .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Scan current directory
|
|
47
|
+
xploitscan scan .
|
|
48
|
+
|
|
49
|
+
# Scan a specific directory
|
|
50
|
+
xploitscan scan ./my-project
|
|
51
|
+
|
|
52
|
+
# Skip AI analysis (faster, no API key needed)
|
|
53
|
+
xploitscan scan . --no-ai
|
|
54
|
+
|
|
55
|
+
# JSON output (for CI pipelines)
|
|
56
|
+
xploitscan scan . --format json
|
|
57
|
+
|
|
58
|
+
# SARIF output (for GitHub Code Scanning)
|
|
59
|
+
xploitscan scan . --format sarif
|
|
60
|
+
|
|
61
|
+
# Verbose output (show per-scanner results)
|
|
62
|
+
xploitscan scan . -v
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## AI-Powered Analysis
|
|
66
|
+
|
|
67
|
+
Set your Anthropic API key for deeper, contextual vulnerability analysis:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
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.
|
|
75
|
+
|
|
76
|
+
## CI Integration
|
|
77
|
+
|
|
78
|
+
### GitHub Actions
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
name: Security Scan
|
|
82
|
+
on: [push, pull_request]
|
|
83
|
+
|
|
84
|
+
jobs:
|
|
85
|
+
xploitscan:
|
|
86
|
+
runs-on: ubuntu-latest
|
|
87
|
+
steps:
|
|
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
|
+
|
|
96
|
+
### Any CI
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npx xploitscan scan . --format sarif --no-ai > results.sarif
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Exit code is 1 when critical or high severity issues are found.
|
|
103
|
+
|
|
104
|
+
## Configuration
|
|
105
|
+
|
|
106
|
+
Create a `.xploitscanrc.json` in your project root:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"exclude": ["tests/**", "scripts/**"],
|
|
111
|
+
"ai": true,
|
|
112
|
+
"severity": "medium",
|
|
113
|
+
"disableRules": ["VC008"]
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Optional: Deeper Scanning
|
|
118
|
+
|
|
119
|
+
Install these tools for additional detection coverage:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# Semgrep - 2000+ community security rules
|
|
123
|
+
pip install semgrep
|
|
124
|
+
|
|
125
|
+
# Gitleaks - advanced secret detection
|
|
126
|
+
brew install gitleaks
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
XploitScan automatically uses them if available.
|
|
130
|
+
|
|
131
|
+
## Auth & Pro Plan
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# Log in to sync scan history
|
|
135
|
+
xploitscan auth login
|
|
136
|
+
|
|
137
|
+
# Check your plan
|
|
138
|
+
xploitscan auth whoami
|
|
139
|
+
|
|
140
|
+
# Upgrade to Pro ($29/mo) for unlimited scans
|
|
141
|
+
xploitscan upgrade
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Free plan: 3 scans/day. Pro: unlimited scans, scan history, team features.
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
checkUsage,
|
|
4
|
+
clearToken,
|
|
5
|
+
getCheckoutUrl,
|
|
6
|
+
getStoredToken,
|
|
7
|
+
incrementUsage,
|
|
8
|
+
isAuthenticated,
|
|
9
|
+
storeToken,
|
|
10
|
+
syncUser,
|
|
11
|
+
uploadScanResults
|
|
12
|
+
} from "./chunk-47X3TUVR.js";
|
|
13
|
+
export {
|
|
14
|
+
checkUsage,
|
|
15
|
+
clearToken,
|
|
16
|
+
getCheckoutUrl,
|
|
17
|
+
getStoredToken,
|
|
18
|
+
incrementUsage,
|
|
19
|
+
isAuthenticated,
|
|
20
|
+
storeToken,
|
|
21
|
+
syncUser,
|
|
22
|
+
uploadScanResults
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=api-QTNXZY4O.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
|
|
9
|
+
// src/utils/api.ts
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
var CONFIG_DIR = join(homedir(), ".xploitscan");
|
|
14
|
+
var TOKEN_FILE = join(CONFIG_DIR, "token.json");
|
|
15
|
+
var API_BASE = process.env.XPLOITSCAN_API_URL ?? "https://api.xploitscan.com";
|
|
16
|
+
if (API_BASE.startsWith("http://") && !API_BASE.includes("localhost") && !API_BASE.includes("127.0.0.1")) {
|
|
17
|
+
console.warn("WARNING: API URL is not using HTTPS. This is insecure.");
|
|
18
|
+
}
|
|
19
|
+
function getStoredToken() {
|
|
20
|
+
try {
|
|
21
|
+
if (!existsSync(TOKEN_FILE)) return null;
|
|
22
|
+
const data = JSON.parse(readFileSync(TOKEN_FILE, "utf-8"));
|
|
23
|
+
if (data.expiresAt && Date.now() > data.expiresAt) {
|
|
24
|
+
unlinkSync(TOKEN_FILE);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return data;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function storeToken(data) {
|
|
33
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
34
|
+
writeFileSync(TOKEN_FILE, JSON.stringify(data, null, 2), { mode: 384 });
|
|
35
|
+
}
|
|
36
|
+
function clearToken() {
|
|
37
|
+
try {
|
|
38
|
+
if (existsSync(TOKEN_FILE)) {
|
|
39
|
+
unlinkSync(TOKEN_FILE);
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function isAuthenticated() {
|
|
45
|
+
return getStoredToken() !== null;
|
|
46
|
+
}
|
|
47
|
+
async function apiRequest(path, options = {}) {
|
|
48
|
+
const token = getStoredToken();
|
|
49
|
+
const headers = {
|
|
50
|
+
"Content-Type": "application/json",
|
|
51
|
+
...options.headers
|
|
52
|
+
};
|
|
53
|
+
if (token) {
|
|
54
|
+
headers.Authorization = `Bearer ${token.token}`;
|
|
55
|
+
}
|
|
56
|
+
return fetch(`${API_BASE}${path}`, {
|
|
57
|
+
...options,
|
|
58
|
+
headers
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async function checkUsage() {
|
|
62
|
+
const token = getStoredToken();
|
|
63
|
+
if (!token) {
|
|
64
|
+
return { allowed: true, plan: "anonymous", remaining: -1, limit: -1 };
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const res = await apiRequest("/api/usage/check");
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
return { allowed: true, plan: "unknown", remaining: -1, limit: -1 };
|
|
70
|
+
}
|
|
71
|
+
return await res.json();
|
|
72
|
+
} catch {
|
|
73
|
+
return { allowed: true, plan: "offline", remaining: -1, limit: -1 };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function incrementUsage() {
|
|
77
|
+
const token = getStoredToken();
|
|
78
|
+
if (!token) return;
|
|
79
|
+
try {
|
|
80
|
+
await apiRequest("/api/usage/increment", { method: "POST" });
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function uploadScanResults(result) {
|
|
85
|
+
const token = getStoredToken();
|
|
86
|
+
if (!token) return;
|
|
87
|
+
try {
|
|
88
|
+
await apiRequest("/api/scans", {
|
|
89
|
+
method: "POST",
|
|
90
|
+
body: JSON.stringify(result)
|
|
91
|
+
});
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function syncUser() {
|
|
96
|
+
try {
|
|
97
|
+
const res = await apiRequest("/api/users/sync", { method: "POST" });
|
|
98
|
+
if (!res.ok) return null;
|
|
99
|
+
const data = await res.json();
|
|
100
|
+
return data.user;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function getCheckoutUrl() {
|
|
106
|
+
try {
|
|
107
|
+
const res = await apiRequest("/api/billing/checkout", { method: "POST" });
|
|
108
|
+
if (!res.ok) return null;
|
|
109
|
+
const data = await res.json();
|
|
110
|
+
return data.url;
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export {
|
|
117
|
+
__require,
|
|
118
|
+
getStoredToken,
|
|
119
|
+
storeToken,
|
|
120
|
+
clearToken,
|
|
121
|
+
isAuthenticated,
|
|
122
|
+
checkUsage,
|
|
123
|
+
incrementUsage,
|
|
124
|
+
uploadScanResults,
|
|
125
|
+
syncUser,
|
|
126
|
+
getCheckoutUrl
|
|
127
|
+
};
|
|
128
|
+
//# sourceMappingURL=chunk-47X3TUVR.js.map
|
|
@@ -0,0 +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":";;;;;;;;;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":[]}
|