← back to Dw Domain Fleet

output/standalone-outbound-href-audit-2026-05-31.md

188 lines

# Standalone storefronts — outbound-href vendor-leak audit

**Date:** 2026-05-31
**Scope:** READ-ONLY. The 3 STANDALONE storefronts (own codebases, NOT on the shared
`dw-domain-fleet/shared/render.js`). Goal: do they carry the same outbound-href vendor
leak the fleet fix `a85eb02` closed?
**Reference fix:** `dw-domain-fleet` commit `a85eb02` — route the PDP buy CTA +
"View on the main store" anchor + canonical through a CLEAN slug, with an internal
`/buy/:slug` 302-redirect that resolves the real handle server-side so the raw
vendor-bearing handle never appears in customer-facing HTML.

---

## TL;DR

| Site | Verdict | Leak vectors | Helpers present | `/buy/:slug` route | Live curl |
|------|---------|--------------|-----------------|--------------------|-----------|
| customdigitalmurals | **LEAKS** | buy-CTA, main-store-anchor, card-sample-href | `redactVendorSlug` + denylist ✅ | ❌ missing | ✗ (port down) |
| carmelwallpapers | **LEAKS** | buy-CTA, "View Full Product" anchor, card-sample-href | `redactVendorSlug` + denylist ✅ | ❌ missing | ✗ (port down) |
| naturaltextilewallpaper | **LEAKS** | buy-CTA, main-store-anchor, card-sample-href | `redactVendorSlug` + denylist ✅ | ❌ missing | ✗ (port down) |

**Common pattern (all 3):** the fix `a85eb02` was **half-applied** on each standalone —
`redactVendorSlug()` already scrubs the **canonical / `/p/` link slug / displayed SKU**,
but the **buy/sample CTA**, the **"View on Designer Wallcoverings" anchor**, and the
**product-card sample link** still emit the **RAW `p.handle`** straight into the DOM.
None of the three has the `/buy/:slug` internal-redirect route that `a85eb02` relies on.

**No autonomous patch made — deploy is Steve-gated** (customer-facing + vendor
confidentiality). Fix details below; ready-to-run version queued at
`~/.claude/yolo-queue/pending-approval/standalone-vendor-href-leak-2026-05-31.md`.

---

## 1) customdigitalmurals

`/Users/stevestudio2/Projects/customdigitalmurals/server.js`

**Verdict:** leaks-via-buy-CTA + main-store-anchor + card-sample-href.
**Data confirmation:** 737 / 3663 handles (20%) carry a denylist vendor token
(versace, arte, clarke, kravet, romo, thibaut, schumacher, …).

**Clean vectors:** JSON-LD `url` (server.js:750) + sitemap `<loc>` (server.js:273) +
all `/product/` card/rail link slugs (188, 199, 630, 698) use `redactVendorSlug(p.handle)`.
No `<link rel="canonical">` tag emitted at all (only JSON-LD url, which is scrubbed).

**Leaks (raw `p.handle`):**
- **server.js:725, 762** — PDP buy CTA:
  ```js
  const sampleUrl = `${DW_BASE}/products/${p.handle}#sample`;          // L725
  <a class="sample-cta" href="${escHtml(sampleUrl)}" ...>Order Memo Sample (free)</a>  // L762
  ```
- **server.js:726, 763** — PDP "View on Designer Wallcoverings" anchor:
  ```js
  const dwUrl = `${DW_BASE}/products/${p.handle}`;                     // L726
  <a class="dw-link" href="${escHtml(dwUrl)}" ...>View on Designer Wallcoverings →</a>  // L763
  ```
- **server.js:636** — product-card sample link (client JS), uses raw `p.handle`
  even though `p.slug` (redacted) is on the same object:
  ```js
  '<a class="card-sample" href="'+esc('https://designerwallcoverings.com/products/'+p.handle+'#sample')+'" ...>Order Free Sample</a>'
  ```

**Denylist note:** `redactVendorSlug` correctly strips hyphen-delimited vendor tokens.
One residual edge case `dwta-65212designerwallcoverings-los-angeles` (glued, no `-` before
"designerwallcoverings" so the `(^|-)` anchor can't match) — 1 product, DW's own name, low
priority. Bare `york`/`brewster`/`fabricut`/`stout`/`knoll`/`designtex` not in denylist but
not currently leaking in data.

---

## 2) carmelwallpapers

`/Users/stevestudio2/Projects/carmelwallpapers/server.js`

**Verdict:** leaks-via-buy-CTA + "View Full Product" anchor + card-sample-href.
**Data confirmation:** 26 / 2182 products carry a denylist token in `handle` →
flows into `product_url` + `sample_url` (e.g. `agra-okra-arte-international`,
`mediterranean-texture-red-wallcovering-versace`,
`highland-wonder-lagoon-wp-clarke-and-clarke`).

**Clean vectors:** `/p/:handle` route URL + card/rail links use `p.slug` (redacted)
(server.js:630, 706). Card titles have 0 vendor tokens; vendor label hardcoded
"Designer Wallcoverings"; no `data-handle`/`data-sku` attrs. No `<link rel="canonical">`
anywhere → canonical vector N/A.

**Leaks (raw handle via `p.sample_url` / `p.product_url`, built in `shapeForSite`
server.js:63,67; source `scripts/pull-from-shopify.js:92-93`):**
- **server.js:428** — PDP buy CTA: `<a href="${p.sample_url}" class="btn btn-sample" ...>`
  (`sample_url` = `…/products/<RAW-handle>#sample`)
