[object Object]

← back to Designer Wallcoverings

cadence-import: scrubBanned() guard on vendor+collection at body_html intro writers

5efac5ec2ca1a820b712c81299c760b87e03353b · 2026-06-25 09:47:03 -0700 · Steve Abrams

Files touched

Diff

commit 5efac5ec2ca1a820b712c81299c760b87e03353b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jun 25 09:47:03 2026 -0700

    cadence-import: scrubBanned() guard on vendor+collection at body_html intro writers
---
 shopify/scripts/cadence/cadence-import.js | 44 ++++++++++++++++++++++++++++---
 1 file changed, 41 insertions(+), 3 deletions(-)

diff --git a/shopify/scripts/cadence/cadence-import.js b/shopify/scripts/cadence/cadence-import.js
index 4ca61306..aefb3943 100644
--- a/shopify/scripts/cadence/cadence-import.js
+++ b/shopify/scripts/cadence/cadence-import.js
@@ -114,6 +114,29 @@ function titleCase(s) {
   return ws.map((w,i) => tcWord(w, i===0, i===ws.length-1)).join(' ');
 }
 const esc = s => String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
+
+// Defense-in-depth: scrub private-label upstream / banned front-facing vendor names out of any
+// string that lands in a customer-facing field (title core, description intro). Mirrors the
+// dw-leak-scanner denylist + the fix-brewster-york-descriptions repair tool's SAFE York handling.
+// "Brewster" and "York Wallcovering(s)"/"by York" are stripped; bare "York" inside "New York" /
+// "York Weave" / "Yorktown" is left intact. Idempotent — clean vendors pass through unchanged.
+function scrubBanned(str) {
+  let s = String(str==null?'':str);
+  s = s
+    .replace(/Brewster\s*&(?:amp;)*\s*York/gi, '')   // "Brewster & York" (any &-encoding)
+    .replace(/\bBrewster\b/gi, '')                    // any standalone Brewster
+    .replace(/\bYork Wallcoverings?\b/gi, '')         // "York Wallcovering(s)" — never bare "New York"
+    .replace(/\bby York\b/gi, '')                     // "by York"
+    .replace(/\byorkwall\b/gi, '')                    // York trade-portal domain — unambiguous leak
+    .replace(/\bWall ?Quest\b/gi, '')                 // WallQuest (→ Malibu Wallpaper)
+    .replace(/\bChesapeake\b/gi, '')                  // WallQuest sub-brand
+    .replace(/\bNext ?Wall\b/gi, '')                  // WallQuest sub-brand
+    .replace(/\bSeabrook\b/gi, '')                    // WallQuest sub-brand
+    .replace(/\bCommand ?54\b/gi, '')                 // Command54 (→ Phillipe Romano)
+    .replace(/\bDesima\b/gi, '')                      // Desima (→ Phillipe Romano Elegante)
+    .replace(/\bCarlsten\b/gi, '');                   // Desima/Carlsten line
+  return s.replace(/\s{2,}/g, ' ').trim();
+}
 // Defensive unwrap for double-encoded metafield values. If a catalog spec column
 // (e.g. material) holds a whole Shopify metafield object — {"type":...,"value":"X"} —
 // stored as its JSON string, writing it raw leaks the blob into BOTH custom.material
@@ -347,9 +370,11 @@ function buildSampleOnlyInput(vendor, cfg, row, activate) {
   if (row.roll_length) specs.push(`<li><strong>Roll Length:</strong> ${esc(row.roll_length)}</li>`);
   if (row.pattern_repeat) specs.push(`<li><strong>Repeat:</strong> ${esc(row.pattern_repeat)}</li>`);
   if (row.collection) specs.push(`<li><strong>Collection:</strong> ${esc(row.collection)}</li>`);
+  const safeVendor = scrubBanned(vendor);
+  const safeCollection = scrubBanned(row.collection);
   const intro = vendorDesc
     ? esc(vendorDesc).replace(/wallpaper/gi,'Wallcovering')
-    : `${pattern||core}${color?' in '+color:''} is a designer wallcovering${row.collection?' from the '+esc(row.collection)+' collection':''} by ${vendor}.`;
+    : `${pattern||core}${color?' in '+color:''} is a designer wallcovering${safeCollection?' from the '+esc(safeCollection)+' collection':''} by ${safeVendor}.`;
   const descriptionHtml = `<p>${intro}</p>${specs.length?`<ul>${specs.join('')}</ul>`:''}`;
 
   // Metafields — NO cost (there is none). Width/specs where present, brand, sku, pattern, color.
@@ -426,7 +451,9 @@ function buildInput(vendor, cfg, row, retail, activate) {
   if (row.roll_length) specs.push(`<li><strong>Roll Length:</strong> ${esc(row.roll_length)}</li>`);
   if (row.pattern_repeat) specs.push(`<li><strong>Repeat:</strong> ${esc(row.pattern_repeat)}</li>`);
   specs.push(`<li><strong>Sold By:</strong> ${esc(cfg.soldBy)}</li>`);
-  const intro = `${pattern}${color?' in '+color:''} is a designer wallcovering${row.collection?' from the '+esc(row.collection)+' collection':''} by ${vendor}. Priced per ${cfg.soldBy.toLowerCase()}.`;
+  const safeVendor = scrubBanned(vendor);
+  const safeCollection = scrubBanned(row.collection);
+  const intro = `${pattern}${color?' in '+color:''} is a designer wallcovering${safeCollection?' from the '+esc(safeCollection)+' collection':''} by ${safeVendor}. Priced per ${cfg.soldBy.toLowerCase()}.`;
   const SL='single_line_text_field', ML='multi_line_text_field', ND='number_decimal';
   const mf=[]; const m=(ns,k,v,t)=>{ if(v!=null&&v!=='') mf.push({namespace:ns,key:k,type:t,value:String(v)}); };
   m('custom','manufacturer_sku',row.mfr_sku,SL); m('global','manufacturer_sku',row.mfr_sku,SL);
@@ -654,10 +681,21 @@ async function createProduct(table, row, payload) {
 
 // Export internals for unit/inspection harnesses (no behavior change when run as a script;
 // the main IIFE below only runs on direct execution via the require.main guard).
-module.exports = { gate, selectSkus, buildInput, buildSampleOnlyInput, brandFilterClause };
+module.exports = { gate, selectSkus, buildInput, buildSampleOnlyInput, brandFilterClause, scrubBanned };
 
 // ---- main ----
 if (require.main === module) (async () => {
+  if (flag('--selftest')) {
+    const cases = [['Brewster',''], ['York Wallcoverings',''], ['New York Night','New York Night'], ['Thibaut','Thibaut']];
+    let ok = 0;
+    for (const [input, expected] of cases) {
+      const got = scrubBanned(input);
+      const pass = got === expected; if (pass) ok++;
+      console.log(`${pass?'PASS':'FAIL'}  "${input}" -> "${got}" (expected "${expected}")`);
+    }
+    console.log(`${ok}/${cases.length} scrubBanned cases passed`);
+    process.exit(ok === cases.length ? 0 : 1);
+  }
   if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
   const all = gate();
   console.log(`\n=== VENDOR GATE (slot=${SLOT}) ===`);

← b4dccb49 Anna French gate+diff audit artifact (734 pass, 239 disconti  ·  back to Designer Wallcoverings  ·  Anna French: build 301 redirect map (789 rows, 0 collisions, 749b4c5d →