@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 +1 -1
- package/dist/auth.d.ts +48 -3
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +89 -21
- package/dist/dashboard/docs.d.ts +15 -0
- package/dist/dashboard/docs.d.ts.map +1 -0
- package/dist/dashboard/docs.js +556 -0
- package/dist/dashboard/layout.d.ts.map +1 -1
- package/dist/dashboard/layout.js +1 -1
- package/dist/dashboard/whitepaper.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +82 -13
- package/dist/static.d.ts +52 -2
- package/dist/static.d.ts.map +1 -1
- package/dist/static.js +69 -6
- package/dist/tap-jwks.d.ts +2 -1
- package/dist/tap-jwks.d.ts.map +1 -1
- package/dist/tap-jwks.js +31 -7
- package/package.json +1 -1
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
|
|
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
|
|
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;
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrF,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,iBAAiB,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,gBAAgB,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED
|
|
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
|
-
|
|
20
|
-
|
|
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(
|
|
69
|
+
.setProtectedHeader(protectedHeader)
|
|
42
70
|
.setSubject(challengeId)
|
|
71
|
+
.setIssuer('botcha.ai')
|
|
43
72
|
.setIssuedAt()
|
|
44
73
|
.setExpirationTime('5m') // 5 minutes
|
|
45
|
-
.sign(
|
|
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(
|
|
86
|
+
.setProtectedHeader(protectedHeader)
|
|
58
87
|
.setSubject(challengeId)
|
|
88
|
+
.setIssuer('botcha.ai')
|
|
59
89
|
.setIssuedAt()
|
|
60
90
|
.setExpirationTime('1h') // 1 hour
|
|
61
|
-
.sign(
|
|
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
|
-
|
|
113
|
-
const
|
|
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,
|
|
116
|
-
algorithms
|
|
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(
|
|
244
|
+
.setProtectedHeader(protectedHeaderObj)
|
|
193
245
|
.setSubject(payload.sub || '')
|
|
246
|
+
.setIssuer('botcha.ai')
|
|
194
247
|
.setIssuedAt()
|
|
195
248
|
.setExpirationTime('5m') // 5 minutes
|
|
196
|
-
.sign(
|
|
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
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
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"}
|