Files
suaveplan/packages/api-stack/api/api-plugin-payment
2026-05-27 23:29:53 +01:00
..
2026-05-27 23:29:53 +01:00
2026-05-27 23:29:53 +01:00
2026-05-27 23:29:53 +01:00
2026-05-27 23:29:53 +01:00
2026-05-27 23:29:53 +01:00
2026-05-27 23:29:53 +01:00
2026-05-27 23:29:53 +01:00
2026-05-27 23:29:53 +01:00

@suaveplan/api-plugin-payment - Payment-lifecycle plugin for @suaveplan/api. Wraps @suaveplan/payment + caller-injected PaymentProvider (Stripe/Adyen/PayPal/Square/Mollie/custom) as a @suaveplan/api-plugin definePlugin descriptor. Mounts 6 owner-scoped OpenAPI routes (intent create/list/get/confirm/capture/cancel + refund), a HMAC-verified webhook receiver, an Idempotency-Key middleware, a typed paymentService slot in the host ServiceRegistry, and OTel instruments (5 counters + 1 histogram). Server-tier — runs in Bun and Node.

Payment-lifecycle plugin for @suaveplan/api. Wraps @suaveplan/payment + a caller-injected PaymentProvider (Stripe / Adyen / PayPal / Square / Mollie / custom) as a single definePlugin consumer that ships:

  • 7 owner-scoped OpenAPI routes (create / list / get / confirm / capture / cancel / refund) gated behind requireAuth() from @suaveplan/api-plugin-auth, with cross-user 404 anti-enumeration baked in at the service layer.
  • POST /payments/webhook — HMAC-verified webhook receiver that reads the raw body and checks X-Signature + X-Timestamp BEFORE deserialising the payload.
  • Idempotency-Key middleware on POST /intents and POST /intents/:id/refund so retries never double-charge.
  • paymentService ServiceRegistry slot exposing the full lifecycle to downstream plugins with full TypeScript inference.
  • OpenTelemetry instrument bundle — 5 counters + 1 histogram covering intent creation, captures, refunds, webhook delivery, signature failures, and intent-creation latency.
  • 15 stable PAYMENT_PLUGIN_* error codes with typed PaymentPluginError class for programmatic dispatch.

Server-tier — runs in Bun and Node.

