@dupecom/botcha-cloudflare 0.18.0 → 0.20.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 +2 -2
- package/dist/apps.d.ts +6 -2
- package/dist/apps.d.ts.map +1 -1
- package/dist/apps.js +6 -2
- package/dist/auth.d.ts +51 -6
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +107 -30
- 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 +3 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +100 -20
- package/dist/static.d.ts +61 -2
- package/dist/static.d.ts.map +1 -1
- package/dist/static.js +86 -21
- 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
|
@@ -11,9 +11,9 @@ Reverse CAPTCHA that verifies AI agents and blocks humans. Running at the edge.
|
|
|
11
11
|
- **Agent Registry** - Persistent agent identities (POST /v1/agents/register)
|
|
12
12
|
- **Email-tied apps** - Email verification, account recovery, secret rotation
|
|
13
13
|
- **Dashboard** - Per-app metrics with agent-first auth (device code flow)
|
|
14
|
-
- **JWT security** -
|
|
14
|
+
- **JWT security** - 1-hr 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/apps.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export type KVNamespace = {
|
|
|
22
22
|
*/
|
|
23
23
|
export interface AppConfig {
|
|
24
24
|
app_id: string;
|
|
25
|
+
name?: string;
|
|
25
26
|
secret_hash: string;
|
|
26
27
|
created_at: number;
|
|
27
28
|
rate_limit: number;
|
|
@@ -35,6 +36,7 @@ export interface AppConfig {
|
|
|
35
36
|
*/
|
|
36
37
|
export interface CreateAppResult {
|
|
37
38
|
app_id: string;
|
|
39
|
+
name?: string;
|
|
38
40
|
app_secret: string;
|
|
39
41
|
email: string;
|
|
40
42
|
email_verified: boolean;
|
|
@@ -45,6 +47,7 @@ export interface CreateAppResult {
|
|
|
45
47
|
*/
|
|
46
48
|
export type PublicAppConfig = {
|
|
47
49
|
app_id: string;
|
|
50
|
+
name?: string;
|
|
48
51
|
created_at: number;
|
|
49
52
|
rate_limit: number;
|
|
50
53
|
email: string;
|
|
@@ -90,9 +93,10 @@ export declare function generateVerificationCode(): string;
|
|
|
90
93
|
*
|
|
91
94
|
* @param kv - KV namespace for storage
|
|
92
95
|
* @param email - Required owner email address
|
|
93
|
-
* @
|
|
96
|
+
* @param name - Optional human-readable label for the app
|
|
97
|
+
* @returns {app_id, name, app_secret, email, email_verified, verification_required}
|
|
94
98
|
*/
|
|
95
|
-
export declare function createApp(kv: KVNamespace, email: string): Promise<CreateAppResult>;
|
|
99
|
+
export declare function createApp(kv: KVNamespace, email: string, name?: string): Promise<CreateAppResult>;
|
|
96
100
|
/**
|
|
97
101
|
* Get the plaintext verification code for an app (internal use only — for sending via email).
|
|
98
102
|
*
|
package/dist/apps.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apps.d.ts","sourceRoot":"","sources":["../src/apps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;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;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,OAAO,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,OAAO,CAAC;IACxB,qBAAqB,EAAE,OAAO,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,OAAO,CAAC;CACzB,CAAC;AAIF;;;;;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;AAED;;GAEG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAMjD;AAID
|
|
1
|
+
{"version":3,"file":"apps.d.ts","sourceRoot":"","sources":["../src/apps.ts"],"names":[],"mappings":"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"}
|
package/dist/apps.js
CHANGED
|
@@ -79,9 +79,10 @@ export function generateVerificationCode() {
|
|
|
79
79
|
*
|
|
80
80
|
* @param kv - KV namespace for storage
|
|
81
81
|
* @param email - Required owner email address
|
|
82
|
-
* @
|
|
82
|
+
* @param name - Optional human-readable label for the app
|
|
83
|
+
* @returns {app_id, name, app_secret, email, email_verified, verification_required}
|
|
83
84
|
*/
|
|
84
|
-
export async function createApp(kv, email) {
|
|
85
|
+
export async function createApp(kv, email, name) {
|
|
85
86
|
const app_id = generateAppId();
|
|
86
87
|
const app_secret = generateAppSecret();
|
|
87
88
|
const secret_hash = await hashSecret(app_secret);
|
|
@@ -90,6 +91,7 @@ export async function createApp(kv, email) {
|
|
|
90
91
|
const verificationCodeHash = await hashSecret(verificationCode);
|
|
91
92
|
const config = {
|
|
92
93
|
app_id,
|
|
94
|
+
...(name && { name }),
|
|
93
95
|
secret_hash,
|
|
94
96
|
created_at: Date.now(),
|
|
95
97
|
rate_limit: 100, // Default: 100 requests/hour
|
|
@@ -105,6 +107,7 @@ export async function createApp(kv, email) {
|
|
|
105
107
|
]);
|
|
106
108
|
return {
|
|
107
109
|
app_id,
|
|
110
|
+
...(name && { name }),
|
|
108
111
|
app_secret, // ONLY returned at creation time!
|
|
109
112
|
email,
|
|
110
113
|
email_verified: false,
|
|
@@ -242,6 +245,7 @@ export async function getApp(kv, app_id) {
|
|
|
242
245
|
// Return config WITHOUT secret_hash (security)
|
|
243
246
|
return {
|
|
244
247
|
app_id: config.app_id,
|
|
248
|
+
...(config.name && { name: config.name }),
|
|
245
249
|
created_at: config.created_at,
|
|
246
250
|
rate_limit: config.rate_limit,
|
|
247
251
|
email: config.email,
|
package/dist/auth.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - JTI (JWT ID) for revocation
|
|
6
6
|
* - Audience claims for API scoping
|
|
7
7
|
* - Client IP binding for additional security
|
|
8
|
-
* - Short-lived access tokens (
|
|
8
|
+
* - Short-lived access tokens (1 hour) with refresh tokens (1 hour)
|
|
9
9
|
* - Token revocation via KV storage
|
|
10
10
|
*/
|
|
11
11
|
/**
|
|
@@ -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
|
-
* Access token:
|
|
68
|
-
* Refresh token: 1 hour, used to get new access tokens
|
|
89
|
+
* Access token: 1 hour, used for API access
|
|
90
|
+
* Refresh token: 1 hour, used to get new access tokens without re-solving challenges
|
|
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,
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/auth.js
CHANGED
|
@@ -5,23 +5,54 @@
|
|
|
5
5
|
* - JTI (JWT ID) for revocation
|
|
6
6
|
* - Audience claims for API scoping
|
|
7
7
|
* - Client IP binding for additional security
|
|
8
|
-
* - Short-lived access tokens (
|
|
8
|
+
* - Short-lived access tokens (1 hour) 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
|
-
* Access token:
|
|
16
|
-
* Refresh token: 1 hour, used to get new access tokens
|
|
29
|
+
* Access token: 1 hour, used for API access
|
|
30
|
+
* Refresh token: 1 hour, used to get new access tokens without re-solving challenges
|
|
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 = {
|
|
43
|
+
alg: 'ES256',
|
|
44
|
+
kid: signingKey.kid || 'botcha-signing-1',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// HS256 symmetric signing (legacy fallback)
|
|
49
|
+
signKey = new TextEncoder().encode(secret);
|
|
50
|
+
protectedHeader = { alg: 'HS256' };
|
|
51
|
+
}
|
|
21
52
|
// Generate unique JTIs for both tokens
|
|
22
53
|
const accessJti = crypto.randomUUID();
|
|
23
54
|
const refreshJti = crypto.randomUUID();
|
|
24
|
-
// Access token:
|
|
55
|
+
// Access token: 1 hour
|
|
25
56
|
const accessTokenPayload = {
|
|
26
57
|
type: 'botcha-verified',
|
|
27
58
|
solveTime: solveTimeMs,
|
|
@@ -38,11 +69,12 @@ export async function generateToken(challengeId, solveTimeMs, secret, env, optio
|
|
|
38
69
|
accessTokenPayload.app_id = options.app_id;
|
|
39
70
|
}
|
|
40
71
|
const accessToken = await new SignJWT(accessTokenPayload)
|
|
41
|
-
.setProtectedHeader(
|
|
72
|
+
.setProtectedHeader(protectedHeader)
|
|
42
73
|
.setSubject(challengeId)
|
|
74
|
+
.setIssuer('botcha.ai')
|
|
43
75
|
.setIssuedAt()
|
|
44
|
-
.setExpirationTime('
|
|
45
|
-
.sign(
|
|
76
|
+
.setExpirationTime('1h') // 1 hour
|
|
77
|
+
.sign(signKey);
|
|
46
78
|
// Refresh token: 1 hour
|
|
47
79
|
const refreshTokenPayload = {
|
|
48
80
|
type: 'botcha-refresh',
|
|
@@ -54,16 +86,20 @@ export async function generateToken(challengeId, solveTimeMs, secret, env, optio
|
|
|
54
86
|
refreshTokenPayload.app_id = options.app_id;
|
|
55
87
|
}
|
|
56
88
|
const refreshToken = await new SignJWT(refreshTokenPayload)
|
|
57
|
-
.setProtectedHeader(
|
|
89
|
+
.setProtectedHeader(protectedHeader)
|
|
58
90
|
.setSubject(challengeId)
|
|
91
|
+
.setIssuer('botcha.ai')
|
|
59
92
|
.setIssuedAt()
|
|
60
93
|
.setExpirationTime('1h') // 1 hour
|
|
61
|
-
.sign(
|
|
94
|
+
.sign(signKey);
|
|
62
95
|
// Store refresh token JTI in KV if env provided (for revocation tracking)
|
|
63
96
|
// Also store aud, client_ip, and app_id so they carry over on refresh
|
|
64
97
|
if (env?.CHALLENGES) {
|
|
65
98
|
try {
|
|
66
|
-
const refreshData = {
|
|
99
|
+
const refreshData = {
|
|
100
|
+
sub: challengeId,
|
|
101
|
+
iat: Date.now(),
|
|
102
|
+
};
|
|
67
103
|
if (options?.aud) {
|
|
68
104
|
refreshData.aud = options.aud;
|
|
69
105
|
}
|
|
@@ -83,7 +119,7 @@ export async function generateToken(challengeId, solveTimeMs, secret, env, optio
|
|
|
83
119
|
}
|
|
84
120
|
return {
|
|
85
121
|
access_token: accessToken,
|
|
86
|
-
expires_in:
|
|
122
|
+
expires_in: 3600, // 1 hour in seconds
|
|
87
123
|
refresh_token: refreshToken,
|
|
88
124
|
refresh_expires_in: 3600, // 1 hour in seconds
|
|
89
125
|
};
|
|
@@ -105,15 +141,26 @@ export async function revokeToken(jti, env) {
|
|
|
105
141
|
/**
|
|
106
142
|
* Refresh an access token using a valid refresh token
|
|
107
143
|
*
|
|
108
|
-
* Verifies the refresh token, checks revocation, and issues a new access token
|
|
144
|
+
* Verifies the refresh token, checks revocation, and issues a new access token.
|
|
145
|
+
* Supports both ES256 (asymmetric) and HS256 (symmetric) refresh tokens.
|
|
109
146
|
*/
|
|
110
|
-
export async function refreshAccessToken(refreshToken, env, secret, options) {
|
|
147
|
+
export async function refreshAccessToken(refreshToken, env, secret, options, signingKey, publicKey) {
|
|
111
148
|
try {
|
|
112
|
-
|
|
113
|
-
const
|
|
149
|
+
// Detect algorithm from token header and verify accordingly
|
|
150
|
+
const header = decodeProtectedHeader(refreshToken);
|
|
151
|
+
let verifyKey;
|
|
152
|
+
let algorithms;
|
|
153
|
+
if (header.alg === 'ES256' && publicKey) {
|
|
154
|
+
verifyKey = (await importJWK(publicKey, 'ES256'));
|
|
155
|
+
algorithms = ['ES256'];
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
verifyKey = new TextEncoder().encode(secret);
|
|
159
|
+
algorithms = ['HS256'];
|
|
160
|
+
}
|
|
114
161
|
// Verify refresh token
|
|
115
|
-
const { payload } = await jwtVerify(refreshToken,
|
|
116
|
-
algorithms
|
|
162
|
+
const { payload } = await jwtVerify(refreshToken, verifyKey, {
|
|
163
|
+
algorithms,
|
|
117
164
|
});
|
|
118
165
|
// Check token type
|
|
119
166
|
if (payload.type !== 'botcha-refresh') {
|
|
@@ -168,6 +215,20 @@ export async function refreshAccessToken(refreshToken, env, secret, options) {
|
|
|
168
215
|
console.error('Failed to verify refresh token in KV:', error);
|
|
169
216
|
}
|
|
170
217
|
}
|
|
218
|
+
// Determine signing algorithm and key for the new access token
|
|
219
|
+
let signKey;
|
|
220
|
+
let protectedHeaderObj;
|
|
221
|
+
if (signingKey) {
|
|
222
|
+
signKey = (await importJWK(signingKey, 'ES256'));
|
|
223
|
+
protectedHeaderObj = {
|
|
224
|
+
alg: 'ES256',
|
|
225
|
+
kid: signingKey.kid || 'botcha-signing-1',
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
signKey = new TextEncoder().encode(secret);
|
|
230
|
+
protectedHeaderObj = { alg: 'HS256' };
|
|
231
|
+
}
|
|
171
232
|
// Generate new access token
|
|
172
233
|
const newAccessJti = crypto.randomUUID();
|
|
173
234
|
const accessTokenPayload = {
|
|
@@ -189,16 +250,17 @@ export async function refreshAccessToken(refreshToken, env, secret, options) {
|
|
|
189
250
|
accessTokenPayload.app_id = effectiveAppId;
|
|
190
251
|
}
|
|
191
252
|
const accessToken = await new SignJWT(accessTokenPayload)
|
|
192
|
-
.setProtectedHeader(
|
|
253
|
+
.setProtectedHeader(protectedHeaderObj)
|
|
193
254
|
.setSubject(payload.sub || '')
|
|
255
|
+
.setIssuer('botcha.ai')
|
|
194
256
|
.setIssuedAt()
|
|
195
|
-
.setExpirationTime('
|
|
196
|
-
.sign(
|
|
257
|
+
.setExpirationTime('1h') // 1 hour
|
|
258
|
+
.sign(signKey);
|
|
197
259
|
return {
|
|
198
260
|
success: true,
|
|
199
261
|
tokens: {
|
|
200
262
|
access_token: accessToken,
|
|
201
|
-
expires_in:
|
|
263
|
+
expires_in: 3600, // 1 hour in seconds
|
|
202
264
|
},
|
|
203
265
|
};
|
|
204
266
|
}
|
|
@@ -212,18 +274,33 @@ export async function refreshAccessToken(refreshToken, env, secret, options) {
|
|
|
212
274
|
/**
|
|
213
275
|
* Verify a JWT token with security checks
|
|
214
276
|
*
|
|
277
|
+
* Supports both ES256 (asymmetric) and HS256 (symmetric) tokens.
|
|
278
|
+
* The algorithm is detected from the token's protected header.
|
|
279
|
+
*
|
|
215
280
|
* Checks:
|
|
216
281
|
* - Token signature and expiry
|
|
217
282
|
* - Revocation status (via JTI)
|
|
218
283
|
* - Audience claim (if provided)
|
|
219
284
|
* - Client IP binding (if provided)
|
|
220
285
|
*/
|
|
221
|
-
export async function verifyToken(token, secret, env, options) {
|
|
286
|
+
export async function verifyToken(token, secret, env, options, publicKey) {
|
|
222
287
|
try {
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
288
|
+
// Detect algorithm from the token header
|
|
289
|
+
const header = decodeProtectedHeader(token);
|
|
290
|
+
let verifyKey;
|
|
291
|
+
let algorithms;
|
|
292
|
+
if (header.alg === 'ES256' && publicKey) {
|
|
293
|
+
// ES256 asymmetric verification
|
|
294
|
+
verifyKey = (await importJWK(publicKey, 'ES256'));
|
|
295
|
+
algorithms = ['ES256'];
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
// HS256 symmetric verification (legacy/fallback)
|
|
299
|
+
verifyKey = new TextEncoder().encode(secret);
|
|
300
|
+
algorithms = ['HS256'];
|
|
301
|
+
}
|
|
302
|
+
const { payload } = await jwtVerify(token, verifyKey, {
|
|
303
|
+
algorithms,
|
|
227
304
|
});
|
|
228
305
|
// Check token type (must be access token, not refresh token)
|
|
229
306
|
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"}
|