[object Object]

← back to Dw Signup Fulfillment

TK-11120: corrected resend tool + apology template (retail-verify localhost outage)

a6edf22ca5d62b163a5a5ee5de51cc42335413a0 · 2026-09-02 13:55:00 -0700 · Steve Abrams

verification/tk11120/resend-corrected.js — safe corrected resender for the 75:
- HARD-REQUIRES PUBLIC_URL (fail-closed baseUrl) + GEORGE_ACCOUNT=info; aborts otherwise
- bypasses the tainted sample_verify_sent flag/old ledger (broken run set them); own ledger
- email.verifyResendEmail: verify letter + one-line apology; DRY_RUN default, --apply gate
- affected-emails.txt: the 75 deduped recipients (Angie excluded)

Proven locally: dry-run builds https link, info@ sender, abort on empty PUBLIC_URL. No send.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017j4qS38tWq21qYdjxMcFTy

Files touched

Diff

commit a6edf22ca5d62b163a5a5ee5de51cc42335413a0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 2 13:55:00 2026 -0700

    TK-11120: corrected resend tool + apology template (retail-verify localhost outage)
    
    verification/tk11120/resend-corrected.js — safe corrected resender for the 75:
    - HARD-REQUIRES PUBLIC_URL (fail-closed baseUrl) + GEORGE_ACCOUNT=info; aborts otherwise
    - bypasses the tainted sample_verify_sent flag/old ledger (broken run set them); own ledger
    - email.verifyResendEmail: verify letter + one-line apology; DRY_RUN default, --apply gate
    - affected-emails.txt: the 75 deduped recipients (Angie excluded)
    
    Proven locally: dry-run builds https link, info@ sender, abort on empty PUBLIC_URL. No send.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_017j4qS38tWq21qYdjxMcFTy
---
 lib/email.js                             | 30 ++++++++++++-
 verification/tk11120/affected-emails.txt | 75 ++++++++++++++++++++++++++++++++
 verification/tk11120/resend-corrected.js | 73 +++++++++++++++++++++++++++++++
 3 files changed, 177 insertions(+), 1 deletion(-)

diff --git a/lib/email.js b/lib/email.js
index d215204..ae6ceac 100644
--- a/lib/email.js
+++ b/lib/email.js
@@ -197,6 +197,34 @@ function verifyEmail({ firstName, url, count }) {
   return { subject, html };
 }
 
+// RESEND of the verify letter after the 2026-09-02 dead-localhost-link incident
+// (TK-11120). Identical to verifyEmail but with a short apology line up top, since some
+// recipients replied confused that the earlier link didn't work.
+function verifyResendEmail({ firstName, url, count }) {
+  const fn = firstName ? String(firstName).trim() : '';
+  const greet = fn ? esc(fn.charAt(0).toUpperCase() + fn.slice(1)) : 'there';
+  const subject = `Here's your working link — your ${count} complimentary samples`;
+  const html = `<div style="font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:600px;margin:0 auto">
+  <div style="border:1px solid #e2ddd4;border-radius:10px;overflow:hidden">
+    <div style="background:#1a1a1a;color:#fff;text-align:center;padding:26px 20px">
+      <div style="font-size:22px;letter-spacing:3px;font-weight:600">DESIGNER WALLCOVERINGS</div>
+      <div style="font-size:11px;letter-spacing:2px;color:#b8afa2;margin-top:4px">FINE WALLCOVERINGS &amp; FABRICS</div>
+    </div>
+    <div style="padding:26px 28px;font-size:15px;line-height:1.7;color:#2a2a2a">
+      <p style="margin:0 0 14px">Dear ${greet},</p>
+      <p style="margin:0 0 14px;color:#8a6d1f;background:rgba(212,160,74,0.10);border-left:3px solid #d4a04a;padding:10px 14px;border-radius:2px">Our earlier confirmation link didn't work — apologies for that. Here's the correct one:</p>
+      <p style="margin:0 0 8px">Your account includes <b>${count} lifetime complimentary samples</b>. Confirm this email, then sign in with the same address when you shop:</p>
+      <div style="text-align:center;margin:22px 0 8px">
+        <a href="${esc(url)}" style="background:#1a1a1a;color:#fff;text-decoration:none;padding:13px 34px;border-radius:30px;font-size:14px;letter-spacing:1px;display:inline-block">Confirm my email</a>
+      </div>
+      <p style="margin:14px 0 0;color:#555;font-size:13px">When signed in, up to your remaining lifetime allowance shows free automatically at checkout—no code needed. Approved design professionals receive unlimited complimentary samples. This confirmation link expires in a few days. If you didn't create an account, ignore this email.</p>
+      <p style="margin:20px 0 0;color:#2a2a2a">Warmly,<br><b>The Designer Wallcoverings Team</b></p>
+    </div>
+  </div>
+</div>`;
+  return { subject, html };
+}
+
 // Confirmation letter sent after a successful /verify — the reward is now live.
 function samplesUnlockedEmail({ firstName, count }) {
   const fn = firstName ? String(firstName).trim() : '';
@@ -305,4 +333,4 @@ function tradeApplicationEmail({ app, approveUrl, rejectUrl, adminUrl }) {
 function money(v) { return `$${Number(v).toFixed(2)}`; }
 function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); }
 
