13 KiB
Slim @suaveplan/payment-providers — extract concrete provider modules into sibling packages
Package: @suaveplan/payment-providers
Status: PROPOSED
Type: refactor (modification of the just-shipped migration 2026-05-17-add-payment-providers)
- Design: ./design.md
- Spec delta: ./specs/payment-providers/spec.md
Why
The just-shipped migration (openspec/archive/2026-05-17-add-payment-providers/)
folded five concrete provider adapters (Stripe, PayPal, Square, Mollie, Adyen)
into a single @suaveplan/payment-providers package. That bundle wired the
shared shape (PaymentProviderError, httpRequest, FetchLike,
ApiKeyConfig, OAuthConfig, MerchantKeyConfig) and the five adapter
classes into one publish unit.
Consumers that only need Stripe pay for PayPal's OAuth machinery, Adyen's
merchant-context wiring, and three more provider trees they never call.
@suaveplan/payment already depends on this package transitively for its
PaymentProvider interface; growing it with every new provider compounds the
problem.
The genesis pattern for foundation-with-sibling-plugins (see
@suaveplan/network + network-plugin-*, @suaveplan/drive +
drive-plugin-*) is to ship the core abstraction + registry in the
foundation package and the concrete plugins in sibling packages. This
change retrofits @suaveplan/payment-providers to match that pattern:
- Slim the core —
@suaveplan/payment-providerskeeps only the shared types (ApiKeyConfig,OAuthConfig,MerchantKeyConfig,HttpOptions,ProviderResponse,ProviderCommonOptions,NowFn,RandomIdFn,Base64EncodeFn,FetchLike), thehttpRequesthelper, thePaymentProviderError+PAYMENT_PROVIDER_CODESregistry, plus the newProviderRegistry+definePaymentProviderfactory. - Add
ProviderRegistry— a thin in-memory registry that storesPaymentProviderinstances by name withregister/get/has/list/unregister, raisingPaymentProviderError(PROVIDER_NOT_REGISTERED)on unknown lookups. - Add
definePaymentProvider— a factory helper that returns aPaymentProvider-shaped object with a stable identity so sibling provider packages can build adapters without duplicating boilerplate. - Remove the five concrete provider trees (Stripe / PayPal / Square /
Mollie / Adyen). They re-land as 5 sibling packages
(
@suaveplan/payment-provider-stripe,-paypal,-square,-mollie,-adyen) in a follow-up wave, each depending on this slimmed core.
What changes
REMOVED
StripeProvider(entiresrc/providers/stripe/tree, including tests +.md). Re-lands as@suaveplan/payment-provider-stripe.StripeProvider.PayPalProvider(entiresrc/providers/paypal/tree). Re-lands as@suaveplan/payment-provider-paypal.PayPalProvider.SquareProvider(entiresrc/providers/square/tree). Re-lands as@suaveplan/payment-provider-square.SquareProvider.MollieProvider(entiresrc/providers/mollie/tree). Re-lands as@suaveplan/payment-provider-mollie.MollieProvider.AdyenProvider(entiresrc/providers/adyen/tree). Re-lands as@suaveplan/payment-provider-adyen.AdyenProvider.- Subpath exports
./providers/{stripe,paypal,square,mollie,adyen}/*frompackage.json#exports. SquareConfigtype alias (Square-specific extension ofApiKeyConfigwithrandomId). The sibling package re-declares it locally — the core ships only provider-agnostic config shapes.
ADDED
ProviderRegistryclass withregister(provider),get(name),has(name),list(),unregister(name), andsize. Lookup miss throwsPaymentProviderError(PROVIDER_NOT_REGISTERED)fromget; duplicate registration throwsPROVIDER_ALREADY_REGISTERED.definePaymentProvider<TName extends string>(spec)factory returning aPaymentProvider & { readonly name: TName }. Preserves caller-supplied method identities; freezes the resulting object so sibling packages can hand the value straight toregistry.register.PROVIDER_NOT_REGISTEREDandPROVIDER_ALREADY_REGISTEREDcodes added toPAYMENT_PROVIDER_CODES.composeAbortSignal(internal, external)extracted fromhttpRequestand re-exported as a public surface so sibling provider packages can compose their own signals without re-implementing the helper.
MODIFIED
package.json#exportsshrinks from 9 entries (5 providers + 4 core) to 5 entries (.,./types/provider,./error-codes/error-codes,./http/http,./registry/registry).README.mdrewritten to describe the slim core + sibling package ecosystem instead of the 5-adapter bundle.package.json#descriptionrewritten to reflect the slim shape.vite.config.ts#rollupOptions.externalstays unchanged (@suaveplan/error,@suaveplan/payment,tslib); no new externals are introduced.
Migration
Consumers that imported a concrete provider from
@suaveplan/payment-providers MUST switch to the matching sibling package.
The provider class names, constructors, and PaymentProvider interface
contract are preserved verbatim — only the package name changes.
Search command:
grep -rln "from ['\"]@suaveplan/payment-providers['\"]" packages/ \
| xargs grep -l "StripeProvider\|PayPalProvider\|SquareProvider\|MollieProvider\|AdyenProvider"
Replacement table:
| Old (pre-slim) | New (post-slim) |
|---|---|
import { StripeProvider } from "@suaveplan/payment-providers" |
import { StripeProvider } from "@suaveplan/payment-provider-stripe" |
import { PayPalProvider } from "@suaveplan/payment-providers" |
import { PayPalProvider } from "@suaveplan/payment-provider-paypal" |
import { SquareProvider } from "@suaveplan/payment-providers" |
import { SquareProvider } from "@suaveplan/payment-provider-square" |
import { MollieProvider } from "@suaveplan/payment-providers" |
import { MollieProvider } from "@suaveplan/payment-provider-mollie" |
import { AdyenProvider } from "@suaveplan/payment-providers" |
import { AdyenProvider } from "@suaveplan/payment-provider-adyen" |
import type { SquareConfig } from "@suaveplan/payment-providers" |
import type { SquareConfig } from "@suaveplan/payment-provider-square" |
ApiKeyConfig, OAuthConfig, MerchantKeyConfig, HttpOptions,
ProviderResponse, ProviderCommonOptions, NowFn, RandomIdFn,
Base64EncodeFn, FetchLike, httpRequest, composeAbortSignal,
PaymentProviderError, PAYMENT_PROVIDER_CODES, httpError,
timeoutError, parseError, oauthTokenFailure continue to be exported
from @suaveplan/payment-providers unchanged.
Consumers that want a name-keyed lookup of multiple providers SHOULD now
build a ProviderRegistry:
import { ProviderRegistry } from "@suaveplan/payment-providers";
import { StripeProvider } from "@suaveplan/payment-provider-stripe";
import { PayPalProvider } from "@suaveplan/payment-provider-paypal";
const registry = new ProviderRegistry();
registry.register(new StripeProvider({ apiKey: STRIPE_KEY }));
registry.register(new PayPalProvider({ clientId, clientSecret }));
const stripe = registry.get("stripe"); // PaymentProvider
Source mapping
Kept-core requirements derive from ../suaveplan/enterprise/payment-providers.md
and the just-shipped openspec/archive/2026-05-17-add-payment-providers/
delta. Removed-provider requirements derive from the same archived delta:
| Surface | Source | Status |
|---|---|---|
| Universal-tier purity | enterprise/payment-providers.md L37 |
kept |
PAYMENT_PROVIDER_CODES registry + PaymentProviderError |
enterprise/payment-providers.md L42, L61 |
kept; codes extended with PROVIDER_NOT_REGISTERED, PROVIDER_ALREADY_REGISTERED |
httpRequest + FetchLike + timeout/abort |
archive/2026-05-17-add-payment-providers/specs/payment-providers/spec.md Req "httpRequest helper..." |
kept; composeAbortSignal now publicly exported |
ApiKeyConfig / OAuthConfig / MerchantKeyConfig / HttpOptions / ProviderResponse / NowFn / RandomIdFn / Base64EncodeFn |
archive/... spec L62-L96 |
kept |
StripeProvider full lifecycle |
archive/... spec Req L100-L138 |
REMOVED — re-lands in @suaveplan/payment-provider-stripe |
PayPalProvider OAuth + lifecycle |
archive/... spec Req L142-L187 |
REMOVED — re-lands in @suaveplan/payment-provider-paypal |
SquareProvider + injectable randomId |
archive/... spec Req L191-L224 |
REMOVED — re-lands in @suaveplan/payment-provider-square |
MollieProvider + string-amount marshalling |
archive/... spec Req L228-L258 |
REMOVED — re-lands in @suaveplan/payment-provider-mollie |
AdyenProvider + merchant context |
archive/... spec Req L262-L293 |
REMOVED — re-lands in @suaveplan/payment-provider-adyen |
ProviderRegistry |
new genesis-originated | ADDED |
definePaymentProvider factory |
new genesis-originated | ADDED |
composeAbortSignal public export |
extracted from internal http.ts |
ADDED |
Out of scope
- The 5 sibling provider packages themselves (
payment-provider-stripe,-paypal,-square,-mollie,-adyen) — those are a follow-up wave, one OpenSpec change per package, each depending on this slimmed core. - Auto-discovery / dynamic loading of provider packages. Consumers explicitly install + register the providers they need.
OpenTelemetryspans / counters on the registry — same deferral as the parent migration (align with@suaveplan/payment).
Impact
Affected dependents
Run bunx turbo ls --filter=...@suaveplan/payment-providers to enumerate.
@suaveplan/payment— [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: imports only thePaymentProviderinterface from itself; does NOT import any concrete adapter or shared type from@suaveplan/payment-providers. Verified bygrep -rn "@suaveplan/payment-providers" packages/universal/payment/→ empty result).
Future dependents (the 5 sibling provider packages) will each carry their own OpenSpec change naming this slimmed core as a peer/runtime dep.
| Future sibling package | Will depend on this core for |
|---|---|
@suaveplan/payment-provider-stripe |
httpRequest, composeAbortSignal, ApiKeyConfig, PaymentProviderError, PAYMENT_PROVIDER_CODES, definePaymentProvider, optionally ProviderRegistry |
@suaveplan/payment-provider-paypal |
httpRequest, composeAbortSignal, OAuthConfig, NowFn, Base64EncodeFn, PaymentProviderError, oauthTokenFailure, definePaymentProvider |
@suaveplan/payment-provider-square |
httpRequest, ApiKeyConfig, RandomIdFn, PaymentProviderError, definePaymentProvider |
@suaveplan/payment-provider-mollie |
httpRequest, ApiKeyConfig, PaymentProviderError, definePaymentProvider |
@suaveplan/payment-provider-adyen |
httpRequest, MerchantKeyConfig, RandomIdFn, PaymentProviderError, definePaymentProvider |
Parity Checklist
This is a slim-down refactor, not a fresh migration, so the parity bar is
against the just-shipped 2026-05-17-add-payment-providers delta — not an
upstream library:
| Capability | Prior shape | Slimmed shape | Notes |
|---|---|---|---|
httpRequest + FetchLike + timeout/abort |
yes | yes | unchanged behaviour; composeAbortSignal extracted as public export |
PaymentProviderError + 4-code registry |
yes | yes (+ 2 codes) | adds PROVIDER_NOT_REGISTERED, PROVIDER_ALREADY_REGISTERED |
ApiKeyConfig / OAuthConfig / MerchantKeyConfig / HttpOptions / ProviderResponse / ProviderCommonOptions / NowFn / RandomIdFn / Base64EncodeFn |
yes | yes | verbatim |
SquareConfig |
yes | 🚫 removed | Square-specific; sibling package re-declares locally so the core has no provider-specific shapes |
| 5 concrete provider classes | yes | 🚫 removed | re-land in 5 sibling packages (payment-provider-{stripe,paypal,square,mollie,adyen}) per the follow-up wave |
ProviderRegistry + definePaymentProvider |
🚫 absent | ✅ added | new foundation API for the sibling-plugin pattern |
Verification approach
src/registry/registry.test.ts— exercises everyProviderRegistrymethod (register,get,has,list,unregister,size), duplicate-registration error, unknown-name error, both new error codes reachable from real call sites.src/registry/define.test.ts— exercisesdefinePaymentProvider, confirms it returns a frozenPaymentProvider, preserves method identities, and the result registers cleanly intoProviderRegistry.src/error-codes/error-codes.test.ts— extended to cover both new codes and the two new factories (providerNotRegistered,providerAlreadyRegistered).src/http/http.test.ts— extended to cover the now-publiccomposeAbortSignalhelper end-to-end.tests/dep-graph.test.ts— unchanged; still asserts the package has no Node / browser / runtime-specific imports.src/index.tsbarrel +package.json#exports— manually verified to contain only the slim core symbols / paths.