@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,13 +1,11 @@
1
1
  import { beforeEach, describe, expect, test } from "bun:test";
2
- import type { NextFunction, Request, Response } from "express";
3
2
  import { AccessTokenIssuer } from "../core/access-token.js";
4
3
  import { ChallengeEngine } from "../core/challenge-engine.js";
5
4
  import {
6
5
  buildHttpPaymentRequirements,
7
- createX402HttpMiddleware,
8
6
  decodePaymentSignature,
9
7
  settleViaFacilitator,
10
- } from "../integrations/x402-http-middleware.js";
8
+ } from "../integrations/settlement.js";
11
9
  import { MockPaymentAdapter } from "../test-utils/index.js";
12
10
  import { TestChallengeStore, TestSeenTxStore } from "../test-utils/stores.js";
13
11
  import { CHAIN_CONFIGS } from "../types/config-shared.js";
@@ -49,61 +47,7 @@ function makeConfig(): SellerConfig {
49
47
  };
50
48
  }
51
49
 
52
- function createMockRequest(body: any, headers: Record<string, string> = {}): Partial<Request> {
53
- return {
54
- body,
55
- headers: headers as any,
56
- };
57
- }
58
-
59
- function createMockResponse(): {
60
- res: Partial<Response>;
61
- statusCode: number;
62
- jsonData: any;
63
- nextCalled: boolean;
64
- headers: Record<string, string>;
65
- } {
66
- let statusCode = 200;
67
- let jsonData: any = null;
68
- const nextCalled = false;
69
- const headers: Record<string, string> = {};
70
-
71
- const res = {
72
- status: function (code: number) {
73
- statusCode = code;
74
- return this;
75
- },
76
- json: function (data: any) {
77
- jsonData = data;
78
- return this;
79
- },
80
- send: function (_data: any) {
81
- return this;
82
- },
83
- setHeader: function (name: string, value: string) {
84
- headers[name] = value;
85
- return this;
86
- },
87
- };
88
-
89
- return {
90
- res: res as Partial<Response>,
91
- get statusCode() {
92
- return statusCode;
93
- },
94
- get jsonData() {
95
- return jsonData;
96
- },
97
- get headers() {
98
- return headers;
99
- },
100
- get nextCalled() {
101
- return nextCalled;
102
- },
103
- };
104
- }
105
-
106
- describe("x402-http-middleware", () => {
50
+ describe("x402 settlement helpers", () => {
107
51
  describe("buildHttpPaymentRequirements", () => {
108
52
  test("should build correct payment requirements", () => {
109
53
  const config = makeConfig();
@@ -114,7 +58,7 @@ describe("x402-http-middleware", () => {
114
58
  // v2 response structure
115
59
  expect(requirements.x402Version).toBe(2);
116
60
  expect(requirements.resource).toBeDefined();
117
- expect(requirements.resource.url).toBe("https://agent.example.com/a2a/jsonrpc");
61
+ expect(requirements.resource.url).toBe("https://agent.example.com/x402/access");
118
62
  expect(requirements.resource.method).toBe("POST");
119
63
 
120
64
  // Payment requirements
@@ -143,209 +87,13 @@ describe("x402-http-middleware", () => {
143
87
  });
144
88
  });
145
89
 
146
- describe("createX402HttpMiddleware", () => {
147
- let engine: ChallengeEngine;
148
- let config: SellerConfig;
149
- let middleware: ReturnType<typeof createX402HttpMiddleware>;
150
-
151
- beforeEach(() => {
152
- config = makeConfig();
153
- const store = new TestChallengeStore();
154
- const seenTxStore = new TestSeenTxStore();
155
- const adapter = new MockPaymentAdapter();
156
-
157
- engine = new ChallengeEngine({
158
- config,
159
- store,
160
- seenTxStore,
161
- adapter,
162
- });
163
-
164
- middleware = createX402HttpMiddleware(engine, config);
165
- });
166
-
167
- test("should pass through if X-A2A-Extensions header is present", async () => {
168
- const req = createMockRequest(
169
- {
170
- method: "message/send",
171
- params: {
172
- message: {
173
- parts: [
174
- {
175
- kind: "data",
176
- data: { type: "AccessRequest", requestId: "test", planId: "basic" },
177
- },
178
- ],
179
- },
180
- },
181
- },
182
- {
183
- "x-a2a-extensions":
184
- "https://github.com/google-agentic-commerce/a2a-x402/blob/main/spec/v0.2",
185
- },
186
- );
187
-
188
- let nextCalled = false;
189
- const next = (() => {
190
- nextCalled = true;
191
- }) as NextFunction;
192
- const mockRes = createMockResponse();
193
-
194
- await middleware(req as Request, mockRes.res as Response, next);
195
-
196
- expect(nextCalled).toBe(true);
197
- });
198
-
199
- test("should pass through if not message/send", async () => {
200
- const req = createMockRequest({
201
- method: "task/get",
202
- params: {},
203
- });
204
-
205
- let nextCalled = false;
206
- const next = (() => {
207
- nextCalled = true;
208
- }) as NextFunction;
209
- const mockRes = createMockResponse();
210
-
211
- await middleware(req as Request, mockRes.res as Response, next);
212
-
213
- expect(nextCalled).toBe(true);
214
- });
215
-
216
- test("should pass through if no AccessRequest in message parts", async () => {
217
- const req = createMockRequest({
218
- method: "message/send",
219
- params: {
220
- message: {
221
- parts: [{ kind: "data", data: { type: "SomethingElse" } }],
222
- },
223
- },
224
- });
225
-
226
- let nextCalled = false;
227
- const next = (() => {
228
- nextCalled = true;
229
- }) as NextFunction;
230
- const mockRes = createMockResponse();
231
-
232
- await middleware(req as Request, mockRes.res as Response, next);
233
-
234
- expect(nextCalled).toBe(true);
235
- });
236
-
237
- test("should return HTTP 402 for AccessRequest without X-Payment", async () => {
238
- const req = createMockRequest({
239
- method: "message/send",
240
- params: {
241
- message: {
242
- parts: [
243
- {
244
- kind: "data",
245
- data: {
246
- type: "AccessRequest",
247
- requestId: "req-123",
248
- planId: "basic",
249
- resourceId: "default",
250
- },
251
- },
252
- ],
253
- },
254
- },
255
- });
256
-
257
- const next = (() => {}) as NextFunction;
258
- const mockRes = createMockResponse();
259
-
260
- await middleware(req as Request, mockRes.res as Response, next);
261
-
262
- expect(mockRes.statusCode).toBe(402);
263
- expect(mockRes.jsonData.x402Version).toBe(2);
264
- expect(mockRes.jsonData.resource).toBeDefined();
265
- expect(mockRes.jsonData.accepts).toHaveLength(1);
266
- expect(mockRes.jsonData.accepts[0].amount).toBe("990000");
267
-
268
- // challengeId from PENDING record should be in the response
269
- expect(mockRes.jsonData.challengeId).toBeDefined();
270
- expect(mockRes.jsonData.challengeId).toMatch(/^http-/);
271
-
272
- // Check PAYMENT-REQUIRED header is set
273
- expect(mockRes.headers["payment-required"]).toBeDefined();
274
- const decodedHeader = JSON.parse(
275
- Buffer.from(mockRes.headers["payment-required"]!, "base64").toString(),
276
- );
277
- expect(decodedHeader.x402Version).toBe(2);
278
- });
279
-
280
- test("should return 400 for invalid tier", async () => {
281
- const req = createMockRequest({
282
- method: "message/send",
283
- params: {
284
- message: {
285
- parts: [
286
- {
287
- kind: "data",
288
- data: {
289
- type: "AccessRequest",
290
- requestId: "req-123",
291
- planId: "invalid-tier",
292
- resourceId: "default",
293
- },
294
- },
295
- ],
296
- },
297
- },
298
- });
299
-
300
- const next = (() => {}) as NextFunction;
301
- const mockRes = createMockResponse();
302
-
303
- await middleware(req as Request, mockRes.res as Response, next);
304
-
305
- expect(mockRes.statusCode).toBe(400);
306
- expect(mockRes.jsonData.code).toBe("TIER_NOT_FOUND");
307
- });
308
-
309
- test("should parse AccessRequest from text part", async () => {
310
- const req = createMockRequest({
311
- method: "message/send",
312
- params: {
313
- message: {
314
- parts: [
315
- {
316
- kind: "text",
317
- text: JSON.stringify({
318
- type: "AccessRequest",
319
- requestId: "req-123",
320
- planId: "basic",
321
- resourceId: "default",
322
- }),
323
- },
324
- ],
325
- },
326
- },
327
- });
328
-
329
- const next = (() => {}) as NextFunction;
330
- const mockRes = createMockResponse();
331
-
332
- await middleware(req as Request, mockRes.res as Response, next);
333
-
334
- expect(mockRes.statusCode).toBe(402);
335
- expect(mockRes.jsonData.x402Version).toBe(2);
336
-
337
- // Check PAYMENT-REQUIRED header is set
338
- expect(mockRes.headers["payment-required"]).toBeDefined();
339
- });
340
- });
341
-
342
90
  describe("settleViaFacilitator", () => {
343
91
  const mockPaymentPayload = {
344
92
  x402Version: 2,
345
93
  network: "eip155:84532",
346
94
  scheme: "exact",
347
95
  resource: {
348
- url: "https://agent.example.com/a2a/jsonrpc",
96
+ url: "https://agent.example.com/x402/access",
349
97
  method: "POST",
350
98
  description: "Access to default",
351
99
  mimeType: "application/json",
@@ -62,26 +62,25 @@ describe("buildAgentCard", () => {
62
62
  test("has two A2A spec-compliant skills", () => {
63
63
  const card = buildAgentCard(makeConfig());
64
64
  expect(card.skills).toHaveLength(2);
65
- expect(card.skills[0]!.id).toBe("discover-products");
65
+ expect(card.skills[0]!.id).toBe("discover-plans");
66
66
  expect(card.skills[1]!.id).toBe("request-access");
67
67
  });
68
68
 
69
- test("discover-products skill has correct structure", () => {
69
+ test("discover-plans skill has correct structure", () => {
70
70
  const card = buildAgentCard(makeConfig());
71
71
  const skill = card.skills[0]!;
72
72
 
73
- expect(skill.id).toBe("discover-products");
74
- expect(skill.name).toBe("Discover Products");
75
- expect(skill.description).toContain("Browse available products");
76
- expect(skill.description).toContain("/x402/access");
73
+ expect(skill.id).toBe("discover-plans");
74
+ expect(skill.name).toBe("Discover Plans");
75
+ expect(skill.description).toContain("Browse available");
76
+ expect(skill.description).toContain("/discovery");
77
77
  expect(skill.tags).toContain("discovery");
78
78
  expect(skill.tags).toContain("catalog");
79
79
  expect(skill.examples).toBeDefined();
80
80
  expect(skill.examples!.length).toBeGreaterThan(0);
81
81
 
82
- // A2A spec: skills should NOT have pricing, inputSchema, outputSchema, url
82
+ // A2A spec: skills should NOT have pricing, outputSchema, url
83
83
  expect((skill as any).pricing).toBeUndefined();
84
- expect((skill as any).inputSchema).toBeUndefined();
85
84
  expect((skill as any).outputSchema).toBeUndefined();
86
85
  expect((skill as any).url).toBeUndefined();
87
86
  });
@@ -100,9 +99,13 @@ describe("buildAgentCard", () => {
100
99
  expect(skill.examples).toBeDefined();
101
100
  expect(skill.examples!.length).toBeGreaterThan(0);
102
101
 
103
- // A2A spec: skills should NOT have pricing, inputSchema, outputSchema, url
102
+ // inputSchema is present for machine-readable validation
103
+ expect((skill as any).inputSchema).toBeDefined();
104
+ expect((skill as any).inputSchema.required).toContain("planId");
105
+ expect((skill as any).inputSchema.required).toContain("requestId");
106
+
107
+ // A2A spec: skills should NOT have pricing, outputSchema, url
104
108
  expect((skill as any).pricing).toBeUndefined();
105
- expect((skill as any).inputSchema).toBeUndefined();
106
109
  expect((skill as any).outputSchema).toBeUndefined();
107
110
  expect((skill as any).url).toBeUndefined();
108
111
  });
@@ -113,7 +116,7 @@ describe("buildAgentCard", () => {
113
116
  const discoverSkill = card.skills[0]!;
114
117
  expect(discoverSkill.examples).toBeDefined();
115
118
  expect(discoverSkill.examples!.length).toBeGreaterThan(0);
116
- expect(discoverSkill.examples!.some((ex) => ex.includes("/x402/access"))).toBe(true);
119
+ expect(discoverSkill.examples!.some((ex) => ex.includes("/discovery"))).toBe(true);
117
120
 
118
121
  const requestSkill = card.skills[1]!;
119
122
  expect(requestSkill.examples).toBeDefined();
@@ -150,7 +153,7 @@ describe("buildAgentCard", () => {
150
153
 
151
154
  // Still just two skills regardless of tier count
152
155
  expect(card.skills).toHaveLength(2);
153
- expect(card.skills[0]!.id).toBe("discover-products");
156
+ expect(card.skills[0]!.id).toBe("discover-plans");
154
157
  expect(card.skills[1]!.id).toBe("request-access");
155
158
  });
156
159
 
@@ -300,7 +300,6 @@ function createMockSql() {
300
300
  // Add helper methods
301
301
  sql.unsafe = (str: string) => str;
302
302
  sql.json = (obj: unknown) => obj;
303
- // biome-ignore lint/suspicious/noExplicitAny: mock implementation
304
303
  sql.begin = async (fn: (sql: any) => Promise<any>) => fn(sql);
305
304
 
306
305
  // Expose tables for inspection
@@ -316,7 +315,6 @@ function createMockSql() {
316
315
  (value: string): any;
317
316
  unsafe(value: string): any;
318
317
  json(value: unknown): any;
319
- // biome-ignore lint/suspicious/noExplicitAny: mock type
320
318
  begin(fn: (sql: any) => Promise<any>): Promise<any>;
321
319
  _tables: Map<string, Row[]>; // for inspection
322
320
  } & { _tables: Map<string, Row[]> };
@@ -8,30 +8,30 @@ export function buildAgentCard(config: SellerConfig): AgentCard {
8
8
 
9
9
  const baseUrl = config.agentUrl.replace(/\/$/, "");
10
10
 
11
- // Two A2A spec-compliant skills (no pricing, no inputSchema, no outputSchema, no url)
11
+ const planIds = config.plans.map((p) => p.planId);
12
+
13
+ // Two A2A spec-compliant skills
12
14
  // Skill 1: Discovery (free) — browse the product catalog
13
15
  // Skill 2: Purchase (x402-gated) — buy an access token
14
16
  const skills: AgentSkill[] = [
15
17
  {
16
- id: "discover-products",
17
- name: "Discover Products",
18
+ id: "discover-plans",
19
+ name: "Discover Plans",
18
20
  description: [
19
- `Browse available products and pricing for ${config.agentName}.`,
21
+ `Browse available plans and pricing for ${config.agentName}.`,
20
22
  `Returns the product catalog with plan IDs, prices (USDC on ${networkName}), wallet address, and chain ID.`,
21
- `POST to ${baseUrl}/x402/access with an empty body or without planId to discover products.`,
23
+ `GET to ${baseUrl}/discovery to discover plans.`,
22
24
  ].join(" "),
23
25
  tags: ["discovery", "catalog", "x402"],
24
- examples: [
25
- `POST ${baseUrl}/x402/access with empty body {}`,
26
- `Or call without planId to get 402 response with product catalog`,
27
- ],
26
+ examples: [`GET ${baseUrl}/discovery`],
27
+ endpoint: { url: `${baseUrl}/discovery`, method: "GET" },
28
28
  },
29
29
  {
30
30
  id: "request-access",
31
31
  name: "Request Access",
32
32
  description: [
33
33
  `Purchase access to a ${config.agentName} product plan via x402 payment on ${networkName}.`,
34
- `First call discover-products to get available plans.`,
34
+ `First call discover-plans to get available plans.`,
35
35
  `Then POST to ${baseUrl}/x402/access with planId and requestId to initiate purchase.`,
36
36
  `Server responds with x402 payment challenge.`,
37
37
  `Complete payment on-chain and include PAYMENT-SIGNATURE header to receive access token.`,
@@ -43,6 +43,22 @@ export function buildAgentCard(config: SellerConfig): AgentCard {
43
43
  `Pay USDC on-chain, retry same request with PAYMENT-SIGNATURE header`,
44
44
  `Receive 200 with access token`,
45
45
  ],
46
+ endpoint: { url: `${baseUrl}/x402/access`, method: "POST" },
47
+ inputSchema: {
48
+ type: "object",
49
+ required: ["planId", "requestId"],
50
+ properties: {
51
+ planId: { type: "string", enum: planIds },
52
+ requestId: { type: "string", format: "uuid" },
53
+ },
54
+ },
55
+ workflow: [
56
+ "POST body with planId + requestId to endpoint.url — expect 402",
57
+ "Extract payment requirements from 402 response body",
58
+ `Sign and broadcast USDC transfer on ${networkName}`,
59
+ "Retry same POST with PAYMENT-SIGNATURE header containing the transaction hash",
60
+ "Receive 200 with accessToken",
61
+ ],
46
62
  },
47
63
  ];
48
64
 
@@ -109,7 +109,7 @@ export class ChallengeEngine {
109
109
  chainId: record.chainId,
110
110
  destination: record.destination,
111
111
  expiresAt: record.expiresAt.toISOString(),
112
- description: `Send ${record.amount} USDC to ${record.destination} on chain ${record.chainId}. Then call the "submit-proof" skill with a PaymentProof containing the txHash, challengeId "${record.challengeId}", requestId "${record.requestId}", chainId ${record.chainId}, amount "${record.amount}", asset "USDC", and your fromAgentId.`,
112
+ description: `Send ${record.amount} USDC to ${record.destination} on chain ${record.chainId}. Then replay the same POST /x402/access request with the PAYMENT-SIGNATURE header containing the signed EIP-3009 authorization for challengeId "${record.challengeId}", requestId "${record.requestId}", chainId ${record.chainId}, amount "${record.amount}", asset "USDC".`,
113
113
  resourceVerified: true,
114
114
  };
115
115
  }
package/src/core/index.ts CHANGED
@@ -12,7 +12,7 @@ export { ChallengeEngine } from "./challenge-engine.js";
12
12
  export { validateSellerConfig } from "./config-validation.js";
13
13
  export type { RefundConfig, RefundResult } from "./refund.js";
14
14
  // Refund Utility
15
- export { processRefunds } from "./refund.js";
15
+ export { processRefunds, retryFailedRefunds } from "./refund.js";
16
16
  export type { PostgresStoreConfig } from "./storage/postgres.js";
17
17
  // Storage — Postgres
18
18
  export {