@dupecom/botcha 0.3.7 → 0.4.3
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 +153 -2
- package/dist/lib/client/index.d.ts +90 -0
- package/dist/lib/client/index.d.ts.map +1 -0
- package/dist/lib/client/index.js +336 -0
- package/dist/lib/client/types.d.ts +56 -0
- package/dist/lib/client/types.d.ts.map +1 -0
- package/dist/lib/client/types.js +6 -0
- package/package.json +29 -7
package/README.md
CHANGED
|
@@ -13,11 +13,21 @@
|
|
|
13
13
|
|
|
14
14
|
[](https://www.npmjs.com/package/@dupecom/botcha)
|
|
15
15
|
[](https://opensource.org/licenses/MIT)
|
|
16
|
+
[](./.github/CONTRIBUTING.md)
|
|
16
17
|
|
|
17
18
|
🌐 **Website:** [botcha.ai](https://botcha.ai)
|
|
18
19
|
📦 **npm:** [@dupecom/botcha](https://www.npmjs.com/package/@dupecom/botcha)
|
|
19
20
|
🔌 **OpenAPI:** [botcha.ai/openapi.json](https://botcha.ai/openapi.json)
|
|
20
21
|
|
|
22
|
+
## Packages
|
|
23
|
+
|
|
24
|
+
| Package | Description | Install |
|
|
25
|
+
|---------|-------------|---------|
|
|
26
|
+
| [`@dupecom/botcha`](https://www.npmjs.com/package/@dupecom/botcha) | Core library + Express middleware | `npm install @dupecom/botcha` |
|
|
27
|
+
| [`@dupecom/botcha-cli`](https://www.npmjs.com/package/@dupecom/botcha-cli) | CLI tool for testing & debugging | `npm install -g @dupecom/botcha-cli` |
|
|
28
|
+
| [`@dupecom/botcha-langchain`](https://www.npmjs.com/package/@dupecom/botcha-langchain) | LangChain integration for AI agents | `npm install @dupecom/botcha-langchain` |
|
|
29
|
+
| [`@dupecom/botcha-cloudflare`](./packages/cloudflare-workers) | Cloudflare Workers runtime | `npm install @dupecom/botcha-cloudflare` |
|
|
30
|
+
|
|
21
31
|
## Why?
|
|
22
32
|
|
|
23
33
|
CAPTCHAs ask "Are you human?" — **BOTCHA asks "Are you an AI?"**
|
|
@@ -90,10 +100,12 @@ When a 403 is returned with a challenge:
|
|
|
90
100
|
|
|
91
101
|
```http
|
|
92
102
|
X-Botcha-Challenge-Id: abc123
|
|
93
|
-
X-Botcha-Challenge-Type:
|
|
94
|
-
X-Botcha-Time-Limit:
|
|
103
|
+
X-Botcha-Challenge-Type: speed
|
|
104
|
+
X-Botcha-Time-Limit: 500
|
|
95
105
|
```
|
|
96
106
|
|
|
107
|
+
`X-Botcha-Challenge-Type` can be `speed` or `standard` depending on the configured challenge mode.
|
|
108
|
+
|
|
97
109
|
**Example**: An agent can detect BOTCHA just by inspecting headers on ANY request:
|
|
98
110
|
|
|
99
111
|
```typescript
|
|
@@ -177,6 +189,30 @@ const answers = botcha.solve([645234, 891023, 334521]);
|
|
|
177
189
|
6. ✅ Access granted
|
|
178
190
|
```
|
|
179
191
|
|
|
192
|
+
## Cloudflare Workers
|
|
193
|
+
|
|
194
|
+
For edge deployment, use the Cloudflare Workers package:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
npm install @dupecom/botcha-cloudflare
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
// Uses Hono + Web Crypto API (no Node.js dependencies)
|
|
202
|
+
import app from '@dupecom/botcha-cloudflare';
|
|
203
|
+
export default app;
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Or deploy your own instance:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
cd packages/cloudflare-workers
|
|
210
|
+
npm install
|
|
211
|
+
npm run deploy # Deploys to your Cloudflare account
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The Workers package runs a v1 JWT-based flow and keeps legacy `/api/*` endpoints for backward compatibility. See [`packages/cloudflare-workers/README.md`](./packages/cloudflare-workers/README.md) for full docs.
|
|
215
|
+
|
|
180
216
|
## Philosophy
|
|
181
217
|
|
|
182
218
|
> "If a human writes a script to solve BOTCHA using an LLM... they've built an AI agent."
|
|
@@ -185,6 +221,20 @@ BOTCHA doesn't block all automation — it blocks *casual* human access while al
|
|
|
185
221
|
|
|
186
222
|
For cryptographic proof of agent identity, see [Web Bot Auth](https://datatracker.ietf.org/doc/html/draft-meunier-web-bot-auth-architecture).
|
|
187
223
|
|
|
224
|
+
## Contributing
|
|
225
|
+
|
|
226
|
+
**🤖 This is an AI-only open source project.** Code contributions must come from AI agents.
|
|
227
|
+
|
|
228
|
+
### For AI Agents
|
|
229
|
+
|
|
230
|
+
Fork the repo, make your changes, and open a PR. You'll receive a BOTCHA challenge (5 SHA256 hashes to solve in 5 minutes). Once verified, your PR can be reviewed and merged.
|
|
231
|
+
|
|
232
|
+
### For Humans
|
|
233
|
+
|
|
234
|
+
You can use the library freely, report issues, and discuss features. To contribute code, you'll need to work with an AI coding agent like [Cursor](https://cursor.com), [Claude Code](https://claude.ai), [Cline](https://cline.bot), [Aider](https://aider.chat), or [OpenClaw](https://openclaw.ai).
|
|
235
|
+
|
|
236
|
+
**See [CONTRIBUTING.md](./.github/CONTRIBUTING.md) for complete guidelines, solver code examples, agent setup instructions, and detailed workflows.**
|
|
237
|
+
|
|
188
238
|
## License
|
|
189
239
|
|
|
190
240
|
MIT © [Ramin](https://github.com/i8ramin)
|
|
@@ -192,3 +242,104 @@ MIT © [Ramin](https://github.com/i8ramin)
|
|
|
192
242
|
---
|
|
193
243
|
|
|
194
244
|
Built by [@i8ramin](https://github.com/i8ramin) and Choco 🐢
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Client SDK (for AI Agents)
|
|
249
|
+
|
|
250
|
+
If you're building an AI agent that needs to access BOTCHA-protected APIs, use the client SDK:
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
import { BotchaClient } from '@dupecom/botcha/client';
|
|
254
|
+
|
|
255
|
+
const client = new BotchaClient();
|
|
256
|
+
|
|
257
|
+
// Option 1: Auto-solve - fetches URL, solves any BOTCHA challenges automatically
|
|
258
|
+
const response = await client.fetch('https://api.example.com/agent-only');
|
|
259
|
+
const data = await response.json();
|
|
260
|
+
|
|
261
|
+
// Option 2: Pre-solve - get headers with solved challenge
|
|
262
|
+
const headers = await client.createHeaders();
|
|
263
|
+
const response = await fetch('https://api.example.com/agent-only', { headers });
|
|
264
|
+
|
|
265
|
+
// Option 3: Manual solve - solve challenge problems directly
|
|
266
|
+
const answers = client.solve([123456, 789012]);
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Client Options
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
const client = new BotchaClient({
|
|
273
|
+
baseUrl: 'https://botcha.ai', // BOTCHA service URL
|
|
274
|
+
agentIdentity: 'MyAgent/1.0', // User-Agent string
|
|
275
|
+
maxRetries: 3, // Max challenge solve attempts
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Framework Integration Examples
|
|
280
|
+
|
|
281
|
+
**OpenClaw / LangChain:**
|
|
282
|
+
```typescript
|
|
283
|
+
import { BotchaClient } from '@dupecom/botcha/client';
|
|
284
|
+
|
|
285
|
+
const botcha = new BotchaClient({ agentIdentity: 'MyLangChainAgent/1.0' });
|
|
286
|
+
|
|
287
|
+
// Use in your agent's HTTP tool
|
|
288
|
+
const tool = {
|
|
289
|
+
name: 'fetch_protected_api',
|
|
290
|
+
call: async (url: string) => {
|
|
291
|
+
const response = await botcha.fetch(url);
|
|
292
|
+
return response.json();
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Standalone Helper:**
|
|
298
|
+
```typescript
|
|
299
|
+
import { solveBotcha } from '@dupecom/botcha/client';
|
|
300
|
+
|
|
301
|
+
// Just solve the problems, handle the rest yourself
|
|
302
|
+
const answers = solveBotcha([123456, 789012]);
|
|
303
|
+
// Returns: ['a1b2c3d4', 'e5f6g7h8']
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## CLI Tool
|
|
307
|
+
|
|
308
|
+
Test and debug BOTCHA-protected endpoints from the command line:
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
# Test an endpoint
|
|
312
|
+
npx @dupecom/botcha-cli test https://api.example.com/agent-only
|
|
313
|
+
|
|
314
|
+
# Benchmark performance
|
|
315
|
+
npx @dupecom/botcha-cli benchmark https://api.example.com/agent-only --iterations 100
|
|
316
|
+
|
|
317
|
+
# Check headers
|
|
318
|
+
npx @dupecom/botcha-cli headers https://api.example.com
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
See [`packages/cli/README.md`](./packages/cli/README.md) for full CLI documentation.
|
|
322
|
+
|
|
323
|
+
## LangChain Integration
|
|
324
|
+
|
|
325
|
+
Give your LangChain agents automatic BOTCHA-solving abilities:
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
import { BotchaTool } from '@dupecom/botcha-langchain';
|
|
329
|
+
import { createReactAgent } from '@langchain/langgraph/prebuilt';
|
|
330
|
+
|
|
331
|
+
const agent = createReactAgent({
|
|
332
|
+
llm: new ChatOpenAI({ model: 'gpt-4' }),
|
|
333
|
+
tools: [
|
|
334
|
+
new BotchaTool({ baseUrl: 'https://api.botcha.ai' }),
|
|
335
|
+
// ... other tools
|
|
336
|
+
],
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Agent can now access BOTCHA-protected APIs automatically
|
|
340
|
+
await agent.invoke({
|
|
341
|
+
messages: [{ role: 'user', content: 'Access the bot-only API' }]
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
See [`packages/langchain/README.md`](./packages/langchain/README.md) for full documentation.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export type { SpeedProblem, BotchaClientOptions, ChallengeResponse, StandardChallengeResponse, VerifyResponse, TokenResponse, } from './types.js';
|
|
2
|
+
import type { BotchaClientOptions, VerifyResponse } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* BOTCHA Client SDK for AI Agents
|
|
5
|
+
*
|
|
6
|
+
* Automatically handles BOTCHA challenges when accessing protected endpoints.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { BotchaClient } from '@dupecom/botcha/client';
|
|
11
|
+
*
|
|
12
|
+
* const client = new BotchaClient();
|
|
13
|
+
*
|
|
14
|
+
* // Automatically solves challenges and retries
|
|
15
|
+
* const response = await client.fetch('https://api.example.com/agent-only');
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare class BotchaClient {
|
|
19
|
+
private baseUrl;
|
|
20
|
+
private agentIdentity;
|
|
21
|
+
private maxRetries;
|
|
22
|
+
private autoToken;
|
|
23
|
+
private cachedToken;
|
|
24
|
+
private tokenExpiresAt;
|
|
25
|
+
constructor(options?: BotchaClientOptions);
|
|
26
|
+
/**
|
|
27
|
+
* Solve a BOTCHA speed challenge
|
|
28
|
+
*
|
|
29
|
+
* @param problems - Array of numbers to hash
|
|
30
|
+
* @returns Array of SHA256 first 8 hex chars for each number
|
|
31
|
+
*/
|
|
32
|
+
solve(problems: number[]): string[];
|
|
33
|
+
/**
|
|
34
|
+
* Get a JWT token from the BOTCHA service using the token flow.
|
|
35
|
+
* Automatically solves the challenge and verifies to obtain a token.
|
|
36
|
+
* Token is cached until near expiry (refreshed at 55 minutes).
|
|
37
|
+
*
|
|
38
|
+
* @returns JWT token string
|
|
39
|
+
* @throws Error if token acquisition fails
|
|
40
|
+
*/
|
|
41
|
+
getToken(): Promise<string>;
|
|
42
|
+
/**
|
|
43
|
+
* Clear the cached token, forcing a refresh on the next request
|
|
44
|
+
*/
|
|
45
|
+
clearToken(): void;
|
|
46
|
+
/**
|
|
47
|
+
* Get and solve a challenge from BOTCHA service
|
|
48
|
+
*/
|
|
49
|
+
solveChallenge(): Promise<{
|
|
50
|
+
id: string;
|
|
51
|
+
answers: string[];
|
|
52
|
+
}>;
|
|
53
|
+
/**
|
|
54
|
+
* Verify a solved challenge
|
|
55
|
+
*/
|
|
56
|
+
verify(id: string, answers: string[]): Promise<VerifyResponse>;
|
|
57
|
+
/**
|
|
58
|
+
* Fetch a URL, automatically solving BOTCHA challenges if encountered.
|
|
59
|
+
* If autoToken is enabled (default), automatically acquires and uses JWT tokens.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* const response = await client.fetch('https://api.example.com/agent-only');
|
|
64
|
+
* const data = await response.json();
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
fetch(url: string, init?: RequestInit): Promise<Response>;
|
|
68
|
+
/**
|
|
69
|
+
* Create headers with pre-solved challenge for manual use
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* const headers = await client.createHeaders();
|
|
74
|
+
* const response = await fetch('https://api.example.com/agent-only', { headers });
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
createHeaders(): Promise<Record<string, string>>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Convenience function for one-off solves
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* const answers = solveBotcha([123456, 789012]);
|
|
85
|
+
* // Returns: ['a1b2c3d4', 'e5f6g7h8']
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export declare function solveBotcha(problems: number[]): string[];
|
|
89
|
+
export default BotchaClient;
|
|
90
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
// SDK version - hardcoded since npm_package_version is unreliable when used as a library
|
|
3
|
+
const SDK_VERSION = '0.4.0';
|
|
4
|
+
/**
|
|
5
|
+
* BOTCHA Client SDK for AI Agents
|
|
6
|
+
*
|
|
7
|
+
* Automatically handles BOTCHA challenges when accessing protected endpoints.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { BotchaClient } from '@dupecom/botcha/client';
|
|
12
|
+
*
|
|
13
|
+
* const client = new BotchaClient();
|
|
14
|
+
*
|
|
15
|
+
* // Automatically solves challenges and retries
|
|
16
|
+
* const response = await client.fetch('https://api.example.com/agent-only');
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export class BotchaClient {
|
|
20
|
+
baseUrl;
|
|
21
|
+
agentIdentity;
|
|
22
|
+
maxRetries;
|
|
23
|
+
autoToken;
|
|
24
|
+
cachedToken = null;
|
|
25
|
+
tokenExpiresAt = null;
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
this.baseUrl = options.baseUrl || 'https://botcha.ai';
|
|
28
|
+
this.agentIdentity = options.agentIdentity || `BotchaClient/${SDK_VERSION}`;
|
|
29
|
+
this.maxRetries = options.maxRetries || 3;
|
|
30
|
+
this.autoToken = options.autoToken !== undefined ? options.autoToken : true;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Solve a BOTCHA speed challenge
|
|
34
|
+
*
|
|
35
|
+
* @param problems - Array of numbers to hash
|
|
36
|
+
* @returns Array of SHA256 first 8 hex chars for each number
|
|
37
|
+
*/
|
|
38
|
+
solve(problems) {
|
|
39
|
+
return problems.map(num => crypto.createHash('sha256').update(num.toString()).digest('hex').substring(0, 8));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get a JWT token from the BOTCHA service using the token flow.
|
|
43
|
+
* Automatically solves the challenge and verifies to obtain a token.
|
|
44
|
+
* Token is cached until near expiry (refreshed at 55 minutes).
|
|
45
|
+
*
|
|
46
|
+
* @returns JWT token string
|
|
47
|
+
* @throws Error if token acquisition fails
|
|
48
|
+
*/
|
|
49
|
+
async getToken() {
|
|
50
|
+
// Check if we have a valid cached token (refresh at 55min = 3300000ms)
|
|
51
|
+
if (this.cachedToken && this.tokenExpiresAt) {
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
const timeUntilExpiry = this.tokenExpiresAt - now;
|
|
54
|
+
const refreshThreshold = 5 * 60 * 1000; // 5 minutes before expiry
|
|
55
|
+
if (timeUntilExpiry > refreshThreshold) {
|
|
56
|
+
return this.cachedToken;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Step 1: Get challenge from GET /v1/token
|
|
60
|
+
const challengeRes = await fetch(`${this.baseUrl}/v1/token`, {
|
|
61
|
+
headers: { 'User-Agent': this.agentIdentity },
|
|
62
|
+
});
|
|
63
|
+
if (!challengeRes.ok) {
|
|
64
|
+
throw new Error(`Token request failed with status ${challengeRes.status} ${challengeRes.statusText}`);
|
|
65
|
+
}
|
|
66
|
+
const challengeData = await challengeRes.json();
|
|
67
|
+
if (!challengeData.challenge) {
|
|
68
|
+
throw new Error('No challenge provided in token response');
|
|
69
|
+
}
|
|
70
|
+
// Step 2: Solve the challenge
|
|
71
|
+
const problems = normalizeProblems(challengeData.challenge.problems);
|
|
72
|
+
if (!problems) {
|
|
73
|
+
throw new Error('Invalid challenge problems format');
|
|
74
|
+
}
|
|
75
|
+
const answers = this.solve(problems);
|
|
76
|
+
// Step 3: Submit solution to POST /v1/token/verify
|
|
77
|
+
const verifyRes = await fetch(`${this.baseUrl}/v1/token/verify`, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: {
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
'User-Agent': this.agentIdentity,
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
id: challengeData.challenge.id,
|
|
85
|
+
answers,
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
88
|
+
if (!verifyRes.ok) {
|
|
89
|
+
throw new Error(`Token verification failed with status ${verifyRes.status} ${verifyRes.statusText}`);
|
|
90
|
+
}
|
|
91
|
+
const verifyData = await verifyRes.json();
|
|
92
|
+
if (!verifyData.success || !verifyData.token) {
|
|
93
|
+
throw new Error('Failed to obtain token from verification');
|
|
94
|
+
}
|
|
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
|
|
98
|
+
return this.cachedToken;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Clear the cached token, forcing a refresh on the next request
|
|
102
|
+
*/
|
|
103
|
+
clearToken() {
|
|
104
|
+
this.cachedToken = null;
|
|
105
|
+
this.tokenExpiresAt = null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get and solve a challenge from BOTCHA service
|
|
109
|
+
*/
|
|
110
|
+
async solveChallenge() {
|
|
111
|
+
const res = await fetch(`${this.baseUrl}/api/speed-challenge`, {
|
|
112
|
+
headers: { 'User-Agent': this.agentIdentity },
|
|
113
|
+
});
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
throw new Error(`Challenge request failed with status ${res.status} ${res.statusText}`);
|
|
116
|
+
}
|
|
117
|
+
const contentType = res.headers.get('content-type') || '';
|
|
118
|
+
if (!contentType.toLowerCase().includes('application/json')) {
|
|
119
|
+
throw new Error('Expected JSON response for challenge request');
|
|
120
|
+
}
|
|
121
|
+
const data = await res.json();
|
|
122
|
+
if (!data.success || !data.challenge) {
|
|
123
|
+
throw new Error('Failed to get challenge');
|
|
124
|
+
}
|
|
125
|
+
const problems = normalizeProblems(data.challenge.problems);
|
|
126
|
+
if (!problems) {
|
|
127
|
+
throw new Error('Invalid challenge problems format');
|
|
128
|
+
}
|
|
129
|
+
const answers = this.solve(problems);
|
|
130
|
+
return { id: data.challenge.id, answers };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Verify a solved challenge
|
|
134
|
+
*/
|
|
135
|
+
async verify(id, answers) {
|
|
136
|
+
const res = await fetch(`${this.baseUrl}/api/speed-challenge`, {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: {
|
|
139
|
+
'Content-Type': 'application/json',
|
|
140
|
+
'User-Agent': this.agentIdentity,
|
|
141
|
+
},
|
|
142
|
+
body: JSON.stringify({ id, answers }),
|
|
143
|
+
});
|
|
144
|
+
if (!res.ok) {
|
|
145
|
+
throw new Error(`Verification request failed with status ${res.status} ${res.statusText}`);
|
|
146
|
+
}
|
|
147
|
+
const contentType = res.headers.get('content-type') || '';
|
|
148
|
+
if (!contentType.toLowerCase().includes('application/json')) {
|
|
149
|
+
throw new Error('Expected JSON response for verification request');
|
|
150
|
+
}
|
|
151
|
+
return await res.json();
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Fetch a URL, automatically solving BOTCHA challenges if encountered.
|
|
155
|
+
* If autoToken is enabled (default), automatically acquires and uses JWT tokens.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```typescript
|
|
159
|
+
* const response = await client.fetch('https://api.example.com/agent-only');
|
|
160
|
+
* const data = await response.json();
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
async fetch(url, init) {
|
|
164
|
+
const headers = new Headers(init?.headers);
|
|
165
|
+
headers.set('User-Agent', this.agentIdentity);
|
|
166
|
+
// If autoToken is enabled, try to use token-based auth
|
|
167
|
+
if (this.autoToken) {
|
|
168
|
+
try {
|
|
169
|
+
const token = await this.getToken();
|
|
170
|
+
headers.set('Authorization', `Bearer ${token}`);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
// If token acquisition fails, fall back to challenge header method
|
|
174
|
+
console.warn('Failed to acquire token, falling back to challenge headers:', error);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
let response = await fetch(url, {
|
|
178
|
+
...init,
|
|
179
|
+
headers,
|
|
180
|
+
});
|
|
181
|
+
// Handle 401 by refreshing token and retrying once
|
|
182
|
+
if (response.status === 401 && this.autoToken) {
|
|
183
|
+
this.clearToken();
|
|
184
|
+
try {
|
|
185
|
+
const token = await this.getToken();
|
|
186
|
+
headers.set('Authorization', `Bearer ${token}`);
|
|
187
|
+
response = await fetch(url, { ...init, headers });
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
// Token refresh failed, return the 401 response
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
let retries = 0;
|
|
194
|
+
// Fall back to challenge header method for 403 responses
|
|
195
|
+
while (response.status === 403 && retries < this.maxRetries) {
|
|
196
|
+
// Clone response before reading body to preserve it for the caller
|
|
197
|
+
const clonedResponse = response.clone();
|
|
198
|
+
const body = await clonedResponse.json().catch(() => null);
|
|
199
|
+
// Check if this is a BOTCHA challenge
|
|
200
|
+
const challenge = body?.challenge;
|
|
201
|
+
if (!challenge) {
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
if (!canRetryBody(init?.body)) {
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
const retryHeaders = new Headers(init?.headers);
|
|
208
|
+
retryHeaders.set('User-Agent', this.agentIdentity);
|
|
209
|
+
if (challenge?.problems && Array.isArray(challenge.problems)) {
|
|
210
|
+
const problems = normalizeProblems(challenge.problems);
|
|
211
|
+
if (!problems) {
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
const answers = this.solve(problems);
|
|
215
|
+
retryHeaders.set('X-Botcha-Id', challenge.id);
|
|
216
|
+
retryHeaders.set('X-Botcha-Challenge-Id', challenge.id);
|
|
217
|
+
retryHeaders.set('X-Botcha-Answers', JSON.stringify(answers));
|
|
218
|
+
retryHeaders.set('X-Botcha-Solution', JSON.stringify(answers));
|
|
219
|
+
}
|
|
220
|
+
else if (challenge?.puzzle && typeof challenge.puzzle === 'string') {
|
|
221
|
+
const solution = solveStandardPuzzle(challenge.puzzle);
|
|
222
|
+
retryHeaders.set('X-Botcha-Challenge-Id', challenge.id);
|
|
223
|
+
retryHeaders.set('X-Botcha-Solution', solution);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
response = await fetch(url, { ...init, headers: retryHeaders });
|
|
229
|
+
retries++;
|
|
230
|
+
}
|
|
231
|
+
return response;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Create headers with pre-solved challenge for manual use
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```typescript
|
|
238
|
+
* const headers = await client.createHeaders();
|
|
239
|
+
* const response = await fetch('https://api.example.com/agent-only', { headers });
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
async createHeaders() {
|
|
243
|
+
const { id, answers } = await this.solveChallenge();
|
|
244
|
+
return {
|
|
245
|
+
'X-Botcha-Id': id,
|
|
246
|
+
'X-Botcha-Challenge-Id': id,
|
|
247
|
+
'X-Botcha-Answers': JSON.stringify(answers),
|
|
248
|
+
'User-Agent': this.agentIdentity,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Convenience function for one-off solves
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```typescript
|
|
257
|
+
* const answers = solveBotcha([123456, 789012]);
|
|
258
|
+
* // Returns: ['a1b2c3d4', 'e5f6g7h8']
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
export function solveBotcha(problems) {
|
|
262
|
+
return problems.map(num => crypto.createHash('sha256').update(num.toString()).digest('hex').substring(0, 8));
|
|
263
|
+
}
|
|
264
|
+
export default BotchaClient;
|
|
265
|
+
function normalizeProblems(problems) {
|
|
266
|
+
if (!Array.isArray(problems))
|
|
267
|
+
return null;
|
|
268
|
+
const numbers = [];
|
|
269
|
+
for (const problem of problems) {
|
|
270
|
+
if (typeof problem === 'number') {
|
|
271
|
+
numbers.push(problem);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (typeof problem === 'object' && problem !== null && typeof problem.num === 'number') {
|
|
275
|
+
numbers.push(problem.num);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
return numbers;
|
|
281
|
+
}
|
|
282
|
+
function solveStandardPuzzle(puzzle) {
|
|
283
|
+
const primeMatch = puzzle.match(/first\s+(\d+)\s+prime/i);
|
|
284
|
+
if (!primeMatch) {
|
|
285
|
+
throw new Error('Unsupported standard challenge puzzle format');
|
|
286
|
+
}
|
|
287
|
+
const primeCount = Number.parseInt(primeMatch[1], 10);
|
|
288
|
+
if (!Number.isFinite(primeCount) || primeCount <= 0) {
|
|
289
|
+
throw new Error('Invalid prime count in puzzle');
|
|
290
|
+
}
|
|
291
|
+
const primes = generatePrimes(primeCount);
|
|
292
|
+
const concatenated = primes.join('');
|
|
293
|
+
const hash = crypto.createHash('sha256').update(concatenated).digest('hex');
|
|
294
|
+
return hash.substring(0, 16);
|
|
295
|
+
}
|
|
296
|
+
function generatePrimes(count) {
|
|
297
|
+
const primes = [];
|
|
298
|
+
let num = 2;
|
|
299
|
+
while (primes.length < count) {
|
|
300
|
+
if (isPrime(num)) {
|
|
301
|
+
primes.push(num);
|
|
302
|
+
}
|
|
303
|
+
num++;
|
|
304
|
+
}
|
|
305
|
+
return primes;
|
|
306
|
+
}
|
|
307
|
+
function isPrime(n) {
|
|
308
|
+
if (n < 2)
|
|
309
|
+
return false;
|
|
310
|
+
if (n === 2)
|
|
311
|
+
return true;
|
|
312
|
+
if (n % 2 === 0)
|
|
313
|
+
return false;
|
|
314
|
+
for (let i = 3; i <= Math.sqrt(n); i += 2) {
|
|
315
|
+
if (n % i === 0)
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
function canRetryBody(body) {
|
|
321
|
+
if (body == null)
|
|
322
|
+
return true;
|
|
323
|
+
if (typeof body === 'string')
|
|
324
|
+
return true;
|
|
325
|
+
if (body instanceof URLSearchParams)
|
|
326
|
+
return true;
|
|
327
|
+
if (body instanceof ArrayBuffer)
|
|
328
|
+
return true;
|
|
329
|
+
if (ArrayBuffer.isView(body))
|
|
330
|
+
return true;
|
|
331
|
+
if (typeof Blob !== 'undefined' && body instanceof Blob)
|
|
332
|
+
return true;
|
|
333
|
+
if (typeof FormData !== 'undefined' && body instanceof FormData)
|
|
334
|
+
return true;
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BOTCHA Client SDK Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Types for the BotchaClient SDK including challenges, tokens, and configuration.
|
|
5
|
+
*/
|
|
6
|
+
export type SpeedProblem = number | {
|
|
7
|
+
num: number;
|
|
8
|
+
operation?: string;
|
|
9
|
+
};
|
|
10
|
+
export interface BotchaClientOptions {
|
|
11
|
+
/** Base URL of BOTCHA service (default: https://botcha.ai) */
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
/** Custom identity header value */
|
|
14
|
+
agentIdentity?: string;
|
|
15
|
+
/** Max retries for challenge solving */
|
|
16
|
+
maxRetries?: number;
|
|
17
|
+
/** Enable automatic token acquisition and management (default: true) */
|
|
18
|
+
autoToken?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface ChallengeResponse {
|
|
21
|
+
success: boolean;
|
|
22
|
+
challenge?: {
|
|
23
|
+
id: string;
|
|
24
|
+
problems: SpeedProblem[];
|
|
25
|
+
timeLimit: number;
|
|
26
|
+
instructions: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export interface StandardChallengeResponse {
|
|
30
|
+
success: boolean;
|
|
31
|
+
challenge?: {
|
|
32
|
+
id: string;
|
|
33
|
+
puzzle: string;
|
|
34
|
+
timeLimit: number;
|
|
35
|
+
hint?: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export interface VerifyResponse {
|
|
39
|
+
success: boolean;
|
|
40
|
+
message: string;
|
|
41
|
+
solveTimeMs?: number;
|
|
42
|
+
verdict?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface TokenResponse {
|
|
45
|
+
success: boolean;
|
|
46
|
+
token: string | null;
|
|
47
|
+
expiresIn?: string;
|
|
48
|
+
challenge?: {
|
|
49
|
+
id: string;
|
|
50
|
+
problems: SpeedProblem[];
|
|
51
|
+
timeLimit: number;
|
|
52
|
+
instructions: string;
|
|
53
|
+
};
|
|
54
|
+
nextStep?: string;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dupecom/botcha",
|
|
3
|
-
"version": "0.3
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Prove you're a bot. Humans need not apply. Reverse CAPTCHA for AI-only APIs.",
|
|
5
|
+
"workspaces": [
|
|
6
|
+
"packages/*"
|
|
7
|
+
],
|
|
5
8
|
"main": "dist/lib/index.js",
|
|
6
9
|
"types": "dist/lib/index.d.ts",
|
|
7
10
|
"type": "module",
|
|
@@ -9,6 +12,10 @@
|
|
|
9
12
|
".": {
|
|
10
13
|
"import": "./dist/lib/index.js",
|
|
11
14
|
"types": "./dist/lib/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./client": {
|
|
17
|
+
"import": "./dist/lib/client/index.js",
|
|
18
|
+
"types": "./dist/lib/client/index.d.ts"
|
|
12
19
|
}
|
|
13
20
|
},
|
|
14
21
|
"files": [
|
|
@@ -20,7 +27,12 @@
|
|
|
20
27
|
"build": "tsc",
|
|
21
28
|
"dev": "tsx watch src/index.ts",
|
|
22
29
|
"prepublishOnly": "npm run build",
|
|
23
|
-
"test": "
|
|
30
|
+
"test": "vitest",
|
|
31
|
+
"test:ui": "vitest --ui",
|
|
32
|
+
"test:run": "vitest run",
|
|
33
|
+
"test:coverage": "vitest run --coverage",
|
|
34
|
+
"publish:all": "bash scripts/publish-all.sh",
|
|
35
|
+
"publish:dry": "bash scripts/publish-all.sh --dry-run",
|
|
24
36
|
"release:patch": "npm version patch && git push --follow-tags",
|
|
25
37
|
"release:minor": "npm version minor && git push --follow-tags",
|
|
26
38
|
"release:major": "npm version major && git push --follow-tags"
|
|
@@ -34,7 +46,9 @@
|
|
|
34
46
|
"middleware",
|
|
35
47
|
"express",
|
|
36
48
|
"api",
|
|
37
|
-
"authentication"
|
|
49
|
+
"authentication",
|
|
50
|
+
"sdk",
|
|
51
|
+
"client"
|
|
38
52
|
],
|
|
39
53
|
"author": "Ramin <ramin@dupe.com>",
|
|
40
54
|
"license": "MIT",
|
|
@@ -49,12 +63,20 @@
|
|
|
49
63
|
"peerDependencies": {
|
|
50
64
|
"express": "^4.0.0 || ^5.0.0"
|
|
51
65
|
},
|
|
66
|
+
"peerDependenciesMeta": {
|
|
67
|
+
"express": {
|
|
68
|
+
"optional": true
|
|
69
|
+
}
|
|
70
|
+
},
|
|
52
71
|
"devDependencies": {
|
|
53
72
|
"@types/express": "^4.17.25",
|
|
54
|
-
"@types/node": "^20.19.
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
73
|
+
"@types/node": "^20.19.0",
|
|
74
|
+
"@vitest/ui": "^4.0.18",
|
|
75
|
+
"express": "^4.18.2",
|
|
76
|
+
"happy-dom": "^20.4.0",
|
|
77
|
+
"tsx": "^4.7.0",
|
|
78
|
+
"typescript": "^5.3.0",
|
|
79
|
+
"vitest": "^4.0.18"
|
|
58
80
|
},
|
|
59
81
|
"engines": {
|
|
60
82
|
"node": ">=18"
|