@bbloker/sdk 0.1.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 +191 -0
- package/README.md +131 -0
- package/dist/adapters/express.cjs +61 -0
- package/dist/adapters/express.d.cts +34 -0
- package/dist/adapters/express.d.ts +34 -0
- package/dist/adapters/express.js +61 -0
- package/dist/adapters/fastify.cjs +64 -0
- package/dist/adapters/fastify.d.cts +33 -0
- package/dist/adapters/fastify.d.ts +33 -0
- package/dist/adapters/fastify.js +64 -0
- package/dist/adapters/hono.cjs +55 -0
- package/dist/adapters/hono.d.cts +32 -0
- package/dist/adapters/hono.d.ts +32 -0
- package/dist/adapters/hono.js +55 -0
- package/dist/adapters/nextjs.cjs +53 -0
- package/dist/adapters/nextjs.d.cts +34 -0
- package/dist/adapters/nextjs.d.ts +34 -0
- package/dist/adapters/nextjs.js +53 -0
- package/dist/adapters/node.cjs +60 -0
- package/dist/adapters/node.d.cts +25 -0
- package/dist/adapters/node.d.ts +25 -0
- package/dist/adapters/node.js +60 -0
- package/dist/chunk-6P6B2RO7.js +422 -0
- package/dist/chunk-KT5GDIXH.cjs +422 -0
- package/dist/index.cjs +6 -0
- package/dist/index.d.cts +94 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +6 -0
- package/package.json +97 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
// src/data/default-rules.json
|
|
2
|
+
var default_rules_default = {
|
|
3
|
+
version: 1,
|
|
4
|
+
updatedAt: "2026-02-06",
|
|
5
|
+
blockedUAs: [
|
|
6
|
+
"GPTBot",
|
|
7
|
+
"ChatGPT-User",
|
|
8
|
+
"OAI-SearchBot",
|
|
9
|
+
"CCBot",
|
|
10
|
+
"anthropic-ai",
|
|
11
|
+
"ClaudeBot",
|
|
12
|
+
"Claude-Web",
|
|
13
|
+
"Meta-ExternalAgent",
|
|
14
|
+
"Meta-ExternalFetcher",
|
|
15
|
+
"FacebookBot",
|
|
16
|
+
"facebookexternalhit",
|
|
17
|
+
"PerplexityBot",
|
|
18
|
+
"Bytespider",
|
|
19
|
+
"Google-Extended",
|
|
20
|
+
"Applebot-Extended",
|
|
21
|
+
"cohere-ai",
|
|
22
|
+
"Diffbot",
|
|
23
|
+
"ImagesiftBot",
|
|
24
|
+
"Omgilibot",
|
|
25
|
+
"Omgili",
|
|
26
|
+
"YouBot",
|
|
27
|
+
"Amazonbot",
|
|
28
|
+
"AI2Bot",
|
|
29
|
+
"Ai2Bot-Dolma",
|
|
30
|
+
"Scrapy",
|
|
31
|
+
"PetalBot",
|
|
32
|
+
"Semrushbot",
|
|
33
|
+
"AhrefsBot",
|
|
34
|
+
"MJ12bot",
|
|
35
|
+
"DotBot",
|
|
36
|
+
"Seekport",
|
|
37
|
+
"BLEXBot",
|
|
38
|
+
"DataForSeoBot",
|
|
39
|
+
"magpie-crawler",
|
|
40
|
+
"Timpibot",
|
|
41
|
+
"Velenpublicwebcrawler",
|
|
42
|
+
"Webzio-Extended",
|
|
43
|
+
"iaskspider",
|
|
44
|
+
"Kangaroo Bot",
|
|
45
|
+
"img2dataset"
|
|
46
|
+
],
|
|
47
|
+
blockedIPs: [
|
|
48
|
+
"20.15.240.0/20",
|
|
49
|
+
"20.171.206.0/23",
|
|
50
|
+
"40.83.2.0/23",
|
|
51
|
+
"52.230.152.0/21",
|
|
52
|
+
"20.171.207.0/24"
|
|
53
|
+
],
|
|
54
|
+
headerPatterns: [
|
|
55
|
+
{
|
|
56
|
+
name: "accept",
|
|
57
|
+
pattern: "^\\*\\/\\*$",
|
|
58
|
+
weight: 0.3
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "accept-language",
|
|
62
|
+
pattern: "^$",
|
|
63
|
+
weight: 0.5
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "accept-encoding",
|
|
67
|
+
pattern: "^$",
|
|
68
|
+
weight: 0.4
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
anomalyThreshold: 0.7
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/core/logger.ts
|
|
75
|
+
var LEVELS = {
|
|
76
|
+
debug: 0,
|
|
77
|
+
info: 1,
|
|
78
|
+
warn: 2,
|
|
79
|
+
error: 3,
|
|
80
|
+
silent: 4
|
|
81
|
+
};
|
|
82
|
+
var Logger = class {
|
|
83
|
+
level = LEVELS.warn;
|
|
84
|
+
setLevel(level) {
|
|
85
|
+
this.level = LEVELS[level];
|
|
86
|
+
}
|
|
87
|
+
debug(...args) {
|
|
88
|
+
if (this.level <= LEVELS.debug) console.debug("[bbloker]", ...args);
|
|
89
|
+
}
|
|
90
|
+
info(...args) {
|
|
91
|
+
if (this.level <= LEVELS.info) console.info("[bbloker]", ...args);
|
|
92
|
+
}
|
|
93
|
+
warn(...args) {
|
|
94
|
+
if (this.level <= LEVELS.warn) console.warn("[bbloker]", ...args);
|
|
95
|
+
}
|
|
96
|
+
error(...args) {
|
|
97
|
+
if (this.level <= LEVELS.error) console.error("[bbloker]", ...args);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
var logger = new Logger();
|
|
101
|
+
|
|
102
|
+
// src/core/rules.ts
|
|
103
|
+
var RuleManager = class {
|
|
104
|
+
rules;
|
|
105
|
+
apiUrl;
|
|
106
|
+
apiKey;
|
|
107
|
+
syncTimer = null;
|
|
108
|
+
// Pre-compiled UA patterns for fast matching
|
|
109
|
+
uaPatterns = [];
|
|
110
|
+
constructor(config) {
|
|
111
|
+
this.apiUrl = config.apiUrl;
|
|
112
|
+
this.apiKey = config.apiKey;
|
|
113
|
+
this.rules = default_rules_default;
|
|
114
|
+
this.compilePatterns();
|
|
115
|
+
this.sync();
|
|
116
|
+
this.syncTimer = setInterval(() => this.sync(), config.syncInterval);
|
|
117
|
+
if (this.syncTimer?.unref) {
|
|
118
|
+
this.syncTimer.unref();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
compilePatterns() {
|
|
122
|
+
this.uaPatterns = this.rules.blockedUAs.map((ua) => ua.toLowerCase());
|
|
123
|
+
}
|
|
124
|
+
get current() {
|
|
125
|
+
return this.rules;
|
|
126
|
+
}
|
|
127
|
+
/** Check if a User-Agent matches any blocked pattern */
|
|
128
|
+
isBlockedUA(ua) {
|
|
129
|
+
const lower = ua.toLowerCase();
|
|
130
|
+
return this.uaPatterns.some((pattern) => lower.includes(pattern));
|
|
131
|
+
}
|
|
132
|
+
/** Check if an IP is in any blocked CIDR range */
|
|
133
|
+
isBlockedIP(ip) {
|
|
134
|
+
return this.rules.blockedIPs.some((cidr) => ipInCidr(ip, cidr));
|
|
135
|
+
}
|
|
136
|
+
/** Calculate header anomaly score (0-1) */
|
|
137
|
+
headerAnomalyScore(headers) {
|
|
138
|
+
let totalWeight = 0;
|
|
139
|
+
let matchWeight = 0;
|
|
140
|
+
for (const pattern of this.rules.headerPatterns) {
|
|
141
|
+
totalWeight += pattern.weight;
|
|
142
|
+
const value = headers[pattern.name.toLowerCase()] ?? "";
|
|
143
|
+
try {
|
|
144
|
+
if (new RegExp(pattern.pattern).test(value)) {
|
|
145
|
+
matchWeight += pattern.weight;
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return totalWeight > 0 ? matchWeight / totalWeight : 0;
|
|
151
|
+
}
|
|
152
|
+
get anomalyThreshold() {
|
|
153
|
+
return this.rules.anomalyThreshold;
|
|
154
|
+
}
|
|
155
|
+
async sync() {
|
|
156
|
+
try {
|
|
157
|
+
const res = await fetch(`${this.apiUrl}/v1/rules`, {
|
|
158
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
159
|
+
signal: AbortSignal.timeout(5e3)
|
|
160
|
+
});
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
logger.warn(`bbloker: rule sync failed: ${res.status}`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const data = await res.json();
|
|
166
|
+
if (data.version > this.rules.version) {
|
|
167
|
+
this.rules = data;
|
|
168
|
+
this.compilePatterns();
|
|
169
|
+
logger.info(`bbloker: rules updated to v${data.version}`);
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
logger.debug("bbloker: rule sync error (using cached rules)");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
destroy() {
|
|
176
|
+
if (this.syncTimer) {
|
|
177
|
+
clearInterval(this.syncTimer);
|
|
178
|
+
this.syncTimer = null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
function ipToLong(ip) {
|
|
183
|
+
const parts = ip.split(".");
|
|
184
|
+
if (parts.length !== 4) return 0;
|
|
185
|
+
return (parseInt(parts[0], 10) << 24 | parseInt(parts[1], 10) << 16 | parseInt(parts[2], 10) << 8 | parseInt(parts[3], 10)) >>> 0;
|
|
186
|
+
}
|
|
187
|
+
function ipInCidr(ip, cidr) {
|
|
188
|
+
if (ip.includes(":")) return false;
|
|
189
|
+
const [range, bits] = cidr.split("/");
|
|
190
|
+
if (!range || !bits) return false;
|
|
191
|
+
const mask = ~(2 ** (32 - parseInt(bits, 10)) - 1) >>> 0;
|
|
192
|
+
const ipLong = ipToLong(ip);
|
|
193
|
+
const rangeLong = ipToLong(range);
|
|
194
|
+
return (ipLong & mask) === (rangeLong & mask);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/core/rate-limiter.ts
|
|
198
|
+
var RateLimiter = class {
|
|
199
|
+
windows = /* @__PURE__ */ new Map();
|
|
200
|
+
maxRequests;
|
|
201
|
+
windowMs;
|
|
202
|
+
cleanupTimer = null;
|
|
203
|
+
constructor(maxRequests, windowMs) {
|
|
204
|
+
this.maxRequests = maxRequests;
|
|
205
|
+
this.windowMs = windowMs;
|
|
206
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), 6e4);
|
|
207
|
+
if (this.cleanupTimer?.unref) {
|
|
208
|
+
this.cleanupTimer.unref();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** Returns true if the IP has exceeded the rate limit */
|
|
212
|
+
isExceeded(ip) {
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
const window = this.windows.get(ip);
|
|
215
|
+
if (!window || now > window.resetAt) {
|
|
216
|
+
this.windows.set(ip, { count: 1, resetAt: now + this.windowMs });
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
window.count++;
|
|
220
|
+
return window.count > this.maxRequests;
|
|
221
|
+
}
|
|
222
|
+
cleanup() {
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
for (const [ip, window] of this.windows) {
|
|
225
|
+
if (now > window.resetAt) {
|
|
226
|
+
this.windows.delete(ip);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
destroy() {
|
|
231
|
+
if (this.cleanupTimer) {
|
|
232
|
+
clearInterval(this.cleanupTimer);
|
|
233
|
+
this.cleanupTimer = null;
|
|
234
|
+
}
|
|
235
|
+
this.windows.clear();
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// src/core/telemetry.ts
|
|
240
|
+
var Telemetry = class {
|
|
241
|
+
buffer = [];
|
|
242
|
+
apiUrl;
|
|
243
|
+
apiKey;
|
|
244
|
+
maxBuffer;
|
|
245
|
+
flushTimer = null;
|
|
246
|
+
enabled;
|
|
247
|
+
constructor(config) {
|
|
248
|
+
this.apiUrl = config.apiUrl;
|
|
249
|
+
this.apiKey = config.apiKey;
|
|
250
|
+
this.maxBuffer = config.bufferSize;
|
|
251
|
+
this.enabled = config.enabled;
|
|
252
|
+
if (!this.enabled) return;
|
|
253
|
+
this.flushTimer = setInterval(() => this.flush(), config.flushInterval);
|
|
254
|
+
if (this.flushTimer?.unref) {
|
|
255
|
+
this.flushTimer.unref();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
push(fp) {
|
|
259
|
+
if (!this.enabled) return;
|
|
260
|
+
this.buffer.push(fp);
|
|
261
|
+
if (this.buffer.length >= this.maxBuffer) {
|
|
262
|
+
this.flush();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async flush() {
|
|
266
|
+
if (this.buffer.length === 0) return;
|
|
267
|
+
const batch = this.buffer.splice(0);
|
|
268
|
+
try {
|
|
269
|
+
const res = await fetch(`${this.apiUrl}/v1/fingerprints`, {
|
|
270
|
+
method: "POST",
|
|
271
|
+
headers: {
|
|
272
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
273
|
+
"Content-Type": "application/json"
|
|
274
|
+
},
|
|
275
|
+
body: JSON.stringify({ events: batch }),
|
|
276
|
+
signal: AbortSignal.timeout(5e3)
|
|
277
|
+
});
|
|
278
|
+
if (!res.ok) {
|
|
279
|
+
logger.warn(`bbloker: telemetry flush failed: ${res.status}`);
|
|
280
|
+
} else {
|
|
281
|
+
logger.debug(`bbloker: flushed ${batch.length} fingerprints`);
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
logger.debug(`bbloker: telemetry flush error (silent)`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
destroy() {
|
|
288
|
+
if (this.flushTimer) {
|
|
289
|
+
clearInterval(this.flushTimer);
|
|
290
|
+
this.flushTimer = null;
|
|
291
|
+
}
|
|
292
|
+
this.buffer = [];
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// src/core/engine.ts
|
|
297
|
+
var DEFAULTS = {
|
|
298
|
+
apiUrl: "https://api.bbloker.com",
|
|
299
|
+
syncInterval: 3e5,
|
|
300
|
+
flushInterval: 1e4,
|
|
301
|
+
bufferSize: 100,
|
|
302
|
+
telemetry: true,
|
|
303
|
+
rateLimit: 60,
|
|
304
|
+
rateLimitWindow: 6e4,
|
|
305
|
+
logLevel: "warn"
|
|
306
|
+
};
|
|
307
|
+
var Bbloker = class {
|
|
308
|
+
rules;
|
|
309
|
+
rateLimiter;
|
|
310
|
+
telemetry;
|
|
311
|
+
config;
|
|
312
|
+
constructor(config) {
|
|
313
|
+
if (!config.apiKey) {
|
|
314
|
+
throw new Error("bbloker: apiKey is required");
|
|
315
|
+
}
|
|
316
|
+
const apiUrl = config.apiUrl ?? DEFAULTS.apiUrl;
|
|
317
|
+
const syncInterval = config.syncInterval ?? DEFAULTS.syncInterval;
|
|
318
|
+
const flushInterval = config.flushInterval ?? DEFAULTS.flushInterval;
|
|
319
|
+
const bufferSize = config.bufferSize ?? DEFAULTS.bufferSize;
|
|
320
|
+
const telemetryEnabled = config.telemetry ?? DEFAULTS.telemetry;
|
|
321
|
+
const rateLimit = config.rateLimit ?? DEFAULTS.rateLimit;
|
|
322
|
+
const rateLimitWindow = config.rateLimitWindow ?? DEFAULTS.rateLimitWindow;
|
|
323
|
+
const logLevel = config.logLevel ?? DEFAULTS.logLevel;
|
|
324
|
+
this.config = {
|
|
325
|
+
apiKey: config.apiKey,
|
|
326
|
+
apiUrl,
|
|
327
|
+
rateLimit,
|
|
328
|
+
rateLimitWindow,
|
|
329
|
+
logLevel,
|
|
330
|
+
onBlock: config.onBlock
|
|
331
|
+
};
|
|
332
|
+
logger.setLevel(logLevel);
|
|
333
|
+
this.rules = new RuleManager({
|
|
334
|
+
apiUrl,
|
|
335
|
+
apiKey: config.apiKey,
|
|
336
|
+
syncInterval
|
|
337
|
+
});
|
|
338
|
+
this.rateLimiter = new RateLimiter(rateLimit, rateLimitWindow);
|
|
339
|
+
this.telemetry = new Telemetry({
|
|
340
|
+
apiUrl,
|
|
341
|
+
apiKey: config.apiKey,
|
|
342
|
+
flushInterval,
|
|
343
|
+
bufferSize,
|
|
344
|
+
enabled: telemetryEnabled
|
|
345
|
+
});
|
|
346
|
+
logger.info("bbloker: initialized");
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Analyze a normalized request and return a block/allow decision.
|
|
350
|
+
* This is the core method — adapters call this.
|
|
351
|
+
*/
|
|
352
|
+
analyze(req) {
|
|
353
|
+
if (req.userAgent && this.rules.isBlockedUA(req.userAgent)) {
|
|
354
|
+
const decision2 = {
|
|
355
|
+
action: "block",
|
|
356
|
+
reason: "known_bot_ua",
|
|
357
|
+
confidence: 0.95
|
|
358
|
+
};
|
|
359
|
+
this.report(req, decision2);
|
|
360
|
+
return decision2;
|
|
361
|
+
}
|
|
362
|
+
if (req.ip && this.rules.isBlockedIP(req.ip)) {
|
|
363
|
+
const decision2 = {
|
|
364
|
+
action: "block",
|
|
365
|
+
reason: "known_bot_ip",
|
|
366
|
+
confidence: 0.9
|
|
367
|
+
};
|
|
368
|
+
this.report(req, decision2);
|
|
369
|
+
return decision2;
|
|
370
|
+
}
|
|
371
|
+
if (req.ip && this.rateLimiter.isExceeded(req.ip)) {
|
|
372
|
+
const decision2 = {
|
|
373
|
+
action: "block",
|
|
374
|
+
reason: "rate_limit",
|
|
375
|
+
confidence: 0.7
|
|
376
|
+
};
|
|
377
|
+
this.report(req, decision2);
|
|
378
|
+
return decision2;
|
|
379
|
+
}
|
|
380
|
+
const anomalyScore = this.rules.headerAnomalyScore(req.headers);
|
|
381
|
+
if (anomalyScore > this.rules.anomalyThreshold) {
|
|
382
|
+
const decision2 = {
|
|
383
|
+
action: "block",
|
|
384
|
+
reason: "header_anomaly",
|
|
385
|
+
confidence: anomalyScore
|
|
386
|
+
};
|
|
387
|
+
this.report(req, decision2);
|
|
388
|
+
return decision2;
|
|
389
|
+
}
|
|
390
|
+
const decision = { action: "allow" };
|
|
391
|
+
this.report(req, decision);
|
|
392
|
+
return decision;
|
|
393
|
+
}
|
|
394
|
+
report(req, decision) {
|
|
395
|
+
const fp = {
|
|
396
|
+
ip: req.ip,
|
|
397
|
+
userAgent: req.userAgent,
|
|
398
|
+
headerOrder: req.headerNames,
|
|
399
|
+
headers: req.headers,
|
|
400
|
+
path: req.path,
|
|
401
|
+
method: req.method,
|
|
402
|
+
ts: Date.now()
|
|
403
|
+
};
|
|
404
|
+
this.telemetry.push(fp);
|
|
405
|
+
if (decision.action === "block") {
|
|
406
|
+
logger.debug(
|
|
407
|
+
`bbloker: blocked ${req.ip} [${decision.reason}] UA="${req.userAgent.slice(0, 80)}"`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/** Clean up timers. Call when shutting down. */
|
|
412
|
+
destroy() {
|
|
413
|
+
this.rules.destroy();
|
|
414
|
+
this.rateLimiter.destroy();
|
|
415
|
+
this.telemetry.destroy();
|
|
416
|
+
logger.info("bbloker: destroyed");
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
export {
|
|
421
|
+
Bbloker
|
|
422
|
+
};
|