- **server.js:429** — PDP "View Full Product": `<a href="${p.product_url}" class="btn btn-full" ...>`
  (`product_url` = `…/products/<RAW-handle>`)
- **server.js:640** — catalog card `.card-sample` anchor (client-rendered raw `sample_url`)

**Denylist note:** missing `york`/`brewster`/`designtex`/`fabricut`/`stout`/`hygge`
(none currently in data). Also serializes raw `product_url`/`sample_url` into the
`/api/products` JSON payload — optional hardening to stop shipping those to the browser.

---

## 3) naturaltextilewallpaper

`/Users/stevestudio2/Projects/naturaltextilewallpaper/server.js`

**Verdict:** leaks-via-buy-CTA + main-store-anchor (canonical + card-title clean).
**Data confirmation:** 4166 products; 98 handles carry a denylisted-or-required vendor
token (versace, arte, …, plus `dig-326251-designerwallcoverings-com`).

**Clean vectors:** `/p/` href uses `p.slug||p.handle` (slug scrubbed at L250); card
title is display `title`; PDP SKU spec (L767) runs through `redactVendorSlug`. No
`data-handle`/`data-sku`. No `<link rel="canonical">` in `<head>` → canonical N/A
(also means no self-canonical SEO tag — separate non-security note). `public/corner-nav.js`
clean.

**Leaks (raw `p.handle`):**
- **server.js:648** — grid card "Order Memo Sample" CTA (client template):
  ```js
  + '<a href="https://designerwallcoverings.com/products/'+p.handle+'#sample" class="sample-cta" ...>Order Memo Sample (free)</a>'
  ```
- **server.js:738-739, 760** — PDP primary CTA:
  ```js
  const dw_url = `${DW}/products/${p.handle}`;
  const sample_url = `${dw_url}#sample`;
  ...
  <a href="${sample_url}" class="btn-primary" ...>Order Memo Sample (free)</a>
  ```
- **server.js:761** — PDP "View on Designer Wallcoverings": `<a href="${dw_url}" class="btn-secondary" ...>`

**Denylist GAP (compounds it):** `VENDORS_DENYLIST` (server.js:39-50) is **missing**
`cowtan`, `designtex`, `knoll`, `brewster`, `york`, `fabricut`, `stout`, `hygge`,
**`designerwallcoverings`** — so even the scrubbed surfaces (slug/SKU) leak these.
Confirmed: handle `dig-326251-designerwallcoverings-com` is NOT stripped.

---

## Fix pattern (mirror a85eb02) — applies to ALL three

Each standalone needs the SAME three-part change:

### A. Add a `/buy/:slug` internal redirect route
Resolves a redacted slug back to the real handle **server-side** and 302s to the main
store — raw handle lives only in the 3xx `Location` header, never in DOM. Insert near
each project's existing `/p/:handle` (or `/product/`) route:
```js
app.get('/buy/:slug', (req, res) => {
  const key = req.params.slug;
  const p = PRODUCTS.find(x => x.handle === key)
         || PRODUCTS.find(x => redactVendorSlug(x.handle) === key);
  if (!p) return res.status(404).send(page404());
  const dest = `${DW}/products/${p.handle}`;          // carmel: p.product_url || `${DW}/...`
  const frag = req.query.sample != null ? '#sample' : '';
  res.redirect(302, dest + frag);
});
```

### B. Point the PDP buy CTA + main-store anchor at `/buy/:slug`
- **customdigitalmurals** L725-726: `sampleUrl` → `/buy/${redactVendorSlug(p.handle)}?sample=1`;
  `dwUrl` → `/buy/${redactVendorSlug(p.handle)}`
- **carmelwallpapers** L428-429: `${p.sample_url}` → `/buy/${p.slug}?sample`;
  `${p.product_url}` → `/buy/${p.slug}`
- **naturaltextilewallpaper** L738-739/760-761:
  `const buySlug = encodeURIComponent(redactVendorSlug(p.handle));`
  `dw_url = /buy/${buySlug}`, `sample_url = /buy/${buySlug}?sample=1`

### C. Fix the client-rendered product-card sample link
- **customdigitalmurals** L636 → `'/buy/'+(p.slug||p.handle)+'?sample=1'`
- **carmelwallpapers** L640 → `/buy/${p.slug||p.handle}?sample`
- **naturaltextilewallpaper** L648 → `'/buy/'+encodeURIComponent(p.slug||p.handle)+'?sample=1'`

### D. (naturaltextilewallpaper + optionally cdm/carmel) close the denylist gap
Add to `VENDORS_DENYLIST`: `cowtan, designtex, knoll, brewster, york, fabricut, stout,
hygge, designerwallcoverings`.

### Post-deploy verification (per site, after Steve approves + deploys)
`curl` `/api/products` + one `/p/<slug>` (PDP) and grep rendered DOM for `products/`
and each vendor token — expect **0** raw-handle hits. (Note: all three ports were down
during this audit so only static + `data/products.json` verification was possible; a
live DOM grep is the confirmation step.)

---

## Hard-rule compliance
- READ-ONLY: no render/server code edited, nothing deployed, no pm2 restart, no server started.
- Leaks REPORTED with file:line + proposed fix; NOT patched.
- Standalone deploys = Steve-gated (customer-facing + vendor-confidentiality) → queued for morning review.
- No `/dtd` needed: the verdict is factual (leaks found by static analysis + data confirmation),
  the fix is a direct mirror of the already-decided `a85eb02` pattern, and execution is gated
  rather than a judgment fork.