import type { HttpClient } from "@market-atlas/connector-sdk"; import { redactUrl, schemaFingerprint } from "@market-atlas/connector-sdk"; /** * Discovery prototype (network-level, no browser yet): fetches a public page unauthenticated, * inventories the data endpoints it references (JSON/XHR URLs, WebSocket URLs, SSE, embedded * JSON state such as __NEXT_DATA__), and scores which ones look like market-data feeds. * Output is a *candidate* report for the rights-review workflow — never an activated connector. */ export interface DiscoveryReport { url: string; status: number; content_type: string | null; bytes: number; websocket_urls: string[]; sse_urls: string[]; json_endpoints: Array<{ url: string; score: number; hints: string[] }>; embedded_state: Array<{ kind: string; fingerprint: string; price_like_fields: string[]; symbol_like_fields: string[]; sample_keys: string[] }>; instrument_hints: string[]; notes: string[]; } const PRICE_KEYS = /^(last|lastprice|last_price|price|lp|px|close|c|regularmarketprice|current_price|bid|ask|mid|value|rate)$/i; const SYMBOL_KEYS = /^(symbol|sym|ticker|s|code|instrument|isin|pair|product_id|instid)$/i; const TS_KEYS = /^(ts|t|time|timestamp|datetime|eventtime|updated|last_trade_time)$/i; export async function runDiscovery(url: string, http: HttpClient): Promise { const res = await http.getText(url, { timeoutMs: 20_000, conditional: false, headers: { accept: "text/html,application/json;q=0.9,*/*;q=0.8" } }); const html = res.text; const notes: string[] = []; const wsUrls = uniq([...html.matchAll(/wss?:\/\/[^\s"'<>\\)]+/g)].map((m) => redactUrl(m[0]))); const sse = uniq([...html.matchAll(/EventSource\((["'`])([^"'`]+)\1/g)].map((m) => m[2]!)); const urls = uniq([...html.matchAll(/https?:\/\/[^\s"'<>\\)]+/g)].map((m) => m[0])) .filter((u) => /api|quote|price|market|ticker|chart|data|feed|graphql|json|v\d\//i.test(u) && !/\.(png|jpg|jpeg|gif|svg|css|woff2?|ico)(\?|$)/i.test(u)) .map((u) => redactUrl(u)); const jsonEndpoints = urls .map((u) => { const hints: string[] = []; let score = 0; if (/quote|price|ticker|last/i.test(u)) (score += 40), hints.push("quote/price keyword"); if (/graphql/i.test(u)) (score += 20), hints.push("GraphQL"); if (/\.json(\?|$)|format=json/i.test(u)) (score += 15), hints.push("JSON"); if (/api\.|\/api\//i.test(u)) (score += 15), hints.push("API path"); if (/chart|history|candles|bars/i.test(u)) (score += 10), hints.push("historical/chart"); if (/symbol=|ticker=|sym=/i.test(u)) (score += 20), hints.push("symbol parameter"); return { url: u, score, hints }; }) .filter((e) => e.score > 0) .sort((a, b) => b.score - a.score) .slice(0, 40); const embedded: DiscoveryReport["embedded_state"] = []; const stateBlocks: Array<[string, RegExp]> = [ ["__NEXT_DATA__", /