← back to Pattern Vault
scripts/signup/bb.js
105 lines
// Browserbase session helper — create/reuse a cloud Chromium session and
// expose a Playwright page. Session id + connect URL persist in session.json
// so successive script runs can keep working in the same browser (keepAlive).
const fs = require('fs');
const path = require('path');
const os = require('os');
const ENV_PATH = path.join(os.homedir(), '.claude/skills/browserbase/.env');
for (const line of fs.readFileSync(ENV_PATH, 'utf8').split('\n')) {
const m = line.match(/^([A-Z_]+)=(.+)$/);
if (m && !process.env[m[1]]) process.env[m[1]] = m[2].trim();
}
const API = 'https://api.browserbase.com/v1';
const HDRS = {
'X-BB-API-Key': process.env.BROWSERBASE_API_KEY,
'Content-Type': 'application/json',
};
const STATE = path.join(__dirname, 'session.json');
async function createSession() {
const res = await fetch(`${API}/sessions`, {
method: 'POST',
headers: HDRS,
body: JSON.stringify({
projectId: process.env.BROWSERBASE_PROJECT_ID,
keepAlive: true,
// residential-ish fingerprint reduces signup-page bot friction
browserSettings: { fingerprint: { devices: ['desktop'], locales: ['en-US'], operatingSystems: ['macos'] } },
}),
});
if (!res.ok) throw new Error(`create session ${res.status}: ${await res.text()}`);
return res.json();
}
async function getSession(id) {
const res = await fetch(`${API}/sessions/${id}`, { headers: HDRS });
if (!res.ok) return null;
return res.json();
}
async function connect({ fresh = false } = {}) {
const { chromium } = require(path.join(os.homedir(), '.npm-global/lib/node_modules/playwright'));
let sess = null;
if (!fresh && fs.existsSync(STATE)) {
const saved = JSON.parse(fs.readFileSync(STATE, 'utf8'));
const live = await getSession(saved.id);
if (live && live.status === 'RUNNING') sess = { id: saved.id, connectUrl: saved.connectUrl };
}
if (!sess) {
const s = await createSession();
sess = { id: s.id, connectUrl: s.connectUrl };
fs.writeFileSync(STATE, JSON.stringify(sess, null, 1));
console.log(`[bb] new session ${s.id}`);
} else {
console.log(`[bb] reusing session ${sess.id}`);
}
const browser = await chromium.connectOverCDP(sess.connectUrl);
const ctx = browser.contexts()[0] || (await browser.newContext());
const page = ctx.pages()[0] || (await ctx.newPage());
return { browser, ctx, page, sessionId: sess.id };
}
async function release(id) {
await fetch(`${API}/sessions/${id}`, {
method: 'POST',
headers: HDRS,
body: JSON.stringify({ projectId: process.env.BROWSERBASE_PROJECT_ID, status: 'REQUEST_RELEASE' }),
}).catch(() => {});
}
// Kill common marketing/consent popups (Klaviyo et al.) that intercept clicks.
async function killPopups(page) {
await page.evaluate(() => {
const sels = [
'[class*="klaviyo"]', '[id*="klaviyo"]', '[class*="kl-private"]',
'[aria-label="POPUP Form"]', '.needsclick[role="dialog"]',
'#onetrust-consent-sdk', '[id*="sailthru"]', '[class*="newsletter-modal"]',
];
for (const s of sels) document.querySelectorAll(s).forEach((el) => el.remove());
}).catch(() => {});
}
// Dump what's actionable on the page so flows can be written from evidence.
async function explore(page, tag) {
await killPopups(page);
const info = await page.evaluate(() => {
const vis = (el) => { const r = el.getBoundingClientRect(); return r.width > 2 && r.height > 2; };
const grab = (sel, fn) => [...document.querySelectorAll(sel)].filter(vis).slice(0, 60).map(fn);
return {
url: location.href,
title: document.title,
inputs: grab('input, select, textarea', (i) => ({ tag: i.tagName, type: i.type, name: i.name, id: i.id, ph: i.placeholder, aria: i.getAttribute('aria-label') })),
buttons: grab('button, [role="button"], input[type="submit"]', (b) => (b.innerText || b.value || b.getAttribute('aria-label') || '').trim().slice(0, 60)).filter(Boolean),
links: grab('a', (a) => ({ t: (a.innerText || '').trim().slice(0, 50), h: a.getAttribute('href') })).filter((l) => l.t),
};
});
const shot = path.join(__dirname, 'shots', `${tag}.png`);
await page.screenshot({ path: shot, fullPage: false }).catch(() => {});
console.log(JSON.stringify(info, null, 1));
console.log(`[shot] ${shot}`);
}
module.exports = { connect, release, killPopups, explore, STATE };