feat(db): mesh data model — meshes, members, invites, audit log

- pgSchema "mesh" with 4 tables isolating the peer mesh domain
- Enums: visibility, transport, tier, role
- audit_log is metadata-only (E2E encryption enforced at broker/client)
- Cascade on mesh delete, soft-delete via archivedAt/revokedAt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-04 21:19:32 +01:00
commit d3163a5bff
1384 changed files with 314925 additions and 0 deletions

View File

@@ -0,0 +1,189 @@
import { HttpStatusCode } from "@turbostarter/shared/constants";
import { logger } from "@turbostarter/shared/logger";
import { HttpException } from "@turbostarter/shared/utils";
import { config } from "../../config";
import { getCustomerByCustomerId, updateCustomer } from "../../lib/customer";
import { BillingModel } from "../../types";
import { getHighestDiscountForPrice } from "../../utils";
import { stripe } from "./client";
import {
createBillingPortalSession,
createOrRetrieveCustomer,
} from "./customer";
import { env } from "./env";
import {
toCheckoutBillingStatus,
toPaymentBillingStatus,
} from "./mappers/to-billing-status";
import {
getPromotionCode,
subscriptionStatusChangeHandler,
} from "./subscription";
import type { BillingProviderStrategy } from "../types";
import type Stripe from "stripe";
const createCheckoutSession = async (
params: Stripe.Checkout.SessionCreateParams,
) => {
try {
return await stripe().checkout.sessions.create(params);
} catch (e) {
logger.error(e);
throw new HttpException(HttpStatusCode.INTERNAL_SERVER_ERROR, {
code: "billing:error.checkout",
});
}
};
const getCheckoutSession = async (sessionId: string) => {
try {
return await stripe().checkout.sessions.retrieve(sessionId, {
expand: ["line_items", "line_items.data.price"],
});
} catch (e) {
logger.error(e);
throw new HttpException(HttpStatusCode.INTERNAL_SERVER_ERROR, {
code: "billing:error.checkoutRetrieve",
});
}
};
export const checkoutStatusChangeHandler = async (
session: Stripe.Checkout.Session,
) => {
const customerId = session.customer as string | null;
if (!customerId) {
throw new HttpException(HttpStatusCode.NOT_FOUND, {
code: "billing:error.customerNotFound",
});
}
if (session.mode === "subscription") {
await subscriptionStatusChangeHandler({
id: session.subscription as string,
customerId,
});
return;
}
const customer = await getCustomerByCustomerId(customerId);
if (!customer) {
throw new HttpException(HttpStatusCode.NOT_FOUND, {
code: "billing:error.customerNotFound",
});
}
const checkoutSession = await getCheckoutSession(session.id);
const priceId = checkoutSession.line_items?.data[0]?.price?.id;
if (!priceId) {
throw new HttpException(HttpStatusCode.NOT_FOUND, {
code: "billing:error.priceNotFound",
});
}
const plan = config.plans.find((p) =>
p.prices.some((price) => price.id === priceId),
);
await updateCustomer(customer.userId, {
status: checkoutSession.status
? toCheckoutBillingStatus(checkoutSession.status)
: toPaymentBillingStatus(checkoutSession.payment_status),
...(plan && { plan: plan.id }),
});
logger.info(
`✅ Checkout status changed for user ${customer.userId} to ${checkoutSession.status}`,
);
};
export const checkout: BillingProviderStrategy["checkout"] = async ({
user,
price: { id },
redirect,
}) => {
try {
const price = config.plans
.find((plan) => plan.prices.some((p) => p.id === id))
?.prices.find((p) => p.id === id);
if (!price) {
throw new HttpException(HttpStatusCode.NOT_FOUND, {
code: "billing:error.priceNotFound",
});
}
const customer = await createOrRetrieveCustomer({
email: user.email,
id: user.id,
});
const discount = getHighestDiscountForPrice(price, config.discounts);
const code = await getPromotionCode(discount?.code ?? "");
const session = await createCheckoutSession({
mode:
env.BILLING_MODEL === BillingModel.RECURRING
? "subscription"
: "payment",
billing_address_collection: "required",
customer,
customer_update: {
address: "auto",
},
line_items: [
{
price: price.id,
quantity: 1,
},
],
success_url: redirect.success,
cancel_url: redirect.cancel,
...("trialDays" in price && price.trialDays
? {
subscription_data: {
trial_period_days: price.trialDays,
},
}
: {}),
...(code && {
discounts: [
{
promotion_code: code.id,
},
],
}),
});
return { url: session.url };
} catch (e) {
logger.error(e);
throw new HttpException(HttpStatusCode.INTERNAL_SERVER_ERROR);
}
};
export const getBillingPortal: BillingProviderStrategy["getBillingPortal"] =
async ({ redirectUrl, user }) => {
try {
const customer = await createOrRetrieveCustomer({
email: user.email,
id: user.id,
});
const { url } = await createBillingPortalSession({
customer,
return_url: redirectUrl,
});
return { url };
} catch (e) {
logger.error(e);
throw new HttpException(HttpStatusCode.INTERNAL_SERVER_ERROR);
}
};

