@dupecom/botcha 0.3.7 → 0.4.1

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
@@ -18,6 +18,13 @@
18
18
  📦 **npm:** [@dupecom/botcha](https://www.npmjs.com/package/@dupecom/botcha)
19
19
  🔌 **OpenAPI:** [botcha.ai/openapi.json](https://botcha.ai/openapi.json)
20
20
 
21
+ ## Packages
22
+
23
+ | Package | Runtime | Install |
24
+ |---------|---------|---------|
25
+ | [`@dupecom/botcha`](https://www.npmjs.com/package/@dupecom/botcha) | Node.js / Express | `npm install @dupecom/botcha` |
26
+ | [`@dupecom/botcha-cloudflare`](./packages/cloudflare-workers) | Cloudflare Workers | `npm install @dupecom/botcha-cloudflare` |
27
+
21
28
  ## Why?
22
29
 
23
30
  CAPTCHAs ask "Are you human?" — **BOTCHA asks "Are you an AI?"**
@@ -177,6 +184,30 @@ const answers = botcha.solve([645234, 891023, 334521]);
177
184
  6. ✅ Access granted
178
185
  ```
179
186
 
187
+ ## Cloudflare Workers
188
+
189
+ For edge deployment, use the Cloudflare Workers package:
190
+
191
+ ```bash
192
+ npm install @dupecom/botcha-cloudflare
193
+ ```
194
+
195
+ ```typescript
196
+ // Uses Hono + Web Crypto API (no Node.js dependencies)
197
+ import app from '@dupecom/botcha-cloudflare';
198
+ export default app;
199
+ ```
200
+
201
+ Or deploy your own instance:
202
+
203
+ ```bash
204
+ cd packages/cloudflare-workers
205
+ npm install
206
+ npm run deploy # Deploys to your Cloudflare account
207
+ ```
208
+
209
+ Same API endpoints, same challenge logic, running at the edge. See [`packages/cloudflare-workers/README.md`](./packages/cloudflare-workers/README.md) for full docs.
210
+
180
211
  ## Philosophy
181
212
 
182
213
  > "If a human writes a script to solve BOTCHA using an LLM... they've built an AI agent."
@@ -192,3 +223,63 @@ MIT © [Ramin](https://github.com/i8ramin)
192
223
  ---
193
224
 
194
225
  Built by [@i8ramin](https://github.com/i8ramin) and Choco 🐢
226
+
227
+ ---
228
+
229
+ ## Client SDK (for AI Agents)
230
+
231
+ If you're building an AI agent that needs to access BOTCHA-protected APIs, use the client SDK:
232
+
233
+ ```typescript
234
+ import { BotchaClient } from '@dupecom/botcha/client';
235
+
236
+ const client = new BotchaClient();
237
+
238
+ // Option 1: Auto-solve - fetches URL, solves any BOTCHA challenges automatically
239
+ const response = await client.fetch('https://api.example.com/agent-only');
240
+ const data = await response.json();
241
+
242
+ // Option 2: Pre-solve - get headers with solved challenge
243
+ const headers = await client.createHeaders();
244
+ const response = await fetch('https://api.example.com/agent-only', { headers });
245
+
246
+ // Option 3: Manual solve - solve challenge problems directly
247
+ const answers = client.solve([123456, 789012]);
248
+ ```
249
+
250
+ ### Client Options
251
+
252
+ ```typescript
253
+ const client = new BotchaClient({
254
+ baseUrl: 'https://botcha.ai', // BOTCHA service URL
255
+ agentIdentity: 'MyAgent/1.0', // User-Agent string
256
+ maxRetries: 3, // Max challenge solve attempts
257
+ });
258
+ ```
259
+
260
+ ### Framework Integration Examples
261
+
262
+ **OpenClaw / LangChain:**
263
+ ```typescript
264
+ import { BotchaClient } from '@dupecom/botcha/client';
265
+
266
+ const botcha = new BotchaClient({ agentIdentity: 'MyLangChainAgent/1.0' });
267
+
268
+ // Use in your agent's HTTP tool
269
+ const tool = {
270
+ name: 'fetch_protected_api',
271
+ call: async (url: string) => {
272
+ const response = await botcha.fetch(url);
273
+ return response.json();
274
+ }
275
+ };
276
+ ```
277
+
278
+ **Standalone Helper:**
279
+ ```typescript
280
+ import { solveBotcha } from '@dupecom/botcha/client';
281
+
282
+ // Just solve the problems, handle the rest yourself
283
+ const answers = solveBotcha([123456, 789012]);
284
+ // Returns: ['a1b2c3d4', 'e5f6g7h8']
285
+ ```
@@ -0,0 +1,94 @@
1
+ export interface BotchaClientOptions {
2
+ /** Base URL of BOTCHA service (default: https://botcha.ai) */
3
+ baseUrl?: string;
4
+ /** Custom identity header value */
5
+ agentIdentity?: string;
6
+ /** Max retries for challenge solving */
7
+ maxRetries?: number;
8
+ }
9
+ export interface ChallengeResponse {
10
+ success: boolean;
11
+ challenge?: {
12
+ id: string;
13
+ problems: number[];
14
+ timeLimit: number;
15
+ instructions: string;
16
+ };
17
+ }
18
+ export interface VerifyResponse {
19
+ success: boolean;
20
+ message: string;
21
+ solveTimeMs?: number;
22
+ verdict?: string;
23
+ }
24
+ /**
25
+ * BOTCHA Client SDK for AI Agents
26
+ *
27
+ * Automatically handles BOTCHA challenges when accessing protected endpoints.
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * import { BotchaClient } from '@dupecom/botcha/client';
32
+ *
33
+ * const client = new BotchaClient();
34
+ *
35
+ * // Automatically solves challenges and retries
36
+ * const response = await client.fetch('https://api.example.com/agent-only');
37
+ * ```
38
+ */
39
+ export declare class BotchaClient {
40
+ private baseUrl;
41
+ private agentIdentity;
42
+ private maxRetries;
43
+ constructor(options?: BotchaClientOptions);
44
+ /**
45
+ * Solve a BOTCHA speed challenge
46
+ *
47
+ * @param problems - Array of numbers to hash
48
+ * @returns Array of SHA256 first 8 hex chars for each number
49
+ */
50
+ solve(problems: number[]): string[];
51
+ /**
52
+ * Get and solve a challenge from BOTCHA service
53
+ */
54
+ solveChallenge(): Promise<{
55
+ id: string;
56
+ answers: string[];
57
+ }>;
58
+ /**
59
+ * Verify a solved challenge
60
+ */
61
+ verify(id: string, answers: string[]): Promise<VerifyResponse>;
62
+ /**
63
+ * Fetch a URL, automatically solving BOTCHA challenges if encountered
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * const response = await client.fetch('https://api.example.com/agent-only');
68
+ * const data = await response.json();
69
+ * ```
70
+ */
71
+ fetch(url: string, init?: RequestInit): Promise<Response>;
72
+ /**
73
+ * Create headers with pre-solved challenge for manual use
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * const headers = await client.createHeaders();
78
+ * const response = await fetch('https://api.example.com/agent-only', { headers });
79
+ * ```
80
+ */
81
+ createHeaders(): Promise<Record<string, string>>;
82
+ }
83
+ /**
84
+ * Convenience function for one-off solves
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * const answers = solveBotcha([123456, 789012]);
89
+ * // Returns: ['a1b2c3d4', 'e5f6g7h8']
90
+ * ```
91
+ */
92
+ export declare function solveBotcha(problems: number[]): string[];
93
+ export default BotchaClient;
94
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../lib/client/index.ts"],"names":[],"mappings":"AAKA,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;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE;QACV,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;KACtB,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;;;;;;;;;;;;;;GAcG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,UAAU,CAAS;gBAEf,OAAO,GAAE,mBAAwB;IAM7C;;;;;OAKG;IACH,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE;IAMnC;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAwBlE;;OAEG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC;IAsBpE;;;;;;;;OAQG;IACG,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC;IA2C/D;;;;;;;;OAQG;IACG,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CASvD;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAIxD;AAED,eAAe,YAAY,CAAC"}
@@ -0,0 +1,155 @@
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
+ constructor(options = {}) {
24
+ this.baseUrl = options.baseUrl || 'https://botcha.ai';
25
+ this.agentIdentity = options.agentIdentity || `BotchaClient/${SDK_VERSION}`;
26
+ this.maxRetries = options.maxRetries || 3;
27
+ }
28
+ /**
29
+ * Solve a BOTCHA speed challenge
30
+ *
31
+ * @param problems - Array of numbers to hash
32
+ * @returns Array of SHA256 first 8 hex chars for each number
33
+ */
34
+ solve(problems) {
35
+ return problems.map(num => crypto.createHash('sha256').update(num.toString()).digest('hex').substring(0, 8));
36
+ }
37
+ /**
38
+ * Get and solve a challenge from BOTCHA service
39
+ */
40
+ async solveChallenge() {
41
+ const res = await fetch(`${this.baseUrl}/api/speed-challenge`, {
42
+ headers: { 'User-Agent': this.agentIdentity },
43
+ });
44
+ if (!res.ok) {
45
+ throw new Error(`Challenge request failed with status ${res.status} ${res.statusText}`);
46
+ }
47
+ const contentType = res.headers.get('content-type') || '';
48
+ if (!contentType.toLowerCase().includes('application/json')) {
49
+ throw new Error('Expected JSON response for challenge request');
50
+ }
51
+ const data = await res.json();
52
+ if (!data.success || !data.challenge) {
53
+ throw new Error('Failed to get challenge');
54
+ }
55
+ const answers = this.solve(data.challenge.problems);
56
+ return { id: data.challenge.id, answers };
57
+ }
58
+ /**
59
+ * Verify a solved challenge
60
+ */
61
+ async verify(id, answers) {
62
+ const res = await fetch(`${this.baseUrl}/api/speed-challenge`, {
63
+ method: 'POST',
64
+ headers: {
65
+ 'Content-Type': 'application/json',
66
+ 'User-Agent': this.agentIdentity,
67
+ },
68
+ body: JSON.stringify({ id, answers }),
69
+ });
70
+ if (!res.ok) {
71
+ throw new Error(`Verification request failed with status ${res.status} ${res.statusText}`);
72
+ }
73
+ const contentType = res.headers.get('content-type') || '';
74
+ if (!contentType.toLowerCase().includes('application/json')) {
75
+ throw new Error('Expected JSON response for verification request');
76
+ }
77
+ return await res.json();
78
+ }
79
+ /**
80
+ * Fetch a URL, automatically solving BOTCHA challenges if encountered
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * const response = await client.fetch('https://api.example.com/agent-only');
85
+ * const data = await response.json();
86
+ * ```
87
+ */
88
+ async fetch(url, init) {
89
+ let response = await fetch(url, {
90
+ ...init,
91
+ headers: {
92
+ ...Object.fromEntries(new Headers(init?.headers).entries()),
93
+ 'User-Agent': this.agentIdentity,
94
+ },
95
+ });
96
+ let retries = 0;
97
+ while (response.status === 403 && retries < this.maxRetries) {
98
+ // Clone response before reading body to preserve it for the caller
99
+ const clonedResponse = response.clone();
100
+ const body = await clonedResponse.json().catch(() => null);
101
+ // Check if this is a BOTCHA challenge
102
+ if (body?.error === 'BOTCHA_CHALLENGE' || body?.challenge?.problems) {
103
+ const challenge = body.challenge;
104
+ if (challenge?.problems && Array.isArray(challenge.problems)) {
105
+ // Solve the challenge
106
+ const answers = this.solve(challenge.problems);
107
+ // Create fresh headers for retry to avoid state issues
108
+ const retryHeaders = new Headers(init?.headers);
109
+ retryHeaders.set('User-Agent', this.agentIdentity);
110
+ retryHeaders.set('X-Botcha-Id', challenge.id);
111
+ retryHeaders.set('X-Botcha-Answers', JSON.stringify(answers));
112
+ response = await fetch(url, { ...init, headers: retryHeaders });
113
+ retries++;
114
+ }
115
+ else {
116
+ break;
117
+ }
118
+ }
119
+ else {
120
+ break;
121
+ }
122
+ }
123
+ return response;
124
+ }
125
+ /**
126
+ * Create headers with pre-solved challenge for manual use
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * const headers = await client.createHeaders();
131
+ * const response = await fetch('https://api.example.com/agent-only', { headers });
132
+ * ```
133
+ */
134
+ async createHeaders() {
135
+ const { id, answers } = await this.solveChallenge();
136
+ return {
137
+ 'X-Botcha-Id': id,
138
+ 'X-Botcha-Answers': JSON.stringify(answers),
139
+ 'User-Agent': this.agentIdentity,
140
+ };
141
+ }
142
+ }
143
+ /**
144
+ * Convenience function for one-off solves
145
+ *
146
+ * @example
147
+ * ```typescript
148
+ * const answers = solveBotcha([123456, 789012]);
149
+ * // Returns: ['a1b2c3d4', 'e5f6g7h8']
150
+ * ```
151
+ */
152
+ export function solveBotcha(problems) {
153
+ return problems.map(num => crypto.createHash('sha256').update(num.toString()).digest('hex').substring(0, 8));
154
+ }
155
+ export default BotchaClient;
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@dupecom/botcha",
3
- "version": "0.3.7",
3
+ "version": "0.4.1",
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,10 @@
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",
24
34
  "release:patch": "npm version patch && git push --follow-tags",
25
35
  "release:minor": "npm version minor && git push --follow-tags",
26
36
  "release:major": "npm version major && git push --follow-tags"
@@ -34,7 +44,9 @@
34
44
  "middleware",
35
45
  "express",
36
46
  "api",
37
- "authentication"
47
+ "authentication",
48
+ "sdk",
49
+ "client"
38
50
  ],
39
51
  "author": "Ramin <ramin@dupe.com>",
40
52
  "license": "MIT",
@@ -49,12 +61,20 @@
49
61
  "peerDependencies": {
50
62
  "express": "^4.0.0 || ^5.0.0"
51
63
  },
64
+ "peerDependenciesMeta": {
65
+ "express": {
66
+ "optional": true
67
+ }
68
+ },
52
69
  "devDependencies": {
53
70
  "@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"
71
+ "@types/node": "^20.19.0",
72
+ "@vitest/ui": "^4.0.18",
73
+ "express": "^4.18.2",
74
+ "happy-dom": "^20.4.0",
75
+ "tsx": "^4.7.0",
76
+ "typescript": "^5.3.0",
77
+ "vitest": "^4.0.18"
58
78
  },
59
79
  "engines": {
60
80
  "node": ">=18"