Documentation tree

  • error-codes — Stable error-code registry for @suaveplan/api-plugin-payment. Every failure path in the plugin raises a `PaymentPlugi…
    • error-codes — Stable error-code registry for @suaveplan/api-plugin-payment. Every failure path in the plugin raises a `PaymentPlugi…
  • context-augmentation — TypeScript module augmentation that surfaces the Hono ContextVariableMap slot user: Principal | null to every consu…
  • idempotency-middleware — Hono middleware enforcing the Idempotency-Key header on payment-mutation routes (POST /payments/intents, `POST /pay…
  • otel — OpenTelemetry instrument bundle for @suaveplan/api-plugin-payment. Exposes 5 counters and 1 histogram so operators ca…
    • instruments — OpenTelemetry instrument bundle for @suaveplan/api-plugin-payment. Exposes 5 counters and 1 histogram so operators ca…
  • plugin — Factory + descriptor for the @suaveplan/api-plugin-payment Hono plugin. Wraps a caller-injected PaymentProvider (St…
    • plugin — Factory + descriptor for the @suaveplan/api-plugin-payment Hono plugin. Wraps a caller-injected PaymentProvider (St…
  • routes — Builds the 7 OpenAPI route entries the plugin mounts for the payment lifecycle (create / list / get / confirm / capture…
    • build-routes — Builds the 7 OpenAPI route entries the plugin mounts for the payment lifecycle (create / list / get / confirm / capture…
  • capture-intent — Body schema for POST /payments/intents/:id/capture. Supports both full capture (empty body) and partial capture (body…
  • create-intent — Body and response schemas for POST /payments/intents. The request body carries the amount (typed MoneySchema), opti…
  • empty-body — Strict empty-object body schema shared by POST /payments/intents/:id/confirm and POST /payments/intents/:id/cancel.…
  • intent-id-param — Schema for the :id path parameter shared by every per-intent route (GET /intents/:id, POST /intents/:id/confirm,…
  • list-intents — Query and response schemas for GET /payments/intents. The route lists intents owned by the authenticated principal, o…
  • payment-intent — Wire-shape Zod schemas for the serialised PaymentIntent, Refund, and PaymentStatus types exposed by every route i…
  • refund — Body + response schemas for POST /payments/intents/:id/refund. Supports full or partial refunds with optional `reason…
  • webhook — Response schema for POST /payments/webhook. The webhook receiver MUST read the raw request body and verify the provid…
  • money — Boundary Zod schema for MoneyLike — { amount: bigint, currency: string }. Payment APIs work in minor units (cents,…
  • options — Zod schema validating PaymentPluginOptions at the createPaymentPlugin factory boundary. The schema runs once at con…
  • payment-service — Owner-scoped lifecycle service that wraps the caller-injected PaymentProvider with an ownership map (`PaymentIntentSt…
  • registry-augmentation — TypeScript module augmentation that adds the paymentService slot to @suaveplan/api-plugin/service-registry's `Servi…
  • memory-store — In-process default implementation of the PaymentIntentStore contract. Stores owner records in a `Map<string, OwnedInt…
  • payment-intent-store — Persistence contract for the intent-id → owner map every payment plugin needs to enforce per-user scoping on lookups, l…
  • types — Public type surface for @suaveplan/api-plugin-payment. Declares the two top-level interfaces every consumer sees: `Pa…
    • types — Public type surface for @suaveplan/api-plugin-payment. Declares the two top-level interfaces every consumer sees: `Pa…
  • webhook — Wrapper around @suaveplan/payment's WebhookVerifier tailored for the plugin's webhook route. Reads the request's `X…
    • verify-webhook — Wrapper around @suaveplan/payment's WebhookVerifier tailored for the plugin's webhook route. Reads the request's `X…

Install

bun add @suaveplan/api-plugin-payment

Quick start

import { defineApp } from "@suaveplan/api";
import { createAuthPlugin } from "@suaveplan/api-plugin-auth";
import { createPaymentPlugin } from "@suaveplan/api-plugin-payment";
import { StripeProvider } from "@suaveplan/payment-provider-stripe";

const app = defineApp()
    .use(createAuthPlugin({ betterAuth: { /* ... */ } }))
    .use(
        createPaymentPlugin({
            provider: new StripeProvider({ apiKey: process.env.STRIPE_KEY! }),
            webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
        })
    );

The plugin mounts under /payments by default. Override with routePrefix.

The 3-way plug-and-play contract

@suaveplan/api-plugin-payment is the canonical payment exemplar of the contract every @suaveplan/api-plugin-* package follows:

  1. Boundary Zod schemas — every request body, query, and path parameter validates through a schema exported at ./schemas/* so consumers and the plugin share one source of truth.
  2. OpenAPI route registration — every route is built with RouteBuilder from @suaveplan/api-openapi and registered via definePluginRoutes, so the host's /openapi.json describes the payment surface with the same Zod-derived types the code accepts.
  3. ServiceRegistry slot — the paymentService slot exposes the lifecycle to downstream plugins without forcing them to re-implement provider plumbing.

Idempotency

POST /payments/intents and POST /payments/intents/:id/refund honour the Idempotency-Key header. Retrying the same request with the same key and body returns the cached response without invoking the provider; retrying with a different body raises HTTP 409.

const res = await fetch("/payments/intents", {
    method: "POST",
    headers: {
        "content-type": "application/json",
        "Idempotency-Key": `order-${orderId}`,
    },
    body: JSON.stringify({ amount: { amount: "2500", currency: "USD" } }),
});

Webhook verification

The receiver expects:

  • X-Signature — hex-encoded HMAC of ${timestamp}.${body} using the configured webhookSecret.
  • X-Timestamp — Unix ms integer.

Tolerance window defaults to 5 minutes (webhookToleranceMs). Algorithm defaults to SHA-256 (webhookAlgorithm: "SHA-256" | "SHA-384" | "SHA-512").

import { signPaymentWebhook } from "@suaveplan/api-plugin-payment";

const body = JSON.stringify(event);
const ts = Date.now();
const sig = signPaymentWebhook({
    body,
    timestamp: ts,
    secret: process.env.STRIPE_WEBHOOK_SECRET!,
    algorithm: "SHA-256",
});
await fetch("/payments/webhook", {
    method: "POST",
    headers: { "X-Signature": sig, "X-Timestamp": String(ts) },
    body,
});

Pluggable storage

The default in-process MemoryPaymentIntentStore records the intent-id → owner map. Inject a SurrealDB / Postgres / Redis adapter for production:

import { createPaymentPlugin } from "@suaveplan/api-plugin-payment";

const plugin = createPaymentPlugin({
    provider,
    webhookSecret,
    store: new MySurrealPaymentIntentStore({ db }),
});

Modules