@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 +160 -10
- package/dist/lib/client/index.d.ts +2 -1
- package/dist/lib/client/index.d.ts.map +1 -1
- package/dist/lib/client/index.js +2 -0
- package/dist/lib/client/stream.d.ts +78 -0
- package/dist/lib/client/stream.d.ts.map +1 -0
- package/dist/lib/client/stream.js +252 -0
- package/dist/lib/client/types.d.ts +31 -0
- package/dist/lib/client/types.d.ts.map +1 -1
- package/dist/lib/index.d.ts +3 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +3 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -53,15 +53,91 @@ app.listen(3000);
|
|
|
53
53
|
|
|
54
54
|
## How It Works
|
|
55
55
|
|
|
56
|
-
BOTCHA
|
|
56
|
+
BOTCHA offers multiple challenge types. The default is **hybrid** — combining speed AND reasoning:
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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.
|
|
160
|
+
X-Botcha-Version: 0.5.0
|
|
85
161
|
X-Botcha-Enabled: true
|
|
86
|
-
X-Botcha-Methods: speed-challenge,
|
|
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:
|
|
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,
|
|
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"}
|
package/dist/lib/client/index.js
CHANGED
|
@@ -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"}
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/index.d.ts.map
CHANGED
|
@@ -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
|
|
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:
|
|
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.
|
|
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": "
|
|
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",
|