← back to Api Token Dashboard

server.mjs

192 lines

// API & Token Connection Dashboard — read-only status + fix links.
// NEVER returns secret values to the browser; only last4 + live status.
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PORT = Number(process.env.PORT) || 9791;
const AUTH = process.env.BASIC_AUTH || 'admin:DW2024!';
const HOME = process.env.HOME;

// ---- read-only env loaders ---------------------------------------------------
function loadEnv(p) {
  const c = {};
  try {
    for (const l of fs.readFileSync(p, 'utf8').split('\n')) {
      const m = l.match(/^([A-Z0-9_]+)=(.*)$/);
      if (m) c[m[1]] = m[2].trim().replace(/^["']|["']$/g, '');
    }
  } catch {}
  return c;
}
const S = () => loadEnv(`${HOME}/Projects/secrets-manager/.env`);
const G = () => loadEnv(`${HOME}/Projects/Designer-Wallcoverings/DW-MCP/.env`);
const last4 = (v) => (v && v.length >= 4 ? '…' + v.slice(-4) : v ? '…' : '');

// ---- probe helpers -----------------------------------------------------------
const TIMEOUT = 9000;
async function fetchT(url, opts = {}) {
  const ac = new AbortController();
  const t = setTimeout(() => ac.abort(), TIMEOUT);
  try { return await fetch(url, { ...opts, signal: ac.signal }); }
  finally { clearTimeout(t); }
}
const OK = (detail = '') => ({ status: 'connected', detail });
const DEAD = (detail) => ({ status: 'dead', detail });
const MISSING = (detail = 'not configured') => ({ status: 'missing', detail });
const UNKNOWN = (detail) => ({ status: 'unknown', detail });

// Shared probe for the common pattern: guard missing key → GET url with headers → r.ok ? OK : DEAD.
async function probeURL(envVal, url, headers) {
  if (!envVal) return MISSING();
  try {
    const r = await fetchT(url, { headers });
    return r.ok ? OK() : DEAD('http ' + r.status);
  } catch (e) { return UNKNOWN(e.name === 'AbortError' ? 'timeout' : e.message); }
}

async function googleRefresh(id, sec, rt) {
  if (!id || !sec || !rt) return MISSING('client id / secret / refresh token absent');
  try {
    const r = await fetchT('https://oauth2.googleapis.com/token', {
      method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({ client_id: id, client_secret: sec, refresh_token: rt, grant_type: 'refresh_token' }),
    });
    const j = await r.json().catch(() => ({}));
    return j.access_token ? OK() : DEAD(j.error || ('http ' + r.status));
  } catch (e) { return UNKNOWN(e.name === 'AbortError' ? 'timeout' : e.message); }
}

// ---- the connection catalog --------------------------------------------------
// Each item: { key, name, category, fixUrl, instructions, probe() }
function catalog() {
  const s = S(), g = G();
  const gid = g.GMAIL_CLIENT_ID, gs = g.GMAIL_CLIENT_SECRET;
  const items = [];
  const add = (o) => items.push(o);

  // ---- Google / George OAuth ----
  const mailbox = (label, rt, envName) => add({
    key: 'george-' + label, name: 'George: ' + label, category: 'Google · George Mail', tag: last4(rt),
    fixUrl: 'http://127.0.0.1:9850/auth' + (label === 'steve-office' ? '' : '/' + label),
    instructions: 'If DEAD: open the auth URL in a browser, sign in to the mailbox, grant consent. George mints a fresh refresh token (~7-day expiry in Testing mode).',
    probe: () => googleRefresh(gid, gs, rt), envName,
  });
  mailbox('steve-office', g.GMAIL_REFRESH_TOKEN, 'GMAIL_REFRESH_TOKEN');
  mailbox('info', g.INFO_REFRESH_TOKEN || g.GMAIL_INFO_REFRESH_TOKEN, 'INFO_REFRESH_TOKEN');
  mailbox('steve-personal', g.STEVE_PERSONAL_REFRESH_TOKEN || g.PERSONAL_REFRESH_TOKEN, 'STEVE_PERSONAL_REFRESH_TOKEN');
  mailbox('agentabrams', g.AGENTABRAMS_REFRESH_TOKEN, 'AGENTABRAMS_REFRESH_TOKEN');

  add({
    key: 'google-calendar', name: 'George Calendar (event create)', category: 'Google · George Mail', tag: '(via GMAIL client)',
    fixUrl: 'https://console.developers.google.com/apis/api/calendar-json.googleapis.com/overview?project=493480746821',
    instructions: 'ONLY blocker: Calendar API is DISABLED in project 493480746821 (403 accessNotConfigured). George already drives calendar via the live GMAIL client (which holds the calendar scope) — no code change, no re-consent, no token needed. Fix: click the link → Enable → this flips green on next re-check. (The old standalone GOOGLE_CALENDAR_REFRESH_TOKEN is vestigial/unused.)',
    envName: 'uses GMAIL_REFRESH_TOKEN',
    probe: async () => {
      if (!gid || !gs || !g.GMAIL_REFRESH_TOKEN) return MISSING('GMAIL client absent');
      try {
        const tr = await fetchT('https://oauth2.googleapis.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ client_id: gid, client_secret: gs, refresh_token: g.GMAIL_REFRESH_TOKEN, grant_type: 'refresh_token' }) });
        const tj = await tr.json().catch(() => ({}));
        if (!tj.access_token) return DEAD('GMAIL token dead');
        const cr = await fetchT('https://www.googleapis.com/calendar/v3/users/me/calendarList?maxResults=1', { headers: { Authorization: 'Bearer ' + tj.access_token } });
        if (cr.ok) return OK();
        const cj = await cr.json().catch(() => ({}));
        return DEAD(cj?.error?.errors?.[0]?.reason || ('http ' + cr.status));
      } catch (e) { return UNKNOWN(e.message); }
    },
  });

  const tsd = loadEnv(`${HOME}/Projects/thesetdecorator/.env`); // Drive token's real home (per routes.json)
  add({
    key: 'google-drive', name: 'Google Drive (TheSetDecorator photo-import)', category: 'Google · Other', tag: last4(tsd.GOOGLE_DRIVE_REFRESH_TOKEN),
    fixUrl: 'https://console.cloud.google.com/apis/credentials',
    instructions: 'NOT SET UP — thesetdecorator/photo-import/ingest-drive.js needs it but it was never minted. Fix: run `node ~/Projects/thesetdecorator/photo-import/scripts/drive-oauth-setup.js`, consent as steveabramsdesigns@gmail.com (read-only Drive scope), paste the printed GOOGLE_DRIVE_REFRESH_TOKEN → secrets skill routes it to thesetdecorator/.env.',
    probe: () => googleRefresh(tsd.GOOGLE_DRIVE_CLIENT_ID, tsd.GOOGLE_DRIVE_CLIENT_SECRET, tsd.GOOGLE_DRIVE_REFRESH_TOKEN), envName: 'GOOGLE_DRIVE_REFRESH_TOKEN',
  });

  add({
    key: 'ventura-scheduling', name: 'Ventura Smart-Scheduling', category: 'Google · Other', tag: last4(s.GOOGLE_OAUTH_CLIENT_ID),
    fixUrl: 'https://venturacorridor.com/api/appointments/oauth/start?owner_email=info@designerwallcoverings.com',
    instructions: 'NOT CONNECTED (0 owners consented) AND the venturacorridor.com domain is parked + the app server is down. Decide REVIVE (deploy + DNS + consent) vs SHELVE first. If reviving: bring the server up, then open the consent URL.',
    probe: async () => {
      try { const r = await fetchT('https://venturacorridor.com/api/appointments/oauth/status?owner_email=info@designerwallcoverings.com'); const j = await r.json().catch(() => ({})); return j.connected ? OK() : DEAD('parked/undeployed — 0 owners'); }
      catch { return DEAD('server unreachable (parked/down)'); }
    }, envName: 'GOOGLE_OAUTH_CLIENT_ID',
  });

  // ---- Meta ----
  add({
    key: 'meta', name: 'Meta — Instagram + Facebook', category: 'Social', tag: last4(s.META_ACCESS_TOKEN),
    fixUrl: 'https://developers.facebook.com/tools/explorer/',
    instructions: 'error 190 = expired. In Graph API Explorer select the DW app + Page, grant pages_manage_posts, instagram_basic, instagram_content_publish. Exchange for a 60-day token at /tools/debug/accesstoken, then route to META_ACCESS_TOKEN via secrets.',
    probe: async () => {
      if (!s.META_ACCESS_TOKEN) return MISSING();
      try { const r = await fetchT('https://graph.facebook.com/v19.0/me?access_token=' + encodeURIComponent(s.META_ACCESS_TOKEN)); const j = await r.json().catch(() => ({})); return j.id ? OK() : DEAD('error ' + (j.error?.code || r.status)); }
      catch (e) { return UNKNOWN(e.message); }
    }, envName: 'META_ACCESS_TOKEN',
  });

  // ---- Programmatic API keys (read-only verify endpoints) ----
  const key = (o) => add({ ...o, category: o.category || 'API keys' });
  key({ key: 'anthropic', name: 'Anthropic', tag: last4(s.ANTHROPIC_API_KEY), fixUrl: 'https://console.anthropic.com/settings/keys', instructions: 'Rotate at the console → paste new key → secrets skill routes it.', envName: 'ANTHROPIC_API_KEY',
    probe: () => probeURL(s.ANTHROPIC_API_KEY, 'https://api.anthropic.com/v1/models', { 'x-api-key': s.ANTHROPIC_API_KEY, 'anthropic-version': '2023-06-01' }) });
  key({ key: 'openai', name: 'OpenAI', tag: last4(s.OPENAI_API_KEY), fixUrl: 'https://platform.openai.com/api-keys', instructions: 'Rotate at platform.openai.com/api-keys.', envName: 'OPENAI_API_KEY',
    probe: () => probeURL(s.OPENAI_API_KEY, 'https://api.openai.com/v1/models', { Authorization: 'Bearer ' + s.OPENAI_API_KEY }) });
  key({ key: 'gemini', name: 'Google Gemini', tag: last4(s.GEMINI_API_KEY), fixUrl: 'https://aistudio.google.com/app/apikey', instructions: 'Create/rotate at AI Studio → API keys.', envName: 'GEMINI_API_KEY',
    probe: () => probeURL(s.GEMINI_API_KEY, 'https://generativelanguage.googleapis.com/v1beta/models?key=' + s.GEMINI_API_KEY, {}) });
  key({ key: 'replicate', name: 'Replicate', tag: last4(s.REPLICATE_API_TOKEN), fixUrl: 'https://replicate.com/account/api-tokens', instructions: 'Rotate at replicate.com/account/api-tokens.', envName: 'REPLICATE_API_TOKEN',
    probe: () => probeURL(s.REPLICATE_API_TOKEN, 'https://api.replicate.com/v1/account', { Authorization: 'Token ' + s.REPLICATE_API_TOKEN }) });
  key({ key: 'elevenlabs', name: 'ElevenLabs', tag: last4(s.ELEVENLABS_API_KEY), fixUrl: 'https://elevenlabs.io/app/settings/api-keys', instructions: 'Rotate in ElevenLabs settings → API keys.', envName: 'ELEVENLABS_API_KEY',
    probe: () => probeURL(s.ELEVENLABS_API_KEY, 'https://api.elevenlabs.io/v1/user', { 'xi-api-key': s.ELEVENLABS_API_KEY }) });
  key({ key: 'cloudflare', name: 'Cloudflare', tag: last4(s.CLOUDFLARE_API_TOKEN), fixUrl: 'https://dash.cloudflare.com/profile/api-tokens', instructions: 'Roll the token in CF dashboard → API tokens.', envName: 'CLOUDFLARE_API_TOKEN',
    probe: async () => { if (!s.CLOUDFLARE_API_TOKEN) return MISSING(); try { const r = await fetchT('https://api.cloudflare.com/client/v4/user/tokens/verify', { headers: { Authorization: 'Bearer ' + s.CLOUDFLARE_API_TOKEN } }); const j = await r.json().catch(() => ({})); return j.success ? OK() : DEAD('http ' + r.status); } catch (e) { return UNKNOWN(e.message); } } });
  key({ key: 'shopify', name: 'Shopify Admin', tag: last4(s.SHOPIFY_ADMIN_TOKEN), fixUrl: 'https://admin.shopify.com/settings/apps', instructions: 'Reinstall/rotate the custom app token in Shopify admin → Apps.', envName: 'SHOPIFY_ADMIN_TOKEN',
    probe: async () => { const dom = s.SHOPIFY_STORE_DOMAIN, tok = s.SHOPIFY_ADMIN_TOKEN; if (!dom || !tok) return MISSING(); try { const r = await fetchT('https://' + dom + '/admin/api/2024-01/shop.json', { headers: { 'X-Shopify-Access-Token': tok } }); return r.ok ? OK() : DEAD('http ' + r.status); } catch (e) { return UNKNOWN(e.message); } } });
  key({ key: 'stripe', name: 'Stripe', tag: last4(s.STRIPE_SECRET_KEY), fixUrl: 'https://dashboard.stripe.com/apikeys', instructions: 'Roll the secret key in the Stripe dashboard → Developers → API keys.', envName: 'STRIPE_SECRET_KEY',
    probe: () => probeURL(s.STRIPE_SECRET_KEY, 'https://api.stripe.com/v1/balance', { Authorization: 'Bearer ' + s.STRIPE_SECRET_KEY }) });
  key({ key: 'twilio', name: 'Twilio', tag: last4(s.TWILIO_AUTH_TOKEN), fixUrl: 'https://console.twilio.com/', instructions: 'Rotate the auth token in the Twilio console.', envName: 'TWILIO_AUTH_TOKEN',
    probe: async () => { const sid = s.TWILIO_ACCOUNT_SID, tok = s.TWILIO_AUTH_TOKEN; if (!sid || !tok) return MISSING(); try { const r = await fetchT('https://api.twilio.com/2010-04-01/Accounts/' + sid + '.json', { headers: { Authorization: 'Basic ' + Buffer.from(sid + ':' + tok).toString('base64') } }); return r.ok ? OK() : DEAD('http ' + r.status); } catch (e) { return UNKNOWN(e.message); } } });
  key({ key: 'purelymail', name: 'Purelymail', tag: last4(s.PURELYMAIL_API_TOKEN), fixUrl: 'https://purelymail.com/manage/api', instructions: 'Regenerate the API token in Purelymail → Manage → API.', envName: 'PURELYMAIL_API_TOKEN',
    probe: async () => { if (!s.PURELYMAIL_API_TOKEN) return MISSING(); try { const r = await fetchT('https://purelymail.com/api/v0/checkAccountCredit', { method: 'POST', headers: { 'Purelymail-Api-Token': s.PURELYMAIL_API_TOKEN, 'Content-Type': 'application/json' }, body: '{}' }); const j = await r.json().catch(() => ({})); return (j.type === 'success' || j.result) ? OK() : DEAD('http ' + r.status); } catch (e) { return UNKNOWN(e.message); } } });
  key({ key: 'browserbase', name: 'Browserbase', tag: last4(s.BROWSERBASE_API_KEY), fixUrl: 'https://www.browserbase.com/settings', instructions: 'Rotate the API key in Browserbase settings.', envName: 'BROWSERBASE_API_KEY',
    probe: () => probeURL(s.BROWSERBASE_API_KEY, 'https://api.browserbase.com/v1/projects', { 'X-BB-API-Key': s.BROWSERBASE_API_KEY }) });
  key({ key: 'moonshot', name: 'Moonshot (Kimi)', tag: last4(s.MOONSHOT_API_KEY), fixUrl: 'https://platform.moonshot.ai/console/api-keys', instructions: 'Rotate the key in the Moonshot console.', envName: 'MOONSHOT_API_KEY',
    probe: () => probeURL(s.MOONSHOT_API_KEY, 'https://api.moonshot.ai/v1/models', { Authorization: 'Bearer ' + s.MOONSHOT_API_KEY }) });
  key({ key: 'godaddy', name: 'GoDaddy', tag: last4(s.GODADDY_API_KEY), fixUrl: 'https://developer.godaddy.com/keys', instructions: 'Create a production key at developer.godaddy.com/keys (OTE keys 403 on prod).', envName: 'GODADDY_API_KEY',
    probe: async () => { const k = s.GODADDY_API_KEY, sec = s.GODADDY_API_SECRET, pat = s.GODADDY_PAT; if (!pat && (!k || !sec)) return MISSING(); const GD_AUTH = pat ? 'Bearer ' + pat : 'sso-key ' + k + ':' + sec; try { const r = await fetchT('https://api.godaddy.com/v1/domains?limit=1', { headers: { Authorization: GD_AUTH } }); return r.ok ? OK() : DEAD('http ' + r.status); } catch (e) { return UNKNOWN(e.message); } } });

  return items;
}

// ---- server ------------------------------------------------------------------
function unauthorized(res) { res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="tokens"' }); res.end('auth required'); }
function checkAuth(req) { const h = req.headers.authorization || ''; const m = h.match(/^Basic (.+)$/); if (!m) return false; return Buffer.from(m[1], 'base64').toString() === AUTH; }

const server = http.createServer(async (req, res) => {
  if (!checkAuth(req)) return unauthorized(res);
  const url = new URL(req.url, 'http://x');
  if (url.pathname === '/api/status') {
    const items = catalog();
    const results = await Promise.all(items.map(async (it) => {
      let r; try { r = await it.probe(); } catch (e) { r = UNKNOWN(e.message); }
      return { key: it.key, name: it.name, category: it.category, tag: it.tag || '', envName: it.envName || '',
        fixUrl: it.fixUrl, instructions: it.instructions, status: r.status, detail: r.detail || '' };
    }));
    res.writeHead(200, { 'Content-Type': 'application/json' });
    return res.end(JSON.stringify({ generatedAt: new Date().toISOString(), results }));
  }
  // static — resolve then confirm the path stays inside public/ (traversal guard)
  const root = path.resolve(__dirname, 'public');
  const fp = path.resolve(root, '.' + path.normalize(url.pathname === '/' ? '/index.html' : url.pathname));
  if (fp !== root && !fp.startsWith(root + path.sep)) { res.writeHead(403); return res.end('forbidden'); }
  fs.readFile(fp, (err, data) => {
    if (err) { res.writeHead(404); return res.end('not found'); }
    const ext = path.extname(fp);
    const ct = ext === '.html' ? 'text/html' : ext === '.js' ? 'text/javascript' : ext === '.css' ? 'text/css' : 'application/octet-stream';
    res.writeHead(200, { 'Content-Type': ct }); res.end(data);
  });
});
server.listen(PORT, () => console.log(`[api-token-dashboard] http://127.0.0.1:${PORT}  (auth ${AUTH.split(':')[0]}/****)`));