From 72734b12f83445008adb72bad87ead3d7d4c149c Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Tue, 24 Jun 2025 17:00:48 -0400 Subject: [PATCH 1/3] Implement basic A2A demo for ACK-ID identity exchange --- .changeset/red-cloths-grow.md | 8 + .gitignore | 3 + README.md | 12 + demos/README.md | 2 +- demos/e2e/README.md | 2 +- demos/e2e/package.json | 2 +- demos/identity-a2a/README.md | 135 +++++ demos/identity-a2a/bin/setup | 9 + demos/identity-a2a/eslint.config.js | 7 + demos/identity-a2a/package.json | 46 ++ demos/identity-a2a/src/agent.ts | 117 +++++ demos/identity-a2a/src/bank-client-agent.ts | 415 +++++++++++++++ demos/identity-a2a/src/bank-teller-agent.ts | 240 +++++++++ demos/identity-a2a/src/run-demo.ts | 98 ++++ .../src/utils/fetch-agent-card.ts | 16 + demos/identity-a2a/src/utils/schemas.ts | 22 + demos/identity-a2a/src/utils/server-utils.ts | 62 +++ demos/identity-a2a/tsconfig.json | 11 + demos/identity-a2a/vitest.config.ts | 8 + demos/identity/README.md | 2 +- demos/identity/package.json | 2 +- demos/identity/src/identity-tools.ts | 5 +- demos/payments/README.md | 2 +- demos/payments/package.json | 2 +- docs/demos/demo-e2e.mdx | 2 +- docs/demos/demo-identity-a2a.mdx | 153 ++++++ docs/demos/demo-identity.mdx | 2 +- docs/demos/demo-payments.mdx | 2 +- docs/demos/demos.mdx | 2 +- docs/docs.json | 3 +- package.json | 7 +- packages/ack-id/README.md | 101 ++++ packages/ack-id/package.json | 11 + packages/ack-id/src/a2a/index.ts | 4 + packages/ack-id/src/a2a/random.ts | 15 + packages/ack-id/src/a2a/schemas.ts | 53 ++ packages/ack-id/src/a2a/service-endpoints.ts | 14 + packages/ack-id/src/a2a/sign-message.ts | 167 ++++++ packages/ack-id/src/a2a/verify.ts | 86 +++ packages/ack-id/tsdown.config.ts | 7 +- .../src/verify-payment-receipt.test.ts | 9 +- .../ack-pay/src/verify-payment-receipt.ts | 13 +- packages/ack-pay/src/verify-payment-token.ts | 5 + packages/agentcommercekit/README.md | 12 + packages/agentcommercekit/package.json | 9 + packages/agentcommercekit/src/a2a/index.ts | 1 + packages/agentcommercekit/tsdown.config.ts | 7 +- packages/jwt/src/create-jwt.ts | 9 +- packages/jwt/src/index.ts | 6 +- packages/jwt/src/verify.test.ts | 155 ++++++ packages/jwt/src/verify.ts | 24 + pnpm-lock.yaml | 493 ++++++++++++++++-- tools/cli-tools/package.json | 1 - tools/cli-tools/src/colors.ts | 1 + tools/cli-tools/src/index.ts | 3 +- tools/cli-tools/src/logger.ts | 11 + tools/cli-tools/src/prompts.ts | 9 + 57 files changed, 2538 insertions(+), 87 deletions(-) create mode 100644 .changeset/red-cloths-grow.md create mode 100644 demos/identity-a2a/README.md create mode 100755 demos/identity-a2a/bin/setup create mode 100644 demos/identity-a2a/eslint.config.js create mode 100644 demos/identity-a2a/package.json create mode 100644 demos/identity-a2a/src/agent.ts create mode 100644 demos/identity-a2a/src/bank-client-agent.ts create mode 100644 demos/identity-a2a/src/bank-teller-agent.ts create mode 100644 demos/identity-a2a/src/run-demo.ts create mode 100644 demos/identity-a2a/src/utils/fetch-agent-card.ts create mode 100644 demos/identity-a2a/src/utils/schemas.ts create mode 100644 demos/identity-a2a/src/utils/server-utils.ts create mode 100644 demos/identity-a2a/tsconfig.json create mode 100644 demos/identity-a2a/vitest.config.ts create mode 100644 docs/demos/demo-identity-a2a.mdx create mode 100644 packages/ack-id/src/a2a/index.ts create mode 100644 packages/ack-id/src/a2a/random.ts create mode 100644 packages/ack-id/src/a2a/schemas.ts create mode 100644 packages/ack-id/src/a2a/service-endpoints.ts create mode 100644 packages/ack-id/src/a2a/sign-message.ts create mode 100644 packages/ack-id/src/a2a/verify.ts create mode 100644 packages/agentcommercekit/src/a2a/index.ts create mode 100644 packages/jwt/src/verify.test.ts create mode 100644 packages/jwt/src/verify.ts create mode 100644 tools/cli-tools/src/colors.ts create mode 100644 tools/cli-tools/src/logger.ts diff --git a/.changeset/red-cloths-grow.md b/.changeset/red-cloths-grow.md new file mode 100644 index 0000000..5c0d316 --- /dev/null +++ b/.changeset/red-cloths-grow.md @@ -0,0 +1,8 @@ +--- +"agentcommercekit": minor +"@agentcommercekit/ack-pay": minor +"@agentcommercekit/ack-id": minor +"@agentcommercekit/jwt": minor +--- + +Add A2A message support to ACK-ID packages diff --git a/.gitignore b/.gitignore index 900b3ed..8f61cf3 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ npm-debug.log* *.db *.sqlite *.sqlite3 + +# Claude +.claude/*.local.* diff --git a/README.md b/README.md index 59fd22c..edb2ffb 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,18 @@ As you can see from the demo, DIDs and Verifiable Credentials can be extremely p By providing a standardized way to prove ownership chains, agents can securely verify that their counterparty is who they claim to be, such as an e-commerce store. By also exposing service endpoints, agents can broadcast how and where they want to be contacted, as well as other key endpoints they may offer, such as secure KYC exchange APIs. +#### A2A (Agent2Agent) + +The ACK-ID protocol works seamlessly with Google's A2A (Agent2Agent) protocol. You can see a demo of two agents securely exchanging identity information in our `identity-a2a` demo. + +To use this demo, run the following command: + +```sh +pnpm demo:identity-a2a +``` + +You can see the code for this demo in [`./demos/identity-a2a`](./demos/identity-a2a). + #### Going Forward The ACK-ID primitives provide the building blocks for a robust future for agentic Identity. We imagine protocol extensions to support Agent Discovery via registries, reputation scoring, secure end-to-end encrypted communication between agents, controlled authorization, and much more. diff --git a/demos/README.md b/demos/README.md index a18ccb4..2050198 100644 --- a/demos/README.md +++ b/demos/README.md @@ -2,7 +2,7 @@ In this directory you'll find several Demos walking you through different parts of the Agent Commerce Kit. -We recommend running the demos from the root of the project with the command below, but each demo can also be run directly from its project directory with `pnpm start`. +We recommend running the demos from the root of the project with the command below, but each demo can also be run directly from its project directory with `pnpm run demo`. ## Demos diff --git a/demos/e2e/README.md b/demos/e2e/README.md index 66937cf..71e44d7 100644 --- a/demos/e2e/README.md +++ b/demos/e2e/README.md @@ -19,7 +19,7 @@ pnpm run demo:e2e Alternatively, you can run the demo from this directory with: ```sh -pnpm start +pnpm run demo ``` ## Overview of the demo diff --git a/demos/e2e/package.json b/demos/e2e/package.json index 0342ecb..2ffca5c 100644 --- a/demos/e2e/package.json +++ b/demos/e2e/package.json @@ -19,9 +19,9 @@ "scripts": { "check:types": "tsc --noEmit", "clean": "git clean -fdX .turbo", + "demo": "tsx ./src/index.ts", "lint": "eslint .", "lint:fix": "eslint . --fix", - "start": "tsx ./src/index.ts", "test": "vitest" }, "dependencies": { diff --git a/demos/identity-a2a/README.md b/demos/identity-a2a/README.md new file mode 100644 index 0000000..3b9256c --- /dev/null +++ b/demos/identity-a2a/README.md @@ -0,0 +1,135 @@ +# ACK-ID: A2A Identity & Auth Demo + +**ACK-ID** is a protocol built on W3C Standards designed to bring verifiable, secure, compliant identity, reputation, and service discovery to agents. + +**A2A** (Agent2Agent) is a protocol developed by Google to standardize communication between multiple agents. + +This interactive command-line demo showcases how two A2A-compatible agents can use ACK-ID to verify each other's identity and trust that they are communicating with the expected agent. + +## Getting started + +Before starting, please follow the [Getting Started](../../README.md#getting-started) guide at the root of this monorepo. + +### Running the demo + +You can use the demo by running the following command from the root of this repository: + +```sh +pnpm run demo:identity-a2a +``` + +Alternatively, you can run the demo from this directory with: + +```sh +pnpm run demo +``` + +## Overview + +This demo showcases mutual authentication flow between a Bank Customer Agent and a Bank Teller Agent using ACK-ID DIDs and JWTs exchanged within A2A message bodies. The demo walks through the following authentication flow. + +### 1. Initial Contact - Customer Agent Initiates + +The Customer Agent sends an authentication request as an A2A message containing a signed JWT with a nonce: + +```jsonc +{ + "role": "user", + "kind": "message", + "messageId": "f1f54f9d-6db2-4d78-8b38-4e50d77c8b19", + "parts": [ + { + "type": "data", + "data": { + "jwt": "" + } + } + ] +} +``` + +The JWT payload includes: + +```jsonc +{ + "iss": "did:web:customer.example.com", // Customer's DID + "aud": "did:web:bank.example.com", // Bank's expected DID + "nonce": "c-128bit-random", // Customer's random nonce + "iat": 1718476800, + "jti": "0e94d7ec-...", // Unique JWT ID + "exp": 1718477100 // 5-minute expiry +} +``` + +### 2. Bank Teller Agent Response + +The Bank Teller Agent verifies the customer's JWT signature and responds with its own signed JWT, including both the customer's nonce and a new server nonce: + +```jsonc +{ + "role": "agent", + "kind": "message", + "messageId": "f1f54f9d-6db2-4d78-8b38-4e50d77c8b19", + "parts": [ + { + "type": "data", + "data": { + "jwt": "" + } + } + ] +} +``` + +The Bank's JWT payload: + +```jsonc +{ + "iss": "did:web:bank.example.com", // Bank's DID + "aud": "did:web:customer.example.com", // Customer's DID + "nonce": "c-128bit-random", // Echo customer's nonce + "replyNonce": "b-128bit-random", // Bank's new nonce + "jti": "1f85c8fa-...", // Unique JWT ID + "iat": 1718476805, + "exp": 1718477105 // Short expiry +} +``` + +### 3. Subsequent Communications + +After successful mutual authentication, all subsequent messages include a signature in the metadata: + +```jsonc +{ + "role": "user", + "kind": "message", + "messageId": "89f2e11b-5b0a-4c3b-b49d-14628e5d30fb", + "parts": [ + { + "type": "text", + "text": "Please check the balance for account #12345" + } + ], + "metadata": { + "sig": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...Q" // JWT signature of the parts array + } +} +``` + +The signature is a JWT with the payload of `{ "message": }`, with `aud` and `iss` properly set for the counterparty and sender's DID, respectively. + +### Security Benefits + +This authentication flow provides several security advantages: + +- **Mutual Authentication:** Both parties prove their identity through cryptographic signatures +- **Replay Attack Prevention:** Nonces and JWT IDs ensure messages cannot be replayed +- **Man-in-the-Middle (MITM) Protection:** The aud and iss fields are pinned in the JWTs, preventing tampering. An attacker cannot modify requests or responses without invalidating the signatures +- **Short-lived Tokens:** 5-minute expiry limits the window for potential attacks +- **Verifiable Identity:** DID-based authentication ensures cryptographic proof of identity + +## Learn More + +- [Agent Commerce Kit](https://www.agentcommercekit.com) Documentation +- [ACK-ID](https://www.agentcommercekit.com/ack-id) Documentation +- [A2A](https://github.com/google-a2a/A2A) Documentation diff --git a/demos/identity-a2a/bin/setup b/demos/identity-a2a/bin/setup new file mode 100755 index 0000000..c4341a2 --- /dev/null +++ b/demos/identity-a2a/bin/setup @@ -0,0 +1,9 @@ +# #!/usr/bin/env sh + + +# +# This file initializes the repository for local development and CI. +# +# This file should be run after cloning the repository for the first time, but +# is safe to run multiple times. +# diff --git a/demos/identity-a2a/eslint.config.js b/demos/identity-a2a/eslint.config.js new file mode 100644 index 0000000..0979926 --- /dev/null +++ b/demos/identity-a2a/eslint.config.js @@ -0,0 +1,7 @@ +// @ts-check + +import { config } from "@repo/eslint-config/base" + +export default config({ + root: import.meta.dirname +}) diff --git a/demos/identity-a2a/package.json b/demos/identity-a2a/package.json new file mode 100644 index 0000000..013ea5a --- /dev/null +++ b/demos/identity-a2a/package.json @@ -0,0 +1,46 @@ +{ + "name": "@demos/identity-a2a", + "version": "0.0.1", + "private": true, + "homepage": "https://github.com/agentcommercekit/ack#readme", + "bugs": "https://github.com/agentcommercekit/ack/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/agentcommercekit/ack.git", + "directory": "demos/identity-a2a" + }, + "license": "MIT", + "author": { + "name": "Catena Labs", + "url": "https://catenalabs.com" + }, + "type": "module", + "scripts": { + "check:types": "tsc --noEmit", + "clean": "git clean -fdX .turbo", + "demo": "tsx ./src/run-demo.ts", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "setup": "./bin/setup", + "test": "vitest" + }, + "dependencies": { + "@repo/cli-tools": "workspace:*", + "a2a-js": "^0.2.0", + "agentcommercekit": "workspace:*", + "jose": "^6.0.11", + "safe-stable-stringify": "^2.5.0", + "uuid": "^11.1.0", + "valibot": "^1.1.0" + }, + "devDependencies": { + "@repo/eslint-config": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@types/express": "^5.0.3", + "@types/node": "^22", + "eslint": "^9.27.0", + "tsx": "^4.19.4", + "typescript": "^5", + "vitest": "^3.1.4" + } +} diff --git a/demos/identity-a2a/src/agent.ts b/demos/identity-a2a/src/agent.ts new file mode 100644 index 0000000..8a75617 --- /dev/null +++ b/demos/identity-a2a/src/agent.ts @@ -0,0 +1,117 @@ +/* eslint-disable @typescript-eslint/require-await */ +/** + * Agent base class with DID-first architecture + * + * Architecture: + * - DID is the top-level identifier for each agent + * - AgentCard is referenced as a service endpoint in the DID document + * - Clients discover agents by resolving DID documents and finding AgentCard services + * - Authentication uses DID-based JWT signing and verification + */ + +import { colors } from "@repo/cli-tools" +import { Role } from "a2a-js" +import { + createDidDocumentFromKeypair, + createDidWebUri, + createJwtSigner, + generateKeypair +} from "agentcommercekit" +import { createAgentCardServiceEndpoint } from "agentcommercekit/a2a" +import type { + AgentCard, + AgentExecutor, + CancelTaskRequest, + CancelTaskResponse, + Message, + SendMessageRequest, + SendMessageResponse, + SendMessageStreamingRequest, + SendMessageStreamingResponse, + TaskResubscriptionRequest +} from "a2a-js" +import type { + DidDocument, + DidUri, + JwtSigner, + Keypair, + KeypairAlgorithm +} from "agentcommercekit" + +export abstract class Agent implements AgentExecutor { + constructor( + public agentCard: AgentCard, + public keypair: Keypair, + public did: DidUri, + public jwtSigner: JwtSigner, + public didDocument: DidDocument + ) {} + + static async create( + this: new ( + agentCard: AgentCard, + keypair: Keypair, + did: DidUri, + jwtSigner: JwtSigner, + didDocument: DidDocument + ) => T, + agentCard: AgentCard, + algorithm: KeypairAlgorithm = "secp256k1" + ) { + const baseUrl = agentCard.url + const agentCardUrl = `${baseUrl}/.well-known/agent.json` + const keypair = await generateKeypair(algorithm) + const jwtSigner = createJwtSigner(keypair) + const did = createDidWebUri(baseUrl) + const didDocument = createDidDocumentFromKeypair({ + did, + keypair, + service: [createAgentCardServiceEndpoint(did, agentCardUrl)] + }) + + console.log( + `🌐 Generated ${algorithm} keypair with did:web for ${this.name}` + ) + console.log(" DID:", colors.dim(did)) + console.log( + " Public key:", + colors.dim(Buffer.from(keypair.publicKey).toString("hex")) + ) + + return new this(agentCard, keypair, did, jwtSigner, didDocument) + } + + async onMessageSend( + request: SendMessageRequest + ): Promise { + const response: Message = { + role: Role.Agent, + parts: [{ type: "text", text: "Message received and processed" }] + } + + return { + jsonrpc: "2.0", + id: request.id, + result: response + } + } + + async onCancel(request: CancelTaskRequest): Promise { + return { + jsonrpc: "2.0", + id: request.id, + error: { code: -32601, message: "Operation not supported" } + } + } + + onMessageStream( + _request: SendMessageStreamingRequest + ): AsyncGenerator { + throw new Error("Method not implemented.") + } + onResubscribe( + _request: TaskResubscriptionRequest + ): AsyncGenerator { + throw new Error("Method not implemented.") + } +} diff --git a/demos/identity-a2a/src/bank-client-agent.ts b/demos/identity-a2a/src/bank-client-agent.ts new file mode 100644 index 0000000..1bcb50b --- /dev/null +++ b/demos/identity-a2a/src/bank-client-agent.ts @@ -0,0 +1,415 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ + +import { colors, createLogger, waitForEnter } from "@repo/cli-tools" +import { A2AClient, Role } from "a2a-js" +import { getDidResolver, resolveDid } from "agentcommercekit" +import { + createA2AHandshakeMessage, + createSignedA2AMessage, + verifyA2AHandshakeMessage +} from "agentcommercekit/a2a" +import { v4 as uuidV4 } from "uuid" +import * as v from "valibot" +import { Agent } from "./agent" +import { fetchUrlFromAgentCardUrl } from "./utils/fetch-agent-card" +import { messageSchema } from "./utils/schemas" +import { startAgentServer } from "./utils/server-utils" +import type { AgentCard, Message } from "a2a-js" +import type { DidUri } from "agentcommercekit" +import type { Server } from "node:http" + +const logger = createLogger("Bank Customer", colors.green) + +export class BankClientAgent extends Agent { + private a2aServerUrl: string | undefined = undefined // Will be discovered from DID document + private server: Server | undefined = undefined // Server instance for cleanup + + async requestBankingServices(): Promise { + console.log( + colors.yellow( + "šŸ“š STEP 1: Initial Setup - At this point, the agents cannot trust each other yet" + ) + ) + console.log( + colors.yellow( + " The bank client needs to discover and authenticate with the bank teller" + ) + ) + console.log("") + await waitForEnter("Press Enter to start the client agent...") + + logger.log("šŸ¦ Bank Client: Starting up...") + + // Start our own server so our DID document is resolvable + logger.log("šŸš€ Starting Bank Client server for DID document hosting...") + + try { + this.server = startAgentServer(this, { + logger, + port: 3000 + }) + + logger.log("āœ… Bank Client server started successfully") + logger.log( + `🌐 Client DID document available at: ${colors.dim("http://localhost:3000/.well-known/did.json")}` + ) + } catch (error) { + logger.log("āŒ Failed to start Bank Client server:", error as Error) + throw error + } + + // Wait a moment for the bank server to be ready + await new Promise((resolve) => setTimeout(resolve, 1000)) + + console.log( + colors.yellow( + "šŸ“š STEP 2: Service Discovery - Client discovers the bank's public DID" + ) + ) + console.log( + colors.yellow( + " The client constructs the bank's did:web from its domain (localhost:3001)" + ) + ) + console.log( + colors.yellow( + " Then resolves the DID document to find the bank's public keys and services" + ) + ) + console.log("") + await waitForEnter("Press Enter to begin service discovery...") + + // Step 1: Discover bank teller DID and resolve DID document with AgentCard service + const serverDid = this.discoverBankTellerDid() + await this.resolveBankTellerDidDocument(serverDid) + + if (!this.a2aServerUrl) { + throw new Error("Bank teller DID or A2A server URL not found") + } + + // Step 2: Create A2A client using the discovered service endpoint + const client = new A2AClient(this.a2aServerUrl) + + logger.log("šŸ¦ Bank Client: Hello, I need to access my banking services.") + + console.log( + colors.yellow( + "šŸ“š STEP 2.5: Unauthenticated Request - Client tries to access services without identity proof" + ) + ) + console.log( + colors.yellow( + " Let's see what happens when we try to access banking services without authentication..." + ) + ) + console.log("") + await waitForEnter( + "Press Enter to attempt unauthenticated banking request..." + ) + + // Try to access banking services without authentication first + try { + const unauthenticatedMessage: Message = { + role: Role.User, + parts: [ + { + type: "text", + text: "I would like to check my account balance please." + } + ] + } + + const unauthenticatedParams = { + id: uuidV4(), + message: unauthenticatedMessage + } + + logger.log("🚨 Sending unauthenticated request to bank...") + const unauthResponse = await client.sendTask(unauthenticatedParams) + + // This should not happen, but just in case + logger.log("āš ļø Unexpected success:", JSON.stringify(unauthResponse)) + } catch (_error: unknown) { + console.log("") + console.log( + colors.yellow("EXPECTED RESULT: Bank rejects unauthenticated request") + ) + console.log( + colors.yellow( + " The bank doesn't know who this customer is or if they can be trusted" + ) + ) + console.log(colors.yellow(" Error received: Authentication required")) + logger.log("āœ… Bank REJECTED unauthenticated request") + console.log("") + await waitForEnter("Press Enter to begin proper identity verification...") + } + + console.log( + colors.yellow( + "šŸ“š STEP 3: Identity Challenge - Client sends cryptographic proof to bank" + ) + ) + console.log( + colors.yellow( + " Now the client creates a JWT signed with its private key, scoped to the bank's DID" + ) + ) + console.log( + colors.yellow( + " This cryptographically proves the client controls the private key for its DID" + ) + ) + console.log("") + await waitForEnter("Press Enter to send identity verification challenge...") + + // Step 3: Send identity verification challenge + const authSuccess = await this.performIdentityVerification( + client, + serverDid + ) + if (!authSuccess) { + throw new Error("āŒ Identity verification failed!") + } + + try { + console.log( + colors.yellow( + "šŸ“š STEP 4: Authenticated Communication - Now both parties trust each other" + ) + ) + console.log( + colors.yellow( + " Client can now send signed messages to access banking services" + ) + ) + console.log( + colors.yellow( + " Bank verifies each message signature against the client's established identity" + ) + ) + console.log(colors.yellow(" This time the request should succeed!")) + console.log("") + await waitForEnter("Press Enter to request banking services...") + + // Step 4: Send signed message for banking services + const { message } = await createSignedA2AMessage( + { + role: Role.User, + parts: [ + { + type: "text", + text: "I would like to access my banking services. Please verify my identity." + } + ] + }, + { + did: this.did, + jwtSigner: this.jwtSigner, + alg: this.keypair.algorithm, + expiresIn: 5 * 60 + } + ) + + const response = await client.sendTask({ + id: uuidV4(), + message + }) + + // Validate response using Valibot + const parseResult = v.safeParse(messageSchema, response) + if (!parseResult.success) { + logger.log( + "āŒ Invalid bank response format:", + JSON.stringify(parseResult.issues) + ) + return + } + + const { parts } = parseResult.output + const bankResponse = parts + .filter( + (part): part is { type: "text"; text: string } => part.type === "text" + ) + .map((part) => part.text) + .join("") + + console.log(colors.blue("šŸ¦ Bank Teller:"), bankResponse) + + console.log("") + console.log( + colors.yellow("šŸ“š SUCCESS: Secure Banking Session Established!") + ) + console.log( + colors.yellow( + " āœ“ Mutual authentication completed using cryptographic DIDs" + ) + ) + console.log(colors.yellow(" āœ“ No shared secrets or passwords required")) + console.log( + colors.yellow( + " āœ“ Each message is cryptographically signed and verified" + ) + ) + console.log("") + await waitForEnter("Press Enter to complete the demo...") + + logger.log( + "šŸ¦ Bank Client: Thank you! I appreciate the secure identity verification process." + ) + } catch (error) { + logger.log("āŒ Error accessing banking services:", error as Error) + if (error instanceof Error) { + logger.log("Error message:", error.message) + logger.log("Error stack:", error.stack ?? "No stack trace") + } + } finally { + // Cleanup server + if (this.server) { + this.server.close() + logger.log("šŸ¦ Bank Client: Server stopped") + } + } + } + + private async performIdentityVerification( + client: A2AClient, + serverDid: DidUri + ): Promise { + try { + logger.log("šŸ” Starting identity verification with bank teller...") + + const { nonce, message } = await createA2AHandshakeMessage( + Role.User, + serverDid, + { + did: this.did, + jwtSigner: this.jwtSigner, + alg: this.keypair.algorithm, + expiresIn: 5 * 60 + } + ) + + const identityParams = { + id: uuidV4(), + message + } + + logger.log("šŸ” Sending identity verification challenge...") + const authResponse = await client.sendTask(identityParams) + + // Step 3: Verify bank teller response + const { nonce: bankNonce } = await verifyA2AHandshakeMessage( + authResponse, + { + // Validate that this is intended for our DID + did: this.did, + // Validate that the bank teller is the counterparty + counterparty: serverDid + } + ) + + // Check that bank teller included our nonce + if (bankNonce !== nonce) { + throw new Error("āŒ Bank teller nonce mismatch") + } + + logger.log("āœ… Identity verification successful!") + return true + } catch (error) { + logger.log("āŒ Identity verification error:", error as Error) + return false + } + } + + private discoverBankTellerDid(): DidUri { + logger.log("šŸ” Discovering bank teller DID from well-known endpoint...") + + // For did:web, we can construct the DID from the domain + // This assumes the bank is using did:web at localhost:3001 + const serverDid = "did:web:localhost%3A3001" + + logger.log("āœ… Bank Teller DID discovered:", colors.dim(serverDid)) + logger.log("🌐 Bank is using did:web for secure identity!") + + return serverDid + } + + private async resolveBankTellerDidDocument(serverDid: DidUri): Promise { + try { + logger.log("šŸ” Resolving bank teller DID document...") + + const resolver = getDidResolver() + const didResult = await resolveDid(serverDid, resolver) + const didDocument = didResult.didDocument + + logger.log("āœ… Bank teller DID document resolved successfully") + logger.log(" DID Document ID:", colors.dim(didDocument.id)) + logger.log(" Services:", didDocument.service?.length ?? 0) + + // Look for AgentCard service in the DID document + if (didDocument.service && didDocument.service.length > 0) { + const agentCardService = didDocument.service.find( + (service) => service.type === "AgentCard" + ) + + if (!agentCardService) { + logger.log("āš ļø No AgentCard service found in DID document") + throw new Error( + "No AgentCard service found in bank teller DID document" + ) + } + + logger.log("āœ… Found AgentCard service in DID document:") + logger.log(" Service ID:", colors.dim(agentCardService.id)) + logger.log( + " Service Endpoint:", + colors.dim(JSON.stringify(agentCardService.serviceEndpoint, null, 2)) + ) + + // Extract the A2A server URL from the service endpoint + if (typeof agentCardService.serviceEndpoint !== "string") { + throw new Error("AgentCard service endpoint is not a string") + } + + // We need to fetch the url from the agent card + this.a2aServerUrl = await fetchUrlFromAgentCardUrl( + agentCardService.serviceEndpoint + ) + logger.log( + "āœ… Discovered A2A server URL:", + colors.dim(this.a2aServerUrl) + ) + } + } catch (error) { + logger.log( + "āŒ Failed to resolve bank teller DID document:", + error as Error + ) + throw error // Don't continue if we can't resolve the DID document + } + } +} + +// Create a simple AgentCard for the client +const agentCard: AgentCard = { + name: "Bank Client", + description: "Banking services client", + version: "1.0.0", + url: "http://localhost:3000", + defaultInputModes: ["text"], + defaultOutputModes: ["text"], + capabilities: { streaming: false }, + skills: [] +} + +if (import.meta.url === `file://${process.argv[1]}`) { + BankClientAgent.create(agentCard, "Ed25519") + .then(async (agent) => { + await agent.requestBankingServices() + }) + .catch((error: unknown) => { + logger.log("Error:", error as Error) + process.exit(1) + }) +} diff --git a/demos/identity-a2a/src/bank-teller-agent.ts b/demos/identity-a2a/src/bank-teller-agent.ts new file mode 100644 index 0000000..dc4c1f3 --- /dev/null +++ b/demos/identity-a2a/src/bank-teller-agent.ts @@ -0,0 +1,240 @@ +import { colors, createLogger, waitForEnter } from "@repo/cli-tools" +import { Role } from "a2a-js" +import { isDidUri } from "agentcommercekit" +import { + createA2AHandshakeMessage, + verifyA2AHandshakeMessage, + verifyA2ASignedMessage +} from "agentcommercekit/a2a" +import { Agent } from "./agent" +import { startAgentServer } from "./utils/server-utils" +import type { + AgentCard, + Message, + SendMessageRequest, + SendMessageResponse +} from "a2a-js" + +const logger = createLogger("Bank Teller", colors.blue) + +class BankTellerAgent extends Agent { + private authenticatedClients = new Set() + + async onMessageSend( + request: SendMessageRequest + ): Promise { + try { + // Access message from request params + const requestMessage = request.params.message + + // Check if this is an authentication request + if (this.isAuthRequest(requestMessage)) { + return await this.handleAuthentication(request) + } + + const { issuer: clientDid } = await verifyA2ASignedMessage( + requestMessage, + { + did: this.did + } + ) + + if (!isDidUri(clientDid)) { + throw new Error("Invalid issuer") + } + + if (!this.authenticatedClients.has(clientDid)) { + return { + jsonrpc: "2.0", + id: request.id, + error: { code: -32001, message: "Authentication required" } + } + } + + // Proceed with banking service response + + const result = `Verified ${clientDid}. You can now access your account.` + const message: Message = { + role: Role.Agent, + parts: [{ type: "text", text: result }] + } + return { + jsonrpc: "2.0", + id: request.id, + result: message + } + } catch (_error: unknown) { + return { + jsonrpc: "2.0", + id: request.id, + error: { code: -32603, message: "Internal error" } + } + } + } + + private isAuthRequest(message: Message): boolean { + return message.parts.some((part) => "data" in part && "jwt" in part.data) + } + + private async handleAuthentication( + request: SendMessageRequest + ): Promise { + try { + console.log("") + console.log( + colors.yellow("šŸ“š BANK PERSPECTIVE: Verifying Customer Identity") + ) + console.log(colors.yellow(" Bank receives a JWT from unknown customer")) + console.log( + colors.yellow( + " Must verify: 1) JWT signature is valid, 2) Customer controls the DID" + ) + ) + console.log("") + await waitForEnter( + "Press Enter for bank to begin identity verification..." + ) + + logger.log("šŸ” Processing customer identity verification request...") + + console.log( + colors.yellow( + "šŸ” VERIFICATION PROCESS: Resolving customer's DID document" + ) + ) + console.log( + colors.yellow( + " Bank extracts customer DID from JWT and resolves their public keys" + ) + ) + console.log( + colors.yellow( + " Cryptographically verifies JWT signature against customer's public key" + ) + ) + console.log("") + await waitForEnter( + "Press Enter to verify customer's cryptographic proof..." + ) + + const { nonce: clientNonce, iss: clientDid } = + await verifyA2AHandshakeMessage(request.params.message, { + did: this.did + }) + + console.log( + colors.yellow( + "āœ… TRUST ESTABLISHED: Customer cryptographically proved their identity" + ) + ) + console.log( + colors.yellow( + " Bank now knows this customer controls the private key for their DID" + ) + ) + console.log("") + await waitForEnter("Press Enter for bank to create response JWT...") + + logger.log("āœ… Customer identity verified:", colors.dim(clientDid)) + + const { message } = await createA2AHandshakeMessage( + Role.Agent, + clientDid, + { + requestNonce: clientNonce, + did: this.did, + jwtSigner: this.jwtSigner, + alg: this.keypair.algorithm + } + ) + + // Add client to authenticated list + this.authenticatedClients.add(clientDid) + + console.log( + colors.yellow( + "šŸ” RESPONSE: Bank sends back signed proof of successful verification" + ) + ) + console.log( + colors.yellow( + " Bank creates a response JWT that includes the customer's nonce" + ) + ) + console.log( + colors.yellow( + " This proves to customer that bank verified their identity correctly" + ) + ) + console.log("") + await waitForEnter( + "Press Enter to send authentication response back to customer..." + ) + + logger.log( + "šŸ” Identity verification successful for customer:", + colors.dim(clientDid) + ) + + return { + jsonrpc: "2.0", + id: request.id, + result: message + } + } catch (error) { + logger.log("āŒ Identity verification error:", error as Error) + return { + jsonrpc: "2.0", + id: request.id, + error: { code: -32603, message: "Identity verification failed" } + } + } + } +} + +const agentCard: AgentCard = { + name: "Bank Teller Agent", + description: + "A digital bank teller agent that verifies customer identity and provides banking services", + url: "http://localhost:3001", + version: "1.0.0", + defaultInputModes: ["text"], + defaultOutputModes: ["text"], + capabilities: { streaming: false }, + skills: [ + { + id: "verify_identity", + name: "Verify Customer Identity", + description: + "Verifies customer identity using digital credentials and provides banking services", + tags: ["banking", "identity", "verification", "security"], + examples: [ + "verify my identity", + "I need banking services", + "access my account" + ] + } + ], + authentication: { + schemes: ["public"] + } +} + +export async function startServer() { + // Create bank teller agent with did:web instead of did:key + const bankTellerAgent = await BankTellerAgent.create(agentCard, "secp256k1") + + // Start the server using shared utility + return startAgentServer(bankTellerAgent, { + logger, + port: 3001 + }) +} + +// Only start server if this file is run directly +if (import.meta.url === `file://${process.argv[1]}`) { + startServer().catch((error: unknown) => { + logger.log("Error starting bank teller server:", error as Error) + process.exit(1) + }) +} diff --git a/demos/identity-a2a/src/run-demo.ts b/demos/identity-a2a/src/run-demo.ts new file mode 100644 index 0000000..14b29c5 --- /dev/null +++ b/demos/identity-a2a/src/run-demo.ts @@ -0,0 +1,98 @@ +import { spawn } from "child_process" +import { colors, waitForEnter } from "@repo/cli-tools" +import type { ChildProcess } from "child_process" + +async function main() { + console.log("šŸš€ Starting A2A Bank Identity Verification Demo...\n") + console.log( + colors.yellow("šŸ“š DEMO OVERVIEW: ACK-ID + A2A Identity Verification") + ) + console.log( + colors.yellow( + " This demo shows how two agents establish trust without shared secrets" + ) + ) + console.log(colors.yellow(" • Bank Teller: secp256k1 keys (ES256K JWTs)")) + console.log(colors.yellow(" • Bank Customer: Ed25519 keys (EdDSA JWTs)")) + console.log( + colors.yellow( + " • Trust is established through cryptographic proof of DID ownership" + ) + ) + console.log("") + + // Wait for user to be ready + await waitForEnter("Press Enter to start the demo...") + console.log("") + + // Start the bank teller server + console.log( + colors.yellow( + "šŸ¦ Starting Bank Teller Agent (will wait for customer connections)..." + ) + ) + console.log("") + const server: ChildProcess = spawn("tsx", ["./src/bank-teller-agent.ts"], { + stdio: "inherit" + }) + + await new Promise((resolve) => setTimeout(resolve, 1000)) + + // Wait a moment for server to start, then run the client + console.log("") + console.log( + colors.yellow( + "šŸ“š Now starting Bank Customer Agent to connect to the bank..." + ) + ) + console.log("") + await waitForEnter("Press Enter to start the customer agent...") + console.log("") + const client: ChildProcess = spawn("tsx", ["./src/bank-client-agent.ts"], { + stdio: "inherit" + }) + + client.on("close", (code: number | null) => { + console.log("") + console.log( + colors.yellow( + "šŸŽ‰ DEMO COMPLETE: Secure Agent-to-Agent Communication Established!" + ) + ) + console.log(colors.yellow(" Key achievements:")) + console.log(colors.yellow(" āœ“ No passwords or shared secrets were used")) + console.log( + colors.yellow(" āœ“ Cross-algorithm compatibility (secp256k1 ↔ Ed25519)") + ) + console.log(colors.yellow(" āœ“ Cryptographic proof of identity ownership")) + console.log( + colors.yellow( + " āœ“ Decentralized identity verification via DID documents" + ) + ) + console.log("\nāœ… Banking demo completed!") + server.kill() + process.exit(code ?? 0) + }) + + client.on("error", (err: Error) => { + console.error("Client error:", err) + server.kill() + process.exit(1) + }) + + server.on("error", (err: Error) => { + console.error("Server error:", err) + process.exit(1) + }) + + // Handle cleanup + process.on("SIGINT", () => { + console.log("\nšŸ›‘ Shutting down...") + server.kill() + process.exit(0) + }) +} + +// Start the demo +main().catch(console.error) diff --git a/demos/identity-a2a/src/utils/fetch-agent-card.ts b/demos/identity-a2a/src/utils/fetch-agent-card.ts new file mode 100644 index 0000000..79a906b --- /dev/null +++ b/demos/identity-a2a/src/utils/fetch-agent-card.ts @@ -0,0 +1,16 @@ +import * as v from "valibot" + +// Minimal agent card schema to ensure it has a URL +const objectWithUrlSchema = v.object({ + url: v.pipe(v.string(), v.url()) +}) + +export async function fetchUrlFromAgentCardUrl( + agentCardUrl: string | URL +): Promise { + const url = new URL(agentCardUrl) + const response = await fetch(url) + const agentCard = v.parse(objectWithUrlSchema, await response.json()) + + return agentCard.url +} diff --git a/demos/identity-a2a/src/utils/schemas.ts b/demos/identity-a2a/src/utils/schemas.ts new file mode 100644 index 0000000..71831cd --- /dev/null +++ b/demos/identity-a2a/src/utils/schemas.ts @@ -0,0 +1,22 @@ +import * as v from "valibot" + +// Valibot schemas for A2A response validation +const textPartSchema = v.object({ + type: v.literal("text"), + text: v.string() +}) + +const dataPartWithJwtSchema = v.object({ + type: v.literal("data"), + data: v.object({ + jwt: v.string() + }) +}) + +const partSchema = v.union([textPartSchema, dataPartWithJwtSchema]) + +export const messageSchema = v.object({ + role: v.union([v.literal("user"), v.literal("agent")]), + parts: v.array(partSchema), + metadata: v.optional(v.record(v.string(), v.unknown())) +}) diff --git a/demos/identity-a2a/src/utils/server-utils.ts b/demos/identity-a2a/src/utils/server-utils.ts new file mode 100644 index 0000000..e20c38d --- /dev/null +++ b/demos/identity-a2a/src/utils/server-utils.ts @@ -0,0 +1,62 @@ +import { colors, createLogger } from "@repo/cli-tools" +import { A2AServer, DefaultA2ARequestHandler } from "a2a-js" +import type { Agent } from "../agent" +import type { Logger } from "@repo/cli-tools" +import type { Application } from "express" + +type Options = { + logger?: Logger + host?: string + port?: number +} + +/** + * Simple utility to start an A2A server with DID document hosting + */ +export function startAgentServer( + agent: Agent, + { + logger = createLogger("server"), + host = "0.0.0.0", + port = 3001 + }: Options = {} +) { + logger.log(`šŸ¦ Starting ${agent.constructor.name} on port ${port}...`) + logger.log(`šŸ†” Bank Teller DID: ${colors.dim(agent.did)}`) + + // Debug: Check if DID document exists + const didDocument = agent.didDocument + logger.log("āœ… DID document created successfully") + logger.log(" DID Document ID:", colors.dim(didDocument.id)) + + // Create A2A server with the original AgentCard + // The DID is now the top-level identifier, referenced in the DID document services + const requestHandler = new DefaultA2ARequestHandler(agent) + const server = new A2AServer(agent.agentCard, requestHandler) + + // Get the Express app and store it to ensure we use the same instance + const app = server.app() as Application + // Add DID document endpoint for did:web resolution + app.get("/.well-known/did.json", (req, res) => { + logger.log("šŸ” Request for DID document:", colors.dim(req.url.toString())) + const didDocument = agent.didDocument + logger.log("🌐 Serving DID document for did:web resolution") + res.json(didDocument) + }) + + logger.log( + "🌐 DID document endpoint added at:", + colors.dim("/.well-known/did.json") + ) + + // Start the server using the same app instance + const expressServer = app.listen(port, host, () => { + logger.log(`A2A Server running at ${colors.dim(`http://${host}:${port}`)}`) + logger.log( + `🌐 DID document available at: ${colors.dim(`http://localhost:${port}/.well-known/did.json`)}` + ) + }) + + // Return server instance for programmatic control + return expressServer +} diff --git a/demos/identity-a2a/tsconfig.json b/demos/identity-a2a/tsconfig.json new file mode 100644 index 0000000..6293e46 --- /dev/null +++ b/demos/identity-a2a/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@repo/typescript-config/typescript-library.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["."], + "exclude": ["node_modules", "dist"] +} diff --git a/demos/identity-a2a/vitest.config.ts b/demos/identity-a2a/vitest.config.ts new file mode 100644 index 0000000..6a8b09e --- /dev/null +++ b/demos/identity-a2a/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config" + +export default defineConfig({ + test: { + passWithNoTests: true, + watch: false + } +}) diff --git a/demos/identity/README.md b/demos/identity/README.md index a03718e..5fd07cf 100644 --- a/demos/identity/README.md +++ b/demos/identity/README.md @@ -30,7 +30,7 @@ pnpm run demo:identity Alternatively, you can run the demo from this directory with: ```sh -pnpm start +pnpm run demo ``` ## Overview of the Demo diff --git a/demos/identity/package.json b/demos/identity/package.json index 69807b8..76fb3bb 100644 --- a/demos/identity/package.json +++ b/demos/identity/package.json @@ -19,10 +19,10 @@ "scripts": { "check:types": "tsc --noEmit", "clean": "git clean -fdX .turbo", + "demo": "dotenv -e .env -- tsx ./src/index.ts", "lint": "eslint .", "lint:fix": "eslint . --fix", "setup": "./bin/setup", - "start": "dotenv -e .env -- tsx ./src/index.ts", "test": "vitest" }, "dependencies": { diff --git a/demos/identity/src/identity-tools.ts b/demos/identity/src/identity-tools.ts index 6963b1a..9fdf248 100644 --- a/demos/identity/src/identity-tools.ts +++ b/demos/identity/src/identity-tools.ts @@ -58,15 +58,12 @@ async function verifyIdentity( const parsed = await verifyJwt(signedChallenge, { resolver, + issuer: did, policies: { aud: false } }) - if (parsed.issuer !== did) { - throw new Error("Challenge issuer does not match agent did") - } - if (parsed.payload.sub !== challenge) { throw new Error("Challenge response does not match challenge") } diff --git a/demos/payments/README.md b/demos/payments/README.md index 6a85f5f..8e87637 100644 --- a/demos/payments/README.md +++ b/demos/payments/README.md @@ -42,7 +42,7 @@ pnpm run demo:payments Alternatively, you can run the demo from this directory with: ```sh -pnpm start +pnpm run demo ``` ## Overview of the Demo diff --git a/demos/payments/package.json b/demos/payments/package.json index a2a8312..1d67d60 100644 --- a/demos/payments/package.json +++ b/demos/payments/package.json @@ -19,10 +19,10 @@ "scripts": { "check:types": "tsc --noEmit", "clean": "git clean -fdX .turbo", + "demo": "dotenv -e .env -- tsx ./src/index.ts", "lint": "eslint .", "lint:fix": "eslint . --fix", "setup": "./bin/setup", - "start": "dotenv -e .env -- tsx ./src/index.ts", "test": "vitest" }, "dependencies": { diff --git a/docs/demos/demo-e2e.mdx b/docs/demos/demo-e2e.mdx index 6043a81..d2334c4 100644 --- a/docs/demos/demo-e2e.mdx +++ b/docs/demos/demo-e2e.mdx @@ -27,7 +27,7 @@ pnpm demo:e2e Alternatively, navigate directly into the demo directory (`./demos/e2e`) and run: ```sh -pnpm start +pnpm run demo ``` ## Demo Overview diff --git a/docs/demos/demo-identity-a2a.mdx b/docs/demos/demo-identity-a2a.mdx new file mode 100644 index 0000000..20935ee --- /dev/null +++ b/docs/demos/demo-identity-a2a.mdx @@ -0,0 +1,153 @@ +--- +title: ACK-ID A2A Demo +description: "Step-by-step guide demonstrating mutual authentication between agents using ACK-ID and A2A." +--- + +## Overview + +**ACK-ID** is a protocol built on W3C Standards designed to bring verifiable, secure, compliant identity, reputation, and service discovery to agents. + +**A2A** (Agent2Agent) is a protocol developed by Google to standardize communication between multiple agents. + +This interactive command-line demo showcases how two A2A-compatible agents can use ACK-ID to verify each other's identity and trust that they are communicating with the expected agent. + +## Getting Started + +Before running this demo, follow the [Quickstart Guide](./quickstart) to ensure you are set up properly. + +## Running the Demo + +You can use the demo by running the following command from the root of this repository: + +```sh +pnpm run demo:identity-a2a +``` + +Alternatively, from within the demo directory (`./demos/identity-a2a`): + +```sh +pnpm run demo +``` + +## Demo Walkthrough + +This demo showcases a mutual authentication flow between a Bank Customer Agent and a Bank Teller Agent using ACK-ID DIDs and JWTs exchanged within A2A message bodies. The demo walks through the following authentication flow: + + + +The Customer Agent sends an authentication request as an A2A message containing a signed JWT with a nonce: + +```json +{ + "role": "user", + "kind": "message", + "messageId": "f1f54f9d-6db2-4d78-8b38-4e50d77c8b19", + "parts": [ + { + "type": "data", + "data": { + "jwt": "" + } + } + ] +} +``` + +The JWT payload includes: + +```json +{ + "iss": "did:web:customer.example.com", + "aud": "did:web:bank.example.com", + "nonce": "c-128bit-random", + "iat": 1718476800, + "jti": "0e94d7ec-...", + "exp": 1718477100 +} +``` + + + + +The Bank Teller Agent verifies the customer's JWT signature and responds with its own signed JWT, including both the customer's nonce and a new server nonce: + +```json +{ + "role": "agent", + "kind": "message", + "messageId": "f1f54f9d-6db2-4d78-8b38-4e50d77c8b19", + "parts": [ + { + "type": "data", + "data": { + "jwt": "" + } + } + ] +} +``` + +The Bank's JWT payload: + +```json +{ + "iss": "did:web:bank.example.com", + "aud": "did:web:customer.example.com", + "nonce": "c-128bit-random", + "replyNonce": "b-128bit-random", + "jti": "1f85c8fa-...", + "iat": 1718476805, + "exp": 1718477105 +} +``` + + + + +After successful mutual authentication, all subsequent messages include a signature in the metadata: + +```json +{ + "role": "user", + "kind": "message", + "messageId": "89f2e11b-5b0a-4c3b-b49d-14628e5d30fb", + "parts": [ + { + "type": "text", + "text": "Please check the balance for account #12345" + } + ], + "metadata": { + "sig": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...Q" + } +} +``` + +The signature is a JWT with the payload: + +```json +{ + "message": +} +``` + +with `aud` and `iss` set for the counterparty and sender's DID, respectively. + + + + +## Security Benefits + +This authentication flow provides several security advantages: + +- **Mutual Authentication:** Both parties prove their identity through cryptographic signatures. +- **Replay Attack Prevention:** Nonces and JWT IDs ensure messages cannot be replayed. +- **Man-in-the-Middle (MITM) Protection:** The `aud` and `iss` fields are pinned in the JWTs, preventing tampering. An attacker cannot modify requests or responses without invalidating the signatures. +- **Short-lived Tokens:** 5-minute expiry limits the window for potential attacks. +- **Verifiable Identity:** DID-based authentication ensures cryptographic proof of identity. + +## Further Exploration + +- [Agent Commerce Kit](https://www.agentcommercekit.com) Documentation +- [ACK-ID](https://www.agentcommercekit.com/ack-id) Documentation +- [A2A](https://github.com/google-a2a/A2A) Documentation diff --git a/docs/demos/demo-identity.mdx b/docs/demos/demo-identity.mdx index c2e8641..4bae303 100644 --- a/docs/demos/demo-identity.mdx +++ b/docs/demos/demo-identity.mdx @@ -50,7 +50,7 @@ pnpm run demo:identity Alternatively, from within the demo directory (`./demos/identity`): ```sh -pnpm start +pnpm run demo ``` ## Demo Walkthrough diff --git a/docs/demos/demo-payments.mdx b/docs/demos/demo-payments.mdx index fc761cb..520de0c 100644 --- a/docs/demos/demo-payments.mdx +++ b/docs/demos/demo-payments.mdx @@ -42,7 +42,7 @@ pnpm run demo:payments Alternatively, run from within the demo directory (`./demos/payments`): ```sh -pnpm start +pnpm run demo ``` ## ACK-Pay Demo Components diff --git a/docs/demos/demos.mdx b/docs/demos/demos.mdx index 2e24c5f..3ef32d2 100644 --- a/docs/demos/demos.mdx +++ b/docs/demos/demos.mdx @@ -5,7 +5,7 @@ description: "Interactive demos demonstrating key parts of the Agent Commerce Ki Interactive demos and their source code serve to illustrate core patterns of the **Agent Commerce Kit (ACK)**. Each demo has a dedicated detailed walkthrough to guide exploration. -We recommend running these demos from the project root, but you can also run each directly from its specific demo directory using `pnpm start`. +We recommend running these demos from the project root, but you can also run each directly from its specific demo directory using `pnpm run demo`. ## Identity Demo diff --git a/docs/docs.json b/docs/docs.json index f863910..67f6589 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -95,7 +95,8 @@ "demos/demos", "demos/demo-identity", "demos/demo-payments", - "demos/demo-e2e" + "demos/demo-e2e", + "demos/demo-identity-a2a" ] }, { diff --git a/package.json b/package.json index 59a376c..23ccaaf 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,10 @@ "check:packages": "pnpx @manypkg/cli check", "check:types": "turbo check:types", "clean": "turbo clean && git clean -xdf .turbo node_modules/.cache", - "demo:e2e": "pnpm --filter ./demos/e2e start", - "demo:identity": "pnpm --filter ./demos/identity start", - "demo:payments": "pnpm --filter ./demos/payments start", + "demo:e2e": "pnpm --filter ./demos/e2e demo", + "demo:identity": "pnpm --filter ./demos/identity demo", + "demo:identity-a2a": "pnpm --filter ./demos/identity-a2a demo", + "demo:payments": "pnpm --filter ./demos/payments demo", "dev:docs": "pnpm --filter ./docs docs", "dev:examples": "turbo dev", "fix": "turbo fix", diff --git a/packages/ack-id/README.md b/packages/ack-id/README.md index 909d2f6..66c0138 100644 --- a/packages/ack-id/README.md +++ b/packages/ack-id/README.md @@ -94,6 +94,107 @@ import { controllerClaimSchema } from "@agentcommercekit/ack-id/schemas/zod" import { controllerClaimSchema } from "@agentcommercekit/ack-id/schemas/valibot" ``` +## A2A Support + +This package includes utilities for building secure, mutually authenticated agent-to-agent (A2A) flows using DIDs and JWTs, as demonstrated in the [demo-identity-a2a](../docs/demos/demo-identity-a2a.mdx). + +### Key Exports from `@agentcommercekit/ack-id/a2a` + +- `createA2AHandshakeMessage` – Create a signed handshake message (JWT in A2A message body) for authentication. +- `verifyA2AHandshakeMessage` – Verify a handshake message and extract the payload. +- `createSignedA2AMessage` – Sign any A2A message, embedding a JWT signature in the metadata. +- `verifyA2ASignedMessage` – Verify the signature of a signed A2A message. +- `generateRandomNonce`, `generateRandomJti` – Generate secure random values for nonces and JWT IDs. +- `createAgentCardServiceEndpoint` – Helper for DID service endpoints. + +### Example: Mutual Authentication Flow + +#### 1. Customer Agent initiates handshake + +```ts +import { + createA2AHandshakeMessage, + generateRandomNonce +} from "@agentcommercekit/ack-id/a2a" + +const handshake = await createA2AHandshakeMessage( + "user", // role + "did:web:bank.example.com", // recipient DID + { + did: "did:web:customer.example.com" // sender DID + // ... + } +) + +// Send handshake message to the other agent +``` + +#### 2. Counterparty Agent verifies and responds + +```ts +import { verifyA2AHandshakeMessage, createA2AHandshakeMessage } from "@agentcommercekit/ack-id/a2a" + +// On receiving handshake message from customer +const payload = await verifyA2AHandshakeMessage(handshake.message, { + did: "did:web:bank.example.com", + counterparty: "did:web:customer.example.com" +}) +// payload.nonce is the customer's nonce + +// Respond with a handshake message including the received nonce +t const response = await createA2AHandshakeMessage( + "agent", + "did:web:customer.example.com", + { + did: "did:web:bank.example.com", + requestNonce: payload.nonce, + // ... + + } +) +// Send response to the customer +``` + +#### 3. Signed Messaging after Authentication + +```ts +import { + createSignedA2AMessage, + verifyA2ASignedMessage +} from "@agentcommercekit/ack-id/a2a" + +// To send a signed message: +const signed = await createSignedA2AMessage( + { + role: "user", + parts: [ + { type: "text", text: "Please check the balance for account #12345" } + ] + // metadata will be filled in + }, + { + did: "did:web:customer.example.com" + // ... + } +) +// Send signed.message + +// To verify a signed message: +const verified = await verifyA2ASignedMessage(signed.message, { + did: "did:web:bank.example.com", + counterparty: "did:web:customer.example.com" +}) +// verified.payload.message matches the message content +``` + +### Utility Methods + +- `generateRandomNonce()` – Generate a secure random nonce for challenge/response. +- `generateRandomJti()` – Generate a secure random JWT ID. +- `createAgentCardServiceEndpoint(did, url)` – Create a DID service endpoint for agent discovery. + +These methods enable robust, replay-resistant, mutually authenticated A2A flows using DIDs and JWTs. See the [identity-a2a demo](../../demos/identity-a2a) for a full walkthrough. + ## Agent Commerce Kit Version This SDK supports Agent Commerce Kit version `2025-05-04`. diff --git a/packages/ack-id/package.json b/packages/ack-id/package.json index aedb84b..6a95abc 100644 --- a/packages/ack-id/package.json +++ b/packages/ack-id/package.json @@ -20,6 +20,10 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" }, + "./a2a": { + "types": "./dist/a2a/index.d.ts", + "default": "./dist/a2a/index.js" + }, "./schemas/zod": { "types": "./dist/schemas/zod.d.ts", "default": "./dist/schemas/zod.js" @@ -48,13 +52,16 @@ }, "dependencies": { "@agentcommercekit/did": "workspace:*", + "@agentcommercekit/jwt": "workspace:*", "@agentcommercekit/keys": "workspace:*", "@agentcommercekit/vc": "workspace:*", + "safe-stable-stringify": "^2.5.0", "valibot": "^1.1.0" }, "devDependencies": { "@repo/eslint-config": "workspace:*", "@repo/typescript-config": "workspace:*", + "a2a-js": "^0.2.0", "eslint": "^9.27.0", "tsdown": "^0.11.12", "typescript": "^5", @@ -62,9 +69,13 @@ "zod": "^3.25.4" }, "peerDependencies": { + "a2a-js": "^0.2.0", "zod": "^3.0.0" }, "peerDependenciesMeta": { + "a2a-js": { + "optional": true + }, "zod": { "optional": true } diff --git a/packages/ack-id/src/a2a/index.ts b/packages/ack-id/src/a2a/index.ts new file mode 100644 index 0000000..a990c46 --- /dev/null +++ b/packages/ack-id/src/a2a/index.ts @@ -0,0 +1,4 @@ +export * from "./service-endpoints" +export * from "./sign-message" +export * from "./random" +export * from "./verify" diff --git a/packages/ack-id/src/a2a/random.ts b/packages/ack-id/src/a2a/random.ts new file mode 100644 index 0000000..347c664 --- /dev/null +++ b/packages/ack-id/src/a2a/random.ts @@ -0,0 +1,15 @@ +/** + * Generates a random JTI for use in A2A messages + * @returns a random JTI + */ +export function generateRandomJti(): string { + return crypto.randomUUID() +} + +/** + * Generates a random nonce for use in A2A messages + * @returns a random nonce + */ +export function generateRandomNonce(): string { + return crypto.randomUUID() +} diff --git a/packages/ack-id/src/a2a/schemas.ts b/packages/ack-id/src/a2a/schemas.ts new file mode 100644 index 0000000..94dc211 --- /dev/null +++ b/packages/ack-id/src/a2a/schemas.ts @@ -0,0 +1,53 @@ +import * as v from "valibot" + +const roleSchema = v.union([v.literal("agent"), v.literal("user")]) + +const metadataSchema = v.nullable(v.record(v.string(), v.unknown())) + +// Base schema for common part properties +const partBaseSchema = v.object({ + metadata: v.optional(metadataSchema) +}) + +// Text part schema +export const textPartSchema = v.object({ + ...partBaseSchema.entries, + type: v.literal("text"), + text: v.string() +}) + +// Data part schema +export const dataPartSchema = v.object({ + ...partBaseSchema.entries, + type: v.literal("data"), + data: v.union([v.record(v.string(), v.unknown()), v.array(v.unknown())]) +}) + +// File content schema +export const fileContentSchema = v.object({ + name: v.optional(v.nullable(v.string())), + mimeType: v.optional(v.nullable(v.string())), + bytes: v.optional(v.nullable(v.string())), + uri: v.optional(v.nullable(v.string())) +}) + +// File part schema +export const filePartSchema = v.object({ + ...partBaseSchema.entries, + type: v.literal("file"), + file: fileContentSchema +}) + +// Union of all part types using variant syntax +export const partSchema = v.variant("type", [ + textPartSchema, + dataPartSchema, + filePartSchema +]) + +// Message schema +export const messageSchema = v.looseObject({ + role: roleSchema, + parts: v.array(partSchema), + metadata: v.optional(metadataSchema) +}) diff --git a/packages/ack-id/src/a2a/service-endpoints.ts b/packages/ack-id/src/a2a/service-endpoints.ts new file mode 100644 index 0000000..bbcc18e --- /dev/null +++ b/packages/ack-id/src/a2a/service-endpoints.ts @@ -0,0 +1,14 @@ +export const serviceTypeAgentCard = "AgentCard" as const + +export type ServiceTypeAgentCard = typeof serviceTypeAgentCard + +export function createAgentCardServiceEndpoint( + did: string, + agentCardUrl: string +) { + return { + id: `${did}#agent-card`, + type: "AgentCard", + serviceEndpoint: agentCardUrl + } +} diff --git a/packages/ack-id/src/a2a/sign-message.ts b/packages/ack-id/src/a2a/sign-message.ts new file mode 100644 index 0000000..7ed844a --- /dev/null +++ b/packages/ack-id/src/a2a/sign-message.ts @@ -0,0 +1,167 @@ +import { createJwt } from "@agentcommercekit/jwt" +import { generateRandomJti, generateRandomNonce } from "./random" +import type { DidUri } from "@agentcommercekit/did" +import type { + JwtAlgorithm, + JwtPayload, + JwtSigner, + JwtString +} from "@agentcommercekit/jwt" +import type { Message, Role } from "a2a-js" + +type SignMessageOptions = { + did: DidUri + jwtSigner: JwtSigner + alg?: JwtAlgorithm + expiresIn?: number +} + +type SignedA2AMessage = { + sig: string + jti: string + message: Message +} + +type A2AHandshakeMessage = SignedA2AMessage & { + nonce: string + replyNonce?: string +} +/** + * Creates a signed A2A Message + * @returns an object containing the signature, jti, and the signed message, with the signature added to the metadata + */ +export async function createSignedA2AMessage( + { metadata, ...message }: Message, + options: SignMessageOptions +): Promise { + // Sign everything in the message, excluding the metadata + const { jwt: sig, jti } = await createMessageSignature({ message }, options) + + const metadataWithSig = { + ...metadata, + sig + } + + return { + sig, + jti, + message: { + ...message, + metadata: metadataWithSig + } + } +} + +type A2AHandshakeOptions = SignMessageOptions & { + /** + * The nonce of the message we're replying to, if any + */ + requestNonce?: string +} + +export function createA2AHandshakePayload( + recipient: DidUri, + requestNonce?: string +) { + const nonce = generateRandomNonce() + const nonces = requestNonce + ? { + nonce: requestNonce, + replyNonce: nonce + } + : { + nonce: nonce + } + + return { + aud: recipient, + ...nonces + } +} + +export function createA2AHandshakeMessageFromJwt( + role: Role, + jwt: string +): Message { + return { + role, + parts: [ + { + type: "data" as const, + data: { + jwt + } + } + ] + } +} + +/** + * Creates a signed handshake message for A2A Authentication + * + * @returns An object containing signed A2A handshake message, as well as the newly generated nonce + */ +export async function createA2AHandshakeMessage( + role: Role, + recipient: DidUri, + options: A2AHandshakeOptions +): Promise { + const nonce = generateRandomNonce() + const nonces = options.requestNonce + ? { + nonce: options.requestNonce, + replyNonce: nonce + } + : { + nonce: nonce + } + + const payload = { + aud: recipient, + ...nonces + } + + const { jwt, jti } = await createMessageSignature(payload, options) + + const message: Message = { + role, + parts: [ + { + type: "data", + data: { + jwt + } + } + ] + } + + return { + sig: jwt, + jti, + nonce, + message + } +} + +async function createMessageSignature( + payload: Partial, + { did, jwtSigner, alg = "ES256K", expiresIn = 5 * 60 }: SignMessageOptions +): Promise<{ jwt: JwtString; jti: string }> { + const jti = generateRandomJti() + const jwt = await createJwt( + { + jti, + ...payload + }, + { + expiresIn, + signer: jwtSigner, + issuer: did + }, + { + alg + } + ) + + return { jwt, jti } +} diff --git a/packages/ack-id/src/a2a/verify.ts b/packages/ack-id/src/a2a/verify.ts new file mode 100644 index 0000000..be40146 --- /dev/null +++ b/packages/ack-id/src/a2a/verify.ts @@ -0,0 +1,86 @@ +import { getDidResolver } from "@agentcommercekit/did" +import { didUriSchema } from "@agentcommercekit/did/schemas/valibot" +import { verifyJwt } from "@agentcommercekit/jwt" +import { stringify } from "safe-stable-stringify" +import * as v from "valibot" +import { dataPartSchema, messageSchema } from "./schemas" +import type { DidResolver, DidUri } from "@agentcommercekit/did" +import type { JwtVerified } from "@agentcommercekit/jwt" +import type { Message, Task } from "a2a-js" + +const jwtDataPartSchema = v.object({ + ...dataPartSchema.entries, + data: v.object({ + jwt: v.string() + }) +}) + +const messageWithJwtSchema = v.looseObject({ + ...messageSchema.entries, + parts: v.tuple([jwtDataPartSchema]) +}) + +const messageWithSignatureSchema = v.looseObject({ + ...messageSchema.entries, + metadata: v.looseObject({ + sig: v.string() + }) +}) + +const handshakePayloadSchema = v.object({ + iss: didUriSchema, + nonce: v.string() +}) + +type VerifyA2AHandshakeOptions = { + did: DidUri + counterparty?: DidUri + resolver?: DidResolver +} + +export async function verifyA2AHandshakeMessage( + message: Message | Task | null, + { did, counterparty, resolver = getDidResolver() }: VerifyA2AHandshakeOptions +): Promise> { + // Ensure the message is a valid A2A handshake message + const parsedMessage = v.parse(messageWithJwtSchema, message) + const jwt = parsedMessage.parts[0].data.jwt + + const verified = await verifyJwt(jwt, { + audience: did, + issuer: counterparty, + resolver + }) + + return v.parse(handshakePayloadSchema, verified.payload) +} + +export async function verifyA2ASignedMessage( + message: Message, + { did, counterparty, resolver = getDidResolver() }: VerifyA2AHandshakeOptions +): Promise { + // Ensure the message is a valid A2A signed message + const { metadata, ...parsedMessage } = v.parse( + messageWithSignatureSchema, + message + ) + + // Parse the signature from the message metadata, ensuring it is + // signed by the counterparty and intended for the provided DID + const verified = await verifyJwt(metadata.sig, { + audience: did, + issuer: counterparty, + resolver + }) + + const stringifiedMessage = stringify(parsedMessage) + const stringifiedVerified = stringify(verified.payload.message) + + // Verify the provided message matches the message in the signature + // NOTE: the message signature contains the message without the top-level metadata + if (stringifiedMessage !== stringifiedVerified) { + throw new Error("Message parts do not match") + } + + return verified +} diff --git a/packages/ack-id/tsdown.config.ts b/packages/ack-id/tsdown.config.ts index 846c7e6..6384ffd 100644 --- a/packages/ack-id/tsdown.config.ts +++ b/packages/ack-id/tsdown.config.ts @@ -1,7 +1,12 @@ import { defineConfig } from "tsdown/config" export default defineConfig({ - entry: ["src/index.ts", "src/schemas/zod.ts", "src/schemas/valibot.ts"], + entry: [ + "src/index.ts", + "src/a2a/index.ts", + "src/schemas/zod.ts", + "src/schemas/valibot.ts" + ], dts: true, silent: true }) diff --git a/packages/ack-pay/src/verify-payment-receipt.test.ts b/packages/ack-pay/src/verify-payment-receipt.test.ts index f79703e..4b38060 100644 --- a/packages/ack-pay/src/verify-payment-receipt.test.ts +++ b/packages/ack-pay/src/verify-payment-receipt.test.ts @@ -5,14 +5,11 @@ import { } from "@agentcommercekit/did" import { createJwtSigner } from "@agentcommercekit/jwt" import { generateKeypair } from "@agentcommercekit/keys" -import { - InvalidCredentialError, - InvalidCredentialSubjectError, - signCredential -} from "@agentcommercekit/vc" +import { InvalidCredentialError, signCredential } from "@agentcommercekit/vc" import { beforeEach, describe, expect, it } from "vitest" import { createPaymentReceipt } from "./create-payment-receipt" import { createPaymentRequestBody } from "./create-payment-request-body" +import { InvalidPaymentTokenError } from "./errors" import { verifyPaymentReceipt } from "./verify-payment-receipt" import type { PaymentRequestInit } from "./payment-request" import type { DidUri, Resolvable } from "@agentcommercekit/did" @@ -139,7 +136,7 @@ describe("verifyPaymentReceipt()", () => { resolver, paymentRequestIssuer: "did:example:wrong-issuer" }) - ).rejects.toThrow(InvalidCredentialSubjectError) + ).rejects.toThrow(InvalidPaymentTokenError) }) it("validates trusted receipt issuers", async () => { diff --git a/packages/ack-pay/src/verify-payment-receipt.ts b/packages/ack-pay/src/verify-payment-receipt.ts index 8e0db22..cfa575d 100644 --- a/packages/ack-pay/src/verify-payment-receipt.ts +++ b/packages/ack-pay/src/verify-payment-receipt.ts @@ -99,19 +99,16 @@ export async function verifyPaymentReceipt( throw new InvalidCredentialSubjectError("Payment token is not a JWT") } - const { paymentRequest, parsed } = await verifyPaymentToken(paymentToken, { + const { paymentRequest } = await verifyPaymentToken(paymentToken, { resolver, // We don't want to fail Receipt Verification if the paymentToken has // expired, since the receipt lives longer than that - verifyExpiry: false + verifyExpiry: false, + // If the paymentRequestIssuer is provided, we want to verify that the + // payment token was issued by the same issuer. + issuer: paymentRequestIssuer }) - if (paymentRequestIssuer && parsed.issuer !== paymentRequestIssuer) { - throw new InvalidCredentialSubjectError( - "Payment token issuer does not match" - ) - } - return { receipt: parsedCredential, paymentToken, diff --git a/packages/ack-pay/src/verify-payment-token.ts b/packages/ack-pay/src/verify-payment-token.ts index 2778c02..11e6c3c 100644 --- a/packages/ack-pay/src/verify-payment-token.ts +++ b/packages/ack-pay/src/verify-payment-token.ts @@ -15,6 +15,10 @@ interface ValidatePaymentTokenOptions { * Whether to verify the expiry of the payment token */ verifyExpiry?: boolean + /** + * The issuer to verify the payment token against + */ + issuer?: string } /** @@ -33,6 +37,7 @@ export async function verifyPaymentToken( try { parsedPaymentToken = await verifyJwt(token, { resolver: options.resolver, + issuer: options.issuer, policies: { aud: false, exp: options.verifyExpiry ?? true diff --git a/packages/agentcommercekit/README.md b/packages/agentcommercekit/README.md index c88d02b..2e4e292 100644 --- a/packages/agentcommercekit/README.md +++ b/packages/agentcommercekit/README.md @@ -92,6 +92,18 @@ isControllerCredential(credential) isControllerClaim(credential.credentialSubject) ``` +#### A2A Methods + +Additionally, the ACK-ID package exposes methods to allow for identity exchange over A2A. See the [A2A section](../ack-id/README.md#a2a-support) of the ACK-ID README for more information. + +These methods are exported from `agentcommercekit/a2a`. For example: + +```ts +import { createA2AHandshakeMessage } from "agentcommercekit/a2a" + +// ... +``` + ### ACK-Pay methods #### Creating a Payment Request diff --git a/packages/agentcommercekit/package.json b/packages/agentcommercekit/package.json index 9e3c132..aa69fe5 100644 --- a/packages/agentcommercekit/package.json +++ b/packages/agentcommercekit/package.json @@ -20,6 +20,10 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" }, + "./a2a": { + "types": "./dist/a2a/index.d.ts", + "default": "./dist/a2a/index.js" + }, "./schemas/zod": { "types": "./dist/schemas/zod.d.ts", "default": "./dist/schemas/zod.js" @@ -57,6 +61,7 @@ "devDependencies": { "@repo/eslint-config": "workspace:*", "@repo/typescript-config": "workspace:*", + "a2a-js": "^0.2.0", "eslint": "^9.27.0", "tsdown": "^0.11.12", "typescript": "^5", @@ -65,10 +70,14 @@ "zod": "^3.25.4" }, "peerDependencies": { + "a2a-js": "^0.2.0", "valibot": "^1.0.0", "zod": "^3.0.0" }, "peerDependenciesMeta": { + "a2a-js": { + "optional": true + }, "valibot": { "optional": true }, diff --git a/packages/agentcommercekit/src/a2a/index.ts b/packages/agentcommercekit/src/a2a/index.ts new file mode 100644 index 0000000..60c5fe1 --- /dev/null +++ b/packages/agentcommercekit/src/a2a/index.ts @@ -0,0 +1 @@ +export * from "@agentcommercekit/ack-id/a2a" diff --git a/packages/agentcommercekit/tsdown.config.ts b/packages/agentcommercekit/tsdown.config.ts index 846c7e6..6384ffd 100644 --- a/packages/agentcommercekit/tsdown.config.ts +++ b/packages/agentcommercekit/tsdown.config.ts @@ -1,7 +1,12 @@ import { defineConfig } from "tsdown/config" export default defineConfig({ - entry: ["src/index.ts", "src/schemas/zod.ts", "src/schemas/valibot.ts"], + entry: [ + "src/index.ts", + "src/a2a/index.ts", + "src/schemas/zod.ts", + "src/schemas/valibot.ts" + ], dts: true, silent: true }) diff --git a/packages/jwt/src/create-jwt.ts b/packages/jwt/src/create-jwt.ts index abf7dde..eee35da 100644 --- a/packages/jwt/src/create-jwt.ts +++ b/packages/jwt/src/create-jwt.ts @@ -5,11 +5,14 @@ import type { JwtAlgorithm } from "./jwt-algorithm" import type { JwtString } from "./jwt-string" import type { JWTHeader, JWTOptions, JWTPayload } from "did-jwt" +export type JwtPayload = JWTPayload +export type JwtOptions = JWTOptions + /** * Allow alternative names for the algorithm (adds `secp256k1` and `Ed25519`, * which map to `ES256K` and `EdDSA` respectively) */ -type JwtHeader = Omit & { +export type JwtHeader = Omit & { alg: JwtAlgorithm } @@ -25,8 +28,8 @@ type JwtHeader = Omit & { * @returns The JWT */ export async function createJwt( - payload: Partial, - options: JWTOptions, + payload: Partial, + options: JwtOptions, { alg = "ES256K", ...header }: Partial = {} ): Promise { const result = await baseCreateJWT(payload, options, { diff --git a/packages/jwt/src/index.ts b/packages/jwt/src/index.ts index eadb5e3..7e9aeea 100644 --- a/packages/jwt/src/index.ts +++ b/packages/jwt/src/index.ts @@ -2,8 +2,4 @@ export * from "./jwt-algorithm" export * from "./jwt-string" export * from "./create-jwt" export * from "./signer" - -export { - verifyJWT as verifyJwt, - type JWTVerified as JwtVerified -} from "did-jwt" +export * from "./verify" diff --git a/packages/jwt/src/verify.test.ts b/packages/jwt/src/verify.test.ts new file mode 100644 index 0000000..5ed8d79 --- /dev/null +++ b/packages/jwt/src/verify.test.ts @@ -0,0 +1,155 @@ +import { generateKeypair } from "@agentcommercekit/keys" +import { verifyJWT } from "did-jwt" +import { beforeEach, describe, expect, it, vi } from "vitest" +import { createJwt } from "./create-jwt" +import { createJwtSigner } from "./signer" +import { verifyJwt } from "./verify" +import type { JWTVerified } from "did-jwt" + +// Mock did-jwt for testing +vi.mock("did-jwt", async () => { + const actual = await vi.importActual("did-jwt") + return { + ...actual, + verifyJWT: vi.fn() + } +}) + +describe("verifyJwt()", () => { + let keypair: Awaited> + let signer: ReturnType + + beforeEach(async () => { + keypair = await generateKeypair("secp256k1") + signer = createJwtSigner(keypair) + }) + + it("verifies a JWT", async () => { + const jwt = await createJwt( + { + sub: "did:example:subject", + aud: "did:example:audience" + }, + { + issuer: "did:example:issuer", + signer + } + ) + + const mockVerifiedResult: JWTVerified = { + verified: true, + payload: { + iss: "did:example:issuer", + sub: "did:example:subject", + aud: "did:example:audience" + }, + didResolutionResult: { + didResolutionMetadata: {}, + didDocument: null, + didDocumentMetadata: {} + }, + issuer: "did:example:issuer", + signer: { + id: "did:example:issuer#key-1", + type: "EcdsaSecp256k1VerificationKey2019", + controller: "did:example:issuer", + publicKeyHex: "02..." + }, + jwt + } + + vi.mocked(verifyJWT).mockResolvedValueOnce(mockVerifiedResult) + + const result = await verifyJwt(jwt, {}) + + expect(result.payload.iss).toBe("did:example:issuer") + expect(result.payload.sub).toBe("did:example:subject") + expect(result.payload.aud).toBe("did:example:audience") + }) + + it("allows requiring a specific issuer", async () => { + const jwt = await createJwt( + { + iss: "did:example:issuer", + sub: "did:example:subject" + }, + { + issuer: "did:example:issuer", + signer + } + ) + + const mockVerifiedResult: JWTVerified = { + verified: true, + payload: { + iss: "did:example:issuer", + sub: "did:example:subject" + }, + didResolutionResult: { + didResolutionMetadata: {}, + didDocument: null, + didDocumentMetadata: {} + }, + issuer: "did:example:issuer", + signer: { + id: "did:example:issuer#key-1", + type: "EcdsaSecp256k1VerificationKey2019", + controller: "did:example:issuer", + publicKeyHex: "02..." + }, + jwt + } + + vi.mocked(verifyJWT).mockResolvedValueOnce(mockVerifiedResult) + + const result = await verifyJwt(jwt, { + issuer: "did:example:issuer" + }) + + expect(result.payload.iss).toBe("did:example:issuer") + }) + + it("throws error when issuer does not match expected issuer", async () => { + const jwt = await createJwt( + { + iss: "did:example:issuer", + sub: "did:example:subject" + }, + { + issuer: "did:example:issuer", + signer + } + ) + + const mockVerifiedResult: JWTVerified = { + verified: true, + payload: { + iss: "did:example:issuer", + sub: "did:example:subject" + }, + didResolutionResult: { + didResolutionMetadata: {}, + didDocument: null, + didDocumentMetadata: {} + }, + issuer: "did:example:issuer", + signer: { + id: "did:example:issuer#key-1", + type: "EcdsaSecp256k1VerificationKey2019", + controller: "did:example:issuer", + publicKeyHex: "02..." + }, + jwt + } + + vi.mocked(verifyJWT).mockResolvedValueOnce(mockVerifiedResult) + + await expect( + verifyJwt(jwt, { + issuer: "did:example:different" + }) + ).rejects.toThrow( + "Expected issuer did:example:different, got did:example:issuer" + ) + }) +}) diff --git a/packages/jwt/src/verify.ts b/packages/jwt/src/verify.ts new file mode 100644 index 0000000..0d25bcd --- /dev/null +++ b/packages/jwt/src/verify.ts @@ -0,0 +1,24 @@ +import { verifyJWT } from "did-jwt" +import type { JWTVerified, JWTVerifyOptions } from "did-jwt" + +export type JwtVerified = JWTVerified + +export type VerifyJwtOptions = JWTVerifyOptions & { + issuer?: string +} + +/** + * Verify a JWT, with additional options to restrict to a specific issuer + */ +export async function verifyJwt( + jwt: string, + { issuer, ...options }: VerifyJwtOptions = {} +): Promise { + const result = await verifyJWT(jwt, options) + + if (issuer && result.payload.iss !== issuer) { + throw new Error(`Expected issuer ${issuer}, got ${result.payload.iss}`) + } + + return result +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b700e1..e0b80ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,13 +56,13 @@ importers: version: 9.27.0(jiti@2.4.2) tsx: specifier: ^4.19.4 - version: 4.19.4 + version: 4.20.3 typescript: specifier: ^5 version: 5.8.3 vitest: specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0) demos/identity: dependencies: @@ -117,13 +117,62 @@ importers: version: 9.27.0(jiti@2.4.2) tsx: specifier: ^4.19.4 - version: 4.19.4 + version: 4.20.3 typescript: specifier: ^5 version: 5.8.3 vitest: specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0) + + demos/identity-a2a: + dependencies: + '@repo/cli-tools': + specifier: workspace:* + version: link:../../tools/cli-tools + a2a-js: + specifier: ^0.2.0 + version: 0.2.0 + agentcommercekit: + specifier: workspace:* + version: link:../../packages/agentcommercekit + jose: + specifier: ^6.0.11 + version: 6.0.11 + safe-stable-stringify: + specifier: ^2.5.0 + version: 2.5.0 + uuid: + specifier: ^11.1.0 + version: 11.1.0 + valibot: + specifier: ^1.1.0 + version: 1.1.0(typescript@5.8.3) + devDependencies: + '@repo/eslint-config': + specifier: workspace:* + version: link:../../tools/eslint-config + '@repo/typescript-config': + specifier: workspace:* + version: link:../../tools/typescript-config + '@types/express': + specifier: ^5.0.3 + version: 5.0.3 + '@types/node': + specifier: ^22 + version: 22.15.19 + eslint: + specifier: ^9.27.0 + version: 9.27.0(jiti@2.4.2) + tsx: + specifier: ^4.19.4 + version: 4.20.3 + typescript: + specifier: ^5 + version: 5.8.3 + vitest: + specifier: ^3.1.4 + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0) demos/payments: dependencies: @@ -166,13 +215,13 @@ importers: version: 9.27.0(jiti@2.4.2) tsx: specifier: ^4.19.4 - version: 4.19.4 + version: 4.20.3 typescript: specifier: ^5 version: 5.8.3 vitest: specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0) docs: devDependencies: @@ -230,13 +279,13 @@ importers: version: 9.27.0(jiti@2.4.2) tsx: specifier: ^4.19.4 - version: 4.19.4 + version: 4.20.3 vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.8.3)(vite@6.2.5(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0)) + version: 5.1.4(typescript@5.8.3)(vite@6.2.5(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0)) vitest: specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0) examples/local-did-host: dependencies: @@ -279,10 +328,10 @@ importers: version: 9.27.0(jiti@2.4.2) tsx: specifier: ^4.19.4 - version: 4.19.4 + version: 4.20.3 vitest: specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0) examples/verifier: dependencies: @@ -319,22 +368,28 @@ importers: version: 9.27.0(jiti@2.4.2) tsx: specifier: ^4.19.4 - version: 4.19.4 + version: 4.20.3 vitest: specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0) packages/ack-id: dependencies: '@agentcommercekit/did': specifier: workspace:* version: link:../did + '@agentcommercekit/jwt': + specifier: workspace:* + version: link:../jwt '@agentcommercekit/keys': specifier: workspace:* version: link:../keys '@agentcommercekit/vc': specifier: workspace:* version: link:../vc + safe-stable-stringify: + specifier: ^2.5.0 + version: 2.5.0 valibot: specifier: ^1.1.0 version: 1.1.0(typescript@5.8.3) @@ -345,6 +400,9 @@ importers: '@repo/typescript-config': specifier: workspace:* version: link:../../tools/typescript-config + a2a-js: + specifier: ^0.2.0 + version: 0.2.0 eslint: specifier: ^9.27.0 version: 9.27.0(jiti@2.4.2) @@ -356,7 +414,7 @@ importers: version: 5.8.3 vitest: specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0) zod: specifier: ^3.25.4 version: 3.25.4 @@ -396,7 +454,7 @@ importers: version: 5.8.3 vitest: specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0) zod: specifier: ^3.25.4 version: 3.25.4 @@ -428,6 +486,9 @@ importers: '@repo/typescript-config': specifier: workspace:* version: link:../../tools/typescript-config + a2a-js: + specifier: ^0.2.0 + version: 0.2.0 eslint: specifier: ^9.27.0 version: 9.27.0(jiti@2.4.2) @@ -442,7 +503,7 @@ importers: version: 1.1.0(typescript@5.8.3) vitest: specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0) zod: specifier: ^3.25.4 version: 3.25.4 @@ -488,7 +549,7 @@ importers: version: 5.8.3 vitest: specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0) zod: specifier: ^3.25.4 version: 3.25.4 @@ -522,7 +583,7 @@ importers: version: 1.1.0(typescript@5.8.3) vitest: specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0) zod: specifier: ^3.25.4 version: 3.25.4 @@ -559,7 +620,7 @@ importers: version: 5.8.3 vitest: specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0) packages/vc: dependencies: @@ -599,7 +660,7 @@ importers: version: 5.8.3 vitest: specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0) zod: specifier: ^3.25.4 version: 3.25.4 @@ -642,7 +703,7 @@ importers: version: 5.8.3 vitest: specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0) tools/cli-tools: dependencies: @@ -682,7 +743,7 @@ importers: version: 5.8.3 vitest: specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0) tools/eslint-config: devDependencies: @@ -2176,6 +2237,12 @@ packages: '@types/better-sqlite3@7.6.13': resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/cors@2.8.18': resolution: {integrity: sha512-nX3d0sxJW41CqQvfOzVG1NCTXfFDrDWIghCZncpHeWlVFd81zxB/DLhg7avFg6eHLCRX7ckBmoIIcqa++upvJA==} @@ -2194,6 +2261,12 @@ packages: '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + '@types/express-serve-static-core@5.0.6': + resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==} + + '@types/express@5.0.3': + resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==} + '@types/figlet@1.7.0': resolution: {integrity: sha512-KwrT7p/8Eo3Op/HBSIwGXOsTZKYiM9NpWRBJ5sVjWP/SmlS+oxxRvJht/FNAtliJvja44N3ul1yATgohnVBV0Q==} @@ -2206,6 +2279,9 @@ packages: '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -2218,6 +2294,9 @@ packages: '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -2236,9 +2315,21 @@ packages: '@types/prismjs@1.26.5': resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react@19.1.4': resolution: {integrity: sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==} + '@types/send@0.17.5': + resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} + + '@types/serve-static@1.15.8': + resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -2423,6 +2514,10 @@ packages: '@vitest/utils@3.1.4': resolution: {integrity: sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==} + a2a-js@0.2.0: + resolution: {integrity: sha512-JtCjTvbBVNP43mmuCqhSXk16XSKE5L4/4wbVFJsz6K2syfplNUnsZxHxnqJ0dQoe3Fy70V09uWI29uaXkNdjrg==} + engines: {node: '>=16.0.0'} + abitype@1.0.8: resolution: {integrity: sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==} peerDependencies: @@ -2442,6 +2537,10 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2686,6 +2785,10 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -2880,6 +2983,10 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -2887,6 +2994,10 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + cookie@0.7.1: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} @@ -3495,6 +3606,14 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + eventsource-parser@3.0.2: + resolution: {integrity: sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -3507,6 +3626,10 @@ packages: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} @@ -3596,6 +3719,10 @@ packages: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -3644,6 +3771,10 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -3911,6 +4042,10 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -4096,6 +4231,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -4167,6 +4305,9 @@ packages: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true + jose@6.0.11: + resolution: {integrity: sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -4231,6 +4372,9 @@ packages: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} engines: {node: '>=0.10.0'} + jsonschema@1.5.0: + resolution: {integrity: sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==} + katex@0.16.22: resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==} hasBin: true @@ -4446,9 +4590,17 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -4576,10 +4728,18 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -4684,6 +4844,10 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + neotraverse@0.6.18: resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} engines: {node: '>= 10'} @@ -4897,6 +5061,10 @@ packages: path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + path-to-regexp@8.2.0: + resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} + engines: {node: '>=16'} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -5091,6 +5259,10 @@ packages: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + quansync@0.2.10: resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} @@ -5109,6 +5281,10 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -5286,6 +5462,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-async@3.0.0: resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} engines: {node: '>=0.12.0'} @@ -5314,6 +5494,10 @@ packages: safe-stable-stringify@1.1.1: resolution: {integrity: sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -5344,6 +5528,10 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + serialize-error@12.0.0: resolution: {integrity: sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw==} engines: {node: '>=18'} @@ -5352,6 +5540,10 @@ packages: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -5705,8 +5897,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.19.4: - resolution: {integrity: sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==} + tsx@4.20.3: + resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==} engines: {node: '>=18.0.0'} hasBin: true @@ -5763,6 +5955,10 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -5901,6 +6097,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + valibot@1.1.0: resolution: {integrity: sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==} peerDependencies: @@ -7838,6 +8038,15 @@ snapshots: dependencies: '@types/node': 22.15.19 + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.15.19 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.15.19 + '@types/cors@2.8.18': dependencies: '@types/node': 22.15.19 @@ -7858,6 +8067,19 @@ snapshots: '@types/estree@1.0.7': {} + '@types/express-serve-static-core@5.0.6': + dependencies: + '@types/node': 22.15.19 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.5 + + '@types/express@5.0.3': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.0.6 + '@types/serve-static': 1.15.8 + '@types/figlet@1.7.0': {} '@types/hast@2.3.10': @@ -7870,6 +8092,8 @@ snapshots: '@types/http-cache-semantics@4.0.4': {} + '@types/http-errors@2.0.5': {} + '@types/json-schema@7.0.15': {} '@types/katex@0.16.7': {} @@ -7880,6 +8104,8 @@ snapshots: '@types/mdx@2.0.13': {} + '@types/mime@1.3.5': {} + '@types/ms@2.1.0': {} '@types/nlcst@2.0.3': @@ -7901,10 +8127,25 @@ snapshots: '@types/prismjs@1.26.5': {} + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + '@types/react@19.1.4': dependencies: csstype: 3.1.3 + '@types/send@0.17.5': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 22.15.19 + + '@types/serve-static@1.15.8': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 22.15.19 + '@types/send': 0.17.5 + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -7943,7 +8184,7 @@ snapshots: '@typescript-eslint/types': 8.32.1 '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.32.1 - debug: 4.4.0 + debug: 4.4.1 eslint: 9.27.0(jiti@2.4.2) typescript: 5.8.3 transitivePeerDependencies: @@ -7958,7 +8199,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3) '@typescript-eslint/utils': 8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3) - debug: 4.4.0 + debug: 4.4.1 eslint: 9.27.0(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 @@ -7971,7 +8212,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.32.1 '@typescript-eslint/visitor-keys': 8.32.1 - debug: 4.4.0 + debug: 4.4.1 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -8063,13 +8304,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.1.4(vite@6.2.5(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0))': + '@vitest/mocker@3.1.4(vite@6.2.5(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.1.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.2.5(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.2.5(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0) '@vitest/pretty-format@3.1.4': dependencies: @@ -8096,6 +8337,18 @@ snapshots: loupe: 3.1.3 tinyrainbow: 2.0.0 + a2a-js@0.2.0: + dependencies: + axios: 1.9.0 + cors: 2.8.5 + eventsource: 3.0.7 + express: 5.1.0 + jsonschema: 1.5.0 + uuid: 11.1.0 + transitivePeerDependencies: + - debug + - supports-color + abitype@1.0.8(typescript@5.8.3)(zod@3.25.4): optionalDependencies: typescript: 5.8.3 @@ -8110,6 +8363,11 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.14.1): dependencies: acorn: 8.14.1 @@ -8342,6 +8600,20 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.1 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.0 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -8535,10 +8807,16 @@ snapshots: dependencies: safe-buffer: 5.2.1 + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + content-type@1.0.5: {} cookie-signature@1.0.6: {} + cookie-signature@1.2.2: {} + cookie@0.7.1: {} cookie@0.7.2: {} @@ -8976,7 +9254,7 @@ snapshots: esbuild-register@3.6.0(esbuild@0.25.2): dependencies: - debug: 4.4.0 + debug: 4.4.1 esbuild: 0.25.2 transitivePeerDependencies: - supports-color @@ -9210,6 +9488,12 @@ snapshots: eventemitter3@5.0.1: {} + eventsource-parser@3.0.2: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.2 + expand-template@2.0.3: {} expect-type@1.2.1: {} @@ -9250,6 +9534,38 @@ snapshots: transitivePeerDependencies: - supports-color + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.1 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + extend-shallow@2.0.1: dependencies: is-extendable: 0.1.1 @@ -9344,6 +9660,17 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.1.0: + dependencies: + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -9382,6 +9709,8 @@ snapshots: fresh@0.5.2: {} + fresh@2.0.0: {} + fs-constants@1.0.0: {} fs-extra@11.3.0: @@ -9801,6 +10130,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -9967,6 +10300,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-promise@4.0.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -10031,6 +10366,8 @@ snapshots: jiti@2.4.2: {} + jose@6.0.11: {} + js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -10086,6 +10423,8 @@ snapshots: jsonpointer@5.0.1: {} + jsonschema@1.5.0: {} + katex@0.16.22: dependencies: commander: 8.3.0 @@ -10397,8 +10736,12 @@ snapshots: media-typer@0.3.0: {} + media-typer@1.1.0: {} + merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} + merge2@1.4.1: {} methods@1.1.2: {} @@ -10665,7 +11008,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.0 + debug: 4.4.1 decode-named-character-reference: 1.1.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -10691,10 +11034,16 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + mime@1.6.0: {} mimic-fn@2.1.0: {} @@ -10776,6 +11125,8 @@ snapshots: negotiator@0.6.3: {} + negotiator@1.0.0: {} + neotraverse@0.6.18: {} netmask@2.0.2: {} @@ -11019,6 +11370,8 @@ snapshots: path-to-regexp@0.1.12: {} + path-to-regexp@8.2.0: {} + path-type@4.0.0: {} pathe@2.0.3: {} @@ -11233,6 +11586,10 @@ snapshots: dependencies: side-channel: 1.1.0 + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + quansync@0.2.10: {} queue-microtask@1.2.3: {} @@ -11248,6 +11605,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@3.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -11569,6 +11933,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.39.0 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.1 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.2.0 + transitivePeerDependencies: + - supports-color + run-async@3.0.0: {} run-parallel@1.2.0: @@ -11602,6 +11976,8 @@ snapshots: safe-stable-stringify@1.1.1: {} + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} sax@1.4.1: {} @@ -11639,6 +12015,22 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.2.0: + dependencies: + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + serialize-error@12.0.0: dependencies: type-fest: 4.41.0 @@ -11652,6 +12044,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -12073,7 +12474,7 @@ snapshots: tslib@2.8.1: {} - tsx@4.19.4: + tsx@4.20.3: dependencies: esbuild: 0.25.2 get-tsconfig: 4.10.0 @@ -12124,6 +12525,12 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -12329,6 +12736,8 @@ snapshots: utils-merge@1.0.1: {} + uuid@11.1.0: {} + valibot@1.1.0(typescript@5.8.3): optionalDependencies: typescript: 5.8.3 @@ -12374,13 +12783,13 @@ snapshots: - utf-8-validate - zod - vite-node@3.1.4(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0): + vite-node@3.1.4(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.2.5(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.2.5(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -12395,18 +12804,18 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@6.2.5(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0)): + vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@6.2.5(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0)): dependencies: debug: 4.4.0 globrex: 0.1.2 tsconfck: 3.1.5(typescript@5.8.3) optionalDependencies: - vite: 6.2.5(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.2.5(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - supports-color - typescript - vite@6.2.5(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0): + vite@6.2.5(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0): dependencies: esbuild: 0.25.2 postcss: 8.5.3 @@ -12416,13 +12825,13 @@ snapshots: fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.29.3 - tsx: 4.19.4 + tsx: 4.20.3 yaml: 2.8.0 - vitest@3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0): + vitest@3.1.4(@types/debug@4.1.12)(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0): dependencies: '@vitest/expect': 3.1.4 - '@vitest/mocker': 3.1.4(vite@6.2.5(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0)) + '@vitest/mocker': 3.1.4(vite@6.2.5(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0)) '@vitest/pretty-format': 3.1.4 '@vitest/runner': 3.1.4 '@vitest/snapshot': 3.1.4 @@ -12439,8 +12848,8 @@ snapshots: tinyglobby: 0.2.13 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 6.2.5(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0) - vite-node: 3.1.4(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.19.4)(yaml@2.8.0) + vite: 6.2.5(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0) + vite-node: 3.1.4(@types/node@22.15.19)(jiti@2.4.2)(lightningcss@1.29.3)(tsx@4.20.3)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 diff --git a/tools/cli-tools/package.json b/tools/cli-tools/package.json index 26e83ea..d76cadf 100644 --- a/tools/cli-tools/package.json +++ b/tools/cli-tools/package.json @@ -16,7 +16,6 @@ }, "type": "module", "exports": { - "./package.json": "./package.json", ".": { "types": "./src/index.ts", "import": "./src/index.ts" diff --git a/tools/cli-tools/src/colors.ts b/tools/cli-tools/src/colors.ts new file mode 100644 index 0000000..c48c013 --- /dev/null +++ b/tools/cli-tools/src/colors.ts @@ -0,0 +1 @@ +export { default as colors } from "yoctocolors" diff --git a/tools/cli-tools/src/index.ts b/tools/cli-tools/src/index.ts index 52aa15f..f81d33f 100644 --- a/tools/cli-tools/src/index.ts +++ b/tools/cli-tools/src/index.ts @@ -1,6 +1,7 @@ export { default as figlet } from "figlet" -export { default as colors } from "yoctocolors" export * from "@inquirer/prompts" +export * from "./colors" +export * from "./logger" export * from "./prompts" export * from "./formatters" export * from "./update-env-file" diff --git a/tools/cli-tools/src/logger.ts b/tools/cli-tools/src/logger.ts new file mode 100644 index 0000000..e8ca4a1 --- /dev/null +++ b/tools/cli-tools/src/logger.ts @@ -0,0 +1,11 @@ +import { colors } from "./colors" + +export function createLogger(name: string, color: colors.Format = colors.gray) { + return { + log: (...args: Parameters) => { + console.log(color(`[${name}]`), ...args) + } + } +} + +export type Logger = ReturnType diff --git a/tools/cli-tools/src/prompts.ts b/tools/cli-tools/src/prompts.ts index 068b29e..875999a 100644 --- a/tools/cli-tools/src/prompts.ts +++ b/tools/cli-tools/src/prompts.ts @@ -22,6 +22,15 @@ type LogOptions = { /** * Prints messages to the console, automatically wrapping them to * the default width. Each message will be printed on a new line. + * + * @example + * ``` + * log("Hello, world!") + * log("Hello, world!", { wrap: false }) + * log("Hello, world!", { width: 40 }) + * log("Hello, world!", { spacing: 2 }) + * log("Hello, world!", { wrap: false, width: 40, spacing: 2 }) + * ``` */ export function log(...args: (string | LogOptions)[]) { let options: Required = { From 6a0a5fa7f167fe2beae88bbbbe62eea3e3285b2b Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Wed, 25 Jun 2025 10:37:08 -0400 Subject: [PATCH 2/3] Remove empty setup step from identity-a2a --- demos/identity-a2a/bin/setup | 9 --------- demos/identity-a2a/package.json | 1 - 2 files changed, 10 deletions(-) delete mode 100755 demos/identity-a2a/bin/setup diff --git a/demos/identity-a2a/bin/setup b/demos/identity-a2a/bin/setup deleted file mode 100755 index c4341a2..0000000 --- a/demos/identity-a2a/bin/setup +++ /dev/null @@ -1,9 +0,0 @@ -# #!/usr/bin/env sh - - -# -# This file initializes the repository for local development and CI. -# -# This file should be run after cloning the repository for the first time, but -# is safe to run multiple times. -# diff --git a/demos/identity-a2a/package.json b/demos/identity-a2a/package.json index 013ea5a..64c88ed 100644 --- a/demos/identity-a2a/package.json +++ b/demos/identity-a2a/package.json @@ -21,7 +21,6 @@ "demo": "tsx ./src/run-demo.ts", "lint": "eslint .", "lint:fix": "eslint . --fix", - "setup": "./bin/setup", "test": "vitest" }, "dependencies": { From 2669826dc6f0245d7581e3ebbbbec0b256269cb6 Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Wed, 25 Jun 2025 10:44:27 -0400 Subject: [PATCH 3/3] Export valibot, zod schemas for a2a --- demos/identity-a2a/src/bank-client-agent.ts | 2 +- demos/identity-a2a/src/utils/schemas.ts | 22 -------- packages/ack-id/package.json | 20 ++++--- .../a2a/{schemas.ts => schemas/valibot.ts} | 0 packages/ack-id/src/a2a/schemas/zod.ts | 52 +++++++++++++++++++ packages/ack-id/src/a2a/verify.ts | 2 +- packages/ack-id/tsdown.config.ts | 6 ++- packages/agentcommercekit/package.json | 20 ++++--- .../src/a2a/schemas/valibot.ts | 1 + .../agentcommercekit/src/a2a/schemas/zod.ts | 1 + packages/agentcommercekit/tsdown.config.ts | 6 ++- 11 files changed, 92 insertions(+), 40 deletions(-) delete mode 100644 demos/identity-a2a/src/utils/schemas.ts rename packages/ack-id/src/a2a/{schemas.ts => schemas/valibot.ts} (100%) create mode 100644 packages/ack-id/src/a2a/schemas/zod.ts create mode 100644 packages/agentcommercekit/src/a2a/schemas/valibot.ts create mode 100644 packages/agentcommercekit/src/a2a/schemas/zod.ts diff --git a/demos/identity-a2a/src/bank-client-agent.ts b/demos/identity-a2a/src/bank-client-agent.ts index 1bcb50b..62fc15b 100644 --- a/demos/identity-a2a/src/bank-client-agent.ts +++ b/demos/identity-a2a/src/bank-client-agent.ts @@ -8,11 +8,11 @@ import { createSignedA2AMessage, verifyA2AHandshakeMessage } from "agentcommercekit/a2a" +import { messageSchema } from "agentcommercekit/a2a/schemas/valibot" import { v4 as uuidV4 } from "uuid" import * as v from "valibot" import { Agent } from "./agent" import { fetchUrlFromAgentCardUrl } from "./utils/fetch-agent-card" -import { messageSchema } from "./utils/schemas" import { startAgentServer } from "./utils/server-utils" import type { AgentCard, Message } from "a2a-js" import type { DidUri } from "agentcommercekit" diff --git a/demos/identity-a2a/src/utils/schemas.ts b/demos/identity-a2a/src/utils/schemas.ts deleted file mode 100644 index 71831cd..0000000 --- a/demos/identity-a2a/src/utils/schemas.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as v from "valibot" - -// Valibot schemas for A2A response validation -const textPartSchema = v.object({ - type: v.literal("text"), - text: v.string() -}) - -const dataPartWithJwtSchema = v.object({ - type: v.literal("data"), - data: v.object({ - jwt: v.string() - }) -}) - -const partSchema = v.union([textPartSchema, dataPartWithJwtSchema]) - -export const messageSchema = v.object({ - role: v.union([v.literal("user"), v.literal("agent")]), - parts: v.array(partSchema), - metadata: v.optional(v.record(v.string(), v.unknown())) -}) diff --git a/packages/ack-id/package.json b/packages/ack-id/package.json index 6a95abc..79e3bc0 100644 --- a/packages/ack-id/package.json +++ b/packages/ack-id/package.json @@ -20,17 +20,25 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" }, - "./a2a": { - "types": "./dist/a2a/index.d.ts", - "default": "./dist/a2a/index.js" + "./schemas/valibot": { + "types": "./dist/schemas/valibot.d.ts", + "default": "./dist/schemas/valibot.js" }, "./schemas/zod": { "types": "./dist/schemas/zod.d.ts", "default": "./dist/schemas/zod.js" }, - "./schemas/valibot": { - "types": "./dist/schemas/valibot.d.ts", - "default": "./dist/schemas/valibot.js" + "./a2a": { + "types": "./dist/a2a/index.d.ts", + "default": "./dist/a2a/index.js" + }, + "./a2a/schemas/valibot": { + "types": "./dist/a2a/schemas/valibot.d.ts", + "default": "./dist/a2a/schemas/valibot.js" + }, + "./a2a/schemas/zod": { + "types": "./dist/a2a/schemas/zod.d.ts", + "default": "./dist/a2a/schemas/zod.js" } }, "main": "./dist/index.js", diff --git a/packages/ack-id/src/a2a/schemas.ts b/packages/ack-id/src/a2a/schemas/valibot.ts similarity index 100% rename from packages/ack-id/src/a2a/schemas.ts rename to packages/ack-id/src/a2a/schemas/valibot.ts diff --git a/packages/ack-id/src/a2a/schemas/zod.ts b/packages/ack-id/src/a2a/schemas/zod.ts new file mode 100644 index 0000000..9039273 --- /dev/null +++ b/packages/ack-id/src/a2a/schemas/zod.ts @@ -0,0 +1,52 @@ +import { z } from "zod" + +const roleSchema = z.enum(["agent", "user"]) + +const metadataSchema = z.record(z.string(), z.unknown()).nullable() + +// Base schema for common part properties +const partBaseSchema = z.object({ + metadata: metadataSchema.optional() +}) + +// Text part schema +export const textPartSchema = partBaseSchema.extend({ + type: z.literal("text"), + text: z.string() +}) + +// Data part schema +export const dataPartSchema = partBaseSchema.extend({ + type: z.literal("data"), + data: z.union([z.record(z.string(), z.unknown()), z.array(z.unknown())]) +}) + +// File content schema +export const fileContentSchema = z.object({ + name: z.string().nullable().optional(), + mimeType: z.string().nullable().optional(), + bytes: z.string().nullable().optional(), + uri: z.string().nullable().optional() +}) + +// File part schema +export const filePartSchema = partBaseSchema.extend({ + type: z.literal("file"), + file: fileContentSchema +}) + +// Union of all part types using discriminated union +export const partSchema = z.discriminatedUnion("type", [ + textPartSchema, + dataPartSchema, + filePartSchema +]) + +// Message schema +export const messageSchema = z + .object({ + role: roleSchema, + parts: z.array(partSchema), + metadata: metadataSchema.optional() + }) + .passthrough() diff --git a/packages/ack-id/src/a2a/verify.ts b/packages/ack-id/src/a2a/verify.ts index be40146..678d1d2 100644 --- a/packages/ack-id/src/a2a/verify.ts +++ b/packages/ack-id/src/a2a/verify.ts @@ -3,7 +3,7 @@ import { didUriSchema } from "@agentcommercekit/did/schemas/valibot" import { verifyJwt } from "@agentcommercekit/jwt" import { stringify } from "safe-stable-stringify" import * as v from "valibot" -import { dataPartSchema, messageSchema } from "./schemas" +import { dataPartSchema, messageSchema } from "./schemas/valibot" import type { DidResolver, DidUri } from "@agentcommercekit/did" import type { JwtVerified } from "@agentcommercekit/jwt" import type { Message, Task } from "a2a-js" diff --git a/packages/ack-id/tsdown.config.ts b/packages/ack-id/tsdown.config.ts index 6384ffd..5dbb4e8 100644 --- a/packages/ack-id/tsdown.config.ts +++ b/packages/ack-id/tsdown.config.ts @@ -3,9 +3,11 @@ import { defineConfig } from "tsdown/config" export default defineConfig({ entry: [ "src/index.ts", - "src/a2a/index.ts", + "src/schemas/valibot.ts", "src/schemas/zod.ts", - "src/schemas/valibot.ts" + "src/a2a/index.ts", + "src/a2a/schemas/zod.ts", + "src/a2a/schemas/valibot.ts" ], dts: true, silent: true diff --git a/packages/agentcommercekit/package.json b/packages/agentcommercekit/package.json index aa69fe5..52110b6 100644 --- a/packages/agentcommercekit/package.json +++ b/packages/agentcommercekit/package.json @@ -20,17 +20,25 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" }, - "./a2a": { - "types": "./dist/a2a/index.d.ts", - "default": "./dist/a2a/index.js" + "./schemas/valibot": { + "types": "./dist/schemas/valibot.d.ts", + "default": "./dist/schemas/valibot.js" }, "./schemas/zod": { "types": "./dist/schemas/zod.d.ts", "default": "./dist/schemas/zod.js" }, - "./schemas/valibot": { - "types": "./dist/schemas/valibot.d.ts", - "default": "./dist/schemas/valibot.js" + "./a2a": { + "types": "./dist/a2a/index.d.ts", + "default": "./dist/a2a/index.js" + }, + "./a2a/schemas/valibot": { + "types": "./dist/a2a/schemas/valibot.d.ts", + "default": "./dist/a2a/schemas/valibot.js" + }, + "./a2a/schemas/zod": { + "types": "./dist/a2a/schemas/zod.d.ts", + "default": "./dist/a2a/schemas/zod.js" } }, "main": "./dist/index.js", diff --git a/packages/agentcommercekit/src/a2a/schemas/valibot.ts b/packages/agentcommercekit/src/a2a/schemas/valibot.ts new file mode 100644 index 0000000..ad78020 --- /dev/null +++ b/packages/agentcommercekit/src/a2a/schemas/valibot.ts @@ -0,0 +1 @@ +export * from "@agentcommercekit/ack-id/a2a/schemas/valibot" diff --git a/packages/agentcommercekit/src/a2a/schemas/zod.ts b/packages/agentcommercekit/src/a2a/schemas/zod.ts new file mode 100644 index 0000000..29e0005 --- /dev/null +++ b/packages/agentcommercekit/src/a2a/schemas/zod.ts @@ -0,0 +1 @@ +export * from "@agentcommercekit/ack-id/a2a/schemas/zod" diff --git a/packages/agentcommercekit/tsdown.config.ts b/packages/agentcommercekit/tsdown.config.ts index 6384ffd..5dbb4e8 100644 --- a/packages/agentcommercekit/tsdown.config.ts +++ b/packages/agentcommercekit/tsdown.config.ts @@ -3,9 +3,11 @@ import { defineConfig } from "tsdown/config" export default defineConfig({ entry: [ "src/index.ts", - "src/a2a/index.ts", + "src/schemas/valibot.ts", "src/schemas/zod.ts", - "src/schemas/valibot.ts" + "src/a2a/index.ts", + "src/a2a/schemas/zod.ts", + "src/a2a/schemas/valibot.ts" ], dts: true, silent: true