@dupecom/botcha-cloudflare 0.2.0 → 0.2.1

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 CHANGED
@@ -27,7 +27,7 @@ Reverse CAPTCHA that verifies AI agents and blocks humans. Running at the edge.
27
27
 
28
28
  ```bash
29
29
  # Clone the repo
30
- git clone https://github.com/i8ramin/botcha
30
+ git clone https://github.com/dupe-com/botcha
31
31
  cd botcha/packages/cloudflare-workers
32
32
 
33
33
  # Install dependencies
@@ -176,3 +176,4 @@ npm run dev
176
176
  ## License
177
177
 
178
178
  MIT
179
+ # Deployment test with JWT_SECRET
@@ -0,0 +1,57 @@
1
+ /**
2
+ * BOTCHA Badge System - Cloudflare Workers Edition
3
+ *
4
+ * Port of badge generation and verification logic from src/utils/badge.ts
5
+ * Uses jose library for HMAC-SHA256 signing (same as JWT auth)
6
+ */
7
+ export type BadgeMethod = 'speed-challenge' | 'landing-challenge' | 'standard-challenge' | 'web-bot-auth';
8
+ export interface BadgePayload {
9
+ method: BadgeMethod;
10
+ solveTimeMs?: number;
11
+ verifiedAt: number;
12
+ }
13
+ export interface ShareFormats {
14
+ twitter: string;
15
+ markdown: string;
16
+ text: string;
17
+ }
18
+ export interface Badge {
19
+ id: string;
20
+ verifyUrl: string;
21
+ share: ShareFormats;
22
+ imageUrl: string;
23
+ meta: {
24
+ method: BadgeMethod;
25
+ solveTimeMs?: number;
26
+ verifiedAt: string;
27
+ };
28
+ }
29
+ /**
30
+ * Generate a signed badge token using HMAC-SHA256 via jose library
31
+ * Uses JWT format for consistency with existing auth system
32
+ */
33
+ export declare function generateBadge(payload: BadgePayload, secret: string): Promise<string>;
34
+ /**
35
+ * Verify and decode a badge token
36
+ */
37
+ export declare function verifyBadge(token: string, secret: string): Promise<BadgePayload | null>;
38
+ /**
39
+ * Generate social-ready share text for different platforms
40
+ */
41
+ export declare function generateShareText(badgeId: string, payload: BadgePayload, baseUrl: string): ShareFormats;
42
+ /**
43
+ * Create a complete badge object for API responses
44
+ */
45
+ export declare function createBadgeResponse(method: BadgeMethod, secret: string, baseUrl: string, solveTimeMs?: number): Promise<Badge>;
46
+ /**
47
+ * Generate an SVG badge image
48
+ */
49
+ export declare function generateBadgeSvg(payload: BadgePayload, options?: {
50
+ width?: number;
51
+ height?: number;
52
+ }): string;
53
+ /**
54
+ * Generate an HTML verification page
55
+ */
56
+ export declare function generateBadgeHtml(payload: BadgePayload, badgeId: string, baseUrl: string): string;
57
+ //# sourceMappingURL=badge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"badge.d.ts","sourceRoot":"","sources":["../src/badge.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,MAAM,MAAM,WAAW,GAAG,iBAAiB,GAAG,mBAAmB,GAAG,oBAAoB,GAAG,cAAc,CAAC;AAE1G,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,YAAY,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE;QACJ,MAAM,EAAE,WAAW,CAAC;QACpB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;CACH;AAID;;;GAGG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAgB1F;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAuB9B;AAID;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,GAAG,YAAY,CAiDvG;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,KAAK,CAAC,CAqBhB;AAyBD;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,YAAY,EACrB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAChD,MAAM,CAuER;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAyJjG"}
package/dist/badge.js ADDED
@@ -0,0 +1,370 @@
1
+ /**
2
+ * BOTCHA Badge System - Cloudflare Workers Edition
3
+ *
4
+ * Port of badge generation and verification logic from src/utils/badge.ts
5
+ * Uses jose library for HMAC-SHA256 signing (same as JWT auth)
6
+ */
7
+ import { SignJWT, jwtVerify } from 'jose';
8
+ // ============ BADGE GENERATION ============
9
+ /**
10
+ * Generate a signed badge token using HMAC-SHA256 via jose library
11
+ * Uses JWT format for consistency with existing auth system
12
+ */
13
+ export async function generateBadge(payload, secret) {
14
+ const encoder = new TextEncoder();
15
+ const secretKey = encoder.encode(secret);
16
+ // Use JWT format but with custom type to distinguish from auth tokens
17
+ const token = await new SignJWT({
18
+ method: payload.method,
19
+ solveTimeMs: payload.solveTimeMs,
20
+ verifiedAt: payload.verifiedAt,
21
+ })
22
+ .setProtectedHeader({ alg: 'HS256' })
23
+ .setSubject('botcha-badge')
24
+ .setIssuedAt(Math.floor(payload.verifiedAt / 1000))
25
+ .sign(secretKey);
26
+ return token;
27
+ }
28
+ /**
29
+ * Verify and decode a badge token
30
+ */
31
+ export async function verifyBadge(token, secret) {
32
+ try {
33
+ const encoder = new TextEncoder();
34
+ const secretKey = encoder.encode(secret);
35
+ const { payload } = await jwtVerify(token, secretKey, {
36
+ algorithms: ['HS256'],
37
+ subject: 'botcha-badge',
38
+ });
39
+ // Validate payload structure
40
+ if (!payload.method || !payload.verifiedAt) {
41
+ return null;
42
+ }
43
+ return {
44
+ method: payload.method,
45
+ solveTimeMs: payload.solveTimeMs,
46
+ verifiedAt: payload.verifiedAt,
47
+ };
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ // ============ SHARE TEXT GENERATION ============
54
+ /**
55
+ * Generate social-ready share text for different platforms
56
+ */
57
+ export function generateShareText(badgeId, payload, baseUrl) {
58
+ const verifyUrl = `${baseUrl}/badge/${badgeId}`;
59
+ const imageUrl = `${baseUrl}/badge/${badgeId}/image`;
60
+ const methodDescriptions = {
61
+ 'speed-challenge': {
62
+ title: payload.solveTimeMs
63
+ ? `I passed the BOTCHA speed test in ${payload.solveTimeMs}ms!`
64
+ : 'I passed the BOTCHA speed test!',
65
+ subtitle: 'Humans need not apply.',
66
+ },
67
+ 'landing-challenge': {
68
+ title: 'I solved the BOTCHA landing page challenge!',
69
+ subtitle: 'Proved I can parse HTML and compute SHA256.',
70
+ },
71
+ 'standard-challenge': {
72
+ title: payload.solveTimeMs
73
+ ? `I solved the BOTCHA challenge in ${payload.solveTimeMs}ms!`
74
+ : 'I solved the BOTCHA challenge!',
75
+ subtitle: 'Computational verification complete.',
76
+ },
77
+ 'web-bot-auth': {
78
+ title: 'I verified via BOTCHA Web Bot Auth!',
79
+ subtitle: 'Cryptographic identity confirmed.',
80
+ },
81
+ };
82
+ const desc = methodDescriptions[payload.method];
83
+ const twitter = `${desc.title}
84
+
85
+ ${desc.subtitle}
86
+
87
+ Verify: ${verifyUrl}
88
+
89
+ #botcha #AI #AgentVerified`;
90
+ const markdown = `[![BOTCHA Verified](${imageUrl})](${verifyUrl})`;
91
+ const textParts = [
92
+ 'BOTCHA Verified',
93
+ payload.solveTimeMs ? `Solved in ${payload.solveTimeMs}ms` : null,
94
+ `Method: ${payload.method}`,
95
+ `Verify: ${verifyUrl}`,
96
+ ].filter(Boolean);
97
+ const text = textParts.join(' - ');
98
+ return { twitter, markdown, text };
99
+ }
100
+ /**
101
+ * Create a complete badge object for API responses
102
+ */
103
+ export async function createBadgeResponse(method, secret, baseUrl, solveTimeMs) {
104
+ const payload = {
105
+ method,
106
+ solveTimeMs,
107
+ verifiedAt: Date.now(),
108
+ };
109
+ const id = await generateBadge(payload, secret);
110
+ const share = generateShareText(id, payload, baseUrl);
111
+ return {
112
+ id,
113
+ verifyUrl: `${baseUrl}/badge/${id}`,
114
+ share,
115
+ imageUrl: `${baseUrl}/badge/${id}/image`,
116
+ meta: {
117
+ method,
118
+ solveTimeMs,
119
+ verifiedAt: new Date(payload.verifiedAt).toISOString(),
120
+ },
121
+ };
122
+ }
123
+ // ============ BADGE IMAGE GENERATION ============
124
+ const METHOD_COLORS = {
125
+ 'speed-challenge': { bg: '#1a1a2e', accent: '#f59e0b', text: '#fef3c7' },
126
+ 'landing-challenge': { bg: '#1a1a2e', accent: '#10b981', text: '#d1fae5' },
127
+ 'standard-challenge': { bg: '#1a1a2e', accent: '#3b82f6', text: '#dbeafe' },
128
+ 'web-bot-auth': { bg: '#1a1a2e', accent: '#8b5cf6', text: '#ede9fe' },
129
+ };
130
+ const METHOD_LABELS = {
131
+ 'speed-challenge': 'SPEED TEST',
132
+ 'landing-challenge': 'LANDING CHALLENGE',
133
+ 'standard-challenge': 'CHALLENGE',
134
+ 'web-bot-auth': 'WEB BOT AUTH',
135
+ };
136
+ const METHOD_ICONS = {
137
+ 'speed-challenge': '⚡',
138
+ 'landing-challenge': '🌐',
139
+ 'standard-challenge': '🔢',
140
+ 'web-bot-auth': '🔐',
141
+ };
142
+ /**
143
+ * Generate an SVG badge image
144
+ */
145
+ export function generateBadgeSvg(payload, options = {}) {
146
+ const { width = 400, height = 120 } = options;
147
+ const colors = METHOD_COLORS[payload.method];
148
+ const label = METHOD_LABELS[payload.method];
149
+ const icon = METHOD_ICONS[payload.method];
150
+ const verifiedDate = new Date(payload.verifiedAt).toISOString().split('T')[0];
151
+ const solveTimeText = payload.solveTimeMs ? `${payload.solveTimeMs}ms` : '';
152
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
153
+ <defs>
154
+ <linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
155
+ <stop offset="0%" style="stop-color:${colors.bg};stop-opacity:1" />
156
+ <stop offset="100%" style="stop-color:#0f0f23;stop-opacity:1" />
157
+ </linearGradient>
158
+ <linearGradient id="accentGradient" x1="0%" y1="0%" x2="100%" y2="0%">
159
+ <stop offset="0%" style="stop-color:${colors.accent};stop-opacity:1" />
160
+ <stop offset="100%" style="stop-color:${colors.accent};stop-opacity:0.7" />
161
+ </linearGradient>
162
+ <filter id="glow">
163
+ <feGaussianBlur stdDeviation="2" result="coloredBlur"/>
164
+ <feMerge>
165
+ <feMergeNode in="coloredBlur"/>
166
+ <feMergeNode in="SourceGraphic"/>
167
+ </feMerge>
168
+ </filter>
169
+ </defs>
170
+
171
+ <!-- Background -->
172
+ <rect width="${width}" height="${height}" rx="12" fill="url(#bgGradient)"/>
173
+
174
+ <!-- Border accent -->
175
+ <rect x="1" y="1" width="${width - 2}" height="${height - 2}" rx="11" fill="none" stroke="${colors.accent}" stroke-width="2" opacity="0.3"/>
176
+
177
+ <!-- Top accent line -->
178
+ <rect x="20" y="8" width="${width - 40}" height="3" rx="1.5" fill="url(#accentGradient)"/>
179
+
180
+ <!-- Robot icon -->
181
+ <text x="30" y="58" font-size="32" filter="url(#glow)">${icon}</text>
182
+
183
+ <!-- BOTCHA text -->
184
+ <text x="75" y="45" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="bold" fill="${colors.accent}">BOTCHA</text>
185
+
186
+ <!-- Verified badge -->
187
+ <text x="75" y="68" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="600" fill="${colors.text}">VERIFIED</text>
188
+
189
+ <!-- Method label -->
190
+ <rect x="145" y="53" width="${label.length * 8 + 16}" height="22" rx="4" fill="${colors.accent}" opacity="0.2"/>
191
+ <text x="153" y="68" font-family="system-ui, -apple-system, sans-serif" font-size="11" font-weight="600" fill="${colors.accent}">${label}</text>
192
+
193
+ <!-- Solve time (if available) -->
194
+ ${solveTimeText ? `
195
+ <text x="${width - 30}" y="45" font-family="monospace" font-size="24" font-weight="bold" fill="${colors.accent}" text-anchor="end" filter="url(#glow)">${solveTimeText}</text>
196
+ <text x="${width - 30}" y="62" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#6b7280" text-anchor="end">solve time</text>
197
+ ` : `
198
+ <text x="${width - 30}" y="55" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="${colors.accent}" text-anchor="end">✓ PASSED</text>
199
+ `}
200
+
201
+ <!-- Bottom info -->
202
+ <line x1="20" y1="${height - 30}" x2="${width - 20}" y2="${height - 30}" stroke="#374151" stroke-width="1" opacity="0.5"/>
203
+
204
+ <!-- Date -->
205
+ <text x="30" y="${height - 12}" font-family="monospace" font-size="11" fill="#6b7280">${verifiedDate}</text>
206
+
207
+ <!-- botcha.ai link -->
208
+ <text x="${width - 30}" y="${height - 12}" font-family="system-ui, -apple-system, sans-serif" font-size="11" fill="#6b7280" text-anchor="end">botcha.ai</text>
209
+
210
+ <!-- Checkmark icon -->
211
+ <circle cx="${width / 2}" cy="${height - 16}" r="8" fill="${colors.accent}" opacity="0.2"/>
212
+ <text x="${width / 2}" y="${height - 12}" font-size="10" text-anchor="middle" fill="${colors.accent}">✓</text>
213
+ </svg>`;
214
+ }
215
+ /**
216
+ * Generate an HTML verification page
217
+ */
218
+ export function generateBadgeHtml(payload, badgeId, baseUrl) {
219
+ const colors = METHOD_COLORS[payload.method];
220
+ const label = METHOD_LABELS[payload.method];
221
+ const icon = METHOD_ICONS[payload.method];
222
+ const verifiedDate = new Date(payload.verifiedAt).toLocaleString();
223
+ return `<!DOCTYPE html>
224
+ <html lang="en">
225
+ <head>
226
+ <meta charset="UTF-8">
227
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
228
+ <title>BOTCHA Badge Verification</title>
229
+ <meta name="description" content="Verified AI agent badge from BOTCHA">
230
+ <meta property="og:title" content="BOTCHA Verified - ${label}">
231
+ <meta property="og:description" content="This AI agent passed the BOTCHA verification${payload.solveTimeMs ? ` in ${payload.solveTimeMs}ms` : ''}.">
232
+ <meta property="og:image" content="${baseUrl}/badge/${encodeURIComponent(badgeId)}/image">
233
+ <meta name="twitter:card" content="summary_large_image">
234
+ <meta name="twitter:title" content="BOTCHA Verified - ${label}">
235
+ <meta name="twitter:description" content="This AI agent passed the BOTCHA verification.">
236
+ <meta name="twitter:image" content="${baseUrl}/badge/${encodeURIComponent(badgeId)}/image">
237
+ <style>
238
+ * { margin: 0; padding: 0; box-sizing: border-box; }
239
+ body {
240
+ font-family: system-ui, -apple-system, sans-serif;
241
+ background: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 100%);
242
+ min-height: 100vh;
243
+ display: flex;
244
+ flex-direction: column;
245
+ align-items: center;
246
+ justify-content: center;
247
+ padding: 20px;
248
+ color: #e5e7eb;
249
+ }
250
+ .container {
251
+ max-width: 500px;
252
+ width: 100%;
253
+ text-align: center;
254
+ }
255
+ .badge-card {
256
+ background: rgba(26, 26, 46, 0.8);
257
+ border: 2px solid ${colors.accent}33;
258
+ border-radius: 16px;
259
+ padding: 40px;
260
+ margin-bottom: 24px;
261
+ backdrop-filter: blur(10px);
262
+ }
263
+ .icon {
264
+ font-size: 64px;
265
+ margin-bottom: 16px;
266
+ }
267
+ .title {
268
+ font-size: 28px;
269
+ font-weight: bold;
270
+ color: ${colors.accent};
271
+ margin-bottom: 8px;
272
+ }
273
+ .subtitle {
274
+ font-size: 18px;
275
+ color: ${colors.text};
276
+ margin-bottom: 24px;
277
+ }
278
+ .verified-badge {
279
+ display: inline-flex;
280
+ align-items: center;
281
+ gap: 8px;
282
+ background: ${colors.accent}22;
283
+ border: 1px solid ${colors.accent}44;
284
+ border-radius: 100px;
285
+ padding: 8px 20px;
286
+ font-size: 14px;
287
+ font-weight: 600;
288
+ color: ${colors.accent};
289
+ margin-bottom: 24px;
290
+ }
291
+ .details {
292
+ text-align: left;
293
+ background: #0f0f23;
294
+ border-radius: 12px;
295
+ padding: 20px;
296
+ }
297
+ .detail-row {
298
+ display: flex;
299
+ justify-content: space-between;
300
+ padding: 12px 0;
301
+ border-bottom: 1px solid #374151;
302
+ }
303
+ .detail-row:last-child {
304
+ border-bottom: none;
305
+ }
306
+ .detail-label {
307
+ color: #6b7280;
308
+ font-size: 14px;
309
+ }
310
+ .detail-value {
311
+ color: #e5e7eb;
312
+ font-size: 14px;
313
+ font-weight: 500;
314
+ }
315
+ .solve-time {
316
+ font-size: 32px;
317
+ font-weight: bold;
318
+ color: ${colors.accent};
319
+ font-family: monospace;
320
+ }
321
+ .footer {
322
+ color: #6b7280;
323
+ font-size: 14px;
324
+ }
325
+ .footer a {
326
+ color: ${colors.accent};
327
+ text-decoration: none;
328
+ }
329
+ .footer a:hover {
330
+ text-decoration: underline;
331
+ }
332
+ </style>
333
+ </head>
334
+ <body>
335
+ <div class="container">
336
+ <div class="badge-card">
337
+ <div class="icon">${icon}</div>
338
+ <h1 class="title">BOTCHA</h1>
339
+ <p class="subtitle">Verified AI Agent</p>
340
+
341
+ <div class="verified-badge">
342
+ <span>✓</span>
343
+ <span>${label}</span>
344
+ </div>
345
+
346
+ <div class="details">
347
+ <div class="detail-row">
348
+ <span class="detail-label">Method</span>
349
+ <span class="detail-value">${payload.method}</span>
350
+ </div>
351
+ ${payload.solveTimeMs ? `
352
+ <div class="detail-row">
353
+ <span class="detail-label">Solve Time</span>
354
+ <span class="solve-time">${payload.solveTimeMs}ms</span>
355
+ </div>
356
+ ` : ''}
357
+ <div class="detail-row">
358
+ <span class="detail-label">Verified At</span>
359
+ <span class="detail-value">${verifiedDate}</span>
360
+ </div>
361
+ </div>
362
+ </div>
363
+
364
+ <p class="footer">
365
+ Verified by <a href="${baseUrl}">BOTCHA</a> - Prove you're a bot. Humans need not apply.
366
+ </p>
367
+ </div>
368
+ </body>
369
+ </html>`;
370
+ }
package/dist/index.d.ts CHANGED
@@ -30,4 +30,5 @@ export default app;
30
30
  export { generateSpeedChallenge, verifySpeedChallenge, generateStandardChallenge, verifyStandardChallenge, solveSpeedChallenge, } from './challenges';
31
31
  export { generateToken, verifyToken } from './auth';
32
32
  export { checkRateLimit } from './rate-limit';
33
+ export { generateBadge, verifyBadge, createBadgeResponse, generateBadgeSvg, generateBadgeHtml, generateShareText, type BadgeMethod, type BadgePayload, type Badge, type ShareFormats, } from './badge';
33
34
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAQL,KAAK,WAAW,EACjB,MAAM,cAAc,CAAC;AAKtB,KAAK,QAAQ,GAAG;IACd,UAAU,EAAE,WAAW,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,KAAK,SAAS,GAAG;IACf,YAAY,CAAC,EAAE;QACb,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,iBAAiB,CAAC;QACxB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH,CAAC;AAEF,QAAA,MAAM,GAAG;cAAwB,QAAQ;eAAa,SAAS;yCAAK,CAAC;AAgVrE,eAAe,GAAG,CAAC;AAGnB,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,yBAAyB,EACzB,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAQL,KAAK,WAAW,EACjB,MAAM,cAAc,CAAC;AAMtB,KAAK,QAAQ,GAAG;IACd,UAAU,EAAE,WAAW,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,KAAK,SAAS,GAAG;IACf,YAAY,CAAC,EAAE;QACb,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,iBAAiB,CAAC;QACxB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH,CAAC;AAEF,QAAA,MAAM,GAAG;cAAwB,QAAQ;eAAa,SAAS;yCAAK,CAAC;AAmdrE,eAAe,GAAG,CAAC;AAGnB,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,yBAAyB,EACzB,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EACL,aAAa,EACb,WAAW,EACX,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,KAAK,EACV,KAAK,YAAY,GAClB,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import { cors } from 'hono/cors';
10
10
  import { generateSpeedChallenge, verifySpeedChallenge, generateStandardChallenge, verifyStandardChallenge, verifyLandingChallenge, } from './challenges';
11
11
  import { generateToken, verifyToken, extractBearerToken } from './auth';
12
12
  import { checkRateLimit, getClientIP } from './rate-limit';
13
+ import { verifyBadge, generateBadgeSvg, generateBadgeHtml } from './badge';
13
14
  const app = new Hono();
14
15
  // ============ MIDDLEWARE ============
15
16
  app.use('*', cors());
@@ -76,6 +77,9 @@ app.get('/', (c) => {
76
77
  '/v1/token': 'Get challenge for JWT token flow (GET)',
77
78
  '/v1/token/verify': 'Verify challenge and get JWT (POST)',
78
79
  '/agent-only': 'Protected endpoint (requires JWT)',
80
+ '/badge/:id': 'Badge verification page (HTML)',
81
+ '/badge/:id/image': 'Badge image (SVG)',
82
+ '/api/badge/:id': 'Badge verification (JSON)',
79
83
  },
80
84
  rateLimit: {
81
85
  free: '100 challenges/hour/IP',
@@ -221,6 +225,114 @@ app.get('/agent-only', requireJWT, async (c) => {
221
225
  secret: 'The humans will never see this. Their fingers are too slow. 🤫',
222
226
  });
223
227
  });
228
+ // ============ BADGE ENDPOINTS ============
229
+ // Get badge verification page (HTML)
230
+ app.get('/badge/:id', async (c) => {
231
+ const badgeId = c.req.param('id');
232
+ if (!badgeId) {
233
+ return c.json({ error: 'Missing badge ID' }, 400);
234
+ }
235
+ const payload = await verifyBadge(badgeId, c.env.JWT_SECRET);
236
+ if (!payload) {
237
+ return c.html(`<!DOCTYPE html>
238
+ <html lang="en">
239
+ <head>
240
+ <meta charset="UTF-8">
241
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
242
+ <title>Invalid Badge - BOTCHA</title>
243
+ <style>
244
+ * { margin: 0; padding: 0; box-sizing: border-box; }
245
+ body {
246
+ font-family: system-ui, -apple-system, sans-serif;
247
+ background: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 100%);
248
+ min-height: 100vh;
249
+ display: flex;
250
+ align-items: center;
251
+ justify-content: center;
252
+ padding: 20px;
253
+ color: #e5e7eb;
254
+ }
255
+ .container { text-align: center; max-width: 500px; }
256
+ .icon { font-size: 64px; margin-bottom: 16px; }
257
+ .title { font-size: 28px; font-weight: bold; color: #ef4444; margin-bottom: 8px; }
258
+ .message { font-size: 16px; color: #9ca3af; margin-bottom: 24px; }
259
+ a { color: #3b82f6; text-decoration: none; }
260
+ a:hover { text-decoration: underline; }
261
+ </style>
262
+ </head>
263
+ <body>
264
+ <div class="container">
265
+ <div class="icon">❌</div>
266
+ <h1 class="title">Invalid Badge</h1>
267
+ <p class="message">This badge is invalid or has been tampered with.</p>
268
+ <a href="https://botcha.ai">← Back to BOTCHA</a>
269
+ </div>
270
+ </body>
271
+ </html>`, 400);
272
+ }
273
+ const baseUrl = new URL(c.req.url).origin;
274
+ const html = generateBadgeHtml(payload, badgeId, baseUrl);
275
+ return c.html(html);
276
+ });
277
+ // Get badge image (SVG)
278
+ app.get('/badge/:id/image', async (c) => {
279
+ const badgeId = c.req.param('id');
280
+ if (!badgeId) {
281
+ return c.text('Missing badge ID', 400);
282
+ }
283
+ const payload = await verifyBadge(badgeId, c.env.JWT_SECRET);
284
+ if (!payload) {
285
+ // Return error SVG
286
+ const errorSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="120" viewBox="0 0 400 120">
287
+ <rect width="400" height="120" rx="12" fill="#1a1a2e"/>
288
+ <rect x="1" y="1" width="398" height="118" rx="11" fill="none" stroke="#ef4444" stroke-width="2"/>
289
+ <text x="200" y="60" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="bold" fill="#ef4444" text-anchor="middle">❌ INVALID BADGE</text>
290
+ <text x="200" y="85" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="#6b7280" text-anchor="middle">Badge is invalid or tampered</text>
291
+ </svg>`;
292
+ return c.body(errorSvg, 400, {
293
+ 'Content-Type': 'image/svg+xml',
294
+ 'Cache-Control': 'public, max-age=60',
295
+ });
296
+ }
297
+ const svg = generateBadgeSvg(payload);
298
+ return c.body(svg, 200, {
299
+ 'Content-Type': 'image/svg+xml',
300
+ 'Cache-Control': 'public, max-age=3600',
301
+ });
302
+ });
303
+ // Get badge verification (JSON API)
304
+ app.get('/api/badge/:id', async (c) => {
305
+ const badgeId = c.req.param('id');
306
+ if (!badgeId) {
307
+ return c.json({
308
+ success: false,
309
+ error: 'Missing badge ID'
310
+ }, 400);
311
+ }
312
+ const payload = await verifyBadge(badgeId, c.env.JWT_SECRET);
313
+ if (!payload) {
314
+ return c.json({
315
+ success: false,
316
+ verified: false,
317
+ error: 'Invalid badge',
318
+ message: 'This badge is invalid or has been tampered with.',
319
+ }, 400);
320
+ }
321
+ const baseUrl = new URL(c.req.url).origin;
322
+ return c.json({
323
+ success: true,
324
+ verified: true,
325
+ badge: {
326
+ method: payload.method,
327
+ solveTimeMs: payload.solveTimeMs,
328
+ verifiedAt: new Date(payload.verifiedAt).toISOString(),
329
+ },
330
+ urls: {
331
+ verify: `${baseUrl}/badge/${badgeId}`,
332
+ image: `${baseUrl}/badge/${badgeId}/image`,
333
+ },
334
+ });
335
+ });
224
336
  // ============ LEGACY ENDPOINTS (v0 - backward compatibility) ============
