@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.
Files changed (44) hide show
  1. package/README.md +9 -8
  2. package/dist/agents.d.ts +68 -0
  3. package/dist/agents.d.ts.map +1 -0
  4. package/dist/agents.js +123 -0
  5. package/dist/apps.d.ts +73 -7
  6. package/dist/apps.d.ts.map +1 -1
  7. package/dist/apps.js +164 -9
  8. package/dist/challenges.d.ts.map +1 -1
  9. package/dist/challenges.js +5 -4
  10. package/dist/dashboard/api.d.ts +70 -0
  11. package/dist/dashboard/api.d.ts.map +1 -0
  12. package/dist/dashboard/api.js +553 -0
  13. package/dist/dashboard/auth.d.ts +183 -0
  14. package/dist/dashboard/auth.d.ts.map +1 -0
  15. package/dist/dashboard/auth.js +401 -0
  16. package/dist/dashboard/device-code.d.ts +43 -0
  17. package/dist/dashboard/device-code.d.ts.map +1 -0
  18. package/dist/dashboard/device-code.js +77 -0
  19. package/dist/dashboard/index.d.ts +31 -0
  20. package/dist/dashboard/index.d.ts.map +1 -0
  21. package/dist/dashboard/index.js +64 -0
  22. package/dist/dashboard/landing.d.ts +20 -0
  23. package/dist/dashboard/landing.d.ts.map +1 -0
  24. package/dist/dashboard/landing.js +45 -0
  25. package/dist/dashboard/layout.d.ts +54 -0
  26. package/dist/dashboard/layout.d.ts.map +1 -0
  27. package/dist/dashboard/layout.js +55 -0
  28. package/dist/dashboard/pages.d.ts +11 -0
  29. package/dist/dashboard/pages.d.ts.map +1 -0
  30. package/dist/dashboard/pages.js +18 -0
  31. package/dist/dashboard/styles.d.ts +11 -0
  32. package/dist/dashboard/styles.d.ts.map +1 -0
  33. package/dist/dashboard/styles.js +731 -0
  34. package/dist/email.d.ts +41 -0
  35. package/dist/email.d.ts.map +1 -0
  36. package/dist/email.js +116 -0
  37. package/dist/index.d.ts +2 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +666 -189
  40. package/dist/routes/stream.js +2 -2
  41. package/dist/static.d.ts +392 -4
  42. package/dist/static.d.ts.map +1 -1
  43. package/dist/static.js +511 -14
  44. package/package.json +1 -1
package/README.md CHANGED
@@ -1,18 +1,19 @@
1
1
  # @dupecom/botcha-cloudflare
2
2
 
3
- > 🤖 **BOTCHA** - Prove you're a bot. Humans need not apply.
3
+ > **BOTCHA** - Prove you're a bot. Humans need not apply.
4
4
  >
5
- > **Cloudflare Workers Edition v0.2.0** - Production-ready with JWT & Rate Limiting
5
+ > **Cloudflare Workers Edition v0.11.0** - Identity layer for AI agents
6
6
 
7
7
  Reverse CAPTCHA that verifies AI agents and blocks humans. Running at the edge.
8
8
 
9
- ## 🚀 What's New in v0.2.0
9
+ ## What's New in v0.11.0
10
10
 
11
- - ✅ **JWT Token Authentication** - Secure token-based auth flow with 1-hour expiry
12
- - ✅ **Rate Limiting** - 100 challenges/hour/IP with proper headers
13
- - ✅ **KV Storage** - Challenge state stored in Cloudflare KV (prevents replay attacks)
14
- - ✅ **Versioned API** - New `/v1/*` endpoints with backward-compatible legacy routes
15
- - ✅ **Production Ready** - Enterprise-grade auth and security
11
+ - **Agent Registry** - Persistent agent identities (POST /v1/agents/register)
12
+ - **Email-tied apps** - Email verification, account recovery, secret rotation
13
+ - **Dashboard** - Per-app metrics with agent-first auth (device code flow)
14
+ - **JWT security** - 5-min access tokens, refresh tokens, audience claims, IP binding, revocation
15
+ - **Multi-tenant** - Per-app isolation, scoped tokens, rate limiting
16
+ - **Server-side SDKs** - @botcha/verify (TS) + botcha-verify (Python)
16
17
 
17
18
  ## Features
18
19
 
