[object Object]

← back to Stringwallpaper

step2 vendor-leak fix: dual-key /sample resolver + handle_display hrefs

88600eff2df10b7c159dffa34726304061d3cde4 · 2026-06-09 13:47:50 -0700 · Steve

Files touched

Diff

commit 88600eff2df10b7c159dffa34726304061d3cde4
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jun 9 13:47:50 2026 -0700

    step2 vendor-leak fix: dual-key /sample resolver + handle_display hrefs
---
 server.js | 35 ++++++++++++++++++++++++++++++++++-
 1 file changed, 34 insertions(+), 1 deletion(-)

diff --git a/server.js b/server.js
index 4d98615..5423aae 100644
--- a/server.js
+++ b/server.js
@@ -227,8 +227,40 @@ app.get('/api/facets', (req, res) => {
 
 app.get('/api/health', (req, res) => res.json({ status: 'ok', count: PRODUCTS.length, dropped: DROPPED }));
 
+// Step 2 of the vendor handle/sku text-leak fix (2026-06-09): the client's
+// /sample/ hrefs now point at the vendor-scrubbed `handle_display` slug
+// (emitted by _shared/api-vendor-redact since Step 1), so the raw vendor-bearing
+// handle never rides in the URL bar. This route is DUAL-KEY — it resolves the raw
+// handle/sku FIRST (old bookmarks + buy path, byte-identical behavior), then falls
+// back to a redacted->product map built after catalog load. If two raw handles
+// redact to the same display slug, that slug is logged and left raw-only.
+const { redactVendorText } = require('../_shared/vendor-text-redact');
+let SAMPLE_DISPLAY = new Map(); // redacted display slug -> product
+function buildSampleDisplayMap() {
+  const map = new Map();
+  const collided = new Set();
+  for (const p of PRODUCTS) {
+    for (const key of [p.handle, p.sku]) {
+      if (!key) continue;
+      const d = redactVendorText(String(key));
+      if (!d || d === key) continue;           // nothing redacted — raw route already matches
+      const prev = map.get(d);
+      if (prev && prev !== p) { collided.add(d); continue; }
+      map.set(d, p);
+    }
+  }
+  for (const d of collided) {
+    map.delete(d);
+    console.warn(`[stringwallpaper] /sample display-slug collision — keeping raw-only for "${d}"`);
+  }
+  SAMPLE_DISPLAY = map;
+  console.log(`[stringwallpaper] /sample dual-key map: ${map.size} display slugs, ${collided.size} collisions (raw-only)`);
+}
+
 app.get('/sample/:handle', (req, res) => {
-  const p = PRODUCTS.find(x => x.handle === req.params.handle || x.sku === req.params.handle);
+  const key = req.params.handle;
+  const p = PRODUCTS.find(x => x.handle === key || x.sku === key) // raw first — old behavior intact
+    || SAMPLE_DISPLAY.get(key);                                        // then the redacted display form
   if (!p) return res.status(404).send('Not found');
   res.redirect(302, p.product_url || `${DW_SHOPIFY}/products/${encodeURIComponent(p.handle)}#sample`);
 });
@@ -273,6 +305,7 @@ if (catalog) catalog.mount(app, { siteSlug: SITE_SLUG, rails: SITE_RAILS });
       console.log(`[${__SITE}] loaded ${PRODUCTS.length} from data/products.json (static mode)`);
     } catch (_) { PRODUCTS = []; }
   }
+  buildSampleDisplayMap(); // Step 2: redacted->raw /sample resolver keys
   app.listen(PORT, '127.0.0.1', () => {
     console.log(`stringwallpaper listening on http://127.0.0.1:${PORT}`);
   });

← dd2eafb Scrub residual vendor names from products.json (title/vendor  ·  back to Stringwallpaper  ·  fix: safeImg allow same-origin /img/ proxy urls (vendor-neut 005e1a1 →