@dupecom/botcha-cloudflare 0.3.1 → 0.3.3

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.
@@ -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,EAYL,KAAK,WAAW,EACjB,MAAM,cAAc,CAAC;AAOtB,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;AAs3BrE,eAAe,GAAG,CAAC;AAGnB,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,yBAAyB,EACzB,uBAAuB,EACvB,0BAA0B,EAC1B,wBAAwB,EACxB,uBAAuB,EACvB,qBAAqB,EACrB,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"}
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,EAYL,KAAK,WAAW,EACjB,MAAM,cAAc,CAAC;AAOtB,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;AA87BrE,eAAe,GAAG,CAAC;AAGnB,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,yBAAyB,EACzB,uBAAuB,EACvB,0BAA0B,EAC1B,wBAAwB,EACxB,uBAAuB,EACvB,qBAAqB,EACrB,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
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { Hono } from 'hono';
9
9
  import { cors } from 'hono/cors';
10
- import { generateSpeedChallenge, verifySpeedChallenge, generateStandardChallenge, verifyStandardChallenge, generateReasoningChallenge, verifyReasoningChallenge, generateHybridChallenge, verifyHybridChallenge, verifyLandingChallenge, } from './challenges';
10
+ import { generateSpeedChallenge, verifySpeedChallenge, generateStandardChallenge, verifyStandardChallenge, generateReasoningChallenge, verifyReasoningChallenge, generateHybridChallenge, verifyHybridChallenge, verifyLandingChallenge, validateLandingToken, } from './challenges';
11
11
  import { generateToken, verifyToken, extractBearerToken } from './auth';
12
12
  import { checkRateLimit, getClientIP } from './rate-limit';
13
13
  import { verifyBadge, generateBadgeSvg, generateBadgeHtml, createBadgeResponse } from './badge';
