[object Object]

← back to Secrets Manager

secrets viewer: add read-only /rotation page (renders ROTATION-CHECKLIST.md as status cards)

4e224bae6fcdf0d6094e98ba5ca796549dc57b88 · 2026-06-03 08:30:33 -0700 · SteveStudio2

- new /rotation route + markdown→HTML renderer (cards w/ outstanding/done pills, summary counts)
- nav link from main viewer page
- checklist: added items 4 (SHOPIFY_ADMIN_TOKEN burned) + 5 (repl_user pw lost on sub drop)
- page shows key names + mint URLs + route-back commands only — never secret values

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 4e224bae6fcdf0d6094e98ba5ca796549dc57b88
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Wed Jun 3 08:30:33 2026 -0700

    secrets viewer: add read-only /rotation page (renders ROTATION-CHECKLIST.md as status cards)
    
    - new /rotation route + markdown→HTML renderer (cards w/ outstanding/done pills, summary counts)
    - nav link from main viewer page
    - checklist: added items 4 (SHOPIFY_ADMIN_TOKEN burned) + 5 (repl_user pw lost on sub drop)
    - page shows key names + mint URLs + route-back commands only — never secret values
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 ROTATION-CHECKLIST.md |  27 ++++++++++++
 viewer.js             | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 141 insertions(+)

diff --git a/ROTATION-CHECKLIST.md b/ROTATION-CHECKLIST.md
index f17152c..62febca 100644
--- a/ROTATION-CHECKLIST.md
+++ b/ROTATION-CHECKLIST.md
@@ -57,3 +57,30 @@ Pre-flight (read-only, 2026-06-01) confirmed safe to run:
 ~/Projects/_shared/scripts/rotate-pg-scoped.sh ~/Projects/Norma sdcc norma_app_user NORMA_APP_DB_PASSWORD
 ```
 **Caveats:** (a) the script is **LOCAL-only** (`127.0.0.1:5432`) — it does not touch prod Kamatera; (b) it's **interactive** (type the new ≥16-char pw twice at the prompt — the header's "pipe via stdin" is stale); (c) it does **not** rotate `dw_admin` — that's item #1 above.
+
+---
+
+## Newly identified (2026-06-03)
+
+## 4. `SHOPIFY_ADMIN_TOKEN`  ⬜ OUTSTANDING  (P1 — blocks DW catalog→Shopify push)
+
+- **Exposed / status:** likely the burned post-bubbe token. The full-monte Phase 8 catalog push reads `process.env.SHOPIFY_ADMIN_TOKEN` and currently gets `undefined` → every push auth-fails silently (catch-up "succeeded" counts are error-caught lies, 0 real writes). DW catalog has not pushed to Shopify since ~Apr 30.
+- **Mint:** Shopify admin → Apps → (custom app) → API credentials → revoke old Admin API access token, install/reveal a new one.
+- **Secret key name:** `SHOPIFY_ADMIN_TOKEN` (confirm against `routes.json`).
+- **Route back:**
+  ```
+  printf 'SHOPIFY_ADMIN_TOKEN=<NEW_TOKEN>' | node ~/Projects/secrets-manager/cli.js import-paste
+  ```
+  Then ensure the full-monte env actually sources it (sourced `.env` or a crontab global `SHOPIFY_ADMIN_TOKEN=` line) — Phase 8 has no `.env` and cron doesn't export it.
+- **Verify:** `node ~/Projects/secrets-manager/cli.js check` + a single Phase-8 dry push shows a real 200/201 write.
+
+## 5. PG `repl_user` replication password  ⬜ OUTSTANDING  (unblocks the frozen dw_unified mirror)
+
+- **Exposed / status:** the password lived only inside the Mac2 subscription's `subconninfo`, which was removed when the broken subscription was dropped 2026-06-03 — so it's now unavailable locally. Not a leak; a loss. The dw_unified mirror (Vendors/edges/settlement panels) stays frozen until this is reset and the subscription recreated.
+- **Console steps (Kamatera publisher):** `ALTER ROLE repl_user WITH PASSWORD '<NEW_PW>';` (also clear the stray `spoon_all_designs_legacy_20260601` table from publication `dw_unified_pub` — see `~/cncp-starter/yolo-queue/pending-approval/mirror-resync-FIX.md`).
+- **Secret key name:** propose `REPL_USER_DB_PASSWORD` (new route — does not exist in routes.json yet).
+- **Route back:**
+  ```
+  printf 'REPL_USER_DB_PASSWORD=<NEW_PW>' | node ~/Projects/secrets-manager/cli.js import-paste
+  ```
+- **Verify:** rerun `REPL_PW=<NEW_PW> bash ~/cncp-starter/yolo-queue/run-mirror-resync.sh`; `max(synced_at)` jumps to ~now + the CNCP mirror-alert banner clears.
diff --git a/viewer.js b/viewer.js
index 1b831ca..f718f1f 100644
--- a/viewer.js
+++ b/viewer.js
@@ -35,6 +35,7 @@ const ROOT = __dirname;
 const REGISTRY = path.join(ROOT, 'registry.json');
 const MASTER_ENV = path.join(ROOT, '.env');
 const REVEAL_LOG = path.join(ROOT, 'viewer-reveal.log');
+const ROTATION_DOC = path.join(ROOT, 'ROTATION-CHECKLIST.md');
 
 function isLoopback(req) {
   const addr = req.socket && req.socket.remoteAddress;
@@ -144,6 +145,7 @@ function renderPage() {
 </head><body>
   <h1>Secrets Viewer</h1>
   <p class="sub">Local-only · binds to <code>127.0.0.1:${PORT}</code> · reads <code>${htmlEsc(path.basename(REGISTRY))}</code> · revealed values are logged to <code>${htmlEsc(path.basename(REVEAL_LOG))}</code></p>
+  <p class="sub">🔄 <a href="/rotation" style="color:#7f1d1d;font-weight:600">Items we need to rotate →</a></p>
 
   <div class="toolbar">
     <input type="search" id="q" placeholder="Filter by key or label (e.g. eleven, godaddy, shopify)" autocomplete="off">
@@ -246,6 +248,114 @@ function renderPage() {
 </body></html>`;
 }
 
