@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.
@@ -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
+ };