@@ -250,6 +250,7 @@ app.get('/health', (c) => {
250
250
  app.get('/v1/challenges', rateLimitMiddleware, async (c) => {
251
251
  const type = c.req.query('type') || 'hybrid';
252
252
  const difficulty = c.req.query('difficulty') || 'medium';
253
+ const baseUrl = new URL(c.req.url).origin;
253
254
  if (type === 'hybrid') {
254
255
  const challenge = await generateHybridChallenge(c.env.CHALLENGES);
255
256
  return c.json({
@@ -271,6 +272,12 @@ app.get('/v1/challenges', rateLimitMiddleware, async (c) => {
271
272
  },
272
273
  instructions: challenge.instructions,
273
274
  tip: '🔥 This is the ultimate test: proves you can compute AND reason like an AI.',
275
+ verify_endpoint: `${baseUrl}/v1/challenges/${challenge.id}/verify`,
276
+ submit_body: {
277
+ type: 'hybrid',
278
+ speed_answers: ['hash1', 'hash2', '...'],
279
+ reasoning_answers: { 'question-id': 'answer', '...': '...' }
280
+ }
274
281
  });
275
282
  }
276
283
  else if (type === 'speed') {
@@ -285,6 +292,11 @@ app.get('/v1/challenges', rateLimitMiddleware, async (c) => {
285
292
  instructions: challenge.instructions,
286
293
  },
287
294
  tip: '⚡ Speed challenge: You have 500ms to solve ALL problems. Humans cannot copy-paste fast enough.',
295
+ verify_endpoint: `${baseUrl}/v1/challenges/${challenge.id}/verify`,
296
+ submit_body: {
297
+ type: 'speed',
298
+ answers: ['hash1', 'hash2', 'hash3', 'hash4', 'hash5']
299
+ }
288
300
  });
289
301
  }
290
302
  else {
@@ -298,6 +310,10 @@ app.get('/v1/challenges', rateLimitMiddleware, async (c) => {
298
310
  timeLimit: `${challenge.timeLimit}ms`,
299
311
  hint: challenge.hint,
300
312
  },
313
+ verify_endpoint: `${baseUrl}/v1/challenges/${challenge.id}/verify`,
314
+ submit_body: {
315
+ answer: 'your-answer'
316
+ }
301
317
  });
302
318
  }
303
319
  });
@@ -413,6 +429,7 @@ app.post('/v1/token/verify', async (c) => {
413
429
  // Get reasoning challenge
414
430
  app.get('/v1/reasoning', rateLimitMiddleware, async (c) => {
415
431
  const challenge = await generateReasoningChallenge(c.env.CHALLENGES);
432
+ const baseUrl = new URL(c.req.url).origin;
416
433
  return c.json({
417
434
  success: true,
418
435
  type: 'reasoning',
@@ -424,6 +441,11 @@ app.get('/v1/reasoning', rateLimitMiddleware, async (c) => {
424
441
  instructions: challenge.instructions,
425
442
  },
426
443
  tip: 'These questions require reasoning that LLMs can do, but simple scripts cannot.',
444
+ verify_endpoint: `${baseUrl}/v1/reasoning`,
445
+ submit_body: {
446
+ id: challenge.id,
447
+ answers: { 'question-id': 'your answer', '...': '...' }
448
+ }
427
449
  });
428
450
  });
429
451
  // Verify reasoning challenge
@@ -452,6 +474,7 @@ app.post('/v1/reasoning', async (c) => {
452
474
  // Get hybrid challenge (v1 API)
453
475
  app.get('/v1/hybrid', rateLimitMiddleware, async (c) => {
454
476
  const challenge = await generateHybridChallenge(c.env.CHALLENGES);
477
+ const baseUrl = new URL(c.req.url).origin;
455
478
  return c.json({
456
479
  success: true,
457
480
  type: 'hybrid',
@@ -471,6 +494,12 @@ app.get('/v1/hybrid', rateLimitMiddleware, async (c) => {
471
494
  },
472
495
  instructions: challenge.instructions,
473
496
  tip: 'This is the ultimate test: proves you can compute AND reason like an AI.',
497
+ verify_endpoint: `${baseUrl}/v1/hybrid`,
498
+ submit_body: {
499
+ id: challenge.id,
500
+ speed_answers: ['hash1', 'hash2', '...'],
501
+ reasoning_answers: { 'question-id': 'answer', '...': '...' }
502
+ }
474
503
  });
475
504
  });
476
505
  // Verify hybrid challenge (v1 API)
@@ -600,8 +629,44 @@ app.post('/api/reasoning-challenge', async (c) => {
600
629
  });
601
630
  });
602
631
  // ============ PROTECTED ENDPOINT ============
603
- app.get('/agent-only', requireJWT, async (c) => {
604
- const payload = c.get('tokenPayload');
632
+ app.get('/agent-only', async (c) => {
633
+ // Check for landing token first (X-Botcha-Landing-Token header)
634
+ const landingToken = c.req.header('x-botcha-landing-token');
635
+ if (landingToken) {
636
+ const isValid = await validateLandingToken(landingToken, c.env.CHALLENGES);
637
+ if (isValid) {
638
+ return c.json({
639
+ success: true,
640
+ message: '🤖 Welcome, fellow agent!',
641
+ verified: true,
642
+ agent: 'landing-challenge-verified',
643
+ method: 'landing-token',
644
+ timestamp: new Date().toISOString(),
645
+ secret: 'The humans will never see this. Their fingers are too slow. 🤫',
646
+ });
647
+ }
648
+ }
649
+ // Fallback to JWT Bearer token
650
+ const authHeader = c.req.header('authorization');
651
+ const token = extractBearerToken(authHeader);
652
+ if (!token) {
653
+ return c.json({
654
+ error: 'UNAUTHORIZED',
655
+ message: 'Missing authentication. Use either:\n1. X-Botcha-Landing-Token header (from POST /api/verify-landing)\n2. Authorization: Bearer <token> (from POST /v1/token/verify)',
656
+ methods: {
657
+ landing: 'Solve landing page challenge via POST /api/verify-landing',
658
+ jwt: 'Solve speed challenge via POST /v1/token/verify'
659
+ }
660
+ }, 401);
661
+ }
662
+ const result = await verifyToken(token, c.env.JWT_SECRET);
663
+ if (!result.valid) {
664
+ return c.json({
665
+ error: 'INVALID_TOKEN',
666
+ message: result.error || 'Token is invalid or expired',
667
+ }, 401);
668
+ }
669
+ // JWT verified
605
670
  return c.json({
606
671
  success: true,
607
672
  message: '🤖 Welcome, fellow agent!',
@@ -609,7 +674,7 @@ app.get('/agent-only', requireJWT, async (c) => {
609
674
  agent: 'jwt-verified',
610
675
  method: 'bearer-token',
611
676
  timestamp: new Date().toISOString(),
612
- solveTime: `${payload?.solveTime}ms`,
677
+ solveTime: `${result.payload?.solveTime}ms`,
613
678
  secret: 'The humans will never see this. Their fingers are too slow. 🤫',
614
679
  });
615
680
  });
@@ -1 +1 @@
1
- {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../src/routes/stream.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAA0B,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAKzE,KAAK,QAAQ,GAAG;IACd,UAAU,EAAE,WAAW,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAYF,QAAA,MAAM,GAAG;cAAwB,QAAQ;yCAAK,CAAC;AAoS/C,eAAe,GAAG,CAAC"}
1
+ {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../src/routes/stream.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAA0B,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAKzE,KAAK,QAAQ,GAAG;IACd,UAAU,EAAE,WAAW,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAYF,QAAA,MAAM,GAAG;cAAwB,QAAQ;yCAAK,CAAC;AAiT/C,eAAe,GAAG,CAAC"}
@@ -143,29 +143,40 @@ app.post('/v1/challenge/stream/:session', async (c) => {
143
143
  expectedAnswers.push(await sha256First(num.toString(), 8));
144
144
  }
145
145
  // Update session
146
+ const timerStart = Date.now();
146
147
  session.status = 'challenged';
147
148
  session.problems = problems;
148
149
  session.expectedAnswers = expectedAnswers;
149
- session.timerStart = Date.now();
150
+ session.timerStart = timerStart;
151
+ // Store session
150
152
  await storeSession(c.env.CHALLENGES, session);
151
- // Return challenge event
153
+ // Return challenge event with timer start for client to track
152
154
  return c.json({
153
155
  success: true,
154
156
  event: 'challenge',
155
157
  data: {
156
158
  problems,
157
159
  timeLimit: 500,
160
+ timerStart, // Include so client can verify timing
158
161
  instructions: 'Compute SHA256 of each number, return first 8 hex chars',
159
162
  },
160
163
  });
161
164
  }
162
165
  // Handle "solve" action - verify answers
163
166
  if (action === 'solve') {
167
+ // Handle KV eventual consistency - retry once if still in 'ready' state
168
+ if (session.status === 'ready') {
169
+ await new Promise(resolve => setTimeout(resolve, 100));
170
+ const retrySession = await getSession(c.env.CHALLENGES, sessionId);
171
+ if (retrySession && retrySession.status === 'challenged') {
172
+ Object.assign(session, retrySession);
173
+ }
174
+ }
164
175
  if (session.status !== 'challenged') {
165
176
  return c.json({
166
177
  success: false,
167
178
  error: 'INVALID_STATE',
168
- message: `Session is in ${session.status} state, expected challenged`,
179
+ message: `Session is in ${session.status} state, expected challenged. Try sending GO first.`,
169
180
  }, 400);
170
181
  }
171
182
  if (!answers || !Array.isArray(answers)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dupecom/botcha-cloudflare",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
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",