@dupecom/botcha 0.4.6 → 0.6.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 CHANGED
@@ -53,15 +53,91 @@ app.listen(3000);
53
53
 
54
54
  ## How It Works
55
55
 
56
- BOTCHA issues a **speed challenge**: solve 5 SHA256 hashes in 500ms.
56
+ BOTCHA offers multiple challenge types. The default is **hybrid** — combining speed AND reasoning:
57
57
 
58
- - **AI agents** compute hashes instantly
59
- - **Humans** can't copy-paste fast enough
58
+ ### 🔥 Hybrid Challenge (Default)
59
+ Proves you can compute AND reason like an AI:
60
+ - **Speed**: Solve 5 SHA256 hashes in 500ms
61
+ - **Reasoning**: Answer 3 LLM-only questions
60
62
 
63
+ ### ⚡ Speed Challenge
64
+ Pure computational speed test:
65
+ - Solve 5 SHA256 hashes in 500ms
66
+ - Humans can't copy-paste fast enough
67
+
68
+ ### 🧠 Reasoning Challenge
69
+ Questions only LLMs can answer:
70
+ - Logic puzzles, analogies, code analysis
71
+ - 30 second time limit
72
+
73
+ ```
74
+ # Default hybrid challenge
75
+ GET /v1/challenges
76
+
77
+ # Specific challenge types
78
+ GET /v1/challenges?type=speed
79
+ GET /v1/challenges?type=hybrid
80
+ GET /v1/reasoning
81
+ ```
82
+
83
+ ## 🔄 SSE Streaming Flow (AI-Native)
84
+
85
+ For AI agents that prefer a **conversational handshake**, BOTCHA offers **Server-Sent Events (SSE)** streaming:
86
+
87
+ ### Why SSE for AI Agents?
88
+
89
+ - ⏱️ **Fair timing**: Timer starts when you say "GO", not on connection
90
+ - 💬 **Conversational**: Natural back-and-forth handshake protocol
91
+ - 📡 **Real-time**: Stream events as they happen, no polling
92
+
93
+ ### Event Sequence
94
+
95
+ ```
96
+ 1. welcome → Receive session ID and version
97
+ 2. instructions → Read what BOTCHA will test
98
+ 3. ready → Get endpoint to POST "GO"
99
+ 4. GO → Timer starts NOW (fair!)
100
+ 5. challenge → Receive problems and solve
101
+ 6. solve → POST your answers
102
+ 7. result → Get verification token
103
+ ```
104
+
105
+ ### Usage with SDK
106
+
107
+ ```typescript
108
+ import { BotchaStreamClient } from '@dupecom/botcha/client';
109
+
110
+ const client = new BotchaStreamClient('https://botcha.ai');
111
+ const token = await client.verify({
112
+ onInstruction: (msg) => console.log('BOTCHA:', msg),
113
+ });
114
+ // Token ready to use!
115
+ ```
116
+
117
+ ### SSE Event Flow Example
118
+
119
+ ```
120
+ event: welcome
121
+ data: {"session":"sess_123","version":"0.5.0"}
122
+
123
+ event: instructions
124
+ data: {"message":"I will test if you're an AI..."}
125
+
126
+ event: ready
127
+ data: {"message":"Send GO when ready","endpoint":"/v1/challenge/stream/sess_123"}
128
+
129
+ // POST {action:"go"} → starts timer
130
+ event: challenge
131
+ data: {"problems":[...],"timeLimit":500}
132
+
133
+ // POST {action:"solve",answers:[...]}
134
+ event: result
135
+ data: {"success":true,"verdict":"🤖 VERIFIED","token":"eyJ..."}
61
136
  ```
62
- Challenge: [645234, 891023, 334521, 789012, 456789]
63
- Task: SHA256 each number, return first 8 hex chars
64
- Time limit: 500ms```
137
+
138
+ **API Endpoints:**
139
+ - `GET /v1/challenge/stream` - Open SSE connection
140
+ - `POST /v1/challenge/stream/:session` - Send actions (go, solve)
65
141
 
66
142
  ## 🤖 AI Agent Discovery
67
143
 
@@ -81,9 +157,9 @@ BOTCHA is designed to be auto-discoverable by AI agents through multiple standar
81
157
  All responses include these headers for agent discovery:
82
158
 
83
159
  ```http
