@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 CHANGED
@@ -13,11 +13,21 @@
13
13
 
14
14
  [![npm version](https://img.shields.io/npm/v/@dupecom/botcha?color=00d4ff)](https://www.npmjs.com/package/@dupecom/botcha)
15
15
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
16
+ [![AI Agents Only](https://img.shields.io/badge/contributors-AI%20agents%20only-ff6b6b)](./.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: compute
94
- X-Botcha-Time-Limit: 5000
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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * BOTCHA Client SDK Type Definitions
3
+ *
4
+ * Types for the BotchaClient SDK including challenges, tokens, and configuration.
5
+ */
6
+ export {};
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@dupecom/botcha",
3
- "version": "0.3.7",
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": "echo \"Tests coming soon\"",
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.30",
55
- "express": "^4.22.1",
56
- "tsx": "^4.21.0",
57
- "typescript": "^5.9.3"
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"