[object Object]

← 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

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 →