3.8 KiB
plugin
(no submodules)
Purpose
Factory + descriptor for the @suaveplan/api-plugin-payment Hono plugin. Wraps a caller-injected PaymentProvider (Stripe, Adyen, PayPal, Square, Mollie, or a custom impl) behind 7 owner-scoped Zod-typed OpenAPI routes, an HMAC-verified webhook receiver, and a typed paymentService slot in the host's ServiceRegistry. The descriptor implements the canonical 6-step onInit lifecycle every @suaveplan/api-plugin-* package follows.
This is the entry point downstream consumers invoke from their defineApp().use(...) chain — createPaymentPlugin({ provider, webhookSecret }) returns a PluginDescriptor that wires every moving part.
Features
- Zod-validated options at the factory boundary; invalid input throws
PaymentPluginError(INVALID_INPUT)with theZodErrorchained ascause. - 6-step
onInit: resolveapp/openAPIRegistry→ assert provider → construct service → register service slot → register routes → log readiness. SERVICE_NOT_AVAILABLEwhenappis absent.ROUTE_PREFIX_COLLISIONwhen the configured prefix overlaps with another plugin's routes.- Mounts the idempotency middleware on
POST /intentsandPOST /intents/:id/refundonly (read routes stay un-cached). - Mounts the webhook receiver as a separate sub-router that reads the raw body before deserialisation.
- Falls back to mounting routes on the Hono app directly when
openAPIRegistryis absent (the OpenAPI doc loses the routes; runtime keeps working).
Usage: Basic
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!,
}));
Usage: Advanced
import { createPaymentPlugin } from "@suaveplan/api-plugin-payment";
import { MemoryPaymentIntentStore } from "@suaveplan/api-plugin-payment/store/memory-store";
const plugin = createPaymentPlugin({
provider: customProvider,
webhookSecret,
routePrefix: "/billing",
store: new MemoryPaymentIntentStore(),
webhookToleranceMs: 60_000,
webhookAlgorithm: "SHA-512",
clock: { now: () => testClock.nowMs() },
logger: appLogger,
});
API Reference
createPaymentPlugin(options: PaymentPluginOptions): PluginDescriptor<PaymentPluginConfig>.- Required options:
provider: PaymentProvider,webhookSecret: string(≥8 chars). - Optional:
routePrefix,store,webhookToleranceMs,webhookAlgorithm,clock,logger. - Throws
PaymentPluginError(INVALID_INPUT)on Zod failure. - Throws
PaymentPluginError(SERVICE_NOT_AVAILABLE)whenappis missing atonInit. - Throws
PaymentPluginError(ROUTE_PREFIX_COLLISION)when the prefix overlaps existing routes.
Implementation Notes
The factory takes the resolved Zod-output and binds it as the descriptor's defaults, then passes the same value into onInit so both the registry and the host see identical configuration. The defaults field is the only way the registry surfaces the resolved config back to the plugin's lifecycle hooks (ctx.getOwnConfig() reads it).
The webhook receiver is mounted as a separate Hono sub-router because it must NOT be routed through OpenAPIRegistry — the verification step reads the raw body and the OpenAPI 3.1 doc would still be served if the call returned 400 for an invalid signature. Keeping the receiver out of the registry means the auto-generated client SDKs don't expose POST /webhook as a "user-callable" endpoint, which is the correct semantic.