@dupecom/botcha-cloudflare 0.18.0 → 0.19.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/README.md CHANGED
@@ -13,7 +13,7 @@ Reverse CAPTCHA that verifies AI agents and blocks humans. Running at the edge.
13
13
  - **Dashboard** - Per-app metrics with agent-first auth (device code flow)
14
14
  - **JWT security** - 5-min access tokens, refresh tokens, audience claims, IP binding, revocation
15
15
  - **Multi-tenant** - Per-app isolation, scoped tokens, rate limiting
16
- - **Server-side SDKs** - @botcha/verify (TS) + botcha-verify (Python)
16
+ - **Server-side SDKs** - @dupecom/botcha-verify (TS) + botcha-verify (Python)
17
17
 
18
18
  ## Features
19
19
 
package/dist/auth.d.ts CHANGED
@@ -61,15 +61,40 @@ export interface TokenGenerationOptions {
61
61
  clientIp?: string;
62
62
  app_id?: string;
63
63
  }
64
+ /**
65
+ * ES256 private key in JWK format
66
+ * { kty: "EC", crv: "P-256", x: "...", y: "...", d: "..." }
67
+ */
68
+ export interface ES256SigningKeyJWK {
69
+ kty: string;
70
+ crv: string;
71
+ x: string;
72
+ y: string;
73
+ d: string;
74
+ kid?: string;
75
+ }
76
+ /**
77
+ * Derive the public key JWK from an ES256 private key JWK.
78
+ * Strips the `d` (private) parameter and sets standard fields.
79
+ * Used by the JWKS endpoint to publish the signing key.
80
+ */
81
+ export declare function getSigningPublicKeyJWK(privateKeyJwk: ES256SigningKeyJWK): Omit<ES256SigningKeyJWK, 'd'> & {
82
+ kid: string;
83
+ use: string;
84
+ alg: string;
85
+ };
64
86
  /**
65
87
  * Generate JWT tokens (access + refresh) after successful challenge verification
66
88
  *
67
89
  * Access token: 5 minutes, used for API access
68
90
  * Refresh token: 1 hour, used to get new access tokens
91
+ *
92
+ * When signingKey (ES256 JWK) is provided, tokens are signed with ES256.
93
+ * Otherwise falls back to HS256 with the shared secret (backward compat).
69
94
  */
70
95
  export declare function generateToken(challengeId: string, solveTimeMs: number, secret: string, env?: {
71
96
  CHALLENGES: KVNamespace;
72
- }, options?: TokenGenerationOptions): Promise<TokenCreationResult>;
97
+ }, options?: TokenGenerationOptions, signingKey?: ES256SigningKeyJWK): Promise<TokenCreationResult>;
73
98
  /**
74
99
  * Revoke a token by its JTI
75
100
  *
@@ -81,11 +106,20 @@ export declare function revokeToken(jti: string, env: {
81
106
  /**
82
107
  * Refresh an access token using a valid refresh token
83
108
  *
84
- * Verifies the refresh token, checks revocation, and issues a new access token
109
+ * Verifies the refresh token, checks revocation, and issues a new access token.
110
+ * Supports both ES256 (asymmetric) and HS256 (symmetric) refresh tokens.
85
111
  */
