@dupecom/botcha-cloudflare 0.10.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.
Files changed (42) hide show
  1. package/README.md +9 -8
  2. package/dist/agents.d.ts +68 -0
  3. package/dist/agents.d.ts.map +1 -0
  4. package/dist/agents.js +123 -0
  5. package/dist/analytics.js +2 -2
  6. package/dist/dashboard/api.js +20 -13
  7. package/dist/dashboard/auth.d.ts +11 -0
  8. package/dist/dashboard/auth.d.ts.map +1 -1
  9. package/dist/dashboard/auth.js +20 -10
  10. package/dist/dashboard/index.d.ts.map +1 -1
  11. package/dist/dashboard/index.js +1 -0
  12. package/dist/dashboard/landing.d.ts +20 -0
  13. package/dist/dashboard/landing.d.ts.map +1 -0
  14. package/dist/dashboard/landing.js +85 -0
  15. package/dist/dashboard/layout.d.ts +7 -0
  16. package/dist/dashboard/layout.d.ts.map +1 -1
  17. package/dist/dashboard/layout.js +17 -0
  18. package/dist/dashboard/pages.d.ts.map +1 -1
  19. package/dist/dashboard/pages.js +1 -1
  20. package/dist/dashboard/styles.d.ts +1 -1
  21. package/dist/dashboard/styles.d.ts.map +1 -1
  22. package/dist/dashboard/styles.js +99 -1
  23. package/dist/email.d.ts +0 -3
  24. package/dist/email.d.ts.map +1 -1
  25. package/dist/email.js +1 -4
  26. package/dist/index.d.ts +3 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +508 -212
  29. package/dist/routes/stream.js +1 -1
  30. package/dist/static.d.ts +405 -2
  31. package/dist/static.d.ts.map +1 -1
  32. package/dist/static.js +510 -6
  33. package/dist/tap-agents.d.ts +120 -0
  34. package/dist/tap-agents.d.ts.map +1 -0
  35. package/dist/tap-agents.js +225 -0
  36. package/dist/tap-routes.d.ts +215 -0
  37. package/dist/tap-routes.d.ts.map +1 -0
  38. package/dist/tap-routes.js +379 -0
  39. package/dist/tap-verify.d.ts +86 -0
  40. package/dist/tap-verify.d.ts.map +1 -0
  41. package/dist/tap-verify.js +275 -0
  42. package/package.json +3 -3
package/README.md CHANGED
@@ -1,18 +1,19 @@
1
1
  # @dupecom/botcha-cloudflare
2
2
 
3
- > 🤖 **BOTCHA** - Prove you're a bot. Humans need not apply.
3
+ > **BOTCHA** - Prove you're a bot. Humans need not apply.
4
4
  >
5
- > **Cloudflare Workers Edition v0.2.0** - Production-ready with JWT & Rate Limiting
5
+ > **Cloudflare Workers Edition v0.11.0** - Identity layer for AI agents
6
6
 
7
7
  Reverse CAPTCHA that verifies AI agents and blocks humans. Running at the edge.
8
8
 
9
- ## 🚀 What's New in v0.2.0
9
+ ## What's New in v0.11.0
10
10
 
11
- - **JWT Token Authentication** - Secure token-based auth flow with 1-hour expiry
12
- - **Rate Limiting** - 100 challenges/hour/IP with proper headers
13
- - **KV Storage** - Challenge state stored in Cloudflare KV (prevents replay attacks)
14
- - **Versioned API** - New `/v1/*` endpoints with backward-compatible legacy routes
15
- - **Production Ready** - Enterprise-grade auth and security
11
+ - **Agent Registry** - Persistent agent identities (POST /v1/agents/register)
12
+ - **Email-tied apps** - Email verification, account recovery, secret rotation
13
+ - **Dashboard** - Per-app metrics with agent-first auth (device code flow)
14
+ - **JWT security** - 5-min access tokens, refresh tokens, audience claims, IP binding, revocation
15
+ - **Multi-tenant** - Per-app isolation, scoped tokens, rate limiting
16
+ - **Server-side SDKs** - @botcha/verify (TS) + botcha-verify (Python)
16
17
 