@@ -0,0 +1,68 @@
1
+ /**
2
+ * BOTCHA Agent Registry
3
+ *
4
+ * Agent registration and management:
5
+ * - Crypto-random agent IDs
6
+ * - KV storage for agent metadata
7
+ * - App-scoped agent lists
8
+ * - Fail-open design for resilience
9
+ */
10
+ export type KVNamespace = {
11
+ get: (key: string, type?: 'text' | 'json' | 'arrayBuffer' | 'stream') => Promise<any>;
12
+ put: (key: string, value: string, options?: {
13
+ expirationTtl?: number;
14
+ }) => Promise<void>;
15
+ delete: (key: string) => Promise<void>;
16
+ };
17
+ /**
18
+ * Agent record stored in KV
19
+ */
20
+ export interface Agent {
21
+ agent_id: string;
22
+ app_id: string;
23
+ name: string;
24
+ operator?: string;
25
+ version?: string;
26
+ created_at: number;
27
+ }
28
+ /**
29
+ * Generate a crypto-random agent ID
30
+ * Format: 'agent_' + 16 hex chars
31
+ *
32
+ * Example: agent_a1b2c3d4e5f6a7b8
33
+ */
34
+ export declare function generateAgentId(): string;
35
+ /**
36
+ * Create a new agent and add it to the app's agent list
37
+ *
38
+ * Stores in KV at:
39
+ * - `agent:{agent_id}` — Agent record
40
+ * - `agents:{app_id}` — Array of agent_ids for this app
41
+ *
42
+ * @param kv - KV namespace for storage
43
+ * @param app_id - Parent app that owns this agent
44
+ * @param data - Agent metadata (name, operator, version)
45
+ * @returns Agent record with generated agent_id
46
+ */
47
+ export declare function createAgent(kv: KVNamespace, app_id: string, data: {
48
+ name: string;
49
+ operator?: string;
50
+ version?: string;
51
+ }): Promise<Agent | null>;
52
+ /**
53
+ * Get agent by agent_id
54
+ *
55
+ * @param kv - KV namespace
56
+ * @param agent_id - The agent ID to retrieve
57
+ * @returns Agent record or null if not found
58
+ */
59
+ export declare function getAgent(kv: KVNamespace, agent_id: string): Promise<Agent | null>;
60
+ /**
61
+ * List all agents for an app
62
+ *
63
+ * @param kv - KV namespace
64
+ * @param app_id - The app ID to list agents for
65
+ * @returns Array of agent records (empty array if none found)
66
+ */
67
+ export declare function listAgents(kv: KVNamespace, app_id: string): Promise<Agent[]>;
68
+ //# sourceMappingURL=agents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,aAAa,GAAG,QAAQ,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IACtF,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzF,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAIF;;GAEG;AACH,MAAM,WAAW,KAAK;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAID;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAOxC;AAID;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAC/B,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,MAAM,EACd,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1D,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAwCvB;AAED;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAC5B,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAcvB;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,KAAK,EAAE,CAAC,CAsBlB"}
package/dist/agents.js ADDED
@@ -0,0 +1,123 @@
1
+ /**
2
+ * BOTCHA Agent Registry
3
+ *
4
+ * Agent registration and management:
5
+ * - Crypto-random agent IDs
6
+ * - KV storage for agent metadata
7
+ * - App-scoped agent lists
8
+ * - Fail-open design for resilience
9
+ */
10
+ // ============ CRYPTO UTILITIES ============
11
+ /**
12
+ * Generate a crypto-random agent ID
13
+ * Format: 'agent_' + 16 hex chars
14
+ *
15
+ * Example: agent_a1b2c3d4e5f6a7b8
16
+ */
17
+ export function generateAgentId() {
18
+ const bytes = new Uint8Array(8); // 8 bytes = 16 hex chars
19
+ crypto.getRandomValues(bytes);
20
+ const hexString = Array.from(bytes)
21
+ .map(b => b.toString(16).padStart(2, '0'))
22
+ .join('');
23
+ return `agent_${hexString}`;
24
+ }
25
+ // ============ AGENT MANAGEMENT ============
26
+ /**
27
+ * Create a new agent and add it to the app's agent list
28
+ *
29
+ * Stores in KV at:
30
+ * - `agent:{agent_id}` — Agent record
31
+ * - `agents:{app_id}` — Array of agent_ids for this app
32
+ *
33
+ * @param kv - KV namespace for storage
34
+ * @param app_id - Parent app that owns this agent
35
+ * @param data - Agent metadata (name, operator, version)
36
+ * @returns Agent record with generated agent_id
37
+ */
38
+ export async function createAgent(kv, app_id, data) {
39
+ try {
40
+ const agent_id = generateAgentId();
41
+ const agent = {
42
+ agent_id,
43
+ app_id,
44
+ name: data.name,
45
+ operator: data.operator,
46
+ version: data.version,
47
+ created_at: Date.now(),
48
+ };
49
+ // Get existing agent list for this app (if any)
50
+ let agentIds = [];
51
+ try {
52
+ const existingList = await kv.get(`agents:${app_id}`, 'text');
53
+ if (existingList) {
54
+ agentIds = JSON.parse(existingList);
55
+ }
56
+ }
57
+ catch (error) {
58
+ console.warn(`Failed to fetch existing agent list for app ${app_id}, starting fresh:`, error);
59
+ // Fail-open: Continue with empty list
60
+ }
61
+ // Add new agent_id to the list
62
+ agentIds.push(agent_id);
63
+ // Store agent record and updated list in parallel
64
+ await Promise.all([
65
+ kv.put(`agent:${agent_id}`, JSON.stringify(agent)),
66
+ kv.put(`agents:${app_id}`, JSON.stringify(agentIds)),
67
+ ]);
68
+ return agent;
69
+ }
70
+ catch (error) {
71
+ console.error(`Failed to create agent for app ${app_id}:`, error);
72
+ // Fail-open: Return null instead of throwing
73
+ return null;
74
+ }
75
+ }
76
+ /**
77
+ * Get agent by agent_id
78
+ *
79
+ * @param kv - KV namespace
80
+ * @param agent_id - The agent ID to retrieve
81
+ * @returns Agent record or null if not found
82
+ */
83
+ export async function getAgent(kv, agent_id) {
84
+ try {
85
+ const data = await kv.get(`agent:${agent_id}`, 'text');
86
+ if (!data) {
87
+ return null;
88
+ }
89
+ return JSON.parse(data);
90
+ }
91
+ catch (error) {
92
+ console.error(`Failed to get agent ${agent_id}:`, error);
93
+ // Fail-open: Return null instead of throwing
94
+ return null;
95
+ }
96
+ }
97
+ /**
98
+ * List all agents for an app
99
+ *
100
+ * @param kv - KV namespace
101
+ * @param app_id - The app ID to list agents for
102
+ * @returns Array of agent records (empty array if none found)
103
+ */
104
+ export async function listAgents(kv, app_id) {
105
+ try {
106
+ // Get the list of agent IDs for this app
107
+ const agentIdsData = await kv.get(`agents:${app_id}`, 'text');
108
+ if (!agentIdsData) {
109
+ return [];
110
+ }
111
+ const agentIds = JSON.parse(agentIdsData);
112
+ // Fetch all agent records in parallel
113
+ const agentPromises = agentIds.map(agent_id => getAgent(kv, agent_id));
114
+ const agents = await Promise.all(agentPromises);
115
+ // Filter out any null results (failed fetches) and return
116
+ return agents.filter((agent) => agent !== null);
117
+ }
118
+ catch (error) {
119
+ console.error(`Failed to list agents for app ${app_id}:`, error);
120
+ // Fail-open: Return empty array instead of throwing
121
+ return [];
122
+ }
123
+ }
package/dist/apps.d.ts CHANGED
@@ -6,6 +6,9 @@
6
6
  * - SHA-256 secret hashing (never store plaintext)
