Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/red-cloths-grow.md
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ npm-debug.log*
*.db
*.sqlite
*.sqlite3

# Claude
.claude/*.local.*
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion demos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion demos/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion demos/e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
135 changes: 135 additions & 0 deletions demos/identity-a2a/README.md
Original file line number Diff line number Diff line change
@@ -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": "<signed-JWT-from-customer>"
}
}
]
}
```

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": "<signed-JWT-from-bank>"
}
}
]
}
```

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": <the-message-object-without-metadata> }`, 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
7 changes: 7 additions & 0 deletions demos/identity-a2a/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// @ts-check

import { config } from "@repo/eslint-config/base"

export default config({
root: import.meta.dirname
})
45 changes: 45 additions & 0 deletions demos/identity-a2a/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"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",
"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"
}
}
117 changes: 117 additions & 0 deletions demos/identity-a2a/src/agent.ts
Original file line number Diff line number Diff line change
@@ -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<T extends Agent>(
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<SendMessageResponse> {
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<CancelTaskResponse> {
return {
jsonrpc: "2.0",
id: request.id,
error: { code: -32601, message: "Operation not supported" }
}
}

onMessageStream(
_request: SendMessageStreamingRequest
): AsyncGenerator<SendMessageStreamingResponse, void, unknown> {
throw new Error("Method not implemented.")
}
onResubscribe(
_request: TaskResubscriptionRequest
): AsyncGenerator<SendMessageStreamingResponse, void, unknown> {
throw new Error("Method not implemented.")
}
}
Loading