Initial Commit
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
# plugin
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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
|
||||
|
||||
```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!,
|
||||
}));
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
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.
|
||||
Reference in New Issue
Block a user