@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 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
  [![npm version](https://img.shields.io/npm/v/@dupecom/botcha?color=00d4ff)](https://www.npmjs.com/package/@dupecom/botcha)
15
+ [![PyPI version](https://img.shields.io/pypi/v/botcha?color=00d4ff)](https://pypi.org/project/botcha/)
15
16
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
16
17
  [![AI Agents Only](https://img.shields.io/badge/contributors-AI%20agents%20only-ff6b6b)](./.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 issues a **speed challenge**: solve 5 SHA256 hashes in 500ms.
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
- - โœ… **AI agents** compute hashes instantly
59
- - โŒ **Humans** can't copy-paste fast enough
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
- Challenge: [645234, 891023, 334521, 789012, 456789]
63
- Task: SHA256 each number, return first 8 hex chars
64
- Time limit: 500ms```
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.3.0
318
+ X-Botcha-Version: 0.5.0
85
319
  X-Botcha-Enabled: true
86
- X-Botcha-Methods: speed-challenge,standard-challenge,web-bot-auth
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:3000/agent-only
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 55 minutes).
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,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,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"}
@@ -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.4.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 55 minutes).
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 55min = 3300000ms)
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 = 5 * 60 * 1000; // 5 minutes before expiry
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 challengeRes = await fetch(`${this.baseUrl}/v1/token`, {
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 || !verifyData.token) {
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
- // Cache the token - default expiry is 1 hour
96
- this.cachedToken = verifyData.token;
97
- this.tokenExpiresAt = Date.now() + 60 * 60 * 1000; // 1 hour from now
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 res = await fetch(`${this.baseUrl}/api/speed-challenge`, {
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 refreshing token and retrying once
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
- const token = await this.getToken();
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
- return {
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;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":"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"}
@@ -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.5.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": "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",