profileShare

rasmusjy / roundtable

Read-only snapshot

No repository description.

main default branch 181 files Expires Sep 13, 2026, 9:06 AM
route.ts 2,585 bytes
1 import { NextResponse } from 'next/server';
2 import { z } from 'zod';
3 import { currentUserId } from '@/auth';
4 import {
5 clearSessionKeyCookie,
6 hasAnyKey,
7 setSessionKeyCookie,
8 } from '@/lib/byok';
9 import { maskKey } from '@/lib/crypto';
10 import { env } from '@/lib/env';
11 import { validateOpenRouterKey } from '@/lib/openrouter';
12 import { deleteSavedKey, getSavedKeyInfo, saveEncryptedKey } from '@/db/repositories';
13 import { encryptSecret } from '@/lib/crypto';
14
15 export const runtime = 'nodejs';
16
17 /** GET: current key status (never returns the key itself). */
18 export async function GET() {
19 const userId = await currentUserId();
20 const saved = userId ? await getSavedKeyInfo(userId) : null;
21 const status = await hasAnyKey();
22 return NextResponse.json({
23 mockMode: env.MOCK_LLM,
24 hasKey: status.has,
25 source: status.source,
26 saved,
27 authenticated: Boolean(userId),
28 });
29 }
30
31 const postSchema = z.object({
32 apiKey: z.string().min(8).max(512),
33 mode: z.enum(['save', 'session']),
34 label: z.string().max(80).optional(),
35 });
36
37 /** POST: validate a key against OpenRouter and store it (encrypted). */
38 export async function POST(request: Request) {
39 const body = await request.json().catch(() => null);
40 const parsed = postSchema.safeParse(body);
41 if (!parsed.success) {
42 return NextResponse.json({ error: 'Invalid request', issues: parsed.error.flatten() }, { status: 400 });
43 }
44 const { apiKey, mode, label } = parsed.data;
45
46 const validation = await validateOpenRouterKey(apiKey);
47 if (!validation.valid) {
48 return NextResponse.json({ error: validation.error ?? 'Key rejected by OpenRouter' }, { status: 400 });
49 }
50
51 if (mode === 'save') {
52 const userId = await currentUserId();
53 if (!userId) {
54 return NextResponse.json({ error: 'Sign in to save a key. Use session mode otherwise.' }, { status: 401 });
55 }
56 await saveEncryptedKey(userId, encryptSecret(apiKey), maskKey(apiKey), label ?? validation.label ?? null);
57 } else {
58 await setSessionKeyCookie(apiKey);
59 }
60
61 return NextResponse.json({
62 ok: true,
63 source: mode,
64 keyMask: maskKey(apiKey),
65 credits: {
66 usage: validation.usage ?? null,
67 limit: validation.limit ?? null,
68 remaining: validation.limitRemaining ?? null,
69 isFreeTier: validation.isFreeTier ?? null,
70 },
71 });
72 }
73
74 /** DELETE: remove the saved key and/or clear the session cookie. */
75 export async function DELETE() {
76 const userId = await currentUserId();
77 if (userId) await deleteSavedKey(userId);
78 await clearSessionKeyCookie();
79 return NextResponse.json({ ok: true });
80 }
81