[object Object]

← back to Credentials Agentabrams

initial scaffold: credentials.agentabrams.com credential-actions dashboard (Basic-auth, seeded from TK-11786 rotation checklist)

51fb7691d28b8f1cc3007bca8fd2fb2fdc93e377 · 2026-09-22 17:51:39 -0700 · Steve

Files touched

Diff

commit 51fb7691d28b8f1cc3007bca8fd2fb2fdc93e377
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Sep 22 17:51:39 2026 -0700

    initial scaffold: credentials.agentabrams.com credential-actions dashboard (Basic-auth, seeded from TK-11786 rotation checklist)
---
 .gitignore            |   8 ++++
 data/credentials.json |  88 ++++++++++++++++++++++++++++++++++++++++
 deploy/.deploy.conf   |   7 ++++
 package.json          |   8 ++++
 public/index.html     | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++
 server.js             |  57 ++++++++++++++++++++++++++
 6 files changed, 278 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1924158
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
diff --git a/data/credentials.json b/data/credentials.json
new file mode 100644
index 0000000..2bb7daf
--- /dev/null
+++ b/data/credentials.json
@@ -0,0 +1,88 @@
+{
+  "meta": {
+    "title": "Credential Actions — agentabrams",
+    "umbrella_ticket": "TK-11786",
+    "source": "~/Projects/secrets-manager/ROTATION-CHECKLIST.md",
+    "note": "Every credential/secret that Steve must personally rotate or supply. Console-only actions are Steve's; Claude cannot mint keys or run prod credential changes. No secret VALUES stored here — only key names, mint URLs, and route-back commands.",
+    "generated_at": "2026-09-22T17:48:00-07:00"
+  },
+  "items": [
+    {
+      "id": "dw-admin-pg-pw",
+      "title": "PG dw_admin password",
+      "priority": "P1",
+      "status": "OUTSTANDING",
+      "class": "console-only",
+      "created_at": "2026-05-30T00:00:00-07:00",
+      "origin": "2026-05-30 Kamatera compromise",
+      "exposed": "Old plaintext still live in 4 local .env.local DATABASE_URL DSNs; apps connect cleanly → dw_admin role still uses the compromised pw.",
+      "runbook": "~/Projects/_shared/docs/pg-rotation-runbook.md",
+      "mint_url": null,
+      "console_steps": "Pick new ≥20-char pw → ALTER ROLE dw_admin WITH PASSWORD on prod Kamatera (sudo -u postgres psql) AND local Mac2 → restart pm2 fleet → fan out.",
+      "route_back": "printf 'DW_ADMIN_DB_PASSWORD=<NEW_PW>' | node ~/Projects/secrets-manager/cli.js import-paste",
+      "verify": "node ~/Projects/secrets-manager/cli.js check",
+      "why": "dw_admin is the shared admin used by wallco, the DW fleet, Norma sdcc, etc. Stays compromised until this runs. ~32 min operation covering local + Kamatera."
+    },
+    {
+      "id": "gemini-key-1",
+      "title": "Gemini API key #1",
+      "priority": "P2",
+      "status": "OUTSTANDING",
+      "class": "console-mint",
+      "created_at": "2026-06-01T00:00:00-07:00",
+      "exposed": "Hardcoded in 7 Patty routes + 3 DW docs.",
+      "runbook": null,
+      "mint_url": "https://aistudio.google.com/apikey",
+      "console_steps": "Create new key, DELETE the old one in the same console.",
+      "route_back": "printf 'GEMINI_API_KEY=<NEW_KEY>' | node ~/Projects/secrets-manager/cli.js import-paste",
+      "verify": "node ~/Projects/secrets-manager/cli.js check (hits Gemini /models)",
+      "why": "Exposed key on-disk; rotate + delete old."
+    },
+    {
+      "id": "gemini-key-2",
+      "title": "Gemini API key #2",
+      "priority": "P2",
+      "status": "OUTSTANDING",
+      "class": "console-mint",
+      "created_at": "2026-06-01T00:00:00-07:00",
+      "exposed": "Hardcoded in PoppyPetitions/lib/gemini.ts.",
+      "runbook": null,
+      "mint_url": "https://aistudio.google.com/apikey",
+      "console_steps": "New key, delete old. Confirm the route name in routes.json (GEMINI_API_KEY_RALPH or poppy-specific).",
+      "route_back": "printf '<KEY_NAME>=<NEW_KEY>' | node ~/Projects/secrets-manager/cli.js import-paste",
+      "verify": "node ~/Projects/secrets-manager/cli.js check",
+      "why": "Exposed key on-disk; rotate + delete old."
+    },
+    {
+      "id": "repl-user-pg-pw",
+      "title": "PG repl_user replication password",
+      "priority": "P2",
+      "status": "OUTSTANDING",
+      "class": "console-only",
+      "created_at": "2026-06-03T00:00:00-07:00",
+      "exposed": "Lost (not leaked) — removed when the broken subscription was dropped 2026-06-03. dw_unified mirror stays frozen until reset.",
+      "runbook": "~/cncp-starter/yolo-queue/pending-approval/mirror-resync-FIX.md",
+      "mint_url": null,
+      "console_steps": "On Kamatera publisher: ALTER ROLE repl_user WITH PASSWORD; clear stray legacy table from publication dw_unified_pub.",
+      "route_back": "printf 'REPL_USER_DB_PASSWORD=<NEW_PW>' | node ~/Projects/secrets-manager/cli.js import-paste",
+      "verify": "REPL_PW=<NEW_PW> bash ~/cncp-starter/yolo-queue/run-mirror-resync.sh — max(synced_at) jumps to now",
+      "why": "Unblocks the frozen dw_unified mirror (Vendors/edges/settlement panels)."
+    },
+    {
+      "id": "shopify-admin-token",
+      "title": "SHOPIFY_ADMIN_TOKEN",
+      "priority": "P3",
+      "status": "RESOLVED",
+      "class": "console-mint",
+      "created_at": "2026-06-03T00:00:00-07:00",
+      "resolved_at": "2026-06-21T00:00:00-07:00",
+      "exposed": "Re-verified 2026-06-21: token WORKS (HTTP 200 against /admin/api/2024-04/shop.json). The earlier 'burned' text no longer holds.",
+      "runbook": null,
+      "mint_url": "https://admin.shopify.com (custom app → API credentials)",
+      "console_steps": "N/A — resolved.",
+      "route_back": "printf 'SHOPIFY_ADMIN_TOKEN=<NEW_TOKEN>' | node ~/Projects/secrets-manager/cli.js import-paste",
+      "verify": "node ~/Projects/secrets-manager/cli.js check",
+      "why": "Separate credential; verified live. Does NOT clear the dw_admin compromise flag."
+    }
+  ]
+}
diff --git a/deploy/.deploy.conf b/deploy/.deploy.conf
new file mode 100644
index 0000000..c5932c9
--- /dev/null
+++ b/deploy/.deploy.conf
@@ -0,0 +1,7 @@
+# credentials.agentabrams.com deploy config (GATED — do not fire without Steve's go)
+PROJECT_NAME=credentials-agentabrams
+DEPLOY_PATH=/root/Projects/credentials-agentabrams
+HEALTH_URL=https://credentials.agentabrams.com/health
+PORT=9769
+# DNS: Cloudflare A/CNAME credentials.agentabrams.com -> Kamatera 45.61.58.125 (GATED)
+# nginx: reverse-proxy vhost + certbot SSL (GATED)
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..ae7a9c7
--- /dev/null
+++ b/package.json
@@ -0,0 +1,8 @@
+{
+  "name": "credentials-agentabrams",
+  "version": "0.1.0",
+  "private": true,
+  "description": "credentials.agentabrams.com — pending credential-actions dashboard (Basic-auth)",
+  "main": "server.js",
+  "scripts": { "start": "node server.js" }
+}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..74c1ee5
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,110 @@
+<!doctype html>
+<html lang="en" data-theme="dark">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
+<title>Credential Actions — agentabrams</title>
+<style>
+  :root{
+    --bg:#0e1116; --panel:#161b22; --panel2:#1c232d; --line:#2a3441; --ink:#e6edf3; --dim:#9aa7b4;
+    --teal:#1abc9c; --amber:#e0a44b; --red:#e5534b; --green:#3fb950; --accent:#4493f8;
+    --cols:3;
+  }
+  *{box-sizing:border-box}
+  body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;padding:0}
+  header{padding:20px 24px 12px;border-bottom:1px solid var(--line);position:sticky;top:env(safe-area-inset-top,0);background:var(--bg);z-index:5}
+  h1{margin:0 0 2px;font-size:20px;letter-spacing:.3px}
+  .sub{color:var(--dim);font-size:12.5px}
+  .sub code{background:var(--panel2);padding:1px 6px;border-radius:5px;color:var(--accent)}
+  .controls{display:flex;gap:14px;align-items:center;flex-wrap:wrap;padding:12px 24px;border-bottom:1px solid var(--line);background:var(--panel)}
+  .controls label{font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--dim);margin-right:6px}
+  select,input[type=range]{background:var(--panel2);color:var(--ink);border:1px solid var(--line);border-radius:7px;padding:6px 9px;font-size:13px}
+  input[type=range]{padding:0;width:150px}
+  .stat{margin-left:auto;font-size:12.5px;color:var(--dim)}
+  .stat b{color:var(--ink)}
+  main{padding:20px 24px;display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:16px}
+  @media(max-width:900px){main{grid-template-columns:1fr}}
+  .card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px;display:flex;flex-direction:column;gap:10px;min-width:0}
+  .card.p1{border-left:3px solid var(--red)}
+  .card.resolved{opacity:.62}
+  .row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
+  .title{font-size:15px;font-weight:650;letter-spacing:.2px}
+  .badge{font-size:10.5px;font-weight:700;letter-spacing:.6px;text-transform:uppercase;padding:3px 8px;border-radius:20px}
+  .b-out{background:rgba(229,83,75,.16);color:#ff8b83}
+  .b-res{background:rgba(63,185,80,.16);color:#6ee787}
+  .b-pri{background:rgba(224,164,75,.16);color:#f0b866}
+  .b-cls{background:rgba(68,147,248,.14);color:#79b8ff}
+  .when{font-size:11px;color:var(--dim);margin-left:auto}
+  .field{font-size:12.5px}
+  .field .k{color:var(--dim);text-transform:uppercase;font-size:10px;letter-spacing:.5px;display:block;margin-bottom:2px}
+  .field .v{color:var(--ink)}
+  a{color:var(--accent);text-decoration:none}
+  a:hover{text-decoration:underline}
+  code{background:var(--panel2);border:1px solid var(--line);border-radius:6px;padding:2px 7px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;color:#c9d6e3;display:inline-block;max-width:100%;overflow-x:auto;white-space:nowrap}
+  .why{font-size:12px;color:var(--dim);border-top:1px dashed var(--line);padding-top:8px}
+</style>
+</head>
+<body>
+<header>
+  <h1>Credential Actions <span style="color:var(--dim);font-weight:400">— agentabrams</span></h1>
+  <div class="sub">Every secret Steve must personally rotate/supply · umbrella <code>TK-11786</code> · source <code>ROTATION-CHECKLIST.md</code></div>
+</header>
+<div class="controls">
+  <div><label>Sort</label>
+  <select id="sort">
+    <option value="priority">Priority</option>
+    <option value="status">Status (outstanding first)</option>
+    <option value="created">Newest</option>
+    <option value="title">Title A→Z</option>
+  </select></div>
+  <div><label>Density</label><input type="range" id="dens" min="1" max="4" value="3"></div>
+  <div class="stat" id="stat"></div>
+</div>
+<main id="grid"></main>
+<script>
+const $ = s => document.querySelector(s);
+let ITEMS = [];
+const PRI = { P1:0, P2:1, P3:2 };
+const fmtDate = iso => { try { return new Date(iso).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); } catch(e){ return iso; } };
+
+function render(){
+  const sort = $('#sort').value;
+  const items = [...ITEMS].sort((a,b)=>{
+    if(sort==='priority') return (PRI[a.priority]??9)-(PRI[b.priority]??9) || a.status.localeCompare(b.status);
+    if(sort==='status') return (a.status==='OUTSTANDING'?0:1)-(b.status==='OUTSTANDING'?0:1) || (PRI[a.priority]??9)-(PRI[b.priority]??9);
+    if(sort==='created') return new Date(b.created_at)-new Date(a.created_at);
+    return a.title.localeCompare(b.title);
+  });
+  const out = items.filter(i=>i.status==='OUTSTANDING').length;
+  $('#stat').innerHTML = `<b>${out}</b> outstanding · <b>${items.length}</b> total`;
+  $('#grid').innerHTML = items.map(i=>{
+    const resolved = i.status==='RESOLVED';
+    return `<div class="card ${i.priority==='P1'?'p1':''} ${resolved?'resolved':''}">
+      <div class="row">
+        <span class="title">${i.title}</span>
+        <span class="when" title="${i.created_at}">🕓 ${fmtDate(i.created_at)}</span>
+      </div>
+      <div class="row">
+        <span class="badge ${resolved?'b-res':'b-out'}">${i.status}</span>
+        <span class="badge b-pri">${i.priority}</span>
+        <span class="badge b-cls">${i.class}</span>
+      </div>
+      <div class="field"><span class="k">Exposed</span><span class="v">${i.exposed||'—'}</span></div>
+      ${i.mint_url?`<div class="field"><span class="k">Mint</span><span class="v"><a href="${i.mint_url}" target="_blank" rel="noopener noreferrer">${i.mint_url}</a></span></div>`:''}
+      ${i.runbook?`<div class="field"><span class="k">Runbook</span><span class="v"><code>${i.runbook}</code></span></div>`:''}
+      <div class="field"><span class="k">Console steps</span><span class="v">${i.console_steps||'—'}</span></div>
+      <div class="field"><span class="k">Route back</span><span class="v"><code>${i.route_back||'—'}</code></span></div>
+      <div class="field"><span class="k">Verify</span><span class="v"><code>${i.verify||'—'}</code></span></div>
+      <div class="why">${i.why||''}</div>
+    </div>`;
+  }).join('');
+}
+
+$('#sort').onchange = ()=>{ render(); try{localStorage.setItem('cred_sort',$('#sort').value);}catch(e){} };
+$('#dens').oninput = e=>{ document.documentElement.style.setProperty('--cols', e.target.value); try{localStorage.setItem('cred_dens',e.target.value);}catch(e){} };
+try{ const s=localStorage.getItem('cred_sort'); if(s)$('#sort').value=s; const d=localStorage.getItem('cred_dens'); if(d){$('#dens').value=d;document.documentElement.style.setProperty('--cols',d);} }catch(e){}
+
+fetch('/api/credentials').then(r=>r.json()).then(d=>{ ITEMS=d.items||[]; render(); }).catch(e=>{ $('#grid').innerHTML='<p style="color:var(--red)">Failed to load: '+e+'</p>'; });
+</script>
+</body>
+</html>
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..96df271
--- /dev/null
+++ b/server.js
@@ -0,0 +1,57 @@
+// credentials.agentabrams.com — pending credential-actions dashboard
+// Basic-auth (admin/DW2024!), zero-dependency Node http. Serves public/ + /api/credentials.
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+
+const PORT = process.env.PORT || 9769;
+const USER = process.env.BASIC_USER || 'admin';
+const PASS = process.env.BASIC_PASS || 'DW2024!';
+const ROOT = __dirname;
+
+const MIME = { '.html': 'text/html; charset=utf-8', '.css': 'text/css', '.js': 'application/javascript', '.json': 'application/json' };
+
+function unauthorized(res) {
+  res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="credentials", charset="UTF-8"' });
+  res.end('Auth required');
+}
+
+function checkAuth(req) {
+  const h = req.headers.authorization || '';
+  if (!h.startsWith('Basic ')) return false;
+  const [u, p] = Buffer.from(h.slice(6), 'base64').toString().split(':');
+  return u === USER && p === PASS;
+}
+
+const server = http.createServer((req, res) => {
+  if (!checkAuth(req)) return unauthorized(res);
+
+  if (req.url === '/api/credentials') {
+    try {
+      const data = fs.readFileSync(path.join(ROOT, 'data', 'credentials.json'), 'utf8');
+      res.writeHead(200, { 'Content-Type': 'application/json' });
+      return res.end(data);
+    } catch (e) {
+      res.writeHead(500, { 'Content-Type': 'application/json' });
+      return res.end(JSON.stringify({ error: String(e) }));
+    }
+  }
+
+  if (req.url === '/health') {
+    res.writeHead(200, { 'Content-Type': 'application/json' });
+    return res.end(JSON.stringify({ ok: true, port: PORT }));
+  }
+
+  // static
+  let rel = req.url.split('?')[0];
+  if (rel === '/' || rel === '') rel = '/index.html';
+  const filePath = path.join(ROOT, 'public', path.normalize(rel));
+  if (!filePath.startsWith(path.join(ROOT, 'public'))) { res.writeHead(403); return res.end('forbidden'); }
+  fs.readFile(filePath, (err, buf) => {
+    if (err) { res.writeHead(404); return res.end('not found'); }
+    res.writeHead(200, { 'Content-Type': MIME[path.extname(filePath)] || 'application/octet-stream' });
+    res.end(buf);
+  });
+});
+
+server.listen(PORT, () => console.log(`credentials dashboard on http://127.0.0.1:${PORT} (admin/DW2024!)`));

(oldest)  ·  back to Credentials Agentabrams  ·  fix credentials dashboard: Shopify token RESOLVED→OUTSTANDIN fffe6ab →