@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.
@@ -0,0 +1,16 @@
1
+ const crypto = require("crypto");
2
+
3
+ function headerFingerPrint(req) {
4
+ const headers = req.headers || {};
5
+ const entries = Object.keys(headers)
6
+ .sort()
7
+ .map(k => `${k}:${headers[k]}`)
8
+ .join(',');
9
+ return crypto
10
+ .createHash("sha256")
11
+ .update(entries)
12
+ .digest("hex")
13
+ .slice(0, 16);
14
+ }
15
+
16
+ module.exports = { headerFingerPrint };
@@ -0,0 +1,37 @@
1
+ const { scoreAgent } = require('./scoreUser');
2
+
3
+ function scoreFromHeaders(req) {
4
+ let score = 0;
5
+ const ua = req.headers['user-agent'] || '';
6
+ const acceptEncoding = req.headers['accept-encoding'] || '';
7
+
8
+ const uaScore = scoreAgent(ua, req.headers);
9
+ const fingerPrintScore = Object.keys(req.headers).length === 0 ? 10 : 0;
10
+
11
+
12
+ const isChromium = /Chrome\/([9][0-9]|[1-9][0-9]{2})/i.test(ua); // Chrome 90+
13
+ if (isChromium) {
14
+ if (!req.headers['sec-ch-ua']) {
15
+ score += 40;
16
+ }
17
+ if (!req.headers['sec-fetch-site']) {
18
+ score += 20;
19
+ }
20
+ const hasBrotli = acceptEncoding.includes('br');
21
+ const hasGzip = acceptEncoding.includes('gzip');
22
+ if (!hasBrotli && !hasGzip) {
23
+ score += 10;
24
+ }
25
+ }
26
+
27
+ const secPlatform = req.headers['sec-ch-ua-platform'];
28
+ if (secPlatform) {
29
+ if (ua.includes('Windows') && !secPlatform.includes('Windows')) score += 50;
30
+ if (ua.includes('Macintosh') && !secPlatform.includes('macOS')) score += 50;
31
+ if (ua.includes('Linux') && !secPlatform.includes('Linux')) score += 50;
32
+ }
33
+
34
+ return uaScore + fingerPrintScore + score;
35
+ }
36
+
37
+ module.exports = { scoreFromHeaders };
@@ -0,0 +1,44 @@
1
+ function scoreAgent(UserAgent, headers = {}) {
2
+ const botPatterns = [
3
+ /bot/i, /crawl/i, /spider/i, /scan/i,
4
+ /fetch/i, /slurp/i, /wget/i, /curl/i,
5
+ /python/i, /java/i, /httpclient/i, /axios/i,
6
+ /scrapy/i, /puppeteer/i, /selenium/i, /playwright/i
7
+ ];
8
+
9
+ let score = 0;
10
+
11
+ if (!headers['accept']) score += 30;
12
+ if (!headers['accept-language']) score += 20;
13
+ if (!headers['accept-encoding']) score += 20;
14
+
15
+ if (!UserAgent || UserAgent.length === 0) {
16
+ score += 40;
17
+ return Math.min(score, 100);
18
+ }
19
+ if (UserAgent.length < 20) score += 25;
20
+ if (UserAgent.length > 200) score += 20;
21
+
22
+ for (const pattern of botPatterns) {
23
+ if (pattern.test(UserAgent)) {
24
+ if (/selenium|puppeteer|playwright/i.test(UserAgent)) score += 30;
25
+ else if (/scrapy|crawler/i.test(UserAgent)) score += 25;
26
+ else score += 15;
27
+ return Math.min(score, 100);
28
+ }
29
+ }
30
+
31
+ if (/Mozilla\/5\.0/.test(UserAgent)) {
32
+ if (/Chrome\/\d+|Firefox\/\d+|Safari\/\d+|Edge\/\d+/.test(UserAgent)) {
33
+ return Math.min(score, 100);
34
+ }
35
+ score += 5;
36
+ }
37
+
38
+ if (/^[^a-zA-Z0-9]/.test(UserAgent)) score += 35;
39
+ if (/[\x00-\x1F]/.test(UserAgent)) score += 50;
40
+
41
+ return Math.min(score, 100);
42
+ }
43
+
44
+ module.exports = { scoreAgent };
package/app/store.js ADDED
@@ -0,0 +1,193 @@
1
+ class Store {
2
+ constructor({ maxSize = 100000, sweepIntervalMs = 5 * 60 * 1000 } = {}) {
3
+ this.memorystore = new Map();
4
+ this.redisstore = null;
5
+ this.useredis = false;
6
+ this.maxMapSize = maxSize;
7
+ this.sweepInterval = null;
8
+
9
+ this._startSweep(sweepIntervalMs);
10
+ }
11
+
12
+ _startSweep(intervalMs) {
13
+ if (this.sweepInterval) return;
14
+ this.sweepInterval = setInterval(() => this._sweepExpired(), intervalMs);
15
+ if (this.sweepInterval.unref) this.sweepInterval.unref();
16
+ }
17
+
18
+ _stopSweep() {
19
+ if (this.sweepInterval) {
20
+ clearInterval(this.sweepInterval);
21
+ this.sweepInterval = null;
22
+ }
23
+ }
24
+
25
+ // Walk the map and delete every entry whose expiresAt < now.
26
+ // This is the proactive cleanup that prevents the leak the old
27
+ // "delete on next access" design had.
28
+ _sweepExpired() {
29
+ if (this.useredis) return;
30
+ const now = Date.now();
31
+ let removed = 0;
32
+ for (const [key, entry] of this.memorystore) {
33
+ if (now > entry.expiresAt) {
34
+ this.memorystore.delete(key);
35
+ removed++;
36
+ }
37
+ }
38
+ return removed;
39
+ }
40
+
41
+ // LRU touch: re-insert a key to move it to the end of iteration order.
42
+ // The oldest (least-recently-used) key is `this.memorystore.keys().next().value`.
43
+ _touch(key) {
44
+ if (!this.memorystore.has(key)) return;
45
+ const entry = this.memorystore.get(key);
46
+ this.memorystore.delete(key);
47
+ this.memorystore.set(key, entry);
48
+ }
49
+
50
+ // Evict the least-recently-used entry. Called when at capacity.
51
+ _evictLRU() {
52
+ const oldestKey = this.memorystore.keys().next().value;
53
+ if (oldestKey !== undefined) this.memorystore.delete(oldestKey);
54
+ }
55
+
56
+ async initRedis(url) {
57
+ if (!url) return;
58
+ try {
59
+ const { createClient } = require('redis');
60
+ this.redisstore = createClient({ url });
61
+ this.redisstore.on('error', (err) => {
62
+ console.error('[Redis Error]', err.message);
63
+ this.useredis = false;
64
+ });
65
+ this.redisstore.on('ready', () => {
66
+ this.useredis = true;
67
+ });
68
+ await this.redisstore.connect();
69
+ } catch (err) {
70
+ console.error('[Redis Init Error]', err.message);
71
+ this.useredis = false;
72
+ }
73
+ }
74
+
75
+ useRedis(client) {
76
+ if (!client || typeof client.get !== 'function' || typeof client.set !== 'function') {
77
+ this.useredis = false;
78
+ this.redisstore = null;
79
+ return false;
80
+ }
81
+ this.redisstore = client;
82
+ this.useredis = true;
83
+ return true;
84
+ }
85
+
86
+ async set(key, value, ttlSeconds = 60) {
87
+ if (this.useredis) {
88
+ try {
89
+ const payload = JSON.stringify(value);
90
+ const client = this.redisstore;
91
+ try {
92
+ return await client.set(key, payload, { EX: ttlSeconds });
93
+ } catch (e1) {
94
+ try {
95
+ return await client.setEx(key, ttlSeconds, payload);
96
+ } catch (e2) {
97
+ await client.set(key, payload);
98
+ await client.expire(key, ttlSeconds);
99
+ return true;
100
+ }
101
+ }
102
+ } catch (err) {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ if (this.memorystore.has(key)) this.memorystore.delete(key);
108
+ if (this.memorystore.size >= this.maxMapSize) this._evictLRU();
109
+ this.memorystore.set(key, {
110
+ value,
111
+ expiresAt: Date.now() + (ttlSeconds * 1000)
112
+ });
113
+ return true;
114
+ }
115
+
116
+ async get(key) {
117
+ if (this.useredis) {
118
+ try {
119
+ const raw = await this.redisstore.get(key);
120
+ if (!raw) return null;
121
+ return JSON.parse(raw);
122
+ } catch (err) {
123
+ return null;
124
+ }
125
+ }
126
+
127
+ const entry = this.memorystore.get(key);
128
+ if (!entry) return null;
129
+
130
+ if (Date.now() > entry.expiresAt) {
131
+ this.memorystore.delete(key);
132
+ return null;
133
+ }
134
+
135
+ // Touch to mark this key as recently used
136
+ this._touch(key);
137
+ return entry.value;
138
+ }
139
+
140
+ async delete(key) {
141
+ if (this.useredis) {
142
+ try {
143
+ return await this.redisstore.del(key);
144
+ } catch (err) {
145
+ return null;
146
+ }
147
+ }
148
+ return this.memorystore.delete(key);
149
+ }
150
+
151
+ async increment(key, ttlSeconds = 5) {
152
+ if (this.useredis) {
153
+ try {
154
+ const multi = this.redisstore.multi();
155
+ multi.incr(key);
156
+ multi.expire(key, ttlSeconds);
157
+ const results = await multi.exec();
158
+ return results[0];
159
+ } catch (err) {
160
+ return 1;
161
+ }
162
+ }
163
+
164
+ const now = Date.now();
165
+ const existing = this.memorystore.get(key);
166
+
167
+ if (!existing || now > existing.expiresAt) {
168
+ if (this.memorystore.has(key)) this.memorystore.delete(key);
169
+ if (this.memorystore.size >= this.maxMapSize) this._evictLRU();
170
+ this.memorystore.set(key, { value: 1, expiresAt: now + (ttlSeconds * 1000) });
171
+ return 1;
172
+ }
173
+
174
+ existing.value += 1;
175
+ this._touch(key);
176
+ return existing.value;
177
+ }
178
+
179
+ stats() {
180
+ return {
181
+ backend: this.useredis ? 'redis' : 'memory',
182
+ size: this.useredis ? null : this.memorystore.size,
183
+ maxSize: this.maxMapSize,
184
+ };
185
+ }
186
+
187
+ shutdown() {
188
+ this._stopSweep();
189
+ }
190
+ }
191
+
192
+ const store = new Store();
193
+ module.exports = { store, Store };
@@ -0,0 +1,102 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const net = require('net');
4
+ const os = require('os');
5
+ const { Address6 } = require('ip-address');
6
+
7
+ const RANGES_FILE = path.join(__dirname, 'ranges.json');
8
+ const TMP_RANGES_FILE = path.join(os.tmpdir(), 'warden-ranges.json');
9
+
10
+ let cachedRanges = { ipv4: [], ipv6: [] };
11
+ let cacheSource = 'empty';
12
+
13
+ // Load subnets from cache (in-memory, tmpdir file, or bundled file)
14
+ function loadRanges() {
15
+ try {
16
+ // 1. In-memory cache (synced from sync.js)
17
+ const { getMemoryCache } = require('./sync');
18
+ const fromMemory = getMemoryCache && getMemoryCache();
19
+ if (fromMemory && (fromMemory.ipv4?.length || fromMemory.ipv6?.length)) {
20
+ applyRanges(fromMemory);
21
+ cacheSource = 'memory';
22
+ return;
23
+ }
24
+
25
+ // 2. Filesystem (prefer tmpdir, fall back to bundled file)
26
+ const candidates = [TMP_RANGES_FILE, RANGES_FILE];
27
+ for (const file of candidates) {
28
+ if (fs.existsSync(file)) {
29
+ const raw = fs.readFileSync(file, 'utf8');
30
+ const data = JSON.parse(raw);
31
+ applyRanges(data);
32
+ cacheSource = `file:${file}`;
33
+ return;
34
+ }
35
+ }
36
+
37
+ console.warn('[warden] No verified bot ranges loaded. Run syncVerifiedBots() to populate.');
38
+ } catch (err) {
39
+ console.error('[warden] Failed to load verified bot ranges:', err);
40
+ }
41
+ }
42
+
43
+ function applyRanges(data) {
44
+ cachedRanges.ipv4 = (data.ipv4 || []).map(cidr => {
45
+ const [subnet, bits] = cidr.split('/');
46
+ const bitCount = parseInt(bits, 10);
47
+ const mask = (bitCount === 0) ? 0 : (~(2 ** (32 - bitCount) - 1)) >>> 0;
48
+ return { subnetInt: ip4ToInt(subnet), mask };
49
+ });
50
+
51
+ cachedRanges.ipv6 = data.ipv6 || [];
52
+ }
53
+
54
+ function ip4ToInt(ip) {
55
+ return ip.split('.').reduce((int, oct) => (int << 8) + parseInt(oct, 10), 0) >>> 0;
56
+ }
57
+
58
+ function matchIPv4(ip) {
59
+ const ipInt = ip4ToInt(ip);
60
+ for (let i = 0; i < cachedRanges.ipv4.length; i++) {
61
+ const { subnetInt, mask } = cachedRanges.ipv4[i];
62
+ if ((ipInt & mask) === (subnetInt & mask)) {
63
+ return true;
64
+ }
65
+ }
66
+ return false;
67
+ }
68
+
69
+ function matchIPv6(ip) {
70
+ try {
71
+ const clientAddr = new Address6(ip);
72
+ for (let i = 0; i < cachedRanges.ipv6.length; i++) {
73
+ const rangeAddr = new Address6(cachedRanges.ipv6[i]);
74
+ if (clientAddr.isInSubnet(rangeAddr)) {
75
+ return true;
76
+ }
77
+ }
78
+ } catch (err) {
79
+ return false;
80
+ }
81
+ return false;
82
+ }
83
+
84
+ function isVerifiedBot(ip) {
85
+ if (!ip) return false;
86
+ const version = net.isIP(ip);
87
+ if (version === 4) return matchIPv4(ip);
88
+ if (version === 6) return matchIPv6(ip);
89
+ return false;
90
+ }
91
+
92
+ function getCacheInfo() {
93
+ return {
94
+ source: cacheSource,
95
+ ipv4_count: cachedRanges.ipv4.length,
96
+ ipv6_count: cachedRanges.ipv6.length,
97
+ };
98
+ }
99
+
100
+ loadRanges();
101
+
102
+ module.exports = { isVerifiedBot, reloadRanges: loadRanges, getCacheInfo };
@@ -0,0 +1,220 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const net = require('net');
5
+
6
+ const RANGES_FILE = path.join(__dirname, 'ranges.json');
7
+ const TMP_RANGES_FILE = path.join(os.tmpdir(), 'warden-ranges.json');
8
+
9
+ const SOURCES = {
10
+ google: 'https://developers.google.com/search/apis/ipranges/googlebot.json',
11
+ bing: 'https://www.bing.com/toolbox/bingbot.json'
12
+ };
13
+
14
+ const FETCH_TIMEOUT_MS = 10000;
15
+
16
+ // In-memory cache (always populated; this is the source of truth)
17
+ let memoryCache = null;
18
+
19
+ // Optional external storage (Redis client or DB with set/get)
20
+ let externalStore = null;
21
+
22
+ async function fetchJSON(url) {
23
+ const controller = new AbortController();
24
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
25
+ try {
26
+ const res = await fetch(url, {
27
+ headers: { 'User-Agent': 'Warden-Sync-Engine/1.0' },
28
+ signal: controller.signal
29
+ });
30
+ if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
31
+ return await res.json();
32
+ } catch (err) {
33
+ if (err && err.name === 'AbortError') {
34
+ throw new Error(`Timeout after ${FETCH_TIMEOUT_MS}ms fetching ${url}`);
35
+ }
36
+ throw err;
37
+ } finally {
38
+ clearTimeout(timer);
39
+ }
40
+ }
41
+
42
+ function isValidCIDR(cidr) {
43
+ if (typeof cidr !== 'string') return false;
44
+ const [ip, bits] = cidr.split('/');
45
+ if (!ip || !bits) return false;
46
+
47
+ const ipVersion = net.isIP(ip);
48
+ const bitNum = parseInt(bits, 10);
49
+
50
+ if (ipVersion === 4) return bitNum >= 0 && bitNum <= 32;
51
+ if (ipVersion === 6) return bitNum >= 0 && bitNum <= 128;
52
+ return false;
53
+ }
54
+
55
+ async function persistToExternal(payload) {
56
+ if (!externalStore) return;
57
+ try {
58
+ if (typeof externalStore.set === 'function') {
59
+ const json = JSON.stringify(payload);
60
+ try {
61
+ await externalStore.set('warden:verified_ranges', json, { EX: 86400 });
62
+ } catch (e1) {
63
+ try {
64
+ await externalStore.setEx('warden:verified_ranges', 86400, json);
65
+ } catch (e2) {
66
+ await externalStore.set('warden:verified_ranges', json);
67
+ }
68
+ }
69
+ } else if (typeof externalStore.query === 'function') {
70
+ await externalStore.query(
71
+ `INSERT INTO warden_verified_ranges (key, payload, synced_at)
72
+ VALUES ($1, $2, NOW())
73
+ ON CONFLICT (key) DO UPDATE SET payload = EXCLUDED.payload, synced_at = NOW()`,
74
+ ['verified_ranges', JSON.stringify(payload)]
75
+ );
76
+ }
77
+ } catch (err) {
78
+ console.warn('[warden] Failed to persist ranges to external store:', err.message);
79
+ }
80
+ }
81
+
82
+ async function loadFromExternal() {
83
+ if (!externalStore) return null;
84
+ try {
85
+ if (typeof externalStore.get === 'function') {
86
+ const raw = await externalStore.get('warden:verified_ranges');
87
+ return raw ? JSON.parse(raw) : null;
88
+ } else if (typeof externalStore.query === 'function') {
89
+ const result = await externalStore.query(
90
+ `SELECT payload FROM warden_verified_ranges WHERE key = $1 LIMIT 1`,
91
+ ['verified_ranges']
92
+ );
93
+ const row = result.rows && result.rows[0];
94
+ return row ? JSON.parse(row.payload) : null;
95
+ }
96
+ } catch (err) {
97
+ console.warn('[warden] Failed to load ranges from external store:', err.message);
98
+ return null;
99
+ }
100
+ return null;
101
+ }
102
+
103
+ function setExternalStore(store) {
104
+ externalStore = store || null;
105
+ }
106
+
107
+ function setMemoryCache(payload) {
108
+ memoryCache = payload;
109
+ }
110
+
111
+ function getMemoryCache() {
112
+ return memoryCache;
113
+ }
114
+
115
+ async function syncVerifiedBots({ writeFile = false } = {}) {
116
+ const ipv4 = new Set();
117
+ const ipv6 = new Set();
118
+
119
+ console.log('[warden] Starting verified bot IP sync...');
120
+
121
+ // 1. Sync Googlebot
122
+ try {
123
+ const data = await fetchJSON(SOURCES.google);
124
+ if (Array.isArray(data.prefixes)) {
125
+ for (const entry of data.prefixes) {
126
+ const cidr = entry.ipv4Prefix || entry.ipv6Prefix;
127
+ if (cidr && isValidCIDR(cidr)) {
128
+ if (entry.ipv4Prefix) ipv4.add(cidr);
129
+ else ipv6.add(cidr);
130
+ }
131
+ }
132
+ }
133
+ } catch (err) {
134
+ console.error('[warden] Error syncing Googlebot ranges:', err.message);
135
+ }
136
+
137
+ // 2. Sync Bingbot
138
+ try {
139
+ const data = await fetchJSON(SOURCES.bing);
140
+ if (Array.isArray(data.prefixes)) {
141
+ for (const entry of data.prefixes) {
142
+ const cidr = entry.ipv4Prefix || entry.ipv6Prefix;
143
+ if (cidr && isValidCIDR(cidr)) {
144
+ if (entry.ipv4Prefix) ipv4.add(cidr);
145
+ else ipv6.add(cidr);
146
+ }
147
+ }
148
+ }
149
+ } catch (err) {
150
+ console.error('[warden] Error syncing Bingbot ranges:', err.message);
151
+ }
152
+
153
+ // Fail-safe: Do not overwrite with empty arrays if APIs both failed
154
+ if (ipv4.size === 0 && ipv6.size === 0) {
155
+ console.error('[warden] Sync failed: No valid ranges retrieved. Keeping existing cache.');
156
+ return false;
157
+ }
158
+
159
+ const payload = {
160
+ synced_at: new Date().toISOString(),
161
+ ipv4: Array.from(ipv4),
162
+ ipv6: Array.from(ipv6)
163
+ };
164
+
165
+ // Always populate the in-memory cache
166
+ memoryCache = payload;
167
+
168
+ // Try to persist to external store (Redis/DB) if provided
169
+ await persistToExternal(payload);
170
+
171
+ // Optional: write to filesystem (off by default — serverless/containers have read-only FS)
172
+ if (writeFile) {
173
+ const targetFile = process.env.WARDEN_RANGES_FILE || TMP_RANGES_FILE;
174
+ try {
175
+ fs.writeFileSync(targetFile, JSON.stringify(payload, null, 2));
176
+ console.log(`[warden] Sync complete. Cached ${ipv4.size} IPv4 and ${ipv6.size} IPv6 ranges at ${targetFile}.`);
177
+ } catch (err) {
178
+ console.warn(`[warden] Could not write ranges to ${targetFile}: ${err.message}`);
179
+ }
180
+ } else {
181
+ console.log(`[warden] Sync complete. Loaded ${ipv4.size} IPv4 and ${ipv6.size} IPv6 ranges into memory.`);
182
+ }
183
+
184
+ return true;
185
+ }
186
+
187
+ async function loadInitialRanges({ writeFile = false } = {}) {
188
+ // Priority: external store > filesystem > empty
189
+ const fromExternal = await loadFromExternal();
190
+ if (fromExternal) {
191
+ memoryCache = fromExternal;
192
+ console.log('[warden] Loaded verified bot ranges from external store.');
193
+ return true;
194
+ }
195
+
196
+ // Try filesystem (prefer tmpdir, fall back to bundled file)
197
+ for (const file of [TMP_RANGES_FILE, RANGES_FILE]) {
198
+ try {
199
+ if (fs.existsSync(file)) {
200
+ const raw = fs.readFileSync(file, 'utf8');
201
+ memoryCache = JSON.parse(raw);
202
+ console.log(`[warden] Loaded verified bot ranges from ${file}.`);
203
+ return true;
204
+ }
205
+ } catch (err) {
206
+ // try next location
207
+ }
208
+ }
209
+
210
+ return false;
211
+ }
212
+
213
+ module.exports = {
214
+ syncVerifiedBots,
215
+ loadInitialRanges,
216
+ setExternalStore,
217
+ setMemoryCache,
218
+ getMemoryCache,
219
+ SOURCES,
220
+ };
package/index.js ADDED
@@ -0,0 +1,14 @@
1
+ const { warden } = require('./app/middleware/warden');
2
+ const { migrate } = require('./app/db_connection/migrate');
3
+ const { syncVerifiedBots, SOURCES } = require('./app/verified-bots/sync');
4
+ const { reloadRanges } = require('./app/verified-bots/matcher');
5
+ const { store } = require('./app/store');
6
+
7
+ module.exports = {
8
+ warden,
9
+ migrate,
10
+ syncVerifiedBots,
11
+ reloadRanges,
12
+ SOURCES,
13
+ store,
14
+ };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@hadi_ali/warden",
3
+ "version": "1.0.0",
4
+ "description": "Express middleware that scores requests and blocks bots/scrapers using header heuristics, IP reputation, rate limiting, and brute-force tracking.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "jest",
8
+ "nodemon": "nodemon",
9
+ "migrate": "node ./app/db_connection/migrate.js"
10
+ },
11
+ "keywords": [
12
+ "middleware",
13
+ "express",
14
+ "security",
15
+ "antibot",
16
+ "antispam",
17
+ "scoring",
18
+ "rate-limit",
19
+ "brute-force",
20
+ "bot-detection",
21
+ "waf"
22
+ ],
23
+ "author": "hadiali",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/hadiali/warden.git"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/hadiali/warden/issues"
31
+ },
32
+ "homepage": "https://github.com/hadiali/warden#readme",
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "bin": {
37
+ "warden": "app/cli/index.js"
38
+ },
39
+ "peerDependencies": {
40
+ "express": "^4.0.0 || ^5.0.0"
41
+ },
42
+ "dependencies": {
43
+ "ip-address": "^10.2.0"
44
+ },
45
+ "optionalDependencies": {
46
+ "pg": "^8.21.0",
47
+ "redis": "^6.0.0"
48
+ },
49
+ "devDependencies": {
50
+ "dotenv": "^17.4.2",
51
+ "express": "^5.2.1",
52
+ "jest": "^30.4.2",
53
+ "nodemon": "^3.1.14",
54
+ "source-map-support": "^0.5.21"
55
+ }
56
+ }