← back to All Designerwallcoverings
lib/romo-lib.js
77 lines
// lib/romo-lib.js — self-contained Browserbase session helper for the live-scrape worker.
//
// Copied from ~/.claude/skills/browserbase/romo-lib.js so the pm2 process on the
// PUBLIC host can require() it WITHOUT the skills dir existing on prod. The ONLY
// behavioural change vs the skill copy: Browserbase creds are read ENV-FIRST
// (process.env, which server.js populates from the project .env via dotenv and
// passes through to the spawned worker), falling back to a co-located / project
// .env — the skill copy hard-read `__dirname/.env` and threw if it was absent.
//
// live-scrape.js only uses newSession(); login()/psql() are kept for parity with
// the skill copy. Creds NEVER travel on a command line or leave this process.
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const { chromium } = require('playwright-core');
const Browserbase = require('@browserbasehq/sdk').default;
// Resolve Browserbase creds env-first, then any co-located / project-root .env.
function bbCreds() {
let PROJECT = process.env.BROWSERBASE_PROJECT_ID;
let KEY = process.env.BROWSERBASE_API_KEY;
if (!PROJECT || !KEY) {
for (const p of [path.join(__dirname, '.env'), path.join(__dirname, '..', '.env')]) {
let txt = '';
try { txt = fs.readFileSync(p, 'utf8'); } catch { continue; }
PROJECT = PROJECT || (txt.match(/^BROWSERBASE_PROJECT_ID=(.*)$/m) || [])[1];
KEY = KEY || (txt.match(/^BROWSERBASE_API_KEY=(.*)$/m) || [])[1];
}
}
PROJECT = (PROJECT || '').trim().replace(/^["']|["']$/g, '');
KEY = (KEY || '').trim().replace(/^["']|["']$/g, '');
if (!PROJECT || !KEY) throw new Error('BROWSERBASE creds not configured (env or .env)');
return { PROJECT, KEY };
}
const psql = (s) => execFileSync('psql', ['-At', '-F', '\t', '-d', process.env.PGDATABASE || 'dw_unified', '-c', s], { encoding: 'utf8' }).trim();
async function newSession() {
const { PROJECT, KEY } = bbCreds();
const bb = new Browserbase({ apiKey: KEY });
const session = await bb.sessions.create({ projectId: PROJECT, browserSettings: { solveCaptchas: true } });
const browser = await chromium.connectOverCDP(session.connectUrl);
const ctx = browser.contexts()[0];
const page = ctx.pages()[0] || await ctx.newPage();
page.setDefaultTimeout(45000);
return { bb, session, browser, ctx, page };
}
// Kept for parity with the skill copy (unused by live-scrape.js).
async function login(page) {
const [user, pass] = psql("SELECT trade_username||E'\\t'||trade_password FROM vendor_registry WHERE vendor_name='Romo'").split('\t');
await page.goto('https://trade.theromogroup.com/', { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3500);
if (!/state=/.test(page.url()) && !(await page.$('#fmregister-email'))) return true;
for (const sel of ['button:has-text("ACCEPT ALL")', 'button:has-text("Accept All")']) {
const el = await page.$(sel).catch(() => null);
if (el) { await el.click().catch(() => {}); await page.waitForTimeout(1200); break; }
}
await page.waitForSelector('#fmregister-email', { timeout: 20000 });
await page.fill('#fmregister-email', user);
await page.fill('#fmregister-password', pass);
await page.waitForTimeout(400);
const before = page.url();
await page.click('#login-form button[type=submit], #login-form button').catch(async () => {
await page.focus('#fmregister-password'); await page.keyboard.press('Enter');
});
for (let i = 0; i < 30; i++) {
await page.waitForTimeout(1000);
const u = page.url();
if (u !== before && !/state=/.test(u)) return true;
if (i === 5) { await page.focus('#fmregister-password').catch(() => {}); await page.keyboard.press('Enter').catch(() => {}); }
}
return !/state=/.test(page.url());
}
module.exports = { newSession, login, psql, bbCreds };