← back to Secrets Manager
add safe local credentials center
d972b48199537ff79374e75127002433f230752b · 2026-09-02 20:35:17 -0700 · Steve Abrams
Files touched
M update-viewer/index.htmlM update-viewer/server.mjsA verification/credentials-center-e2e-proof.json
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,'&').replace(/</g,'<').replace(/"/g,'"');
+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,'"')+')">Copy add command</button><button onclick="copy('+JSON.stringify(check).replace(/"/g,'"')+')">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 →