← back to Pattern Vault

wpb-uploaders/capture-session.js

97 lines

#!/usr/bin/env node
/*
 * Capture a logged-in session (Playwright storageState) for any WPB uploader platform.
 * Opens a HEADED browser to the platform's login page; YOU log in manually (SSO, 2FA, CAPTCHA
 * all fine). It AUTO-DETECTS when you're logged in and saves the session — no keypress needed,
 * so it works even when launched via the `!` prefix. Times out after 5 minutes.
 *
 *   node capture-session.js <creativemarket|redbubble|society6|patternbank>
 *
 * (Etsy uses OAuth, not a browser session — paste an ETSY_OAUTH_TOKEN instead.)
 * Writes sessions/<platform>-state.json. Nothing is uploaded — this only captures the login.
 */
const path = require('path'), fs = require('fs');
const { chromium, webkit } = require('/Users/macstudio3/.npm-global/lib/node_modules/playwright');
// Engine: WebKit = Safari's engine (Steve prefers Safari). Cookies are engine-portable, so a
// session captured in WebKit loads fine in the chromium uploader. Override with WPB_ENGINE.
const ENGINE = process.env.WPB_ENGINE === 'chromium' ? chromium : webkit;

const platform = process.argv[2];
const PLATFORMS = ['creativemarket', 'redbubble', 'society6', 'patternbank'];
if (!platform || !PLATFORMS.includes(platform)) {
  console.error(`usage: node capture-session.js <${PLATFORMS.join('|')}>`);
  if (platform === 'etsy') console.error('  (Etsy uses OAuth, not a browser session — paste an ETSY_OAUTH_TOKEN instead)');
  process.exit(1);
}
const cfg = require(path.join(__dirname, 'platforms', `${platform}.js`));
const OUT = cfg.sessionFile;
const LOGIN = { creativemarket: 'https://creativemarket.com/login', redbubble: 'https://www.redbubble.com/auth/login',
  society6: 'https://society6.com/login', patternbank: 'https://patternbank.com/login' }[platform];
const sleep = ms => new Promise(r => setTimeout(r, ms));

// logged-in heuristic — REQUIRES a POSITIVE authenticated marker (no weak domain/absence fallback,
// which false-positived on 2026-07-09). Must be off the login URL, no password field, AND an actual
// logout/sign-out control OR clear account-page text present.
// AUTHORITATIVE session check — don't guess from the login page's DOM. Open a probe tab in the
// SAME context (shares your cookies) and load the account URL: if it reaches the account page
// with a real logout/account marker (not redirected to login, no password form), you're in.
async function sessionAuthenticated(ctx) {
  const probe = await ctx.newPage();
  try {
    await probe.goto(cfg.healthUrl, { waitUntil: 'domcontentloaded', timeout: 30000 }).catch(() => {});
    await probe.waitForTimeout(2000);
    const u = probe.url();
    if (/\/login|\/signin|\/auth\/login|\/sign_in|accounts\.google\.com/i.test(u)) return false; // bounced to login
    if (await probe.$('input[type=password]').catch(() => null)) return false;
    const logout = await probe.$('a[href*="logout" i], a[href*="sign_out" i], a[href*="signout" i], button:has-text("Log out"), button:has-text("Sign out")').catch(() => null);
    if (logout) return true;
    const txt = (await probe.evaluate(() => document.body.innerText).catch(() => '') || '').toLowerCase();
    return /sign out|log out|my account|dashboard|purchases|downloads|my shop|order history/.test(txt);
  } catch { return false; }
  finally { await probe.close().catch(() => {}); }
}

(async () => {
  fs.mkdirSync(path.dirname(OUT), { recursive: true });
  console.log(`\n🔐 Capturing ${cfg.name} session → ${OUT}`);
  console.log(`   A browser window is opening at ${LOGIN}`);
  console.log(`   Log in fully (SSO / 2FA / CAPTCHA all OK) and reach your dashboard.`);
  console.log(`   I'll auto-detect the login and save it — no keypress needed.\n`);

  const b = await ENGINE.launch({ headless: false });
  const ctx = await b.newContext({ viewport: null });
  const pg = await ctx.newPage();
  await pg.goto(LOGIN, { waitUntil: 'domcontentloaded', timeout: 60000 }).catch(() => {});

  // nudge the window to the front (Playwright's browser often opens behind others)
  const { exec } = require('child_process');
  for (const nm of ['Playwright', 'WebKit', 'Chrome for Testing', 'Safari']) {
    exec(`osascript -e 'tell application "System Events" to set frontmost of (first process whose name contains "${nm}") to true' 2>/dev/null`);
  }

  const start = Date.now();
  const deadline = start + 10 * 60 * 1000; // 10 min
  let saved = false;
  while (Date.now() < deadline) {
    await sleep(8000); // probe is authoritative but heavier; poll every 8s
    // time guard: give you 20s to actually start logging in before the first probe
    if (Date.now() - start < 20000) continue;
    let ok = false;
    try { ok = await sessionAuthenticated(ctx); } catch { /* navigating */ }
    if (ok) { // one authoritative pass (real account-page check) is enough
      await ctx.storageState({ path: OUT });
      saved = true;
      break;
    }
    if (Math.round((deadline - Date.now()) / 1000) % 30 === 0) console.log(`   …waiting for login (${Math.round((deadline - Date.now()) / 1000)}s left)`);
  }
  await b.close();

  if (saved && fs.existsSync(OUT) && fs.statSync(OUT).size > 200) {
    console.log(`\n✅ Saved ${cfg.name} session (${fs.statSync(OUT).size} bytes) → ${OUT}`);
    console.log(`   Tell Claude "captured ${platform}" and it will calibrate the uploader (still fail-loud + <=4/day).`);
  } else {
    console.log(`\n⚠️  No login detected within 5 min — session NOT saved. Re-run and complete the login.`);
  }
})();