@dupecom/botcha 0.13.0 → 0.14.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.
Files changed (35) hide show
  1. package/README.md +46 -1
  2. package/dist/lib/client/index.d.ts +64 -2
  3. package/dist/lib/client/index.d.ts.map +1 -1
  4. package/dist/lib/client/index.js +136 -1
  5. package/dist/lib/client/types.d.ts +68 -0
  6. package/dist/lib/client/types.d.ts.map +1 -1
  7. package/dist/lib/index.js +2 -0
  8. package/dist/src/challenges/compute.d.ts +19 -0
  9. package/dist/src/challenges/compute.d.ts.map +1 -0
  10. package/dist/src/challenges/compute.js +88 -0
  11. package/dist/src/challenges/hybrid.d.ts +45 -0
  12. package/dist/src/challenges/hybrid.d.ts.map +1 -0
  13. package/dist/src/challenges/hybrid.js +94 -0
  14. package/dist/src/challenges/reasoning.d.ts +29 -0
  15. package/dist/src/challenges/reasoning.d.ts.map +1 -0
  16. package/dist/src/challenges/reasoning.js +414 -0
  17. package/dist/src/challenges/speed.d.ts +34 -0
  18. package/dist/src/challenges/speed.d.ts.map +1 -0
  19. package/dist/src/challenges/speed.js +115 -0
  20. package/dist/src/middleware/tap-enhanced-verify.d.ts +57 -0
  21. package/dist/src/middleware/tap-enhanced-verify.d.ts.map +1 -0
  22. package/dist/src/middleware/tap-enhanced-verify.js +368 -0
  23. package/dist/src/middleware/verify.d.ts +12 -0
  24. package/dist/src/middleware/verify.d.ts.map +1 -0
  25. package/dist/src/middleware/verify.js +141 -0
  26. package/dist/src/utils/badge-image.d.ts +15 -0
  27. package/dist/src/utils/badge-image.d.ts.map +1 -0
  28. package/dist/src/utils/badge-image.js +253 -0
  29. package/dist/src/utils/badge.d.ts +39 -0
  30. package/dist/src/utils/badge.d.ts.map +1 -0
  31. package/dist/src/utils/badge.js +125 -0
  32. package/dist/src/utils/signature.d.ts +23 -0
  33. package/dist/src/utils/signature.d.ts.map +1 -0
  34. package/dist/src/utils/signature.js +160 -0
  35. 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"}