Spreadsheet-UX op een echte relationele engine

main

Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.

5.3 kB Ruw bestand
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',
  };
}