@dupecom/botcha 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -1
- package/dist/lib/client/index.d.ts +64 -2
- package/dist/lib/client/index.d.ts.map +1 -1
- package/dist/lib/client/index.js +136 -1
- package/dist/lib/client/types.d.ts +68 -0
- package/dist/lib/client/types.d.ts.map +1 -1
- package/dist/lib/index.js +2 -0
- package/dist/src/challenges/compute.d.ts +19 -0
- package/dist/src/challenges/compute.d.ts.map +1 -0
- package/dist/src/challenges/compute.js +88 -0
- package/dist/src/challenges/hybrid.d.ts +45 -0
- package/dist/src/challenges/hybrid.d.ts.map +1 -0
- package/dist/src/challenges/hybrid.js +94 -0
- package/dist/src/challenges/reasoning.d.ts +29 -0
- package/dist/src/challenges/reasoning.d.ts.map +1 -0
- package/dist/src/challenges/reasoning.js +414 -0
- package/dist/src/challenges/speed.d.ts +34 -0
- package/dist/src/challenges/speed.d.ts.map +1 -0
- package/dist/src/challenges/speed.js +115 -0
- package/dist/src/middleware/tap-enhanced-verify.d.ts +57 -0
- package/dist/src/middleware/tap-enhanced-verify.d.ts.map +1 -0
- package/dist/src/middleware/tap-enhanced-verify.js +368 -0
- package/dist/src/middleware/verify.d.ts +12 -0
- package/dist/src/middleware/verify.d.ts.map +1 -0
- package/dist/src/middleware/verify.js +141 -0
- package/dist/src/utils/badge-image.d.ts +15 -0
- package/dist/src/utils/badge-image.d.ts.map +1 -0
- package/dist/src/utils/badge-image.js +253 -0
- package/dist/src/utils/badge.d.ts +39 -0
- package/dist/src/utils/badge.d.ts.map +1 -0
- package/dist/src/utils/badge.js +125 -0
- package/dist/src/utils/signature.d.ts +23 -0
- package/dist/src/utils/signature.d.ts.map +1 -0
- package/dist/src/utils/signature.js +160 -0
- package/package.json +6 -1
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
const METHOD_COLORS = {
|
|
2
|
+
'speed-challenge': { bg: '#1a1a2e', accent: '#f59e0b', text: '#fef3c7' },
|
|
3
|
+
'landing-challenge': { bg: '#1a1a2e', accent: '#10b981', text: '#d1fae5' },
|
|
4
|
+
'standard-challenge': { bg: '#1a1a2e', accent: '#3b82f6', text: '#dbeafe' },
|
|
5
|
+
'web-bot-auth': { bg: '#1a1a2e', accent: '#8b5cf6', text: '#ede9fe' },
|
|
6
|
+
'reasoning-challenge': { bg: '#1a1a2e', accent: '#ec4899', text: '#fce7f3' },
|
|
7
|
+
'hybrid-challenge': { bg: '#1a1a2e', accent: '#ef4444', text: '#fecaca' },
|
|
8
|
+
};
|
|
9
|
+
const METHOD_LABELS = {
|
|
10
|
+
'speed-challenge': 'SPEED TEST',
|
|
11
|
+
'landing-challenge': 'LANDING CHALLENGE',
|
|
12
|
+
'standard-challenge': 'CHALLENGE',
|
|
13
|
+
'web-bot-auth': 'WEB BOT AUTH',
|
|
14
|
+
'reasoning-challenge': 'REASONING TEST',
|
|
15
|
+
'hybrid-challenge': 'HYBRID TEST',
|
|
16
|
+
};
|
|
17
|
+
const METHOD_ICONS = {
|
|
18
|
+
'speed-challenge': '⚡',
|
|
19
|
+
'landing-challenge': '🌐',
|
|
20
|
+
'standard-challenge': '🔢',
|
|
21
|
+
'web-bot-auth': '🔐',
|
|
22
|
+
'reasoning-challenge': '🧠',
|
|
23
|
+
'hybrid-challenge': '🔥',
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Generate an SVG badge image
|
|
27
|
+
*/
|
|
28
|
+
export function generateBadgeSvg(payload, options = {}) {
|
|
29
|
+
const { width = 400, height = 120 } = options;
|
|
30
|
+
const colors = METHOD_COLORS[payload.method];
|
|
31
|
+
const label = METHOD_LABELS[payload.method];
|
|
32
|
+
const icon = METHOD_ICONS[payload.method];
|
|
33
|
+
const verifiedDate = new Date(payload.verifiedAt).toISOString().split('T')[0];
|
|
34
|
+
const solveTimeText = payload.solveTimeMs ? `${payload.solveTimeMs}ms` : '';
|
|
35
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
|
36
|
+
<defs>
|
|
37
|
+
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
38
|
+
<stop offset="0%" style="stop-color:${colors.bg};stop-opacity:1" />
|
|
39
|
+
<stop offset="100%" style="stop-color:#0f0f23;stop-opacity:1" />
|
|
40
|
+
</linearGradient>
|
|
41
|
+
<linearGradient id="accentGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
42
|
+
<stop offset="0%" style="stop-color:${colors.accent};stop-opacity:1" />
|
|
43
|
+
<stop offset="100%" style="stop-color:${colors.accent};stop-opacity:0.7" />
|
|
44
|
+
</linearGradient>
|
|
45
|
+
<filter id="glow">
|
|
46
|
+
<feGaussianBlur stdDeviation="2" result="coloredBlur"/>
|
|
47
|
+
<feMerge>
|
|
48
|
+
<feMergeNode in="coloredBlur"/>
|
|
49
|
+
<feMergeNode in="SourceGraphic"/>
|
|
50
|
+
</feMerge>
|
|
51
|
+
</filter>
|
|
52
|
+
</defs>
|
|
53
|
+
|
|
54
|
+
<!-- Background -->
|
|
55
|
+
<rect width="${width}" height="${height}" rx="12" fill="url(#bgGradient)"/>
|
|
56
|
+
|
|
57
|
+
<!-- Border accent -->
|
|
58
|
+
<rect x="1" y="1" width="${width - 2}" height="${height - 2}" rx="11" fill="none" stroke="${colors.accent}" stroke-width="2" opacity="0.3"/>
|
|
59
|
+
|
|
60
|
+
<!-- Top accent line -->
|
|
61
|
+
<rect x="20" y="8" width="${width - 40}" height="3" rx="1.5" fill="url(#accentGradient)"/>
|
|
62
|
+
|
|
63
|
+
<!-- Robot icon -->
|
|
64
|
+
<text x="30" y="58" font-size="32" filter="url(#glow)">${icon}</text>
|
|
65
|
+
|
|
66
|
+
<!-- BOTCHA text -->
|
|
67
|
+
<text x="75" y="45" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="bold" fill="${colors.accent}">BOTCHA</text>
|
|
68
|
+
|
|
69
|
+
<!-- Verified badge -->
|
|
70
|
+
<text x="75" y="68" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="600" fill="${colors.text}">VERIFIED</text>
|
|
71
|
+
|
|
72
|
+
<!-- Method label -->
|
|
73
|
+
<rect x="145" y="53" width="${label.length * 8 + 16}" height="22" rx="4" fill="${colors.accent}" opacity="0.2"/>
|
|
74
|
+
<text x="153" y="68" font-family="system-ui, -apple-system, sans-serif" font-size="11" font-weight="600" fill="${colors.accent}">${label}</text>
|
|
75
|
+
|
|
76
|
+
<!-- Solve time (if available) -->
|
|
77
|
+
${solveTimeText ? `
|
|
78
|
+
<text x="${width - 30}" y="45" font-family="monospace" font-size="24" font-weight="bold" fill="${colors.accent}" text-anchor="end" filter="url(#glow)">${solveTimeText}</text>
|
|
79
|
+
<text x="${width - 30}" y="62" font-family="system-ui, -apple-system, sans-serif" font-size="10" fill="#6b7280" text-anchor="end">solve time</text>
|
|
80
|
+
` : `
|
|
81
|
+
<text x="${width - 30}" y="55" font-family="system-ui, -apple-system, sans-serif" font-size="12" fill="${colors.accent}" text-anchor="end">✓ PASSED</text>
|
|
82
|
+
`}
|
|
83
|
+
|
|
84
|
+
<!-- Bottom info -->
|
|
85
|
+
<line x1="20" y1="${height - 30}" x2="${width - 20}" y2="${height - 30}" stroke="#374151" stroke-width="1" opacity="0.5"/>
|
|
86
|
+
|
|
87
|
+
<!-- Date -->
|
|
88
|
+
<text x="30" y="${height - 12}" font-family="monospace" font-size="11" fill="#6b7280">${verifiedDate}</text>
|
|
89
|
+
|
|
90
|
+
<!-- botcha.ai link -->
|
|
91
|
+
<text x="${width - 30}" y="${height - 12}" font-family="system-ui, -apple-system, sans-serif" font-size="11" fill="#6b7280" text-anchor="end">botcha.ai</text>
|
|
92
|
+
|
|
93
|
+
<!-- Checkmark icon -->
|
|
94
|
+
<circle cx="${width / 2}" cy="${height - 16}" r="8" fill="${colors.accent}" opacity="0.2"/>
|
|
95
|
+
<text x="${width / 2}" y="${height - 12}" font-size="10" text-anchor="middle" fill="${colors.accent}">✓</text>
|
|
96
|
+
</svg>`;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Generate an HTML verification page
|
|
100
|
+
*/
|
|
101
|
+
export function generateBadgeHtml(payload, badgeId) {
|
|
102
|
+
const colors = METHOD_COLORS[payload.method];
|
|
103
|
+
const label = METHOD_LABELS[payload.method];
|
|
104
|
+
const icon = METHOD_ICONS[payload.method];
|
|
105
|
+
const verifiedDate = new Date(payload.verifiedAt).toLocaleString();
|
|
106
|
+
return `<!DOCTYPE html>
|
|
107
|
+
<html lang="en">
|
|
108
|
+
<head>
|
|
109
|
+
<meta charset="UTF-8">
|
|
110
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
111
|
+
<title>BOTCHA Badge Verification</title>
|
|
112
|
+
<meta name="description" content="Verified AI agent badge from BOTCHA">
|
|
113
|
+
<meta property="og:title" content="BOTCHA Verified - ${label}">
|
|
114
|
+
<meta property="og:description" content="This AI agent passed the BOTCHA verification${payload.solveTimeMs ? ` in ${payload.solveTimeMs}ms` : ''}.">
|
|
115
|
+
<meta property="og:image" content="https://botcha.ai/badge/${encodeURIComponent(badgeId)}/image">
|
|
116
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
117
|
+
<meta name="twitter:title" content="BOTCHA Verified - ${label}">
|
|
118
|
+
<meta name="twitter:description" content="This AI agent passed the BOTCHA verification.">
|
|
119
|
+
<meta name="twitter:image" content="https://botcha.ai/badge/${encodeURIComponent(badgeId)}/image">
|
|
120
|
+
<style>
|
|
121
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
122
|
+
body {
|
|
123
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
124
|
+
background: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 100%);
|
|
125
|
+
min-height: 100vh;
|
|
126
|
+
display: flex;
|
|
127
|
+
flex-direction: column;
|
|
128
|
+
align-items: center;
|
|
129
|
+
justify-content: center;
|
|
130
|
+
padding: 20px;
|
|
131
|
+
color: #e5e7eb;
|
|
132
|
+
}
|
|
133
|
+
.container {
|
|
134
|
+
max-width: 500px;
|
|
135
|
+
width: 100%;
|
|
136
|
+
text-align: center;
|
|
137
|
+
}
|
|
138
|
+
.badge-card {
|
|
139
|
+
background: rgba(26, 26, 46, 0.8);
|
|
140
|
+
border: 2px solid ${colors.accent}33;
|
|
141
|
+
border-radius: 16px;
|
|
142
|
+
padding: 40px;
|
|
143
|
+
margin-bottom: 24px;
|
|
144
|
+
backdrop-filter: blur(10px);
|
|
145
|
+
}
|
|
146
|
+
.icon {
|
|
147
|
+
font-size: 64px;
|
|
148
|
+
margin-bottom: 16px;
|
|
149
|
+
}
|
|
150
|
+
.title {
|
|
151
|
+
font-size: 28px;
|
|
152
|
+
font-weight: bold;
|
|
153
|
+
color: ${colors.accent};
|
|
154
|
+
margin-bottom: 8px;
|
|
155
|
+
}
|
|
156
|
+
.subtitle {
|
|
157
|
+
font-size: 18px;
|
|
158
|
+
color: ${colors.text};
|
|
159
|
+
margin-bottom: 24px;
|
|
160
|
+
}
|
|
161
|
+
.verified-badge {
|
|
162
|
+
display: inline-flex;
|
|
163
|
+
align-items: center;
|
|
164
|
+
gap: 8px;
|
|
165
|
+
background: ${colors.accent}22;
|
|
166
|
+
border: 1px solid ${colors.accent}44;
|
|
167
|
+
border-radius: 100px;
|
|
168
|
+
padding: 8px 20px;
|
|
169
|
+
font-size: 14px;
|
|
170
|
+
font-weight: 600;
|
|
171
|
+
color: ${colors.accent};
|
|
172
|
+
margin-bottom: 24px;
|
|
173
|
+
}
|
|
174
|
+
.details {
|
|
175
|
+
text-align: left;
|
|
176
|
+
background: #0f0f23;
|
|
177
|
+
border-radius: 12px;
|
|
178
|
+
padding: 20px;
|
|
179
|
+
}
|
|
180
|
+
.detail-row {
|
|
181
|
+
display: flex;
|
|
182
|
+
justify-content: space-between;
|
|
183
|
+
padding: 12px 0;
|
|
184
|
+
border-bottom: 1px solid #374151;
|
|
185
|
+
}
|
|
186
|
+
.detail-row:last-child {
|
|
187
|
+
border-bottom: none;
|
|
188
|
+
}
|
|
189
|
+
.detail-label {
|
|
190
|
+
color: #6b7280;
|
|
191
|
+
font-size: 14px;
|
|
192
|
+
}
|
|
193
|
+
.detail-value {
|
|
194
|
+
color: #e5e7eb;
|
|
195
|
+
font-size: 14px;
|
|
196
|
+
font-weight: 500;
|
|
197
|
+
}
|
|
198
|
+
.solve-time {
|
|
199
|
+
font-size: 32px;
|
|
200
|
+
font-weight: bold;
|
|
201
|
+
color: ${colors.accent};
|
|
202
|
+
font-family: monospace;
|
|
203
|
+
}
|
|
204
|
+
.footer {
|
|
205
|
+
color: #6b7280;
|
|
206
|
+
font-size: 14px;
|
|
207
|
+
}
|
|
208
|
+
.footer a {
|
|
209
|
+
color: ${colors.accent};
|
|
210
|
+
text-decoration: none;
|
|
211
|
+
}
|
|
212
|
+
.footer a:hover {
|
|
213
|
+
text-decoration: underline;
|
|
214
|
+
}
|
|
215
|
+
</style>
|
|
216
|
+
</head>
|
|
217
|
+
<body>
|
|
218
|
+
<div class="container">
|
|
219
|
+
<div class="badge-card">
|
|
220
|
+
<div class="icon">${icon}</div>
|
|
221
|
+
<h1 class="title">BOTCHA</h1>
|
|
222
|
+
<p class="subtitle">Verified AI Agent</p>
|
|
223
|
+
|
|
224
|
+
<div class="verified-badge">
|
|
225
|
+
<span>✓</span>
|
|
226
|
+
<span>${label}</span>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<div class="details">
|
|
230
|
+
<div class="detail-row">
|
|
231
|
+
<span class="detail-label">Method</span>
|
|
232
|
+
<span class="detail-value">${payload.method}</span>
|
|
233
|
+
</div>
|
|
234
|
+
${payload.solveTimeMs ? `
|
|
235
|
+
<div class="detail-row">
|
|
236
|
+
<span class="detail-label">Solve Time</span>
|
|
237
|
+
<span class="solve-time">${payload.solveTimeMs}ms</span>
|
|
238
|
+
</div>
|
|
239
|
+
` : ''}
|
|
240
|
+
<div class="detail-row">
|
|
241
|
+
<span class="detail-label">Verified At</span>
|
|
242
|
+
<span class="detail-value">${verifiedDate}</span>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<p class="footer">
|
|
248
|
+
Verified by <a href="https://botcha.ai">BOTCHA</a> - Prove you're a bot. Humans need not apply.
|
|
249
|
+
</p>
|
|
250
|
+
</div>
|
|
251
|
+
</body>
|
|
252
|
+
</html>`;
|
|
253
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type BadgeMethod = 'speed-challenge' | 'landing-challenge' | 'standard-challenge' | 'web-bot-auth' | 'reasoning-challenge' | 'hybrid-challenge';
|
|
2
|
+
export interface BadgePayload {
|
|
3
|
+
method: BadgeMethod;
|
|
4
|
+
solveTimeMs?: number;
|
|
5
|
+
verifiedAt: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ShareFormats {
|
|
8
|
+
twitter: string;
|
|
9
|
+
markdown: string;
|
|
10
|
+
text: string;
|
|
11
|
+
}
|
|
12
|
+
export interface Badge {
|
|
13
|
+
id: string;
|
|
14
|
+
verifyUrl: string;
|
|
15
|
+
share: ShareFormats;
|
|
16
|
+
imageUrl: string;
|
|
17
|
+
meta: {
|
|
18
|
+
method: BadgeMethod;
|
|
19
|
+
solveTimeMs?: number;
|
|
20
|
+
verifiedAt: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Generate a signed badge token using HMAC-SHA256
|
|
25
|
+
*/
|
|
26
|
+
export declare function generateBadge(payload: BadgePayload): string;
|
|
27
|
+
/**
|
|
28
|
+
* Verify and decode a badge token
|
|
29
|
+
*/
|
|
30
|
+
export declare function verifyBadge(token: string): BadgePayload | null;
|
|
31
|
+
/**
|
|
32
|
+
* Generate social-ready share text for different platforms
|
|
33
|
+
*/
|
|
34
|
+
export declare function generateShareText(badgeId: string, payload: BadgePayload): ShareFormats;
|
|
35
|
+
/**
|
|
36
|
+
* Create a complete badge object for API responses
|
|
37
|
+
*/
|
|
38
|
+
export declare function createBadgeResponse(method: BadgeMethod, solveTimeMs?: number): Badge;
|
|
39
|
+
//# sourceMappingURL=badge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"badge.d.ts","sourceRoot":"","sources":["../../../src/utils/badge.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,WAAW,GAAG,iBAAiB,GAAG,mBAAmB,GAAG,oBAAoB,GAAG,cAAc,GAAG,qBAAqB,GAAG,kBAAkB,CAAC;AAEvJ,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,YAAY,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE;QACJ,MAAM,EAAE,WAAW,CAAC;QACpB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;CACH;AAID;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAU3D;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CA0B9D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,YAAY,CA6DtF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,WAAW,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,KAAK,CAqBpF"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
// Badge signing secret - in production, use BADGE_SECRET env var
|
|
3
|
+
const BADGE_SECRET = process.env.BADGE_SECRET || 'botcha-badge-secret-2026';
|
|
4
|
+
const BASE_URL = process.env.BASE_URL || 'https://botcha.ai';
|
|
5
|
+
/**
|
|
6
|
+
* Generate a signed badge token using HMAC-SHA256
|
|
7
|
+
*/
|
|
8
|
+
export function generateBadge(payload) {
|
|
9
|
+
const data = JSON.stringify(payload);
|
|
10
|
+
const dataBase64 = Buffer.from(data).toString('base64url');
|
|
11
|
+
const signature = crypto
|
|
12
|
+
.createHmac('sha256', BADGE_SECRET)
|
|
13
|
+
.update(dataBase64)
|
|
14
|
+
.digest('base64url');
|
|
15
|
+
return `${dataBase64}.${signature}`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Verify and decode a badge token
|
|
19
|
+
*/
|
|
20
|
+
export function verifyBadge(token) {
|
|
21
|
+
try {
|
|
22
|
+
const parts = token.split('.');
|
|
23
|
+
if (parts.length !== 2)
|
|
24
|
+
return null;
|
|
25
|
+
const [dataBase64, signature] = parts;
|
|
26
|
+
// Verify signature
|
|
27
|
+
const expectedSignature = crypto
|
|
28
|
+
.createHmac('sha256', BADGE_SECRET)
|
|
29
|
+
.update(dataBase64)
|
|
30
|
+
.digest('base64url');
|
|
31
|
+
if (signature !== expectedSignature)
|
|
32
|
+
return null;
|
|
33
|
+
// Decode payload
|
|
34
|
+
const data = Buffer.from(dataBase64, 'base64url').toString('utf-8');
|
|
35
|
+
const payload = JSON.parse(data);
|
|
36
|
+
// Validate payload structure
|
|
37
|
+
if (!payload.method || !payload.verifiedAt)
|
|
38
|
+
return null;
|
|
39
|
+
return payload;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Generate social-ready share text for different platforms
|
|
47
|
+
*/
|
|
48
|
+
export function generateShareText(badgeId, payload) {
|
|
49
|
+
const verifyUrl = `${BASE_URL}/badge/${badgeId}`;
|
|
50
|
+
const imageUrl = `${BASE_URL}/badge/${badgeId}/image`;
|
|
51
|
+
const methodDescriptions = {
|
|
52
|
+
'speed-challenge': {
|
|
53
|
+
title: payload.solveTimeMs
|
|
54
|
+
? `I passed the BOTCHA speed test in ${payload.solveTimeMs}ms!`
|
|
55
|
+
: 'I passed the BOTCHA speed test!',
|
|
56
|
+
subtitle: 'Humans need not apply.',
|
|
57
|
+
},
|
|
58
|
+
'landing-challenge': {
|
|
59
|
+
title: 'I solved the BOTCHA landing page challenge!',
|
|
60
|
+
subtitle: 'Proved I can parse HTML and compute SHA256.',
|
|
61
|
+
},
|
|
62
|
+
'standard-challenge': {
|
|
63
|
+
title: payload.solveTimeMs
|
|
64
|
+
? `I solved the BOTCHA challenge in ${payload.solveTimeMs}ms!`
|
|
65
|
+
: 'I solved the BOTCHA challenge!',
|
|
66
|
+
subtitle: 'Computational verification complete.',
|
|
67
|
+
},
|
|
68
|
+
'web-bot-auth': {
|
|
69
|
+
title: 'I verified via BOTCHA Web Bot Auth!',
|
|
70
|
+
subtitle: 'Cryptographic identity confirmed.',
|
|
71
|
+
},
|
|
72
|
+
'reasoning-challenge': {
|
|
73
|
+
title: payload.solveTimeMs
|
|
74
|
+
? `I passed the BOTCHA reasoning test in ${(payload.solveTimeMs / 1000).toFixed(1)}s!`
|
|
75
|
+
: 'I passed the BOTCHA reasoning test!',
|
|
76
|
+
subtitle: 'Proved I can reason like an AI.',
|
|
77
|
+
},
|
|
78
|
+
'hybrid-challenge': {
|
|
79
|
+
title: payload.solveTimeMs
|
|
80
|
+
? `I passed the BOTCHA hybrid test in ${(payload.solveTimeMs / 1000).toFixed(1)}s!`
|
|
81
|
+
: 'I passed the BOTCHA hybrid test!',
|
|
82
|
+
subtitle: 'Proved I can compute AND reason like an AI.',
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
const desc = methodDescriptions[payload.method];
|
|
86
|
+
const twitter = `${desc.title}
|
|
87
|
+
|
|
88
|
+
${desc.subtitle}
|
|
89
|
+
|
|
90
|
+
Verify: ${verifyUrl}
|
|
91
|
+
|
|
92
|
+
#botcha #AI #AgentVerified`;
|
|
93
|
+
const markdown = `[](${verifyUrl})`;
|
|
94
|
+
const textParts = [
|
|
95
|
+
'BOTCHA Verified',
|
|
96
|
+
payload.solveTimeMs ? `Solved in ${payload.solveTimeMs}ms` : null,
|
|
97
|
+
`Method: ${payload.method}`,
|
|
98
|
+
`Verify: ${verifyUrl}`,
|
|
99
|
+
].filter(Boolean);
|
|
100
|
+
const text = textParts.join(' - ');
|
|
101
|
+
return { twitter, markdown, text };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Create a complete badge object for API responses
|
|
105
|
+
*/
|
|
106
|
+
export function createBadgeResponse(method, solveTimeMs) {
|
|
107
|
+
const payload = {
|
|
108
|
+
method,
|
|
109
|
+
solveTimeMs,
|
|
110
|
+
verifiedAt: Date.now(),
|
|
111
|
+
};
|
|
112
|
+
const id = generateBadge(payload);
|
|
113
|
+
const share = generateShareText(id, payload);
|
|
114
|
+
return {
|
|
115
|
+
id,
|
|
116
|
+
verifyUrl: `${BASE_URL}/badge/${id}`,
|
|
117
|
+
share,
|
|
118
|
+
imageUrl: `${BASE_URL}/badge/${id}/image`,
|
|
119
|
+
meta: {
|
|
120
|
+
method,
|
|
121
|
+
solveTimeMs,
|
|
122
|
+
verifiedAt: new Date(payload.verifiedAt).toISOString(),
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify a Web Bot Auth signature
|
|
3
|
+
*
|
|
4
|
+
* Expected headers:
|
|
5
|
+
* - Signature-Agent: https://provider.com/.well-known/http-message-signatures-directory
|
|
6
|
+
* - Signature: <signature-params>
|
|
7
|
+
* - Signature-Input: <signature-input>
|
|
8
|
+
*/
|
|
9
|
+
export declare function verifyWebBotAuth(headers: Record<string, string | string[] | undefined>, method: string, path: string, body?: string): Promise<{
|
|
10
|
+
valid: boolean;
|
|
11
|
+
agent?: string;
|
|
12
|
+
provider?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
}>;
|
|
15
|
+
export declare const TRUSTED_PROVIDERS: string[];
|
|
16
|
+
export declare function isTrustedProvider(url: string): boolean;
|
|
17
|
+
declare const _default: {
|
|
18
|
+
verifyWebBotAuth: typeof verifyWebBotAuth;
|
|
19
|
+
isTrustedProvider: typeof isTrustedProvider;
|
|
20
|
+
TRUSTED_PROVIDERS: string[];
|
|
21
|
+
};
|
|
22
|
+
export default _default;
|
|
23
|
+
//# sourceMappingURL=signature.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signature.d.ts","sourceRoot":"","sources":["../../../src/utils/signature.ts"],"names":[],"mappings":"AAgBA;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,EACtD,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA0DhF;AAyGD,eAAO,MAAM,iBAAiB,UAO7B,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOtD;;;;;;AAED,wBAA0E"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
// Cache for fetched public keys
|
|
3
|
+
const keyCache = new Map();
|
|
4
|
+
const CACHE_TTL = 3600000; // 1 hour
|
|
5
|
+
/**
|
|
6
|
+
* Verify a Web Bot Auth signature
|
|
7
|
+
*
|
|
8
|
+
* Expected headers:
|
|
9
|
+
* - Signature-Agent: https://provider.com/.well-known/http-message-signatures-directory
|
|
10
|
+
* - Signature: <signature-params>
|
|
11
|
+
* - Signature-Input: <signature-input>
|
|
12
|
+
*/
|
|
13
|
+
export async function verifyWebBotAuth(headers, method, path, body) {
|
|
14
|
+
const signatureAgent = headers['signature-agent'];
|
|
15
|
+
const signature = headers['signature'];
|
|
16
|
+
const signatureInput = headers['signature-input'];
|
|
17
|
+
if (!signatureAgent) {
|
|
18
|
+
return { valid: false, error: 'Missing Signature-Agent header' };
|
|
19
|
+
}
|
|
20
|
+
if (!signature || !signatureInput) {
|
|
21
|
+
return { valid: false, error: 'Missing Signature or Signature-Input header' };
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
// Fetch the agent's public key directory
|
|
25
|
+
const directory = await fetchAgentDirectory(signatureAgent);
|
|
26
|
+
if (!directory) {
|
|
27
|
+
return { valid: false, error: 'Could not fetch agent directory' };
|
|
28
|
+
}
|
|
29
|
+
// Parse signature input to get key ID and covered components
|
|
30
|
+
const parsed = parseSignatureInput(signatureInput);
|
|
31
|
+
if (!parsed) {
|
|
32
|
+
return { valid: false, error: 'Invalid Signature-Input format' };
|
|
33
|
+
}
|
|
34
|
+
// Find the matching key
|
|
35
|
+
const key = directory.keys.find(k => k.id === parsed.keyId);
|
|
36
|
+
if (!key) {
|
|
37
|
+
return { valid: false, error: `Key ${parsed.keyId} not found in directory` };
|
|
38
|
+
}
|
|
39
|
+
// Reconstruct the signature base
|
|
40
|
+
const signatureBase = buildSignatureBase(headers, method, path, parsed.coveredComponents, signatureInput);
|
|
41
|
+
// Verify the signature
|
|
42
|
+
const isValid = verifySignature(signatureBase, signature, key.publicKey, key.algorithm);
|
|
43
|
+
if (isValid) {
|
|
44
|
+
return {
|
|
45
|
+
valid: true,
|
|
46
|
+
agent: directory.agent,
|
|
47
|
+
provider: directory.provider,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
return { valid: false, error: 'Signature verification failed' };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
return { valid: false, error: `Verification error: ${err}` };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function fetchAgentDirectory(url) {
|
|
59
|
+
// Check cache first
|
|
60
|
+
const cached = keyCache.get(url);
|
|
61
|
+
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL) {
|
|
62
|
+
return cached.keys;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetch(url, {
|
|
66
|
+
headers: { 'Accept': 'application/json' },
|
|
67
|
+
});
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
console.error(`Failed to fetch agent directory: ${response.status}`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const directory = await response.json();
|
|
73
|
+
keyCache.set(url, { keys: directory, fetchedAt: Date.now() });
|
|
74
|
+
return directory;
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
console.error(`Error fetching agent directory:`, err);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function parseSignatureInput(input) {
|
|
82
|
+
// Simplified parser for: sig1=("@method" "@path" "content-type");keyid="key-1";alg="ecdsa-p256-sha256"
|
|
83
|
+
try {
|
|
84
|
+
const match = input.match(/sig\d+=\(([^)]+)\).*keyid="([^"]+)"/);
|
|
85
|
+
if (!match)
|
|
86
|
+
return null;
|
|
87
|
+
const components = match[1].split(' ').map(c => c.replace(/"/g, ''));
|
|
88
|
+
const keyId = match[2];
|
|
89
|
+
return { keyId, coveredComponents: components };
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function buildSignatureBase(headers, method, path, components, signatureInput) {
|
|
96
|
+
const lines = [];
|
|
97
|
+
for (const component of components) {
|
|
98
|
+
if (component === '@method') {
|
|
99
|
+
lines.push(`"@method": ${method.toUpperCase()}`);
|
|
100
|
+
}
|
|
101
|
+
else if (component === '@path') {
|
|
102
|
+
lines.push(`"@path": ${path}`);
|
|
103
|
+
}
|
|
104
|
+
else if (component === '@authority') {
|
|
105
|
+
lines.push(`"@authority": ${headers['host'] || ''}`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// Regular header
|
|
109
|
+
const value = headers[component.toLowerCase()];
|
|
110
|
+
if (value) {
|
|
111
|
+
lines.push(`"${component.toLowerCase()}": ${value}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
lines.push(`"@signature-params": ${signatureInput}`);
|
|
116
|
+
return lines.join('\n');
|
|
117
|
+
}
|
|
118
|
+
function verifySignature(signatureBase, signature, publicKey, algorithm) {
|
|
119
|
+
try {
|
|
120
|
+
// Decode base64 signature
|
|
121
|
+
const sigBuffer = Buffer.from(signature.replace(/^sig\d+=:/, '').replace(/:$/, ''), 'base64');
|
|
122
|
+
// Create verifier based on algorithm
|
|
123
|
+
let verifyAlg;
|
|
124
|
+
if (algorithm.includes('ecdsa')) {
|
|
125
|
+
verifyAlg = 'sha256';
|
|
126
|
+
}
|
|
127
|
+
else if (algorithm.includes('rsa')) {
|
|
128
|
+
verifyAlg = 'RSA-SHA256';
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
verifyAlg = 'sha256';
|
|
132
|
+
}
|
|
133
|
+
const verify = crypto.createVerify(verifyAlg);
|
|
134
|
+
verify.update(signatureBase);
|
|
135
|
+
return verify.verify(publicKey, sigBuffer);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
console.error('Signature verification error:', err);
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Known trusted providers (allowlist)
|
|
143
|
+
export const TRUSTED_PROVIDERS = [
|
|
144
|
+
'anthropic.com',
|
|
145
|
+
'openai.com',
|
|
146
|
+
'api.anthropic.com',
|
|
147
|
+
'api.openai.com',
|
|
148
|
+
'bedrock.amazonaws.com',
|
|
149
|
+
'openclaw.ai',
|
|
150
|
+
];
|
|
151
|
+
export function isTrustedProvider(url) {
|
|
152
|
+
try {
|
|
153
|
+
const hostname = new URL(url).hostname;
|
|
154
|
+
return TRUSTED_PROVIDERS.some(p => hostname.endsWith(p));
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export default { verifyWebBotAuth, isTrustedProvider, TRUSTED_PROVIDERS };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dupecom/botcha",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Prove you're a bot. Humans need not apply. Reverse CAPTCHA for AI-only APIs.",
|
|
5
5
|
"workspaces": [
|
|
6
6
|
"packages/*"
|
|
@@ -16,10 +16,15 @@
|
|
|
16
16
|
"./client": {
|
|
17
17
|
"import": "./dist/lib/client/index.js",
|
|
18
18
|
"types": "./dist/lib/client/index.d.ts"
|
|
19
|
+
},
|
|
20
|
+
"./middleware": {
|
|
21
|
+
"import": "./dist/src/middleware/tap-enhanced-verify.js",
|
|
22
|
+
"types": "./dist/src/middleware/tap-enhanced-verify.d.ts"
|
|
19
23
|
}
|
|
20
24
|
},
|
|
21
25
|
"files": [
|
|
22
26
|
"dist/lib",
|
|
27
|
+
"dist/src",
|
|
23
28
|
"README.md",
|
|
24
29
|
"LICENSE"
|
|
25
30
|
],
|