first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,160 @@
# @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`.