@dupecom/botcha-cloudflare 0.3.3 → 0.9.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 +93 -0
- package/dist/apps.d.ts.map +1 -0
- package/dist/apps.js +152 -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 +550 -144
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +412 -46
- 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/static.d.ts +517 -0
- package/dist/static.d.ts.map +1 -0
- package/dist/static.js +635 -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,93 @@
|
|
|
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
|
+
*/
|
|
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
|
+
* App configuration stored in KV
|
|
19
|
+
*/
|
|
20
|
+
export interface AppConfig {
|
|
21
|
+
app_id: string;
|
|
22
|
+
secret_hash: string;
|
|
23
|
+
created_at: number;
|
|
24
|
+
rate_limit: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Result of app creation (includes plaintext secret - only shown once!)
|
|
28
|
+
*/
|
|
29
|
+
export interface CreateAppResult {
|
|
30
|
+
app_id: string;
|
|
31
|
+
app_secret: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Generate a crypto-random app ID
|
|
35
|
+
* Format: 'app_' + 16 hex chars
|
|
36
|
+
*
|
|
37
|
+
* Example: app_a1b2c3d4e5f6a7b8
|
|
38
|
+
*/
|
|
39
|
+
export declare function generateAppId(): string;
|
|
40
|
+
/**
|
|
41
|
+
* Generate a crypto-random app secret
|
|
42
|
+
* Format: 'sk_' + 32 hex chars
|
|
43
|
+
*
|
|
44
|
+
* Example: sk_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
|
|
45
|
+
*/
|
|
46
|
+
export declare function generateAppSecret(): string;
|
|
47
|
+
/**
|
|
48
|
+
* Hash a secret using SHA-256
|
|
49
|
+
* Returns hex-encoded hash string
|
|
50
|
+
*
|
|
51
|
+
* @param secret - The plaintext secret to hash
|
|
52
|
+
* @returns SHA-256 hash as hex string
|
|
53
|
+
*/
|
|
54
|
+
export declare function hashSecret(secret: string): Promise<string>;
|
|
55
|
+
/**
|
|
56
|
+
* Create a new app with crypto-random credentials
|
|
57
|
+
*
|
|
58
|
+
* Generates:
|
|
59
|
+
* - app_id: 'app_' + 16 hex chars
|
|
60
|
+
* - app_secret: 'sk_' + 32 hex chars
|
|
61
|
+
*
|
|
62
|
+
* 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)
|
|
67
|
+
*
|
|
68
|
+
* @param kv - KV namespace for storage
|
|
69
|
+
* @returns {app_id, app_secret} - SECRET ONLY RETURNED ONCE!
|
|
70
|
+
*/
|
|
71
|
+
export declare function createApp(kv: KVNamespace): Promise<CreateAppResult>;
|
|
72
|
+
/**
|
|
73
|
+
* Get app configuration by app_id
|
|
74
|
+
*
|
|
75
|
+
* Returns app config WITHOUT secret_hash for security
|
|
76
|
+
*
|
|
77
|
+
* @param kv - KV namespace
|
|
78
|
+
* @param app_id - The app ID to retrieve
|
|
79
|
+
* @returns App config (without secret_hash) or null if not found
|
|
80
|
+
*/
|
|
81
|
+
export declare function getApp(kv: KVNamespace, app_id: string): Promise<Omit<AppConfig, 'secret_hash'> | null>;
|
|
82
|
+
/**
|
|
83
|
+
* Validate an app secret against stored hash
|
|
84
|
+
*
|
|
85
|
+
* Uses constant-time comparison to prevent timing attacks
|
|
86
|
+
*
|
|
87
|
+
* @param kv - KV namespace
|
|
88
|
+
* @param app_id - The app ID
|
|
89
|
+
* @param app_secret - The plaintext secret to validate
|
|
90
|
+
* @returns true if valid, false otherwise
|
|
91
|
+
*/
|
|
92
|
+
export declare function validateAppSecret(kv: KVNamespace, app_id: string, app_secret: string): Promise<boolean>;
|
|
93
|
+
//# sourceMappingURL=apps.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/apps.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
*/
|
|
10
|
+
// ============ CRYPTO UTILITIES ============
|
|
11
|
+
/**
|
|
12
|
+
* Generate a crypto-random app ID
|
|
13
|
+
* Format: 'app_' + 16 hex chars
|
|
14
|
+
*
|
|
15
|
+
* Example: app_a1b2c3d4e5f6a7b8
|
|
16
|
+
*/
|
|
17
|
+
export function generateAppId() {
|
|
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 `app_${hexString}`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Generate a crypto-random app secret
|
|
27
|
+
* Format: 'sk_' + 32 hex chars
|
|
28
|
+
*
|
|
29
|
+
* Example: sk_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
|
|
30
|
+
*/
|
|
31
|
+
export function generateAppSecret() {
|
|
32
|
+
const bytes = new Uint8Array(16); // 16 bytes = 32 hex chars
|
|
33
|
+
crypto.getRandomValues(bytes);
|
|
34
|
+
const hexString = Array.from(bytes)
|
|
35
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
36
|
+
.join('');
|
|
37
|
+
return `sk_${hexString}`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Hash a secret using SHA-256
|
|
41
|
+
* Returns hex-encoded hash string
|
|
42
|
+
*
|
|
43
|
+
* @param secret - The plaintext secret to hash
|
|
44
|
+
* @returns SHA-256 hash as hex string
|
|
45
|
+
*/
|
|
46
|
+
export async function hashSecret(secret) {
|
|
47
|
+
const encoder = new TextEncoder();
|
|
48
|
+
const data = encoder.encode(secret);
|
|
49
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
50
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
51
|
+
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
52
|
+
return hashHex;
|
|
53
|
+
}
|
|
54
|
+
// ============ APP MANAGEMENT ============
|
|
55
|
+
/**
|
|
56
|
+
* Create a new app with crypto-random credentials
|
|
57
|
+
*
|
|
58
|
+
* Generates:
|
|
59
|
+
* - app_id: 'app_' + 16 hex chars
|
|
60
|
+
* - app_secret: 'sk_' + 32 hex chars
|
|
61
|
+
*
|
|
62
|
+
* 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)
|
|
67
|
+
*
|
|
68
|
+
* @param kv - KV namespace for storage
|
|
69
|
+
* @returns {app_id, app_secret} - SECRET ONLY RETURNED ONCE!
|
|
70
|
+
*/
|
|
71
|
+
export async function createApp(kv) {
|
|
72
|
+
const app_id = generateAppId();
|
|
73
|
+
const app_secret = generateAppSecret();
|
|
74
|
+
const secret_hash = await hashSecret(app_secret);
|
|
75
|
+
const config = {
|
|
76
|
+
app_id,
|
|
77
|
+
secret_hash,
|
|
78
|
+
created_at: Date.now(),
|
|
79
|
+
rate_limit: 100, // Default: 100 requests/hour
|
|
80
|
+
};
|
|
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));
|
|
84
|
+
return {
|
|
85
|
+
app_id,
|
|
86
|
+
app_secret, // ONLY returned at creation time!
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Get app configuration by app_id
|
|
91
|
+
*
|
|
92
|
+
* Returns app config WITHOUT secret_hash for security
|
|
93
|
+
*
|
|
94
|
+
* @param kv - KV namespace
|
|
95
|
+
* @param app_id - The app ID to retrieve
|
|
96
|
+
* @returns App config (without secret_hash) or null if not found
|
|
97
|
+
*/
|
|
98
|
+
export async function getApp(kv, app_id) {
|
|
99
|
+
try {
|
|
100
|
+
const data = await kv.get(`app:${app_id}`, 'text');
|
|
101
|
+
if (!data) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const config = JSON.parse(data);
|
|
105
|
+
// Return config WITHOUT secret_hash (security)
|
|
106
|
+
return {
|
|
107
|
+
app_id: config.app_id,
|
|
108
|
+
created_at: config.created_at,
|
|
109
|
+
rate_limit: config.rate_limit,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
console.error(`Failed to get app ${app_id}:`, error);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Validate an app secret against stored hash
|
|
119
|
+
*
|
|
120
|
+
* Uses constant-time comparison to prevent timing attacks
|
|
121
|
+
*
|
|
122
|
+
* @param kv - KV namespace
|
|
123
|
+
* @param app_id - The app ID
|
|
124
|
+
* @param app_secret - The plaintext secret to validate
|
|
125
|
+
* @returns true if valid, false otherwise
|
|
126
|
+
*/
|
|
127
|
+
export async function validateAppSecret(kv, app_id, app_secret) {
|
|
128
|
+
try {
|
|
129
|
+
const data = await kv.get(`app:${app_id}`, 'text');
|
|
130
|
+
if (!data) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
const config = JSON.parse(data);
|
|
134
|
+
const providedHash = await hashSecret(app_secret);
|
|
135
|
+
// Constant-time comparison to prevent timing attacks
|
|
136
|
+
// Compare each character to avoid early exit
|
|
137
|
+
if (providedHash.length !== config.secret_hash.length) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
let isValid = true;
|
|
141
|
+
for (let i = 0; i < providedHash.length; i++) {
|
|
142
|
+
if (providedHash[i] !== config.secret_hash[i]) {
|
|
143
|
+
isValid = false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return isValid;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
console.error(`Failed to validate app secret for ${app_id}:`, error);
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,26 +1,113 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* BOTCHA Authentication & JWT Token Management
|
|
3
3
|
*
|
|
4
|
-
* Token-based auth flow
|
|
4
|
+
* Token-based auth flow with security features:
|
|
5
|
+
* - JTI (JWT ID) for revocation
|
|
6
|
+
* - Audience claims for API scoping
|
|
7
|
+
* - Client IP binding for additional security
|
|
8
|
+
* - Short-lived access tokens (5 min) with refresh tokens (1 hour)
|
|
9
|
+
* - Token revocation via KV storage
|
|
5
10
|
*/
|
|
6
11
|
/**
|
|
7
|
-
*
|
|
12
|
+
* KV namespace interface (Cloudflare Workers)
|
|
13
|
+
*/
|
|
14
|
+
export interface KVNamespace {
|
|
15
|
+
get(key: string): Promise<string | null>;
|
|
16
|
+
put(key: string, value: string, options?: {
|
|
17
|
+
expirationTtl?: number;
|
|
18
|
+
}): Promise<void>;
|
|
19
|
+
delete(key: string): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* JWT payload structure for access tokens
|
|
8
23
|
*/
|
|
9
24
|
export interface BotchaTokenPayload {
|
|
10
25
|
sub: string;
|
|
11
26
|
iat: number;
|
|
12
27
|
exp: number;
|
|
28
|
+
jti: string;
|
|
13
29
|
type: 'botcha-verified';
|
|
14
30
|
solveTime: number;
|
|
31
|
+
aud?: string;
|
|
32
|
+
client_ip?: string;
|
|
33
|
+
app_id?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* JWT payload structure for refresh tokens
|
|
37
|
+
*/
|
|
38
|
+
export interface BotchaRefreshTokenPayload {
|
|
39
|
+
sub: string;
|
|
40
|
+
iat: number;
|
|
41
|
+
exp: number;
|
|
42
|
+
jti: string;
|
|
43
|
+
type: 'botcha-refresh';
|
|
44
|
+
solveTime: number;
|
|
45
|
+
app_id?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Token creation result
|
|
49
|
+
*/
|
|
50
|
+
export interface TokenCreationResult {
|
|
51
|
+
access_token: string;
|
|
52
|
+
expires_in: number;
|
|
53
|
+
refresh_token: string;
|
|
54
|
+
refresh_expires_in: number;
|
|
15
55
|
}
|
|
16
56
|
/**
|
|
17
|
-
*
|
|
57
|
+
* Token generation options
|
|
18
58
|
*/
|
|
19
|
-
export
|
|
59
|
+
export interface TokenGenerationOptions {
|
|
60
|
+
aud?: string;
|
|
61
|
+
clientIp?: string;
|
|
62
|
+
app_id?: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Generate JWT tokens (access + refresh) after successful challenge verification
|
|
66
|
+
*
|
|
67
|
+
* Access token: 5 minutes, used for API access
|
|
68
|
+
* Refresh token: 1 hour, used to get new access tokens
|
|
69
|
+
*/
|
|
70
|
+
export declare function generateToken(challengeId: string, solveTimeMs: number, secret: string, env?: {
|
|
71
|
+
CHALLENGES: KVNamespace;
|
|
72
|
+
}, options?: TokenGenerationOptions): Promise<TokenCreationResult>;
|
|
73
|
+
/**
|
|
74
|
+
* Revoke a token by its JTI
|
|
75
|
+
*
|
|
76
|
+
* Stores the JTI in the revocation list (KV) with 1 hour TTL
|
|
77
|
+
*/
|
|
78
|
+
export declare function revokeToken(jti: string, env: {
|
|
79
|
+
CHALLENGES: KVNamespace;
|
|
80
|
+
}): Promise<void>;
|
|
20
81
|
/**
|
|
21
|
-
*
|
|
82
|
+
* Refresh an access token using a valid refresh token
|
|
83
|
+
*
|
|
84
|
+
* Verifies the refresh token, checks revocation, and issues a new access token
|
|
85
|
+
*/
|
|
86
|
+
export declare function refreshAccessToken(refreshToken: string, env: {
|
|
87
|
+
CHALLENGES: KVNamespace;
|
|
88
|
+
}, secret: string, options?: TokenGenerationOptions): Promise<{
|
|
89
|
+
success: boolean;
|
|
90
|
+
tokens?: Omit<TokenCreationResult, 'refresh_token' | 'refresh_expires_in'> & {
|
|
91
|
+
access_token: string;
|
|
92
|
+
expires_in: number;
|
|
93
|
+
};
|
|
94
|
+
error?: string;
|
|
95
|
+
}>;
|
|
96
|
+
/**
|
|
97
|
+
* Verify a JWT token with security checks
|
|
98
|
+
*
|
|
99
|
+
* Checks:
|
|
100
|
+
* - Token signature and expiry
|
|
101
|
+
* - Revocation status (via JTI)
|
|
102
|
+
* - Audience claim (if provided)
|
|
103
|
+
* - Client IP binding (if provided)
|
|
22
104
|
*/
|
|
23
|
-
export declare function verifyToken(token: string, secret: string
|
|
105
|
+
export declare function verifyToken(token: string, secret: string, env?: {
|
|
106
|
+
CHALLENGES: KVNamespace;
|
|
107
|
+
}, options?: {
|
|
108
|
+
requiredAud?: string;
|
|
109
|
+
clientIp?: string;
|
|
110
|
+
}): Promise<{
|
|
24
111
|
valid: boolean;
|
|
25
112
|
payload?: BotchaTokenPayload;
|
|
26
113
|
error?: string;
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|