[object Object]

← back to Pattern Vault

spoonflower: drive real Chrome (channel:chrome) w/ persistent profile + webdriver-hiding instead of bundled Chromium, to clear Cloudflare

958c6bec14a002c1da73ac3d000e78345a59336c · 2026-07-16 09:24:58 -0700 · Steve Abrams

Files touched

Diff

commit 958c6bec14a002c1da73ac3d000e78345a59336c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 16 09:24:58 2026 -0700

    spoonflower: drive real Chrome (channel:chrome) w/ persistent profile + webdriver-hiding instead of bundled Chromium, to clear Cloudflare
---
 .../daily/scripts/capture_sf_session.js            | 24 ++++++++++++-----
 whimsical-compare/daily/scripts/upload_sf.js       | 31 ++++++++++++++++++----
 2 files changed, 43 insertions(+), 12 deletions(-)

diff --git a/whimsical-compare/daily/scripts/capture_sf_session.js b/whimsical-compare/daily/scripts/capture_sf_session.js
index 8457dec..9bc21fa 100644
--- a/whimsical-compare/daily/scripts/capture_sf_session.js
+++ b/whimsical-compare/daily/scripts/capture_sf_session.js
@@ -40,9 +40,19 @@ console.log(`🔐 capturing session for ${EMAIL} → ${STATE}`);
 
 (async () => {
   fs.mkdirSync(path.dirname(STATE), { recursive: true });
-  const b = await chromium.launch({ headless: !HEADFUL });
-  const ctx = await b.newContext({ userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML,like Gecko) Chrome/126 Safari/537.36' });
-  const pg = await ctx.newPage();
+  // REAL Google Chrome (channel:'chrome'), NOT Playwright's bundled Chromium. Cloudflare
+  // fingerprints bundled Chromium and loops the "verify you are human" check forever even after
+  // you solve it. A persistent profile keeps the cf_clearance cookie + login together so the
+  // clearance you earn once survives into upload_sf.js. NO userAgent override — a spoofed UA that
+  // mismatches the real Chrome binary is itself a CF red flag and voids cf_clearance.
+  const PROFILE = process.env.SF_PROFILE || path.join(os.homedir(), '.config/pattern-vault/sf-chrome-profile');
+  fs.mkdirSync(PROFILE, { recursive: true });
+  const ctx = await chromium.launchPersistentContext(PROFILE, {
+    headless: !HEADFUL, channel: 'chrome', viewport: null,
+    ignoreDefaultArgs: ['--enable-automation'],           // drop the automation banner
+    args: ['--disable-blink-features=AutomationControlled'], // hide navigator.webdriver from CF
+  });
+  const pg = ctx.pages()[0] || await ctx.newPage();
   try {
     await pg.goto('https://www.spoonflower.com/login', { waitUntil: 'networkidle', timeout: 60000 });
     // Dismiss the cookie-consent banner — it overlays and intercepts the Sign In button.
@@ -74,7 +84,7 @@ console.log(`🔐 capturing session for ${EMAIL} → ${STATE}`);
       console.log('   Waiting up to 3 min for login to complete…\n');
       let ok = false;
       for (let i = 0; i < 90; i++) { await pg.waitForTimeout(2000); if (!pg.url().includes('/login')) { ok = true; break; } }
-      if (!ok) { console.error('❌ timed out (still on /login) — captcha/login not completed.'); await b.close(); process.exit(1); }
+      if (!ok) { console.error('❌ timed out (still on /login) — captcha/login not completed.'); await ctx.close(); process.exit(1); }
     } else {
       const submit = pg.locator('button[type="submit"], button:has-text("Log In"), button:has-text("Sign In")').first();
       await submit.scrollIntoViewIfNeeded().catch(() => {});
@@ -84,7 +94,7 @@ console.log(`🔐 capturing session for ${EMAIL} → ${STATE}`);
       if (pg.url().includes('/login')) {
         await pg.screenshot({ path: '/tmp/sf-capture-fail.png' });
         console.error(`❌ still on /login after submit — captcha/challenge. Screenshot: /tmp/sf-capture-fail.png`);
-        await b.close(); process.exit(1);
+        await ctx.close(); process.exit(1);
       }
     }
     const url = pg.url();
@@ -94,10 +104,10 @@ console.log(`🔐 capturing session for ${EMAIL} → ${STATE}`);
     await ctx.storageState({ path: STATE });
     const cookies = JSON.parse(fs.readFileSync(STATE, 'utf8')).cookies?.length || 0;
     console.log(`✅ session saved (${cookies} cookies). landed: ${url}${who ? ' | ' + who : ''}`);
-    await b.close();
+    await ctx.close();
   } catch (e) {
     try { await pg.screenshot({ path: '/tmp/sf-capture-fail.png' }); } catch {}
     console.error('❌ capture error:', e.message, '(screenshot /tmp/sf-capture-fail.png)');
-    await b.close(); process.exit(1);
+    await ctx.close(); process.exit(1);
   }
 })();
