@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.
- package/dist/analytics.d.ts +60 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +130 -0
- package/dist/apps.d.ts +159 -0
- package/dist/apps.d.ts.map +1 -0
- package/dist/apps.js +307 -0
- package/dist/auth.d.ts +93 -6
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +251 -9
- package/dist/challenges.d.ts +31 -7
- package/dist/challenges.d.ts.map +1 -1
- package/dist/challenges.js +551 -144
- package/dist/dashboard/api.d.ts +70 -0
- package/dist/dashboard/api.d.ts.map +1 -0
- package/dist/dashboard/api.js +546 -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/layout.d.ts +47 -0
- package/dist/dashboard/layout.d.ts.map +1 -0
- package/dist/dashboard/layout.js +38 -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 +633 -0
- package/dist/email.d.ts +44 -0
- package/dist/email.d.ts.map +1 -0
- package/dist/email.js +119 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +644 -50
- package/dist/rate-limit.d.ts +11 -1
- package/dist/rate-limit.d.ts.map +1 -1
- package/dist/rate-limit.js +13 -2
- package/dist/routes/stream.js +1 -1
- package/dist/static.d.ts +728 -0
- package/dist/static.d.ts.map +1 -0
- package/dist/static.js +818 -0
- 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
|
+
}
|