@key0ai/key0 0.1.0 → 0.2.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 (54) hide show
  1. package/README.md +66 -21
  2. package/dist/__tests__/e2e.test.js +1 -1
  3. package/dist/__tests__/e2e.test.js.map +1 -1
  4. package/dist/__tests__/x402-http-middleware.test.js +4 -207
  5. package/dist/__tests__/x402-http-middleware.test.js.map +1 -1
  6. package/dist/core/__tests__/agent-card.test.js +14 -12
  7. package/dist/core/__tests__/agent-card.test.js.map +1 -1
  8. package/dist/core/__tests__/storage-postgres.test.js +0 -1
  9. package/dist/core/__tests__/storage-postgres.test.js.map +1 -1
  10. package/dist/core/agent-card.d.ts.map +1 -1
  11. package/dist/core/agent-card.js +25 -10
  12. package/dist/core/agent-card.js.map +1 -1
  13. package/dist/core/challenge-engine.js +1 -1
  14. package/dist/core/challenge-engine.js.map +1 -1
  15. package/dist/core/index.d.ts +1 -1
  16. package/dist/core/index.d.ts.map +1 -1
  17. package/dist/core/index.js +1 -1
  18. package/dist/core/index.js.map +1 -1
  19. package/dist/integrations/express.d.ts +6 -4
  20. package/dist/integrations/express.d.ts.map +1 -1
  21. package/dist/integrations/express.js +27 -29
  22. package/dist/integrations/express.js.map +1 -1
  23. package/dist/integrations/fastify.d.ts +5 -1
  24. package/dist/integrations/fastify.d.ts.map +1 -1
  25. package/dist/integrations/fastify.js +117 -8
  26. package/dist/integrations/fastify.js.map +1 -1
  27. package/dist/integrations/hono.d.ts +8 -2
  28. package/dist/integrations/hono.d.ts.map +1 -1
  29. package/dist/integrations/hono.js +116 -12
  30. package/dist/integrations/hono.js.map +1 -1
  31. package/dist/integrations/settlement.d.ts.map +1 -1
  32. package/dist/integrations/settlement.js +1 -3
  33. package/dist/integrations/settlement.js.map +1 -1
  34. package/dist/types/agent-card.d.ts +16 -0
  35. package/dist/types/agent-card.d.ts.map +1 -1
  36. package/package.json +3 -1
  37. package/src/__tests__/e2e.test.ts +1 -1
  38. package/src/__tests__/x402-http-middleware.test.ts +4 -256
  39. package/src/core/__tests__/agent-card.test.ts +15 -12
  40. package/src/core/__tests__/storage-postgres.test.ts +0 -2
  41. package/src/core/agent-card.ts +26 -10
  42. package/src/core/challenge-engine.ts +1 -1
  43. package/src/core/index.ts +1 -1
  44. package/src/integrations/express.ts +221 -235
  45. package/src/integrations/fastify.ts +160 -8
  46. package/src/integrations/hono.ts +168 -12
  47. package/src/integrations/settlement.ts +1 -3
  48. package/src/types/agent-card.ts +13 -2
  49. package/src/types/config.ts +1 -1
  50. package/dist/integrations/x402-http-middleware.d.ts +0 -15
  51. package/dist/integrations/x402-http-middleware.d.ts.map +0 -1
  52. package/dist/integrations/x402-http-middleware.js +0 -171
  53. package/dist/integrations/x402-http-middleware.js.map +0 -1
  54. package/src/integrations/x402-http-middleware.ts +0 -246
@@ -1,10 +1,5 @@
1
1
  import { AGENT_CARD_PATH } from "@a2a-js/sdk";
2
- import {
3
- agentCardHandler,
4
- jsonRpcHandler,
5
- restHandler,
6
- UserBuilder,
7
- } from "@a2a-js/sdk/server/express";
2
+ import { agentCardHandler, jsonRpcHandler, UserBuilder } from "@a2a-js/sdk/server/express";
8
3
  import { type NextFunction, type Request, type Response, Router } from "express";
9
4
  import { createKey0, type Key0Config } from "../factory.js";
10
5
  import type { ValidateAccessTokenConfig } from "../middleware.js";
