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