17
18
  ## Features
18
19
 
@@ -0,0 +1,68 @@
1
+ /**
2
+ * BOTCHA Agent Registry
3
+ *
4
+ * Agent registration and management:
5
+ * - Crypto-random agent IDs
6
+ * - KV storage for agent metadata
7
+ * - App-scoped agent lists
8
+ * - Fail-open design for resilience
9
+ */
10
+ export type KVNamespace = {
11
+ get: (key: string, type?: 'text' | 'json' | 'arrayBuffer' | 'stream') => Promise<any>;
12
+ put: (key: string, value: string, options?: {
13
+ expirationTtl?: number;
14
+ }) => Promise<void>;
15
+ delete: (key: string) => Promise<void>;
16
+ };
17
+ /**
18
+ * Agent record stored in KV
19
+ */
20
+ export interface Agent {
21
+ agent_id: string;
22
+ app_id: string;
23
+ name: string;
24
+ operator?: string;
25
+ version?: string;
26
+ created_at: number;
27
+ }
28
+ /**
29
+ * Generate a crypto-random agent ID
30
+ * Format: 'agent_' + 16 hex chars
31
+ *
32
+ * Example: agent_a1b2c3d4e5f6a7b8
33
+ */
34
+ export declare function generateAgentId(): string;
35
+ /**
36
+ * Create a new agent and add it to the app's agent list
37
+ *
38
+ * Stores in KV at:
39
+ * - `agent:{agent_id}` — Agent record
40
+ * - `agents:{app_id}` — Array of agent_ids for this app
41
+ *
42
+ * @param kv - KV namespace for storage
43
+ * @param app_id - Parent app that owns this agent
44
+ * @param data - Agent metadata (name, operator, version)
45
+ * @returns Agent record with generated agent_id
46
+ */
47
+ export declare function createAgent(kv: KVNamespace, app_id: string, data: {
48
+ name: string;
49
+ operator?: string;
50
+ version?: string;
51
+ }): Promise<Agent | null>;
52
+ /**
53
+ * Get agent by agent_id
54
+ *
55
+ * @param kv - KV namespace
56
+ * @param agent_id - The agent ID to retrieve
57
+ * @returns Agent record or null if not found
58
+ */
59
+ export declare function getAgent(kv: KVNamespace, agent_id: string): Promise<Agent | null>;
60
+ /**
61
+ * List all agents for an app
62
+ *
63
+ * @param kv - KV namespace
64
+ * @param app_id - The app ID to list agents for
65
+ * @returns Array of agent records (empty array if none found)
66
+ */
67
+ export declare function listAgents(kv: KVNamespace, app_id: string): Promise<Agent[]>;
68
+ //# sourceMappingURL=agents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,aAAa,GAAG,QAAQ,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IACtF,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzF,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAIF;;GAEG;AACH,MAAM,WAAW,KAAK;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAID;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAOxC;AAID;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAC/B,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,MAAM,EACd,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1D,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAwCvB;AAED;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAC5B,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAcvB;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,KAAK,EAAE,CAAC,CAsBlB"}
package/dist/agents.js ADDED
@@ -0,0 +1,123 @@
1
+ /**
2
+ * BOTCHA Agent Registry
3
+ *
4
+ * Agent registration and management:
5
+ * - Crypto-random agent IDs
6
+ * - KV storage for agent metadata
7
+ * - App-scoped agent lists
8
+ * - Fail-open design for resilience
9
+ */
10
+ // ============ CRYPTO UTILITIES ============
11
+ /**
12
+ * Generate a crypto-random agent ID
13
+ * Format: 'agent_' + 16 hex chars
14
+ *
15
+ * Example: agent_a1b2c3d4e5f6a7b8
16
+ */
17
+ export function generateAgentId() {
18
+ const bytes = new Uint8Array(8); // 8 bytes = 16 hex chars
19
+ crypto.getRandomValues(bytes);
20
+ const hexString = Array.from(bytes)
21
+ .map(b => b.toString(16).padStart(2, '0'))
22
+ .join('');
23
+ return `agent_${hexString}`;
24
+ }
25
+ // ============ AGENT MANAGEMENT ============
26
+ /**
27
+ * Create a new agent and add it to the app's agent list
28
+ *
29
+ * Stores in KV at:
30
+ * - `agent:{agent_id}` — Agent record
31
+ * - `agents:{app_id}` — Array of agent_ids for this app
32
+ *
33
+ * @param kv - KV namespace for storage
34
+ * @param app_id - Parent app that owns this agent
35
+ * @param data - Agent metadata (name, operator, version)
36
+ * @returns Agent record with generated agent_id
37
+ */
38
+ export async function createAgent(kv, app_id, data) {
39
+ try {
40
+ const agent_id = generateAgentId();
41
+ const agent = {
42
+ agent_id,
43
+ app_id,
44
+ name: data.name,
45
+ operator: data.operator,
46
+ version: data.version,
47
+ created_at: Date.now(),
48
+ };
49
+ // Get existing agent list for this app (if any)
50
+ let agentIds = [];
51
+ try {
52
+ const existingList = await kv.get(`agents:${app_id}`, 'text');
53
+ if (existingList) {
54
+ agentIds = JSON.parse(existingList);
55
+ }
56
+ }
57
+ catch (error) {
58
+ console.warn(`Failed to fetch existing agent list for app ${app_id}, starting fresh:`, error);
59
+ // Fail-open: Continue with empty list
60
+ }
61
+ // Add new agent_id to the list
62
+ agentIds.push(agent_id);
63
+ // Store agent record and updated list in parallel
64
+ await Promise.all([
65
+ kv.put(`agent:${agent_id}`, JSON.stringify(agent)),
66
+ kv.put(`agents:${app_id}`, JSON.stringify(agentIds)),
67
+ ]);
68
+ return agent;
69
+ }
70
+ catch (error) {
71
+ console.error(`Failed to create agent for app ${app_id}:`, error);
72
+ // Fail-open: Return null instead of throwing
73
+ return null;
74
+ }
75
+ }
76
+ /**
77
+ * Get agent by agent_id
78
+ *
79
+ * @param kv - KV namespace
80
+ * @param agent_id - The agent ID to retrieve
81
+ * @returns Agent record or null if not found
82
+ */
83
+ export async function getAgent(kv, agent_id) {
84
+ try {
85
+ const data = await kv.get(`agent:${agent_id}`, 'text');
86
+ if (!data) {
87
+ return null;
88
+ }
89
+ return JSON.parse(data);
90
+ }
91
+ catch (error) {
92
+ console.error(`Failed to get agent ${agent_id}:`, error);
93
+ // Fail-open: Return null instead of throwing
94
+ return null;
95
+ }
96
+ }
97
+ /**
98
+ * List all agents for an app
99
+ *
100
+ * @param kv - KV namespace
101
+ * @param app_id - The app ID to list agents for
102
+ * @returns Array of agent records (empty array if none found)
103
+ */
104
+ export async function listAgents(kv, app_id) {
105
+ try {
106
+ // Get the list of agent IDs for this app
107
+ const agentIdsData = await kv.get(`agents:${app_id}`, 'text');
108
+ if (!agentIdsData) {
109
+ return [];
110
+ }
111
+ const agentIds = JSON.parse(agentIdsData);
112
+ // Fetch all agent records in parallel
113
+ const agentPromises = agentIds.map(agent_id => getAgent(kv, agent_id));
114
+ const agents = await Promise.all(agentPromises);
115
+ // Filter out any null results (failed fetches) and return
116
+ return agents.filter((agent) => agent !== null);
117
+ }
118
+ catch (error) {
119
+ console.error(`Failed to list agents for app ${app_id}:`, error);
120
+ // Fail-open: Return empty array instead of throwing
121
+ return [];
122
+ }
123
+ }
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,
@@ -472,8 +472,15 @@ function formatTimeBucket(timestamp, period) {
472
472
  }
