[object Object]

← back to All Designerwallcoverings

astek CF-Access: env-gated service-token headers on crawler + feed proxy (TK-11, memo 260727A)

26c4bba393d10015fdaa09efe2b10a9fbffc83a0 · 2026-07-27 15:05:58 -0700 · Steve

Inert/byte-identical (Basic-Auth-only) until CF_ACCESS_ASTEK_ID/SECRET are set;
scoped to astek.designerwallcoverings.com only. Deploy to Kamatera stays gated.

Files touched

Diff

commit 26c4bba393d10015fdaa09efe2b10a9fbffc83a0
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Jul 27 15:05:58 2026 -0700

    astek CF-Access: env-gated service-token headers on crawler + feed proxy (TK-11, memo 260727A)
    
    Inert/byte-identical (Basic-Auth-only) until CF_ACCESS_ASTEK_ID/SECRET are set;
    scoped to astek.designerwallcoverings.com only. Deploy to Kamatera stays gated.
---
 scripts/crawl-microsites.js | 18 ++++++++++++++++++
 server.js                   | 14 +++++++++++++-
 2 files changed, 31 insertions(+), 1 deletion(-)

diff --git a/scripts/crawl-microsites.js b/scripts/crawl-microsites.js
index 45cd86d..19ffac0 100644
--- a/scripts/crawl-microsites.js
+++ b/scripts/crawl-microsites.js
@@ -222,9 +222,27 @@ function isInternalHost(url) {
     return h === '127.0.0.1' || h === 'localhost' || /(^|\.)designerwallcoverings\.com$/i.test(h);
   } catch { return false; }
 }
+// CF Access service-token headers for astek (Zero-Trust pilot — memo 260727A / TK-11).
+// Once astek moves behind Cloudflare Access and its nginx auth_basic is dropped, the
+// shared Basic-Auth cred no longer authenticates the crawler — the CF Access service
+// token does. Scoped to the astek host ONLY (a CF Access service token is per-application).
+// Inert + byte-identical (Basic-Auth-only, exactly as today) whenever CF_ACCESS_ASTEK_ID /
+// CF_ACCESS_ASTEK_SECRET are unset. During the transition window (Access enforced but the
+// origin nginx auth_basic still present) BOTH headers ride — satisfying the CF edge AND
+// the origin gate — and once auth_basic is dropped the leftover Basic header is harmless.
+const CF_ACCESS_ASTEK_ID = process.env.CF_ACCESS_ASTEK_ID || '';
+const CF_ACCESS_ASTEK_SECRET = process.env.CF_ACCESS_ASTEK_SECRET || '';
+function isAstekHost(url) {
+  try { return new URL(url).hostname.toLowerCase() === 'astek.designerwallcoverings.com'; }
+  catch { return false; }
+}
 async function getText(url, ms) {
   const headers = { 'user-agent': 'all-dw-crawler/1.0 (+all.designerwallcoverings.com)' };
   if (MICROSITE_AUTH && isInternalHost(url)) headers.Authorization = MICROSITE_AUTH;
+  if (CF_ACCESS_ASTEK_ID && CF_ACCESS_ASTEK_SECRET && isAstekHost(url)) {
+    headers['CF-Access-Client-Id'] = CF_ACCESS_ASTEK_ID;
+    headers['CF-Access-Client-Secret'] = CF_ACCESS_ASTEK_SECRET;
+  }
   const r = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(ms), headers });
   return { status: r.status, ok: r.ok, finalUrl: r.url, body: await r.text() };
 }
diff --git a/server.js b/server.js
index d1e310d..2f649c0 100644
--- a/server.js
+++ b/server.js
@@ -541,6 +541,13 @@ const MICROSITE_PER_SITE_CAP = 800;   // full catalog per site is ≤735 — wel
 // are readable. Same override the crawler uses; defaults to the standard admin cred.
 const MICROSITE_FEED_AUTH = process.env.MICROSITE_BASIC_AUTH || 'admin:DW2024!';
 const MICROSITE_FEED_HDR = 'Basic ' + Buffer.from(MICROSITE_FEED_AUTH).toString('base64');
+// CF Access service-token headers for astek (Zero-Trust pilot — memo 260727A / TK-11).
+// When astek moves behind Cloudflare Access, the on-demand feed proxy needs the service
+// token too. Scoped to the astek host ONLY (per-application token). Inert + byte-identical
+// (Basic-Auth-only, as today) whenever CF_ACCESS_ASTEK_ID / CF_ACCESS_ASTEK_SECRET are unset.
+const CF_ACCESS_ASTEK_ID = process.env.CF_ACCESS_ASTEK_ID || '';
+const CF_ACCESS_ASTEK_SECRET = process.env.CF_ACCESS_ASTEK_SECRET || '';
+const isAstekHost = (u) => { try { return new URL(u).hostname.toLowerCase() === 'astek.designerwallcoverings.com'; } catch { return false; } };
 const MICROSITE_FEED_TIMEOUT_MS = 12000;
 
 // Map a heterogeneous microsite feed row (each of the 4 feeds has a slightly different
@@ -628,9 +635,14 @@ function deriveMicrositeRow(p, feed) {
 }
 
 async function fetchMicrositeFeed(feed) {
+  const headers = { 'user-agent': 'all-dw/1.0', Authorization: MICROSITE_FEED_HDR };
+  if (CF_ACCESS_ASTEK_ID && CF_ACCESS_ASTEK_SECRET && isAstekHost(feed.url)) {
+    headers['CF-Access-Client-Id'] = CF_ACCESS_ASTEK_ID;
+    headers['CF-Access-Client-Secret'] = CF_ACCESS_ASTEK_SECRET;
+  }
   const r = await fetch(feed.url, {
     redirect: 'follow', signal: AbortSignal.timeout(MICROSITE_FEED_TIMEOUT_MS),
-    headers: { 'user-agent': 'all-dw/1.0', Authorization: MICROSITE_FEED_HDR },
+    headers,
   });
   if (r.status !== 200) throw new Error(`HTTP ${r.status}`);
   const j = JSON.parse(await r.text());

← ae33f6d auto-save: 2026-07-27T14:51:27 (1 files) — server.js  ·  back to All Designerwallcoverings  ·  chore: v1.7.0 — async job+poll live-check feature (session c 699a5fd →