@@ -15,21 +10,22 @@ import { mountMcpRoutes } from "./mcp.js";
15
10
  import {
16
11
  buildDiscoveryResponse,
17
12
  buildHttpPaymentRequirements,
18
- createX402HttpMiddleware,
19
13
  decodePaymentSignature,
20
14
  settlePayment,
21
- } from "./x402-http-middleware.js";
15
+ } from "./settlement.js";
22
16
 
23
17
  /**
24
- * Create an Express router that serves the agent card and A2A endpoint.
18
+ * Create an Express router that serves the agent card and the unified x402 endpoint.
25
19
  *
26
20
  * Usage:
27
21
  * app.use(key0Router({ config, adapter }));
28
22
  *
29
23
  * This auto-serves:
30
- * GET /.well-known/agent.json
31
- * POST {config.basePath}/jsonrpc (A2A JSON-RPC)
32
- * POST {config.basePath}/access (Simple x402 HTTP)
24
+ * GET /.well-known/agent.json — A2A agent card (discovery)
25
+ * POST /x402/access — unified endpoint
26
+ * • X-A2A-Extensions header present delegates to A2A JSON-RPC handler
27
+ * • No header → x402 HTTP flow (discovery / challenge / settle)
28
+ * POST /mcp — MCP Streamable HTTP (when mcp: true)
33
29
  */
