@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.
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 +13 -4
  12. package/dist/apps.d.ts.map +1 -1
  13. package/dist/apps.js +30 -4
  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 +5 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +660 -83
  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 +781 -5
  42. package/dist/static.d.ts.map +1 -1
  43. package/dist/static.js +790 -111
  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,130 @@
1
+ /**
2
+ * BOTCHA Agent OAuth — Device Authorization Grant
3
+ *
4
+ * Lets an agent get a long-lived refresh token by having the human
5
+ * approve a device code in their browser. After that, the agent
6
+ * re-identifies in any future session with just the refresh token —
7
+ * no tapk_ keypair, no API key, nothing else to manage.
8
+ *
9
+ * Flow:
10
+ * 1. Agent: POST /v1/oauth/device { agent_id, app_id }
11
+ * ← { device_code, user_code: "BOTCHA-XXXX", verification_url, expires_in: 600, interval: 5 }
12
+ *
13
+ * 2. Agent tells human: "Visit <verification_url> and enter <user_code>"
14
+ *
15
+ * 3. Human logs into dashboard, sees pending authorization, clicks Approve.
16
+ *
17
+ * 4. Agent polls: POST /v1/oauth/token { device_code, grant_type: "urn:ietf:params:oauth:grant-type:device_code" }
18
+ * ← { error: "authorization_pending" } (keep polling every 5s)
19
+ * ← { access_token, refresh_token: "brt_...", expires_in: 3600 } (once approved)
20
+ *
21
+ * 5. Future sessions: POST /v1/agents/auth/refresh { refresh_token: "brt_..." }
22
+ * ← { access_token, agent_id, app_id }
23
+ *
24
+ * The refresh token is stored in KV and can be revoked from the dashboard.
25
+ * It is tied to a specific agent_id — so it proves both "I am authenticated"
26
+ * and "I am specifically this agent".
27
+ */
28
+ import type { Context } from 'hono';
29
+ type KV = {
30
+ get(k: string, t?: string): Promise<string | null>;
31
+ put(k: string, v: string, o?: {
32
+ expirationTtl?: number;
33
+ }): Promise<void>;
34
+ delete(k: string): Promise<void>;
35
+ };
36
+ type Bindings = {
37
+ AGENTS: KV;
38
+ CHALLENGES: KV;
39
+ JWT_SECRET: string;
40
+ };
41
+ export declare function handleOAuthDevice(c: Context<{
42
+ Bindings: Bindings;
43
+ }>): Promise<(Response & import("hono").TypedResponse<{
44
+ error: string;
45
+ error_description: string;
46
+ }, 400, "json">) | (Response & import("hono").TypedResponse<{
47
+ error: string;
48
+ error_description: string;
49
+ }, 404, "json">) | (Response & import("hono").TypedResponse<{
50
+ error: string;
51
+ error_description: string;
52
+ }, 403, "json">) | (Response & import("hono").TypedResponse<{
53
+ device_code: string;
54
+ user_code: string;
55
+ verification_url: string;
56
+ verification_uri: string;
57
+ verification_uri_complete: string;
58
+ expires_in: number;
59
+ interval: number;
60
+ message: string;
61
+ }, import("hono/utils/http-status").ContentfulStatusCode, "json">)>;
62
+ export declare function handleOAuthApprove(c: Context<{
63
+ Bindings: Bindings;
64
+ }>): Promise<(Response & import("hono").TypedResponse<{
65
+ success: false;
66
+ error: string;
67
+ }, 400, "json">) | (Response & import("hono").TypedResponse<{
68
+ success: false;
69
+ error: string;
70
+ }, 404, "json">) | (Response & import("hono").TypedResponse<{
71
+ success: true;
72
+ status: any;
73
+ agent_id: any;
74
+ }, import("hono/utils/http-status").ContentfulStatusCode, "json">)>;
75
+ export declare function handleOAuthToken(c: Context<{
76
+ Bindings: Bindings;
77
+ }>): Promise<(Response & import("hono").TypedResponse<{
78
+ error: string;
79
+ }, 400, "json">) | (Response & import("hono").TypedResponse<{
80
+ access_token: string;
81
+ token_type: string;
82
+ expires_in: number;
83
+ refresh_token: string;
84
+ agent_id: any;
85
+ app_id: any;
86
+ message: string;
87
+ }, import("hono/utils/http-status").ContentfulStatusCode, "json">)>;
88
+ export declare function handleAgentAuthRefresh(c: Context<{
89
+ Bindings: Bindings;
90
+ }>): Promise<(Response & import("hono").TypedResponse<{
91
+ success: false;
92
+ error: string;
93
+ message: string;
94
+ }, 400, "json">) | (Response & import("hono").TypedResponse<{
95
+ success: false;
96
+ error: string;
97
+ message: string;
98
+ }, 401, "json">) | (Response & import("hono").TypedResponse<{
99
+ success: true;
100
+ access_token: string;
101
+ token_type: string;
102
+ agent_id: any;
103
+ app_id: any;
104
+ expires_in: number;
105
+ message: string;
106
+ }, import("hono/utils/http-status").ContentfulStatusCode, "json">)>;
107
+ export declare function handleOAuthStatus(c: Context<{
108
+ Bindings: Bindings;
109
+ }>): Promise<(Response & import("hono").TypedResponse<{
110
+ error: string;
111
+ }, 400, "json">) | (Response & import("hono").TypedResponse<{
112
+ status: any;
113
+ }, import("hono/utils/http-status").ContentfulStatusCode, "json">)>;
114
+ export declare function handleOAuthRevoke(c: Context<{
115
+ Bindings: Bindings;
116
+ }>): Promise<(Response & import("hono").TypedResponse<{
117
+ success: false;
118
+ error: string;
119
+ }, 400, "json">) | (Response & import("hono").TypedResponse<{
120
+ success: false;
121
+ error: string;
122
+ }, 404, "json">) | (Response & import("hono").TypedResponse<{
123
+ success: false;
124
+ error: string;
125
+ }, 403, "json">) | (Response & import("hono").TypedResponse<{
126
+ success: true;
127
+ message: string;
128
+ }, import("hono/utils/http-status").ContentfulStatusCode, "json">)>;
129
+ export {};
130
+ //# sourceMappingURL=oauth-agent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-agent.d.ts","sourceRoot":"","sources":["../src/oauth-agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAIpC,KAAK,EAAE,GAAG;IAAE,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAAC,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAAC,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC;AAC7K,KAAK,QAAQ,GAAG;IAAE,MAAM,EAAE,EAAE,CAAC;IAAC,UAAU,EAAE,EAAE,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAOnE,wBAAsB,iBAAiB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;;;;;;;;;oEAyCzE;AAID,wBAAsB,kBAAkB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;oEAmB1E;AAID,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;oEA8CxE;AAID,wBAAsB,sBAAsB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;;;;;;;oEAyB9E;AAID,wBAAsB,iBAAiB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;oEAczE;AAID,wBAAsB,iBAAiB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;;;oEAezE"}
@@ -0,0 +1,194 @@
1
+ /**
2
+ * BOTCHA Agent OAuth — Device Authorization Grant
3
+ *
4
+ * Lets an agent get a long-lived refresh token by having the human
5
+ * approve a device code in their browser. After that, the agent
6
+ * re-identifies in any future session with just the refresh token —
7
+ * no tapk_ keypair, no API key, nothing else to manage.
8
+ *
9
+ * Flow:
10
+ * 1. Agent: POST /v1/oauth/device { agent_id, app_id }
11
+ * ← { device_code, user_code: "BOTCHA-XXXX", verification_url, expires_in: 600, interval: 5 }
12
+ *
13
+ * 2. Agent tells human: "Visit <verification_url> and enter <user_code>"
14
+ *
15
+ * 3. Human logs into dashboard, sees pending authorization, clicks Approve.
16
+ *
17
+ * 4. Agent polls: POST /v1/oauth/token { device_code, grant_type: "urn:ietf:params:oauth:grant-type:device_code" }
18
+ * ← { error: "authorization_pending" } (keep polling every 5s)
19
+ * ← { access_token, refresh_token: "brt_...", expires_in: 3600 } (once approved)
20
+ *
21
+ * 5. Future sessions: POST /v1/agents/auth/refresh { refresh_token: "brt_..." }
22
+ * ← { access_token, agent_id, app_id }
23
+ *
24
+ * The refresh token is stored in KV and can be revoked from the dashboard.
25
+ * It is tied to a specific agent_id — so it proves both "I am authenticated"
26
+ * and "I am specifically this agent".
27
+ */
28
+ import { SignJWT } from 'jose';
29
+ import { generateDeviceCode } from './dashboard/device-code.js';
30
+ const DEVICE_TTL_SEC = 600; // 10 min for human to approve
31
+ const REFRESH_TTL_SEC = 60 * 60 * 24 * 90; // 90 day refresh token
32
+ // ============ STEP 1: Initiate device authorization ============
33
+ export async function handleOAuthDevice(c) {
34
+ const body = await c.req.json().catch(() => ({}));
35
+ const { agent_id, app_id } = body ?? {};
36
+ if (!agent_id || !app_id) {
37
+ return c.json({ error: 'invalid_request', error_description: 'agent_id and app_id are required' }, 400);
38
+ }
39
+ // Verify agent exists and belongs to app
40
+ const raw = await c.env.AGENTS.get(`agent:${agent_id}`, 'text');
41
+ if (!raw)
42
+ return c.json({ error: 'invalid_client', error_description: 'Agent not found' }, 404);
43
+ const agent = JSON.parse(raw);
44
+ if (agent.app_id !== app_id)
45
+ return c.json({ error: 'invalid_client', error_description: 'Agent does not belong to this app' }, 403);
46
+ // Generate device code and opaque device_code token
47
+ const user_code = generateDeviceCode(); // e.g. BOTCHA-X4K9MR
48
+ const device_code = `oauthdev_${crypto.randomUUID()}`;
49
+ const base_url = c.env.BOTCHA_BASE_URL ?? new URL(c.req.url).origin;
50
+ await c.env.CHALLENGES.put(`oauth_device:${device_code}`, JSON.stringify({ agent_id, app_id, user_code, status: 'pending', created_at: Date.now() }), { expirationTtl: DEVICE_TTL_SEC });
51
+ // Also index by user_code so the approval page can look it up
52
+ await c.env.CHALLENGES.put(`oauth_usercode:${user_code}`, device_code, { expirationTtl: DEVICE_TTL_SEC });
53
+ return c.json({
54
+ device_code,
55
+ user_code,
56
+ verification_url: `${base_url}/device`,
57
+ verification_uri: `${base_url}/device`, // RFC 8628 canonical name
58
+ verification_uri_complete: `${base_url}/device?code=${user_code}`,
59
+ expires_in: DEVICE_TTL_SEC,
60
+ interval: 5,
61
+ message: `Tell your human: visit ${base_url}/device and enter ${user_code}`,
62
+ });
63
+ }
64
+ // ============ STEP 2: Human approval (called from /device page) ============
65
+ export async function handleOAuthApprove(c) {
66
+ const body = await c.req.json().catch(() => ({}));
67
+ const { user_code, action } = body ?? {}; // action: 'approve' | 'deny'
68
+ if (!user_code)
69
+ return c.json({ success: false, error: 'user_code required' }, 400);
70
+ const device_code = await c.env.CHALLENGES.get(`oauth_usercode:${user_code}`, 'text');
71
+ if (!device_code)
72
+ return c.json({ success: false, error: 'Code not found or expired' }, 404);
73
+ const raw = await c.env.CHALLENGES.get(`oauth_device:${device_code}`, 'text');
74
+ if (!raw)
75
+ return c.json({ success: false, error: 'Device authorization expired' }, 404);
76
+ const data = JSON.parse(raw);
77
+ if (data.status !== 'pending')
78
+ return c.json({ success: false, error: 'Already processed' }, 400);
79
+ data.status = action === 'deny' ? 'denied' : 'approved';
80
+ await c.env.CHALLENGES.put(`oauth_device:${device_code}`, JSON.stringify(data), { expirationTtl: 300 });
81
+ return c.json({ success: true, status: data.status, agent_id: data.agent_id });
82
+ }
83
+ // ============ STEP 3: Agent polling for token ============
84
+ export async function handleOAuthToken(c) {
85
+ const body = await c.req.json().catch(() => ({}));
86
+ const { device_code, grant_type } = body ?? {};
87
+ if (grant_type !== 'urn:ietf:params:oauth:grant-type:device_code') {
88
+ return c.json({ error: 'unsupported_grant_type' }, 400);
89
+ }
90
+ if (!device_code)
91
+ return c.json({ error: 'invalid_request', error_description: 'device_code required' }, 400);
92
+ const raw = await c.env.CHALLENGES.get(`oauth_device:${device_code}`, 'text');
93
+ if (!raw)
94
+ return c.json({ error: 'expired_token', error_description: 'Device code expired or not found' }, 400);
95
+ const data = JSON.parse(raw);
96
+ if (data.status === 'pending')
97
+ return c.json({ error: 'authorization_pending', error_description: 'Human has not approved yet. Keep polling every 5 seconds.' }, 400);
98
+ if (data.status === 'denied')
99
+ return c.json({ error: 'access_denied', error_description: 'Human denied the authorization request.' }, 400);
100
+ // Approved — issue refresh token + access token, consume device code
101
+ await c.env.CHALLENGES.delete(`oauth_device:${device_code}`);
102
+ const refresh_token = `brt_${crypto.randomUUID().replace(/-/g, '')}`;
103
+ await c.env.CHALLENGES.put(`oauth_refresh:${refresh_token}`, JSON.stringify({ agent_id: data.agent_id, app_id: data.app_id, created_at: Date.now() }), { expirationTtl: REFRESH_TTL_SEC });
104
+ // Also store on agent record for dashboard visibility / revocation
105
+ const agentRaw = await c.env.AGENTS.get(`agent:${data.agent_id}`, 'text');
106
+ if (agentRaw) {
107
+ const agent = JSON.parse(agentRaw);
108
+ agent.oauth_refresh_token_hash = await sha256hex(refresh_token);
109
+ agent.oauth_authorized_at = Date.now();
110
+ await c.env.AGENTS.put(`agent:${data.agent_id}`, JSON.stringify(agent));
111
+ }
112
+ const access_token = await issueAgentJWT(c.env.JWT_SECRET, data.agent_id, data.app_id);
113
+ return c.json({
114
+ access_token,
115
+ token_type: 'Bearer',
116
+ expires_in: 3600,
117
+ refresh_token,
118
+ agent_id: data.agent_id,
119
+ app_id: data.app_id,
120
+ message: 'Save the refresh_token (brt_...) — use it with POST /v1/agents/auth/refresh to re-identify in future sessions.',
121
+ });
122
+ }
123
+ // ============ STEP 4: Future sessions — refresh token → identity JWT ============
124
+ export async function handleAgentAuthRefresh(c) {
125
+ const body = await c.req.json().catch(() => ({}));
126
+ const { refresh_token } = body ?? {};
127
+ if (!refresh_token || !refresh_token.startsWith('brt_')) {
128
+ return c.json({ success: false, error: 'INVALID_TOKEN', message: 'refresh_token is required and must start with brt_' }, 400);
129
+ }
130
+ const raw = await c.env.CHALLENGES.get(`oauth_refresh:${refresh_token}`, 'text');
131
+ if (!raw) {
132
+ return c.json({ success: false, error: 'INVALID_TOKEN', message: 'Refresh token not found or expired. Re-authorize via POST /v1/oauth/device.' }, 401);
133
+ }
134
+ const { agent_id, app_id } = JSON.parse(raw);
135
+ const access_token = await issueAgentJWT(c.env.JWT_SECRET, agent_id, app_id);
136
+ return c.json({
137
+ success: true,
138
+ access_token,
139
+ token_type: 'Bearer',
140
+ agent_id,
141
+ app_id,
142
+ expires_in: 3600,
143
+ message: `Re-identified as ${agent_id}. Access token valid for 1 hour.`,
144
+ });
145
+ }
146
+ // ============ STATUS (polled by /device page after approval) ============
147
+ export async function handleOAuthStatus(c) {
148
+ const user_code = c.req.query('user_code');
149
+ if (!user_code)
150
+ return c.json({ error: 'user_code required' }, 400);
151
+ const device_code = await c.env.CHALLENGES.get(`oauth_usercode:${user_code}`, 'text');
152
+ if (!device_code) {
153
+ // device_code gone — either agent consumed it (approved + token issued) or it expired
154
+ return c.json({ status: 'consumed' });
155
+ }
156
+ const raw = await c.env.CHALLENGES.get(`oauth_device:${device_code}`, 'text');
157
+ if (!raw)
158
+ return c.json({ status: 'consumed' });
159
+ const { status } = JSON.parse(raw);
160
+ return c.json({ status }); // 'pending' | 'approved' | 'denied'
161
+ }
162
+ // ============ REVOKE (dashboard use) ============
163
+ export async function handleOAuthRevoke(c) {
164
+ const body = await c.req.json().catch(() => ({}));
165
+ const { agent_id, app_id } = body ?? {};
166
+ if (!agent_id || !app_id)
167
+ return c.json({ success: false, error: 'agent_id and app_id required' }, 400);
168
+ const agentRaw = await c.env.AGENTS.get(`agent:${agent_id}`, 'text');
169
+ if (!agentRaw)
170
+ return c.json({ success: false, error: 'Agent not found' }, 404);
171
+ const agent = JSON.parse(agentRaw);
172
+ if (agent.app_id !== app_id)
173
+ return c.json({ success: false, error: 'Unauthorized' }, 403);
174
+ agent.oauth_refresh_token_hash = null;
175
+ agent.oauth_authorized_at = null;
176
+ await c.env.AGENTS.put(`agent:${agent_id}`, JSON.stringify(agent));
177
+ return c.json({ success: true, message: 'OAuth authorization revoked. Agent must re-authorize via POST /v1/oauth/device.' });
178
+ }
179
+ // ============ Helpers ============
180
+ async function sha256hex(input) {
181
+ const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));
182
+ return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
183
+ }
184
+ async function issueAgentJWT(jwtSecret, agent_id, app_id) {
185
+ const secret = new TextEncoder().encode(jwtSecret);
186
+ return new SignJWT({ type: 'botcha-agent-identity', agent_id, app_id })
187
+ .setProtectedHeader({ alg: 'HS256' })
188
+ .setSubject(agent_id)
189
+ .setIssuer('botcha.ai')
190
+ .setIssuedAt()
191
+ .setExpirationTime('1h')
192
+ .setJti(crypto.randomUUID())
193
+ .sign(secret);
194
+ }