[object Object]

← back to Dw Signup Fulfillment

TK-11114: reconcile deploy paste — supersede stale webhook FIX-PASTE with verified BLOCK A (file-sync + clean-env restart)

7963d0a4fc96432f6e4286708b6b04edbf9e9fb7 · 2026-09-02 11:37:07 -0700 · Steve Abrams

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

Files touched

Diff

commit 7963d0a4fc96432f6e4286708b6b04edbf9e9fb7
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 2 11:37:07 2026 -0700

    TK-11114: reconcile deploy paste — supersede stale webhook FIX-PASTE with verified BLOCK A (file-sync + clean-env restart)
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01WzrHtgk5Qq7kpdfaWeDk97
---
 apply-verified-tag.sh                             |  32 +++++
 arm-webhook-env.sh                                | 165 ++++++++++++++++++++++
 arm-webhook.sh                                    |  18 +++
 claim-kelly.sh                                    |   9 ++
 create-kelly-customer.sh                          |  14 ++
 refresh-webhook.sh                                |  11 ++
 send-kelly-reply.js                               |  27 ++++
 verification/tk11114-remediation/BLOCK-A-PASTE.md |  41 ++++++
 verification/tk11114/FIX-PASTE.md                 |  28 ++++
 9 files changed, 345 insertions(+)

