Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
0c0b423
feat(typescript): add TypeScript samples for the Agent Payments Protocol
miguelvelasquezdev Apr 16, 2026
f9b16dc
feat(typescript): rename roles to match Python convention + scaffold …
miguelvelasquezdev May 23, 2026
42a1872
refactor(typescript): move samples to code/samples/typescript (v0.2 l…
miguelvelasquezdev May 30, 2026
93fe2e3
feat(typescript): migrate HNP/credentials flow to real SD-JWT with ha…
miguelvelasquezdev May 31, 2026
979b93c
feat(typescript): integrate Verifiable Intent SD-JWT chain, retire ha…
miguelvelasquezdev Jun 1, 2026
a124051
chore(typescript): track adk web entry-point (src/web-agents)
miguelvelasquezdev Jun 1, 2026
ba62ff4
chore(typescript): add autonomous improve+test loop harness
miguelvelasquezdev Jun 1, 2026
f450097
feat(typescript): pin L3 audience in role verifiers (reject misaddres…
miguelvelasquezdev Jun 7, 2026
8326dbe
test(typescript): add cross-role file-contract integration test
miguelvelasquezdev Jun 7, 2026
c0d52bf
test(typescript): add immediate (2-layer) flow rejection tests
miguelvelasquezdev Jun 7, 2026
0fd6c82
test(typescript): add v8 coverage for the VI integration core
miguelvelasquezdev Jun 7, 2026
184fd5f
test(typescript): cover the file-backed key store (keys.ts 0% -> 100%)
miguelvelasquezdev Jun 7, 2026
4570c58
test(typescript): cover payment amount/currency constraint boundaries
miguelvelasquezdev Jun 7, 2026
3c9cd3b
fix(typescript): consent resolves item_id before signing L2 (prompt a…
miguelvelasquezdev Jun 7, 2026
bf35c98
test(typescript): reject impostor-agent L3 and tampered L2
miguelvelasquezdev Jun 7, 2026
8f95acb
docs(typescript): add common/vi README (flow, contract, security prop…
miguelvelasquezdev Jun 7, 2026
ce33fd1
docs(typescript): log checkout-side constraint enforcement gap + loop…
miguelvelasquezdev Jun 7, 2026
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
3 changes: 3 additions & 0 deletions .cspell/custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@ Garena
gemini
genai
generativeai
genkit
genproto
glog
gofmt
googleai
gopkg
gradletasknamecache
gradlew
Expand Down Expand Up @@ -169,6 +171,7 @@ Truelayer
Trulioo
udpa
unmarshal
uuidv
viewmodel
vulnz
Wallex
Expand Down
39 changes: 39 additions & 0 deletions .github/linters/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"env": {
"node": true,
"es2022": true
},
"rules": {
"@typescript-eslint/no-explicit-any": "error"
},
"overrides": [
{
"files": ["code/samples/**/*.ts"],
"parserOptions": {
"project": "./code/samples/typescript/tsconfig.json"
},
"settings": {
"import/resolver": {
"typescript": {
"alwaysTryTypes": true,
"project": "./code/samples/typescript/tsconfig.json"
}
}
},
"rules": {
"@typescript-eslint/no-explicit-any": "off"
}
}
]
}
11 changes: 10 additions & 1 deletion .github/workflows/linter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ jobs:
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Install TypeScript Sample Dependencies
working-directory: code/samples/typescript
run: npm ci

- name: Lint Code Base
uses: super-linter/super-linter/slim@v8
env:
Expand All @@ -23,7 +32,7 @@ jobs:
LOG_LEVEL: WARN
SHELLCHECK_OPTS: -e SC1091 -e 2086
VALIDATE_ALL_CODEBASE: false
FILTER_REGEX_EXCLUDE: "^(\\.github/|\\.vscode/|code/samples/).*|CODE_OF_CONDUCT.md|CHANGELOG.md"
FILTER_REGEX_EXCLUDE: "^(\\.github/|\\.vscode/|code/samples/(python|go|android|certs)/).*|CODE_OF_CONDUCT.md|CHANGELOG.md"
VALIDATE_BIOME_FORMAT: false
VALIDATE_PYTHON_BLACK: false
VALIDATE_PYTHON_FLAKE8: false
Expand Down
10 changes: 10 additions & 0 deletions code/samples/typescript/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Copy this file to .env and fill in your own values.

# --- Google AI Studio (Option 1, recommended for development) ---
# Obtain a key from https://aistudio.google.com/apikey
GOOGLE_API_KEY=your_google_api_key_here

# --- Vertex AI (Option 2, recommended for production) ---
# GOOGLE_GENAI_USE_VERTEXAI=true
# GOOGLE_CLOUD_PROJECT=your-project-id
# GOOGLE_CLOUD_LOCATION=global
32 changes: 32 additions & 0 deletions code/samples/typescript/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Dependencies
node_modules/

# Build output
dist/

# Environment
.env
.env.*
!.env.example

# Source maps
*.js.map

# IDE
.vscode/
.idea/

# OS
.DS_Store
Thumbs.db

# Test coverage
coverage/

# Debug logs
*.log

# Runtime state
.temp-db/
scenarios/**/.logs/
scenarios/**/.temp-db/
36 changes: 36 additions & 0 deletions code/samples/typescript/BACKLOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Verifiable Intent TS integration — improvement backlog

`improve-loop.sh` works ONE unchecked item at a time. Keep items small and
independently verifiable (`npx tsc --noEmit` + `npm run lint` +
`npx vitest run test/unit` all green). Check items off (`- [x]`) when done.

> **Status (after 10 improve-loop iterations):** the unit-testable security +
> coverage + docs backlog is essentially complete — `src/common/vi` ≈96% stmts /
> 91% funcs, 30 unit tests, all gates green. The remaining open items below are
> **e2e-only** (need the running MCP/A2A servers + a Gemini key to fix *and*
> verify): payment-token↔checkout binding, replay/nonce dedup, checkout-side

Check warning on line 11 in code/samples/typescript/BACKLOG.md

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (dedup)
> constraint enforcement, the MCP-tool-extraction refactor, and live validation
> of the item-id consent fix. These should be done in a fresh, bounded session
> with the servers running — not this unbounded unit-only loop.

## Test coverage & hardening
- [x] Enable coverage: `@vitest/coverage-v8` + `test:coverage:unit` script + a coverage block in `vitest.config.ts` (scoped to `src/common/vi`). Baseline: **85.7% stmts / 82.5% branch / 73.9% funcs / 85.6% lines**.
- [x] **Coverage gap closed:** `keys.ts` now **100%** — round-trip persist/reload, per-role kids + fallback, legacy (no-kid) format, `loadViPublicJwk` missing→null + no private-scalar leak, and a persisted key signing a verifiable credential (`test/unit/vi-keys.test.ts`). Overall `src/common/vi` now **96.2% stmts / 91.3% funcs**. (`fixtures.ts` `getCatalog` still uncovered — minor.)
- [x] Strengthen `src/common/vi` negative-path tests: impostor-agent case (L3 signed by a non-delegated key whose kid matches but whose signature can't verify against L2 `cnf.jwk` → invalid) and tampered-L2 case (mutated L2 signature → invalid). In `test/unit/vi-chain-negative.test.ts`.
- [x] Add immediate-flow rejection tests: wrong issuer key, and an L2 whose `sd_hash` does not match L1 (verified against a different L1) → `verifyChain` invalid. In `test/unit/vi-chain-negative.test.ts`.
- [x] Constraint amount/currency boundaries: at-cap allowed, +1 minor unit rejected, currency mismatch rejected (`test/unit/vi-constraints.test.ts`). (Empty `acceptable_items` line-items wildcard still untested — minor.)

## Make the MCP role logic testable without servers
- [ ] Extract the pure logic of merchant-agent-mcp `complete_checkout`, credentials-provider-mcp `issue_payment_credential`, and merchant `create_checkout` into small exported functions (no `McpServer`/stdio), and unit-test their happy + error paths against the persisted-file contract in a temp `TEMP_DB`.
- [x] Add a deterministic cross-role integration test via the persisted-file contract (`test/unit/vi-integration.test.ts`): agent writes `l1/l2/chk_*/pay_*` to a temp dir, merchant + network read them back and verify with aud pinning; asserts a valid purchase and an over-budget rejection. No Gemini/servers. (Tool-level tests of the actual `*.execute` handlers are still worth adding — see item above.)
- [~] **Gap (flow):** merchant MCP serves generated slug item ids (`<slug>_0`) but L2 `acceptable_items` defaulted to VI skus, so `createAgentFulfillment` couldn't match the item. **Mitigated (iter 7):** consent prompt now calls `search_inventory` FIRST and threads the resolved `item_id` into `assembleAndSignMandates` (REQUIRED), so L2 binds the exact merchant item. Prompt audit also confirmed all v2 prompts match the current tool signatures. **Still needs e2e validation** (prompt behavior isn't unit-verifiable); a `merchant-serves-VI-catalog` alternative remains an option.

## Robustness
- [x] Pin `expectedL3*Aud` in the role `verifyChain` calls so a presentation addressed to a different party is rejected — merchant pins `MERCHANT_AUD`, CP pins `NETWORK_AUD`, agent stamps both via shared fixtures constants. Test in `test/unit/vi-chain-negative.test.ts`.
- [ ] Replay protection: verifiers don't pin nonce/`transaction_id`, and used payment tokens / mandates aren't deduped — the same authorization could be replayed. Track spent nonces/tokens across the role servers (the VI lib leaves replay to the caller) + add a test.
- [ ] **Gap (binding):** credentials-provider mints `pay_token_*` after verifying the payment chain, but the token isn't cryptographically bound to `checkout_jwt_hash`, and `complete_checkout` / the PSP don't re-verify the chain — a minted token could be presented for a different settlement. Bind the token to the checkout hash (and/or have the PSP re-verify) + add a test.
- [ ] **Gap (enforcement asymmetry, found iter 10):** `verifyCheckoutChain` (merchant) only verifies chain structure + aud — it does NOT run `checkConstraints` on the checkout-side constraints (`allowed_merchants`, `line_items`/`acceptable_items`); only `verifyPaymentChainAndConstraints` (network) enforces constraints (amount/payee). So the merchant trusts the agent's self-enforcement in `createAgentFulfillment` that the checked-out item is acceptable. Fix: enforce checkout constraints in the merchant path — decode the L3b `checkout_jwt` cart, resolve `acceptable_items` SD-refs, run `checkConstraints` (line_items max-flow). Needs careful design + e2e validation.
- [ ] `verifyPaymentChainAndConstraints`: surface a clear error when the issuer key is missing, and test it.

## Docs
- [x] Add `src/common/vi/README.md` documenting the L1→L2→L3 flow (layer model, role→layer mapping diagram, file contract, public API, and the security properties the tests enforce).
74 changes: 74 additions & 0 deletions code/samples/typescript/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# TypeScript Samples for the Agent Payments Protocol (AP2)

This directory contains TypeScript samples demonstrating how to build AP2
agents using the [Agent Development Kit (ADK)](https://google.github.io/adk-docs/)
and the [A2A SDK](https://www.npmjs.com/package/@a2a-js/sdk).

## Available Scenarios

- **[Human-Present Card Payment](./scenarios/a2a/human-present/cards/README.md)**
- Complete card payment flow with all four agents implemented in TypeScript.

See the [scenario README](./scenarios/a2a/human-present/cards/README.md) for
detailed setup and usage instructions.

## Why TypeScript for AP2 Agents?

TypeScript is a natural fit for AP2 agents that are deployed alongside web,
Node.js, or edge runtimes:

- **Type Safety**: Compile-time validation of protocol structures via Zod
schemas mirrored from the AP2 reference types.
- **Ecosystem**: Direct access to the npm ecosystem and the official
`@a2a-js/sdk` and `@google/adk` packages.
- **Portability**: Run on any Node.js 18+ runtime, including serverless and
container platforms.

## Project Structure

```text
code/samples/typescript/
├── src/
│ ├── roles/ # Agent role implementations and entry points
│ │ ├── shopping/ # Shopping Agent (root orchestrator)
│ │ │ └── subagents/ # shopper, shipping-collector, payment-collector
│ │ ├── merchant/ # Merchant Agent
│ │ ├── credentials-provider/
│ │ └── payment-processor/
│ └── common/ # Shared modules used across roles
│ ├── server/ # A2A server bootstrap, executor, middleware
│ ├── utils/ # Message and artifact helpers
│ ├── types/ # AP2 protocol object types
│ ├── schemas/ # Zod schemas mirroring the protocol
│ ├── vc/ # W3C Verifiable Credential helpers
│ ├── config/ # Session and runtime configuration
│ └── constants/ # Shared constants
├── test/
│ └── e2e/ # End-to-end smoke tests
└── scenarios/
└── a2a/
└── human-present/
└── cards/ # Card payment scenario
```

## Development

```sh
# Install dependencies
npm install

# Type-check and build
npm run build

# Start all four agents plus the ADK web UI
npm run dev

# Run the e2e smoke tests against running agents
npm test
```

See the scenario README for a full walkthrough.

## License

Copyright 2025 Google LLC. Licensed under the Apache License, Version 2.0.
50 changes: 50 additions & 0 deletions code/samples/typescript/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import importPlugin from 'eslint-plugin-import';
import globals from 'globals';

export default [
{ ignores: ['dist/**', 'node_modules/**', 'coverage/**'] },
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['src/**/*.ts', 'test/**/*.ts'],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
globals: { ...globals.node },
parserOptions: {
project: './tsconfig.json',
},
},
plugins: { import: importPlugin },
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
'import/no-unresolved': 'off',
},
},
];
35 changes: 35 additions & 0 deletions code/samples/typescript/improve-loop.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Autonomous improve + test loop — Verifiable Intent TypeScript integration

You are running headless inside `code/samples/typescript` (the AP2 TypeScript
sample). Your job: make ONE small, fully-verified improvement per run that
hardens or tests the Verifiable Intent integration (`src/common/vi` and the role
wiring in `src/roles`). Then stop.

## Procedure (do this exactly, once)

1. Read `BACKLOG.md`. Choose the single highest-priority unchecked item `- [ ]`.
If every item is already checked, pick the most valuable NEW hardening/test
task you can find and add it.
2. Make a small, focused change. Strongly prefer ADDING or STRENGTHENING tests
over changing production code. If you change production code, it must be to
fix a real bug that a test now proves.
3. Verify — ALL of these must pass before you keep the change:
- `npx tsc --noEmit` (this sample has no `typecheck` script)
- `npm run lint`
- `npx vitest run test/unit`
Do NOT run `npm test` or anything in `test/e2e/` — e2e needs a live Gemini
API key and running servers, and will fail or hang here.
4. If green: tick the item in `BACKLOG.md` (`- [x]`), then `git add -A` and
`git commit -m "loop: <what you did>"`. If you discovered new useful work,
append new `- [ ]` items to `BACKLOG.md`.
5. If you cannot get to green: run `git checkout -- .` to revert your changes,
leave the item unchecked, and append a short `blocked:` note under it.

## Hard rules

- Never delete, skip, weaken, or loosen an assertion just to make tests pass.
- Never edit files under `.git/`, `.github/`, `.claude/`, or the npm-linked

Check warning on line 31 in code/samples/typescript/improve-loop.prompt.md

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (claude)
`node_modules/@verifiable-intent` package. Only touch this sample.
- Keep each change minimal and reversible. One backlog item per run, then stop.
- Amounts are in minor units (cents). Scenario fixtures live in
`src/common/vi/fixtures.ts`; the layer model lives in `src/common/vi/flow.ts`.
61 changes: 61 additions & 0 deletions code/samples/typescript/improve-loop.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
#
# Autonomous improve + test loop for the Verifiable Intent TS integration.
#
# Each iteration runs Claude Code headless against improve-loop.prompt.md, which
# completes ONE backlog item, verifies it (typecheck + lint + unit tests), and
# commits it. The loop stops when all gates are green AND BACKLOG.md has no
# unchecked items, or after MAX_ITERS iterations.
#
# Guardrails: --bare (no hooks/plugins), acceptEdits, a scoped tool allowlist
# (only npm/npx/git/node + file tools — no network), and a git commit per
# iteration so any step is one `git reset` away from rollback. e2e tests are
# never run here (they need a Gemini key + live servers).
#
# Usage: ./improve-loop.sh [max_iterations] # default 12
# LOOP_MODEL=opus ./improve-loop.sh 5 # override model
#
set -uo pipefail
cd "$(dirname "$0")"

MAX_ITERS="${1:-12}"
MODEL="${LOOP_MODEL:-sonnet}"
PROMPT_FILE="improve-loop.prompt.md"
ALLOWED="Read,Edit,Write,Glob,Grep,Bash(npm:*),Bash(npx:*),Bash(git:*),Bash(node:*)"

if [ ! -f "$PROMPT_FILE" ]; then
echo "error: $PROMPT_FILE not found (run from code/samples/typescript)" >&2
exit 1
fi

gates_green() {
npx tsc --noEmit >/dev/null 2>&1 \
&& npm run lint >/dev/null 2>&1 \
&& npx vitest run test/unit >/dev/null 2>&1
}

# No unchecked "- [ ]" items remain in the backlog.
backlog_done() {
! grep -qE '^[[:space:]]*-[[:space:]]\[ \]' BACKLOG.md
}

for i in $(seq 1 "$MAX_ITERS"); do
echo "──────────────────────────────────────────────────────────"
echo " iteration $i / $MAX_ITERS"
echo "──────────────────────────────────────────────────────────"

claude --bare -p "$(cat "$PROMPT_FILE")" \

Check warning on line 47 in code/samples/typescript/improve-loop.sh

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (claude)
--model "$MODEL" \
--permission-mode acceptEdits \
--allowedTools "$ALLOWED"

if gates_green && backlog_done; then
echo "✅ all gates green and BACKLOG.md fully checked — stopping."
break
fi
echo "… not done yet; continuing to next iteration."
done

echo ""
echo "Loop finished. Recent commits:"
git --no-pager log --oneline -"$MAX_ITERS" 2>/dev/null || true

Check warning on line 61 in code/samples/typescript/improve-loop.sh

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (oneline)
Loading
Loading