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