@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 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,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 55 minutes).
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;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,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.4.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 55 minutes).
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 55min = 3300000ms)
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 = 5 * 60 * 1000; // 5 minutes before expiry
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 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, {
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 || !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) {
95
117
  throw new Error('Failed to obtain token from verification');
96
118
  }
97
- // Cache the token - default expiry is 1 hour
98
- this.cachedToken = verifyData.token;
99
- 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;
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 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, {
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 refreshing token and retrying once
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
- 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
+ }
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
- return {
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;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE;QACV,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,YAAY,EAAE,CAAC;QACzB,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;CACH;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE;QACV,EAAE,EAAE,MAAM,CAAC;QACX,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE;QACV,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,YAAY,EAAE,CAAC;QACzB,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AAEH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,OAAO,GAAG,aAAa,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;IAClE,IAAI,EAAE,GAAG,CAAC;CACX;AAED,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,sBAAsB;IACrC,wCAAwC;IACxC,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,0DAA0D;IAC1D,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC;IACpE,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAC;IAC1C,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../lib/client/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dupecom/botcha",
3
- "version": "0.6.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/*"