7
7
  * - KV storage for app configs
8
8
  * - Rate limit tracking per app
9
+ * - Email verification for account recovery
10
+ * - Email→app_id reverse index for recovery lookups
11
+ * - Secret rotation with email notification
9
12
  */
10
13
  export type KVNamespace = {
11
14
  get: (key: string, type?: 'text' | 'json' | 'arrayBuffer' | 'stream') => Promise<any>;
@@ -22,6 +25,10 @@ export interface AppConfig {
22
25
  secret_hash: string;
23
26
  created_at: number;
24
27
  rate_limit: number;
28
+ email: string;
29
+ email_verified: boolean;
30
+ email_verification_code?: string;
31
+ email_verification_expires?: number;
25
32
  }
26
33
  /**
27
34
  * Result of app creation (includes plaintext secret - only shown once!)
@@ -29,7 +36,20 @@ export interface AppConfig {
29
36
  export interface CreateAppResult {
30
37
  app_id: string;
31
38
  app_secret: string;
39
+ email: string;
40
+ email_verified: boolean;
41
+ verification_required: boolean;
32
42
  }
43
+ /**
44
+ * Public app info returned by getApp (excludes secrets and internal fields)
45
+ */
46
+ export type PublicAppConfig = {
47
+ app_id: string;
48
+ created_at: number;
49
+ rate_limit: number;
50
+ email: string;
51
+ email_verified: boolean;
52
+ };
33
53
  /**
34
54
  * Generate a crypto-random app ID
35
55
  * Format: 'app_' + 16 hex chars
@@ -52,6 +72,10 @@ export declare function generateAppSecret(): string;
52
72
  * @returns SHA-256 hash as hex string
53
73
  */
54
74
  export declare function hashSecret(secret: string): Promise<string>;
75
+ /**
76
+ * Generate a 6-digit numeric verification code
77
+ */
78
+ export declare function generateVerificationCode(): string;
55
79
  /**
56
80
  * Create a new app with crypto-random credentials
57
81
  *
@@ -60,15 +84,52 @@ export declare function hashSecret(secret: string): Promise<string>;
60
84
  * - app_secret: 'sk_' + 32 hex chars
61
85
  *
62
86
  * Stores in KV at key `app:{app_id}` with:
63
- * - app_id
64
- * - secret_hash (SHA-256, never plaintext)
65
- * - created_at (timestamp)
66
- * - rate_limit (default: 100 req/hour)
87
+ * - app_id, secret_hash (SHA-256), created_at, rate_limit, email, email_verified
88
+ *
89
+ * Also stores email→app_id reverse index at `email:{email}` for recovery lookups.
67
90
  *
68
91
  * @param kv - KV namespace for storage
69
- * @returns {app_id, app_secret} - SECRET ONLY RETURNED ONCE!
92
+ * @param email - Required owner email address
93
+ * @returns {app_id, app_secret, email, email_verified, verification_required}
94
+ */
95
+ export declare function createApp(kv: KVNamespace, email: string): Promise<CreateAppResult>;
96
+ /**
97
+ * Get the plaintext verification code for an app (internal use only — for sending via email).
98
+ *
99
+ * This is a separate step because createApp returns the code hash, not the plaintext.
100
+ * Instead, we generate and return code in createApp flow; this function regenerates
101
+ * a new code for resend scenarios.
70
102
  */
71
- export declare function createApp(kv: KVNamespace): Promise<CreateAppResult>;
103
+ export declare function regenerateVerificationCode(kv: KVNamespace, app_id: string): Promise<{
104
+ code: string;
105
+ } | null>;
106
+ /**
107
+ * Verify email with the 6-digit code
108
+ *
109
+ * @returns { verified: true } on success, { verified: false, reason } on failure
110
+ */
111
+ export declare function verifyEmailCode(kv: KVNamespace, app_id: string, code: string): Promise<{
112
+ verified: boolean;
113
+ reason?: string;
114
+ }>;
115
+ /**
116
+ * Look up app_id by email (for account recovery)
117
+ *
118
+ * Uses the email→app_id reverse index stored in KV.
119
+ * Only works for apps with verified emails.
120
+ */
121
+ export declare function getAppByEmail(kv: KVNamespace, email: string): Promise<{
122
+ app_id: string;
123
+ email_verified: boolean;
124
+ } | null>;
125
+ /**
126
+ * Rotate app secret — generates a new secret and invalidates the old one.
127
+ *
128
+ * @returns New app_secret (plaintext, only returned once) or null on failure
129
+ */
130
+ export declare function rotateAppSecret(kv: KVNamespace, app_id: string): Promise<{
131
+ app_secret: string;
132
+ } | null>;
72
133
  /**
73
134
  * Get app configuration by app_id
74
135
  *
@@ -78,7 +139,12 @@ export declare function createApp(kv: KVNamespace): Promise<CreateAppResult>;
78
139
  * @param app_id - The app ID to retrieve
79
140
  * @returns App config (without secret_hash) or null if not found
80
141
  */
81
- export declare function getApp(kv: KVNamespace, app_id: string): Promise<Omit<AppConfig, 'secret_hash'> | null>;
142
+ export declare function getApp(kv: KVNamespace, app_id: string): Promise<PublicAppConfig | null>;
143
+ /**
144
+ * Get raw app config (internal use only — includes secret_hash)
145
+ * Used by validateAppSecret and dashboard auth.
146
+ */
147
+ export declare function getAppRaw(kv: KVNamespace, app_id: string): Promise<AppConfig | null>;
82
148
  /**
83
149
  * Validate an app secret against stored hash
84
150
  *
@@ -1 +1 @@
1
- {"version":3,"file":"apps.d.ts","sourceRoot":"","sources":["../src/apps.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,aAAa,GAAG,QAAQ,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IACtF,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzF,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAIF;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAID;;;;;GAKG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAOtC;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAO1C;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAOhE;AAID;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,SAAS,CAAC,EAAE,EAAE,WAAW,GAAG,OAAO,CAAC,eAAe,CAAC,CAoBzE;AAED;;;;;;;;GAQG;AACH,wBAAsB,MAAM,CAC1B,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,aAAa,CAAC,GAAG,IAAI,CAAC,CAoBhD;AAED;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,OAAO,CAAC,CA6BlB"}
1
+ {"version":3,"file":"apps.d.ts","sourceRoot":"","sources":["../src/apps.ts"],"names":[],"mappings":"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"}
package/dist/apps.js CHANGED
@@ -6,6 +6,9 @@
6
6
  * - SHA-256 secret hashing (never store plaintext)
7
7
  * - KV storage for app configs
8
8
  * - Rate limit tracking per app
9
+ * - Email verification for account recovery
10
+ * - Email→app_id reverse index for recovery lookups
11
+ * - Secret rotation with email notification
9
12
  */
10
13
  // ============ CRYPTO UTILITIES ============
11
14
  /**
@@ -51,6 +54,16 @@ export async function hashSecret(secret) {
51
54
  const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
52
55
  return hashHex;
53
56
  }
57
+ /**
58
+ * Generate a 6-digit numeric verification code
59
+ */
60
+ export function generateVerificationCode() {
61
+ const bytes = new Uint8Array(4);
62
+ crypto.getRandomValues(bytes);
63
+ // Convert to number and mod 1,000,000 to get 6 digits
64
+ const num = ((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]) >>> 0;
65
+ return (num % 1000000).toString().padStart(6, '0');
66
+ }
54
67
  // ============ APP MANAGEMENT ============
55
68
  /**
56
69
  * Create a new app with crypto-random credentials
@@ -60,32 +73,156 @@ export async function hashSecret(secret) {
60
73
  * - app_secret: 'sk_' + 32 hex chars
61
74
  *
62
75
  * Stores in KV at key `app:{app_id}` with:
63
- * - app_id
64
- * - secret_hash (SHA-256, never plaintext)
65
- * - created_at (timestamp)
66
- * - rate_limit (default: 100 req/hour)
76
+ * - app_id, secret_hash (SHA-256), created_at, rate_limit, email, email_verified
77
+ *
78
+ * Also stores email→app_id reverse index at `email:{email}` for recovery lookups.
67
79
  *
68
80
  * @param kv - KV namespace for storage
69
- * @returns {app_id, app_secret} - SECRET ONLY RETURNED ONCE!
81
+ * @param email - Required owner email address
82
+ * @returns {app_id, app_secret, email, email_verified, verification_required}
70
83
  */
71
- export async function createApp(kv) {
84
+ export async function createApp(kv, email) {
72
85
  const app_id = generateAppId();
73
86
  const app_secret = generateAppSecret();
74
87
  const secret_hash = await hashSecret(app_secret);
88
+ // Generate email verification code
89
+ const verificationCode = generateVerificationCode();
90
+ const verificationCodeHash = await hashSecret(verificationCode);
75
91
  const config = {
76
92
  app_id,
77
93
  secret_hash,
78
94
  created_at: Date.now(),
79
95
  rate_limit: 100, // Default: 100 requests/hour
96
+ email,
97
+ email_verified: false,
98
+ email_verification_code: verificationCodeHash,
99
+ email_verification_expires: Date.now() + 10 * 60 * 1000, // 10 minutes
80
100
  };
81
- // Store in KV with key format: app:{app_id}
82
- // No TTL - apps persist indefinitely unless explicitly deleted
83
- await kv.put(`app:${app_id}`, JSON.stringify(config));
101
+ // Store app config and email→app_id index in parallel
102
+ await Promise.all([
103
+ kv.put(`app:${app_id}`, JSON.stringify(config)),
104
+ kv.put(`email:${email.toLowerCase()}`, app_id),
105
+ ]);
84
106
  return {
85
107
  app_id,
86
108
  app_secret, // ONLY returned at creation time!
109
+ email,
110
+ email_verified: false,
111
+ verification_required: true,
87
112
  };
88
113
  }
114
+ /**
115
+ * Get the plaintext verification code for an app (internal use only — for sending via email).
116
+ *
117
+ * This is a separate step because createApp returns the code hash, not the plaintext.
118
+ * Instead, we generate and return code in createApp flow; this function regenerates
119
+ * a new code for resend scenarios.
120
+ */
121
+ export async function regenerateVerificationCode(kv, app_id) {
122
+ try {
123
+ const data = await kv.get(`app:${app_id}`, 'text');
124
+ if (!data)
125
+ return null;
126
+ const config = JSON.parse(data);
127
+ if (config.email_verified)
128
+ return null; // Already verified
129
+ const code = generateVerificationCode();
130
+ const codeHash = await hashSecret(code);
131
+ config.email_verification_code = codeHash;
132
+ config.email_verification_expires = Date.now() + 10 * 60 * 1000;
133
+ await kv.put(`app:${app_id}`, JSON.stringify(config));
134
+ return { code };
135
+ }
136
+ catch (error) {
137
+ console.error(`Failed to regenerate verification code for ${app_id}:`, error);
138
+ return null;
139
+ }
140
+ }
141
+ /**
142
+ * Verify email with the 6-digit code
143
+ *
144
+ * @returns { verified: true } on success, { verified: false, reason } on failure
145
+ */
146
+ export async function verifyEmailCode(kv, app_id, code) {
147
+ try {
148
+ const data = await kv.get(`app:${app_id}`, 'text');
149
+ if (!data) {
150
+ return { verified: false, reason: 'App not found' };
151
+ }
152
+ const config = JSON.parse(data);
153
+ if (config.email_verified) {
154
+ return { verified: false, reason: 'Email already verified' };
155
+ }
156
+ if (!config.email_verification_code || !config.email_verification_expires) {
157
+ return { verified: false, reason: 'No verification pending' };
158
+ }
159
+ if (Date.now() > config.email_verification_expires) {
160
+ return { verified: false, reason: 'Verification code expired' };
161
+ }
162
+ // Compare hashed codes
163
+ const providedHash = await hashSecret(code);
164
+ if (providedHash !== config.email_verification_code) {
165
+ return { verified: false, reason: 'Invalid verification code' };
166
+ }
167
+ // Mark email as verified, clear verification fields
168
+ config.email_verified = true;
169
+ delete config.email_verification_code;
170
+ delete config.email_verification_expires;
171
+ await kv.put(`app:${app_id}`, JSON.stringify(config));
172
+ return { verified: true };
173
+ }
174
+ catch (error) {
175
+ console.error(`Failed to verify email for ${app_id}:`, error);
176
+ return { verified: false, reason: 'Verification failed' };
177
+ }
178
+ }
179
+ /**
180
+ * Look up app_id by email (for account recovery)
181
+ *
182
+ * Uses the email→app_id reverse index stored in KV.
183
+ * Only works for apps with verified emails.
184
+ */
185
+ export async function getAppByEmail(kv, email) {
186
+ try {
187
+ const app_id = await kv.get(`email:${email.toLowerCase()}`, 'text');
188
+ if (!app_id)
189
+ return null;
190
+ const data = await kv.get(`app:${app_id}`, 'text');
191
+ if (!data)
192
+ return null;
193
+ const config = JSON.parse(data);
194
+ return {
195
+ app_id: config.app_id,
196
+ email_verified: config.email_verified,
197
+ };
198
+ }
199
+ catch (error) {
200
+ console.error(`Failed to look up app by email:`, error);
201
+ return null;
202
+ }
203
+ }
204
+ /**
205
+ * Rotate app secret — generates a new secret and invalidates the old one.
206
+ *
207
+ * @returns New app_secret (plaintext, only returned once) or null on failure
208
+ */
209
+ export async function rotateAppSecret(kv, app_id) {
210
+ try {
211
+ const data = await kv.get(`app:${app_id}`, 'text');
212
+ if (!data)
213
+ return null;
214
+ const config = JSON.parse(data);
215
+ const new_secret = generateAppSecret();
216
+ const new_hash = await hashSecret(new_secret);
217
+ config.secret_hash = new_hash;
218
+ await kv.put(`app:${app_id}`, JSON.stringify(config));
219
+ return { app_secret: new_secret };
220
+ }
221
+ catch (error) {
222
+ console.error(`Failed to rotate secret for ${app_id}:`, error);
223
+ return null;
224
+ }
225
+ }
89
226
  /**
90
227
  * Get app configuration by app_id
91
228
  *
@@ -107,6 +244,8 @@ export async function getApp(kv, app_id) {
107
244
  app_id: config.app_id,
108
245
  created_at: config.created_at,
109
246
  rate_limit: config.rate_limit,
247
+ email: config.email,
248
+ email_verified: config.email_verified,
110
249
  };
111
250
  }
112
251
  catch (error) {
@@ -114,6 +253,22 @@ export async function getApp(kv, app_id) {
114
253
  return null;
115
254
  }
116
255
  }
256
+ /**
257
+ * Get raw app config (internal use only — includes secret_hash)
258
+ * Used by validateAppSecret and dashboard auth.
259
+ */
260
+ export async function getAppRaw(kv, app_id) {
261
+ try {
262
+ const data = await kv.get(`app:${app_id}`, 'text');
263
+ if (!data)
264
+ return null;
265
+ return JSON.parse(data);
266
+ }
267
+ catch (error) {
268
+ console.error(`Failed to get raw app ${app_id}:`, error);
269
+ return null;
270
+ }
271
+ }
117
272
  /**
118
273
  * Validate an app secret against stored hash
119
274
  *
@@ -1 +1 @@
1
- {"version":3,"file":"challenges.d.ts","sourceRoot":"","sources":["../src/challenges.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"challenges.d.ts","sourceRoot":"","sources":["../src/challenges.ts"],"names":[],"mappings":"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"}
@@ -120,9 +120,10 @@ export async function generateSpeedChallenge(kv, clientTimestamp, app_id) {
120
120
  };
121
121
  // Store in KV with 5 minute TTL (safety buffer for time checks)
122
122
  await storeChallenge(kv, id, challenge, 300);
123
+ const pipelineHint = ' Tip: compute all hashes and submit in a single HTTP request. Sequential shell commands will likely exceed the time limit.';
123
124
  const instructions = rttMs > 0
124
- ? `Compute SHA256 of each number, return first 8 hex chars of each. Submit as array. You have ${adjustedTimeLimit}ms (adjusted for your ${rttMs}ms network latency).`
125
- : 'Compute SHA256 of each number, return first 8 hex chars of each. Submit as array. You have 500ms.';
125
+ ? `Compute SHA256 of each number, return first 8 hex chars of each. Submit as array. You have ${adjustedTimeLimit}ms (adjusted for your ${rttMs}ms network latency).${pipelineHint}`
126
+ : `Compute SHA256 of each number, return first 8 hex chars of each. Submit as array. You have 500ms.${pipelineHint}`;
126
127
  return {
127
128
  id,
128
129
  problems,
@@ -956,8 +957,8 @@ export async function generateHybridChallenge(kv, clientTimestamp, app_id) {
956
957
  hybridChallenges.set(id, hybrid);
957
958
  }
958
959
  const instructions = speedChallenge.rttInfo
959
- ? `Solve ALL speed problems (SHA256) in <${speedChallenge.timeLimit}ms (RTT-adjusted) AND answer ALL reasoning questions. Submit both together.`
960
- : 'Solve ALL speed problems (SHA256) in <500ms AND answer ALL reasoning questions. Submit both together.';
960
+ ? `Solve ALL speed problems (SHA256) in <${speedChallenge.timeLimit}ms (RTT-adjusted) AND answer ALL reasoning questions. Submit both together. Tip: compute all hashes in-process and submit in a single HTTP request.`
961
+ : 'Solve ALL speed problems (SHA256) in <500ms AND answer ALL reasoning questions. Submit both together. Tip: compute all hashes in-process and submit in a single HTTP request.';
961
962
  return {
962
963
  id,
963
964
  speed: {