← back to Google Places Key Provisioner

all-in-one.js

140 lines

#!/usr/bin/env node
/**
 * One-shot Google Places API key provisioner via Browserbase.
 * Creates a session, navigates to GCP, prints live URL, polls every 10s
 * for "user has logged in" (URL no longer on accounts.google.com),
 * then drives the rest: project create → enable Places → create key → extract.
 *
 * Run:  node all-in-one.js
 * Times out after 6 minutes if user never logs in.
 */
const Browserbase = require('@browserbasehq/sdk').default;
const { chromium } = require('playwright-core');
const fs = require('fs');
const path = require('path');
require('dotenv').config({ path: require('os').homedir() + '/.claude/skills/browserbase/.env' });

(async () => {
  const bb = new Browserbase({ apiKey: process.env.BROWSERBASE_API_KEY });
  const session = await bb.sessions.create({
    projectId: process.env.BROWSERBASE_PROJECT_ID,
    keepAlive: true,
    proxies: false,
  });
  const live = `https://www.browserbase.com/devtools-fullscreen/inspector.html?wss=connect.browserbase.com/debug/${session.id}/devtools/browser/`;
  console.log('SESSION_ID  =', session.id);
  console.log('LIVE_URL    =', live);
  fs.writeFileSync(path.join(__dirname, 'session.json'),
    JSON.stringify({ id: session.id, connectUrl: session.connectUrl, debugUrl: live }, null, 2));

  const browser = await chromium.connectOverCDP(session.connectUrl);
  const ctx = browser.contexts()[0];
  const page = ctx.pages()[0] || await ctx.newPage();
  await page.goto('https://console.cloud.google.com/projectcreate', { waitUntil: 'domcontentloaded', timeout: 60000 });
  console.log('parked at:', page.url());
  console.log('\n→ OPEN THE LIVE_URL ABOVE AND LOG IN. I will poll every 10s for up to 6 min.');

  // Poll for logged-in state
  let loggedIn = false;
  for (let i = 0; i < 36; i++) {
    await page.waitForTimeout(10000);
    const url = page.url();
    process.stdout.write(`  [${i+1}/36] url=${url.slice(0,80)}\n`);
    if (!url.includes('accounts.google.com') && url.includes('console.cloud.google.com')) {
      loggedIn = true;
      console.log('  ✓ logged in detected');
      break;
    }
  }
  if (!loggedIn) {
    console.error('TIMEOUT — did not detect login within 6 min. Session left alive at', live);
    process.exit(1);
  }

  // STEP 1: project create
  console.log('\n--- STEP 1: create project ---');
  try {
    await page.goto('https://console.cloud.google.com/projectcreate', { waitUntil: 'domcontentloaded', timeout: 30000 });
    await page.waitForTimeout(4000);
    const projectName = 'lacountyeats-' + Date.now().toString(36).slice(-5);
    const nameInput = page.locator('input[aria-label*="roject name" i]').first();
    await nameInput.waitFor({ timeout: 10000 });
    await nameInput.click();
    await nameInput.fill('');
    await nameInput.fill(projectName);
    console.log('  filled project name:', projectName);
    await page.waitForTimeout(1500);
    const createBtn = page.locator('button:has-text("Create"), button:has-text("CREATE")').first();
    await createBtn.click();
    console.log('  clicked Create — waiting 25s for provision…');
    await page.waitForTimeout(25000);
    console.log('  url after create:', page.url());
  } catch (e) {
    console.error('  step1 error:', e.message);
  }

  // STEP 2: enable Places API
  console.log('\n--- STEP 2: enable Places API ---');
  try {
    await page.goto('https://console.cloud.google.com/apis/library/places-backend.googleapis.com', { waitUntil: 'domcontentloaded', timeout: 30000 });
    await page.waitForTimeout(7000);
    // Try several enable-button shapes
    const enableSelectors = [
      'button:has-text("Enable")',
      'button:has-text("ENABLE")',
      'cfc-progress-button button',
    ];
    let clicked = false;
    for (const sel of enableSelectors) {
      const btn = page.locator(sel).first();
      if (await btn.isVisible({ timeout: 3000 }).catch(() => false)) {
        const txt = await btn.textContent();
        if (txt && txt.toLowerCase().includes('enable')) {
          await btn.click();
          console.log('  clicked Enable — waiting 20s…');
          await page.waitForTimeout(20000);
          clicked = true;
          break;
        }
      }
    }
    if (!clicked) console.log('  Places API likely already enabled (no Enable button found)');
  } catch (e) {
    console.error('  step2 error:', e.message);
  }

  // STEP 3: create API key
  console.log('\n--- STEP 3: create API key ---');
  let keyValue = null;
  try {
    await page.goto('https://console.cloud.google.com/apis/credentials', { waitUntil: 'domcontentloaded', timeout: 30000 });
    await page.waitForTimeout(7000);
    // Try header "+ Create credentials" button
    const cc = page.locator('button:has-text("Create credentials"), button:has-text("CREATE CREDENTIALS")').first();
    await cc.waitFor({ timeout: 10000 });
    await cc.click();
    await page.waitForTimeout(2000);
    // Menu item "API key"
    const apiItem = page.locator('text=/^API key$/i').first();
    await apiItem.click();
    console.log('  clicked Create Credentials → API key — waiting for modal…');
    await page.waitForTimeout(10000);
    // Extract key value
    const html = await page.content();
    const m = html.match(/AIzaSy[A-Za-z0-9_-]{30,}/);
    if (m) {
      keyValue = m[0];
      console.log('\n=== KEY ===\n' + keyValue + '\n=== /KEY ===');
      fs.writeFileSync(path.join(__dirname, 'key.txt'), keyValue + '\n', { mode: 0o600 });
    } else {
      console.error('  could not extract key from modal — open live URL and copy manually');
    }
  } catch (e) {
    console.error('  step3 error:', e.message);
  }

  console.log('\nLive URL still alive for manual cleanup:', live);
  console.log('Done.');
  // Don't close — leave session alive for manual inspection if needed
})();