Provider-agnostic payments for the edge — one unified request schema (authorize / capture / void / refund / sync), switch processor by config not code. A Workers-native reimplementation of the Hyperswitch Prism interface.
Part of Suluk — one typed OpenAPI v4 contract projecting into every full-stack layer.
CANDIDATE tooling — not official OpenAPI. Suluk is a single-contributor candidate for OpenAPI Specification v4.0 ("Moonwalk"), unaffiliated with the OpenAPI Initiative and unable to ratify anything on the SIG's behalf.
bun add @suluk/payments
One unified payment schema for every processor (C048). Hyperswitch Prism
ships as a native FFI addon (a Rust core) that can't run in a Cloudflare Worker, so @suluk/payments
adopts its interface — a single request schema for authorize / capture / void / refund / sync,
plus optional customer / tokenize / recurring / webhook surfaces — and implements it over fetch:
zero native deps, edge-safe, swappable. You pass one connectorConfig naming the processor and
its credentials; switching Stripe → Adyen is a config change, not a code change.
The status enums mirror Prism's integer values exactly, so a real Prism backend stays a drop-in
later and connector semantics match. A soft decline is returned in-band as
status: PaymentStatus.* (e.g. ROUTER_DECLINED), never thrown — thrown errors are reserved for
integration/network faults. Sensitive values are wrapped in Secret<T> so they're explicit at every
call site and never accidentally logged.
This barrel ships the interface + a mockConnector, the first real backend (stripeConnector),
the processor-agnostic pricing primitives, and the SDK-free Stripe webhook surface — the
seam that supersedes @suluk/stripe.
import {
paymentClient, stripeConnector,
CaptureMethod, AuthenticationType, Currency, PaymentStatus,
type ConnectorConfig, type AuthorizeRequest,
} from "@suluk/payments";
// Config selects the connector — name exactly one processor + its credentials.
const config: ConnectorConfig = { connectorConfig: { stripe: { apiKey: { value: env.STRIPE_SECRET_KEY } } } };
// `registry` maps a processor name → its connector factory.
const client = paymentClient(config, { stripe: stripeConnector });
const req: AuthorizeRequest = {
merchantTransactionId: "txn_1",
amount: { minorAmount: 2000, currency: Currency.USD }, // integer minor units
captureMethod: CaptureMethod.AUTOMATIC,
authType: AuthenticationType.NO_THREE_DS,
paymentMethod: { card: {
cardNumber: { value: "4242424242424242" }, cardExpMonth: { value: "12" },
cardExpYear: { value: "2030" }, cardCvc: { value: "123" },
} },
};
const res = await client.authorize(req);
res.status; // PaymentStatus.CHARGED — a soft decline is a status, never a throw
res.connectorTransactionId; // "pi_..." — carry it into capture/void/refund/sync
// Later: refund part of the charge.
await client.refund({
merchantRefundId: "r1",
connectorTransactionId: res.connectorTransactionId,
refundAmount: { minorAmount: 500, currency: Currency.USD },
paymentAmount: 2000,
});
The transport seam is mockable — pass { fetch } as the third arg to paymentClient (or use
mockConnector with MOCK_DECLINE_CARD / MOCK_3DS_CARD) to test the whole flow with no live
processor.
| Export | What it does |
|---|---|
paymentClient(config, registry, http?) |
Resolve the config-named processor to a bound PaymentConnector. |
PaymentConnector (interface) |
The unified surface every processor implements (core flows required; advanced optional). |
stripeConnector |
The first real backend — fetch → Stripe REST, Workers-native. |
mockConnector, MOCK_DECLINE_CARD, MOCK_3DS_CARD |
An in-memory connector + fixtures for tests. |
IntegrationError / ConnectorError / NetworkError / PaymentLibError |
The thrown error taxonomy (a soft decline is a status, not an error). |
subtotal, orderTotal, composeTotal, computeDiscountAmount, validateDiscount, prorateDiscount, verifyAmount, cartFingerprint, idempotencyKey, … |
Processor-agnostic pricing math (anti-tampering, proration, checkout totals). |
verifyStripeSignature, timingSafeHexEqual, webhookRouter, STRIPE_EVENTS |
SDK-free Stripe webhook verification + a typed event router. |
stripePost / stripeGet / toForm (stripe-transport) |
The low-level one-client Stripe transport for platform ops the agnostic seam doesn't model. |
The type vocabulary (AuthorizeRequest, PaymentResponse, RefundRequest, MinorAmount, Secret,
Currency, CaptureMethod, AuthenticationType, PaymentStatus, RefundStatus, WebhookEvent,
…) is re-exported from ./types.
@suluk/payments is the provider-agnostic seam that supersedes @suluk/stripe — interface-first
(C048), with real connectors (Adyen, …) and the @suluk/billing rewire as follow-on builds. It is
dependency-free (zero native deps, only fetch), stores nothing itself, and returns money status
in-band. It's the transport @suluk/billing builds its Stripe plumbing on.
Apache-2.0