TypeScript 97.5%
SQL 1.4%
Python 0.8%
1"use server";23import { revalidatePath } from "next/cache";4import { z } from "zod";5import { getDb, apiKeys, projects, eq, and } from "@fetcha/db";6import { API_KEY_SCOPES, generateApiKey, newId, type ApiKeyScope } from "@fetcha/core";7import { getEmailService } from "@fetcha/email";8import { getWorkspace, requestMeta } from "@/lib/session";9import { writeAudit } from "@/lib/audit";10import { internalApi } from "@/lib/api";11import type { ActionResult } from "./projects";1213const createSchema = z.object({14 name: z.string().trim().min(1, "Give the key a name").max(64),15 projectId: z.string().min(1),16 mode: z.enum(["live", "test"]).default("live"),17 scopes: z.array(z.enum(API_KEY_SCOPES)).min(1, "Select at least one scope"),18 expiresInDays: z.coerce.number().int().min(0).max(3650).optional(),19});2021export interface CreatedKey {22 id: string;23 plaintext: string;24 prefix: string;25 name: string;26 projectId: string;27}2829export async function createApiKey(input: { name: string; projectId: string; mode?: "live" | "test"; scopes: string[]; expiresInDays?: number }): Promise<ActionResult<CreatedKey>> {30 const ws = await getWorkspace();31 const parsed = createSchema.safeParse(input);32 if (!parsed.success) return { ok: false, error: parsed.error.issues[0]?.message ?? "Invalid input" };33 const d = parsed.data;34 const db = getDb();35 const [project] = await db.select().from(projects).where(and(eq(projects.id, d.projectId), eq(projects.organizationId, ws.organization.id))).limit(1);36 if (!project) return { ok: false, error: "Project not found" };37 const key = generateApiKey(d.mode);38 const id = newId("key");39 await db.insert(apiKeys).values({40 id,41 organizationId: ws.organization.id,42 projectId: project.id,43 createdByUserId: ws.user.id,44 name: d.name,45 keyHash: key.hash,46 keyPrefix: key.prefix,47 last4: key.last4,48 mode: d.mode,49 scopes: d.scopes as ApiKeyScope[],50 expiresAt: d.expiresInDays ? new Date(Date.now() + d.expiresInDays * 86_400_000) : null,51 });52 const meta = await requestMeta();53 await writeAudit({ userId: ws.user.id, organizationId: ws.organization.id, action: "api_key.created", target: id, metadata: { name: d.name, project: project.name, prefix: key.prefix, mode: d.mode }, ipAddress: meta.ip, userAgent: meta.userAgent });54 getEmailService()55 .sendApiKeyCreated(ws.user.email, { keyName: d.name, prefix: key.prefix, projectName: project.name })56 .catch(() => {});57 revalidatePath("/dashboard", "layout");58 return { ok: true, data: { id, plaintext: key.plaintext, prefix: key.prefix, name: d.name, projectId: project.id } };59}6061export async function revokeApiKey(keyId: string): Promise<ActionResult> {62 const ws = await getWorkspace();63 const db = getDb();64 const [k] = await db.select().from(apiKeys).where(and(eq(apiKeys.id, keyId), eq(apiKeys.organizationId, ws.organization.id))).limit(1);65 if (!k) return { ok: false, error: "Key not found" };66 if (k.revokedAt) return { ok: true };67 await db.update(apiKeys).set({ revokedAt: new Date() }).where(eq(apiKeys.id, keyId));68 await internalApi.invalidateKey(k.keyHash);69 const meta = await requestMeta();70 await writeAudit({ userId: ws.user.id, organizationId: ws.organization.id, action: "api_key.revoked", target: keyId, metadata: { name: k.name, prefix: k.keyPrefix }, ipAddress: meta.ip, userAgent: meta.userAgent });71 getEmailService()72 .sendApiKeyRevoked(ws.user.email, { keyName: k.name, prefix: k.keyPrefix })73 .catch(() => {});74 revalidatePath("/dashboard", "layout");75 return { ok: true };76}7778/** Rotate: create a new key with the same settings, revoke the old one after a short grace period (immediately in V1). */79export async function rotateApiKey(keyId: string): Promise<ActionResult<CreatedKey>> {80 const ws = await getWorkspace();81 const db = getDb();82 const [k] = await db.select().from(apiKeys).where(and(eq(apiKeys.id, keyId), eq(apiKeys.organizationId, ws.organization.id))).limit(1);83 if (!k) return { ok: false, error: "Key not found" };84 const key = generateApiKey(k.mode as "live" | "test");85 const id = newId("key");86 await db.insert(apiKeys).values({87 id,88 organizationId: ws.organization.id,89 projectId: k.projectId,90 createdByUserId: ws.user.id,91 name: k.name,92 keyHash: key.hash,93 keyPrefix: key.prefix,94 last4: key.last4,95 mode: k.mode,96 scopes: k.scopes,97 expiresAt: k.expiresAt,98 rotatedFromId: k.id,99 });100 await db.update(apiKeys).set({ revokedAt: new Date() }).where(eq(apiKeys.id, k.id));101 await internalApi.invalidateKey(k.keyHash);102 const meta = await requestMeta();103 await writeAudit({ userId: ws.user.id, organizationId: ws.organization.id, action: "api_key.rotated", target: id, metadata: { from: k.id, name: k.name }, ipAddress: meta.ip, userAgent: meta.userAgent });104 revalidatePath("/dashboard", "layout");105 return { ok: true, data: { id, plaintext: key.plaintext, prefix: key.prefix, name: k.name, projectId: k.projectId } };106}107