[object Object]

← back to Dw Domain Viewer

initial scaffold — DW domain viewer (aa-viewer pattern, 180 CF-sourced subdomains)

c4fe1700acc3ef5df33d914312e8e85888a4dd5c · 2026-07-23 08:06:10 -0700 · Steve Abrams

Files touched

Diff

commit c4fe1700acc3ef5df33d914312e8e85888a4dd5c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 23 08:06:10 2026 -0700

    initial scaffold — DW domain viewer (aa-viewer pattern, 180 CF-sourced subdomains)
---
 .gitignore                 |   8 ++
 domains.json               | 182 +++++++++++++++++++++++++++++++++++++++++++++
 public/index.html          | 136 +++++++++++++++++++++++++++++++++
 scripts/refresh-domains.sh |  21 ++++++
 server.js                  | 155 ++++++++++++++++++++++++++++++++++++++
 5 files changed, 502 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1924158
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
diff --git a/domains.json b/domains.json
new file mode 100644
index 0000000..e36d448
--- /dev/null
+++ b/domains.json
@@ -0,0 +1,182 @@
+[
+ "designerwallcoverings.com",
+ "1838-wallcoverings.designerwallcoverings.com",
+ "all.designerwallcoverings.com",
+ "andrew-martin.designerwallcoverings.com",
+ "anna-french.designerwallcoverings.com",
+ "api.designerwallcoverings.com",
+ "ardsley-textiles.designerwallcoverings.com",
+ "arte-international.designerwallcoverings.com",
+ "artmura.designerwallcoverings.com",
+ "as-creation.designerwallcoverings.com",
+ "astek.designerwallcoverings.com",
+ "atomic-50-ceilings.designerwallcoverings.com",
+ "atomic50.designerwallcoverings.com",
+ "baker-lifestyle.designerwallcoverings.com",
+ "black-edition.designerwallcoverings.com",
+ "brand-mckenzie.designerwallcoverings.com",
+ "british-walls.designerwallcoverings.com",
+ "britishwalls.designerwallcoverings.com",
+ "brunschwig-fils.designerwallcoverings.com",
+ "carlise-co.designerwallcoverings.com",
+ "carlisle-co.designerwallcoverings.com",
+ "carlrobinson.designerwallcoverings.com",
+ "catchii.designerwallcoverings.com",
+ "celebrity.designerwallcoverings.com",
+ "chat.designerwallcoverings.com",
+ "china-seas.designerwallcoverings.com",
+ "china.designerwallcoverings.com",
+ "clarke-and-clarke.designerwallcoverings.com",
+ "cmo-paris.designerwallcoverings.com",
+ "coastline-reserve.designerwallcoverings.com",
+ "cole-son-sg.designerwallcoverings.com",
+ "cole-son.designerwallcoverings.com",
+ "coleandson.designerwallcoverings.com",
+ "colefax-fowler.designerwallcoverings.com",
+ "colour-design.designerwallcoverings.com",
+ "coordonn.designerwallcoverings.com",
+ "cowtan-tout.designerwallcoverings.com",
+ "cowtan.designerwallcoverings.com",
+ "crezana-internal.designerwallcoverings.com",
+ "crezana.designerwallcoverings.com",
+ "daisybennett.designerwallcoverings.com",
+ "dedar.designerwallcoverings.com",
+ "denovo-wall.designerwallcoverings.com",
+ "designers-guild.designerwallcoverings.com",
+ "designtex.designerwallcoverings.com",
+ "dolce-gabbana.designerwallcoverings.com",
+ "donghia.designerwallcoverings.com",
+ "eijffinger.designerwallcoverings.com",
+ "elitis.designerwallcoverings.com",
+ "fabricut.designerwallcoverings.com",
+ "fallingstar.designerwallcoverings.com",
+ "farrow-ball.designerwallcoverings.com",
+ "fentucci.designerwallcoverings.com",
+ "fernwood-weave.designerwallcoverings.com",
+ "followup.designerwallcoverings.com",
+ "franquemont.designerwallcoverings.com",
+ "fromental-internal.designerwallcoverings.com",
+ "fromental.designerwallcoverings.com",
+ "gaston-y-daniela.designerwallcoverings.com",
+ "glitter-wallcoverings.designerwallcoverings.com",
+ "glitter-walls.designerwallcoverings.com",
+ "goodrich.designerwallcoverings.com",
+ "gp-j-baker.designerwallcoverings.com",
+ "gracie-internal.designerwallcoverings.com",
+ "gracie.designerwallcoverings.com",
+ "graham-brown.designerwallcoverings.com",
+ "greenland.designerwallcoverings.com",
+ "harlequin-sg.designerwallcoverings.com",
+ "harlequin.designerwallcoverings.com",
+ "holly-hunt.designerwallcoverings.com",
+ "hollywood-wallcoverings.designerwallcoverings.com",
+ "hygge-west.designerwallcoverings.com",
+ "innovations.designerwallcoverings.com",
+ "internal.astek.designerwallcoverings.com",
+ "internal.crezana.designerwallcoverings.com",
+ "internal.fromental.designerwallcoverings.com",
+ "internal.gracie.designerwallcoverings.com",
+ "internal.greenland.designerwallcoverings.com",
+ "internal.momentum.designerwallcoverings.com",
+ "internal.reidwitlin.designerwallcoverings.com",
+ "internal.schumacher.designerwallcoverings.com",
+ "internal.zuber.designerwallcoverings.com",
+ "j-josephson.designerwallcoverings.com",
+ "japan.designerwallcoverings.com",
+ "jeffrey-stevens.designerwallcoverings.com",
+ "jf-fabrics.designerwallcoverings.com",
+ "jim-dultz.designerwallcoverings.com",
+ "kamism.designerwallcoverings.com",
+ "kirkby-design.designerwallcoverings.com",
+ "knoll.designerwallcoverings.com",
+ "koroseal-sg.designerwallcoverings.com",
+ "koroseal.designerwallcoverings.com",
+ "kravet.designerwallcoverings.com",
+ "la-scala-milano.designerwallcoverings.com",
+ "lawalls.designerwallcoverings.com",
+ "lee-jofa.designerwallcoverings.com",
+ "leed-walls.designerwallcoverings.com",
+ "leedwalls.designerwallcoverings.com",
+ "lillianaugust.designerwallcoverings.com",
+ "lincrusta.designerwallcoverings.com",
+ "maharam.designerwallcoverings.com",
+ "malibu-wallpaper.designerwallcoverings.com",
+ "manuel-canovas.designerwallcoverings.com",
+ "marburg.designerwallcoverings.com",
+ "mark-alexander.designerwallcoverings.com",
+ "marketing.designerwallcoverings.com",
+ "maya-romanoff.designerwallcoverings.com",
+ "milton-king.designerwallcoverings.com",
+ "mind-the-gap.designerwallcoverings.com",
+ "momentum.designerwallcoverings.com",
+ "mulberry.designerwallcoverings.com",
+ "murals.designerwallcoverings.com",
+ "naturale-54.designerwallcoverings.com",
+ "new.designerwallcoverings.com",
+ "newmor.designerwallcoverings.com",
+ "newwall.designerwallcoverings.com",
+ "nina-campbell.designerwallcoverings.com",
+ "nlxl.designerwallcoverings.com",
+ "nobilis.designerwallcoverings.com",
+ "novasuede.designerwallcoverings.com",
+ "omexco.designerwallcoverings.com",
+ "p3tec.designerwallcoverings.com",
+ "pairs.designerwallcoverings.com",
+ "paperandink.designerwallcoverings.com",
+ "philipjeffries.designerwallcoverings.com",
+ "phillip-jeffries.designerwallcoverings.com",
+ "phillipe-romano-textiles.designerwallcoverings.com",
+ "phillipe-romano-vinyls.designerwallcoverings.com",
+ "phillipe-romano.designerwallcoverings.com",
+ "photo.designerwallcoverings.com",
+ "photocapture.designerwallcoverings.com",
+ "pierre-frey.designerwallcoverings.com",
+ "pitch.designerwallcoverings.com",
+ "pitchguard.designerwallcoverings.com",
+ "prestige.designerwallcoverings.com",
+ "primoleathers.designerwallcoverings.com",
+ "quadrille-showroom.designerwallcoverings.com",
+ "quadrille.designerwallcoverings.com",
+ "quadrilleshowroom.designerwallcoverings.com",
+ "rebel-walls.designerwallcoverings.com",
+ "reid-witlin.designerwallcoverings.com",
+ "reidwitlin.designerwallcoverings.com",
+ "roberto-cavalli.designerwallcoverings.com",
+ "romo.designerwallcoverings.com",
+ "sancar.designerwallcoverings.com",
+ "sandberg.designerwallcoverings.com",
+ "sangetsu.designerwallcoverings.com",
+ "scalamandre.designerwallcoverings.com",
+ "schumacher.designerwallcoverings.com",
+ "sister-parish.designerwallcoverings.com",
+ "staged.designerwallcoverings.com",
+ "studio-printworks.designerwallcoverings.com",
+ "substitutes.designerwallcoverings.com",
+ "surface-stick.designerwallcoverings.com",
+ "surfacestick.designerwallcoverings.com",
+ "tecnografica.designerwallcoverings.com",
+ "texam.designerwallcoverings.com",
+ "texdecor.designerwallcoverings.com",
+ "thibaut.designerwallcoverings.com",
+ "threads.designerwallcoverings.com",
+ "timorous-beasties.designerwallcoverings.com",
+ "trending.designerwallcoverings.com",
+ "tres-tintas.designerwallcoverings.com",
+ "vahallan.designerwallcoverings.com",
+ "versa-design-surfaces.designerwallcoverings.com",
+ "versace.designerwallcoverings.com",
+ "villa-nova.designerwallcoverings.com",
+ "wallquest.designerwallcoverings.com",
+ "william-morris.designerwallcoverings.com",
+ "wolf-gordon.designerwallcoverings.com",
+ "york-contract.designerwallcoverings.com",
+ "york-wallcoverings.designerwallcoverings.com",
+ "zambaiti-parati.designerwallcoverings.com",
+ "zambaiti.designerwallcoverings.com",
+ "zimmer-rohde.designerwallcoverings.com",
+ "zinc-textile.designerwallcoverings.com",
+ "zintra.designerwallcoverings.com",
+ "zoffany.designerwallcoverings.com",
+ "zuber-internal.designerwallcoverings.com",
+ "zuber.designerwallcoverings.com"
+]
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..7724fc2
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,136 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>designerwallcoverings.com — Domain Viewer</title>
+<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%231e2a3a'/%3E%3Ctext x='8' y='12' text-anchor='middle' font-family='Georgia,serif' font-size='11' fill='%23f7f1e6'%3Ed%3C/text%3E%3C/svg%3E">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<style>
+  :root { --rail: 320px; --bg:#0e1116; --panel:#161b22; --line:#2b313b; --txt:#dfe6ee; --dim:#8b95a3; --accent:#3fb6a8; }
+  * { box-sizing: border-box; }
+  body { margin:0; height:100vh; display:flex; font:14px/1.45 -apple-system, "SF Pro Text", Helvetica, sans-serif; background:var(--bg); color:var(--txt); }
+
+  #rail { width:var(--rail); min-width:240px; max-width:500px; display:flex; flex-direction:column; border-right:1px solid var(--line); background:var(--panel); }
+  #rail header { padding:14px 14px 10px; border-bottom:1px solid var(--line); }
+  #rail h1 { margin:0 0 8px; font-size:15px; font-weight:700; letter-spacing:.02em; }
+  #rail h1 small { color:var(--dim); font-weight:400; }
+  #q { width:100%; padding:7px 10px; border-radius:7px; border:1px solid var(--line); background:var(--bg); color:var(--txt); outline:none; }
+  #q:focus { border-color:var(--accent); }
+  #list { flex:1; overflow-y:auto; padding:6px; }
+  .item { display:flex; align-items:center; gap:8px; padding:6px 9px; border-radius:7px; cursor:pointer; color:var(--txt); user-select:none; }
+  .item:hover { background:#1e2530; }
+  .item.active { background:#213132; outline:1px solid var(--accent); }
+  .dot { width:8px; height:8px; border-radius:50%; flex:none; background:#555; }
+  .dot.up { background:#3fbf6f; }
+  .dot.auth { background:#d9a53a; }
+  .dot.down { background:#d95555; }
+  .name { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
+  .name b { color:var(--accent); font-weight:600; }
+  .tag { flex:none; font-size:9.5px; letter-spacing:.04em; color:var(--dim); border:1px solid var(--line); border-radius:4px; padding:1px 4px; }
+  .ms { color:var(--dim); font-size:11px; flex:none; }
+  #foot { padding:8px 14px; border-top:1px solid var(--line); color:var(--dim); font-size:11.5px; }
+
+  #main { flex:1; display:flex; flex-direction:column; min-width:0; }
+  #bar { display:flex; align-items:center; gap:10px; padding:8px 12px; border-bottom:1px solid var(--line); background:var(--panel); }
+  #cur { font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
+  #cur a { color:var(--accent); text-decoration:none; }
+  #bar .sp { flex:1; }
+  button { padding:6px 12px; border-radius:7px; border:1px solid var(--line); background:var(--bg); color:var(--txt); cursor:pointer; font-size:13px; }
+  button:hover { border-color:var(--accent); color:var(--accent); }
+  #frame { flex:1; border:0; width:100%; background:#fff; }
+  #empty { flex:1; display:flex; align-items:center; justify-content:center; color:var(--dim); flex-direction:column; gap:8px; }
+  #empty .big { font-size:40px; }
+</style>
+</head>
+<body>
+  <div id="rail">
+    <header>
+      <h1>designerwallcoverings.com <small id="count"></small></h1>
+      <input id="q" type="search" placeholder="Filter sites…" autocomplete="off">
+    </header>
+    <div id="list"></div>
+    <div id="foot">click a site → renders 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">
+      <span id="cur">— pick a site</span>
+      <span class="sp"></span>
+      <button id="reload" title="Reload iframe">⟳ Reload</button>
+      <button id="newtab" title="Open in a new browser tab">↗ New Tab</button>
+    </div>
+    <div id="empty"><div class="big">🌐</div><div>Select a site on the left to view it live here.</div></div>
+    <iframe id="frame" style="display:none"></iframe>
+  </div>
+
+<script>
+let domains = [], status = {}, current = localStorage.getItem('dwv-current') || '';
+const list = document.getElementById('list'), q = document.getElementById('q');
+const frame = document.getElementById('frame'), empty = document.getElementById('empty');
+const cur = document.getElementById('cur'), count = document.getElementById('count');
+
+// internal.* and *-internal hosts are curation tools, not customer-facing microsites.
+const isInternal = (d) => /^internal\./.test(d) || /-internal\./.test(d);
+// Rail shows the short label; the full host lives in the title tooltip.
+const shortName = (d) => d === 'designerwallcoverings.com' ? '● apex (DW store)' : d.replace('.designerwallcoverings.com', '');
+
+function render() {
+  const f = q.value.trim().toLowerCase();
+  const shown = domains.filter(d => d.includes(f));
+  count.textContent = `(${shown.length}/${domains.length})`;
+  list.innerHTML = '';
+  for (const d of shown) {
+    const s = status[d];
+    const row = document.createElement('div');
+    row.className = 'item' + (d === current ? ' active' : '');
+    row.title = d;
+    const dotCls = !s ? '' : s.authFail ? 'down' : s.gated ? 'auth' : s.up ? 'up' : 'down';
+    const label = f ? shortName(d).replace(f, `<b>${f}</b>`) : shortName(d);
+    row.innerHTML = `<span class="dot ${dotCls}"></span><span class="name">${label}</span>` +
+      (isInternal(d) ? `<span class="tag">INT</span>` : '') +
+      (s && s.up ? `<span class="ms">${s.ms}ms</span>` : s ? `<span class="ms">—</span>` : '');
+    row.onclick = () => select(d);
+    list.appendChild(row);
+  }
+}
+
+function select(d) {
+  current = d;
+  localStorage.setItem('dwv-current', d);
+  cur.innerHTML = `<a href="https://${d}" target="_blank" rel="noopener noreferrer">${d}</a>`;
+  // 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 = '/site/' + current + '/'; };
+document.getElementById('newtab').onclick = () => { if (current) window.open('https://' + current, '_blank', 'noopener'); };
+q.oninput = render;
+
+(async () => {
+  try {
+    const dres = await fetch('/api/domains');
+    if (!dres.ok) throw new Error('domains HTTP ' + dres.status);
+    domains = await dres.json();
+    render();
+    if (current && domains.includes(current)) select(current);
+  } catch (e) {
+    count.textContent = '(load failed — reload)';
+    return;
+  }
+  // Health dots load after the list — probes take ~60-90s for 180 domains. A fetch
+  // aborted mid-flight (tab closed, server restarted) must not surface as an
+  // unhandled rejection; the list already rendered, dots are best-effort.
+  try {
+    const sres = await fetch('/api/status');
+    if (!sres.ok) throw new Error('status HTTP ' + sres.status);
+    const st = await sres.json();
+    for (const s of st) status[s.domain] = s;
+    render();
+  } catch (e) { /* dots stay grey; next reload retries */ }
+})();
+</script>
+</body>
+</html>
diff --git a/scripts/refresh-domains.sh b/scripts/refresh-domains.sh
new file mode 100755
index 0000000..85f1ef1
--- /dev/null
+++ b/scripts/refresh-domains.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+# Re-pull the *.designerwallcoverings.com record list from Cloudflare into domains.json.
+# Run after adding/removing a subdomain in the CF zone. $0 (local + CF API reads).
+set -euo pipefail
+export $(grep -E "^CLOUDFLARE_API_TOKEN" ~/Projects/secrets-manager/.env | xargs)
+ZONE=$(curl -s "https://api.cloudflare.com/client/v4/zones?name=designerwallcoverings.com" \
+  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" | python3 -c "import json,sys; print(json.load(sys.stdin)['result'][0]['id'])")
+curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE/dns_records?per_page=200" \
+  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" | python3 -c "
+import json, sys, os
+d = json.load(sys.stdin)
+names = sorted(set(r['name'] for r in d['result'] if r['type'] in ('A', 'AAAA', 'CNAME')))
+drop = {'_domainconnect.designerwallcoverings.com',
+        'workforce.agentabrams.com.designerwallcoverings.com',
+        'www.designerwallcoverings.com'}
+names = [n for n in names if n not in drop]
+names.sort(key=lambda n: (n != 'designerwallcoverings.com', n))
+out = os.path.expanduser('~/Projects/dw-domain-viewer/domains.json')
+json.dump(names, open(out, 'w'), indent=1)
+print(len(names), 'domains written to', out)
+"
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..49e0353
--- /dev/null
+++ b/server.js
@@ -0,0 +1,155 @@
+#!/usr/bin/env node
+// dw-domain-viewer — left-panel list of every *.designerwallcoverings.com site,
+// right-panel view of the live site. Pattern cloned from agentabrams-viewer (:9783).
+// 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');
+const path = require('path');
+
+const PORT = process.env.PORT || 9784;
+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' };
+
+function loadDomains() {
+  return JSON.parse(fs.readFileSync(path.join(ROOT, 'domains.json'), 'utf8'));
+}
+
+// Server-side health probe — the browser can't check cross-origin status itself.
+// 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 headers = withAuth ? { authorization: UPSTREAM_AUTH } : {};
+    const req = https.request({ host: domain, method: 'HEAD', path: '/', headers, timeout: 6000, rejectUnauthorized: true }, (res) => {
+      res.resume();
+      resolve(res.statusCode);
+    });
+    req.on('timeout', () => { req.destroy(); resolve(0); });
+    req.on('error', () => resolve(0));
+    req.end();
+  });
+}
+
+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) => {
+  const url = new URL(req.url, 'http://x');
+
+  if (url.pathname === '/healthz') { res.writeHead(200); return res.end('ok'); }
+
+  if (req.headers.authorization !== AUTH) {
+    res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="dw-domain-viewer"' });
+    return res.end('auth required');
+  }
+
+  if (url.pathname === '/api/domains') {
+    res.writeHead(200, { 'Content-Type': 'application/json' });
+    return res.end(JSON.stringify(loadDomains()));
+  }
+
+  if (url.pathname === '/api/status') {
+    const domains = loadDomains();
+    // Probe in batches of 16 so 180 HEADs don't stampede.
+    const out = [];
+    for (let i = 0; i < domains.length; i += 16) {
+      out.push(...await Promise.all(domains.slice(i, i + 16).map(probe)));
+    }
+    res.writeHead(200, { 'Content-Type': 'application/json' });
+    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(); }
+    const domains = loadDomains();
+    if (!domains.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'];
+      // The root-relative asset escape hatch below routes by Referer; an upstream
+      // no-referrer policy would blind it and every /assets/* request would 404.
+      delete h['referrer-policy'];
+      delete h.connection;
+      if (h.location) {
+        try {
+          const loc = new URL(h.location, `https://${domain}`);
+          if (domains.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) => {
+          // A Domain= attribute means the cookie is cross-subdomain — scope it to
+          // /site/ so it follows the user across every proxied domain.
+          const fleetWide = /;\s*Domain=/i.test(c);
+          return c
+            .replace(/;\s*Domain=[^;]+/gi, '')
+            // Viewer is served over plain http on loopback; Safari drops Secure cookies there.
+            .replace(/;\s*Secure/gi, '')
+            .replace(/;\s*Path=\/([^;]*)/i, fleetWide ? '; Path=/site/' : `; Path=/site/${domain}/$1`);
+        });
+      }
+      res.writeHead(pres.statusCode, h);
+      pres.pipe(res);
+    });
+    preq.on('timeout', () => preq.destroy(new Error('upstream timeout')));
+    preq.on('error', (e) => { if (res.writableEnded) return; 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' });
+    res.end(data);
+  });
+});
+
+server.listen(PORT, () => console.log(`dw-domain-viewer on http://127.0.0.1:${PORT}`));

(oldest)  ·  back to Dw Domain Viewer  ·  fleet green pass: certs+443 for 4 vendor landings, 8 dead DN af1ed5d →