Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
12 KiB
@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 checksX-Signature+X-TimestampBEFORE deserialising the payload.Idempotency-Keymiddleware onPOST /intentsandPOST /intents/:id/refundso retries never double-charge.paymentServiceServiceRegistry 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 typedPaymentPluginErrorclass 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…
- error-codes — Stable error-code registry for
- context-augmentation — TypeScript module augmentation that surfaces the Hono
ContextVariableMapslotuser: Principal | nullto every consu… - idempotency-middleware — Hono middleware enforcing the
Idempotency-Keyheader 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…
- instruments — OpenTelemetry instrument bundle for
- plugin — Factory + descriptor for the
@suaveplan/api-plugin-paymentHono plugin. Wraps a caller-injectedPaymentProvider(St…- plugin — Factory + descriptor for the
@suaveplan/api-plugin-paymentHono plugin. Wraps a caller-injectedPaymentProvider(St…
- plugin — Factory + descriptor for the
- 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 (typedMoneySchema), opti… - empty-body — Strict empty-object body schema shared by
POST /payments/intents/:id/confirmandPOST /payments/intents/:id/cancel.… - intent-id-param — Schema for the
:idpath 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, andPaymentStatustypes 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
PaymentPluginOptionsat thecreatePaymentPluginfactory boundary. The schema runs once at con… - payment-service — Owner-scoped lifecycle service that wraps the caller-injected
PaymentProviderwith an ownership map (`PaymentIntentSt… - registry-augmentation — TypeScript module augmentation that adds the
paymentServiceslot to@suaveplan/api-plugin/service-registry's `Servi… - memory-store — In-process default implementation of the
PaymentIntentStorecontract. 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…
- types — Public type surface for
- webhook — Wrapper around
@suaveplan/payment'sWebhookVerifiertailored for the plugin's webhook route. Reads the request's `X…- verify-webhook — Wrapper around
@suaveplan/payment'sWebhookVerifiertailored for the plugin's webhook route. Reads the request's `X…
- verify-webhook — Wrapper around
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:
- 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. - OpenAPI route registration — every route is built with
RouteBuilderfrom@suaveplan/api-openapiand registered viadefinePluginRoutes, so the host's/openapi.jsondescribes the payment surface with the same Zod-derived types the code accepts. - ServiceRegistry slot — the
paymentServiceslot 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 configuredwebhookSecret.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
./src/plugin/plugin.md— factory + 6-steponInit../src/services/payment-service.md— owner-scoped lifecycle../src/services/registry-augmentation.md—ServiceRegistry["paymentService"]slot../src/middleware/idempotency-middleware.md— Idempotency-Key middleware../src/webhook/verify-webhook.md— HMAC verifier + signer../src/routes/build-routes.md— 7 owner-scoped routes../src/store/payment-intent-store.md— store contract../src/store/memory-store.md— in-process default../src/schemas/money.md—Moneyboundary schema../src/schemas/options.md—PaymentPluginOptionsSchema../src/schemas/routes/payment-intent.md— intent/refund wire shape../src/schemas/routes/create-intent.md—POST /intentsbody../src/schemas/routes/intent-id-param.md—:idpath param../src/schemas/routes/list-intents.md—GET /intentsquery../src/schemas/routes/capture-intent.md— capture body../src/schemas/routes/refund.md— refund body + response../src/schemas/routes/empty-body.md— empty-body schema../src/schemas/routes/webhook.md— webhook response../src/otel/instruments.md— 5 counters + 1 histogram../src/error-codes/error-codes.md— 15PAYMENT_PLUGIN_*codes../src/types/types.md—PaymentPluginOptions/PaymentPluginConfig.