← back to Norma
app/api/registry/route.ts
66 lines
// GET /api/registry — returns the integration registry + set/missing status
// for each env var. Values themselves NEVER leave the server.
//
// Admin-gated: requires the norma-auth cookie with admin role.
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { INTEGRATIONS, CATEGORY_ORDER, statusFor } from '@/lib/api-registry';
import { verifySessionToken, type AuthSession } from '@/lib/auth';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
// SECURITY: verify the HMAC-signed session token — do NOT "tolerant-decode" the
// cookie. The old code read parts[1] (the signature, not the payload) so real
// admins always got 403, AND an unsigned crafted token could have claimed
// role:admin. verifySessionToken checks the HMAC signature + expiry.
async function readSession(): Promise<AuthSession | null> {
const jar = await cookies();
const raw = jar.get('norma-auth')?.value;
if (!raw) return null;
return verifySessionToken(raw);
}
export async function GET() {
const session = await readSession();
if (!session || session.role !== 'admin') {
return NextResponse.json({ error: 'admin only' }, { status: 403 });
}
const entries = INTEGRATIONS.map(e => ({
id: e.id,
name: e.name,
category: e.category,
purpose: e.purpose,
required: e.required,
envVars: e.envVars,
status: statusFor(e),
signupUrl: e.signupUrl,
docsUrl: e.docsUrl,
scopes: e.scopes,
pricing: e.pricing,
getSteps: e.getSteps,
// Which env vars are set (boolean only — no values).
envVarStatus: Object.fromEntries(
e.envVars.map(v => [v, !!process.env[v] && String(process.env[v]).length > 0]),
),
}));
const setCount = entries.filter(e => e.status === 'set').length;
const partialCount = entries.filter(e => e.status === 'partial').length;
const missingCount = entries.filter(e => e.status === 'missing').length;
return NextResponse.json({
categories: CATEGORY_ORDER,
entries,
summary: {
total: entries.length,
set: setCount,
partial: partialCount,
missing: missingCount,
requiredMissing: entries.filter(e => e.required && e.status !== 'set').length,
},
});
}