import { createServerFn } from "@tanstack/react-start"; import { z } from "zod"; import { requireViewer, viewerMiddleware } from "./middleware"; import { all, chunks, placeholders } from "~/server/db"; import { accountDb, repoDb } from "~/server/scoped-db"; import { CAPS, limitWrite, limitAccountWrite } from "~/server/limits"; import { assertNotArchived, requireRepoAccess } from "~/server/repos"; import { parseTokenPermissions, TOKEN_AREAS } from "~/server/token-scope"; import { createDeployToken as mintDeployToken, createFineGrainedToken as mintFineGrainedToken, revokeDeployToken as removeDeployToken, } from "~/server/token-write"; import { confirmPassword } from "~/server/account-security"; import { repoRef } from "~/lib/validation"; const areaAccess = z.enum(["read", "write"]).optional(); const permissionsInput = z.object(Object.fromEntries(TOKEN_AREAS.map((area) => [area, areaAccess]))); const expiresInDays = z.number().int().min(0).max(366); export const listTokenGrants = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .handler(async ({ context }) => { const viewer = requireViewer(context.viewer); const db = accountDb(viewer.id); const tokens = await db.all<{ id: string; name: string; token_prefix: string; permissions: string | null; all_repos: number; last_used_at: number | null; expires_at: number | null; created_at: number; parent_token_id: string | null; }>( `SELECT id, name, token_prefix, permissions, all_repos, last_used_at, expires_at, created_at, parent_token_id FROM access_tokens WHERE user_id = ? ORDER BY created_at DESC`, viewer.id, ); const links = await db.all<{ token_id: string; repo_id: string }>( `SELECT tr.token_id, tr.repo_id FROM access_token_repos tr JOIN access_tokens t ON t.id = tr.token_id WHERE t.user_id = ?`, viewer.id, ); const names = new Map(); for (const ids of chunks([...new Set(links.map((link) => link.repo_id))])) { const rows = await all<{ id: string; owner_name: string; name: string }>( `SELECT r.id, a.name AS owner_name, r.name FROM repos r JOIN accounts a ON a.id = r.owner_id WHERE r.id IN (${placeholders(ids.length)})`, ...ids, ); for (const row of rows) names.set(row.id, `${row.owner_name}/${row.name}`); } const repos = links.flatMap((link) => { const name = names.get(link.repo_id); return name ? [{ token_id: link.token_id, name }] : []; }); return tokens.map((token) => ({ id: token.id, name: token.name, prefix: token.token_prefix, permissions: token.permissions === null ? {} : parseTokenPermissions(token.permissions), allRepos: token.all_repos === 1, repos: repos.filter((repo) => repo.token_id === token.id).map((repo) => repo.name), lastUsedAt: token.last_used_at, expiresAt: token.expires_at, createdAt: token.created_at, /** Set when another token minted this one (REST); revoking that token revokes this one too. */ parentTokenId: token.parent_token_id, })); }); export const createFineGrainedToken = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator( z.object({ name: z.string().trim().min(1).max(100), expiresInDays, allRepos: z.boolean(), repos: z.array(z.string().regex(/^[^/\s]+\/[^/\s]+$/, "Use owner/name")).max(CAPS.reposPerFineGrainedToken), permissions: permissionsInput, password: z.string().max(200), }), ) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); const { password, ...input } = data; await confirmPassword(viewer.id, password); const { token } = await mintFineGrainedToken(viewer, input); return { token }; }); export const listDeployTokens = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(repoRef) .handler(async ({ data, context }) => { const { repo } = await requireRepoAccess(data.owner, data.repo, context.viewer, "admin"); const rows = await repoDb(repo.id).all<{ id: string; name: string; token_prefix: string; permission: "read" | "write"; last_used_at: number | null; expires_at: number | null; created_at: number; }>( "SELECT id, name, token_prefix, permission, last_used_at, expires_at, created_at FROM deploy_tokens WHERE repo_id = ? ORDER BY created_at DESC", repo.id, ); return rows.map((row) => ({ id: row.id, name: row.name, prefix: row.token_prefix, permission: row.permission, lastUsedAt: row.last_used_at, expiresAt: row.expires_at, createdAt: row.created_at, })); }); export const createDeployToken = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator( repoRef.extend({ name: z.string().trim().min(1).max(100), permission: z.enum(["read", "write"]), expiresInDays }), ) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, viewer, "admin"); assertNotArchived(repo); const { token } = await mintDeployToken(repo, viewer, data); return { token }; }); export const revokeDeployToken = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ id: z.string().min(1).max(64) })) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitAccountWrite(viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, viewer, "admin"); await removeDeployToken(repo.id, data.id); return { ok: true }; });