Repository Public MIT
1import { randomUUID } from "node:crypto";2import type { Sql } from "@skills/db";3import type { Scope } from "@skills/registry-core";4import { db } from "./db";56export interface UserRow {7 id: string;8 email: string;9 display_name: string;10 platform_role: "owner" | "admin" | "reviewer" | "member";11 status: "active" | "suspended" | "disabled";12 must_change_password: boolean;13 email_verified_at: Date | null;14}1516export type Principal =17 | { kind: "anonymous" }18 | { kind: "session"; user: UserRow; sessionId: string; reauthAt: Date; csrfSecret: string }19 | { kind: "token"; user: UserRow; token: { id: string; name: string; prefix: string; scopes: Scope[]; namespaces: string[]; elevated: boolean; expiresAt: Date } }20 /** Operator with shell access on the server (admin CLI). Never reachable over the network. */21 | { kind: "local"; user: UserRow | null };2223export interface Ctx {24 sql: Sql;25 requestId: string;26 ip: string;27 userAgent: string;28 principal: Principal;29}3031export function makeCtx(p: Partial<Ctx> = {}): Ctx {32 return { sql: p.sql ?? db(), requestId: p.requestId ?? randomUUID(), ip: p.ip ?? "local", userAgent: p.userAgent ?? "", principal: p.principal ?? { kind: "anonymous" } };33}3435export function clientIp(headers: Headers): string {36 // The app is only reachable through the Caddy gateway, which appends the real peer as the last hop.37 const xff = headers.get("x-forwarded-for");38 if (xff) {39 const last = xff.split(",").pop()!.trim();40 if (last) return last.slice(0, 64);41 }42 return (headers.get("x-real-ip") ?? "local").slice(0, 64);43}4445export function userOf(p: Principal): UserRow | null {46 return p.kind === "anonymous" ? null : p.user;47}48export function actorLabel(p: Principal): string {49 if (p.kind === "anonymous") return "anonymous";50 if (p.kind === "local") return `local-operator${p.user ? `:${p.user.email}` : ""}`;51 if (p.kind === "token") return `token:${p.token.prefix}(${p.token.name})`;52 return `session:${p.user.email}`;53}54