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