[object Object]

← back to Secrets Manager

add safe local credentials center

d972b48199537ff79374e75127002433f230752b · 2026-09-02 20:35:17 -0700 · Steve Abrams

Files touched

Diff

commit d972b48199537ff79374e75127002433f230752b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 2 20:35:17 2026 -0700

    add safe local credentials center
---
 update-viewer/index.html                       | 48 ++++++++++++++++++++++++++
 update-viewer/server.mjs                       | 46 +++++++++++++++++++++++-
 verification/credentials-center-e2e-proof.json | 33 ++++++++++++++++++
 3 files changed, 126 insertions(+), 1 deletion(-)

diff --git a/update-viewer/index.html b/update-viewer/index.html
index 6ae214f..cda9177 100644
--- a/update-viewer/index.html
+++ b/update-viewer/index.html
@@ -37,6 +37,20 @@
   .mint:hover{background:#7dd3fc11}
   .runnable{margin-top:10px;font-size:11px;color:var(--ok)}
   .row-label{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin:14px 0 2px}
+  .catalog-shell{max-width:1400px;margin:34px auto 0;padding:0 24px}
+  .catalog-head{display:flex;align-items:end;justify-content:space-between;gap:18px;margin-bottom:12px;flex-wrap:wrap}
+  .catalog-head h2{font-family:'Cormorant Garamond',Georgia,serif;font-size:25px}
+  .controls{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
+  .controls input,.controls select{background:#11151b;color:var(--ink);border:1px solid var(--rule);border-radius:7px;padding:7px 9px}
+  .controls input[type=search]{width:260px}.controls input[type=range]{padding:0;width:110px}
+  .cred-grid{--cols:3;display:grid;grid-template-columns:repeat(var(--cols),minmax(0,1fr));gap:10px}
+  .cred{background:var(--panel);border:1px solid var(--rule);border-radius:10px;padding:13px;min-width:0}
+  .cred.missing{border-left:3px solid var(--p1)}.cred.ready{border-left:3px solid var(--ok)}
+  .cred-name{font:600 12px 'SF Mono',ui-monospace,monospace;overflow-wrap:anywhere}.cred-label{font-size:12px;color:#bdc3ce;margin:6px 0;line-height:1.35}
+  .meta{display:flex;gap:6px;flex-wrap:wrap;margin-top:8px}.meta span{font-size:10px;color:var(--muted);border:1px solid var(--rule);border-radius:999px;padding:2px 7px}
+  .when{display:block;font-size:10px;color:var(--muted);margin-top:8px}.cred-actions{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}
+  .cred-actions button{cursor:pointer;background:#0a0c10;color:#9fd;border:1px solid var(--rule);border-radius:6px;padding:5px 8px;font-size:10px}
+  @media(max-width:900px){.cred-grid{grid-template-columns:1fr!important}.controls input[type=search]{width:100%}}
 </style>
 </head>
 <body>
@@ -46,8 +60,22 @@
   <div class="counts" id="counts"></div>
 </header>
 <div class="wrap" id="wrap"></div>
+<section class="catalog-shell">
+  <div class="catalog-head"><div><h2>Canonical credential registry</h2><div class="sub">Metadata only—secret values never enter this page.</div></div>
+    <div class="controls">
+      <input id="credSearch" type="search" placeholder="Search credentials…">
+      <select id="credStatus"><option value="all">All</option><option value="missing">Missing</option><option value="ready">Configured</option></select>
+      <select id="credSort"><option value="status">Missing first</option><option value="updated">Recently updated</option><option value="name">Name A→Z</option><option value="routes">Most destinations</option></select>
+      <label class="sub">Density <input id="credDensity" type="range" min="1" max="5" value="3"></label>
+    </div>
+  </div>
+  <div class="counts" id="credCounts"></div>
+  <div class="cred-grid" id="credGrid"></div>
+</section>
 <script>
 const esc=s=>String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
+const fmtDate=v=>v?new Date(v).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}):'Never registered';
+const copy=t=>navigator.clipboard&&navigator.clipboard.writeText(t);
 function cmdBox(label,txt){ if(!txt)return ''; return '<div class="cmd" onclick="navigator.clipboard&&navigator.clipboard.writeText(this.dataset.c)" data-c="'+esc(txt)+'"><span class="lbl">'+label+' · copy</span>'+esc(txt)+'</div>'; }
 async function load(){
   const d=await (await fetch('/api/items')).json();
@@ -107,6 +135,26 @@ async function load(){
   });
 }
 load();
+
+let CREDS=[];
+function renderCreds(){
+  const q=document.getElementById('credSearch').value.trim().toLowerCase();
+  const status=document.getElementById('credStatus').value;
+  const sort=document.getElementById('credSort').value;
+  let rows=CREDS.filter(x=>(status==='all'||(status==='ready')===x.configured)&&(!q||(x.name+' '+x.label).toLowerCase().includes(q)));
+  rows.sort((a,b)=>sort==='name'?a.name.localeCompare(b.name):sort==='updated'?String(b.lastUpdated||'').localeCompare(String(a.lastUpdated||'')):sort==='routes'?b.destinationCount-a.destinationCount:Number(a.configured)-Number(b.configured)||a.name.localeCompare(b.name));
+  document.getElementById('credCounts').innerHTML='<span class="pill"><b>'+CREDS.length+'</b> total</span><span class="pill"><b>'+CREDS.filter(x=>x.configured).length+'</b> configured</span><span class="pill" style="color:var(--p1)"><b>'+CREDS.filter(x=>!x.configured).length+'</b> missing</span><span class="pill"><b>'+rows.length+'</b> shown</span>';
+  document.getElementById('credGrid').innerHTML=rows.map(x=>{
+    const add="node ~/Projects/secrets-manager/cli.js add "+x.name+" '<PASTE_VALUE>'";
+    const check='node ~/Projects/secrets-manager/cli.js check '+x.name;
+    return '<article class="cred '+(x.configured?'ready':'missing')+'"><div class="cred-name">'+esc(x.name)+'</div><div class="cred-label">'+esc(x.label)+'</div><div class="meta"><span>'+(x.configured?'✓ configured':'● missing')+'</span><span>'+x.destinationCount+' destinations</span><span>'+(x.hasVerifier?'verify available':'manual verify')+'</span>'+(x.digest?'<span>'+esc(x.digest)+'</span>':'')+'</div><time class="when" title="'+esc(x.lastUpdated||'')+'">🕓 '+esc(fmtDate(x.lastUpdated))+'</time><div class="cred-actions"><button onclick="copy('+JSON.stringify(add).replace(/"/g,'&quot;')+')">Copy add command</button><button onclick="copy('+JSON.stringify(check).replace(/"/g,'&quot;')+')">Copy check</button></div></article>';
+  }).join('');
+}
+async function loadCreds(){CREDS=(await (await fetch('/api/catalog')).json()).items;renderCreds();}
+for(const id of ['credSearch','credStatus','credSort'])document.getElementById(id).addEventListener(id==='credSearch'?'input':'change',e=>{localStorage.setItem('secrets-'+id,e.target.value);renderCreds();});
+for(const id of ['credStatus','credSort']){const v=localStorage.getItem('secrets-'+id);if(v)document.getElementById(id).value=v;}
+const density=document.getElementById('credDensity');density.value=localStorage.getItem('secrets-density')||3;document.getElementById('credGrid').style.setProperty('--cols',density.value);density.oninput=e=>{document.getElementById('credGrid').style.setProperty('--cols',e.target.value);localStorage.setItem('secrets-density',e.target.value);};
+loadCreds();
 </script>
 </body>
 </html>
diff --git a/update-viewer/server.mjs b/update-viewer/server.mjs
index d59bea7..d79a1ec 100644
--- a/update-viewer/server.mjs
+++ b/update-viewer/server.mjs
@@ -7,14 +7,58 @@ 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 {
@@ -28,4 +72,4 @@ http.createServer((req, res) => {
     return;
   }
   send(res, 404, 'text/plain', 'not found');
-}).listen(PORT, () => console.log(`Credentials Update Tracker → http://localhost:${PORT}`));
+}).listen(PORT, '127.0.0.1', () => console.log(`Credentials Update Tracker → http://127.0.0.1:${PORT}`));
diff --git a/verification/credentials-center-e2e-proof.json b/verification/credentials-center-e2e-proof.json
new file mode 100644
index 0000000..c859557
--- /dev/null
+++ b/verification/credentials-center-e2e-proof.json
@@ -0,0 +1,33 @@
+{
+  "intent": "Give Steve one local, metadata-only place to find configured and missing credentials and copy safe CLI commands.",
+  "riskTier": "R2",
+  "environment": "local macOS, PM2 credentials-center, http://127.0.0.1:9800",
+  "ticket": "TK-11137",
+  "timestamp": "2026-09-02T23:55:00Z",
+  "precondition": "Canonical registry and routes exist; credential values must never cross the API or UI boundary.",
+  "checks": [
+    { "boundary": "service", "assertion": "PM2 process online and /healthz returns ok", "verdict": "PASS" },
+    { "boundary": "API", "assertion": "216 metadata records; 190 configured and 26 missing; safe schema", "verdict": "PASS" },
+    { "boundary": "security", "assertion": "API and rendered UI contain zero recognized secret-shaped values", "verdict": "PASS" },
+    { "boundary": "HTTP", "assertion": "200 HTML plus successful /healthz and /api/catalog contracts", "verdict": "PASS" },
+    { "boundary": "render", "assertion": "Headless Chrome produced a nonblank 158 KB screenshot", "verdict": "PASS" },
+    { "boundary": "interaction", "assertion": "216 cards render with zero JavaScript errors; search narrows to one; status filter, name sort, and density control work", "verdict": "PASS" },
+    { "boundary": "persistence", "assertion": "Name sort and five-column density survive reload via localStorage", "verdict": "PASS" },
+    { "boundary": "admin timestamp", "assertion": "Created/updated date and time is visible on credential cards", "verdict": "PASS" },
+    { "boundary": "cross-browser", "assertion": "Installed Google Chrome automated render passes", "verdict": "PASS" },
+    { "boundary": "Safari/Firefox", "assertion": "Engines unavailable in automated runner; noncritical parity checks skipped", "verdict": "SKIP", "reason": "Only Chrome automation is installed; core local journey was fully exercised." }
+  ],
+  "artifacts": [
+    "verification/credentials-center-e2e.png",
+    "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/3x-8D85HD/render.png",
+    "/var/folders/rq/j8g1f7nn6jv6_lr1cfmqym6w0000gn/T/3x-8D85HD/e2e.png"
+  ],
+  "negativeChecks": [
+    "Secret-pattern sentinel across API response",
+    "Secret-pattern sentinel across rendered page text",
+    "Zero browser console and page errors",
+    "Localhost-only bind"
+  ],
+  "cleanup": "No credential values were written or changed. Test browser closed; PM2 service intentionally retained.",
+  "verdict": "PASS"
+}

← 08ddb77 auto-data-snapshot: 2026-09-02T17:24:05 (1 data files) — ver  ·  back to Secrets Manager  ·  Route Charge and Explore Stripe test key 3990c33 →