@dupecom/botcha 0.6.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 +218 -0
- package/dist/lib/client/index.d.ts +12 -1
- package/dist/lib/client/index.d.ts.map +1 -1
- package/dist/lib/client/index.js +104 -19
- package/dist/lib/client/types.d.ts +10 -0
- package/dist/lib/client/types.d.ts.map +1 -1
- package/package.json +1 -1
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,6 +65,21 @@ 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
85
|
BOTCHA offers multiple challenge types. The default is **hybrid** โ combining speed AND reasoning:
|
|
@@ -80,6 +109,135 @@ GET /v1/challenges?type=hybrid
|
|
|
80
109
|
GET /v1/reasoning
|
|
81
110
|
```
|
|
82
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
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
# Get app details (secret is NOT included)
|
|
238
|
+
curl https://botcha.ai/v1/apps/app_abc123
|
|
239
|
+
```
|
|
240
|
+
|
|
83
241
|
## ๐ SSE Streaming Flow (AI-Native)
|
|
84
242
|
|
|
85
243
|
For AI agents that prefer a **conversational handshake**, BOTCHA offers **Server-Sent Events (SSE)** streaming:
|
|
@@ -352,6 +510,50 @@ You can use the library freely, report issues, and discuss features. To contribu
|
|
|
352
510
|
|
|
353
511
|
**See [CONTRIBUTING.md](./.github/CONTRIBUTING.md) for complete guidelines, solver code examples, agent setup instructions, and detailed workflows.**
|
|
354
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
|
+
|
|
355
557
|
## Client SDK (for AI Agents)
|
|
356
558
|
|
|
357
559
|
If you're building an AI agent that needs to access BOTCHA-protected APIs, use the client SDK:
|
|
@@ -410,6 +612,22 @@ const answers = solveBotcha([123456, 789012]);
|
|
|
410
612
|
// Returns: ['a1b2c3d4', 'e5f6g7h8']
|
|
411
613
|
```
|
|
412
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
|
+
|
|
413
631
|
## License
|
|
414
632
|
|
|
415
633
|
MIT ยฉ [Dupe](https://dupe.com)
|
|
@@ -21,7 +21,10 @@ export declare class BotchaClient {
|
|
|
21
21
|
private agentIdentity;
|
|
22
22
|
private maxRetries;
|
|
23
23
|
private autoToken;
|
|
24
|
+
private appId?;
|
|
25
|
+
private opts;
|
|
24
26
|
private cachedToken;
|
|
27
|
+
private _refreshToken;
|
|
25
28
|
private tokenExpiresAt;
|
|
26
29
|
constructor(options?: BotchaClientOptions);
|
|
27
30
|
/**
|
|
@@ -34,12 +37,20 @@ export declare class BotchaClient {
|
|
|
34
37
|
/**
|
|
35
38
|
* Get a JWT token from the BOTCHA service using the token flow.
|
|
36
39
|
* Automatically solves the challenge and verifies to obtain a token.
|
|
37
|
-
* Token is cached until near expiry (refreshed at
|
|
40
|
+
* Token is cached until near expiry (refreshed at 4 minutes).
|
|
38
41
|
*
|
|
39
42
|
* @returns JWT token string
|
|
40
43
|
* @throws Error if token acquisition fails
|
|
41
44
|
*/
|
|
42
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>;
|
|
43
54
|
/**
|
|
44
55
|
* Clear the cached token, forcing a refresh on the next request
|
|
45
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,EACb,aAAa,EACb,WAAW,EACX,OAAO,EACP,YAAY,EACZ,sBAAsB,GACvB,MAAM,YAAY,CAAC;AAEpB,OAAO,KAAK,EAEV,mBAAmB,EAGnB,cAAc,EAEf,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAU;IAC3B,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,cAAc,CAAuB;gBAEjC,OAAO,GAAE,mBAAwB;
|
|
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,6 @@
|
|
|
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
4
|
// Export stream client
|
|
5
5
|
export { BotchaStreamClient } from './stream.js';
|
|
6
6
|
/**
|
|
@@ -23,13 +23,18 @@ export class BotchaClient {
|
|
|
23
23
|
agentIdentity;
|
|
24
24
|
maxRetries;
|
|
25
25
|
autoToken;
|
|
26
|
+
appId;
|
|
27
|
+
opts;
|
|
26
28
|
cachedToken = null;
|
|
29
|
+
_refreshToken = null;
|
|
27
30
|
tokenExpiresAt = null;
|
|
28
31
|
constructor(options = {}) {
|
|
32
|
+
this.opts = options;
|
|
29
33
|
this.baseUrl = options.baseUrl || 'https://botcha.ai';
|
|
30
34
|
this.agentIdentity = options.agentIdentity || `BotchaClient/${SDK_VERSION}`;
|
|
31
35
|
this.maxRetries = options.maxRetries || 3;
|
|
32
36
|
this.autoToken = options.autoToken !== undefined ? options.autoToken : true;
|
|
37
|
+
this.appId = options.appId;
|
|
33
38
|
}
|
|
34
39
|
/**
|
|
35
40
|
* Solve a BOTCHA speed challenge
|
|
@@ -43,23 +48,26 @@ export class BotchaClient {
|
|
|
43
48
|
/**
|
|
44
49
|
* Get a JWT token from the BOTCHA service using the token flow.
|
|
45
50
|
* Automatically solves the challenge and verifies to obtain a token.
|
|
46
|
-
* Token is cached until near expiry (refreshed at
|
|
51
|
+
* Token is cached until near expiry (refreshed at 4 minutes).
|
|
47
52
|
*
|
|
48
53
|
* @returns JWT token string
|
|
49
54
|
* @throws Error if token acquisition fails
|
|
50
55
|
*/
|
|
51
56
|
async getToken() {
|
|
52
|
-
// 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)
|
|
53
58
|
if (this.cachedToken && this.tokenExpiresAt) {
|
|
54
59
|
const now = Date.now();
|
|
55
60
|
const timeUntilExpiry = this.tokenExpiresAt - now;
|
|
56
|
-
const refreshThreshold =
|
|
61
|
+
const refreshThreshold = 1 * 60 * 1000; // 1 minute before expiry
|
|
57
62
|
if (timeUntilExpiry > refreshThreshold) {
|
|
58
63
|
return this.cachedToken;
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
66
|
// Step 1: Get challenge from GET /v1/token
|
|
62
|
-
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, {
|
|
63
71
|
headers: { 'User-Agent': this.agentIdentity },
|
|
64
72
|
});
|
|
65
73
|
if (!challengeRes.ok) {
|
|
@@ -76,27 +84,80 @@ export class BotchaClient {
|
|
|
76
84
|
}
|
|
77
85
|
const answers = this.solve(problems);
|
|
78
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
|
+
}
|
|
79
99
|
const verifyRes = await fetch(`${this.baseUrl}/v1/token/verify`, {
|
|
80
100
|
method: 'POST',
|
|
81
101
|
headers: {
|
|
82
102
|
'Content-Type': 'application/json',
|
|
83
103
|
'User-Agent': this.agentIdentity,
|
|
84
104
|
},
|
|
85
|
-
body: JSON.stringify(
|
|
86
|
-
id: challengeData.challenge.id,
|
|
87
|
-
answers,
|
|
88
|
-
}),
|
|
105
|
+
body: JSON.stringify(verifyBody),
|
|
89
106
|
});
|
|
90
107
|
if (!verifyRes.ok) {
|
|
91
108
|
throw new Error(`Token verification failed with status ${verifyRes.status} ${verifyRes.statusText}`);
|
|
92
109
|
}
|
|
93
110
|
const verifyData = await verifyRes.json();
|
|
94
|
-
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) {
|
|
95
117
|
throw new Error('Failed to obtain token from verification');
|
|
96
118
|
}
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
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;
|
|
100
161
|
return this.cachedToken;
|
|
101
162
|
}
|
|
102
163
|
/**
|
|
@@ -104,13 +165,17 @@ export class BotchaClient {
|
|
|
104
165
|
*/
|
|
105
166
|
clearToken() {
|
|
106
167
|
this.cachedToken = null;
|
|
168
|
+
this._refreshToken = null;
|
|
107
169
|
this.tokenExpiresAt = null;
|
|
108
170
|
}
|
|
109
171
|
/**
|
|
110
172
|
* Get and solve a challenge from BOTCHA service
|
|
111
173
|
*/
|
|
112
174
|
async solveChallenge() {
|
|
113
|
-
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, {
|
|
114
179
|
headers: { 'User-Agent': this.agentIdentity },
|
|
115
180
|
});
|
|
116
181
|
if (!res.ok) {
|
|
@@ -180,16 +245,31 @@ export class BotchaClient {
|
|
|
180
245
|
...init,
|
|
181
246
|
headers,
|
|
182
247
|
});
|
|
183
|
-
// Handle 401 by
|
|
248
|
+
// Handle 401 by trying refresh first, then full re-verify if refresh fails
|
|
184
249
|
if (response.status === 401 && this.autoToken) {
|
|
185
|
-
this.clearToken();
|
|
186
250
|
try {
|
|
187
|
-
|
|
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
|
+
}
|
|
188
268
|
headers.set('Authorization', `Bearer ${token}`);
|
|
189
269
|
response = await fetch(url, { ...init, headers });
|
|
190
270
|
}
|
|
191
271
|
catch (error) {
|
|
192
|
-
// Token refresh failed, return the 401 response
|
|
272
|
+
// Token refresh/acquisition failed, return the 401 response
|
|
193
273
|
}
|
|
194
274
|
}
|
|
195
275
|
let retries = 0;
|
|
@@ -243,12 +323,17 @@ export class BotchaClient {
|
|
|
243
323
|
*/
|
|
244
324
|
async createHeaders() {
|
|
245
325
|
const { id, answers } = await this.solveChallenge();
|
|
246
|
-
|
|
326
|
+
const headers = {
|
|
247
327
|
'X-Botcha-Id': id,
|
|
248
328
|
'X-Botcha-Challenge-Id': id,
|
|
249
329
|
'X-Botcha-Answers': JSON.stringify(answers),
|
|
250
330
|
'User-Agent': this.agentIdentity,
|
|
251
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;
|
|
252
337
|
}
|
|
253
338
|
}
|
|
254
339
|
/**
|
|
@@ -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,6 +60,8 @@ export interface TokenResponse {
|
|
|
52
60
|
instructions: string;
|
|
53
61
|
};
|
|
54
62
|
nextStep?: string;
|
|
63
|
+
verified?: boolean;
|
|
64
|
+
solveTimeMs?: number;
|
|
55
65
|
}
|
|
56
66
|
/**
|
|
57
67
|
* Stream-related types for BotchaStreamClient
|
|
@@ -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":"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"}
|