{"version":3,"file":"serpbase.mjs","names":[],"sources":["../../src/providers/serpbase.ts"],"sourcesContent":["import type { SearchResult, SearchRequestOptions, ProviderConfig } from \"../core/types.ts\";\nimport { Provider, type ProviderSearchPage } from \"../core/provider.ts\";\nimport { SERPBASE_MAX_RESULTS, SERPBASE_SEARCH_CATEGORIES } from \"../core/providers.ts\";\nimport {\n  WebError,\n  AuthError,\n  InvalidSearchContinuationError,\n  PaymentError,\n  RateLimitError,\n  normalizeError,\n} from \"../core/errors.ts\";\n\ninterface SerpBaseSearchRequest {\n  readonly q: string;\n  readonly hl?: string;\n  readonly gl?: string;\n  readonly page?: number;\n}\n\ninterface SerpBaseResult {\n  readonly rank?: number;\n  readonly position?: number;\n  readonly title?: string;\n  readonly link?: string;\n  readonly url?: string;\n  readonly source_url?: string;\n  readonly display_url?: string;\n  readonly display_link?: string;\n  readonly snippet?: string;\n  readonly date?: string;\n  readonly published_at?: string;\n  readonly icon?: string;\n  readonly image_url?: string;\n  readonly thumbnail_url?: string;\n  readonly thumbnail?: string;\n  readonly source?: string;\n  readonly domain?: string;\n  readonly time?: string;\n  readonly duration?: string;\n}\n\ninterface SerpBaseSearchResponse {\n  readonly status: number;\n  readonly error?: string;\n  readonly request_id: string;\n  readonly elapsed_ms: number;\n  readonly credits_charged: number;\n  readonly search_type: string;\n  readonly query?: string;\n  readonly organic?: readonly SerpBaseResult[];\n  readonly images?: readonly SerpBaseResult[];\n  readonly news?: readonly SerpBaseResult[];\n  readonly videos?: readonly SerpBaseResult[];\n}\n\nexport class SerpBaseProvider extends Provider {\n  static readonly providerName = \"serpbase\";\n  static readonly defaultBaseURL = \"https://api.serpbase.dev\";\n\n  private readonly apiKey: string;\n\n  constructor(config: Readonly<ProviderConfig>) {\n    super(config, SerpBaseProvider);\n    if (!config.apiKey) {\n      throw new AuthError(\"Missing API key for SerpBase. Set SERPBASE_API_KEY\", \"serpbase\");\n    }\n\n    this.apiKey = config.apiKey;\n  }\n\n  async search(query: string, options?: SearchRequestOptions): Promise<SearchResult[]> {\n    return (await this.searchPage(query, options)).results;\n  }\n\n  async searchPage(\n    query: string,\n    options?: SearchRequestOptions,\n    continuation?: string,\n  ): Promise<ProviderSearchPage> {\n    const page = serpBasePage(continuation);\n    const endpoint = endpointForCategory(options?.category);\n    const body = {\n      q: query,\n      hl: \"en\",\n      gl: \"us\",\n      page,\n    } satisfies SerpBaseSearchRequest;\n\n    try {\n      const url = `${this.baseURL}${endpoint}`;\n      const headers = { \"X-API-Key\": this.apiKey };\n      const response = await this.client.postJSON<SerpBaseSearchResponse>(\n        url,\n        body,\n        headers,\n        options?.signal,\n      );\n      assertSerpBaseSuccess(response, url);\n      const providerResults = resultsForResponse(response);\n      return {\n        results: providerResults\n          .slice(0, clampMaxResults(options?.maxResults ?? 10))\n          .map((result) => mapResult(result, response)),\n        ...(providerResults.length > 0\n          ? { continuation: String(page + 1), continuationStatus: \"unknown\" as const }\n          : {}),\n      };\n    } catch (error) {\n      throw normalizeError(error, \"serpbase\");\n    }\n  }\n}\n\nfunction serpBasePage(continuation?: string): number {\n  if (continuation === undefined) return 1;\n  if (!/^[1-9]\\d*$/u.test(continuation)) throw new InvalidSearchContinuationError();\n  const page = Number(continuation);\n  if (!Number.isSafeInteger(page)) throw new InvalidSearchContinuationError();\n  return page;\n}\n\nfunction endpointForCategory(\n  category: string | undefined,\n): \"/google/search\" | \"/google/images\" | \"/google/news\" | \"/google/videos\" {\n  if (!isSerpBaseSearchCategory(category)) return \"/google/search\";\n  switch (category) {\n    case \"images\":\n    case \"image\":\n      return \"/google/images\";\n    case \"news\":\n      return \"/google/news\";\n    case \"videos\":\n    case \"video\":\n      return \"/google/videos\";\n    default:\n      return \"/google/search\";\n  }\n}\n\nfunction isSerpBaseSearchCategory(\n  category: string | undefined,\n): category is (typeof SERPBASE_SEARCH_CATEGORIES)[number] {\n  return SERPBASE_SEARCH_CATEGORIES.some((supported) => supported === category);\n}\n\nfunction clampMaxResults(maxResults: number): number {\n  return Math.min(Math.max(maxResults, 1), SERPBASE_MAX_RESULTS);\n}\n\n/**\n * Status 1020 in SerpBase's HTTP 200 envelope means a spent balance, not a bad key.\n * @param response - Parsed SerpBase envelope.\n * @param url - Endpoint that answered.\n */\nfunction assertSerpBaseSuccess(response: SerpBaseSearchResponse, url: string): void {\n  if (response.status === 0) return;\n\n  const message = response.error ?? `SerpBase API error: status=${response.status}`;\n  switch (response.status) {\n    case 1001:\n      throw new AuthError(`Authentication failed: ${message}`, \"serpbase\");\n    case 1029:\n      throw new RateLimitError(60);\n    case 1020:\n      throw new PaymentError(200, url, JSON.stringify(response));\n    default:\n      throw new WebError(`SerpBase API error ${response.status}: ${message}`);\n  }\n}\n\nfunction resultsForResponse(response: SerpBaseSearchResponse): readonly SerpBaseResult[] {\n  switch (response.search_type) {\n    case \"images\":\n      return response.images ?? [];\n    case \"news\":\n      return response.news ?? [];\n    case \"videos\":\n      return response.videos ?? [];\n    default:\n      return response.organic ?? [];\n  }\n}\n\nfunction mapResult(result: SerpBaseResult, response: SerpBaseSearchResponse): SearchResult {\n  return {\n    url: firstDefined(result.url, result.link, result.source_url, result.image_url) ?? \"\",\n    title: firstDefined(result.title, result.source, result.domain) ?? \"\",\n    snippet: result.snippet ?? \"\",\n    publishedDate: firstDefined(result.published_at, result.date, result.time),\n    image: firstDefined(result.image_url, result.thumbnail_url, result.thumbnail),\n    favicon: result.icon,\n    metadata: {\n      position: result.position ?? result.rank,\n      rank: result.rank,\n      displayUrl: result.display_url,\n      displayLink: result.display_link,\n      sourceUrl: result.source_url,\n      source: result.source,\n      domain: result.domain,\n      duration: result.duration,\n      searchType: response.search_type,\n      requestId: response.request_id,\n      elapsedMs: response.elapsed_ms,\n      creditsCharged: response.credits_charged,\n    },\n  };\n}\n\nfunction firstDefined<T>(...values: readonly (T | undefined)[]): T | undefined {\n  return values.find((value) => value !== undefined);\n}\n"],"mappings":"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"}