← back to Secrets Manager

update-viewer/server.mjs

76 lines

#!/usr/bin/env node
// Credentials & Tokens Update Tracker — local web viewer. Zero deps.
//   node server.mjs   → http://localhost:9778
// Reads update-items.json; lets Steve toggle status (persisted). No secret VALUES.
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';

const DIR = path.dirname(new URL(import.meta.url).pathname);
const ROOT = path.dirname(DIR);
const DATA = path.join(DIR, 'update-items.json');
const REGISTRY = path.join(ROOT, 'registry.json');
const ROUTES = path.join(ROOT, 'routes.json');
const PORT = process.env.PORT || 9778;
const send = (res, code, type, body) => { res.writeHead(code, { 'Content-Type': type, 'Cache-Control': 'no-store' }); res.end(body); };

function safeText(value) {
  return String(value || '')
    .replace(/sk_(?:live|test)_/gi, 'sk_[redacted]_')
    .replace(/xox[baprs]-/gi, 'slack-[redacted]-')
    .replace(/xapp-/gi, 'slack-app-[redacted]-')
    .replace(/ghp_/gi, 'github-[redacted]_');
}

function catalog() {
  const registry = JSON.parse(fs.readFileSync(REGISTRY, 'utf8'));
  const routes = JSON.parse(fs.readFileSync(ROUTES, 'utf8'));
  const saved = registry.secrets || {};
  const configuredRoutes = { ...(routes.services || {}) };
  for (const [key, value] of Object.entries(routes)) {
    if (/^[A-Z][A-Z0-9_]+$/.test(key) && value && typeof value === 'object') configuredRoutes[key] = value;
  }
  const names = [...new Set([...Object.keys(configuredRoutes), ...Object.keys(saved)])].sort();
  return names.map(name => {
    const record = saved[name] || {};
    const route = configuredRoutes[name] || {};
    const destinations = Array.isArray(route.destinations) ? route.destinations
      : Array.isArray(route.files) ? route.files
      : Array.isArray(record.written_to) ? record.written_to : [];
    return {
      name,
      label: safeText(route.label || record.label || name.replaceAll('_', ' ').toLowerCase()),
      configured: Boolean(saved[name]),
      digest: record.digest || null,
      validated: record.validated === true,
      verifyStatus: record.verify_status ?? null,
      lastUpdated: record.last_updated || null,
      destinationCount: destinations.length,
      hasVerifier: Boolean(route.verify),
      mintUrl: route.mintUrl || route.mint_url || null
    };
  });
}

http.createServer((req, res) => {
  const u = new URL(req.url, 'http://localhost');
  if (u.pathname === '/') return send(res, 200, 'text/html; charset=utf-8', fs.readFileSync(path.join(DIR, 'index.html')));
  if (u.pathname === '/favicon.ico') { res.writeHead(204, { 'Cache-Control': 'public, max-age=86400' }); return res.end(); }
  if (u.pathname === '/healthz') return send(res, 200, 'application/json', JSON.stringify({ ok: true, service: 'credentials-center' }));
  if (u.pathname === '/api/items') return send(res, 200, 'application/json', fs.readFileSync(DATA));
  if (u.pathname === '/api/catalog') return send(res, 200, 'application/json', JSON.stringify({ items: catalog() }));
  if (u.pathname === '/api/toggle' && req.method === 'POST') {
    let b = ''; req.on('data', c => b += c); req.on('end', () => {
      try {
        const { id, status } = JSON.parse(b);
        const d = JSON.parse(fs.readFileSync(DATA, 'utf8'));
        const it = d.items.find(x => x.id === id);
        if (it) { it.status = status; fs.writeFileSync(DATA, JSON.stringify(d, null, 2) + '\n'); }
        send(res, 200, 'application/json', JSON.stringify({ ok: !!it }));
      } catch (e) { send(res, 400, 'application/json', JSON.stringify({ ok: false, error: e.message })); }
    });
    return;
  }
  send(res, 404, 'text/plain', 'not found');
}).listen(PORT, '127.0.0.1', () => console.log(`Credentials Update Tracker → http://127.0.0.1:${PORT}`));