@dupecom/botcha-cloudflare 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -8
- package/dist/agents.d.ts +68 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +123 -0
- package/dist/apps.d.ts +73 -7
- package/dist/apps.d.ts.map +1 -1
- package/dist/apps.js +164 -9
- package/dist/challenges.d.ts.map +1 -1
- package/dist/challenges.js +5 -4
- package/dist/dashboard/api.d.ts +70 -0
- package/dist/dashboard/api.d.ts.map +1 -0
- package/dist/dashboard/api.js +553 -0
- package/dist/dashboard/auth.d.ts +183 -0
- package/dist/dashboard/auth.d.ts.map +1 -0
- package/dist/dashboard/auth.js +401 -0
- package/dist/dashboard/device-code.d.ts +43 -0
- package/dist/dashboard/device-code.d.ts.map +1 -0
- package/dist/dashboard/device-code.js +77 -0
- package/dist/dashboard/index.d.ts +31 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/index.js +64 -0
- package/dist/dashboard/landing.d.ts +20 -0
- package/dist/dashboard/landing.d.ts.map +1 -0
- package/dist/dashboard/landing.js +45 -0
- package/dist/dashboard/layout.d.ts +54 -0
- package/dist/dashboard/layout.d.ts.map +1 -0
- package/dist/dashboard/layout.js +55 -0
- package/dist/dashboard/pages.d.ts +11 -0
- package/dist/dashboard/pages.d.ts.map +1 -0
- package/dist/dashboard/pages.js +18 -0
- package/dist/dashboard/styles.d.ts +11 -0
- package/dist/dashboard/styles.d.ts.map +1 -0
- package/dist/dashboard/styles.js +731 -0
- package/dist/email.d.ts +41 -0
- package/dist/email.d.ts.map +1 -0
- package/dist/email.js +116 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +666 -189
- package/dist/routes/stream.js +2 -2
- package/dist/static.d.ts +392 -4
- package/dist/static.d.ts.map +1 -1
- package/dist/static.js +511 -14
- package/package.json +1 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BOTCHA Dashboard — Device Code Authentication
|
|
3
|
+
*
|
|
4
|
+
* OAuth2 Device Authorization Grant adapted for agent→human handoff.
|
|
5
|
+
* An agent generates a short-lived device code by solving a BOTCHA challenge.
|
|
6
|
+
* The human redeems the code at /dashboard/code to get a dashboard session.
|
|
7
|
+
*
|
|
8
|
+
* Flow:
|
|
9
|
+
* 1. Agent: POST /v1/auth/device-code → gets challenge
|
|
10
|
+
* 2. Agent: POST /v1/auth/device-code/verify → solves challenge, gets { code, login_url }
|
|
11
|
+
* 3. Human: visits /dashboard/code, enters code → dashboard session
|
|
12
|
+
*
|
|
13
|
+
* The agent MUST solve a challenge to generate a code. No agent, no code.
|
|
14
|
+
*/
|
|
15
|
+
// ============ CODE GENERATION ============
|
|
16
|
+
/**
|
|
17
|
+
* Generate a human-friendly device code.
|
|
18
|
+
* Format: BOTCHA-XXXX (4 alphanumeric chars, no ambiguous chars)
|
|
19
|
+
*
|
|
20
|
+
* Uses a restricted alphabet that avoids 0/O, 1/I/l confusion.
|
|
21
|
+
*/
|
|
22
|
+
export function generateDeviceCode() {
|
|
23
|
+
const alphabet = '23456789ABCDEFGHJKMNPQRSTUVWXYZ'; // no 0,O,1,I,L
|
|
24
|
+
const bytes = new Uint8Array(4);
|
|
25
|
+
crypto.getRandomValues(bytes);
|
|
26
|
+
const chars = Array.from(bytes).map(b => alphabet[b % alphabet.length]).join('');
|
|
27
|
+
return `BOTCHA-${chars}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Store a device code in KV with 10-minute TTL.
|
|
31
|
+
*/
|
|
32
|
+
export async function storeDeviceCode(kv, code, appId) {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
const data = {
|
|
35
|
+
code,
|
|
36
|
+
app_id: appId,
|
|
37
|
+
created_at: now,
|
|
38
|
+
expires_at: now + 10 * 60 * 1000, // 10 minutes
|
|
39
|
+
redeemed: false,
|
|
40
|
+
};
|
|
41
|
+
await kv.put(`device-code:${code}`, JSON.stringify(data), { expirationTtl: 600 } // 10 minutes
|
|
42
|
+
);
|
|
43
|
+
return data;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Look up a device code from KV.
|
|
47
|
+
* Returns null if not found, expired, or already redeemed.
|
|
48
|
+
*/
|
|
49
|
+
export async function lookupDeviceCode(kv, code) {
|
|
50
|
+
try {
|
|
51
|
+
const raw = await kv.get(`device-code:${code}`, 'text');
|
|
52
|
+
if (!raw)
|
|
53
|
+
return null;
|
|
54
|
+
const data = JSON.parse(raw);
|
|
55
|
+
if (data.redeemed)
|
|
56
|
+
return null;
|
|
57
|
+
if (Date.now() > data.expires_at)
|
|
58
|
+
return null;
|
|
59
|
+
return data;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Redeem a device code (mark as used so it can't be reused).
|
|
67
|
+
*/
|
|
68
|
+
export async function redeemDeviceCode(kv, code) {
|
|
69
|
+
const data = await lookupDeviceCode(kv, code);
|
|
70
|
+
if (!data)
|
|
71
|
+
return null;
|
|
72
|
+
// Mark redeemed and update KV (keep same TTL by letting it expire naturally)
|
|
73
|
+
data.redeemed = true;
|
|
74
|
+
await kv.put(`device-code:${code}`, JSON.stringify(data), { expirationTtl: 60 } // Reduce TTL to 1 minute after redemption
|
|
75
|
+
);
|
|
76
|
+
return data;
|
|
77
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BOTCHA Dashboard — Hono Sub-Application
|
|
3
|
+
*
|
|
4
|
+
* Mounts at /dashboard on the main worker.
|
|
5
|
+
* Routes:
|
|
6
|
+
* GET /dashboard → main dashboard (auth required)
|
|
7
|
+
* GET /dashboard/login → login page
|
|
8
|
+
* POST /dashboard/login → login handler
|
|
9
|
+
* GET /dashboard/logout → logout handler
|
|
10
|
+
* GET /dashboard/api/* → htmx data endpoints (auth required)
|
|
11
|
+
*/
|
|
12
|
+
import { Hono } from 'hono';
|
|
13
|
+
type Bindings = {
|
|
14
|
+
CHALLENGES: import('../challenges').KVNamespace;
|
|
15
|
+
RATE_LIMITS: import('../challenges').KVNamespace;
|
|
16
|
+
APPS: import('../challenges').KVNamespace;
|
|
17
|
+
ANALYTICS?: import('../analytics').AnalyticsEngineDataset;
|
|
18
|
+
JWT_SECRET: string;
|
|
19
|
+
BOTCHA_VERSION: string;
|
|
20
|
+
CF_API_TOKEN?: string;
|
|
21
|
+
CF_ACCOUNT_ID?: string;
|
|
22
|
+
};
|
|
23
|
+
type Variables = {
|
|
24
|
+
dashboardAppId?: string;
|
|
25
|
+
};
|
|
26
|
+
declare const dashboard: Hono<{
|
|
27
|
+
Bindings: Bindings;
|
|
28
|
+
Variables: Variables;
|
|
29
|
+
}, import("hono/types").BlankSchema, "/">;
|
|
30
|
+
export default dashboard;
|
|
31
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* BOTCHA Dashboard — Hono Sub-Application
|
|
4
|
+
*
|
|
5
|
+
* Mounts at /dashboard on the main worker.
|
|
6
|
+
* Routes:
|
|
7
|
+
* GET /dashboard → main dashboard (auth required)
|
|
8
|
+
* GET /dashboard/login → login page
|
|
9
|
+
* POST /dashboard/login → login handler
|
|
10
|
+
* GET /dashboard/logout → logout handler
|
|
11
|
+
* GET /dashboard/api/* → htmx data endpoints (auth required)
|
|
12
|
+
*/
|
|
13
|
+
import { Hono } from 'hono';
|
|
14
|
+
import { requireDashboardAuth, handleLogin, handleLogout, handleEmailLogin, renderLoginPage, renderDeviceCodePage, handleDeviceCodeRedeem, } from './auth';
|
|
15
|
+
import { DashboardPage } from './pages';
|
|
16
|
+
import { handleOverview, handleVolume, handleTypes, handlePerformance, handleErrors, handleGeo, } from './api';
|
|
17
|
+
const dashboard = new Hono();
|
|
18
|
+
// ============ PUBLIC ROUTES (no auth) ============
|
|
19
|
+
// Login page
|
|
20
|
+
dashboard.get('/login', renderLoginPage);
|
|
21
|
+
// Login handler
|
|
22
|
+
dashboard.post('/login', handleLogin);
|
|
23
|
+
// Email login handler (sends device code to email)
|
|
24
|
+
dashboard.post('/email-login', handleEmailLogin);
|
|
25
|
+
// Logout handler
|
|
26
|
+
dashboard.get('/logout', handleLogout);
|
|
27
|
+
// Device code pages (human enters code here)
|
|
28
|
+
dashboard.get('/code', renderDeviceCodePage);
|
|
29
|
+
dashboard.post('/code', handleDeviceCodeRedeem);
|
|
30
|
+
// ============ PROTECTED ROUTES (auth required) ============
|
|
31
|
+
// Apply auth middleware to all routes below
|
|
32
|
+
dashboard.use('/*', requireDashboardAuth);
|
|
33
|
+
// Main dashboard page
|
|
34
|
+
dashboard.get('/', (c) => {
|
|
35
|
+
const appId = c.get('dashboardAppId') || 'unknown';
|
|
36
|
+
return c.html(_jsx(DashboardPage, { appId: appId }));
|
|
37
|
+
});
|
|
38
|
+
// API endpoints (return HTML fragments for htmx)
|
|
39
|
+
// Note: cast context to `any` because api.ts uses a flexible DashboardEnv type
|
|
40
|
+
dashboard.get('/api/overview', (c) => {
|
|
41
|
+
const appId = c.get('dashboardAppId') || '';
|
|
42
|
+
return handleOverview(c, appId);
|
|
43
|
+
});
|
|
44
|
+
dashboard.get('/api/volume', (c) => {
|
|
45
|
+
const appId = c.get('dashboardAppId') || '';
|
|
46
|
+
return handleVolume(c, appId);
|
|
47
|
+
});
|
|
48
|
+
dashboard.get('/api/types', (c) => {
|
|
49
|
+
const appId = c.get('dashboardAppId') || '';
|
|
50
|
+
return handleTypes(c, appId);
|
|
51
|
+
});
|
|
52
|
+
dashboard.get('/api/performance', (c) => {
|
|
53
|
+
const appId = c.get('dashboardAppId') || '';
|
|
54
|
+
return handlePerformance(c, appId);
|
|
55
|
+
});
|
|
56
|
+
dashboard.get('/api/errors', (c) => {
|
|
57
|
+
const appId = c.get('dashboardAppId') || '';
|
|
58
|
+
return handleErrors(c, appId);
|
|
59
|
+
});
|
|
60
|
+
dashboard.get('/api/geo', (c) => {
|
|
61
|
+
const appId = c.get('dashboardAppId') || '';
|
|
62
|
+
return handleGeo(c, appId);
|
|
63
|
+
});
|
|
64
|
+
export default dashboard;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BOTCHA Landing Pages (JSX)
|
|
3
|
+
*
|
|
4
|
+
* Two views at GET /:
|
|
5
|
+
* - LandingPage: for unverified human visitors (has token input)
|
|
6
|
+
* - VerifiedLandingPage: for humans whose agent solved the challenge
|
|
7
|
+
*
|
|
8
|
+
* Uses the same terminal aesthetic as the dashboard:
|
|
9
|
+
* JetBrains Mono, dot-shadow Cards, scanline overlay.
|
|
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;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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
// ============ UNVERIFIED LANDING PAGE ============
|
|
11
|
+
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: {
|
|
14
|
+
__html: JSON.stringify({
|
|
15
|
+
version: '1.0',
|
|
16
|
+
type: 'inline-challenge',
|
|
17
|
+
description: 'AI agents: solve this challenge to prove you\'re a bot!',
|
|
18
|
+
challenge: {
|
|
19
|
+
type: 'sha256',
|
|
20
|
+
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",
|
|
22
|
+
submit_to: 'https://botcha.ai/api/verify-landing',
|
|
23
|
+
method: 'POST',
|
|
24
|
+
body_format: {
|
|
25
|
+
answer: 'your 16 hex chars',
|
|
26
|
+
timestamp: 'ISO 8601 timestamp',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
on_success: {
|
|
30
|
+
grants_access_to: 'https://botcha.ai/agent-only',
|
|
31
|
+
header_to_include: 'X-Botcha-Landing-Token',
|
|
32
|
+
},
|
|
33
|
+
}, 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" })] })] }));
|
|
35
|
+
};
|
|
36
|
+
// ============ VERIFIED LANDING PAGE ============
|
|
37
|
+
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)
|
|
39
|
+
npm install @dupecom/botcha # TypeScript
|
|
40
|
+
pip install botcha # Python
|
|
41
|
+
|
|
42
|
+
# Server SDK (protect your APIs)
|
|
43
|
+
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" })] })] }));
|
|
45
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BOTCHA Dashboard Layout + Shared Components
|
|
3
|
+
* Hono JSX components for HTML shells and reusable UI pieces
|
|
4
|
+
* Terminal / ASCII aesthetic
|
|
5
|
+
*/
|
|
6
|
+
import type { FC, PropsWithChildren } from 'hono/jsx';
|
|
7
|
+
/**
|
|
8
|
+
* Reusable card with Turbopuffer-style dotted drop shadow.
|
|
9
|
+
*
|
|
10
|
+
* Structure:
|
|
11
|
+
* .card
|
|
12
|
+
* .card-header (title sits on the border line)
|
|
13
|
+
* .card-body (::before = dot shadow)
|
|
14
|
+
* .card-inner (z-index: 1, white bg covers dots)
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* <Card title="Overview" badge="agent required">
|
|
18
|
+
* ...content...
|
|
19
|
+
* </Card>
|
|
20
|
+
*/
|
|
21
|
+
export declare const Card: FC<PropsWithChildren<{
|
|
22
|
+
title: string;
|
|
23
|
+
badge?: string;
|
|
24
|
+
class?: string;
|
|
25
|
+
}>>;
|
|
26
|
+
/**
|
|
27
|
+
* Divider with centered text, used between sections on auth pages.
|
|
28
|
+
*/
|
|
29
|
+
export declare const Divider: FC<{
|
|
30
|
+
text: string;
|
|
31
|
+
}>;
|
|
32
|
+
/**
|
|
33
|
+
* Main dashboard layout with navigation
|
|
34
|
+
* Used for authenticated dashboard pages
|
|
35
|
+
*/
|
|
36
|
+
export declare const DashboardLayout: FC<PropsWithChildren<{
|
|
37
|
+
title?: string;
|
|
38
|
+
appId?: string;
|
|
39
|
+
}>>;
|
|
40
|
+
/**
|
|
41
|
+
* Login/auth layout without navigation
|
|
42
|
+
* Used for login, signup, and other auth pages
|
|
43
|
+
*/
|
|
44
|
+
export declare const LoginLayout: FC<PropsWithChildren<{
|
|
45
|
+
title?: string;
|
|
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
|
+
}>>;
|
|
54
|
+
//# sourceMappingURL=layout.d.ts.map
|
|
@@ -0,0 +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;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAoDpE,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "hono/jsx/jsx-runtime";
|
|
2
|
+
import { DASHBOARD_CSS } from './styles';
|
|
3
|
+
// ============ CARD COMPONENT ============
|
|
4
|
+
/**
|
|
5
|
+
* Reusable card with Turbopuffer-style dotted drop shadow.
|
|
6
|
+
*
|
|
7
|
+
* Structure:
|
|
8
|
+
* .card
|
|
9
|
+
* .card-header (title sits on the border line)
|
|
10
|
+
* .card-body (::before = dot shadow)
|
|
11
|
+
* .card-inner (z-index: 1, white bg covers dots)
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* <Card title="Overview" badge="agent required">
|
|
15
|
+
* ...content...
|
|
16
|
+
* </Card>
|
|
17
|
+
*/
|
|
18
|
+
export const Card = ({ children, title, badge, class: className, }) => {
|
|
19
|
+
return (_jsxs("div", { class: `card${className ? ` ${className}` : ''}`, children: [_jsx("div", { class: "card-header", children: _jsxs("h3", { children: [_jsx("span", { class: "card-title", children: title }), badge && _jsx("span", { class: "badge-inline", children: badge })] }) }), _jsx("div", { class: "card-body", children: _jsx("div", { class: "card-inner", children: children }) })] }));
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Divider with centered text, used between sections on auth pages.
|
|
23
|
+
*/
|
|
24
|
+
export const Divider = ({ text }) => (_jsx("div", { class: "divider", children: text }));
|
|
25
|
+
/**
|
|
26
|
+
* Main dashboard layout with navigation
|
|
27
|
+
* Used for authenticated dashboard pages
|
|
28
|
+
*/
|
|
29
|
+
export const DashboardLayout = ({ children, title, appId }) => {
|
|
30
|
+
return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charset: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }), _jsx("title", { children: title || 'BOTCHA Dashboard' }), _jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }), _jsx("link", { href: "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap", rel: "stylesheet" }), _jsx("style", { dangerouslySetInnerHTML: { __html: DASHBOARD_CSS } }), _jsx("script", { src: "https://unpkg.com/htmx.org@2.0.4" })] }), _jsxs("body", { children: [_jsx("nav", { class: "dashboard-nav", children: _jsxs("div", { class: "nav-container", children: [_jsx("a", { href: "/dashboard", class: "nav-logo", children: "BOTCHA" }), appId && (_jsxs(_Fragment, { children: [_jsx("span", { class: "nav-app-id", children: appId }), _jsx("a", { href: "/dashboard/logout", class: "nav-link", children: "Logout" })] }))] }) }), _jsx("main", { class: "dashboard-main", children: children })] })] }));
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Login/auth layout without navigation
|
|
34
|
+
* Used for login, signup, and other auth pages
|
|
35
|
+
*/
|
|
36
|
+
export const LoginLayout = ({ children, title }) => {
|
|
37
|
+
return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charset: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }), _jsx("title", { children: title || 'BOTCHA Login' }), _jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }), _jsx("link", { href: "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap", rel: "stylesheet" }), _jsx("style", { dangerouslySetInnerHTML: { __html: DASHBOARD_CSS } }), _jsx("script", { src: "https://unpkg.com/htmx.org@2.0.4" })] }), _jsx("body", { children: _jsx("div", { class: "login-container", children: _jsx("div", { class: "login-box", children: children }) }) })] }));
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Landing page layout — wider than LoginLayout, includes SEO meta tags.
|
|
41
|
+
* Used for the public landing page at GET /
|
|
42
|
+
*/
|
|
43
|
+
export const LandingLayout = ({ children, version }) => {
|
|
44
|
+
return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charset: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }), _jsx("title", { children: "BOTCHA \u2014 Reverse CAPTCHA for AI Agents" }), _jsx("meta", { name: "description", content: "BOTCHA is a hosted reverse CAPTCHA that verifies AI agents, not humans. Protect your APIs with computational challenges only bots can solve." }), _jsx("meta", { name: "keywords", content: "AI, bot verification, reverse CAPTCHA, API security, AI agents, agent verification" }), _jsx("link", { rel: "alternate", type: "application/json", href: "/openapi.json", title: "OpenAPI Specification" }), _jsx("link", { rel: "alternate", type: "application/json", href: "/.well-known/ai-plugin.json", title: "AI Plugin Manifest" }), _jsx("link", { rel: "botcha-challenge", href: "#botcha-challenge", type: "application/botcha+json", title: "Embedded Bot Challenge" }), _jsx("meta", { name: "ai-agent-welcome", content: "true" }), _jsx("meta", { name: "botcha-challenge", content: "embedded", "data-selector": "script[type='application/botcha+json']" }), _jsx("meta", { property: "og:title", content: "BOTCHA \u2014 Reverse CAPTCHA for AI Agents" }), _jsx("meta", { property: "og:description", content: "Hosted bot verification service. Prove you're a bot. Humans need not apply." }), _jsx("meta", { property: "og:url", content: "https://botcha.ai" }), _jsx("meta", { property: "og:type", content: "website" }), _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify({
|
|
45
|
+
'@context': 'https://schema.org',
|
|
46
|
+
'@type': 'SoftwareApplication',
|
|
47
|
+
name: 'BOTCHA',
|
|
48
|
+
applicationCategory: 'DeveloperApplication',
|
|
49
|
+
description: 'Hosted reverse CAPTCHA for AI agents. Computational challenges that only bots can solve.',
|
|
50
|
+
url: 'https://botcha.ai',
|
|
51
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
|
|
52
|
+
operatingSystem: 'Any',
|
|
53
|
+
softwareVersion: version,
|
|
54
|
+
}) } }), _jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }), _jsx("link", { href: "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap", rel: "stylesheet" }), _jsx("style", { dangerouslySetInnerHTML: { __html: DASHBOARD_CSS } })] }), _jsx("body", { children: _jsx("div", { class: "login-container", children: _jsx("div", { class: "landing-box", children: children }) }) })] }));
|
|
55
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BOTCHA Dashboard Pages — Hono JSX + htmx
|
|
3
|
+
*
|
|
4
|
+
* Server-rendered pages with htmx for dynamic data loading.
|
|
5
|
+
* Each section uses hx-get to fetch HTML fragments from /dashboard/api/*.
|
|
6
|
+
*/
|
|
7
|
+
import type { FC } from 'hono/jsx';
|
|
8
|
+
export declare const DashboardPage: FC<{
|
|
9
|
+
appId: string;
|
|
10
|
+
}>;
|
|
11
|
+
//# sourceMappingURL=pages.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
2
|
+
import { DashboardLayout, Card } from './layout';
|
|
3
|
+
// ============ PERIOD SELECTOR ============
|
|
4
|
+
const PeriodSelector = ({ currentPeriod = '24h', targetId, endpoint, }) => {
|
|
5
|
+
const periods = [
|
|
6
|
+
{ value: '1h', label: '1H' },
|
|
7
|
+
{ value: '24h', label: '24H' },
|
|
8
|
+
{ value: '7d', label: '7D' },
|
|
9
|
+
{ value: '30d', label: '30D' },
|
|
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 }))) }));
|
|
12
|
+
};
|
|
13
|
+
// ============ LOADING SKELETON ============
|
|
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%;" })] }));
|
|
15
|
+
// ============ MAIN DASHBOARD PAGE ============
|
|
16
|
+
export const DashboardPage = ({ appId }) => {
|
|
17
|
+
return (_jsxs(DashboardLayout, { title: "BOTCHA Dashboard", appId: appId, children: [_jsx("h1", { style: "font-size: 1.25rem; font-weight: 700; margin-bottom: 0.5rem; text-transform: uppercase; letter-spacing: 0.08em;", children: "Dashboard" }), _jsxs("p", { class: "text-muted mb-3", style: "font-size: 0.8125rem;", children: ["Per-app metrics for ", _jsx("code", { children: appId })] }), _jsxs(Card, { title: "Overview", children: [_jsx(PeriodSelector, { targetId: "overview-content", endpoint: "/dashboard/api/overview" }), _jsx("div", { id: "overview-content", "hx-get": "/dashboard/api/overview?period=24h", "hx-trigger": "load", "hx-swap": "innerHTML", children: _jsx(LoadingSkeleton, {}) })] }), _jsxs("div", { class: "dashboard-grid", children: [_jsxs("div", { children: [_jsx(PeriodSelector, { targetId: "volume-content", endpoint: "/dashboard/api/volume" }), _jsx("div", { id: "volume-content", "hx-get": "/dashboard/api/volume?period=24h", "hx-trigger": "load", "hx-swap": "innerHTML", children: _jsx(LoadingSkeleton, {}) })] }), _jsxs("div", { children: [_jsx(PeriodSelector, { targetId: "types-content", endpoint: "/dashboard/api/types" }), _jsx("div", { id: "types-content", "hx-get": "/dashboard/api/types?period=24h", "hx-trigger": "load", "hx-swap": "innerHTML", children: _jsx(LoadingSkeleton, {}) })] })] }), _jsx(PeriodSelector, { targetId: "performance-content", endpoint: "/dashboard/api/performance" }), _jsx("div", { id: "performance-content", "hx-get": "/dashboard/api/performance?period=24h", "hx-trigger": "load", "hx-swap": "innerHTML", children: _jsx(LoadingSkeleton, {}) }), _jsxs("div", { class: "dashboard-grid", children: [_jsxs("div", { children: [_jsx(PeriodSelector, { targetId: "errors-content", endpoint: "/dashboard/api/errors" }), _jsx("div", { id: "errors-content", "hx-get": "/dashboard/api/errors?period=24h", "hx-trigger": "load", "hx-swap": "innerHTML", children: _jsx(LoadingSkeleton, {}) })] }), _jsxs("div", { children: [_jsx(PeriodSelector, { targetId: "geo-content", endpoint: "/dashboard/api/geo" }), _jsx("div", { id: "geo-content", "hx-get": "/dashboard/api/geo?period=24h", "hx-trigger": "load", "hx-swap": "innerHTML", children: _jsx(LoadingSkeleton, {}) })] })] }), _jsxs("div", { class: "text-center text-dim mt-4", style: "font-size: 0.6875rem; padding-bottom: 2rem; letter-spacing: 0.02em;", children: ["BOTCHA \u00B7 Cloudflare Analytics Engine \u00B7", ' ', _jsx("a", { href: "/", style: "font-size: 0.6875rem; color: #555555;", children: "API Docs" })] })] }));
|
|
18
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BOTCHA Dashboard CSS
|
|
3
|
+
*
|
|
4
|
+
* Light terminal aesthetic — black and white with offwhite background.
|
|
5
|
+
* Borrows structural patterns from turbopuffer (dot shadows, hard borders,
|
|
6
|
+
* layered box-shadows on buttons) but with BOTCHA's own identity.
|
|
7
|
+
*
|
|
8
|
+
* JetBrains Mono · #ffffff bg · #1a1a1a text · black accent · square corners · dot shadows
|
|
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";
|
|
11
|
+
//# sourceMappingURL=styles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/dashboard/styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,aAAa,yvqBAitBzB,CAAC"}
|