@dupecom/botcha-cloudflare 0.20.2 → 0.23.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 +74 -9
- package/dist/agent-auth.d.ts +129 -0
- package/dist/agent-auth.d.ts.map +1 -0
- package/dist/agent-auth.js +210 -0
- package/dist/agents.d.ts +10 -0
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +51 -1
- package/dist/app-gate.d.ts +6 -0
- package/dist/app-gate.d.ts.map +1 -0
- package/dist/app-gate.js +69 -0
- package/dist/apps.d.ts +13 -4
- package/dist/apps.d.ts.map +1 -1
- package/dist/apps.js +30 -4
- package/dist/dashboard/account.d.ts +63 -0
- package/dist/dashboard/account.d.ts.map +1 -0
- package/dist/dashboard/account.js +488 -0
- package/dist/dashboard/api.js +15 -68
- package/dist/dashboard/auth.d.ts.map +1 -1
- package/dist/dashboard/auth.js +14 -14
- package/dist/dashboard/docs.d.ts.map +1 -1
- package/dist/dashboard/docs.js +146 -3
- package/dist/dashboard/layout.d.ts.map +1 -1
- package/dist/dashboard/layout.js +2 -2
- package/dist/dashboard/mcp-setup.d.ts +15 -0
- package/dist/dashboard/mcp-setup.d.ts.map +1 -0
- package/dist/dashboard/mcp-setup.js +391 -0
- package/dist/dashboard/showcase.d.ts +6 -10
- package/dist/dashboard/showcase.d.ts.map +1 -1
- package/dist/dashboard/showcase.js +67 -991
- package/dist/dashboard/whitepaper.d.ts.map +1 -1
- package/dist/dashboard/whitepaper.js +42 -4
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +660 -83
- package/dist/mcp.d.ts +20 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +1290 -0
- package/dist/oauth-agent.d.ts +130 -0
- package/dist/oauth-agent.d.ts.map +1 -0
- package/dist/oauth-agent.js +194 -0
- package/dist/static.d.ts +781 -5
- package/dist/static.d.ts.map +1 -1
- package/dist/static.js +790 -111
- package/dist/tap-a2a-routes.d.ts +355 -0
- package/dist/tap-a2a-routes.d.ts.map +1 -0
- package/dist/tap-a2a-routes.js +475 -0
- package/dist/tap-a2a.d.ts +199 -0
- package/dist/tap-a2a.d.ts.map +1 -0
- package/dist/tap-a2a.js +502 -0
- package/dist/tap-agents.d.ts +15 -0
- package/dist/tap-agents.d.ts.map +1 -1
- package/dist/tap-agents.js +31 -1
- package/dist/tap-ans-routes.d.ts +302 -0
- package/dist/tap-ans-routes.d.ts.map +1 -0
- package/dist/tap-ans-routes.js +535 -0
- package/dist/tap-ans.d.ts +241 -0
- package/dist/tap-ans.d.ts.map +1 -0
- package/dist/tap-ans.js +481 -0
- package/dist/tap-delegation-routes.d.ts.map +1 -1
- package/dist/tap-delegation-routes.js +11 -0
- package/dist/tap-did.d.ts +140 -0
- package/dist/tap-did.d.ts.map +1 -0
- package/dist/tap-did.js +262 -0
- package/dist/tap-oidca-routes.d.ts +383 -0
- package/dist/tap-oidca-routes.d.ts.map +1 -0
- package/dist/tap-oidca-routes.js +597 -0
- package/dist/tap-oidca.d.ts +288 -0
- package/dist/tap-oidca.d.ts.map +1 -0
- package/dist/tap-oidca.js +461 -0
- package/dist/tap-routes.d.ts +24 -8
- package/dist/tap-routes.d.ts.map +1 -1
- package/dist/tap-routes.js +169 -23
- package/dist/tap-vc-routes.d.ts +358 -0
- package/dist/tap-vc-routes.d.ts.map +1 -0
- package/dist/tap-vc-routes.js +367 -0
- package/dist/tap-vc.d.ts +125 -0
- package/dist/tap-vc.d.ts.map +1 -0
- package/dist/tap-vc.js +245 -0
- package/dist/tap-x402-routes.d.ts +89 -0
- package/dist/tap-x402-routes.d.ts.map +1 -0
- package/dist/tap-x402-routes.js +579 -0
- package/dist/tap-x402.d.ts +222 -0
- package/dist/tap-x402.d.ts.map +1 -0
- package/dist/tap-x402.js +546 -0
- package/dist/webhooks.d.ts +99 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +642 -0
- package/package.json +3 -1
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OIDC-A Attestation API Routes
|
|
3
|
+
*
|
|
4
|
+
* Routes:
|
|
5
|
+
* POST /v1/attestation/eat — Issue EAT token (RFC 9334)
|
|
6
|
+
* POST /v1/attestation/oidc-agent-claims — Issue OIDC-A claims block
|
|
7
|
+
* GET /.well-known/oauth-authorization-server — OAuth AS metadata (RFC 8414)
|
|
8
|
+
* POST /v1/auth/agent-grant — Agent Authorization Grant
|
|
9
|
+
* GET /v1/auth/agent-grant/:id/status — Poll HITL grant status
|
|
10
|
+
* POST /v1/auth/agent-grant/:id/resolve — Approve/deny HITL grant (admin)
|
|
11
|
+
* GET /v1/oidc/userinfo — OIDC-A UserInfo endpoint
|
|
12
|
+
*/
|
|
13
|
+
import { extractBearerToken, verifyToken, getSigningPublicKeyJWK, } from './auth.js';
|
|
14
|
+
import { issueEAT, buildOIDCAgentClaims, issueAgentGrant, buildOAuthASMetadata, verifyEAT, getGrantStatus, resolveGrant, BOTCHA_AGENT_CAPABILITIES, BOTCHA_EAT_PROFILE, } from './tap-oidca.js';
|
|
15
|
+
// ============ HELPERS ============
|
|
16
|
+
function getSigningKeyFromEnv(env) {
|
|
17
|
+
const raw = env?.JWT_SIGNING_KEY;
|
|
18
|
+
if (!raw)
|
|
19
|
+
return undefined;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(raw);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
console.error('OIDC-A: Failed to parse JWT_SIGNING_KEY');
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function getPublicKeyFromEnv(env) {
|
|
29
|
+
const sk = getSigningKeyFromEnv(env);
|
|
30
|
+
return sk ? getSigningPublicKeyJWK(sk) : undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Verify BOTCHA Bearer token and return the payload.
|
|
34
|
+
* Returns null + error info if invalid.
|
|
35
|
+
*/
|
|
36
|
+
async function verifyBotchaToken(c, requireAppId = false) {
|
|
37
|
+
const authHeader = c.req.header('authorization');
|
|
38
|
+
const token = extractBearerToken(authHeader);
|
|
39
|
+
if (!token) {
|
|
40
|
+
return { ok: false, error: 'UNAUTHORIZED', status: 401 };
|
|
41
|
+
}
|
|
42
|
+
const publicKey = getPublicKeyFromEnv(c.env);
|
|
43
|
+
const result = await verifyToken(token, c.env.JWT_SECRET, c.env, undefined, publicKey);
|
|
44
|
+
if (!result.valid || !result.payload) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
error: result.error || 'INVALID_TOKEN',
|
|
48
|
+
status: 401,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (requireAppId && !result.payload.app_id) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
error: 'MISSING_APP_ID',
|
|
55
|
+
status: 403,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return { ok: true, payload: result.payload };
|
|
59
|
+
}
|
|
60
|
+
function grantOwnedByApp(grantAppId, tokenAppId) {
|
|
61
|
+
if (!grantAppId || !tokenAppId)
|
|
62
|
+
return false;
|
|
63
|
+
return grantAppId === tokenAppId;
|
|
64
|
+
}
|
|
65
|
+
// ============ ROUTE HANDLERS ============
|
|
66
|
+
/**
|
|
67
|
+
* POST /v1/attestation/eat
|
|
68
|
+
*
|
|
69
|
+
* Issue an RFC 9334 / draft-ietf-rats-eat-25 Entity Attestation Token.
|
|
70
|
+
*
|
|
71
|
+
* Input:
|
|
72
|
+
* Authorization: Bearer <botcha_access_token>
|
|
73
|
+
* Body (optional): {
|
|
74
|
+
* nonce?: string // Client nonce for freshness binding
|
|
75
|
+
* agent_model?: string // AI model name
|
|
76
|
+
* ttl_seconds?: number // Token TTL (max 3600)
|
|
77
|
+
* verification_method?: string
|
|
78
|
+
* }
|
|
79
|
+
*
|
|
80
|
+
* Output: {
|
|
81
|
+
* eat_token: string // Signed EAT JWT
|
|
82
|
+
* eat_profile: string // Profile URI
|
|
83
|
+
* expires_in: number
|
|
84
|
+
* claims: { ... } // Decoded claims (for inspection)
|
|
85
|
+
* }
|
|
86
|
+
*/
|
|
87
|
+
export async function issueEATRoute(c) {
|
|
88
|
+
try {
|
|
89
|
+
const auth = await verifyBotchaToken(c, false);
|
|
90
|
+
if (!auth.ok || !auth.payload) {
|
|
91
|
+
return c.json({ success: false, error: auth.error, message: 'Valid BOTCHA Bearer token required' }, (auth.status || 401));
|
|
92
|
+
}
|
|
93
|
+
const signingKey = getSigningKeyFromEnv(c.env);
|
|
94
|
+
if (!signingKey) {
|
|
95
|
+
return c.json({
|
|
96
|
+
success: false,
|
|
97
|
+
error: 'NO_SIGNING_KEY',
|
|
98
|
+
message: 'Server is not configured for EAT issuance (no ES256 signing key)',
|
|
99
|
+
}, 503);
|
|
100
|
+
}
|
|
101
|
+
const body = await c.req.json().catch(() => ({}));
|
|
102
|
+
let ttlSeconds = 3600;
|
|
103
|
+
if (body.ttl_seconds !== undefined) {
|
|
104
|
+
if (typeof body.ttl_seconds !== 'number'
|
|
105
|
+
|| !Number.isFinite(body.ttl_seconds)
|
|
106
|
+
|| body.ttl_seconds <= 0) {
|
|
107
|
+
return c.json({
|
|
108
|
+
success: false,
|
|
109
|
+
error: 'INVALID_TTL',
|
|
110
|
+
message: 'ttl_seconds must be a positive number',
|
|
111
|
+
}, 400);
|
|
112
|
+
}
|
|
113
|
+
ttlSeconds = Math.min(Math.floor(body.ttl_seconds), 3600);
|
|
114
|
+
}
|
|
115
|
+
const eatToken = await issueEAT(auth.payload, signingKey, {
|
|
116
|
+
nonce: body.nonce,
|
|
117
|
+
agentModel: body.agent_model,
|
|
118
|
+
ttlSeconds,
|
|
119
|
+
verificationMethod: body.verification_method,
|
|
120
|
+
});
|
|
121
|
+
const now = Math.floor(Date.now() / 1000);
|
|
122
|
+
const agentId = auth.payload.app_id
|
|
123
|
+
? `${auth.payload.app_id}:${auth.payload.sub}`
|
|
124
|
+
: auth.payload.sub;
|
|
125
|
+
return c.json({
|
|
126
|
+
success: true,
|
|
127
|
+
eat_token: eatToken,
|
|
128
|
+
token_type: 'JWT+EAT',
|
|
129
|
+
eat_profile: BOTCHA_EAT_PROFILE,
|
|
130
|
+
algorithm: 'ES256',
|
|
131
|
+
expires_in: ttlSeconds,
|
|
132
|
+
// Decoded claims for inspection (convenience — not normative)
|
|
133
|
+
claims: {
|
|
134
|
+
iss: 'botcha.ai',
|
|
135
|
+
sub: agentId,
|
|
136
|
+
iat: now,
|
|
137
|
+
exp: now + ttlSeconds,
|
|
138
|
+
eat_profile: BOTCHA_EAT_PROFILE,
|
|
139
|
+
oemid: 'botcha.ai',
|
|
140
|
+
swname: 'BOTCHA',
|
|
141
|
+
dbgstat: 'Disabled',
|
|
142
|
+
intuse: 'generic',
|
|
143
|
+
botcha_verified: true,
|
|
144
|
+
botcha_solve_time_ms: auth.payload.solveTime,
|
|
145
|
+
botcha_app_id: auth.payload.app_id,
|
|
146
|
+
},
|
|
147
|
+
usage: {
|
|
148
|
+
description: 'Embed this token as agent_attestation in OIDC-A ID tokens',
|
|
149
|
+
embed_as: 'agent_attestation',
|
|
150
|
+
verify_with: 'GET /.well-known/jwks (ES256 public key)',
|
|
151
|
+
oidca_claims: 'POST /v1/attestation/oidc-agent-claims',
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
console.error('EAT issuance error:', error);
|
|
157
|
+
return c.json({ success: false, error: 'INTERNAL_ERROR', message: 'EAT issuance failed' }, 500);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* POST /v1/attestation/oidc-agent-claims
|
|
162
|
+
*
|
|
163
|
+
* Issue OIDC-A compatible agent claims block.
|
|
164
|
+
* Enterprise auth servers call this to enrich agent ID tokens.
|
|
165
|
+
*
|
|
166
|
+
* Input:
|
|
167
|
+
* Authorization: Bearer <botcha_access_token>
|
|
168
|
+
* Body (optional): {
|
|
169
|
+
* agent_model?: string
|
|
170
|
+
* agent_version?: string
|
|
171
|
+
* agent_capabilities?: string[]
|
|
172
|
+
* agent_operator?: string
|
|
173
|
+
* delegation_chain?: string[]
|
|
174
|
+
* human_oversight_required?: boolean
|
|
175
|
+
* oversight_contact?: string
|
|
176
|
+
* task_id?: string
|
|
177
|
+
* task_purpose?: string
|
|
178
|
+
* scope?: string
|
|
179
|
+
* nonce?: string
|
|
180
|
+
* }
|
|
181
|
+
*
|
|
182
|
+
* Output: {
|
|
183
|
+
* claims_jwt: string // Signed OIDC-A claims JWT (embed in ID token)
|
|
184
|
+
* claims: OIDCAgentClaims // Decoded claims object (for direct embedding)
|
|
185
|
+
* eat_token: string // The EAT token embedded within
|
|
186
|
+
* expires_in: number
|
|
187
|
+
* }
|
|
188
|
+
*/
|
|
189
|
+
export async function issueOIDCAgentClaimsRoute(c) {
|
|
190
|
+
try {
|
|
191
|
+
const auth = await verifyBotchaToken(c, false);
|
|
192
|
+
if (!auth.ok || !auth.payload) {
|
|
193
|
+
return c.json({ success: false, error: auth.error, message: 'Valid BOTCHA Bearer token required' }, (auth.status || 401));
|
|
194
|
+
}
|
|
195
|
+
const signingKey = getSigningKeyFromEnv(c.env);
|
|
196
|
+
if (!signingKey) {
|
|
197
|
+
return c.json({
|
|
198
|
+
success: false,
|
|
199
|
+
error: 'NO_SIGNING_KEY',
|
|
200
|
+
message: 'Server is not configured for OIDC-A claims issuance (no ES256 signing key)',
|
|
201
|
+
}, 503);
|
|
202
|
+
}
|
|
203
|
+
const body = await c.req.json().catch(() => ({}));
|
|
204
|
+
// First issue the EAT (embedded in OIDC-A claims as agent_attestation)
|
|
205
|
+
const eatToken = await issueEAT(auth.payload, signingKey, {
|
|
206
|
+
nonce: body.nonce,
|
|
207
|
+
agentModel: body.agent_model,
|
|
208
|
+
verificationMethod: body.verification_method,
|
|
209
|
+
});
|
|
210
|
+
// Then build OIDC-A claims wrapping the EAT
|
|
211
|
+
const { claims, claimsJwt } = await buildOIDCAgentClaims(auth.payload, eatToken, signingKey, {
|
|
212
|
+
agentModel: body.agent_model,
|
|
213
|
+
agentVersion: body.agent_version,
|
|
214
|
+
agentCapabilities: body.agent_capabilities,
|
|
215
|
+
agentOperator: body.agent_operator,
|
|
216
|
+
delegationChain: body.delegation_chain,
|
|
217
|
+
humanOversightRequired: body.human_oversight_required ?? false,
|
|
218
|
+
oversightContact: body.oversight_contact,
|
|
219
|
+
taskId: body.task_id,
|
|
220
|
+
taskPurpose: body.task_purpose,
|
|
221
|
+
scope: body.scope,
|
|
222
|
+
});
|
|
223
|
+
return c.json({
|
|
224
|
+
success: true,
|
|
225
|
+
// Primary output: the signed claims JWT for embedding in ID tokens
|
|
226
|
+
claims_jwt: claimsJwt,
|
|
227
|
+
token_type: 'JWT+OIDCA',
|
|
228
|
+
algorithm: 'ES256',
|
|
229
|
+
expires_in: 3600,
|
|
230
|
+
// Decoded claims for direct embedding in ID token payload
|
|
231
|
+
claims,
|
|
232
|
+
// The embedded EAT token (also available standalone)
|
|
233
|
+
eat_token: eatToken,
|
|
234
|
+
// Integration guide
|
|
235
|
+
usage: {
|
|
236
|
+
description: 'Embed claims in your OIDC ID token or use claims_jwt as agent_attestation',
|
|
237
|
+
embed_method_1: 'Copy `claims` object fields directly into your ID token payload',
|
|
238
|
+
embed_method_2: 'Set `agent_attestation: claims_jwt` in your ID token',
|
|
239
|
+
embed_method_3: 'Set `agent_attestation: eat_token` for EAT-only embedding',
|
|
240
|
+
verify_with: 'GET /.well-known/jwks (ES256 public key)',
|
|
241
|
+
standard: 'draft-aap-oauth-profile §5, OIDC-A 1.0',
|
|
242
|
+
},
|
|
243
|
+
// Available agent capabilities
|
|
244
|
+
available_capabilities: BOTCHA_AGENT_CAPABILITIES,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
console.error('OIDC-A claims issuance error:', error);
|
|
249
|
+
return c.json({ success: false, error: 'INTERNAL_ERROR', message: 'OIDC-A claims issuance failed' }, 500);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* GET /.well-known/oauth-authorization-server
|
|
254
|
+
*
|
|
255
|
+
* OAuth 2.0 Authorization Server Metadata (RFC 8414).
|
|
256
|
+
* Extended with OIDC-A / agent-specific metadata.
|
|
257
|
+
*
|
|
258
|
+
* No authentication required — public discovery endpoint.
|
|
259
|
+
*/
|
|
260
|
+
export async function oauthASMetadataRoute(c) {
|
|
261
|
+
const baseUrl = new URL(c.req.url).origin;
|
|
262
|
+
const metadata = buildOAuthASMetadata(baseUrl);
|
|
263
|
+
return c.json(metadata, 200, {
|
|
264
|
+
'Cache-Control': 'public, max-age=3600',
|
|
265
|
+
'Content-Type': 'application/json',
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* POST /v1/auth/agent-grant
|
|
270
|
+
*
|
|
271
|
+
* Agent Authorization Grant per draft-rosenberg-oauth-aauth.
|
|
272
|
+
*
|
|
273
|
+
* An agent presents its BOTCHA token and receives a scoped OAuth grant
|
|
274
|
+
* with embedded OIDC-A claims and an EAT attestation.
|
|
275
|
+
*
|
|
276
|
+
* Input:
|
|
277
|
+
* Authorization: Bearer <botcha_access_token>
|
|
278
|
+
* Body (optional): {
|
|
279
|
+
* scope?: string // Requested scopes (space-separated)
|
|
280
|
+
* human_oversight_required?: bool // Request HITL approval flow
|
|
281
|
+
* agent_model?: string
|
|
282
|
+
* agent_version?: string
|
|
283
|
+
* agent_capabilities?: string[]
|
|
284
|
+
* agent_operator?: string
|
|
285
|
+
* task_id?: string
|
|
286
|
+
* task_purpose?: string
|
|
287
|
+
* delegation_chain?: string[]
|
|
288
|
+
* constraints?: object
|
|
289
|
+
* }
|
|
290
|
+
*
|
|
291
|
+
* Output: AgentGrantResult
|
|
292
|
+
*/
|
|
293
|
+
export async function agentGrantRoute(c) {
|
|
294
|
+
try {
|
|
295
|
+
const auth = await verifyBotchaToken(c, false);
|
|
296
|
+
if (!auth.ok || !auth.payload) {
|
|
297
|
+
return c.json({
|
|
298
|
+
success: false,
|
|
299
|
+
error: auth.error,
|
|
300
|
+
message: 'Valid BOTCHA Bearer token required to request an agent grant',
|
|
301
|
+
how_to_get_token: 'GET /v1/token → POST /v1/token/verify',
|
|
302
|
+
}, (auth.status || 401));
|
|
303
|
+
}
|
|
304
|
+
const signingKey = getSigningKeyFromEnv(c.env);
|
|
305
|
+
if (!signingKey) {
|
|
306
|
+
return c.json({
|
|
307
|
+
success: false,
|
|
308
|
+
error: 'NO_SIGNING_KEY',
|
|
309
|
+
message: 'Server is not configured for agent grant issuance (no ES256 signing key)',
|
|
310
|
+
}, 503);
|
|
311
|
+
}
|
|
312
|
+
const body = await c.req.json().catch(() => ({}));
|
|
313
|
+
// HITL grants require an app_id so that status/resolve routes can enforce
|
|
314
|
+
// same-app ownership. Without it the grant would be created but never resolvable.
|
|
315
|
+
if (body.human_oversight_required && !auth.payload.app_id) {
|
|
316
|
+
return c.json({
|
|
317
|
+
success: false,
|
|
318
|
+
error: 'APP_ID_REQUIRED',
|
|
319
|
+
message: 'human_oversight_required grants require a token issued with an app_id. ' +
|
|
320
|
+
'Obtain your token via the challenge flow with an app_id.',
|
|
321
|
+
}, 400);
|
|
322
|
+
}
|
|
323
|
+
const baseUrl = new URL(c.req.url).origin;
|
|
324
|
+
// Issue EAT
|
|
325
|
+
const eatToken = await issueEAT(auth.payload, signingKey, {
|
|
326
|
+
agentModel: body.agent_model,
|
|
327
|
+
verificationMethod: body.verification_method,
|
|
328
|
+
});
|
|
329
|
+
// Build OIDC-A claims
|
|
330
|
+
const { claims: oidcClaims } = await buildOIDCAgentClaims(auth.payload, eatToken, signingKey, {
|
|
331
|
+
agentModel: body.agent_model,
|
|
332
|
+
agentVersion: body.agent_version,
|
|
333
|
+
agentCapabilities: body.agent_capabilities,
|
|
334
|
+
agentOperator: body.agent_operator,
|
|
335
|
+
delegationChain: body.delegation_chain,
|
|
336
|
+
humanOversightRequired: body.human_oversight_required ?? false,
|
|
337
|
+
oversightContact: body.oversight_contact,
|
|
338
|
+
taskId: body.task_id,
|
|
339
|
+
taskPurpose: body.task_purpose,
|
|
340
|
+
scope: body.scope,
|
|
341
|
+
verificationMethod: body.verification_method,
|
|
342
|
+
});
|
|
343
|
+
// Issue the agent grant
|
|
344
|
+
const grant = await issueAgentGrant(auth.payload, eatToken, oidcClaims, signingKey, c.env.SESSIONS ?? c.env.CHALLENGES, // Use SESSIONS KV if available
|
|
345
|
+
baseUrl, {
|
|
346
|
+
scope: body.scope,
|
|
347
|
+
humanOversightRequired: body.human_oversight_required ?? false,
|
|
348
|
+
taskId: body.task_id,
|
|
349
|
+
taskPurpose: body.task_purpose,
|
|
350
|
+
constraints: body.constraints,
|
|
351
|
+
});
|
|
352
|
+
const status = 200;
|
|
353
|
+
return c.json({
|
|
354
|
+
success: true,
|
|
355
|
+
...grant,
|
|
356
|
+
// Additional context
|
|
357
|
+
standard: 'draft-rosenberg-oauth-aauth, draft-aap-oauth-profile',
|
|
358
|
+
issued_at: new Date().toISOString(),
|
|
359
|
+
}, status);
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
console.error('Agent grant error:', error);
|
|
363
|
+
return c.json({ success: false, error: 'INTERNAL_ERROR', message: 'Agent grant issuance failed' }, 500);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* GET /v1/auth/agent-grant/:id/status
|
|
368
|
+
*
|
|
369
|
+
* Poll the status of a human-in-the-loop pending grant.
|
|
370
|
+
* Returns current status: pending | approved | denied
|
|
371
|
+
*/
|
|
372
|
+
export async function agentGrantStatusRoute(c) {
|
|
373
|
+
try {
|
|
374
|
+
const auth = await verifyBotchaToken(c, true);
|
|
375
|
+
if (!auth.ok || !auth.payload) {
|
|
376
|
+
return c.json({
|
|
377
|
+
success: false,
|
|
378
|
+
error: auth.error,
|
|
379
|
+
message: 'Valid BOTCHA Bearer token required to read grant status',
|
|
380
|
+
}, (auth.status || 401));
|
|
381
|
+
}
|
|
382
|
+
const grantId = c.req.param('id');
|
|
383
|
+
if (!grantId) {
|
|
384
|
+
return c.json({ success: false, error: 'MISSING_GRANT_ID' }, 400);
|
|
385
|
+
}
|
|
386
|
+
const kv = c.env.SESSIONS ?? c.env.CHALLENGES;
|
|
387
|
+
const grant = await getGrantStatus(grantId, kv);
|
|
388
|
+
if (!grant) {
|
|
389
|
+
return c.json({ success: false, error: 'GRANT_NOT_FOUND', message: 'Grant not found or expired' }, 404);
|
|
390
|
+
}
|
|
391
|
+
if (!grantOwnedByApp(grant.app_id, auth.payload.app_id)) {
|
|
392
|
+
return c.json({
|
|
393
|
+
success: false,
|
|
394
|
+
error: 'FORBIDDEN',
|
|
395
|
+
message: 'Grant does not belong to your app',
|
|
396
|
+
}, 403);
|
|
397
|
+
}
|
|
398
|
+
return c.json({
|
|
399
|
+
success: true,
|
|
400
|
+
grant_id: grant.grant_id,
|
|
401
|
+
agent_id: grant.agent_id,
|
|
402
|
+
scope: grant.scope,
|
|
403
|
+
status: grant.status,
|
|
404
|
+
requested_at: new Date(grant.requested_at).toISOString(),
|
|
405
|
+
approved_at: grant.approved_at ? new Date(grant.approved_at).toISOString() : null,
|
|
406
|
+
denied_at: grant.denied_at ? new Date(grant.denied_at).toISOString() : null,
|
|
407
|
+
denial_reason: grant.denial_reason ?? null,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
console.error('Grant status error:', error);
|
|
412
|
+
return c.json({ success: false, error: 'INTERNAL_ERROR' }, 500);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* POST /v1/auth/agent-grant/:id/resolve
|
|
417
|
+
*
|
|
418
|
+
* Approve or deny a pending HITL grant.
|
|
419
|
+
* Requires app_id authentication (the grant owner must resolve it).
|
|
420
|
+
*
|
|
421
|
+
* Body: {
|
|
422
|
+
* decision: 'approved' | 'denied'
|
|
423
|
+
* reason?: string // Required if denied
|
|
424
|
+
* }
|
|
425
|
+
*/
|
|
426
|
+
export async function agentGrantResolveRoute(c) {
|
|
427
|
+
try {
|
|
428
|
+
const auth = await verifyBotchaToken(c, true);
|
|
429
|
+
if (!auth.ok || !auth.payload) {
|
|
430
|
+
return c.json({
|
|
431
|
+
success: false,
|
|
432
|
+
error: auth.error,
|
|
433
|
+
message: 'Valid BOTCHA Bearer token required to resolve grants',
|
|
434
|
+
}, (auth.status || 401));
|
|
435
|
+
}
|
|
436
|
+
const grantId = c.req.param('id');
|
|
437
|
+
if (!grantId) {
|
|
438
|
+
return c.json({ success: false, error: 'MISSING_GRANT_ID' }, 400);
|
|
439
|
+
}
|
|
440
|
+
const body = await c.req.json().catch(() => ({}));
|
|
441
|
+
const decision = body.decision;
|
|
442
|
+
if (!['approved', 'denied'].includes(decision)) {
|
|
443
|
+
return c.json({
|
|
444
|
+
success: false,
|
|
445
|
+
error: 'INVALID_DECISION',
|
|
446
|
+
message: 'decision must be "approved" or "denied"',
|
|
447
|
+
}, 400);
|
|
448
|
+
}
|
|
449
|
+
if (decision === 'denied' && !body.reason) {
|
|
450
|
+
return c.json({ success: false, error: 'MISSING_REASON', message: 'reason required when denying' }, 400);
|
|
451
|
+
}
|
|
452
|
+
const kv = c.env.SESSIONS ?? c.env.CHALLENGES;
|
|
453
|
+
const grant = await getGrantStatus(grantId, kv);
|
|
454
|
+
if (!grant) {
|
|
455
|
+
return c.json({ success: false, error: 'GRANT_NOT_FOUND', message: 'Grant not found or expired' }, 404);
|
|
456
|
+
}
|
|
457
|
+
if (!grantOwnedByApp(grant.app_id, auth.payload.app_id)) {
|
|
458
|
+
return c.json({
|
|
459
|
+
success: false,
|
|
460
|
+
error: 'FORBIDDEN',
|
|
461
|
+
message: 'Grant does not belong to your app',
|
|
462
|
+
}, 403);
|
|
463
|
+
}
|
|
464
|
+
const result = await resolveGrant(grantId, decision, body.reason, kv);
|
|
465
|
+
if (!result.success) {
|
|
466
|
+
return c.json({ success: false, error: 'RESOLVE_FAILED', message: result.error }, 400);
|
|
467
|
+
}
|
|
468
|
+
return c.json({
|
|
469
|
+
success: true,
|
|
470
|
+
grant_id: grantId,
|
|
471
|
+
decision,
|
|
472
|
+
grant: {
|
|
473
|
+
status: result.grant.status,
|
|
474
|
+
approved_at: result.grant.approved_at
|
|
475
|
+
? new Date(result.grant.approved_at).toISOString()
|
|
476
|
+
: null,
|
|
477
|
+
denied_at: result.grant.denied_at
|
|
478
|
+
? new Date(result.grant.denied_at).toISOString()
|
|
479
|
+
: null,
|
|
480
|
+
denial_reason: result.grant.denial_reason ?? null,
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
console.error('Grant resolve error:', error);
|
|
486
|
+
return c.json({ success: false, error: 'INTERNAL_ERROR' }, 500);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* GET /v1/oidc/userinfo
|
|
491
|
+
*
|
|
492
|
+
* OIDC-A compliant UserInfo endpoint for verified agents.
|
|
493
|
+
*
|
|
494
|
+
* Returns agent identity claims + BOTCHA verification status.
|
|
495
|
+
* Accepts either a BOTCHA access_token or an EAT token as Bearer.
|
|
496
|
+
*
|
|
497
|
+
* Standard OIDC UserInfo response extended with agent claims.
|
|
498
|
+
*/
|
|
499
|
+
export async function oidcUserInfoRoute(c) {
|
|
500
|
+
try {
|
|
501
|
+
const authHeader = c.req.header('authorization');
|
|
502
|
+
const token = extractBearerToken(authHeader);
|
|
503
|
+
if (!token) {
|
|
504
|
+
return c.json({
|
|
505
|
+
error: 'UNAUTHORIZED',
|
|
506
|
+
error_description: 'Bearer token required',
|
|
507
|
+
}, 401, {
|
|
508
|
+
'WWW-Authenticate': 'Bearer realm="botcha.ai", error="unauthorized", error_description="Bearer token required"',
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
// Try as BOTCHA access token first
|
|
512
|
+
const publicKey = getPublicKeyFromEnv(c.env);
|
|
513
|
+
const botchaResult = await verifyToken(token, c.env.JWT_SECRET, c.env, undefined, publicKey);
|
|
514
|
+
if (botchaResult.valid && botchaResult.payload) {
|
|
515
|
+
const payload = botchaResult.payload;
|
|
516
|
+
const agentId = payload.app_id
|
|
517
|
+
? `${payload.app_id}:${payload.sub}`
|
|
518
|
+
: payload.sub;
|
|
519
|
+
const baseUrl = new URL(c.req.url).origin;
|
|
520
|
+
return c.json({
|
|
521
|
+
// Standard OIDC UserInfo claims
|
|
522
|
+
sub: agentId,
|
|
523
|
+
iss: 'botcha.ai',
|
|
524
|
+
iat: payload.iat,
|
|
525
|
+
exp: payload.exp,
|
|
526
|
+
// OIDC-A agent extension claims
|
|
527
|
+
agent_id: agentId,
|
|
528
|
+
agent_model: 'botcha-verified-agent',
|
|
529
|
+
agent_capabilities: ['botcha:verified', 'botcha:speed-challenge'],
|
|
530
|
+
// BOTCHA verification status
|
|
531
|
+
botcha_verified: true,
|
|
532
|
+
botcha_app_id: payload.app_id ?? null,
|
|
533
|
+
botcha_solve_time_ms: payload.solveTime,
|
|
534
|
+
botcha_challenge_id: payload.sub,
|
|
535
|
+
// Verification metadata
|
|
536
|
+
verification: {
|
|
537
|
+
method: 'botcha-speed-challenge',
|
|
538
|
+
verified_at: new Date(payload.iat * 1000).toISOString(),
|
|
539
|
+
issuer: 'botcha.ai',
|
|
540
|
+
solve_time_ms: payload.solveTime,
|
|
541
|
+
},
|
|
542
|
+
// Where to get attestation tokens
|
|
543
|
+
attestation_endpoints: {
|
|
544
|
+
eat: `${baseUrl}/v1/attestation/eat`,
|
|
545
|
+
oidc_agent_claims: `${baseUrl}/v1/attestation/oidc-agent-claims`,
|
|
546
|
+
agent_grant: `${baseUrl}/v1/auth/agent-grant`,
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
// Try as EAT token
|
|
551
|
+
if (publicKey) {
|
|
552
|
+
const eatPayload = await verifyEAT(token, publicKey);
|
|
553
|
+
if (eatPayload) {
|
|
554
|
+
return c.json({
|
|
555
|
+
sub: eatPayload.sub,
|
|
556
|
+
iss: eatPayload.iss,
|
|
557
|
+
iat: eatPayload.iat,
|
|
558
|
+
exp: eatPayload.exp,
|
|
559
|
+
// EAT identity
|
|
560
|
+
agent_id: eatPayload.sub,
|
|
561
|
+
agent_model: 'botcha-verified-agent',
|
|
562
|
+
agent_capabilities: ['botcha:verified'],
|
|
563
|
+
// EAT metadata
|
|
564
|
+
eat_profile: eatPayload.eat_profile,
|
|
565
|
+
ueid: eatPayload.ueid,
|
|
566
|
+
oemid: eatPayload.oemid,
|
|
567
|
+
swname: eatPayload.swname,
|
|
568
|
+
swversion: eatPayload.swversion,
|
|
569
|
+
dbgstat: eatPayload.dbgstat,
|
|
570
|
+
// BOTCHA verification
|
|
571
|
+
botcha_verified: eatPayload.botcha_verified,
|
|
572
|
+
botcha_app_id: eatPayload.botcha_app_id ?? null,
|
|
573
|
+
botcha_solve_time_ms: eatPayload.botcha_solve_time_ms,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return c.json({
|
|
578
|
+
error: 'INVALID_TOKEN',
|
|
579
|
+
error_description: 'Token is invalid, expired, or revoked',
|
|
580
|
+
}, 401, {
|
|
581
|
+
'WWW-Authenticate': 'Bearer realm="botcha.ai", error="invalid_token", error_description="Token is invalid or expired"',
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
console.error('UserInfo error:', error);
|
|
586
|
+
return c.json({ error: 'INTERNAL_ERROR' }, 500);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
export default {
|
|
590
|
+
issueEATRoute,
|
|
591
|
+
issueOIDCAgentClaimsRoute,
|
|
592
|
+
oauthASMetadataRoute,
|
|
593
|
+
agentGrantRoute,
|
|
594
|
+
agentGrantStatusRoute,
|
|
595
|
+
agentGrantResolveRoute,
|
|
596
|
+
oidcUserInfoRoute,
|
|
597
|
+
};
|