// cogeco-checker/server.js — REST API for competitor (Cogeco) serviceability. // targo-hub (3300) -> cogeco-checker (3302) -> cogeco.ca address checker // Internal only, token auth, rate-limited (real browser + reCAPTCHA upstream). const http = require('http') const url = require('url') const cogeco = require('./lib/cogeco-session') const PORT = parseInt(process.env.CHECKER_PORT || '3302') const TOKEN = process.env.CHECKER_TOKEN || '' // Serialize checks: one real browser context at a time + a small gap so we // don't hammer Cogeco (reCAPTCHA score protection). Concurrency=1 by design. const MIN_GAP_MS = parseInt(process.env.CHECKER_MIN_GAP_MS || '4000') let _chain = Promise.resolve() let _lastRun = 0 function enqueue (fn) { const run = _chain.then(async () => { const wait = Math.max(0, MIN_GAP_MS - (Date.now() - _lastRun)) if (wait) await new Promise(r => setTimeout(r, wait)) try { return await fn() } finally { _lastRun = Date.now() } }) // Keep the chain alive even if one job throws. _chain = run.catch(() => {}) return run } function json (res, data, status = 200) { res.writeHead(status, { 'Content-Type': 'application/json' }) res.end(JSON.stringify(data)) } function err (res, msg, status = 400) { json(res, { error: msg }, status) } function parseBody (req) { return new Promise((resolve, reject) => { let body = '' req.on('data', c => { body += c }) req.on('end', () => { try { resolve(body ? JSON.parse(body) : {}) } catch { reject(new Error('Invalid JSON')) } }) req.on('error', reject) }) } function checkAuth (req, res) { if (!TOKEN) return true if (req.headers['authorization'] === `Bearer ${TOKEN}`) return true err(res, 'Unauthorized', 401) return false } const server = http.createServer(async (req, res) => { const parsed = url.parse(req.url, true) const path = parsed.pathname const method = req.method res.setHeader('Access-Control-Allow-Origin', '*') res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type') if (method === 'OPTIONS') { res.writeHead(204); res.end(); return } if (path === '/health' && method === 'GET') { return json(res, { status: 'ok', uptime: process.uptime() }) } if (!checkAuth(req, res)) return try { // POST /check { address, debug? } → { available, max_download_mbps, plans, confidence } if (path === '/check' && method === 'POST') { const body = await parseBody(req) const address = (body.address || '').trim() if (!address || address.length < 5) return err(res, 'address required (min 5 chars)') const debug = !!body.debug const out = await enqueue(() => cogeco.checkAddress(address, { debug })) return json(res, out) } err(res, 'Not found', 404) } catch (e) { console.error('[cogeco-checker] error:', e) err(res, 'Internal error: ' + e.message, 500) } }) server.listen(PORT, () => { console.log(`[cogeco-checker] listening on ${PORT}, auth ${TOKEN ? 'on' : 'OFF (dev)'}, min-gap ${MIN_GAP_MS}ms`) }) for (const sig of ['SIGTERM', 'SIGINT']) { process.on(sig, async () => { console.log(`[cogeco-checker] ${sig}, shutting down`) await cogeco.shutdown() server.close() process.exit(0) }) } process.on('uncaughtException', e => console.error('[cogeco-checker] uncaught:', e))