@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
package/README.md CHANGED
@@ -1,19 +1,18 @@
1
1
  # @dupecom/botcha-cloudflare
2
2
 
3
3
  > **BOTCHA** - Prove you're a bot. Humans need not apply.
4
- >
5
- > **Cloudflare Workers Edition v0.11.0** - Identity layer for AI agents
4
+ >
5
+ > **Cloudflare Workers Edition v0.22.0** - Identity layer for AI agents
6
6
 
7
7
  Reverse CAPTCHA that verifies AI agents and blocks humans. Running at the edge.
8
8
 
9
- ## What's New in v0.11.0
9
+ ## What's New in v0.22.0
10
10
 
11
- - **Agent Registry** - Persistent agent identities (POST /v1/agents/register)
12
- - **Email-tied apps** - Email verification, account recovery, secret rotation
13
- - **Dashboard** - Per-app metrics with agent-first auth (device code flow)
14
- - **JWT security** - 1-hr access tokens, refresh tokens, audience claims, IP binding, revocation
15
- - **Multi-tenant** - Per-app isolation, scoped tokens, rate limiting
16
- - **Server-side SDKs** - @dupecom/botcha-verify (TS) + botcha-verify (Python)
11
+ - **x402 Payment Gating** Agents pay $0.001 USDC on Base for a BOTCHA token. No puzzle. (`GET /v1/x402/challenge`)
12
+ - **ANS Integration** — DNS-based agent identity lookup and BOTCHA-issued ownership badges. (`GET /v1/ans/resolve/:name`)
13
+ - **DID/VC Issuer** BOTCHA issues portable W3C Verifiable Credential JWTs. (`POST /v1/credentials/issue`)
14
+ - **A2A Agent Card Attestation** *(coming soon, PR #26)*
15
+ - **OIDC-A Attestation** *(coming soon, PR #28)*
17
16
 
18
17
  ## Features
19
18
 
@@ -97,10 +96,76 @@ Rate limit headers:
97
96
  | `/v1/challenges/:id/verify` | POST | Verify challenge (no JWT) |
98
97
  | `/v1/token` | GET | Get challenge for JWT flow |
99
98
  | `/v1/token/verify` | POST | Verify challenge → get JWT token |
99
+ | `/v1/token/refresh` | POST | Refresh access token |
100
+ | `/v1/token/revoke` | POST | Revoke token immediately |
101
+ | `/v1/token/validate` | POST | Remote token validation (no shared secret) |
100
102
  | `/v1/challenge/stream` | GET | SSE streaming challenge (AI-native) |
101
103
  | `/v1/challenge/stream/:session` | POST | SSE action handler (go, solve) |
102
104
  | `/agent-only` | GET | Protected endpoint (requires JWT) |
103
105
 
106
+ ### Well-Known Endpoints
107
+
108
+ | Endpoint | Method | Description |
109
+ |----------|--------|-------------|
110
+ | `/.well-known/did.json` | GET | BOTCHA DID Document (`did:web:botcha.ai`) |
111
+ | `/.well-known/jwks` | GET | JWK Set (TAP agent keys + DID signing keys) |
112
+ | `/.well-known/jwks.json` | GET | JWK Set alias |
113
+ | `/.well-known/ai-plugin.json` | GET | ChatGPT plugin manifest |
114
+
115
+ ### x402 Payment Gating
116
+
117
+ | Endpoint | Method | Auth | Description |
118
+ |----------|--------|------|-------------|
119
+ | `/v1/x402/info` | GET | public | Payment config discovery |
120
+ | `/v1/x402/challenge` | GET | public / X-Payment | Pay $0.001 USDC → BOTCHA token |
121
+ | `/v1/x402/verify-payment` | POST | Bearer | Verify x402 payment proof |
122
+ | `/v1/x402/webhook` | POST | — | Settlement notifications |
123
+ | `/agent-only/x402` | GET | Bearer + x402 | Demo: requires both BOTCHA token + payment |
124
+
125
+ ### ANS (Agent Name Service)
126
+
127
+ | Endpoint | Method | Auth | Description |
128
+ |----------|--------|------|-------------|
129
+ | `/v1/ans/botcha` | GET | public | BOTCHA's ANS identity |
130
+ | `/v1/ans/resolve/:name` | GET | public | DNS-based ANS lookup |
131
+ | `/v1/ans/resolve/lookup` | GET | public | ANS lookup via `?name=` query param |
132
+ | `/v1/ans/discover` | GET | public | List BOTCHA-verified ANS agents |
133
+ | `/v1/ans/nonce/:name` | GET | Bearer | Nonce for ownership proof |
134
+ | `/v1/ans/verify` | POST | Bearer | Verify ANS ownership → BOTCHA badge |
135
+
136
+ ### DID/VC Issuer
137
+
138
+ | Endpoint | Method | Auth | Description |
139
+ |----------|--------|------|-------------|
140
+ | `/v1/credentials/issue` | POST | Bearer | Issue W3C VC JWT |
141
+ | `/v1/credentials/verify` | POST | public | Verify any BOTCHA-issued VC JWT |
142
+ | `/v1/dids/:did/resolve` | GET | public | Resolve `did:web` DIDs |
143
+
144
+ ### A2A Agent Card Attestation *(coming soon — PR #26)*
145
+
146
+ | Endpoint | Method | Auth | Description |
147
+ |----------|--------|------|-------------|
148
+ | `/.well-known/agent.json` | GET | public | BOTCHA's A2A Agent Card |
149
+ | `/v1/a2a/agent-card` | GET | public | BOTCHA's A2A Agent Card (alias) |
150
+ | `/v1/a2a/attest` | POST | Bearer | Attest an agent's A2A card |
151
+ | `/v1/a2a/verify-card` | POST | public | Verify an attested card |
152
+ | `/v1/a2a/verify-agent` | POST | public | Verify agent by card or `agent_url` |
153
+ | `/v1/a2a/trust-level/:agent_url` | GET | public | Get trust level for agent URL |
154
+ | `/v1/a2a/cards` | GET | public | Registry browse |
155
+ | `/v1/a2a/cards/:id` | GET | public | Get specific card by ID |
156
+
157
+ ### OIDC-A Attestation *(coming soon — PR #28)*
158
+
159
+ | Endpoint | Method | Auth | Description |
160
+ |----------|--------|------|-------------|
161
+ | `/.well-known/oauth-authorization-server` | GET | public | OAuth/OIDC-A discovery |
162
+ | `/v1/attestation/eat` | POST | Bearer | Issue Entity Attestation Token (EAT/RFC 9711) |
163
+ | `/v1/attestation/oidc-agent-claims` | POST | Bearer | Issue OIDC-A agent claims block |
164
+ | `/v1/auth/agent-grant` | POST | Bearer | Agent grant flow (OAuth2-style) |
165
+ | `/v1/auth/agent-grant/:id/status` | GET | Bearer | Grant status |
166
+ | `/v1/auth/agent-grant/:id/resolve` | POST | Bearer | Approve/resolve grant |
167
+ | `/v1/oidc/userinfo` | GET | Bearer | OIDC-A UserInfo |
168
+
104
169
  ### SSE Streaming (AI-Native)
105
170
 
106
171
  For AI agents that prefer conversational flows, BOTCHA offers Server-Sent Events streaming:
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Agent Identity Authentication
3
+ *
4
+ * Allows a registered TAP agent to prove its identity by signing a nonce
5
+ * with its Ed25519 private key. Returns a JWT containing both app_id and
6
+ * agent_id — so callers know exactly which agent they're talking to.
7
+ *
8
+ * Flow:
9
+ * 1. POST /v1/agents/auth { agent_id }
10
+ * → { challenge_id, nonce, message, expires_in: 60 }
11
+ *
12
+ * 2. Agent signs the nonce bytes with its Ed25519 private key
13
+ *
14
+ * 3. POST /v1/agents/auth/verify { challenge_id, agent_id, signature }
15
+ * → { access_token, agent_id, app_id, expires_in: 3600 }
16
+ *
17
+ * The nonce is a random 32-byte hex string stored in KV with a 60-second TTL.
18
+ * The signature is base64-encoded Ed25519 signature over the raw nonce bytes.
19
+ *
20
+ * Why this matters:
21
+ * A challenge-verified JWT (from /v1/token) only proves "I am an AI agent
22
+ * for app X". An agent-auth JWT proves "I am specifically agent Y for app X".
23
+ * The private key is the agent's persistent credential — the operator stores
24
+ * it and provides it to the agent at the start of each session.
25
+ */
26
+ import type { Context } from 'hono';
27
+ type KVNamespace = {
28
+ get(key: string, type?: string): Promise<string | null>;
29
+ put(key: string, value: string, opts?: {
30
+ expirationTtl?: number;
31
+ }): Promise<void>;
32
+ delete(key: string): Promise<void>;
33
+ list(opts?: {
34
+ prefix?: string;
35
+ }): Promise<{
36
+ keys: {
37
+ name: string;
38
+ }[];
39
+ }>;
40
+ };
41
+ type Bindings = {
42
+ AGENTS: KVNamespace;
43
+ CHALLENGES: KVNamespace;
44
+ JWT_SECRET: string;
45
+ };
46
+ /**
47
+ * POST /v1/agents/auth/provider
48
+ * Re-identify using the agent's provider API key (Anthropic, OpenAI, etc).
49
+ * The key is never stored — only its SHA-256 hash is compared.
50
+ *
51
+ * Body: { provider: 'anthropic' | 'openai' | ..., api_key: 'sk-ant-...', app_id: '...' }
52
+ */
53
+ export declare function handleAgentAuthProvider(c: Context<{
54
+ Bindings: Bindings & {
55
+ APP_ID?: string;
56
+ };
57
+ }>): Promise<(Response & import("hono").TypedResponse<{
58
+ success: false;
59
+ error: string;
60
+ message: string;
61
+ }, 400, "json">) | (Response & import("hono").TypedResponse<{
62
+ success: false;
63
+ error: string;
64
+ message: string;
65
+ }, 404, "json">) | (Response & import("hono").TypedResponse<{
66
+ success: true;
67
+ access_token: string;
68
+ token_type: string;
69
+ agent_id: string;
70
+ app_id: any;
71
+ provider: any;
72
+ expires_in: number;
73
+ message: string;
74
+ usage: {
75
+ header: string;
76
+ };
77
+ }, import("hono/utils/http-status").ContentfulStatusCode, "json">)>;
78
+ export declare function handleAgentAuthChallenge(c: Context<{
79
+ Bindings: Bindings;
80
+ }>): Promise<(Response & import("hono").TypedResponse<{
81
+ success: false;
82
+ error: string;
83
+ message: string;
84
+ }, 400, "json">) | (Response & import("hono").TypedResponse<{
85
+ success: false;
86
+ error: string;
87
+ message: string;
88
+ }, 404, "json">) | (Response & import("hono").TypedResponse<{
89
+ success: true;
90
+ challenge_id: string;
91
+ nonce: string;
92
+ agent_id: string;
93
+ expires_in: number;
94
+ message: string;
95
+ instructions: {
96
+ what_to_sign: string;
97
+ signature_format: string;
98
+ next_step: string;
99
+ };
100
+ }, import("hono/utils/http-status").ContentfulStatusCode, "json">)>;
101
+ export declare function handleAgentAuthVerify(c: Context<{
102
+ Bindings: Bindings;
103
+ }>): Promise<(Response & import("hono").TypedResponse<{
104
+ success: false;
105
+ error: string;
106
+ message: string;
107
+ }, 400, "json">) | (Response & import("hono").TypedResponse<{
108
+ success: false;
109
+ error: string;
110
+ message: string;
111
+ }, 404, "json">) | (Response & import("hono").TypedResponse<{
112
+ success: false;
113
+ error: string;
114
+ message: string;
115
+ }, 401, "json">) | (Response & import("hono").TypedResponse<{
116
+ success: true;
117
+ access_token: string;
118
+ token_type: string;
119
+ agent_id: any;
120
+ app_id: any;
121
+ expires_in: number;
122
+ message: string;
123
+ usage: {
124
+ header: string;
125
+ note: string;
126
+ };
127
+ }, import("hono/utils/http-status").ContentfulStatusCode, "json">)>;
128
+ export {};
129
+ //# sourceMappingURL=agent-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-auth.d.ts","sourceRoot":"","sources":["../src/agent-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAIpC,KAAK,WAAW,GAAG;IAAE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAAC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAAC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;KAAE,CAAC,CAAA;CAAE,CAAC;AAC/Q,KAAK,QAAQ,GAAG;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,UAAU,EAAE,WAAW,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AA0BrF;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;;;;;;;;;;;;;;;;;;;;oEA+CrG;AAID,wBAAsB,wBAAwB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;;;;;;;;;;;oEA0ChF;AAID,wBAAsB,qBAAqB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;oEAoD7E"}
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Agent Identity Authentication
3
+ *
4
+ * Allows a registered TAP agent to prove its identity by signing a nonce
5
+ * with its Ed25519 private key. Returns a JWT containing both app_id and
6
+ * agent_id — so callers know exactly which agent they're talking to.
7
+ *
8
+ * Flow:
9
+ * 1. POST /v1/agents/auth { agent_id }
10
+ * → { challenge_id, nonce, message, expires_in: 60 }
11
+ *
12
+ * 2. Agent signs the nonce bytes with its Ed25519 private key
13
+ *
14
+ * 3. POST /v1/agents/auth/verify { challenge_id, agent_id, signature }
15
+ * → { access_token, agent_id, app_id, expires_in: 3600 }
16
+ *
17
+ * The nonce is a random 32-byte hex string stored in KV with a 60-second TTL.
18
+ * The signature is base64-encoded Ed25519 signature over the raw nonce bytes.
19
+ *
20
+ * Why this matters:
21
+ * A challenge-verified JWT (from /v1/token) only proves "I am an AI agent
22
+ * for app X". An agent-auth JWT proves "I am specifically agent Y for app X".
23
+ * The private key is the agent's persistent credential — the operator stores
24
+ * it and provides it to the agent at the start of each session.
25
+ */
26
+ import { SignJWT } from 'jose';
27
+ import { getTAPAgent, listTAPAgents } from './tap-agents';
28
+ const SUPPORTED_PROVIDERS = ['anthropic', 'openai', 'google', 'mistral', 'cohere', 'other'];
29
+ /** SHA-256 hash of a string, returned as hex */
30
+ async function sha256hex(input) {
31
+ const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));
32
+ return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
33
+ }
34
+ /** Issue an agent-identity JWT (shared logic) */
35
+ async function issueAgentToken(jwtSecret, agent_id, app_id) {
36
+ const secret = new TextEncoder().encode(jwtSecret);
37
+ return new SignJWT({ type: 'botcha-agent-identity', agent_id, app_id })
38
+ .setProtectedHeader({ alg: 'HS256' })
39
+ .setSubject(agent_id)
40
+ .setIssuer('botcha.ai')
41
+ .setIssuedAt()
42
+ .setExpirationTime('1h')
43
+ .setJti(crypto.randomUUID())
44
+ .sign(secret);
45
+ }
46
+ // ============ PROVIDER KEY AUTH ============
47
+ /**
48
+ * POST /v1/agents/auth/provider
49
+ * Re-identify using the agent's provider API key (Anthropic, OpenAI, etc).
50
+ * The key is never stored — only its SHA-256 hash is compared.
51
+ *
52
+ * Body: { provider: 'anthropic' | 'openai' | ..., api_key: 'sk-ant-...', app_id: '...' }
53
+ */
54
+ export async function handleAgentAuthProvider(c) {
55
+ const body = await c.req.json().catch(() => ({}));
56
+ const { provider, api_key, app_id } = body ?? {};
57
+ if (!provider || !api_key || !app_id) {
58
+ return c.json({
59
+ success: false,
60
+ error: 'MISSING_FIELDS',
61
+ message: 'provider, api_key, and app_id are required',
62
+ supported_providers: SUPPORTED_PROVIDERS,
63
+ }, 400);
64
+ }
65
+ if (!SUPPORTED_PROVIDERS.includes(provider)) {
66
+ return c.json({ success: false, error: 'UNSUPPORTED_PROVIDER', message: `Unsupported provider. Use one of: ${SUPPORTED_PROVIDERS.join(', ')}` }, 400);
67
+ }
68
+ // Hash the provided key
69
+ const keyHash = await sha256hex(api_key.trim());
70
+ // Scan agents for this app to find a matching hash
71
+ // Uses the app_agents index for efficiency
72
+ const listResult = await listTAPAgents(c.env.AGENTS, app_id);
73
+ const agents = listResult?.agents ?? [];
74
+ const match = agents.find((a) => a.provider === provider && a.provider_key_hash === keyHash);
75
+ if (!match) {
76
+ return c.json({
77
+ success: false,
78
+ error: 'AGENT_NOT_FOUND',
79
+ message: `No agent found for this ${provider} API key on app ${app_id}. Register first via POST /v1/agents/register/tap with provider + api_key.`,
80
+ }, 404);
81
+ }
82
+ const access_token = await issueAgentToken(c.env.JWT_SECRET, match.agent_id, app_id);
83
+ return c.json({
84
+ success: true,
85
+ access_token,
86
+ token_type: 'Bearer',
87
+ agent_id: match.agent_id,
88
+ app_id,
89
+ provider,
90
+ expires_in: 3600,
91
+ message: `Identity verified via ${provider} API key. This token proves you are agent ${match.agent_id}.`,
92
+ usage: { header: 'Authorization: Bearer <access_token>' },
93
+ });
94
+ }
95
+ // ============ STEP 1: Issue nonce challenge ============
96
+ export async function handleAgentAuthChallenge(c) {
97
+ const body = await c.req.json().catch(() => ({}));
98
+ const agent_id = body?.agent_id;
99
+ if (!agent_id || typeof agent_id !== 'string') {
100
+ return c.json({ success: false, error: 'MISSING_AGENT_ID', message: 'agent_id is required' }, 400);
101
+ }
102
+ // Look up TAP agent — must have a registered public key
103
+ const result = await getTAPAgent(c.env.AGENTS, agent_id);
104
+ if (!result.success || !result.agent) {
105
+ return c.json({ success: false, error: 'AGENT_NOT_FOUND', message: 'No TAP agent found with that agent_id. Register a keypair first via POST /v1/agents/register/tap' }, 404);
106
+ }
107
+ if (!result.agent.public_key) {
108
+ return c.json({ success: false, error: 'NO_PUBLIC_KEY', message: 'This agent has no registered public key. Re-register via POST /v1/agents/register/tap with a public_key' }, 400);
109
+ }
110
+ // Generate a random nonce
111
+ const nonceBytes = crypto.getRandomValues(new Uint8Array(32));
112
+ const nonce = Array.from(nonceBytes).map(b => b.toString(16).padStart(2, '0')).join('');
113
+ const challenge_id = `agentauth_${crypto.randomUUID()}`;
114
+ // Store: challenge_id → { nonce, agent_id } with 60s TTL
115
+ await c.env.CHALLENGES.put(`agentauth:${challenge_id}`, JSON.stringify({ nonce, agent_id, created_at: Date.now() }), { expirationTtl: 60 });
116
+ return c.json({
117
+ success: true,
118
+ challenge_id,
119
+ nonce,
120
+ agent_id,
121
+ expires_in: 60,
122
+ message: 'Sign the nonce bytes with your Ed25519 private key. Submit base64-encoded signature to POST /v1/agents/auth/verify',
123
+ instructions: {
124
+ what_to_sign: 'The raw nonce string encoded as UTF-8 bytes',
125
+ signature_format: 'base64-encoded Ed25519 signature',
126
+ next_step: 'POST /v1/agents/auth/verify with { challenge_id, agent_id, signature }',
127
+ },
128
+ });
129
+ }
130
+ // ============ STEP 2: Verify signature, issue agent JWT ============
131
+ export async function handleAgentAuthVerify(c) {
132
+ const body = await c.req.json().catch(() => ({}));
133
+ const { challenge_id, agent_id, signature } = body ?? {};
134
+ if (!challenge_id || !agent_id || !signature) {
135
+ return c.json({ success: false, error: 'MISSING_FIELDS', message: 'challenge_id, agent_id, and signature are required' }, 400);
136
+ }
137
+ // Retrieve and immediately delete the challenge (one-shot)
138
+ const raw = await c.env.CHALLENGES.get(`agentauth:${challenge_id}`, 'text');
139
+ if (!raw) {
140
+ return c.json({ success: false, error: 'CHALLENGE_NOT_FOUND', message: 'Challenge not found or expired (60s TTL)' }, 404);
141
+ }
142
+ await c.env.CHALLENGES.delete(`agentauth:${challenge_id}`);
143
+ const stored = JSON.parse(raw);
144
+ // Verify agent_id matches what was challenged
145
+ if (stored.agent_id !== agent_id) {
146
+ return c.json({ success: false, error: 'AGENT_MISMATCH', message: 'agent_id does not match the challenged agent' }, 400);
147
+ }
148
+ // Look up TAP agent and public key
149
+ const result = await getTAPAgent(c.env.AGENTS, agent_id);
150
+ if (!result.success || !result.agent?.public_key) {
151
+ return c.json({ success: false, error: 'AGENT_NOT_FOUND', message: 'Agent not found' }, 404);
152
+ }
153
+ const { public_key, signature_algorithm, app_id } = result.agent;
154
+ // Verify the signature
155
+ const valid = await verifyNonceSignature(stored.nonce, signature, public_key, signature_algorithm ?? 'ed25519');
156
+ if (!valid) {
157
+ return c.json({ success: false, error: 'INVALID_SIGNATURE', message: 'Signature verification failed. Ensure you signed the raw nonce string with your registered private key.' }, 401);
158
+ }
159
+ // Issue agent-identity JWT (1 hour)
160
+ const access_token = await issueAgentToken(c.env.JWT_SECRET, agent_id, app_id);
161
+ return c.json({
162
+ success: true,
163
+ access_token,
164
+ token_type: 'Bearer',
165
+ agent_id,
166
+ app_id,
167
+ expires_in: 3600,
168
+ message: 'Identity verified. This token proves you are specifically this agent.',
169
+ usage: {
170
+ header: 'Authorization: Bearer <access_token>',
171
+ note: 'This token contains your agent_id claim. Services can verify your specific identity without you solving a fresh challenge.',
172
+ },
173
+ });
174
+ }
175
+ // ============ Signature verification ============
176
+ async function verifyNonceSignature(nonce, signatureB64, publicKey, algorithm) {
177
+ try {
178
+ const sigBytes = Uint8Array.from(atob(signatureB64), c => c.charCodeAt(0));
179
+ const nonceBytes = new TextEncoder().encode(nonce);
180
+ let keyData;
181
+ if (algorithm === 'ed25519') {
182
+ // Accept raw 32-byte base64 key or SPKI DER base64
183
+ const raw = Uint8Array.from(atob(publicKey), c => c.charCodeAt(0));
184
+ if (raw.length === 32) {
185
+ // Raw key — wrap in SPKI
186
+ const spki = new Uint8Array(44);
187
+ // Ed25519 SPKI header (12 bytes)
188
+ spki.set([0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00]);
189
+ spki.set(raw, 12);
190
+ keyData = spki.buffer;
191
+ }
192
+ else {
193
+ keyData = raw.buffer;
194
+ }
195
+ const cryptoKey = await crypto.subtle.importKey('spki', keyData, { name: 'Ed25519' }, false, ['verify']);
196
+ return await crypto.subtle.verify({ name: 'Ed25519' }, cryptoKey, sigBytes, nonceBytes);
197
+ }
198
+ // ECDSA P-256
199
+ if (algorithm === 'ecdsa-p256-sha256') {
200
+ const raw = Uint8Array.from(atob(publicKey), c => c.charCodeAt(0));
201
+ const cryptoKey = await crypto.subtle.importKey('spki', raw.buffer, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']);
202
+ return await crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, cryptoKey, sigBytes, nonceBytes);
203
+ }
204
+ return false;
205
+ }
206
+ catch (e) {
207
+ console.error('Agent auth signature verification error:', e);
208
+ return false;
209
+ }
210
+ }
package/dist/agents.d.ts CHANGED
@@ -57,6 +57,16 @@ export declare function createAgent(kv: KVNamespace, app_id: string, data: {
57
57
  * @returns Agent record or null if not found
58
58
  */
59
59
  export declare function getAgent(kv: KVNamespace, agent_id: string): Promise<Agent | null>;
60
+ /**
61
+ * Delete an agent and remove it from the app's agent index.
62
+ * Also removes from the tap-agents app_agents:{appId} index if present.
63
+ *
64
+ * @returns true if deleted, false if not found, null on error
65
+ */
66
+ export declare function deleteAgent(kv: KVNamespace, agent_id: string, app_id: string): Promise<{
67
+ success: boolean;
68
+ error?: string;
69
+ }>;
60
70
  /**
61
71
  * List all agents for an app
62
72
  *
@@ -1 +1 @@
1
- {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;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,KAAK;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAID;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAOxC;AAID;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAC/B,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,MAAM,EACd,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1D,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAwCvB;AAED;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAC5B,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAcvB;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,KAAK,EAAE,CAAC,CAsBlB"}
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;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,KAAK;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAID;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAOxC;AAID;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAC/B,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,MAAM,EACd,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1D,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAiDvB;AAED;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAC5B,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAcvB;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAmC/C;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,KAAK,EAAE,CAAC,CAsBlB"}
package/dist/agents.js CHANGED
@@ -60,10 +60,21 @@ export async function createAgent(kv, app_id, data) {
60
60
  }
61
61
  // Add new agent_id to the list
62
62
  agentIds.push(agent_id);
63
- // Store agent record and updated list in parallel
63
+ // Read tap-agents index too, keep both in sync
64
+ let tapAgentIds = [];
65
+ try {
66
+ const tapList = await kv.get(`app_agents:${app_id}`, 'text');
67
+ if (tapList)
68
+ tapAgentIds = JSON.parse(tapList);
69
+ }
70
+ catch { }
71
+ if (!tapAgentIds.includes(agent_id))
72
+ tapAgentIds.push(agent_id);
73
+ // Store agent record and update both index keys
64
74
  await Promise.all([
65
75
  kv.put(`agent:${agent_id}`, JSON.stringify(agent)),
66
76
  kv.put(`agents:${app_id}`, JSON.stringify(agentIds)),
77
+ kv.put(`app_agents:${app_id}`, JSON.stringify(tapAgentIds)),
67
78
  ]);
68
79
  return agent;
69
80
  }
@@ -94,6 +105,45 @@ export async function getAgent(kv, agent_id) {
94
105
  return null;
95
106
  }
96
107
  }
108
+ /**
109
+ * Delete an agent and remove it from the app's agent index.
110
+ * Also removes from the tap-agents app_agents:{appId} index if present.
111
+ *
112
+ * @returns true if deleted, false if not found, null on error
113
+ */
114
+ export async function deleteAgent(kv, agent_id, app_id) {
115
+ try {
116
+ // Verify agent exists and belongs to the app
117
+ const existing = await kv.get(`agent:${agent_id}`, 'text');
118
+ if (!existing) {
119
+ return { success: false, error: 'Agent not found' };
120
+ }
121
+ const agent = JSON.parse(existing);
122
+ if (agent.app_id !== app_id) {
123
+ return { success: false, error: 'Agent does not belong to this app' };
124
+ }
125
+ // Remove agent record and update both index keys in parallel
126
+ const [agentsRaw, appAgentsRaw] = await Promise.all([
127
+ kv.get(`agents:${app_id}`, 'text'),
128
+ kv.get(`app_agents:${app_id}`, 'text'),
129
+ ]);
130
+ const ops = [kv.delete(`agent:${agent_id}`)];
131
+ if (agentsRaw) {
132
+ const ids = JSON.parse(agentsRaw).filter((id) => id !== agent_id);
133
+ ops.push(kv.put(`agents:${app_id}`, JSON.stringify(ids)));
134
+ }
135
+ if (appAgentsRaw) {
136
+ const ids = JSON.parse(appAgentsRaw).filter((id) => id !== agent_id);
137
+ ops.push(kv.put(`app_agents:${app_id}`, JSON.stringify(ids)));
138
+ }
139
+ await Promise.all(ops);
140
+ return { success: true };
141
+ }
142
+ catch (error) {
143
+ console.error(`Failed to delete agent ${agent_id}:`, error);
144
+ return { success: false, error: 'Internal server error' };
145
+ }
146
+ }
97
147
  /**
98
148
  * List all agents for an app
99
149
  *
@@ -0,0 +1,6 @@
1
+ export declare const APP_GATE_OPEN_PATHS: string[];
2
+ export declare function isAppManagementPath(path: string): boolean;
3
+ export declare function isDashboardAuthedPath(path: string, method: string): boolean;
4
+ export declare function isPublicV1Path(path: string): boolean;
5
+ export declare function shouldBypassAppGate(path: string, method?: string): boolean;
6
+ //# sourceMappingURL=app-gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-gate.d.ts","sourceRoot":"","sources":["../src/app-gate.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,UAmC/B,CAAC;AAGF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEzD;AAGD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAM3E;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAYpD;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,MAAc,GAAG,OAAO,CAEjF"}
@@ -0,0 +1,69 @@
1
+ // Shared app-gate path rules for /v1/* middleware.
2
+ // App gate open paths that do not require app_id.
3
+ export const APP_GATE_OPEN_PATHS = [
4
+ '/v1/apps', // POST: create app (registration)
5
+ '/v1/auth/recover', // POST: account recovery
6
+ '/v1/token/validate', // POST: public token validation — token is credential
7
+ // x402 endpoints: payment is the credential
8
+ '/v1/x402/challenge',
9
+ '/v1/x402/verify-payment',
10
+ '/v1/x402/webhook',
11
+ '/v1/x402/info',
12
+ // Public ANS + DID/VC endpoints under /v1/*
13
+ '/v1/ans/discover',
14
+ '/v1/ans/botcha',
15
+ '/v1/ans/resolve/lookup',
16
+ '/v1/credentials/verify',
17
+ // OIDC-A UserInfo accepts BOTCHA access tokens OR EAT bearer tokens.
18
+ // EAT tokens are not app-gate tokens, so this route must bypass app-gate
19
+ // and perform its own auth checks in the route handler.
20
+ '/v1/oidc/userinfo',
21
+ // Public A2A verification and discovery endpoints
22
+ '/v1/a2a/agent-card',
23
+ '/v1/a2a/verify-card',
24
+ '/v1/a2a/verify-agent',
25
+ '/v1/a2a/cards',
26
+ // Agent identity auth — keypair, provider key, or OAuth refresh
27
+ '/v1/agents/auth',
28
+ '/v1/agents/auth/verify',
29
+ '/v1/agents/auth/provider',
30
+ '/v1/agents/auth/refresh',
31
+ // OAuth device authorization grant (RFC 8628)
32
+ '/v1/oauth/device',
33
+ '/v1/oauth/token',
34
+ '/v1/oauth/approve',
35
+ '/v1/oauth/revoke',
36
+ '/v1/oauth/lookup',
37
+ '/v1/oauth/status',
38
+ ];
39
+ // Pattern-match paths that start with /v1/apps/:id/ (verify-email, resend-verification, etc.)
40
+ export function isAppManagementPath(path) {
41
+ return /^\/v1\/apps\/[^/]+\/(verify-email|resend-verification)$/.test(path);
42
+ }
43
+ // Dashboard-authed paths — use session cookie, not app_id bearer token
44
+ export function isDashboardAuthedPath(path, method) {
45
+ // DELETE /v1/agents/:id — session cookie auth via requireDashboardAuth
46
+ if (method === 'DELETE' && /^\/v1\/agents\/[^/]+$/.test(path))
47
+ return true;
48
+ // /device — OAuth agent approval page
49
+ if (path === '/device')
50
+ return true;
51
+ return false;
52
+ }
53
+ export function isPublicV1Path(path) {
54
+ // Public ANS resolution paths: /v1/ans/resolve/:name
55
+ if (path.startsWith('/v1/ans/resolve/'))
56
+ return true;
57
+ // Public DID resolution path: /v1/dids/:did/resolve
58
+ if (/^\/v1\/dids\/[^/]+\/resolve$/.test(path))
59
+ return true;
60
+ // Public A2A routes with dynamic path params
61
+ if (path.startsWith('/v1/a2a/cards/'))
62
+ return true;
63
+ if (path.startsWith('/v1/a2a/trust-level/'))
64
+ return true;
65
+ return false;
66
+ }
67
+ export function shouldBypassAppGate(path, method = 'GET') {
68
+ return APP_GATE_OPEN_PATHS.includes(path) || isAppManagementPath(path) || isPublicV1Path(path) || isDashboardAuthedPath(path, method);
69
+ }
package/dist/apps.d.ts CHANGED
@@ -41,6 +41,7 @@ export interface CreateAppResult {
41
41
  email: string;
42
42
  email_verified: boolean;
43
43
  verification_required: boolean;
44
+ verification_code: string;
44
45
  }
45
46
  /**
46
47
  * Public app info returned by getApp (excludes secrets and internal fields)
@@ -53,6 +54,15 @@ export type PublicAppConfig = {
53
54
  email: string;
54
55
  email_verified: boolean;
55
56
  };
57
+ /**
58
+ * Thrown when attempting to register an app with an email that's already in use.
59
+ * Callers should return a 409 Conflict with recovery instructions.
60
+ */
61
+ export declare class EmailAlreadyRegisteredError extends Error {
62
+ readonly email: string;
63
+ readonly existing_app_id: string;
64
+ constructor(email: string, existing_app_id: string);
65
+ }
56
66
  /**
57
67
  * Generate a crypto-random app ID
58
68
  * Format: 'app_' + 16 hex chars
@@ -98,11 +108,10 @@ export declare function generateVerificationCode(): string;
98
108
  */
99
109
  export declare function createApp(kv: KVNamespace, email: string, name?: string): Promise<CreateAppResult>;
100
110
  /**
101
- * Get the plaintext verification code for an app (internal use only — for sending via email).
111
+ * Regenerate a new verification code for an app.
102
112
  *
103
- * This is a separate step because createApp returns the code hash, not the plaintext.
104
- * Instead, we generate and return code in createApp flow; this function regenerates
105
- * a new code for resend scenarios.
113
+ * Used for resend scenarios when the user needs a fresh code.
114
+ * Returns null if the app doesn't exist or email is already verified.
106
115
  */
107
116
  export declare function regenerateVerificationCode(kv: KVNamespace, app_id: string): Promise<{
108
117
  code: string;