← 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
A .gitignoreA data/credentials.jsonA deploy/.deploy.confA package.jsonA public/index.htmlA server.js
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 →