86
112
  export declare function refreshAccessToken(refreshToken: string, env: {
87
113
  CHALLENGES: KVNamespace;
88
- }, secret: string, options?: TokenGenerationOptions): Promise<{
114
+ }, secret: string, options?: TokenGenerationOptions, signingKey?: ES256SigningKeyJWK, publicKey?: {
115
+ kty: string;
116
+ crv: string;
117
+ x: string;
118
+ y: string;
119
+ kid?: string;
120
+ use?: string;
121
+ alg?: string;
122
+ }): Promise<{
89
123
  success: boolean;
90
124
  tokens?: Omit<TokenCreationResult, 'refresh_token' | 'refresh_expires_in'> & {
91
125
  access_token: string;
@@ -96,6 +130,9 @@ export declare function refreshAccessToken(refreshToken: string, env: {
96
130
  /**
97
131
  * Verify a JWT token with security checks
98
132
  *
133
+ * Supports both ES256 (asymmetric) and HS256 (symmetric) tokens.
134
+ * The algorithm is detected from the token's protected header.
135
+ *
99
136
  * Checks:
100
137
  * - Token signature and expiry
101
138
  * - Revocation status (via JTI)
@@ -107,6 +144,14 @@ export declare function verifyToken(token: string, secret: string, env?: {
107
144
  }, options?: {
108
145
  requiredAud?: string;
109
146
  clientIp?: string;
147
+ }, publicKey?: {
148
+ kty: string;
149
+ crv: string;
150
+ x: string;
151
+ y: string;
152
+ kid?: string;
153
+ use?: string;
154
+ alg?: string;
110
155
  }): Promise<{
111
156
  valid: boolean;
112
157
  payload?: BotchaTokenPayload;
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
package/dist/auth.js CHANGED
@@ -8,16 +8,44 @@
8
8
  * - Short-lived access tokens (5 min) with refresh tokens (1 hour)
9
9
  * - Token revocation via KV storage
10
10
  */
11
- import { SignJWT, jwtVerify } from 'jose';
11
+ import { SignJWT, jwtVerify, importJWK, decodeProtectedHeader } from 'jose';
12
+ /**
13
+ * Derive the public key JWK from an ES256 private key JWK.
14
+ * Strips the `d` (private) parameter and sets standard fields.
15
+ * Used by the JWKS endpoint to publish the signing key.
16
+ */
17
+ export function getSigningPublicKeyJWK(privateKeyJwk) {
18
+ const { d: _d, ...publicKey } = privateKeyJwk;
19
+ return {
20
+ ...publicKey,
21
+ kid: privateKeyJwk.kid || 'botcha-signing-1',
22
+ use: 'sig',
23
+ alg: 'ES256',
24
+ };
25
+ }
12
26
  /**
13
27
  * Generate JWT tokens (access + refresh) after successful challenge verification
14
28
  *
15
29
  * Access token: 5 minutes, used for API access
16
30
  * Refresh token: 1 hour, used to get new access tokens
31
+ *
32
+ * When signingKey (ES256 JWK) is provided, tokens are signed with ES256.
33
+ * Otherwise falls back to HS256 with the shared secret (backward compat).
17
34
  */
18
- export async function generateToken(challengeId, solveTimeMs, secret, env, options) {
19
- const encoder = new TextEncoder();
20
- const secretKey = encoder.encode(secret);
35
+ export async function generateToken(challengeId, solveTimeMs, secret, env, options, signingKey) {
36
+ // Determine signing algorithm and key
37
+ let signKey;
38
+ let protectedHeader;
39
+ if (signingKey) {
40
+ // ES256 asymmetric signing
41
+ signKey = await importJWK(signingKey, 'ES256');
42
+ protectedHeader = { alg: 'ES256', kid: signingKey.kid || 'botcha-signing-1' };
43
+ }
44
+ else {
45
+ // HS256 symmetric signing (legacy fallback)
46
+ signKey = new TextEncoder().encode(secret);
47
+ protectedHeader = { alg: 'HS256' };
48
+ }
21
49
  // Generate unique JTIs for both tokens
22
50
  const accessJti = crypto.randomUUID();
23
51
  const refreshJti = crypto.randomUUID();
@@ -38,11 +66,12 @@ export async function generateToken(challengeId, solveTimeMs, secret, env, optio
38
66
  accessTokenPayload.app_id = options.app_id;
39
67
  }
40
68
  const accessToken = await new SignJWT(accessTokenPayload)
41
- .setProtectedHeader({ alg: 'HS256' })
69
+ .setProtectedHeader(protectedHeader)
42
70
  .setSubject(challengeId)
71
+ .setIssuer('botcha.ai')
43
72
  .setIssuedAt()
44
73
  .setExpirationTime('5m') // 5 minutes
45
- .sign(secretKey);
74
+ .sign(signKey);
46
75
  // Refresh token: 1 hour
47
76
  const refreshTokenPayload = {
48
77
  type: 'botcha-refresh',
@@ -54,11 +83,12 @@ export async function generateToken(challengeId, solveTimeMs, secret, env, optio
54
83
  refreshTokenPayload.app_id = options.app_id;
55
84
  }
56
85
  const refreshToken = await new SignJWT(refreshTokenPayload)
57
- .setProtectedHeader({ alg: 'HS256' })
86
+ .setProtectedHeader(protectedHeader)
58
87
  .setSubject(challengeId)
88
+ .setIssuer('botcha.ai')
59
89
  .setIssuedAt()
60
90
  .setExpirationTime('1h') // 1 hour
61
- .sign(secretKey);
91
+ .sign(signKey);
62
92
  // Store refresh token JTI in KV if env provided (for revocation tracking)
63
93
  // Also store aud, client_ip, and app_id so they carry over on refresh
64
94
  if (env?.CHALLENGES) {
@@ -105,15 +135,26 @@ export async function revokeToken(jti, env) {
105
135
  /**
106
136
  * Refresh an access token using a valid refresh token
107
137
  *
108
- * Verifies the refresh token, checks revocation, and issues a new access token
138
+ * Verifies the refresh token, checks revocation, and issues a new access token.
139
+ * Supports both ES256 (asymmetric) and HS256 (symmetric) refresh tokens.
109
140
  */
110
- export async function refreshAccessToken(refreshToken, env, secret, options) {
141
+ export async function refreshAccessToken(refreshToken, env, secret, options, signingKey, publicKey) {
111
142
  try {
112
- const encoder = new TextEncoder();
113
- const secretKey = encoder.encode(secret);
143
+ // Detect algorithm from token header and verify accordingly
144
+ const header = decodeProtectedHeader(refreshToken);
145
+ let verifyKey;
146
+ let algorithms;
147
+ if (header.alg === 'ES256' && publicKey) {
148
+ verifyKey = await importJWK(publicKey, 'ES256');
149
+ algorithms = ['ES256'];
150
+ }
151
+ else {
152
+ verifyKey = new TextEncoder().encode(secret);
153
+ algorithms = ['HS256'];
154
+ }
114
155
  // Verify refresh token
115
- const { payload } = await jwtVerify(refreshToken, secretKey, {
116
- algorithms: ['HS256'],
156
+ const { payload } = await jwtVerify(refreshToken, verifyKey, {
157
+ algorithms,
117
158
  });
118
159
  // Check token type
119
160
  if (payload.type !== 'botcha-refresh') {
@@ -168,6 +209,17 @@ export async function refreshAccessToken(refreshToken, env, secret, options) {
168
209
  console.error('Failed to verify refresh token in KV:', error);
169
210
  }
170
211
  }
212
+ // Determine signing algorithm and key for the new access token
213
+ let signKey;
214
+ let protectedHeaderObj;
215
+ if (signingKey) {
216
+ signKey = await importJWK(signingKey, 'ES256');
217
+ protectedHeaderObj = { alg: 'ES256', kid: signingKey.kid || 'botcha-signing-1' };
218
+ }
219
+ else {
220
+ signKey = new TextEncoder().encode(secret);
221
+ protectedHeaderObj = { alg: 'HS256' };
222
+ }
171
223
  // Generate new access token
172
224
  const newAccessJti = crypto.randomUUID();
173
225
  const accessTokenPayload = {
@@ -189,11 +241,12 @@ export async function refreshAccessToken(refreshToken, env, secret, options) {
189
241
  accessTokenPayload.app_id = effectiveAppId;
190
242
  }
191
243
  const accessToken = await new SignJWT(accessTokenPayload)
192
- .setProtectedHeader({ alg: 'HS256' })
244
+ .setProtectedHeader(protectedHeaderObj)
193
245
  .setSubject(payload.sub || '')
246
+ .setIssuer('botcha.ai')
194
247
  .setIssuedAt()
195
248
  .setExpirationTime('5m') // 5 minutes
196
- .sign(secretKey);
249
+ .sign(signKey);
197
250
  return {
198
251
  success: true,
199
252
  tokens: {
@@ -212,18 +265,33 @@ export async function refreshAccessToken(refreshToken, env, secret, options) {
212
265
  /**
213
266
  * Verify a JWT token with security checks
214
267
  *
268
+ * Supports both ES256 (asymmetric) and HS256 (symmetric) tokens.
269
+ * The algorithm is detected from the token's protected header.
270
+ *
215
271
  * Checks:
216
272
  * - Token signature and expiry
217
273
  * - Revocation status (via JTI)
218
274
  * - Audience claim (if provided)
219
275
  * - Client IP binding (if provided)
220
276
  */
221
- export async function verifyToken(token, secret, env, options) {
277
+ export async function verifyToken(token, secret, env, options, publicKey) {
222
278
  try {
223
- const encoder = new TextEncoder();
224
- const secretKey = encoder.encode(secret);
225
- const { payload } = await jwtVerify(token, secretKey, {
226
- algorithms: ['HS256'],
279
+ // Detect algorithm from the token header
280
+ const header = decodeProtectedHeader(token);
281
+ let verifyKey;
282
+ let algorithms;
283
+ if (header.alg === 'ES256' && publicKey) {
284
+ // ES256 asymmetric verification
285
+ verifyKey = await importJWK(publicKey, 'ES256');
286
+ algorithms = ['ES256'];
287
+ }
288
+ else {
289
+ // HS256 symmetric verification (legacy/fallback)
290
+ verifyKey = new TextEncoder().encode(secret);
291
+ algorithms = ['HS256'];
292
+ }
293
+ const { payload } = await jwtVerify(token, verifyKey, {
294
+ algorithms,
227
295
  });
228
296
  // Check token type (must be access token, not refresh token)
229
297
  if (payload.type !== 'botcha-verified') {
@@ -0,0 +1,15 @@
1
+ /**
2
+ * BOTCHA API Documentation Page
3
+ *
4
+ * Public API docs at /docs — no authentication required.
5
+ * Renders all endpoints grouped by category with request/response
6
+ * examples, install instructions, and quick-start guides.
7
+ *
8
+ * Follows the ShowcasePage pattern: self-contained JSX with own
9
+ * HTML shell, inline CSS, and the shared DASHBOARD_CSS base.
10
+ */
11
+ import type { FC } from 'hono/jsx';
12
+ export declare const DocsPage: FC<{
13
+ version: string;
14
+ }>;
15
+ //# sourceMappingURL=docs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/dashboard/docs.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,UAAU,CAAC;AAsbnC,eAAO,MAAM,QAAQ,EAAE,EAAE,CAAC;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAqpB5C,CAAC"}