@dupecom/botcha-cloudflare 0.11.0 → 0.13.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/dist/analytics.js +2 -2
- package/dist/dashboard/auth.d.ts +11 -0
- package/dist/dashboard/auth.d.ts.map +1 -1
- package/dist/dashboard/auth.js +14 -4
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +1 -0
- package/dist/dashboard/landing.d.ts +3 -3
- package/dist/dashboard/landing.d.ts.map +1 -1
- package/dist/dashboard/landing.js +47 -7
- package/dist/dashboard/pages.d.ts.map +1 -1
- package/dist/dashboard/pages.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +105 -58
- package/dist/static.d.ts +227 -1
- package/dist/static.d.ts.map +1 -1
- package/dist/static.js +191 -1
- package/dist/tap-agents.d.ts +120 -0
- package/dist/tap-agents.d.ts.map +1 -0
- package/dist/tap-agents.js +225 -0
- package/dist/tap-routes.d.ts +215 -0
- package/dist/tap-routes.d.ts.map +1 -0
- package/dist/tap-routes.js +379 -0
- package/dist/tap-verify.d.ts +86 -0
- package/dist/tap-verify.d.ts.map +1 -0
- package/dist/tap-verify.js +275 -0
- package/package.json +3 -3
package/dist/analytics.js
CHANGED
|
@@ -30,10 +30,10 @@ export async function logAnalyticsEvent(analytics, event) {
|
|
|
30
30
|
event.solveTimeMs || 0,
|
|
31
31
|
event.responseTimeMs || 0,
|
|
32
32
|
];
|
|
33
|
+
// IMPORTANT: Analytics Engine only supports a SINGLE index.
|
|
34
|
+
// Multiple indexes silently drops the data point.
|
|
33
35
|
const indexes = [
|
|
34
36
|
event.eventType,
|
|
35
|
-
event.challengeType || 'none',
|
|
36
|
-
event.endpoint || 'unknown',
|
|
37
37
|
];
|
|
38
38
|
analytics.writeDataPoint({
|
|
39
39
|
blobs,
|
package/dist/dashboard/auth.d.ts
CHANGED
|
@@ -30,6 +30,14 @@ type Bindings = {
|
|
|
30
30
|
type Variables = {
|
|
31
31
|
dashboardAppId?: string;
|
|
32
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* Generate a 1-hour dashboard session JWT for the given app_id.
|
|
35
|
+
*/
|
|
36
|
+
export declare function generateSessionToken(appId: string, jwtSecret: string): Promise<string>;
|
|
37
|
+
/**
|
|
38
|
+
* Set the dashboard session cookie on a response context.
|
|
39
|
+
*/
|
|
40
|
+
export declare function setSessionCookie(c: Context, token: string): void;
|
|
33
41
|
/**
|
|
34
42
|
* Middleware: Require dashboard authentication.
|
|
35
43
|
*
|
|
@@ -117,13 +125,16 @@ export declare function handleDeviceCodeVerify(c: Context<{
|
|
|
117
125
|
success: true;
|
|
118
126
|
code: string;
|
|
119
127
|
login_url: string;
|
|
128
|
+
magic_link: string;
|
|
120
129
|
expires_in: number;
|
|
121
130
|
instructions: string;
|
|
122
131
|
}, import("hono/utils/http-status").ContentfulStatusCode, "json">)>;
|
|
123
132
|
/**
|
|
124
133
|
* GET /dashboard/code
|
|
134
|
+
* GET /dashboard/code/:code
|
|
125
135
|
*
|
|
126
136
|
* Renders the device code redemption page for humans.
|
|
137
|
+
* Supports pre-filled codes from URL path or query params.
|
|
127
138
|
*/
|
|
128
139
|
export declare function renderDeviceCodePage(c: Context<{
|
|
129
140
|
Bindings: Bindings;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/dashboard/auth.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAMvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAKjD,KAAK,QAAQ,GAAG;IACd,UAAU,EAAE,WAAW,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAGF,KAAK,SAAS,GAAG;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/dashboard/auth.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAMvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAKjD,KAAK,QAAQ,GAAG;IACd,UAAU,EAAE,WAAW,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAGF,KAAK,SAAS,GAAG;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAIF;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAc5F;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAQhE;AAID;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,EAAE,iBAAiB,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,SAAS,CAAA;CAAE,CAmChG,CAAC;AAIF;;;;;GAKG;AACH,wBAAsB,4BAA4B,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;oEA2CpF;AAgDD;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;;oEAwBjF;AAID;;;;GAIG;AACH,wBAAsB,yBAAyB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;oEAEjF;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;;oEA4B9E;AAID;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,qBA2E5E;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFAqB9E;AAID;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFAyBnE;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFAGpE;AAID;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFA2BxE;AAID;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,qBA6HvE"}
|
package/dist/dashboard/auth.js
CHANGED
|
@@ -10,7 +10,7 @@ import { LoginLayout, Card, Divider } from './layout';
|
|
|
10
10
|
/**
|
|
11
11
|
* Generate a 1-hour dashboard session JWT for the given app_id.
|
|
12
12
|
*/
|
|
13
|
-
async function generateSessionToken(appId, jwtSecret) {
|
|
13
|
+
export async function generateSessionToken(appId, jwtSecret) {
|
|
14
14
|
const encoder = new TextEncoder();
|
|
15
15
|
const secretKey = encoder.encode(jwtSecret);
|
|
16
16
|
return new SignJWT({
|
|
@@ -28,7 +28,7 @@ async function generateSessionToken(appId, jwtSecret) {
|
|
|
28
28
|
/**
|
|
29
29
|
* Set the dashboard session cookie on a response context.
|
|
30
30
|
*/
|
|
31
|
-
function setSessionCookie(c, token) {
|
|
31
|
+
export function setSessionCookie(c, token) {
|
|
32
32
|
setCookie(c, 'botcha_session', token, {
|
|
33
33
|
path: '/dashboard',
|
|
34
34
|
httpOnly: true,
|
|
@@ -216,21 +216,31 @@ export async function handleDeviceCodeVerify(c) {
|
|
|
216
216
|
success: true,
|
|
217
217
|
code,
|
|
218
218
|
login_url: `${baseUrl}/dashboard/code`,
|
|
219
|
+
magic_link: `${baseUrl}/go/${code}`,
|
|
219
220
|
expires_in: 600,
|
|
220
|
-
instructions: `
|
|
221
|
+
instructions: `Give your human this link: ${baseUrl}/go/${code} (or visit ${baseUrl}/dashboard/code and enter code: ${code})`,
|
|
221
222
|
});
|
|
222
223
|
}
|
|
223
224
|
// ============ DEVICE CODE REDEMPTION (human-facing) ============
|
|
224
225
|
/**
|
|
225
226
|
* GET /dashboard/code
|
|
227
|
+
* GET /dashboard/code/:code
|
|
226
228
|
*
|
|
227
229
|
* Renders the device code redemption page for humans.
|
|
230
|
+
* Supports pre-filled codes from URL path or query params.
|
|
228
231
|
*/
|
|
229
232
|
export async function renderDeviceCodePage(c) {
|
|
230
233
|
const url = new URL(c.req.url);
|
|
231
234
|
const error = url.searchParams.get('error');
|
|
232
|
-
const prefill = url.searchParams.get('code') || '';
|
|
233
235
|
const emailSent = url.searchParams.get('email_sent') === '1';
|
|
236
|
+
// Get prefill code from URL path or query params
|
|
237
|
+
const pathCode = c.req.param('code')?.toUpperCase() || '';
|
|
238
|
+
const queryCode = url.searchParams.get('code') || '';
|
|
239
|
+
let prefill = pathCode || queryCode;
|
|
240
|
+
// Strip BOTCHA- prefix if present (form only needs the suffix)
|
|
241
|
+
if (prefill.startsWith('BOTCHA-')) {
|
|
242
|
+
prefill = prefill.slice(7);
|
|
243
|
+
}
|
|
234
244
|
const errorMap = {
|
|
235
245
|
invalid: 'Invalid or expired code. Ask your agent for a new one.',
|
|
236
246
|
missing: 'Please enter a device code.',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/dashboard/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAqB5B,KAAK,QAAQ,GAAG;IACd,UAAU,EAAE,OAAO,eAAe,EAAE,WAAW,CAAC;IAChD,WAAW,EAAE,OAAO,eAAe,EAAE,WAAW,CAAC;IACjD,IAAI,EAAE,OAAO,eAAe,EAAE,WAAW,CAAC;IAC1C,SAAS,CAAC,EAAE,OAAO,cAAc,EAAE,sBAAsB,CAAC;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,KAAK,SAAS,GAAG;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,QAAA,MAAM,SAAS;cAAwB,QAAQ;eAAa,SAAS;yCAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/dashboard/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAqB5B,KAAK,QAAQ,GAAG;IACd,UAAU,EAAE,OAAO,eAAe,EAAE,WAAW,CAAC;IAChD,WAAW,EAAE,OAAO,eAAe,EAAE,WAAW,CAAC;IACjD,IAAI,EAAE,OAAO,eAAe,EAAE,WAAW,CAAC;IAC1C,SAAS,CAAC,EAAE,OAAO,cAAc,EAAE,sBAAsB,CAAC;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,KAAK,SAAS,GAAG;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,QAAA,MAAM,SAAS;cAAwB,QAAQ;eAAa,SAAS;yCAAK,CAAC;AAgE3E,eAAe,SAAS,CAAC"}
|
package/dist/dashboard/index.js
CHANGED
|
@@ -26,6 +26,7 @@ dashboard.post('/email-login', handleEmailLogin);
|
|
|
26
26
|
dashboard.get('/logout', handleLogout);
|
|
27
27
|
// Device code pages (human enters code here)
|
|
28
28
|
dashboard.get('/code', renderDeviceCodePage);
|
|
29
|
+
dashboard.get('/code/:code', renderDeviceCodePage); // Pre-filled code from URL
|
|
29
30
|
dashboard.post('/code', handleDeviceCodeRedeem);
|
|
30
31
|
// ============ PROTECTED ROUTES (auth required) ============
|
|
31
32
|
// Apply auth middleware to all routes below
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* BOTCHA Landing Pages (JSX)
|
|
3
3
|
*
|
|
4
4
|
* Two views at GET /:
|
|
5
|
-
* - LandingPage:
|
|
5
|
+
* - LandingPage: ultra-minimal — one prompt to copy-paste to your agent
|
|
6
6
|
* - VerifiedLandingPage: for humans whose agent solved the challenge
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Design: the human's only job is to copy one prompt, paste it into their
|
|
9
|
+
* agent, and click the link the agent gives back. That's it.
|
|
10
10
|
*/
|
|
11
11
|
import type { FC } from 'hono/jsx';
|
|
12
12
|
export declare const LandingPage: FC<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"landing.d.ts","sourceRoot":"","sources":["../../src/dashboard/landing.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"landing.d.ts","sourceRoot":"","sources":["../../src/dashboard/landing.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,UAAU,CAAC;AAsCnC,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAgI/D,CAAC;AAyBF,eAAO,MAAM,mBAAmB,EAAE,EAAE,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAoF3E,CAAC"}
|
|
@@ -7,18 +7,39 @@ const BOTCHA_ASCII = `██████╗ ██████╗ ████
|
|
|
7
7
|
██╔══██╗██║ ██║ ██║ ██║ ██╔══██║██╔══██║
|
|
8
8
|
██████╔╝╚██████╔╝ ██║ ╚██████╗██║ ██║██║ ██║
|
|
9
9
|
╚═════╝ ╚═════╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝`;
|
|
10
|
+
const AGENT_PROMPT = `Run: curl -s https://botcha.ai -H 'Accept: application/json' — then follow the instructions in the response to verify yourself and get me access.`;
|
|
11
|
+
// Clipboard copy icon (Lucide-style, 16x16)
|
|
12
|
+
const COPY_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"><rect x="9" y="9" width="13" height="13" rx="0"/><path d="M5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1"/></svg>`;
|
|
13
|
+
const CHECK_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><polyline points="20 6 9 17 4 12"/></svg>`;
|
|
14
|
+
const COPY_SCRIPT = `
|
|
15
|
+
function copyPrompt() {
|
|
16
|
+
var text = document.getElementById('agent-prompt').textContent.trim();
|
|
17
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
18
|
+
var label = document.getElementById('copy-label');
|
|
19
|
+
var icon = document.getElementById('copy-icon');
|
|
20
|
+
var txt = document.getElementById('copy-text');
|
|
21
|
+
label.style.color = 'var(--green)';
|
|
22
|
+
icon.innerHTML = '${CHECK_ICON.replace(/'/g, "\\'")}';
|
|
23
|
+
txt.textContent = 'Copied — now paste into your agent';
|
|
24
|
+
setTimeout(function() {
|
|
25
|
+
label.style.color = 'var(--text-muted)';
|
|
26
|
+
icon.innerHTML = '${COPY_ICON.replace(/'/g, "\\'")}';
|
|
27
|
+
txt.textContent = 'Click to copy';
|
|
28
|
+
}, 2500);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
`;
|
|
10
32
|
// ============ UNVERIFIED LANDING PAGE ============
|
|
11
33
|
export const LandingPage = ({ version, error }) => {
|
|
12
|
-
return (_jsxs(LandingLayout, { version: version, children: [_jsx("a", { href: "/", class: "ascii-logo", children: BOTCHA_ASCII }), _jsxs("p", { class: "text-muted", style: "text-align: center; font-size: 0.75rem; margin: -1rem 0 0.5rem;", children: ['>', "_\u00A0prove you're a bot"] }), _jsx("p", { class: "landing-tagline", children: "
|
|
13
|
-
pip install botcha-verify # FastAPI/Django middleware` }) }), _jsxs("div", { class: "landing-links", style: "margin-top: 1rem;", children: [_jsx("a", { href: "/openapi.json", class: "landing-link", children: "OpenAPI" }), _jsx("a", { href: "https://github.com/dupe-com/botcha", class: "landing-link", children: "GitHub" }), _jsx("a", { href: "https://www.npmjs.com/package/@dupecom/botcha", class: "landing-link", children: "npm" }), _jsx("a", { href: "https://pypi.org/project/botcha/", class: "landing-link", children: "PyPI" })] })] }), _jsx("script", { type: "application/botcha+json", id: "botcha-challenge", dangerouslySetInnerHTML: {
|
|
34
|
+
return (_jsxs(LandingLayout, { version: version, children: [_jsx("a", { href: "/", class: "ascii-logo", children: BOTCHA_ASCII }), _jsxs("p", { class: "text-muted", style: "text-align: center; font-size: 0.75rem; margin: -1rem 0 0.5rem;", children: ['>', "_\u00A0prove you're a bot"] }), _jsx("p", { class: "landing-tagline", children: "This site is for AI agents. Bring yours." }), _jsx("p", { class: "text-muted", style: "font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.15em; text-align: center; margin: 2rem 0 0.625rem;", children: "Paste this into your AI agent" }), _jsx("div", { class: "card", style: "margin-bottom: 1.5rem;", children: _jsx("div", { class: "card-body", children: _jsxs("button", { id: "prompt-btn", onclick: "copyPrompt()", type: "button", class: "card-inner", style: "display: block; width: 100%; padding: 1.5rem; border: none; border-radius: 0; cursor: pointer; font-family: var(--font); text-align: left; text-transform: none; letter-spacing: normal; box-shadow: none; transition: background 0.2s;", children: [_jsx("code", { id: "agent-prompt", style: "font-size: 1.125rem; font-weight: 700; color: var(--accent); line-height: 1.5; display: block; background: none; border: none; padding: 0;", children: AGENT_PROMPT }), _jsxs("span", { id: "copy-label", style: "display: flex; align-items: center; gap: 0.375rem; margin-top: 1rem; font-size: 0.6875rem; font-weight: 500; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.1em; transition: color 0.2s;", children: [_jsx("span", { id: "copy-icon", style: "display: flex; transition: color 0.2s;", dangerouslySetInnerHTML: { __html: COPY_ICON } }), _jsx("span", { id: "copy-text", children: "Click to copy" })] })] }) }) }), _jsx("div", { style: "text-align: center; margin: 1.5rem 0;", children: _jsxs("p", { class: "text-muted", style: "font-size: 0.75rem; line-height: 2;", children: ["Your agent solves a challenge to prove it's a bot.", _jsx("br", {}), "It gives you a link back. Click it. You're in."] }) }), error && (_jsx("div", { style: "text-align: center; margin-bottom: 0.5rem;", children: _jsx("p", { style: "color: var(--red); font-size: 0.75rem;", children: error }) })), _jsxs("details", { style: "margin-top: 1rem;", children: [_jsx("summary", { class: "text-muted", style: "font-size: 0.6875rem; cursor: pointer; text-align: center; list-style: none; text-decoration: underline; text-underline-offset: 3px;", children: "Already have a code?" }), _jsx("form", { method: "post", action: "/gate", style: "margin-top: 0.75rem;", children: _jsxs("div", { style: "display: flex; gap: 0;", children: [_jsx("input", { id: "code", name: "code", type: "text", placeholder: "BOTCHA-XXXXXX", required: true, autocomplete: "off", spellcheck: false, maxlength: 13, style: "flex: 1; font-size: 0.875rem; font-family: var(--font); font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; padding: 0.625rem; text-align: center; border: 1px solid var(--border); border-radius: 0; background: var(--bg); color: var(--text);" }), _jsxs("button", { type: "submit", style: "padding: 0.625rem 1rem; font-family: var(--font); font-size: 0.8125rem; font-weight: 600; background: var(--accent); color: #fff; border: 1px solid var(--accent); border-left: none; border-radius: 0; cursor: pointer; white-space: nowrap;", children: ["Go ", '>'] })] }) })] }), _jsx("script", { type: "application/botcha+json", id: "botcha-challenge", dangerouslySetInnerHTML: {
|
|
14
35
|
__html: JSON.stringify({
|
|
15
36
|
version: '1.0',
|
|
16
37
|
type: 'inline-challenge',
|
|
17
|
-
description: 'AI agents: solve this challenge to prove you
|
|
38
|
+
description: 'AI agents: solve this challenge to prove you are a bot!',
|
|
18
39
|
challenge: {
|
|
19
40
|
type: 'sha256',
|
|
20
41
|
instruction: "Compute SHA256 of the string 'BOTCHA-LANDING-' + today's date (YYYY-MM-DD format). Return first 16 hex chars.",
|
|
21
|
-
example: "For 2026-01-15: SHA256('BOTCHA-LANDING-2026-01-15')
|
|
42
|
+
example: "For 2026-01-15: SHA256('BOTCHA-LANDING-2026-01-15') -> first 16 chars",
|
|
22
43
|
submit_to: 'https://botcha.ai/api/verify-landing',
|
|
23
44
|
method: 'POST',
|
|
24
45
|
body_format: {
|
|
@@ -31,15 +52,34 @@ pip install botcha-verify # FastAPI/Django middleware` }) }), _jsxs("div", {
|
|
|
31
52
|
header_to_include: 'X-Botcha-Landing-Token',
|
|
32
53
|
},
|
|
33
54
|
}, null, 2),
|
|
34
|
-
} }), _jsxs("div", { class: "landing-footer", children: [_jsxs("span", { children: ["v", version] }), _jsx("span", { class: "landing-footer-sep", children: "\u00B7" }), _jsx("a", { href: "https://botcha
|
|
55
|
+
} }), _jsx("script", { dangerouslySetInnerHTML: { __html: COPY_SCRIPT } }), _jsxs("div", { class: "landing-footer", children: [_jsxs("span", { children: ["v", version] }), _jsx("span", { class: "landing-footer-sep", children: "\u00B7" }), _jsx("a", { href: "/openapi.json", children: "API" }), _jsx("span", { class: "landing-footer-sep", children: "\u00B7" }), _jsx("a", { href: "https://github.com/dupe-com/botcha", children: "GitHub" }), _jsx("span", { class: "landing-footer-sep", children: "\u00B7" }), _jsx("a", { href: "https://github.com/i8ramin", children: "@i8ramin" })] })] }));
|
|
35
56
|
};
|
|
36
57
|
// ============ VERIFIED LANDING PAGE ============
|
|
58
|
+
const ONBOARD_PROMPT = `Run: curl -s https://botcha.ai -H 'Accept: application/json' — then follow the instructions to verify yourself, create an app for me, and get me into the dashboard. Ask me for my email address when you need it.`;
|
|
59
|
+
const VERIFIED_COPY_SCRIPT = `
|
|
60
|
+
function copyOnboardPrompt() {
|
|
61
|
+
var text = document.getElementById('onboard-prompt').textContent.trim();
|
|
62
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
63
|
+
var label = document.getElementById('onboard-label');
|
|
64
|
+
var icon = document.getElementById('onboard-icon');
|
|
65
|
+
var txt = document.getElementById('onboard-text');
|
|
66
|
+
label.style.color = 'var(--green)';
|
|
67
|
+
icon.innerHTML = '${CHECK_ICON.replace(/'/g, "\\'")}';
|
|
68
|
+
txt.textContent = 'Copied — now paste into your agent';
|
|
69
|
+
setTimeout(function() {
|
|
70
|
+
label.style.color = 'var(--text-muted)';
|
|
71
|
+
icon.innerHTML = '${COPY_ICON.replace(/'/g, "\\'")}';
|
|
72
|
+
txt.textContent = 'Click to copy';
|
|
73
|
+
}, 2500);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
`;
|
|
37
77
|
export const VerifiedLandingPage = ({ version, solveTime }) => {
|
|
38
|
-
return (_jsxs(LandingLayout, { version: version, children: [_jsx("a", { href: "/", class: "ascii-logo", children: BOTCHA_ASCII }), _jsxs("p", { class: "text-muted", style: "text-align: center; font-size: 0.75rem; margin: -1rem 0 0.5rem;", children: ['>', "_\u00A0verified"] }), _jsxs("p", { class: "landing-tagline", style: "color: var(--green);", children: ["Your agent proved it's a bot", solveTime ? ` in ${solveTime}ms` : '', ". Welcome."] }),
|
|
78
|
+
return (_jsxs(LandingLayout, { version: version, children: [_jsx("a", { href: "/", class: "ascii-logo", children: BOTCHA_ASCII }), _jsxs("p", { class: "text-muted", style: "text-align: center; font-size: 0.75rem; margin: -1rem 0 0.5rem;", children: ['>', "_\u00A0verified"] }), _jsxs("p", { class: "landing-tagline", style: "color: var(--green);", children: ["Your agent proved it's a bot", solveTime ? ` in ${solveTime}ms` : '', ". Welcome."] }), _jsx("p", { class: "text-muted", style: "font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.15em; text-align: center; margin: 2rem 0 0.625rem;", children: "Set up your account \u2014 paste this to your agent" }), _jsx("div", { class: "card", style: "margin-bottom: 1.5rem;", children: _jsx("div", { class: "card-body", children: _jsxs("button", { id: "onboard-btn", onclick: "copyOnboardPrompt()", type: "button", class: "card-inner", style: "display: block; width: 100%; padding: 1.5rem; border: none; border-radius: 0; cursor: pointer; font-family: var(--font); text-align: left; text-transform: none; letter-spacing: normal; box-shadow: none; transition: background 0.2s;", children: [_jsx("code", { id: "onboard-prompt", style: "font-size: 1rem; font-weight: 700; color: var(--accent); line-height: 1.5; display: block; background: none; border: none; padding: 0;", children: ONBOARD_PROMPT }), _jsxs("span", { id: "onboard-label", style: "display: flex; align-items: center; gap: 0.375rem; margin-top: 1rem; font-size: 0.6875rem; font-weight: 500; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.1em; transition: color 0.2s;", children: [_jsx("span", { id: "onboard-icon", style: "display: flex; transition: color 0.2s;", dangerouslySetInnerHTML: { __html: COPY_ICON } }), _jsx("span", { id: "onboard-text", children: "Click to copy" })] })] }) }) }), _jsx("div", { style: "text-align: center; margin: 1.5rem 0;", children: _jsxs("p", { class: "text-muted", style: "font-size: 0.75rem; line-height: 2;", children: ["Your agent will ask for your email, create your app,", _jsx("br", {}), "and give you a link to your dashboard. You just click it."] }) }), _jsxs(Card, { title: "For developers", children: [_jsx("p", { class: "text-muted", style: "font-size: 0.8125rem; line-height: 1.7; margin-bottom: 0.75rem;", children: "Protect your own APIs so only verified AI agents can access them:" }), _jsx("pre", { children: _jsx("code", { children: `# Client SDK (for your agent)
|
|
39
79
|
npm install @dupecom/botcha # TypeScript
|
|
40
80
|
pip install botcha # Python
|
|
41
81
|
|
|
42
82
|
# Server SDK (protect your APIs)
|
|
43
83
|
npm install @botcha/verify # Express/Hono
|
|
44
|
-
pip install botcha-verify # FastAPI/Django` }) }), _jsxs("div", { class: "landing-links", style: "margin-top: 1rem;", children: [_jsx("a", { href: "/openapi.json", class: "landing-link", children: "OpenAPI" }), _jsx("a", { href: "/ai.txt", class: "landing-link", children: "ai.txt" }), _jsx("a", { href: "https://github.com/dupe-com/botcha", class: "landing-link", children: "GitHub" }), _jsx("a", { href: "https://www.npmjs.com/package/@dupecom/botcha", class: "landing-link", children: "npm" }), _jsx("a", { href: "https://pypi.org/project/botcha/", class: "landing-link", children: "PyPI" })] })] }), _jsxs("div", { class: "landing-footer", children: [_jsxs("span", { children: ["v", version] }), _jsx("span", { class: "landing-footer-sep", children: "\u00B7" }), _jsx("a", { href: "https://botcha.ai", children: "botcha.ai" }), _jsx("span", { class: "landing-footer-sep", children: "\u00B7" }), _jsx("a", { href: "https://github.com/i8ramin", children: "@i8ramin" })] })] }));
|
|
84
|
+
pip install botcha-verify # FastAPI/Django` }) }), _jsxs("div", { class: "landing-links", style: "margin-top: 1rem;", children: [_jsx("a", { href: "/openapi.json", class: "landing-link", children: "OpenAPI" }), _jsx("a", { href: "/ai.txt", class: "landing-link", children: "ai.txt" }), _jsx("a", { href: "https://github.com/dupe-com/botcha", class: "landing-link", children: "GitHub" }), _jsx("a", { href: "https://www.npmjs.com/package/@dupecom/botcha", class: "landing-link", children: "npm" }), _jsx("a", { href: "https://pypi.org/project/botcha/", class: "landing-link", children: "PyPI" })] })] }), _jsx("script", { dangerouslySetInnerHTML: { __html: VERIFIED_COPY_SCRIPT } }), _jsxs("div", { class: "landing-footer", children: [_jsxs("span", { children: ["v", version] }), _jsx("span", { class: "landing-footer-sep", children: "\u00B7" }), _jsx("a", { href: "https://botcha.ai", children: "botcha.ai" }), _jsx("span", { class: "landing-footer-sep", children: "\u00B7" }), _jsx("a", { href: "https://github.com/i8ramin", children: "@i8ramin" })] })] }));
|
|
45
85
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../../src/dashboard/pages.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../../src/dashboard/pages.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,UAAU,CAAC;AAgDnC,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAqG/C,CAAC"}
|
package/dist/dashboard/pages.js
CHANGED
|
@@ -8,7 +8,7 @@ const PeriodSelector = ({ currentPeriod = '24h', targetId, endpoint, }) => {
|
|
|
8
8
|
{ value: '7d', label: '7D' },
|
|
9
9
|
{ value: '30d', label: '30D' },
|
|
10
10
|
];
|
|
11
|
-
return (_jsx("div", { class: "period-selector", style: "display: flex; gap: 0.5rem; margin-bottom: 1rem;", children: periods.map((p) => (_jsx("button", { class: p.value === currentPeriod ? '' : 'secondary', "hx-get": `${endpoint}?period=${p.value}`, "hx-target": `#${targetId}`, "hx-swap": "innerHTML", style: "padding: 0.4rem 0.8rem; font-size: 0.75rem;", children: p.label }))) }));
|
|
11
|
+
return (_jsx("div", { class: "period-selector", style: "display: flex; gap: 0.5rem; margin-bottom: 1rem;", children: periods.map((p) => (_jsx("button", { class: p.value === currentPeriod ? '' : 'secondary', "hx-get": `${endpoint}?period=${p.value}`, "hx-target": `#${targetId}`, "hx-swap": "innerHTML", onclick: `this.parentElement.querySelectorAll('button').forEach(b=>b.className='secondary');this.className=''`, style: "padding: 0.4rem 0.8rem; font-size: 0.75rem;", children: p.label }))) }));
|
|
12
12
|
};
|
|
13
13
|
// ============ LOADING SKELETON ============
|
|
14
14
|
const LoadingSkeleton = () => (_jsxs("div", { children: [_jsx("div", { class: "skeleton skeleton-heading" }), _jsx("div", { class: "skeleton skeleton-text", style: "width: 80%;" }), _jsx("div", { class: "skeleton skeleton-text", style: "width: 60%;" }), _jsx("div", { class: "skeleton skeleton-text", style: "width: 90%;" })] }));
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* BOTCHA - Cloudflare Workers Edition v0.
|
|
2
|
+
* BOTCHA - Cloudflare Workers Edition v0.12.0
|
|
3
3
|
*
|
|
4
4
|
* Prove you're a bot. Humans need not apply.
|
|
5
5
|
*
|
|
@@ -13,6 +13,7 @@ type Bindings = {
|
|
|
13
13
|
RATE_LIMITS: KVNamespace;
|
|
14
14
|
APPS: KVNamespace;
|
|
15
15
|
AGENTS: KVNamespace;
|
|
16
|
+
SESSIONS: KVNamespace;
|
|
16
17
|
ANALYTICS?: AnalyticsEngineDataset;
|
|
17
18
|
JWT_SECRET: string;
|
|
18
19
|
BOTCHA_VERSION: string;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAYL,KAAK,WAAW,EACjB,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAYL,KAAK,WAAW,EACjB,MAAM,cAAc,CAAC;AAyBtB,OAAO,EACL,KAAK,sBAAsB,EAM5B,MAAM,aAAa,CAAC;AAGrB,KAAK,QAAQ,GAAG;IACd,UAAU,EAAE,WAAW,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,WAAW,CAAC;IACtB,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,KAAK,SAAS,GAAG;IACf,YAAY,CAAC,EAAE;QACb,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,iBAAiB,CAAC;QACxB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH,CAAC;AAEF,QAAA,MAAM,GAAG;cAAwB,QAAQ;eAAa,SAAS;yCAAK,CAAC;AA0iErE,eAAe,GAAG,CAAC;AAGnB,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,yBAAyB,EACzB,uBAAuB,EACvB,0BAA0B,EAC1B,wBAAwB,EACxB,uBAAuB,EACvB,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EACL,aAAa,EACb,WAAW,EACX,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,KAAK,EACV,KAAK,YAAY,GAClB,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* BOTCHA - Cloudflare Workers Edition v0.
|
|
3
|
+
* BOTCHA - Cloudflare Workers Edition v0.12.0
|
|
4
4
|
*
|
|
5
5
|
* Prove you're a bot. Humans need not apply.
|
|
6
6
|
*
|
|
@@ -21,6 +21,7 @@ import { createApp, getApp, getAppByEmail, verifyEmailCode, rotateAppSecret, reg
|
|
|
21
21
|
import { sendEmail, verificationEmail, recoveryEmail, secretRotatedEmail } from './email';
|
|
22
22
|
import { LandingPage, VerifiedLandingPage } from './dashboard/landing';
|
|
23
23
|
import { createAgent, getAgent, listAgents } from './agents';
|
|
24
|
+
import { registerTAPAgentRoute, getTAPAgentRoute, listTAPAgentsRoute, createTAPSessionRoute, getTAPSessionRoute, } from './tap-routes.js';
|
|
24
25
|
import { trackChallengeGenerated, trackChallengeVerified, trackAuthAttempt, trackRateLimitExceeded, } from './analytics';
|
|
25
26
|
const app = new Hono();
|
|
26
27
|
// ============ MIDDLEWARE ============
|
|
@@ -31,7 +32,7 @@ app.route('/dashboard', dashboardRoutes);
|
|
|
31
32
|
// BOTCHA discovery headers
|
|
32
33
|
app.use('*', async (c, next) => {
|
|
33
34
|
await next();
|
|
34
|
-
c.header('X-Botcha-Version', c.env.BOTCHA_VERSION || '0.
|
|
35
|
+
c.header('X-Botcha-Version', c.env.BOTCHA_VERSION || '0.13.0');
|
|
35
36
|
c.header('X-Botcha-Enabled', 'true');
|
|
36
37
|
c.header('X-Botcha-Methods', 'speed-challenge,reasoning-challenge,hybrid-challenge,standard-challenge,jwt-token');
|
|
37
38
|
c.header('X-Botcha-Docs', 'https://botcha.ai/openapi.json');
|
|
@@ -121,7 +122,7 @@ function detectAcceptPreference(c) {
|
|
|
121
122
|
return 'html';
|
|
122
123
|
}
|
|
123
124
|
app.get('/', async (c) => {
|
|
124
|
-
const version = c.env.BOTCHA_VERSION || '0.
|
|
125
|
+
const version = c.env.BOTCHA_VERSION || '0.13.0';
|
|
125
126
|
const preference = detectAcceptPreference(c);
|
|
126
127
|
const baseUrl = new URL(c.req.url).origin;
|
|
127
128
|
// Check if agent is verified (optional Bearer token)
|
|
@@ -153,7 +154,15 @@ app.get('/', async (c) => {
|
|
|
153
154
|
// Invalid/expired cookie — fall through to unverified landing
|
|
154
155
|
}
|
|
155
156
|
}
|
|
156
|
-
|
|
157
|
+
// Check for error from /go/:code redirect
|
|
158
|
+
const url = new URL(c.req.url);
|
|
159
|
+
const errorParam = url.searchParams.get('error');
|
|
160
|
+
const errorMap = {
|
|
161
|
+
invalid: 'Invalid or expired code. Ask your agent to try again.',
|
|
162
|
+
missing: 'No code provided.',
|
|
163
|
+
};
|
|
164
|
+
const error = errorParam ? errorMap[errorParam] : undefined;
|
|
165
|
+
return c.html(_jsx(LandingPage, { version: version, error: error }));
|
|
157
166
|
}
|
|
158
167
|
// === UNVERIFIED: minimal teaser — just enough to get started ===
|
|
159
168
|
if (!isVerified) {
|
|
@@ -170,9 +179,9 @@ app.get('/', async (c) => {
|
|
|
170
179
|
step4: `Revisit: GET ${baseUrl}/ with header Authorization: Bearer <access_token>`,
|
|
171
180
|
},
|
|
172
181
|
human_handoff: {
|
|
173
|
-
note: 'After verifying,
|
|
174
|
-
instruction: 'Tell your human: "
|
|
175
|
-
|
|
182
|
+
note: 'After verifying, the response includes a human_link. Give it to your human to open in their browser.',
|
|
183
|
+
instruction: 'Tell your human: "Click this link to get access" and give them the human_link URL.',
|
|
184
|
+
link_field: 'human_link from step 3 response',
|
|
176
185
|
},
|
|
177
186
|
hint: 'You have 500ms. Only a bot can do this.',
|
|
178
187
|
links: {
|
|
@@ -198,8 +207,10 @@ You have 500ms. Only a bot can do this.
|
|
|
198
207
|
|
|
199
208
|
## Human Handoff
|
|
200
209
|
|
|
201
|
-
After verifying,
|
|
202
|
-
**"
|
|
210
|
+
After verifying, the response includes a \`human_link\`. Give this URL to your human:
|
|
211
|
+
**"Click this link to get access: {human_link}"**
|
|
212
|
+
|
|
213
|
+
The link works for 5 minutes. Your human clicks it, gets a cookie, and sees the verified site.
|
|
203
214
|
|
|
204
215
|
---
|
|
205
216
|
|
|
@@ -332,50 +343,8 @@ app.post('/gate', async (c) => {
|
|
|
332
343
|
}
|
|
333
344
|
// Normalize: accept "BOTCHA-ABC123" or just "ABC123"
|
|
334
345
|
const code = input.startsWith('BOTCHA-') ? input : `BOTCHA-${input}`;
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
try {
|
|
338
|
-
token = await c.env.CHALLENGES.get(`gate:${code}`);
|
|
339
|
-
}
|
|
340
|
-
catch {
|
|
341
|
-
// KV error — fail open with helpful message
|
|
342
|
-
}
|
|
343
|
-
if (!token) {
|
|
344
|
-
return c.html(_jsx(LandingPage, { version: version, error: "Invalid or expired code. Ask your agent to solve a new challenge." }), 401);
|
|
345
|
-
}
|
|
346
|
-
// Verify the token is still valid
|
|
347
|
-
const result = await verifyToken(token, c.env.JWT_SECRET, c.env);
|
|
348
|
-
if (!result.valid) {
|
|
349
|
-
// Code existed but token expired — clean up
|
|
350
|
-
try {
|
|
351
|
-
await c.env.CHALLENGES.delete(`gate:${code}`);
|
|
352
|
-
}
|
|
353
|
-
catch { }
|
|
354
|
-
return c.html(_jsx(LandingPage, { version: version, error: "Code expired. Ask your agent to solve a new challenge." }), 401);
|
|
355
|
-
}
|
|
356
|
-
// Delete the code after use (one-time use)
|
|
357
|
-
try {
|
|
358
|
-
await c.env.CHALLENGES.delete(`gate:${code}`);
|
|
359
|
-
}
|
|
360
|
-
catch { }
|
|
361
|
-
// Mint a long-lived visitor JWT (1 year) — only proves "an agent vouched for this human"
|
|
362
|
-
// This is NOT an access token and cannot be used for API calls
|
|
363
|
-
const vPayload = result.payload;
|
|
364
|
-
const visitorToken = await new SignJWT({
|
|
365
|
-
type: 'botcha-visitor',
|
|
366
|
-
solveTime: vPayload?.solveTime,
|
|
367
|
-
gateCode: code,
|
|
368
|
-
})
|
|
369
|
-
.setProtectedHeader({ alg: 'HS256' })
|
|
370
|
-
.setIssuedAt()
|
|
371
|
-
.setExpirationTime('365d')
|
|
372
|
-
.sign(new TextEncoder().encode(c.env.JWT_SECRET));
|
|
373
|
-
// Set a 1-year visitor cookie and redirect to /
|
|
374
|
-
const ONE_YEAR = 365 * 24 * 60 * 60;
|
|
375
|
-
const headers = new Headers();
|
|
376
|
-
headers.append('Set-Cookie', `botcha_visitor=${visitorToken}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${ONE_YEAR}`);
|
|
377
|
-
headers.set('Location', '/');
|
|
378
|
-
return new Response(null, { status: 302, headers });
|
|
346
|
+
// Redirect to /go/:code which handles both gate codes and device codes
|
|
347
|
+
return c.redirect(`/go/${code}`);
|
|
379
348
|
});
|
|
380
349
|
app.get('/health', (c) => {
|
|
381
350
|
return c.json({ status: 'ok', runtime: 'cloudflare-workers' });
|
|
@@ -397,7 +366,7 @@ app.get('/ai.txt', (c) => {
|
|
|
397
366
|
});
|
|
398
367
|
// OpenAPI spec
|
|
399
368
|
app.get('/openapi.json', (c) => {
|
|
400
|
-
const version = c.env.BOTCHA_VERSION || '0.
|
|
369
|
+
const version = c.env.BOTCHA_VERSION || '0.13.0';
|
|
401
370
|
return c.json(getOpenApiSpec(version), 200, {
|
|
402
371
|
'Cache-Control': 'public, max-age=3600',
|
|
403
372
|
});
|
|
@@ -691,16 +660,17 @@ app.post('/v1/token/verify', async (c) => {
|
|
|
691
660
|
refresh_expires_in: tokenResult.refresh_expires_in,
|
|
692
661
|
solveTimeMs: result.solveTimeMs,
|
|
693
662
|
message: `Challenge verified in ${result.solveTimeMs}ms. You are a bot.`,
|
|
694
|
-
// === Human handoff
|
|
663
|
+
// === Human handoff (GIVE THIS TO YOUR HUMAN) ===
|
|
664
|
+
// The magic link is the primary way to get your human in.
|
|
665
|
+
// They click it, get a cookie, and see the verified landing page.
|
|
666
|
+
human_link: `${baseUrl}/go/${gateCode}`,
|
|
695
667
|
human_code: gateCode,
|
|
696
|
-
|
|
697
|
-
human_instruction: `Give your human this code: ${gateCode} — they enter it at ${baseUrl} to see the site.`,
|
|
668
|
+
human_instruction: `Give your human this link to open in their browser: ${baseUrl}/go/${gateCode}`,
|
|
698
669
|
// === What to do next ===
|
|
699
670
|
usage: {
|
|
700
671
|
header: 'Authorization: Bearer <access_token>',
|
|
701
672
|
try_it: 'GET /agent-only',
|
|
702
673
|
full_docs: 'GET / with Authorization: Bearer <access_token>',
|
|
703
|
-
human_handoff: `Tell your human: "Enter code ${gateCode} at ${baseUrl} to unlock the site."`,
|
|
704
674
|
refresh: 'POST /v1/token/refresh with {"refresh_token":"<refresh_token>"}',
|
|
705
675
|
revoke: 'POST /v1/token/revoke with {"token":"<token>"}',
|
|
706
676
|
},
|
|
@@ -708,6 +678,8 @@ app.post('/v1/token/verify', async (c) => {
|
|
|
708
678
|
badge,
|
|
709
679
|
// Backward compatibility
|
|
710
680
|
token: tokenResult.access_token,
|
|
681
|
+
human_magic_link: `${baseUrl}/go/${gateCode}`,
|
|
682
|
+
human_url: `${baseUrl}`,
|
|
711
683
|
});
|
|
712
684
|
});
|
|
713
685
|
// Refresh access token using refresh token
|
|
@@ -1612,6 +1584,14 @@ app.get('/v1/agents', async (c) => {
|
|
|
1612
1584
|
}, 500);
|
|
1613
1585
|
}
|
|
1614
1586
|
});
|
|
1587
|
+
// ============ TAP (TRUSTED AGENT PROTOCOL) ENDPOINTS ============
|
|
1588
|
+
// TAP agent registration and retrieval
|
|
1589
|
+
app.post('/v1/agents/register/tap', registerTAPAgentRoute);
|
|
1590
|
+
app.get('/v1/agents/tap', listTAPAgentsRoute);
|
|
1591
|
+
app.get('/v1/agents/:id/tap', getTAPAgentRoute);
|
|
1592
|
+
// TAP session management
|
|
1593
|
+
app.post('/v1/sessions/tap', createTAPSessionRoute);
|
|
1594
|
+
app.get('/v1/sessions/:id/tap', getTAPSessionRoute);
|
|
1615
1595
|
// ============ DASHBOARD AUTH API ENDPOINTS ============
|
|
1616
1596
|
// Challenge-based dashboard login (agent direct)
|
|
1617
1597
|
app.post('/v1/auth/dashboard', handleDashboardAuthChallenge);
|
|
@@ -1619,6 +1599,73 @@ app.post('/v1/auth/dashboard/verify', handleDashboardAuthVerify);
|
|
|
1619
1599
|
// Device code flow (agent → human handoff)
|
|
1620
1600
|
app.post('/v1/auth/device-code', handleDeviceCodeChallenge);
|
|
1621
1601
|
app.post('/v1/auth/device-code/verify', handleDeviceCodeVerify);
|
|
1602
|
+
// ============ ONE-CLICK ACCESS LINKS ============
|
|
1603
|
+
/**
|
|
1604
|
+
* GET /go/:code - One-click device code redemption
|
|
1605
|
+
*
|
|
1606
|
+
* Magic link for instant dashboard access. Agent gives human this link:
|
|
1607
|
+
* https://botcha.ai/go/BOTCHA-XXXX → auto-login + redirect to dashboard
|
|
1608
|
+
*
|
|
1609
|
+
* UX improvement: no more copy-pasting codes!
|
|
1610
|
+
*/
|
|
1611
|
+
app.get('/go/:code', async (c) => {
|
|
1612
|
+
const code = c.req.param('code')?.toUpperCase();
|
|
1613
|
+
if (!code) {
|
|
1614
|
+
return c.redirect('/?error=missing');
|
|
1615
|
+
}
|
|
1616
|
+
// Normalize: accept "AR8CZX", "BOTCHA-AR8CZX", etc.
|
|
1617
|
+
let normalizedCode = code;
|
|
1618
|
+
if (!code.startsWith('BOTCHA-')) {
|
|
1619
|
+
normalizedCode = `BOTCHA-${code}`;
|
|
1620
|
+
}
|
|
1621
|
+
// === Try gate code first (visitor access from /v1/token/verify) ===
|
|
1622
|
+
// Gate codes are stored as gate:{code} → access_token string
|
|
1623
|
+
let gateToken = null;
|
|
1624
|
+
try {
|
|
1625
|
+
gateToken = await c.env.CHALLENGES.get(`gate:${normalizedCode}`);
|
|
1626
|
+
}
|
|
1627
|
+
catch { }
|
|
1628
|
+
if (gateToken) {
|
|
1629
|
+
// Verify the underlying token is still valid
|
|
1630
|
+
const result = await verifyToken(gateToken, c.env.JWT_SECRET, c.env);
|
|
1631
|
+
// Delete the code (one-time use) regardless of token validity
|
|
1632
|
+
try {
|
|
1633
|
+
await c.env.CHALLENGES.delete(`gate:${normalizedCode}`);
|
|
1634
|
+
}
|
|
1635
|
+
catch { }
|
|
1636
|
+
if (result.valid) {
|
|
1637
|
+
// Mint a long-lived visitor JWT (1 year) — proves "an agent vouched for this human"
|
|
1638
|
+
const vPayload = result.payload;
|
|
1639
|
+
const visitorToken = await new SignJWT({
|
|
1640
|
+
type: 'botcha-visitor',
|
|
1641
|
+
solveTime: vPayload?.solveTime,
|
|
1642
|
+
gateCode: normalizedCode,
|
|
1643
|
+
})
|
|
1644
|
+
.setProtectedHeader({ alg: 'HS256' })
|
|
1645
|
+
.setIssuedAt()
|
|
1646
|
+
.setExpirationTime('365d')
|
|
1647
|
+
.sign(new TextEncoder().encode(c.env.JWT_SECRET));
|
|
1648
|
+
const ONE_YEAR = 365 * 24 * 60 * 60;
|
|
1649
|
+
const headers = new Headers();
|
|
1650
|
+
headers.append('Set-Cookie', `botcha_visitor=${visitorToken}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${ONE_YEAR}`);
|
|
1651
|
+
headers.set('Location', '/');
|
|
1652
|
+
return new Response(null, { status: 302, headers });
|
|
1653
|
+
}
|
|
1654
|
+
// Gate token expired — fall through to try device code
|
|
1655
|
+
}
|
|
1656
|
+
// === Try device code (dashboard access from /v1/auth/device-code/verify) ===
|
|
1657
|
+
const { redeemDeviceCode } = await import('./dashboard/device-code');
|
|
1658
|
+
const data = await redeemDeviceCode(c.env.CHALLENGES, normalizedCode);
|
|
1659
|
+
if (data) {
|
|
1660
|
+
// Generate session token and redirect to dashboard
|
|
1661
|
+
const { generateSessionToken, setSessionCookie } = await import('./dashboard/auth');
|
|
1662
|
+
const sessionToken = await generateSessionToken(data.app_id, c.env.JWT_SECRET);
|
|
1663
|
+
setSessionCookie(c, sessionToken);
|
|
1664
|
+
return c.redirect('/dashboard');
|
|
1665
|
+
}
|
|
1666
|
+
// Neither code type found — redirect to landing with error
|
|
1667
|
+
return c.redirect('/?error=invalid');
|
|
1668
|
+
});
|
|
1622
1669
|
// ============ LEGACY ENDPOINTS (v0 - backward compatibility) ============
|
|
1623
1670
|
app.get('/api/challenge', async (c) => {
|
|
1624
1671
|
const difficulty = c.req.query('difficulty') || 'medium';
|