← back to Norma

app/api/registry/route.ts

75 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';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

interface SessionPayload {
  role?: string;
  user?: string;
}

async function readSession(): Promise<SessionPayload | null> {
  const jar = await cookies();
  const raw = jar.get('norma-auth')?.value;
  if (!raw) return null;
  try {
    // The cookie is JWT-style or signed-JSON depending on lib/auth.ts; we
    // only need the payload to check the role. Tolerant decode.
    const parts = raw.split('.');
    const body = parts.length >= 2 ? parts[1] : parts[0];
    const json = Buffer.from(body, 'base64url').toString('utf8');
    return JSON.parse(json) as SessionPayload;
  } catch {
    return null;
  }
}

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,
    },
  });
}