← back to Secrets Manager
snapshot: 3 file(s) changed, +1 new, ~2 modified
4adfd0aab6b4f946a8221790d32a5abc818c30ec · 2026-05-13 08:58:00 -0700 · Steve
Files touched
M registry.jsonM routes.jsonA viewer.js
Diff
commit 4adfd0aab6b4f946a8221790d32a5abc818c30ec
Author: Steve <steve@designerwallcoverings.com>
Date: Wed May 13 08:58:00 2026 -0700
snapshot: 3 file(s) changed, +1 new, ~2 modified
---
registry.json | 33 +++++++
routes.json | 60 ++++++++-----
viewer.js | 284 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 354 insertions(+), 23 deletions(-)
diff --git a/registry.json b/registry.json
index 644ee55..6b54f79 100644
--- a/registry.json
+++ b/registry.json
@@ -692,6 +692,39 @@
"/Users/stevestudio2/Projects/secrets-manager/.env",
"/Users/stevestudio2/Desktop/site-factory.env"
]
+ },
+ "BUTLR_OWNER_TOKEN": {
+ "digest": "da8c:693bb5f9",
+ "label": "BUTLR_OWNER_TOKEN",
+ "last_updated": "2026-05-12T23:03:48.872Z",
+ "validated": false,
+ "verify_status": null,
+ "written_to": [
+ "/Users/stevestudio2/Projects/secrets-manager/.env",
+ "/Users/stevestudio2/Desktop/site-factory.env"
+ ]
+ },
+ "VAPI_API_KEY": {
+ "digest": "4da9:a1d8d0df",
+ "label": "VAPI_API_KEY",
+ "last_updated": "2026-05-13T15:26:33.358Z",
+ "validated": false,
+ "verify_status": null,
+ "written_to": [
+ "/Users/stevestudio2/Projects/secrets-manager/.env",
+ "/Users/stevestudio2/Desktop/site-factory.env"
+ ]
+ },
+ "VAPI_PUBLIC_KEY": {
+ "digest": "4df0:c8ace254",
+ "label": "VAPI_PUBLIC_KEY",
+ "last_updated": "2026-05-13T15:26:33.360Z",
+ "validated": false,
+ "verify_status": null,
+ "written_to": [
+ "/Users/stevestudio2/Projects/secrets-manager/.env",
+ "/Users/stevestudio2/Desktop/site-factory.env"
+ ]
}
}
}
\ No newline at end of file
diff --git a/routes.json b/routes.json
index e913dcc..0e35b28 100644
--- a/routes.json
+++ b/routes.json
@@ -221,7 +221,7 @@
]
},
"STRIPE_VCL_WEBHOOK_SECRET": {
- "label": "Stripe webhook signing secret — VCL endpoint we_1TUF1Y63uNmiRsMbNr1kLu52",
+ "label": "Stripe webhook signing secret \u2014 VCL endpoint we_1TUF1Y63uNmiRsMbNr1kLu52",
"destinations": [
{
"type": "project",
@@ -236,7 +236,7 @@
]
},
"STRIPE_PRICE_VCL_STARTER_MONTH": {
- "label": "Stripe Price ID — VCL Starter $49/mo",
+ "label": "Stripe Price ID \u2014 VCL Starter $49/mo",
"destinations": [
{
"type": "project",
@@ -251,7 +251,7 @@
]
},
"STRIPE_PRICE_VCL_STANDARD_MONTH": {
- "label": "Stripe Price ID — VCL Standard $99/mo",
+ "label": "Stripe Price ID \u2014 VCL Standard $99/mo",
"destinations": [
{
"type": "project",
@@ -266,7 +266,7 @@
]
},
"STRIPE_PRICE_VCL_PREMIER_MONTH": {
- "label": "Stripe Price ID — VCL Premier $199/mo",
+ "label": "Stripe Price ID \u2014 VCL Premier $199/mo",
"destinations": [
{
"type": "project",
@@ -281,7 +281,7 @@
]
},
"HEYGEN_API_KEY": {
- "label": "HeyGen API key — talking-head avatar video render (used by session-debrief skill)",
+ "label": "HeyGen API key \u2014 talking-head avatar video render (used by session-debrief skill)",
"verify": {
"method": "GET",
"url": "https://api.heygen.com/v1/voice.list",
@@ -300,7 +300,7 @@
]
},
"INFO_VENTURACLAW_COM_PASSWORD": {
- "label": "Purelymail mailbox password — info@venturaclaw.com (used by Ventura Claw Leads + the existing ventura-claw app)",
+ "label": "Purelymail mailbox password \u2014 info@venturaclaw.com (used by Ventura Claw Leads + the existing ventura-claw app)",
"destinations": [
{
"type": "project",
@@ -325,7 +325,7 @@
]
},
"INFO_NATIONALPAPERHANGERS_COM_PASSWORD": {
- "label": "Purelymail mailbox password — info@nationalpaperhangers.com",
+ "label": "Purelymail mailbox password \u2014 info@nationalpaperhangers.com",
"destinations": [
{
"type": "project",
@@ -340,7 +340,7 @@
]
},
"STRIPE_NPH_WEBHOOK_SECRET": {
- "label": "Stripe Webhook Signing Secret — NPH endpoint we_1TUDHV63uNmiRsMbRJea7EqU",
+ "label": "Stripe Webhook Signing Secret \u2014 NPH endpoint we_1TUDHV63uNmiRsMbRJea7EqU",
"destinations": [
{
"type": "project",
@@ -355,7 +355,7 @@
]
},
"STRIPE_PRICE_PRO_MONTH": {
- "label": "Stripe Price ID — NPH Pro $39/mo",
+ "label": "Stripe Price ID \u2014 NPH Pro $39/mo",
"destinations": [
{
"type": "project",
@@ -370,7 +370,7 @@
]
},
"STRIPE_PRICE_PRO_YEAR": {
- "label": "Stripe Price ID — NPH Pro $399/yr",
+ "label": "Stripe Price ID \u2014 NPH Pro $399/yr",
"destinations": [
{
"type": "project",
@@ -385,7 +385,7 @@
]
},
"STRIPE_PRICE_SIGNATURE_MONTH": {
- "label": "Stripe Price ID — NPH Signature $149/mo",
+ "label": "Stripe Price ID \u2014 NPH Signature $149/mo",
"destinations": [
{
"type": "project",
@@ -400,7 +400,7 @@
]
},
"STRIPE_PRICE_SIGNATURE_YEAR": {
- "label": "Stripe Price ID — NPH Signature $1500/yr",
+ "label": "Stripe Price ID \u2014 NPH Signature $1500/yr",
"destinations": [
{
"type": "project",
@@ -415,7 +415,7 @@
]
},
"STRIPE_PRICE_ENTERPRISE_MONTH": {
- "label": "Stripe Price ID — NPH Enterprise $399/mo",
+ "label": "Stripe Price ID \u2014 NPH Enterprise $399/mo",
"destinations": [
{
"type": "project",
@@ -773,8 +773,8 @@
]
},
"LINKEDIN_CLIENT_ID": {
- "label": "LinkedIn OAuth Client ID — \"For Claude - 5-6-26\" app, bound to Designer Wallcoverings Page",
- "_note": "No verify endpoint — LinkedIn requires the ID+SECRET pair to validate, and the CLI verifies one key at a time. Manual probe via POST /oauth/v2/accessToken with a fake code expects 'invalid_request: authorization code not found' = creds OK.",
+ "label": "LinkedIn OAuth Client ID \u2014 \"For Claude - 5-6-26\" app, bound to Designer Wallcoverings Page",
+ "_note": "No verify endpoint \u2014 LinkedIn requires the ID+SECRET pair to validate, and the CLI verifies one key at a time. Manual probe via POST /oauth/v2/accessToken with a fake code expects 'invalid_request: authorization code not found' = creds OK.",
"destinations": [
{
"type": "project",
@@ -788,7 +788,7 @@
]
},
"LINKEDIN_CLIENT_SECRET": {
- "label": "LinkedIn OAuth Client Secret — \"For Claude - 5-6-26\" app, bound to Designer Wallcoverings Page",
+ "label": "LinkedIn OAuth Client Secret \u2014 \"For Claude - 5-6-26\" app, bound to Designer Wallcoverings Page",
"destinations": [
{
"type": "project",
@@ -802,7 +802,7 @@
]
},
"NPH_GOOGLE_OAUTH_CLIENT_ID": {
- "label": "Google OAuth Client ID — buyer sign-in on National Paper Hangers",
+ "label": "Google OAuth Client ID \u2014 buyer sign-in on National Paper Hangers",
"destinations": [
{
"type": "project",
@@ -816,7 +816,7 @@
]
},
"NPH_GOOGLE_OAUTH_CLIENT_SECRET": {
- "label": "Google OAuth Client Secret — buyer sign-in on National Paper Hangers",
+ "label": "Google OAuth Client Secret \u2014 buyer sign-in on National Paper Hangers",
"destinations": [
{
"type": "project",
@@ -1049,7 +1049,7 @@
]
},
"GOOGLE_DRIVE_CLIENT_ID": {
- "description": "Google Drive OAuth Client ID — TSD photo-import-cli desktop app",
+ "description": "Google Drive OAuth Client ID \u2014 TSD photo-import-cli desktop app",
"destinations": [
{
"type": "env-file",
@@ -1058,7 +1058,7 @@
]
},
"GOOGLE_DRIVE_CLIENT_SECRET": {
- "description": "Google Drive OAuth Client Secret — TSD photo-import-cli",
+ "description": "Google Drive OAuth Client Secret \u2014 TSD photo-import-cli",
"destinations": [
{
"type": "env-file",
@@ -1067,7 +1067,7 @@
]
},
"GOOGLE_DRIVE_REFRESH_TOKEN": {
- "description": "Google Drive OAuth refresh token — read-only scope, owned by steveabramsdesigns@gmail.com",
+ "description": "Google Drive OAuth refresh token \u2014 read-only scope, owned by steveabramsdesigns@gmail.com",
"destinations": [
{
"type": "env-file",
@@ -1106,7 +1106,7 @@
]
},
"TMDB_API_KEY": {
- "label": "TMDB v3 API key — asseeninmovies Phase B drain (6.46M-stub queue)",
+ "label": "TMDB v3 API key \u2014 asseeninmovies Phase B drain (6.46M-stub queue)",
"verify": {
"method": "GET",
"url": "https://api.themoviedb.org/3/authentication?api_key=__VALUE__",
@@ -1118,7 +1118,7 @@
]
},
"TMDB_READ_ACCESS_TOKEN": {
- "label": "TMDB v4 read-access token (bearer) — alt to v3 api_key",
+ "label": "TMDB v4 read-access token (bearer) \u2014 alt to v3 api_key",
"verify": {
"method": "GET",
"url": "https://api.themoviedb.org/3/authentication",
@@ -1128,5 +1128,19 @@
"destinations": [
"/Users/stevestudio2/Projects/asseeninmovies/.env"
]
+ },
+ "VAPI_API_KEY": {
+ "label": "VAPI_API_KEY",
+ "destinations": [
+ "/Users/stevestudio2/Projects/holdforme/.env",
+ "remote:kamatera:/root/public-projects/butlr/.env"
+ ]
+ },
+ "VAPI_PUBLIC_KEY": {
+ "label": "VAPI_PUBLIC_KEY",
+ "destinations": [
+ "/Users/stevestudio2/Projects/holdforme/.env",
+ "remote:kamatera:/root/public-projects/butlr/.env"
+ ]
}
}
\ No newline at end of file
diff --git a/viewer.js b/viewer.js
new file mode 100644
index 0000000..1b831ca
--- /dev/null
+++ b/viewer.js
@@ -0,0 +1,284 @@
+#!/usr/bin/env node
+/**
+ * Local-only web viewer for the secrets registry.
+ *
+ * Lists every secret with its digest, label, validation status, and the
+ * destinations cli.js wrote it to. A "Reveal" button per row makes an
+ * authenticated POST to read the raw value out of the master .env — that
+ * action requires a same-origin POST + the X-Confirm-Reveal header, so a
+ * stray GET / scroll never leaks anything.
+ *
+ * Hard security guards:
+ * - listen() binds to 127.0.0.1 only (never 0.0.0.0)
+ * - every handler checks req.socket.remoteAddress ∈ {127.0.0.1, ::1}
+ * - /api/reveal returns the value as JSON (so the browser tab is the only
+ * place it appears; nothing logged server-side)
+ * - reveal logs an entry to viewer-reveal.log with timestamp + key name
+ * (NOT the value) so Steve can audit which keys he viewed and when
+ *
+ * Zero deps — pure Node http module.
+ *
+ * Usage:
+ * node viewer.js
+ * open http://127.0.0.1:9698/
+ */
+
+'use strict';
+
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+const url = require('url');
+
+const PORT = parseInt(process.env.SECRETS_VIEWER_PORT, 10) || 9698;
+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');
+
+function isLoopback(req) {
+ const addr = req.socket && req.socket.remoteAddress;
+ return addr === '127.0.0.1' || addr === '::1' || addr === '::ffff:127.0.0.1';
+}
+
+function readRegistry() {
+ try { return JSON.parse(fs.readFileSync(REGISTRY, 'utf8')); } catch { return { secrets: {} }; }
+}
+
+function parseEnv(file) {
+ const out = {};
+ let raw;
+ try { raw = fs.readFileSync(file, 'utf8'); } catch { return out; }
+ for (const line of raw.split(/\r?\n/)) {
+ if (!line || line.startsWith('#')) continue;
+ const eq = line.indexOf('=');
+ if (eq < 1) continue;
+ const k = line.slice(0, eq).trim();
+ let v = line.slice(eq + 1);
+ // Strip surrounding single or double quotes if perfectly balanced.
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
+ v = v.slice(1, -1);
+ }
+ out[k] = v;
+ }
+ return out;
+}
+
+function htmlEsc(s) {
+ return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({
+ '&': '&', '<': '<', '>': '>', '"': '"', "'": '''
+ }[c]));
+}
+
+function renderPage() {
+ const reg = readRegistry();
+ const rows = Object.entries(reg.secrets || {})
+ .sort(([a], [b]) => a.localeCompare(b))
+ .map(([key, meta]) => {
+ const destCount = (meta.written_to || []).length;
+ const dests = (meta.written_to || []).map(htmlEsc).join('<br>');
+ const validatedPill = meta.validated
+ ? `<span class="pill pill-ok">verified · ${htmlEsc(meta.verify_status || '')}</span>`
+ : (meta.verify_status === null || meta.verify_status === undefined
+ ? `<span class="pill pill-unk">unverified</span>`
+ : `<span class="pill pill-bad">failed · ${htmlEsc(meta.verify_status)}</span>`);
+ return `
+ <tr data-key="${htmlEsc(key)}">
+ <td class="k">${htmlEsc(key)}</td>
+ <td class="lbl">${htmlEsc(meta.label || '')}</td>
+ <td class="dig"><code>${htmlEsc(meta.digest || '—')}</code></td>
+ <td class="ver">${validatedPill}</td>
+ <td class="dest" title="${htmlEsc((meta.written_to || []).join('\n'))}">${destCount} file${destCount === 1 ? '' : 's'}</td>
+ <td class="upd">${htmlEsc((meta.last_updated || '').slice(0, 10))}</td>
+ <td class="act">
+ <button type="button" class="btn-reveal" data-key="${htmlEsc(key)}">Reveal</button>
+ <button type="button" class="btn-copy" data-key="${htmlEsc(key)}" style="display:none">Copy</button>
+ <button type="button" class="btn-hide" data-key="${htmlEsc(key)}" style="display:none">Hide</button>
+ <div class="value-cell" style="display:none;margin-top:6px"><code class="val"></code></div>
+ <div class="dest-list" style="display:none;margin-top:6px">${dests}</div>
+ <button type="button" class="btn-dests" data-key="${htmlEsc(key)}">where?</button>
+ </td>
+ </tr>`;
+ }).join('');
+
+ return `<!doctype html><html lang="en"><head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Secrets Viewer — localhost</title>
+<style>
+ :root { --ink:#0f0e0c; --soft:#555; --faint:#999; --line:#ddd; --bg:#faf8f3; --card:#fff;
+ --ok:#14532d; --okBg:#d1fae5; --bad:#7f1d1d; --badBg:#fee2e2; --unk:#374151; --unkBg:#e5e7eb; }
+ * { box-sizing: border-box; }
+ body { font:14px/1.5 -apple-system, system-ui, sans-serif; color:var(--ink); background:var(--bg); margin:0; padding:24px 28px; }
+ h1 { font:300 28px/1.1 Georgia, serif; margin:0 0 6px; }
+ .sub { color:var(--soft); margin:0 0 22px; font-size:13px }
+ .sub code { background:#e7e3d8; padding:2px 6px; border-radius:3px; font-size:12px }
+ .toolbar { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-bottom:14px }
+ .toolbar input[type=search] { flex:1; min-width:240px; padding:8px 12px; border:1px solid var(--line); border-radius:6px; font:14px var(--sans); background:var(--card) }
+ .toolbar select { padding:8px 10px; border:1px solid var(--line); border-radius:6px; background:var(--card); font:13px var(--sans) }
+ table { width:100%; border-collapse:collapse; background:var(--card); border:1px solid var(--line); border-radius:8px; overflow:hidden }
+ th { text-align:left; font:11px/1 var(--sans); text-transform:uppercase; letter-spacing:.08em; color:var(--faint); padding:11px 12px; background:#f3eee2; border-bottom:1px solid var(--line) }
+ td { padding:10px 12px; border-top:1px solid #ece8dd; vertical-align:top; font-size:13px }
+ td.k { font:13px ui-monospace, Menlo, monospace; font-weight:600 }
+ td.lbl { color:var(--soft) }
+ td.dig code { font:12px ui-monospace, Menlo, monospace; color:var(--soft) }
+ td.upd { color:var(--faint); font-size:12px; white-space:nowrap }
+ td.act { white-space:nowrap }
+ td.act button { font:12px var(--sans); padding:5px 11px; border:1px solid var(--line); background:#f3eee2; border-radius:4px; cursor:pointer; margin-right:4px }
+ td.act button:hover { background:#e7e3d8 }
+ td.act .btn-reveal { background:var(--ink); color:#fff; border-color:var(--ink) }
+ td.act .btn-copy { background:#1e40af; color:#fff; border-color:#1e40af }
+ td.act .btn-hide { background:#7f1d1d; color:#fff; border-color:#7f1d1d }
+ td.act .btn-dests { background:transparent; color:var(--soft); border-color:#e7e3d8; font-size:11px }
+ .pill { display:inline-block; padding:2px 8px; border-radius:999px; font-size:11px; font-weight:600 }
+ .pill-ok { background:var(--okBg); color:var(--ok) }
+ .pill-bad { background:var(--badBg); color:var(--bad) }
+ .pill-unk { background:var(--unkBg); color:var(--unk) }
+ .value-cell { background:#fffbeb; border:1px solid #fcd34d; padding:7px 9px; border-radius:4px; max-width:480px; word-break:break-all }
+ .value-cell code { font:12px ui-monospace, Menlo, monospace; color:var(--ink) }
+ .dest-list { background:#f3eee2; border:1px solid var(--line); padding:7px 10px; border-radius:4px; font:11px ui-monospace, Menlo, monospace; color:var(--soft); max-width:520px }
+ .stats { color:var(--soft); font-size:12px; margin-bottom:8px }
+ .hide { display:none !important }
+ footer { color:var(--faint); font-size:11px; margin-top:18px }
+</style>
+</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>
+
+ <div class="toolbar">
+ <input type="search" id="q" placeholder="Filter by key or label (e.g. eleven, godaddy, shopify)" autocomplete="off">
+ <select id="filter">
+ <option value="all">All secrets</option>
+ <option value="ok">Verified only</option>
+ <option value="bad">Failed verification</option>
+ <option value="unk">Unverified</option>
+ </select>
+ <span class="stats" id="stats"></span>
+ </div>
+
+ <table>
+ <thead>
+ <tr>
+ <th>Key</th><th>Label</th><th>Digest</th><th>Status</th><th>Destinations</th><th>Updated</th><th>Action</th>
+ </tr>
+ </thead>
+ <tbody id="rows">${rows}</tbody>
+ </table>
+
+ <footer>Loopback-only. Every reveal click is logged to <code>${htmlEsc(REVEAL_LOG)}</code> with timestamp + key name (never the value).</footer>
+
+<script>
+(function(){
+ const q = document.getElementById('q');
+ const filter = document.getElementById('filter');
+ const stats = document.getElementById('stats');
+ const rows = Array.from(document.querySelectorAll('#rows tr'));
+
+ function refilter(){
+ const needle = (q.value || '').toLowerCase();
+ const mode = filter.value;
+ let visible = 0;
+ rows.forEach(r => {
+ const k = (r.querySelector('.k')?.textContent || '').toLowerCase();
+ const lbl = (r.querySelector('.lbl')?.textContent || '').toLowerCase();
+ const matches = !needle || k.includes(needle) || lbl.includes(needle);
+ let modeOk = true;
+ if (mode === 'ok') modeOk = !!r.querySelector('.pill-ok');
+ if (mode === 'bad') modeOk = !!r.querySelector('.pill-bad');
+ if (mode === 'unk') modeOk = !!r.querySelector('.pill-unk');
+ const show = matches && modeOk;
+ r.classList.toggle('hide', !show);
+ if (show) visible++;
+ });
+ stats.textContent = visible + ' of ' + rows.length + ' secrets';
+ }
+ q.addEventListener('input', refilter);
+ filter.addEventListener('change', refilter);
+ refilter();
+
+ document.body.addEventListener('click', async function(e){
+ const t = e.target;
+ if (!(t instanceof HTMLButtonElement)) return;
+ const row = t.closest('tr'); if (!row) return;
+ const key = t.dataset.key;
+ const reveal = row.querySelector('.btn-reveal');
+ const copy = row.querySelector('.btn-copy');
+ const hide = row.querySelector('.btn-hide');
+ const valCell = row.querySelector('.value-cell');
+ const valEl = row.querySelector('.val');
+ const destList = row.querySelector('.dest-list');
+ const destsBtn = row.querySelector('.btn-dests');
+
+ if (t.classList.contains('btn-reveal')) {
+ try {
+ const r = await fetch('/api/reveal/' + encodeURIComponent(key), {
+ method: 'POST',
+ headers: { 'X-Confirm-Reveal': 'yes', 'Content-Type': 'application/json' },
+ body: JSON.stringify({})
+ });
+ if (!r.ok) { alert('Reveal failed: HTTP ' + r.status); return; }
+ const j = await r.json();
+ valEl.textContent = j.value || '(not found in master .env)';
+ valCell.style.display = 'block';
+ reveal.style.display = 'none';
+ copy.style.display = 'inline-block';
+ hide.style.display = 'inline-block';
+ } catch (err) { alert('Reveal error: ' + err.message); }
+ }
+ if (t.classList.contains('btn-copy')) {
+ try { await navigator.clipboard.writeText(valEl.textContent); t.textContent = 'Copied ✓'; setTimeout(() => { t.textContent = 'Copy'; }, 1400); } catch (e) { alert('Clipboard error: ' + e.message); }
+ }
+ if (t.classList.contains('btn-hide')) {
+ valEl.textContent = '';
+ valCell.style.display = 'none';
+ reveal.style.display = 'inline-block';
+ copy.style.display = 'none';
+ hide.style.display = 'none';
+ }
+ if (t.classList.contains('btn-dests')) {
+ const open = destList.style.display !== 'none';
+ destList.style.display = open ? 'none' : 'block';
+ destsBtn.textContent = open ? 'where?' : 'hide';
+ }
+ });
+})();
+</script>
+</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);
+}
+
+const server = http.createServer((req, res) => {
+ if (!isLoopback(req)) { send(res, 403, 'text/plain', 'loopback only'); return; }
+ const parsed = url.parse(req.url, true);
+ const p = parsed.pathname || '/';
+
+ if (req.method === 'GET' && (p === '/' || p === '/index.html')) {
+ return send(res, 200, 'text/html; charset=utf-8', renderPage());
+ }
+
+ if (req.method === 'GET' && p === '/api/registry') {
+ return send(res, 200, 'application/json', JSON.stringify(readRegistry(), null, 2));
+ }
+
+ if (req.method === 'POST' && p.startsWith('/api/reveal/')) {
+ if (req.headers['x-confirm-reveal'] !== 'yes') { send(res, 400, 'application/json', '{"error":"missing X-Confirm-Reveal header"}'); return; }
+ const key = decodeURIComponent(p.slice('/api/reveal/'.length));
+ if (!/^[A-Z0-9_]{2,64}$/.test(key)) { send(res, 400, 'application/json', '{"error":"invalid key name"}'); return; }
+ const env = parseEnv(MASTER_ENV);
+ const value = env[key];
+ try {
+ fs.appendFileSync(REVEAL_LOG, new Date().toISOString() + ' reveal ' + key + ' (' + (value ? 'found' : 'missing') + ')\n');
+ } catch {}
+ return send(res, 200, 'application/json', JSON.stringify({ key, value: value || null }));
+ }
+
+ send(res, 404, 'text/plain', 'not found');
+});
+
+server.listen(PORT, '127.0.0.1', () => {
+ console.log('Secrets viewer on http://127.0.0.1:' + PORT + '/');
+});
← 6033408 feat(secrets): TMDB_API_KEY + TMDB_READ_ACCESS_TOKEN routes
·
back to Secrets Manager
·
snapshot: backup uncommitted work (1 files) d6a1fe6 →