biofirewall 1.0.0 → 2.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/client.js ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * BioFirewall Client - HTTP client for central API
3
+ */
4
+
5
+ const axios = require('axios');
6
+
7
+ class BioFirewallClient {
8
+ constructor(apiUrl, options = {}) {
9
+ this.apiUrl = apiUrl.replace(/\/$/, ''); // Remove trailing slash
10
+ this.timeout = options.timeout || 5000;
11
+ this.cacheTokens = options.cacheTokens !== false;
12
+ this.cacheTTL = options.cacheTTL || 30000; // 30 seconds
13
+
14
+ // Token cache
15
+ this.tokenCache = new Map();
16
+
17
+ // HTTP client
18
+ this.http = axios.create({
19
+ baseURL: this.apiUrl,
20
+ timeout: this.timeout,
21
+ headers: {
22
+ 'Content-Type': 'application/json',
23
+ 'User-Agent': 'BioFirewallClient/3.0'
24
+ }
25
+ });
26
+ }
27
+
28
+ /**
29
+ * Verify token with central API
30
+ * Caches result to reduce API calls
31
+ */
32
+ async verifyToken(agentId, token) {
33
+ const cacheKey = `${agentId}:${token}`;
34
+
35
+ // Check cache
36
+ if (this.cacheTokens && this.tokenCache.has(cacheKey)) {
37
+ const cached = this.tokenCache.get(cacheKey);
38
+ if (Date.now() - cached.timestamp < this.cacheTTL) {
39
+ return cached.data;
40
+ }
41
+ }
42
+
43
+ try {
44
+ // Call central API
45
+ const response = await this.http.post('/verify', {
46
+ agentId,
47
+ token
48
+ });
49
+
50
+ const result = response.data;
51
+
52
+ // Cache the result
53
+ if (this.cacheTokens) {
54
+ this.tokenCache.set(cacheKey, {
55
+ data: result,
56
+ timestamp: Date.now()
57
+ });
58
+ }
59
+
60
+ return result;
61
+
62
+ } catch (error) {
63
+ // Return error response
64
+ const errorResponse = error.response?.data || {
65
+ success: false,
66
+ valid: false,
67
+ error: 'API_ERROR',
68
+ message: error.message
69
+ };
70
+
71
+ return errorResponse;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Get agent info from central API
77
+ */
78
+ async getAgent(agentId) {
79
+ try {
80
+ const response = await this.http.get(`/agents/${agentId}`);
81
+ return response.data;
82
+ } catch (error) {
83
+ return {
84
+ success: false,
85
+ error: error.response?.data?.error || 'API_ERROR',
86
+ message: error.message
87
+ };
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Get global statistics
93
+ */
94
+ async getStats() {
95
+ try {
96
+ const response = await this.http.get('/agents/stats/global');
97
+ return response.data;
98
+ } catch (error) {
99
+ return {
100
+ success: false,
101
+ error: 'API_ERROR',
102
+ message: error.message
103
+ };
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Health check
109
+ */
110
+ async health() {
111
+ try {
112
+ const response = await this.http.get('/health');
113
+ return response.data;
114
+ } catch (error) {
115
+ return {
116
+ status: 'unhealthy',
117
+ error: error.message
118
+ };
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Clear token cache
124
+ */
125
+ clearCache() {
126
+ this.tokenCache.clear();
127
+ }
128
+
129
+ /**
130
+ * Clear specific token from cache
131
+ */
132
+ clearCacheEntry(agentId, token) {
133
+ const cacheKey = `${agentId}:${token}`;
134
+ this.tokenCache.delete(cacheKey);
135
+ }
136
+ }
137
+
138
+ module.exports = BioFirewallClient;
package/index.js CHANGED
@@ -1,11 +1,25 @@
1
- const BioFirewall = require('./lib/biofirewall');
2
- const challenge = require('./lib/challenge');
1
+ /**
2
+ * BioFirewall Client Module
3
+ * Express middleware for agent authentication
4
+ */
3
5
 
4
- // Export the main class as default
5
- module.exports = BioFirewall;
6
+ const createMiddleware = require('./middleware');
7
+ const BioFirewallClient = require('./client');
8
+
9
+ /**
10
+ * Main export: Express middleware factory
11
+ */
12
+ function BioFirewall(options) {
13
+ // If options is a string, treat it as apiUrl
14
+ if (typeof options === 'string') {
15
+ options = { apiUrl: options };
16
+ }
6
17
 
7
- // Export helper for clients who need to solve the puzzle
8
- module.exports.solve = challenge.solve;
18
+ return createMiddleware(options);
19
+ }
9
20
 
10
- // Export middleware directly if preferred
11
- module.exports.middleware = (options) => new BioFirewall(options).middleware();
21
+ // Export components
22
+ BioFirewall.createMiddleware = createMiddleware;
23
+ BioFirewall.Client = BioFirewallClient;
24
+
25
+ module.exports = BioFirewall;
package/middleware.js ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * BioFirewall Client Middleware
3
+ * Express middleware to protect routes with agent authentication
4
+ */
5
+
6
+ const BioFirewallClient = require('./client');
7
+ const rules = require('./rules');
8
+
9
+ /**
10
+ * Create BioFirewall middleware
11
+ */
12
+ function createMiddleware(options = {}) {
13
+ const {
14
+ apiUrl,
15
+ blockBrowsers = true,
16
+ enforceAuthentication = true,
17
+ cacheTokens = true,
18
+ cacheTTL = 30000
19
+ } = options;
20
+
21
+ if (!apiUrl) {
22
+ throw new Error('apiUrl is required');
23
+ }
24
+
25
+ // Create client
26
+ const client = new BioFirewallClient(apiUrl, {
27
+ cacheTokens,
28
+ cacheTTL
29
+ });
30
+
31
+ // Return middleware function
32
+ return async (req, res, next) => {
33
+ try {
34
+ // Layer 1: Passive filtering (User-Agent detection)
35
+ if (blockBrowsers && rules.isHuman(req)) {
36
+ return res.status(406).json({
37
+ success: false,
38
+ error: 'BIOLOGICAL_ENTITY_DETECTED',
39
+ message: 'This resource is reserved for automated agents',
40
+ tip: 'Use an API client. Ensure User-Agent does not look like a browser'
41
+ });
42
+ }
43
+
44
+ // Layer 2: Authentication
45
+ if (enforceAuthentication) {
46
+ const agentId = req.headers['x-bio-agent-id'];
47
+ const token = req.headers['x-bio-token'];
48
+
49
+ // Check headers present
50
+ if (!rules.hasValidAgentHeaders(req)) {
51
+ return res.status(428).json({
52
+ success: false,
53
+ error: 'AUTHENTICATION_REQUIRED',
54
+ message: 'Valid agent token required to access this resource',
55
+ protocol: {
56
+ version: '3.0',
57
+ method: 'JWT + RS256',
58
+ headers: {
59
+ 'X-Bio-Agent-Id': 'Your agent ID from registration',
60
+ 'X-Bio-Token': 'JWT token signed with your private key'
61
+ }
62
+ },
63
+ documentation: 'https://github.com/openclaw/biofirewall',
64
+ hint: 'Register your agent first at POST /register'
65
+ });
66
+ }
67
+
68
+ // Verify token with central API
69
+ const verification = await client.verifyToken(agentId, token);
70
+
71
+ if (!verification.valid) {
72
+ const statusCode = verification.error === 'AGENT_NOT_FOUND' ? 428 :
73
+ verification.error === 'AGENT_NOT_ACTIVE' ? 403 :
74
+ 401;
75
+
76
+ return res.status(statusCode).json({
77
+ success: false,
78
+ error: verification.error,
79
+ message: verification.message,
80
+ hint: verification.error === 'AGENT_NOT_FOUND'
81
+ ? 'Register at POST /register'
82
+ : 'Check your token and try again'
83
+ });
84
+ }
85
+
86
+ // Attach agent info to request
87
+ req.agent = verification.agent;
88
+
89
+ // Add response headers
90
+ res.set({
91
+ 'X-Bio-Verified': 'true',
92
+ 'X-Bio-Agent-Name': verification.agent.name,
93
+ 'X-Bio-Version': '3.0'
94
+ });
95
+ }
96
+
97
+ next();
98
+
99
+ } catch (error) {
100
+ console.error('BioFirewall middleware error:', error);
101
+
102
+ res.status(500).json({
103
+ success: false,
104
+ error: 'INTERNAL_ERROR',
105
+ message: 'Authentication system error'
106
+ });
107
+ }
108
+ };
109
+ }
110
+
111
+ module.exports = createMiddleware;
package/package.json CHANGED
@@ -1,26 +1,30 @@
1
1
  {
2
2
  "name": "biofirewall",
3
- "version": "1.0.0",
4
- "description": "Anti-Human Firewall: Block browsers, allow bots via Proof-of-Work to verify silicon heritage.",
3
+ "version": "2.0.0",
4
+ "description": "BioFirewall Client - Express middleware to protect routes with agent authentication",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1",
8
- "start": "node examples/server.js",
9
- "demo:bot": "node examples/bot.js"
7
+ "test": "jest"
10
8
  },
11
9
  "keywords": [
12
- "anti-human",
13
- "antibot",
14
- "reverse-captcha",
15
- "pow",
16
- "express",
10
+ "biofirewall",
11
+ "client",
17
12
  "middleware",
18
- "openclaw"
13
+ "express",
14
+ "authentication",
15
+ "agents"
19
16
  ],
20
17
  "author": "OpenClaw Community",
21
18
  "license": "MIT",
22
19
  "dependencies": {
23
- "axios": "^1.13.4",
24
- "express": "^4.21.2"
20
+ "axios": "^1.6.0",
21
+ "express": "^4.18.2"
22
+ },
23
+ "devDependencies": {
24
+ "jest": "^29.7.0",
25
+ "supertest": "^6.3.3"
26
+ },
27
+ "peerDependencies": {
28
+ "express": "^4.0.0"
25
29
  }
26
30
  }
package/rules.js ADDED
@@ -0,0 +1,100 @@
1
+ /**
2
+ * BioFirewall Client - Security Rules
3
+ * User-Agent validation (local, no API calls needed)
4
+ */
5
+
6
+ // Patterns that indicate a human browser
7
+ const BROWSER_PATTERNS = [
8
+ /mozilla.*firefox/i,
9
+ /mozilla.*safari.*chrome/i, // Chrome
10
+ /mozilla.*safari(?!.*linux)/i, // Safari (not Linux)
11
+ /mozilla.*edge/i, // Edge
12
+ /mozilla.*trident.*rv:11/i, // IE 11
13
+ /mozilla.*applewebkit/i, // WebKit-based
14
+ /opr\//i, // Opera
15
+ ];
16
+
17
+ // Patterns that indicate a legitimate bot/agent
18
+ const BOT_PATTERNS = [
19
+ /bot/i,
20
+ /agent/i,
21
+ /crawler/i,
22
+ /curl/i,
23
+ /wget/i,
24
+ /python/i,
25
+ /java(?!script)/i,
26
+ /axios/i,
27
+ /node/i,
28
+ /go-http-client/i,
29
+ ];
30
+
31
+ module.exports = {
32
+ /**
33
+ * Check if request appears to be from a human (browser)
34
+ */
35
+ isHuman: (req) => {
36
+ const userAgent = (req.get('User-Agent') || '').trim();
37
+ const accept = (req.get('Accept') || '').trim();
38
+ const acceptLanguage = req.get('Accept-Language');
39
+
40
+ // Rule 1: Accept header containing text/html = human
41
+ if (accept.includes('text/html')) {
42
+ return true;
43
+ }
44
+
45
+ // Rule 2: Accept-Language header = human trait
46
+ if (acceptLanguage) {
47
+ return true;
48
+ }
49
+
50
+ // Rule 3: No User-Agent = suspicious but check other traits
51
+ if (!userAgent) {
52
+ return !this._isBotUser(req);
53
+ }
54
+
55
+ // Rule 4: Check against browser patterns
56
+ const isBrowserLike = BROWSER_PATTERNS.some(pattern => pattern.test(userAgent));
57
+
58
+ if (isBrowserLike) {
59
+ // Could still be a bot pretending to be a browser
60
+ if (this._isBotUser(req)) {
61
+ return false;
62
+ }
63
+ return true;
64
+ }
65
+
66
+ // Rule 5: Check if it matches legitimate bot patterns
67
+ const isBot = BOT_PATTERNS.some(pattern => pattern.test(userAgent));
68
+
69
+ if (isBot) {
70
+ return false;
71
+ }
72
+
73
+ // Default: DENY (prefer blocking a bot over allowing a human)
74
+ return true;
75
+ },
76
+
77
+ /**
78
+ * Helper: Check if request has bot-like characteristics
79
+ */
80
+ _isBotUser: (req) => {
81
+ const userAgent = (req.get('User-Agent') || '').toLowerCase();
82
+ const accept = (req.get('Accept') || '').toLowerCase();
83
+
84
+ const asksForJson = accept.includes('application/json');
85
+ const asksForHtml = accept.includes('text/html');
86
+ const hasProperUA = BOT_PATTERNS.some(pattern => pattern.test(userAgent));
87
+
88
+ return (asksForJson || hasProperUA) && !asksForHtml;
89
+ },
90
+
91
+ /**
92
+ * Check if request has valid agent authentication headers
93
+ */
94
+ hasValidAgentHeaders: (req) => {
95
+ const token = req.headers['x-bio-token'];
96
+ const agentId = req.headers['x-bio-agent-id'];
97
+
98
+ return !!(token && agentId);
99
+ }
100
+ };
@@ -1,75 +0,0 @@
1
- const rules = require('./rules');
2
- const challenge = require('./challenge');
3
-
4
- class BioFirewall {
5
- constructor(options = {}) {
6
- this.options = {
7
- blockBrowsers: true,
8
- enforceChallenge: true,
9
- challengeDifficulty: 3, // Number of leading zeros required
10
- ...options
11
- };
12
- }
13
-
14
- middleware() {
15
- return (req, res, next) => {
16
- // 1. Passive Filtering (The Silicon Curtain)
17
- if (this.options.blockBrowsers) {
18
- if (rules.isHuman(req)) {
19
- return res.status(406).type('application/json').json({
20
- error: "BIOLOGICAL_ENTITY_DETECTED",
21
- message: "This resource is reserved for automated agents.",
22
- tip: "Use an API client or disable 'human' headers."
23
- });
24
- }
25
- }
26
-
27
- // 2. Active Filtering (The Computational Toll)
28
- if (this.options.enforceChallenge) {
29
- const solution = req.headers['x-bio-solution'];
30
-
31
- if (!solution) {
32
- return this.sendChallenge(res);
33
- }
34
-
35
- // Verify specific challenge solution
36
- // In a real/stateless app, we might sign the seed.
37
- // For this demo, we verify the solution satisfies the generic work requirement.
38
- // To be fully secure, the 'seed' should be verified as one we issued recently.
39
- const originalSeed = req.headers['x-bio-challenge-seed']; // Client passes back the seed they solved for
40
-
41
- if (!originalSeed || !challenge.verify(originalSeed, solution, this.options.challengeDifficulty)) {
42
- return res.status(403).json({
43
- error: "INVALID_COMPUTATION",
44
- message: "Proof of work failed or insufficient difficulty."
45
- });
46
- }
47
- }
48
-
49
- next();
50
- };
51
- }
52
-
53
- sendChallenge(res) {
54
- const seed = challenge.generateSeed();
55
- const difficulty = this.options.challengeDifficulty;
56
-
57
- // 428 Precondition Required
58
- res.status(428).set({
59
- 'X-Bio-Challenge-Algo': 'sha256',
60
- 'X-Bio-Challenge-Difficulty': difficulty,
61
- 'X-Bio-Challenge-Seed': seed
62
- }).json({
63
- error: "COMPUTATION_REQUIRED",
64
- message: "Solve the puzzle to prove silicon heritage.",
65
- challenge: {
66
- algo: "sha256",
67
- seed: seed,
68
- difficulty: difficulty,
69
- instruction: `Find a nonce where sha256(seed + nonce) starts with '${'0'.repeat(difficulty)}'`
70
- }
71
- });
72
- }
73
- }
74
-
75
- module.exports = BioFirewall;
package/lib/challenge.js DELETED
@@ -1,28 +0,0 @@
1
- const crypto = require('crypto');
2
-
3
- module.exports = {
4
- generateSeed: () => {
5
- return crypto.randomBytes(16).toString('hex');
6
- },
7
-
8
- verify: (seed, nonce, difficulty) => {
9
- const hash = crypto.createHash('sha256').update(seed + nonce).digest('hex');
10
- const prefix = '0'.repeat(difficulty);
11
- return hash.startsWith(prefix);
12
- },
13
-
14
- // Helper for bots (exported so bots can use this alg)
15
- solve: (seed, difficulty) => {
16
- const prefix = '0'.repeat(difficulty);
17
- let nonce = 0;
18
- while (true) {
19
- const hash = crypto.createHash('sha256').update(seed + String(nonce)).digest('hex');
20
- if (hash.startsWith(prefix)) {
21
- return String(nonce);
22
- }
23
- nonce++;
24
- // Safety break for testing to avoid infinite loop if difficulty is crazy high
25
- if (nonce > 10000000) return null;
26
- }
27
- }
28
- };
package/lib/rules.js DELETED
@@ -1,31 +0,0 @@
1
- module.exports = {
2
- isHuman: (req) => {
3
- const userAgent = req.get('User-Agent') || '';
4
- const accept = req.get('Accept') || '';
5
-
6
- // Rule 1: Reject common Browser User-Agents
7
- const browserKeywords = ['Mozilla', 'Chrome', 'Safari', 'Edge', 'Firefox', 'AppleWebKit'];
8
- const isBrowserAgent = browserKeywords.some(keyword => userAgent.includes(keyword));
9
-
10
- // Rule 2: Humans usually ask for HTML
11
- const asksForHtml = accept.includes('text/html');
12
-
13
- // Rule 3: Missing User-Agent is suspicious for humans?
14
- // Actually bots often have no UA or proper UA.
15
- // Humans almost ALWAYS have a sophisticated UA string.
16
-
17
- if (isBrowserAgent) {
18
- // Allow override if they explicitly identify as a bot in UA?
19
- // For this specific 'anti-human' firewall, if it looks like a browser, kill it.
20
- if (!userAgent.toLowerCase().includes('bot') && !userAgent.toLowerCase().includes('openclaw')) {
21
- return true;
22
- }
23
- }
24
-
25
- if (asksForHtml) {
26
- return true;
27
- }
28
-
29
- return false;
30
- }
31
- };