← 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);
}
})();