← back to Pattern Vault
signup tooling snapshot: browserbase step-driver (bb.js/step.js) from abandoned auto-signup attempt; gitignore session/passwords/shots; probe inquiry event
65485ea73d28adf8353baa48ec2f622c7b836981 · 2026-07-02 07:41:47 -0700 · Steve Abrams
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Files touched
M .gitignoreM data/license-events.jsonlA scripts/signup/bb.jsA scripts/signup/step.js
Diff
commit 65485ea73d28adf8353baa48ec2f622c7b836981
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 2 07:41:47 2026 -0700
signup tooling snapshot: browserbase step-driver (bb.js/step.js) from abandoned auto-signup attempt; gitignore session/passwords/shots; probe inquiry event
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
.gitignore | 3 ++
data/license-events.jsonl | 1 +
scripts/signup/bb.js | 104 ++++++++++++++++++++++++++++++++++++++++++++++
scripts/signup/step.js | 46 ++++++++++++++++++++
4 files changed, 154 insertions(+)
diff --git a/.gitignore b/.gitignore
index 0f93243..a09194e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,6 @@ tmp/
.DS_Store
dist/
build/
+scripts/signup/passwords.local
+scripts/signup/session.json
+scripts/signup/shots/
diff --git a/data/license-events.jsonl b/data/license-events.jsonl
index c0d3678..c6d9065 100644
--- a/data/license-events.jsonl
+++ b/data/license-events.jsonl
@@ -1 +1,2 @@
{"ts":"2026-07-02T13:34:56.223Z","type":"inquiry","designId":"57710","tier":"digital_single_use","email":"t@e.com"}
+{"ts":"2026-07-02T14:24:05.092Z","type":"inquiry","designId":"57710","tier":"digital_single_use","email":"probe@local.test"}
diff --git a/scripts/signup/bb.js b/scripts/signup/bb.js
new file mode 100644
index 0000000..1960dd0
--- /dev/null
+++ b/scripts/signup/bb.js
@@ -0,0 +1,104 @@
+// 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 };
diff --git a/scripts/signup/step.js b/scripts/signup/step.js
new file mode 100644
index 0000000..359e47a
--- /dev/null
+++ b/scripts/signup/step.js
@@ -0,0 +1,46 @@
+// Step driver: run one action against the persistent Browserbase session,
+// then dump page evidence (inputs/buttons/links + screenshot).
+// node step.js goto <url> <tag>
+// node step.js click <text-or-selector> <tag> (text matched on buttons/links)
+// node step.js fill <selector> <value> <tag>
+// node step.js press <key> <tag>
+// node step.js explore <tag>
+// node step.js eval "<js>" <tag>
+const { connect, explore, killPopups } = require('./bb');
+
+(async () => {
+ const [cmd, a, b, c] = process.argv.slice(2);
+ const { browser, page } = await connect();
+ try {
+ if (cmd === 'goto') {
+ await page.goto(a, { waitUntil: 'domcontentloaded', timeout: 60000 });
+ await page.waitForTimeout(3500);
+ await explore(page, b || 'goto');
+ } else if (cmd === 'click') {
+ await killPopups(page);
+ let target;
+ if (a.startsWith('css=')) target = page.locator(a.slice(4)).first();
+ else target = page.locator(`button:has-text("${a}"), [role="button"]:has-text("${a}"), a:has-text("${a}"), input[type="submit"][value*="${a}"]`).first();
+ await target.click({ timeout: 15000 });
+ await page.waitForTimeout(3500);
+ await explore(page, b || 'click');
+ } else if (cmd === 'fill') {
+ await killPopups(page);
+ await page.locator(a).first().fill(b, { timeout: 15000 });
+ await page.waitForTimeout(400);
+ await explore(page, c || 'fill');
+ } else if (cmd === 'press') {
+ await page.keyboard.press(a);
+ await page.waitForTimeout(3500);
+ await explore(page, b || 'press');
+ } else if (cmd === 'eval') {
+ const out = await page.evaluate(a);
+ console.log(JSON.stringify(out, null, 1));
+ await explore(page, b || 'eval');
+ } else {
+ await explore(page, a || 'explore');
+ }
+ } finally {
+ await browser.close(); // detach only; keepAlive holds the session
+ }
+})().catch((e) => { console.error('STEP-ERR', e.message); process.exit(1); });
← 8c2d263 setup guide page at /setup.html — live status pills, copy-pa
·
back to Pattern Vault
·
setup guide + README: codify the new-Stripe-account rail (Pa 8eb308a →