@dupecom/botcha 0.13.1 → 0.15.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/README.md +48 -2
- package/dist/lib/client/index.d.ts +64 -2
- package/dist/lib/client/index.d.ts.map +1 -1
- package/dist/lib/client/index.js +136 -1
- package/dist/lib/client/types.d.ts +68 -0
- package/dist/lib/client/types.d.ts.map +1 -1
- package/dist/lib/index.js +2 -0
- package/dist/src/challenges/compute.d.ts +19 -0
- package/dist/src/challenges/compute.d.ts.map +1 -0
- package/dist/src/challenges/compute.js +88 -0
- package/dist/src/challenges/hybrid.d.ts +45 -0
- package/dist/src/challenges/hybrid.d.ts.map +1 -0
- package/dist/src/challenges/hybrid.js +94 -0
- package/dist/src/challenges/reasoning.d.ts +29 -0
- package/dist/src/challenges/reasoning.d.ts.map +1 -0
- package/dist/src/challenges/reasoning.js +414 -0
- package/dist/src/challenges/speed.d.ts +34 -0
- package/dist/src/challenges/speed.d.ts.map +1 -0
- package/dist/src/challenges/speed.js +115 -0
- package/dist/src/middleware/tap-enhanced-verify.d.ts +57 -0
- package/dist/src/middleware/tap-enhanced-verify.d.ts.map +1 -0
- package/dist/src/middleware/tap-enhanced-verify.js +368 -0
- package/dist/src/middleware/verify.d.ts +12 -0
- package/dist/src/middleware/verify.d.ts.map +1 -0
- package/dist/src/middleware/verify.js +141 -0
- package/dist/src/utils/badge-image.d.ts +15 -0
- package/dist/src/utils/badge-image.d.ts.map +1 -0
- package/dist/src/utils/badge-image.js +253 -0
- package/dist/src/utils/badge.d.ts +39 -0
- package/dist/src/utils/badge.d.ts.map +1 -0
- package/dist/src/utils/badge.js +125 -0
- package/dist/src/utils/signature.d.ts +23 -0
- package/dist/src/utils/signature.d.ts.map +1 -0
- package/dist/src/utils/signature.js +160 -0
- package/package.json +6 -1
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TAP-Enhanced BOTCHA Verification Middleware
|
|
3
|
+
* Extends existing BOTCHA middleware with Trusted Agent Protocol support
|
|
4
|
+
*
|
|
5
|
+
* Provides multiple verification modes:
|
|
6
|
+
* - TAP (full): Cryptographic signature + computational challenge
|
|
7
|
+
* - Signature-only: Cryptographic verification without challenge
|
|
8
|
+
* - Challenge-only: Existing BOTCHA computational challenge
|
|
9
|
+
* - Flexible: TAP preferred but allows fallback
|
|
10
|
+
*/
|
|
11
|
+
import { generateSpeedChallenge, verifySpeedChallenge } from '../challenges/speed.js';
|
|
12
|
+
import { verifyHTTPMessageSignature, parseTAPIntent, extractTAPHeaders, getVerificationMode, buildTAPChallengeResponse } from '../../packages/cloudflare-workers/src/tap-verify.js';
|
|
13
|
+
import { getTAPAgent, updateAgentVerification, createTAPSession } from '../../packages/cloudflare-workers/src/tap-agents.js';
|
|
14
|
+
const defaultTAPOptions = {
|
|
15
|
+
// BOTCHA defaults
|
|
16
|
+
requireSignature: false,
|
|
17
|
+
allowChallenge: true,
|
|
18
|
+
challengeType: 'speed',
|
|
19
|
+
challengeDifficulty: 'medium',
|
|
20
|
+
// TAP defaults
|
|
21
|
+
requireTAP: false,
|
|
22
|
+
preferTAP: true,
|
|
23
|
+
tapEnabled: true,
|
|
24
|
+
auditLogging: false,
|
|
25
|
+
trustedIssuers: ['openclaw.ai', 'anthropic.com', 'openai.com'],
|
|
26
|
+
maxSessionDuration: 3600,
|
|
27
|
+
signatureAlgorithms: ['ecdsa-p256-sha256', 'rsa-pss-sha256'],
|
|
28
|
+
requireCapabilities: []
|
|
29
|
+
};
|
|
30
|
+
// ============ MAIN MIDDLEWARE ============
|
|
31
|
+
/**
|
|
32
|
+
* Enhanced BOTCHA middleware with TAP support
|
|
33
|
+
*/
|
|
34
|
+
export function tapEnhancedVerify(options = {}) {
|
|
35
|
+
const opts = { ...defaultTAPOptions, ...options };
|
|
36
|
+
return async (req, res, next) => {
|
|
37
|
+
const startTime = Date.now();
|
|
38
|
+
try {
|
|
39
|
+
// Determine verification approach
|
|
40
|
+
const { mode, hasTAPHeaders, hasChallenge } = getVerificationMode(req.headers);
|
|
41
|
+
let result;
|
|
42
|
+
// Route to appropriate verification method
|
|
43
|
+
if (mode === 'tap' && opts.tapEnabled) {
|
|
44
|
+
result = await performFullTAPVerification(req, opts);
|
|
45
|
+
}
|
|
46
|
+
else if (mode === 'signature-only' && opts.tapEnabled) {
|
|
47
|
+
result = await performSignatureOnlyVerification(req, opts);
|
|
48
|
+
}
|
|
49
|
+
else if (mode === 'challenge-only') {
|
|
50
|
+
result = await performChallengeOnlyVerification(req, opts);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// Fallback or error case
|
|
54
|
+
result = await handleVerificationFallback(req, opts, mode);
|
|
55
|
+
}
|
|
56
|
+
// Custom verification hook
|
|
57
|
+
if (opts.customVerify && result.verified) {
|
|
58
|
+
const customResult = await opts.customVerify(req);
|
|
59
|
+
if (!customResult) {
|
|
60
|
+
result.verified = false;
|
|
61
|
+
result.error = 'Custom verification failed';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Audit logging
|
|
65
|
+
if (opts.auditLogging) {
|
|
66
|
+
logVerificationAttempt(req, result, Date.now() - startTime);
|
|
67
|
+
}
|
|
68
|
+
// Handle successful verification
|
|
69
|
+
if (result.verified) {
|
|
70
|
+
// Attach verification context to request
|
|
71
|
+
req.tapAgent = result.agent_id ? await getTAPAgentById(opts.agentsKV, result.agent_id) : null;
|
|
72
|
+
req.verificationMethod = result.verification_method;
|
|
73
|
+
req.tapSession = result.session_id;
|
|
74
|
+
req.challengesPassed = result.challenges_passed;
|
|
75
|
+
req.verificationDuration = Date.now() - startTime;
|
|
76
|
+
return next();
|
|
77
|
+
}
|
|
78
|
+
// Handle verification failure
|
|
79
|
+
return sendVerificationChallenge(res, result, opts);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
console.error('TAP verification error:', error);
|
|
83
|
+
if (opts.auditLogging) {
|
|
84
|
+
console.log('TAP_VERIFICATION_ERROR', {
|
|
85
|
+
error: error instanceof Error ? error.message : String(error),
|
|
86
|
+
path: req.path,
|
|
87
|
+
method: req.method,
|
|
88
|
+
timestamp: new Date().toISOString()
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return res.status(500).json({
|
|
92
|
+
success: false,
|
|
93
|
+
error: 'VERIFICATION_ERROR',
|
|
94
|
+
message: 'Internal verification error'
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// ============ VERIFICATION METHODS ============
|
|
100
|
+
/**
|
|
101
|
+
* Full TAP verification (crypto + computational)
|
|
102
|
+
*/
|
|
103
|
+
async function performFullTAPVerification(req, opts) {
|
|
104
|
+
const { tapHeaders } = extractTAPHeaders(req.headers);
|
|
105
|
+
if (!tapHeaders['x-tap-agent-id'] || !opts.agentsKV) {
|
|
106
|
+
return {
|
|
107
|
+
verified: false,
|
|
108
|
+
verification_method: 'tap',
|
|
109
|
+
challenges_passed: { computational: false, cryptographic: false },
|
|
110
|
+
error: 'Missing TAP agent ID or storage not configured'
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Get agent from registry
|
|
114
|
+
const agentResult = await getTAPAgent(opts.agentsKV, tapHeaders['x-tap-agent-id']);
|
|
115
|
+
if (!agentResult.success || !agentResult.agent) {
|
|
116
|
+
return {
|
|
117
|
+
verified: false,
|
|
118
|
+
verification_method: 'tap',
|
|
119
|
+
challenges_passed: { computational: false, cryptographic: false },
|
|
120
|
+
error: `Agent ${tapHeaders['x-tap-agent-id']} not found`
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const agent = agentResult.agent;
|
|
124
|
+
// Verify cryptographic signature
|
|
125
|
+
const cryptoResult = await verifyCryptographicSignature(req, agent);
|
|
126
|
+
// Verify computational challenge
|
|
127
|
+
const challengeResult = await verifyComputationalChallenge(req);
|
|
128
|
+
// Both must pass for full TAP
|
|
129
|
+
const verified = cryptoResult.valid && challengeResult.valid;
|
|
130
|
+
// Update agent verification timestamp
|
|
131
|
+
if (opts.agentsKV) {
|
|
132
|
+
await updateAgentVerification(opts.agentsKV, agent.agent_id, verified);
|
|
133
|
+
}
|
|
134
|
+
// Create session if successful
|
|
135
|
+
let sessionId;
|
|
136
|
+
if (verified && opts.sessionsKV && tapHeaders['x-tap-intent'] && tapHeaders['x-tap-user-context']) {
|
|
137
|
+
const intentResult = parseTAPIntent(tapHeaders['x-tap-intent']);
|
|
138
|
+
if (intentResult.valid && intentResult.intent) {
|
|
139
|
+
const sessionResult = await createTAPSession(opts.sessionsKV, agent.agent_id, agent.app_id, tapHeaders['x-tap-user-context'], agent.capabilities || [], intentResult.intent);
|
|
140
|
+
if (sessionResult.success) {
|
|
141
|
+
sessionId = sessionResult.session?.session_id;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
verified,
|
|
147
|
+
agent_id: agent.agent_id,
|
|
148
|
+
verification_method: 'tap',
|
|
149
|
+
challenges_passed: {
|
|
150
|
+
computational: challengeResult.valid,
|
|
151
|
+
cryptographic: cryptoResult.valid
|
|
152
|
+
},
|
|
153
|
+
session_id: sessionId,
|
|
154
|
+
error: verified ? undefined : `Crypto: ${cryptoResult.error || 'OK'}, Challenge: ${challengeResult.error || 'OK'}`,
|
|
155
|
+
metadata: {
|
|
156
|
+
solve_time_ms: challengeResult.solveTimeMs,
|
|
157
|
+
signature_valid: cryptoResult.valid,
|
|
158
|
+
capabilities: agent.capabilities?.map((c) => c.action)
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Signature-only verification
|
|
164
|
+
*/
|
|
165
|
+
async function performSignatureOnlyVerification(req, opts) {
|
|
166
|
+
const { tapHeaders } = extractTAPHeaders(req.headers);
|
|
167
|
+
if (!tapHeaders['x-tap-agent-id'] || !opts.agentsKV) {
|
|
168
|
+
return {
|
|
169
|
+
verified: false,
|
|
170
|
+
verification_method: 'signature-only',
|
|
171
|
+
challenges_passed: { computational: false, cryptographic: false },
|
|
172
|
+
error: 'Missing TAP agent ID'
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
// Get agent from registry
|
|
176
|
+
const agentResult = await getTAPAgent(opts.agentsKV, tapHeaders['x-tap-agent-id']);
|
|
177
|
+
if (!agentResult.success || !agentResult.agent) {
|
|
178
|
+
return {
|
|
179
|
+
verified: false,
|
|
180
|
+
verification_method: 'signature-only',
|
|
181
|
+
challenges_passed: { computational: false, cryptographic: false },
|
|
182
|
+
error: 'Agent not found'
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
// Verify signature
|
|
186
|
+
const cryptoResult = await verifyCryptographicSignature(req, agentResult.agent);
|
|
187
|
+
return {
|
|
188
|
+
verified: cryptoResult.valid,
|
|
189
|
+
agent_id: agentResult.agent.agent_id,
|
|
190
|
+
verification_method: 'signature-only',
|
|
191
|
+
challenges_passed: {
|
|
192
|
+
computational: false,
|
|
193
|
+
cryptographic: cryptoResult.valid
|
|
194
|
+
},
|
|
195
|
+
error: cryptoResult.error,
|
|
196
|
+
metadata: {
|
|
197
|
+
signature_valid: cryptoResult.valid
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Challenge-only verification (existing BOTCHA)
|
|
203
|
+
*/
|
|
204
|
+
async function performChallengeOnlyVerification(req, opts) {
|
|
205
|
+
const challengeResult = await verifyComputationalChallenge(req);
|
|
206
|
+
return {
|
|
207
|
+
verified: challengeResult.valid,
|
|
208
|
+
verification_method: 'challenge',
|
|
209
|
+
challenges_passed: {
|
|
210
|
+
computational: challengeResult.valid,
|
|
211
|
+
cryptographic: false
|
|
212
|
+
},
|
|
213
|
+
error: challengeResult.error,
|
|
214
|
+
metadata: {
|
|
215
|
+
solve_time_ms: challengeResult.solveTimeMs
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Handle verification fallback cases
|
|
221
|
+
*/
|
|
222
|
+
async function handleVerificationFallback(req, opts, mode) {
|
|
223
|
+
if (opts.requireTAP) {
|
|
224
|
+
return {
|
|
225
|
+
verified: false,
|
|
226
|
+
verification_method: 'tap',
|
|
227
|
+
challenges_passed: { computational: false, cryptographic: false },
|
|
228
|
+
error: 'TAP authentication required'
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
// Fallback to challenge-only if allowed
|
|
232
|
+
if (opts.allowChallenge) {
|
|
233
|
+
return await performChallengeOnlyVerification(req, opts);
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
verified: false,
|
|
237
|
+
verification_method: 'challenge',
|
|
238
|
+
challenges_passed: { computational: false, cryptographic: false },
|
|
239
|
+
error: 'No valid verification method available'
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// ============ HELPER FUNCTIONS ============
|
|
243
|
+
async function verifyCryptographicSignature(req, agent) {
|
|
244
|
+
if (!agent.public_key || !agent.signature_algorithm) {
|
|
245
|
+
return { valid: false, error: 'Agent has no cryptographic key configured' };
|
|
246
|
+
}
|
|
247
|
+
const verificationRequest = {
|
|
248
|
+
method: req.method,
|
|
249
|
+
path: req.path,
|
|
250
|
+
headers: req.headers,
|
|
251
|
+
body: typeof req.body === 'string' ? req.body : JSON.stringify(req.body)
|
|
252
|
+
};
|
|
253
|
+
return await verifyHTTPMessageSignature(verificationRequest, agent.public_key, agent.signature_algorithm);
|
|
254
|
+
}
|
|
255
|
+
async function verifyComputationalChallenge(req) {
|
|
256
|
+
const challengeId = req.headers['x-botcha-challenge-id'];
|
|
257
|
+
const answers = req.headers['x-botcha-answers'];
|
|
258
|
+
if (!challengeId || !answers) {
|
|
259
|
+
return { valid: false, error: 'Missing challenge ID or answers' };
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
const answersArray = JSON.parse(answers);
|
|
263
|
+
if (!Array.isArray(answersArray)) {
|
|
264
|
+
return { valid: false, error: 'Answers must be an array' };
|
|
265
|
+
}
|
|
266
|
+
const result = verifySpeedChallenge(challengeId, answersArray);
|
|
267
|
+
return {
|
|
268
|
+
valid: result.valid,
|
|
269
|
+
error: result.reason,
|
|
270
|
+
solveTimeMs: result.solveTimeMs
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
return { valid: false, error: `Challenge verification error: ${err instanceof Error ? err.message : 'Unknown error'}` };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async function getTAPAgentById(agentsKV, agentId) {
|
|
278
|
+
if (!agentsKV)
|
|
279
|
+
return null;
|
|
280
|
+
try {
|
|
281
|
+
const result = await getTAPAgent(agentsKV, agentId);
|
|
282
|
+
return result.success ? result.agent : null;
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function sendVerificationChallenge(res, result, opts) {
|
|
289
|
+
// Generate challenge if needed
|
|
290
|
+
let challenge = null;
|
|
291
|
+
if (!result.challenges_passed.computational && opts.allowChallenge) {
|
|
292
|
+
challenge = generateSpeedChallenge();
|
|
293
|
+
}
|
|
294
|
+
// Build TAP challenge response
|
|
295
|
+
const response = buildTAPChallengeResponse(result, challenge);
|
|
296
|
+
// Add challenge headers
|
|
297
|
+
if (challenge) {
|
|
298
|
+
res.header('X-Botcha-Challenge-Id', challenge.id);
|
|
299
|
+
res.header('X-Botcha-Challenge-Type', 'speed');
|
|
300
|
+
res.header('X-Botcha-Time-Limit', challenge.timeLimit.toString());
|
|
301
|
+
}
|
|
302
|
+
// 403 Forbidden for all verification failures (not 401 which implies re-authentication)
|
|
303
|
+
const statusCode = 403;
|
|
304
|
+
res.status(statusCode).json(response);
|
|
305
|
+
}
|
|
306
|
+
function logVerificationAttempt(req, result, durationMs) {
|
|
307
|
+
const logEntry = {
|
|
308
|
+
timestamp: new Date().toISOString(),
|
|
309
|
+
method: req.method,
|
|
310
|
+
path: req.path,
|
|
311
|
+
userAgent: req.headers['user-agent'],
|
|
312
|
+
clientIP: req.ip || req.socket?.remoteAddress,
|
|
313
|
+
verificationMethod: result.verification_method,
|
|
314
|
+
verified: result.verified,
|
|
315
|
+
challengesPassed: result.challenges_passed,
|
|
316
|
+
agentId: result.agent_id,
|
|
317
|
+
sessionId: result.session_id,
|
|
318
|
+
durationMs,
|
|
319
|
+
error: result.error
|
|
320
|
+
};
|
|
321
|
+
console.log('TAP_VERIFICATION_AUDIT', logEntry);
|
|
322
|
+
}
|
|
323
|
+
// ============ PRE-CONFIGURED MODES ============
|
|
324
|
+
export const tapVerifyModes = {
|
|
325
|
+
/**
|
|
326
|
+
* Require full TAP authentication (crypto + challenge)
|
|
327
|
+
*/
|
|
328
|
+
strict: (options = {}) => tapEnhancedVerify({
|
|
329
|
+
requireTAP: true,
|
|
330
|
+
allowChallenge: true,
|
|
331
|
+
auditLogging: true,
|
|
332
|
+
...options
|
|
333
|
+
}),
|
|
334
|
+
/**
|
|
335
|
+
* Prefer TAP but allow computational fallback
|
|
336
|
+
*/
|
|
337
|
+
flexible: (options = {}) => tapEnhancedVerify({
|
|
338
|
+
preferTAP: true,
|
|
339
|
+
allowChallenge: true,
|
|
340
|
+
requireTAP: false,
|
|
341
|
+
...options
|
|
342
|
+
}),
|
|
343
|
+
/**
|
|
344
|
+
* Signature-only verification (no computational challenge)
|
|
345
|
+
*/
|
|
346
|
+
signatureOnly: (options = {}) => tapEnhancedVerify({
|
|
347
|
+
requireTAP: false,
|
|
348
|
+
allowChallenge: false,
|
|
349
|
+
tapEnabled: true,
|
|
350
|
+
...options
|
|
351
|
+
}),
|
|
352
|
+
/**
|
|
353
|
+
* Development mode with relaxed security
|
|
354
|
+
*/
|
|
355
|
+
development: (options = {}) => tapEnhancedVerify({
|
|
356
|
+
requireTAP: false,
|
|
357
|
+
allowChallenge: true,
|
|
358
|
+
auditLogging: true,
|
|
359
|
+
trustedIssuers: ['test-issuer', 'localhost', 'development'],
|
|
360
|
+
...options
|
|
361
|
+
})
|
|
362
|
+
};
|
|
363
|
+
/**
|
|
364
|
+
* Alias for tapEnhancedVerify for backward compatibility with docs.
|
|
365
|
+
* Docs reference: import { createTAPVerifyMiddleware } from '@dupecom/botcha/middleware'
|
|
366
|
+
*/
|
|
367
|
+
export const createTAPVerifyMiddleware = tapEnhancedVerify;
|
|
368
|
+
export default tapEnhancedVerify;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
interface BotchaOptions {
|
|
3
|
+
requireSignature?: boolean;
|
|
4
|
+
allowChallenge?: boolean;
|
|
5
|
+
challengeType?: 'standard' | 'speed';
|
|
6
|
+
challengeDifficulty?: 'easy' | 'medium' | 'hard';
|
|
7
|
+
trustedProviders?: string[];
|
|
8
|
+
customVerify?: (req: Request) => Promise<boolean>;
|
|
9
|
+
}
|
|
10
|
+
export declare function botchaVerify(options?: BotchaOptions): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
11
|
+
export default botchaVerify;
|
|
12
|
+
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../../src/middleware/verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAK1D,UAAU,aAAa;IACrB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC;IACrC,mBAAmB,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;IACjD,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACnD;AASD,wBAAgB,YAAY,CAAC,OAAO,GAAE,aAAkB,IAGxC,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,mBAqD9D;AA0FD,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { generateChallenge, verifyChallenge } from '../challenges/compute.js';
|
|
2
|
+
import { generateSpeedChallenge, verifySpeedChallenge } from '../challenges/speed.js';
|
|
3
|
+
import { verifyWebBotAuth, isTrustedProvider } from '../utils/signature.js';
|
|
4
|
+
const defaultOptions = {
|
|
5
|
+
requireSignature: false,
|
|
6
|
+
allowChallenge: true,
|
|
7
|
+
challengeType: 'standard',
|
|
8
|
+
challengeDifficulty: 'medium',
|
|
9
|
+
};
|
|
10
|
+
export function botchaVerify(options = {}) {
|
|
11
|
+
const opts = { ...defaultOptions, ...options };
|
|
12
|
+
return async (req, res, next) => {
|
|
13
|
+
const result = await verifyAgent(req, opts);
|
|
14
|
+
if (result.verified) {
|
|
15
|
+
req.agent = result.agent;
|
|
16
|
+
req.verificationMethod = result.method;
|
|
17
|
+
req.provider = result.provider;
|
|
18
|
+
return next();
|
|
19
|
+
}
|
|
20
|
+
// Not verified - return challenge or denial
|
|
21
|
+
const challenge = opts.allowChallenge
|
|
22
|
+
? (opts.challengeType === 'speed'
|
|
23
|
+
? generateSpeedChallenge()
|
|
24
|
+
: generateChallenge(opts.challengeDifficulty))
|
|
25
|
+
: undefined;
|
|
26
|
+
// Add challenge-specific headers
|
|
27
|
+
if (challenge) {
|
|
28
|
+
res.header('X-Botcha-Challenge-Id', challenge.id);
|
|
29
|
+
res.header('X-Botcha-Challenge-Type', opts.challengeType === 'speed' ? 'speed' : 'standard');
|
|
30
|
+
res.header('X-Botcha-Time-Limit', challenge.timeLimit.toString());
|
|
31
|
+
}
|
|
32
|
+
res.status(403).json({
|
|
33
|
+
success: false,
|
|
34
|
+
error: 'BOTCHA_VERIFICATION_FAILED',
|
|
35
|
+
code: 'BOTCHA_CHALLENGE',
|
|
36
|
+
message: '🚫 Access denied. This endpoint is for AI agents only.',
|
|
37
|
+
hint: result.hint,
|
|
38
|
+
challenge: challenge ? (opts.challengeType === 'speed'
|
|
39
|
+
? {
|
|
40
|
+
id: challenge.id,
|
|
41
|
+
type: 'speed',
|
|
42
|
+
problems: challenge.challenges,
|
|
43
|
+
timeLimit: `${challenge.timeLimit}ms`,
|
|
44
|
+
instructions: challenge.instructions,
|
|
45
|
+
submitHeader: 'X-Botcha-Challenge-Id',
|
|
46
|
+
answerHeader: 'X-Botcha-Answers',
|
|
47
|
+
}
|
|
48
|
+
: {
|
|
49
|
+
id: challenge.id,
|
|
50
|
+
type: 'standard',
|
|
51
|
+
puzzle: challenge.puzzle,
|
|
52
|
+
timeLimit: `${challenge.timeLimit}ms`,
|
|
53
|
+
hint: challenge.hint,
|
|
54
|
+
submitHeader: 'X-Botcha-Challenge-Id',
|
|
55
|
+
answerHeader: 'X-Botcha-Solution',
|
|
56
|
+
}) : undefined,
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async function verifyAgent(req, opts) {
|
|
61
|
+
// Method 1: Web Bot Auth cryptographic signature (strongest)
|
|
62
|
+
const signatureAgent = req.headers['signature-agent'];
|
|
63
|
+
if (signatureAgent) {
|
|
64
|
+
// Check if from trusted provider
|
|
65
|
+
if (!isTrustedProvider(signatureAgent)) {
|
|
66
|
+
return {
|
|
67
|
+
verified: false,
|
|
68
|
+
hint: `Provider ${signatureAgent} not in trusted list`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const sigResult = await verifyWebBotAuth(req.headers, req.method, req.path, typeof req.body === 'string' ? req.body : JSON.stringify(req.body));
|
|
72
|
+
if (sigResult.valid) {
|
|
73
|
+
return {
|
|
74
|
+
verified: true,
|
|
75
|
+
method: 'signature',
|
|
76
|
+
agent: sigResult.agent,
|
|
77
|
+
provider: sigResult.provider,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Method 2: Challenge-Response (if challenge solution provided)
|
|
82
|
+
const challengeId = req.headers['x-botcha-challenge-id']
|
|
83
|
+
|| req.headers['x-botcha-id'];
|
|
84
|
+
const solution = req.headers['x-botcha-solution'];
|
|
85
|
+
const answersHeader = req.headers['x-botcha-answers'];
|
|
86
|
+
if (challengeId && (solution || answersHeader)) {
|
|
87
|
+
const speedPayload = answersHeader || (solution && looksLikeJsonArray(solution) ? solution : undefined);
|
|
88
|
+
if (speedPayload) {
|
|
89
|
+
const answers = parseJsonArray(speedPayload);
|
|
90
|
+
if (!answers) {
|
|
91
|
+
return { verified: false, hint: 'Invalid speed challenge answers format' };
|
|
92
|
+
}
|
|
93
|
+
const speedResult = verifySpeedChallenge(challengeId, answers);
|
|
94
|
+
if (speedResult.valid) {
|
|
95
|
+
return {
|
|
96
|
+
verified: true,
|
|
97
|
+
method: 'challenge',
|
|
98
|
+
agent: `speed-challenge-verified (${speedResult.solveTimeMs}ms)`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return { verified: false, hint: speedResult.reason };
|
|
102
|
+
}
|
|
103
|
+
if (!solution) {
|
|
104
|
+
return { verified: false, hint: 'Missing challenge solution' };
|
|
105
|
+
}
|
|
106
|
+
const result = verifyChallenge(challengeId, solution);
|
|
107
|
+
if (result.valid) {
|
|
108
|
+
return {
|
|
109
|
+
verified: true,
|
|
110
|
+
method: 'challenge',
|
|
111
|
+
agent: `challenge-verified (${result.timeMs}ms)`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return { verified: false, hint: result.reason };
|
|
115
|
+
}
|
|
116
|
+
// Method 3: X-Agent-Identity header (dev/testing ONLY — should NOT be enabled in production)
|
|
117
|
+
// NOTE: This is intentionally NOT enabled by default in the standalone verify middleware.
|
|
118
|
+
// Use the Express middleware (lib/index.ts) with allowTestHeader: true for dev mode.
|
|
119
|
+
// No verification succeeded — User-Agent patterns are NOT sufficient for verification.
|
|
120
|
+
// A User-Agent header can be trivially spoofed by any HTTP client.
|
|
121
|
+
return {
|
|
122
|
+
verified: false,
|
|
123
|
+
hint: 'Provide Signature-Agent header (Web Bot Auth) or solve a challenge (X-Botcha-Challenge-Id + X-Botcha-Solution)',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export default botchaVerify;
|
|
127
|
+
function looksLikeJsonArray(value) {
|
|
128
|
+
const trimmed = value.trim();
|
|
129
|
+
return trimmed.startsWith('[') && trimmed.endsWith(']');
|
|
130
|
+
}
|
|
131
|
+
function parseJsonArray(value) {
|
|
132
|
+
try {
|
|
133
|
+
const parsed = JSON.parse(value);
|
|
134
|
+
if (!Array.isArray(parsed))
|
|
135
|
+
return null;
|
|
136
|
+
return parsed.map(item => String(item));
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BadgePayload } from './badge.js';
|
|
2
|
+
interface BadgeImageOptions {
|
|
3
|
+
width?: number;
|
|
4
|
+
height?: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Generate an SVG badge image
|
|
8
|
+
*/
|
|
9
|
+
export declare function generateBadgeSvg(payload: BadgePayload, options?: BadgeImageOptions): string;
|
|
10
|
+
/**
|
|
11
|
+
* Generate an HTML verification page
|
|
12
|
+
*/
|
|
13
|
+
export declare function generateBadgeHtml(payload: BadgePayload, badgeId: string): string;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=badge-image.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"badge-image.d.ts","sourceRoot":"","sources":["../../../src/utils/badge-image.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAe,MAAM,YAAY,CAAC;AAEvD,UAAU,iBAAiB;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA6BD;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,YAAY,EACrB,OAAO,GAAE,iBAAsB,GAC9B,MAAM,CAuER;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAyJhF"}
|