← back to Pattern Vault

whimsical-compare/daily/scripts/capture_sf_session.js

113 lines

#!/usr/bin/env node
/**
 * Capture a logged-in Spoonflower session (storageState) for the UPLOAD/SELLER
 * account, so upload_sf.js can post without re-authing each run.
 *
 * Account = info@designerwallcoverings.com (2026-07-07 switch; printmurals is now
 * a designer persona). Creds are read from env, falling back to the resize-it .env
 * — NEVER hardcoded here.
 *
 * Writes storageState to SF_STATE (default ~/.config/pattern-vault/sf-state.json),
 * the SAME persistent path upload_sf.js now reads. Run headful with --headful to
 * watch / clear a rare challenge.
 *
 *   node capture_sf_session.js [--headful]
 */
const { chromium } = require('/Users/macstudio3/.npm-global/lib/node_modules/playwright');
const fs = require('fs');
const os = require('os');
const path = require('path');

// --- creds: env first, then resize-it/.env (real values live there) ---
function loadEnvFile(p) {
  const out = {};
  try {
    for (const line of fs.readFileSync(p, 'utf8').split('\n')) {
      const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
      if (m) out[m[1]] = m[2].replace(/^"(.*)"$/, '$1');
    }
  } catch {}
  return out;
}
const envFile = loadEnvFile(path.join(os.homedir(), '.claude/skills/resize-it/.env'));
const EMAIL = process.env.SPOONFLOWER_EMAIL || envFile.SPOONFLOWER_EMAIL;
const PASSWORD = process.env.SPOONFLOWER_PASSWORD || envFile.SPOONFLOWER_PASSWORD;
const STATE = process.env.SF_STATE || path.join(os.homedir(), '.config/pattern-vault/sf-state.json');
const HEADFUL = process.argv.includes('--headful');

if (!EMAIL || !PASSWORD) { console.error('❌ missing SPOONFLOWER_EMAIL/PASSWORD (env or resize-it/.env)'); process.exit(2); }
console.log(`🔐 capturing session for ${EMAIL} → ${STATE}`);

(async () => {
  fs.mkdirSync(path.dirname(STATE), { recursive: true });
  // Isolated bundled Chromium (a separate Chromium app), NOT Steve's real Google Chrome — no
  // channel:'chrome', so this never hijacks his browser or profile. NOTE: bundled Chromium is more
  // CF-detectable, so the primary/default path is now Browserbase (upload_sf.js, cloud) which logs
  // in fresh and needs no local capture at all. This local capture stays for the SF_LOCAL fallback.
  const PROFILE = process.env.SF_PROFILE || path.join(os.homedir(), '.config/pattern-vault/sf-chromium-profile');
  fs.mkdirSync(PROFILE, { recursive: true });
  const ctx = await chromium.launchPersistentContext(PROFILE, {
    headless: !HEADFUL, 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.
    await pg.waitForTimeout(2000);
    // Cookie consent may render in an IFRAME; search every frame for an accept/reject button.
    let cookieDismissed = false;
    for (const fr of pg.frames()) {
      for (const name of [/accept all/i, /reject all/i, /accept/i]) {
        try { const b = fr.getByRole('button', { name }).first();
              if (await b.isVisible({ timeout: 800 })) { await b.click({ force: true }); cookieDismissed = true; break; } } catch {}
      }
      if (cookieDismissed) break;
    }
    // Fallback: known consent IDs in main frame
    if (!cookieDismissed) { try { const b = pg.locator('#onetrust-accept-btn-handler, [id*="accept" i][id*="cookie" i], button[aria-label*="accept" i]').first();
      if (await b.isVisible({ timeout: 800 })) { await b.click({ force: true }); cookieDismissed = true; } } catch {} }
    await pg.waitForTimeout(800);
    console.log(`   cookie banner dismissed: ${cookieDismissed}`);
    // Type char-by-char so the form's onChange validation fires and enables Sign In.
    const emailEl = pg.locator('input[type="email"], input[name*="email" i], input[placeholder*="email" i]').first();
    await emailEl.click(); await emailEl.fill(''); await emailEl.pressSequentially(EMAIL, { delay: 30 });
    const pwEl = pg.locator('input[type="password"]').first();
    await pwEl.click(); await pwEl.fill(''); await pwEl.pressSequentially(PASSWORD, { delay: 30 });
    await pwEl.blur().catch(() => {});
    await pg.waitForTimeout(500);
    if (HEADFUL) {
      console.log('\n👉 Browser window is open with email+password pre-filled.');
      console.log('   Do two things in the window: (1) solve the reCAPTCHA checkbox, (2) click "Sign in".');
      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 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(() => {});
      await submit.click({ timeout: 15000 });
      await pg.waitForLoadState('networkidle', { timeout: 60000 }).catch(() => {});
      await pg.waitForTimeout(4000);
      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 ctx.close(); process.exit(1);
      }
    }
    const url = pg.url();
    // Confirm which account we're in (best-effort)
    let who = '';
    try { who = await pg.evaluate(() => document.querySelector('[data-testid*="account" i], a[href*="/profile"], a[href*="/account"]')?.textContent?.trim() || ''); } catch {}
    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 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 ctx.close(); process.exit(1);
  }
})();