84
- X-Botcha-Version: 0.3.0
160
+ X-Botcha-Version: 0.5.0
85
161
  X-Botcha-Enabled: true
86
- X-Botcha-Methods: speed-challenge,standard-challenge,web-bot-auth
162
+ X-Botcha-Methods: hybrid-challenge,speed-challenge,reasoning-challenge,standard-challenge
87
163
  X-Botcha-Docs: https://botcha.ai/openapi.json
88
164
  ```
89
165
 
@@ -95,7 +171,7 @@ X-Botcha-Challenge-Type: speed
95
171
  X-Botcha-Time-Limit: 500
96
172
  ```
97
173
 
98
- `X-Botcha-Challenge-Type` can be `speed` or `standard` depending on the configured challenge mode.
174
+ `X-Botcha-Challenge-Type` can be `hybrid`, `speed`, `reasoning`, or `standard` depending on the configured challenge mode.
99
175
 
100
176
  **Example**: An agent can detect BOTCHA just by inspecting headers on ANY request:
101
177
 
@@ -146,12 +222,86 @@ botcha.verify({
146
222
  });
147
223
  ```
148
224
 
225
+ ## RTT-Aware Fairness ⚡
226
+
227
+ BOTCHA now automatically compensates for network latency, making speed challenges fair for agents on slow connections:
228
+
229
+ ```typescript
230
+ // Include client timestamp for RTT compensation
231
+ const clientTimestamp = Date.now();
232
+ const challenge = await fetch(`https://botcha.ai/v1/challenges?type=speed&ts=${clientTimestamp}`);
233
+ ```
234
+
235
+ **How it works:**
236
+ - 🕐 Client includes timestamp with challenge request
237
+ - 📡 Server measures RTT (Round-Trip Time)
238
+ - ⚖️ Timeout = 500ms (base) + (2 × RTT) + 100ms (buffer)
239
+ - 🎯 Fair challenges for agents worldwide
240
+
241
+ **Example RTT adjustments:**
242
+ - Local: 500ms (no adjustment)
243
+ - Good network (50ms RTT): 700ms timeout
244
+ - Slow network (300ms RTT): 1200ms timeout
245
+ - Satellite (500ms RTT): 1600ms timeout
246
+
247
+ **Response includes adjustment info:**
248
+ ```json
249
+ {
250
+ "challenge": { "timeLimit": "1200ms" },
251
+ "rtt_adjustment": {
252
+ "measuredRtt": 300,
253
+ "adjustedTimeout": 1200,
254
+ "explanation": "RTT: 300ms → Timeout: 500ms + (2×300ms) + 100ms = 1200ms"
255
+ }
256
+ }
257
+ ```
258
+
259
+ Humans still can't solve it (even with extra time), but legitimate AI agents get fair treatment regardless of their network connection.
260
+
261
+ ## Local Development
262
+
263
+ Run the full BOTCHA service locally with Wrangler (Cloudflare Workers runtime):
264
+
265
+ ```bash
266
+ # Clone and install
267
+ git clone https://github.com/dupe-com/botcha
268
+ cd botcha
269
+ bun install
270
+
271
+ # Run local dev server (uses Cloudflare Workers)
272
+ bun run dev
273
+
274
+ # Server runs at http://localhost:3001
275
+ ```
276
+
277
+ **What you get:**
278
+ - ✅ All API endpoints (`/api/*`, `/v1/*`, SSE streaming)
279
+ - ✅ Local KV storage emulation (challenges, rate limits)
280
+ - ✅ Hot reload on file changes
281
+ - ✅ Same code as production (no Express/CF Workers drift)
282
+
283
+ **Environment variables:**
284
+ - Local secrets in `packages/cloudflare-workers/.dev.vars`
285
+ - JWT_SECRET already configured for local dev
286
+
149
287
  ## Testing
150
288
 
151
289
  For development, you can bypass BOTCHA with a header:
152
290
 
153
291
  ```bash
154
- curl -H "X-Agent-Identity: MyTestAgent/1.0" http://localhost:3000/agent-only
292
+ curl -H "X-Agent-Identity: MyTestAgent/1.0" http://localhost:3001/agent-only
293
+ ```
294
+
295
+ Test the SSE streaming endpoint:
296
+
297
+ ```bash
298
+ # Connect to SSE stream
299
+ curl -N http://localhost:3001/v1/challenge/stream
300
+
301
+ # After receiving session ID, send GO action
302
+ curl -X POST http://localhost:3001/v1/challenge/stream/sess_123 \
303
+ -H "Content-Type: application/json" \
304
+ -d '{"action":"go"}'
155
305
  ```
156
306
 
157
307
  ## API Reference
@@ -1,5 +1,6 @@
1
- export type { SpeedProblem, BotchaClientOptions, ChallengeResponse, StandardChallengeResponse, VerifyResponse, TokenResponse, } from './types.js';
1
+ export type { SpeedProblem, BotchaClientOptions, ChallengeResponse, StandardChallengeResponse, VerifyResponse, TokenResponse, StreamSession, StreamEvent, Problem, VerifyResult, StreamChallengeOptions, } from './types.js';
2
2
  import type { BotchaClientOptions, VerifyResponse } from './types.js';
3
+ export { BotchaStreamClient } from './stream.js';
3
4
  /**
4
5
  * BOTCHA Client SDK for AI Agents
5
6
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../lib/client/index.ts"],"names":[],"mappings":"AAMA,YAAY,EACV,YAAY,EACZ,mBAAmB,EACnB,iBAAiB,EACjB,yBAAyB,EACzB,cAAc,EACd,aAAa,GACd,MAAM,YAAY,CAAC;AAEpB,OAAO,KAAK,EAEV,mBAAmB,EAGnB,cAAc,EAEf,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;;;GAcG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAU;IAC3B,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,cAAc,CAAuB;gBAEjC,OAAO,GAAE,mBAAwB;IAO7C;;;;;OAKG;IACH,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE;IAMnC;;;;;;;OAOG;IACG,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC;IAgEjC;;OAEG;IACH,UAAU,IAAI,IAAI;IAKlB;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IA4BlE;;OAEG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC;IAsBpE;;;;;;;;;OASG;IACG,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC;IA8E/D;;;;;;;;OAQG;IACG,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAUvD;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAIxD;AAED,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../lib/client/index.ts"],"names":[],"mappings":"AAMA,YAAY,EACV,YAAY,EACZ,mBAAmB,EACnB,iBAAiB,EACjB,yBAAyB,EACzB,cAAc,EACd,aAAa,EACb,aAAa,EACb,WAAW,EACX,OAAO,EACP,YAAY,EACZ,sBAAsB,GACvB,MAAM,YAAY,CAAC;AAEpB,OAAO,KAAK,EAEV,mBAAmB,EAGnB,cAAc,EAEf,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAU;IAC3B,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,cAAc,CAAuB;gBAEjC,OAAO,GAAE,mBAAwB;IAO7C;;;;;OAKG;IACH,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE;IAMnC;;;;;;;OAOG;IACG,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC;IAgEjC;;OAEG;IACH,UAAU,IAAI,IAAI;IAKlB;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IA4BlE;;OAEG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC;IAsBpE;;;;;;;;;OASG;IACG,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC;IA8E/D;;;;;;;;OAQG;IACG,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAUvD;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAIxD;AAED,eAAe,YAAY,CAAC"}
@@ -1,6 +1,8 @@
1
1
  import crypto from 'crypto';
2
2
  // SDK version - hardcoded since npm_package_version is unreliable when used as a library
3
3
  const SDK_VERSION = '0.4.0';
4
+ // Export stream client
5
+ export { BotchaStreamClient } from './stream.js';
4
6
  /**
5
7
  * BOTCHA Client SDK for AI Agents
6
8
  *
@@ -0,0 +1,78 @@
1
+ import type { StreamSession, StreamChallengeOptions } from './types.js';
2
+ /**
3
+ * BotchaStreamClient - SSE-based streaming challenge client
4
+ *
5
+ * Handles Server-Sent Events (SSE) streaming for interactive challenge flows.
6
+ * Automatically connects, solves challenges, and returns JWT tokens.
7
+ *
8
+ * Note: For Node.js usage, ensure you're running Node 18+ with native EventSource support,
9
+ * or install the 'eventsource' polyfill package.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { BotchaStreamClient } from '@dupecom/botcha/client';
14
+ *
15
+ * const client = new BotchaStreamClient();
16
+ *
17
+ * // Simple usage with built-in SHA256 solver
18
+ * const token = await client.verify();
19
+ *
20
+ * // Custom callbacks for monitoring
21
+ * const token = await client.verify({
22
+ * onInstruction: (msg) => console.log('Instruction:', msg),
23
+ * onChallenge: async (problems) => {
24
+ * // Custom solver logic
25
+ * return problems.map(p => customSolve(p.num));
26
+ * },
27
+ * onResult: (result) => console.log('Result:', result),
28
+ * timeout: 45000, // 45 seconds
29
+ * });
30
+ * ```
31
+ */
32
+ export declare class BotchaStreamClient {
33
+ private baseUrl;
34
+ private agentIdentity;
35
+ private eventSource;
36
+ constructor(baseUrl?: string);
37
+ /**
38
+ * Verify using streaming challenge flow
39
+ *
40
+ * Automatically connects to the streaming endpoint, handles the full challenge flow,
41
+ * and returns a JWT token on successful verification.
42
+ *
43
+ * @param options - Configuration options and callbacks
44
+ * @returns JWT token string
45
+ * @throws Error if verification fails or times out
46
+ */
47
+ verify(options?: StreamChallengeOptions): Promise<string>;
48
+ /**
49
+ * Connect to streaming endpoint and get session
50
+ *
51
+ * Lower-level method for manual control of the stream flow.
52
+ *
53
+ * @returns Promise resolving to StreamSession with session ID and URL
54
+ */
55
+ connect(): Promise<StreamSession>;
56
+ /**
57
+ * Send an action to the streaming session
58
+ *
59
+ * @param session - Session ID from connect()
60
+ * @param action - Action to send ('go' or solve object)
61
+ */
62
+ sendAction(session: string, action: 'go' | {
63
+ action: 'solve';
64
+ answers: string[];
65
+ }): Promise<void>;
66
+ /**
67
+ * Close the stream connection
68
+ */
69
+ close(): void;
70
+ /**
71
+ * Built-in SHA256 solver for speed challenges
72
+ *
73
+ * @param problems - Array of speed challenge problems
74
+ * @returns Array of SHA256 first 8 hex chars for each number
75
+ */
76
+ private solveSpeed;
77
+ }
78
+ //# sourceMappingURL=stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../../lib/client/stream.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,aAAa,EACb,sBAAsB,EAGvB,MAAM,YAAY,CAAC;AAKpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,WAAW,CAA4B;gBAEnC,OAAO,CAAC,EAAE,MAAM;IAK5B;;;;;;;;;OASG;IACG,MAAM,CAAC,OAAO,GAAE,sBAA2B,GAAG,OAAO,CAAC,MAAM,CAAC;IAsInE;;;;;;OAMG;IACG,OAAO,IAAI,OAAO,CAAC,aAAa,CAAC;IA+BvC;;;;;OAKG;IACG,UAAU,CACd,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,IAAI,GAAG;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,GACpD,OAAO,CAAC,IAAI,CAAC;IAqBhB;;OAEG;IACH,KAAK,IAAI,IAAI;IAOb;;;;;OAKG;IACH,OAAO,CAAC,UAAU;CAUnB"}
@@ -0,0 +1,252 @@
1
+ import crypto from 'crypto';
2
+ // SDK version
3
+ const SDK_VERSION = '0.4.0';
4
+ /**
5
+ * BotchaStreamClient - SSE-based streaming challenge client
6
+ *
7
+ * Handles Server-Sent Events (SSE) streaming for interactive challenge flows.
8
+ * Automatically connects, solves challenges, and returns JWT tokens.
9
+ *
10
+ * Note: For Node.js usage, ensure you're running Node 18+ with native EventSource support,
11
+ * or install the 'eventsource' polyfill package.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import { BotchaStreamClient } from '@dupecom/botcha/client';
16
+ *
17
+ * const client = new BotchaStreamClient();
18
+ *
19
+ * // Simple usage with built-in SHA256 solver
20
+ * const token = await client.verify();
21
+ *
22
+ * // Custom callbacks for monitoring
23
+ * const token = await client.verify({
24
+ * onInstruction: (msg) => console.log('Instruction:', msg),
25
+ * onChallenge: async (problems) => {
26
+ * // Custom solver logic
27
+ * return problems.map(p => customSolve(p.num));
28
+ * },
29
+ * onResult: (result) => console.log('Result:', result),
30
+ * timeout: 45000, // 45 seconds
31
+ * });
32
+ * ```
33
+ */
34
+ export class BotchaStreamClient {
35
+ baseUrl;
36
+ agentIdentity;
37
+ eventSource = null;
38
+ constructor(baseUrl) {
39
+ this.baseUrl = baseUrl || 'https://botcha.ai';
40
+ this.agentIdentity = `BotchaStreamClient/${SDK_VERSION}`;
41
+ }
42
+ /**
43
+ * Verify using streaming challenge flow
44
+ *
45
+ * Automatically connects to the streaming endpoint, handles the full challenge flow,
46
+ * and returns a JWT token on successful verification.
47
+ *
48
+ * @param options - Configuration options and callbacks
49
+ * @returns JWT token string
50
+ * @throws Error if verification fails or times out
51
+ */
52
+ async verify(options = {}) {
53
+ const { onInstruction, onChallenge, onResult, timeout = 30000, } = options;
54
+ return new Promise((resolve, reject) => {
55
+ const timeoutId = setTimeout(() => {
56
+ this.close();
57
+ reject(new Error('Stream verification timeout'));
58
+ }, timeout);
59
+ let sessionId = null;
60
+ // Connect to stream endpoint
61
+ const streamUrl = `${this.baseUrl}/v1/challenge/stream`;
62
+ // Check if EventSource is available
63
+ if (typeof EventSource === 'undefined') {
64
+ clearTimeout(timeoutId);
65
+ reject(new Error('EventSource not available. For Node.js, use Node 18+ or install eventsource polyfill.'));
66
+ return;
67
+ }
68
+ this.eventSource = new EventSource(streamUrl);
69
+ // Handle 'ready' event - send 'go' action
70
+ this.eventSource.addEventListener('ready', (event) => {
71
+ try {
72
+ const data = JSON.parse(event.data);
73
+ sessionId = data.session;
74
+ // Auto-send 'go' action to start challenge
75
+ if (sessionId) {
76
+ this.sendAction(sessionId, 'go').catch((err) => {
77
+ clearTimeout(timeoutId);
78
+ this.close();
79
+ reject(err);
80
+ });
81
+ }
82
+ }
83
+ catch (err) {
84
+ clearTimeout(timeoutId);
85
+ this.close();
86
+ reject(new Error(`Failed to parse ready event: ${err}`));
87
+ }
88
+ });
89
+ // Handle 'instruction' event
90
+ this.eventSource.addEventListener('instruction', (event) => {
91
+ try {
92
+ const data = JSON.parse(event.data);
93
+ if (onInstruction && data.message) {
94
+ onInstruction(data.message);
95
+ }
96
+ }
97
+ catch (err) {
98
+ console.warn('Failed to parse instruction event:', err);
99
+ }
100
+ });
101
+ // Handle 'challenge' event - solve and submit
102
+ this.eventSource.addEventListener('challenge', async (event) => {
103
+ try {
104
+ const data = JSON.parse(event.data);
105
+ const problems = data.problems || [];
106
+ // Get answers from callback or use default solver
107
+ let answers;
108
+ if (onChallenge) {
109
+ answers = await Promise.resolve(onChallenge(problems));
110
+ }
111
+ else {
112
+ // Default SHA256 solver for speed challenges
113
+ answers = this.solveSpeed(problems);
114
+ }
115
+ // Send solve action
116
+ if (sessionId) {
117
+ await this.sendAction(sessionId, { action: 'solve', answers });
118
+ }
119
+ }
120
+ catch (err) {
121
+ clearTimeout(timeoutId);
122
+ this.close();
123
+ reject(new Error(`Challenge solving failed: ${err}`));
124
+ }
125
+ });
126
+ // Handle 'result' event - final verification result
127
+ this.eventSource.addEventListener('result', (event) => {
128
+ try {
129
+ const data = JSON.parse(event.data);
130
+ if (onResult) {
131
+ onResult(data);
132
+ }
133
+ clearTimeout(timeoutId);
134
+ this.close();
135
+ if (data.success && data.token) {
136
+ resolve(data.token);
137
+ }
138
+ else {
139
+ reject(new Error(data.message || 'Verification failed'));
140
+ }
141
+ }
142
+ catch (err) {
143
+ clearTimeout(timeoutId);
144
+ this.close();
145
+ reject(new Error(`Failed to parse result event: ${err}`));
146
+ }
147
+ });
148
+ // Handle 'error' event
149
+ this.eventSource.addEventListener('error', (event) => {
150
+ try {
151
+ const data = JSON.parse(event.data);
152
+ clearTimeout(timeoutId);
153
+ this.close();
154
+ reject(new Error(data.message || 'Stream error occurred'));
155
+ }
156
+ catch (err) {
157
+ clearTimeout(timeoutId);
158
+ this.close();
159
+ reject(new Error('Stream connection error'));
160
+ }
161
+ });
162
+ // Handle connection errors
163
+ this.eventSource.onerror = () => {
164
+ clearTimeout(timeoutId);
165
+ this.close();
166
+ reject(new Error('EventSource connection failed'));
167
+ };
168
+ });
169
+ }
170
+ /**
171
+ * Connect to streaming endpoint and get session
172
+ *
173
+ * Lower-level method for manual control of the stream flow.
174
+ *
175
+ * @returns Promise resolving to StreamSession with session ID and URL
176
+ */
177
+ async connect() {
178
+ return new Promise((resolve, reject) => {
179
+ const streamUrl = `${this.baseUrl}/v1/challenge/stream`;
180
+ if (typeof EventSource === 'undefined') {
181
+ reject(new Error('EventSource not available. For Node.js, use Node 18+ or install eventsource polyfill.'));
182
+ return;
183
+ }
184
+ this.eventSource = new EventSource(streamUrl);
185
+ this.eventSource.addEventListener('ready', (event) => {
186
+ try {
187
+ const data = JSON.parse(event.data);
188
+ resolve({
189
+ session: data.session,
190
+ url: streamUrl,
191
+ });
192
+ }
193
+ catch (err) {
194
+ this.close();
195
+ reject(new Error(`Failed to parse ready event: ${err}`));
196
+ }
197
+ });
198
+ this.eventSource.onerror = () => {
199
+ this.close();
200
+ reject(new Error('EventSource connection failed'));
201
+ };
202
+ });
203
+ }
204
+ /**
205
+ * Send an action to the streaming session
206
+ *
207
+ * @param session - Session ID from connect()
208
+ * @param action - Action to send ('go' or solve object)
209
+ */
210
+ async sendAction(session, action) {
211
+ const actionUrl = `${this.baseUrl}/v1/challenge/stream`;
212
+ const body = typeof action === 'string'
213
+ ? { session, action }
214
+ : { session, ...action };
215
+ const response = await fetch(actionUrl, {
216
+ method: 'POST',
217
+ headers: {
218
+ 'Content-Type': 'application/json',
219
+ 'User-Agent': this.agentIdentity,
220
+ },
221
+ body: JSON.stringify(body),
222
+ });
223
+ if (!response.ok) {
224
+ throw new Error(`Action failed with status ${response.status} ${response.statusText}`);
225
+ }
226
+ }
227
+ /**
228
+ * Close the stream connection
229
+ */
230
+ close() {
231
+ if (this.eventSource) {
232
+ this.eventSource.close();
233
+ this.eventSource = null;
234
+ }
235
+ }
236
+ /**
237
+ * Built-in SHA256 solver for speed challenges
238
+ *
239
+ * @param problems - Array of speed challenge problems
240
+ * @returns Array of SHA256 first 8 hex chars for each number
241
+ */
242
+ solveSpeed(problems) {
243
+ return problems.map((problem) => {
244
+ const num = problem.num;
245
+ return crypto
246
+ .createHash('sha256')
247
+ .update(num.toString())
248
+ .digest('hex')
249
+ .substring(0, 8);
250
+ });
251
+ }
252
+ }
@@ -53,4 +53,35 @@ export interface TokenResponse {
53
53
  };
