/** * POST /_emdash/api/auth/session-tokens * * A short-lived personal API token for the signed-in user, for browser-side * helpers (toolbar extensions) that hand an agent or an integration a * credential acting as that user. Session-only: a token can never mint * another token, so a leaked one cannot fan out. The token carries the * caller's own policies (or a subset of them), expires within a day, and is * intersected with the owner's grants on every request like any other token. */ import type { APIRoute } from "astro"; import { z } from "zod"; import { requirePerm } from "#api/authorize.js"; import { apiError, apiSuccess, handleError } from "#api/error.js"; import { handleApiTokenCreate } from "#api/handlers/api-tokens.js"; import { isParseError, parseBody } from "#api/parse.js"; export const prerender = false; export const MAX_TTL_SECONDS = 24 * 60 * 60; export const DEFAULT_TTL_SECONDS = 8 * 60 * 60; const schema = z.object({ /** Names the token in Settings → API tokens (" · session"). */ purpose: z.string().trim().min(1).max(60), expiresInSeconds: z.number().int().min(60).max(MAX_TTL_SECONDS).optional(), /** Policy slugs to carry; omitted = every policy the caller's role holds. */ policies: z.array(z.string().min(1)).max(50).optional(), }); export const POST: APIRoute = async ({ request, locals }) => { const { emdash, user, authz } = locals; if (!emdash?.db) return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500); if (locals.tokenAuth) { return apiError( "TOKEN_AUTH_FORBIDDEN", "Session tokens can only be minted from a signed-in session.", 403, ); } const denied = requirePerm(user, "content:edit_own"); if (denied) return denied; if (!authz) return apiError("NOT_CONFIGURED", "Authorization was not resolved for this request", 500); try { const body = await parseBody(request, schema); if (isParseError(body)) return body; const held = new Set(authz.rolePolicies); const policies = body.policies?.length ? body.policies.filter((slug) => held.has(slug)) : [...authz.rolePolicies]; if (policies.length === 0) { return apiError( "POLICY_NOT_HELD", "None of the requested policies are held by your role.", 403, ); } const ttl = body.expiresInSeconds ?? DEFAULT_TTL_SECONDS; const expiresAt = new Date(Date.now() + ttl * 1000).toISOString(); const result = await handleApiTokenCreate(emdash.db, user!.id, { name: `${body.purpose} · session`, policies, expiresAt, }); if (!result.success) return apiError(result.error.code, result.error.message, 500); return apiSuccess({ id: result.data.info.id, token: result.data.token, expiresAt, policies }); } catch (error) { return handleError(error, "Failed to mint a session token", "SESSION_TOKEN_ERROR"); } };