@synza/http
v0.1.0-alpha.0
Published
Host-authenticated Web Request and Response handlers for Synza billing
Readme
@synza/http
A framework-independent Web Request/Response adapter for Synza's public Promise billing methods. This experimental 0.1.0-alpha.0 package starts no server and borrows your configured billing instance. Your host verifies callers, registers routes, and supplies CSRF protection for cookie sessions.
Table of contents
- Install
- Configure the handler
- Send requests
- Route webhooks
- Security and failures
- Validation limits
- License
Install
Use Node.js >=22.12, ESM, and a framework that supplies Web Request and Response objects. If the host creates its own billing instance, install @synza/stripe explicitly alongside its peers:
pnpm add @synza/http@alpha @synza/stripe@alpha [email protected] stripe@^23.0.0Effect is an internal runtime dependency. This package has no native Effect entry point. See the Stripe package for schema and database prerequisites.
Configure the handler
The host boundaries below are function parameters, not Synza authentication exports. resolveCaller verifies credentials and returns the admitted Jazz account UUID as Caller.accountId, or null for recognized authentication rejection.
// Register a bearer-authenticated handler using host-owned resources and policy.
import { createSynza, type Caller, type OrganizationAuthorizationPolicy } from '@synza/stripe'
import { createSynzaHttpHandler } from '@synza/http'
import type { Db } from 'jazz-tools'
import Stripe from 'stripe'
export function configureBillingHttp(input: {
db: () => Db | Promise<Db>
stripeSecretKey: string
provider: { stripeAccountId: string; livemode: boolean }
webhookSecret: string
priceId: string
idempotencyNamespace: string
resolveCaller: (request: Request) => Promise<Caller | null>
authorizeOrganization: OrganizationAuthorizationPolicy
}) {
const billing = createSynza({
db: input.db,
stripe: new Stripe(input.stripeSecretKey),
provider: input.provider,
authorization: { authorizeOrganization: input.authorizeOrganization },
checkout: {
offers: {
team: {
mode: 'subscription',
params: {
success_url: 'https://app.example/billing/success?session_id={CHECKOUT_SESSION_ID}',
cancel_url: 'https://app.example/billing',
},
lineItems: [
{ ref: 'base', price: input.priceId, quantity: { type: 'fixed', quantity: 1 } },
],
},
},
},
portal: { returnUrl: 'https://app.example/billing' },
webhooks: { secret: input.webhookSecret },
})
return createSynzaHttpHandler({
billing,
resolveCaller: input.resolveCaller,
security: { type: 'bearer' },
idempotencyNamespace: input.idempotencyNamespace,
basePath: '/api/billing',
})
}Choose a stable idempotencyNamespace, distinct across provider account/mode contexts, such as app.primary.sandbox. Keep that namespace unchanged for retries. Register the returned function with your host router. The compiled host fixture demonstrates Jazz bearer resolution and host-derived quantity configuration.
Send requests
All routes are POST under basePath (default /api/billing). Application routes require JSON, verified authentication, and explicit scope such as { "type": "account" } or { "type": "organization", "orgId": "team-id" }.
| Route | Request fields besides scope |
| --------------------------- | ------------------------------------------------------------------------------ |
| /checkout | Configured offerKey, optional manualQuantities |
| /portal | Optional flow selecting Portal or owned subscription cancellation |
| /subscriptions/cancel | stripeSubscriptionId, timing: immediate or periodEnd |
| /subscriptions/reactivate | stripeSubscriptionId |
| /subscriptions/quantity | stripeSubscriptionId, stripeSubscriptionItemId, optional manual quantity |
| /checkout/refresh | sessionId |
/checkout calls configured offer Checkout, not raw Stripe Checkout. Prices, destinations, and quantity policy stay on the server. Quantity updates require a configured item policy. There are no Customer creation or billing-read routes. See the HTTP contract for request shapes.
Subscription commands require Idempotency-Key; Checkout accepts it optionally. The adapter derives provider keys using namespace, caller, scope, action, target, and browser key. It retains no attempt ledger and does not freeze resolved parameters. Direct core keys are forwarded unchanged.
Route webhooks
Expose POST /api/billing/webhook to Stripe without host bearer or cookie middleware. This route bypasses resolveCaller and CSRF checks, then verifies stripe-signature through billing's configured webhook secret. Preserve the original body; do not parse, consume, or rewrite it upstream. Configure the Stripe endpoint separately. Successful processing and hooks return empty 204. Hooks repeat on every delivery; the host deduplicates business effects.
Security and failures
Bearer mode checks header shape; the host resolver verifies the token and admitted account. Cookie mode requires { type: 'cookie', allowedOrigins, verifyCsrf }, with exact allowed origins and host CSRF verification returning true. A token header's presence alone is insufficient. The adapter adds no automatic CORS policy. Runtime organization policy uses the central nine-action authorization.authorizeOrganization callback; read and management grants remain independent.
Return null or throw HttpAuthenticationError only for known credential rejection. Unknown resolver failures remain 500. Never derive caller identity from request JSON. Extra fields, raw Stripe parameters, arbitrary URLs, financial options, and replay context are rejected. Responses redact internal failures; successful application calls return JSON 200. Defaults bound application bodies to 64 KiB and webhook bodies to 1 MiB, configurable through maxBodyBytes and maxWebhookBodyBytes.
Validation limits
The ledger records failing full gates. Combined direct Jazz read validation is blocked by Jazz #3981; it is not validated for deployment. HTTP fixtures do not certify host authentication or live Stripe behavior. Synza supplies billing facts, not entitlements or fulfillment. This alpha makes no release-readiness claim.
License
MIT.
