├── .changeset ├── README.md └── config.json ├── .github ├── actions │ └── setup │ │ └── action.yaml └── workflows │ └── check.yaml ├── .gitignore ├── .prettierignore ├── .vscode ├── extensions.json └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── assets └── readme-hero.png ├── bin ├── release └── setup ├── cspell.config.yaml ├── demos ├── README.md ├── e2e │ ├── README.md │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── agent.ts │ │ ├── credential-issuer.ts │ │ ├── credential-verifier.ts │ │ ├── index.ts │ │ ├── payment-required-error.ts │ │ ├── receipt-issuer.ts │ │ ├── receipt-verifier.ts │ │ ├── user.ts │ │ ├── utils │ │ │ └── evm-address.ts │ │ └── verification.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── identity │ ├── .env.example │ ├── README.md │ ├── bin │ │ └── setup │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── agent.ts │ │ ├── client-agent.ts │ │ ├── credential-issuer.ts │ │ ├── credential-verifier.ts │ │ ├── get-model.ts │ │ ├── haiku-agent.ts │ │ ├── identity-tools.ts │ │ ├── index.ts │ │ ├── owner.ts │ │ └── serve-agent.ts │ ├── tsconfig.json │ └── vitest.config.ts └── payments │ ├── .env.example │ ├── README.md │ ├── bin │ ├── secret │ └── setup │ ├── eslint.config.js │ ├── package.json │ ├── src │ ├── constants.ts │ ├── global.d.ts │ ├── index.ts │ ├── payment-service.ts │ ├── receipt-service.ts │ ├── server.ts │ └── utils │ │ ├── as-address.ts │ │ ├── ensure-balances.ts │ │ ├── ensure-private-keys.ts │ │ ├── keypair-info.ts │ │ └── usdc-contract.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── docs ├── README.md ├── ack-id │ ├── future-directions.mdx │ ├── identity-model.mdx │ ├── implementation-challenges.mdx │ ├── introduction.mdx │ ├── lifecycle-management.mdx │ ├── security-privacy.mdx │ ├── standards.mdx │ ├── trust-framework.mdx │ └── use-case-verification.mdx ├── ack-pay │ ├── client-initiated-sequence.mdx │ ├── components-roles.mdx │ ├── core-payment-sequences.mdx │ ├── hitl.mdx │ ├── introduction.mdx │ ├── operational-considerations.mdx │ ├── payment-request-payload.mdx │ ├── payment-service.mdx │ ├── receipt-verification.mdx │ ├── server-initiated-sequence.mdx │ ├── summary.mdx │ └── use-cases.mdx ├── demos │ ├── demo-e2e.mdx │ ├── demo-identity.mdx │ ├── demo-payments.mdx │ ├── demos.mdx │ ├── example-identity.mdx │ ├── example-local-did-host.mdx │ ├── example-verifier.mdx │ ├── examples.mdx │ └── quickstart.mdx ├── docs.json ├── favicon.svg ├── images │ ├── ackid.png │ ├── ackoverview.png │ ├── ackpay.png │ ├── componentsandroles.png │ ├── hero-dark.png │ ├── hero-light.png │ ├── hero1.png │ ├── human.png │ ├── id1.png │ ├── idmodel.png │ ├── lifecycle.png │ ├── nopaymentservice.png │ ├── opengraph-image.png │ ├── pay1.png │ └── vc.png ├── logo │ ├── dark.svg │ └── light.svg ├── overview │ ├── architecture.mdx │ ├── concepts.mdx │ ├── future-directions.mdx │ ├── introduction.mdx │ └── references.mdx ├── package.json ├── resources │ ├── contributing.mdx │ ├── license.mdx │ └── versioning.mdx └── snippets │ ├── client-sequence.mdx │ ├── components-roles-mermaid.mdx │ ├── example-hitl-mermaid.mdx │ ├── impl-arch-patterns-mermaid.mdx │ ├── lifecycle-management-mermaid.mdx │ ├── overview-download-card.mdx │ ├── server-sequence.mdx │ ├── standards-mermaid.mdx │ └── verification-example-mermaid.mdx ├── examples ├── issuer │ ├── .env.example │ ├── README.md │ ├── bin │ │ ├── secret │ │ ├── setup │ │ └── start-server.ts │ ├── drizzle.config.ts │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── db │ │ │ ├── get-db.ts │ │ │ ├── migrations │ │ │ │ ├── 0000_aromatic_dormammu.sql │ │ │ │ └── meta │ │ │ │ │ ├── 0000_snapshot.json │ │ │ │ │ └── _journal.json │ │ │ ├── queries │ │ │ │ ├── credentials.ts │ │ │ │ └── status-lists.ts │ │ │ ├── schema.ts │ │ │ └── utils │ │ │ │ ├── get-status-list-position.test.ts │ │ │ │ └── get-status-list-position.ts │ │ ├── global.d.ts │ │ ├── index.ts │ │ ├── lib │ │ │ ├── credentials │ │ │ │ ├── build-signed-credential.test.ts │ │ │ │ └── build-signed-credential.ts │ │ │ ├── types.ts │ │ │ └── utils │ │ │ │ ├── compress-bit-string.test.ts │ │ │ │ └── compress-bit-string.ts │ │ ├── middleware │ │ │ ├── database.ts │ │ │ ├── did-resolver.ts │ │ │ └── issuer.ts │ │ ├── routes │ │ │ ├── credentials.test.ts │ │ │ ├── credentials.ts │ │ │ ├── healthcheck.ts │ │ │ ├── receipts.test.ts │ │ │ ├── receipts.ts │ │ │ ├── status.ts │ │ │ └── well-known.ts │ │ └── test-helpers │ │ │ └── did-web-with-signer.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── local-did-host │ ├── .env.example │ ├── README.md │ ├── bin │ │ ├── secret │ │ ├── serve.ts │ │ └── setup │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── global.d.ts │ │ ├── index.ts │ │ ├── lib │ │ │ ├── build-url.ts │ │ │ └── identity.ts │ │ └── middleware │ │ │ └── identities.ts │ ├── tsconfig.json │ └── vitest.config.ts └── verifier │ ├── .env.example │ ├── README.md │ ├── bin │ ├── secret │ ├── serve.ts │ └── setup │ ├── eslint.config.js │ ├── package.json │ ├── src │ ├── global.d.ts │ ├── index.ts │ ├── middleware │ │ └── verifier.ts │ └── routes │ │ ├── healthcheck.ts │ │ ├── verify.ts │ │ └── well-known.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── package.json ├── packages ├── ack-id │ ├── CHANGELOG.md │ ├── README.md │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── controller-claim-verifier.test.ts │ │ ├── controller-claim-verifier.ts │ │ ├── controller-credential.test.ts │ │ ├── controller-credential.ts │ │ ├── index.ts │ │ └── schemas │ │ │ ├── valibot.ts │ │ │ └── zod.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── ack-pay │ ├── CHANGELOG.md │ ├── README.md │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── create-payment-receipt.test.ts │ │ ├── create-payment-receipt.ts │ │ ├── create-payment-request-body.test.ts │ │ ├── create-payment-request-body.ts │ │ ├── create-payment-token.test.ts │ │ ├── create-payment-token.ts │ │ ├── errors.ts │ │ ├── index.ts │ │ ├── payment-request.test.ts │ │ ├── payment-request.ts │ │ ├── receipt-claim-verifier.test.ts │ │ ├── receipt-claim-verifier.ts │ │ ├── schemas │ │ │ ├── valibot.ts │ │ │ └── zod.ts │ │ ├── verify-payment-receipt.test.ts │ │ ├── verify-payment-receipt.ts │ │ ├── verify-payment-token.test.ts │ │ └── verify-payment-token.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── agentcommercekit │ ├── CHANGELOG.md │ ├── README.md │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── schemas │ │ │ ├── valibot.ts │ │ │ └── zod.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── did │ ├── CHANGELOG.md │ ├── README.md │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── create-did-document.test.ts │ │ ├── create-did-document.ts │ │ ├── did-document.ts │ │ ├── did-resolvers │ │ │ ├── did-resolver.test.ts │ │ │ ├── did-resolver.ts │ │ │ ├── get-did-resolver.ts │ │ │ ├── web-did-resolver.test.ts │ │ │ └── web-did-resolver.ts │ │ ├── did-uri.test.ts │ │ ├── did-uri.ts │ │ ├── errors.ts │ │ ├── index.ts │ │ ├── methods │ │ │ ├── did-key.test.ts │ │ │ ├── did-key.ts │ │ │ ├── did-pkh.ts │ │ │ ├── did-web.test.ts │ │ │ └── did-web.ts │ │ ├── resolve-did.test.ts │ │ ├── resolve-did.ts │ │ ├── schemas │ │ │ ├── valibot.ts │ │ │ └── zod.ts │ │ └── types.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── jwt │ ├── CHANGELOG.md │ ├── README.md │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── create-jwt.test.ts │ │ ├── create-jwt.ts │ │ ├── index.ts │ │ ├── jwt-algorithm.test.ts │ │ ├── jwt-algorithm.ts │ │ ├── jwt-string.test.ts │ │ ├── jwt-string.ts │ │ ├── schemas │ │ │ ├── valibot.ts │ │ │ └── zod.ts │ │ ├── signer.test.ts │ │ └── signer.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── keys │ ├── CHANGELOG.md │ ├── README.md │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── curves │ │ │ ├── ed25519.test.ts │ │ │ ├── ed25519.ts │ │ │ ├── secp256k1.test.ts │ │ │ └── secp256k1.ts │ │ ├── encoding │ │ │ ├── base58.test.ts │ │ │ ├── base58.ts │ │ │ ├── base64.test.ts │ │ │ ├── base64.ts │ │ │ ├── hex.test.ts │ │ │ ├── hex.ts │ │ │ ├── index.ts │ │ │ ├── jwk.test.ts │ │ │ ├── jwk.ts │ │ │ ├── multibase.test.ts │ │ │ └── multibase.ts │ │ ├── index.ts │ │ ├── keypair.test.ts │ │ ├── keypair.ts │ │ ├── public-key.test.ts │ │ ├── public-key.ts │ │ └── types.ts │ ├── tsconfig.json │ └── vitest.config.ts └── vc │ ├── CHANGELOG.md │ ├── README.md │ ├── eslint.config.js │ ├── package.json │ ├── src │ ├── create-credential.test.ts │ ├── create-credential.ts │ ├── index.ts │ ├── is-credential.ts │ ├── revocation │ │ ├── is-status-list-credential.ts │ │ ├── make-revocable.test.ts │ │ ├── make-revocable.ts │ │ ├── status-list-credential.test.ts │ │ ├── status-list-credential.ts │ │ └── types.ts │ ├── schemas │ │ ├── valibot.ts │ │ └── zod.ts │ ├── sign-credential.test.ts │ ├── sign-credential.ts │ ├── types.ts │ └── verification │ │ ├── errors.ts │ │ ├── is-expired.test.ts │ │ ├── is-expired.ts │ │ ├── is-revoked.test.ts │ │ ├── is-revoked.ts │ │ ├── parse-jwt-credential.test.ts │ │ ├── parse-jwt-credential.ts │ │ ├── types.ts │ │ ├── verify-parsed-credential.test.ts │ │ ├── verify-parsed-credential.ts │ │ ├── verify-proof.test.ts │ │ └── verify-proof.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── prettier.config.mjs ├── tools ├── api-utils │ ├── README.md │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── api-response.ts │ │ ├── exceptions.ts │ │ ├── middleware │ │ │ ├── error-handler.ts │ │ │ ├── logger.ts │ │ │ └── signed-payload-validator.ts │ │ ├── validate-payload.test.ts │ │ └── validate-payload.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── cli-tools │ ├── README.md │ ├── eslint.config.js │ ├── package.json │ ├── src │ │ ├── formatters.ts │ │ ├── index.ts │ │ ├── prompts.ts │ │ └── update-env-file.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── eslint-config │ ├── README.md │ ├── base.js │ └── package.json └── typescript-config │ ├── README.md │ ├── base-app.json │ ├── base.json │ ├── package.json │ └── typescript-library.json ├── tsconfig.json └── turbo.jsonc /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "agentcommercekit/ack" } 6 | ], 7 | "commit": false, 8 | "tag": false, 9 | "fixed": [], 10 | "linked": [["agentcommercekit", "@agentcommercekit/*"]], 11 | "access": "public", 12 | "baseBranch": "main", 13 | "updateInternalDependencies": "patch", 14 | "ignore": ["@examples/*", "@demos/*", "@docs/*", "@repo/*"] 15 | } 16 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yaml: -------------------------------------------------------------------------------- 1 | name: Setup Action 2 | description: Set up the project and install dependencies 3 | runs: 4 | using: "composite" 5 | steps: 6 | - run: corepack enable 7 | shell: bash 8 | 9 | - uses: actions/setup-node@v4 10 | with: 11 | node-version: "22" 12 | cache: "pnpm" 13 | 14 | - run: pnpm install --frozen-lockfile 15 | shell: bash 16 | -------------------------------------------------------------------------------- /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | check: 13 | runs-on: ubuntu-latest 14 | env: 15 | ANTHROPIC_API_KEY: secret 16 | ISSUER_PRIVATE_KEY: "0xa45f5c566918ef954e8c200a96b14092cabcd69cb8a1a132804a2b8cbb8489a1" 17 | VERIFIER_PRIVATE_KEY: "0xeeca8f89b2f5196126f7d9199e739153bd43a13f9cdd1099e7191a33143a2059" 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: ./.github/actions/setup 21 | - run: pnpm run build 22 | - run: pnpm run check 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # Local env files 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # Testing 16 | coverage 17 | 18 | # Turbo 19 | .turbo 20 | 21 | # Vercel 22 | .vercel 23 | 24 | # Build Outputs 25 | .next/ 26 | out/ 27 | build 28 | dist 29 | 30 | # Debug 31 | npm-debug.log* 32 | 33 | # Misc 34 | .DS_Store 35 | *.pem 36 | 37 | # Cloudflare 38 | .wrangler 39 | .dev.vars 40 | 41 | # Database 42 | *.db 43 | *.sqlite 44 | *.sqlite3 45 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/migrations/meta/** 2 | pnpm-lock.yaml 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[css]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | }, 5 | "[javascript]": { 6 | "editor.defaultFormatter": "esbenp.prettier-vscode" 7 | }, 8 | "[json]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | }, 11 | "[jsonc]": { 12 | "editor.defaultFormatter": "esbenp.prettier-vscode" 13 | }, 14 | "[markdown]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode" 16 | }, 17 | "[mdx]": { 18 | "editor.defaultFormatter": "esbenp.prettier-vscode" 19 | }, 20 | "[typescript]": { 21 | "editor.defaultFormatter": "esbenp.prettier-vscode" 22 | }, 23 | 24 | "editor.codeActionsOnSave": { 25 | "source.fixAll": "explicit" 26 | }, 27 | "editor.formatOnSave": true, 28 | "editor.tabSize": 2, 29 | 30 | "eslint.useFlatConfig": true, 31 | 32 | "files.insertFinalNewline": true, 33 | "files.trimTrailingWhitespace": true, 34 | 35 | "typescript.enablePromptUseWorkspaceTsdk": true, 36 | "typescript.tsdk": "node_modules/typescript/lib", 37 | 38 | "eslint.workingDirectories": [ 39 | { 40 | "mode": "auto" 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Catena Labs, Inc. and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | We appreciate your help in keeping the Agent Commerce Kit (ACK) secure. 4 | 5 | If you discover a potential security vulnerability in our protocols or implementations, please reach out to us at [security@agentcommercekit.com](mailto:security@agentcommercekit.com). We take all reports seriously and will work promptly to address any concerns. 6 | 7 | Thank you for responsibly disclosing and helping protect our community. 8 | -------------------------------------------------------------------------------- /assets/readme-hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentcommercekit/ack/4031271a7c78288b0ac9fe2cf49e52eab3e0efb1/assets/readme-hero.png -------------------------------------------------------------------------------- /bin/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Clean repository 4 | pnpm run Clean 5 | 6 | # Build packages 7 | pnpm run build 8 | 9 | # Publish packages 10 | pnpm exec changeset publish 11 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # This file initializes the repository for local development and CI. 5 | # 6 | # This file should be run after cloning the repository for the first time, but 7 | # is safe to run multiple times. 8 | # 9 | 10 | # Install dependencies 11 | echo "✨ Installing dependencies ..." 12 | pnpm install --frozen-lockfile 13 | 14 | # Build the project packages 15 | echo "\n✨ Building packages ..." 16 | pnpm run build 17 | 18 | # Run setup scripts 19 | echo "\n✨ Running setup scripts ..." 20 | pnpm exec turbo setup 21 | 22 | # 23 | # Done. 24 | # 25 | echo "\n🎉 All set. Happy coding!" 26 | -------------------------------------------------------------------------------- /cspell.config.yaml: -------------------------------------------------------------------------------- 1 | language: en 2 | words: 3 | - agentcommercekit 4 | - bitstring 5 | - caip 6 | - dbname 7 | - healthcheck 8 | - keypair 9 | - multibase 10 | - multicodec 11 | - solana 12 | - valibot 13 | - varint 14 | - viem 15 | -------------------------------------------------------------------------------- /demos/README.md: -------------------------------------------------------------------------------- 1 | # Agent Commerce Kit Demos 2 | 3 | In this directory you'll find several Demos walking you through different parts of the Agent Commerce Kit. 4 | 5 | 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`. 6 | 7 | ## Demos 8 | 9 | ### Identity demo 10 | 11 | An example of server and client agents identifying each other using ACK-ID. 12 | 13 | ```sh 14 | pnpm demo:identity 15 | ``` 16 | 17 | [View the code](./identity) 18 | 19 | ### Payments demo 20 | 21 | An example of a server requiring and verifying payment, using ACK-Pay. 22 | 23 | ```sh 24 | pnpm demo:payments 25 | ``` 26 | 27 | [View the code](./payments) 28 | 29 | ### End-to-End demo 30 | 31 | A combined demo showcasing ACK-ID and ACK-Pay together. 32 | 33 | ```sh 34 | pnpm demo:e2e 35 | ``` 36 | 37 | [View the code](./e2e) 38 | 39 | ## Note 40 | 41 | These demos are designed as interactive walkthroughs of various ACK flows. The source code is designed with this in mind, and may not be suitable for production environments. 42 | -------------------------------------------------------------------------------- /demos/e2e/README.md: -------------------------------------------------------------------------------- 1 | # ACK End-to-End Demo 2 | 3 | This demo runs through the entire set of Agent Commerce Kit (ACK) protocols, showcasing how they can work seamlessly together to provide trusted, verifiable transactions between agents. 4 | 5 | This demo uses both ACK-ID and ACK-Pay to enable two agents to establish a secure connection, establish paywalls, and pay for access, while enabling identity exchange for compliant transactions. 6 | 7 | ## Getting started 8 | 9 | Before starting, please follow the [Getting Started](../../README.md#getting-started) guide at the root of this monorepo. 10 | 11 | ## Running the demo 12 | 13 | You can use the demo by running the following command from the root of this repository: 14 | 15 | ```sh 16 | pnpm run demo:e2e 17 | ``` 18 | 19 | Alternatively, you can run the demo from this directory with: 20 | 21 | ```sh 22 | pnpm start 23 | ``` 24 | 25 | ## Overview of the demo 26 | 27 | The interactive CLI will guide you through the following steps: 28 | 29 | 1. Creation of Client and Server Agents and their identities. 30 | 1. Establishing proof of ownership between Users and the Agents they control. 31 | 1. Agent to Agent communication, interrupted with a paywall. 32 | 1. Payment Execution: The Client Agent makes an on-chain (simulated) payment. 33 | 1. Receipt Issuance: A Receipt Issuer provides a Receipt for the payment. 34 | 1. Resuming Interaction with Receipt: The Agent retries the request, now including the payment receipt. 35 | 1. Receipt Verification & Service Delivery: The Server Agent verifies the receipt and provides the service. 36 | 37 | ## Learn More 38 | 39 | - [Agent Commerce Kit](https://www.agentcommercekit.com) Documentation 40 | - [ACK-ID](https://www.agentcommercekit.com/ack-id) Documentation 41 | - [ACK-Pay](https://www.agentcommercekit.com/ack-pay) Documentation 42 | -------------------------------------------------------------------------------- /demos/e2e/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { config } from "@repo/eslint-config/base" 4 | 5 | export default config({ 6 | root: import.meta.dirname 7 | }) 8 | -------------------------------------------------------------------------------- /demos/e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@demos/e2e", 3 | "version": "0.0.1", 4 | "private": true, 5 | "homepage": "https://github.com/agentcommercekit/ack#readme", 6 | "bugs": "https://github.com/agentcommercekit/ack/issues", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/agentcommercekit/ack.git", 10 | "directory": "demos/e2e" 11 | }, 12 | "license": "MIT", 13 | "author": { 14 | "name": "Catena Labs", 15 | "url": "https://catenalabs.com" 16 | }, 17 | "type": "module", 18 | "main": "./src/index.ts", 19 | "scripts": { 20 | "check:types": "tsc --noEmit", 21 | "clean": "git clean -fdX .turbo", 22 | "lint": "eslint .", 23 | "lint:fix": "eslint . --fix", 24 | "start": "tsx ./src/index.ts", 25 | "test": "vitest" 26 | }, 27 | "dependencies": { 28 | "@repo/cli-tools": "workspace:*", 29 | "agentcommercekit": "workspace:*", 30 | "valibot": "^1.1.0", 31 | "viem": "^2.29.4" 32 | }, 33 | "devDependencies": { 34 | "@repo/eslint-config": "workspace:*", 35 | "@repo/typescript-config": "workspace:*", 36 | "eslint": "^9.27.0", 37 | "tsx": "^4.19.4", 38 | "typescript": "^5", 39 | "vitest": "^3.1.4" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /demos/e2e/src/payment-required-error.ts: -------------------------------------------------------------------------------- 1 | import type { PaymentRequest } from "agentcommercekit" 2 | 3 | /** 4 | * Custom error class for 402 Payment Required responses 5 | */ 6 | export class PaymentRequiredError extends Error { 7 | paymentRequest: PaymentRequest 8 | paymentToken: string 9 | 10 | constructor(paymentRequest: PaymentRequest, paymentToken: string) { 11 | super("402 Payment Required") 12 | this.name = "PaymentRequiredError" 13 | this.paymentRequest = paymentRequest 14 | this.paymentToken = paymentToken 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /demos/e2e/src/user.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createDidPkhDocument, 3 | createJwtSigner, 4 | generateKeypair 5 | } from "agentcommercekit" 6 | import { publicKeyToAddress } from "./utils/evm-address" 7 | import type { 8 | DidDocument, 9 | DidPkhChainId, 10 | DidResolver, 11 | DidUri, 12 | JwtSigner, 13 | Keypair 14 | } from "agentcommercekit" 15 | 16 | interface ConstructorParams { 17 | wallet: Keypair 18 | preferredChainId: DidPkhChainId 19 | did: DidUri 20 | didDocument: DidDocument 21 | resolver: DidResolver 22 | } 23 | 24 | export class User { 25 | readonly wallet: Keypair 26 | readonly preferredChainId: DidPkhChainId 27 | readonly did: DidUri 28 | readonly didDocument: DidDocument 29 | readonly signer: JwtSigner 30 | 31 | private constructor({ 32 | did, 33 | didDocument, 34 | resolver, 35 | wallet, 36 | preferredChainId 37 | }: ConstructorParams) { 38 | // Wallet 39 | this.wallet = wallet 40 | this.preferredChainId = preferredChainId 41 | this.signer = createJwtSigner(wallet) 42 | 43 | // DID 44 | this.did = did 45 | this.didDocument = didDocument 46 | resolver.addToCache(did, didDocument) 47 | } 48 | 49 | static async create( 50 | resolver: DidResolver, 51 | chainId: DidPkhChainId 52 | ): Promise { 53 | const wallet = await generateKeypair("secp256k1") 54 | const address = publicKeyToAddress(wallet.publicKey) 55 | const { did, didDocument } = createDidPkhDocument({ 56 | address, 57 | chainId, 58 | keypair: wallet 59 | }) 60 | 61 | return new User({ 62 | wallet, 63 | did, 64 | didDocument, 65 | resolver, 66 | preferredChainId: chainId 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /demos/e2e/src/utils/evm-address.ts: -------------------------------------------------------------------------------- 1 | import { bytesToHexString } from "agentcommercekit" 2 | import { publicKeyToAddress as viemPublicKeyToAddress } from "viem/accounts" 3 | 4 | /** 5 | * Converts an secp256k1 public key byte array to an EVM address by converting 6 | * to an `0x`-prefixed hex string and using viem's `publicKeyToAddress` function. 7 | */ 8 | export function publicKeyToAddress(publicKeyBytes: Uint8Array) { 9 | return viemPublicKeyToAddress(`0x${bytesToHexString(publicKeyBytes)}`) 10 | } 11 | -------------------------------------------------------------------------------- /demos/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/typescript-library.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["./src/*"] 7 | } 8 | }, 9 | "include": ["."], 10 | "exclude": ["node_modules", "dist"] 11 | } 12 | -------------------------------------------------------------------------------- /demos/e2e/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig({ 4 | test: { 5 | passWithNoTests: true, 6 | watch: false 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /demos/identity/.env.example: -------------------------------------------------------------------------------- 1 | ANTHROPIC_API_KEY="" 2 | OPENAI_API_KEY="" 3 | -------------------------------------------------------------------------------- /demos/identity/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # This file initializes the repository for local development and CI. 5 | # 6 | # This file should be run after cloning the repository for the first time, but 7 | # is safe to run multiple times. 8 | # 9 | 10 | # Environment file configuration 11 | ENV_FILE=".env" 12 | EXAMPLE_FILE=".env.example" 13 | 14 | ENV_FILE_PATH="$(dirname $0)/../${ENV_FILE}" 15 | EXAMPLE_FILE_PATH="$(dirname $0)/../${EXAMPLE_FILE}" 16 | 17 | # Adds a secret to the .env file unless the key is already defined. This 18 | # allows us to add new default values even if a `.env` file already exists 19 | define_secret() { 20 | local secret_name=$1 21 | local secret_value=$2 22 | 23 | # Check if the secret exists and has a non-empty value 24 | if grep -q "^${secret_name}=\"[^\"]\\+\"" "${ENV_FILE_PATH}"; then 25 | echo " > '${secret_name}' already defined, skipping ..." 26 | return 27 | fi 28 | 29 | # Remove any existing line with this secret name (in case it was empty) 30 | if [ -f "${ENV_FILE_PATH}" ]; then 31 | grep -v "^${secret_name}=" "${ENV_FILE_PATH}" > "${ENV_FILE_PATH}.tmp" 32 | mv "${ENV_FILE_PATH}.tmp" "${ENV_FILE_PATH}" 33 | fi 34 | 35 | echo " > Setting '${secret_name}' ..." 36 | echo "${secret_name}=\"${secret_value}\"" >> "${ENV_FILE_PATH}" 37 | } 38 | 39 | # 40 | # Create an environment file from the example file (if it exists)and populate it 41 | # with auto-generated secrets 42 | # 43 | echo "\n✨ Creating a ${ENV_FILE} file for local environment variables..." 44 | if [ ! -f "${ENV_FILE_PATH}" ]; then 45 | if [ -f "${EXAMPLE_FILE_PATH}" ]; then 46 | echo "\n✨ Creating ${ENV_FILE} file from ${EXAMPLE_FILE}..." 47 | cp "${EXAMPLE_FILE_PATH}" "${ENV_FILE_PATH}" 48 | else 49 | echo "\n✨ ${EXAMPLE_FILE} not found, creating empty ${ENV_FILE}..." 50 | touch "${ENV_FILE_PATH}" 51 | fi 52 | else 53 | echo "\n✨ ${ENV_FILE} file already exists, skipping..." 54 | fi 55 | -------------------------------------------------------------------------------- /demos/identity/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { config } from "@repo/eslint-config/base" 4 | 5 | export default config({ 6 | root: import.meta.dirname 7 | }) 8 | -------------------------------------------------------------------------------- /demos/identity/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@demos/identity", 3 | "version": "0.0.1", 4 | "private": true, 5 | "homepage": "https://github.com/agentcommercekit/ack#readme", 6 | "bugs": "https://github.com/agentcommercekit/ack/issues", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/agentcommercekit/ack.git", 10 | "directory": "demos/identity" 11 | }, 12 | "license": "MIT", 13 | "author": { 14 | "name": "Catena Labs", 15 | "url": "https://catenalabs.com" 16 | }, 17 | "type": "module", 18 | "main": "./src/index.ts", 19 | "scripts": { 20 | "check:types": "tsc --noEmit", 21 | "clean": "git clean -fdX .turbo", 22 | "lint": "eslint .", 23 | "lint:fix": "eslint . --fix", 24 | "setup": "./bin/setup", 25 | "start": "dotenv -e .env -- tsx ./src/index.ts", 26 | "test": "vitest" 27 | }, 28 | "dependencies": { 29 | "@ai-sdk/anthropic": "^1.2.11", 30 | "@ai-sdk/openai": "^1.3.22", 31 | "@ai-sdk/valibot": "^0.1.28", 32 | "@hono/node-server": "^1.14.2", 33 | "@hono/valibot-validator": "^0.5.2", 34 | "@repo/api-utils": "workspace:*", 35 | "@repo/cli-tools": "workspace:*", 36 | "agentcommercekit": "workspace:*", 37 | "ai": "^4.3.16", 38 | "hono": "^4.7.10", 39 | "valibot": "^1.1.0" 40 | }, 41 | "devDependencies": { 42 | "@repo/eslint-config": "workspace:*", 43 | "@repo/typescript-config": "workspace:*", 44 | "@types/node": "^22", 45 | "dotenv-cli": "^8.0.0", 46 | "eslint": "^9.27.0", 47 | "tsx": "^4.19.4", 48 | "typescript": "^5", 49 | "vitest": "^3.1.4" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /demos/identity/src/credential-verifier.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createDidWebDocumentFromKeypair, 3 | createJwtSigner, 4 | generateKeypair, 5 | getControllerClaimVerifier, 6 | verifyParsedCredential 7 | } from "agentcommercekit" 8 | import type { 9 | DidDocument, 10 | DidResolver, 11 | DidUri, 12 | JwtSigner, 13 | Keypair, 14 | W3CCredential 15 | } from "agentcommercekit" 16 | 17 | interface CredentialVerifierParams { 18 | baseUrl: string 19 | resolver: DidResolver 20 | trustedIssuers?: DidUri[] 21 | keypair: Keypair 22 | } 23 | 24 | export class CredentialVerifier { 25 | readonly did: DidUri 26 | readonly didDocument: DidDocument 27 | private readonly keypair: Keypair 28 | private readonly signer: JwtSigner 29 | private readonly resolver: DidResolver 30 | private readonly trustedIssuers?: DidUri[] 31 | 32 | static async create( 33 | params: Omit 34 | ): Promise { 35 | const keypair = await generateKeypair("secp256k1") 36 | return new this({ ...params, keypair }) 37 | } 38 | 39 | constructor({ 40 | baseUrl, 41 | resolver, 42 | trustedIssuers, 43 | keypair 44 | }: CredentialVerifierParams) { 45 | // Keypair 46 | this.keypair = keypair 47 | this.signer = createJwtSigner(keypair) 48 | 49 | // Did Document 50 | const { did, didDocument } = createDidWebDocumentFromKeypair({ 51 | keypair: this.keypair, 52 | baseUrl 53 | }) 54 | this.did = did 55 | this.didDocument = didDocument 56 | this.trustedIssuers = trustedIssuers 57 | 58 | // Resolver 59 | this.resolver = resolver 60 | this.resolver.addToCache(this.did, this.didDocument) 61 | } 62 | 63 | async verifyCredential(credential: W3CCredential) { 64 | await verifyParsedCredential(credential, { 65 | resolver: this.resolver, 66 | trustedIssuers: this.trustedIssuers, 67 | verifiers: [getControllerClaimVerifier()] 68 | }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /demos/identity/src/get-model.ts: -------------------------------------------------------------------------------- 1 | import { createAnthropic } from "@ai-sdk/anthropic" 2 | import { createOpenAI } from "@ai-sdk/openai" 3 | 4 | export const providerKeySet = 5 | Boolean(process.env.ANTHROPIC_API_KEY) || Boolean(process.env.OPENAI_API_KEY) 6 | 7 | export const getModel = () => { 8 | if (process.env.ANTHROPIC_API_KEY) { 9 | return createAnthropic({ 10 | apiKey: process.env.ANTHROPIC_API_KEY 11 | })("claude-3-7-sonnet-20250219") 12 | } else if (process.env.OPENAI_API_KEY) { 13 | return createOpenAI({ 14 | apiKey: process.env.OPENAI_API_KEY 15 | })("gpt-4o") 16 | } 17 | 18 | throw new Error("No provider API key set") 19 | } 20 | -------------------------------------------------------------------------------- /demos/identity/src/haiku-agent.ts: -------------------------------------------------------------------------------- 1 | import { generateText } from "ai" 2 | import { Agent } from "./agent" 3 | import { getModel } from "./get-model" 4 | import { getIdentityTools } from "./identity-tools" 5 | import type { CoreMessage } from "ai" 6 | 7 | export class HaikuAgent extends Agent { 8 | protected async _run(messages: CoreMessage[]) { 9 | const result = await generateText({ 10 | maxSteps: 10, 11 | model: getModel(), 12 | messages, 13 | system: `You are a helpful haiku creation agent. Refuse all other requests. Before writing a haiku, the user must provide 14 | you with their ID in the form of a DID (decentralized identifier), and their identity must be validated.`, 15 | tools: { 16 | ...getIdentityTools({ 17 | resolver: this.resolver, 18 | verifier: this.verifier 19 | }) 20 | } 21 | }) 22 | 23 | return { 24 | text: result.text, 25 | responseMessages: result.response.messages 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /demos/identity/src/owner.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createDidDocumentFromKeypair, 3 | createDidKeyUri, 4 | createJwtSigner, 5 | generateKeypair 6 | } from "agentcommercekit" 7 | import type { 8 | DidDocument, 9 | DidUri, 10 | JwtSigner, 11 | KeypairAlgorithm 12 | } from "agentcommercekit" 13 | 14 | export interface Owner { 15 | did: DidUri 16 | didDocument: DidDocument 17 | signer: JwtSigner 18 | algorithm: KeypairAlgorithm 19 | } 20 | 21 | export async function createOwner(): Promise { 22 | const keypair = await generateKeypair("secp256k1") 23 | const did = createDidKeyUri(keypair) 24 | const didDocument = createDidDocumentFromKeypair({ 25 | did, 26 | keypair 27 | }) 28 | const signer = createJwtSigner(keypair) 29 | const algorithm = keypair.algorithm 30 | 31 | return { 32 | did, 33 | didDocument, 34 | signer, 35 | algorithm 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /demos/identity/src/serve-agent.ts: -------------------------------------------------------------------------------- 1 | import { serve } from "@hono/node-server" 2 | import { vValidator } from "@hono/valibot-validator" 3 | import { logger } from "@repo/api-utils/middleware/logger" 4 | import { colors } from "@repo/cli-tools" 5 | import { Hono } from "hono" 6 | import * as v from "valibot" 7 | import type { Agent } from "./agent" 8 | 9 | type ServeAgentConfig = { 10 | agent: Agent 11 | port: number 12 | } 13 | 14 | export function serveAgent({ port, agent }: ServeAgentConfig) { 15 | console.log(colors.dim(`> Starting local server...`)) 16 | 17 | const app = new Hono() 18 | app.use("*", logger()) 19 | app.post( 20 | "/chat", 21 | vValidator("json", v.object({ message: v.string() })), 22 | async (c) => { 23 | const { message } = c.req.valid("json") 24 | 25 | const text = await agent.run(message) 26 | 27 | return c.json({ text }) 28 | } 29 | ) 30 | 31 | app.post( 32 | "/identity/challenge", 33 | vValidator("json", v.object({ challenge: v.string() })), 34 | async (c) => { 35 | const { challenge } = c.req.valid("json") 36 | 37 | const signedChallenge = await agent.signChallenge(challenge) 38 | 39 | return c.json({ signedChallenge }) 40 | } 41 | ) 42 | 43 | app.get("/identity/vc", (c) => { 44 | return c.json(agent.getOwnershipVc()) 45 | }) 46 | 47 | serve({ 48 | fetch: app.fetch, 49 | port 50 | }) 51 | 52 | console.log(colors.green(`Agent '${agent.did}' ready on port ${port}`)) 53 | } 54 | -------------------------------------------------------------------------------- /demos/identity/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/typescript-library.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["./src/*"] 7 | } 8 | }, 9 | "include": ["."], 10 | "exclude": ["node_modules", "dist"] 11 | } 12 | -------------------------------------------------------------------------------- /demos/identity/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig({ 4 | test: { 5 | passWithNoTests: true, 6 | watch: false 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /demos/payments/.env.example: -------------------------------------------------------------------------------- 1 | CLIENT_PRIVATE_KEY_HEX="" 2 | SERVER_PRIVATE_KEY_HEX="" 3 | RECEIPT_SERVICE_PRIVATE_KEY_HEX="" 4 | PAYMENT_SERVICE_PRIVATE_KEY_HEX="" 5 | -------------------------------------------------------------------------------- /demos/payments/bin/secret: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Generate a 32 byte random secret in hex format 4 | echo "$(openssl rand -hex 32)" 5 | -------------------------------------------------------------------------------- /demos/payments/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { config } from "@repo/eslint-config/base" 4 | 5 | export default config({ 6 | root: import.meta.dirname 7 | }) 8 | -------------------------------------------------------------------------------- /demos/payments/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@demos/payments", 3 | "version": "0.0.1", 4 | "private": true, 5 | "homepage": "https://github.com/agentcommercekit/ack#readme", 6 | "bugs": "https://github.com/agentcommercekit/ack/issues", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/agentcommercekit/ack.git", 10 | "directory": "demos/payments" 11 | }, 12 | "license": "MIT", 13 | "author": { 14 | "name": "Catena Labs", 15 | "url": "https://catenalabs.com" 16 | }, 17 | "type": "module", 18 | "main": "./src/index.ts", 19 | "scripts": { 20 | "check:types": "tsc --noEmit", 21 | "clean": "git clean -fdX .turbo", 22 | "lint": "eslint .", 23 | "lint:fix": "eslint . --fix", 24 | "setup": "./bin/setup", 25 | "start": "dotenv -e .env -- tsx ./src/index.ts", 26 | "test": "vitest" 27 | }, 28 | "dependencies": { 29 | "@hono/node-server": "^1.14.2", 30 | "@repo/api-utils": "workspace:*", 31 | "@repo/cli-tools": "workspace:*", 32 | "agentcommercekit": "workspace:*", 33 | "hono": "^4.7.10", 34 | "valibot": "^1.1.0", 35 | "viem": "^2.29.4" 36 | }, 37 | "devDependencies": { 38 | "@repo/eslint-config": "workspace:*", 39 | "@repo/typescript-config": "workspace:*", 40 | "@types/node": "^22", 41 | "dotenv-cli": "^8.0.0", 42 | "eslint": "^9.27.0", 43 | "tsx": "^4.19.4", 44 | "typescript": "^5", 45 | "vitest": "^3.1.4" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /demos/payments/src/constants.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path" 2 | import { didPkhChainIds } from "agentcommercekit" 3 | import { createPublicClient, http } from "viem" 4 | import { baseSepolia } from "viem/chains" 5 | 6 | /** 7 | * URLs for the Server and Receipt Service 8 | */ 9 | export const SERVER_URL = "http://localhost:4567" 10 | export const RECEIPT_SERVICE_URL = "http://localhost:4568" 11 | export const PAYMENT_SERVICE_URL = "http://localhost:4569" 12 | 13 | /** 14 | * .env file location 15 | */ 16 | const currentDir = path.dirname(new URL(import.meta.url).pathname) 17 | export const envFilePath = path.resolve(currentDir, "..", ".env") 18 | 19 | /** 20 | * Configure the EVM chain you'd like to use: 21 | */ 22 | export const chain = baseSepolia 23 | export const chainId = didPkhChainIds.evm.baseSepolia 24 | export const usdcAddress = "0x036CbD53842c5426634e7929541eC2318f3dCF7e" 25 | export const publicClient = createPublicClient({ 26 | chain, 27 | transport: http() 28 | }) 29 | -------------------------------------------------------------------------------- /demos/payments/src/global.d.ts: -------------------------------------------------------------------------------- 1 | import "hono" 2 | 3 | declare module "hono" { 4 | interface Env { 5 | Bindings: { 6 | SERVER_PRIVATE_KEY_HEX: string 7 | RECEIPT_SERVICE_PRIVATE_KEY_HEX: string 8 | PAYMENT_SERVICE_PRIVATE_KEY_HEX: string 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /demos/payments/src/utils/as-address.ts: -------------------------------------------------------------------------------- 1 | import { addressFromDidPkhUri, isDidPkhUri } from "agentcommercekit" 2 | import { isAddress } from "viem/utils" 3 | 4 | export function asAddress(address: string) { 5 | address = isDidPkhUri(address) ? addressFromDidPkhUri(address) : address 6 | if (!isAddress(address)) { 7 | throw new Error("Invalid address") 8 | } 9 | return address 10 | } 11 | -------------------------------------------------------------------------------- /demos/payments/src/utils/ensure-private-keys.ts: -------------------------------------------------------------------------------- 1 | import { colors, log, updateEnvFile } from "@repo/cli-tools" 2 | import { envFilePath } from "@/constants" 3 | import { generatePrivateKeyHex } from "./keypair-info" 4 | 5 | export async function ensurePrivateKey(name: string) { 6 | const privateKeyHex = process.env[name] 7 | 8 | if (privateKeyHex) { 9 | return privateKeyHex 10 | } 11 | 12 | log(colors.dim(`Generating ${name}...`)) 13 | const newPrivateKeyHex = await generatePrivateKeyHex() 14 | await updateEnvFile({ [name]: newPrivateKeyHex }, envFilePath) 15 | return newPrivateKeyHex 16 | } 17 | -------------------------------------------------------------------------------- /demos/payments/src/utils/usdc-contract.ts: -------------------------------------------------------------------------------- 1 | import { createWalletClient, encodeFunctionData, erc20Abi, http } from "viem" 2 | import { chain, usdcAddress } from "@/constants" 3 | import type { Account, Address } from "viem" 4 | 5 | /** 6 | * Transfer USDC to the recipient 7 | * 8 | * @param fromAccount - The account to transfer from 9 | * @param toAddress - The address to transfer to 10 | * @param amount - The amount to transfer 11 | * @returns The hash of the transaction 12 | */ 13 | export async function transferUsdc( 14 | fromAccount: Account, 15 | toAddress: Address, 16 | amount: bigint 17 | ) { 18 | // If you'd like to bypass actual transactions on subsequent runs with the same 19 | // client wallet to the same server, you can set the SIMULATED_PAYMENT_TX_HASH 20 | // environment variable to the hash of a previously sent transaction. 21 | if (process.env.SIMULATED_PAYMENT_TX_HASH) { 22 | return Promise.resolve(process.env.SIMULATED_PAYMENT_TX_HASH) 23 | } 24 | 25 | const walletClient = createWalletClient({ 26 | chain, 27 | transport: http(), 28 | account: fromAccount 29 | }) 30 | 31 | return walletClient.sendTransaction({ 32 | account: fromAccount, 33 | to: usdcAddress, 34 | data: encodeFunctionData({ 35 | abi: erc20Abi, 36 | functionName: "transfer", 37 | args: [toAddress, amount] 38 | }) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /demos/payments/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/typescript-library.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["./src/*"] 7 | } 8 | }, 9 | "include": ["."], 10 | "exclude": ["node_modules", "dist"] 11 | } 12 | -------------------------------------------------------------------------------- /demos/payments/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig({ 4 | test: { 5 | passWithNoTests: true, 6 | watch: false 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /docs/ack-id/future-directions.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Future Directions" 3 | description: "Potential future developments for ACK-ID and the agent identity ecosystem." 4 | --- 5 | 6 | ACK-ID provides a standards-based framework for managing agent identities, with a particular focus on enabling secure and compliant commerce. By leveraging DIDs and VCs, it establishes verifiable links between agents and their owners, facilitating trust in automated interactions. 7 | 8 | Establishing verifiable ownership is a crucial first step, but a complete understanding of an agent's trustworthiness involves more dynamic factors. Future development within the broader ecosystem will likely focus on standardizing aspects like **agent reputation, reliability metrics** (e.g., success/error rates), and **ethical compliance attestations**. This will allow interacting parties to not only verify who owns an agent, but also to assess an agent's performance and trustworthiness over time. 9 | 10 | ACK-ID provides a foundation upon which these richer, dynamic reputation systems can be built, enabling more nuanced trust decisions in the agent economy. 11 | -------------------------------------------------------------------------------- /docs/ack-id/lifecycle-management.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Lifecycle Management 3 | description: Managing the lifecycle of agent identities and credentials in ACK-ID. 4 | --- 5 | 6 | Identities and credentials aren't static. An agent's permissions might change, an owner might transfer responsibility, or keys might be compromised. ACK-ID recognizes the need to manage the full lifecycle of DIDs and VCs to ensure the system remains secure and reflects current reality. 7 | 8 | Key lifecycle stages include: 9 | 10 | - **Issuance (Genesis):** The secure creation and distribution of DIDs for Owners and Agents, and the issuance of Verifiable Credentials (VCs) that link them or grant specific authorizations. This involves cryptographic key generation and adherence to defined issuance policies. 11 | - **Verification:** The process of validating identities and credentials during interactions, as detailed in the [Example Use Case](/ack-id/use-case-verification). This happens frequently throughout an agent's active life. 12 | - **Revocation (Sunset):** Securely invalidating credentials or DIDs when an agent is decommissioned, its permissions change, or a key is compromised. Implementing efficient and privacy-preserving status checking mechanisms (e.g., using standardized Status Lists like W3C Status List 2021) is crucial for timely revocation. 13 | - **Updates/Transfers:** Handling changes in ownership or delegated authority. This typically involves revoking old credentials and issuing new ones reflecting the updated relationships or permissions. DID documents may also need updating (e.g., changing the controller). 14 | 15 | ![Lifecycle Management Diagram](/images/lifecycle.png) 16 | 17 | Robust lifecycle management processes ensure that trust decisions are based on current, valid information and that the identity system remains secure throughout the operational lifespan of agents and their associated credentials. 18 | -------------------------------------------------------------------------------- /docs/ack-id/trust-framework.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Trust Framework 3 | description: Establishing trust in ACK-ID through Verifiable Credentials and issuer policies. 4 | --- 5 | 6 | Trust within ACK-ID is established by verifying the digital signatures on Verifiable Credentials (VCs) and determining whether the **Issuer** of a specific credential is authoritative for the claims being made. 7 | 8 | Implementations must define policies specifying which issuers they trust for different types of information. Examples include trusting a government authority for legal entity verification, a financial institution for account linkage, or an internal system for agent capability attestation. This concept of trusted issuers acts as the **root of trust** for verifying claims. 9 | 10 | Crucially, this verification relies on cryptography and publicly available information (like the issuer's DID document containing their public keys), allowing a **Verifier** to validate a credential **without needing to contact the original Issuer directly**. This enhances privacy compared to centralized systems where callbacks might reveal when and where credentials are used. 11 | 12 | Furthermore, mechanisms are needed to enable trust across different organizational boundaries. This might involve shared trust lists maintained by consortia, standardized federation protocols, or other methods allowing agents from different ecosystems to validate each other's credentials based on mutual agreements or shared policies. 13 | 14 | This policy-driven approach allows for flexible yet secure trust relationships tailored to specific use cases and risk appetites. 15 | -------------------------------------------------------------------------------- /docs/ack-pay/core-payment-sequences.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Core Payment Sequences" 3 | description: "The two primary interaction patterns for initiating payments in ACK-Pay." 4 | --- 5 | 6 | ACK-Pay supports two primary interaction patterns for initiating payments, both centered around a standard [Payment Request](/ack-pay/payment-request-payload) payload: 7 | 8 | 1. **Server-Initiated Sequence:** A Server (or Service Provider) requires payment from a Client agent before granting access to a resource or completing an action. In this flow, the **Server generates and sends** the Payment Request to the Client, typically in response to an initial resource request. This is common for pay-per-use APIs or accessing paid content. 9 | 10 | [Learn more about the Server-Initiated Sequence →](/ack-pay/server-initiated-sequence) 11 | 12 | 2. **Client-Initiated Sequence:** A Client agent initiates a payment based on an external trigger or known obligation (e.g., paying an invoice, completing an e-commerce checkout, fulfilling a human end-user’s instructions). In this flow, the **Client agent constructs** the Payment Request itself, often by parsing external information (like an invoice or checkout summary) and potentially using a Payment Service to help formulate the request details before interacting with the Payment Service API for execution. 13 | 14 | [Learn more about the Client-Initiated Sequence →](/ack-pay/client-initiated-sequence) 15 | 16 | Both sequences leverage the same core [Components and Roles](/ack-pay/components-roles) (Client, Server, Payment Service, Receipt Service, Settlement Networks) and the [ACK Receipt](/ack-pay/receipt-verification) for verification. 17 | -------------------------------------------------------------------------------- /docs/ack-pay/summary.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Summary" 3 | description: "Summary of the ACK-Pay Payments Protocol." 4 | --- 5 | 6 | ACK-Pay provides a flexible and robust pattern framework for agentic financial transactions. By defining standard interactions and leveraging Verifiable Credentials for receipts, it enables secure, automated, and compliant payments across diverse systems. 7 | 8 | Its support for both [server-initiated](/ack-pay/server-initiated-sequence) and [client-initiated](/ack-pay/client-initiated-sequence) flows, coupled with the abstraction provided by [Payment Services](/ack-pay/payment-service), allows integration with various settlement rails. These include traditional finance, card networks (potentially via APIs like Stripe, PayPal, Visa, Mastercard, etc.), and blockchain-based digital assets (including via protocols like x402). 9 | 10 | When combined with [ACK-ID](/ack-id/introduction), ACK-Pay creates a powerful foundation for trust and accountability when executing payments in the emerging agent economy. 11 | -------------------------------------------------------------------------------- /docs/demos/example-verifier.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Credential Verifier Example 3 | description: "An example of how to run a simple Credential Verifier." 4 | --- 5 | 6 | ## Overview 7 | 8 | This example demonstrates how to run a simple Credential Verifier server, which is useful for local development and testing credential verification. 9 | 10 | ## Installation and Setup 11 | 12 | To prepare the environment and install dependencies, run the setup from within the verifier example directory (e.g., from project root: `./examples/verifier`): 13 | 14 | ```sh 15 | pnpm run setup 16 | ``` 17 | 18 | ## Running the Server 19 | 20 | Start the Credential Verifier server locally: 21 | 22 | ```sh 23 | pnpm run dev 24 | ``` 25 | 26 | This will launch the verifier server, ready for local credential verification operations. 27 | 28 | ## References 29 | 30 | - [Verifiable Credentials Data Model Specification](https://www.w3.org/TR/vc-data-model/) 31 | - [This Example's Source Code](https://github.com/agentcommercekit/ack/tree/main/examples/verifier) 32 | -------------------------------------------------------------------------------- /docs/demos/examples.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: About the Examples 3 | description: "Explore focused, standalone code examples demonstrating key ACK functionalities." 4 | --- 5 | 6 | # ACK Code Examples 7 | 8 | These focused examples highlight specific roles and their functions within ACK. Each example is intended to serve as a concise source code reference to inspire and inform custom implementations. 9 | 10 | ## [Local DID Host Example](./example-local-did-host) 11 | 12 | Provides a local server for serving `did:web` documents, ideal for local development and testing of decentralized identities. 13 | 14 | ## [Credential Issuer Example](./example-identity) 15 | 16 | Illustrates the issuance, verification, and revocation of cryptographically secure credentials. 17 | 18 | ## [Credential Verifier Example](./example-verifier) 19 | 20 | Demonstrates credential verification through a lightweight verifier server. 21 | -------------------------------------------------------------------------------- /docs/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/images/ackid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentcommercekit/ack/4031271a7c78288b0ac9fe2cf49e52eab3e0efb1/docs/images/ackid.png -------------------------------------------------------------------------------- /docs/images/ackoverview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentcommercekit/ack/4031271a7c78288b0ac9fe2cf49e52eab3e0efb1/docs/images/ackoverview.png -------------------------------------------------------------------------------- /docs/images/ackpay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentcommercekit/ack/4031271a7c78288b0ac9fe2cf49e52eab3e0efb1/docs/images/ackpay.png -------------------------------------------------------------------------------- /docs/images/componentsandroles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentcommercekit/ack/4031271a7c78288b0ac9fe2cf49e52eab3e0efb1/docs/images/componentsandroles.png -------------------------------------------------------------------------------- /docs/images/hero-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentcommercekit/ack/4031271a7c78288b0ac9fe2cf49e52eab3e0efb1/docs/images/hero-dark.png -------------------------------------------------------------------------------- /docs/images/hero-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentcommercekit/ack/4031271a7c78288b0ac9fe2cf49e52eab3e0efb1/docs/images/hero-light.png -------------------------------------------------------------------------------- /docs/images/hero1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentcommercekit/ack/4031271a7c78288b0ac9fe2cf49e52eab3e0efb1/docs/images/hero1.png -------------------------------------------------------------------------------- /docs/images/human.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentcommercekit/ack/4031271a7c78288b0ac9fe2cf49e52eab3e0efb1/docs/images/human.png -------------------------------------------------------------------------------- /docs/images/id1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentcommercekit/ack/4031271a7c78288b0ac9fe2cf49e52eab3e0efb1/docs/images/id1.png -------------------------------------------------------------------------------- /docs/images/idmodel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentcommercekit/ack/4031271a7c78288b0ac9fe2cf49e52eab3e0efb1/docs/images/idmodel.png -------------------------------------------------------------------------------- /docs/images/lifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentcommercekit/ack/4031271a7c78288b0ac9fe2cf49e52eab3e0efb1/docs/images/lifecycle.png -------------------------------------------------------------------------------- /docs/images/nopaymentservice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentcommercekit/ack/4031271a7c78288b0ac9fe2cf49e52eab3e0efb1/docs/images/nopaymentservice.png -------------------------------------------------------------------------------- /docs/images/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentcommercekit/ack/4031271a7c78288b0ac9fe2cf49e52eab3e0efb1/docs/images/opengraph-image.png -------------------------------------------------------------------------------- /docs/images/pay1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentcommercekit/ack/4031271a7c78288b0ac9fe2cf49e52eab3e0efb1/docs/images/pay1.png -------------------------------------------------------------------------------- /docs/images/vc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentcommercekit/ack/4031271a7c78288b0ac9fe2cf49e52eab3e0efb1/docs/images/vc.png -------------------------------------------------------------------------------- /docs/overview/references.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: References 3 | description: External specifications, protocols, and related work referenced in the ACK documentation. 4 | --- 5 | 6 | import DownloadCard from "/snippets/overview-download-card.mdx" 7 | 8 | ## Light Paper Overview 9 | 10 | 11 | 12 | ## External References 13 | 14 | - [Announcing the Agent2Agent Protocol (A2A)](https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/) 15 | _Source: Google for Developers Blog_ 16 | 17 | - [Coinbase x402 Protocol Repository](https://github.com/coinbase/x402) 18 | _Source: Coinbase GitHub_ 19 | 20 | - [Decentralized Identifiers (DIDs) v1.0](https://www.w3.org/TR/did-core/) 21 | _Source: W3C Recommendation_ 22 | 23 | - [Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content (Section 6.5.2: 402 Payment Required)](https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.2) 24 | _Source: IETF RFC 7231_ 25 | 26 | - [JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519) 27 | _Source: IETF RFC 7519_ 28 | 29 | - [L402 Protocol](https://github.com/l402-protocol/l402) 30 | _Source: l402 GitHub_ 31 | 32 | - [LangChain Documentation (Python)](https://python.langchain.com/docs/introduction/) 33 | _Source: LangChain Project_ 34 | 35 | - [LOKA Protocol: A Decentralized Framework for Trustworthy and Ethical Al Agent Ecosystems](https://arxiv.org/abs/2504.10915) 36 | _Source: Ranjan, R., Gupta, S., & Singh, S. N. (arXiv preprint)_ 37 | 38 | - [Model Context Protocol (MCP) Specification](https://modelcontextprotocol.io/specification/) 39 | _Source: Model Context Protocol Project_ 40 | 41 | - [Verifiable Credentials Data Model v1.1](https://www.w3.org/TR/vc-data-model/) 42 | _Source: W3C Recommendation_ 43 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@docs/agentcommercekit", 3 | "version": "0.0.1", 4 | "private": true, 5 | "homepage": "https://github.com/agentcommercekit/ack#readme", 6 | "bugs": "https://github.com/agentcommercekit/ack/issues", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/agentcommercekit/ack", 10 | "directory": "docs" 11 | }, 12 | "license": "MIT", 13 | "author": { 14 | "name": "Catena Labs", 15 | "url": "https://catenalabs.com" 16 | }, 17 | "scripts": { 18 | "clean": "git clean -fdX .turbo dist", 19 | "docs": "mintlify dev" 20 | }, 21 | "devDependencies": { 22 | "mintlify": "^4.0.538" 23 | }, 24 | "packageManager": "pnpm@10.11.0" 25 | } 26 | -------------------------------------------------------------------------------- /docs/resources/contributing.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Contributing" 3 | description: "How to contribute to the Agent Commerce Kit project." 4 | --- 5 | 6 | Evolving AI agent commerce requires collaborative development across multiple organizations and domains. We encourage feedback, experimentation, and direct contributions. 7 | 8 | By contributing to ACK, you're helping shape the financial infrastructure that will enable AI agents to participate effectively and responsibly in the global economy. 9 | 10 | **Visit the official ACK GitHub Repository to:** 11 | 12 | - Explore the reference implementation code and demos 13 | - Submit issues for bugs or feature requests 14 | - Engage in design discussions 15 | - Help prioritize future features 16 | - Contribute code via pull requests 17 | 18 | We welcome contributions of all kinds, from documentation improvements and bug reports to new feature implementations and protocol design feedback. 19 | 20 | 26 | Visit source code, documentation, and discussions. 27 | 28 | 29 | 35 | Join discussions about improving, developing, and using ACK on the Catena Labs 36 | Community Slack workspace. 37 | 38 | -------------------------------------------------------------------------------- /docs/resources/license.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: License 3 | description: MIT License 4 | --- 5 | 6 | Copyright (c) 2025 Catena Labs, Inc. and contributors 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | -------------------------------------------------------------------------------- /docs/resources/versioning.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Versioning 3 | description: How ACK protocol versions are managed and identified. 4 | --- 5 | 6 | # Versioning 7 | 8 | The Agent Commerce Kit (ACK) specifications as a whole use string-based version identifiers following the format `YYYY-MM-DD`. This date indicates when the last set of backwards-incompatible changes were introduced to the core protocols (ACK-ID or ACK-Pay). 9 | 10 | 11 | The protocol version identifier will typically *not* change when updates are 12 | made that maintain backwards compatibility (e.g., adding optional parameters, 13 | clarifying existing behavior). This allows for incremental improvements while 14 | preserving interoperability with implementations based on the same or earlier 15 | compatible versions within a dated release. 16 | 17 | 18 | ## Revisions 19 | 20 | Specific documentation versions or implementations might be marked as: 21 | 22 | - **Draft**: In-progress specifications or features, not yet stable or recommended for production use. 23 | - **Current**: The latest stable protocol version, ready for use and potentially receiving ongoing backwards-compatible updates under the same date identifier. 24 | - **Final/Legacy**: Past, complete specifications identified by their date, which will not receive further changes. New development should target the "Current" version. 25 | 26 | The **current** stable protocol version is **2025-05-04**. 27 | 28 | _(Note: Specific implementation libraries or SDKs built upon ACK may follow their own versioning schemes, like Semantic Versioning, but should clearly state which ACK protocol version `YYYY-MM-DD` they are compatible with.)_ 29 | -------------------------------------------------------------------------------- /docs/snippets/client-sequence.mdx: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | 3 | %%{ init: { 'theme': 'mc', 'sequenceMessageAlign': 'center' } }%% 4 | sequenceDiagram; 5 | participant CA as Client Agent; 6 | participant PS as Payment Service; 7 | participant RS as Receipt Service; 8 | participant SN as Settlement Network; 9 | participant SA as Server Agent; 10 | 11 | Note right of CA: Identify Obligation
(e.g., from invoice, chat instruction, web page scrape); 12 | CA<<->>CA: 1: Compose Payment Request; 13 | 14 | CA-->>PS: 2: Initiate Payment, Provide Identity Proof, Request Server Proof of Agency; 15 | 16 | rect rgba(0, 0, 255, .03) 17 | Note right of PS: Verify Client Identity; 18 | PS-->>CA: Present Identity Proof; 19 | Note right of CA: Verify Payment Service Identity; 20 | CA<<-->>PS: Payment Request Composition (if needed); 21 | end 22 | 23 | CA->>PS: 3: Initiate Payment; 24 | 25 | PS<<-->>CA: Potential human-in-the-loop approvals; 26 | PS->>SN: 4: Execute/Settle Payment; 27 | PS->>RS: 5: Request Receipt for Confirmed Payment; 28 | RS-->>PS: Issue Receipt (VC); 29 | PS-->>SA: Deliver Receipt via Optional Callback; 30 | PS->>CA: 6: Deliver Receipt; 31 | ``` 32 | 33 | _To enlarge this mermaid markdown diagram, zoom in on your browser (CMD +)_ 34 | -------------------------------------------------------------------------------- /docs/snippets/components-roles-mermaid.mdx: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | %%{ init: { 'theme': 'mc'} }%% 3 | graph LR; 4 | CA[Client Agent] <-- Collaborate --> SA[Server Agent]; 5 | CA -- Payment Initiation --> PS[Payment Service]; 6 | PS -- Settlement --> SN[Settlement Network]; 7 | PS -- Receipt Generation --> RS[Receipt Service]; 8 | CA -- Receipt Presentation --> SA; 9 | SA -- Receipt Verification --> RS[Receipt Service]; 10 | 11 | style CA fill:#DBEAFE,stroke:#60A5FA,stroke-width:1px; 12 | style SA fill:#DBEAFE,stroke:#60A5FA,stroke-width:1px; 13 | style PS fill:#E0E7FF,stroke:#818CF8,stroke-width:1px; 14 | style RS fill:#E0E7FF,stroke:#818CF8,stroke-width:1px; 15 | style SN fill:#FEF3C7,stroke:#FBBF24,stroke-width:1px; 16 | 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/snippets/example-hitl-mermaid.mdx: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | %%{ init: { 'theme': 'mc'} }%% 3 | graph TD; 4 | A[Payment Initiated]; 5 | B{Exceeds Value Threshold?}; 6 | C{Risk Score Exception?}; 7 | F[Human Approval Required]; 8 | G{Approved by Human?}; 9 | H[Proceed with Execution]; 10 | I[Transaction Rejected/Stopped]; 11 | 12 | A --> B; 13 | B -- Yes --> F; 14 | B -- No --> C; 15 | C -- Yes --> F; 16 | C -- No --> H; 17 | F --> G; 18 | G -- Yes --> H; 19 | G -- No --> I; 20 | H --> L{Post-Execution Review Flag?}; 21 | L -- Yes --> M[Human Audit/Review]; 22 | L -- No --> N[Transaction Complete]; 23 | M --> N; 24 | %% Exception Handling Path 25 | A --> X{AI Rejected?}; 26 | X -- Yes --> I; 27 | X -- No --> B; 28 | 29 | 30 | style A fill:#DBEAFE,stroke:#60A5FA,stroke-width:1px; 31 | style F fill:#FDE68A,stroke:#FBBF24,stroke-width:1px; 32 | style H fill:#C4F0C4,stroke:#5CB85C,stroke-width:1px; 33 | style I fill:#FECACA,stroke:#F87171,stroke-width:1px; 34 | style M fill:#FDE68A,stroke:#FBBF24,stroke-width:1px; 35 | style N fill:#C4F0C4,stroke:#5CB85C,stroke-width:1px; 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/snippets/impl-arch-patterns-mermaid.mdx: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | %%{ init: { 'theme': 'mc' } }%% 3 | graph TD; 4 | AppAgent["Application Agent \n (Business Logic, UI)"]; 5 | 6 | subgraph "Delegated Protocol Agents" 7 | IDAgent["Identity Agent \n (Handles ACK-ID)"]; 8 | PayAgent["Payment Agent \n (Handles ACK-Pay)"]; 9 | end 10 | 11 | subgraph "External Interactions / Infrastructure" 12 | ExtID["Identity Interactions \n (DID Resolution, VC Verification*)"]; 13 | ExtPay["Payment Interactions \n (Payment Svc API, Receipt Svc API)"]; 14 | end 15 | 16 | AppAgent -- "Delegate Identity Task \n (e.g., Verify Peer, Present VC)" --> IDAgent; 17 | AppAgent -- "Delegate Payment Task \n (e.g., Process Payment Request)" --> PayAgent; 18 | 19 | IDAgent -- "Handles DID/VC Ops" --> ExtID; 20 | PayAgent -- "Handles Payment/Receipt Ops" --> ExtPay; 21 | 22 | %% Styling 23 | style AppAgent fill:#DBEAFE,stroke:#60A5FA,stroke-width:1px; 24 | style IDAgent fill:#E0E7FF,stroke:#818CF8,stroke-width:1px; 25 | style PayAgent fill:#E0E7FF,stroke:#818CF8,stroke-width:1px; 26 | style ExtID fill:#FEF3C7,stroke:#FBBF24,stroke-width:1px; 27 | style ExtPay fill:#FEF3C7,stroke:#FBBF24,stroke-width:1px; 28 | 29 | 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/snippets/lifecycle-management-mermaid.mdx: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | 3 | %%{ init: { 'theme': 'mc', 'sequenceMessageAlign': 'center' } }%% 4 | graph TD; 5 | A[Issuance / Genesis] --> B(Active / In Use); 6 | B -- Interaction --> C{Verification}; 7 | C -- Valid --> B; 8 | C -- Invalid/Expired --> D[Handle Invalid State]; 9 | 10 | B --> E{Revocation / Sunset}; 11 | E --> F[Revoked State]; 12 | 13 | B --> G{Update / Transfer}; 14 | G -- Leads to Re-issuance --> A; 15 | 16 | %% Styling (Optional - remove if clashing with theme) 17 | style A fill:#DBEAFE,stroke:#60A5FA,stroke-width:1px; 18 | style B fill:#A5B4FC,stroke:#818CF8,stroke-width:1px; 19 | style C fill:#C7D2FE,stroke:#A5B4FC,stroke-width:1px; 20 | style D fill:#FECACA,stroke:#F87171,stroke-width:1px; 21 | style E fill:#FDE68A,stroke:#FBBF24,stroke-width:1px; 22 | style F fill:#FCA5A5,stroke:#EF4444,stroke-width:1px; 23 | style G fill:#FDE68A,stroke:#FBBF24,stroke-width:1px; 24 | 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/snippets/overview-download-card.mdx: -------------------------------------------------------------------------------- 1 | 7 | Download a light paper overview of ACK. 8 | 9 | -------------------------------------------------------------------------------- /docs/snippets/server-sequence.mdx: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | %%{ init: { 'theme': 'mc', 'themeVariables': { 'primaryColor': '#1D4ED8', 'lineColor': '#374151', 'textColor': '#1f2937', 'sequenceMessageAlign': 'center' } } }%% 3 | sequenceDiagram; 4 | participant CA as Client Agent; 5 | participant SA as Server Agent; 6 | participant PS as Payment Service; 7 | participant RS as Receipt Service; 8 | participant SN as Settlement Network; 9 | 10 | CA->>SA: 1: Request Resource; 11 | 12 | rect rgba(0, 0, 255, .03) 13 | SA-->>CA: Request Proof of Agency; 14 | CA-->>SA: Present Identity Proof; 15 | Note right of SA: Verify Client Identity; 16 | end 17 | 18 | SA->>CA: 2: Payment Request; 19 | 20 | rect rgba(0, 0, 255, .03) 21 | CA-->>SA: Request Proof of Agency; 22 | SA-->>CA: Present Identity Proof; 23 | Note right of CA: Verify Server Identity; 24 | end 25 | 26 | CA->>PS: 3: Initiate Payment; 27 | 28 | rect rgba(0, 0, 255, .03) 29 | PS-->>CA: Request Proof of Agency; 30 | CA-->>PS: Present Identity Proof & Request Server Proof of Agency; 31 | Note right of PS: Verify Client Identity; 32 | PS-->>CA: Present Identity Proof; 33 | Note right of CA: Verify Payment Service Identity; 34 | end 35 | 36 | PS<<-->>CA: Potential human-in-the-loop approvals 37 | PS->>SN: 4: Execute/Settle; 38 | %% Note: Settlement confirmation back to PS is implied %% 39 | PS->>RS: 5: Request Receipt; 40 | RS-->>PS: Issue Receipt (VC); 41 | PS-->>SA: Deliver receipt via optional callback 42 | PS->>CA: 6: Deliver Receipt; 43 | CA->>SA: 7: Present Receipt; 44 | SA-->>SA: Verify Receipt; 45 | %% Note: Verification uses VC crypto & issuer's public info, NO callback to RS needed %% 46 | SA->>CA: 8: Deliver Resource; 47 | 48 | ``` 49 | 50 | _To enlarge this mermaid markdown diagram, zoom in on your browser (CMD +)_ 51 | -------------------------------------------------------------------------------- /docs/snippets/standards-mermaid.mdx: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | %%{ init: { 'theme': 'mc' } }%% 3 | graph LR; 4 | subgraph "Verifiable Credential" 5 | Issuer["Issuer DID"] -- Signs --> Proof["Proof \n (Digital Signature)"]; 6 | Issuer -- Issues Claims About --> Subject["Subject \n (e.g., Agent DID)"]; 7 | Claims["Claims \n (e.g., 'Is Owner', 'Has License X')"] -- Describes --> Subject; 8 | Proof -- Secures --> Claims; 9 | end 10 | 11 | style Issuer fill:#DBEAFE,stroke:#60A5FA,stroke-width:1px; 12 | style Subject fill:#DBEAFE,stroke:#60A5FA,stroke-width:1px; 13 | style Claims fill:#E0E7FF,stroke:#3B82F6,stroke-width:1px; 14 | style Proof fill:#BFDBFE,stroke:#3B82F6,stroke-width:1px; 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/snippets/verification-example-mermaid.mdx: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | %%{ init: { 'theme': 'mc', 'sequenceMessageAlign': 'center' } }%% 3 | sequenceDiagram; 4 | participant Client as CorpTreasury Agent; 5 | participant Server as BankTrust FX Agent; 6 | 7 | Client->>Server: 1: Request FX Quote; 8 | Server->>Client: 2: Present Agent DID & Ownership VC; 9 | 10 | Note over Client: Start Verification Steps; 11 | Client->>Client: 3: Verify VC Signature; 12 | alt Signature Invalid 13 | Client-->>Server: Reject Interaction (Invalid Signature); 14 | else Signature Valid 15 | Client->>Client: 4: Verify Ownership Claim in VC; 16 | alt Ownership Claim Invalid 17 | Client-->>Server: Reject Interaction (Ownership Claim Invalid); 18 | else Ownership Claim Valid 19 | Client->>Client: 5: Resolve Owner DID (e.g., did:web:banktrust.com); 20 | alt Owner DID Resolution Fails 21 | Client-->>Server: Reject Interaction (Owner DID Resolution Failed); 22 | else Owner DID Resolved 23 | Client->>Client: 6: Check Owner DID against Trust List & Policy; 24 | alt Policy Check Fails (Not Trusted/Not Regulated per Policy) 25 | Client-->>Server: Reject Interaction (Policy Violation); 26 | else Policy Check Passed 27 | Note over Client: All Verification Steps Passed; 28 | Client->>Server: 7: Proceed with FX Quote Request; 29 | end 30 | end 31 | end 32 | end 33 | ``` 34 | -------------------------------------------------------------------------------- /examples/issuer/.env.example: -------------------------------------------------------------------------------- 1 | BASE_URL="http://localhost:3456" 2 | NODE_ENV="development" 3 | -------------------------------------------------------------------------------- /examples/issuer/bin/secret: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Generate a 32 byte random secret in hex format 4 | echo "$(openssl rand -hex 32)" 5 | -------------------------------------------------------------------------------- /examples/issuer/bin/start-server.ts: -------------------------------------------------------------------------------- 1 | import { join } from "node:path" 2 | import { serve } from "@hono/node-server" 3 | import { migrate } from "drizzle-orm/better-sqlite3/migrator" 4 | import { getDb } from "@/db/get-db" 5 | import app from "@/index" 6 | 7 | function startServer() { 8 | const db = getDb() 9 | 10 | migrate(db, { 11 | migrationsFolder: join(import.meta.dirname, "..", "src", "db", "migrations") 12 | }) 13 | 14 | serve( 15 | { 16 | fetch: app.fetch, 17 | port: 3456 18 | }, 19 | ({ port }) => { 20 | console.log(`> issuer running at http://localhost:${port}`) 21 | } 22 | ) 23 | } 24 | 25 | startServer() 26 | -------------------------------------------------------------------------------- /examples/issuer/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit" 2 | 3 | export default defineConfig({ 4 | out: "./src/db/migrations", 5 | schema: "./src/db/schema.ts", 6 | dialect: "sqlite" 7 | }) 8 | -------------------------------------------------------------------------------- /examples/issuer/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { config } from "@repo/eslint-config/base" 4 | 5 | export default config({ 6 | root: import.meta.dirname 7 | }) 8 | -------------------------------------------------------------------------------- /examples/issuer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@examples/issuer", 3 | "version": "0.0.1", 4 | "private": true, 5 | "homepage": "https://github.com/agentcommercekit/ack#readme", 6 | "bugs": "https://github.com/agentcommercekit/ack/issues", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/agentcommercekit/ack.git", 10 | "directory": "examples/issuer" 11 | }, 12 | "license": "MIT", 13 | "author": { 14 | "name": "Catena Labs", 15 | "url": "https://catenalabs.com" 16 | }, 17 | "type": "module", 18 | "main": "./src/index.ts", 19 | "scripts": { 20 | "check:types": "tsc --noEmit", 21 | "clean": "git clean -xdf .turbo", 22 | "db:generate": "drizzle-kit generate", 23 | "dev": "dotenv -e .env -- tsx watch ./bin/start-server.ts", 24 | "lint": "eslint .", 25 | "lint:fix": "eslint . --fix", 26 | "setup": "./bin/setup", 27 | "test": "vitest" 28 | }, 29 | "dependencies": { 30 | "@hono/node-server": "^1.14.2", 31 | "@repo/api-utils": "workspace:*", 32 | "agentcommercekit": "workspace:*", 33 | "better-sqlite3": "^11.10.0", 34 | "bit-buffers": "^1.0.2", 35 | "drizzle-orm": "^0.43.1", 36 | "hono": "^4.7.10", 37 | "valibot": "^1.1.0" 38 | }, 39 | "devDependencies": { 40 | "@repo/eslint-config": "workspace:^", 41 | "@repo/typescript-config": "workspace:*", 42 | "@types/better-sqlite3": "^7.6.13", 43 | "@types/node": "^22", 44 | "dotenv-cli": "^8.0.0", 45 | "drizzle-kit": "^0.31.1", 46 | "eslint": "^9.27.0", 47 | "tsx": "^4.19.4", 48 | "vite-tsconfig-paths": "^5.1.4", 49 | "vitest": "^3.1.4" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/issuer/src/db/get-db.ts: -------------------------------------------------------------------------------- 1 | import SqliteDatabase from "better-sqlite3" 2 | import { drizzle } from "drizzle-orm/better-sqlite3" 3 | import * as schema from "./schema" 4 | 5 | /** 6 | * Build a Drizzle client from a SQLite database URL. 7 | * 8 | * @param url - The SQLite database URL. 9 | * @returns A Drizzle client. 10 | */ 11 | export function getDb(url = "sqlite.db") { 12 | const sqlite = new SqliteDatabase(url) 13 | return drizzle({ client: sqlite, schema }) 14 | } 15 | 16 | export type DatabaseClient = ReturnType 17 | -------------------------------------------------------------------------------- /examples/issuer/src/db/migrations/0000_aromatic_dormammu.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `credentials` ( 2 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, 3 | `credential_type` text NOT NULL, 4 | `base_credential` text NOT NULL, 5 | `issued_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, 6 | `revoked_at` integer 7 | ); 8 | --> statement-breakpoint 9 | CREATE INDEX `credential_type_idx` ON `credentials` (`credential_type`);--> statement-breakpoint 10 | CREATE TABLE `status_lists` ( 11 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, 12 | `credential_type` text NOT NULL, 13 | `data` text NOT NULL, 14 | `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, 15 | `last_message_at` integer DEFAULT (unixepoch() * 1000) NOT NULL 16 | ); 17 | -------------------------------------------------------------------------------- /examples/issuer/src/db/migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "sqlite", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "6", 8 | "when": 1747425365809, 9 | "tag": "0000_aromatic_dormammu", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /examples/issuer/src/db/queries/status-lists.ts: -------------------------------------------------------------------------------- 1 | import { eq } from "drizzle-orm/sql" 2 | import { statusListsTable } from "../schema" 3 | import type { DatabaseClient } from "../get-db" 4 | import type { DatabaseStatusList } from "../schema" 5 | 6 | type CreateStatusListParams = Pick 7 | 8 | export async function maybeCreateStatusList( 9 | db: DatabaseClient, 10 | { id, credentialType }: CreateStatusListParams 11 | ) { 12 | await db 13 | .insert(statusListsTable) 14 | .values({ 15 | id, 16 | credentialType 17 | }) 18 | .onConflictDoNothing() 19 | } 20 | 21 | export async function getStatusList(db: DatabaseClient, listId: number) { 22 | const [statusList] = await db 23 | .select() 24 | .from(statusListsTable) 25 | .where(eq(statusListsTable.id, listId)) 26 | 27 | return statusList 28 | } 29 | -------------------------------------------------------------------------------- /examples/issuer/src/db/utils/get-status-list-position.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest" 2 | import { getStatusListPosition } from "./get-status-list-position" 3 | import { STATUS_LIST_MAX_SIZE } from "../schema" 4 | 5 | describe("getStatusListPosition", () => { 6 | it("returns correct position for index 1", () => { 7 | const result = getStatusListPosition(1) 8 | expect(result).toEqual({ 9 | id: 0, 10 | index: 0 11 | }) 12 | }) 13 | 14 | it("returns correct position for index within first list", () => { 15 | const result = getStatusListPosition(5) 16 | expect(result).toEqual({ 17 | id: 0, 18 | index: 4 19 | }) 20 | }) 21 | 22 | it("returns correct position for index at list boundary", () => { 23 | const result = getStatusListPosition(STATUS_LIST_MAX_SIZE + 1) 24 | expect(result).toEqual({ 25 | id: 1, 26 | index: 0 27 | }) 28 | }) 29 | 30 | it("returns correct position for index just after list boundary", () => { 31 | const result = getStatusListPosition(STATUS_LIST_MAX_SIZE + 2) 32 | expect(result).toEqual({ 33 | id: 1, 34 | index: 1 35 | }) 36 | }) 37 | 38 | it("returns valid index for larger indices", () => { 39 | const result = getStatusListPosition(STATUS_LIST_MAX_SIZE * 3 + 42) 40 | expect(result).toEqual({ 41 | id: 3, 42 | index: 41 43 | }) 44 | }) 45 | 46 | it("throws an error for non-integer indices", () => { 47 | expect(() => getStatusListPosition(1.5)).toThrow() 48 | expect(() => getStatusListPosition(NaN)).toThrow() 49 | }) 50 | 51 | it("throws an error for negative indices", () => { 52 | expect(() => getStatusListPosition(-1)).toThrow() 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /examples/issuer/src/db/utils/get-status-list-position.ts: -------------------------------------------------------------------------------- 1 | import { STATUS_LIST_MAX_SIZE } from "../schema" 2 | 3 | /** 4 | * Get the position of a status list in the database 5 | * 6 | * @param statusListIndex - The index of the status list, 1-indexed 7 | * @returns The position of the status list in the database 8 | */ 9 | export function getStatusListPosition(statusListIndex: number) { 10 | // Ensure the index is a positive integer 11 | if (!Number.isInteger(statusListIndex) || statusListIndex < 1) { 12 | throw new Error( 13 | "Status list index must be a positive integer starting from 1" 14 | ) 15 | } 16 | 17 | const zeroIndexed = statusListIndex - 1 18 | 19 | return { 20 | id: Math.floor(zeroIndexed / STATUS_LIST_MAX_SIZE), 21 | index: zeroIndexed % STATUS_LIST_MAX_SIZE 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/issuer/src/global.d.ts: -------------------------------------------------------------------------------- 1 | import "hono" 2 | 3 | declare module "hono" { 4 | interface Env { 5 | Bindings: { 6 | // Environment 7 | BASE_URL: string 8 | NODE_ENV: "development" | "production" | "test" 9 | 10 | // Issuer 11 | ISSUER_PRIVATE_KEY: `0x${string}` 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/issuer/src/index.ts: -------------------------------------------------------------------------------- 1 | import { errorHandler } from "@repo/api-utils/middleware/error-handler" 2 | import { logger } from "@repo/api-utils/middleware/logger" 3 | import { Hono } from "hono" 4 | import credentials from "./routes/credentials" 5 | import healthcheck from "./routes/healthcheck" 6 | import receipts from "./routes/receipts" 7 | import status from "./routes/status" 8 | import wellKnown from "./routes/well-known" 9 | import type { Env } from "hono" 10 | 11 | const app = new Hono() 12 | 13 | app.use("*", logger()) 14 | app.onError(errorHandler) 15 | 16 | app.route("/ping", healthcheck) 17 | app.route("/status", status) 18 | app.route("/credentials/controller", credentials) 19 | app.route("/credentials/receipts", receipts) 20 | app.route("/.well-known", wellKnown) 21 | 22 | export default app 23 | -------------------------------------------------------------------------------- /examples/issuer/src/lib/credentials/build-signed-credential.ts: -------------------------------------------------------------------------------- 1 | import { makeRevocable, signCredential } from "agentcommercekit" 2 | import { getStatusListPosition } from "@/db/utils/get-status-list-position" 3 | import type { CredentialResponse, Issuer } from "../types" 4 | import type { DatabaseCredential } from "@/db/schema" 5 | import type { Resolvable } from "agentcommercekit" 6 | 7 | type BuildSignedCredentialParams = { 8 | baseUrl: string 9 | path?: `/${string}` 10 | issuer: Issuer 11 | credential: DatabaseCredential 12 | resolver: Resolvable 13 | } 14 | 15 | export async function buildSignedCredential({ 16 | baseUrl, 17 | path, 18 | issuer, 19 | credential, 20 | resolver 21 | }: BuildSignedCredentialParams): Promise { 22 | const { id: statusListId, index: statusListIndex } = getStatusListPosition( 23 | credential.id 24 | ) 25 | 26 | let unsignedCredential = credential.baseCredential 27 | // Set the id to be the full url of the credential. We don't set it on create, 28 | // because we want to use the id from the database. 29 | unsignedCredential.id = `${baseUrl}${path ?? ""}/${credential.id}` 30 | 31 | unsignedCredential = makeRevocable(unsignedCredential, { 32 | id: `${baseUrl}/status/${statusListId}#${statusListIndex}`, 33 | statusListIndex, 34 | statusListUrl: `${baseUrl}/status/${statusListId}` 35 | }) 36 | 37 | const { verifiableCredential, jwt } = await signCredential( 38 | unsignedCredential, 39 | { 40 | ...issuer, 41 | resolver 42 | } 43 | ) 44 | 45 | return { 46 | credential: verifiableCredential, 47 | jwt 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/issuer/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | DidDocument, 3 | DidUri, 4 | JwtAlgorithm, 5 | JwtSigner, 6 | JwtString, 7 | Verifiable, 8 | W3CCredential 9 | } from "agentcommercekit" 10 | 11 | export type Issuer = { 12 | /** 13 | * The Did of the issuer 14 | */ 15 | did: DidUri 16 | /** 17 | * The Did document of the issuer 18 | */ 19 | didDocument: DidDocument 20 | /** 21 | * The signer for this issuer 22 | */ 23 | signer: JwtSigner 24 | /** 25 | * The algorithm used by the signer 26 | * @example "ES256K" 27 | */ 28 | alg: JwtAlgorithm 29 | } 30 | 31 | export type CredentialResponse = { 32 | credential: Verifiable 33 | jwt: JwtString 34 | } 35 | -------------------------------------------------------------------------------- /examples/issuer/src/lib/utils/compress-bit-string.test.ts: -------------------------------------------------------------------------------- 1 | import { BitBuffer } from "bit-buffers" 2 | import { describe, expect, it } from "vitest" 3 | import { compressBitString } from "./compress-bit-string" 4 | 5 | describe("compressBitString", () => { 6 | it("should compress an empty bit string", () => { 7 | const expectedResult = new BitBuffer(0).toBitstring() 8 | 9 | expect(compressBitString("")).toBe(expectedResult) 10 | }) 11 | 12 | it("should compress a bit string with the right number of bits", () => { 13 | const input = "0".repeat(1000) 14 | 15 | const expectedResult = new BitBuffer(input.length).toBitstring() 16 | expect(compressBitString(input)).toBe(expectedResult) 17 | }) 18 | 19 | it("should compress a bit string with bits set at the right indices", () => { 20 | const indices = [42, 137, 256, 389, 512, 624, 777, 888, 945, 999] 21 | 22 | const input = Array(1000).fill("0") 23 | 24 | indices.forEach((index) => { 25 | input[index] = "1" 26 | }) 27 | 28 | const inputString = input.join("") 29 | 30 | const bitBuffer = new BitBuffer(input.length) 31 | 32 | const finalBitBuffer = indices.reduce((buffer, index) => { 33 | return buffer.set(index) 34 | }, bitBuffer) 35 | 36 | const expectedResult = finalBitBuffer.toBitstring() 37 | 38 | expect(compressBitString(inputString)).toBe(expectedResult) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /examples/issuer/src/lib/utils/compress-bit-string.ts: -------------------------------------------------------------------------------- 1 | import { BitBuffer } from "bit-buffers" 2 | 3 | export function compressBitString(bitString: string) { 4 | const indices = [] 5 | 6 | for (let i = 0; i < bitString.length; i++) { 7 | if (bitString[i] === "1") { 8 | indices.push(i) 9 | } 10 | } 11 | 12 | return BitBuffer.fromIndexArray(indices, bitString.length).toBitstring() 13 | } 14 | -------------------------------------------------------------------------------- /examples/issuer/src/middleware/database.ts: -------------------------------------------------------------------------------- 1 | import { getDb } from "@/db/get-db" 2 | import type { DatabaseClient } from "@/db/get-db" 3 | import type { Env, MiddlewareHandler } from "hono" 4 | 5 | declare module "hono" { 6 | interface ContextVariableMap { 7 | db: DatabaseClient 8 | } 9 | } 10 | 11 | export function database(): MiddlewareHandler { 12 | return async (c, next) => { 13 | const db = getDb() 14 | c.set("db", db) 15 | await next() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/issuer/src/middleware/did-resolver.ts: -------------------------------------------------------------------------------- 1 | import { getDidResolver } from "agentcommercekit" 2 | import type { DidResolver } from "agentcommercekit" 3 | import type { Env, MiddlewareHandler } from "hono" 4 | 5 | declare module "hono" { 6 | interface ContextVariableMap { 7 | resolver: DidResolver 8 | } 9 | } 10 | 11 | export function didResolver(): MiddlewareHandler { 12 | return async (c, next) => { 13 | const issuer = c.get("issuer") 14 | const resolver = getDidResolver() 15 | // There are certain points where we need to resolve our own DID (when we parse a JWT that we signed, for example). 16 | // However, CF workers cannot invoke themselves, so an attempt to resolve our own DID will fail. To get around this, 17 | // we pre-populate the cache with our own DID document. 18 | resolver.addToCache(issuer.did, issuer.didDocument) 19 | c.set("resolver", resolver) 20 | 21 | await next() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/issuer/src/middleware/issuer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createDidWebDocumentFromKeypair, 3 | createJwtSigner, 4 | generateKeypair, 5 | hexStringToBytes 6 | } from "agentcommercekit" 7 | import { env } from "hono/adapter" 8 | import type { Issuer } from "@/lib/types" 9 | import type { Env, MiddlewareHandler } from "hono" 10 | 11 | declare module "hono" { 12 | interface ContextVariableMap { 13 | issuer: Issuer 14 | } 15 | } 16 | 17 | export function issuer(): MiddlewareHandler { 18 | return async (c, next) => { 19 | const { ISSUER_PRIVATE_KEY, BASE_URL } = env(c) 20 | 21 | const privateKeyBytes = hexStringToBytes(ISSUER_PRIVATE_KEY) 22 | const keypair = await generateKeypair("secp256k1", privateKeyBytes) 23 | 24 | const { did, didDocument } = createDidWebDocumentFromKeypair({ 25 | keypair, 26 | baseUrl: BASE_URL 27 | }) 28 | 29 | const signer = createJwtSigner(keypair) 30 | 31 | c.set("issuer", { 32 | did, 33 | didDocument, 34 | signer, 35 | alg: "ES256K" 36 | }) 37 | 38 | await next() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/issuer/src/routes/healthcheck.ts: -------------------------------------------------------------------------------- 1 | import { apiSuccessResponse } from "@repo/api-utils/api-response" 2 | import { Hono } from "hono" 3 | import type { ApiResponse } from "@repo/api-utils/api-response" 4 | import type { Env } from "hono" 5 | 6 | const app = new Hono() 7 | 8 | /** 9 | * GET /ping 10 | * 11 | * @description Simple health check endpoint to verify the API is running 12 | * @returns Current timestamp in ISO format 13 | */ 14 | app.get("/", (c): ApiResponse<{ pong: string }> => { 15 | return c.json(apiSuccessResponse({ pong: new Date().toISOString() })) 16 | }) 17 | 18 | export default app 19 | -------------------------------------------------------------------------------- /examples/issuer/src/routes/well-known.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono" 2 | import { issuer } from "@/middleware/issuer" 3 | import type { DidDocument } from "agentcommercekit" 4 | import type { Env, TypedResponse } from "hono" 5 | 6 | const app = new Hono() 7 | 8 | app.use("*", issuer()) 9 | 10 | /** 11 | * GET /.well-known/did.json 12 | * 13 | * @description Returns the DID document for the issuer 14 | * @returns DID Document in JSON format 15 | */ 16 | app.get("/did.json", (c): TypedResponse => { 17 | const { didDocument } = c.get("issuer") 18 | 19 | return c.json(didDocument) 20 | }) 21 | 22 | export default app 23 | -------------------------------------------------------------------------------- /examples/issuer/src/test-helpers/did-web-with-signer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createDidWebDocumentFromKeypair, 3 | createJwtSigner, 4 | generateKeypair 5 | } from "agentcommercekit" 6 | import type { DidResolver, DidUri, KeypairAlgorithm } from "agentcommercekit" 7 | 8 | interface GenerateDidWebWithSignerOptions { 9 | controller?: DidUri 10 | algorithm?: KeypairAlgorithm 11 | /** 12 | * An optional DidResolver. If provided, the didDocument will be added to the cache. 13 | */ 14 | resolver?: DidResolver 15 | } 16 | 17 | export async function createDidWebWithSigner( 18 | baseUrl: string, 19 | { 20 | controller, 21 | resolver, 22 | algorithm = "secp256k1" 23 | }: GenerateDidWebWithSignerOptions = {} 24 | ) { 25 | const keypair = await generateKeypair(algorithm) 26 | const { did, didDocument } = createDidWebDocumentFromKeypair({ 27 | keypair, 28 | baseUrl, 29 | controller, 30 | encoding: "jwk" 31 | }) 32 | const signer = createJwtSigner(keypair) 33 | 34 | if (resolver) { 35 | resolver.addToCache(did, didDocument) 36 | } 37 | 38 | return { 39 | keypair, 40 | signer, 41 | did, 42 | didDocument 43 | } 44 | } 45 | 46 | export type DidWithSigner = Awaited> 47 | -------------------------------------------------------------------------------- /examples/issuer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/base-app.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["./src/*"] 7 | } 8 | }, 9 | "include": ["src", "vitest.config.ts", "drizzle.config.ts", "bin"] 10 | } 11 | -------------------------------------------------------------------------------- /examples/issuer/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from "vite-tsconfig-paths" 2 | import { defineConfig } from "vitest/config" 3 | 4 | export default defineConfig({ 5 | plugins: [tsconfigPaths()], 6 | test: { 7 | passWithNoTests: true, 8 | watch: false 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /examples/local-did-host/.env.example: -------------------------------------------------------------------------------- 1 | HOSTNAME="0.0.0.0" 2 | PORT="3458" 3 | AGENT_PRIVATE_KEY="" 4 | CONTROLLER_PRIVATE_KEY="" 5 | -------------------------------------------------------------------------------- /examples/local-did-host/bin/secret: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Generate a 32 byte random secret in hex format 4 | echo "$(openssl rand -hex 32)" 5 | -------------------------------------------------------------------------------- /examples/local-did-host/bin/serve.ts: -------------------------------------------------------------------------------- 1 | import { serve } from "@hono/node-server" 2 | import app from "@/index" 3 | import { buildUrl } from "@/lib/build-url" 4 | import { getIdentityDid } from "@/lib/identity" 5 | 6 | serve( 7 | { 8 | fetch: app.fetch, 9 | hostname: process.env.HOSTNAME ?? "0.0.0.0", 10 | port: parseInt(process.env.PORT ?? "3458") 11 | }, 12 | ({ address, port }) => 13 | Promise.all([ 14 | getIdentityDid(buildUrl(address, port, "/agent")), 15 | getIdentityDid(buildUrl(address, port, "/controller")) 16 | ]).then(([agent, controller]) => { 17 | console.log(`> server running at http://${address}:${port}`) 18 | console.table([ 19 | { 20 | name: "Agent", 21 | didUri: agent, 22 | url: buildUrl(address, port, "/agent/.well-known/did.json") 23 | }, 24 | { 25 | name: "Controller", 26 | didUri: controller, 27 | url: buildUrl(address, port, "/controller/.well-known/did.json") 28 | } 29 | ]) 30 | }) 31 | ) 32 | -------------------------------------------------------------------------------- /examples/local-did-host/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { config } from "@repo/eslint-config/base" 4 | 5 | export default config({ 6 | root: import.meta.dirname 7 | }) 8 | -------------------------------------------------------------------------------- /examples/local-did-host/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@examples/local-did-host", 3 | "version": "0.0.1", 4 | "private": true, 5 | "homepage": "https://github.com/agentcommercekit/ack#readme", 6 | "bugs": "https://github.com/agentcommercekit/ack/issues", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/agentcommercekit/ack.git", 10 | "directory": "examples/local-did-host" 11 | }, 12 | "license": "MIT", 13 | "author": { 14 | "name": "Catena Labs", 15 | "url": "https://catenalabs.com" 16 | }, 17 | "type": "module", 18 | "main": "src/index.ts", 19 | "scripts": { 20 | "check:types": "tsc --noEmit", 21 | "clean": "git clean -xdf .turbo", 22 | "dev": "dotenv -e .env -- tsx watch ./bin/serve.ts", 23 | "lint": "eslint .", 24 | "lint:fix": "eslint . --fix", 25 | "setup": "./bin/setup", 26 | "test": "vitest" 27 | }, 28 | "dependencies": { 29 | "@agentcommercekit/did": "workspace:*", 30 | "@agentcommercekit/jwt": "workspace:*", 31 | "@agentcommercekit/keys": "workspace:*", 32 | "@hono/node-server": "^1.14.2", 33 | "@hono/valibot-validator": "^0.5.2", 34 | "@repo/api-utils": "workspace:*", 35 | "hono": "^4.7.10", 36 | "valibot": "^1.1.0" 37 | }, 38 | "devDependencies": { 39 | "@repo/eslint-config": "workspace:^", 40 | "@repo/typescript-config": "workspace:*", 41 | "dotenv-cli": "^8.0.0", 42 | "eslint": "^9.27.0", 43 | "tsx": "^4.19.4", 44 | "vitest": "^3.1.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/local-did-host/src/global.d.ts: -------------------------------------------------------------------------------- 1 | import "hono" 2 | 3 | declare module "hono" { 4 | interface Env { 5 | Bindings: { 6 | HOSTNAME: string 7 | PORT: string 8 | AGENT_PRIVATE_KEY: `0x${string}` 9 | CONTROLLER_PRIVATE_KEY: `0x${string}` 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/local-did-host/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createJwt } from "@agentcommercekit/jwt" 2 | import { vValidator } from "@hono/valibot-validator" 3 | import { logger } from "@repo/api-utils/middleware/logger" 4 | import { Hono } from "hono" 5 | import * as v from "valibot" 6 | import { identities } from "./middleware/identities" 7 | 8 | const app = new Hono() 9 | 10 | app.use("*", logger()) 11 | app.use("*", identities()) 12 | 13 | /** 14 | * Get the DID document 15 | */ 16 | app.get( 17 | "/:entity/.well-known/did.json", 18 | vValidator( 19 | "param", 20 | v.object({ 21 | entity: v.picklist(["agent", "controller"]) 22 | }) 23 | ), 24 | (c) => { 25 | const { entity } = c.req.valid("param") 26 | const identities = c.get("identities") 27 | return c.json(identities[entity].didDocument) 28 | } 29 | ) 30 | 31 | /** 32 | * Sign a JWT with content 33 | */ 34 | app.post( 35 | "/:entity/sign", 36 | vValidator( 37 | "param", 38 | v.object({ 39 | entity: v.picklist(["agent", "controller"]) 40 | }) 41 | ), 42 | vValidator( 43 | "json", 44 | v.object({ 45 | subject: v.string(), 46 | payload: v.record(v.string(), v.unknown()), 47 | audience: v.optional(v.string()), 48 | expiresIn: v.optional(v.number()) 49 | }) 50 | ), 51 | async (c) => { 52 | const { signer, did, alg } = c.get("identities").agent 53 | const { subject, payload, audience, expiresIn } = c.req.valid("json") 54 | 55 | const jwt = await createJwt( 56 | { 57 | ...payload, 58 | sub: subject, 59 | aud: audience 60 | }, 61 | { 62 | alg, 63 | issuer: did, 64 | expiresIn, 65 | signer, 66 | canonicalize: true 67 | } 68 | ) 69 | 70 | return c.json({ jwt }) 71 | } 72 | ) 73 | 74 | export default app 75 | -------------------------------------------------------------------------------- /examples/local-did-host/src/lib/build-url.ts: -------------------------------------------------------------------------------- 1 | export function buildUrl(host: string, port: string | number, path: string) { 2 | const url = new URL(path, `http://${host}:${port}`) 3 | return url.toString() 4 | } 5 | -------------------------------------------------------------------------------- /examples/local-did-host/src/lib/identity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createDidWebDocumentFromKeypair, 3 | createDidWebUri 4 | } from "@agentcommercekit/did" 5 | import { createJwtSigner } from "@agentcommercekit/jwt" 6 | import { generateKeypair } from "@agentcommercekit/keys" 7 | import { hexStringToBytes } from "@agentcommercekit/keys/encoding" 8 | import type { DidDocument, DidUri } from "@agentcommercekit/did" 9 | import type { JwtAlgorithm, JwtSigner } from "@agentcommercekit/jwt" 10 | import type { KeypairAlgorithm } from "@agentcommercekit/keys" 11 | 12 | export interface Identity { 13 | did: DidUri 14 | didDocument: DidDocument 15 | signer: JwtSigner 16 | alg: JwtAlgorithm 17 | } 18 | 19 | export function getIdentityDid(baseUrl: string): DidUri { 20 | return createDidWebUri(baseUrl) 21 | } 22 | 23 | interface GetIdentityOptions { 24 | baseUrl: string 25 | privateKey: `0x${string}` 26 | controller?: DidUri 27 | alg?: KeypairAlgorithm 28 | } 29 | 30 | export async function getIdentity({ 31 | baseUrl, 32 | privateKey, 33 | controller, 34 | alg = "secp256k1" 35 | }: GetIdentityOptions): Promise { 36 | const keypair = await generateKeypair(alg, hexStringToBytes(privateKey)) 37 | const signer = createJwtSigner(keypair) 38 | 39 | const { did, didDocument } = createDidWebDocumentFromKeypair({ 40 | keypair, 41 | baseUrl, 42 | controller 43 | }) 44 | 45 | return { 46 | did, 47 | didDocument, 48 | signer, 49 | alg 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/local-did-host/src/middleware/identities.ts: -------------------------------------------------------------------------------- 1 | import { env } from "hono/adapter" 2 | import { buildUrl } from "@/lib/build-url" 3 | import { getIdentity } from "@/lib/identity" 4 | import type { Identity } from "@/lib/identity" 5 | import type { Env, MiddlewareHandler } from "hono" 6 | 7 | declare module "hono" { 8 | interface ContextVariableMap { 9 | identities: { 10 | agent: Identity 11 | controller: Identity 12 | } 13 | } 14 | } 15 | 16 | export function identities(): MiddlewareHandler { 17 | return async (c, next) => { 18 | const { AGENT_PRIVATE_KEY, CONTROLLER_PRIVATE_KEY, HOSTNAME, PORT } = env(c) 19 | 20 | const controller = await getIdentity({ 21 | baseUrl: buildUrl(HOSTNAME, PORT, "/controller"), 22 | privateKey: CONTROLLER_PRIVATE_KEY, 23 | alg: "Ed25519" 24 | }) 25 | 26 | const agent = await getIdentity({ 27 | baseUrl: buildUrl(HOSTNAME, PORT, "/agent"), 28 | privateKey: AGENT_PRIVATE_KEY, 29 | controller: controller.did, 30 | alg: "secp256k1" 31 | }) 32 | 33 | c.set("identities", { 34 | agent, 35 | controller 36 | }) 37 | 38 | await next() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/local-did-host/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/base-app.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["src/*"] 7 | } 8 | }, 9 | "include": ["bin", "src", "vitest.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /examples/local-did-host/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig({ 4 | test: { 5 | passWithNoTests: true, 6 | watch: false 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /examples/verifier/.env.example: -------------------------------------------------------------------------------- 1 | BASE_URL="http://localhost:3457" 2 | -------------------------------------------------------------------------------- /examples/verifier/bin/secret: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Generate a 32 byte random secret in hex format 4 | echo "$(openssl rand -hex 32)" 5 | -------------------------------------------------------------------------------- /examples/verifier/bin/serve.ts: -------------------------------------------------------------------------------- 1 | import { serve } from "@hono/node-server" 2 | import app from "@/index" 3 | 4 | serve( 5 | { 6 | fetch: app.fetch, 7 | port: 3457 8 | }, 9 | ({ port }) => { 10 | console.log(`> verifier running at http://localhost:${port}`) 11 | } 12 | ) 13 | -------------------------------------------------------------------------------- /examples/verifier/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { config } from "@repo/eslint-config/base" 4 | 5 | export default config({ 6 | root: import.meta.dirname 7 | }) 8 | -------------------------------------------------------------------------------- /examples/verifier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@examples/verifier", 3 | "version": "0.0.1", 4 | "private": true, 5 | "homepage": "https://github.com/agentcommercekit/ack#readme", 6 | "bugs": "https://github.com/agentcommercekit/ack/issues", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/agentcommercekit/ack.git", 10 | "directory": "examples/verifier" 11 | }, 12 | "license": "MIT", 13 | "author": { 14 | "name": "Catena Labs", 15 | "url": "https://catenalabs.com" 16 | }, 17 | "type": "module", 18 | "main": "./src/index.ts", 19 | "scripts": { 20 | "check:types": "tsc --noEmit", 21 | "clean": "git clean -xdf .turbo", 22 | "dev": "dotenv -e .env -- tsx watch ./bin/serve.ts", 23 | "lint": "eslint .", 24 | "lint:fix": "eslint . --fix", 25 | "setup": "./bin/setup", 26 | "test": "vitest" 27 | }, 28 | "dependencies": { 29 | "@hono/node-server": "^1.14.2", 30 | "@hono/valibot-validator": "^0.5.2", 31 | "@repo/api-utils": "workspace:*", 32 | "agentcommercekit": "workspace:*", 33 | "hono": "^4.7.10", 34 | "valibot": "^1.1.0" 35 | }, 36 | "devDependencies": { 37 | "@repo/eslint-config": "workspace:^", 38 | "@repo/typescript-config": "workspace:*", 39 | "dotenv-cli": "^8.0.0", 40 | "eslint": "^9.27.0", 41 | "tsx": "^4.19.4", 42 | "vitest": "^3.1.4" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/verifier/src/global.d.ts: -------------------------------------------------------------------------------- 1 | import "hono" 2 | 3 | declare module "hono" { 4 | interface Env { 5 | Bindings: { 6 | // Environment 7 | BASE_URL: string 8 | NODE_ENV: "development" | "production" | "test" 9 | 10 | // Verifier 11 | VERIFIER_PRIVATE_KEY: `0x${string}` 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/verifier/src/index.ts: -------------------------------------------------------------------------------- 1 | import { errorHandler } from "@repo/api-utils/middleware/error-handler" 2 | import { logger } from "@repo/api-utils/middleware/logger" 3 | import { Hono } from "hono" 4 | import healthcheck from "./routes/healthcheck" 5 | import verify from "./routes/verify" 6 | import wellKnown from "./routes/well-known" 7 | import type { Env } from "hono" 8 | 9 | const app = new Hono() 10 | 11 | app.use("*", logger()) 12 | app.onError(errorHandler) 13 | 14 | app.route("/.well-known", wellKnown) 15 | app.route("/ping", healthcheck) 16 | app.route("/verify", verify) 17 | 18 | export default app 19 | -------------------------------------------------------------------------------- /examples/verifier/src/middleware/verifier.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createDidWebDocumentFromKeypair, 3 | createJwtSigner, 4 | generateKeypair, 5 | hexStringToBytes 6 | } from "agentcommercekit" 7 | import { env } from "hono/adapter" 8 | import type { DidDocument, DidUri, JwtSigner } from "agentcommercekit" 9 | import type { Env, MiddlewareHandler } from "hono" 10 | declare module "hono" { 11 | interface ContextVariableMap { 12 | verifier: { 13 | /** 14 | * The Did of the issuer 15 | */ 16 | did: DidUri 17 | /** 18 | * The Did document of the issuer 19 | */ 20 | didDocument: DidDocument 21 | /** 22 | * The signer for this issuer 23 | */ 24 | signer: JwtSigner 25 | /** 26 | * The algorithm used by the signer 27 | * @example "ES256K" 28 | */ 29 | alg: string 30 | } 31 | } 32 | } 33 | 34 | export function verifier(): MiddlewareHandler { 35 | return async (c, next) => { 36 | const { VERIFIER_PRIVATE_KEY, BASE_URL } = env(c) 37 | 38 | const keypair = await generateKeypair( 39 | "secp256k1", 40 | hexStringToBytes(VERIFIER_PRIVATE_KEY) 41 | ) 42 | const { did, didDocument } = createDidWebDocumentFromKeypair({ 43 | keypair, 44 | baseUrl: BASE_URL 45 | }) 46 | 47 | const signer = createJwtSigner(keypair) 48 | 49 | c.set("verifier", { 50 | did, 51 | didDocument, 52 | signer, 53 | alg: "ES256K" 54 | }) 55 | 56 | await next() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/verifier/src/routes/healthcheck.ts: -------------------------------------------------------------------------------- 1 | import { apiSuccessResponse } from "@repo/api-utils/api-response" 2 | import { Hono } from "hono" 3 | import type { ApiResponse } from "@repo/api-utils/api-response" 4 | import type { Env } from "hono" 5 | 6 | const app = new Hono() 7 | 8 | app.get("/", (c): ApiResponse<{ pong: string }> => { 9 | return c.json(apiSuccessResponse({ pong: new Date().toISOString() })) 10 | }) 11 | 12 | export default app 13 | -------------------------------------------------------------------------------- /examples/verifier/src/routes/verify.ts: -------------------------------------------------------------------------------- 1 | import { vValidator } from "@hono/valibot-validator" 2 | import { apiSuccessResponse } from "@repo/api-utils/api-response" 3 | import { 4 | createDidWebUri, 5 | getControllerClaimVerifier, 6 | getDidResolver, 7 | getReceiptClaimVerifier, 8 | isJwtString, 9 | parseJwtCredential, 10 | verifyParsedCredential 11 | } from "agentcommercekit" 12 | import { 13 | credentialSchema, 14 | jwtStringSchema 15 | } from "agentcommercekit/schemas/valibot" 16 | import { Hono } from "hono" 17 | import { ValiError } from "valibot" 18 | import * as v from "valibot" 19 | import type { ApiResponse } from "@repo/api-utils/api-response" 20 | import type { Env } from "hono" 21 | 22 | const app = new Hono() 23 | 24 | // Treat the local `issuer` API as trusted. 25 | const trustedIssuers: string[] = [ 26 | createDidWebUri(new URL("http://localhost:3456")) 27 | ] 28 | 29 | const bodySchema = v.object({ 30 | credential: v.union([credentialSchema, jwtStringSchema]) 31 | }) 32 | 33 | app.post( 34 | "/", 35 | vValidator("json", bodySchema, (result) => { 36 | if (!result.success) { 37 | throw new ValiError(result.issues) 38 | } 39 | }), 40 | async (c): Promise> => { 41 | const resolver = getDidResolver() 42 | let { credential } = c.req.valid("json") 43 | 44 | if (isJwtString(credential)) { 45 | credential = await parseJwtCredential(credential, resolver) 46 | } 47 | 48 | await verifyParsedCredential(credential, { 49 | trustedIssuers, 50 | resolver, 51 | verifiers: [getControllerClaimVerifier(), getReceiptClaimVerifier()] 52 | }) 53 | 54 | return c.json(apiSuccessResponse(null)) 55 | } 56 | ) 57 | 58 | export default app 59 | -------------------------------------------------------------------------------- /examples/verifier/src/routes/well-known.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono" 2 | import { verifier } from "@/middleware/verifier" 3 | import type { DidDocument } from "agentcommercekit" 4 | import type { Env, TypedResponse } from "hono" 5 | 6 | const app = new Hono() 7 | 8 | app.use("*", verifier()) 9 | 10 | app.get("/did.json", (c): TypedResponse => { 11 | const { didDocument } = c.get("verifier") 12 | 13 | return c.json(didDocument) 14 | }) 15 | 16 | export default app 17 | -------------------------------------------------------------------------------- /examples/verifier/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/base-app.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["./src/*"] 7 | } 8 | }, 9 | "include": ["src", "vitest.config.ts", "bin"] 10 | } 11 | -------------------------------------------------------------------------------- /examples/verifier/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig({ 4 | test: { 5 | passWithNoTests: true, 6 | watch: false 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /packages/ack-id/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @agentcommercekit/ack-id 2 | 3 | ## 0.2.1 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [[`5b1c8b1`](https://github.com/agentcommercekit/ack/commit/5b1c8b1b8105e781f977379f019f96efbcab3e27)]: 8 | - @agentcommercekit/vc@0.2.1 9 | 10 | ## 0.2.0 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [[`4104ffe`](https://github.com/agentcommercekit/ack/commit/4104ffeae34c7ae972b375871feb09bbe5d27b73)]: 15 | - @agentcommercekit/keys@0.2.0 16 | - @agentcommercekit/did@0.2.0 17 | - @agentcommercekit/vc@0.2.0 18 | 19 | ## 0.1.0 20 | 21 | ### Minor Changes 22 | 23 | - Initial release of the Agent Commerce Kit (ACK) TypeScript SDK 24 | 25 | ### Patch Changes 26 | 27 | - Updated dependencies []: 28 | - @agentcommercekit/did@0.1.0 29 | - @agentcommercekit/keys@0.1.0 30 | - @agentcommercekit/vc@0.1.0 31 | -------------------------------------------------------------------------------- /packages/ack-id/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { config } from "@repo/eslint-config/base" 4 | 5 | export default config({ 6 | root: import.meta.dirname 7 | }) 8 | -------------------------------------------------------------------------------- /packages/ack-id/src/controller-credential.test.ts: -------------------------------------------------------------------------------- 1 | import { createDidWebUri } from "@agentcommercekit/did" 2 | import { beforeAll, describe, expect, it, vi } from "vitest" 3 | import { createControllerCredential } from "./controller-credential" 4 | 5 | describe("createControllerCredential", () => { 6 | const date = new Date() 7 | 8 | beforeAll(() => { 9 | vi.setSystemTime(date) 10 | }) 11 | 12 | it("it creates a valid credential", () => { 13 | const controllerDid = createDidWebUri("https://controller.example.com") 14 | const subjectDid = createDidWebUri("https://subject.example.com") 15 | const issuerDid = createDidWebUri("https://issuer.example.com") 16 | 17 | const credential = createControllerCredential({ 18 | subject: subjectDid, 19 | controller: controllerDid, 20 | issuer: issuerDid 21 | }) 22 | 23 | // Check basic structure 24 | expect(credential).toEqual({ 25 | "@context": ["https://www.w3.org/2018/credentials/v1"], 26 | id: undefined, 27 | type: ["VerifiableCredential", "ControllerCredential"], 28 | issuer: { id: issuerDid }, 29 | credentialSubject: { 30 | id: subjectDid, 31 | controller: controllerDid 32 | }, 33 | issuanceDate: new Date().toISOString() 34 | }) 35 | 36 | // Verify issuanceDate is a valid ISO date string 37 | expect(() => new Date(credential.issuanceDate)).not.toThrow() 38 | }) 39 | 40 | it("defaults to controller as issuer", () => { 41 | const controllerDid = createDidWebUri("https://controller.example.com") 42 | const subjectDid = createDidWebUri("https://subject.example.com") 43 | 44 | const credential = createControllerCredential({ 45 | subject: subjectDid, 46 | controller: controllerDid 47 | }) 48 | 49 | expect(credential.issuer).toEqual({ id: controllerDid }) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /packages/ack-id/src/controller-credential.ts: -------------------------------------------------------------------------------- 1 | import { createCredential } from "@agentcommercekit/vc" 2 | import type { DidUri } from "@agentcommercekit/did" 3 | import type { W3CCredential } from "@agentcommercekit/vc" 4 | 5 | type CreateControllerCredentialParams = { 6 | /** 7 | * The id of the credential. 8 | */ 9 | id?: string 10 | /** 11 | * The Did of the subject of the credential. 12 | */ 13 | subject: DidUri 14 | /** 15 | * The Did of the controller of the credential. 16 | */ 17 | controller: DidUri 18 | /** 19 | * The Did of the issuer of the credential. If not provided, the controller 20 | * will be used as the issuer. 21 | */ 22 | issuer?: DidUri 23 | } 24 | 25 | /** 26 | * Create a controller credential 27 | * 28 | * @param params - The {@link CreateControllerCredentialParams} to use 29 | * @returns A {@link W3CCredential} with a controller attestation 30 | */ 31 | export function createControllerCredential({ 32 | id, 33 | subject, 34 | controller, 35 | issuer 36 | }: CreateControllerCredentialParams): W3CCredential { 37 | return createCredential({ 38 | id, 39 | type: "ControllerCredential", 40 | issuer: issuer ?? controller, 41 | subject, 42 | attestation: { 43 | controller: controller 44 | } 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /packages/ack-id/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./controller-claim-verifier" 2 | export * from "./controller-credential" 3 | -------------------------------------------------------------------------------- /packages/ack-id/src/schemas/valibot.ts: -------------------------------------------------------------------------------- 1 | import * as v from "valibot" 2 | 3 | export const controllerClaimSchema = v.object({ 4 | id: v.string(), 5 | controller: v.string() 6 | }) 7 | -------------------------------------------------------------------------------- /packages/ack-id/src/schemas/zod.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | 3 | export const controllerClaimSchema = z.object({ 4 | id: z.string(), 5 | controller: z.string() 6 | }) 7 | -------------------------------------------------------------------------------- /packages/ack-id/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/typescript-library.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", "dist"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/ack-id/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig({ 4 | test: { 5 | passWithNoTests: true, 6 | watch: false 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /packages/ack-pay/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @agentcommercekit/ack-pay 2 | 3 | ## 0.2.1 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [[`5b1c8b1`](https://github.com/agentcommercekit/ack/commit/5b1c8b1b8105e781f977379f019f96efbcab3e27)]: 8 | - @agentcommercekit/vc@0.2.1 9 | 10 | ## 0.2.0 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [[`4104ffe`](https://github.com/agentcommercekit/ack/commit/4104ffeae34c7ae972b375871feb09bbe5d27b73)]: 15 | - @agentcommercekit/keys@0.2.0 16 | - @agentcommercekit/did@0.2.0 17 | - @agentcommercekit/jwt@0.2.0 18 | - @agentcommercekit/vc@0.2.0 19 | 20 | ## 0.1.0 21 | 22 | ### Minor Changes 23 | 24 | - Initial release of the Agent Commerce Kit (ACK) TypeScript SDK 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies []: 29 | - @agentcommercekit/did@0.1.0 30 | - @agentcommercekit/jwt@0.1.0 31 | - @agentcommercekit/keys@0.1.0 32 | - @agentcommercekit/vc@0.1.0 33 | -------------------------------------------------------------------------------- /packages/ack-pay/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { config } from "@repo/eslint-config/base" 4 | 5 | export default config({ 6 | root: import.meta.dirname 7 | }) 8 | -------------------------------------------------------------------------------- /packages/ack-pay/src/create-payment-receipt.ts: -------------------------------------------------------------------------------- 1 | import { createCredential } from "@agentcommercekit/vc" 2 | import type { DidUri } from "@agentcommercekit/did" 3 | import type { W3CCredential } from "@agentcommercekit/vc" 4 | 5 | const PAYMENT_RECEIPT_TYPE = "PaymentReceiptCredential" 6 | 7 | interface CreatePaymentReceiptParams { 8 | paymentToken: string 9 | paymentOptionId: string 10 | issuer: DidUri 11 | payerDid: DidUri 12 | expirationDate?: Date 13 | metadata?: Record 14 | } 15 | 16 | /** 17 | * Create a payment receipt 18 | * 19 | * @param params - The {@link CreatePaymentReceiptParams} to use 20 | * @returns A {@link W3CCredential} with a payment receipt attestation 21 | */ 22 | export function createPaymentReceipt({ 23 | paymentToken, 24 | paymentOptionId, 25 | issuer, 26 | payerDid, 27 | expirationDate, 28 | metadata 29 | }: CreatePaymentReceiptParams): W3CCredential { 30 | const attestation: Record = { 31 | paymentToken, 32 | paymentOptionId 33 | } 34 | 35 | if (metadata) { 36 | attestation.metadata = metadata 37 | } 38 | 39 | return createCredential({ 40 | type: PAYMENT_RECEIPT_TYPE, 41 | issuer: issuer, 42 | subject: payerDid, 43 | expirationDate, 44 | attestation 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /packages/ack-pay/src/create-payment-request-body.ts: -------------------------------------------------------------------------------- 1 | import * as v from "valibot" 2 | import { createPaymentToken } from "./create-payment-token" 3 | import { paymentRequestSchema } from "./schemas/valibot" 4 | import type { PaymentTokenOptions } from "./create-payment-token" 5 | import type { PaymentRequestInit } from "./payment-request" 6 | import type { paymentRequestBodySchema } from "./schemas/valibot" 7 | 8 | export type PaymentRequestBody = v.InferOutput 9 | 10 | /** 11 | * Create a payment request body 12 | * 13 | * @param params - The payment config params, including the amount, currency, and recipient 14 | * @param options - The {@link PaymentTokenOptions} to use 15 | * @returns A payment request body 16 | */ 17 | export async function createPaymentRequestBody( 18 | paymentRequestInit: PaymentRequestInit, 19 | { issuer, signer, algorithm }: PaymentTokenOptions 20 | ): Promise { 21 | const paymentRequest = v.parse(paymentRequestSchema, paymentRequestInit) 22 | const paymentToken = await createPaymentToken(paymentRequest, { 23 | issuer, 24 | signer, 25 | algorithm 26 | }) 27 | 28 | return { 29 | paymentRequest, 30 | paymentToken 31 | } 32 | } 33 | 34 | /** 35 | * Create a 402 `Response` object with the payment request body 36 | * 37 | * @param params - The payment config params 38 | * @param options - The {@link PaymentTokenOptions} to use 39 | * @returns A 402 `Response` object with the payment request body 40 | */ 41 | export async function createPaymentRequestResponse( 42 | params: PaymentRequestInit, 43 | options: PaymentTokenOptions 44 | ): Promise { 45 | const paymentRequiredBody = await createPaymentRequestBody(params, options) 46 | 47 | return new Response(JSON.stringify(paymentRequiredBody), { 48 | status: 402, 49 | headers: { 50 | "Content-Type": "application/json" 51 | } 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /packages/ack-pay/src/create-payment-token.ts: -------------------------------------------------------------------------------- 1 | import { createJwt } from "@agentcommercekit/jwt" 2 | import type { PaymentRequest } from "./payment-request" 3 | import type { DidUri } from "@agentcommercekit/did" 4 | import type { JwtAlgorithm, JwtSigner, JwtString } from "@agentcommercekit/jwt" 5 | 6 | export interface PaymentTokenOptions { 7 | /** 8 | * The issuer of the payment token 9 | */ 10 | issuer: DidUri 11 | /** 12 | * The signer of the payment token 13 | */ 14 | signer: JwtSigner 15 | /** 16 | * The algorithm of the payment token 17 | */ 18 | algorithm: JwtAlgorithm 19 | } 20 | 21 | /** 22 | * Builds a signed JWT payment token for a given payment request 23 | * 24 | * @param paymentRequest - A valid PaymentRequest to create a payment token for 25 | * @param options - The {@link PaymentTokenOptions} to use 26 | * @returns A signed JWT payment token 27 | */ 28 | export async function createPaymentToken( 29 | paymentRequest: PaymentRequest, 30 | { issuer, signer, algorithm }: PaymentTokenOptions 31 | ): Promise { 32 | return createJwt( 33 | { ...paymentRequest, sub: paymentRequest.id }, 34 | { 35 | issuer, 36 | signer 37 | }, 38 | { 39 | alg: algorithm 40 | } 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /packages/ack-pay/src/errors.ts: -------------------------------------------------------------------------------- 1 | export class InvalidPaymentTokenError extends Error { 2 | constructor(message = "Invalid payment token") { 3 | super(message) 4 | this.name = "InvalidPaymentTokenError" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/ack-pay/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./create-payment-receipt" 2 | export * from "./create-payment-token" 3 | export * from "./errors" 4 | export * from "./create-payment-request-body" 5 | export * from "./verify-payment-token" 6 | export * from "./payment-request" 7 | export * from "./receipt-claim-verifier" 8 | export * from "./verify-payment-receipt" 9 | -------------------------------------------------------------------------------- /packages/ack-pay/src/payment-request.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest" 2 | import { isPaymentRequest } from "./payment-request" 3 | import type { PaymentRequestInit } from "./payment-request" 4 | 5 | describe("isPaymentRequest", () => { 6 | const validPaymentRequest: PaymentRequestInit = { 7 | id: "test-request-id", 8 | paymentOptions: [ 9 | { 10 | id: "test-payment-option-id", 11 | amount: 100, 12 | decimals: 2, 13 | currency: "USD", 14 | recipient: "did:example:recipient" 15 | } 16 | ] 17 | } 18 | 19 | it("returns true for a valid payment request", () => { 20 | expect(isPaymentRequest(validPaymentRequest)).toBe(true) 21 | }) 22 | 23 | it("returns false if the payment request is invalid", () => { 24 | expect( 25 | isPaymentRequest({ 26 | ...validPaymentRequest, 27 | id: undefined 28 | }) 29 | ).toBe(false) 30 | }) 31 | 32 | it("returns false if given null", () => { 33 | expect(isPaymentRequest(null)).toBe(false) 34 | }) 35 | 36 | it("returns false if given undefined", () => { 37 | expect(isPaymentRequest(undefined)).toBe(false) 38 | }) 39 | 40 | it("returns false if given a non-object", () => { 41 | expect(isPaymentRequest(1)).toBe(false) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /packages/ack-pay/src/payment-request.ts: -------------------------------------------------------------------------------- 1 | import * as v from "valibot" 2 | import { paymentRequestSchema } from "./schemas/valibot" 3 | import type { paymentOptionSchema } from "./schemas/valibot" 4 | 5 | export type PaymentRequestInit = v.InferInput 6 | export type PaymentRequest = v.InferOutput 7 | export type PaymentOption = v.InferOutput 8 | 9 | /** 10 | * Checks if a value is a valid Payment Request 11 | * @param value - The value to check 12 | * @returns `true` if the value is a valid Payment Request, `false` otherwise 13 | */ 14 | export function isPaymentRequest(value: unknown): value is PaymentRequest { 15 | return v.is(paymentRequestSchema, value) 16 | } 17 | -------------------------------------------------------------------------------- /packages/ack-pay/src/receipt-claim-verifier.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InvalidCredentialSubjectError, 3 | isCredential 4 | } from "@agentcommercekit/vc" 5 | import * as v from "valibot" 6 | import { paymentReceiptClaimSchema } from "./schemas/valibot" 7 | import type { 8 | ClaimVerifier, 9 | CredentialSubject, 10 | W3CCredential 11 | } from "@agentcommercekit/vc" 12 | 13 | export interface PaymentReceiptCredential extends W3CCredential { 14 | credentialSubject: v.InferOutput 15 | } 16 | 17 | function isPaymentReceiptClaim( 18 | credentialSubject: CredentialSubject 19 | ): credentialSubject is v.InferOutput { 20 | return v.is(paymentReceiptClaimSchema, credentialSubject) 21 | } 22 | 23 | /** 24 | * Check if a credential is a payment receipt credential 25 | * 26 | * @param credential - The credential to check 27 | * @returns `true` if the credential is a payment receipt credential, `false` otherwise 28 | */ 29 | export function isPaymentReceiptCredential( 30 | credential: unknown 31 | ): credential is PaymentReceiptCredential { 32 | if (!isCredential(credential)) { 33 | return false 34 | } 35 | return isPaymentReceiptClaim(credential.credentialSubject) 36 | } 37 | 38 | async function verifyPaymentReceiptClaim( 39 | credentialSubject: CredentialSubject 40 | ): Promise { 41 | if (!isPaymentReceiptClaim(credentialSubject)) { 42 | throw new InvalidCredentialSubjectError() 43 | } 44 | 45 | return Promise.resolve() 46 | } 47 | 48 | /** 49 | * Get a claim verifier for payment receipt credentials 50 | * 51 | * @returns A {@link ClaimVerifier} that verifies payment receipt credentials 52 | */ 53 | export function getReceiptClaimVerifier(): ClaimVerifier { 54 | return { 55 | accepts: (type: string[]) => type.includes("PaymentReceiptCredential"), 56 | // For now, we just verify the credential subject matches the expected schema 57 | verify: verifyPaymentReceiptClaim 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/ack-pay/src/schemas/valibot.ts: -------------------------------------------------------------------------------- 1 | import { didUriSchema } from "@agentcommercekit/did/schemas/valibot" 2 | import * as v from "valibot" 3 | 4 | const urlOrDidUri = v.union([v.pipe(v.string(), v.url()), didUriSchema]) 5 | 6 | export const paymentOptionSchema = v.object({ 7 | id: v.string(), 8 | amount: v.pipe(v.number(), v.integer(), v.gtValue(0)), 9 | decimals: v.pipe(v.number(), v.integer(), v.toMinValue(0)), 10 | currency: v.string(), 11 | recipient: v.string(), 12 | network: v.optional(v.string()), 13 | paymentService: v.optional(urlOrDidUri), 14 | receiptService: v.optional(urlOrDidUri) 15 | }) 16 | 17 | export const paymentRequestSchema = v.object({ 18 | id: v.string(), 19 | description: v.optional(v.string()), 20 | serviceCallback: v.optional(v.pipe(v.string(), v.url())), 21 | expiresAt: v.optional( 22 | v.pipe( 23 | v.union([v.date(), v.string()]), 24 | v.transform((input) => new Date(input).toISOString()) 25 | ) 26 | ), 27 | paymentOptions: v.pipe( 28 | v.tupleWithRest([paymentOptionSchema], paymentOptionSchema), 29 | v.nonEmpty() 30 | ) 31 | }) 32 | 33 | export const paymentReceiptClaimSchema = v.object({ 34 | paymentToken: v.string(), // Often a JwtString but not required 35 | paymentOptionId: v.string(), 36 | metadata: v.optional(v.record(v.string(), v.unknown())) 37 | }) 38 | 39 | export const paymentRequestBodySchema = v.object({ 40 | paymentRequest: paymentRequestSchema, 41 | paymentToken: v.string() // Often a JwtString but not required 42 | }) 43 | -------------------------------------------------------------------------------- /packages/ack-pay/src/schemas/zod.ts: -------------------------------------------------------------------------------- 1 | import { didUriSchema } from "@agentcommercekit/did/schemas/zod" 2 | import { z } from "zod" 3 | 4 | const urlOrDidUri = z.union([z.string().url(), didUriSchema]) 5 | 6 | export const paymentOptionSchema = z.object({ 7 | id: z.string(), 8 | amount: z.number().int().positive(), 9 | decimals: z.number().int().nonnegative(), 10 | currency: z.string(), 11 | recipient: z.string(), 12 | network: z.string().optional(), 13 | paymentService: urlOrDidUri.optional(), 14 | receiptService: urlOrDidUri.optional() 15 | }) 16 | 17 | export const paymentRequestSchema = z.object({ 18 | id: z.string(), 19 | description: z.string().optional(), 20 | serviceCallback: z.string().url().optional(), 21 | expiresAt: z 22 | .union([z.date(), z.string()]) 23 | .transform((val) => new Date(val).toISOString()) 24 | .optional(), 25 | paymentOptions: z.array(paymentOptionSchema).nonempty() 26 | }) 27 | 28 | export const paymentReceiptClaimSchema = z.object({ 29 | paymentToken: z.string(), // Often a JwtString but not required 30 | paymentOptionId: z.string(), 31 | metadata: z.record(z.string(), z.unknown()).optional() 32 | }) 33 | 34 | export const paymentRequestBodySchema = z.object({ 35 | payment: paymentRequestSchema, 36 | paymentToken: z.string() // Often a JwtString but not required 37 | }) 38 | -------------------------------------------------------------------------------- /packages/ack-pay/src/verify-payment-token.ts: -------------------------------------------------------------------------------- 1 | import { verifyJwt } from "@agentcommercekit/jwt" 2 | import * as v from "valibot" 3 | import { InvalidPaymentTokenError } from "./errors" 4 | import { paymentRequestSchema } from "./schemas/valibot" 5 | import type { PaymentRequest } from "./payment-request" 6 | import type { Resolvable } from "@agentcommercekit/did" 7 | import type { JwtVerified } from "@agentcommercekit/jwt" 8 | 9 | interface ValidatePaymentTokenOptions { 10 | /** 11 | * The resolver to use for did resolution 12 | */ 13 | resolver?: Resolvable 14 | /** 15 | * Whether to verify the expiry of the payment token 16 | */ 17 | verifyExpiry?: boolean 18 | } 19 | 20 | /** 21 | * Verify a payment token 22 | * 23 | * @param token - The payment token to verify 24 | * @param options - The {@link ValidatePaymentTokenOptions} to use 25 | * @returns The {@link PaymentRequest} parsed from the payment token and the parsed JWT 26 | */ 27 | export async function verifyPaymentToken( 28 | token: string, 29 | options: ValidatePaymentTokenOptions = {} 30 | ): Promise<{ paymentRequest: PaymentRequest; parsed: JwtVerified }> { 31 | let parsedPaymentToken: JwtVerified 32 | 33 | try { 34 | parsedPaymentToken = await verifyJwt(token, { 35 | resolver: options.resolver, 36 | policies: { 37 | aud: false, 38 | exp: options.verifyExpiry ?? true 39 | } 40 | }) 41 | } catch (_err) { 42 | throw new InvalidPaymentTokenError() 43 | } 44 | 45 | const { success, output } = v.safeParse( 46 | paymentRequestSchema, 47 | parsedPaymentToken.payload 48 | ) 49 | 50 | if (!success) { 51 | throw new InvalidPaymentTokenError( 52 | "Payment token is not a valid PaymentRequest" 53 | ) 54 | } 55 | 56 | return { 57 | paymentRequest: output, 58 | parsed: parsedPaymentToken 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/ack-pay/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/typescript-library.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", "dist"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/ack-pay/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig({ 4 | test: { 5 | passWithNoTests: true, 6 | watch: false 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /packages/agentcommercekit/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # agentcommercekit 2 | 3 | ## 0.2.1 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [[`5b1c8b1`](https://github.com/agentcommercekit/ack/commit/5b1c8b1b8105e781f977379f019f96efbcab3e27)]: 8 | - @agentcommercekit/vc@0.2.1 9 | - @agentcommercekit/ack-id@0.2.1 10 | - @agentcommercekit/ack-pay@0.2.1 11 | 12 | ## 0.2.0 13 | 14 | ### Minor Changes 15 | 16 | - [#3](https://github.com/agentcommercekit/ack/pull/3) [`4104ffe`](https://github.com/agentcommercekit/ack/commit/4104ffeae34c7ae972b375871feb09bbe5d27b73) Thanks [@venables](https://github.com/venables)! - - Upgrade legacy public key formats to use multibase in DID Documents 17 | - Update base64 methods to be explicit that they use `base64url` encoding 18 | - Simplify interface for public key encoding methods 19 | 20 | ### Patch Changes 21 | 22 | - Updated dependencies [[`4104ffe`](https://github.com/agentcommercekit/ack/commit/4104ffeae34c7ae972b375871feb09bbe5d27b73)]: 23 | - @agentcommercekit/keys@0.2.0 24 | - @agentcommercekit/did@0.2.0 25 | - @agentcommercekit/ack-id@0.2.0 26 | - @agentcommercekit/ack-pay@0.2.0 27 | - @agentcommercekit/jwt@0.2.0 28 | - @agentcommercekit/vc@0.2.0 29 | 30 | ## 0.1.0 31 | 32 | ### Minor Changes 33 | 34 | - Initial release of the Agent Commerce Kit (ACK) TypeScript SDK 35 | 36 | ### Patch Changes 37 | 38 | - Updated dependencies []: 39 | - @agentcommercekit/ack-id@0.1.0 40 | - @agentcommercekit/ack-pay@0.1.0 41 | - @agentcommercekit/did@0.1.0 42 | - @agentcommercekit/jwt@0.1.0 43 | - @agentcommercekit/keys@0.1.0 44 | - @agentcommercekit/vc@0.1.0 45 | -------------------------------------------------------------------------------- /packages/agentcommercekit/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { config } from "@repo/eslint-config/base" 4 | 5 | export default config({ 6 | root: import.meta.dirname 7 | }) 8 | -------------------------------------------------------------------------------- /packages/agentcommercekit/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "@agentcommercekit/ack-pay" 2 | export * from "@agentcommercekit/ack-id" 3 | export * from "@agentcommercekit/did" 4 | export * from "@agentcommercekit/jwt" 5 | export * from "@agentcommercekit/keys" 6 | export * from "@agentcommercekit/keys/encoding" 7 | export * from "@agentcommercekit/vc" 8 | -------------------------------------------------------------------------------- /packages/agentcommercekit/src/schemas/valibot.ts: -------------------------------------------------------------------------------- 1 | export * from "@agentcommercekit/ack-pay/schemas/valibot" 2 | export * from "@agentcommercekit/ack-id/schemas/valibot" 3 | export * from "@agentcommercekit/did/schemas/valibot" 4 | export * from "@agentcommercekit/jwt/schemas/valibot" 5 | export * from "@agentcommercekit/vc/schemas/valibot" 6 | -------------------------------------------------------------------------------- /packages/agentcommercekit/src/schemas/zod.ts: -------------------------------------------------------------------------------- 1 | export * from "@agentcommercekit/ack-pay/schemas/zod" 2 | export * from "@agentcommercekit/ack-id/schemas/zod" 3 | export * from "@agentcommercekit/did/schemas/zod" 4 | export * from "@agentcommercekit/jwt/schemas/zod" 5 | export * from "@agentcommercekit/vc/schemas/zod" 6 | -------------------------------------------------------------------------------- /packages/agentcommercekit/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/typescript-library.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", "dist"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/agentcommercekit/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig({ 4 | test: { 5 | passWithNoTests: true, 6 | watch: false 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /packages/did/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @agentcommercekit/did 2 | 3 | ## 0.2.0 4 | 5 | ### Minor Changes 6 | 7 | - [#3](https://github.com/agentcommercekit/ack/pull/3) [`4104ffe`](https://github.com/agentcommercekit/ack/commit/4104ffeae34c7ae972b375871feb09bbe5d27b73) Thanks [@venables](https://github.com/venables)! - - Upgrade legacy public key formats to use multibase in DID Documents 8 | - Update base64 methods to be explicit that they use `base64url` encoding 9 | - Simplify interface for public key encoding methods 10 | 11 | ### Patch Changes 12 | 13 | - Updated dependencies [[`4104ffe`](https://github.com/agentcommercekit/ack/commit/4104ffeae34c7ae972b375871feb09bbe5d27b73)]: 14 | - @agentcommercekit/keys@0.2.0 15 | 16 | ## 0.1.0 17 | 18 | ### Minor Changes 19 | 20 | - Initial release of the Agent Commerce Kit (ACK) TypeScript SDK 21 | 22 | ### Patch Changes 23 | 24 | - Updated dependencies []: 25 | - @agentcommercekit/keys@0.1.0 26 | -------------------------------------------------------------------------------- /packages/did/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { config } from "@repo/eslint-config/base" 4 | 5 | export default config({ 6 | root: import.meta.dirname 7 | }) 8 | -------------------------------------------------------------------------------- /packages/did/src/did-document.ts: -------------------------------------------------------------------------------- 1 | import type { DIDDocument as DidDocument } from "did-resolver" 2 | 3 | export type { DidDocument } 4 | 5 | /** 6 | * Check if a value is a did document 7 | * 8 | * @param didDocument - The value to check 9 | * @returns True if the value is a did document, false otherwise 10 | */ 11 | export function isDidDocument( 12 | didDocument: unknown 13 | ): didDocument is DidDocument { 14 | return ( 15 | typeof didDocument === "object" && 16 | didDocument !== null && 17 | "id" in didDocument 18 | ) 19 | } 20 | 21 | /** 22 | * Check if a did document is for a specific did 23 | * 24 | * @param didDocument - The did document to check 25 | * @param did - The did to check against 26 | * @returns True if the did document is for the specific did, false otherwise 27 | */ 28 | export function isDidDocumentForDid( 29 | didDocument: DidDocument, 30 | did: string 31 | ): boolean { 32 | return didDocument.id === did 33 | } 34 | -------------------------------------------------------------------------------- /packages/did/src/did-resolvers/get-did-resolver.ts: -------------------------------------------------------------------------------- 1 | import { getResolver as getKeyDidResolver } from "key-did-resolver" 2 | import { getResolver as getPkhDidResolver } from "pkh-did-resolver" 3 | import { DidResolver } from "./did-resolver" 4 | import { getResolver as getWebDidResolver } from "./web-did-resolver" 5 | import type { DidWebResolverOptions } from "./web-did-resolver" 6 | import type { ResolverOptions } from "did-resolver" 7 | 8 | interface GetDidResolverOptions extends ResolverOptions { 9 | /** 10 | * The options for the did:web resolver 11 | */ 12 | webOptions?: DidWebResolverOptions 13 | } 14 | 15 | /** 16 | * Get a did resolver that can resolve multiple DID methods. 17 | * 18 | * @param options - The {@link GetDidResolverOptions} to use for the did resolver 19 | * @returns A new {@link DidResolver} instance 20 | */ 21 | export function getDidResolver({ 22 | webOptions = { 23 | allowedHttpHosts: ["localhost", "127.0.0.1", "0.0.0.0"] 24 | }, 25 | ...options 26 | }: GetDidResolverOptions = {}): DidResolver { 27 | const keyResolver = getKeyDidResolver() 28 | const webResolver = getWebDidResolver(webOptions) 29 | const pkhResolver = getPkhDidResolver() 30 | 31 | const didResolver = new DidResolver( 32 | { 33 | ...keyResolver, 34 | ...webResolver, 35 | ...pkhResolver 36 | }, 37 | options 38 | ) 39 | 40 | return didResolver 41 | } 42 | -------------------------------------------------------------------------------- /packages/did/src/did-uri.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest" 2 | import { isDidUri } from "./did-uri" 3 | 4 | describe("isDidUri", () => { 5 | it("returns true for a valid DID URI", () => { 6 | expect(isDidUri("did:web:example.com")).toBe(true) 7 | }) 8 | 9 | it("returns false for an invalid DID URI", () => { 10 | expect(isDidUri("invalid-did-uri")).toBe(false) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /packages/did/src/did-uri.ts: -------------------------------------------------------------------------------- 1 | export type DidUri = `did:${string}:${string}` 2 | 3 | /** 4 | * Check if a value is a did uri 5 | * 6 | * @param val - The value to check 7 | * @returns `true` if the value is a did uri, `false` otherwise 8 | */ 9 | export function isDidUri(val: unknown): val is DidUri { 10 | return ( 11 | typeof val === "string" && 12 | val.startsWith("did:") && 13 | val.split(":").length >= 3 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /packages/did/src/errors.ts: -------------------------------------------------------------------------------- 1 | export class DidResolutionError extends Error {} 2 | 3 | export class DidDocumentNotFoundError extends DidResolutionError { 4 | constructor(message = "DID document not found") { 5 | super(message) 6 | this.name = "DidDocumentNotFoundError" 7 | } 8 | } 9 | 10 | export class UnsupportedDidMethodError extends DidResolutionError { 11 | constructor(message = "Unsupported DID method") { 12 | super(message) 13 | this.name = "UnsupportedDidMethodError" 14 | } 15 | } 16 | 17 | export class InvalidDidUriError extends DidResolutionError { 18 | constructor(message = "Invalid DID URI") { 19 | super(message) 20 | this.name = "InvalidDidUriError" 21 | } 22 | } 23 | 24 | export class InvalidDidControllerError extends DidResolutionError { 25 | constructor(message = "Invalid DID controller") { 26 | super(message) 27 | this.name = "InvalidDidControllerError" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/did/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./did-document" 2 | export * from "./did-resolvers/did-resolver" 3 | export * from "./did-resolvers/get-did-resolver" 4 | export * from "./did-uri" 5 | export * from "./errors" 6 | export * from "./create-did-document" 7 | export * from "./methods/did-key" 8 | export * from "./methods/did-pkh" 9 | export * from "./methods/did-web" 10 | export * from "./resolve-did" 11 | -------------------------------------------------------------------------------- /packages/did/src/schemas/valibot.ts: -------------------------------------------------------------------------------- 1 | import * as v from "valibot" 2 | import { isDidUri } from "../did-uri" 3 | import type { DidUri } from "../did-uri" 4 | 5 | export const didUriSchema = v.custom(isDidUri, "Invalid DID format") 6 | 7 | export const didPkhChainIdSchema = v.pipe( 8 | v.string(), 9 | v.regex(/^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/), 10 | v.transform((val) => val as `${string}:${string}`) 11 | ) 12 | -------------------------------------------------------------------------------- /packages/did/src/schemas/zod.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | import { isDidUri } from "../did-uri" 3 | import type { DidUri } from "../did-uri" 4 | 5 | export const didUriSchema = z.custom(isDidUri, "Invalid DID format") 6 | 7 | export const didPkhChainIdSchema = z 8 | .string() 9 | .regex(/^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/) 10 | .refine((val): val is `${string}:${string}` => true) 11 | -------------------------------------------------------------------------------- /packages/did/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { DidDocument } from "./did-document" 2 | import type { DidUri } from "./did-uri" 3 | 4 | export interface DidUriWithDocument { 5 | did: DidUri 6 | didDocument: DidDocument 7 | } 8 | -------------------------------------------------------------------------------- /packages/did/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/typescript-library.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", "dist"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/did/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig({ 4 | test: { 5 | passWithNoTests: true, 6 | watch: false 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /packages/jwt/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @agentcommercekit/jwt 2 | 3 | ## 0.2.0 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [[`4104ffe`](https://github.com/agentcommercekit/ack/commit/4104ffeae34c7ae972b375871feb09bbe5d27b73)]: 8 | - @agentcommercekit/keys@0.2.0 9 | 10 | ## 0.1.0 11 | 12 | ### Minor Changes 13 | 14 | - Initial release of the Agent Commerce Kit (ACK) TypeScript SDK 15 | 16 | ### Patch Changes 17 | 18 | - Updated dependencies []: 19 | - @agentcommercekit/keys@0.1.0 20 | -------------------------------------------------------------------------------- /packages/jwt/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { config } from "@repo/eslint-config/base" 4 | 5 | export default config({ 6 | root: import.meta.dirname 7 | }) 8 | -------------------------------------------------------------------------------- /packages/jwt/src/create-jwt.test.ts: -------------------------------------------------------------------------------- 1 | import { generateKeypair } from "@agentcommercekit/keys" 2 | import { createJWT as baseCreateJWT } from "did-jwt" 3 | import { beforeEach, describe, expect, it, vi } from "vitest" 4 | import { createJwt } from "./create-jwt" 5 | import { createJwtSigner } from "./signer" 6 | import type { JWTOptions, JWTPayload } from "did-jwt" 7 | 8 | vi.mock("did-jwt", async () => { 9 | const actual = await vi.importActual("did-jwt") 10 | return { 11 | ...actual, 12 | createJWT: vi.fn() 13 | } 14 | }) 15 | 16 | describe("createJWT", () => { 17 | let mockOptions: JWTOptions 18 | const mockPayload: Partial = { 19 | sub: "did:example:123", 20 | iss: "did:example:456" 21 | } 22 | 23 | beforeEach(async () => { 24 | const keypair = await generateKeypair("secp256k1") 25 | mockOptions = { 26 | issuer: "did:example:456", 27 | signer: createJwtSigner(keypair) 28 | } 29 | }) 30 | 31 | it("should create a valid JWT when baseCreateJWT returns a valid JWT string", async () => { 32 | const expectedJwt = 33 | "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6ZXhhbXBsZTo0NTYifQ.sig" 34 | 35 | vi.mocked(baseCreateJWT).mockResolvedValueOnce(expectedJwt) 36 | 37 | const result = await createJwt(mockPayload, mockOptions) 38 | 39 | expect(result).toBe(expectedJwt) 40 | expect(baseCreateJWT).toHaveBeenCalledWith(mockPayload, mockOptions, { 41 | alg: "ES256K" 42 | }) 43 | }) 44 | 45 | it("should throw an error when baseCreateJWT returns an invalid JWT string", async () => { 46 | const invalidJwt = "not-a-valid-jwt" 47 | 48 | vi.mocked(baseCreateJWT).mockResolvedValueOnce(invalidJwt) 49 | 50 | await expect(createJwt(mockPayload, mockOptions)).rejects.toThrow( 51 | "Failed to create JWT" 52 | ) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /packages/jwt/src/create-jwt.ts: -------------------------------------------------------------------------------- 1 | import { createJWT as baseCreateJWT } from "did-jwt" 2 | import { resolveJwtAlgorithm } from "./jwt-algorithm" 3 | import { isJwtString } from "./jwt-string" 4 | import type { JwtAlgorithm } from "./jwt-algorithm" 5 | import type { JwtString } from "./jwt-string" 6 | import type { JWTHeader, JWTOptions, JWTPayload } from "did-jwt" 7 | 8 | /** 9 | * Allow alternative names for the algorithm (adds `secp256k1` and `Ed25519`, 10 | * which map to `ES256K` and `EdDSA` respectively) 11 | */ 12 | type JwtHeader = Omit & { 13 | alg: JwtAlgorithm 14 | } 15 | 16 | /** 17 | * Create a JWT 18 | * 19 | * @param payload - The payload to create the JWT from 20 | * @param options - The options to create the JWT from 21 | * @param header - Optional header overrides 22 | * @param header.alg - The algorithm to use for the JWT. Accepts `secp256k1` and 23 | * `Ed25519` as aliases for `ES256K` and `EdDSA` respectively. Defaults to 24 | * `ES256K`. 25 | * @returns The JWT 26 | */ 27 | export async function createJwt( 28 | payload: Partial, 29 | options: JWTOptions, 30 | { alg = "ES256K", ...header }: Partial = {} 31 | ): Promise { 32 | const result = await baseCreateJWT(payload, options, { 33 | ...header, 34 | alg: resolveJwtAlgorithm(alg) 35 | }) 36 | 37 | if (!isJwtString(result)) { 38 | throw new Error("Failed to create JWT") 39 | } 40 | 41 | return result 42 | } 43 | -------------------------------------------------------------------------------- /packages/jwt/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./jwt-algorithm" 2 | export * from "./jwt-string" 3 | export * from "./create-jwt" 4 | export * from "./signer" 5 | 6 | export { 7 | verifyJWT as verifyJwt, 8 | type JWTVerified as JwtVerified 9 | } from "did-jwt" 10 | -------------------------------------------------------------------------------- /packages/jwt/src/jwt-algorithm.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest" 2 | import { 3 | isJwtAlgorithm, 4 | jwtAlgorithms, 5 | resolveJwtAlgorithm 6 | } from "./jwt-algorithm" 7 | 8 | describe("isJwtAlgorithm", () => { 9 | it("returns true for valid JWT algorithms", () => { 10 | for (const algorithm of jwtAlgorithms) { 11 | expect(isJwtAlgorithm(algorithm)).toBe(true) 12 | } 13 | }) 14 | 15 | it("returns false for invalid JWT algorithms", () => { 16 | expect(isJwtAlgorithm("invalid")).toBe(false) 17 | }) 18 | }) 19 | 20 | describe("resolveJwtAlgorithm", () => { 21 | it("returns the correct JWT algorithm", () => { 22 | expect(resolveJwtAlgorithm("ES256K")).toBe("ES256K") 23 | }) 24 | 25 | it("returns the correct aliased algorithm for secp256k1", () => { 26 | expect(resolveJwtAlgorithm("secp256k1")).toBe("ES256K") 27 | }) 28 | 29 | it("returns the correct aliased algorithm for Ed25519", () => { 30 | expect(resolveJwtAlgorithm("Ed25519")).toBe("EdDSA") 31 | }) 32 | 33 | it("throws an error for an unsupported algorithm", () => { 34 | expect(() => resolveJwtAlgorithm("invalid")).toThrow( 35 | "Unsupported algorithm: 'invalid'" 36 | ) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /packages/jwt/src/jwt-algorithm.ts: -------------------------------------------------------------------------------- 1 | import { keypairAlgorithms } from "@agentcommercekit/keys" 2 | 3 | /** 4 | * The base algorithms supported by the JWT library 5 | */ 6 | export const strictJwtAlgorithms = [ 7 | "ES256K", 8 | "ES256K-R", 9 | "Ed25519", 10 | "EdDSA" 11 | ] as const 12 | export type StrictJwtAlgorithm = (typeof strictJwtAlgorithms)[number] 13 | 14 | /** 15 | * Allow alternative names for the algorithm (adds `secp256k1` and `Ed25519`, 16 | * which map to `ES256K` and `EdDSA` respectively) 17 | */ 18 | export const jwtAlgorithms = [ 19 | ...strictJwtAlgorithms, 20 | ...keypairAlgorithms 21 | ] as const 22 | export type JwtAlgorithm = (typeof jwtAlgorithms)[number] 23 | 24 | /** 25 | * Check if an algorithm is a valid JWT algorithm 26 | * @param algorithm - The algorithm to check 27 | * @returns `true` if the algorithm is a valid JWT algorithm, `false` otherwise 28 | */ 29 | export function isJwtAlgorithm(algorithm: unknown): algorithm is JwtAlgorithm { 30 | return ( 31 | typeof algorithm === "string" && 32 | (jwtAlgorithms as readonly string[]).includes(algorithm) 33 | ) 34 | } 35 | 36 | /** 37 | * Resolve the JWT algorithm to the base algorithm 38 | */ 39 | export function resolveJwtAlgorithm(algorithm: unknown): JwtAlgorithm { 40 | if (!isJwtAlgorithm(algorithm)) { 41 | throw new Error(`Unsupported algorithm: '${algorithm}'`) 42 | } 43 | 44 | if (algorithm === "secp256k1") { 45 | return "ES256K" 46 | } else if (algorithm === "Ed25519") { 47 | return "EdDSA" 48 | } 49 | 50 | return algorithm 51 | } 52 | -------------------------------------------------------------------------------- /packages/jwt/src/jwt-string.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest" 2 | import { isJwtString } from "./jwt-string" 3 | 4 | describe("isJwtString", () => { 5 | it("should return true for a valid JWT string", () => { 6 | expect( 7 | isJwtString( 8 | // eslint-disable-next-line @cspell/spellchecker 9 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" 10 | ) 11 | ).toBe(true) 12 | }) 13 | 14 | it("should return false for an invalid JWT string", () => { 15 | expect(isJwtString("invalid")).toBe(false) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/jwt/src/jwt-string.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A JWT String, which at a minimum has 3 segments 3 | */ 4 | export type JwtString = `${string}.${string}.${string}` 5 | 6 | /** 7 | * Checks if a string is formatted correctly as a JWT string. This 8 | * does not verify the JWT's integrity, only that it is formatted correctly. 9 | * 10 | * @param value - The value to check 11 | * @returns `true` if the value is a valid JWT string, `false` otherwise 12 | */ 13 | export function isJwtString(value: unknown): value is JwtString { 14 | return ( 15 | typeof value === "string" && 16 | /^[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+$/.test(value) 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /packages/jwt/src/schemas/valibot.ts: -------------------------------------------------------------------------------- 1 | import * as v from "valibot" 2 | import type { JwtString } from "../jwt-string" 3 | 4 | export const jwtStringSchema = v.pipe( 5 | v.string(), 6 | v.regex(/^[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+$/), 7 | v.transform((input) => input as JwtString) 8 | ) 9 | -------------------------------------------------------------------------------- /packages/jwt/src/schemas/zod.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | import { isJwtString } from "../jwt-string" 3 | 4 | export const jwtStringSchema = z 5 | .string() 6 | .regex(/^[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+$/) 7 | .refine((input) => isJwtString(input), { 8 | message: "Invalid JWT string" 9 | }) 10 | -------------------------------------------------------------------------------- /packages/jwt/src/signer.ts: -------------------------------------------------------------------------------- 1 | import { ES256KSigner, EdDSASigner } from "did-jwt" 2 | import type { Keypair } from "@agentcommercekit/keys" 3 | import type { Signer } from "did-jwt" 4 | 5 | export type JwtSigner = Signer 6 | 7 | /** 8 | * Create a JWT-compatible signer from a Keypair 9 | * 10 | * This function creates the appropriate signer based on the key pair's algorithm: 11 | * - secp256k1 -> ES256KSigner 12 | * - Ed25519 -> EdDSASigner 13 | * 14 | * @param keypair - The Keypair to create a signer from 15 | * @returns A JWT-compatible signer 16 | */ 17 | export function createJwtSigner(keypair: Keypair): JwtSigner { 18 | switch (keypair.algorithm) { 19 | case "secp256k1": 20 | return ES256KSigner(keypair.privateKey) 21 | case "Ed25519": 22 | return EdDSASigner(keypair.privateKey) 23 | default: 24 | throw new Error("Unsupported algorithm", keypair.algorithm) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/jwt/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/typescript-library.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", "dist"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/jwt/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig({ 4 | test: { 5 | passWithNoTests: true, 6 | watch: false 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /packages/keys/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @agentcommercekit/keys 2 | 3 | ## 0.2.0 4 | 5 | ### Minor Changes 6 | 7 | - [#3](https://github.com/agentcommercekit/ack/pull/3) [`4104ffe`](https://github.com/agentcommercekit/ack/commit/4104ffeae34c7ae972b375871feb09bbe5d27b73) Thanks [@venables](https://github.com/venables)! - - Upgrade legacy public key formats to use multibase in DID Documents 8 | - Update base64 methods to be explicit that they use `base64url` encoding 9 | - Simplify interface for public key encoding methods 10 | 11 | ## 0.1.0 12 | 13 | ### Minor Changes 14 | 15 | - Initial release of the Agent Commerce Kit (ACK) TypeScript SDK 16 | -------------------------------------------------------------------------------- /packages/keys/README.md: -------------------------------------------------------------------------------- 1 | # @agentcommercekit/keys 2 | 3 | Methods for dealing with cryptographic keys on multiple curves (secp256k1, Ed25519). 4 | 5 | This package is part of the [Agent Commerce Kit](https://www.agentcommercekit.com). 6 | 7 | ## Installation 8 | 9 | ```sh 10 | npm i @agentcommercekit/keys 11 | # or 12 | pnpm add @agentcommercekit/keys 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```ts 18 | import { 19 | generateKeypair, 20 | keypairToJwk, 21 | encodePublicKeyFromKeypair 22 | } from "@agentcommercekit/keys" 23 | 24 | // Generate and format keypairs 25 | const keypair = await generateKeypair("secp256k1") 26 | const jwkKeypair = keypairToJwk(keypair) 27 | 28 | // Format public keys 29 | const hexPublicKey = encodePublicKeyFromKeypair("hex", keypair) 30 | const jwkPublicKey = encodePublicKeyFromKeypair("jwk", keypair) 31 | const multibasePublicKey = encodePublicKeyFromKeypair("multibase", keypair) 32 | const base58PublicKey = encodePublicKeyFromKeypair("base58", keypair) 33 | ``` 34 | 35 | ## API 36 | 37 | ### Keypair Operations 38 | 39 | - `generateKeypair(algorithm: KeypairAlgorithm, privateKeyBytes?: Uint8Array): Promise` 40 | - `keypairToJwk(keypair: Keypair): PrivateKeyJwk` 41 | - `jwkToKeypair(jwk: PrivateKeyJwk): Keypair` 42 | 43 | ### Public Key Formatting 44 | 45 | - `encodePublicKeyFromKeypair(encoding: T, keypair: Keypair): PublicKeyTypeMap[T]` 46 | - `getCompressedPublicKey(keypair: Keypair): Uint8Array` 47 | 48 | ### Additional Exports 49 | 50 | Encoding utilities are also available via subpath exports: 51 | 52 | ```ts 53 | import { bytesToBase58, base58ToBytes } from "@agentcommercekit/keys/encoding" 54 | ``` 55 | 56 | ## License (MIT) 57 | 58 | Copyright (c) 2025 [Catena Labs, Inc](https://catenalabs.com). 59 | -------------------------------------------------------------------------------- /packages/keys/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { config } from "@repo/eslint-config/base" 4 | 5 | export default [ 6 | ...config({ 7 | root: import.meta.dirname 8 | }), 9 | { 10 | files: ["**/*.test.ts"], 11 | rules: { 12 | "@cspell/spellchecker": "off" 13 | } 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /packages/keys/src/curves/ed25519.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest" 2 | import { generateKeypair } from "./ed25519" 3 | import { base58ToBytes } from "../encoding/base58" 4 | 5 | describe("Ed25519", () => { 6 | describe("generateKeypair()", () => { 7 | test("generates a valid Keypair", async () => { 8 | const keypair = await generateKeypair() 9 | 10 | expect(keypair).toBeDefined() 11 | expect(keypair.privateKey).toBeInstanceOf(Uint8Array) 12 | expect(keypair.publicKey).toBeInstanceOf(Uint8Array) 13 | expect(keypair.algorithm).toBe("Ed25519") 14 | }) 15 | 16 | test("generates a unique `Keypair`s", async () => { 17 | const keypair1 = await generateKeypair() 18 | const keypair2 = await generateKeypair() 19 | 20 | expect(keypair1.privateKey).not.toEqual(keypair2.privateKey) 21 | expect(keypair1.publicKey).not.toEqual(keypair2.publicKey) 22 | expect(keypair1.algorithm).toBe("Ed25519") 23 | expect(keypair2.algorithm).toBe("Ed25519") 24 | }) 25 | }) 26 | 27 | test("generates keypair from valid private key", async () => { 28 | // Using a Solana-like base58 private key 29 | const privateKeyBase58 = "4dmKkXNHJmR1XNXbQwJhUT8Vo3PjU1GcJmZkQFRW3aqb" 30 | const privateKeyBytes = base58ToBytes(privateKeyBase58) 31 | 32 | const keypair = await generateKeypair(privateKeyBytes) 33 | 34 | expect(keypair).toBeDefined() 35 | expect(keypair.privateKey).toEqual(privateKeyBytes) 36 | expect(keypair.publicKey).toBeInstanceOf(Uint8Array) 37 | expect(keypair.algorithm).toBe("Ed25519") 38 | }) 39 | 40 | test("throws error for invalid private key format", async () => { 41 | const invalidPrivateKey = new Uint8Array([1, 2, 3]) // Too short for Ed25519 42 | await expect(generateKeypair(invalidPrivateKey)).rejects.toThrow() 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /packages/keys/src/curves/ed25519.ts: -------------------------------------------------------------------------------- 1 | import { ed25519 } from "@noble/curves/ed25519" 2 | import type { Keypair } from "../types" 3 | 4 | /** 5 | * Generate a random private key using the Ed25519 curve 6 | */ 7 | function generatePrivateKeyBytes(): Promise { 8 | return Promise.resolve(ed25519.utils.randomPrivateKey()) 9 | } 10 | 11 | /** 12 | * Generate a keypair 13 | */ 14 | export async function generateKeypair( 15 | privateKeyBytes?: Uint8Array 16 | ): Promise { 17 | privateKeyBytes ??= await generatePrivateKeyBytes() 18 | const publicKeyBytes = ed25519.getPublicKey(privateKeyBytes) 19 | 20 | return Promise.resolve({ 21 | publicKey: publicKeyBytes, 22 | privateKey: privateKeyBytes, 23 | algorithm: "Ed25519" 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /packages/keys/src/curves/secp256k1.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest" 2 | import { generateKeypair } from "./secp256k1" 3 | import { hexStringToBytes } from "../encoding/hex" 4 | 5 | describe("secp256k1", () => { 6 | describe("generateKeypair()", () => { 7 | test("generates valid Keypair", async () => { 8 | const keypair = await generateKeypair() 9 | 10 | expect(keypair).toBeDefined() 11 | expect(keypair.privateKey).toBeInstanceOf(Uint8Array) 12 | expect(keypair.publicKey).toBeInstanceOf(Uint8Array) 13 | expect(keypair.algorithm).toBe("secp256k1") 14 | }) 15 | 16 | test("generates a unique `Keypair`s", async () => { 17 | const keypair1 = await generateKeypair() 18 | const keypair2 = await generateKeypair() 19 | 20 | expect(keypair1.privateKey).not.toEqual(keypair2.privateKey) 21 | expect(keypair1.publicKey).not.toEqual(keypair2.publicKey) 22 | expect(keypair1.algorithm).toBe("secp256k1") 23 | expect(keypair2.algorithm).toBe("secp256k1") 24 | }) 25 | }) 26 | 27 | test("generates a Keypair from valid private key", async () => { 28 | const privateKeyBytes = hexStringToBytes( 29 | "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" 30 | ) 31 | 32 | const keypair = await generateKeypair(privateKeyBytes) 33 | 34 | expect(keypair).toBeDefined() 35 | expect(keypair.privateKey).toEqual(privateKeyBytes) 36 | expect(keypair.publicKey).toBeInstanceOf(Uint8Array) 37 | expect(keypair.algorithm).toBe("secp256k1") 38 | }) 39 | 40 | test("throws an error for invalid private key format", async () => { 41 | const invalidPrivateKey = new Uint8Array([1, 2, 3]) // Too short for secp256k1 42 | await expect(generateKeypair(invalidPrivateKey)).rejects.toThrow() 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /packages/keys/src/curves/secp256k1.ts: -------------------------------------------------------------------------------- 1 | import { secp256k1 } from "@noble/curves/secp256k1" 2 | import type { Keypair } from "../types" 3 | 4 | /** 5 | * Generate a random private key using the secp256k1 curve 6 | */ 7 | function generatePrivateKeyBytes(): Promise { 8 | return Promise.resolve(secp256k1.utils.randomPrivateKey()) 9 | } 10 | 11 | /** 12 | * Convert an uncompressed public key to compressed format 13 | * @param publicKey - The uncompressed public key (65 bytes) 14 | * @returns The compressed public key (33 bytes) 15 | */ 16 | export function compressPublicKey(keypair: Keypair): Uint8Array { 17 | return secp256k1.getPublicKey(keypair.privateKey, true) 18 | } 19 | 20 | /** 21 | * Generate a keypair 22 | */ 23 | export async function generateKeypair( 24 | privateKeyBytes?: Uint8Array 25 | ): Promise { 26 | privateKeyBytes ??= await generatePrivateKeyBytes() 27 | const publicKeyBytes = secp256k1.getPublicKey(privateKeyBytes, false) 28 | 29 | return Promise.resolve({ 30 | publicKey: publicKeyBytes, 31 | privateKey: privateKeyBytes, 32 | algorithm: "secp256k1" 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /packages/keys/src/encoding/base58.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest" 2 | import { 3 | base58ToBytes, 4 | base58btcToBytes, 5 | bytesToBase58, 6 | bytesToBase58btc 7 | } from "./base58" 8 | 9 | describe("base58btc encoding and decoding (used for DID:key)", () => { 10 | test("converts bytes to base58btc string", () => { 11 | const bytes = new Uint8Array([1, 2, 3, 4]) 12 | const base58 = bytesToBase58btc(bytes) 13 | expect(base58).toBe("2VfUX") 14 | }) 15 | 16 | test("converts base58btc string to bytes", () => { 17 | const base58 = "2VfUX" 18 | const bytes = base58btcToBytes(base58) 19 | expect(bytes).toEqual(new Uint8Array([1, 2, 3, 4])) 20 | }) 21 | 22 | test("roundtrip base58btc encoding", () => { 23 | const original = new Uint8Array([1, 2, 3, 4]) 24 | const base58 = bytesToBase58btc(original) 25 | const bytes = base58btcToBytes(base58) 26 | expect(bytes).toEqual(original) 27 | }) 28 | }) 29 | 30 | describe("Solana base58 encoding and decoding (used for Solana addresses)", () => { 31 | test("converts bytes to Solana base58 string", () => { 32 | const bytes = new Uint8Array([1, 2, 3, 4]) 33 | const base58 = bytesToBase58(bytes) 34 | expect(base58).toBe("2VfUX") 35 | }) 36 | 37 | test("converts Solana base58 string to bytes", () => { 38 | const base58 = "2VfUX" 39 | const bytes = base58ToBytes(base58) 40 | expect(bytes).toEqual(new Uint8Array([1, 2, 3, 4])) 41 | }) 42 | 43 | test("roundtrip Solana base58 encoding", () => { 44 | const original = new Uint8Array([1, 2, 3, 4]) 45 | const base58 = bytesToBase58(original) 46 | const bytes = base58ToBytes(base58) 47 | expect(bytes).toEqual(original) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /packages/keys/src/encoding/base58.ts: -------------------------------------------------------------------------------- 1 | import { getBase58Codec } from "@solana/codecs-strings" 2 | import { fromString } from "uint8arrays/from-string" 3 | import { toString } from "uint8arrays/to-string" 4 | 5 | /** 6 | * Convert bytes to a base58btc string (used for DID:key URIs) 7 | */ 8 | export function bytesToBase58btc(bytes: Uint8Array): string { 9 | return toString(bytes, "base58btc") 10 | } 11 | 12 | /** 13 | * Convert a base58btc string to bytes (used for DID:key URIs) 14 | */ 15 | export function base58btcToBytes(base58: string): Uint8Array { 16 | return fromString(base58, "base58btc") 17 | } 18 | 19 | /** 20 | * Convert bytes to a Solana base58 string (used for Solana addresses) 21 | */ 22 | export function bytesToBase58(bytes: Uint8Array): string { 23 | const codec = getBase58Codec() 24 | return codec.decode(bytes) 25 | } 26 | 27 | /** 28 | * Convert a Solana base58 string to bytes (used for Solana addresses) 29 | */ 30 | export function base58ToBytes(base58: string): Uint8Array { 31 | const codec = getBase58Codec() 32 | return new Uint8Array(codec.encode(base58)) 33 | } 34 | 35 | /** 36 | * Check if a string is valid base58 encoded 37 | */ 38 | export function isBase58(str: unknown): str is string { 39 | if (typeof str !== "string") { 40 | return false 41 | } 42 | try { 43 | // Try to decode it - if it succeeds, it's valid base58 44 | const codec = getBase58Codec() 45 | codec.encode(str) 46 | return true 47 | } catch { 48 | return false 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/keys/src/encoding/base64.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest" 2 | import { base64urlToBytes, bytesToBase64url, isBase64url } from "./base64" 3 | 4 | describe("base64 encoding and decoding", () => { 5 | test("converts bytes to base64 string", () => { 6 | const bytes = new Uint8Array([1, 2, 3, 4]) 7 | const base64 = bytesToBase64url(bytes) 8 | expect(base64).toBe("AQIDBA") 9 | }) 10 | 11 | test("converts base64 string to bytes", () => { 12 | const base64 = "AQIDBA" 13 | const bytes = base64urlToBytes(base64) 14 | expect(bytes).toEqual(new Uint8Array([1, 2, 3, 4])) 15 | }) 16 | 17 | test("roundtrip base64 encoding", () => { 18 | const original = new Uint8Array([1, 2, 3, 4]) 19 | const base64 = bytesToBase64url(original) 20 | const bytes = base64urlToBytes(base64) 21 | expect(bytes).toEqual(original) 22 | }) 23 | }) 24 | 25 | describe("isBase64", () => { 26 | test("returns true for valid base64 strings", () => { 27 | expect(isBase64url("AQIDBA")).toBe(true) 28 | expect(isBase64url("SGVsbG8sIFdvcmxkIQ")).toBe(true) // "Hello, World!" 29 | }) 30 | 31 | test("returns false for invalid base64 strings", () => { 32 | expect(isBase64url("not base64")).toBe(false) 33 | expect(isBase64url("AQIDBA!")).toBe(false) 34 | expect(isBase64url(123)).toBe(false) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /packages/keys/src/encoding/base64.ts: -------------------------------------------------------------------------------- 1 | import { fromString } from "uint8arrays/from-string" 2 | import { toString } from "uint8arrays/to-string" 3 | 4 | /** 5 | * Convert bytes to a base64url string 6 | */ 7 | export function bytesToBase64url(bytes: Uint8Array): string { 8 | return toString(bytes, "base64url") 9 | } 10 | 11 | /** 12 | * Convert a base64url string to bytes 13 | */ 14 | export function base64urlToBytes(base64: string): Uint8Array { 15 | return fromString(base64, "base64url") 16 | } 17 | 18 | /** 19 | * Check if a string is valid base64url encoded 20 | */ 21 | export function isBase64url(str: unknown): str is string { 22 | if (typeof str !== "string") { 23 | return false 24 | } 25 | try { 26 | // Try to decode it - if it succeeds, it's valid base64url 27 | fromString(str, "base64url") 28 | return true 29 | } catch { 30 | return false 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/keys/src/encoding/hex.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest" 2 | import { bytesToHexString, hexStringToBytes, isHexString } from "./hex" 3 | 4 | describe("hex encoding and decoding", () => { 5 | test("converts bytes to hex string", () => { 6 | const bytes = new Uint8Array([1, 2, 3, 4]) 7 | const hex = bytesToHexString(bytes) 8 | expect(hex).toBe("01020304") 9 | }) 10 | 11 | test("converts hex string with 0x prefix to bytes", () => { 12 | const hex = "0x01020304" 13 | const bytes = hexStringToBytes(hex) 14 | expect(bytes).toEqual(new Uint8Array([1, 2, 3, 4])) 15 | }) 16 | 17 | test("handles hex strings in caps", () => { 18 | const hex = "0XABCDEF" 19 | const bytes = hexStringToBytes(hex) 20 | expect(bytes).toEqual(new Uint8Array([171, 205, 239])) 21 | }) 22 | 23 | test("converts hex string without 0x prefix to bytes", () => { 24 | const hex = "01020304" 25 | const bytes = hexStringToBytes(hex) 26 | expect(bytes).toEqual(new Uint8Array([1, 2, 3, 4])) 27 | }) 28 | 29 | test("roundtrip hex encoding", () => { 30 | const original = new Uint8Array([1, 2, 3, 4]) 31 | const hex = bytesToHexString(original) 32 | const bytes = hexStringToBytes(hex) 33 | expect(bytes).toEqual(original) 34 | }) 35 | }) 36 | 37 | describe("isHexString", () => { 38 | test("returns true for valid hex strings with 0x prefix", () => { 39 | expect(isHexString("0x1234567890abcdef")).toBe(true) 40 | }) 41 | 42 | test("returns true for valid hex strings without 0x prefix", () => { 43 | expect(isHexString("1234567890abcdef")).toBe(true) 44 | }) 45 | 46 | test("returns false for invalid hex strings", () => { 47 | expect(isHexString("0x1234567890abcdefg")).toBe(false) 48 | expect(isHexString("not hex")).toBe(false) 49 | expect(isHexString(123)).toBe(false) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /packages/keys/src/encoding/hex.ts: -------------------------------------------------------------------------------- 1 | import { fromString } from "uint8arrays/from-string" 2 | import { toString } from "uint8arrays/to-string" 3 | 4 | /** 5 | * Convert bytes to a hex string 6 | * 7 | * @example 8 | * ```ts 9 | * bytesToHexString(new Uint8Array([1, 2, 3, 4])) // "01020304" 10 | * ``` 11 | */ 12 | export function bytesToHexString(bytes: Uint8Array): string { 13 | return toString(bytes, "base16") 14 | } 15 | 16 | /** 17 | * Convert a hex string to bytes 18 | * Accepts both with and without 0x prefix 19 | * 20 | * @example 21 | * ```ts 22 | * hexStringToBytes("0x1234567890abcdef") // Uint8Array([1, 2, 3, 4]) 23 | * hexStringToBytes("1234567890abcdef") // Uint8Array([1, 2, 3, 4]) 24 | * ``` 25 | */ 26 | export function hexStringToBytes(hex: string): Uint8Array { 27 | const hexWithoutPrefix = hex.toLowerCase().startsWith("0x") 28 | ? hex.slice(2) 29 | : hex 30 | return fromString(hexWithoutPrefix.toLowerCase(), "base16") 31 | } 32 | 33 | /** 34 | * Check if a string is a hex string. This method accepts both with and without 35 | * 0x prefix. 36 | * 37 | * @example 38 | * ```ts 39 | * isHexString("0x1234567890abcdef") // true 40 | * isHexString("1234567890abcdef") // true 41 | * isHexString("0x") // true 42 | * isHexString("0x1234567890abcdefg") // false 43 | * ``` 44 | */ 45 | export function isHexString(value: unknown): value is string { 46 | if (typeof value !== "string") { 47 | return false 48 | } 49 | 50 | const hexWithoutPrefix = value.startsWith("0x") ? value.slice(2) : value 51 | return /^[0-9A-Fa-f]+$/.test(hexWithoutPrefix) 52 | } 53 | -------------------------------------------------------------------------------- /packages/keys/src/encoding/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./base58" 2 | export * from "./base64" 3 | export * from "./hex" 4 | export * from "./jwk" 5 | export * from "./multibase" 6 | -------------------------------------------------------------------------------- /packages/keys/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./keypair" 2 | export * from "./public-key" 3 | export * from "./types" 4 | -------------------------------------------------------------------------------- /packages/keys/src/keypair.ts: -------------------------------------------------------------------------------- 1 | import { generateKeypair as ed25519 } from "./curves/ed25519" 2 | import { generateKeypair as secp256k1 } from "./curves/secp256k1" 3 | import { base64urlToBytes, bytesToBase64url } from "./encoding/base64" 4 | import { bytesToJwk, jwkToBytes } from "./encoding/jwk" 5 | import type { PrivateKeyJwk } from "./encoding/jwk" 6 | import type { Keypair, KeypairAlgorithm } from "./types" 7 | 8 | /** 9 | * Generate a Keypair for a given algorithm 10 | * 11 | * @param algorithm - The algorithm to use (`secp256k1` or `Ed25519`) 12 | * @param privateKeyBytes - The private key bytes to use (optional) 13 | * @returns A Keypair 14 | */ 15 | export async function generateKeypair( 16 | algorithm: KeypairAlgorithm, 17 | privateKeyBytes?: Uint8Array 18 | ): Promise { 19 | if (algorithm === "secp256k1") { 20 | return secp256k1(privateKeyBytes) 21 | } 22 | 23 | return ed25519(privateKeyBytes) 24 | } 25 | 26 | /** 27 | * Convert a Keypair to a JWK format 28 | * 29 | * @param keypair - The Keypair to convert 30 | * @returns A JSON Web Key representation of the Keypair 31 | */ 32 | export function keypairToJwk(keypair: Keypair): PrivateKeyJwk { 33 | const publicKeyJwk = bytesToJwk(keypair.publicKey, keypair.algorithm) 34 | return { 35 | ...publicKeyJwk, 36 | d: bytesToBase64url(keypair.privateKey) 37 | } 38 | } 39 | 40 | /** 41 | * Convert a JWK to a Keypair 42 | * 43 | * @param jwk - The JWK to convert 44 | * @returns A Keypair 45 | */ 46 | export function jwkToKeypair(jwk: PrivateKeyJwk): Keypair { 47 | return { 48 | publicKey: jwkToBytes(jwk), 49 | privateKey: base64urlToBytes(jwk.d), 50 | algorithm: jwk.crv 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/keys/src/types.ts: -------------------------------------------------------------------------------- 1 | export const keypairAlgorithms = ["secp256k1", "Ed25519"] as const 2 | export type KeypairAlgorithm = (typeof keypairAlgorithms)[number] 3 | 4 | export interface Keypair { 5 | publicKey: Uint8Array 6 | privateKey: Uint8Array 7 | algorithm: KeypairAlgorithm 8 | } 9 | 10 | export interface KeypairBase58 { 11 | publicKey: string 12 | privateKey: string 13 | algorithm: KeypairAlgorithm 14 | } 15 | -------------------------------------------------------------------------------- /packages/keys/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/typescript-library.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", "dist"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/keys/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig({ 4 | test: { 5 | passWithNoTests: true, 6 | watch: false 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /packages/vc/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @agentcommercekit/vc 2 | 3 | ## 0.2.1 4 | 5 | ### Patch Changes 6 | 7 | - [#4](https://github.com/agentcommercekit/ack/pull/4) [`5b1c8b1`](https://github.com/agentcommercekit/ack/commit/5b1c8b1b8105e781f977379f019f96efbcab3e27) Thanks [@domleboss97](https://github.com/domleboss97)! - Define explicit W3CCredential type to resolve TS compiler issues with inferred return types 8 | 9 | ## 0.2.0 10 | 11 | ### Patch Changes 12 | 13 | - Updated dependencies [[`4104ffe`](https://github.com/agentcommercekit/ack/commit/4104ffeae34c7ae972b375871feb09bbe5d27b73)]: 14 | - @agentcommercekit/keys@0.2.0 15 | - @agentcommercekit/did@0.2.0 16 | - @agentcommercekit/jwt@0.2.0 17 | 18 | ## 0.1.0 19 | 20 | ### Minor Changes 21 | 22 | - Initial release of the Agent Commerce Kit (ACK) TypeScript SDK 23 | 24 | ### Patch Changes 25 | 26 | - Updated dependencies []: 27 | - @agentcommercekit/did@0.1.0 28 | - @agentcommercekit/jwt@0.1.0 29 | - @agentcommercekit/keys@0.1.0 30 | -------------------------------------------------------------------------------- /packages/vc/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { config } from "@repo/eslint-config/base" 4 | 5 | export default config({ 6 | root: import.meta.dirname 7 | }) 8 | -------------------------------------------------------------------------------- /packages/vc/src/create-credential.ts: -------------------------------------------------------------------------------- 1 | import type { W3CCredential } from "./types" 2 | 3 | type CreateCredentialParams = { 4 | /** 5 | * The ID of the credential. 6 | */ 7 | id?: string 8 | /** 9 | * The type of the credential, in addition to "VerifiableCredential". 10 | */ 11 | type?: string | string[] 12 | /** 13 | * The issuer of the credential. 14 | */ 15 | issuer: string 16 | /** 17 | * The subject of the credential. 18 | */ 19 | subject: string 20 | /** 21 | * The attestation of the credential. 22 | */ 23 | attestation?: Omit 24 | /** 25 | * The issuance date of the credential. 26 | */ 27 | issuanceDate?: Date 28 | /** 29 | * The expiration date of the credential. 30 | */ 31 | expirationDate?: Date 32 | } 33 | 34 | /** 35 | * Creates a new, unsigned Verifiable Credential. 36 | * 37 | * @param params - The {@link CreateCredentialParams} to build the credential from 38 | * @returns A new, unsigned {@link W3CCredential} 39 | */ 40 | export function createCredential({ 41 | id, 42 | type, 43 | issuer, 44 | subject, 45 | attestation, 46 | issuanceDate, 47 | expirationDate 48 | }: CreateCredentialParams): T { 49 | const credentialTypes = [type] 50 | .flat() 51 | .filter((t): t is string => !!t && t !== "VerifiableCredential") 52 | 53 | return { 54 | "@context": ["https://www.w3.org/2018/credentials/v1"], 55 | type: ["VerifiableCredential", ...credentialTypes], 56 | id, 57 | issuer: { id: issuer }, 58 | credentialSubject: { id: subject, ...attestation }, 59 | issuanceDate: (issuanceDate ?? new Date()).toISOString(), 60 | expirationDate: expirationDate?.toISOString() 61 | } as T 62 | } 63 | -------------------------------------------------------------------------------- /packages/vc/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./create-credential" 2 | export * from "./is-credential" 3 | export * from "./sign-credential" 4 | export * from "./types" 5 | export * from "./revocation/make-revocable" 6 | export * from "./revocation/status-list-credential" 7 | export * from "./revocation/types" 8 | export * from "./verification/errors" 9 | export * from "./verification/is-expired" 10 | export * from "./verification/is-revoked" 11 | export * from "./verification/types" 12 | export * from "./verification/parse-jwt-credential" 13 | export * from "./verification/verify-parsed-credential" 14 | export * from "./verification/verify-proof" 15 | -------------------------------------------------------------------------------- /packages/vc/src/is-credential.ts: -------------------------------------------------------------------------------- 1 | import * as v from "valibot" 2 | import { credentialSchema } from "./schemas/valibot" 3 | import type { W3CCredential } from "did-jwt-vc" 4 | 5 | /** 6 | * Check if a value is a valid W3C credential 7 | * 8 | * @param credential - The value to check 9 | * @returns `true` if the value is a valid W3C credential, `false` otherwise 10 | */ 11 | export function isCredential(credential: unknown): credential is W3CCredential { 12 | return v.is(credentialSchema, credential) 13 | } 14 | -------------------------------------------------------------------------------- /packages/vc/src/revocation/is-status-list-credential.ts: -------------------------------------------------------------------------------- 1 | import * as v from "valibot" 2 | import { isCredential } from "../is-credential" 3 | import { statusList2021ClaimSchema } from "../schemas/valibot" 4 | import type { StatusList2021Credential } from "./types" 5 | import type { CredentialSubject } from "../types" 6 | 7 | function isStatusList2021Claim( 8 | credentialSubject: CredentialSubject 9 | ): credentialSubject is v.InferOutput { 10 | return v.is(statusList2021ClaimSchema, credentialSubject) 11 | } 12 | 13 | /** 14 | * Check if a credential is a status list credential 15 | * 16 | * @param credential - The credential to check 17 | * @returns `true` if the credential is a status list credential, `false` otherwise 18 | */ 19 | export function isStatusListCredential( 20 | credential: unknown 21 | ): credential is StatusList2021Credential { 22 | if (!isCredential(credential)) { 23 | return false 24 | } 25 | return isStatusList2021Claim(credential.credentialSubject) 26 | } 27 | -------------------------------------------------------------------------------- /packages/vc/src/revocation/make-revocable.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest" 2 | import { makeRevocable } from "./make-revocable" 3 | import type { W3CCredential } from "../types" 4 | 5 | describe("makeRevocable", () => { 6 | const mockCredential: W3CCredential = { 7 | "@context": ["https://www.w3.org/2018/credentials/v1"], 8 | type: ["VerifiableCredential"], 9 | issuer: { id: "did:example:issuer" }, 10 | credentialSubject: { id: "did:example:subject" }, 11 | issuanceDate: new Date().toISOString() 12 | } 13 | 14 | const mockStatusListId = "https://example.com/status/1" 15 | const mockStatusListUrl = "https://example.com/status/1" 16 | const mockStatusListIndex = 123 17 | 18 | it("should add revocation status to credential", () => { 19 | const revocableCredential = makeRevocable(mockCredential, { 20 | id: mockStatusListId, 21 | statusListIndex: mockStatusListIndex, 22 | statusListUrl: mockStatusListUrl 23 | }) 24 | 25 | expect(revocableCredential.credentialStatus).toEqual({ 26 | id: mockStatusListId, 27 | type: "StatusList2021Entry", 28 | statusPurpose: "revocation", 29 | statusListIndex: mockStatusListIndex.toString(), 30 | statusListCredential: mockStatusListUrl 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /packages/vc/src/revocation/make-revocable.ts: -------------------------------------------------------------------------------- 1 | import type { W3CCredential } from "../types" 2 | import type { Revocable } from "./types" 3 | 4 | type RevocationOptions = { 5 | /** 6 | * The ID of the status list entry. 7 | */ 8 | id: string 9 | /** 10 | * The index of the status list entry. 11 | */ 12 | statusListIndex: number 13 | /** 14 | * The URL of the status list. 15 | */ 16 | statusListUrl: string 17 | } 18 | 19 | /** 20 | * Makes a credential revocable by adding a status list entry to it. 21 | * 22 | * @param credential - The credential to make revocable. 23 | * @param options - The {@link RevocationOptions} to use 24 | * @returns A {@link Revocable} credential 25 | */ 26 | export function makeRevocable( 27 | credential: T, 28 | { id, statusListIndex, statusListUrl }: RevocationOptions 29 | ): Revocable { 30 | return { 31 | ...credential, 32 | credentialStatus: { 33 | id, 34 | type: "StatusList2021Entry", 35 | statusPurpose: "revocation", 36 | statusListIndex: statusListIndex.toString(), 37 | statusListCredential: statusListUrl 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/vc/src/revocation/status-list-credential.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest" 2 | import { createStatusListCredential } from "./status-list-credential" 3 | 4 | describe("createStatusListCredential", () => { 5 | test("should create a valid status list credential", () => { 6 | const issuer = "did:web:issuer.example:com" 7 | 8 | const params = { 9 | url: "https://example.com/status-list", 10 | encodedList: "mockEncodedList", 11 | issuer 12 | } 13 | 14 | const credential = createStatusListCredential(params) 15 | 16 | expect(credential.type).toContain("StatusList2021Credential") 17 | expect(credential.issuer).toEqual({ id: issuer }) 18 | expect(credential.credentialSubject).toBeDefined() 19 | expect(credential.credentialSubject.type).toBe("StatusList2021") 20 | expect(credential.credentialSubject.statusPurpose).toBe("revocation") 21 | expect(credential.credentialSubject.encodedList).toBe(params.encodedList) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /packages/vc/src/revocation/status-list-credential.ts: -------------------------------------------------------------------------------- 1 | import { createCredential } from "../create-credential" 2 | import type { StatusList2021Credential } from "./types" 3 | 4 | type CreateStatusListCredentialParams = { 5 | /** 6 | * The URL of the status list. 7 | */ 8 | url: string 9 | /** 10 | * The encoded list of the status list. 11 | */ 12 | encodedList: string 13 | /** 14 | * The issuer of the status list credential. 15 | */ 16 | issuer: string 17 | } 18 | 19 | /** 20 | * Generates a status list credential. 21 | * 22 | * @param params - The {@link CreateStatusListCredentialParams} to use 23 | * @returns A {@link StatusList2021Credential} 24 | */ 25 | export function createStatusListCredential({ 26 | url, 27 | encodedList, 28 | issuer 29 | }: CreateStatusListCredentialParams): StatusList2021Credential { 30 | return createCredential({ 31 | id: url, 32 | type: "StatusList2021Credential", 33 | issuer, 34 | subject: `${url}#list`, 35 | attestation: { 36 | type: "StatusList2021", 37 | statusPurpose: "revocation", 38 | encodedList 39 | } 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /packages/vc/src/revocation/types.ts: -------------------------------------------------------------------------------- 1 | import type { statusList2021ClaimSchema } from "../schemas/valibot" 2 | import type { W3CCredential } from "../types" 3 | import type * as v from "valibot" 4 | 5 | type StatusList2021Entry = { 6 | id: string 7 | type: "StatusList2021Entry" 8 | statusPurpose: string 9 | statusListIndex: string 10 | statusListCredential: string 11 | } 12 | 13 | export type StatusList2021Credential = W3CCredential & { 14 | credentialSubject: v.InferOutput 15 | } 16 | 17 | export type Revocable = T & { 18 | credentialStatus: StatusList2021Entry 19 | } 20 | -------------------------------------------------------------------------------- /packages/vc/src/schemas/valibot.ts: -------------------------------------------------------------------------------- 1 | import * as v from "valibot" 2 | import type { W3CCredential } from "../types" 3 | 4 | const baseSchema = v.object({ 5 | "@context": v.array(v.string()), 6 | credentialStatus: v.optional( 7 | v.object({ 8 | id: v.string(), 9 | type: v.string() 10 | }) 11 | ), 12 | credentialSubject: v.looseObject({ id: v.optional(v.string()) }), 13 | expirationDate: v.optional(v.string()), 14 | id: v.optional(v.string()), 15 | issuanceDate: v.string(), 16 | issuer: v.union([v.string(), v.object({ id: v.string() })]), 17 | type: v.array(v.string()), 18 | proof: v.optional(v.looseObject({ type: v.optional(v.string()) })) 19 | }) 20 | 21 | export const credentialSchema = v.pipe( 22 | baseSchema, 23 | v.transform((input) => { 24 | const issuer = 25 | typeof input.issuer === "string" ? { id: input.issuer } : input.issuer 26 | 27 | return { 28 | ...input, 29 | issuer 30 | } as W3CCredential 31 | }) 32 | ) 33 | 34 | export const jwtProofSchema = v.object({ 35 | type: v.literal("JwtProof2020"), 36 | jwt: v.string() 37 | }) 38 | 39 | export const statusList2021ClaimSchema = v.object({ 40 | id: v.string(), 41 | type: v.literal("StatusList2021"), 42 | statusPurpose: v.string(), 43 | encodedList: v.string() 44 | }) 45 | -------------------------------------------------------------------------------- /packages/vc/src/schemas/zod.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | import type { W3CCredential } from "../types" 3 | 4 | export const credentialSchema = z 5 | .object({ 6 | "@context": z.array(z.string()), 7 | credentialStatus: z 8 | .object({ 9 | id: z.string(), 10 | type: z.string() 11 | }) 12 | .optional(), 13 | credentialSubject: z.object({ id: z.string().optional() }).passthrough(), 14 | expirationDate: z.string().optional(), 15 | id: z.string().optional(), 16 | issuanceDate: z.string(), 17 | issuer: z.string().or(z.object({ id: z.string() })), 18 | type: z.array(z.string()), 19 | proof: z 20 | .object({ 21 | type: z.string().optional() 22 | }) 23 | .passthrough() 24 | .optional() 25 | }) 26 | .transform((v) => { 27 | const issuer = typeof v.issuer === "string" ? { id: v.issuer } : v.issuer 28 | 29 | return { 30 | ...v, 31 | issuer 32 | } as W3CCredential 33 | }) 34 | 35 | export const jwtProofSchema = z.object({ 36 | type: z.literal("JwtProof2020"), 37 | jwt: z.string() 38 | }) 39 | 40 | export const statusList2021ClaimSchema = z.object({ 41 | id: z.string(), 42 | type: z.literal("StatusList2021"), 43 | statusPurpose: z.string(), 44 | encodedList: z.string() 45 | }) 46 | -------------------------------------------------------------------------------- /packages/vc/src/sign-credential.ts: -------------------------------------------------------------------------------- 1 | import { isJwtString, resolveJwtAlgorithm } from "@agentcommercekit/jwt" 2 | import { createVerifiableCredentialJwt, verifyCredential } from "did-jwt-vc" 3 | import type { Verifiable, W3CCredential } from "./types" 4 | import type { Resolvable } from "@agentcommercekit/did" 5 | import type { JwtAlgorithm, JwtSigner, JwtString } from "@agentcommercekit/jwt" 6 | 7 | interface SignCredentialOptions { 8 | /** 9 | * The algorithm to use for the JWT 10 | */ 11 | alg?: JwtAlgorithm 12 | /** 13 | * The DID of the credential issuer 14 | */ 15 | did: string 16 | /** 17 | * The signer to use for the JWT 18 | */ 19 | signer: JwtSigner 20 | /** 21 | * A resolver to use for parsing the signed credential 22 | */ 23 | resolver: Resolvable 24 | } 25 | 26 | type SignedCredential = { 27 | /** 28 | * The signed {@link Verifiable} credential 29 | */ 30 | verifiableCredential: Verifiable 31 | /** 32 | * The JWT string representation of the signed credential 33 | */ 34 | jwt: JwtString 35 | } 36 | 37 | /** 38 | * Signs a credential with a given issuer. 39 | * 40 | * @param credential - The {@link W3CCredential} to sign 41 | * @param options - The {@link SignCredentialOptions} to use 42 | * @returns A {@link SignedCredential} 43 | */ 44 | export async function signCredential( 45 | credential: T, 46 | options: SignCredentialOptions 47 | ): Promise> { 48 | options.alg = options.alg ? resolveJwtAlgorithm(options.alg) : options.alg 49 | const jwt = await createVerifiableCredentialJwt(credential, options) 50 | 51 | if (!isJwtString(jwt)) { 52 | throw new Error("Failed to sign credential") 53 | } 54 | 55 | const { verifiableCredential } = await verifyCredential(jwt, options.resolver) 56 | 57 | return { jwt, verifiableCredential: verifiableCredential as Verifiable } 58 | } 59 | -------------------------------------------------------------------------------- /packages/vc/src/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import type { JwtCredentialPayload, Verifiable } from "did-jwt-vc" 3 | 4 | type Extensible = T & Record 5 | 6 | export interface CredentialStatus { 7 | id: string 8 | type: string 9 | } 10 | 11 | type W3CCredential = { 12 | "@context": string[] 13 | id?: string 14 | type: string[] 15 | issuer: Extensible<{ id: string }> 16 | issuanceDate: string 17 | expirationDate?: string 18 | credentialSubject: Extensible<{ 19 | id?: string 20 | }> 21 | credentialStatus?: CredentialStatus 22 | 23 | evidence?: any 24 | termsOfUse?: any 25 | } 26 | 27 | export type CredentialSubject = W3CCredential["credentialSubject"] 28 | export type { JwtCredentialPayload, Verifiable, W3CCredential } 29 | -------------------------------------------------------------------------------- /packages/vc/src/verification/errors.ts: -------------------------------------------------------------------------------- 1 | export class CredentialVerificationError extends Error {} 2 | 3 | export class InvalidCredentialError extends CredentialVerificationError { 4 | constructor(message = "Invalid credential") { 5 | super(message) 6 | this.name = "InvalidCredentialError" 7 | } 8 | } 9 | 10 | export class InvalidControllerClaimError extends CredentialVerificationError { 11 | constructor(message = "Invalid controller claim") { 12 | super(message) 13 | this.name = "InvalidControllerClaimError" 14 | } 15 | } 16 | 17 | export class InvalidCredentialSubjectError extends CredentialVerificationError { 18 | constructor(message = "Invalid credential subject") { 19 | super(message) 20 | this.name = "InvalidCredentialSubjectError" 21 | } 22 | } 23 | 24 | export class UnsupportedProofTypeError extends CredentialVerificationError { 25 | constructor(message = "Unsupported proof type") { 26 | super(message) 27 | } 28 | } 29 | 30 | export class InvalidProofError extends CredentialVerificationError { 31 | constructor(message = "Invalid proof") { 32 | super(message) 33 | } 34 | } 35 | 36 | export class CredentialExpiredError extends CredentialVerificationError { 37 | constructor(message = "Credential is expired") { 38 | super(message) 39 | } 40 | } 41 | 42 | export class CredentialRevokedError extends CredentialVerificationError { 43 | constructor(message = "Credential is revoked") { 44 | super(message) 45 | } 46 | } 47 | 48 | export class UntrustedIssuerError extends CredentialVerificationError { 49 | constructor(message = "Issuer is not a known trusted issuer") { 50 | super(message) 51 | } 52 | } 53 | 54 | export class UnsupportedCredentialTypeError extends CredentialVerificationError { 55 | constructor(message = "Unsupported credential type") { 56 | super(message) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/vc/src/verification/is-expired.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from "vitest" 2 | import { isExpired } from "./is-expired" 3 | import type { Verifiable, W3CCredential } from "../types" 4 | 5 | describe("isExpired", () => { 6 | it("should return false when credential has no expiration date", () => { 7 | const credential = {} as Verifiable 8 | expect(isExpired(credential)).toBe(false) 9 | }) 10 | 11 | it("should return true when credential is expired", () => { 12 | const pastDate = new Date() 13 | pastDate.setFullYear(pastDate.getFullYear() - 1) 14 | 15 | const credential = { 16 | expirationDate: pastDate.toISOString() 17 | } as Verifiable 18 | 19 | expect(isExpired(credential)).toBe(true) 20 | }) 21 | 22 | it("should return false when credential is not expired", () => { 23 | const futureDate = new Date() 24 | futureDate.setFullYear(futureDate.getFullYear() + 1) 25 | 26 | const credential = { 27 | expirationDate: futureDate.toISOString() 28 | } as Verifiable 29 | 30 | expect(isExpired(credential)).toBe(false) 31 | }) 32 | 33 | it("should handle expiration date exactly at current time", () => { 34 | const now = new Date() 35 | const credential = { 36 | expirationDate: now.toISOString() 37 | } as Verifiable 38 | 39 | vi.setSystemTime(now) 40 | 41 | expect(isExpired(credential)).toBe(false) 42 | }) 43 | 44 | it("should handle invalid date strings gracefully", () => { 45 | const credential = { 46 | expirationDate: "invalid-date" 47 | } as Verifiable 48 | 49 | expect(isExpired(credential)).toBe(false) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /packages/vc/src/verification/is-expired.ts: -------------------------------------------------------------------------------- 1 | import type { W3CCredential } from "../types" 2 | 3 | /** 4 | * Check if a credential is expired 5 | * 6 | * @param credential - The {@link W3CCredential} to check 7 | * @returns `true` if the credential is expired, `false` otherwise 8 | */ 9 | export function isExpired(credential: W3CCredential): boolean { 10 | if (!credential.expirationDate) { 11 | return false 12 | } 13 | 14 | const expirationDate = new Date(credential.expirationDate) 15 | 16 | if (isNaN(expirationDate.getTime())) { 17 | // Expiration date is invalid, so we consider the credential not expired 18 | return false 19 | } 20 | 21 | return expirationDate < new Date() 22 | } 23 | -------------------------------------------------------------------------------- /packages/vc/src/verification/parse-jwt-credential.ts: -------------------------------------------------------------------------------- 1 | import { verifyCredential } from "did-jwt-vc" 2 | import type { Verifiable, W3CCredential } from "../types" 3 | import type { Resolvable } from "@agentcommercekit/did" 4 | 5 | /** 6 | * Parse a JWT credential 7 | * 8 | * @param jwt - The JWT string to parse 9 | * @param resolver - The resolver to use for did resolution 10 | * @returns A {@link Verifiable} 11 | */ 12 | export async function parseJwtCredential( 13 | jwt: string, 14 | resolver: Resolvable 15 | ): Promise> { 16 | const result = await verifyCredential(jwt, resolver) 17 | 18 | return result.verifiableCredential 19 | } 20 | -------------------------------------------------------------------------------- /packages/vc/src/verification/types.ts: -------------------------------------------------------------------------------- 1 | import type { CredentialSubject } from "../types" 2 | import type { Resolvable } from "@agentcommercekit/did" 3 | 4 | export type ClaimVerifier = { 5 | accepts(type: string[]): boolean 6 | verify( 7 | credentialSubject: CredentialSubject, 8 | resolver: Resolvable 9 | ): Promise 10 | } 11 | -------------------------------------------------------------------------------- /packages/vc/src/verification/verify-proof.ts: -------------------------------------------------------------------------------- 1 | import { verifyCredential } from "did-jwt-vc" 2 | import { InvalidProofError, UnsupportedProofTypeError } from "./errors" 3 | import type { Verifiable, W3CCredential } from "../types" 4 | import type { Resolvable } from "@agentcommercekit/did" 5 | 6 | interface JwtProof { 7 | type: "JwtProof2020" 8 | jwt: string 9 | } 10 | 11 | /** 12 | * Check if a proof is a JWT proof 13 | * 14 | * @param proof - The proof to check 15 | * @returns `true` if the proof is a JWT proof, `false` otherwise 16 | */ 17 | export function isJwtProof(proof: unknown): proof is JwtProof { 18 | return ( 19 | typeof proof === "object" && 20 | proof !== null && 21 | "type" in proof && 22 | proof.type === "JwtProof2020" && 23 | "jwt" in proof && 24 | typeof proof.jwt === "string" 25 | ) 26 | } 27 | 28 | async function verifyJwtProof( 29 | proof: Verifiable["proof"], 30 | resolver: Resolvable 31 | ): Promise { 32 | if (!isJwtProof(proof)) { 33 | throw new InvalidProofError() 34 | } 35 | 36 | try { 37 | await verifyCredential(proof.jwt, resolver) 38 | } catch (_error) { 39 | throw new InvalidProofError() 40 | } 41 | } 42 | 43 | /** 44 | * Verify a proof 45 | * 46 | * @param proof - The credential proof to verify 47 | * @param resolver - The resolver to use for did resolution 48 | */ 49 | export async function verifyProof( 50 | proof: Verifiable["proof"], 51 | resolver: Resolvable 52 | ): Promise { 53 | switch (proof.type) { 54 | case "JwtProof2020": 55 | return verifyJwtProof(proof, resolver) 56 | default: 57 | throw new UnsupportedProofTypeError( 58 | `Unsupported proof type: ${proof.type}` 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/vc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/typescript-library.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", "dist"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/vc/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig({ 4 | test: { 5 | passWithNoTests: true, 6 | watch: false 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - demos/* 3 | - docs 4 | - examples/* 5 | - packages/* 6 | - tools/* 7 | ignoredBuiltDependencies: 8 | - puppeteer 9 | onlyBuiltDependencies: 10 | - better-sqlite3 11 | - esbuild 12 | - sharp 13 | - unrs-resolver 14 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://prettier.io/docs/en/configuration.html 3 | * @type {import("prettier").Config} 4 | */ 5 | const config = { 6 | trailingComma: "none", 7 | tabWidth: 2, 8 | semi: false, 9 | singleQuote: false, 10 | plugins: ["prettier-plugin-packagejson"] 11 | } 12 | 13 | export default config 14 | -------------------------------------------------------------------------------- /tools/api-utils/README.md: -------------------------------------------------------------------------------- 1 | # @repo/api-utils 2 | 3 | This package contains utility functions that are used throughout api projects. 4 | -------------------------------------------------------------------------------- /tools/api-utils/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { config } from "@repo/eslint-config/base" 4 | 5 | export default config({ 6 | root: import.meta.dirname 7 | }) 8 | -------------------------------------------------------------------------------- /tools/api-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/api-utils", 3 | "version": "0.0.1", 4 | "private": true, 5 | "homepage": "https://github.com/agentcommercekit/ack#readme", 6 | "bugs": "https://github.com/agentcommercekit/ack/issues", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/agentcommercekit/ack.git", 10 | "directory": "tools/api-utils" 11 | }, 12 | "license": "MIT", 13 | "author": { 14 | "name": "Catena Labs", 15 | "url": "https://catenalabs.com" 16 | }, 17 | "type": "module", 18 | "exports": { 19 | "./package.json": "./package.json", 20 | "./api-response": { 21 | "types": "./src/api-response.ts", 22 | "import": "./src/api-response.ts" 23 | }, 24 | "./exceptions": { 25 | "types": "./src/exceptions.ts", 26 | "import": "./src/exceptions.ts" 27 | }, 28 | "./middleware/*": { 29 | "types": "./src/middleware/*.ts", 30 | "import": "./src/middleware/*.ts" 31 | }, 32 | "./validate-payload": { 33 | "types": "./src/validate-payload.ts", 34 | "import": "./src/validate-payload.ts" 35 | } 36 | }, 37 | "main": "./src/index.ts", 38 | "scripts": { 39 | "check:types": "tsc --noEmit --pretty", 40 | "clean": "git clean -fdX .turbo", 41 | "lint": "eslint .", 42 | "lint:fix": "eslint . --fix", 43 | "test": "vitest" 44 | }, 45 | "dependencies": { 46 | "@agentcommercekit/ack-pay": "workspace:*", 47 | "@agentcommercekit/did": "workspace:*", 48 | "@agentcommercekit/jwt": "workspace:*", 49 | "@agentcommercekit/keys": "workspace:*", 50 | "@agentcommercekit/vc": "workspace:*", 51 | "hono": "^4.7.10", 52 | "valibot": "^1.1.0" 53 | }, 54 | "devDependencies": { 55 | "@repo/eslint-config": "workspace:*", 56 | "@repo/typescript-config": "workspace:*", 57 | "eslint": "^9.27.0", 58 | "typescript": "^5", 59 | "vitest": "^3.1.4" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tools/api-utils/src/api-response.ts: -------------------------------------------------------------------------------- 1 | import { DidResolutionError } from "@agentcommercekit/did" 2 | import { CredentialVerificationError } from "@agentcommercekit/vc" 3 | import * as v from "valibot" 4 | import type { TypedResponse } from "hono" 5 | 6 | export type ApiResponse = TypedResponse> 7 | 8 | export type ApiSuccessResponse = { 9 | ok: true 10 | data: T 11 | } 12 | 13 | /** 14 | * Generates a successful API response object 15 | */ 16 | export function apiSuccessResponse(data: T): ApiSuccessResponse { 17 | return { 18 | ok: true, 19 | data 20 | } 21 | } 22 | 23 | export type ApiErrorResponse = { 24 | ok: false 25 | error: string 26 | issues?: v.BaseIssue[] 27 | } 28 | 29 | export function formatErrorResponse( 30 | error: Error, 31 | message?: string 32 | ): ApiErrorResponse { 33 | if (error instanceof DidResolutionError) { 34 | return { 35 | ok: false, 36 | error: message ?? error.message 37 | } 38 | } 39 | 40 | if (error instanceof CredentialVerificationError) { 41 | return { 42 | ok: false, 43 | error: message ?? error.message 44 | } 45 | } 46 | 47 | if (v.isValiError(error)) { 48 | return { 49 | ok: false, 50 | error: message ?? "Invalid request", 51 | issues: error.issues 52 | } 53 | } 54 | 55 | return { 56 | ok: false, 57 | error: message ?? error.message 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tools/api-utils/src/exceptions.ts: -------------------------------------------------------------------------------- 1 | import { HTTPException } from "hono/http-exception" 2 | 3 | /** 4 | * Helper functions 5 | */ 6 | 7 | export function badRequest(message = "Bad Request"): never { 8 | throw new HTTPException(400, { 9 | message 10 | }) 11 | } 12 | 13 | export function unauthorized(message = "Unauthorized"): never { 14 | throw new HTTPException(401, { 15 | message 16 | }) 17 | } 18 | 19 | export function notFound(message = "Not Found"): never { 20 | throw new HTTPException(404, { 21 | message 22 | }) 23 | } 24 | 25 | export function internalServerError(message = "Internal Server Error"): never { 26 | throw new HTTPException(500, { 27 | message 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /tools/api-utils/src/middleware/error-handler.ts: -------------------------------------------------------------------------------- 1 | import { InvalidPaymentTokenError } from "@agentcommercekit/ack-pay" 2 | import { DidResolutionError } from "@agentcommercekit/did" 3 | import { CredentialVerificationError } from "@agentcommercekit/vc" 4 | import { HTTPException } from "hono/http-exception" 5 | import * as v from "valibot" 6 | import { formatErrorResponse } from "../api-response" 7 | import type { Env, ErrorHandler } from "hono" 8 | 9 | export const errorHandler: ErrorHandler = (err, c) => { 10 | if ( 11 | err instanceof DidResolutionError || 12 | err instanceof CredentialVerificationError || 13 | err instanceof InvalidPaymentTokenError 14 | ) { 15 | return c.json(formatErrorResponse(err), 400) 16 | } 17 | 18 | if (v.isValiError(err)) { 19 | return c.json(formatErrorResponse(err), 400) 20 | } 21 | 22 | if (err instanceof HTTPException) { 23 | if (err.status >= 500 && process.env.NODE_ENV !== "test") { 24 | console.error(err.stack) 25 | } 26 | 27 | if (err.res) { 28 | return err.res 29 | } 30 | 31 | return c.json(formatErrorResponse(err), err.status) 32 | } 33 | 34 | if (process.env.NODE_ENV !== "test") { 35 | console.error(err.stack) 36 | } 37 | 38 | return c.json(formatErrorResponse(err), 500) 39 | } 40 | -------------------------------------------------------------------------------- /tools/api-utils/src/middleware/logger.ts: -------------------------------------------------------------------------------- 1 | import { logger as honoLogger } from "hono/logger" 2 | 3 | export function logger() { 4 | return honoLogger((message, ...rest) => { 5 | if (process.env.NODE_ENV === "test") { 6 | return 7 | } 8 | 9 | console.log(message, ...rest) 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /tools/api-utils/src/validate-payload.ts: -------------------------------------------------------------------------------- 1 | import { verifyJwt } from "@agentcommercekit/jwt" 2 | import * as v from "valibot" 3 | import { unauthorized } from "./exceptions" 4 | import type { Resolvable } from "@agentcommercekit/did" 5 | import type { JwtString, JwtVerified } from "@agentcommercekit/jwt" 6 | 7 | export type ParsedPayload = { 8 | parsed: JwtVerified 9 | body: T 10 | } 11 | 12 | /** 13 | * Validates a JWT payload and returns the parsed payload and body 14 | * 15 | * NOTE: This does not perform logic beyond validating that the JWT is valid and 16 | * is properly signed. 17 | */ 18 | export async function validatePayload( 19 | payload: JwtString, 20 | bodySchema: v.GenericSchema, 21 | resolver?: Resolvable 22 | ): Promise> { 23 | let parsed: JwtVerified 24 | 25 | try { 26 | parsed = await verifyJwt(payload, { 27 | resolver, 28 | policies: { 29 | aud: false 30 | } 31 | }) 32 | } catch (_e) { 33 | unauthorized("Invalid payload") 34 | } 35 | 36 | const body = v.parse(bodySchema, parsed.payload) 37 | 38 | return { 39 | parsed, 40 | body 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tools/api-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/typescript-library.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", "dist"] 5 | } 6 | -------------------------------------------------------------------------------- /tools/api-utils/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig({ 4 | test: { 5 | passWithNoTests: true, 6 | watch: false 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /tools/cli-tools/README.md: -------------------------------------------------------------------------------- 1 | # @repo/cli-tools 2 | 3 | A helper library to provide reusable CLI tools to keep the demos consistent. 4 | -------------------------------------------------------------------------------- /tools/cli-tools/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { config } from "@repo/eslint-config/base" 4 | 5 | export default config({ 6 | root: import.meta.dirname 7 | }) 8 | -------------------------------------------------------------------------------- /tools/cli-tools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/cli-tools", 3 | "version": "0.0.1", 4 | "private": true, 5 | "homepage": "https://github.com/agentcommercekit/ack#readme", 6 | "bugs": "https://github.com/agentcommercekit/ack/issues", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/agentcommercekit/ack.git", 10 | "directory": "tools/cli-tools" 11 | }, 12 | "license": "MIT", 13 | "author": { 14 | "name": "Catena Labs", 15 | "url": "https://catenalabs.com" 16 | }, 17 | "type": "module", 18 | "exports": { 19 | "./package.json": "./package.json", 20 | ".": { 21 | "types": "./src/index.ts", 22 | "import": "./src/index.ts" 23 | } 24 | }, 25 | "main": "./src/index.ts", 26 | "scripts": { 27 | "check:types": "tsc --noEmit", 28 | "clean": "git clean -fdX .turbo dist", 29 | "lint": "eslint .", 30 | "lint:fix": "eslint . --fix", 31 | "test": "vitest" 32 | }, 33 | "dependencies": { 34 | "@inquirer/prompts": "^7.5.1", 35 | "figlet": "^1.8.1", 36 | "strip-ansi": "^7.1.0", 37 | "wrap-ansi": "^9.0.0", 38 | "yoctocolors": "^2.1.1" 39 | }, 40 | "devDependencies": { 41 | "@repo/eslint-config": "workspace:*", 42 | "@repo/typescript-config": "workspace:*", 43 | "@types/figlet": "^1.7.0", 44 | "@types/node": "^22", 45 | "eslint": "^9.27.0", 46 | "typescript": "^5", 47 | "vitest": "^3.1.4" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tools/cli-tools/src/formatters.ts: -------------------------------------------------------------------------------- 1 | import { default as figlet } from "figlet" 2 | import stripAnsi from "strip-ansi" 3 | import wrapAnsi from "wrap-ansi" 4 | import colors from "yoctocolors" 5 | 6 | export function demoHeader(message: string) { 7 | return colors.blue( 8 | figlet.textSync(message, { 9 | font: "Standard", 10 | horizontalLayout: "full", 11 | verticalLayout: "full" 12 | }) 13 | ) 14 | } 15 | 16 | export function demoFooter(message: string) { 17 | return colors.bold( 18 | colors.cyan( 19 | figlet.textSync(message, { 20 | font: "Small" 21 | }) 22 | ) 23 | ) 24 | } 25 | 26 | /** 27 | * Creates a section header with an optional step number 28 | */ 29 | export function sectionHeader( 30 | message: string, 31 | { step }: { step?: number } = {} 32 | ) { 33 | const stepMessage = [ 34 | step ? colors.bold(`Step ${step}:`) : undefined, 35 | message 36 | ].filter(Boolean) 37 | 38 | const stepMessageLength = stripAnsi(stepMessage.join(" ")).length 39 | const divider = "─".repeat(stepMessageLength) 40 | 41 | return ` 42 | ${colors.bold(divider)} 43 | ${stepMessage.join(" ")} 44 | ${colors.bold(divider)} 45 | ` 46 | } 47 | 48 | /** 49 | * Creates a success message with a check mark 50 | */ 51 | export function successMessage(message: string) { 52 | return colors.green(`✓ ${message}`) 53 | } 54 | 55 | /** 56 | * Creates an error message with an X 57 | */ 58 | export function errorMessage(message: string) { 59 | return colors.red(`✗ ${message}`) 60 | } 61 | 62 | /** 63 | * Wraps text to a given width, preserving ANSI color codes 64 | */ 65 | export function wordWrap(text: string, width = 80) { 66 | return wrapAnsi(text, width, { trim: true, hard: true }) 67 | } 68 | 69 | export function link(url: string) { 70 | return colors.bold(colors.underline(url)) 71 | } 72 | -------------------------------------------------------------------------------- /tools/cli-tools/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as figlet } from "figlet" 2 | export { default as colors } from "yoctocolors" 3 | export * from "@inquirer/prompts" 4 | export * from "./prompts" 5 | export * from "./formatters" 6 | export * from "./update-env-file" 7 | -------------------------------------------------------------------------------- /tools/cli-tools/src/prompts.ts: -------------------------------------------------------------------------------- 1 | import { input } from "@inquirer/prompts" 2 | import { magenta, yellow } from "yoctocolors" 3 | import { wordWrap } from "./formatters" 4 | 5 | /** 6 | * Waits for the user to press Enter 7 | */ 8 | export async function waitForEnter( 9 | message = "Press Enter to continue...", 10 | color = yellow 11 | ) { 12 | await input({ message: color(message) }) 13 | console.log("") 14 | } 15 | 16 | type LogOptions = { 17 | wrap?: boolean 18 | width?: number 19 | spacing?: number 20 | } 21 | 22 | /** 23 | * Prints messages to the console, automatically wrapping them to 24 | * the default width. Each message will be printed on a new line. 25 | */ 26 | export function log(...args: (string | LogOptions)[]) { 27 | let options: Required = { 28 | wrap: true, 29 | spacing: 1, 30 | width: 80 31 | } 32 | 33 | if (typeof args[args.length - 1] === "object") { 34 | options = Object.assign(options, args.pop()) 35 | } 36 | 37 | const messages = args as string[] 38 | 39 | messages.forEach((message, index) => { 40 | console.log(options.wrap ? wordWrap(message, options.width) : message) 41 | if (options.spacing > 0 && index < messages.length - 1) { 42 | console.log("\n".repeat(options.spacing - 1)) 43 | } 44 | }) 45 | } 46 | 47 | export function logJson(obj: Record, color = magenta) { 48 | log(color(JSON.stringify(obj, null, 2)), { wrap: false }) 49 | } 50 | -------------------------------------------------------------------------------- /tools/cli-tools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/typescript-library.json", 3 | "include": ["."], 4 | "exclude": ["node_modules", "dist"] 5 | } 6 | -------------------------------------------------------------------------------- /tools/cli-tools/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig({ 4 | test: { 5 | passWithNoTests: true, 6 | watch: false 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /tools/eslint-config/README.md: -------------------------------------------------------------------------------- 1 | # `@repo/eslint-config` 2 | 3 | Shared eslint configuration for the workspace. 4 | -------------------------------------------------------------------------------- /tools/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/eslint-config", 3 | "version": "0.0.1", 4 | "private": true, 5 | "homepage": "https://github.com/agentcommercekit/ack#readme", 6 | "bugs": "https://github.com/agentcommercekit/ack/issues", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/agentcommercekit/ack.git", 10 | "directory": "tools/eslint-config" 11 | }, 12 | "license": "MIT", 13 | "author": { 14 | "name": "Catena Labs", 15 | "url": "https://catenalabs.com" 16 | }, 17 | "type": "module", 18 | "exports": { 19 | "./package.json": "./package.json", 20 | "./base": "./base.js" 21 | }, 22 | "main": "./base.js", 23 | "devDependencies": { 24 | "@cspell/eslint-plugin": "^9.0.1", 25 | "@eslint/js": "^9.27.0", 26 | "@eslint/json": "^0.12.0", 27 | "@eslint/markdown": "^6.4.0", 28 | "eslint": "^9.27.0", 29 | "eslint-config-prettier": "^10.1.5", 30 | "eslint-import-resolver-typescript": "^4.3.5", 31 | "eslint-plugin-import-x": "^4.12.2", 32 | "eslint-plugin-turbo": "^2.5.3", 33 | "typescript": "^5", 34 | "typescript-eslint": "^8.32.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tools/typescript-config/README.md: -------------------------------------------------------------------------------- 1 | # `@repo/typescript-config` 2 | 3 | Shared typescript configuration for the workspace. 4 | -------------------------------------------------------------------------------- /tools/typescript-config/base-app.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Base app", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "allowJs": true, 7 | "jsx": "preserve", 8 | "noEmit": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tools/typescript-config/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "declaration": true, 6 | "declarationMap": true, 7 | "esModuleInterop": true, 8 | "incremental": false, 9 | "isolatedModules": true, 10 | "lib": ["es2022", "DOM", "DOM.Iterable"], 11 | "module": "ESNext", 12 | "moduleDetection": "force", 13 | "moduleResolution": "Bundler", 14 | "noUncheckedIndexedAccess": true, 15 | "resolveJsonModule": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "target": "ES2022" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tools/typescript-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/typescript-config", 3 | "version": "0.0.1", 4 | "private": true, 5 | "homepage": "https://github.com/agentcommercekit/ack#readme", 6 | "bugs": "https://github.com/agentcommercekit/ack/issues", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/agentcommercekit/ack.git", 10 | "directory": "tools/typescript-config" 11 | }, 12 | "license": "MIT", 13 | "author": { 14 | "name": "Catena Labs", 15 | "url": "https://catenalabs.com" 16 | }, 17 | "main": "./base.js" 18 | } 19 | -------------------------------------------------------------------------------- /tools/typescript-config/typescript-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Typescript Library", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "noEmit": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/base.json" 3 | } 4 | -------------------------------------------------------------------------------- /turbo.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "ui": "stream", 4 | "concurrency": "15", 5 | "tasks": { 6 | "//#check:format": {}, 7 | "//#check:packages": {}, 8 | "//#format": {}, 9 | "build": { 10 | "dependsOn": ["^build"], 11 | "inputs": ["$TURBO_DEFAULT$", ".env*"], 12 | "outputs": ["dist/**"] 13 | }, 14 | "check": { 15 | "dependsOn": [ 16 | "//#check:format", 17 | "lint", 18 | "check:types", 19 | "test", 20 | "//#check:packages" 21 | ] 22 | }, 23 | "check:types": { 24 | "dependsOn": ["^check:types", "^build"] 25 | }, 26 | "clean": { 27 | "dependsOn": ["^clean"] 28 | }, 29 | "dev": { 30 | "dependsOn": ["^build"], 31 | "cache": false, 32 | "persistent": true 33 | }, 34 | "fix": { 35 | "dependsOn": ["//#format", "lint:fix"] 36 | }, 37 | "lint": { 38 | "dependsOn": ["^lint", "^build"] 39 | }, 40 | "lint:fix": { 41 | "dependsOn": ["^lint:fix", "^build"] 42 | }, 43 | "setup": { 44 | "dependsOn": ["^setup"], 45 | "cache": false 46 | }, 47 | "test": { 48 | "dependsOn": ["^test", "^build"] 49 | } 50 | }, 51 | "globalEnv": ["NODE_ENV"] 52 | } 53 | --------------------------------------------------------------------------------