← 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,
};