-module.exports = { sendEmail, verifyEmail, samplesUnlockedEmail, retailCodeEmail, retailGiftEmail, designerWelcomeEmail, repNotifyEmail, tradeApprovedEmail, tradeRejectedEmail, tradeApplicationEmail, money, esc };
+module.exports = { sendEmail, verifyEmail, verifyResendEmail, samplesUnlockedEmail, retailCodeEmail, retailGiftEmail, designerWelcomeEmail, repNotifyEmail, tradeApprovedEmail, tradeRejectedEmail, tradeApplicationEmail, money, esc };
diff --git a/verification/tk11120/affected-emails.txt b/verification/tk11120/affected-emails.txt
new file mode 100644
index 0000000..468835f
--- /dev/null
+++ b/verification/tk11120/affected-emails.txt
@@ -0,0 +1,75 @@
+alexroudinski@gmail.com
+amyalley1@gmail.com
+annabaynecarouge@gmail.com
+anne.bufordhq@icloud.com
+asht97@aol.com
+astrassel@berwind.com
+beewitched392@gmail.com
+bkarsen4@gmail.com
+bwbd57@gmail.com
+c.stephenson987@outlook.com
+cassie.carlin03@gmail.com
+chelle.fowler@gmail.com
+chris.brezsko@gmail.com
+chris@brooklynfabrication.com
+coatesrx@gmail.com
+cursedjerome@gmail.com
+dbrancati@emergencymdsc.com
+denigeni@outlook.com
+denisestevensonwood@gmail.com
+designteam@gruvercooley.com
+dessasurratt@gmail.com
+emmadykstra15@gmail.com
+eviesmama@yahoo.com
+frontrangewallpaper@gmail.com
+fss5901@aol.com
+garthee@gmail.com
+h.yoshida@mygg.it
+info@adinainteriordesign.com
+info@drycreek.com
+info@loulineinteriors.com
+info@projectluna.de
+jenniferbray214@gmail.com
+jenniferhogan00@yahoo.com
+jill@jilltabor.com
+jlhguy18@gmail.com
+jolainajones@me.com
+jonesfamily2002@verizon.net
+julieguttman2@gmail.com
+karynmouser@gmail.com
+katefiello@gmail.com
+kchlex00@msn.com
+kim@philanthropy.solutions
+lindahleff18@gmail.com
+lizzy@lizzyswicknutrition.com
+lotomimi24@gmail.com
+maccambo4@gmail.com
+maurakessler97@gmail.com
+mck919@aol.com
+michellemooresrq@gmail.com
+mmatthiasfl@gmail.com
+mocowling@gmail.com
+monica.alvarez@gmail.com
+morganld85@gmail.com
+mspalomita@gmail.com
+mwithat@aol.com
+nicole@jacquesmariemage.com
+nmarcoux56@gmail.com
+pamallabondy@gmail.com
+paul.millan@live.co.uk
+pjbennett11@yahoo.com
+shaheenkhosla@gmail.com
+shotcha@gmail.com
+suruchee@polishednest.ca
+talenedw@gmail.com
+talliemac@gmail.com
+taudunoyer@gmail.com
+terrivassar@gmail.com
+thahzeenkhalid@gmail.com
+tiaalford@outlook.com
+tim@timothydeclue.com
+vblack@lasirenapb.com
+vickideur@gmail.com
+wendykramell@yahoo.com
+winniecwtse@gmail.com
+yeoryiayinsella@gmail.com
diff --git a/verification/tk11120/resend-corrected.js b/verification/tk11120/resend-corrected.js
new file mode 100644
index 0000000..ad9b4a3
--- /dev/null
+++ b/verification/tk11120/resend-corrected.js
@@ -0,0 +1,73 @@
+'use strict';
+// TK-11120 — CORRECTED resend of the retail-verify letter to the ~75 customers who got a
+// DEAD http://127.0.0.1:9856 link on 2026-09-02 (the backfill job ran with PUBLIC_URL
+// unset). This sender is safe-by-design and specifically works around the two things that
+// would otherwise break a resend:
+//   1) It HARD-REQUIRES config.PUBLIC_URL (the fail-closed baseUrl now returns '' without
+//      it) — so it can never re-ship a localhost link.
+//   2) It BYPASSES the tainted `custom.sample_verify_sent=true` flag + the old backfill
+//      ledger (those were set on the broken run), because every one of these customers
+//      still needs a WORKING link. Idempotency is instead tracked in this script's OWN
+//      ledger so re-runs never double-send.
+// Sender identity: requires GEORGE_ACCOUNT=info so mail is genuinely from info@.
+// Copy: email.verifyResendEmail (adds a one-line apology).
+//
+// Modes:
+//   node resend-corrected.js                      -> DRY_RUN preview of the whole list (no send)
+//   node resend-corrected.js --only <email>       -> just that address (works for a test addr)
+//   node resend-corrected.js --limit N            -> first N of the list
+//   DRY_RUN=0 ... --apply                          -> actually send (both required)
+const fs = require('fs');
+const path = require('path');
+const shopify = require(path.join(__dirname, '..', '..', 'lib', 'shopify'));
+const verify = require(path.join(__dirname, '..', '..', 'lib', 'verify'));
+const email = require(path.join(__dirname, '..', '..', 'lib', 'email'));
+const config = require(path.join(__dirname, '..', '..', 'lib', 'config'));
+
+const args = process.argv.slice(2);
+const APPLY = args.includes('--apply');
+const ONLY = args.includes('--only') ? args[args.indexOf('--only') + 1] : null;
+const LIMIT = args.includes('--limit') ? parseInt(args[args.indexOf('--limit') + 1], 10) : Infinity;
+const LISTP = path.join(__dirname, 'affected-emails.txt');
+const LEDGER = path.join(__dirname, 'tk11120-resend-ledger.jsonl');
+
+function ledgerDone() { const s = new Set(); try { for (const l of fs.readFileSync(LEDGER, 'utf8').split('\n')) { if (!l.trim()) continue; const r = JSON.parse(l); if (r.ok) s.add(String(r.email).toLowerCase()); } } catch {} return s; }
+function ledgerAppend(row) { fs.appendFileSync(LEDGER, JSON.stringify(row) + '\n'); }
+function mask(e) { return String(e).replace(/(.{2}).*@/, '$1***@'); }
+
+(async () => {
+  // ---- HARD PRE-FLIGHT: never re-ship a bad link or wrong sender ----
+  if (!config.PUBLIC_URL) { console.error('ABORT: PUBLIC_URL is empty — refusing to resend (would ship a dead localhost link). Set PUBLIC_URL=https://signup.designerwallcoverings.com'); process.exit(2); }
+  if (config.GEORGE_ACCOUNT !== 'info') { console.error(`ABORT: GEORGE_ACCOUNT='${config.GEORGE_ACCOUNT}', expected 'info' (Steve directive — send from info@).`); process.exit(2); }
+  const base = verify.baseUrl();
+  if (!base || /127\.0\.0\.1|localhost/.test(base)) { console.error(`ABORT: baseUrl() resolved to '${base}' — not a public https host.`); process.exit(2); }
+
+  let list = ONLY ? [ONLY] : fs.readFileSync(LISTP, 'utf8').split('\n').map(s => s.trim()).filter(Boolean);
+  const done = ledgerDone();
+  console.log(`resend-corrected: ${list.length} target(s) · base=${base} · account=${config.GEORGE_ACCOUNT} · from=${config.GEORGE_FROM} · mode=${config.DRY_RUN ? 'DRY_RUN' : 'LIVE'} · apply=${APPLY} · only=${ONLY || '-'}`);
+  if (!config.DRY_RUN && !APPLY) console.log('LIVE env but no --apply → will only PREVIEW. Add --apply to actually send.');
+  let n = 0, sent = 0, skipped = 0, failed = 0;
+  for (const addr0 of list) {
+    if (n >= LIMIT) break;
+    const addr = String(addr0).trim().toLowerCase();
+    if (!addr) continue;
+    if (done.has(addr)) { skipped++; console.log(`  [skip] ${mask(addr)} already resent (ledger)`); continue; }
+    n++;
+    // Look up the Shopify customer for id + first name (best-effort; resend regardless).
+    let customerId = null, firstName = '';
+    try {
+      customerId = await shopify.findCustomerByEmail(addr);
+      if (customerId) { const g = await shopify.getCustomer(customerId); const c = g && g.json && g.json.customer; if (c) firstName = c.first_name || ''; }
+    } catch (e) { /* non-fatal — resend with email-prefix greeting */ }
+    const token = verify.mintToken({ email: addr, customerId });
+    if (!token) { failed++; ledgerAppend({ email: addr, ok: false, reason: 'no_token(secret?)', ts: new Date().toISOString() }); console.log(`  [FAIL] ${mask(addr)} could not mint token (VERIFY_SECRET?)`); continue; }
+    const url = `${base}/verify?token=${encodeURIComponent(token)}`;
+    const tpl = email.verifyResendEmail({ firstName: firstName || addr.split('@')[0], url, count: config.FREE_SAMPLE_COUNT });
+    if (config.DRY_RUN || !APPLY) { console.log(`  [would-send] ${mask(addr)} id=${customerId || '-'} host_ok=${url.startsWith(base)}`); continue; }
+    const r = await email.sendEmail({ to: addr, subject: tpl.subject, html: tpl.html, source: 'retail-verify-resend' });
+    if (r && r.ok !== false) { sent++; ledgerAppend({ email: addr, ok: true, customerId: customerId || null, ts: new Date().toISOString() }); console.log(`  [sent] ${mask(addr)} status=${r.status != null ? r.status : (r.dryRun ? 'dry' : '?')}`); }
+    else { failed++; ledgerAppend({ email: addr, ok: false, reason: (r && (r.errorCode || r.error || r.status)) || 'unknown', ts: new Date().toISOString() }); console.log(`  [FAIL] ${mask(addr)} status=${r && r.status} err=${r && (r.errorCode || r.error)}`); }
+    await new Promise(res => setTimeout(res, 500)); // gentle pacing for George
+  }
+  console.log(`DONE: targeted=${n} sent=${sent} skipped=${skipped} failed=${failed}`);
+})().catch(e => { console.error('ERR', e.message); process.exit(1); });

← 0016d65 TK-11120: verify links fail-closed on missing PUBLIC_URL + s  ·  back to Dw Signup Fulfillment  ·  TK-11120: server.js baseUrl() also fails closed on missing P 52afecd →