@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.
@@ -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 for production API access
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
- * JWT payload structure
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
- * Generate a JWT token after successful challenge verification
57
+ * Token generation options
18
58
  */
19
- export declare function generateToken(challengeId: string, solveTimeMs: number, secret: string): Promise<string>;
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
- * Verify a JWT token
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): Promise<{
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;
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,iBAAiB,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAejB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAyB3E;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKrE"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}