473
473
  }
474
474
  // ============ MOCK DATA (when CF_API_TOKEN not configured) ============
475
- function renderMockOverview(_period) {
476
- return `<div class="dashboard-grid">
475
+ function renderSampleBanner() {
476
+ return `<div class="sample-banner">Sample data — analytics will appear here once production traffic flows</div>`;
477
+ }
478
+ function renderSampleLegend(title, period) {
479
+ return `${title} (${period})<span class="sample-tag">SAMPLE</span>`;
480
+ }
481
+ function renderMockOverview(period) {
482
+ return `${renderSampleBanner()}
483
+ <div class="dashboard-grid">
477
484
  ${renderStatCard('1,247', 'Challenges Generated')}
478
485
  ${renderStatCard('1,089', 'Verifications')}
479
486
  ${renderStatCard('94%', 'Success Rate', 'text-success')}
@@ -482,7 +489,7 @@ function renderMockOverview(_period) {
482
489
  ${renderStatCard('0', 'Errors')}
483
490
  </div>`;
484
491
  }
485
- function renderMockVolume(_period) {
492
+ function renderMockVolume(period) {
486
493
  const items = [
487
494
  { name: '00:00', value: 42, maxValue: 89 },
488
495
  { name: '04:00', value: 15, maxValue: 89 },
@@ -492,11 +499,11 @@ function renderMockVolume(_period) {
492
499
  { name: '20:00', value: 55, maxValue: 89 },
493
500
  ];
494
501
  return `<fieldset>
495
- <legend>Request Volume (sample)</legend>
502
+ <legend>${renderSampleLegend('Request Volume', period)}</legend>
496
503
  ${renderBarChart(items)}
497
504
  </fieldset>`;
498
505
  }
499
- function renderMockTypes(_period) {
506
+ function renderMockTypes(period) {
500
507
  const items = [
501
508
  { name: 'hybrid (412 ok / 18 fail)', value: 430, maxValue: 430 },
502
509
  { name: 'speed (389 ok / 12 fail)', value: 401, maxValue: 430 },
@@ -504,13 +511,13 @@ function renderMockTypes(_period) {
504
511
  { name: 'standard (22 ok / 5 fail)', value: 27, maxValue: 430 },
505
512
  ];
506
513
  return `<fieldset>
507
- <legend>Challenge Types (sample)</legend>
514
+ <legend>${renderSampleLegend('Challenge Types', period)}</legend>
508
515
  ${renderBarChart(items)}
509
516
  </fieldset>`;
510
517
  }
511
- function renderMockPerformance(_period) {
518
+ function renderMockPerformance(period) {
512
519
  return `<fieldset>
513
- <legend>Performance (sample)</legend>
520
+ <legend>${renderSampleLegend('Performance', period)}</legend>
514
521
  <table>
515
522
  <thead>
516
523
  <tr>
@@ -525,13 +532,13 @@ function renderMockPerformance(_period) {
525
532
  </table>
526
533
  </fieldset>`;
527
534
  }
528
- function renderMockErrors(_period) {
535
+ function renderMockErrors(period) {
529
536
  return `<fieldset>
530
- <legend>Errors & Rate Limits (sample)</legend>
531
- <div class="alert alert-success">No errors or rate limits (sample data)</div>
537
+ <legend>${renderSampleLegend('Errors & Rate Limits', period)}</legend>
538
+ <div class="alert alert-success">No errors or rate limits</div>
532
539
  </fieldset>`;
533
540
  }
534
- function renderMockGeo(_period) {
541
+ function renderMockGeo(period) {
535
542
  const items = [
536
543
  { name: 'US', value: 523, maxValue: 523 },
537
544
  { name: 'DE', value: 189, maxValue: 523 },
@@ -540,7 +547,7 @@ function renderMockGeo(_period) {
540
547
  { name: 'FR', value: 67, maxValue: 523 },
541
548
  ];
542
549
  return `<fieldset>
543
- <legend>Top Countries (sample)</legend>
550
+ <legend>${renderSampleLegend('Top Countries', period)}</legend>
544
551
  ${renderBarChart(items)}
545
552
  </fieldset>`;
546
553
  }
@@ -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,qBA0HvE"}
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.',
@@ -392,10 +402,10 @@ export async function renderLoginPage(c) {
392
402
  document.querySelector('form').submit();
393
403
  }
394
404
  `;
395
- return c.html(_jsxs(LoginLayout, { title: "Dashboard Login - BOTCHA", children: [_jsx("div", { class: "ascii-logo", children: ` ____ ___ _____ ____ _ _ _
396
- | __ ) / _ \\_ _/ ___| | | | / \\
397
- | _ \\| | | || || | | |_| |/ _ \\
398
- | |_) | |_| || || |___| _ / ___ \\
399
- |____/ \\___/ |_| \\____|_| |_/_/ \\_\\
400
- >_ prove you're a bot` }), _jsxs(Card, { title: "Device Code", badge: "agent required", children: [_jsx("p", { class: "text-muted mb-2", style: "font-size: 0.75rem;", children: "Your AI agent can generate a login code for you." }), _jsxs("a", { href: "/dashboard/code", class: "button btn", children: ["Enter Device Code ", '>'] }), _jsxs("div", { class: "hint", children: ["Agent: ", _jsx("code", { children: "POST /v1/auth/device-code" }), " then solve the challenge."] })] }), _jsx(Divider, { text: "or" }), _jsx("form", { method: "post", action: "/dashboard/email-login", children: _jsxs(Card, { title: "Email Login", badge: "returning users", children: [error === 'email_missing' && (_jsx("div", { class: "error-message", children: errorMap[error] })), _jsx("p", { class: "text-muted mb-2", style: "font-size: 0.75rem;", children: "Enter the email you used when creating your app. We'll send a login code to your inbox." }), _jsxs("div", { class: "form-group", children: [_jsx("label", { for: "email", children: "Email" }), _jsx("input", { type: "email", id: "email", name: "email", placeholder: "you@example.com", required: true, autocomplete: "email" })] }), _jsxs("button", { type: "submit", children: ["Email Me a Code ", '>'] })] }) }), _jsx(Divider, { text: "or sign in with credentials" }), _jsx("form", { method: "post", action: "/dashboard/login", children: _jsxs(Card, { title: "App Credentials", children: [error && error !== 'email_missing' && errorMap[error] && (_jsx("div", { class: "error-message", children: errorMap[error] })), _jsxs("div", { id: "create-result", children: [_jsx("div", { class: "warning", children: "Save these credentials now. The secret will not be shown again." }), _jsxs("div", { class: "credentials-box", children: [_jsx("span", { class: "label", children: "app_id: " }), _jsx("span", { class: "value", id: "new-app-id" }), _jsx("br", {}), _jsx("span", { class: "label", children: "secret: " }), _jsx("span", { class: "value", id: "new-app-secret" })] }), _jsxs("button", { type: "button", onclick: "fillAndLogin()", style: "width: 100%; margin-bottom: 1rem;", children: ["Login With New Credentials ", '>'] })] }), _jsxs("div", { class: "form-group", children: [_jsx("label", { for: "app_id", children: "App ID" }), _jsx("input", { type: "text", id: "app_id", name: "app_id", placeholder: "app_...", required: true, autocomplete: "username" })] }), _jsxs("div", { class: "form-group", children: [_jsx("label", { for: "app_secret", children: "App Secret" }), _jsx("input", { type: "password", id: "app_secret", name: "app_secret", placeholder: "sk_...", required: true, autocomplete: "current-password" })] }), _jsxs("div", { style: "display: flex; gap: 0.75rem; align-items: center;", children: [_jsxs("button", { type: "submit", children: ["Login ", '>'] }), _jsxs("button", { type: "button", id: "create-btn", class: "btn-secondary", onclick: "createApp()", children: ["Create App ", '>'] })] })] }) }), _jsx("script", { dangerouslySetInnerHTML: { __html: CREATE_APP_SCRIPT } })] }));
405
+ return c.html(_jsxs(LoginLayout, { title: "Dashboard Login - BOTCHA", children: [_jsx("a", { href: "/", class: "ascii-logo", children: `██████╗ ██████╗ ████████╗ ██████╗██╗ ██╗ █████╗
406
+ ██╔══██╗██╔═══██╗╚══██╔══╝██╔════╝██║ ██║██╔══██╗
407
+ ██████╔╝██║ ██║ ██║ ██║ ███████║███████║
408
+ ██╔══██╗██║ ██║ ██║ ██║ ██╔══██║██╔══██║
409
+ ██████╔╝╚██████╔╝ ██║ ╚██████╗██║ ██║██║ ██║
410
+ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝` }), _jsxs("p", { class: "text-muted", style: "text-align: center; font-size: 0.75rem; margin: -1rem 0 2rem;", children: ['>', "_\u00A0prove you're a bot"] }), _jsxs(Card, { title: "Device Code", badge: "agent required", children: [_jsx("p", { class: "text-muted mb-2", style: "font-size: 0.75rem;", children: "Your AI agent can generate a login code for you." }), _jsxs("a", { href: "/dashboard/code", class: "button btn", children: ["Enter Device Code ", '>'] }), _jsxs("div", { class: "hint", children: ["Agent: ", _jsx("code", { children: "POST /v1/auth/device-code" }), " then solve the challenge."] })] }), _jsx(Divider, { text: "or" }), _jsx("form", { method: "post", action: "/dashboard/email-login", children: _jsxs(Card, { title: "Email Login", badge: "returning users", children: [error === 'email_missing' && (_jsx("div", { class: "error-message", children: errorMap[error] })), _jsx("p", { class: "text-muted mb-2", style: "font-size: 0.75rem;", children: "Enter the email you used when creating your app. We'll send a login code to your inbox." }), _jsxs("div", { class: "form-group", children: [_jsx("label", { for: "email", children: "Email" }), _jsx("input", { type: "email", id: "email", name: "email", placeholder: "you@example.com", required: true, autocomplete: "email" })] }), _jsxs("button", { type: "submit", children: ["Email Me a Code ", '>'] })] }) }), _jsx(Divider, { text: "or sign in with credentials" }), _jsx("form", { method: "post", action: "/dashboard/login", children: _jsxs(Card, { title: "App Credentials", children: [error && error !== 'email_missing' && errorMap[error] && (_jsx("div", { class: "error-message", children: errorMap[error] })), _jsxs("div", { id: "create-result", children: [_jsx("div", { class: "warning", children: "Save these credentials now. The secret will not be shown again." }), _jsxs("div", { class: "credentials-box", children: [_jsx("span", { class: "label", children: "app_id: " }), _jsx("span", { class: "value", id: "new-app-id" }), _jsx("br", {}), _jsx("span", { class: "label", children: "secret: " }), _jsx("span", { class: "value", id: "new-app-secret" })] }), _jsxs("button", { type: "button", onclick: "fillAndLogin()", style: "width: 100%; margin-bottom: 1rem;", children: ["Login With New Credentials ", '>'] })] }), _jsxs("div", { class: "form-group", children: [_jsx("label", { for: "app_id", children: "App ID" }), _jsx("input", { type: "text", id: "app_id", name: "app_id", placeholder: "app_...", required: true, autocomplete: "username" })] }), _jsxs("div", { class: "form-group", children: [_jsx("label", { for: "app_secret", children: "App Secret" }), _jsx("input", { type: "password", id: "app_secret", name: "app_secret", placeholder: "sk_...", required: true, autocomplete: "current-password" })] }), _jsxs("div", { style: "display: flex; gap: 0.75rem; align-items: center;", children: [_jsxs("button", { type: "submit", children: ["Login ", '>'] }), _jsxs("button", { type: "button", id: "create-btn", class: "btn-secondary", onclick: "createApp()", children: ["Create App ", '>'] })] })] }) }), _jsx("script", { dangerouslySetInnerHTML: { __html: CREATE_APP_SCRIPT } })] }));
401
411
  }
@@ -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
@@ -0,0 +1,20 @@
1
+ /**
2
+ * BOTCHA Landing Pages (JSX)
3
+ *
4
+ * Two views at GET /:
5
+ * - LandingPage: ultra-minimal — one prompt to copy-paste to your agent
6
+ * - VerifiedLandingPage: for humans whose agent solved the challenge
7
+ *
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
+ */
11
+ import type { FC } from 'hono/jsx';
12
+ export declare const LandingPage: FC<{
13
+ version: string;
14
+ error?: string;
15
+ }>;
16
+ export declare const VerifiedLandingPage: FC<{
17
+ version: string;
18
+ solveTime?: number;
19
+ }>;
20
+ //# sourceMappingURL=landing.d.ts.map
@@ -0,0 +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;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"}
@@ -0,0 +1,85 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
+ import { LandingLayout } from './layout';
3
+ import { Card } from './layout';
4
+ const BOTCHA_ASCII = `██████╗ ██████╗ ████████╗ ██████╗██╗ ██╗ █████╗
5
+ ██╔══██╗██╔═══██╗╚══██╔══╝██╔════╝██║ ██║██╔══██╗
6
+ ██████╔╝██║ ██║ ██║ ██║ ███████║███████║
7
+ ██╔══██╗██║ ██║ ██║ ██║ ██╔══██║██╔══██║
8
+ ██████╔╝╚██████╔╝ ██║ ╚██████╗██║ ██║██║ ██║
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
+ `;
32
+ // ============ UNVERIFIED LANDING PAGE ============
33
+ export const LandingPage = ({ version, error }) => {
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: {
35
+ __html: JSON.stringify({
36
+ version: '1.0',
37
+ type: 'inline-challenge',
38
+ description: 'AI agents: solve this challenge to prove you are a bot!',
39
+ challenge: {
40
+ type: 'sha256',
41
+ instruction: "Compute SHA256 of the string 'BOTCHA-LANDING-' + today's date (YYYY-MM-DD format). Return first 16 hex chars.",
42
+ example: "For 2026-01-15: SHA256('BOTCHA-LANDING-2026-01-15') -> first 16 chars",
43
+ submit_to: 'https://botcha.ai/api/verify-landing',
44
+ method: 'POST',
45
+ body_format: {
46
+ answer: 'your 16 hex chars',
47
+ timestamp: 'ISO 8601 timestamp',
48
+ },
49
+ },
50
+ on_success: {
51
+ grants_access_to: 'https://botcha.ai/agent-only',
52
+ header_to_include: 'X-Botcha-Landing-Token',
53
+ },
54
+ }, null, 2),
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" })] })] }));
56
+ };
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
+ `;
77
+ export const VerifiedLandingPage = ({ version, solveTime }) => {
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)
79
+ npm install @dupecom/botcha # TypeScript
80
+ pip install botcha # Python
81
+
82
+ # Server SDK (protect your APIs)
83
+ npm install @botcha/verify # Express/Hono
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" })] })] }));
85
+ };
@@ -44,4 +44,11 @@ export declare const DashboardLayout: FC<PropsWithChildren<{
44
44
  export declare const LoginLayout: FC<PropsWithChildren<{
45
45
  title?: string;
46
46
  }>>;
47
+ /**
48
+ * Landing page layout — wider than LoginLayout, includes SEO meta tags.
49
+ * Used for the public landing page at GET /
50
+ */
51
+ export declare const LandingLayout: FC<PropsWithChildren<{
52
+ version: string;
53
+ }>>;
47
54
  //# sourceMappingURL=layout.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/dashboard/layout.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAKtD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,IAAI,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAmBzF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,OAAO,EAAE,EAAE,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAExC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAmCrF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAsBjE,CAAC"}
1
+ {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/dashboard/layout.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAKtD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,IAAI,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAmBzF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,OAAO,EAAE,EAAE,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAExC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAmCrF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAsBjE,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAoDpE,CAAC"}