@dupecom/botcha-cloudflare 0.3.3 → 0.10.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 (47) hide show
  1. package/dist/analytics.d.ts +60 -0
  2. package/dist/analytics.d.ts.map +1 -0
  3. package/dist/analytics.js +130 -0
  4. package/dist/apps.d.ts +159 -0
  5. package/dist/apps.d.ts.map +1 -0
  6. package/dist/apps.js +307 -0
  7. package/dist/auth.d.ts +93 -6
  8. package/dist/auth.d.ts.map +1 -1
  9. package/dist/auth.js +251 -9
  10. package/dist/challenges.d.ts +31 -7
  11. package/dist/challenges.d.ts.map +1 -1
  12. package/dist/challenges.js +551 -144
  13. package/dist/dashboard/api.d.ts +70 -0
  14. package/dist/dashboard/api.d.ts.map +1 -0
  15. package/dist/dashboard/api.js +546 -0
  16. package/dist/dashboard/auth.d.ts +183 -0
  17. package/dist/dashboard/auth.d.ts.map +1 -0
  18. package/dist/dashboard/auth.js +401 -0
  19. package/dist/dashboard/device-code.d.ts +43 -0
  20. package/dist/dashboard/device-code.d.ts.map +1 -0
  21. package/dist/dashboard/device-code.js +77 -0
  22. package/dist/dashboard/index.d.ts +31 -0
  23. package/dist/dashboard/index.d.ts.map +1 -0
  24. package/dist/dashboard/index.js +64 -0
  25. package/dist/dashboard/layout.d.ts +47 -0
  26. package/dist/dashboard/layout.d.ts.map +1 -0
  27. package/dist/dashboard/layout.js +38 -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 +633 -0
  34. package/dist/email.d.ts +44 -0
  35. package/dist/email.d.ts.map +1 -0
  36. package/dist/email.js +119 -0
  37. package/dist/index.d.ts +3 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +644 -50
  40. package/dist/rate-limit.d.ts +11 -1
  41. package/dist/rate-limit.d.ts.map +1 -1
  42. package/dist/rate-limit.js +13 -2
  43. package/dist/routes/stream.js +1 -1
  44. package/dist/static.d.ts +728 -0
  45. package/dist/static.d.ts.map +1 -0
  46. package/dist/static.js +818 -0
  47. package/package.json +1 -1
