(raw: string | null)
| 111 | // ── Main parser ── |
| 112 | |
| 113 | export function parseResolutionSource(raw: string | null): MonitorTarget { |
| 114 | if (!raw) return { type: "unmonitorable" }; |
| 115 | |
| 116 | // Try URL-based parsing first |
| 117 | try { |
| 118 | const url = new URL(raw); |
| 119 | const hostname = url.hostname.replace(/^www\./, ""); |
| 120 | |
| 121 | // Phase 1: Price feeds |
| 122 | if (hostname === "binance.com") { |
| 123 | const symbol = parseBinanceSymbol(raw); |
| 124 | if (symbol) return { type: "price_feed", provider: "binance", symbol, url: raw }; |
| 125 | } |
| 126 | if (hostname === "data.chain.link") { |
| 127 | const symbol = parseChainlinkPair(raw); |
| 128 | if (symbol) return { type: "price_feed", provider: "binance", symbol, url: raw }; |
| 129 | } |
| 130 | if (hostname === "finance.yahoo.com") { |
| 131 | const symbol = parseYahooSymbol(raw); |
| 132 | if (symbol) return { type: "price_feed", provider: "yahoo", symbol, url: raw }; |
| 133 | } |
| 134 | if (hostname === "cmegroup.com") { |
| 135 | const symbol = parseCmeSymbol(raw); |
| 136 | if (symbol) return { type: "price_feed", provider: "cme", symbol, url: raw }; |
| 137 | } |
| 138 | if (hostname === "nasdaq.com") { |
| 139 | // https://www.nasdaq.com/market-activity/stocks/aapl |
| 140 | const match = raw.match(/\/stocks\/([\w]+)/i); |
| 141 | if (match) return { type: "price_feed", provider: "yahoo", symbol: match[1].toUpperCase(), url: raw }; |
| 142 | } |
| 143 | |
| 144 | // Phase 2: Sports feeds |
| 145 | const sportEntry = SPORTS_FEED_MAP[hostname]; |
| 146 | if (sportEntry) { |
| 147 | return { type: "sports_feed", feedUrl: sportEntry.feedUrl, sport: sportEntry.sport, domain: hostname, source: sportEntry.source }; |
| 148 | } |
| 149 | } catch { |
| 150 | // Not a valid URL — fall through to text matching |
| 151 | } |
| 152 | |
| 153 | // V1 fallback: text-based org name matching |
| 154 | const lower = ` ${raw.toLowerCase()} `; |
| 155 | for (const { patterns, sourceName } of ORG_PATTERNS) { |
| 156 | for (const pattern of patterns) { |
| 157 | if (lower.includes(pattern)) { |
| 158 | const source = sourceByName.get(sourceName); |
| 159 | if (source) { |
| 160 | return { type: "known_feed", feedUrl: source.feedUrl, orgName: sourceName }; |
| 161 | } |
| 162 | } |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | return { type: "unmonitorable" }; |
| 167 | } |
no test coverage detected