Spreadsheet-UX op een echte relationele engine
Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.
import { createHmac, timingSafeEqual } from 'node:crypto';
import type { PlanId, SubscriptionStatus } from '@wellformed/shared';
import { config } from '../config.js';
/**
* Taking money.
*
* Wellformed uses a **merchant of record** — Paddle here — rather than a raw card
* processor, because a one-person company selling software across the EU
* otherwise inherits VAT registration, rate tables and filing in every country
* it sells into. That is not a feature to build; it is a business to run.
*
* What that leaves on this side is small and is all of it below: a link to a
* hosted checkout, and a webhook that says what happened. Wellformed never sees a
* card number, never stores one, and has no PCI surface at all.
*
* With no provider configured the product still works end to end — every
* workspace is simply on the free plan, and the operator moves one with
* `npm run plan`. That is the self-hosted case, and it is not a degraded mode.
*/
export interface CheckoutRequest {
workspaceId: string;
workspaceSlug: string;
plan: PlanId;
email: string;
/** Where the provider should send them back to. */
returnUrl: string;
}
export function billingEnabled(): boolean {
return config.billing.provider !== 'none';
}
/**
* The hosted checkout to send someone to.
*
* `workspaceId` rides along as custom data so the webhook can say which
* workspace was paid for without trusting anything in the browser.
*/
export function checkoutUrl(request: CheckoutRequest): string | null {
const { provider, checkoutBase, priceIds } = config.billing;
if (provider === 'none' || !checkoutBase) return null;
const price = priceIds[request.plan];
if (!price) return null;
const url = new URL(checkoutBase);
url.searchParams.set('items[0][price_id]', price);
url.searchParams.set('items[0][quantity]', '1');
url.searchParams.set('customer_email', request.email);
url.searchParams.set('custom_data[workspace_id]', request.workspaceId);
url.searchParams.set('custom_data[workspace_slug]', request.workspaceSlug);
url.searchParams.set('success_url', request.returnUrl);
return url.toString();
}
/**
* Verify a Paddle webhook signature.
*
* The header is `ts=<unix>;h1=<hex>`, and the signed payload is
* `<ts>:<raw body>`. Two details matter and both are security-relevant: the
* comparison is constant-time, and the timestamp is checked so a captured
* webhook cannot be replayed a month later to reinstate a cancelled plan.
*/
export const WEBHOOK_TOLERANCE_MS = 5 * 60_000;
export function verifySignature(header: string | undefined, rawBody: string, now = Date.now()): boolean {
const secret = config.billing.webhookSecret;
if (!secret || !header) return false;
const parts = new Map(
header
.split(';')
.map((piece) => piece.split('=', 2))
.filter((pair): pair is [string, string] => pair.length === 2)
.map(([key, value]) => [key.trim(), value.trim()]),
);
const ts = parts.get('ts');
const signature = parts.get('h1');
if (!ts || !signature) return false;
const age = Math.abs(now - Number(ts) * 1000);
if (!Number.isFinite(age) || age > WEBHOOK_TOLERANCE_MS) return false;
const expected = createHmac('sha256', secret).update(`${ts}:${rawBody}`).digest('hex');
const given = Buffer.from(signature, 'utf8');
const mine = Buffer.from(expected, 'utf8');
return given.length === mine.length && timingSafeEqual(given, mine);
}
/** The subset of a provider event Wellformed acts on. */
export interface BillingEvent {
id: string;
kind: string;
workspaceId: string | null;
subscriptionId: string | null;
customerId: string | null;
status: SubscriptionStatus | null;
plan: PlanId | null;
periodEnd: string | null;
cancelAtPeriodEnd: boolean;
}
interface PaddlePayload {
event_id?: string;
event_type?: string;
data?: {
id?: string;
status?: string;
customer_id?: string;
scheduled_change?: { action?: string } | null;
current_billing_period?: { ends_at?: string } | null;
custom_data?: { workspace_id?: string } | null;
items?: { price?: { id?: string } }[];
};
}
const STATUS_MAP: Record<string, SubscriptionStatus> = {
active: 'active',
trialing: 'trialing',
past_due: 'past_due',
paused: 'past_due',
canceled: 'cancelled',
cancelled: 'cancelled',
};
/**
* Read a provider event into Wellformed's terms.
*
* The plan comes from the price id rather than from anything the payload calls
* a plan: the price is what was actually charged, and it is the only field the
* customer cannot influence.
*/
export function parseEvent(payload: unknown): BillingEvent | null {
const body = payload as PaddlePayload;
const id = body.event_id;
const kind = body.event_type;
if (!id || !kind) return null;
const data = body.data ?? {};
const priceId = data.items?.[0]?.price?.id ?? null;
let plan: PlanId | null = null;
for (const [candidate, configured] of Object.entries(config.billing.priceIds)) {
if (configured && configured === priceId) plan = candidate as PlanId;
}
return {
id,
kind,
workspaceId: data.custom_data?.workspace_id ?? null,
subscriptionId: data.id ?? null,
customerId: data.customer_id ?? null,
status: data.status ? (STATUS_MAP[data.status] ?? null) : null,
plan,
periodEnd: data.current_billing_period?.ends_at ?? null,
cancelAtPeriodEnd: data.scheduled_change?.action === 'cancel',
};
}