diff --git a/apply-verified-tag.sh b/apply-verified-tag.sh
new file mode 100644
index 0000000..5aff861
--- /dev/null
+++ b/apply-verified-tag.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+# TK-10830 — apply the verified-sample entitlement tag to Kelly's EXISTING customer
+# (id 8388564123699). This is the end-state the verify-click produces (completeVerification
+# -> addTags), applied directly since Shopify's search index is blind to this record so the
+# /claim + verify flow can't resolve it. Idempotent (merges, never drops existing tags).
+set -euo pipefail
+CID=8388564123699
+FT="$(grep -E '^SHOPIFY_FULFILLMENT_TOKEN=' "$HOME/Projects/secrets-manager/.env" | cut -d= -f2-)"
+SHOP="designer-laboratory-sandbox.myshopify.com"
+BASE="https://$SHOP/admin/api/2024-10"
+
+cur="$(curl -s "$BASE/customers/$CID.json" -H "X-Shopify-Access-Token: $FT")"
+oldtags="$(echo "$cur" | python3 -c "import sys,json;print(json.load(sys.stdin).get('customer',{}).get('tags',''))")"
+echo "current tags: [$oldtags]"
+
+# merge desired tags into existing
+newtags="$(python3 - "$oldtags" <<'PY'
+import sys
+have=[t.strip() for t in sys.argv[1].split(',') if t.strip()]
+want=['verified-sample','zopim-referral','tk-10830']
+for w in want:
+    if w.lower() not in [h.lower() for h in have]: have.append(w)
+print(', '.join(have))
+PY
+)"
+echo "new tags:     [$newtags]"
+
+printf '{"customer":{"id":%s,"tags":"%s"}}' "$CID" "$newtags" > /tmp/kelly-tag-update.json
+resp="$(curl -s -X PUT "$BASE/customers/$CID.json" -H "X-Shopify-Access-Token: $FT" -H "Content-Type: application/json" --data @/tmp/kelly-tag-update.json)"
+echo "$resp" | python3 -c "import sys,json
+c=json.load(sys.stdin).get('customer')
+print('UPDATED customer',c['id'],'tags now:',repr(c.get('tags'))) if c else print('FAIL:',sys.stdin.read()[:300])"
diff --git a/arm-webhook-env.sh b/arm-webhook-env.sh
new file mode 100755
index 0000000..70b2c91
--- /dev/null
+++ b/arm-webhook-env.sh
@@ -0,0 +1,165 @@
+#!/usr/bin/env bash
+# TK-11114 — Option 1: arm the LIVE dw-signup-fulfillment service env so its
+# WEBHOOK_URL_TOKEN matches the token the EXISTING customers/create webhook
+# already carries. It does NOT register/delete/modify any webhook (no new scope,
+# no duplicate-webhook risk — the opposite end from refresh-webhook.sh/Option 2).
+#
+# WHY IT MATTERS
+#   server.js webhookAuth (~L72): if config.WEBHOOK_URL_TOKEN is set and the path
+#   token != it  -> 401; if it is UNSET while live -> 503. Either way the signup
+#   callback is rejected and no verify letter is sent.
+#   config.js resolves WEBHOOK_URL_TOKEN with firstEnv order:
+#       process.env  ->  <project>/.env  ->  ~/Projects/secrets-manager/.env
+#   The pm2 ecosystem env does NOT set this key, so the highest-priority live
+#   source is the PROJECT .env. This script sets it there (and .env is never
+#   rewritten by deploy.sh), so the running service resolves the webhook's token.
+#
+# SAFE BY CONSTRUCTION
+#   * DRY-RUN by default — prints the plan and exits. Mutates only with --apply.
+#   * Self-verifying + idempotent — if the live service ALREADY resolves the
+#     webhook's token it reports NO-OP and exits 0 without changing anything.
+#     (This is what resolves the "is the token actually mismatched?" question on
+#     prod, where the truth lives — Mac2 is classifier-blocked from prod SSH.)
+#   * Reversible — timestamped .env backup before any write; rollback printed.
+#   * Never touches DRY_RUN (go-live state) or any other unrelated env key.
+#
+# GATED — this changes the LIVE customer-facing service (env + pm2 restart). Run
+# on the Kamatera prod box, pasted by Steve (Mac2 is blocked from prod SSH):
+#   ! ssh root@45.61.58.125 'cd /root/Projects/dw-signup-fulfillment && git pull -q && bash arm-webhook-env.sh'         # dry-run first
+#   ! ssh root@45.61.58.125 'cd /root/Projects/dw-signup-fulfillment && bash arm-webhook-env.sh --apply'                # then apply
+set -euo pipefail
+
+APPLY=0
+for a in "$@"; do [ "$a" = "--apply" ] && APPLY=1; done
+
+# --- locate project + inputs (portable: Mac2 steve / Kamatera root) ---------
+SELF="${BASH_SOURCE[0]:-}"
+if [ -n "$SELF" ] && [ -f "$SELF" ]; then PROJECT_DIR="$(cd "$(dirname "$SELF")" && pwd)"
+elif [ -d /root/Projects/dw-signup-fulfillment ]; then PROJECT_DIR=/root/Projects/dw-signup-fulfillment
+else PROJECT_DIR="$PWD"; fi
+ENV_FILE="$PROJECT_DIR/.env"
+SECRETS="$HOME/Projects/secrets-manager/.env"
+SHOP="designer-laboratory-sandbox.myshopify.com"
+BASE="https://$SHOP/admin/api/2024-10"
+PM2_APP="dw-signup-fulfillment"
+PORT="9862"
+
+mask(){ local t="${1:-}"; if [ "${#t}" -gt 6 ]; then printf '…%s' "${t: -6}"; else printf '%s' "$t"; fi; }
+# readers: always succeed (empty on miss) so `set -e` doesn't kill an absent-key lookup
+sget(){ { grep -E "^$1=" "$SECRETS" 2>/dev/null || true; } | head -1 | cut -d= -f2- | sed -E "s/^[\"']|[\"']\$//g"; }
+eget(){ { grep -E "^$1=" "$2" 2>/dev/null || true; } | head -1 | cut -d= -f2- | sed -E "s/^[\"']|[\"']\$//g"; }
+
+# --- required secrets (task item 1: read VERIFY_SECRET + fulfillment token) --
+FT="$(sget SHOPIFY_FULFILLMENT_TOKEN)"
+VS="$(sget DW_SIGNUP_VERIFY_SECRET)"   # config.js reads VERIFY_SECRET from this key
+[ -z "$FT" ] && { echo "ABORT: SHOPIFY_FULFILLMENT_TOKEN not found in $SECRETS"; exit 1; }
+[ -z "$VS" ] && { echo "ABORT: DW_SIGNUP_VERIFY_SECRET not found in $SECRETS"; exit 1; }
+
+# --- read the token the EXISTING webhook carries (the source of truth) -------
+WH_JSON="$(curl -fsS "$BASE/webhooks.json?topic=customers/create" -H "X-Shopify-Access-Token: $FT")" \
+  || { echo "ABORT: could not read webhooks from Shopify (check SHOPIFY_FULFILLMENT_TOKEN scopes)"; exit 1; }
+
+PARSE="$(printf '%s' "$WH_JSON" | python3 -c "
+import sys,json
+whs=[w for w in json.load(sys.stdin).get('webhooks',[]) if '/webhooks/customers/create/' in (w.get('address') or '')]
+if not whs:
+    print('NONE NONE 0'); sys.exit(0)
+if len(whs) > 1:
+    print('MULTI NONE %d' % len(whs)); sys.exit(0)
+w=whs[0]; tok=(w.get('address') or '').rstrip('/').split('/')[-1]
+print(w.get('id'), tok, 1)
+")"
+read -r WH_ID WH_TOKEN WH_N <<<"$PARSE"
+
+if [ "$WH_ID" = "NONE" ]; then
+  echo "ABORT: no customers/create webhook is registered. Option 1 has nothing to match."
+  echo "       (This is the 'do NOT register a webhook' case flagged by the peer session;"
+  echo "        registering one is Option 2 / a separate gated decision — see refresh-webhook.sh.)"
+  exit 1
+fi
+if [ "$WH_ID" = "MULTI" ]; then
+  echo "ABORT: $WH_N customers/create webhooks are registered (duplicate). Option 1 can only match ONE token."
+  echo "       Resolve the duplicates first (delete the stale one), then re-run."
+  exit 1
+fi
+
+# --- what the SERVICE resolves right now -------------------------------------
+CUR_ENV="$(eget WEBHOOK_URL_TOKEN "$ENV_FILE")"
+CUR_SECR="$(sget WEBHOOK_URL_TOKEN)"
+if [ -n "$CUR_ENV" ]; then CUR="$CUR_ENV"; SRC="$ENV_FILE"
+elif [ -n "$CUR_SECR" ]; then CUR="$CUR_SECR"; SRC="$SECRETS"
+else CUR=""; SRC="(unset)"; fi
+
+echo "project dir           : $PROJECT_DIR"
+echo "webhook id            : $WH_ID"
+echo "webhook token         : $(mask "$WH_TOKEN")  (len ${#WH_TOKEN})"
+echo "service resolves      : $(mask "$CUR")  from $SRC"
+echo "secrets-manager holds : $(mask "$CUR_SECR")"
+
+if [ "$CUR" = "$WH_TOKEN" ]; then
+  echo
+  echo "RESULT: NO-OP — the live service already resolves the webhook's token."
+  echo "        There is no token mismatch; TK-11114's signup failure (if any) is NOT the"
+  echo "        webhook token — look at the SEND side (service->George). No env change made."
+  exit 0
+fi
+
+echo
+echo "RESULT: MISMATCH — service uses $(mask "$CUR"), webhook sends $(mask "$WH_TOKEN")."
+echo "        Option 1 fix = set WEBHOOK_URL_TOKEN=<webhook token> in $ENV_FILE"
+if [ "$APPLY" != "1" ]; then
+  echo
+  echo "(dry-run) re-run with --apply to write the env, restart pm2, and verify. Nothing changed."
+  exit 0
+fi
+
+# --- APPLY (reversible) ------------------------------------------------------
+TS="$(date +%Y%m%dT%H%M%S)"
+if [ -f "$ENV_FILE" ]; then
+  cp "$ENV_FILE" "$ENV_FILE.tk11114.bak.$TS"; echo "backup: $ENV_FILE.tk11114.bak.$TS"
+else
+  : > "$ENV_FILE"; echo "created $ENV_FILE"
+fi
+
+upsert(){ # upsert KEY VALUE into $ENV_FILE (idempotent, value-safe via python)
+  local k="$1" v="$2"
+  if grep -qE "^$k=" "$ENV_FILE"; then
+    python3 - "$ENV_FILE" "$k" "$v" <<'PY'
+import sys,re
+f,k,v=sys.argv[1],sys.argv[2],sys.argv[3]
+lines=open(f).read().splitlines()
+out=[(k+'='+v) if re.match('^'+re.escape(k)+'=',l) else l for l in lines]
+open(f,'w').write('\n'.join(out)+'\n')
+PY
+  else
+    printf '%s=%s\n' "$k" "$v" >> "$ENV_FILE"
+  fi
+}
+
+upsert WEBHOOK_URL_TOKEN "$WH_TOKEN"
+# ensure the rest of the verify path is present, but NEVER clobber an existing value
+grep -qE '^DW_SIGNUP_VERIFY_SECRET=' "$ENV_FILE"    || upsert DW_SIGNUP_VERIFY_SECRET "$VS"
+grep -qE '^SHOPIFY_FULFILLMENT_TOKEN=' "$ENV_FILE"  || upsert SHOPIFY_FULFILLMENT_TOKEN "$FT"
+chmod 600 "$ENV_FILE"
+echo "wrote WEBHOOK_URL_TOKEN=$(mask "$WH_TOKEN") to $ENV_FILE"
+
+echo "restarting pm2 $PM2_APP (re-reads .env at boot)…"
+pm2 restart "$PM2_APP" >/dev/null 2>&1 || echo "WARN: 'pm2 restart $PM2_APP' failed — restart it manually."
+
+sleep 2
+echo "--- verify (no handler side effects) ---"
+curl -s -o /dev/null -w "  /healthz            -> %{http_code} (expect 200)\n" "http://127.0.0.1:$PORT/healthz" || true
+curl -s -o /dev/null -w "  bad-token POST      -> %{http_code} (expect 401 = token now enforced, not 503-unset)\n" \
+  -X POST "http://127.0.0.1:$PORT/webhooks/customers/create/deadbeefdeadbeef" -H 'Content-Type: application/json' -d '{"id":0}' || true
+NOW_ENV="$(eget WEBHOOK_URL_TOKEN "$ENV_FILE")"
+[ "$NOW_ENV" = "$WH_TOKEN" ] && echo "  .env now resolves    -> $(mask "$NOW_ENV") == webhook token ✓" \
+                             || echo "  .env now resolves    -> $(mask "$NOW_ENV") (UNEXPECTED — investigate)"
+
+echo
+echo "ROLLBACK:"
+if [ -f "$ENV_FILE.tk11114.bak.$TS" ]; then
+  echo "  cp '$ENV_FILE.tk11114.bak.$TS' '$ENV_FILE' && pm2 restart $PM2_APP"
+else
+  echo "  (no prior .env) remove the WEBHOOK_URL_TOKEN line from $ENV_FILE && pm2 restart $PM2_APP"
+fi
+echo "DONE — TK-11114 Option 1 applied."
diff --git a/arm-webhook.sh b/arm-webhook.sh
new file mode 100644
index 0000000..b2f5861
--- /dev/null
+++ b/arm-webhook.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+# TK-11114 — register the customers/create webhook so every DW signup triggers the verify
+# letter. Idempotent by EXISTENCE (skips if any customers/create webhook is registered).
+# NOTE: does NOT check token-match — if a stale webhook with the WRONG token exists, DELETE it
+# first, then run this so it registers one with the current secrets WEBHOOK_URL_TOKEN.
+# Run as Steve:  ! bash ~/Projects/dw-signup-fulfillment/arm-webhook.sh
+set -euo pipefail
+SHOP="designer-laboratory-sandbox.myshopify.com"
+BASE="https://$SHOP/admin/api/2024-10"
+PUBLIC_URL="https://signup.designerwallcoverings.com"
+FT="$(grep -E '^SHOPIFY_FULFILLMENT_TOKEN=' "$HOME/Projects/secrets-manager/.env" | cut -d= -f2-)"
+WT="$(grep -E '^WEBHOOK_URL_TOKEN=' "$HOME/Projects/secrets-manager/.env" | cut -d= -f2-)"
+[ -z "$WT" ] && { echo "ABORT: WEBHOOK_URL_TOKEN not in secrets-manager/.env"; exit 1; }
+existing="$(curl -s "$BASE/webhooks.json?topic=customers/create" -H "X-Shopify-Access-Token: $FT" | python3 -c "import sys,json;print(len(json.load(sys.stdin).get('webhooks',[])))")"
+if [ "$existing" != "0" ]; then echo "Already registered ($existing customers/create webhook). To fix a wrong token, DELETE the stale id first then re-run."; exit 0; fi
+printf '{"webhook":{"topic":"customers/create","address":"%s/webhooks/customers/create/%s","format":"json"}}' "$PUBLIC_URL" "$WT" > /tmp/dw-webhook.json
+curl -s -X POST "$BASE/webhooks.json" -H "X-Shopify-Access-Token: $FT" -H "Content-Type: application/json" --data @/tmp/dw-webhook.json \
+ | python3 -c "import sys,json;w=json.load(sys.stdin).get('webhook');print('REGISTERED id=',w['id']) if w else print('FAIL:',sys.stdin.read()[:400])"
diff --git a/claim-kelly.sh b/claim-kelly.sh
new file mode 100644
index 0000000..93d72d6
--- /dev/null
+++ b/claim-kelly.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+# Step 2 for TK-10830 — send Kelly's verify letter via the LIVE service /claim.
+# Run AFTER step 1 (customer exists) so /claim resolves her customerId and the link works.
+# Run as Steve:  ! bash ~/Projects/dw-signup-fulfillment/claim-kelly.sh
+set -euo pipefail
+curl -s -X POST 'https://signup.designerwallcoverings.com/claim' \
+  -H 'Content-Type: application/json' \
+  --data '{"email":"kcyounge@gmail.com","first_name":"Kelly"}'
+echo
diff --git a/create-kelly-customer.sh b/create-kelly-customer.sh
new file mode 100644
index 0000000..c723f42
--- /dev/null
+++ b/create-kelly-customer.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+# Step 1 for TK-10830 — create Kelly's Shopify customer (fulfillment token has write_customers).
+# Run as Steve:  ! bash ~/Projects/dw-signup-fulfillment/create-kelly-customer.sh
+set -euo pipefail
+DIR="$HOME/Projects/dw-signup-fulfillment"
+FT="$(grep -E '^SHOPIFY_FULFILLMENT_TOKEN=' "$HOME/Projects/secrets-manager/.env" | cut -d= -f2-)"
+SHOP="designer-laboratory-sandbox.myshopify.com"
+resp="$(curl -s -X POST "https://$SHOP/admin/api/2024-10/customers.json" \
+  -H "X-Shopify-Access-Token: $FT" \
+  -H "Content-Type: application/json" \
+  --data @"$DIR/kelly-customer.json")"
+echo "$resp" | python3 -c "import sys,json
+d=json.load(sys.stdin); c=d.get('customer')
+print('CREATED customer id =', c['id'], '| tags:', c.get('tags')) if c else print('FAIL:', json.dumps(d.get('errors', d))[:400])"
diff --git a/refresh-webhook.sh b/refresh-webhook.sh
new file mode 100644
index 0000000..27fcf1d
--- /dev/null
+++ b/refresh-webhook.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+# ⛔ DO NOT RUN — NEUTRALIZED 2026-09-02.
+# This script was written on a WRONG diagnosis (webhook token mismatch). Functional tests
+# proved the OPPOSITE: the live service's WEBHOOK_URL_TOKEN == the REGISTERED webhook token
+# (...44e9c9) — POST with ...44e9c9 -> 200 (accepted), POST with ...f100e1 -> 401 (rejected).
+# The earlier "...f100e1" reading came from the WRONG pid (pgrep|head -1 grabbed another proc).
+# The webhook is CORRECT and WORKING. Deleting + re-registering with the secrets token
+# (...f100e1) would install a token the live service REJECTS -> BREAK the working pipeline.
+echo "⛔ ABORTED: refresh-webhook.sh is neutralized. The webhook is already correct + matched"
+echo "   (live token = ...44e9c9 = the registered one). Re-registering would BREAK it. Do nothing."
+exit 1
diff --git a/send-kelly-reply.js b/send-kelly-reply.js
new file mode 100644
index 0000000..8f1ca04
--- /dev/null
+++ b/send-kelly-reply.js
@@ -0,0 +1,27 @@
+#!/usr/bin/env node
+'use strict';
+// One-shot: send Kelly's TK-10830 reply via George (info@ / steve-office) to the live thread.
+// RUN AFTER: (1) her Shopify customer exists (kcyounge@gmail.com) and (2) /claim has sent the
+// verify letter — so the "we've just sent a confirmation link" line below is TRUE at send time.
+// Usage (Steve, via `!`):  cd ~/Projects/dw-signup-fulfillment && DRY_RUN=0 node send-kelly-reply.js
+// DRY_RUN defaults ON; pass DRY_RUN=0 to actually send. Creds resolve from secrets-manager/.env
+// (GEORGE_AUTH) + george-gmail/.env (GEORGE_EXTERNAL_SEND_TOKEN) via lib/config.js.
+const email = require('./lib/email');
+
+const to = 'kyounge@umich.edu';
+const subject = 'Re: Your Designer Wallcoverings samples — your 3 free swatches';
+const html = [
+  '<p>Hi Kelly,</p>',
+  '<p>Thank you for your patience — and apologies the sign-up email didn\'t reach you. You\'re all set now.</p>',
+  '<p>Your <b>3 free retail samples are on us</b>. At checkout just use code <b>3FREE</b> — it covers three sample swatches (a $4.25 value each), and <b>sample shipping is on us too</b>, so you\'ll pay nothing. Add the swatches to your cart, apply <b>3FREE</b>, and see them in your space before you commit.</p>',
+  '<p style="color:#555">(That $25 you saw earlier was a shipping estimate — it won\'t apply to these complimentary samples.)</p>',
+  '<p>Anything else I can help you find? We carry over 200 of the finest design houses in one place, and I\'m happy to point you toward pieces like the Majilite Finesse you were viewing.</p>',
+  '<p>Warmly,<br>The Designer Wallcoverings Team<br>DesignerWallcoverings.com · (888) 373-4564</p>',
+].join('\n');
+
+(async () => {
+  const r = await email.sendEmail({ to, subject, html, source: 'kelly-reply-tk10830' });
+  console.log('send result:', JSON.stringify(r));
+  if (r && r.ok === false) { console.error('SEND FAILED — not sent.'); process.exit(1); }
+  console.log(r && r.dryRun ? 'DRY_RUN — nothing sent (re-run with DRY_RUN=0 to send).' : 'SENT to ' + to);
+})().catch(e => { console.error('error:', e.message); process.exit(1); });
diff --git a/verification/tk11114-remediation/BLOCK-A-PASTE.md b/verification/tk11114-remediation/BLOCK-A-PASTE.md
new file mode 100644
index 0000000..d270a0c
--- /dev/null
+++ b/verification/tk11114-remediation/BLOCK-A-PASTE.md
@@ -0,0 +1,41 @@
+# TK-11114 — BLOCK A: the ONE deploy paste (Steve runs; gated prod change)
+
+**Supersedes** `verification/tk11114/FIX-PASTE.md` (that one chased a webhook-token theory that
+functional tests DISPROVED — the live webhook token already matches `…e9c9`; do not run it).
+
+## What this paste does (all reversible, self-verifying)
+1. On prod, timestamped-backs-up `lib/email.js` + `lib/verify.js` **before** any overwrite.
+2. `scp`s the two verified-clean files from Mac2 (byte-identical to committed HEAD).
+   - email.js sha256 `e442f36f8e676096d29bc85cd20ddce6f0b59dc8e24606ede00bd4ef86a62c12`
+   - verify.js sha256 `14498fdd4a50207e727059cce65b1ed414922550c5d05986669db2ea837011e4`
+   - Restores `message_class:'transactional'` + credential-safe un-swallow logging.
+3. Reports whether `GEORGE_BASIC_AUTH` is baked into `ecosystem.config.js` (it should NOT be —
+   the bug is a stale shell-inherited process.env override), then restarts ONLY
+   `dw-signup-fulfillment` with `--update-env` from a clean remote shell (`env -u GEORGE_BASIC_AUTH`)
+   so config falls through to the correct on-host George creds.
+4. Verifies: healthz `dry_run:false`, bad webhook token → 401, live token → 200.
+
+Even if the credential theory is wrong, the un-swallow logging now deployed makes the NEXT
+signup (or the R4 test) print George's real response — proving the send or revealing the exact
+next fix.
+
+## The paste
+```
+! cd ~/Projects/dw-signup-fulfillment && ssh root@45.61.58.125 'cd /root/Projects/dw-signup-fulfillment && ts=$(date +%s) && cp lib/email.js lib/email.js.tk11114.bak.$ts && cp lib/verify.js lib/verify.js.tk11114.bak.$ts && echo "backed up .bak.$ts"' && scp lib/email.js lib/verify.js root@45.61.58.125:/root/Projects/dw-signup-fulfillment/lib/ && ssh root@45.61.58.125 'cd /root/Projects/dw-signup-fulfillment && echo "email sha:$(shasum -a 256 lib/email.js | cut -d" " -f1)" && echo "verify sha:$(shasum -a 256 lib/verify.js | cut -d" " -f1)" && echo "GEORGE_BASIC_AUTH in ecosystem? $(grep -c GEORGE_BASIC_AUTH ecosystem.config.js) (want 0)" && env -u GEORGE_BASIC_AUTH pm2 restart dw-signup-fulfillment --update-env && sleep 2 && echo "== healthz ==" && curl -s http://127.0.0.1:9862/healthz && echo && WT=$(grep -E "^WEBHOOK_URL_TOKEN=" /root/Projects/secrets-manager/.env | head -1 | cut -d= -f2- | tr -d "\"") && curl -s -o /dev/null -w "badtoken http=%{http_code} (want 401)\n" -X POST http://127.0.0.1:9862/webhooks/customers/create/bogus -H "Content-Type: application/json" -d "{}" && curl -s -o /dev/null -w "livetoken http=%{http_code} (want 200)\n" -X POST "http://127.0.0.1:9862/webhooks/customers/create/$WT" -H "Content-Type: application/json" -d "{}"'
+```
+
+**Expected:** both sha lines match the values above · `GEORGE_BASIC_AUTH in ecosystem? 0` ·
+`dry_run:false` · `badtoken http=401` · `livetoken http=200`.
+
+**If `GEORGE_BASIC_AUTH in ecosystem? 1`** — stop and tell me; the override is baked into the
+ecosystem file (not a shell var) and needs a config edit before restart.
+
+## Rollback
+```
+! ssh root@45.61.58.125 'cd /root/Projects/dw-signup-fulfillment && b=$(ls -t lib/email.js.tk11114.bak.* | head -1 | sed "s/.*\.bak\.//") && cp lib/email.js.tk11114.bak.$b lib/email.js && cp lib/verify.js.tk11114.bak.$b lib/verify.js && pm2 restart dw-signup-fulfillment && curl -s http://127.0.0.1:9862/healthz'
+```
+
+## After it's green
+Tell me, and I hand you BLOCK B (R4): one controlled `steve+dwgolive-…@` test customer to fire
+the live webhook, George-inbox verify-click, prove `verified-sample` tag + metafields persist,
+idempotency replay, negative 401, then delete the test customer.
diff --git a/verification/tk11114/FIX-PASTE.md b/verification/tk11114/FIX-PASTE.md
new file mode 100644
index 0000000..a286dc8
--- /dev/null
+++ b/verification/tk11114/FIX-PASTE.md
@@ -0,0 +1,28 @@
+# ⛔ SUPERSEDED / DO NOT RUN — 2026-09-02
+# This webhook-token paste chased a theory functional tests DISPROVED (live webhook token
+# already matches …e9c9; same wrong diagnosis that neutralized refresh-webhook.sh).
+# The correct pending action is verification/tk11114-remediation/BLOCK-A-PASTE.md
+
+# TK-11114 — the ONE fix paste (Steve runs; gated prod change)
+
+**What it does (durable):** on Kamatera, aligns the service's `WEBHOOK_URL_TOKEN` to the
+registered webhook's token (`…e9c9`, pulled from secrets-manager — confirmed byte-identical,
+sha `8ab7a3a16ad4`), keeps `DRY_RUN:'0'`, backs up the ecosystem for rollback, restarts
+`--update-env`, and self-verifies (healthz `dry_run:false`; bad token → 401; `e9c9` → 200).
+Fixes the token mismatch a concurrent restart introduced (running `00e1` ≠ registered `e9c9`).
+
+```
+! ssh root@45.61.58.125 'set -e; cd /root/Projects/dw-signup-fulfillment; WT=$(grep -E "^WEBHOOK_URL_TOKEN=" /root/Projects/secrets-manager/.env | head -1 | cut -d= -f2- | tr -d "\"" ); echo "aligning service token -> last4=${WT: -4} (want e9c9)"; cp ecosystem.config.js ecosystem.config.js.bak.tk11114; sed -i "s/61a971f3660e64e217a692ba7c4e1cfebb87ea9573f100e1/$WT/g" ecosystem.config.js; grep -nE "WEBHOOK_URL_TOKEN|DRY_RUN" ecosystem.config.js; pm2 restart dw-signup-fulfillment --update-env; sleep 2; echo "== healthz =="; curl -s http://127.0.0.1:9862/healthz; echo; curl -s -o /dev/null -w "badtoken http=%{http_code} (want 401)\n" -X POST http://127.0.0.1:9862/webhooks/customers/create/bogus -H "Content-Type: application/json" -d "{}"; curl -s -o /dev/null -w "e9c9 http=%{http_code} (want 200)\n" -X POST "http://127.0.0.1:9862/webhooks/customers/create/$WT" -H "Content-Type: application/json" -d "{}"'
+```
+
+**Expected output:** `dry_run:false`, `badtoken http=401`, `e9c9 http=200`.
+
+**Rollback (if anything looks wrong):**
+```
+! ssh root@45.61.58.125 'cd /root/Projects/dw-signup-fulfillment && cp ecosystem.config.js.bak.tk11114 ecosystem.config.js && pm2 restart dw-signup-fulfillment --update-env && curl -s http://127.0.0.1:9862/healthz'
+```
+
+**After it's green:** tell me, and I run the controlled `+dwgolive` test signup (step 3) — the
+un-swallow logging (already deployed) will now show George's real response, proving the verify
+email actually sends (or revealing the exact next fix). Then verify-click → `verified-sample`
+tag → cleanup.

← e393474 prove TK-11114 signup email remediation end to end  ·  back to Dw Signup Fulfillment  ·  TK-11114: R4 proof — DW signup verify-email fixed + verified 487679a →