import express from "express"; import { chromium, type Browser, type BrowserContext, type Page } from "playwright"; const PORT = Number(process.env.PORT ?? 3003); const SESSION_TTL_MS = 3 * 60 * 1000; // 3 min // ── Singleton browser ───────────────────────────────────────────────────────── let _browser: Browser | null = null; async function getBrowser(): Promise { if (!_browser || !_browser.isConnected()) { console.log("[gst-service] Launching Chromium…"); _browser = await chromium.launch({ headless: true, args: ["--no-sandbox", "--disable-setuid-sandbox"], }); console.log("[gst-service] Chromium ready."); } return _browser; } // ── Session store ───────────────────────────────────────────────────────────── type Session = { ctx: BrowserContext; page: Page; captchaB64: string; expires: number; }; const sessions = new Map(); function makeId() { return Math.random().toString(36).slice(2) + Date.now().toString(36); } function pruneExpired() { const now = Date.now(); for (const [id, s] of sessions) { if (s.expires < now) { s.ctx.close().catch(() => {}); sessions.delete(id); } } } // ── Express app ─────────────────────────────────────────────────────────────── const app = express(); app.use(express.json()); // Allow the Pelagia app to call us (CORS) app.use((_req, res, next) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Headers", "Content-Type"); next(); }); app.get("/health", (_req, res) => res.json({ ok: true })); /** * GET /captcha * Loads the GST search page, fetches the CAPTCHA image. * Returns: { sessionId, captchaBase64 } */ app.get("/captcha", async (_req, res) => { pruneExpired(); try { const browser = await getBrowser(); const ctx = await browser.newContext({ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", viewport: { width: 1280, height: 900 }, }); const page = await ctx.newPage(); await page.goto("https://services.gst.gov.in/services/searchtp", { waitUntil: "networkidle", timeout: 30000, }); const captchaB64: string = await page.evaluate(() => fetch("/services/captcha", { headers: { Accept: "image/png,image/*" } }) .then((r) => r.blob()) .then( (blob) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve((reader.result as string).split(",")[1]); reader.onerror = reject; reader.readAsDataURL(blob); }) ) ); if (!captchaB64) throw new Error("Empty CAPTCHA response"); const sessionId = makeId(); sessions.set(sessionId, { ctx, page, captchaB64, expires: Date.now() + SESSION_TTL_MS, }); console.log(`[gst-service] Session ${sessionId} created`); res.json({ sessionId, captchaBase64: captchaB64 }); } catch (e) { console.error("[gst-service] /captcha error:", e); res.status(502).json({ error: String(e) }); } }); /** * POST /search * Body: { sessionId, gstin, captcha } * Returns taxpayer data or { error } */ app.post("/search", async (req, res) => { const { sessionId, gstin, captcha } = req.body ?? {}; if (!sessionId || !gstin || !captcha) { return res.status(400).json({ error: "sessionId, gstin and captcha are required" }); } const session = sessions.get(sessionId); if (!session || session.expires < Date.now()) { sessions.delete(sessionId); return res.status(410).json({ error: "Session expired — please fetch a new CAPTCHA." }); } try { const raw: Record = await session.page.evaluate( ([g, c]: [string, string]) => fetch("/services/api/search/tp", { method: "POST", headers: { Accept: "application/json, text/plain", "Content-Type": "application/json;charset=UTF-8", }, body: JSON.stringify({ gstin: g, captcha: c }), }) .then((r) => r.json()) .catch((e: Error) => ({ error: e.message })), [gstin, captcha] as [string, string] ); // Always close session after use session.ctx.close().catch(() => {}); sessions.delete(sessionId); console.log(`[gst-service] Session ${sessionId} closed`); if (raw.error) return res.status(502).json({ error: raw.error }); if (raw.errorCode === "SWEB_9034") return res.status(422).json({ error: "Wrong CAPTCHA — please try again." }); if (raw.errorCode) return res.status(422).json({ error: `GST portal error: ${raw.errorCode}` }); if (!raw.gstin) return res.status(422).json({ error: "No data found for that GSTIN." }); // Parse address / pincode const pradr = (raw.pradr as Record)?.adr as string ?? ""; const pincodeMatch = pradr.match(/\b(\d{6})\b/); res.json({ legalName: raw.lgnm ?? "", tradeName: raw.tradeNam ?? raw.lgnm ?? "", address: pradr, state: String(raw.stj ?? "").split(",")[0].replace(/^State\s*-\s*/i, ""), pincode: pincodeMatch?.[1] ?? "", gstin: raw.gstin, status: raw.sts ?? "", businessType: raw.ctb ?? raw.dty ?? "", registrationDate: raw.rgdt ?? "", }); } catch (e) { console.error("[gst-service] /search error:", e); res.status(500).json({ error: String(e) }); } }); app.listen(PORT, () => { console.log(`[gst-service] Listening on http://localhost:${PORT}`); });