← back to Rentv Adintel

src/search/provider.js

128 lines

'use strict';
/**
 * SearchProvider interface (spec §9, §14).
 *
 * getSearchProvider(name) returns a provider for one of:
 *   google_cse | brave | bing | serper | manual
 * Default comes from process.env.SEARCH_PROVIDER (falls back to 'manual').
 *
 * Real API providers are thin stubs: they read their env key and throw a
 * friendly "configure X" error if it is absent — the app must WORK with the
 * manual provider when no key is configured (§9: "Do not scrape Google result
 * HTML. The app must work with manual search links when no API key is
 * configured.").
 *
 * The `manual` provider returns one-click SEARCH URLS (google/bing web search)
 * and NEVER fetches. buildLinkedInSearchUrls() returns Google/Bing search URLs
 * using site:linkedin.com/in and site:linkedin.com/company templates — these
 * are HUMAN-CLICK URLs only; the app must NEVER fetch linkedin.com (§6.3/§6.4).
 */

const T = require('../../lib/types');
const queries = require('./queries');

function googleSearchUrl(query) {
  return `https://www.google.com/search?q=${encodeURIComponent(query)}`;
}
function bingSearchUrl(query) {
  return `https://www.bing.com/search?q=${encodeURIComponent(query)}`;
}

/**
 * Manual provider — never fetches. search() returns click-URLs the human opens.
 */
const manualProvider = {
  name: 'manual',
  fetches: false,
  async search(query, _opts = {}) {
    return {
      provider: 'manual',
      query,
      urls: {
        google: googleSearchUrl(query),
        bing: bingSearchUrl(query),
      },
      note: 'Manual mode: open one of these URLs in a browser. The app does not fetch results.',
    };
  },
};

/** Factory for the API-key-gated stub providers. */
function apiStub(name, envKey, humanLabel) {
  return {
    name,
    fetches: true,
    async search(_query, _opts = {}) {
      if (!process.env[envKey]) {
        throw new Error(
          `Search provider "${name}" is not configured — set ${envKey} to enable it, or use SEARCH_PROVIDER=manual (§9).`
        );
      }
      // NOTE: real HTTP calls to the provider's OFFICIAL API go here. They must
      // use the official search API (§6.20) — never scrape result HTML — and any
      // outbound fetch must go through lib/compliance/fetch-guard.safeFetch.
      throw new Error(
        `Search provider "${name}" (${humanLabel}) API call not yet implemented — key present but live query wiring pending.`
      );
    },
  };
}

const PROVIDERS = {
  manual: () => manualProvider,
  google_cse: () => apiStub('google_cse', 'GOOGLE_CSE_API_KEY', 'Google Programmable Search'),
  brave: () => apiStub('brave', 'BRAVE_SEARCH_API_KEY', 'Brave Search'),
  bing: () => apiStub('bing', 'BING_SEARCH_API_KEY', 'Bing Web Search'),
  serper: () => apiStub('serper', 'SERPER_API_KEY', 'Serper.dev'),
};

/** getSearchProvider(name) — resolves name → provider, defaulting via env. */
function getSearchProvider(name) {
  const chosen = name || process.env.SEARCH_PROVIDER || 'manual';
  if (!T.SEARCH_PROVIDERS.includes(chosen)) {
    throw new Error(
      `getSearchProvider: unknown provider "${chosen}" — must be one of ${T.SEARCH_PROVIDERS.join(', ')}`
    );
  }
  return PROVIDERS[chosen]();
}

/**
 * buildLinkedInSearchUrls(org) — returns Google/Bing SEARCH URLs (human-click
 * only) for the three §14 LinkedIn templates. The app NEVER fetches linkedin.com;
 * these open a normal browser tab for a person to inspect the public profile.
 *
 * org: { companyName, companyDomain }
 */
function buildLinkedInSearchUrls(org = {}) {
  const companyQ = queries.linkedinCompanyQuery(org);
  const inCaQ = queries.linkedinInCaliforniaQuery(org);
  const inAzQ = queries.linkedinInArizonaQuery(org);
  return {
    company: { query: companyQ, google: googleSearchUrl(companyQ), bing: bingSearchUrl(companyQ) },
    peopleCalifornia: { query: inCaQ, google: googleSearchUrl(inCaQ), bing: bingSearchUrl(inCaQ) },
    peopleArizona: { query: inAzQ, google: googleSearchUrl(inAzQ), bing: bingSearchUrl(inAzQ) },
    note: 'Human-click search URLs only. The application must NEVER fetch linkedin.com (§6.3/§6.4).',
  };
}

/**
 * openLinkedInProfileUrl(url) — passthrough. Returns the URL for a human to
 * open in a normal browser (the "Open LinkedIn" button, §14). Deliberately
 * does NOT fetch; it only validates the shape and hands the URL back.
 */
function openLinkedInProfileUrl(url) {
  if (!url || typeof url !== 'string') {
    throw new Error('openLinkedInProfileUrl: a url string is required');
  }
  return url; // for a human to open — never fetched by the app
}

module.exports = {
  getSearchProvider,
  buildLinkedInSearchUrls,
  openLinkedInProfileUrl,
  googleSearchUrl,
  bingSearchUrl,
};