Files
2026-05-27 23:29:53 +01:00

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 the ZodError chained as cause.
  • 6-step onInit: resolve app/openAPIRegistry → assert provider → construct service → register service slot → register routes → log readiness.
  • SERVICE_NOT_AVAILABLE when app is absent.
  • ROUTE_PREFIX_COLLISION when the configured prefix overlaps with another plugin's routes.
  • Mounts the idempotency middleware on POST /intents and POST /intents/:id/refund only (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 openAPIRegistry is 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) when app is missing at onInit.
  • 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.