+// ── Rotation view ────────────────────────────────────────────────────────────
+// Renders ROTATION-CHECKLIST.md (the authoritative "items we need to rotate"
+// list) as status cards. Read-only; the doc holds NO secret values (key names +
+// mint URLs + route-back commands only), so this page is safe to display.
+function mdInlineEsc(s) {
+  // input is raw markdown; escape HTML first, then apply a few inline rules.
+  let h = htmlEsc(s);
+  // protect inline code spans
+  const codes = [];
+  h = h.replace(/`([^`]+)`/g, (_, c) => { codes.push(c); return '' + (codes.length - 1) + ''; });
+  h = h.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
+  h = h.replace(/\[([^\]]+)\]\((https?:[^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
+  // bare URLs (not already inside an anchor)
+  h = h.replace(/(^|[\s(])(https?:\/\/[^\s)<]+)/g, '$1<a href="$2" target="_blank" rel="noopener">$2</a>');
+  h = h.replace(/(\d+)/g, (_, i) => '<code>' + codes[+i] + '</code>');
+  return h;
+}
+function mdBlock(lines) {
+  const out = [];
+  let i = 0, inList = false;
+  const closeList = () => { if (inList) { out.push('</ul>'); inList = false; } };
+  while (i < lines.length) {
+    const ln = lines[i];
+    if (/^```/.test(ln)) {                       // fenced code block
+      closeList();
+      const buf = []; i++;
+      while (i < lines.length && !/^```/.test(lines[i])) { buf.push(htmlEsc(lines[i])); i++; }
+      i++; // skip closing fence
+      out.push('<pre><code>' + buf.join('\n') + '</code></pre>');
+      continue;
+    }
+    if (/^---\s*$/.test(ln)) { closeList(); i++; continue; }
+    const li = ln.match(/^\s*-\s+(.*)$/);
+    if (li) { if (!inList) { out.push('<ul>'); inList = true; } out.push('<li>' + mdInlineEsc(li[1]) + '</li>'); i++; continue; }
+    if (!ln.trim()) { closeList(); i++; continue; }
+    closeList();
+    out.push('<p>' + mdInlineEsc(ln) + '</p>');
+    i++;
+  }
+  closeList();
+  return out.join('\n');
+}
+function renderRotation() {
+  let raw;
+  try { raw = fs.readFileSync(ROTATION_DOC, 'utf8'); }
+  catch { return '<!doctype html><meta charset=utf-8><body style="font:14px system-ui;padding:24px">ROTATION-CHECKLIST.md not found.</body>'; }
+  const lines = raw.split(/\r?\n/);
+  const secs = []; let cur = { head: null, body: [] };
+  for (const ln of lines) {
+    const m = ln.match(/^##\s+(.*)$/);
+    if (m) { secs.push(cur); cur = { head: m[1].trim(), body: [] }; }
+    else cur.body.push(ln);
+  }
+  secs.push(cur);
+  const classify = (head) => {
+    if (/✅|☑|✓\s*done|\bDONE\b|\bROTATED\b/i.test(head)) return 'done';
+    if (/⬜|\bOUTSTANDING\b|\bTODO\b|\bPENDING\b/i.test(head)) return 'out';
+    return 'neutral';
+  };
+  let nOut = 0, nDone = 0;
+  const cards = [];
+  let preamble = '';
+  for (const s of secs) {
+    if (s.head === null) { preamble = mdBlock(s.body); continue; }
+    const cls = classify(s.head);
+    if (cls === 'out') nOut++; if (cls === 'done') nDone++;
+    const title = s.head.replace(/⬜|✅|☑/g, '').trim();
+    const pill = cls === 'done' ? '<span class="pill done">✅ done</span>'
+               : cls === 'out' ? '<span class="pill out">⬜ outstanding</span>'
+               : '<span class="pill neutral">note</span>';
+    cards.push('<section class="card ' + cls + '"><h2>' + pill + ' ' + mdInlineEsc(title) + '</h2>' + mdBlock(s.body) + '</section>');
+  }
+  return `<!doctype html><html lang="en"><head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Rotation — items we need to rotate</title>
+<style>
+  :root{--ink:#0f0e0c;--soft:#555;--faint:#999;--line:#ddd;--bg:#faf8f3;--card:#fff;
+        --out:#7f1d1d;--outBg:#fee2e2;--done:#14532d;--doneBg:#d1fae5;--neu:#374151;--neuBg:#e5e7eb;}
+  *{box-sizing:border-box} body{font:14px/1.55 -apple-system,system-ui,sans-serif;color:var(--ink);background:var(--bg);margin:0;padding:24px 28px;max-width:920px}
+  h1{font:300 28px/1.1 Georgia,serif;margin:0 0 6px}
+  .sub{color:var(--soft);margin:0 0 8px;font-size:13px}
+  .sub code{background:#e7e3d8;padding:2px 6px;border-radius:3px;font-size:12px}
+  .nav{margin:0 0 16px;font-size:13px} .nav a{color:#1e40af;text-decoration:none} .nav a:hover{text-decoration:underline}
+  .summary{display:flex;gap:10px;margin:0 0 18px}
+  .chip{padding:6px 12px;border-radius:8px;font-size:13px;font-weight:600}
+  .chip.out{background:var(--outBg);color:var(--out)} .chip.done{background:var(--doneBg);color:var(--done)}
+  .preamble{background:#fffbeb;border:1px solid #fcd34d;border-radius:8px;padding:10px 14px;font-size:12.5px;color:#5b4a16;margin:0 0 18px}
+  .preamble p{margin:6px 0} .preamble code{background:#fdf2c8;padding:1px 5px;border-radius:3px}
+  .card{background:var(--card);border:1px solid var(--line);border-left-width:4px;border-radius:8px;padding:14px 18px;margin:0 0 14px}
+  .card.out{border-left-color:var(--out)} .card.done{border-left-color:var(--done);opacity:.72} .card.neutral{border-left-color:#cbd5e1}
+  .card h2{font:600 16px/1.3 system-ui;margin:0 0 8px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
+  .pill{display:inline-block;padding:2px 9px;border-radius:999px;font-size:11px;font-weight:700}
+  .pill.out{background:var(--outBg);color:var(--out)} .pill.done{background:var(--doneBg);color:var(--done)} .pill.neutral{background:var(--neuBg);color:var(--neu)}
+  .card p{margin:6px 0} .card ul{margin:6px 0 6px 2px;padding-left:18px} .card li{margin:3px 0}
+  code{font:12px ui-monospace,Menlo,monospace} :not(pre)>code{background:#f1ede2;padding:1px 5px;border-radius:3px;word-break:break-word}
+  pre{background:#0f1117;color:#e6edf3;padding:10px 12px;border-radius:6px;overflow:auto;margin:8px 0} pre code{background:none;color:inherit}
+  a{color:#1e40af} footer{color:var(--faint);font-size:11px;margin-top:18px}
+</style></head><body>
+  <h1>Items we need to rotate</h1>
+  <p class="sub">Read-only render of <code>ROTATION-CHECKLIST.md</code> · no secret values on this page (key names + mint URLs + route-back commands only).</p>
+  <p class="nav">← <a href="/">Secrets viewer</a></p>
+  <div class="summary"><span class="chip out">${nOut} outstanding</span><span class="chip done">${nDone} done</span></div>
+  ${preamble ? '<div class="preamble">' + preamble + '</div>' : ''}
+  ${cards.join('\n')}
+  <footer>Loopback-only · binds 127.0.0.1:${PORT} · console-only actions (Claude can't mint keys or run prod credential changes) — route each new value back via <code>cli.js import-paste</code>.</footer>
+</body></html>`;
+}
+
 function send(res, status, ct, body) {
   res.writeHead(status, { 'Content-Type': ct, 'Cache-Control': 'no-store', 'X-Robots-Tag': 'noindex' });
   res.end(body);
@@ -260,6 +370,10 @@ const server = http.createServer((req, res) => {
     return send(res, 200, 'text/html; charset=utf-8', renderPage());
   }
 
+  if (req.method === 'GET' && (p === '/rotation' || p === '/rotate')) {
+    return send(res, 200, 'text/html; charset=utf-8', renderRotation());
+  }
+
   if (req.method === 'GET' && p === '/api/registry') {
     return send(res, 200, 'application/json', JSON.stringify(readRegistry(), null, 2));
   }

← f461d5c update tracker: add 'to configure' section for 16 not-set ke  ·  back to Secrets Manager  ·  add rotate-dw-admin-full.sh — complete the forward dw_admin 8b0e8f0 →