View File

@@ -0,0 +1,15 @@
import Stripe from "stripe";
import { env } from "./env";
let stripeInstance: Stripe | null = null;
export const stripe = () => {
const key = env.STRIPE_SECRET_KEY;
if (!key) {
throw new Error("STRIPE_SECRET_KEY is required when using Stripe billing");
}
stripeInstance ??= new Stripe(key);
return stripeInstance;
};

View File

@@ -0,0 +1,86 @@
import { HttpStatusCode } from "@turbostarter/shared/constants";
import { logger } from "@turbostarter/shared/logger";
import { HttpException } from "@turbostarter/shared/utils";
import {
getCustomerByUserId,
updateCustomer,
upsertCustomer,
} from "../../lib/customer";
import { stripe } from "./client";
import type Stripe from "stripe";
const getStripeCustomerById = async (stripeId: string) => {
return stripe().customers.retrieve(stripeId);
};
const getStripeCustomerByEmail = async (email: string) => {
const customers = await stripe().customers.list({ email: email });
return customers.data.length > 0 ? customers.data[0] : null;
};
const createStripeCustomer = async (id: string, email: string) => {
const customerData = { metadata: { userId: id }, email: email };
const newCustomer = await stripe().customers.create(customerData);
return newCustomer.id;
};
export const createOrRetrieveCustomer = async ({
email,
id,
}: {
email: string;
id: string;
}) => {
const existingCustomer = await getCustomerByUserId(id);
const stripeCustomerId = existingCustomer?.customerId
? (await getStripeCustomerById(existingCustomer.customerId)).id
: (await getStripeCustomerByEmail(email))?.id;
const stripeIdToInsert =
stripeCustomerId ?? (await createStripeCustomer(id, email));
if (!stripeIdToInsert) {
throw new HttpException(HttpStatusCode.INTERNAL_SERVER_ERROR, {
code: "billing:error.customerCreation",
});
}
if (existingCustomer && stripeCustomerId) {
if (existingCustomer.customerId !== stripeCustomerId) {
await updateCustomer(id, {
customerId: stripeCustomerId,
});
logger.warn(
`Customer ${id} had a different customerId. Updated to ${stripeCustomerId}.`,
);
}
return stripeCustomerId;
}
await upsertCustomer({
userId: id,
customerId: stripeIdToInsert,
});
return stripeIdToInsert;
};
export const createBillingPortalSession = async (
params: Stripe.BillingPortal.SessionCreateParams,
) => {
try {
return await stripe().billingPortal.sessions.create(params);
} catch (e) {
logger.error(e);
throw new HttpException(HttpStatusCode.INTERNAL_SERVER_ERROR, {
code: "billing:error.portal",
});
}
};

View File

@@ -0,0 +1,22 @@
import { defineEnv } from "envin";
import * as z from "zod";
import { envConfig } from "@turbostarter/shared/constants";
import { sharedPreset } from "../../utils/env";
import type { Preset } from "envin/types";
export const preset = {
id: "stripe",
server: {
STRIPE_SECRET_KEY: z.string().optional(),
STRIPE_WEBHOOK_SECRET: z.string().optional(),
},
extends: [sharedPreset],
} as const satisfies Preset;
export const env = defineEnv({
...envConfig,
...preset,
});

View File

@@ -0,0 +1,2 @@
export { checkout, getBillingPortal } from "./checkout";
export { webhookHandler } from "./webhook";

View File

@@ -0,0 +1,51 @@
import { BillingStatus } from "../../../types";
export const toBillingStatus = (status: string): BillingStatus => {
switch (status) {
case "active":
return BillingStatus.ACTIVE;
case "trialing":
return BillingStatus.TRIALING;
case "past_due":
return BillingStatus.PAST_DUE;
case "incomplete":
return BillingStatus.INCOMPLETE;
case "incomplete_expired":
return BillingStatus.INCOMPLETE_EXPIRED;
case "canceled":
return BillingStatus.CANCELED;
case "unpaid":
return BillingStatus.UNPAID;
case "paused":
return BillingStatus.PAUSED;
default:
throw new Error(`Invalid billing status: ${status}`);
}
};
export const toCheckoutBillingStatus = (status: string): BillingStatus => {
switch (status) {
case "open":
return BillingStatus.PAUSED;
case "complete":
return BillingStatus.ACTIVE;
case "expired":
return BillingStatus.CANCELED;
default:
throw new Error(`Invalid checkout billing status: ${status}`);
}
};
export const toPaymentBillingStatus = (status: string): BillingStatus => {
switch (status) {
case "paid":
return BillingStatus.ACTIVE;
case "unpaid":
return BillingStatus.UNPAID;
case "no_payment_required":
return BillingStatus.ACTIVE;
default:
throw new Error(`Invalid payment billing status: ${status}`);
}
};