34
30
  export function key0Router(opts: Key0Config): Router {
35
31
  const { requestHandler, engine } = createKey0(opts);
@@ -40,255 +36,245 @@ export function key0Router(opts: Key0Config): Router {
40
36
  router.use(`/${AGENT_CARD_PATH}`, agentCardHandler({ agentCardProvider: requestHandler }));
41
37
  router.use("/.well-known/agent.json", agentCardHandler({ agentCardProvider: requestHandler }));
42
38
 
43
- // A2A endpoint with x402 middleware
44
- const basePath = opts.config.basePath ?? "/a2a";
45
- router.use(
46
- `${basePath}/jsonrpc`,
47
- createX402HttpMiddleware(engine, opts.config), // x402 HTTP middleware (before A2A handler)
48
- jsonRpcHandler({ requestHandler, userBuilder: UserBuilder.noAuthentication }),
49
- );
50
- router.use(
51
- `${basePath}/rest`,
52
- restHandler({ requestHandler, userBuilder: UserBuilder.noAuthentication }),
53
- );
39
+ // Unified x402 endpoint with A2A JSON-RPC fallback
40
+ // When X-A2A-Extensions header is present, the request is delegated to the
41
+ // A2A JSON-RPC handler (executor). Otherwise, it is handled as plain x402 HTTP.
42
+ router.post(
43
+ `/x402/access`,
44
+ async (req: Request, res: Response, next: NextFunction) => {
45
+ // ===== A2A-native clients: delegate to JSON-RPC handler =====
46
+ if (req.headers["x-a2a-extensions"]) {
47
+ console.log(
48
+ "[x402-access] X-A2A-Extensions header detected → delegating to JSON-RPC handler",
49
+ );
50
+ return next();
51
+ }
54
52
 
55
- // Simple x402 HTTP endpoint (no JSON-RPC wrapping)
56
- router.post(`/x402/access`, async (req: Request, res: Response) => {
57
- const startTime = Date.now();
58
- try {
59
- console.log("\n[x402-access] ========== NEW REQUEST ==========");
60
- console.log(`[x402-access] Timestamp: ${new Date().toISOString()}`);
61
- console.log(`[x402-access] Method: ${req.method}`);
62
- console.log(`[x402-access] URL: ${req.url}`);
63
- console.log("[x402-access] Body:", JSON.stringify(req.body, null, 2));
64
- console.log("[x402-access] Headers:", JSON.stringify(req.headers, null, 2));
65
-
66
- // Parse body (allow empty for discovery)
67
- const body = req.body || {};
68
- let { planId, resourceId = "default" } = body;
69
- let { requestId } = body;
70
-
71
- // Check for PAYMENT-SIGNATURE header
72
- const paymentSignature = req.headers["payment-signature"] as string | undefined;
73
- console.log(`[x402-access] PAYMENT-SIGNATURE present: ${!!paymentSignature}`);
74
-
75
- // ===== x402 shortcut: extract planId from PAYMENT-SIGNATURE if not in body =====
76
- // Standard x402 clients replay the same request with PAYMENT-SIGNATURE header.
77
- // The planId is embedded in accepted.extra.planId within the signed payload.
78
- if (!planId && paymentSignature) {
79
- try {
80
- const decoded = decodePaymentSignature(paymentSignature);
81
- const sigTierId = decoded.accepted?.extra?.["planId"] as string | undefined;
82
- if (sigTierId) {
53
+ const startTime = Date.now();
54
+ try {
55
+ console.log("\n[x402-access] ========== NEW REQUEST ==========");
56
+ console.log(`[x402-access] Timestamp: ${new Date().toISOString()}`);
57
+ console.log(`[x402-access] Method: ${req.method}`);
58
+ console.log(`[x402-access] URL: ${req.url}`);
59
+ console.log("[x402-access] Body:", JSON.stringify(req.body, null, 2));
60
+ console.log("[x402-access] Headers:", JSON.stringify(req.headers, null, 2));
61
+
62
+ // Parse body (allow empty for discovery)
63
+ const body = req.body || {};
64
+ let { planId, resourceId = "default" } = body;
65
+ let { requestId } = body;
66
+
67
+ // Check for PAYMENT-SIGNATURE header
68
+ const paymentSignature = req.headers["payment-signature"] as string | undefined;
69
+ console.log(`[x402-access] PAYMENT-SIGNATURE present: ${!!paymentSignature}`);
70
+
71
+ // ===== x402 shortcut: extract planId from PAYMENT-SIGNATURE if not in body =====
72
+ // Standard x402 clients replay the same request with PAYMENT-SIGNATURE header.
73
+ // The planId is embedded in accepted.extra.planId within the signed payload.
74
+ if (!planId && paymentSignature) {
75
+ try {
76
+ const decoded = decodePaymentSignature(paymentSignature);
77
+ const sigTierId = decoded.accepted?.extra?.["planId"] as string | undefined;
78
+ if (sigTierId) {
79
+ console.log(
80
+ `[x402-access] ✓ Extracted planId="${sigTierId}" from PAYMENT-SIGNATURE (x402 replay)`,
81
+ );
82
+ planId = sigTierId;
83
+ }
84
+ } catch (err) {
83
85
  console.log(
84
- `[x402-access] Extracted planId="${sigTierId}" from PAYMENT-SIGNATURE (x402 replay)`,
86
+ `[x402-access] Failed to decode PAYMENT-SIGNATURE for planId extraction: ${err}`,
85
87
  );
86
- planId = sigTierId;
88
+ // Fall through to discovery — the signature might be malformed
87
89
  }
88
- } catch (err) {
89
- console.log(
90
- `[x402-access] ⚠ Failed to decode PAYMENT-SIGNATURE for planId extraction: ${err}`,
91
- );
92
- // Fall through to discovery — the signature might be malformed
93
90
  }
94
- }
95
-
96
- // ===== CASE 1: No planId → Discovery (402 with all tiers, no PENDING record) =====
97
- if (!planId) {
98
- console.log("[x402-access] → CASE 1: No planId provided, returning discovery 402");
99
-
100
- const discoveryResponse = buildDiscoveryResponse(opts.config, networkConfig);
101
- console.log(
102
- "[x402-access] Discovery response:",
103
- JSON.stringify(discoveryResponse, null, 2),
104
- );
105
-
106
- // Encode as base64 for PAYMENT-REQUIRED header
107
- const encoded = Buffer.from(JSON.stringify(discoveryResponse)).toString("base64");
108
- res.setHeader("payment-required", encoded);
109
91
 
110
- // Add www-authenticate header for HTTP spec compliance
111
- const authHeader = `Payment realm="${opts.config.agentUrl}", accept="exact"`;
112
- res.setHeader("www-authenticate", authHeader);
113
-
114
- console.log(`[x402-access] payment-required header set (${encoded.length} bytes)`);
115
- console.log(`[x402-access] www-authenticate header set`);
116
- console.log("[x402-access] → Returning HTTP 402 Payment Required (Discovery)");
117
-
118
- return res.status(402).json({
119
- ...discoveryResponse,
120
- error: "Payment required",
121
- });
122
- }
123
-
124
- // Auto-generate requestId if not provided (for simplicity)
125
- if (!requestId) {
126
- requestId = `http-${crypto.randomUUID()}`;
127
- console.log(`[x402-access] Auto-generated requestId: ${requestId}`);
128
- }
92
+ // ===== CASE 1: No planId 400 pointing to GET /discovery =====
93
+ if (!planId) {
94
+ return res.status(400).json({
95
+ error:
96
+ "Please select a plan from the discovery API response to purchase access. Endpoint: GET /discovery",
97
+ });
98
+ }
129
99
 
130
- console.log(
131
- `[x402-access] Parsed request - planId: ${planId}, requestId: ${requestId}, resourceId: ${resourceId}`,
132
- );
100
+ // Auto-generate requestId if not provided (for simplicity)
101
+ if (!requestId) {
102
+ requestId = `http-${crypto.randomUUID()}`;
103
+ console.log(`[x402-access] Auto-generated requestId: ${requestId}`);
104
+ }
133
105
 
134
- // ===== CASE 2: planId present, no PAYMENT-SIGNATURE → Challenge (402 + PENDING record) =====
135
- if (!paymentSignature) {
136
106
  console.log(
137
- "[x402-access] CASE 2: planId provided, no PAYMENT-SIGNATURE, issuing 402 challenge",
107
+ `[x402-access] Parsed request - planId: ${planId}, requestId: ${requestId}, resourceId: ${resourceId}`,
138
108
  );
139
- console.log(`[x402-access] Creating PENDING record for requestId: ${requestId}`);
140
109
 
141
- // Create PENDING record via engine (handles tier/resource validation and idempotency)
142
- const { challengeId } = await engine.requestHttpAccess(requestId, planId, resourceId);
143
- console.log(`[x402-access] ✓ PENDING record created, challengeId=${challengeId}`);
144
-
145
- // Build payment requirements with schema
146
- console.log(`[x402-access] Building payment requirements for tier: ${planId}`);
147
- const requirements: X402PaymentRequiredResponse = buildHttpPaymentRequirements(
148
- planId,
149
- resourceId,
150
- opts.config,
151
- networkConfig,
152
- {
153
- inputSchema: {
154
- type: "object",
155
- properties: {
156
- planId: {
157
- type: "string",
158
- description: `Tier to purchase. Must be '${planId}'`,
159
- },
160
- requestId: {
161
- type: "string",
162
- description: "Client-generated UUID for idempotency (auto-generated if omitted)",
163
- },
164
- resourceId: {
165
- type: "string",
166
- description: "Optional: Specific resource identifier (defaults to 'default')",
110
+ // ===== CASE 2: planId present, no PAYMENT-SIGNATURE → Challenge (402 + PENDING record) =====
111
+ if (!paymentSignature) {
112
+ console.log(
113
+ "[x402-access] → CASE 2: planId provided, no PAYMENT-SIGNATURE, issuing 402 challenge",
114
+ );
115
+ console.log(`[x402-access] Creating PENDING record for requestId: ${requestId}`);
116
+
117
+ // Create PENDING record via engine (handles tier/resource validation and idempotency)
118
+ const { challengeId } = await engine.requestHttpAccess(requestId, planId, resourceId);
119
+ console.log(`[x402-access] ✓ PENDING record created, challengeId=${challengeId}`);
120
+
121
+ // Build payment requirements with schema
122
+ console.log(`[x402-access] Building payment requirements for tier: ${planId}`);
123
+ const requirements: X402PaymentRequiredResponse = buildHttpPaymentRequirements(
124
+ planId,
125
+ resourceId,
126
+ opts.config,
127
+ networkConfig,
128
+ {
129
+ inputSchema: {
130
+ type: "object",
131
+ properties: {
132
+ planId: {
133
+ type: "string",
134
+ description: `Tier to purchase. Must be '${planId}'`,
135
+ },
136
+ requestId: {
137
+ type: "string",
138
+ description:
139
+ "Client-generated UUID for idempotency (auto-generated if omitted)",
140
+ },
141
+ resourceId: {
142
+ type: "string",
143
+ description: "Optional: Specific resource identifier (defaults to 'default')",
144
+ },
167
145
  },
146
+ required: ["planId"],
168
147
  },
169
- required: ["planId"],
170
- },
171
- outputSchema: {
172
- type: "object",
173
- properties: {
174
- accessToken: { type: "string", description: "JWT token for API access" },
175
- tokenType: { type: "string", description: "Token type (usually 'Bearer')" },
176
- expiresAt: { type: "string", description: "ISO 8601 expiration timestamp" },
177
- resourceEndpoint: {
178
- type: "string",
179
- description: "URL to access the protected resource",
148
+ outputSchema: {
149
+ type: "object",
150
+ properties: {
151
+ accessToken: { type: "string", description: "JWT token for API access" },
152
+ tokenType: { type: "string", description: "Token type (usually 'Bearer')" },
153
+ expiresAt: { type: "string", description: "ISO 8601 expiration timestamp" },
154
+ resourceEndpoint: {
155
+ type: "string",
156
+ description: "URL to access the protected resource",
157
+ },
158
+ txHash: { type: "string", description: "On-chain transaction hash" },
159
+ explorerUrl: { type: "string", description: "Blockchain explorer URL" },
180
160
  },
181
- txHash: { type: "string", description: "On-chain transaction hash" },
182
- explorerUrl: { type: "string", description: "Blockchain explorer URL" },
183
161
  },
162
+ description: `Access to ${resourceId} via ${planId} tier`,
184
163
  },
185
- description: `Access to ${resourceId} via ${planId} tier`,
186
- },
187
- );
188
- console.log(`[x402-access] Payment requirements:`, JSON.stringify(requirements, null, 2));
164
+ );
165
+ console.log(`[x402-access] Payment requirements:`, JSON.stringify(requirements, null, 2));
166
+
167
+ // Encode as base64 for payment-required header
168
+ const encoded = Buffer.from(JSON.stringify(requirements)).toString("base64");
169
+ res.setHeader("payment-required", encoded);
170
+
171
+ // Add www-authenticate header
172
+ const authHeader = `Payment realm="${opts.config.agentUrl}", accept="exact", challenge="${challengeId}"`;
173
+ res.setHeader("www-authenticate", authHeader);
174
+
175
+ console.log(`[x402-access] payment-required header set (${encoded.length} bytes)`);
176
+ console.log(`[x402-access] www-authenticate header set`);
177
+ console.log("[x402-access] → Returning HTTP 402 Payment Required (Challenge)");
178
+
179
+ return res.status(402).json({
180
+ ...requirements,
181
+ challengeId,
182
+ requestId,
183
+ error: "Payment required",
184
+ });
185
+ }
189
186
 
190
- // Encode as base64 for payment-required header
191
- const encoded = Buffer.from(JSON.stringify(requirements)).toString("base64");
192
- res.setHeader("payment-required", encoded);
187
+ // ===== CASE 3: planId + PAYMENT-SIGNATURE → Settle and return access grant =====
188
+ console.log("[x402-access] CASE 3: Processing payment");
189
+ console.log(
190
+ `[x402-access] PAYMENT-SIGNATURE header received (${paymentSignature.length} bytes)`,
191
+ );
193
192
 
194
- // Add www-authenticate header
195
- const authHeader = `Payment realm="${opts.config.agentUrl}", accept="exact", challenge="${challengeId}"`;
196
- res.setHeader("www-authenticate", authHeader);
193
+ // Pre-settlement check: avoid burning USDC if already delivered/expired/cancelled
194
+ console.log("[x402-access] Pre-settlement check for requestId:", requestId);
195
+ const existingGrant = await engine.preSettlementCheck(requestId);
196
+ if (existingGrant) {
197
+ console.log("[x402-access] ✓ Already delivered, returning cached grant");
198
+ return res.status(200).json(existingGrant);
199
+ }
197
200
 
198
- console.log(`[x402-access] payment-required header set (${encoded.length} bytes)`);
199
- console.log(`[x402-access] www-authenticate header set`);
200
- console.log("[x402-access] Returning HTTP 402 Payment Required (Challenge)");
201
+ // Decode header then settle via shared settlement layer
202
+ console.log("[x402-access] Decoding payment signature...");
203
+ const paymentPayload = decodePaymentSignature(paymentSignature);
204
+ console.log(
205
+ "[x402-access] Payment payload decoded:",
206
+ JSON.stringify(paymentPayload, null, 2),
207
+ );
201
208
 
202
- return res.status(402).json({
203
- ...requirements,
204
- challengeId,
205
- error: "Payment required",
206
- });
207
- }
209
+ console.log("[x402-access] Settling payment on-chain...");
210
+ const { txHash, settleResponse, payer } = await settlePayment(
211
+ paymentPayload,
212
+ opts.config,
213
+ networkConfig,
214
+ );
208
215
 
209
- // ===== CASE 3: planId + PAYMENT-SIGNATURE Settle and return access grant =====
210
- console.log("[x402-access] CASE 3: Processing payment");
211
- console.log(
212
- `[x402-access] PAYMENT-SIGNATURE header received (${paymentSignature.length} bytes)`,
213
- );
214
-
215
- // Pre-settlement check: avoid burning USDC if already delivered/expired/cancelled
216
- console.log("[x402-access] Pre-settlement check for requestId:", requestId);
217
- const existingGrant = await engine.preSettlementCheck(requestId);
218
- if (existingGrant) {
219
- console.log("[x402-access] ✓ Already delivered, returning cached grant");
220
- return res.status(200).json(existingGrant);
221
- }
216
+ console.log(`[x402-access] Payment settled successfully`);
217
+ console.log(`[x402-access] - Transaction Hash: ${txHash}`);
218
+ console.log(`[x402-access] - Payer: ${payer}`);
219
+ console.log(`[x402-access] - Settle Response:`, JSON.stringify(settleResponse, null, 2));
222
220
 
223
- // Decode header then settle via shared settlement layer
224
- console.log("[x402-access] Decoding payment signature...");
225
- const paymentPayload = decodePaymentSignature(paymentSignature);
226
- console.log(
227
- "[x402-access] Payment payload decoded:",
228
- JSON.stringify(paymentPayload, null, 2),
229
- );
230
-
231
- console.log("[x402-access] Settling payment on-chain...");
232
- const { txHash, settleResponse, payer } = await settlePayment(
233
- paymentPayload,
234
- opts.config,
235
- networkConfig,
236
- );
237
-
238
- console.log(`[x402-access] Payment settled successfully`);
239
- console.log(`[x402-access] - Transaction Hash: ${txHash}`);
240
- console.log(`[x402-access] - Payer: ${payer}`);
241
- console.log(`[x402-access] - Settle Response:`, JSON.stringify(settleResponse, null, 2));
242
-
243
- // Process payment with full lifecycle tracking (PENDING PAID → DELIVERED)
244
- console.log(`[x402-access] Processing payment for requestId: ${requestId}`);
245
- const grant = await engine.processHttpPayment(
246
- requestId,
247
- planId,
248
- resourceId,
249
- txHash,
250
- payer as `0x${string}` | undefined,
251
- );
252
- console.log("[x402-access] ✓ Access grant issued successfully");
253
- console.log("[x402-access] Grant details:", JSON.stringify(grant, null, 2));
254
-
255
- // Set payment-response header
256
- const paymentResponse = Buffer.from(JSON.stringify(settleResponse)).toString("base64");
257
- res.setHeader("payment-response", paymentResponse);
258
- console.log(`[x402-access] payment-response header set (${paymentResponse.length} bytes)`);
259
-
260
- console.log("[x402-access] → Returning HTTP 200 OK with access grant");
261
- return res.status(200).json(grant);
262
- } catch (err: unknown) {
263
- const elapsed = Date.now() - startTime;
264
- console.error(`[x402-access] ✗ Error occurred after ${elapsed}ms`);
265
- console.error("[x402-access] Error type:", err?.constructor?.name || typeof err);
266
- console.error("[x402-access] Error details:", err);
267
-
268
- if (err instanceof Error) {
269
- console.error("[x402-access] Error message:", err.message);
270
- console.error("[x402-access] Error stack:", err.stack);
271
- }
221
+ // Process payment with full lifecycle tracking (PENDING → PAID → DELIVERED)
222
+ console.log(`[x402-access] Processing payment for requestId: ${requestId}`);
223
+ const grant = await engine.processHttpPayment(
224
+ requestId,
225
+ planId,
226
+ resourceId,
227
+ txHash,
228
+ payer as `0x${string}` | undefined,
229
+ );
230
+ console.log("[x402-access] Access grant issued successfully");
231
+ console.log("[x402-access] Grant details:", JSON.stringify(grant, null, 2));
232
+
233
+ // Set payment-response header
234
+ const paymentResponse = Buffer.from(JSON.stringify(settleResponse)).toString("base64");
235
+ res.setHeader("payment-response", paymentResponse);
236
+ console.log(`[x402-access] payment-response header set (${paymentResponse.length} bytes)`);
237
+
238
+ console.log("[x402-access] Returning HTTP 200 OK with access grant");
239
+ return res.status(200).json(grant);
240
+ } catch (err: unknown) {
241
+ const elapsed = Date.now() - startTime;
242
+ console.error(`[x402-access] Error occurred after ${elapsed}ms`);
243
+ console.error("[x402-access] Error type:", err?.constructor?.name || typeof err);
244
+ console.error("[x402-access] Error details:", err);
245
+
246
+ if (err instanceof Error) {
247
+ console.error("[x402-access] Error message:", err.message);
248
+ console.error("[x402-access] Error stack:", err.stack);
249
+ }
272
250
 
273
- if (err instanceof Key0Error) {
274
- console.error(`[x402-access] Key0 error: ${err.code} (HTTP ${err.httpStatus})`);
275
- // Return the grant directly for PROOF_ALREADY_REDEEMED (status 200)
276
- if (err.code === "PROOF_ALREADY_REDEEMED" && err.details?.["grant"]) {
277
- return res.status(200).json(err.details["grant"]);
251
+ if (err instanceof Key0Error) {
252
+ console.error(`[x402-access] Key0 error: ${err.code} (HTTP ${err.httpStatus})`);
253
+ // Return the grant directly for PROOF_ALREADY_REDEEMED (status 200)
254
+ if (err.code === "PROOF_ALREADY_REDEEMED" && err.details?.["grant"]) {
255
+ return res.status(200).json(err.details["grant"]);
256
+ }
257
+ return res.status(err.httpStatus).json(err.toJSON());
278
258
  }
279
- return res.status(err.httpStatus).json(err.toJSON());
259
+
260
+ console.error("[x402-access] Returning 500 Internal Server Error");
261
+ return res.status(500).json({
262
+ error: "INTERNAL_ERROR",
263
+ message: err instanceof Error ? err.message : "Internal server error",
264
+ });
265
+ } finally {
266
+ const elapsed = Date.now() - startTime;
267
+ console.log(`[x402-access] Request completed in ${elapsed}ms`);
268
+ console.log("[x402-access] ========== REQUEST COMPLETE ==========\n");
280
269
  }
270
+ },
271
+ // A2A JSON-RPC fallback: reached when X-A2A-Extensions header is present
272
+ jsonRpcHandler({ requestHandler, userBuilder: UserBuilder.noAuthentication }),
273
+ );
281
274
 
282
- console.error("[x402-access] Returning 500 Internal Server Error");
283
- return res.status(500).json({
284
- error: "INTERNAL_ERROR",
285
- message: err instanceof Error ? err.message : "Internal server error",
286
- });
287
- } finally {
288
- const elapsed = Date.now() - startTime;
289
- console.log(`[x402-access] Request completed in ${elapsed}ms`);
290
- console.log("[x402-access] ========== REQUEST COMPLETE ==========\n");
291
- }
275
+ router.get("/discovery", (_req: Request, res: Response) => {
276
+ const discoveryResponse = buildDiscoveryResponse(opts.config, networkConfig);
277
+ return res.status(200).json({ discoveryResponse });
292
278
  });
293
279
 
294
280
  // MCP routes (when mcp: true)