diff --git a/whimsical-compare/daily/scripts/upload_sf.js b/whimsical-compare/daily/scripts/upload_sf.js
index 13f3d74..78b9555 100755
--- a/whimsical-compare/daily/scripts/upload_sf.js
+++ b/whimsical-compare/daily/scripts/upload_sf.js
@@ -66,8 +66,29 @@ async function messyType(el, text) {
 
   if (!fs.existsSync(STATE)) { result.sessionExpired = true; result.error = 'no session file'; console.log(JSON.stringify(result)); return; }
 
-  const b = await chromium.launch({ headless: (process.env.SF_HEADLESS === '1') ? true : !ARMED, args: ['--start-maximized'] });
-  const ctx = await b.newContext({ viewport: ARMED ? null : { width: 1400, height: 900 }, storageState: STATE, userAgent: UA });
+  // REAL Google Chrome + the SAME persistent profile capture_sf_session.js cleared Cloudflare in.
+  // The old chromium.launch() drove Playwright's bundled Chromium, which CF fingerprints and blocks;
+  // the spoofed Chrome/126 UA (const UA) mismatched the binary and made it worse. channel:'chrome'
+  // + no UA override = one honest fingerprint, and the profile carries a valid cf_clearance.
+  const PROFILE = process.env.SF_PROFILE || require('path').join(require('os').homedir(), '.config/pattern-vault/sf-chrome-profile');
+  require('fs').mkdirSync(PROFILE, { recursive: true });
+  const ctx = await chromium.launchPersistentContext(PROFILE, {
+    headless: (process.env.SF_HEADLESS === '1') ? true : !ARMED,
+    channel: 'chrome',
+    viewport: ARMED ? null : { width: 1400, height: 900 },
+    ignoreDefaultArgs: ['--enable-automation'],              // drop the automation banner
+    args: ['--start-maximized', '--disable-blink-features=AutomationControlled'], // hide navigator.webdriver
+  });
+  // Bridge the existing Spoonflower login from the legacy storageState file (portable auth cookies)
+  // in case this profile is fresh. cf_clearance is fingerprint-bound and intentionally NOT imported —
+  // real Chrome earns its own valid one.
+  try {
+    if (fs.existsSync(STATE)) {
+      const saved = JSON.parse(fs.readFileSync(STATE, 'utf8')).cookies || [];
+      const authCookies = saved.filter(c => !/^cf_|__cf|cf_clearance/i.test(c.name));
+      if (authCookies.length) await ctx.addCookies(authCookies).catch(() => {});
+    }
+  } catch {}
   // DIAGNOSED 2026-07-09: an intermittent consent/marketing overlay (bounceexchange iframe +
   // "Your Privacy Choices / Agree & Continue") intercepts the uploader clicks. Block those
   // vendors at the network layer so the overlay never loads. (Spoonflower's own uploader is unaffected.)
@@ -76,7 +97,7 @@ async function messyType(el, text) {
     if (/bounceexchange\.com|bounce\.|onetrust|cookielaw|osano|consent|quantcast|trustarc/i.test(u)) return route.abort();
     return route.continue();
   }).catch(() => {});
-  const pg = await ctx.newPage();
+  const pg = ctx.pages()[0] || await ctx.newPage();
 
   // iframe-aware consent dismissal — the "Agree & Continue" button lives in an iframe, so scan
   // every frame (not just the top document) and click it wherever it is.
@@ -95,7 +116,7 @@ async function messyType(el, text) {
   // session health check
   await pg.goto('https://www.spoonflower.com/en/my_account', { waitUntil: 'domcontentloaded', timeout: 45000 }).catch(() => {});
   await sleep(2500);
-  if (pg.url().includes('/login')) { result.sessionExpired = true; await b.close(); console.log(JSON.stringify(result)); return; }
+  if (pg.url().includes('/login')) { result.sessionExpired = true; await ctx.close(); console.log(JSON.stringify(result)); return; }
 
   // reach the uploader the proven way: homepage -> account menu -> "UPLOAD A DESIGN"
   async function openUploader() {
@@ -209,6 +230,6 @@ async function messyType(el, text) {
     }
   }
 
-  await b.close();
+  await ctx.close();
   console.log(JSON.stringify(result));
 })().catch(e => { console.log(JSON.stringify({ error: String(e), uploaded: [], failed: [] })); process.exit(1); });

← 378dec5 auto-save: 2026-07-13T06:53:18 (2 files) — data/trend-board.  ·  back to Pattern Vault  ·  spoonflower: add plain-real-Chrome manual-login fallback (sf ee917f9 →