View File

@@ -0,0 +1,62 @@
import { HttpStatusCode } from "@turbostarter/shared/constants";
import { logger } from "@turbostarter/shared/logger";
import { HttpException } from "@turbostarter/shared/utils";
import { config } from "../../config";
import { getCustomerByCustomerId, updateCustomer } from "../../lib/customer";
import { stripe } from "./client";
import { toBillingStatus } from "./mappers/to-billing-status";
import type Stripe from "stripe";
const getSubscription = async (subscriptionId: string) => {
return stripe().subscriptions.retrieve(subscriptionId) as Promise<
Stripe.Response<Stripe.Subscription & { plan: Stripe.Plan }>
>;
};
export const getPromotionCode = async (code: string) => {
try {
const { data } = await stripe().promotionCodes.list({
code,
});
return data[0];
} catch (e) {
logger.error(e);
throw new HttpException(HttpStatusCode.INTERNAL_SERVER_ERROR, {
code: "billing:error.promotionCodeRetrieve",
});
}
};
export const subscriptionStatusChangeHandler = async ({
id,
customerId,
}: {
id: string;
customerId: string;
}) => {
const customer = await getCustomerByCustomerId(customerId);
if (!customer) {
throw new HttpException(HttpStatusCode.NOT_FOUND, {
code: "billing:error.customerNotFound",
});
}
const subscription = await getSubscription(id);
const priceId = subscription.plan.id;
const plan = config.plans.find((p) => p.prices.find((x) => x.id === priceId));
await updateCustomer(customer.userId, {
status: toBillingStatus(subscription.status),
...(plan && { plan: plan.id }),
});
logger.info(
`✅ Subscription status changed for user ${customer.userId} to ${subscription.status}`,
);
};

View File

@@ -0,0 +1 @@
export const STRIPE_SIGNATURE_HEADER = "stripe-signature";

View File

@@ -0,0 +1,9 @@
import { stripe } from "../client";
export const constructEvent = (data: {
payload: string;
sig: string;
secret: string;
}) => {
return stripe().webhooks.constructEvent(data.payload, data.sig, data.secret);
};

View File

@@ -0,0 +1,77 @@
import { HttpStatusCode } from "@turbostarter/shared/constants";
import { logger } from "@turbostarter/shared/logger";
import { HttpException } from "@turbostarter/shared/utils";
import { checkoutStatusChangeHandler } from "../checkout";
import { env } from "../env";
import { subscriptionStatusChangeHandler } from "../subscription";
import { STRIPE_SIGNATURE_HEADER } from "./constants";
import { constructEvent } from "./event";
import type { BillingProviderStrategy } from "../../types";
export const webhookHandler: BillingProviderStrategy["webhookHandler"] = async (
req,
callbacks,
) => {
const body = await req.text();
const sig = req.headers.get(STRIPE_SIGNATURE_HEADER);
if (!sig) {
throw new HttpException(HttpStatusCode.BAD_REQUEST, {
code: "billing:error.webhook.signatureNotFound",
});
}
const secret = env.STRIPE_WEBHOOK_SECRET;
if (!secret) {
throw new HttpException(HttpStatusCode.INTERNAL_SERVER_ERROR, {
code: "billing:error.webhook.secretNotConfigured",
});
}
const event = constructEvent({
payload: body,
sig,
secret,
});
logger.info(`🔔 Webhook received: ${event.type}`);
await callbacks?.onEvent?.(event);
switch (event.type) {
case "customer.subscription.created":
await callbacks?.onSubscriptionCreated?.(event.data.object.id);
await subscriptionStatusChangeHandler({
id: event.data.object.id,
customerId: event.data.object.customer as string,
});
break;
case "customer.subscription.updated":
await callbacks?.onSubscriptionUpdated?.(event.data.object.id);
await subscriptionStatusChangeHandler({
id: event.data.object.id,
customerId: event.data.object.customer as string,
});
break;
case "customer.subscription.deleted":
await callbacks?.onSubscriptionDeleted?.(event.data.object.id);
await subscriptionStatusChangeHandler({
id: event.data.object.id,
customerId: event.data.object.customer as string,
});
break;
case "checkout.session.completed":
await callbacks?.onCheckoutSessionCompleted?.(event.data.object.id);
await checkoutStatusChangeHandler(event.data.object);
break;
}
return new Response(JSON.stringify({ received: true }), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
};