@dupecom/botcha-cloudflare 0.21.0 → 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.
Files changed (88) hide show
  1. package/README.md +74 -9
  2. package/dist/agent-auth.d.ts +129 -0
  3. package/dist/agent-auth.d.ts.map +1 -0
  4. package/dist/agent-auth.js +210 -0
  5. package/dist/agents.d.ts +10 -0
  6. package/dist/agents.d.ts.map +1 -1
  7. package/dist/agents.js +51 -1
  8. package/dist/app-gate.d.ts +6 -0
  9. package/dist/app-gate.d.ts.map +1 -0
  10. package/dist/app-gate.js +69 -0
  11. package/dist/apps.d.ts +9 -0
  12. package/dist/apps.d.ts.map +1 -1
  13. package/dist/apps.js +26 -0
  14. package/dist/dashboard/account.d.ts +63 -0
  15. package/dist/dashboard/account.d.ts.map +1 -0
  16. package/dist/dashboard/account.js +488 -0
  17. package/dist/dashboard/api.js +15 -68
  18. package/dist/dashboard/auth.d.ts.map +1 -1
  19. package/dist/dashboard/auth.js +14 -14
  20. package/dist/dashboard/docs.d.ts.map +1 -1
  21. package/dist/dashboard/docs.js +146 -3
  22. package/dist/dashboard/layout.d.ts.map +1 -1
  23. package/dist/dashboard/layout.js +2 -2
  24. package/dist/dashboard/mcp-setup.d.ts +15 -0
  25. package/dist/dashboard/mcp-setup.d.ts.map +1 -0
  26. package/dist/dashboard/mcp-setup.js +391 -0
  27. package/dist/dashboard/showcase.d.ts +6 -10
  28. package/dist/dashboard/showcase.d.ts.map +1 -1
  29. package/dist/dashboard/showcase.js +67 -991
  30. package/dist/dashboard/whitepaper.d.ts.map +1 -1
  31. package/dist/dashboard/whitepaper.js +42 -4
  32. package/dist/index.d.ts +3 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +452 -52
  35. package/dist/mcp.d.ts +20 -0
  36. package/dist/mcp.d.ts.map +1 -0
  37. package/dist/mcp.js +1290 -0
  38. package/dist/oauth-agent.d.ts +130 -0
  39. package/dist/oauth-agent.d.ts.map +1 -0
  40. package/dist/oauth-agent.js +194 -0
  41. package/dist/static.d.ts +732 -1
  42. package/dist/static.d.ts.map +1 -1
  43. package/dist/static.js +646 -2
  44. package/dist/tap-a2a-routes.d.ts +355 -0
  45. package/dist/tap-a2a-routes.d.ts.map +1 -0
  46. package/dist/tap-a2a-routes.js +475 -0
  47. package/dist/tap-a2a.d.ts +199 -0
  48. package/dist/tap-a2a.d.ts.map +1 -0
  49. package/dist/tap-a2a.js +502 -0
  50. package/dist/tap-agents.d.ts +15 -0
  51. package/dist/tap-agents.d.ts.map +1 -1
  52. package/dist/tap-agents.js +31 -1
  53. package/dist/tap-ans-routes.d.ts +302 -0
  54. package/dist/tap-ans-routes.d.ts.map +1 -0
  55. package/dist/tap-ans-routes.js +535 -0
  56. package/dist/tap-ans.d.ts +241 -0
  57. package/dist/tap-ans.d.ts.map +1 -0
  58. package/dist/tap-ans.js +481 -0
  59. package/dist/tap-delegation-routes.d.ts.map +1 -1
  60. package/dist/tap-delegation-routes.js +11 -0
  61. package/dist/tap-did.d.ts +140 -0
  62. package/dist/tap-did.d.ts.map +1 -0
  63. package/dist/tap-did.js +262 -0
  64. package/dist/tap-oidca-routes.d.ts +383 -0
  65. package/dist/tap-oidca-routes.d.ts.map +1 -0
  66. package/dist/tap-oidca-routes.js +597 -0
  67. package/dist/tap-oidca.d.ts +288 -0
  68. package/dist/tap-oidca.d.ts.map +1 -0
  69. package/dist/tap-oidca.js +461 -0
  70. package/dist/tap-routes.d.ts +24 -8
  71. package/dist/tap-routes.d.ts.map +1 -1
  72. package/dist/tap-routes.js +169 -23
  73. package/dist/tap-vc-routes.d.ts +358 -0
  74. package/dist/tap-vc-routes.d.ts.map +1 -0
  75. package/dist/tap-vc-routes.js +367 -0
  76. package/dist/tap-vc.d.ts +125 -0
  77. package/dist/tap-vc.d.ts.map +1 -0
  78. package/dist/tap-vc.js +245 -0
  79. package/dist/tap-x402-routes.d.ts +89 -0
  80. package/dist/tap-x402-routes.d.ts.map +1 -0
  81. package/dist/tap-x402-routes.js +579 -0
  82. package/dist/tap-x402.d.ts +222 -0
  83. package/dist/tap-x402.d.ts.map +1 -0
  84. package/dist/tap-x402.js +546 -0
  85. package/dist/webhooks.d.ts +99 -0
  86. package/dist/webhooks.d.ts.map +1 -0
  87. package/dist/webhooks.js +642 -0
  88. 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
+ };