@dupecom/botcha-cloudflare 0.11.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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;
@@ -170,11 +181,9 @@ export declare function handleEmailLogin(c: Context<{
170
181
  /**
171
182
  * GET /dashboard/login
172
183
  *
173
- * Four ways in:
174
- * 1. Device code (agent generated the code) — primary
184
+ * Two ways in:
185
+ * 1. Agent prompt (primary) — copy curl command, agent gets device code
175
186
  * 2. Email login (returning users — code emailed to verified address)
176
- * 3. App ID + Secret (agent created the app)
177
- * 4. Create new app (triggers POST /v1/apps)
178
187
  */
179
188
  export declare function renderLoginPage(c: Context<{
180
189
  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;AA4BD;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,qBAiFvE"}
@@ -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.',
@@ -344,58 +354,44 @@ export async function handleEmailLogin(c) {
344
354
  return c.redirect('/dashboard/code?email_sent=1');
345
355
  }
346
356
  // ============ LOGIN PAGE ============
357
+ const DASHBOARD_PROMPT = `Run: curl -s https://botcha.ai -H 'Accept: application/json' — then follow the instructions to verify yourself and get me a dashboard login code for botcha.ai.`;
358
+ const COPY_ICON_SVG = `<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>`;
359
+ const CHECK_ICON_SVG = `<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>`;
360
+ const LOGIN_COPY_SCRIPT = `
361
+ function copyDashPrompt() {
362
+ var text = document.getElementById('dash-prompt').textContent.trim();
363
+ navigator.clipboard.writeText(text).then(function() {
364
+ var label = document.getElementById('dash-copy-label');
365
+ var icon = document.getElementById('dash-copy-icon');
366
+ var txt = document.getElementById('dash-copy-text');
367
+ label.style.color = 'var(--green)';
368
+ icon.innerHTML = '${CHECK_ICON_SVG.replace(/'/g, "\\'")}';
369
+ txt.textContent = 'Copied — now paste into your agent';
370
+ setTimeout(function() {
371
+ label.style.color = 'var(--text-muted)';
372
+ icon.innerHTML = '${COPY_ICON_SVG.replace(/'/g, "\\'")}';
373
+ txt.textContent = 'Click to copy';
374
+ }, 2500);
375
+ });
376
+ }
377
+ `;
347
378
  /**
348
379
  * GET /dashboard/login
349
380
  *
350
- * Four ways in:
351
- * 1. Device code (agent generated the code) — primary
381
+ * Two ways in:
382
+ * 1. Agent prompt (primary) — copy curl command, agent gets device code
352
383
  * 2. Email login (returning users — code emailed to verified address)
353
- * 3. App ID + Secret (agent created the app)
354
- * 4. Create new app (triggers POST /v1/apps)
355
384
  */
356
385
  export async function renderLoginPage(c) {
357
386
  const url = new URL(c.req.url);
358
387
  const error = url.searchParams.get('error');
359
388
  const errorMap = {
360
- invalid: 'Invalid app ID or secret',
361
- missing: 'Please provide both app ID and secret',
362
- server: 'Server error. Please try again.',
363
389
  email_missing: 'Please enter your email address.',
364
390
  };
365
- const CREATE_APP_SCRIPT = `
366
- async function createApp() {
367
- var btn = document.getElementById('create-btn');
368
- btn.classList.add('loading');
369
- btn.textContent = 'Creating...';
370
- try {
371
- var resp = await fetch('/v1/apps', { method: 'POST' });
372
- var data = await resp.json();
373
- if (data.app_id && data.app_secret) {
374
- document.getElementById('new-app-id').textContent = data.app_id;
375
- document.getElementById('new-app-secret').textContent = data.app_secret;
376
- document.getElementById('create-result').classList.add('show');
377
- btn.style.display = 'none';
378
- } else {
379
- btn.textContent = '[ERR] try again >';
380
- btn.classList.remove('loading');
381
- }
382
- } catch (e) {
383
- btn.textContent = '[ERR] try again >';
384
- btn.classList.remove('loading');
385
- }
386
- }
387
- function fillAndLogin() {
388
- var appId = document.getElementById('new-app-id').textContent;
389
- var secret = document.getElementById('new-app-secret').textContent;
390
- document.getElementById('app_id').value = appId;
391
- document.getElementById('app_secret').value = secret;
392
- document.querySelector('form').submit();
393
- }
394
- `;
395
391
  return c.html(_jsxs(LoginLayout, { title: "Dashboard Login - BOTCHA", children: [_jsx("a", { href: "/", class: "ascii-logo", children: `██████╗ ██████╗ ████████╗ ██████╗██╗ ██╗ █████╗
396
392
  ██╔══██╗██╔═══██╗╚══██╔══╝██╔════╝██║ ██║██╔══██╗
397
393
  ██████╔╝██║ ██║ ██║ ██║ ███████║███████║
398
394
  ██╔══██╗██║ ██║ ██║ ██║ ██╔══██║██╔══██║
399
395
  ██████╔╝╚██████╔╝ ██║ ╚██████╗██║ ██║██║ ██║
400
- ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝` }), _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 } })] }));
396
+ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝` }), _jsxs("p", { class: "text-muted", style: "text-align: center; font-size: 0.75rem; margin: -1rem 0 2rem;", children: ['>', "_\u00A0dashboard login"] }), _jsx("p", { class: "text-muted", style: "font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.15em; text-align: center; margin-bottom: 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: "dash-prompt-btn", onclick: "copyDashPrompt()", 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: "dash-prompt", style: "font-size: 0.9375rem; font-weight: 700; color: var(--accent); line-height: 1.5; display: block; background: none; border: none; padding: 0;", children: DASHBOARD_PROMPT }), _jsxs("span", { id: "dash-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: "dash-copy-icon", style: "display: flex; transition: color 0.2s;", dangerouslySetInnerHTML: { __html: COPY_ICON_SVG } }), _jsx("span", { id: "dash-copy-text", children: "Click to copy" })] })] }) }) }), _jsxs("p", { class: "text-muted", style: "font-size: 0.75rem; text-align: center; line-height: 1.8; margin-bottom: 1.5rem;", children: ["Your agent solves a challenge, gets a code, and gives you a link.", _jsx("br", {}), "Click it. You're in the dashboard."] }), _jsx(Divider, { text: "returning user?" }), _jsx("form", { method: "post", action: "/dashboard/email-login", children: _jsxs(Card, { title: "Email Login", 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("script", { dangerouslySetInnerHTML: { __html: LOGIN_COPY_SCRIPT } })] }));
401
397
  }
@@ -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,CAyI/D,CAAC;AAyBF,eAAO,MAAM,mBAAmB,EAAE,EAAE,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CA8F3E,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("div", { style: "text-align: center; margin-top: 1.5rem; display: flex; flex-direction: column; align-items: center; gap: 0.625rem;", children: [_jsx("a", { href: "/dashboard/login", class: "text-muted", style: "font-size: 0.6875rem; text-decoration: underline; text-underline-offset: 3px;", children: "Already have an account? Log in" }), _jsxs("details", { style: "margin: 0;", 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: "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."] }) }), _jsx("div", { style: "text-align: center; margin-bottom: 1.5rem;", children: _jsx("a", { href: "/dashboard/login", style: "font-size: 0.75rem; color: var(--text-muted); text-decoration: underline; text-underline-offset: 3px;", children: "Already have an account? Go to dashboard" }) }), _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%;" })] }));
@@ -7,5 +7,5 @@
7
7
  *
8
8
  * JetBrains Mono · #ffffff bg · #1a1a1a text · black accent · square corners · dot shadows
9
9
  */
10
- export declare const DASHBOARD_CSS = "\n /* ============ Reset ============ */\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n :root {\n /* ---- palette (black & white) ---- */\n --bg: #ffffff;\n --bg-card: #ffffff;\n --bg-raised: #eae8e4;\n --text: #1a1a1a;\n --text-muted: #6b6b6b;\n --text-dim: #a0a0a0;\n --accent: #1a1a1a;\n --accent-dim: #333333;\n --red: #cc2222;\n --amber: #b87a00;\n --green: #1a8a2a;\n --border: #ddd9d4;\n --border-bright: #c0bbb5;\n\n /* ---- type ---- */\n --font: 'JetBrains Mono', 'Courier New', monospace;\n\n /* ---- dot shadow (turbopuffer SVG pattern, black fill) ---- */\n --dot-shadow: url(\"data:image/svg+xml,%3Csvg width='7' height='13' viewBox='0 0 7 13' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5.58984 12.2344V10.7051H6.52734V12.2344H5.58984ZM1.86328 12.2344V10.7051H2.79492V12.2344H1.86328ZM3.72656 10.0957V8.56641H4.6582V10.0957H3.72656ZM0 10.0957V8.56641H0.925781V10.0957H0ZM5.58984 7.95117V6.42188H6.52734V7.95117H5.58984ZM1.86328 7.95117V6.42188H2.79492V7.95117H1.86328ZM3.72656 5.8125V4.2832H4.6582V5.8125H3.72656ZM0 5.8125V4.2832H0.925781V5.8125H0ZM5.58984 3.66797V2.13867H6.52734V3.66797H5.58984ZM1.86328 3.66797V2.13867H2.79492V3.66797H1.86328ZM3.72656 1.5293V0H4.6582V1.5293H3.72656ZM0 1.5293V0H0.925781V1.5293H0Z' fill='%231a1a1a'/%3E%3C/svg%3E\");\n }\n\n /* ============ Base ============ */\n html, body {\n height: 100%;\n font-family: var(--font);\n font-size: 16px;\n line-height: 1.6;\n background: var(--bg);\n color: var(--text);\n -webkit-font-smoothing: antialiased;\n }\n\n body { display: flex; flex-direction: column; }\n\n ::selection { background: var(--accent); color: #fff; }\n\n a { color: var(--accent); }\n a:hover { text-decoration: none; opacity: 0.65; }\n\n /* ============ Scanline overlay (subtle CRT feel) ============ */\n body::before {\n content: '';\n position: fixed;\n inset: 0;\n background: repeating-linear-gradient(\n 0deg,\n transparent,\n transparent 2px,\n rgba(0, 0, 0, 0.012) 2px,\n rgba(0, 0, 0, 0.012) 4px\n );\n pointer-events: none;\n z-index: 9999;\n }\n\n /* ============ Dot shadow utility ============ */\n .dot-shadow { position: relative; }\n .dot-shadow::after {\n content: '';\n position: absolute;\n top: 0.5rem; left: 0.5rem;\n right: -0.5rem; bottom: -0.5rem;\n background-image: var(--dot-shadow);\n background-repeat: repeat;\n z-index: -1;\n pointer-events: none;\n opacity: 0.6;\n }\n\n /* ============ Navigation ============ */\n .dashboard-nav {\n background: var(--bg);\n border-bottom: 1px solid var(--border);\n position: sticky; top: 0; z-index: 100;\n }\n\n .nav-container {\n max-width: 1200px;\n margin: 0 auto;\n padding: 0.75rem 1.5rem;\n display: flex;\n align-items: center;\n gap: 1.5rem;\n }\n\n .nav-logo {\n font-weight: 700;\n font-size: 0.875rem;\n color: var(--text);\n text-decoration: none;\n letter-spacing: 0.15em;\n text-transform: uppercase;\n }\n .nav-logo:hover { opacity: 1; }\n\n .nav-app-id {\n color: var(--text-muted);\n font-size: 0.75rem;\n margin-left: auto;\n }\n\n .nav-link {\n color: var(--text);\n text-decoration: none;\n font-size: 0.75rem;\n border: 1px solid var(--border-bright);\n padding: 0.25rem 0.75rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n transition: background 0.1s, color 0.1s;\n }\n .nav-link:hover {\n background: var(--accent);\n color: var(--bg);\n border-color: var(--accent);\n opacity: 1;\n }\n\n /* ============ Main content ============ */\n .dashboard-main {\n flex: 1;\n max-width: 1200px;\n width: 100%;\n margin: 0 auto;\n padding: 2rem 1.5rem;\n }\n\n /* ============ Card \u2014 primary container (Turbopuffer-style) ============ */\n .card {\n display: flex;\n flex-direction: column;\n margin-bottom: 1.5rem;\n }\n\n .card-header {\n margin-bottom: -1px; /* overlap the border */\n padding: 0;\n }\n\n .card-header h3 {\n position: relative;\n display: inline-flex;\n align-items: center;\n z-index: 10;\n top: 0.5rem;\n left: 0.5rem;\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n line-height: 1;\n color: var(--text);\n background: var(--bg);\n margin: 0;\n padding: 0 0.5rem;\n }\n\n .card-header h3 .card-title {\n }\n\n .card-header h3 .badge-inline {\n }\n\n .card-body {\n position: relative;\n border: 2px solid var(--border-bright);\n }\n\n .card-body::before {\n content: '';\n position: absolute;\n top: 0.5rem;\n left: 0.5rem;\n right: -0.5rem;\n bottom: -0.5rem;\n background-image: var(--dot-shadow);\n background-repeat: repeat;\n pointer-events: none;\n opacity: 0.6;\n }\n\n .card-inner {\n position: relative;\n z-index: 1;\n background: var(--bg-card);\n padding: 1.5rem;\n }\n\n /* Legacy fieldset support (deprecated \u2014 use .card instead) */\n fieldset {\n border: 2px solid var(--border-bright);\n border-radius: 0;\n padding: 1.5rem;\n margin-bottom: 1.5rem;\n background: var(--bg-card);\n position: relative;\n z-index: 0;\n }\n\n legend {\n padding: 0 0.5rem;\n font-size: 0.75rem;\n color: var(--text);\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n }\n\n /* ============ Dashboard grid ============ */\n .dashboard-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));\n gap: 1.5rem;\n margin-bottom: 1.5rem;\n }\n\n /* ============ Stat cards ============ */\n .stat-card {\n display: flex; flex-direction: column; gap: 0.25rem;\n padding: 1rem;\n border: 1px solid var(--border);\n background: var(--bg-card);\n }\n\n .stat-card .stat-value {\n font-size: 2rem; font-weight: 700; line-height: 1;\n color: var(--text);\n font-variant-numeric: tabular-nums;\n }\n\n .stat-card .stat-label {\n font-size: 0.6875rem;\n color: var(--text-muted);\n text-transform: uppercase;\n letter-spacing: 0.1em;\n }\n\n .stat-card .stat-change { font-size: 0.75rem; font-weight: 500; }\n .stat-card .stat-change.positive { color: var(--green); }\n .stat-card .stat-change.negative { color: var(--red); }\n\n /* ============ Bar chart ============ */\n .bar-chart { width: 100%; }\n\n .bar-chart .bar-item { margin-bottom: 0.75rem; }\n\n .bar-chart .bar-label {\n display: flex; justify-content: space-between; align-items: center;\n margin-bottom: 0.125rem; font-size: 0.75rem;\n }\n .bar-chart .bar-name { color: var(--text); }\n .bar-chart .bar-value {\n color: var(--text-muted);\n font-weight: 700; font-variant-numeric: tabular-nums;\n }\n\n .bar-chart .bar {\n height: 18px;\n background: var(--accent);\n border-radius: 0;\n transition: width 0.3s ease;\n position: relative; overflow: hidden;\n opacity: 0.8;\n }\n .bar-chart .bar:hover { opacity: 1; }\n\n .bar-chart .bar-fill { height: 100%; background: var(--accent); }\n\n /* ============ Form controls ============ */\n input, select, textarea, button { font-family: var(--font); font-size: 0.875rem; }\n\n input[type=\"text\"],\n input[type=\"email\"],\n input[type=\"password\"],\n input[type=\"number\"],\n select,\n textarea {\n width: 100%;\n padding: 0.625rem 0.75rem;\n background: var(--bg);\n border: 1px solid var(--border-bright);\n border-radius: 0;\n color: var(--text);\n }\n\n input:focus, select:focus, textarea:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: 0 0 0 1px var(--accent);\n }\n\n input::placeholder, textarea::placeholder { color: var(--text-dim); }\n\n label {\n display: block;\n margin-bottom: 0.375rem;\n font-size: 0.6875rem;\n color: var(--text-muted);\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n }\n\n .form-group { margin-bottom: 1.25rem; }\n\n /* ============ Buttons ============ */\n button, .button {\n display: inline-block;\n padding: 0.625rem 1.25rem;\n background: var(--accent);\n color: #fff;\n border: 1px solid var(--accent);\n border-radius: 0;\n font-weight: 700;\n cursor: pointer;\n text-decoration: none;\n text-align: center;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n font-size: 0.75rem;\n box-shadow:\n inset 1px 1px 0 rgba(255,255,255,0.15),\n inset -1px -1px 0 rgba(0,0,0,0.15),\n 2px 2px 0 rgba(0,0,0,0.1);\n transition: box-shadow 0.1s, transform 0.1s;\n }\n button:hover, .button:hover {\n box-shadow:\n inset 1px 1px 0 rgba(255,255,255,0.1),\n inset -1px -1px 0 rgba(0,0,0,0.15),\n 3px 3px 0 rgba(0,0,0,0.12);\n opacity: 1;\n }\n button:active, .button:active {\n transform: translate(1px, 1px);\n box-shadow: inset 1px 1px 3px rgba(0,0,0,0.25);\n }\n button:disabled, .button:disabled { opacity: 0.25; cursor: not-allowed; }\n\n button.secondary, .button.secondary {\n background: transparent;\n color: var(--text);\n border-color: var(--border-bright);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.05);\n }\n button.secondary:hover, .button.secondary:hover {\n border-color: var(--accent);\n color: var(--accent);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.1);\n }\n\n button.danger, .button.danger {\n background: var(--red);\n border-color: var(--red);\n color: #fff;\n box-shadow: 2px 2px 0 rgba(204,34,34,0.15);\n }\n button.danger:hover, .button.danger:hover {\n background: transparent;\n color: var(--red);\n }\n\n /* ============ Tables ============ */\n table { width: 100%; border-collapse: collapse; }\n\n thead { border-bottom: 1px solid var(--border-bright); }\n th {\n padding: 0.5rem 0.75rem; text-align: left;\n font-size: 0.6875rem; color: var(--text);\n font-weight: 700; text-transform: uppercase;\n letter-spacing: 0.1em;\n background: var(--bg-raised);\n }\n\n td {\n padding: 0.5rem 0.75rem;\n border-bottom: 1px solid var(--border);\n font-size: 0.75rem;\n font-variant-numeric: tabular-nums;\n }\n\n tbody tr:hover { background: var(--bg-raised); }\n\n /* ============ Code ============ */\n code, pre {\n font-family: var(--font);\n background: var(--bg-raised);\n padding: 0.125rem 0.375rem;\n border-radius: 0;\n font-size: 0.75rem;\n border: 1px solid var(--border);\n color: var(--text);\n }\n pre { padding: 1rem; overflow-x: auto; border: 1px solid var(--border-bright); }\n pre code { background: none; padding: 0; border: none; }\n\n /* ============ Login layout ============ */\n .login-container {\n min-height: 100vh;\n display: flex; align-items: center; justify-content: center;\n padding: 2rem;\n background: var(--bg);\n }\n .login-box { width: 100%; max-width: 420px; }\n\n .login-header { text-align: center; margin-bottom: 2rem; }\n .login-header h1 {\n font-size: 1.5rem; font-weight: 700; color: var(--text);\n letter-spacing: 0.15em; text-transform: uppercase;\n margin-bottom: 0.25rem;\n }\n .login-header p { color: var(--text-muted); font-size: 0.75rem; }\n\n /* ============ htmx loading ============ */\n .htmx-indicator { opacity: 0; transition: opacity 0.15s; }\n .htmx-request .htmx-indicator { opacity: 1; }\n .htmx-request.htmx-swapping { opacity: 0.3; pointer-events: none; }\n\n /* ============ Skeleton \u2014 blinking cursor ============ */\n .skeleton {\n background: var(--bg-raised);\n border: 1px solid var(--border);\n position: relative;\n overflow: hidden;\n }\n .skeleton::after {\n content: '';\n position: absolute; left: 0; top: 0;\n width: 2px; height: 100%;\n background: var(--text);\n animation: cursor-blink 0.8s step-end infinite;\n }\n @keyframes cursor-blink {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0; }\n }\n\n .skeleton-text { height: 1rem; margin-bottom: 0.5rem; }\n .skeleton-heading { height: 1.5rem; width: 60%; margin-bottom: 1rem; }\n\n /* ============ Utilities ============ */\n .text-center { text-align: center; }\n .text-right { text-align: right; }\n .text-muted { color: var(--text-muted); }\n .text-dim { color: var(--text-dim); }\n .text-success { color: var(--green); }\n .text-danger { color: var(--red); }\n .text-warning { color: var(--amber); }\n\n .mb-0 { margin-bottom: 0; }\n .mb-1 { margin-bottom: 0.5rem; }\n .mb-2 { margin-bottom: 1rem; }\n .mb-3 { margin-bottom: 1.5rem; }\n .mb-4 { margin-bottom: 2rem; }\n .mt-0 { margin-top: 0; }\n .mt-1 { margin-top: 0.5rem; }\n .mt-2 { margin-top: 1rem; }\n .mt-3 { margin-top: 1.5rem; }\n .mt-4 { margin-top: 2rem; }\n\n /* ============ Period selector ============ */\n .period-selector button {\n font-size: 0.625rem;\n padding: 0.2rem 0.5rem;\n }\n\n /* ============ Responsive ============ */\n @media (max-width: 768px) {\n html, body { font-size: 14px; }\n .dashboard-main { padding: 1rem; }\n .nav-container { padding: 0.5rem 1rem; }\n .dashboard-grid { grid-template-columns: 1fr; gap: 1rem; }\n .card-inner { padding: 1rem; }\n .card { margin-bottom: 1rem; }\n fieldset { padding: 1rem; margin-bottom: 1rem; }\n .stat-card .stat-value { font-size: 1.75rem; }\n table { font-size: 0.625rem; }\n th, td { padding: 0.375rem 0.5rem; }\n }\n\n @media (max-width: 480px) {\n .nav-container { flex-wrap: wrap; }\n .nav-app-id { margin-left: 0; width: 100%; order: 3; }\n }\n\n /* ============ Alerts ============ */\n .alert {\n padding: 0.75rem 1rem;\n border-radius: 0;\n margin-bottom: 1.5rem;\n border: 1px solid var(--border-bright);\n font-size: 0.75rem;\n background: var(--bg-card);\n }\n .alert::before { font-weight: 700; margin-right: 0.5rem; }\n\n .alert-info { border-color: var(--border-bright); color: var(--text); }\n .alert-info::before { content: '>'; color: var(--text); }\n\n .alert-success { border-color: var(--green); color: var(--green); }\n .alert-success::before { content: '[ok]'; }\n\n .alert-warning { border-color: var(--amber); color: var(--amber); }\n .alert-warning::before { content: '[!!]'; }\n\n .alert-danger { border-color: var(--red); color: var(--red); }\n .alert-danger::before { content: '[ERR]'; }\n\n /* ============ Badges ============ */\n .badge {\n display: inline-block;\n padding: 0.125rem 0.375rem;\n font-size: 0.625rem; font-weight: 700;\n border-radius: 0;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n border: 1px solid;\n }\n .badge-success { color: var(--green); border-color: var(--green); background: transparent; }\n .badge-danger { color: var(--red); border-color: var(--red); background: transparent; }\n .badge-warning { color: var(--amber); border-color: var(--amber); background: transparent; }\n .badge-info { color: #fff; border-color: var(--accent); background: var(--accent); }\n\n /* ============ Sample data indicator ============ */\n .sample-banner {\n background: #fffbe6;\n border: 1px solid var(--amber);\n color: var(--amber);\n padding: 0.5rem 0.75rem;\n font-size: 0.6875rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin-bottom: 1rem;\n }\n .sample-banner::before { content: '[demo]'; margin-right: 0.5rem; }\n .sample-tag {\n color: var(--amber);\n font-size: 0.625rem;\n font-weight: 700;\n letter-spacing: 0.05em;\n border: 1px solid var(--amber);\n padding: 0.0625rem 0.3rem;\n margin-left: 0.375rem;\n vertical-align: middle;\n background: #fffbe6;\n }\n\n /* ============ Empty state ============ */\n .empty-state { text-align: center; padding: 3rem 1rem; color: var(--text-muted); }\n .empty-state-icon { font-size: 1.5rem; margin-bottom: 0.75rem; color: var(--text); font-weight: 700; }\n .empty-state-text { font-size: 0.8125rem; margin-bottom: 0.25rem; }\n .empty-state-subtext { font-size: 0.6875rem; color: var(--text-dim); }\n\n /* ============ Auth pages ============ */\n .ascii-logo {\n display: block; text-align: center; margin-bottom: 2rem;\n color: var(--text); font-size: 0.55rem; line-height: 1.2;\n white-space: pre; font-weight: 400;\n text-decoration: none;\n }\n\n .badge-inline {\n display: inline-block; font-size: 0.5625rem; font-weight: 700;\n color: var(--text-muted); border: 1px solid var(--border-bright);\n border-radius: 0; padding: 0.1rem 0.4rem;\n margin-left: 0.5rem; vertical-align: middle;\n text-transform: uppercase; letter-spacing: 0.05em;\n }\n\n .divider {\n display: flex; align-items: center; gap: 0.75rem;\n color: var(--text-dim); font-size: 0.6875rem;\n margin: 1.5rem 0;\n text-transform: uppercase; letter-spacing: 0.1em;\n white-space: nowrap;\n }\n .divider::before, .divider::after {\n content: ''; flex: 1;\n height: 1px; background: var(--border-bright);\n }\n\n .credentials-box {\n background: var(--bg); border: 1px solid var(--accent-dim);\n padding: 1rem; margin-bottom: 1rem;\n font-size: 0.75rem; line-height: 1.8; word-break: break-all;\n }\n .credentials-box .label { color: var(--text-muted); }\n .credentials-box .value { color: var(--text); font-weight: 700; }\n\n .warning {\n background: rgba(184,122,0,0.06); border: 1px solid var(--amber);\n padding: 0.75rem; margin-bottom: 1rem;\n font-size: 0.7rem; color: var(--amber);\n }\n .warning::before { content: '[!!] '; font-weight: 700; }\n\n .error-message {\n color: var(--red); margin: 0 0 1rem 0; font-size: 0.75rem;\n padding: 0.5rem 0.75rem;\n border: 1px solid rgba(204,34,34,0.3);\n background: var(--bg);\n }\n .error-message::before { content: '[ERR] '; font-weight: 700; }\n\n .hint {\n font-size: 0.6875rem; color: var(--text-muted); line-height: 1.6;\n margin-top: 0.75rem;\n }\n .hint code {\n color: var(--text); background: var(--bg-raised);\n padding: 0.125rem 0.375rem; border: 1px solid var(--border);\n }\n\n .btn {\n display: block; width: 100%; text-align: center; text-decoration: none;\n }\n .btn-secondary {\n background: transparent; color: var(--text);\n border-color: var(--border-bright);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.05);\n }\n .btn-secondary:hover {\n border-color: var(--accent); color: var(--accent);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.1);\n }\n\n #create-result { display: none; }\n #create-result.show { display: block; }\n #create-btn.loading { opacity: 0.25; pointer-events: none; }\n\n /* ============ Landing page ============ */\n .landing-box { width: 100%; max-width: 580px; }\n .landing-box .ascii-logo { font-size: 0.75rem; margin-bottom: 1rem; }\n\n /* Landing page flows from top, not vertically centered like login */\n .login-container:has(.landing-box) {\n align-items: flex-start; padding-top: 4rem;\n }\n\n .landing-tagline {\n text-align: center; font-size: 0.8125rem;\n color: var(--text-muted); margin-bottom: 1.5rem;\n }\n\n .landing-links {\n display: flex; flex-wrap: wrap; justify-content: center;\n gap: 0.5rem; margin-bottom: 2rem;\n }\n .landing-link {\n font-size: 0.6875rem; color: var(--text);\n text-decoration: none; padding: 0.25rem 0.625rem;\n border: 1px solid var(--border-bright);\n transition: border-color 0.15s, background 0.15s;\n }\n .landing-link:hover {\n border-color: var(--accent); background: var(--bg-raised);\n }\n\n .landing-features {\n display: flex; flex-direction: column; gap: 0.75rem;\n margin-top: 1rem;\n }\n .landing-feature {\n display: flex; gap: 0.75rem; align-items: baseline;\n font-size: 0.75rem;\n }\n .landing-feature-label {\n font-weight: 700; color: var(--text);\n white-space: nowrap; min-width: 10rem;\n }\n .landing-feature-desc { color: var(--text-muted); }\n\n .landing-steps { display: flex; flex-direction: column; gap: 0.75rem; }\n .landing-step {\n display: flex; gap: 0.75rem; align-items: flex-start;\n font-size: 0.75rem; line-height: 1.6;\n }\n .landing-step-num {\n display: inline-flex; align-items: center; justify-content: center;\n min-width: 1.5rem; height: 1.5rem;\n border: 1px solid var(--border-bright);\n font-size: 0.6875rem; font-weight: 700; color: var(--text);\n flex-shrink: 0;\n }\n .landing-step-hint {\n display: block; font-size: 0.6875rem; color: var(--text-dim);\n margin-top: 0.125rem;\n }\n\n .landing-footer {\n text-align: center; padding: 2rem 0 0;\n font-size: 0.6875rem; color: var(--text-dim);\n }\n .landing-footer a {\n color: var(--text-muted); text-decoration: none;\n }\n .landing-footer a:hover { color: var(--text); }\n .landing-footer-sep { margin: 0 0.375rem; }\n\n /* ============ Scrollbar ============ */\n ::-webkit-scrollbar { width: 6px; height: 6px; }\n ::-webkit-scrollbar-track { background: var(--bg-raised); }\n ::-webkit-scrollbar-thumb { background: var(--border-bright); }\n ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }\n\n /* ============ Responsive (small screens) ============ */\n @media (max-width: 480px) {\n .ascii-logo { font-size: 0.4rem; }\n .landing-box .ascii-logo { font-size: 0.5rem; }\n .login-container { padding: 1rem; }\n .card-inner { padding: 1rem; }\n .landing-feature { flex-direction: column; gap: 0.125rem; }\n .landing-feature-label { min-width: auto; }\n }\n";
10
+ export declare const DASHBOARD_CSS = "\n /* ============ Reset ============ */\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n :root {\n /* ---- palette (black & white) ---- */\n --bg: #ffffff;\n --bg-card: #ffffff;\n --bg-raised: #eae8e4;\n --text: #1a1a1a;\n --text-muted: #6b6b6b;\n --text-dim: #a0a0a0;\n --accent: #1a1a1a;\n --accent-dim: #333333;\n --red: #cc2222;\n --amber: #b87a00;\n --green: #1a8a2a;\n --border: #ddd9d4;\n --border-bright: #c0bbb5;\n\n /* ---- type ---- */\n --font: 'JetBrains Mono', 'Courier New', monospace;\n\n /* ---- dot shadow (turbopuffer SVG pattern, black fill) ---- */\n --dot-shadow: url(\"data:image/svg+xml,%3Csvg width='7' height='13' viewBox='0 0 7 13' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5.58984 12.2344V10.7051H6.52734V12.2344H5.58984ZM1.86328 12.2344V10.7051H2.79492V12.2344H1.86328ZM3.72656 10.0957V8.56641H4.6582V10.0957H3.72656ZM0 10.0957V8.56641H0.925781V10.0957H0ZM5.58984 7.95117V6.42188H6.52734V7.95117H5.58984ZM1.86328 7.95117V6.42188H2.79492V7.95117H1.86328ZM3.72656 5.8125V4.2832H4.6582V5.8125H3.72656ZM0 5.8125V4.2832H0.925781V5.8125H0ZM5.58984 3.66797V2.13867H6.52734V3.66797H5.58984ZM1.86328 3.66797V2.13867H2.79492V3.66797H1.86328ZM3.72656 1.5293V0H4.6582V1.5293H3.72656ZM0 1.5293V0H0.925781V1.5293H0Z' fill='%231a1a1a'/%3E%3C/svg%3E\");\n }\n\n /* ============ Base ============ */\n html, body {\n height: 100%;\n font-family: var(--font);\n font-size: 16px;\n line-height: 1.6;\n background: var(--bg);\n color: var(--text);\n -webkit-font-smoothing: antialiased;\n }\n\n body { display: flex; flex-direction: column; }\n\n ::selection { background: var(--accent); color: #fff; }\n\n a { color: var(--accent); }\n a:hover { text-decoration: none; opacity: 0.65; }\n\n /* ============ Scanline overlay (subtle CRT feel) ============ */\n body::before {\n content: '';\n position: fixed;\n inset: 0;\n background: repeating-linear-gradient(\n 0deg,\n transparent,\n transparent 2px,\n rgba(0, 0, 0, 0.012) 2px,\n rgba(0, 0, 0, 0.012) 4px\n );\n pointer-events: none;\n z-index: 9999;\n }\n\n /* ============ Dot shadow utility ============ */\n .dot-shadow { position: relative; }\n .dot-shadow::after {\n content: '';\n position: absolute;\n top: 0.5rem; left: 0.5rem;\n right: -0.5rem; bottom: -0.5rem;\n background-image: var(--dot-shadow);\n background-repeat: repeat;\n z-index: -1;\n pointer-events: none;\n opacity: 0.6;\n }\n\n /* ============ Navigation ============ */\n .dashboard-nav {\n background: var(--bg);\n border-bottom: 1px solid var(--border);\n position: sticky; top: 0; z-index: 100;\n }\n\n .nav-container {\n max-width: 1200px;\n margin: 0 auto;\n padding: 0.75rem 1.5rem;\n display: flex;\n align-items: center;\n gap: 1.5rem;\n }\n\n .nav-logo {\n font-weight: 700;\n font-size: 0.875rem;\n color: var(--text);\n text-decoration: none;\n letter-spacing: 0.15em;\n text-transform: uppercase;\n }\n .nav-logo:hover { opacity: 1; }\n\n .nav-app-id {\n color: var(--text-muted);\n font-size: 0.75rem;\n margin-left: auto;\n }\n\n .nav-link {\n color: var(--text);\n text-decoration: none;\n font-size: 0.75rem;\n border: 1px solid var(--border-bright);\n padding: 0.25rem 0.75rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n transition: background 0.1s, color 0.1s;\n }\n .nav-link:hover {\n background: var(--accent);\n color: var(--bg);\n border-color: var(--accent);\n opacity: 1;\n }\n\n /* ============ Main content ============ */\n .dashboard-main {\n flex: 1;\n max-width: 1200px;\n width: 100%;\n margin: 0 auto;\n padding: 2rem 1.5rem;\n }\n\n /* ============ Card \u2014 primary container (Turbopuffer-style) ============ */\n .card {\n display: flex;\n flex-direction: column;\n margin-bottom: 1.5rem;\n }\n\n .card-header {\n margin-bottom: -1px; /* overlap the border */\n padding: 0;\n }\n\n .card-header h3 {\n position: relative;\n display: inline-flex;\n align-items: center;\n z-index: 10;\n top: 0.5rem;\n left: 0.5rem;\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n line-height: 1;\n color: var(--text);\n background: var(--bg);\n margin: 0;\n padding: 0 0.5rem;\n }\n\n .card-header h3 .card-title {\n }\n\n .card-header h3 .badge-inline {\n }\n\n .card-body {\n position: relative;\n border: 2px solid var(--border-bright);\n }\n\n .card-body::before {\n content: '';\n position: absolute;\n top: 0.5rem;\n left: 0.5rem;\n right: -0.5rem;\n bottom: -0.5rem;\n background-image: var(--dot-shadow);\n background-repeat: repeat;\n pointer-events: none;\n opacity: 0.6;\n }\n\n .card-inner {\n position: relative;\n z-index: 1;\n background: var(--bg-card);\n padding: 1.5rem;\n }\n\n /* Legacy fieldset support (deprecated \u2014 use .card instead) */\n fieldset {\n border: 2px solid var(--border-bright);\n border-radius: 0;\n padding: 1.5rem;\n margin-bottom: 1.5rem;\n background: var(--bg-card);\n position: relative;\n z-index: 0;\n }\n\n legend {\n padding: 0 0.5rem;\n font-size: 0.75rem;\n color: var(--text);\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n }\n\n /* ============ Dashboard grid ============ */\n .dashboard-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));\n gap: 1.5rem;\n margin-bottom: 1.5rem;\n }\n\n /* ============ Stat cards ============ */\n .stat-card {\n display: flex; flex-direction: column; gap: 0.25rem;\n padding: 1rem;\n border: 1px solid var(--border);\n background: var(--bg-card);\n }\n\n .stat-card .stat-value {\n font-size: 2rem; font-weight: 700; line-height: 1;\n color: var(--text);\n font-variant-numeric: tabular-nums;\n }\n\n .stat-card .stat-label {\n font-size: 0.6875rem;\n color: var(--text-muted);\n text-transform: uppercase;\n letter-spacing: 0.1em;\n }\n\n .stat-card .stat-change { font-size: 0.75rem; font-weight: 500; }\n .stat-card .stat-change.positive { color: var(--green); }\n .stat-card .stat-change.negative { color: var(--red); }\n\n /* ============ Bar chart ============ */\n .bar-chart { width: 100%; }\n\n .bar-chart .bar-item { margin-bottom: 0.75rem; }\n\n .bar-chart .bar-label {\n display: flex; justify-content: space-between; align-items: center;\n margin-bottom: 0.125rem; font-size: 0.75rem;\n }\n .bar-chart .bar-name { color: var(--text); }\n .bar-chart .bar-value {\n color: var(--text-muted);\n font-weight: 700; font-variant-numeric: tabular-nums;\n }\n\n .bar-chart .bar {\n height: 18px;\n background: var(--accent);\n border-radius: 0;\n transition: width 0.3s ease;\n position: relative; overflow: hidden;\n opacity: 0.8;\n }\n .bar-chart .bar:hover { opacity: 1; }\n\n .bar-chart .bar-fill { height: 100%; background: var(--accent); }\n\n /* ============ Form controls ============ */\n input, select, textarea, button { font-family: var(--font); font-size: 0.875rem; }\n\n input[type=\"text\"],\n input[type=\"email\"],\n input[type=\"password\"],\n input[type=\"number\"],\n select,\n textarea {\n width: 100%;\n padding: 0.625rem 0.75rem;\n background: var(--bg);\n border: 1px solid var(--border-bright);\n border-radius: 0;\n color: var(--text);\n }\n\n input:focus, select:focus, textarea:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: 0 0 0 1px var(--accent);\n }\n\n input::placeholder, textarea::placeholder { color: var(--text-dim); }\n\n label {\n display: block;\n margin-bottom: 0.375rem;\n font-size: 0.6875rem;\n color: var(--text-muted);\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n }\n\n .form-group { margin-bottom: 1.25rem; }\n\n /* ============ Buttons ============ */\n button, .button {\n display: inline-block;\n padding: 0.625rem 1.25rem;\n background: var(--accent);\n color: #fff;\n border: 1px solid var(--accent);\n border-radius: 0;\n font-weight: 700;\n cursor: pointer;\n text-decoration: none;\n text-align: center;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n font-size: 0.75rem;\n box-shadow:\n inset 1px 1px 0 rgba(255,255,255,0.15),\n inset -1px -1px 0 rgba(0,0,0,0.15),\n 2px 2px 0 rgba(0,0,0,0.1);\n transition: box-shadow 0.1s, transform 0.1s;\n }\n button:hover, .button:hover {\n box-shadow:\n inset 1px 1px 0 rgba(255,255,255,0.1),\n inset -1px -1px 0 rgba(0,0,0,0.15),\n 3px 3px 0 rgba(0,0,0,0.12);\n opacity: 1;\n }\n button:active, .button:active {\n transform: translate(1px, 1px);\n box-shadow: inset 1px 1px 3px rgba(0,0,0,0.25);\n }\n button:disabled, .button:disabled { opacity: 0.25; cursor: not-allowed; }\n\n button.secondary, .button.secondary {\n background: transparent;\n color: var(--text);\n border-color: var(--border-bright);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.05);\n }\n button.secondary:hover, .button.secondary:hover {\n border-color: var(--accent);\n color: var(--accent);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.1);\n }\n\n button.danger, .button.danger {\n background: var(--red);\n border-color: var(--red);\n color: #fff;\n box-shadow: 2px 2px 0 rgba(204,34,34,0.15);\n }\n button.danger:hover, .button.danger:hover {\n background: transparent;\n color: var(--red);\n }\n\n /* ============ Tables ============ */\n table { width: 100%; border-collapse: collapse; }\n\n thead { border-bottom: 1px solid var(--border-bright); }\n th {\n padding: 0.5rem 0.75rem; text-align: left;\n font-size: 0.6875rem; color: var(--text);\n font-weight: 700; text-transform: uppercase;\n letter-spacing: 0.1em;\n background: var(--bg-raised);\n }\n\n td {\n padding: 0.5rem 0.75rem;\n border-bottom: 1px solid var(--border);\n font-size: 0.75rem;\n font-variant-numeric: tabular-nums;\n }\n\n tbody tr:hover { background: var(--bg-raised); }\n\n /* ============ Code ============ */\n code, pre {\n font-family: var(--font);\n background: var(--bg-raised);\n padding: 0.125rem 0.375rem;\n border-radius: 0;\n font-size: 0.75rem;\n border: 1px solid var(--border);\n color: var(--text);\n }\n pre { padding: 1rem; overflow-x: auto; border: 1px solid var(--border-bright); }\n pre code { background: none; padding: 0; border: none; }\n\n /* ============ Login layout ============ */\n .login-container {\n min-height: 100vh;\n display: flex; align-items: flex-start; justify-content: center;\n padding: 4rem 2rem 2rem;\n background: var(--bg);\n }\n .login-box { width: 100%; max-width: 420px; }\n\n .login-header { text-align: center; margin-bottom: 2rem; }\n .login-header h1 {\n font-size: 1.5rem; font-weight: 700; color: var(--text);\n letter-spacing: 0.15em; text-transform: uppercase;\n margin-bottom: 0.25rem;\n }\n .login-header p { color: var(--text-muted); font-size: 0.75rem; }\n\n /* ============ htmx loading ============ */\n .htmx-indicator { opacity: 0; transition: opacity 0.15s; }\n .htmx-request .htmx-indicator { opacity: 1; }\n .htmx-request.htmx-swapping { opacity: 0.3; pointer-events: none; }\n\n /* ============ Skeleton \u2014 blinking cursor ============ */\n .skeleton {\n background: var(--bg-raised);\n border: 1px solid var(--border);\n position: relative;\n overflow: hidden;\n }\n .skeleton::after {\n content: '';\n position: absolute; left: 0; top: 0;\n width: 2px; height: 100%;\n background: var(--text);\n animation: cursor-blink 0.8s step-end infinite;\n }\n @keyframes cursor-blink {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0; }\n }\n\n .skeleton-text { height: 1rem; margin-bottom: 0.5rem; }\n .skeleton-heading { height: 1.5rem; width: 60%; margin-bottom: 1rem; }\n\n /* ============ Utilities ============ */\n .text-center { text-align: center; }\n .text-right { text-align: right; }\n .text-muted { color: var(--text-muted); }\n .text-dim { color: var(--text-dim); }\n .text-success { color: var(--green); }\n .text-danger { color: var(--red); }\n .text-warning { color: var(--amber); }\n\n .mb-0 { margin-bottom: 0; }\n .mb-1 { margin-bottom: 0.5rem; }\n .mb-2 { margin-bottom: 1rem; }\n .mb-3 { margin-bottom: 1.5rem; }\n .mb-4 { margin-bottom: 2rem; }\n .mt-0 { margin-top: 0; }\n .mt-1 { margin-top: 0.5rem; }\n .mt-2 { margin-top: 1rem; }\n .mt-3 { margin-top: 1.5rem; }\n .mt-4 { margin-top: 2rem; }\n\n /* ============ Period selector ============ */\n .period-selector button {\n font-size: 0.625rem;\n padding: 0.2rem 0.5rem;\n }\n\n /* ============ Responsive ============ */\n @media (max-width: 768px) {\n html, body { font-size: 14px; }\n .dashboard-main { padding: 1rem; }\n .nav-container { padding: 0.5rem 1rem; }\n .dashboard-grid { grid-template-columns: 1fr; gap: 1rem; }\n .card-inner { padding: 1rem; }\n .card { margin-bottom: 1rem; }\n fieldset { padding: 1rem; margin-bottom: 1rem; }\n .stat-card .stat-value { font-size: 1.75rem; }\n table { font-size: 0.625rem; }\n th, td { padding: 0.375rem 0.5rem; }\n }\n\n @media (max-width: 480px) {\n .nav-container { flex-wrap: wrap; }\n .nav-app-id { margin-left: 0; width: 100%; order: 3; }\n }\n\n /* ============ Alerts ============ */\n .alert {\n padding: 0.75rem 1rem;\n border-radius: 0;\n margin-bottom: 1.5rem;\n border: 1px solid var(--border-bright);\n font-size: 0.75rem;\n background: var(--bg-card);\n }\n .alert::before { font-weight: 700; margin-right: 0.5rem; }\n\n .alert-info { border-color: var(--border-bright); color: var(--text); }\n .alert-info::before { content: '>'; color: var(--text); }\n\n .alert-success { border-color: var(--green); color: var(--green); }\n .alert-success::before { content: '[ok]'; }\n\n .alert-warning { border-color: var(--amber); color: var(--amber); }\n .alert-warning::before { content: '[!!]'; }\n\n .alert-danger { border-color: var(--red); color: var(--red); }\n .alert-danger::before { content: '[ERR]'; }\n\n /* ============ Badges ============ */\n .badge {\n display: inline-block;\n padding: 0.125rem 0.375rem;\n font-size: 0.625rem; font-weight: 700;\n border-radius: 0;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n border: 1px solid;\n }\n .badge-success { color: var(--green); border-color: var(--green); background: transparent; }\n .badge-danger { color: var(--red); border-color: var(--red); background: transparent; }\n .badge-warning { color: var(--amber); border-color: var(--amber); background: transparent; }\n .badge-info { color: #fff; border-color: var(--accent); background: var(--accent); }\n\n /* ============ Sample data indicator ============ */\n .sample-banner {\n background: #fffbe6;\n border: 1px solid var(--amber);\n color: var(--amber);\n padding: 0.5rem 0.75rem;\n font-size: 0.6875rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin-bottom: 1rem;\n }\n .sample-banner::before { content: '[demo]'; margin-right: 0.5rem; }\n .sample-tag {\n color: var(--amber);\n font-size: 0.625rem;\n font-weight: 700;\n letter-spacing: 0.05em;\n border: 1px solid var(--amber);\n padding: 0.0625rem 0.3rem;\n margin-left: 0.375rem;\n vertical-align: middle;\n background: #fffbe6;\n }\n\n /* ============ Empty state ============ */\n .empty-state { text-align: center; padding: 3rem 1rem; color: var(--text-muted); }\n .empty-state-icon { font-size: 1.5rem; margin-bottom: 0.75rem; color: var(--text); font-weight: 700; }\n .empty-state-text { font-size: 0.8125rem; margin-bottom: 0.25rem; }\n .empty-state-subtext { font-size: 0.6875rem; color: var(--text-dim); }\n\n /* ============ Auth pages ============ */\n .ascii-logo {\n display: block; text-align: center; margin-bottom: 2rem;\n color: var(--text); font-size: 0.55rem; line-height: 1.2;\n white-space: pre; font-weight: 400;\n text-decoration: none;\n }\n\n .badge-inline {\n display: inline-block; font-size: 0.5625rem; font-weight: 700;\n color: var(--text-muted); border: 1px solid var(--border-bright);\n border-radius: 0; padding: 0.1rem 0.4rem;\n margin-left: 0.5rem; vertical-align: middle;\n text-transform: uppercase; letter-spacing: 0.05em;\n }\n\n .divider {\n display: flex; align-items: center; gap: 0.75rem;\n color: var(--text-dim); font-size: 0.6875rem;\n margin: 1.5rem 0;\n text-transform: uppercase; letter-spacing: 0.1em;\n white-space: nowrap;\n }\n .divider::before, .divider::after {\n content: ''; flex: 1;\n height: 1px; background: var(--border-bright);\n }\n\n .credentials-box {\n background: var(--bg); border: 1px solid var(--accent-dim);\n padding: 1rem; margin-bottom: 1rem;\n font-size: 0.75rem; line-height: 1.8; word-break: break-all;\n }\n .credentials-box .label { color: var(--text-muted); }\n .credentials-box .value { color: var(--text); font-weight: 700; }\n\n .warning {\n background: rgba(184,122,0,0.06); border: 1px solid var(--amber);\n padding: 0.75rem; margin-bottom: 1rem;\n font-size: 0.7rem; color: var(--amber);\n }\n .warning::before { content: '[!!] '; font-weight: 700; }\n\n .error-message {\n color: var(--red); margin: 0 0 1rem 0; font-size: 0.75rem;\n padding: 0.5rem 0.75rem;\n border: 1px solid rgba(204,34,34,0.3);\n background: var(--bg);\n }\n .error-message::before { content: '[ERR] '; font-weight: 700; }\n\n .hint {\n font-size: 0.6875rem; color: var(--text-muted); line-height: 1.6;\n margin-top: 0.75rem;\n }\n .hint code {\n color: var(--text); background: var(--bg-raised);\n padding: 0.125rem 0.375rem; border: 1px solid var(--border);\n }\n\n .btn {\n display: block; width: 100%; text-align: center; text-decoration: none;\n }\n .btn-secondary {\n background: transparent; color: var(--text);\n border-color: var(--border-bright);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.05);\n }\n .btn-secondary:hover {\n border-color: var(--accent); color: var(--accent);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.1);\n }\n\n #create-result { display: none; }\n #create-result.show { display: block; }\n #create-btn.loading { opacity: 0.25; pointer-events: none; }\n\n /* ============ Landing page ============ */\n .landing-box { width: 100%; max-width: 580px; }\n .landing-box .ascii-logo { font-size: 0.75rem; margin-bottom: 1rem; }\n\n /* Landing page flows from top, not vertically centered like login */\n .login-container:has(.landing-box) {\n align-items: flex-start; padding-top: 4rem;\n }\n\n .landing-tagline {\n text-align: center; font-size: 0.8125rem;\n color: var(--text-muted); margin-bottom: 1.5rem;\n }\n\n .landing-links {\n display: flex; flex-wrap: wrap; justify-content: center;\n gap: 0.5rem; margin-bottom: 2rem;\n }\n .landing-link {\n font-size: 0.6875rem; color: var(--text);\n text-decoration: none; padding: 0.25rem 0.625rem;\n border: 1px solid var(--border-bright);\n transition: border-color 0.15s, background 0.15s;\n }\n .landing-link:hover {\n border-color: var(--accent); background: var(--bg-raised);\n }\n\n .landing-features {\n display: flex; flex-direction: column; gap: 0.75rem;\n margin-top: 1rem;\n }\n .landing-feature {\n display: flex; gap: 0.75rem; align-items: baseline;\n font-size: 0.75rem;\n }\n .landing-feature-label {\n font-weight: 700; color: var(--text);\n white-space: nowrap; min-width: 10rem;\n }\n .landing-feature-desc { color: var(--text-muted); }\n\n .landing-steps { display: flex; flex-direction: column; gap: 0.75rem; }\n .landing-step {\n display: flex; gap: 0.75rem; align-items: flex-start;\n font-size: 0.75rem; line-height: 1.6;\n }\n .landing-step-num {\n display: inline-flex; align-items: center; justify-content: center;\n min-width: 1.5rem; height: 1.5rem;\n border: 1px solid var(--border-bright);\n font-size: 0.6875rem; font-weight: 700; color: var(--text);\n flex-shrink: 0;\n }\n .landing-step-hint {\n display: block; font-size: 0.6875rem; color: var(--text-dim);\n margin-top: 0.125rem;\n }\n\n .landing-footer {\n text-align: center; padding: 2rem 0 3rem;\n font-size: 0.6875rem; color: var(--text-dim);\n }\n .landing-footer a {\n color: var(--text-muted); text-decoration: none;\n }\n .landing-footer a:hover { color: var(--text); }\n .landing-footer-sep { margin: 0 0.375rem; }\n\n /* ============ Scrollbar ============ */\n ::-webkit-scrollbar { width: 6px; height: 6px; }\n ::-webkit-scrollbar-track { background: var(--bg-raised); }\n ::-webkit-scrollbar-thumb { background: var(--border-bright); }\n ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }\n\n /* ============ Responsive (small screens) ============ */\n @media (max-width: 480px) {\n .ascii-logo { font-size: 0.4rem; }\n .landing-box .ascii-logo { font-size: 0.5rem; }\n .login-container { padding: 1rem; }\n .card-inner { padding: 1rem; }\n .landing-feature { flex-direction: column; gap: 0.125rem; }\n .landing-feature-label { min-width: auto; }\n }\n";
11
11
  //# sourceMappingURL=styles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/dashboard/styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,aAAa,yvqBAitBzB,CAAC"}
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/dashboard/styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,aAAa,0wqBAitBzB,CAAC"}
@@ -412,8 +412,8 @@ export const DASHBOARD_CSS = `
412
412
  /* ============ Login layout ============ */
413
413
  .login-container {
414
414
  min-height: 100vh;
415
- display: flex; align-items: center; justify-content: center;
416
- padding: 2rem;
415
+ display: flex; align-items: flex-start; justify-content: center;
416
+ padding: 4rem 2rem 2rem;
417
417
  background: var(--bg);
418
418
  }
419
419
  .login-box { width: 100%; max-width: 420px; }
@@ -704,7 +704,7 @@ export const DASHBOARD_CSS = `
704
704
  }
705
705
 
706
706
  .landing-footer {
707
- text-align: center; padding: 2rem 0 0;
707
+ text-align: center; padding: 2rem 0 3rem;
708
708
  font-size: 0.6875rem; color: var(--text-dim);
709
709
  }
710
710
  .landing-footer a {
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"}