From 782b1fe57a2aa48705c2569b390c81e4040fcd2c Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Wed, 27 Aug 2025 21:36:55 -0400 Subject: [PATCH] Rename `paymentToken` to `paymentRequestToken` --- .changeset/afraid-clocks-greet.md | 8 +++ demos/e2e/src/agent.ts | 61 ++++++++--------- demos/e2e/src/index.ts | 12 ++-- demos/e2e/src/payment-required-error.ts | 6 +- demos/e2e/src/receipt-issuer.ts | 17 +++-- demos/e2e/src/receipt-verifier.ts | 2 +- demos/payments/src/index.ts | 42 +++++++----- demos/payments/src/payment-service.ts | 35 ++++++---- demos/payments/src/receipt-service.ts | 14 ++-- demos/payments/src/server.ts | 15 +++-- docs/ack-pay/payment-request-payload.mdx | 36 +++++----- docs/ack-pay/receipt-verification.mdx | 26 +++++--- docs/demos/example-identity.mdx | 2 +- examples/issuer/README.md | 8 +-- examples/issuer/src/routes/receipts.test.ts | 28 ++++---- examples/issuer/src/routes/receipts.ts | 30 +++++---- packages/ack-pay/README.md | 23 +++---- .../src/create-payment-receipt.test.ts | 29 +++++---- .../ack-pay/src/create-payment-receipt.ts | 6 +- .../src/create-payment-request-body.ts | 53 --------------- ...s => create-payment-request-token.test.ts} | 40 +++++++----- ...ken.ts => create-payment-request-token.ts} | 20 +++--- ... => create-signed-payment-request.test.ts} | 18 ++--- .../src/create-signed-payment-request.ts | 49 ++++++++++++++ packages/ack-pay/src/errors.ts | 6 +- packages/ack-pay/src/index.ts | 6 +- .../src/receipt-claim-verifier.test.ts | 19 +++--- packages/ack-pay/src/schemas/valibot.ts | 8 +-- packages/ack-pay/src/schemas/zod/v3.ts | 8 +-- packages/ack-pay/src/schemas/zod/v4.ts | 8 +-- .../src/verify-payment-receipt.test.ts | 36 +++++----- .../ack-pay/src/verify-payment-receipt.ts | 52 ++++++++------- ...s => verify-payment-request-token.test.ts} | 34 +++++----- .../src/verify-payment-request-token.ts | 65 +++++++++++++++++++ packages/ack-pay/src/verify-payment-token.ts | 65 ------------------- packages/agentcommercekit/README.md | 15 ++--- .../api-utils/src/middleware/error-handler.ts | 4 +- 37 files changed, 472 insertions(+), 434 deletions(-) create mode 100644 .changeset/afraid-clocks-greet.md delete mode 100644 packages/ack-pay/src/create-payment-request-body.ts rename packages/ack-pay/src/{create-payment-token.test.ts => create-payment-request-token.test.ts} (65%) rename packages/ack-pay/src/{create-payment-token.ts => create-payment-request-token.ts} (55%) rename packages/ack-pay/src/{create-payment-request-body.test.ts => create-signed-payment-request.test.ts} (80%) create mode 100644 packages/ack-pay/src/create-signed-payment-request.ts rename packages/ack-pay/src/{verify-payment-token.test.ts => verify-payment-request-token.test.ts} (80%) create mode 100644 packages/ack-pay/src/verify-payment-request-token.ts delete mode 100644 packages/ack-pay/src/verify-payment-token.ts diff --git a/.changeset/afraid-clocks-greet.md b/.changeset/afraid-clocks-greet.md new file mode 100644 index 0000000..7c6f72b --- /dev/null +++ b/.changeset/afraid-clocks-greet.md @@ -0,0 +1,8 @@ +--- +"agentcommercekit": minor +"@agentcommercekit/ack-pay": minor +--- + +- Deprecate `createPaymentRequestBody` in favor of `createSignedPaymentRequest` +- Rename `paymentToken` to `paymentRequestToken` in payment requests and receipts +- Remove `createPaymentRequestResponse`, which only built a `Response` object in a demo diff --git a/demos/e2e/src/agent.ts b/demos/e2e/src/agent.ts index 1bf9258..0757bb0 100644 --- a/demos/e2e/src/agent.ts +++ b/demos/e2e/src/agent.ts @@ -3,7 +3,7 @@ import { createDidPkhDocument, createDidWebDocumentFromKeypair, createJwtSigner, - createPaymentRequestBody, + createSignedPaymentRequest, curveToJwtAlgorithm, generateKeypair } from "agentcommercekit" @@ -205,45 +205,46 @@ ${colors.bold(otherAgent.did)} // If no receipt provided, generate payment request if (!receipt) { - const { paymentRequest, paymentToken } = await createPaymentRequestBody( - { - id: crypto.randomUUID(), - expiresAt: new Date(Date.now() + 3600000), // 1 hour from now - paymentOptions: [ - { - id: crypto.randomUUID(), - amount: BigInt(500).toString(), - decimals: 2, - currency: "USD", - recipient: this.walletDid - } - ] - }, - { - issuer: this.did, - signer: this.signer, - algorithm: curveToJwtAlgorithm(this.keypair.curve) - } - ) - - // Optional: Store the pending payment tokens. - this.pendingRequests[paymentToken] = paymentRequest - - throw new PaymentRequiredError(paymentRequest, paymentToken) + const { paymentRequest, paymentRequestToken } = + await createSignedPaymentRequest( + { + id: crypto.randomUUID(), + expiresAt: new Date(Date.now() + 3600000), // 1 hour from now + paymentOptions: [ + { + id: crypto.randomUUID(), + amount: BigInt(500).toString(), + decimals: 2, + currency: "USD", + recipient: this.walletDid + } + ] + }, + { + issuer: this.did, + signer: this.signer, + algorithm: curveToJwtAlgorithm(this.keypair.curve) + } + ) + + // Optional: Store the pending payment request tokens. + this.pendingRequests[paymentRequestToken] = paymentRequest + + throw new PaymentRequiredError(paymentRequest, paymentRequestToken) } // Verify the receipt - const { paymentToken } = await this.receiptVerifier.verifyReceipt( + const { paymentRequestToken } = await this.receiptVerifier.verifyReceipt( receipt, this.did ) - // Optional: Ensure the payment token was for this same type of request. + // Optional: Ensure the payment request token was for this same type of request. // Payment requests and receipts are designed to allow for stateless // operation, but it can be useful to map a given request to a specific // action on the server. - if (!this.pendingRequests[paymentToken]) { - throw new Error("Payment token not found") + if (!this.pendingRequests[paymentRequestToken]) { + throw new Error("Payment Request token not found") } // Provide the service diff --git a/demos/e2e/src/index.ts b/demos/e2e/src/index.ts index 71109ef..1eccab7 100644 --- a/demos/e2e/src/index.ts +++ b/demos/e2e/src/index.ts @@ -198,7 +198,11 @@ If identity is verified, Agent 2 will then respond with a 402 Payment Required e const message = "What's the current price of AAPL stock?" - const { paymentRequest, paymentToken } = await chat(agent1, agent2, message) + const { paymentRequest, paymentRequestToken } = await chat( + agent1, + agent2, + message + ) log( colors.dim(` @@ -217,7 +221,7 @@ Next, we will perform the payment and fetch a Receipt. log( colors.dim(` Payment must be made using token: -${paymentToken} +${paymentRequestToken} `) ) @@ -241,7 +245,7 @@ ${paymentToken} const receipt = await receiptIssuer.issueReceipt({ payerDid: agent1.walletDid, txHash, - paymentToken + paymentRequestToken }) log(successMessage("Payment Receipt VC Issued!")) logJson(await parseJwtCredential(receipt, resolver)) @@ -291,7 +295,7 @@ async function chat(agent1: Agent, agent2: Agent, message: string) { if (error instanceof PaymentRequiredError) { return { paymentRequest: error.paymentRequest, - paymentToken: error.paymentToken + paymentRequestToken: error.paymentRequestToken } } diff --git a/demos/e2e/src/payment-required-error.ts b/demos/e2e/src/payment-required-error.ts index 4083b9d..b9cd3be 100644 --- a/demos/e2e/src/payment-required-error.ts +++ b/demos/e2e/src/payment-required-error.ts @@ -5,12 +5,12 @@ import type { PaymentRequest } from "agentcommercekit" */ export class PaymentRequiredError extends Error { paymentRequest: PaymentRequest - paymentToken: string + paymentRequestToken: string - constructor(paymentRequest: PaymentRequest, paymentToken: string) { + constructor(paymentRequest: PaymentRequest, paymentRequestToken: string) { super("402 Payment Required") this.name = "PaymentRequiredError" this.paymentRequest = paymentRequest - this.paymentToken = paymentToken + this.paymentRequestToken = paymentRequestToken } } diff --git a/demos/e2e/src/receipt-issuer.ts b/demos/e2e/src/receipt-issuer.ts index e69cf8d..91adc4c 100644 --- a/demos/e2e/src/receipt-issuer.ts +++ b/demos/e2e/src/receipt-issuer.ts @@ -4,7 +4,7 @@ import { createPaymentReceipt, generateKeypair, signCredential, - verifyPaymentToken + verifyPaymentRequestToken } from "agentcommercekit" import type { DidDocument, @@ -65,15 +65,18 @@ export class ReceiptIssuer { async issueReceipt({ payerDid, txHash, - paymentToken + paymentRequestToken }: { payerDid: DidUri txHash: string - paymentToken: string + paymentRequestToken: string }): Promise { - const { paymentRequest } = await verifyPaymentToken(paymentToken, { - resolver: this.resolver - }) + const { paymentRequest } = await verifyPaymentRequestToken( + paymentRequestToken, + { + resolver: this.resolver + } + ) // Verify the payment on-chain const paymentVerified = await this.verifyPaymentOnChain( @@ -86,7 +89,7 @@ export class ReceiptIssuer { // Create and sign the receipt credential const credential = createPaymentReceipt({ - paymentToken, + paymentRequestToken, paymentOptionId: paymentRequest.paymentOptions[0].id, issuer: this.did, payerDid diff --git a/demos/e2e/src/receipt-verifier.ts b/demos/e2e/src/receipt-verifier.ts index d04c7f2..f50b796 100644 --- a/demos/e2e/src/receipt-verifier.ts +++ b/demos/e2e/src/receipt-verifier.ts @@ -67,7 +67,7 @@ export class ReceiptVerifier { resolver: this.resolver, trustedReceiptIssuers: this.trustedIssuers, paymentRequestIssuer: paymentRequestIssuer, - verifyPaymentTokenJwt: true + verifyPaymentRequestTokenJwt: true }) } } diff --git a/demos/payments/src/index.ts b/demos/payments/src/index.ts index 9461ce0..0e82673 100644 --- a/demos/payments/src/index.ts +++ b/demos/payments/src/index.ts @@ -20,7 +20,10 @@ import { isJwtString, parseJwtCredential } from "agentcommercekit" -import { paymentRequestBodySchema } from "agentcommercekit/schemas/valibot" +import { + jwtStringSchema, + paymentRequestSchema +} from "agentcommercekit/schemas/valibot" import * as v from "valibot" import { isAddress } from "viem" import { @@ -118,14 +121,17 @@ The Client attempts to access a protected resource on the Server. Since no valid throw new Error("Server did not respond with 402") } - const { paymentToken, paymentRequest } = v.parse( - paymentRequestBodySchema, + const { paymentRequestToken, paymentRequest } = v.parse( + v.object({ + paymentRequestToken: jwtStringSchema, + paymentRequest: paymentRequestSchema + }), await response1.json() ) - // This demo uses JWT strings for the payment token, but this is not a requirement of the protocol. - if (!isJwtString(paymentToken)) { - throw new Error(errorMessage("Invalid payment token")) + // This demo uses JWT strings for the payment request token, but this is not a requirement of the protocol. + if (!isJwtString(paymentRequestToken)) { + throw new Error(errorMessage("Invalid payment request token")) } log( @@ -138,7 +144,7 @@ The Client attempts to access a protected resource on the Server. Since no valid log( colors.magenta( wordWrap( - "\nšŸ’” The 'paymentToken' is a JWT signed by the Server, ensuring the integrity and authenticity of the payment request. The Client will include this token when requesting a receipt, along with the payment option id and metadata." + "\nšŸ’” The 'paymentRequestToken' is a JWT signed by the Server, ensuring the integrity and authenticity of the payment request. The Client will include this token when requesting a receipt, along with the payment option id and metadata." ) ) ) @@ -168,7 +174,7 @@ The Client attempts to access a protected resource on the Server. Since no valid const paymentResult = await performStripePayment( clientKeypairInfo, selectedPaymentOption, - paymentToken + paymentRequestToken ) receipt = paymentResult.receipt details = paymentResult.details @@ -176,7 +182,7 @@ The Client attempts to access a protected resource on the Server. Since no valid const paymentResult = await performOnChainPayment( clientKeypairInfo, selectedPaymentOption, - paymentToken + paymentRequestToken ) receipt = paymentResult.receipt details = paymentResult.details @@ -215,7 +221,7 @@ The Client Agent now retries the original request to the Server Agent, this time ${colors.bold("The Server Agent then performs its own verifications:")} 1. Validates the receipt's signature (ensuring it was issued by a trusted Receipt Service and hasn't been tampered with). -2. Checks the receipt's claims: Confirms the receipt is for the correct payment (e.g., matches the 'paymentToken' it originally issued), is not expired, and meets any other criteria for accessing the resource. +2. Checks the receipt's claims: Confirms the receipt is for the correct payment (e.g., matches the 'paymentRequestToken' it originally issued), is not expired, and meets any other criteria for accessing the resource. If the receipt is valid, the Server grants access to the protected resource.` ) @@ -262,7 +268,7 @@ If the receipt is valid, the Server grants access to the protected resource.` async function performOnChainPayment( client: KeypairInfo, paymentOption: PaymentRequest["paymentOptions"][number], - paymentToken: JwtString + paymentRequestToken: JwtString ) { const receiptServiceUrl = paymentOption.receiptService if (!receiptServiceUrl) { @@ -346,11 +352,11 @@ The Client Agent now uses the details from the Payment Request to make the payme colors.dim( `${colors.bold("Client Agent šŸ‘¤ -> Receipt Service 🧾")} -With the payment confirmed, the Client Agent now requests a formal, cryptographically verifiable payment receipt from the Receipt Service. The Client sends the original 'paymentToken' (received from the Server in Step 1) and the transaction hash (as metadata) to the Receipt Service. The Client also signs this request with its own DID to prove it's the one who made the payment. +With the payment confirmed, the Client Agent now requests a formal, cryptographically verifiable payment receipt from the Receipt Service. The Client sends the original 'paymentRequestToken' (received from the Server in Step 1) and the transaction hash (as metadata) to the Receipt Service. The Client also signs this request with its own DID to prove it's the one who made the payment. ${colors.bold("The Receipt Service then performs several crucial verifications:")} -1. Validates the 'paymentToken' (e.g., signature, expiry, ensuring it was issued by a trusted server for the expected payment context). -2. Verifies the on-chain transaction: Confirms that the transaction hash is valid, the correct amount of the specified currency was transferred to the correct recipient address as per the 'paymentToken'. +1. Validates the 'paymentRequestToken' (e.g., signature, expiry, ensuring it was issued by a trusted server for the expected payment context). +2. Verifies the on-chain transaction: Confirms that the transaction hash is valid, the correct amount of the specified currency was transferred to the correct recipient address as per the 'paymentRequestToken'. 3. Verifies the Client's signature on the request, ensuring the payer is who they claim to be (linking the payment action to the Client's DID). If all checks pass, the Receipt Service issues a Verifiable Credential (VC) serving as the payment receipt.` @@ -364,7 +370,7 @@ If all checks pass, the Receipt Service issues a Verifiable Credential (VC) serv ) const payload = { - paymentToken, + paymentRequestToken, paymentOptionId: paymentOption.id, metadata: { txHash: hash, @@ -397,7 +403,7 @@ If all checks pass, the Receipt Service issues a Verifiable Credential (VC) serv async function performStripePayment( _client: KeypairInfo, paymentOption: PaymentRequest["paymentOptions"][number], - paymentToken: JwtString + paymentRequestToken: JwtString ) { const paymentServiceUrl = paymentOption.paymentService if (!paymentServiceUrl) { @@ -429,7 +435,7 @@ This flow is simulated in this example. method: "POST", body: JSON.stringify({ paymentOptionId: paymentOption.id, - paymentToken + paymentRequestToken }) }) @@ -468,7 +474,7 @@ This flow is simulated in this example. method: "POST", body: JSON.stringify({ paymentOptionId: paymentOption.id, - paymentToken, + paymentRequestToken, metadata: { eventId: "evt_" + Math.random().toString(36).substring(7) // Simulated Stripe event ID } diff --git a/demos/payments/src/payment-service.ts b/demos/payments/src/payment-service.ts index 1f9e9f7..cf93f2d 100644 --- a/demos/payments/src/payment-service.ts +++ b/demos/payments/src/payment-service.ts @@ -1,7 +1,11 @@ import { serve } from "@hono/node-server" import { logger } from "@repo/api-utils/middleware/logger" import { colors, errorMessage, log } from "@repo/cli-tools" -import { createJwt, getDidResolver, verifyPaymentToken } from "agentcommercekit" +import { + createJwt, + getDidResolver, + verifyPaymentRequestToken +} from "agentcommercekit" import { jwtStringSchema } from "agentcommercekit/schemas/valibot" import { Hono } from "hono" import { env } from "hono/adapter" @@ -21,7 +25,7 @@ app.use(logger()) const bodySchema = v.object({ paymentOptionId: v.string(), - paymentToken: jwtStringSchema + paymentRequestToken: jwtStringSchema }) const name = colors.green(colors.bold("[Payment Service]")) @@ -32,13 +36,13 @@ const name = colors.green(colors.bold("[Payment Service]")) * the payment can be completed. */ app.post("/", async (c): Promise> => { - const { paymentOptionId, paymentToken } = v.parse( + const { paymentOptionId, paymentRequestToken } = v.parse( bodySchema, await c.req.json() ) - // Verify the payment token and payment option are valid - await validatePaymentOption(paymentOptionId, paymentToken) + // Verify the payment request token and payment option are valid + await validatePaymentOption(paymentOptionId, paymentRequestToken) log(colors.dim(`${name} Generating Stripe payment URL ...`)) @@ -65,15 +69,15 @@ app.post( env(c).PAYMENT_SERVICE_PRIVATE_KEY_HEX ) - const { paymentOptionId, paymentToken, metadata } = v.parse( + const { paymentOptionId, paymentRequestToken, metadata } = v.parse( callbackSchema, await c.req.json() ) - // Verify the payment token and payment option are valid + // Verify the payment request token and payment option are valid const { paymentOption } = await validatePaymentOption( paymentOptionId, - paymentToken + paymentRequestToken ) const receiptServiceUrl = paymentOption.receiptService if (!receiptServiceUrl) { @@ -81,7 +85,7 @@ app.post( } const payload = { - paymentToken, + paymentRequestToken, paymentOptionId, metadata: { network: "stripe", @@ -117,14 +121,17 @@ app.post( async function validatePaymentOption( paymentOptionId: string, - paymentToken: JwtString + paymentRequestToken: JwtString ) { const didResolver = getDidResolver() - log(colors.dim(`${name} Verifying payment token...`)) - const { paymentRequest } = await verifyPaymentToken(paymentToken, { - resolver: didResolver - }) + log(colors.dim(`${name} Verifying payment request token...`)) + const { paymentRequest } = await verifyPaymentRequestToken( + paymentRequestToken, + { + resolver: didResolver + } + ) log(colors.dim(`${name} Checking for payment option...`)) const paymentOption = paymentRequest.paymentOptions.find( diff --git a/demos/payments/src/receipt-service.ts b/demos/payments/src/receipt-service.ts index ab388ad..62e5700 100644 --- a/demos/payments/src/receipt-service.ts +++ b/demos/payments/src/receipt-service.ts @@ -14,7 +14,7 @@ import { parseJwtCredential, signCredential, verifyJwt, - verifyPaymentToken + verifyPaymentRequestToken } from "agentcommercekit" import { caip2ChainIdSchema } from "agentcommercekit/schemas/valibot" import { Hono } from "hono" @@ -48,7 +48,7 @@ const paymentDetailsSchema = v.object({ }) ]), payerDid: v.string(), - paymentToken: v.string() + paymentRequestToken: v.string() }) /** @@ -87,10 +87,10 @@ app.post("/", async (c) => { log(colors.dim("Payment details:")) logJson(paymentDetails, colors.cyan) - log(colors.dim("Verifying payment token...")) - // Verify the payment token is not expired, etc. - const { paymentRequest } = await verifyPaymentToken( - paymentDetails.paymentToken, + log(colors.dim("Verifying payment request token...")) + // Verify the payment request token is not expired, etc. + const { paymentRequest } = await verifyPaymentRequestToken( + paymentDetails.paymentRequestToken, { resolver: didResolver } @@ -120,7 +120,7 @@ app.post("/", async (c) => { log(colors.dim("\nCreating payment receipt...")) const receipt = createPaymentReceipt({ - paymentToken: paymentDetails.paymentToken, + paymentRequestToken: paymentDetails.paymentRequestToken, paymentOptionId: paymentOption.id, issuer: serverIdentity.did, payerDid: parsed.issuer diff --git a/demos/payments/src/server.ts b/demos/payments/src/server.ts index 8091d40..4ab38d4 100644 --- a/demos/payments/src/server.ts +++ b/demos/payments/src/server.ts @@ -2,7 +2,7 @@ import { serve } from "@hono/node-server" import { logger } from "@repo/api-utils/middleware/logger" import { colors, errorMessage, log, successMessage } from "@repo/cli-tools" import { - createPaymentRequestResponse, + createSignedPaymentRequest, curveToJwtAlgorithm, getDidResolver, verifyPaymentReceipt @@ -83,7 +83,7 @@ app.get("/", async (c): Promise> => { } // Generate the ACK-Pay Payment Request, which is a signed JWT detailing what needs to be paid. - const paymentRequest402Response = await createPaymentRequestResponse( + const paymentRequestBody = await createSignedPaymentRequest( paymentRequestInit, { issuer: serverIdentity.did, @@ -92,8 +92,15 @@ app.get("/", async (c): Promise> => { } ) + const res = new Response(JSON.stringify(paymentRequestBody), { + status: 402, + headers: { + "Content-Type": "application/json" + } + }) + log(successMessage("Payment request generated")) - throw new HTTPException(402, { res: paymentRequest402Response }) + throw new HTTPException(402, { res }) } try { @@ -103,7 +110,7 @@ app.get("/", async (c): Promise> => { resolver: didResolver, trustedReceiptIssuers, paymentRequestIssuer: serverIdentity.did, - verifyPaymentTokenJwt: true + verifyPaymentRequestTokenJwt: true }) } catch (e) { console.log(errorMessage("Error verifying receipt"), e) diff --git a/docs/ack-pay/payment-request-payload.mdx b/docs/ack-pay/payment-request-payload.mdx index d926dfe..2ee463f 100644 --- a/docs/ack-pay/payment-request-payload.mdx +++ b/docs/ack-pay/payment-request-payload.mdx @@ -52,29 +52,29 @@ Every Payment Request payload contains essential properties: }, // Signed token covering the paymentRequest structure above - "paymentToken": "signed-representation-of-the-entire-object" + "paymentRequestToken": "signed-representation-of-the-entire-object" } ``` ## Key Fields Explained -| Field | Type / Status | Description | -| :------------------- | :----------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `paymentToken` | String / Required | A cryptographic token (e.g., JWT) generated by the Server, signing the `paymentRequest` payload to ensure integrity and authenticity. This token is verified by several participants in the protocol. | -| `paymentRequest` | Object / Required | The main payment request object containing all payment details. | -| ` id` | String / Required | A unique identifier generated for this specific payment request. Used for tracking and preventing replay attacks. | -| ` expiresAt` | String / Optional | An ISO 8601 timestamp indicating when this payment request becomes invalid. Clients should not attempt payment after this time. | -| ` description` | String / Optional | A human-readable description of the service or reason for the payment, useful for display or logging. | -| ` serviceCallback` | String / Optional | A URL or DID URI that the Payment Service or Receipt Service can use to notify the original Server upon successful payment confirmation. Enables asynchronous workflows. | -| ` paymentOptions` | Array / Required | An array containing one or more objects, each defining a distinct payment option containing the fields below. | -| ` id` | String / Required | A server-defined identifier for this specific payment option. | -| ` currency` | String / Required | The currency or token symbol required for this option (e.g., "USD", "USDC"). | -| ` network` | String / Optional | An identifier for the required payment network (e.g., "stripe", "eip155:8453"). Helps the Client select a compatible method. | -| ` amount` | Integer / Required | The required payment amount expressed as an integer in the smallest unit of the currency (e.g., cents for USD, atomic units for tokens). | -| ` decimals` | Integer / Required | The number of decimal places for the specified `currency`, used to interpret the `amount`. | -| ` recipient` | String / Required | Identifier for the entity receiving payment for this option (e.g., pay-in address/account ID for the `paymentService`). Format may vary by network. | -| ` paymentService` | String / Optional | The URL or DID URI endpoint of the Payment Service designated to handle execution for this specific option. | -| ` receiptService` | String / Optional | The URL or DID URI endpoint of the Receipt Service designated to issue/verify receipts for payments made using this option. | +| Field | Type / Status | Description | +| :-------------------- | :----------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `paymentRequestToken` | String / Required | A cryptographic token (e.g., JWT) generated by the Server, signing the `paymentRequest` payload to ensure integrity and authenticity. This token is verified by several participants in the protocol. | +| `paymentRequest` | Object / Required | The main payment request object containing all payment details. | +| ` id` | String / Required | A unique identifier generated for this specific payment request. Used for tracking and preventing replay attacks. | +| ` expiresAt` | String / Optional | An ISO 8601 timestamp indicating when this payment request becomes invalid. Clients should not attempt payment after this time. | +| ` description` | String / Optional | A human-readable description of the service or reason for the payment, useful for display or logging. | +| ` serviceCallback` | String / Optional | A URL or DID URI that the Payment Service or Receipt Service can use to notify the original Server upon successful payment confirmation. Enables asynchronous workflows. | +| ` paymentOptions` | Array / Required | An array containing one or more objects, each defining a distinct payment option containing the fields below. | +| ` id` | String / Required | A server-defined identifier for this specific payment option. | +| ` currency` | String / Required | The currency or token symbol required for this option (e.g., "USD", "USDC"). | +| ` network` | String / Optional | An identifier for the required payment network (e.g., "stripe", "eip155:8453"). Helps the Client select a compatible method. | +| ` amount` | Integer / Required | The required payment amount expressed as an integer in the smallest unit of the currency (e.g., cents for USD, atomic units for tokens). | +| ` decimals` | Integer / Required | The number of decimal places for the specified `currency`, used to interpret the `amount`. | +| ` recipient` | String / Required | Identifier for the entity receiving payment for this option (e.g., pay-in address/account ID for the `paymentService`). Format may vary by network. | +| ` paymentService` | String / Optional | The URL or DID URI endpoint of the Payment Service designated to handle execution for this specific option. | +| ` receiptService` | String / Optional | The URL or DID URI endpoint of the Receipt Service designated to issue/verify receipts for payments made using this option. | ## Payment Service Abstraction diff --git a/docs/ack-pay/receipt-verification.mdx b/docs/ack-pay/receipt-verification.mdx index 29ed119..39ac7ff 100644 --- a/docs/ack-pay/receipt-verification.mdx +++ b/docs/ack-pay/receipt-verification.mdx @@ -29,7 +29,7 @@ _Example ACK Receipt Verifiable Credential:_ "credentialSubject": { // ID of the entity that made the payment (Client Agent DID) "id": "did:key:z6MkmCJAZansQ3d7Mi6moQoAFj6vpuPP9e4vWKsjWEY4Hd9t", - "paymentToken": "", + "paymentRequestToken": "", "paymentOptionId": "unique-payment-option-id-123", // The payment option that was used to make the payment "metadata": { // Additional metadata about the payment, depending on the payment option @@ -83,16 +83,22 @@ When an ACK Receipt is presented by a Client Agent to a Server Agent (e.g., when (e.g., querying a status list endpoint). - Examine the claims within `credentialSubject.paymentToken`, + Examine the claims within `credentialSubject.paymentRequestToken`, `credentialSubject.paymentOptionId`, and `credentialSubject.metadata` to - ensure they match the service requirements: - Is the original `paymentToken` - valid and correctly signed? - Is the `paymentOptionId` valid and matches an - expected payment option? - Is the `recipient` correct (e.g., does it match - the Server Agent's expected identifier)? - Is the `amount`, `currency`, and - `decimals` sufficient for the requested service? - Does the `id` match the - one originally issued (if applicable in a Server-Initiated flow)? - Are the - credential `issuanceDate` and settlement `timestamp` values recent enough, - or has the receipt potentially expired based on policy? + ensure they match the service requirements: + + - Is the original `paymentRequestToken` valid and correctly signed? + + - Is the `paymentOptionId` valid and matches an expected payment option? + + - Is the `recipient` correct (e.g., does it match the Server Agent's expected identifier)? + + - Is the `amount`, `currency`, and `decimals` sufficient for the requested service? + + - Does the `id` match the one originally issued (if applicable in a Server-Initiated flow)? + + - Are the credential `issuanceDate` and settlement `timestamp` values recent enough, or has the receipt potentially expired based on policy? + If all these checks pass, the Server can confidently treat the payment as valid diff --git a/docs/demos/example-identity.mdx b/docs/demos/example-identity.mdx index 6e06ded..bffaf4d 100644 --- a/docs/demos/example-identity.mdx +++ b/docs/demos/example-identity.mdx @@ -122,7 +122,7 @@ curl --request POST \ --data '{ "metadata": { "txHash": "0x123abc456def" }, "payerDid": "did:web:0.0.0.0%3A3458:wallet", - "paymentToken": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...", + "paymentRequestToken": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...", "paymentOptionId": "option1" }' ``` diff --git a/examples/issuer/README.md b/examples/issuer/README.md index 03c9ac1..7855768 100644 --- a/examples/issuer/README.md +++ b/examples/issuer/README.md @@ -169,7 +169,7 @@ SignedPayload<{ txHash: "0x123..." } payerDid: "did:..." - paymentToken: "jwt-token" + paymentRequestToken: "jwt-token" paymentOptionId: "option-id" }> ``` @@ -200,7 +200,7 @@ curl --request POST \ "txHash": "0x123abc456def" }, "payerDid": "did:web:0.0.0.0%3A3458:wallet", - "paymentToken": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...", + "paymentRequestToken": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...", "paymentOptionId": "option1" }' ``` @@ -234,9 +234,9 @@ curl --request GET \ Revokes a payment receipt credential by flipping the bit on the credential's Status List. -For demo purposes, we only allow the original payment token issuer to revoke the receipt. +For demo purposes, we only allow the original payment request token issuer to revoke the receipt. -**Request Payload**, signed by the original payment token issuer +**Request Payload**, signed by the original payment request token issuer ```ts SignedPayload<{ diff --git a/examples/issuer/src/routes/receipts.test.ts b/examples/issuer/src/routes/receipts.test.ts index d92383c..2fa75f9 100644 --- a/examples/issuer/src/routes/receipts.test.ts +++ b/examples/issuer/src/routes/receipts.test.ts @@ -3,10 +3,10 @@ import { bytesToHexString, createJwt, createPaymentReceipt, - createPaymentRequestBody, + createSignedPaymentRequest, curveToJwtAlgorithm, getDidResolver, - verifyPaymentToken + verifyPaymentRequestToken } from "agentcommercekit" import { credentialSchema, @@ -26,7 +26,7 @@ vi.mock("agentcommercekit", async () => { const actual = await vi.importActual("agentcommercekit") return { ...actual, - verifyPaymentToken: vi.fn(), + verifyPaymentRequestToken: vi.fn(), getDidResolver: vi.fn() } }) @@ -56,7 +56,7 @@ vi.mock("@/db/queries/credentials", async () => { id, credentialType: "PaymentReceiptCredential", baseCredential: createPaymentReceipt({ - paymentToken: "test.payment.token", + paymentRequestToken: "test.payment.token", paymentOptionId: "test-payment-option-id", issuer: "did:web:issuer.example.com", payerDid: "did:web:payer.example.com" @@ -95,17 +95,15 @@ async function generatePayload( resourceServer: DidWithSigner, paymentService: DidWithSigner ) { - const { paymentToken, paymentRequest } = await createPaymentRequestBody( - paymentRequestInit, - { + const { paymentRequestToken, paymentRequest } = + await createSignedPaymentRequest(paymentRequestInit, { issuer: resourceServer.did, signer: resourceServer.signer, algorithm: curveToJwtAlgorithm(resourceServer.keypair.curve) - } - ) + }) const payload = { - paymentToken, + paymentRequestToken, paymentOptionId: paymentRequest.paymentOptions[0].id, metadata: { txHash: "test-tx-hash" @@ -154,7 +152,7 @@ describe("POST /credentials/receipts", () => { process.env.ISSUER_PRIVATE_KEY = bytesToHexString(issuer.keypair.privateKey) process.env.BASE_URL = "https://issuer.example.com" - vi.mocked(verifyPaymentToken).mockResolvedValue({ + vi.mocked(verifyPaymentRequestToken).mockResolvedValue({ paymentRequest, // @ts-expect-error - Not a full parsed JWT parsed: { @@ -224,7 +222,7 @@ describe("POST /credentials/receipts", () => { it("validates the parsed payload", async () => { const signedPayload = await createJwt( { - paymentToken: "test.jwt.token", + paymentRequestToken: "test.jwt.token", paymentOptionId: "test-payment-option-id", metadata: { txHash: "test-tx-hash" @@ -296,7 +294,7 @@ describe("DELETE /credentials/receipts", () => { process.env.ISSUER_PRIVATE_KEY = bytesToHexString(issuer.keypair.privateKey) process.env.BASE_URL = "https://issuer.example.com" - vi.mocked(verifyPaymentToken).mockResolvedValue({ + vi.mocked(verifyPaymentRequestToken).mockResolvedValue({ paymentRequest, // @ts-expect-error - Not a full parsed JWT parsed: { @@ -379,13 +377,13 @@ describe("DELETE /credentials/receipts", () => { it("throws an error if stored credential is invalid", async () => { const invalidCredential = createPaymentReceipt({ - paymentToken: "test.payment.token", + paymentRequestToken: "test.payment.token", paymentOptionId: "test-payment-option-id", issuer: "did:web:issuer.example.com", payerDid: "did:web:payer.example.com" }) - delete invalidCredential.credentialSubject.paymentToken + delete invalidCredential.credentialSubject.paymentRequestToken vi.mocked(getCredential).mockResolvedValueOnce({ id: 1, diff --git a/examples/issuer/src/routes/receipts.ts b/examples/issuer/src/routes/receipts.ts index a9b7f50..4268bc5 100644 --- a/examples/issuer/src/routes/receipts.ts +++ b/examples/issuer/src/routes/receipts.ts @@ -8,7 +8,7 @@ import { signedPayloadValidator } from "@repo/api-utils/middleware/signed-payloa import { createPaymentReceipt, isPaymentReceiptCredential, - verifyPaymentToken + verifyPaymentRequestToken } from "agentcommercekit" import { didUriSchema } from "agentcommercekit/schemas/valibot" import { Hono } from "hono" @@ -39,7 +39,7 @@ const bodySchema = v.object({ txHash: v.string() }), payerDid: didUriSchema, - paymentToken: v.string(), + paymentRequestToken: v.string(), paymentOptionId: v.string() }) @@ -71,7 +71,7 @@ async function verifyPayment( * txHash: string // Transaction hash of the payment * }, * payerDid: string, // DID URI of the payer - * paymentToken: string, // Signed payment token + * paymentRequestToken: string, // Signed payment request token * paymentOptionId: string // ID of the payment option used * } * ``` @@ -93,11 +93,15 @@ app.post( const resolver = c.get("resolver") const { BASE_URL } = env(c) - const { paymentToken, paymentOptionId, metadata, payerDid } = payload.body + const { paymentRequestToken, paymentOptionId, metadata, payerDid } = + payload.body - const { paymentRequest } = await verifyPaymentToken(paymentToken, { - resolver - }) + const { paymentRequest } = await verifyPaymentRequestToken( + paymentRequestToken, + { + resolver + } + ) const verified = await verifyPayment( paymentRequest, @@ -110,7 +114,7 @@ app.post( } const receipt = createPaymentReceipt({ - paymentToken, + paymentRequestToken, paymentOptionId, issuer: issuer.did, payerDid @@ -180,9 +184,9 @@ const deleteBodySchema = v.object({ * * @description * Revokes a payment receipt credential by flipping the bit on the credential's Status List. - * For demo purposes, we only allow the original payment token issuer to revoke the receipt. + * For demo purposes, we only allow the original payment request token issuer to revoke the receipt. * - * Request Body, signed by the original payment token issuer: + * Request Body, signed by the original payment request token issuer: * ```ts * SignedPayload<{ * id: string // ID of the receipt credential to revoke @@ -211,14 +215,14 @@ app.delete( return internalServerError("Invalid stored credential") } - const { parsed } = await verifyPaymentToken( - credential.credentialSubject.paymentToken, + const { parsed } = await verifyPaymentRequestToken( + credential.credentialSubject.paymentRequestToken, { resolver } ) - // For now, only allows the original issuer of the payment token + // For now, only allows the original issuer of the payment request token // to revoke the receipt. if (parsed.issuer !== payload.issuer) { return unauthorized() diff --git a/packages/ack-pay/README.md b/packages/ack-pay/README.md index c14e895..669e3e3 100644 --- a/packages/ack-pay/README.md +++ b/packages/ack-pay/README.md @@ -19,10 +19,7 @@ pnpm add @agentcommercekit/ack-pay ### Creating a Payment Request ```ts -import { - createPaymentRequestBody, - createPaymentRequestResponse -} from "@agentcommercekit/ack-pay" +import { createPaymentRequestBody } from "@agentcommercekit/ack-pay" import { createDidWebUri } from "@agentcommercekit/did" import { createJwtSigner, curveToJwtAlgorithm } from "@agentcommercekit/jwt" import { generateKeypair } from "@agentcommercekit/keys" @@ -46,17 +43,16 @@ const paymentRequest = { const keypair = await generateKeypair("secp256k1") // Create a payment request body with a signed token -const paymentBody = await createPaymentRequestBody(paymentRequest, { +const paymentRequestBody = await createPaymentRequestBody(paymentRequest, { issuer: createDidWebUri("https://server.example.com"), signer: createJwtSigner(keypair), algorithm: curveToJwtAlgorithm(keypair.curve) }) // Create a 402 Payment Required response -const response = await createPaymentRequestResponse(paymentRequest, { - issuer: createDidWebUri("https://server.example.com"), - signer: createJwtSigner(keypair), - algorithm: curveToJwtAlgorithm(keypair.curve) +const response = new Response(JSON.stringify(paymentRequestBody, { + status: 402, + contentType: "application/json" }) ``` @@ -66,7 +62,7 @@ const response = await createPaymentRequestResponse(paymentRequest, { import { createPaymentReceipt } from "@agentcommercekit/ack-pay" const receipt = createPaymentReceipt({ - paymentToken: "", + paymentRequestToken: "", paymentOptionId: "", issuer: "did:web:receipt-service.example.com", payerDid: "did:web:customer.example.com" @@ -105,13 +101,12 @@ isPaymentReceiptClaim(credential.credentialSubject) ### Payment Requests - `createPaymentRequestBody(params, options)` - Creates a payment request with a signed JWT token -- `createPaymentRequestResponse(params, options)` - Creates a HTTP 402 Response with payment request - `isPaymentRequest(value)` - Type guard for payment requests -### Payment Tokens +### Payment Request Tokens -- `createPaymentToken(paymentRequest, options)` - Creates a signed JWT token for a payment request -- `verifyPaymentToken(token, options)` - Verifies a payment token JWT +- `createPaymentRequestToken(paymentRequest, options)` - Creates a signed JWT token for a payment request +- `verifyPaymentRequestToken(token, options)` - Verifies a payment request token JWT ### Payment Receipts diff --git a/packages/ack-pay/src/create-payment-receipt.test.ts b/packages/ack-pay/src/create-payment-receipt.test.ts index ef3e25b..782ee58 100644 --- a/packages/ack-pay/src/create-payment-receipt.test.ts +++ b/packages/ack-pay/src/create-payment-receipt.test.ts @@ -3,12 +3,12 @@ import { createJwtSigner, curveToJwtAlgorithm } from "@agentcommercekit/jwt" import { generateKeypair } from "@agentcommercekit/keys/ed25519" import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest" import { createPaymentReceipt } from "./create-payment-receipt" -import { createPaymentRequestBody } from "./create-payment-request-body" +import { createSignedPaymentRequest } from "./create-signed-payment-request" import type { PaymentRequestInit } from "./payment-request" describe("createPaymentReceipt", () => { const date = new Date("2024-12-31T23:59:59Z") - let paymentToken: string + let paymentRequestToken: string beforeAll(() => { vi.setSystemTime(date) @@ -31,18 +31,21 @@ describe("createPaymentReceipt", () => { ] } - const paymentRequiredBody = await createPaymentRequestBody(paymentRequest, { - issuer: createDidKeyUri(keypair), - signer: createJwtSigner(keypair), - algorithm: curveToJwtAlgorithm(keypair.curve) - }) + const paymentRequiredBody = await createSignedPaymentRequest( + paymentRequest, + { + issuer: createDidKeyUri(keypair), + signer: createJwtSigner(keypair), + algorithm: curveToJwtAlgorithm(keypair.curve) + } + ) - paymentToken = paymentRequiredBody.paymentToken + paymentRequestToken = paymentRequiredBody.paymentRequestToken }) it("creates a payment receipt with valid inputs", () => { const receipt = createPaymentReceipt({ - paymentToken, + paymentRequestToken, paymentOptionId: "test-payment-option-id", issuer: "did:example:issuer", payerDid: "did:example:payer" @@ -56,14 +59,14 @@ describe("createPaymentReceipt", () => { issuanceDate: date.toISOString(), credentialSubject: { id: "did:example:payer", - paymentToken + paymentRequestToken } }) }) it("allows passing metadata for inclusion in the attestation", () => { const receipt = createPaymentReceipt({ - paymentToken, + paymentRequestToken, paymentOptionId: "test-payment-option-id", issuer: "did:example:issuer", payerDid: "did:example:payer", @@ -80,7 +83,7 @@ describe("createPaymentReceipt", () => { issuanceDate: date.toISOString(), credentialSubject: { id: "did:example:payer", - paymentToken, + paymentRequestToken, metadata: { test: "test" } @@ -91,7 +94,7 @@ describe("createPaymentReceipt", () => { it("creates a payment receipt with an expiration date", () => { const expirationDate = new Date("2024-12-31T23:59:59Z") const receipt = createPaymentReceipt({ - paymentToken, + paymentRequestToken, paymentOptionId: "test-payment-option-id", issuer: "did:example:issuer", payerDid: "did:example:payer", diff --git a/packages/ack-pay/src/create-payment-receipt.ts b/packages/ack-pay/src/create-payment-receipt.ts index a4d9407..aafef5a 100644 --- a/packages/ack-pay/src/create-payment-receipt.ts +++ b/packages/ack-pay/src/create-payment-receipt.ts @@ -5,7 +5,7 @@ import type { W3CCredential } from "@agentcommercekit/vc" const PAYMENT_RECEIPT_TYPE = "PaymentReceiptCredential" interface CreatePaymentReceiptParams { - paymentToken: string + paymentRequestToken: string paymentOptionId: string issuer: DidUri payerDid: DidUri @@ -20,7 +20,7 @@ interface CreatePaymentReceiptParams { * @returns A {@link W3CCredential} with a payment receipt attestation */ export function createPaymentReceipt({ - paymentToken, + paymentRequestToken, paymentOptionId, issuer, payerDid, @@ -28,7 +28,7 @@ export function createPaymentReceipt({ metadata }: CreatePaymentReceiptParams): W3CCredential { const attestation: Record = { - paymentToken, + paymentRequestToken, paymentOptionId } diff --git a/packages/ack-pay/src/create-payment-request-body.ts b/packages/ack-pay/src/create-payment-request-body.ts deleted file mode 100644 index 4928f2a..0000000 --- a/packages/ack-pay/src/create-payment-request-body.ts +++ /dev/null @@ -1,53 +0,0 @@ -import * as v from "valibot" -import { createPaymentToken } from "./create-payment-token" -import { paymentRequestSchema } from "./schemas/valibot" -import type { PaymentTokenOptions } from "./create-payment-token" -import type { PaymentRequestInit } from "./payment-request" -import type { paymentRequestBodySchema } from "./schemas/valibot" - -export type PaymentRequestBody = v.InferOutput - -/** - * Create a payment request body - * - * @param params - The payment config params, including the amount, currency, and recipient - * @param options - The {@link PaymentTokenOptions} to use - * @returns A payment request body - */ -export async function createPaymentRequestBody( - paymentRequestInit: PaymentRequestInit, - { issuer, signer, algorithm }: PaymentTokenOptions -): Promise { - const paymentRequest = v.parse(paymentRequestSchema, paymentRequestInit) - const paymentToken = await createPaymentToken(paymentRequest, { - issuer, - signer, - algorithm - }) - - return { - paymentRequest, - paymentToken - } -} - -/** - * Create a 402 `Response` object with the payment request body - * - * @param params - The payment config params - * @param options - The {@link PaymentTokenOptions} to use - * @returns A 402 `Response` object with the payment request body - */ -export async function createPaymentRequestResponse( - params: PaymentRequestInit, - options: PaymentTokenOptions -): Promise { - const paymentRequiredBody = await createPaymentRequestBody(params, options) - - return new Response(JSON.stringify(paymentRequiredBody), { - status: 402, - headers: { - "Content-Type": "application/json" - } - }) -} diff --git a/packages/ack-pay/src/create-payment-token.test.ts b/packages/ack-pay/src/create-payment-request-token.test.ts similarity index 65% rename from packages/ack-pay/src/create-payment-token.test.ts rename to packages/ack-pay/src/create-payment-request-token.test.ts index 6514173..d57d21b 100644 --- a/packages/ack-pay/src/create-payment-token.test.ts +++ b/packages/ack-pay/src/create-payment-request-token.test.ts @@ -12,14 +12,14 @@ import { import { generateKeypair } from "@agentcommercekit/keys" import * as v from "valibot" import { beforeEach, describe, expect, it } from "vitest" -import { createPaymentToken } from "./create-payment-token" +import { createPaymentRequestToken } from "./create-payment-request-token" import { paymentRequestSchema } from "./schemas/valibot" import type { PaymentRequestInit } from "./payment-request" import type { DidUri } from "@agentcommercekit/did" import type { JwtSigner } from "@agentcommercekit/jwt" import type { Keypair } from "@agentcommercekit/keys" -describe("createPaymentToken()", () => { +describe("createPaymentRequestToken()", () => { let keypair: Keypair let signer: JwtSigner let issuerDid: DidUri @@ -44,22 +44,28 @@ describe("createPaymentToken()", () => { issuerDid = createDidKeyUri(keypair) }) - it("generates a paymentToken", async () => { - const paymentToken = await createPaymentToken(paymentRequest, { - issuer: issuerDid, - signer, - algorithm: curveToJwtAlgorithm(keypair.curve) - }) + it("generates a paymentRequestToken", async () => { + const paymentRequestToken = await createPaymentRequestToken( + paymentRequest, + { + issuer: issuerDid, + signer, + algorithm: curveToJwtAlgorithm(keypair.curve) + } + ) - expect(isJwtString(paymentToken)).toBe(true) + expect(isJwtString(paymentRequestToken)).toBe(true) }) - it("generates a valid jwt payment token", async () => { - const paymentToken = await createPaymentToken(paymentRequest, { - issuer: issuerDid, - signer, - algorithm: curveToJwtAlgorithm(keypair.curve) - }) + it("generates a valid jwt payment request token", async () => { + const paymentRequestToken = await createPaymentRequestToken( + paymentRequest, + { + issuer: issuerDid, + signer, + algorithm: curveToJwtAlgorithm(keypair.curve) + } + ) const resolver = getDidResolver() resolver.addToCache( @@ -71,8 +77,8 @@ describe("createPaymentToken()", () => { ) // Verify the JWT is valid (disable audience validation) - // TODO: Use parsePaymentToken when it returns the issuer - const result = await verifyJwt(paymentToken, { + // TODO: Use parsePaymentRequestToken when it returns the issuer + const result = await verifyJwt(paymentRequestToken, { resolver }) diff --git a/packages/ack-pay/src/create-payment-token.ts b/packages/ack-pay/src/create-payment-request-token.ts similarity index 55% rename from packages/ack-pay/src/create-payment-token.ts rename to packages/ack-pay/src/create-payment-request-token.ts index d10d0b3..456d085 100644 --- a/packages/ack-pay/src/create-payment-token.ts +++ b/packages/ack-pay/src/create-payment-request-token.ts @@ -3,31 +3,31 @@ import type { PaymentRequest } from "./payment-request" import type { DidUri } from "@agentcommercekit/did" import type { JwtAlgorithm, JwtSigner, JwtString } from "@agentcommercekit/jwt" -export interface PaymentTokenOptions { +export interface PaymentRequestTokenOptions { /** - * The issuer of the payment token + * The issuer of the payment request token */ issuer: DidUri /** - * The signer of the payment token + * The signer of the payment request token */ signer: JwtSigner /** - * The algorithm of the payment token + * The algorithm of the payment request token */ algorithm: JwtAlgorithm } /** - * Builds a signed JWT payment token for a given payment request + * Builds a signed JWT payment request token for a given payment request * - * @param paymentRequest - A valid PaymentRequest to create a payment token for - * @param options - The {@link PaymentTokenOptions} to use - * @returns A signed JWT payment token + * @param paymentRequest - A valid PaymentRequest to create a payment request token for + * @param options - The {@link PaymentRequestTokenOptions} to use + * @returns A signed JWT payment request token */ -export async function createPaymentToken( +export async function createPaymentRequestToken( paymentRequest: PaymentRequest, - { issuer, signer, algorithm }: PaymentTokenOptions + { issuer, signer, algorithm }: PaymentRequestTokenOptions ): Promise { return createJwt( { ...paymentRequest, sub: paymentRequest.id }, diff --git a/packages/ack-pay/src/create-payment-request-body.test.ts b/packages/ack-pay/src/create-signed-payment-request.test.ts similarity index 80% rename from packages/ack-pay/src/create-payment-request-body.test.ts rename to packages/ack-pay/src/create-signed-payment-request.test.ts index 4d505cb..8134fc5 100644 --- a/packages/ack-pay/src/create-payment-request-body.test.ts +++ b/packages/ack-pay/src/create-signed-payment-request.test.ts @@ -10,14 +10,14 @@ import { } from "@agentcommercekit/jwt" import { generateKeypair } from "@agentcommercekit/keys" import { beforeEach, describe, expect, it } from "vitest" -import { createPaymentRequestBody } from "./create-payment-request-body" -import { verifyPaymentToken } from "./verify-payment-token" +import { createSignedPaymentRequest } from "./create-signed-payment-request" +import { verifyPaymentRequestToken } from "./verify-payment-request-token" import type { PaymentRequestInit } from "./payment-request" import type { DidUri } from "@agentcommercekit/did" import type { JwtSigner } from "@agentcommercekit/jwt" import type { Keypair } from "@agentcommercekit/keys" -describe("createPaymentRequestBody()", () => { +describe("createSignedPaymentRequest()", () => { let keypair: Keypair let signer: JwtSigner let issuerDid: DidUri @@ -42,7 +42,7 @@ describe("createPaymentRequestBody()", () => { }) it("generates a valid 402 response", async () => { - const result = await createPaymentRequestBody(paymentRequest, { + const result = await createSignedPaymentRequest(paymentRequest, { issuer: issuerDid, signer, algorithm: curveToJwtAlgorithm(keypair.curve) @@ -61,11 +61,11 @@ describe("createPaymentRequestBody()", () => { ] }) - expect(isJwtString(result.paymentToken)).toBe(true) + expect(isJwtString(result.paymentRequestToken)).toBe(true) }) - it("generates a valid jwt payment token", async () => { - const body = await createPaymentRequestBody(paymentRequest, { + it("generates a valid jwt payment request token", async () => { + const body = await createSignedPaymentRequest(paymentRequest, { issuer: issuerDid, signer, algorithm: curveToJwtAlgorithm(keypair.curve) @@ -80,7 +80,7 @@ describe("createPaymentRequestBody()", () => { }) ) - const result = await verifyPaymentToken(body.paymentToken, { + const result = await verifyPaymentRequestToken(body.paymentRequestToken, { resolver }) @@ -94,7 +94,7 @@ describe("createPaymentRequestBody()", () => { it("includes expiresAt in ISO string format when provided", async () => { const expiresAt = new Date("2024-12-31T23:59:59Z") - const result = await createPaymentRequestBody( + const result = await createSignedPaymentRequest( { ...paymentRequest, expiresAt }, { issuer: issuerDid, diff --git a/packages/ack-pay/src/create-signed-payment-request.ts b/packages/ack-pay/src/create-signed-payment-request.ts new file mode 100644 index 0000000..83d667d --- /dev/null +++ b/packages/ack-pay/src/create-signed-payment-request.ts @@ -0,0 +1,49 @@ +import * as v from "valibot" +import { createPaymentRequestToken } from "./create-payment-request-token" +import { paymentRequestSchema } from "./schemas/valibot" +import type { PaymentRequestTokenOptions } from "./create-payment-request-token" +import type { PaymentRequest, PaymentRequestInit } from "./payment-request" +import type { JwtString } from "@agentcommercekit/jwt" + +/** + * Create a signed payment request + * + * @param params - The payment config params, including the amount, currency, and recipient + * @param options - The {@link PaymentRequestTokenOptions} to use + * @returns A payment request with a signed token + */ +export async function createSignedPaymentRequest( + paymentRequestInit: PaymentRequestInit, + { issuer, signer, algorithm }: PaymentRequestTokenOptions +): Promise<{ + paymentRequest: PaymentRequest + paymentRequestToken: JwtString +}> { + const paymentRequest = v.parse(paymentRequestSchema, paymentRequestInit) + const paymentRequestToken = await createPaymentRequestToken(paymentRequest, { + issuer, + signer, + algorithm + }) + + return { + paymentRequest, + paymentRequestToken + } +} + +/** + * @deprecated Use {@link createSignedPaymentRequest} instead + */ +export async function createPaymentRequestBody( + ...args: Parameters +): Promise<{ + paymentRequest: PaymentRequest + paymentToken: string +}> { + const result = await createSignedPaymentRequest(...args) + return { + paymentRequest: result.paymentRequest, + paymentToken: result.paymentRequestToken + } +} diff --git a/packages/ack-pay/src/errors.ts b/packages/ack-pay/src/errors.ts index e6c703f..3765649 100644 --- a/packages/ack-pay/src/errors.ts +++ b/packages/ack-pay/src/errors.ts @@ -1,6 +1,6 @@ -export class InvalidPaymentTokenError extends Error { - constructor(message = "Invalid payment token") { +export class InvalidPaymentRequestTokenError extends Error { + constructor(message = "Invalid payment request token") { super(message) - this.name = "InvalidPaymentTokenError" + this.name = "InvalidPaymentRequestTokenError" } } diff --git a/packages/ack-pay/src/index.ts b/packages/ack-pay/src/index.ts index 7552499..bd4091f 100644 --- a/packages/ack-pay/src/index.ts +++ b/packages/ack-pay/src/index.ts @@ -1,8 +1,8 @@ export * from "./create-payment-receipt" -export * from "./create-payment-token" +export * from "./create-payment-request-token" export * from "./errors" -export * from "./create-payment-request-body" -export * from "./verify-payment-token" +export * from "./create-signed-payment-request" +export * from "./verify-payment-request-token" export * from "./payment-request" export * from "./receipt-claim-verifier" export * from "./verify-payment-receipt" diff --git a/packages/ack-pay/src/receipt-claim-verifier.test.ts b/packages/ack-pay/src/receipt-claim-verifier.test.ts index 6b50fa0..563efb8 100644 --- a/packages/ack-pay/src/receipt-claim-verifier.test.ts +++ b/packages/ack-pay/src/receipt-claim-verifier.test.ts @@ -4,7 +4,7 @@ import { generateKeypair } from "@agentcommercekit/keys" import { InvalidCredentialSubjectError } from "@agentcommercekit/vc" import * as v from "valibot" import { describe, expect, it } from "vitest" -import { createPaymentToken } from "./create-payment-token" +import { createPaymentRequestToken } from "./create-payment-request-token" import { getReceiptClaimVerifier } from "./receipt-claim-verifier" import { paymentRequestSchema } from "./schemas/valibot" import type { paymentReceiptClaimSchema } from "./schemas/valibot" @@ -27,7 +27,7 @@ describe("getReceiptClaimVerifier", () => { const verifier = getReceiptClaimVerifier() const invalidSubject = { - paymentToken: null + paymentRequestToken: null } await expect(verifier.verify(invalidSubject, resolver)).rejects.toThrow( @@ -52,14 +52,17 @@ describe("getReceiptClaimVerifier", () => { ] }) - const paymentToken = await createPaymentToken(paymentRequest, { - issuer: issuerDid, - signer, - algorithm: curveToJwtAlgorithm(keypair.curve) - }) + const paymentRequestToken = await createPaymentRequestToken( + paymentRequest, + { + issuer: issuerDid, + signer, + algorithm: curveToJwtAlgorithm(keypair.curve) + } + ) const receiptSubject: v.InferOutput = { paymentOptionId: paymentRequest.paymentOptions[0].id, - paymentToken + paymentRequestToken } const verifier = getReceiptClaimVerifier() diff --git a/packages/ack-pay/src/schemas/valibot.ts b/packages/ack-pay/src/schemas/valibot.ts index 13882f8..75d5c76 100644 --- a/packages/ack-pay/src/schemas/valibot.ts +++ b/packages/ack-pay/src/schemas/valibot.ts @@ -1,4 +1,5 @@ import { didUriSchema } from "@agentcommercekit/did/schemas/valibot" +import { jwtStringSchema } from "@agentcommercekit/jwt/schemas/valibot" import * as v from "valibot" const urlOrDidUri = v.union([v.pipe(v.string(), v.url()), didUriSchema]) @@ -31,12 +32,7 @@ export const paymentRequestSchema = v.object({ }) export const paymentReceiptClaimSchema = v.object({ - paymentToken: v.string(), // Often a JwtString but not required + paymentRequestToken: jwtStringSchema, paymentOptionId: v.string(), metadata: v.optional(v.record(v.string(), v.unknown())) }) - -export const paymentRequestBodySchema = v.object({ - paymentRequest: paymentRequestSchema, - paymentToken: v.string() // Often a JwtString but not required -}) diff --git a/packages/ack-pay/src/schemas/zod/v3.ts b/packages/ack-pay/src/schemas/zod/v3.ts index 2c6dadc..c8649f1 100644 --- a/packages/ack-pay/src/schemas/zod/v3.ts +++ b/packages/ack-pay/src/schemas/zod/v3.ts @@ -1,4 +1,5 @@ import { didUriSchema } from "@agentcommercekit/did/schemas/zod/v3" +import { jwtStringSchema } from "@agentcommercekit/jwt/schemas/zod" import { z } from "zod/v3" const urlOrDidUri = z.union([z.string().url(), didUriSchema]) @@ -26,12 +27,7 @@ export const paymentRequestSchema = z.object({ }) export const paymentReceiptClaimSchema = z.object({ - paymentToken: z.string(), // Often a JwtString but not required + paymentRequestToken: jwtStringSchema, paymentOptionId: z.string(), metadata: z.record(z.string(), z.unknown()).optional() }) - -export const paymentRequestBodySchema = z.object({ - payment: paymentRequestSchema, - paymentToken: z.string() // Often a JwtString but not required -}) diff --git a/packages/ack-pay/src/schemas/zod/v4.ts b/packages/ack-pay/src/schemas/zod/v4.ts index e833934..3c6c290 100644 --- a/packages/ack-pay/src/schemas/zod/v4.ts +++ b/packages/ack-pay/src/schemas/zod/v4.ts @@ -1,4 +1,5 @@ import { didUriSchema } from "@agentcommercekit/did/schemas/zod/v4" +import { jwtStringSchema } from "@agentcommercekit/jwt/schemas/zod/v4" import * as z from "zod/v4" const urlOrDidUri = z.union([z.url(), didUriSchema]) @@ -26,12 +27,7 @@ export const paymentRequestSchema = z.object({ }) export const paymentReceiptClaimSchema = z.object({ - paymentToken: z.string(), // Often a JwtString but not required + paymentRequestToken: jwtStringSchema, paymentOptionId: z.string(), metadata: z.record(z.string(), z.unknown()).optional() }) - -export const paymentRequestBodySchema = z.object({ - payment: paymentRequestSchema, - paymentToken: z.string() // Often a JwtString but not required -}) diff --git a/packages/ack-pay/src/verify-payment-receipt.test.ts b/packages/ack-pay/src/verify-payment-receipt.test.ts index f3acae0..43009c8 100644 --- a/packages/ack-pay/src/verify-payment-receipt.test.ts +++ b/packages/ack-pay/src/verify-payment-receipt.test.ts @@ -12,8 +12,8 @@ import { } from "@agentcommercekit/vc" import { beforeEach, describe, expect, it } from "vitest" import { createPaymentReceipt } from "./create-payment-receipt" -import { createPaymentRequestBody } from "./create-payment-request-body" -import { InvalidPaymentTokenError } from "./errors" +import { createSignedPaymentRequest } from "./create-signed-payment-request" +import { InvalidPaymentRequestTokenError } from "./errors" import { verifyPaymentReceipt } from "./verify-payment-receipt" import type { PaymentRequestInit } from "./payment-request" import type { DidUri, Resolvable } from "@agentcommercekit/did" @@ -50,17 +50,15 @@ describe("verifyPaymentReceipt()", () => { ] } - const { paymentToken, paymentRequest } = await createPaymentRequestBody( - paymentRequestInit, - { + const { paymentRequestToken, paymentRequest } = + await createSignedPaymentRequest(paymentRequestInit, { issuer: paymentRequestIssuerDid, signer: createJwtSigner(paymentRequestIssuerKeypair), algorithm: curveToJwtAlgorithm(paymentRequestIssuerKeypair.curve) - } - ) + }) unsignedReceipt = createPaymentReceipt({ - paymentToken, + paymentRequestToken, paymentOptionId: paymentRequest.paymentOptions[0].id, issuer: receiptIssuerDid, payerDid: createDidPkhUri( @@ -80,7 +78,7 @@ describe("verifyPaymentReceipt()", () => { it("validates a JWT receipt string", async () => { const result = await verifyPaymentReceipt(signedReceiptJwt, { resolver }) expect(result.receipt).toBeDefined() - expect(result.paymentToken).toBeDefined() + expect(result.paymentRequestToken).toBeDefined() expect(result.paymentRequest).toBeDefined() }) @@ -89,7 +87,7 @@ describe("verifyPaymentReceipt()", () => { resolver }) expect(result.receipt).toBeDefined() - expect(result.paymentToken).toBeDefined() + expect(result.paymentRequestToken).toBeDefined() expect(result.paymentRequest).toBeDefined() }) @@ -102,7 +100,7 @@ describe("verifyPaymentReceipt()", () => { it("throws for invalid credential subject", async () => { const invalidCredential = { ...unsignedReceipt, - credentialSubject: { paymentToken: null } + credentialSubject: { paymentRequestToken: null } } await expect( @@ -111,33 +109,33 @@ describe("verifyPaymentReceipt()", () => { ).rejects.toThrow(InvalidCredentialError) }) - it("skips payment token verification when disabled", async () => { + it("skips payment request token verification when disabled", async () => { const result = await verifyPaymentReceipt(signedReceiptJwt, { resolver, - verifyPaymentTokenJwt: false + verifyPaymentRequestTokenJwt: false }) expect(result.receipt).toBeDefined() - expect(result.paymentToken).toBeDefined() + expect(result.paymentRequestToken).toBeDefined() expect(result.paymentRequest).toBeNull() }) - it("validates payment token issuer when specified", async () => { + it("validates payment request token issuer when specified", async () => { const result = await verifyPaymentReceipt(signedReceiptJwt, { resolver, paymentRequestIssuer: paymentRequestIssuerDid }) expect(result.receipt).toBeDefined() - expect(result.paymentToken).toBeDefined() + expect(result.paymentRequestToken).toBeDefined() expect(result.paymentRequest).toBeDefined() }) - it("throws for invalid payment token issuer", async () => { + it("throws for invalid payment request token issuer", async () => { await expect( verifyPaymentReceipt(signedReceiptJwt, { resolver, paymentRequestIssuer: "did:example:wrong-issuer" }) - ).rejects.toThrow(InvalidPaymentTokenError) + ).rejects.toThrow(InvalidPaymentRequestTokenError) }) it("validates trusted receipt issuers", async () => { @@ -146,7 +144,7 @@ describe("verifyPaymentReceipt()", () => { trustedReceiptIssuers: [receiptIssuerDid] }) expect(result.receipt).toBeDefined() - expect(result.paymentToken).toBeDefined() + expect(result.paymentRequestToken).toBeDefined() expect(result.paymentRequest).toBeDefined() }) diff --git a/packages/ack-pay/src/verify-payment-receipt.ts b/packages/ack-pay/src/verify-payment-receipt.ts index cfa575d..67bd06c 100644 --- a/packages/ack-pay/src/verify-payment-receipt.ts +++ b/packages/ack-pay/src/verify-payment-receipt.ts @@ -10,7 +10,7 @@ import { getReceiptClaimVerifier, isPaymentReceiptCredential } from "./receipt-claim-verifier" -import { verifyPaymentToken } from "./verify-payment-token" +import { verifyPaymentRequestToken } from "./verify-payment-request-token" import type { PaymentRequest } from "./payment-request" import type { Resolvable } from "@agentcommercekit/did" import type { JwtString } from "@agentcommercekit/jwt" @@ -26,11 +26,11 @@ interface VerifyPaymentReceiptOptions { */ trustedReceiptIssuers?: string[] /** - * Whether to verify the paymentToken as a JWT + * Whether to verify the paymentRequestToken as a JWT */ - verifyPaymentTokenJwt?: boolean + verifyPaymentRequestTokenJwt?: boolean /** - * The issuer of the paymentToken + * The issuer of the paymentRequestToken */ paymentRequestIssuer?: string } @@ -48,17 +48,17 @@ export async function verifyPaymentReceipt( resolver, trustedReceiptIssuers, paymentRequestIssuer, - verifyPaymentTokenJwt = true + verifyPaymentRequestTokenJwt = true }: VerifyPaymentReceiptOptions ): Promise< | { receipt: Verifiable - paymentToken: string + paymentRequestToken: string paymentRequest: null } | { receipt: Verifiable - paymentToken: JwtString + paymentRequestToken: JwtString paymentRequest: PaymentRequest } > { @@ -84,34 +84,40 @@ export async function verifyPaymentReceipt( verifiers: [getReceiptClaimVerifier()] }) - // Verify the paymentToken is a valid JWT - const paymentToken = parsedCredential.credentialSubject.paymentToken + // Verify the paymentRequestToken is a valid JWT + const paymentRequestToken = + parsedCredential.credentialSubject.paymentRequestToken - if (!verifyPaymentTokenJwt) { + if (!verifyPaymentRequestTokenJwt) { return { receipt: parsedCredential, - paymentToken, + paymentRequestToken, paymentRequest: null } } - if (!isJwtString(paymentToken)) { - throw new InvalidCredentialSubjectError("Payment token is not a JWT") + if (!isJwtString(paymentRequestToken)) { + throw new InvalidCredentialSubjectError( + "Payment Request token is not a JWT" + ) } - const { paymentRequest } = await verifyPaymentToken(paymentToken, { - resolver, - // We don't want to fail Receipt Verification if the paymentToken has - // expired, since the receipt lives longer than that - verifyExpiry: false, - // If the paymentRequestIssuer is provided, we want to verify that the - // payment token was issued by the same issuer. - issuer: paymentRequestIssuer - }) + const { paymentRequest } = await verifyPaymentRequestToken( + paymentRequestToken, + { + resolver, + // We don't want to fail Receipt Verification if the paymentRequestToken has + // expired, since the receipt lives longer than that + verifyExpiry: false, + // If the paymentRequestIssuer is provided, we want to verify that the + // payment request token was issued by the same issuer. + issuer: paymentRequestIssuer + } + ) return { receipt: parsedCredential, - paymentToken, + paymentRequestToken, paymentRequest } } diff --git a/packages/ack-pay/src/verify-payment-token.test.ts b/packages/ack-pay/src/verify-payment-request-token.test.ts similarity index 80% rename from packages/ack-pay/src/verify-payment-token.test.ts rename to packages/ack-pay/src/verify-payment-request-token.test.ts index 91175cf..f04ae28 100644 --- a/packages/ack-pay/src/verify-payment-token.test.ts +++ b/packages/ack-pay/src/verify-payment-request-token.test.ts @@ -10,8 +10,8 @@ import { } from "@agentcommercekit/jwt" import { generateKeypair } from "@agentcommercekit/keys" import { beforeEach, describe, expect, it } from "vitest" -import { createPaymentRequestBody } from "./create-payment-request-body" -import { verifyPaymentToken } from "./verify-payment-token" +import { createSignedPaymentRequest } from "./create-signed-payment-request" +import { verifyPaymentRequestToken } from "./verify-payment-request-token" import type { PaymentRequestInit } from "./payment-request" import type { DidDocument, DidUri } from "@agentcommercekit/did" import type { JwtSigner } from "@agentcommercekit/jwt" @@ -26,7 +26,7 @@ function cleanPaymentRequest(paymentRequest: PaymentRequestInit) { ) } -describe("verifyPaymentToken", () => { +describe("verifyPaymentRequestToken", () => { let keypair: Keypair let signer: JwtSigner let issuerDid: DidUri @@ -55,9 +55,9 @@ describe("verifyPaymentToken", () => { }) }) - it("verifies a valid payment token", async () => { - // Generate a valid payment token - const body = await createPaymentRequestBody(paymentRequest, { + it("verifies a valid payment request token", async () => { + // Generate a valid payment request token + const body = await createSignedPaymentRequest(paymentRequest, { issuer: issuerDid, signer, algorithm: curveToJwtAlgorithm(keypair.curve) @@ -67,7 +67,7 @@ describe("verifyPaymentToken", () => { resolver.addToCache(issuerDid, issuerDidDocument) // Verify the token - const result = await verifyPaymentToken(body.paymentToken, { + const result = await verifyPaymentRequestToken(body.paymentRequestToken, { resolver }) @@ -82,10 +82,10 @@ describe("verifyPaymentToken", () => { resolver.addToCache(issuerDid, issuerDidDocument) await expect( - verifyPaymentToken("invalid.jwt.token", { + verifyPaymentRequestToken("invalid.jwt.token", { resolver }) - ).rejects.toThrow("Invalid payment token") + ).rejects.toThrow("Invalid payment request token") }) it("throws for expired JWT", async () => { @@ -111,10 +111,10 @@ describe("verifyPaymentToken", () => { resolver.addToCache(issuerDid, issuerDidDocument) await expect( - verifyPaymentToken(expiredToken, { + verifyPaymentRequestToken(expiredToken, { resolver }) - ).rejects.toThrow("Invalid payment token") + ).rejects.toThrow("Invalid payment request token") }) it("allows expired JWT when expiry verification is disabled", async () => { @@ -140,7 +140,7 @@ describe("verifyPaymentToken", () => { resolver.addToCache(issuerDid, issuerDidDocument) // Verify with verifyExpiry set to false - const result = await verifyPaymentToken(expiredToken, { + const result = await verifyPaymentRequestToken(expiredToken, { resolver, verifyExpiry: false }) @@ -150,7 +150,7 @@ describe("verifyPaymentToken", () => { }) it("throws for JWT with invalid signature", async () => { - const body = await createPaymentRequestBody(paymentRequest, { + const body = await createSignedPaymentRequest(paymentRequest, { issuer: issuerDid, signer, algorithm: curveToJwtAlgorithm(keypair.curve) @@ -168,10 +168,10 @@ describe("verifyPaymentToken", () => { ) await expect( - verifyPaymentToken(body.paymentToken, { + verifyPaymentRequestToken(body.paymentRequestToken, { resolver }) - ).rejects.toThrow("Invalid payment token") + ).rejects.toThrow("Invalid payment request token") }) it("throws for a JWT that does not contain a payment config", async () => { @@ -188,9 +188,9 @@ describe("verifyPaymentToken", () => { resolver.addToCache(issuerDid, issuerDidDocument) await expect( - verifyPaymentToken(invalidToken, { + verifyPaymentRequestToken(invalidToken, { resolver }) - ).rejects.toThrow("Payment token is not a valid PaymentRequest") + ).rejects.toThrow("Payment Request token is not a valid PaymentRequest") }) }) diff --git a/packages/ack-pay/src/verify-payment-request-token.ts b/packages/ack-pay/src/verify-payment-request-token.ts new file mode 100644 index 0000000..a09c3f9 --- /dev/null +++ b/packages/ack-pay/src/verify-payment-request-token.ts @@ -0,0 +1,65 @@ +import { verifyJwt } from "@agentcommercekit/jwt" +import * as v from "valibot" +import { InvalidPaymentRequestTokenError } from "./errors" +import { paymentRequestSchema } from "./schemas/valibot" +import type { PaymentRequest } from "./payment-request" +import type { Resolvable } from "@agentcommercekit/did" +import type { JwtVerified } from "@agentcommercekit/jwt" + +interface ValidatePaymentRequestTokenOptions { + /** + * The resolver to use for did resolution + */ + resolver?: Resolvable + /** + * Whether to verify the expiry of the payment request token + */ + verifyExpiry?: boolean + /** + * The issuer to verify the payment request token against + */ + issuer?: string +} + +/** + * Verify a payment request token + * + * @param token - The payment request token to verify + * @param options - The {@link ValidatePaymentRequestTokenOptions} to use + * @returns The {@link PaymentRequest} parsed from the payment request token and the parsed JWT + */ +export async function verifyPaymentRequestToken( + token: string, + options: ValidatePaymentRequestTokenOptions = {} +): Promise<{ paymentRequest: PaymentRequest; parsed: JwtVerified }> { + let parsedPaymentRequestToken: JwtVerified + + try { + parsedPaymentRequestToken = await verifyJwt(token, { + resolver: options.resolver, + issuer: options.issuer, + policies: { + aud: false, + exp: options.verifyExpiry ?? true + } + }) + } catch (_err) { + throw new InvalidPaymentRequestTokenError() + } + + const { success, output } = v.safeParse( + paymentRequestSchema, + parsedPaymentRequestToken.payload + ) + + if (!success) { + throw new InvalidPaymentRequestTokenError( + "Payment Request token is not a valid PaymentRequest" + ) + } + + return { + paymentRequest: output, + parsed: parsedPaymentRequestToken + } +} diff --git a/packages/ack-pay/src/verify-payment-token.ts b/packages/ack-pay/src/verify-payment-token.ts deleted file mode 100644 index 11e6c3c..0000000 --- a/packages/ack-pay/src/verify-payment-token.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { verifyJwt } from "@agentcommercekit/jwt" -import * as v from "valibot" -import { InvalidPaymentTokenError } from "./errors" -import { paymentRequestSchema } from "./schemas/valibot" -import type { PaymentRequest } from "./payment-request" -import type { Resolvable } from "@agentcommercekit/did" -import type { JwtVerified } from "@agentcommercekit/jwt" - -interface ValidatePaymentTokenOptions { - /** - * The resolver to use for did resolution - */ - resolver?: Resolvable - /** - * Whether to verify the expiry of the payment token - */ - verifyExpiry?: boolean - /** - * The issuer to verify the payment token against - */ - issuer?: string -} - -/** - * Verify a payment token - * - * @param token - The payment token to verify - * @param options - The {@link ValidatePaymentTokenOptions} to use - * @returns The {@link PaymentRequest} parsed from the payment token and the parsed JWT - */ -export async function verifyPaymentToken( - token: string, - options: ValidatePaymentTokenOptions = {} -): Promise<{ paymentRequest: PaymentRequest; parsed: JwtVerified }> { - let parsedPaymentToken: JwtVerified - - try { - parsedPaymentToken = await verifyJwt(token, { - resolver: options.resolver, - issuer: options.issuer, - policies: { - aud: false, - exp: options.verifyExpiry ?? true - } - }) - } catch (_err) { - throw new InvalidPaymentTokenError() - } - - const { success, output } = v.safeParse( - paymentRequestSchema, - parsedPaymentToken.payload - ) - - if (!success) { - throw new InvalidPaymentTokenError( - "Payment token is not a valid PaymentRequest" - ) - } - - return { - paymentRequest: output, - parsed: parsedPaymentToken - } -} diff --git a/packages/agentcommercekit/README.md b/packages/agentcommercekit/README.md index 2a8582b..5f7fbf6 100644 --- a/packages/agentcommercekit/README.md +++ b/packages/agentcommercekit/README.md @@ -110,8 +110,7 @@ import { createA2AHandshakeMessage } from "agentcommercekit/a2a" ```ts import { - createPaymentRequestBody, - createPaymentRequestResponse, + createSignedPaymentRequest, createDidWebUri, createJwtSigner, curveToJwtAlgorithm, @@ -137,17 +136,17 @@ const paymentRequest = { const keypair = await generateKeypair("secp256k1") // Create a payment request body with a signed token -const paymentBody = await createPaymentRequestBody(paymentRequest, { +const paymentRequestBody = await createSignedPaymentRequest(paymentRequest, { issuer: createDidWebUri("https://server.example.com"), signer: createJwtSigner(keypair), algorithm: curveToJwtAlgorithm(keypair.curve) }) // Create a 402 Payment Required response -const response = await createPaymentRequestResponse(paymentRequest, { - issuer: createDidWebUri("https://server.example.com"), - signer: createJwtSigner(keypair), - algorithm: curveToJwtAlgorithm(keypair.curve) +// Create a 402 Payment Required response +const response = new Response(JSON.stringify(paymentRequestBody, { + status: 402, + contentType: "application/json" }) ``` @@ -157,7 +156,7 @@ const response = await createPaymentRequestResponse(paymentRequest, { import { createPaymentReceipt } from "agentcommercekit" const receipt = createPaymentReceipt({ - paymentToken: "", + paymentRequestToken: "", paymentOptionId: "", issuer: "did:web:receipt-service.example.com", payerDid: "did:web:customer.example.com" diff --git a/tools/api-utils/src/middleware/error-handler.ts b/tools/api-utils/src/middleware/error-handler.ts index 06c8c94..5935e28 100644 --- a/tools/api-utils/src/middleware/error-handler.ts +++ b/tools/api-utils/src/middleware/error-handler.ts @@ -1,4 +1,4 @@ -import { InvalidPaymentTokenError } from "@agentcommercekit/ack-pay" +import { InvalidPaymentRequestTokenError } from "@agentcommercekit/ack-pay" import { DidResolutionError } from "@agentcommercekit/did" import { CredentialVerificationError } from "@agentcommercekit/vc" import { HTTPException } from "hono/http-exception" @@ -10,7 +10,7 @@ export const errorHandler: ErrorHandler = (err, c) => { if ( err instanceof DidResolutionError || err instanceof CredentialVerificationError || - err instanceof InvalidPaymentTokenError + err instanceof InvalidPaymentRequestTokenError ) { return c.json(formatErrorResponse(err), 400) }