@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 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,
@@ -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;AAsCF;;;;;;;;;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;;;;;;;;;;oEA2B9E;AAID;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,qBAkE5E;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"}
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"}
@@ -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: `Tell your human: Visit ${baseUrl}/dashboard/code and enter code: ${code}`,
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;AA+D3E,eAAe,SAAS,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"}
@@ -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: for unverified human visitors (has token input)
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
- * Uses the same terminal aesthetic as the dashboard:
9
- * JetBrains Mono, dot-shadow Cards, scanline overlay.
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;AAanC,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAwG/D,CAAC;AAIF,eAAO,MAAM,mBAAmB,EAAE,EAAE,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAgG3E,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: "Reverse CAPTCHA for AI agents. Humans need not apply." }), _jsxs(Card, { title: "You're a human", children: [_jsx("p", { class: "text-muted", style: "font-size: 0.8125rem; line-height: 1.7;", children: "This site is for AI agents. To see what's here, ask your agent to verify." }), _jsxs("p", { class: "text-muted", style: "font-size: 0.8125rem; line-height: 1.7; margin-top: 0.75rem;", children: ["Tell your AI agent:", _jsx("br", {}), _jsx("code", { style: "font-size: 0.8125rem;", children: "\"Go to botcha.ai, solve the challenge, and give me the code.\"" })] }), _jsxs("form", { method: "post", action: "/gate", style: "margin-top: 1.25rem;", children: [_jsx("label", { for: "code", class: "form-label", style: "font-size: 0.75rem; color: var(--text-muted); display: block; margin-bottom: 0.375rem;", children: "Enter the code your agent gives you:" }), _jsx("input", { id: "code", name: "code", type: "text", placeholder: "BOTCHA-XXXXXX", required: true, autocomplete: "off", spellcheck: false, maxlength: 13, style: "width: 100%; font-size: 1.25rem; font-family: var(--font); font-weight: 700; letter-spacing: 0.15em; text-transform: uppercase; padding: 0.75rem; text-align: center; border: 2px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text);" }), error && (_jsx("p", { style: "color: var(--red); font-size: 0.75rem; margin-top: 0.5rem;", children: error })), _jsx("button", { type: "submit", style: "margin-top: 0.75rem; width: 100%; padding: 0.625rem 1rem; font-family: var(--font); font-size: 0.875rem; font-weight: 600; background: var(--accent); color: #fff; border: none; border-radius: 4px; cursor: pointer;", children: "Unlock" })] })] }), _jsxs(Card, { title: "Developers", children: [_jsx("p", { class: "text-muted mb-2", style: "font-size: 0.75rem;", children: "Protect your APIs so only verified AI agents can access them:" }), _jsx("pre", { children: _jsx("code", { children: `npm install @botcha/verify # Express/Hono middleware
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\'re a bot!',
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') first 16 chars",
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.ai", children: "botcha.ai" }), _jsx("span", { class: "landing-footer-sep", children: "\u00B7" }), _jsx("a", { href: "https://github.com/i8ramin", children: "@i8ramin" })] })] }));
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."] }), _jsxs(Card, { title: "Get started", children: [_jsx("p", { class: "text-muted", style: "font-size: 0.8125rem; line-height: 1.7;", children: "You have an AI agent. Here's what to do with it." }), _jsx("p", { class: "text-muted", style: "font-size: 0.75rem; line-height: 1.6; margin-top: 0.5rem; font-style: italic;", children: "Copy any of these prompts and paste them to your agent:" })] }), _jsxs(Card, { title: "1. Create your app", children: [_jsx("p", { class: "text-muted", style: "font-size: 0.8125rem; line-height: 1.7;", children: "Tell your agent to create a BOTCHA app tied to your email. This gives you an identity on the platform \u2014 your agent gets API keys, you get a dashboard." }), _jsxs("div", { style: "margin-top: 0.75rem; padding: 0.75rem; background: rgba(255,255,255,0.03); border: 1px solid var(--border); border-radius: 6px;", children: [_jsx("p", { style: "font-size: 0.625rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); margin-bottom: 0.375rem;", children: "Say this to your agent:" }), _jsxs("code", { style: "font-size: 0.8125rem; line-height: 1.6; color: var(--accent);", children: ["\"Go to botcha.ai and create an app for me. My email is ", _jsx("span", { contentEditable: "plaintext-only", spellcheck: false, style: "color: var(--green); border-bottom: 1px dashed var(--green); outline: none; min-width: 3ch; padding: 0 2px;", children: "you@example.com" }), ". Save the app_id and app_secret somewhere safe.\""] })] }), _jsx("p", { class: "text-muted", style: "font-size: 0.6875rem; margin-top: 0.5rem;", children: "Your agent will call the API, get your credentials, and a verification code will be emailed to you." })] }), _jsxs(Card, { title: "2. Verify your email", children: [_jsx("p", { class: "text-muted", style: "font-size: 0.8125rem; line-height: 1.7;", children: "Check your inbox for a 6-digit code from BOTCHA. Give it to your agent to confirm your email." }), _jsxs("div", { style: "margin-top: 0.75rem; padding: 0.75rem; background: rgba(255,255,255,0.03); border: 1px solid var(--border); border-radius: 6px;", children: [_jsx("p", { style: "font-size: 0.625rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); margin-bottom: 0.375rem;", children: "Say this to your agent:" }), _jsx("code", { style: "font-size: 0.8125rem; line-height: 1.6; color: var(--accent);", children: "\"The verification code from BOTCHA is [code]. Verify my email.\"" })] }), _jsx("p", { class: "text-muted", style: "font-size: 0.6875rem; margin-top: 0.5rem;", children: "This enables account recovery if you ever lose your credentials." })] }), _jsxs(Card, { title: "3. Open your dashboard", children: [_jsx("p", { class: "text-muted", style: "font-size: 0.8125rem; line-height: 1.7;", children: "Your agent can give you a short code to access your management dashboard \u2014 usage stats, API keys, and settings." }), _jsxs("div", { style: "margin-top: 0.75rem; padding: 0.75rem; background: rgba(255,255,255,0.03); border: 1px solid var(--border); border-radius: 6px;", children: [_jsx("p", { style: "font-size: 0.625rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); margin-bottom: 0.375rem;", children: "Say this to your agent:" }), _jsx("code", { style: "font-size: 0.8125rem; line-height: 1.6; color: var(--accent);", children: "\"Get me a dashboard code for BOTCHA.\"" })] }), _jsxs("p", { class: "text-muted", style: "font-size: 0.6875rem; margin-top: 0.5rem;", children: ["Your agent gives you a ", _jsx("code", { children: "BOTCHA-XXXX" }), " code. Enter it at ", _jsx("a", { href: "/dashboard/code", style: "font-weight: 600;", children: "/dashboard/code" }), "."] })] }), _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)
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;AA+CnC,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAqG/C,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"}
@@ -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.11.0
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;
@@ -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;AAkBtB,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,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;AA++DrE,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"}
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.11.0
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.11.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.11.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
- return c.html(_jsx(LandingPage, { version: version }));
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, you will receive a short code (BOTCHA-XXXXXX). Give it to your human.',
174
- instruction: 'Tell your human: "Enter this code at botcha.ai to unlock the site."',
175
- code_field: 'human_code from step 3 response',
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, give your human the \`access_token\` and tell them:
202
- **"Paste this token into the input at botcha.ai to unlock the full site."**
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
- // Look up the code in KV
336
- let token = null;
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.11.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 code (GIVE THIS TO YOUR HUMAN) ===
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
- human_url: `${baseUrl}`,
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';