← back to Designerwallcoverings
TK-11397: swap dead SHOPIFY_FULL_ACCESS_TOKEN for live ADMIN token, fail-loud on 401/403 across GMC-404 batch scripts
5624b21b9a4b0a21e8dde627245f3c855db83edd · 2026-09-23 13:06:56 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0144GfLFF14MFmAnCREHST5n
Files touched
A scripts/tk11397-gmc-404-batch/audit-prompt.shA scripts/tk11397-gmc-404-batch/check-unpub.mjsA scripts/tk11397-gmc-404-batch/probe.mjsA scripts/tk11397-gmc-404-batch/resolve-nulls.mjsM scripts/tk11397-gmc-404-batch/resolve-variants.mjsA scripts/tk11397-gmc-404-batch/verify-hop.mjsA scripts/tk11397-gmc-404-batch/verify-unpub-404.mjs
Diff
commit 5624b21b9a4b0a21e8dde627245f3c855db83edd
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 23 13:06:56 2026 -0700
TK-11397: swap dead SHOPIFY_FULL_ACCESS_TOKEN for live ADMIN token, fail-loud on 401/403 across GMC-404 batch scripts
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0144GfLFF14MFmAnCREHST5n
---
scripts/tk11397-gmc-404-batch/audit-prompt.sh | 9 +++++
scripts/tk11397-gmc-404-batch/check-unpub.mjs | 43 ++++++++++++++++++++++
scripts/tk11397-gmc-404-batch/probe.mjs | 24 ++++++++++++
scripts/tk11397-gmc-404-batch/resolve-nulls.mjs | 34 +++++++++++++++++
scripts/tk11397-gmc-404-batch/resolve-variants.mjs | 12 +++++-
scripts/tk11397-gmc-404-batch/verify-hop.mjs | 28 ++++++++++++++
scripts/tk11397-gmc-404-batch/verify-unpub-404.mjs | 12 ++++++
7 files changed, 160 insertions(+), 2 deletions(-)
diff --git a/scripts/tk11397-gmc-404-batch/audit-prompt.sh b/scripts/tk11397-gmc-404-batch/audit-prompt.sh
new file mode 100755
index 0000000..61c85ea
--- /dev/null
+++ b/scripts/tk11397-gmc-404-batch/audit-prompt.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+cd "$(dirname "$0")"
+echo "COUNT=$(jq length redirect-pairs.json)"
+echo "FINDING=$(jq -r '.finding' restore-map-prestate-TK-11397.json)"
+FROM=$(jq -r '.[39].from' redirect-pairs.json)
+TO=$(jq -r '.[39].to' redirect-pairs.json)
+echo "PAIR39_FROM=$FROM"
+echo "PAIR39_TO=$TO"
+echo "CURL=$(curl -s -o /dev/null -w '%{http_code} %{redirect_url}' -A 'Mozilla/5.0' https://www.designerwallcoverings.com/products/$FROM)"
diff --git a/scripts/tk11397-gmc-404-batch/check-unpub.mjs b/scripts/tk11397-gmc-404-batch/check-unpub.mjs
new file mode 100644
index 0000000..45f9ec0
--- /dev/null
+++ b/scripts/tk11397-gmc-404-batch/check-unpub.mjs
@@ -0,0 +1,43 @@
+// TK-11397: for the 35 approved-unpublished offers, verify product DRAFT + no redirect + no live URL
+import { readFileSync, writeFileSync } from 'node:fs';
+const env = readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8');
+// SHOPIFY_FULL_ACCESS_TOKEN is dead (401 since ~2026-09-22); ADMIN is the live
+// Full-Access app. Guard the parse so a missing key fails loud, not with a bare
+// TypeError on `[1]` of a null match.
+const tm = env.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m);
+if (!tm) { console.error('FATAL: SHOPIFY_ADMIN_TOKEN not found in secrets-manager/.env'); process.exit(1); }
+const TOKEN = tm[1].replace(/["']/g, '').trim();
+const ENDPOINT = 'https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+async function gql(q, v) {
+ for (let a = 0; a < 5; a++) {
+ try {
+ const r = await fetch(ENDPOINT, { method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ query: q, variables: v }) });
+ if (r.status === 429 || r.status >= 500) { await sleep(1200 * (a + 1)); continue; }
+ // Auth failure is fatal, never a data point. Returning here would let the
+ // caller read `undefined` data as p_status:null = "product deleted".
+ if (r.status === 401 || r.status === 403) throw new Error(`Shopify auth failed: HTTP ${r.status} (token dead or missing scope)`);
+ const j = await r.json();
+ if (!j.data) throw new Error(`Shopify returned no data: ${JSON.stringify(j.errors || j).slice(0, 300)}`);
+ return j;
+ } catch (e) {
+ if (/auth failed|no data/.test(e.message)) throw e; // don't retry a hard failure
+ await sleep(1200 * (a + 1));
+ }
+ }
+ throw new Error('gql: retries exhausted (network/5xx) — aborting rather than reporting null state');
+}
+const rows = JSON.parse(readFileSync('/tmp/tk11397-unpub-approved.json', 'utf8'));
+const q = `query($id: ID!){ productVariant(id: $id){ id product{ id handle status } } }`;
+const out = [];
+for (const r of rows) {
+ const vid = (r.key.match(/_(\d{6,})$/) || [])[1] || r.key;
+ const j = await gql(q, { id: 'gid://shopify/ProductVariant/' + vid });
+ const v = j && j.data && j.data.productVariant;
+ out.push({ offer_key: r.key, dead_handle: r.handle, variant_id: vid, p_status: v && v.product ? v.product.status : null, p_handle: v && v.product ? v.product.handle : null, serving: r.serving });
+ await sleep(260);
+}
+writeFileSync('unpub-state.json', JSON.stringify(out, null, 1));
+const st = out.reduce((m, r) => (m[r.p_status] = (m[r.p_status] || 0) + 1, m), {});
+console.log('product status:', JSON.stringify(st));
+console.log('published-as-redirect check: any p_handle != dead_handle?', out.filter(r => r.p_handle && r.p_handle !== r.dead_handle).length);
diff --git a/scripts/tk11397-gmc-404-batch/probe.mjs b/scripts/tk11397-gmc-404-batch/probe.mjs
new file mode 100644
index 0000000..3c636df
--- /dev/null
+++ b/scripts/tk11397-gmc-404-batch/probe.mjs
@@ -0,0 +1,24 @@
+import { readFileSync, writeFileSync } from 'node:fs';
+const { targets, deads } = JSON.parse(readFileSync('/tmp/tk11397-probe-lists.json', 'utf8'));
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+async function probe(handle) {
+ for (let a = 0; a < 3; a++) {
+ try {
+ const r = await fetch(`https://www.designerwallcoverings.com/products/${handle}.js`, {
+ headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36' },
+ redirect: 'manual'
+ });
+ return r.status;
+ } catch (e) { await sleep(2000 * (a + 1)); }
+ }
+ return 'ERR';
+}
+const tres = [];
+for (const h of targets) { tres.push({ handle: h, status: await probe(h) }); await sleep(900); }
+writeFileSync('probe-targets.json', JSON.stringify(tres));
+const dres = [];
+for (const h of deads) { dres.push({ handle: h, status: await probe(h) }); await sleep(900); }
+writeFileSync('probe-deads.json', JSON.stringify(dres));
+const tOK = tres.filter(r => r.status === 200).length;
+const d404 = dres.filter(r => r.status === 404).length;
+console.log(`targets 200: ${tOK}/${tres.length} | deads 404: ${d404}/${dres.length}`);
diff --git a/scripts/tk11397-gmc-404-batch/resolve-nulls.mjs b/scripts/tk11397-gmc-404-batch/resolve-nulls.mjs
new file mode 100644
index 0000000..1f48faa
--- /dev/null
+++ b/scripts/tk11397-gmc-404-batch/resolve-nulls.mjs
@@ -0,0 +1,34 @@
+// TK-11397: 77 variant-null rows -> resolve via mirror SKU lookup (variant_id->sku DWQW-NNNNN)
+import { readFileSync, writeFileSync } from 'node:fs';
+import { execFileSync } from 'node:child_process';
+const res = JSON.parse(readFileSync('resolved-variants.json', 'utf8'));
+const nulls = res.filter(r => !r.cur_handle);
+const out = [];
+for (const r of nulls) {
+ const m = r.dead_handle.match(/dwqw-(\d{4,7})/);
+ const num = m ? m[1] : null;
+ let cur = null, status = null, sku = null, err = null;
+ if (num) {
+ try {
+ // Match the SELLABLE code, which lives in `sku` (DWQW-56856). `variant_sku`
+ // carries the sample-suffixed twin (DWQW-56856-Sample), so filtering on it
+ // matches 0 rows and every offer reads as a false "not found".
+ const o = execFileSync('psql', ['-d','dw_unified','-t','-A','-F','|','-c',
+ `SELECT handle, status, sku FROM shopify_products WHERE sku='DWQW-${num}' LIMIT 1`], {encoding:'utf8'});
+ const line = o.trim().split('\n').filter(Boolean)[0];
+ if (line) { const p = line.split('|'); cur = p[0]; status = p[1]; sku = p[2]; }
+ } catch (e) {
+ // A psql failure is NOT a clean miss — mark it NOT-MEASURED so a query/DB
+ // error can never masquerade as "handle is dead" (false-negative class).
+ err = String(e.message || e).split('\n')[0];
+ console.error(`psql lookup failed for DWQW-${num}: ${err}`);
+ }
+ }
+ out.push({ offer_key: r.offer_key, dead_handle: r.dead_handle, variant_id: r.variant_id, serving: r.serving, title: r.title, dwqw_num: num, cur_handle: cur, status, sku, err });
+}
+writeFileSync('nulls-resolved.json', JSON.stringify(out, null, 1));
+const f = out.filter(r => r.cur_handle);
+const errored = out.filter(r => r.err);
+console.log('nulls:', out.length, '| resolved via mirror sku:', f.length, '| lookup-errors (NOT-MEASURED):', errored.length, '| status:', JSON.stringify(f.reduce((m,r)=>(m[r.status]=(m[r.status]||0)+1,m),{})));
+console.log('unresolved (clean miss):', JSON.stringify(out.filter(r=>!r.cur_handle && !r.err).map(r=>r.dead_handle)));
+if (errored.length) console.log('NOT-MEASURED (psql failed, do NOT treat as dead):', JSON.stringify(errored.map(r=>r.dead_handle)));
diff --git a/scripts/tk11397-gmc-404-batch/resolve-variants.mjs b/scripts/tk11397-gmc-404-batch/resolve-variants.mjs
index 074ef61..386fbcf 100644
--- a/scripts/tk11397-gmc-404-batch/resolve-variants.mjs
+++ b/scripts/tk11397-gmc-404-batch/resolve-variants.mjs
@@ -1,7 +1,11 @@
// TK-11397 batch: resolve uncovered rename_drift variants -> current product handle
import { readFileSync, writeFileSync } from 'node:fs';
const env = readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8');
-const TOKEN = (env.match(/^SHOPIFY_FULL_ACCESS_TOKEN=(.+)$/m) || [])[1].replace(/["']/g, '').trim();
+// SHOPIFY_FULL_ACCESS_TOKEN is dead (401 since ~2026-09-22); ADMIN is the live
+// Full-Access app. Guard the parse so a missing key fails loud.
+const tm = env.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m);
+if (!tm) { console.error('FATAL: SHOPIFY_ADMIN_TOKEN not found in secrets-manager/.env'); process.exit(1); }
+const TOKEN = tm[1].replace(/["']/g, '').trim();
const ENDPOINT = 'https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json';
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function gql(query, variables) {
@@ -10,11 +14,15 @@ async function gql(query, variables) {
try { r = await fetch(ENDPOINT, { method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ query, variables }) }); }
catch (e) { await sleep(1500 * (a + 1)); continue; }
if (r.status === 429 || r.status >= 500) { await sleep(1500 * (a + 1)); continue; }
+ // Auth failure is fatal, never a "null product" data point.
+ if (r.status === 401 || r.status === 403) throw new Error(`Shopify auth failed: HTTP ${r.status} (token dead or missing scope)`);
const j = await r.json();
if (j.errors && !j.data) { await sleep(1200); continue; }
return j;
}
- return null;
+ // Retries exhausted = NOT-MEASURED. Throw rather than return null, which the
+ // caller would record as cur_handle:null = a falsely "dead" handle.
+ throw new Error('gql: retries exhausted — aborting rather than reporting null product state');
}
const uncovered = JSON.parse(readFileSync('/tmp/tk11397-uncovered-rd.json', 'utf8'));
const q = `query($id: ID!){ productVariant(id: $id){ id sku product{ id handle status } } }`;
diff --git a/scripts/tk11397-gmc-404-batch/verify-hop.mjs b/scripts/tk11397-gmc-404-batch/verify-hop.mjs
new file mode 100644
index 0000000..0ff18b6
--- /dev/null
+++ b/scripts/tk11397-gmc-404-batch/verify-hop.mjs
@@ -0,0 +1,28 @@
+// TK-11397 verify: /products/<dead_handle> (HTML) -> 301 -> target 200 (real ad-click path)
+import { readFileSync } from 'node:fs';
+const pairs = JSON.parse(readFileSync('redirect-pairs.json', 'utf8'));
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+// deterministic sample: every 10th pair = 15 samples
+const sample = pairs.filter((_, i) => i % 10 === 0);
+const out = [];
+for (const p of sample) {
+ try {
+ const r = await fetch(`https://www.designerwallcoverings.com/products/${p.from}`, {
+ headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36' },
+ redirect: 'manual'
+ });
+ const loc = r.headers.get('location') || '';
+ const locPath = loc.replace(/^https?:\/\/[^/]+/, '').split('?')[0];
+ const expected = `/products/${p.to}`;
+ let t200 = null;
+ if (r.status === 301 && locPath === expected) {
+ const r2 = await fetch(`https://www.designerwallcoverings.com${expected}`, { headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh) Chrome/126.0' }, redirect: 'manual' });
+ t200 = r2.status;
+ }
+ out.push({ from: p.from, hop_status: r.status, loc: locPath, expected, target_status: t200 });
+ console.log(`${p.from} -> ${r.status} ${locPath} | target ${t200} ${locPath === expected ? 'EXACT' : 'MISMATCH'}`);
+ } catch (e) { out.push({ from: p.from, err: e.message }); console.log(p.from, 'ERR', e.message); }
+ await sleep(900);
+}
+const exact = out.filter(o => o.hop_status === 301 && o.loc === o.expected && o.target_status === 200).length;
+console.log(`EXACT+200: ${exact}/${out.length}`);
diff --git a/scripts/tk11397-gmc-404-batch/verify-unpub-404.mjs b/scripts/tk11397-gmc-404-batch/verify-unpub-404.mjs
new file mode 100644
index 0000000..565166b
--- /dev/null
+++ b/scripts/tk11397-gmc-404-batch/verify-unpub-404.mjs
@@ -0,0 +1,12 @@
+import { readFileSync } from 'node:fs';
+const rows = JSON.parse(readFileSync('unpub-state.json', 'utf8'));
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+// spot-check 8: dead handle HTML must 404 (DRAFT product page = unpublished)
+const sample = rows.filter((_, i) => i % 4 === 0).slice(0, 8);
+for (const r of sample) {
+ try {
+ const res = await fetch(`https://www.designerwallcoverings.com/products/${r.p_handle || r.dead_handle}`, { headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh) Chrome/126.0' }, redirect: 'manual' });
+ console.log(`${r.p_handle || r.dead_handle} [${r.p_status}] -> ${res.status} ${res.headers.get('location') || ''}`);
+ } catch (e) { console.log(r.p_handle, 'ERR', e.message); }
+ await sleep(900);
+}
← d4c017e auto-data-snapshot: 2026-09-23T12:42:16 (1 data files) — scr
·
back to Designerwallcoverings
·
Retire: README-RETIRED banners for 7 dormant image-scan scri 886e350 →