← back to Agentabrams Viewer
viewer: credential-injecting reverse proxy so unified admin pw auto-applies to in-panel sites
02f58ea7d218462fd7c9c2d33589522907952aa0 · 2026-07-22 19:03:30 -0700 · Steve Abrams
- /site/<domain>/ proxies upstream with admin/DW2024! injected (browsers won't basic-auth cross-origin iframes)
- strips X-Frame-Options/CSP so authed sites render in the panel; TLS verify stays ON
- status probe now does a 2nd authed HEAD to flag a rejected pw (authFail) vs a healthy gate
- allowlisted to domains.json; localhost-only, still behind admin/DW2024!
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Files touched
M public/index.htmlM server.js
Diff
commit 02f58ea7d218462fd7c9c2d33589522907952aa0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jul 22 19:03:30 2026 -0700
viewer: credential-injecting reverse proxy so unified admin pw auto-applies to in-panel sites
- /site/<domain>/ proxies upstream with admin/DW2024! injected (browsers won't basic-auth cross-origin iframes)
- strips X-Frame-Options/CSP so authed sites render in the panel; TLS verify stays ON
- status probe now does a 2nd authed HEAD to flag a rejected pw (authFail) vs a healthy gate
- allowlisted to domains.json; localhost-only, still behind admin/DW2024!
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
public/index.html | 26 ++++++----------
server.js | 91 +++++++++++++++++++++++++++++++++++++++++++++++--------
2 files changed, 87 insertions(+), 30 deletions(-)
diff --git a/public/index.html b/public/index.html
index 49f58bb..c12afc6 100644
--- a/public/index.html
+++ b/public/index.html
@@ -47,7 +47,7 @@
<input id="q" type="search" placeholder="Filter domains…" autocomplete="off">
</header>
<div id="list"></div>
- <div id="foot">click a domain → loads in the right panel · dot: <span style="color:#3fbf6f">up</span> / <span style="color:#d9a53a">auth-gated</span> / <span style="color:#d95555">down</span></div>
+ <div id="foot">click a domain → loads in the right panel (unified admin pw auto-applied) · dot: <span style="color:#3fbf6f">up</span> / <span style="color:#d9a53a">auth-gated</span> / <span style="color:#d95555">down or pw rejected</span></div>
</div>
<div id="main">
<div id="bar">
@@ -57,7 +57,7 @@
<button id="newtab" title="Open in a new browser tab">↗ New Tab</button>
</div>
<div id="empty"><div class="big">🌐</div><div>Select a domain on the left to view the live site here.</div></div>
- <iframe id="frame" style="display:none" referrerpolicy="no-referrer"></iframe>
+ <iframe id="frame" style="display:none"></iframe>
</div>
<script>
@@ -75,7 +75,7 @@ function render() {
const s = status[d];
const row = document.createElement('div');
row.className = 'item' + (d === current ? ' active' : '');
- const dotCls = !s ? '' : s.status === 401 ? 'auth' : s.up ? 'up' : 'down';
+ const dotCls = !s ? '' : s.authFail ? 'down' : s.gated ? 'auth' : s.up ? 'up' : 'down';
const label = f ? d.replace(f, `<b>${f}</b>`) : d;
row.innerHTML = `<span class="dot ${dotCls}"></span><span class="name">${label}</span>` +
(s && s.up ? `<span class="ms">${s.ms}ms</span>` : s ? `<span class="ms">—</span>` : '');
@@ -88,21 +88,15 @@ function select(d) {
current = d;
localStorage.setItem('aav-current', d);
cur.innerHTML = `<a href="https://${d}" target="_blank" rel="noopener noreferrer">${d}</a>`;
- const s = status[d];
- if (s && s.framable === false) {
- frame.style.display = 'none'; frame.src = 'about:blank';
- empty.style.display = 'flex';
- empty.innerHTML = `<div class="big">🚫</div><div><b>${d}</b> sends X-Frame-Options and refuses to render inside a panel.</div>` +
- `<button onclick="window.open('https://${d}','_blank','noopener')">↗ Open in New Tab</button>`;
- } else {
- empty.style.display = 'none';
- frame.style.display = 'block';
- frame.src = 'https://' + d;
- }
+ // Load through the server-side proxy — it injects the unified admin credential
+ // and strips X-Frame-Options, so every site renders in-panel without prompting.
+ empty.style.display = 'none';
+ frame.style.display = 'block';
+ frame.src = '/site/' + d + '/';
render();
}
-document.getElementById('reload').onclick = () => { if (current) frame.src = 'https://' + current; };
+document.getElementById('reload').onclick = () => { if (current) frame.src = '/site/' + current + '/'; };
document.getElementById('newtab').onclick = () => { if (current) window.open('https://' + current, '_blank', 'noopener'); };
q.oninput = render;
@@ -113,8 +107,6 @@ q.oninput = render;
// Health dots load after the list — probes take a few seconds for 47 domains.
const st = await (await fetch('/api/status')).json();
for (const s of st) status[s.domain] = s;
- // Re-select so a frame-blocked domain picked before probes finished swaps to the fallback card.
- if (current && status[current] && status[current].framable === false) select(current);
render();
})();
</script>
diff --git a/server.js b/server.js
index 013e3cf..6178306 100644
--- a/server.js
+++ b/server.js
@@ -1,6 +1,9 @@
#!/usr/bin/env node
-// agentabrams-viewer — left-panel domain list, right-panel iframe of the live site.
+// agentabrams-viewer — left-panel domain list, right-panel view of the live site.
// Zero-dependency Node http server. Basic auth admin/DW2024! (internal-viewer standard).
+// The right panel loads /site/<domain>/… — a reverse proxy that injects the unified
+// admin credential upstream (browsers refuse to basic-auth cross-origin iframes) and
+// strips X-Frame-Options/CSP so every fleet site renders inside the panel.
const http = require('http');
const https = require('https');
const fs = require('fs');
@@ -9,6 +12,8 @@ const path = require('path');
const PORT = process.env.PORT || 9783;
const ROOT = __dirname;
const AUTH = 'Basic ' + Buffer.from(process.env.BASIC_AUTH || 'admin:DW2024!').toString('base64');
+// Credential injected into upstream fleet sites (the unified admin pw).
+const UPSTREAM_AUTH = 'Basic ' + Buffer.from(process.env.UPSTREAM_AUTH || 'admin:DW2024!').toString('base64');
const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.png': 'image/png', '.svg': 'image/svg+xml' };
@@ -17,25 +22,37 @@ function loadDomains() {
}
// Server-side health probe — the browser can't check cross-origin status itself.
-// 401 counts as UP (basic-auth gate is the healthy state for several of these sites).
-function probe(domain) {
+// Gated sites get a second HEAD with the unified credential so a dead password
+// (authFail) is distinguishable from a healthy auth gate.
+function head(domain, withAuth) {
return new Promise((resolve) => {
- const started = Date.now();
- const req = https.request({ host: domain, method: 'HEAD', path: '/', timeout: 6000, rejectUnauthorized: false }, (res) => {
+ const headers = withAuth ? { authorization: UPSTREAM_AUTH } : {};
+ const req = https.request({ host: domain, method: 'HEAD', path: '/', headers, timeout: 6000, rejectUnauthorized: true }, (res) => {
res.resume();
- resolve({ domain, status: res.statusCode, ms: Date.now() - started, up: res.statusCode < 500, framable: !frameBlocked(res.headers) });
+ resolve(res.statusCode);
});
- req.on('timeout', () => { req.destroy(); resolve({ domain, status: 0, ms: Date.now() - started, up: false, framable: true }); });
- req.on('error', () => resolve({ domain, status: 0, ms: Date.now() - started, up: false, framable: true }));
+ req.on('timeout', () => { req.destroy(); resolve(0); });
+ req.on('error', () => resolve(0));
req.end();
});
}
-function frameBlocked(headers) {
- const xfo = (headers['x-frame-options'] || '').toLowerCase();
- if (xfo.includes('deny') || xfo.includes('sameorigin')) return true;
- const csp = (headers['content-security-policy'] || '').toLowerCase();
- return /frame-ancestors\s+(?!.*agentabrams)('none'|'self')/.test(csp);
+async function probe(domain) {
+ const started = Date.now();
+ const plain = await head(domain, false);
+ let gated = plain === 401, authFail = false, effective = plain;
+ if (gated) {
+ effective = await head(domain, true);
+ authFail = effective === 401;
+ }
+ return {
+ domain,
+ status: plain,
+ ms: Date.now() - started,
+ gated,
+ authFail,
+ up: effective !== 0 && effective !== 401 && effective < 500,
+ };
}
const server = http.createServer(async (req, res) => {
@@ -64,9 +81,57 @@ const server = http.createServer(async (req, res) => {
return res.end(JSON.stringify(out));
}
+ // Reverse proxy: /site/<domain>/<path> → https://<domain>/<path> with the
+ // unified admin credential injected. Only domains in domains.json are proxied.
+ if (url.pathname.startsWith('/site/')) {
+ const rest = url.pathname.slice('/site/'.length);
+ const slash = rest.indexOf('/');
+ const domain = slash < 0 ? rest : rest.slice(0, slash);
+ if (slash < 0) { res.writeHead(302, { Location: `/site/${domain}/` }); return res.end(); }
+ if (!loadDomains().includes(domain)) { res.writeHead(404); return res.end('unknown domain'); }
+ const upstreamPath = rest.slice(slash) + url.search;
+ const headers = { ...req.headers, host: domain, authorization: UPSTREAM_AUTH };
+ delete headers.connection;
+ const preq = https.request({ host: domain, method: req.method, path: upstreamPath, headers, timeout: 20000, rejectUnauthorized: true }, (pres) => {
+ const h = { ...pres.headers };
+ // The whole point of the proxy: let everything render in the panel.
+ delete h['x-frame-options'];
+ delete h['content-security-policy'];
+ delete h['strict-transport-security'];
+ delete h.connection;
+ if (h.location) {
+ try {
+ const loc = new URL(h.location, `https://${domain}`);
+ if (loadDomains().includes(loc.host)) h.location = `/site/${loc.host}${loc.pathname}${loc.search}`;
+ } catch {}
+ }
+ if (h['set-cookie']) {
+ h['set-cookie'] = h['set-cookie'].map((c) =>
+ c.replace(/;\s*Domain=[^;]+/gi, '').replace(/;\s*Path=\//i, `; Path=/site/${domain}/`));
+ }
+ res.writeHead(pres.statusCode, h);
+ pres.pipe(res);
+ });
+ preq.on('timeout', () => preq.destroy(new Error('upstream timeout')));
+ preq.on('error', (e) => { if (!res.headersSent) res.writeHead(502); res.end('proxy error: ' + e.message); });
+ req.pipe(preq);
+ return;
+ }
+
let file = url.pathname === '/' ? '/index.html' : url.pathname;
const fp = path.join(ROOT, 'public', path.normalize(file).replace(/^(\.\.[\/\\])+/, ''));
if (!fp.startsWith(path.join(ROOT, 'public'))) { res.writeHead(403); return res.end(); }
+
+ // Root-relative escape hatch: a proxied page requesting "/app.js" lands here.
+ // If the request came from inside /site/<domain>/, bounce it back into the proxy.
+ if (!fs.existsSync(fp)) {
+ const m = (req.headers.referer || '').match(/\/site\/([^/]+)\//);
+ if (m && loadDomains().includes(m[1])) {
+ res.writeHead(302, { Location: `/site/${m[1]}${url.pathname}${url.search}` });
+ return res.end();
+ }
+ }
+
fs.readFile(fp, (err, data) => {
if (err) { res.writeHead(404); return res.end('not found'); }
res.writeHead(200, { 'Content-Type': MIME[path.extname(fp)] || 'application/octet-stream' });
← 21d5179 agentabrams domain viewer: left-rail list of all 47 *.agenta
·
back to Agentabrams Viewer
·
fix SSO cookie rewrite in site proxy: fleet-wide (Domain=) c 8ef170b →