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,338 @@
import { zValidator } from "@hono/zod-validator";
import { eq, sql } from "drizzle-orm";
import { env } from "hono/adapter";
import { createMiddleware } from "hono/factory";
import { getAllRolesAtOrAbove, hasAdminPermission } from "@turbostarter/auth";
import { MemberRole } from "@turbostarter/auth";
import { auth } from "@turbostarter/auth/server";
import { creditTransaction, customer } from "@turbostarter/db/schema";
import { db } from "@turbostarter/db/server";
import { makeZodI18nMap } from "@turbostarter/i18n";
import {
getLocaleFromRequest,
getTranslation,
} from "@turbostarter/i18n/server";
import { HttpStatusCode, NodeEnv } from "@turbostarter/shared/constants";
import { generateId, HttpException } from "@turbostarter/shared/utils";
import type { User } from "@turbostarter/auth";
import type { TFunction } from "@turbostarter/i18n";
import type { Context, ValidationTargets } from "hono";
import type { $ZodRawIssue, $ZodType } from "zod/v4/core";
type PermissionsInput = NonNullable<
NonNullable<
Parameters<typeof auth.api.hasPermission>[0]
>["body"]["permissions"]
>;
/**
* Reusable middleware that enforces users are logged in before running the
* procedure
*/
export const enforceAuth = createMiddleware<{
Variables: {
user: User;
};
}>(async (c, next) => {
const session = await auth.api.getSession({ headers: c.req.raw.headers });
const user = session?.user ?? null;
if (!user) {
throw new HttpException(HttpStatusCode.UNAUTHORIZED, {
code: "error.unauthorized",
});
}
c.set("user", user);
await next();
});
/**
* Reusable middleware that enforces that the authenticated user
* has global admin permissions
*/
export const enforceAdmin = createMiddleware<{
Variables: {
user: User;
};
}>(async (c, next) => {
const user = c.var.user;
if (!hasAdminPermission(user)) {
throw new HttpException(HttpStatusCode.FORBIDDEN, {
code: "error.forbidden",
});
}
await next();
});
/**
* Reusable middleware that enforces that the authenticated user
* has the specified permissions in the user scope
*/
export const enforceUserPermission = ({
permissions,
}: {
permissions: PermissionsInput;
}) =>
createMiddleware<{ Variables: { user: User } }>(async (c, next) => {
const hasPermission = await auth.api.hasPermission({
body: {
permissions,
},
headers: c.req.raw.headers,
});
if (!hasPermission.success) {
throw new HttpException(HttpStatusCode.FORBIDDEN, {
code: "error.forbidden",
});
}
await next();
});
/**
* Middleware to enforce that the authenticated user has the required permissions
* for a given organization before allowing access to the route handler.
*/
export const enforceOrganizationPermission = ({
organizationId,
permissions,
}: {
organizationId?: string;
permissions: PermissionsInput;
}) =>
createMiddleware<{
Variables: {
user: User;
};
}>(async (c, next) => {
const hasPermission = await auth.api.hasPermission({
body: {
organizationId,
permissions,
},
headers: c.req.raw.headers,
});
if (!hasPermission.success) {
throw new HttpException(HttpStatusCode.FORBIDDEN, {
code: "error.forbidden",
});
}
await next();
});
/**
* Middleware to enforce that the authenticated user is at least a member
* of the given organization before allowing access to the route handler.
*/
export const enforceMembership = ({
organizationId,
role = MemberRole.MEMBER,
}: {
organizationId: string;
role?: MemberRole;
}) =>
createMiddleware<{
Variables: {
user: User;
};
}>(async (c, next) => {
const user = c.var.user;
try {
const { members } = await auth.api.listMembers({
query: {
organizationId,
filterField: "userId",
filterValue: user.id,
filterOperator: "eq",
},
headers: c.req.raw.headers,
});
const member = members.find((member) => member.userId === user.id);
if (!member || !getAllRolesAtOrAbove(role).includes(member.role)) {
throw new HttpException(HttpStatusCode.FORBIDDEN, {
code: "error.forbidden",
});
}
} catch {
throw new HttpException(HttpStatusCode.FORBIDDEN, {
code: "error.forbidden",
});
}
await next();
});
/**
* Middleware for adding an articifial delay in development.
*
* You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating
* network latency that would occur in production but not in local development.
*/
export const delay = createMiddleware<{
Bindings: {
NODE_ENV: string;
};
}>(async (c, next) => {
if (env(c).NODE_ENV === NodeEnv.DEVELOPMENT) {
// artificial delay in dev 100-500ms
const waitMs = Math.floor(Math.random() * 400) + 100;
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
await next();
});
/**
* Middleware for setting the language based on the cookie and accept-language header.
*/
export const localize = createMiddleware<{
Variables: {
locale: string;
};
}>(async (c, next) => {
const locale = getLocaleFromRequest(c.req.raw);
c.set("locale", locale);
await next();
});
/**
* Middleware for validating the request input using Zod.
*/
export const validate = <
T extends $ZodType,
Target extends keyof ValidationTargets,
>(
target: Target,
schema: T,
) =>
zValidator(
target,
schema,
async (result, c: Context<{ Variables: { locale?: string } }, string>) => {
if (!result.success) {
const { t } = await getTranslation({
locale: c.var.locale,
});
const error = result.error.issues[0];
if (!error) {
throw new HttpException(HttpStatusCode.UNPROCESSABLE_ENTITY);
}
const { message, code } = makeZodI18nMap({ t: t as TFunction })(
error as $ZodRawIssue,
);
throw new HttpException(HttpStatusCode.UNPROCESSABLE_ENTITY, {
code,
message,
});
}
},
);
/**
* Simple in-memory rate limiter middleware for AI endpoints.
* Limits requests per user per time window.
*/
const rateLimitStore = new Map<string, { count: number; resetAt: number }>();
export const rateLimiter = createMiddleware<{
Variables: {
user: User;
};
}>(async (c, next) => {
const user = c.var.user;
const windowMs = 60 * 1000; // 1 minute
const maxRequests = 30; // 30 requests per minute
const now = Date.now();
const key = `rate-limit:${user.id}`;
const record = rateLimitStore.get(key);
if (!record || record.resetAt < now) {
rateLimitStore.set(key, { count: 1, resetAt: now + windowMs });
await next();
return;
}
if (record.count >= maxRequests) {
throw new HttpException(HttpStatusCode.TOO_MANY_REQUESTS, {
code: "error.rateLimit",
message: "Too many requests. Please try again later.",
});
}
record.count += 1;
await next();
});
/**
* Middleware to deduct credits from a user's account before processing AI requests.
* Takes the amount of credits to deduct and optional feature name for audit logging.
*/
export const deductCredits = (amount: number, feature?: string) =>
createMiddleware<{
Variables: {
user: User;
};
}>(async (c, next) => {
const user = c.var.user;
// Find user's customer record
const [customerRecord] = await db
.select()
.from(customer)
.where(eq(customer.userId, user.id));
if (!customerRecord) {
throw new HttpException(HttpStatusCode.FORBIDDEN, {
code: "error.noCredits",
message: "No subscription found. Please subscribe to use AI features.",
});
}
if (customerRecord.credits < amount) {
throw new HttpException(HttpStatusCode.PAYMENT_REQUIRED, {
code: "error.insufficientCredits",
message: "Insufficient credits. Please add more credits to continue.",
});
}
const newBalance = customerRecord.credits - amount;
// Deduct credits and log transaction
await db.transaction(async (tx) => {
await tx
.update(customer)
.set({
credits: sql`${customer.credits} - ${amount}`,
})
.where(eq(customer.id, customerRecord.id));
await tx.insert(creditTransaction).values({
id: generateId(),
customerId: customerRecord.id,
amount: -amount,
type: "usage",
reason: feature ?? "AI feature usage",
balanceAfter: newBalance,
metadata: JSON.stringify({
endpoint: c.req.path,
feature,
}),
});
});
await next();
});