← back to Commercialrealestate

scripts/discover2.js

72 lines

// discover2.js — capture Crexi's anonymous Bearer token + the UI's exact /universal-search
// request body, then replay POST /universal-search/search (token attached) with SFV filters.
const fs = require('fs');
const path = require('path');
const { chromium } = require('playwright-core');
const Browserbase = require('@browserbasehq/sdk').default;
const skillEnv = fs.readFileSync(process.env.HOME + '/.claude/skills/browserbase/.env', 'utf8');
const get = (t, k) => (t.match(new RegExp('^' + k + '=(.*)$', 'm')) || [])[1]?.replace(/['"]/g, '').trim();
const bb = new Browserbase({ apiKey: get(skillEnv, 'BROWSERBASE_API_KEY') });
const RAW = path.join(__dirname, '..', 'data', 'raw');

(async () => {
  const session = await bb.sessions.create({ projectId: get(skillEnv, 'BROWSERBASE_PROJECT_ID'),
    browserSettings: { solveCaptchas: true, viewport: { width: 1440, height: 900 } } });
  console.log('bb session', session.id);
  const browser = await chromium.connectOverCDP(session.connectUrl);
  const ctx = browser.contexts()[0];
  const page = ctx.pages()[0] || await ctx.newPage();
  page.setDefaultTimeout(60000);

  let bearer = null;
  const uiBodies = [];
  page.on('request', (req) => {
    const u = req.url();
    if (!u.includes('api.crexi.com')) return;
    const auth = req.headers()['authorization'];
    if (auth && auth.toLowerCase().startsWith('bearer') && !bearer) { bearer = auth; console.log('  got bearer token'); }
    if (req.method() === 'POST' && u.includes('universal-search')) {
      const pd = req.postData(); if (pd) uiBodies.push({ url: new URL(u).pathname, body: pd });
    }
  });

  console.log('→ warm + interact');
  await page.goto('https://www.crexi.com/properties?types[]=Multifamily', { waitUntil: 'domcontentloaded' }).catch(()=>{});
  await page.waitForTimeout(8000);
  for (let i = 0; i < 4; i++) { await page.mouse.wheel(0, 3000).catch(()=>{}); await page.waitForTimeout(2500); }
  fs.writeFileSync(path.join(RAW, 'ui-bodies.json'), JSON.stringify(uiBodies, null, 2));
  console.log(`  UI universal-search POST bodies: ${uiBodies.length}`);
  uiBodies.slice(0,3).forEach(b => console.log('   ', b.url, b.body.slice(0,300)));

  if (!bearer) { console.log('NO BEARER — aborting'); await browser.close().catch(()=>{}); process.exit(2); }

  // Replay /universal-search/search with token + SFV filters. Try several filter shapes.
  const SFV = { north: 34.33, south: 34.12, east: -118.28, west: -118.68 };
  const shapes = [
    { tag: 'types', filters: { types: ['Multifamily'] } },
    { tag: 'mapBounds', filters: { mapBounds: { northEast: { lat: SFV.north, lon: SFV.east }, southWest: { lat: SFV.south, lon: SFV.west } } } },
    { tag: 'mapBox', filters: { mapBox: SFV } },
    { tag: 'geography', filters: { geography: { boundingBox: SFV } } },
    { tag: 'searchText', filters: {}, searchText: 'Van Nuys, CA' },
  ];
  const out = [];
  for (const s of shapes) {
    const r = await page.evaluate(async ({ bearer, s }) => {
      const body = Object.assign({ filters: s.filters, searchTypes: ['Sales'], size: 5, from: 0 }, s.searchText ? { searchText: s.searchText } : {});
      const res = await fetch('https://api.crexi.com/universal-search/search', {
        method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': bearer }, body: JSON.stringify(body) });
      const txt = await res.text(); let j; try { j = JSON.parse(txt); } catch { j = null; }
      const arr = j ? (Array.isArray(j) ? j : (j.data || j.results || j.assets || j.items || [])) : [];
      return { status: res.status, total: j && (j.total || j.totalCount), count: arr.length,
        cities: arr.slice(0,5).map(a => a.locations?.[0]?.city || a.address?.city || a.city),
        sampleKeys: arr[0] ? Object.keys(arr[0]).slice(0,30) : null, err: (j && j.message) || txt.slice(0,150) };
    }, { bearer, s });
    console.log(`  [${s.tag}] status=${r.status} count=${r.count} total=${r.total} cities=${JSON.stringify(r.cities)}`);
    out.push({ tag: s.tag, ...r });
  }
  fs.writeFileSync(path.join(RAW, 'replay.json'), JSON.stringify(out, null, 2));
  fs.writeFileSync(path.join(RAW, 'bearer.txt'), bearer);
  await browser.close().catch(()=>{});
  process.exit(0);
})().catch(e => { console.error('FATAL', e); process.exit(1); });