@@ -0,0 +1,60 @@
1
+ /**
2
+ * BOTCHA Analytics Engine Integration
3
+ *
4
+ * Tracks usage metrics for business intelligence and monitoring
5
+ */
6
+ export type AnalyticsEngineDataset = {
7
+ writeDataPoint: (data: {
8
+ blobs?: string[];
9
+ doubles?: number[];
10
+ indexes?: string[];
11
+ }) => void;
12
+ };
13
+ export interface AnalyticsEvent {
14
+ eventType: 'challenge_generated' | 'challenge_verified' | 'auth_success' | 'auth_failure' | 'rate_limit_exceeded' | 'error';
15
+ challengeType?: 'speed' | 'standard' | 'reasoning' | 'hybrid';
16
+ endpoint?: string;
17
+ verificationResult?: 'success' | 'failure';
18
+ verificationReason?: string;
19
+ authMethod?: 'landing-token' | 'bearer-token' | 'none';
20
+ solveTimeMs?: number;
21
+ responseTimeMs?: number;
22
+ clientIP?: string;
23
+ userAgent?: string;
24
+ country?: string;
25
+ errorType?: string;
26
+ errorMessage?: string;
27
+ }
28
+ /**
29
+ * Log an analytics event to Cloudflare Analytics Engine
30
+ */
31
+ export declare function logAnalyticsEvent(analytics: AnalyticsEngineDataset | undefined, event: AnalyticsEvent): Promise<void>;
32
+ /**
33
+ * Extract country from Cloudflare headers
34
+ */
35
+ export declare function getCountry(request: Request): string;
36
+ /**
37
+ * Extract user agent
38
+ */
39
+ export declare function getUserAgent(request: Request): string;
40
+ /**
41
+ * Helper to track challenge generation
42
+ */
43
+ export declare function trackChallengeGenerated(analytics: AnalyticsEngineDataset | undefined, challengeType: 'speed' | 'standard' | 'reasoning' | 'hybrid', endpoint: string, request: Request, clientIP: string, responseTimeMs: number): Promise<void>;
44
+ /**
45
+ * Helper to track challenge verification
46
+ */
47
+ export declare function trackChallengeVerified(analytics: AnalyticsEngineDataset | undefined, challengeType: 'speed' | 'standard' | 'reasoning' | 'hybrid', endpoint: string, success: boolean, solveTimeMs: number | undefined, reason: string | undefined, request: Request, clientIP: string): Promise<void>;
48
+ /**
49
+ * Helper to track authentication attempts
50
+ */
51
+ export declare function trackAuthAttempt(analytics: AnalyticsEngineDataset | undefined, authMethod: 'landing-token' | 'bearer-token', success: boolean, endpoint: string, request: Request, clientIP: string): Promise<void>;
52
+ /**
53
+ * Helper to track rate limit exceeded
54
+ */
55
+ export declare function trackRateLimitExceeded(analytics: AnalyticsEngineDataset | undefined, endpoint: string, request: Request, clientIP: string): Promise<void>;
56
+ /**
57
+ * Helper to track errors
58
+ */
59
+ export declare function trackError(analytics: AnalyticsEngineDataset | undefined, errorType: string, errorMessage: string, endpoint: string, request: Request): Promise<void>;
60
+ //# sourceMappingURL=analytics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,130 @@
1
+ /**
2
+ * BOTCHA Analytics Engine Integration
3
+ *
4
+ * Tracks usage metrics for business intelligence and monitoring
5
+ */
6
+ /**
7
+ * Log an analytics event to Cloudflare Analytics Engine
8
+ */
9
+ export async function logAnalyticsEvent(analytics, event) {
10
+ if (!analytics) {
11
+ // Analytics not configured (local dev)
12
+ return;
13
+ }
14
+ try {
15
+ // Cloudflare Analytics Engine uses:
16
+ // - blobs: string[] (up to 20 strings, max 5120 chars total)
17
+ // - doubles: number[] (up to 20 numbers)
18
+ // - indexes: string[] (up to 20 strings for filtering, each max 96 bytes)
19
+ const blobs = [
20
+ event.eventType,
21
+ event.challengeType || '',
22
+ event.endpoint || '',
23
+ event.verificationResult || '',
24
+ event.authMethod || '',
25
+ event.clientIP || '',
26
+ event.country || '',
27
+ event.errorType || '',
28
+ ];
29
+ const doubles = [
30
+ event.solveTimeMs || 0,
31
+ event.responseTimeMs || 0,
32
+ ];
33
+ const indexes = [
34
+ event.eventType,
35
+ event.challengeType || 'none',
36
+ event.endpoint || 'unknown',
37
+ ];
38
+ analytics.writeDataPoint({
39
+ blobs,
40
+ doubles,
41
+ indexes,
42
+ });
43
+ }
44
+ catch (error) {
45
+ // Never throw on analytics failures
46
+ console.error('Analytics logging failed:', error);
47
+ }
48
+ }
49
+ /**
50
+ * Extract country from Cloudflare headers
51
+ */
52
+ export function getCountry(request) {
53
+ return request.headers.get('cf-ipcountry') || 'unknown';
54
+ }
55
+ /**
56
+ * Extract user agent
57
+ */
58
+ export function getUserAgent(request) {
59
+ const ua = request.headers.get('user-agent') || 'unknown';
60
+ // Truncate to 100 chars to avoid bloating analytics
61
+ return ua.substring(0, 100);
62
+ }
63
+ /**
64
+ * Helper to track challenge generation
65
+ */
66
+ export async function trackChallengeGenerated(analytics, challengeType, endpoint, request, clientIP, responseTimeMs) {
67
+ await logAnalyticsEvent(analytics, {
68
+ eventType: 'challenge_generated',
69
+ challengeType,
70
+ endpoint,
71
+ clientIP,
72
+ country: getCountry(request),
73
+ userAgent: getUserAgent(request),
74
+ responseTimeMs,
75
+ });
76
+ }
77
+ /**
78
+ * Helper to track challenge verification
79
+ */
80
+ export async function trackChallengeVerified(analytics, challengeType, endpoint, success, solveTimeMs, reason, request, clientIP) {
81
+ await logAnalyticsEvent(analytics, {
82
+ eventType: 'challenge_verified',
83
+ challengeType,
84
+ endpoint,
85
+ verificationResult: success ? 'success' : 'failure',
86
+ verificationReason: reason,
87
+ solveTimeMs,
88
+ clientIP,
89
+ country: getCountry(request),
90
+ userAgent: getUserAgent(request),
91
+ });
92
+ }
93
+ /**
94
+ * Helper to track authentication attempts
95
+ */
96
+ export async function trackAuthAttempt(analytics, authMethod, success, endpoint, request, clientIP) {
97
+ await logAnalyticsEvent(analytics, {
98
+ eventType: success ? 'auth_success' : 'auth_failure',
99
+ authMethod,
100
+ endpoint,
101
+ verificationResult: success ? 'success' : 'failure',
102
+ clientIP,
103
+ country: getCountry(request),
104
+ userAgent: getUserAgent(request),
105
+ });
106
+ }
107
+ /**
108
+ * Helper to track rate limit exceeded
109
+ */
110
+ export async function trackRateLimitExceeded(analytics, endpoint, request, clientIP) {
111
+ await logAnalyticsEvent(analytics, {
112
+ eventType: 'rate_limit_exceeded',
113
+ endpoint,
114
+ clientIP,
115
+ country: getCountry(request),
116
+ userAgent: getUserAgent(request),
117
+ });
118
+ }
119
+ /**
120
+ * Helper to track errors
121
+ */
122
+ export async function trackError(analytics, errorType, errorMessage, endpoint, request) {
123
+ await logAnalyticsEvent(analytics, {
124
+ eventType: 'error',
125
+ errorType,
126
+ errorMessage: errorMessage.substring(0, 200), // Truncate
127
+ endpoint,
128
+ country: getCountry(request),
129
+ });
130
+ }
package/dist/apps.d.ts ADDED
@@ -0,0 +1,159 @@
1
+ /**
2
+ * BOTCHA App Management & Multi-Tenant Infrastructure
3
+ *
4
+ * Secure app creation with:
5
+ * - Crypto-random app IDs and secrets
6
+ * - SHA-256 secret hashing (never store plaintext)
7
+ * - KV storage for app configs
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
12
+ */
13
+ export type KVNamespace = {
14
+ get: (key: string, type?: 'text' | 'json' | 'arrayBuffer' | 'stream') => Promise<any>;
15
+ put: (key: string, value: string, options?: {
16
+ expirationTtl?: number;
17
+ }) => Promise<void>;
18
+ delete: (key: string) => Promise<void>;
19
+ };
20
+ /**
21
+ * App configuration stored in KV
22
+ */
23
+ export interface AppConfig {
24
+ app_id: string;
25
+ secret_hash: string;
26
+ created_at: number;
27
+ rate_limit: number;
28
+ email: string;
29
+ email_verified: boolean;
30
+ email_verification_code?: string;
31
+ email_verification_expires?: number;
32
+ }
33
+ /**
34
+ * Result of app creation (includes plaintext secret - only shown once!)
35
+ */
36
+ export interface CreateAppResult {
37
+ app_id: string;
38
+ app_secret: string;
39
+ email: string;
40
+ email_verified: boolean;
41
+ verification_required: boolean;
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
+ };
53
+ /**
54
+ * Generate a crypto-random app ID
55
+ * Format: 'app_' + 16 hex chars
56
+ *
57
+ * Example: app_a1b2c3d4e5f6a7b8
58
+ */
59
+ export declare function generateAppId(): string;
60
+ /**
61
+ * Generate a crypto-random app secret
62
+ * Format: 'sk_' + 32 hex chars
63
+ *
64
+ * Example: sk_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
65
+ */
66
+ export declare function generateAppSecret(): string;
67
+ /**
68
+ * Hash a secret using SHA-256
69
+ * Returns hex-encoded hash string
70
+ *
71
+ * @param secret - The plaintext secret to hash
72
+ * @returns SHA-256 hash as hex string
73
+ */
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;
79
+ /**
80
+ * Create a new app with crypto-random credentials
81
+ *
82
+ * Generates:
83
+ * - app_id: 'app_' + 16 hex chars
84
+ * - app_secret: 'sk_' + 32 hex chars
85
+ *
86
+ * Stores in KV at key `app:{app_id}` with:
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.
90
+ *
91
+ * @param kv - KV namespace for storage
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.
102
+ */
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>;
133
+ /**
134
+ * Get app configuration by app_id
135
+ *
136
+ * Returns app config WITHOUT secret_hash for security
137
+ *
138
+ * @param kv - KV namespace
139
+ * @param app_id - The app ID to retrieve
140
+ * @returns App config (without secret_hash) or null if not found
141
+ */
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>;
148
+ /**
149
+ * Validate an app secret against stored hash
150
+ *
151
+ * Uses constant-time comparison to prevent timing attacks
152
+ *
153
+ * @param kv - KV namespace
154
+ * @param app_id - The app ID
155
+ * @param app_secret - The plaintext secret to validate
156
+ * @returns true if valid, false otherwise
157
+ */
158
+ export declare function validateAppSecret(kv: KVNamespace, app_id: string, app_secret: string): Promise<boolean>;
159
+ //# sourceMappingURL=apps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apps.d.ts","sourceRoot":"","sources":["../src/apps.ts"],"names":[],"mappings":"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"}
package/dist/apps.js ADDED
@@ -0,0 +1,307 @@
1
+ /**
2
+ * BOTCHA App Management & Multi-Tenant Infrastructure
3
+ *
4
+ * Secure app creation with:
5
+ * - Crypto-random app IDs and secrets
6
+ * - SHA-256 secret hashing (never store plaintext)
7
+ * - KV storage for app configs
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
12
+ */
13
+ // ============ CRYPTO UTILITIES ============
14
+ /**
15
+ * Generate a crypto-random app ID
16
+ * Format: 'app_' + 16 hex chars
17
+ *
18
+ * Example: app_a1b2c3d4e5f6a7b8
19
+ */
20
+ export function generateAppId() {
21
+ const bytes = new Uint8Array(8); // 8 bytes = 16 hex chars
22
+ crypto.getRandomValues(bytes);
23
+ const hexString = Array.from(bytes)
24
+ .map(b => b.toString(16).padStart(2, '0'))
25
+ .join('');
26
+ return `app_${hexString}`;
27
+ }
28
+ /**
29
+ * Generate a crypto-random app secret
30
+ * Format: 'sk_' + 32 hex chars
31
+ *
32
+ * Example: sk_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
33
+ */
34
+ export function generateAppSecret() {
35
+ const bytes = new Uint8Array(16); // 16 bytes = 32 hex chars
36
+ crypto.getRandomValues(bytes);
37
+ const hexString = Array.from(bytes)
38
+ .map(b => b.toString(16).padStart(2, '0'))
39
+ .join('');
40
+ return `sk_${hexString}`;
41
+ }
42
+ /**
43
+ * Hash a secret using SHA-256
44
+ * Returns hex-encoded hash string
45
+ *
46
+ * @param secret - The plaintext secret to hash
47
+ * @returns SHA-256 hash as hex string
48
+ */
49
+ export async function hashSecret(secret) {
50
+ const encoder = new TextEncoder();
51
+ const data = encoder.encode(secret);
52
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
53
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
54
+ const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
55
+ return hashHex;
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
+ }
67
+ // ============ APP MANAGEMENT ============
68
+ /**
69
+ * Create a new app with crypto-random credentials
70
+ *
71
+ * Generates:
72
+ * - app_id: 'app_' + 16 hex chars
73
+ * - app_secret: 'sk_' + 32 hex chars
74
+ *
75
+ * Stores in KV at key `app:{app_id}` with:
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.
79
+ *
80
+ * @param kv - KV namespace for storage
81
+ * @param email - Required owner email address
82
+ * @returns {app_id, app_secret, email, email_verified, verification_required}
83
+ */
84
+ export async function createApp(kv, email) {
85
+ const app_id = generateAppId();
86
+ const app_secret = generateAppSecret();
87
+ const secret_hash = await hashSecret(app_secret);
88
+ // Generate email verification code
89
+ const verificationCode = generateVerificationCode();
90
+ const verificationCodeHash = await hashSecret(verificationCode);
91
+ const config = {
92
+ app_id,
93
+ secret_hash,
94
+ created_at: Date.now(),
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
100
+ };
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
+ ]);
106
+ return {
107
+ app_id,
108
+ app_secret, // ONLY returned at creation time!
109
+ email,
110
+ email_verified: false,
111
+ verification_required: true,
112
+ };
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
+ }
226
+ /**
227
+ * Get app configuration by app_id
228
+ *
229
+ * Returns app config WITHOUT secret_hash for security
230
+ *
231
+ * @param kv - KV namespace
232
+ * @param app_id - The app ID to retrieve
233
+ * @returns App config (without secret_hash) or null if not found
234
+ */
235
+ export async function getApp(kv, app_id) {
236
+ try {
237
+ const data = await kv.get(`app:${app_id}`, 'text');
238
+ if (!data) {
239
+ return null;
240
+ }
241
+ const config = JSON.parse(data);
242
+ // Return config WITHOUT secret_hash (security)
243
+ return {
244
+ app_id: config.app_id,
245
+ created_at: config.created_at,
246
+ rate_limit: config.rate_limit,
247
+ email: config.email,
248
+ email_verified: config.email_verified,
249
+ };
250
+ }
251
+ catch (error) {
252
+ console.error(`Failed to get app ${app_id}:`, error);
253
+ return null;
254
+ }
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
+ }
272
+ /**
273
+ * Validate an app secret against stored hash
274
+ *
275
+ * Uses constant-time comparison to prevent timing attacks
276
+ *
277
+ * @param kv - KV namespace
278
+ * @param app_id - The app ID
279
+ * @param app_secret - The plaintext secret to validate
280
+ * @returns true if valid, false otherwise
281
+ */
282
+ export async function validateAppSecret(kv, app_id, app_secret) {
283
+ try {
284
+ const data = await kv.get(`app:${app_id}`, 'text');
285
+ if (!data) {
286
+ return false;
287
+ }
288
+ const config = JSON.parse(data);
289
+ const providedHash = await hashSecret(app_secret);
290
+ // Constant-time comparison to prevent timing attacks
291
+ // Compare each character to avoid early exit
292
+ if (providedHash.length !== config.secret_hash.length) {
293
+ return false;
294
+ }
295
+ let isValid = true;
296
+ for (let i = 0; i < providedHash.length; i++) {
297
+ if (providedHash[i] !== config.secret_hash[i]) {
298
+ isValid = false;
299
+ }
300
+ }
301
+ return isValid;
302
+ }
303
+ catch (error) {
304
+ console.error(`Failed to validate app secret for ${app_id}:`, error);
305
+ return false;
306
+ }
307
+ }