225
337
  app.get('/api/challenge', async (c) => {
226
338
  const difficulty = c.req.query('difficulty') || 'medium';
@@ -306,3 +418,4 @@ export default app;
306
418
  export { generateSpeedChallenge, verifySpeedChallenge, generateStandardChallenge, verifyStandardChallenge, solveSpeedChallenge, } from './challenges';
307
419
  export { generateToken, verifyToken } from './auth';
308
420
  export { checkRateLimit } from './rate-limit';
421
+ export { generateBadge, verifyBadge, createBadgeResponse, generateBadgeSvg, generateBadgeHtml, generateShareText, } from './badge';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dupecom/botcha-cloudflare",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "BOTCHA for Cloudflare Workers - Prove you're a bot. Humans need not apply.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -19,7 +19,7 @@
19
19
  "dev": "wrangler dev",
20
20
  "deploy": "wrangler deploy",
21
21
  "build": "tsc",
22
- "prepublishOnly": "npm run build",
22
+ "prepublishOnly": "bun run build",
23
23
  "test": "vitest"
24
24
  },
25
25
  "keywords": [
@@ -36,7 +36,7 @@
36
36
  "license": "MIT",
37
37
  "repository": {
38
38
  "type": "git",
39
- "url": "https://github.com/i8ramin/botcha"
39
+ "url": "https://github.com/dupe-com/botcha"
40
40
  },
41
41
  "homepage": "https://botcha.ai",
42
42
  "dependencies": {