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