@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 +91 -0
- package/dist/lib/client/index.d.ts +94 -0
- package/dist/lib/client/index.d.ts.map +1 -0
- package/dist/lib/client/index.js +155 -0
- package/package.json +27 -7
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
|
+
"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": "
|
|
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.
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
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"
|