@hadi_ali/warden 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 hadiali
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,209 @@
1
+ Warden 🛡️
2
+
3
+ A high-performance Layer 7 Web Application Firewall (WAF) and bot-mitigation middleware for Express.js.
4
+
5
+ Warden doesn't just look at rate limits; it analyzes how a client interacts with your server. By combining header heuristics, Chromium fingerprinting, honeypot traps(including SubDomains), and intelligent error tracking, it blocks automated scanners, scrapers, and brute-force tools while letting legitimate users and verified search engines pass through with near-zero overhead.
6
+ 🚀 Installation
7
+ ```bash
8
+ npm install warden
9
+ ```
10
+
11
+ (Optional) Install drivers if you plan to use persistent storage:
12
+ ```bash
13
+ npm install pg redis
14
+ ```
15
+
16
+ Requires Node.js 18+
17
+ 🛡️ What it Protects Against
18
+
19
+ Warden acts as a behavioral firewall. It instantly detects and blocks:
20
+
21
+ Directory Fuzzers (Gobuster, ffuf, Nikto): Traps scanners hitting common sensitive paths (/.env, /wp-admin) and uses intelligent 404-tracking (which safely ignores missing images/CSS) to block rapid discovery attempts.
22
+
23
+ Headless Browsers & Scrapers (Puppeteer, Playwright, Selenium): Analyzes Chromium header anomalies. If a script claims to be Chrome 120 but is missing modern sec-ch-ua headers, or spoofs a Windows OS on a Linux machine, it is penalized and blocked.
24
+
25
+ Credential Stuffing & Brute-Forcing (Hydra, Intruder): Tracks repeated 401 Unauthorized and 400 Bad Request errors natively in the background, locking out attackers attempting to guess passwords.
26
+
27
+ Reconnaissance Bots: Penalizes requests targeting sensitive subdomains (e.g., admin.*, dev.*, vpn.*) to stop infrastructure mapping.
28
+
29
+ L7 Application DDoS: Implements dynamic rate limiting. Legitimate users get generous limits, while suspicious IPs are aggressively throttled.
30
+
31
+ Fake Search Engine Bots: Attackers spoofing Googlebot or Bingbot user-agents are blocked. Warden verifies true search engine IPs directly against Google and Bing's official CIDR ranges using high-speed bitwise math.
32
+
33
+ ⚠️ What it Does Not Do (Limitations)
34
+
35
+ To maintain sub-millisecond performance, Warden is designed as a behavioral WAF. You should be aware of what it does not cover:
36
+
37
+ Volumetric L3/L4 DDoS Attacks: If a botnet sends 50 Gbps of junk TCP traffic, Warden cannot save you. You still need Cloudflare or AWS Shield at the edge.
38
+
39
+ Payload Inspection (SQLi / XSS): Warden does not parse req.body to look for SQL injection or Cross-Site Scripting strings.
40
+
41
+ 💻 Usage
42
+
43
+ Warden is designed to be highly resilient. You can run it entirely in memory (Zero-Config), or attach Redis and PostgreSQL for cross-server rate-limiting and long-term IP reputation tracking.
44
+ Option A: Zero-Config (In-Memory)
45
+
46
+ Perfect for single-server setups or quick deployments. Features a self-cleaning LRU cache to prevent memory leaks.
47
+ ```javascript
48
+ const express = require('express');
49
+ const { warden, syncVerifiedBots, reloadRanges } = require('warden');
50
+
51
+ const app = express();
52
+
53
+ // 1. Trust your proxy if behind AWS ELB, Heroku, or Cloudflare
54
+ app.set('trust proxy', 1);
55
+
56
+ // 2. Sync Google/Bing verified IPs into memory
57
+ syncVerifiedBots().then(success => {
58
+ if (success) reloadRanges();
59
+ });
60
+
61
+ // 3. Attach middleware
62
+ app.use(warden({
63
+ scorethreshold: 30,
64
+ knownSubdomains: ['api', 'www'] // Whitelist your legit subdomains
65
+ }));
66
+
67
+ app.get('/', (req, res) => res.send('Protected by Warden!'));
68
+ app.listen(3000);
69
+ ```
70
+
71
+ Option B: Production (Postgres + Redis)
72
+
73
+ For distributed setups. Redis handles cross-server rate limiting, and Postgres remembers bad IPs for days/weeks. If either database goes down, Warden safely fails open to memory.
74
+ ```javascript
75
+ const express = require('express');
76
+ const { Pool } = require('pg');
77
+ const { createClient } = require('redis');
78
+ const { warden, syncVerifiedBots, reloadRanges, migrate } = require('warden');
79
+
80
+ const app = express();
81
+ app.set('trust proxy', 1);
82
+
83
+ async function start() {
84
+ const db = new Pool({ connectionString: process.env.DATABASE_URL });
85
+ const redis = createClient({ url: process.env.REDIS_URL });
86
+ await redis.connect();
87
+
88
+ // Auto-create necessary tables (warden_reputation, etc.)
89
+ await migrate(db);
90
+
91
+ // Sync verified bots (Stores them in DB/Redis so other servers don't have to fetch)
92
+ const success = await syncVerifiedBots();
93
+ if (success) reloadRanges();
94
+
95
+ app.use(warden({
96
+ db: db,
97
+ redis: redis,
98
+ scorethreshold: 30,
99
+ failopen: true, // If Redis/PG crashes, API stays online
100
+ }));
101
+
102
+ app.get('/', (req, res) => res.send('Enterprise protection active!'));
103
+ app.listen(3000);
104
+ }
105
+
106
+ start();
107
+ ```
108
+
109
+ (Note: You can also run migrations via CLI: CONNECTION_STRING="..." node node_modules/warden/app/db_connection/migrate.js)
110
+
111
+ ## 🛠️ CLI
112
+
113
+ After `npm install -g warden` (or `npx warden`) you get a `warden` command:
114
+
115
+ ```bash
116
+ warden --help # Show all commands
117
+ warden --version # Show version
118
+ warden cleanup --help # Cleanup options
119
+ ```
120
+
121
+ ### `warden cleanup` — Data Retention
122
+
123
+ Warden never deletes your data automatically. The `cleanup` command is the supported way to purge old rows from the three Warden Postgres tables. Schedule it yourself — cron, Kubernetes CronJob, `pg_cron`, GitHub Actions schedule, anything.
124
+
125
+ ```bash
126
+ # Defaults: reputation > 3 days, requests > 24 hours, sessions > 30 days
127
+ warden cleanup
128
+
129
+ # See what would be deleted without actually deleting
130
+ warden cleanup --dry-run
131
+
132
+ # Custom retention
133
+ warden cleanup --reputation-days 7 --requests-hours 48 --sessions-days 90
134
+
135
+ # Override the connection string
136
+ warden cleanup --connection postgres://user:pass@host/db
137
+ ```
138
+
139
+ **Schedule with cron** (Linux):
140
+
141
+ ```cron
142
+ # Run every hour
143
+ 0 * * * * cd /srv/myapp && /usr/bin/warden cleanup >> /var/log/warden.log 2>&1
144
+ ```
145
+
146
+ **Schedule with Kubernetes CronJob:**
147
+
148
+ ```yaml
149
+ apiVersion: batch/v1
150
+ kind: CronJob
151
+ metadata:
152
+ name: warden-cleanup
153
+ spec:
154
+ schedule: "0 * * * *"
155
+ jobTemplate:
156
+ spec:
157
+ template:
158
+ spec:
159
+ containers:
160
+ - name: cleanup
161
+ image: node:20-alpine
162
+ command: ["warden", "cleanup"]
163
+ env:
164
+ - name: CONNECTION_STRING
165
+ valueFrom:
166
+ secretKeyRef:
167
+ name: warden-db
168
+ key: connection-string
169
+ restartPolicy: OnFailure
170
+ ```
171
+
172
+ **Schedule with `pg_cron` (no app process needed):**
173
+
174
+ ```sql
175
+ SELECT cron.schedule('warden-cleanup', '0 * * * *', $$
176
+ DELETE FROM warden_reputation WHERE score < 20 AND last_seen < NOW() - INTERVAL '3 days';
177
+ DELETE FROM warden_requests WHERE timestamp < NOW() - INTERVAL '24 hours';
178
+ DELETE FROM warden_sessions WHERE last_seen < NOW() - INTERVAL '30 days';
179
+ $$);
180
+ ```
181
+
182
+ ## ⚙️ Configuration Options
183
+ Option Type Default Description
184
+ db pg.Pool null PostgreSQL pool for long-term reputation tracking and verified bot storage.
185
+ redis RedisClient null Redis client for distributed rate limiting. Falls back to in-memory LRU store.
186
+ allowlist string[] [] Array of exact IPs or CIDR ranges (e.g. ['10.0.0.0/8']) to always bypass checks.
187
+ failopen boolean true If true, errors within Warden allow the request to proceed. If false, returns 500.
188
+ scorethreshold number 30 The heuristic score at which a request is blocked with a 403 Forbidden.
189
+ suspiciousThreshold number 30 The score at which the onSuspicious webhook/callback fires.
190
+ trustHeaders boolean false Security: By default, Warden trusts req.ip. Set to true only if you safely parse X-Forwarded-For upstream.
191
+ knownSubdomains string[] [] Subdomains to ignore in the recon-scanner (e.g., ['api', 'app']).
192
+ limits object {...} Custom request limits for clean, suspicious, and hostile tiers.
193
+ getSessionId function null Custom function to extract a user session ID from req for session-based scoring.
194
+ onBlock function null Callback fired on block: (ipHash, score, reason, req) => {}
195
+ onSuspicious function null Callback fired on suspicious traffic: (ipHash, score, req) => {}
196
+ onRateLimit function null Callback fired on 429: (ipHash, score, req) => {}
197
+ ⚡ Performance & Architecture
198
+
199
+ Warden is engineered to add < 2ms of latency to legitimate requests.
200
+
201
+ Zero-Score DB Bypass: If a user connects with a standard, valid browser, their heuristic score evaluates to 0. Warden immediately skips the PostgreSQL reputation UPSERT entirely, saving heavy database I/O.
202
+
203
+ Post-Response Math: Brute-force calculations (like tracking 404s/401s) are executed asynchronously via res.on('finish'). The user receives their HTTP response instantly; the math happens in the background.
204
+
205
+ Bitwise CIDR Matching: Verified bots are resolved using 32-bit integer conversion and bitwise masking, allowing hundreds of Google/Bing CIDR ranges to be evaluated in fractions of a millisecond.
206
+
207
+ 📝 License
208
+
209
+ MIT License © 2025
package/app/Loger.js ADDED
@@ -0,0 +1,14 @@
1
+ function log({ reqId, ip_hash, score, action, reason, path, duration_ms }) {
2
+ console.log(JSON.stringify({
3
+ reqId,
4
+ ip_hash,
5
+ score,
6
+ action,
7
+ reason,
8
+ path,
9
+ duration_ms,
10
+ timestamp: new Date().toISOString(),
11
+ }));
12
+ }
13
+
14
+ module.exports = { log };
@@ -0,0 +1,43 @@
1
+ const { getClientIP } = require("./getClientIP.js");
2
+ const { normalizeIP } = require("./normalizeIP.js");
3
+ const { Address6, Address4 } = require('ip-address');
4
+ const net = require('net');
5
+
6
+ function isInSubnet(ip, cidr) {
7
+ if (!ip || !cidr) return false;
8
+
9
+ // Check if CIDR contains a slash
10
+ if (!cidr.includes('/')) {
11
+ return ip === cidr;
12
+ }
13
+
14
+ const ipVersion = net.isIP(ip.split('/')[0]);
15
+
16
+ try {
17
+ if (ipVersion === 6) {
18
+ const addr = new Address6(ip);
19
+ const subnet = new Address6(cidr);
20
+ return addr.isInSubnet(subnet);
21
+ } else if (ipVersion === 4) {
22
+ const addr = new Address4(ip);
23
+ const subnet = new Address4(cidr);
24
+ return addr.isInSubnet(subnet);
25
+ }
26
+ } catch (e) {
27
+ return false;
28
+ }
29
+ return false;
30
+ }
31
+
32
+ function checkAllowlist(req, allowlist=[]) {
33
+ const ip = normalizeIP(getClientIP(req));
34
+ if (!ip) return false;
35
+
36
+ for (const entry of allowlist) {
37
+ if (isInSubnet(ip, entry)) {
38
+ return true;
39
+ }
40
+ }
41
+ return false;
42
+ }
43
+ module.exports = { checkAllowlist };
@@ -0,0 +1,13 @@
1
+ const { store } = require('./store');
2
+
3
+ const WINDOW_SECONDS = 5;
4
+
5
+ async function trackFailure(ip_hash) {
6
+
7
+ const key = `bf:${ip_hash}`;
8
+ const count = await store.increment(key, WINDOW_SECONDS);
9
+ return count;
10
+ }
11
+
12
+
13
+ module.exports = { trackFailure };
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+ // Warden data-retention CLI.
3
+ // Deletes old rows from the Warden Postgres tables. Run on your own schedule
4
+ // (cron, Kubernetes CronJob, pg_cron, GitHub Actions schedule, etc.) — the
5
+ // library itself NEVER deletes data automatically.
6
+ //
7
+ // Usage:
8
+ // warden cleanup [options]
9
+ //
10
+ // Options:
11
+ // --reputation-days <N> Delete warden_reputation rows where last_seen > N days (default: 3)
12
+ // --requests-hours <N> Delete warden_requests rows older than N hours (default: 24)
13
+ // --sessions-days <N> Delete warden_sessions rows where last_seen > N days (default: 30)
14
+ // --dry-run Count rows that WOULD be deleted, but don't delete anything
15
+ // --connection <string> Postgres connection string (default: $CONNECTION_STRING)
16
+ // --help, -h Show this message
17
+
18
+ function parseArgs(argv) {
19
+ const opts = {
20
+ reputationDays: 3,
21
+ requestsHours: 24,
22
+ sessionsDays: 30,
23
+ dryRun: false,
24
+ connection: process.env.CONNECTION_STRING || null,
25
+ };
26
+
27
+ for (let i = 0; i < argv.length; i++) {
28
+ const a = argv[i];
29
+ switch (a) {
30
+ case '--reputation-days':
31
+ case '--requests-hours':
32
+ case '--sessions-days': {
33
+ const key = a.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
34
+ const v = parseInt(argv[++i], 10);
35
+ if (!Number.isFinite(v) || v < 0) {
36
+ console.error(`Error: ${a} requires a non-negative integer`);
37
+ process.exit(2);
38
+ }
39
+ opts[key] = v;
40
+ break;
41
+ }
42
+ case '--dry-run':
43
+ opts.dryRun = true;
44
+ break;
45
+ case '--connection':
46
+ opts.connection = argv[++i];
47
+ if (!opts.connection) {
48
+ console.error('Error: --connection requires a value');
49
+ process.exit(2);
50
+ }
51
+ break;
52
+ case '--help':
53
+ case '-h':
54
+ printUsage();
55
+ process.exit(0);
56
+ default:
57
+ console.error(`Error: unknown option: ${a}`);
58
+ printUsage();
59
+ process.exit(2);
60
+ }
61
+ }
62
+
63
+ return opts;
64
+ }
65
+
66
+ function printUsage() {
67
+ process.stdout.write(`Warden cleanup CLI
68
+
69
+ Usage: warden cleanup [options]
70
+
71
+ Options:
72
+ --reputation-days <N> Delete warden_reputation rows where last_seen > N days (default: 3)
73
+ --requests-hours <N> Delete warden_requests rows older than N hours (default: 24)
74
+ --sessions-days <N> Delete warden_sessions rows where last_seen > N days (default: 30)
75
+ --dry-run Count rows that WOULD be deleted, but don't delete anything
76
+ --connection <string> Postgres connection string (default: \$CONNECTION_STRING)
77
+ --help, -h Show this message
78
+
79
+ Examples:
80
+ warden cleanup --dry-run
81
+ warden cleanup --reputation-days 7 --requests-hours 48
82
+ CONNECTION_STRING=postgres://user:pass@host/db warden cleanup
83
+
84
+ Schedule with cron:
85
+ 0 * * * * cd /srv/myapp && /usr/bin/warden cleanup >> /var/log/warden.log 2>&1
86
+ `);
87
+ }
88
+
89
+ function buildQueries(opts) {
90
+ const rep = parseInt(opts.reputationDays, 10);
91
+ const req = parseInt(opts.requestsHours, 10);
92
+ const ses = parseInt(opts.sessionsDays, 10);
93
+
94
+ return [
95
+ {
96
+ name: 'reputation',
97
+ label: `warden_reputation (score<20 AND last_seen > ${rep}d)`,
98
+ countSql: `SELECT COUNT(*)::int AS n FROM warden_reputation WHERE score < 20 AND last_seen < NOW() - INTERVAL '${rep} days'`,
99
+ deleteSql: `DELETE FROM warden_reputation WHERE score < 20 AND last_seen < NOW() - INTERVAL '${rep} days'`,
100
+ },
101
+ {
102
+ name: 'requests',
103
+ label: `warden_requests (timestamp > ${req}h)`,
104
+ countSql: `SELECT COUNT(*)::int AS n FROM warden_requests WHERE timestamp < NOW() - INTERVAL '${req} hours'`,
105
+ deleteSql: `DELETE FROM warden_requests WHERE timestamp < NOW() - INTERVAL '${req} hours'`,
106
+ },
107
+ {
108
+ name: 'sessions',
109
+ label: `warden_sessions (last_seen > ${ses}d)`,
110
+ countSql: `SELECT COUNT(*)::int AS n FROM warden_sessions WHERE last_seen < NOW() - INTERVAL '${ses} days'`,
111
+ deleteSql: `DELETE FROM warden_sessions WHERE last_seen < NOW() - INTERVAL '${ses} days'`,
112
+ },
113
+ ];
114
+ }
115
+
116
+ async function run(argv) {
117
+ const opts = parseArgs(argv);
118
+
119
+ if (!opts.connection) {
120
+ console.error('Error: --connection or CONNECTION_STRING environment variable required.');
121
+ process.exit(2);
122
+ }
123
+
124
+ const { Pool } = require('pg');
125
+ const pool = new Pool({ connectionString: opts.connection });
126
+ let hadError = false;
127
+
128
+ try {
129
+ const queries = buildQueries(opts);
130
+ const mode = opts.dryRun ? 'DRY RUN' : 'CLEANUP';
131
+ console.log(`[warden] ${mode} starting at ${new Date().toISOString()}`);
132
+
133
+ let totalAffected = 0;
134
+ for (const q of queries) {
135
+ try {
136
+ const sql = opts.dryRun ? q.countSql : q.deleteSql;
137
+ const res = await pool.query(sql);
138
+ const n = opts.dryRun ? res.rows[0].n : res.rowCount;
139
+ totalAffected += n;
140
+ const verb = opts.dryRun ? 'would delete' : 'deleted';
141
+ console.log(`[warden] ${q.label}: ${verb} ${n} row(s)`);
142
+ } catch (err) {
143
+ hadError = true;
144
+ console.error(`[warden] ${q.name} failed:`, err.message);
145
+ }
146
+ }
147
+
148
+ const summary = opts.dryRun
149
+ ? `[warden] DRY RUN complete. Would have deleted ${totalAffected} row(s) total.`
150
+ : `[warden] Cleanup complete. Deleted ${totalAffected} row(s) total.`;
151
+ console.log(summary);
152
+ } finally {
153
+ await pool.end().catch(() => {});
154
+ }
155
+
156
+ process.exit(hadError ? 1 : 0);
157
+ }
158
+
159
+ // Run when invoked directly. When required as a module, expose `run` for tests.
160
+ if (require.main === module) {
161
+ run(process.argv.slice(2)).catch((err) => {
162
+ console.error('[warden] cleanup crashed:', err.message);
163
+ process.exit(1);
164
+ });
165
+ }
166
+
167
+ module.exports = { run, parseArgs, buildQueries };
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ // Warden CLI dispatcher.
3
+ //
4
+ // Usage: warden <command> [options]
5
+ //
6
+ // Commands:
7
+ // cleanup [options] Delete old rows from Warden Postgres tables
8
+ // (see `warden cleanup --help`)
9
+ // help Show this message
10
+ // --version, -v Print the Warden version
11
+
12
+ const path = require('path');
13
+ const pkg = require(path.join(__dirname, '..', '..', 'package.json'));
14
+
15
+ const args = process.argv.slice(2);
16
+ const cmd = args[0];
17
+
18
+ function printHelp() {
19
+ process.stdout.write(`Warden CLI v${pkg.version}
20
+
21
+ Usage: warden <command> [options]
22
+
23
+ Commands:
24
+ cleanup [options] Delete old rows from Warden Postgres tables.
25
+ Schedule this from your own cron / Kubernetes CronJob /
26
+ pg_cron / GitHub Actions schedule.
27
+ Run \`warden cleanup --help\` for options.
28
+ help Show this message
29
+ --version, -v Print the Warden version
30
+
31
+ Examples:
32
+ warden cleanup --dry-run
33
+ warden cleanup --reputation-days 7 --requests-hours 48
34
+ CONNECTION_STRING=postgres://user:pass@host/db warden cleanup
35
+
36
+ More info: ${pkg.homepage}
37
+ `);
38
+ }
39
+
40
+ function printVersion() {
41
+ process.stdout.write(`${pkg.version}\n`);
42
+ }
43
+
44
+ switch (cmd) {
45
+ case 'cleanup':
46
+ require('./cleanup').run(args.slice(1));
47
+ break;
48
+ case 'help':
49
+ case '--help':
50
+ case '-h':
51
+ case undefined:
52
+ printHelp();
53
+ break;
54
+ case '--version':
55
+ case '-v':
56
+ printVersion();
57
+ break;
58
+ default:
59
+ console.error(`Unknown command: ${cmd}`);
60
+ console.error(`Run \`warden help\` for usage.`);
61
+ process.exit(2);
62
+ }
@@ -0,0 +1,43 @@
1
+ const { Pool } = require('pg');
2
+
3
+ // Users must set CONNECTION_STRING in their environment before requiring this module.
4
+ // As an npm library, Warden does not call dotenv.config() so it will not overwrite
5
+ // the consumer's environment variables or crash when a .env file is absent.
6
+ //
7
+ // IMPORTANT: The pg.Pool is created LAZILY. Importing this module does not open
8
+ // a DB connection; the pool is only created when getPool() is called (i.e. when
9
+ // a user passes a pool or calls test()).
10
+ //
11
+ // Data retention: Warden does NOT auto-delete rows. Use the CLI:
12
+ // warden cleanup [--reputation-days N] [--requests-hours N] [--sessions-days N] [--dry-run]
13
+ // and run it from your own cron / Kubernetes CronJob / pg_cron.
14
+
15
+ let pool = null;
16
+
17
+ function getPool() {
18
+ if (pool) return pool;
19
+ pool = new Pool({
20
+ connectionString: process.env.CONNECTION_STRING,
21
+ ssl: { rejectUnauthorized: false },
22
+ max: 10,
23
+ idleTimeoutMillis: 30000,
24
+ connectionTimeoutMillis: 2000,
25
+ });
26
+ return pool;
27
+ }
28
+
29
+ async function test() {
30
+ try {
31
+ const p = getPool();
32
+ const res = await p.query('SELECT NOW()');
33
+ return res;
34
+ } catch (err) {
35
+ console.log("DB_ERROR", err);
36
+ throw err;
37
+ }
38
+ }
39
+
40
+ module.exports = {
41
+ get pool() { return getPool(); },
42
+ test,
43
+ };
@@ -0,0 +1,63 @@
1
+ const { pool } = require('./db');
2
+
3
+ async function migrate(db = pool) {
4
+ await Promise.all([
5
+ db.query(`CREATE TABLE IF NOT EXISTS warden_reputation (
6
+ ip_hash VARCHAR(64) PRIMARY KEY,
7
+ score SMALLINT NOT NULL DEFAULT 0,
8
+ last_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
9
+ rdns_verified BOOLEAN DEFAULT NULL,
10
+ rdns_checked_at TIMESTAMPTZ DEFAULT NULL,
11
+ blocked_until TIMESTAMPTZ DEFAULT NULL,
12
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
13
+ )`),
14
+ db.query(`CREATE TABLE IF NOT EXISTS warden_requests (
15
+ id BIGSERIAL PRIMARY KEY,
16
+ ip_hash VARCHAR(64) NOT NULL,
17
+ timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
18
+ path TEXT,
19
+ score_delta SMALLINT,
20
+ action VARCHAR(16),
21
+ reason VARCHAR(32)
22
+ )`),
23
+
24
+ db.query(`CREATE TABLE IF NOT EXISTS warden_sessions (
25
+ session_hash VARCHAR(64) PRIMARY KEY,
26
+ score SMALLINT NOT NULL DEFAULT 0,
27
+ last_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
28
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
29
+ )`),
30
+
31
+ db.query(`CREATE TABLE IF NOT EXISTS warden_verified_ranges (
32
+ key VARCHAR(64) PRIMARY KEY,
33
+ payload JSONB NOT NULL,
34
+ synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
35
+ )`)
36
+ ]);
37
+
38
+ await Promise.all([
39
+ db.query(`CREATE INDEX IF NOT EXISTS idx_reputation_last_seen ON warden_reputation(last_seen)`),
40
+ db.query(`CREATE INDEX IF NOT EXISTS idx_reputation_blocked ON warden_reputation(blocked_until) WHERE blocked_until IS NOT NULL`),
41
+ db.query(`CREATE INDEX IF NOT EXISTS idx_requests_ip_time ON warden_requests(ip_hash, timestamp DESC)`),
42
+ db.query(`CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON warden_requests(timestamp)`),
43
+ db.query(`CREATE INDEX IF NOT EXISTS idx_sessions_last_seen ON warden_sessions(last_seen)`),
44
+ ]);
45
+
46
+ console.log('Migration complete');
47
+ }
48
+
49
+ if (require.main === module) {
50
+ if (!process.env.CONNECTION_STRING) {
51
+ console.error('Error: CONNECTION_STRING environment variable is required.');
52
+ process.exit(1);
53
+ }
54
+
55
+ migrate()
56
+ .then(() => pool.end())
57
+ .catch((err) => {
58
+ console.error('Migration failed:', err);
59
+ process.exit(1);
60
+ });
61
+ }
62
+
63
+ module.exports = { migrate };