← 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
M shopify/scripts/cadence/cadence-import.js
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,'&').replace(/</g,'<').replace(/>/g,'>');
+
+// 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 →