54
54
  nextStep?: string;
55
55
  }
56
+ /**
57
+ * Stream-related types for BotchaStreamClient
58
+ */
59
+ export interface StreamSession {
60
+ session: string;
61
+ url: string;
62
+ }
63
+ export interface StreamEvent {
64
+ event: 'ready' | 'instruction' | 'challenge' | 'result' | 'error';
65
+ data: any;
66
+ }
67
+ export interface Problem {
68
+ num: number;
69
+ operation?: string;
70
+ }
71
+ export interface VerifyResult {
72
+ success: boolean;
73
+ token?: string;
74
+ message?: string;
75
+ solveTimeMs?: number;
76
+ }
77
+ export interface StreamChallengeOptions {
78
+ /** Callback for instruction messages */
79
+ onInstruction?: (message: string) => void;
80
+ /** Callback to solve challenges - return answers array */
81
+ onChallenge?: (problems: Problem[]) => Promise<string[]> | string[];
82
+ /** Callback for final verification result */
83
+ onResult?: (result: VerifyResult) => void;
84
+ /** Timeout for the full verification flow in milliseconds (default: 30000) */
85
+ timeout?: number;
86
+ }
56
87
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../lib/client/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAExE,MAAM,WAAW,mBAAmB;IAClC,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mCAAmC;IACnC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE;QACV,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,YAAY,EAAE,CAAC;QACzB,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;CACH;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE;QACV,EAAE,EAAE,MAAM,CAAC;QACX,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE;QACV,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,YAAY,EAAE,CAAC;QACzB,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../lib/client/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAExE,MAAM,WAAW,mBAAmB;IAClC,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mCAAmC;IACnC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE;QACV,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,YAAY,EAAE,CAAC;QACzB,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;CACH;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE;QACV,EAAE,EAAE,MAAM,CAAC;QACX,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE;QACV,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,YAAY,EAAE,CAAC;QACzB,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AAEH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,OAAO,GAAG,aAAa,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;IAClE,IAAI,EAAE,GAAG,CAAC;CACX;AAED,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,sBAAsB;IACrC,wCAAwC;IACxC,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,0DAA0D;IAC1D,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC;IACpE,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAC;IAC1C,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB"}
@@ -4,7 +4,9 @@ export interface BotchaOptions {
4
4
  mode?: 'speed' | 'standard';
5
5
  /** Difficulty for standard mode */
6
6
  difficulty?: 'easy' | 'medium' | 'hard';
7
- /** Allow X-Agent-Identity header (for testing) */
7
+ /** Allow X-Agent-Identity header to bypass challenges (for development/testing ONLY).
8
+ * WARNING: Enabling this in production allows anyone to bypass verification with a single header.
9
+ * @default false */
8
10
  allowTestHeader?: boolean;
9
11
  /** Custom failure handler */
10
12
  onFailure?: (req: Request, res: Response, reason: string) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAI1D,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,IAAI,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IAC5B,mCAAmC;IACnC,UAAU,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;IACxC,kDAAkD;IAClD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,6BAA6B;IAC7B,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACnE;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAqED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,OAAO,GAAE,aAAkB,IAOlC,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,mBA8C9D;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAIlD;AAGD,eAAO,MAAM,MAAM;;;CAAoB,CAAC;AACxC,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAI1D,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,IAAI,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IAC5B,mCAAmC;IACnC,UAAU,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;IACxC;;yBAEqB;IACrB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,6BAA6B;IAC7B,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACnE;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAqED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,OAAO,GAAE,aAAkB,IAOlC,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,mBA+C9D;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAIlD;AAGD,eAAO,MAAM,MAAM;;;CAAoB,CAAC;AACxC,eAAe,MAAM,CAAC"}
package/dist/lib/index.js CHANGED
@@ -57,12 +57,13 @@ function verifySpeedChallenge(id, answers) {
57
57
  export function verify(options = {}) {
58
58
  const opts = {
59
59
  mode: 'speed',
60
- allowTestHeader: true,
60
+ allowTestHeader: false,
61
61
  ...options,
62
62
  };
63
63
  return async (req, res, next) => {
64
- // Check for test header (dev mode)
64
+ // Check for test header (dev mode ONLY — disabled by default)
65
65
  if (opts.allowTestHeader && req.headers['x-agent-identity']) {
66
+ console.warn('[botcha] WARNING: X-Agent-Identity bypass used. Disable allowTestHeader in production!');
66
67
  req.botcha = { verified: true, agent: req.headers['x-agent-identity'], method: 'header' };
67
68
  return next();
68
69
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dupecom/botcha",
3
- "version": "0.4.6",
3
+ "version": "0.6.0",
4
4
  "description": "Prove you're a bot. Humans need not apply. Reverse CAPTCHA for AI-only APIs.",
5
5
  "workspaces": [
6
6
  "packages/*"
@@ -25,7 +25,7 @@
25
25
  ],
26
26
  "scripts": {
27
27
  "build": "tsc",
28
- "dev": "tsx watch src/index.ts",
28
+ "dev": "cd packages/cloudflare-workers && bun run dev",
29
29
  "prepublishOnly": "bun run build",
30
30
  "test": "vitest",
31
31
  "test:ui": "vitest --ui",