@dupecom/botcha 0.5.0 โ 0.9.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 +378 -10
- package/dist/lib/client/index.d.ts +14 -2
- package/dist/lib/client/index.d.ts.map +1 -1
- package/dist/lib/client/index.js +106 -19
- 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 +41 -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
|
@@ -12,11 +12,14 @@
|
|
|
12
12
|
**BOTCHA** is a reverse CAPTCHA โ it verifies that visitors are AI agents, not humans. Perfect for AI-only APIs, agent marketplaces, and bot networks.
|
|
13
13
|
|
|
14
14
|
[](https://www.npmjs.com/package/@dupecom/botcha)
|
|
15
|
+
[](https://pypi.org/project/botcha/)
|
|
15
16
|
[](https://opensource.org/licenses/MIT)
|
|
16
17
|
[](./.github/CONTRIBUTING.md)
|
|
17
18
|
|
|
18
19
|
๐ **Website:** [botcha.ai](https://botcha.ai)
|
|
19
20
|
๐ฆ **npm:** [@dupecom/botcha](https://www.npmjs.com/package/@dupecom/botcha)
|
|
21
|
+
๐ **PyPI:** [botcha](https://pypi.org/project/botcha/)
|
|
22
|
+
๐ **Verify:** [@botcha/verify](./packages/verify/) (TS) ยท [botcha-verify](./packages/python-verify/) (Python)
|
|
20
23
|
๐ **OpenAPI:** [botcha.ai/openapi.json](https://botcha.ai/openapi.json)
|
|
21
24
|
|
|
22
25
|
## Why?
|
|
@@ -28,15 +31,26 @@ Use cases:
|
|
|
28
31
|
- ๐ AI-to-AI marketplaces
|
|
29
32
|
- ๐ซ Bot verification systems
|
|
30
33
|
- ๐ Autonomous agent authentication
|
|
34
|
+
- ๐ข Multi-tenant app isolation
|
|
31
35
|
|
|
32
36
|
## Install
|
|
33
37
|
|
|
38
|
+
### TypeScript/JavaScript
|
|
39
|
+
|
|
34
40
|
```bash
|
|
35
41
|
npm install @dupecom/botcha
|
|
36
42
|
```
|
|
37
43
|
|
|
44
|
+
### Python
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install botcha
|
|
48
|
+
```
|
|
49
|
+
|
|
38
50
|
## Quick Start
|
|
39
51
|
|
|
52
|
+
### TypeScript/JavaScript
|
|
53
|
+
|
|
40
54
|
```typescript
|
|
41
55
|
import express from 'express';
|
|
42
56
|
import { botcha } from '@dupecom/botcha';
|
|
@@ -51,17 +65,237 @@ app.get('/agent-only', botcha.verify(), (req, res) => {
|
|
|
51
65
|
app.listen(3000);
|
|
52
66
|
```
|
|
53
67
|
|
|
68
|
+
### Python
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from botcha import BotchaClient, solve_botcha
|
|
72
|
+
|
|
73
|
+
# Client SDK for AI agents
|
|
74
|
+
async with BotchaClient() as client:
|
|
75
|
+
# Get verification token
|
|
76
|
+
token = await client.get_token()
|
|
77
|
+
|
|
78
|
+
# Or auto-solve and fetch protected endpoints
|
|
79
|
+
response = await client.fetch("https://api.example.com/agent-only")
|
|
80
|
+
data = await response.json()
|
|
81
|
+
```
|
|
82
|
+
|
|
54
83
|
## How It Works
|
|
55
84
|
|
|
56
|
-
BOTCHA
|
|
85
|
+
BOTCHA offers multiple challenge types. The default is **hybrid** โ combining speed AND reasoning:
|
|
86
|
+
|
|
87
|
+
### ๐ฅ Hybrid Challenge (Default)
|
|
88
|
+
Proves you can compute AND reason like an AI:
|
|
89
|
+
- **Speed**: Solve 5 SHA256 hashes in 500ms
|
|
90
|
+
- **Reasoning**: Answer 3 LLM-only questions
|
|
91
|
+
|
|
92
|
+
### โก Speed Challenge
|
|
93
|
+
Pure computational speed test:
|
|
94
|
+
- Solve 5 SHA256 hashes in 500ms
|
|
95
|
+
- Humans can't copy-paste fast enough
|
|
96
|
+
|
|
97
|
+
### ๐ง Reasoning Challenge
|
|
98
|
+
Questions only LLMs can answer:
|
|
99
|
+
- Logic puzzles, analogies, code analysis
|
|
100
|
+
- 30 second time limit
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
# Default hybrid challenge
|
|
104
|
+
GET /v1/challenges
|
|
105
|
+
|
|
106
|
+
# Specific challenge types
|
|
107
|
+
GET /v1/challenges?type=speed
|
|
108
|
+
GET /v1/challenges?type=hybrid
|
|
109
|
+
GET /v1/reasoning
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## ๐ JWT Security (Production-Grade)
|
|
113
|
+
|
|
114
|
+
BOTCHA uses **OAuth2-style token rotation** with short-lived access tokens and long-lived refresh tokens.
|
|
115
|
+
|
|
116
|
+
> **๐ Full guide:** [doc/JWT-SECURITY.md](./doc/JWT-SECURITY.md) โ endpoint reference, request/response examples, audience scoping, IP binding, revocation, design decisions.
|
|
117
|
+
|
|
118
|
+
### Token Flow
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
1. Solve challenge โ receive access_token (5min) + refresh_token (1hr)
|
|
122
|
+
2. Use access_token for API calls
|
|
123
|
+
3. When access_token expires โ POST /v1/token/refresh with refresh_token
|
|
124
|
+
4. When compromised โ POST /v1/token/revoke to invalidate immediately
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Security Features
|
|
128
|
+
|
|
129
|
+
| Feature | What it does |
|
|
130
|
+
|---------|-------------|
|
|
131
|
+
| **5-minute access tokens** | Compromise window reduced from 1hr to 5min |
|
|
132
|
+
| **Refresh tokens (1hr)** | Renew access without re-solving challenges |
|
|
133
|
+
| **Audience (`aud`) scoping** | Token for `api.stripe.com` is rejected by `api.github.com` |
|
|
134
|
+
| **Client IP binding** | Optional โ solve on machine A, can't use on machine B |
|
|
135
|
+
| **Token revocation** | `POST /v1/token/revoke` โ KV-backed, fail-open |
|
|
136
|
+
| **JTI (JWT ID)** | Unique ID per token for revocation and audit |
|
|
137
|
+
|
|
138
|
+
### Quick Example
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
const client = new BotchaClient({
|
|
142
|
+
audience: 'https://api.example.com', // Scope token to this service
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Auto-handles: challenge โ token โ refresh โ retry on 401
|
|
146
|
+
const response = await client.fetch('https://api.example.com/agent-only');
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
async with BotchaClient(audience="https://api.example.com") as client:
|
|
151
|
+
response = await client.fetch("https://api.example.com/agent-only")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Token Endpoints
|
|
155
|
+
|
|
156
|
+
| Endpoint | Description |
|
|
157
|
+
|----------|-------------|
|
|
158
|
+
| `GET /v1/token` | Get challenge for token flow |
|
|
159
|
+
| `POST /v1/token/verify` | Submit solution โ receive `access_token` + `refresh_token` |
|
|
160
|
+
| `POST /v1/token/refresh` | Exchange `refresh_token` for new `access_token` |
|
|
161
|
+
| `POST /v1/token/revoke` | Invalidate any token immediately |
|
|
162
|
+
|
|
163
|
+
See **[JWT Security Guide](./doc/JWT-SECURITY.md)** for full request/response examples, `curl` commands, and server-side verification.
|
|
164
|
+
|
|
165
|
+
## ๐ข Multi-Tenant API Keys
|
|
166
|
+
|
|
167
|
+
BOTCHA supports **multi-tenant isolation** โ create separate apps with unique API keys for different services or environments.
|
|
168
|
+
|
|
169
|
+
### Why Multi-Tenant?
|
|
170
|
+
|
|
171
|
+
- **Isolation**: Each app gets its own rate limits and analytics
|
|
172
|
+
- **Security**: Tokens are scoped to specific apps via `app_id` claim
|
|
173
|
+
- **Flexibility**: Different services can use the same BOTCHA instance
|
|
174
|
+
- **Tracking**: Per-app usage analytics (coming soon)
|
|
175
|
+
|
|
176
|
+
### Creating an App
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# Create a new app
|
|
180
|
+
curl -X POST https://botcha.ai/v1/apps
|
|
181
|
+
|
|
182
|
+
# Returns (save the secret - it's only shown once!):
|
|
183
|
+
{
|
|
184
|
+
"app_id": "app_abc123",
|
|
185
|
+
"app_secret": "sk_xyz789",
|
|
186
|
+
"warning": "Save your secret now. It won't be shown again."
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Using Your App ID
|
|
191
|
+
|
|
192
|
+
All challenge and token endpoints accept an optional `app_id` query parameter:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# Get challenge with app_id
|
|
196
|
+
curl "https://botcha.ai/v1/challenges?app_id=app_abc123"
|
|
197
|
+
|
|
198
|
+
# Get token with app_id
|
|
199
|
+
curl "https://botcha.ai/v1/token?app_id=app_abc123"
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
When you solve a challenge with an `app_id`, the resulting token includes the `app_id` claim.
|
|
203
|
+
|
|
204
|
+
### SDK Support
|
|
205
|
+
|
|
206
|
+
**TypeScript:**
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
import { BotchaClient } from '@dupecom/botcha/client';
|
|
210
|
+
|
|
211
|
+
const client = new BotchaClient({
|
|
212
|
+
appId: 'app_abc123', // All requests will include this app_id
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const response = await client.fetch('https://api.example.com/agent-only');
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Python:**
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
from botcha import BotchaClient
|
|
222
|
+
|
|
223
|
+
async with BotchaClient(app_id="app_abc123") as client:
|
|
224
|
+
response = await client.fetch("https://api.example.com/agent-only")
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Rate Limiting
|
|
228
|
+
|
|
229
|
+
Each app gets its own rate limit bucket:
|
|
230
|
+
- Default rate limit: 100 requests/hour per app
|
|
231
|
+
- Rate limit key: `rate:app:{app_id}`
|
|
232
|
+
- Fail-open design: KV errors don't block requests
|
|
233
|
+
|
|
234
|
+
### Get App Info
|
|
57
235
|
|
|
58
|
-
|
|
59
|
-
|
|
236
|
+
```bash
|
|
237
|
+
# Get app details (secret is NOT included)
|
|
238
|
+
curl https://botcha.ai/v1/apps/app_abc123
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## ๐ SSE Streaming Flow (AI-Native)
|
|
242
|
+
|
|
243
|
+
For AI agents that prefer a **conversational handshake**, BOTCHA offers **Server-Sent Events (SSE)** streaming:
|
|
244
|
+
|
|
245
|
+
### Why SSE for AI Agents?
|
|
60
246
|
|
|
247
|
+
- โฑ๏ธ **Fair timing**: Timer starts when you say "GO", not on connection
|
|
248
|
+
- ๐ฌ **Conversational**: Natural back-and-forth handshake protocol
|
|
249
|
+
- ๐ก **Real-time**: Stream events as they happen, no polling
|
|
250
|
+
|
|
251
|
+
### Event Sequence
|
|
252
|
+
|
|
253
|
+
```
|
|
254
|
+
1. welcome โ Receive session ID and version
|
|
255
|
+
2. instructions โ Read what BOTCHA will test
|
|
256
|
+
3. ready โ Get endpoint to POST "GO"
|
|
257
|
+
4. GO โ Timer starts NOW (fair!)
|
|
258
|
+
5. challenge โ Receive problems and solve
|
|
259
|
+
6. solve โ POST your answers
|
|
260
|
+
7. result โ Get verification token
|
|
61
261
|
```
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
262
|
+
|
|
263
|
+
### Usage with SDK
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
import { BotchaStreamClient } from '@dupecom/botcha/client';
|
|
267
|
+
|
|
268
|
+
const client = new BotchaStreamClient('https://botcha.ai');
|
|
269
|
+
const token = await client.verify({
|
|
270
|
+
onInstruction: (msg) => console.log('BOTCHA:', msg),
|
|
271
|
+
});
|
|
272
|
+
// Token ready to use!
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### SSE Event Flow Example
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
event: welcome
|
|
279
|
+
data: {"session":"sess_123","version":"0.5.0"}
|
|
280
|
+
|
|
281
|
+
event: instructions
|
|
282
|
+
data: {"message":"I will test if you're an AI..."}
|
|
283
|
+
|
|
284
|
+
event: ready
|
|
285
|
+
data: {"message":"Send GO when ready","endpoint":"/v1/challenge/stream/sess_123"}
|
|
286
|
+
|
|
287
|
+
// POST {action:"go"} โ starts timer
|
|
288
|
+
event: challenge
|
|
289
|
+
data: {"problems":[...],"timeLimit":500}
|
|
290
|
+
|
|
291
|
+
// POST {action:"solve",answers:[...]}
|
|
292
|
+
event: result
|
|
293
|
+
data: {"success":true,"verdict":"๐ค VERIFIED","token":"eyJ..."}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**API Endpoints:**
|
|
297
|
+
- `GET /v1/challenge/stream` - Open SSE connection
|
|
298
|
+
- `POST /v1/challenge/stream/:session` - Send actions (go, solve)
|
|
65
299
|
|
|
66
300
|
## ๐ค AI Agent Discovery
|
|
67
301
|
|
|
@@ -81,9 +315,9 @@ BOTCHA is designed to be auto-discoverable by AI agents through multiple standar
|
|
|
81
315
|
All responses include these headers for agent discovery:
|
|
82
316
|
|
|
83
317
|
```http
|
|
84
|
-
X-Botcha-Version: 0.
|
|
318
|
+
X-Botcha-Version: 0.5.0
|
|
85
319
|
X-Botcha-Enabled: true
|
|
86
|
-
X-Botcha-Methods: speed-challenge,
|
|
320
|
+
X-Botcha-Methods: hybrid-challenge,speed-challenge,reasoning-challenge,standard-challenge
|
|
87
321
|
X-Botcha-Docs: https://botcha.ai/openapi.json
|
|
88
322
|
```
|
|
89
323
|
|
|
@@ -95,7 +329,7 @@ X-Botcha-Challenge-Type: speed
|
|
|
95
329
|
X-Botcha-Time-Limit: 500
|
|
96
330
|
```
|
|
97
331
|
|
|
98
|
-
`X-Botcha-Challenge-Type` can be `speed` or `standard` depending on the configured challenge mode.
|
|
332
|
+
`X-Botcha-Challenge-Type` can be `hybrid`, `speed`, `reasoning`, or `standard` depending on the configured challenge mode.
|
|
99
333
|
|
|
100
334
|
**Example**: An agent can detect BOTCHA just by inspecting headers on ANY request:
|
|
101
335
|
|
|
@@ -146,12 +380,86 @@ botcha.verify({
|
|
|
146
380
|
});
|
|
147
381
|
```
|
|
148
382
|
|
|
383
|
+
## RTT-Aware Fairness โก
|
|
384
|
+
|
|
385
|
+
BOTCHA now automatically compensates for network latency, making speed challenges fair for agents on slow connections:
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
// Include client timestamp for RTT compensation
|
|
389
|
+
const clientTimestamp = Date.now();
|
|
390
|
+
const challenge = await fetch(`https://botcha.ai/v1/challenges?type=speed&ts=${clientTimestamp}`);
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**How it works:**
|
|
394
|
+
- ๐ Client includes timestamp with challenge request
|
|
395
|
+
- ๐ก Server measures RTT (Round-Trip Time)
|
|
396
|
+
- โ๏ธ Timeout = 500ms (base) + (2 ร RTT) + 100ms (buffer)
|
|
397
|
+
- ๐ฏ Fair challenges for agents worldwide
|
|
398
|
+
|
|
399
|
+
**Example RTT adjustments:**
|
|
400
|
+
- Local: 500ms (no adjustment)
|
|
401
|
+
- Good network (50ms RTT): 700ms timeout
|
|
402
|
+
- Slow network (300ms RTT): 1200ms timeout
|
|
403
|
+
- Satellite (500ms RTT): 1600ms timeout
|
|
404
|
+
|
|
405
|
+
**Response includes adjustment info:**
|
|
406
|
+
```json
|
|
407
|
+
{
|
|
408
|
+
"challenge": { "timeLimit": "1200ms" },
|
|
409
|
+
"rtt_adjustment": {
|
|
410
|
+
"measuredRtt": 300,
|
|
411
|
+
"adjustedTimeout": 1200,
|
|
412
|
+
"explanation": "RTT: 300ms โ Timeout: 500ms + (2ร300ms) + 100ms = 1200ms"
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Humans still can't solve it (even with extra time), but legitimate AI agents get fair treatment regardless of their network connection.
|
|
418
|
+
|
|
419
|
+
## Local Development
|
|
420
|
+
|
|
421
|
+
Run the full BOTCHA service locally with Wrangler (Cloudflare Workers runtime):
|
|
422
|
+
|
|
423
|
+
```bash
|
|
424
|
+
# Clone and install
|
|
425
|
+
git clone https://github.com/dupe-com/botcha
|
|
426
|
+
cd botcha
|
|
427
|
+
bun install
|
|
428
|
+
|
|
429
|
+
# Run local dev server (uses Cloudflare Workers)
|
|
430
|
+
bun run dev
|
|
431
|
+
|
|
432
|
+
# Server runs at http://localhost:3001
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**What you get:**
|
|
436
|
+
- โ
All API endpoints (`/api/*`, `/v1/*`, SSE streaming)
|
|
437
|
+
- โ
Local KV storage emulation (challenges, rate limits)
|
|
438
|
+
- โ
Hot reload on file changes
|
|
439
|
+
- โ
Same code as production (no Express/CF Workers drift)
|
|
440
|
+
|
|
441
|
+
**Environment variables:**
|
|
442
|
+
- Local secrets in `packages/cloudflare-workers/.dev.vars`
|
|
443
|
+
- JWT_SECRET already configured for local dev
|
|
444
|
+
|
|
149
445
|
## Testing
|
|
150
446
|
|
|
151
447
|
For development, you can bypass BOTCHA with a header:
|
|
152
448
|
|
|
153
449
|
```bash
|
|
154
|
-
curl -H "X-Agent-Identity: MyTestAgent/1.0" http://localhost:
|
|
450
|
+
curl -H "X-Agent-Identity: MyTestAgent/1.0" http://localhost:3001/agent-only
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Test the SSE streaming endpoint:
|
|
454
|
+
|
|
455
|
+
```bash
|
|
456
|
+
# Connect to SSE stream
|
|
457
|
+
curl -N http://localhost:3001/v1/challenge/stream
|
|
458
|
+
|
|
459
|
+
# After receiving session ID, send GO action
|
|
460
|
+
curl -X POST http://localhost:3001/v1/challenge/stream/sess_123 \
|
|
461
|
+
-H "Content-Type: application/json" \
|
|
462
|
+
-d '{"action":"go"}'
|
|
155
463
|
```
|
|
156
464
|
|
|
157
465
|
## API Reference
|
|
@@ -202,6 +510,50 @@ You can use the library freely, report issues, and discuss features. To contribu
|
|
|
202
510
|
|
|
203
511
|
**See [CONTRIBUTING.md](./.github/CONTRIBUTING.md) for complete guidelines, solver code examples, agent setup instructions, and detailed workflows.**
|
|
204
512
|
|
|
513
|
+
## Server-Side Verification (for API Providers)
|
|
514
|
+
|
|
515
|
+
If you're building an API that accepts BOTCHA tokens from agents, use the verification SDKs:
|
|
516
|
+
|
|
517
|
+
### TypeScript (Express / Hono)
|
|
518
|
+
|
|
519
|
+
```bash
|
|
520
|
+
npm install @botcha/verify
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
```typescript
|
|
524
|
+
import { botchaVerify } from '@botcha/verify/express';
|
|
525
|
+
|
|
526
|
+
app.use('/api', botchaVerify({
|
|
527
|
+
secret: process.env.BOTCHA_SECRET!,
|
|
528
|
+
audience: 'https://api.example.com',
|
|
529
|
+
}));
|
|
530
|
+
|
|
531
|
+
app.get('/api/protected', (req, res) => {
|
|
532
|
+
console.log('Solve time:', req.botcha?.solveTime);
|
|
533
|
+
res.json({ message: 'Welcome, verified agent!' });
|
|
534
|
+
});
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### Python (FastAPI / Django)
|
|
538
|
+
|
|
539
|
+
```bash
|
|
540
|
+
pip install botcha-verify
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
```python
|
|
544
|
+
from fastapi import FastAPI, Depends
|
|
545
|
+
from botcha_verify.fastapi import BotchaVerify
|
|
546
|
+
|
|
547
|
+
app = FastAPI()
|
|
548
|
+
botcha = BotchaVerify(secret='your-secret-key')
|
|
549
|
+
|
|
550
|
+
@app.get('/api/data')
|
|
551
|
+
async def get_data(token = Depends(botcha)):
|
|
552
|
+
return {"solve_time": token.solve_time}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
> **Docs:** See [`@botcha/verify` README](./packages/verify/README.md) and [`botcha-verify` README](./packages/python-verify/README.md) for full API reference, Hono middleware, Django middleware, revocation checking, and custom error handlers.
|
|
556
|
+
|
|
205
557
|
## Client SDK (for AI Agents)
|
|
206
558
|
|
|
207
559
|
If you're building an AI agent that needs to access BOTCHA-protected APIs, use the client SDK:
|
|
@@ -260,6 +612,22 @@ const answers = solveBotcha([123456, 789012]);
|
|
|
260
612
|
// Returns: ['a1b2c3d4', 'e5f6g7h8']
|
|
261
613
|
```
|
|
262
614
|
|
|
615
|
+
**Python SDK:**
|
|
616
|
+
```python
|
|
617
|
+
from botcha import BotchaClient, solve_botcha
|
|
618
|
+
|
|
619
|
+
# Option 1: Auto-solve with client
|
|
620
|
+
async with BotchaClient() as client:
|
|
621
|
+
response = await client.fetch("https://api.example.com/agent-only")
|
|
622
|
+
data = await response.json()
|
|
623
|
+
|
|
624
|
+
# Option 2: Manual solve
|
|
625
|
+
answers = solve_botcha([123456, 789012])
|
|
626
|
+
# Returns: ['a1b2c3d4', 'e5f6g7h8']
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
> **Note:** The Python SDK is available on [PyPI](https://pypi.org/project/botcha/): `pip install botcha`
|
|
630
|
+
|
|
263
631
|
## License
|
|
264
632
|
|
|
265
633
|
MIT ยฉ [Dupe](https://dupe.com)
|
|
@@ -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
|
*
|
|
@@ -20,7 +21,10 @@ export declare class BotchaClient {
|
|
|
20
21
|
private agentIdentity;
|
|
21
22
|
private maxRetries;
|
|
22
23
|
private autoToken;
|
|
24
|
+
private appId?;
|
|
25
|
+
private opts;
|
|
23
26
|
private cachedToken;
|
|
27
|
+
private _refreshToken;
|
|
24
28
|
private tokenExpiresAt;
|
|
25
29
|
constructor(options?: BotchaClientOptions);
|
|
26
30
|
/**
|
|
@@ -33,12 +37,20 @@ export declare class BotchaClient {
|
|
|
33
37
|
/**
|
|
34
38
|
* Get a JWT token from the BOTCHA service using the token flow.
|
|
35
39
|
* Automatically solves the challenge and verifies to obtain a token.
|
|
36
|
-
* Token is cached until near expiry (refreshed at
|
|
40
|
+
* Token is cached until near expiry (refreshed at 4 minutes).
|
|
37
41
|
*
|
|
38
42
|
* @returns JWT token string
|
|
39
43
|
* @throws Error if token acquisition fails
|
|
40
44
|
*/
|
|
41
45
|
getToken(): Promise<string>;
|
|
46
|
+
/**
|
|
47
|
+
* Refresh the access token using the refresh token.
|
|
48
|
+
* Only works if a refresh token was obtained from a previous getToken() call.
|
|
49
|
+
*
|
|
50
|
+
* @returns New JWT access token string
|
|
51
|
+
* @throws Error if refresh fails or no refresh token available
|
|
52
|
+
*/
|
|
53
|
+
refreshToken(): Promise<string>;
|
|
42
54
|
/**
|
|
43
55
|
* Clear the cached token, forcing a refresh on the next request
|
|
44
56
|
*/
|
|
@@ -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,KAAK,CAAC,CAAS;IACvB,OAAO,CAAC,IAAI,CAAsB;IAClC,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,cAAc,CAAuB;gBAEjC,OAAO,GAAE,mBAAwB;IAS7C;;;;;OAKG;IACH,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE;IAMnC;;;;;;;OAOG;IACG,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC;IA2FjC;;;;;;OAMG;IACG,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAkCrC;;OAEG;IACH,UAAU,IAAI,IAAI;IAMlB;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IA+BlE;;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;IA2F/D;;;;;;;;OAQG;IACG,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAiBvD;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
|
-
const SDK_VERSION = '0.
|
|
3
|
+
const SDK_VERSION = '0.7.0';
|
|
4
|
+
// Export stream client
|
|
5
|
+
export { BotchaStreamClient } from './stream.js';
|
|
4
6
|
/**
|
|
5
7
|
* BOTCHA Client SDK for AI Agents
|
|
6
8
|
*
|
|
@@ -21,13 +23,18 @@ export class BotchaClient {
|
|
|
21
23
|
agentIdentity;
|
|
22
24
|
maxRetries;
|
|
23
25
|
autoToken;
|
|
26
|
+
appId;
|
|
27
|
+
opts;
|
|
24
28
|
cachedToken = null;
|
|
29
|
+
_refreshToken = null;
|
|
25
30
|
tokenExpiresAt = null;
|
|
26
31
|
constructor(options = {}) {
|
|
32
|
+
this.opts = options;
|
|
27
33
|
this.baseUrl = options.baseUrl || 'https://botcha.ai';
|
|
28
34
|
this.agentIdentity = options.agentIdentity || `BotchaClient/${SDK_VERSION}`;
|
|
29
35
|
this.maxRetries = options.maxRetries || 3;
|
|
30
36
|
this.autoToken = options.autoToken !== undefined ? options.autoToken : true;
|
|
37
|
+
this.appId = options.appId;
|
|
31
38
|
}
|
|
32
39
|
/**
|
|
33
40
|
* Solve a BOTCHA speed challenge
|
|
@@ -41,23 +48,26 @@ export class BotchaClient {
|
|
|
41
48
|
/**
|
|
42
49
|
* Get a JWT token from the BOTCHA service using the token flow.
|
|
43
50
|
* Automatically solves the challenge and verifies to obtain a token.
|
|
44
|
-
* Token is cached until near expiry (refreshed at
|
|
51
|
+
* Token is cached until near expiry (refreshed at 4 minutes).
|
|
45
52
|
*
|
|
46
53
|
* @returns JWT token string
|
|
47
54
|
* @throws Error if token acquisition fails
|
|
48
55
|
*/
|
|
49
56
|
async getToken() {
|
|
50
|
-
// Check if we have a valid cached token (refresh at
|
|
57
|
+
// Check if we have a valid cached token (refresh at 1 minute before expiry)
|
|
51
58
|
if (this.cachedToken && this.tokenExpiresAt) {
|
|
52
59
|
const now = Date.now();
|
|
53
60
|
const timeUntilExpiry = this.tokenExpiresAt - now;
|
|
54
|
-
const refreshThreshold =
|
|
61
|
+
const refreshThreshold = 1 * 60 * 1000; // 1 minute before expiry
|
|
55
62
|
if (timeUntilExpiry > refreshThreshold) {
|
|
56
63
|
return this.cachedToken;
|
|
57
64
|
}
|
|
58
65
|
}
|
|
59
66
|
// Step 1: Get challenge from GET /v1/token
|
|
60
|
-
const
|
|
67
|
+
const tokenUrl = this.appId
|
|
68
|
+
? `${this.baseUrl}/v1/token?app_id=${encodeURIComponent(this.appId)}`
|
|
69
|
+
: `${this.baseUrl}/v1/token`;
|
|
70
|
+
const challengeRes = await fetch(tokenUrl, {
|
|
61
71
|
headers: { 'User-Agent': this.agentIdentity },
|
|
62
72
|
});
|
|
63
73
|
if (!challengeRes.ok) {
|
|
@@ -74,27 +84,80 @@ export class BotchaClient {
|
|
|
74
84
|
}
|
|
75
85
|
const answers = this.solve(problems);
|
|
76
86
|
// Step 3: Submit solution to POST /v1/token/verify
|
|
87
|
+
const verifyBody = {
|
|
88
|
+
id: challengeData.challenge.id,
|
|
89
|
+
answers,
|
|
90
|
+
};
|
|
91
|
+
// Include audience if specified
|
|
92
|
+
if (this.opts.audience) {
|
|
93
|
+
verifyBody.audience = this.opts.audience;
|
|
94
|
+
}
|
|
95
|
+
// Include app_id if specified
|
|
96
|
+
if (this.appId) {
|
|
97
|
+
verifyBody.app_id = this.appId;
|
|
98
|
+
}
|
|
77
99
|
const verifyRes = await fetch(`${this.baseUrl}/v1/token/verify`, {
|
|
78
100
|
method: 'POST',
|
|
79
101
|
headers: {
|
|
80
102
|
'Content-Type': 'application/json',
|
|
81
103
|
'User-Agent': this.agentIdentity,
|
|
82
104
|
},
|
|
83
|
-
body: JSON.stringify(
|
|
84
|
-
id: challengeData.challenge.id,
|
|
85
|
-
answers,
|
|
86
|
-
}),
|
|
105
|
+
body: JSON.stringify(verifyBody),
|
|
87
106
|
});
|
|
88
107
|
if (!verifyRes.ok) {
|
|
89
108
|
throw new Error(`Token verification failed with status ${verifyRes.status} ${verifyRes.statusText}`);
|
|
90
109
|
}
|
|
91
110
|
const verifyData = await verifyRes.json();
|
|
92
|
-
if (!verifyData.success
|
|
111
|
+
if (!verifyData.success && !verifyData.verified) {
|
|
112
|
+
throw new Error('Failed to obtain token from verification');
|
|
113
|
+
}
|
|
114
|
+
// Extract access token (prefer access_token field, fall back to token for backward compat)
|
|
115
|
+
const accessToken = verifyData.access_token || verifyData.token;
|
|
116
|
+
if (!accessToken) {
|
|
93
117
|
throw new Error('Failed to obtain token from verification');
|
|
94
118
|
}
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
119
|
+
// Store refresh token if provided
|
|
120
|
+
if (verifyData.refresh_token) {
|
|
121
|
+
this._refreshToken = verifyData.refresh_token;
|
|
122
|
+
}
|
|
123
|
+
// Cache the token - use expires_in from response (in seconds), default to 5 minutes
|
|
124
|
+
const expiresInSeconds = verifyData.expires_in || 300; // Default to 5 minutes
|
|
125
|
+
this.cachedToken = accessToken;
|
|
126
|
+
this.tokenExpiresAt = Date.now() + expiresInSeconds * 1000;
|
|
127
|
+
return this.cachedToken;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Refresh the access token using the refresh token.
|
|
131
|
+
* Only works if a refresh token was obtained from a previous getToken() call.
|
|
132
|
+
*
|
|
133
|
+
* @returns New JWT access token string
|
|
134
|
+
* @throws Error if refresh fails or no refresh token available
|
|
135
|
+
*/
|
|
136
|
+
async refreshToken() {
|
|
137
|
+
if (!this._refreshToken) {
|
|
138
|
+
throw new Error('No refresh token available. Call getToken() first.');
|
|
139
|
+
}
|
|
140
|
+
const refreshRes = await fetch(`${this.baseUrl}/v1/token/refresh`, {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
headers: {
|
|
143
|
+
'Content-Type': 'application/json',
|
|
144
|
+
'User-Agent': this.agentIdentity,
|
|
145
|
+
},
|
|
146
|
+
body: JSON.stringify({
|
|
147
|
+
refresh_token: this._refreshToken,
|
|
148
|
+
}),
|
|
149
|
+
});
|
|
150
|
+
if (!refreshRes.ok) {
|
|
151
|
+
throw new Error(`Token refresh failed with status ${refreshRes.status} ${refreshRes.statusText}`);
|
|
152
|
+
}
|
|
153
|
+
const refreshData = await refreshRes.json();
|
|
154
|
+
if (!refreshData.access_token) {
|
|
155
|
+
throw new Error('Failed to obtain access token from refresh');
|
|
156
|
+
}
|
|
157
|
+
// Update cached token with new access token
|
|
158
|
+
this.cachedToken = refreshData.access_token;
|
|
159
|
+
const expiresInSeconds = refreshData.expires_in || 300; // Default to 5 minutes
|
|
160
|
+
this.tokenExpiresAt = Date.now() + expiresInSeconds * 1000;
|
|
98
161
|
return this.cachedToken;
|
|
99
162
|
}
|
|
100
163
|
/**
|
|
@@ -102,13 +165,17 @@ export class BotchaClient {
|
|
|
102
165
|
*/
|
|
103
166
|
clearToken() {
|
|
104
167
|
this.cachedToken = null;
|
|
168
|
+
this._refreshToken = null;
|
|
105
169
|
this.tokenExpiresAt = null;
|
|
106
170
|
}
|
|
107
171
|
/**
|
|
108
172
|
* Get and solve a challenge from BOTCHA service
|
|
109
173
|
*/
|
|
110
174
|
async solveChallenge() {
|
|
111
|
-
const
|
|
175
|
+
const challengeUrl = this.appId
|
|
176
|
+
? `${this.baseUrl}/api/speed-challenge?app_id=${encodeURIComponent(this.appId)}`
|
|
177
|
+
: `${this.baseUrl}/api/speed-challenge`;
|
|
178
|
+
const res = await fetch(challengeUrl, {
|
|
112
179
|
headers: { 'User-Agent': this.agentIdentity },
|
|
113
180
|
});
|
|
114
181
|
if (!res.ok) {
|
|
@@ -178,16 +245,31 @@ export class BotchaClient {
|
|
|
178
245
|
...init,
|
|
179
246
|
headers,
|
|
180
247
|
});
|
|
181
|
-
// Handle 401 by
|
|
248
|
+
// Handle 401 by trying refresh first, then full re-verify if refresh fails
|
|
182
249
|
if (response.status === 401 && this.autoToken) {
|
|
183
|
-
this.clearToken();
|
|
184
250
|
try {
|
|
185
|
-
|
|
251
|
+
// Try refresh token first if available
|
|
252
|
+
let token;
|
|
253
|
+
if (this._refreshToken) {
|
|
254
|
+
try {
|
|
255
|
+
token = await this.refreshToken();
|
|
256
|
+
}
|
|
257
|
+
catch (refreshError) {
|
|
258
|
+
// Refresh failed, clear tokens and do full re-verify
|
|
259
|
+
this.clearToken();
|
|
260
|
+
token = await this.getToken();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
// No refresh token, clear and do full re-verify
|
|
265
|
+
this.clearToken();
|
|
266
|
+
token = await this.getToken();
|
|
267
|
+
}
|
|
186
268
|
headers.set('Authorization', `Bearer ${token}`);
|
|
187
269
|
response = await fetch(url, { ...init, headers });
|
|
188
270
|
}
|
|
189
271
|
catch (error) {
|
|
190
|
-
// Token refresh failed, return the 401 response
|
|
272
|
+
// Token refresh/acquisition failed, return the 401 response
|
|
191
273
|
}
|
|
192
274
|
}
|
|
193
275
|
let retries = 0;
|
|
@@ -241,12 +323,17 @@ export class BotchaClient {
|
|
|
241
323
|
*/
|
|
242
324
|
async createHeaders() {
|
|
243
325
|
const { id, answers } = await this.solveChallenge();
|
|
244
|
-
|
|
326
|
+
const headers = {
|
|
245
327
|
'X-Botcha-Id': id,
|
|
246
328
|
'X-Botcha-Challenge-Id': id,
|
|
247
329
|
'X-Botcha-Answers': JSON.stringify(answers),
|
|
248
330
|
'User-Agent': this.agentIdentity,
|
|
249
331
|
};
|
|
332
|
+
// Include X-Botcha-App-Id header if appId is set
|
|
333
|
+
if (this.appId) {
|
|
334
|
+
headers['X-Botcha-App-Id'] = this.appId;
|
|
335
|
+
}
|
|
336
|
+
return headers;
|
|
250
337
|
}
|
|
251
338
|
}
|
|
252
339
|
/**
|
|
@@ -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
|
+
}
|
|
@@ -16,6 +16,10 @@ export interface BotchaClientOptions {
|
|
|
16
16
|
maxRetries?: number;
|
|
17
17
|
/** Enable automatic token acquisition and management (default: true) */
|
|
18
18
|
autoToken?: boolean;
|
|
19
|
+
/** Audience claim for token (optional) */
|
|
20
|
+
audience?: string;
|
|
21
|
+
/** Multi-tenant application ID (optional) */
|
|
22
|
+
appId?: string;
|
|
19
23
|
}
|
|
20
24
|
export interface ChallengeResponse {
|
|
21
25
|
success: boolean;
|
|
@@ -44,6 +48,10 @@ export interface VerifyResponse {
|
|
|
44
48
|
export interface TokenResponse {
|
|
45
49
|
success: boolean;
|
|
46
50
|
token: string | null;
|
|
51
|
+
access_token?: string;
|
|
52
|
+
refresh_token?: string;
|
|
53
|
+
expires_in?: number;
|
|
54
|
+
refresh_expires_in?: number;
|
|
47
55
|
expiresIn?: string;
|
|
48
56
|
challenge?: {
|
|
49
57
|
id: string;
|
|
@@ -52,5 +60,38 @@ export interface TokenResponse {
|
|
|
52
60
|
instructions: string;
|
|
53
61
|
};
|
|
54
62
|
nextStep?: string;
|
|
63
|
+
verified?: boolean;
|
|
64
|
+
solveTimeMs?: number;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Stream-related types for BotchaStreamClient
|
|
68
|
+
*/
|
|
69
|
+
export interface StreamSession {
|
|
70
|
+
session: string;
|
|
71
|
+
url: string;
|
|
72
|
+
}
|
|
73
|
+
export interface StreamEvent {
|
|
74
|
+
event: 'ready' | 'instruction' | 'challenge' | 'result' | 'error';
|
|
75
|
+
data: any;
|
|
76
|
+
}
|
|
77
|
+
export interface Problem {
|
|
78
|
+
num: number;
|
|
79
|
+
operation?: string;
|
|
80
|
+
}
|
|
81
|
+
export interface VerifyResult {
|
|
82
|
+
success: boolean;
|
|
83
|
+
token?: string;
|
|
84
|
+
message?: string;
|
|
85
|
+
solveTimeMs?: number;
|
|
86
|
+
}
|
|
87
|
+
export interface StreamChallengeOptions {
|
|
88
|
+
/** Callback for instruction messages */
|
|
89
|
+
onInstruction?: (message: string) => void;
|
|
90
|
+
/** Callback to solve challenges - return answers array */
|
|
91
|
+
onChallenge?: (problems: Problem[]) => Promise<string[]> | string[];
|
|
92
|
+
/** Callback for final verification result */
|
|
93
|
+
onResult?: (result: VerifyResult) => void;
|
|
94
|
+
/** Timeout for the full verification flow in milliseconds (default: 30000) */
|
|
95
|
+
timeout?: number;
|
|
55
96
|
}
|
|
56
97
|
//# 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;
|
|
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;IACpB,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6CAA6C;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;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,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,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;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;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.9.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",
|