feat(gst): replace API-key lookup with Playwright microservice

Problem: GST portal's public taxpayer search (services.gst.gov.in/
searchtp) now requires human CAPTCHA verification but no login.
The BIG-IP WAF blocks direct Node.js HTTP clients via TLS
fingerprinting; Playwright (real Chromium) bypasses it successfully.
Confirmed working: GSTIN 27AAHCP5787B1Z6 → full PELAGIA MARINE
SERVICES data including address, jurisdiction, filing status.

GstService/ (new standalone microservice):
- src/index.ts: Express + Playwright singleton browser
  GET  /health  → { ok: true }
  GET  /captcha → launches browser, loads GST portal, fetches
                  CAPTCHA image from same origin (sets CaptchaCookie),
                  stores BrowserContext in session map (3 min TTL)
                  → { sessionId, captchaBase64 }
  POST /search  → { sessionId, gstin, captcha } → submits form
                  via page.evaluate fetch() using live browser session,
                  closes context, returns parsed taxpayer data
- package.json, tsconfig.json, npm install
- src/test-lookup.ts: interactive CLI test (prompted user for captcha)

App changes:
- Remove playwright dep from Next.js app (was incorrectly added)
- Remove lib/gst-lookup.ts (sandbox.co.in placeholder — unused)
- Remove lib/gst-browser.ts (Playwright singleton — moved to service)
- app/api/gst/captcha/route.ts: thin proxy → GST_SERVICE_URL/captcha
- app/api/gst/route.ts: thin proxy POST → GST_SERVICE_URL/search
- vendor-form.tsx: two-step captcha UI
    Step 1: "Look up" → calls /api/gst/captcha → shows PNG inline
    Step 2: user types 6 digits → "Verify" → calls /api/gst → fills
            form (name, address, lat/lng from Nominatim geocoding)
    Wrong captcha → SWEB_9034 error with retry option
- .env.example: GST_SERVICE_URL=http://localhost:3002

Start the microservice: cd GstService && npm run dev

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hardik 2026-05-14 13:27:15 +05:30
parent bea798324c
commit f372fae953
15 changed files with 2038 additions and 96 deletions

View file

@ -34,3 +34,9 @@ R2_PUBLIC_URL=https://your-bucket.your-account.r2.cloudflarestorage.com
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx
EMAIL_FROM=noreply@pelagiaportal.com EMAIL_FROM=noreply@pelagiaportal.com
EMAIL_FROM_NAME="Pelagia Portal" EMAIL_FROM_NAME="Pelagia Portal"
# ── GST Lookup microservice ───────────────────────────────────
# Run the GstService/ microservice alongside the app.
# Development default (localhost:3002) is used if this is unset.
# Start the service with: cd GstService && npm run dev
GST_SERVICE_URL=http://localhost:3002

View file

@ -13,7 +13,11 @@ type VendorRow = {
const INPUT = "w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20"; const INPUT = "w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20";
type GstData = { legalName: string; tradeName: string; address: string; state: string; pincode: string; lat: number | null; lng: number | null; error?: string }; type GstResult = {
legalName: string; tradeName: string; address: string; state: string;
pincode: string; gstin: string; status: string; businessType: string;
registrationDate: string; lat: number | null; lng: number | null;
};
function VendorFormFields({ vendor }: { vendor?: VendorRow }) { function VendorFormFields({ vendor }: { vendor?: VendorRow }) {
const [gstin, setGstin] = useState(vendor?.gstin ?? ""); const [gstin, setGstin] = useState(vendor?.gstin ?? "");
@ -21,39 +25,131 @@ function VendorFormFields({ vendor }: { vendor?: VendorRow }) {
const [address, setAddress] = useState(vendor?.address ?? ""); const [address, setAddress] = useState(vendor?.address ?? "");
const [lat, setLat] = useState(""); const [lat, setLat] = useState("");
const [lng, setLng] = useState(""); const [lng, setLng] = useState("");
const [looking, setLooking] = useState(false);
const [gstError, setGstError] = useState("");
async function lookupGst() { // CAPTCHA flow state
const [captchaStep, setCaptchaStep] = useState<"idle" | "loading" | "ready" | "verifying">("idle");
const [captchaB64, setCaptchaB64] = useState("");
const [captchaAnswer, setCaptchaAnswer] = useState("");
const [sessionId, setSessionId] = useState("");
const [gstError, setGstError] = useState("");
const [gstSuccess, setGstSuccess] = useState("");
async function fetchCaptcha() {
if (gstin.length < 15) { setGstError("Enter a valid 15-character GSTIN"); return; } if (gstin.length < 15) { setGstError("Enter a valid 15-character GSTIN"); return; }
setLooking(true); setGstError(""); setCaptchaStep("loading"); setGstError(""); setGstSuccess(""); setCaptchaAnswer("");
try { try {
const res = await fetch(`/api/gst?gstin=${encodeURIComponent(gstin)}`); const res = await fetch("/api/gst/captcha");
const data: GstData = await res.json(); const data = await res.json();
if (data.error) { setGstError(data.error); return; } if (data.error) { setGstError(data.error); setCaptchaStep("idle"); return; }
if (!name) setName(data.tradeName || data.legalName); setCaptchaB64(data.captchaB64);
setAddress([data.address, data.state, data.pincode].filter(Boolean).join(", ")); setSessionId(data.sessionId);
setCaptchaStep("ready");
} catch { setGstError("Failed to load CAPTCHA"); setCaptchaStep("idle"); }
}
async function submitSearch() {
if (!captchaAnswer || captchaAnswer.length !== 6) { setGstError("Enter the 6-digit CAPTCHA"); return; }
setCaptchaStep("verifying"); setGstError("");
try {
const res = await fetch("/api/gst", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ gstin, captcha: captchaAnswer, sessionId }),
});
const data: GstResult & { error?: string } = await res.json();
if (data.error) {
setGstError(data.error);
// Wrong captcha → allow retry with fresh captcha
if (data.error.includes("CAPTCHA") || data.error.includes("captcha")) {
setCaptchaStep("idle");
} else {
setCaptchaStep("idle");
}
return;
}
// Fill form fields
setName(data.tradeName || data.legalName);
setAddress(data.address);
if (data.lat) setLat(String(data.lat)); if (data.lat) setLat(String(data.lat));
if (data.lng) setLng(String(data.lng)); if (data.lng) setLng(String(data.lng));
} catch { setGstError("Lookup failed — check network and API key"); } setGstSuccess(`${data.legalName}${data.status} since ${data.registrationDate}`);
finally { setLooking(false); } setCaptchaStep("idle");
} catch { setGstError("Lookup failed"); setCaptchaStep("idle"); }
} }
return ( return (
<div className="space-y-3"> <div className="space-y-3">
{/* GSTIN + captcha flow */}
<div> <div>
<label className="block text-xs font-medium text-neutral-700 mb-1"> <label className="block text-xs font-medium text-neutral-700 mb-1">
GSTIN <span className="text-neutral-400 font-normal">(recommended auto-fills name, address & location)</span> GSTIN <span className="text-neutral-400 font-normal">(auto-fills name, address & location from GST portal)</span>
</label> </label>
<div className="flex gap-2"> <div className="flex gap-2">
<input name="gstin" value={gstin} onChange={(e) => setGstin(e.target.value.toUpperCase())} <input
className={INPUT + " font-mono tracking-widest"} placeholder="e.g. 27AAACG1840M1ZL" maxLength={15} /> name="gstin"
<button type="button" onClick={lookupGst} disabled={looking || gstin.length < 15} value={gstin}
className="shrink-0 rounded-lg border border-primary-300 bg-primary-50 px-3 py-2 text-sm font-medium text-primary-700 hover:bg-primary-100 disabled:opacity-50 whitespace-nowrap"> onChange={(e) => { setGstin(e.target.value.toUpperCase()); setCaptchaStep("idle"); setGstError(""); setGstSuccess(""); }}
{looking ? "Looking up…" : "Look up"} className={INPUT + " font-mono tracking-widest"}
placeholder="e.g. 27AAHCP5787B1Z6"
maxLength={15}
/>
<button
type="button"
onClick={fetchCaptcha}
disabled={captchaStep === "loading" || captchaStep === "verifying" || gstin.length < 15}
className="shrink-0 rounded-lg border border-primary-300 bg-primary-50 px-3 py-2 text-sm font-medium text-primary-700 hover:bg-primary-100 disabled:opacity-50 whitespace-nowrap"
>
{captchaStep === "loading" ? "Loading…" : "Look up"}
</button> </button>
</div> </div>
{/* CAPTCHA challenge */}
{captchaStep === "ready" && captchaB64 && (
<div className="mt-2 rounded-lg border border-neutral-200 bg-neutral-50 p-3 space-y-2">
<p className="text-xs text-neutral-600">Enter the code shown in the image to verify with the GST portal:</p>
<img
src={`data:image/png;base64,${captchaB64}`}
alt="CAPTCHA"
className="rounded border border-neutral-200 bg-white"
style={{ imageRendering: "pixelated", height: 48 }}
/>
<div className="flex gap-2 items-center">
<input
type="text"
inputMode="numeric"
maxLength={6}
value={captchaAnswer}
onChange={(e) => setCaptchaAnswer(e.target.value.replace(/\D/g, ""))}
placeholder="6 digits"
className="w-28 rounded-lg border border-neutral-300 px-3 py-1.5 text-sm font-mono tracking-widest focus:border-primary-500 focus:outline-none"
autoFocus
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); submitSearch(); } }}
/>
<button
type="button"
onClick={submitSearch}
disabled={captchaAnswer.length !== 6}
className="rounded-lg bg-primary-600 px-3 py-1.5 text-sm font-semibold text-white hover:bg-primary-700 disabled:opacity-50"
>
Verify
</button>
<button
type="button"
onClick={fetchCaptcha}
className="text-xs text-neutral-500 hover:underline"
>
New image
</button>
</div>
</div>
)}
{captchaStep === "verifying" && (
<p className="mt-1 text-xs text-neutral-500">Verifying with GST portal</p>
)}
{gstError && <p className="mt-1 text-xs text-danger-600">{gstError}</p>} {gstError && <p className="mt-1 text-xs text-danger-600">{gstError}</p>}
{gstSuccess && <p className="mt-1 text-xs text-success-700">{gstSuccess}</p>}
</div> </div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">

View file

@ -0,0 +1,18 @@
import { auth } from "@/auth";
import { NextResponse } from "next/server";
const GST_SERVICE = process.env.GST_SERVICE_URL ?? "http://localhost:3002";
/** Proxy: load GST portal page + fetch CAPTCHA → { sessionId, captchaBase64 } */
export async function GET() {
const session = await auth();
if (!session?.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
try {
const res = await fetch(`${GST_SERVICE}/captcha`, { cache: "no-store" });
const data = await res.json();
return NextResponse.json(data, { status: res.ok ? 200 : 502 });
} catch (e) {
return NextResponse.json({ error: `GST service unavailable: ${String(e)}` }, { status: 502 });
}
}

View file

@ -1,27 +1,28 @@
import { auth } from "@/auth"; import { auth } from "@/auth";
import { lookupGstin } from "@/lib/gst-lookup";
import { geocodePincode } from "@/lib/geo";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
export async function GET(req: NextRequest) { const GST_SERVICE = process.env.GST_SERVICE_URL ?? "http://localhost:3002";
/** Proxy: submit GSTIN + captcha answer → taxpayer data */
export async function POST(req: NextRequest) {
const session = await auth(); const session = await auth();
if (!session?.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); if (!session?.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const gstin = req.nextUrl.searchParams.get("gstin")?.trim().toUpperCase() ?? ""; const body = await req.json();
if (!/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/.test(gstin)) { if (!body.sessionId || !body.gstin || !body.captcha) {
return NextResponse.json({ error: "Invalid GSTIN format" }, { status: 400 }); return NextResponse.json({ error: "sessionId, gstin and captcha are required" }, { status: 400 });
} }
const result = await lookupGstin(gstin); try {
if ("error" in result) return NextResponse.json(result, { status: 422 }); const res = await fetch(`${GST_SERVICE}/search`, {
method: "POST",
// Geocode the pincode to get coordinates headers: { "Content-Type": "application/json" },
let lat: number | null = null; body: JSON.stringify(body),
let lng: number | null = null; cache: "no-store",
if (result.pincode) { });
const geo = await geocodePincode(result.pincode); const data = await res.json();
if (geo) { lat = geo.lat; lng = geo.lng; } return NextResponse.json(data, { status: res.ok ? 200 : res.status });
} catch (e) {
return NextResponse.json({ error: `GST service unavailable: ${String(e)}` }, { status: 502 });
} }
return NextResponse.json({ ...result, lat, lng });
} }

View file

@ -1,61 +0,0 @@
/**
* AbhiAPI GST verification.
* Env: ABHIAPI_KEY
*/
export type GstLookupResult = {
legalName: string;
tradeName: string;
address: string;
state: string;
pincode: string;
status: string;
businessType: string;
};
const STATE_CODES: Record<string, string> = {
"01": "Jammu & Kashmir", "02": "Himachal Pradesh", "03": "Punjab",
"04": "Chandigarh", "05": "Uttarakhand", "06": "Haryana",
"07": "Delhi", "08": "Rajasthan", "09": "Uttar Pradesh",
"10": "Bihar", "11": "Sikkim", "12": "Arunachal Pradesh",
"13": "Nagaland", "14": "Manipur", "15": "Mizoram",
"16": "Tripura", "17": "Meghalaya", "18": "Assam",
"19": "West Bengal", "20": "Jharkhand", "21": "Odisha",
"22": "Chhattisgarh", "23": "Madhya Pradesh", "24": "Gujarat",
"26": "Dadra & Nagar Haveli", "27": "Maharashtra", "28": "Andhra Pradesh",
"29": "Karnataka", "30": "Goa", "31": "Lakshadweep",
"32": "Kerala", "33": "Tamil Nadu", "34": "Puducherry",
"35": "Andaman & Nicobar", "36": "Telangana", "37": "Andhra Pradesh (new)",
};
export function parseGstinState(gstin: string): string {
return STATE_CODES[gstin.substring(0, 2)] ?? "Unknown";
}
export async function lookupGstin(gstin: string): Promise<GstLookupResult | { error: string }> {
const apiKey = process.env.ABHIAPI_KEY;
if (!apiKey) return { error: "GST lookup API key not configured (ABHIAPI_KEY)" };
try {
const res = await fetch(`https://api.abhiapi.com/gst/search?gstin=${encodeURIComponent(gstin)}`, {
headers: { "x-api-key": apiKey },
next: { revalidate: 3600 },
});
const json = await res.json();
if (!res.ok || !json.success) {
return { error: json.message ?? "GST lookup failed" };
}
const d = json.data;
return {
legalName: d.legalName ?? d.lgnm ?? "",
tradeName: d.tradeName ?? d.tradeNam ?? d.legalName ?? "",
address: d.address ?? d.pradr?.adr ?? "",
state: d.state ?? parseGstinState(gstin),
pincode: d.pincode ?? "",
status: d.status ?? d.sts ?? "Unknown",
businessType: d.businessType ?? d.dty ?? "",
};
} catch (e) {
return { error: String(e) };
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

1559
GstService/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

21
GstService/package.json Normal file
View file

@ -0,0 +1,21 @@
{
"name": "gst-service",
"version": "1.0.0",
"description": "GST portal CAPTCHA proxy — fetches taxpayer details from services.gst.gov.in via Playwright",
"main": "dist/index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"express": "^4.18.2",
"playwright": "^1.49.0"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^22.0.0",
"tsx": "^4.19.2",
"typescript": "^5.7.2"
}
}

182
GstService/src/index.ts Normal file
View file

@ -0,0 +1,182 @@
import express from "express";
import { chromium, type Browser, type BrowserContext, type Page } from "playwright";
const PORT = Number(process.env.PORT ?? 3002);
const SESSION_TTL_MS = 3 * 60 * 1000; // 3 min
// ── Singleton browser ─────────────────────────────────────────────────────────
let _browser: Browser | null = null;
async function getBrowser(): Promise<Browser> {
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<string, Session>();
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<string>((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<string, unknown> = 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<string, unknown>)?.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}`);
});

View file

@ -0,0 +1,108 @@
/**
* Interactive GST taxpayer lookup via services.gst.gov.in/services/searchtp
* Run: npx tsx scripts/test-gst-scrape.ts <GSTIN>
*
* Flow:
* 1. Load the search page (establishes session)
* 2. Fetch the CAPTCHA image save to scripts/captcha.png
* 3. Prompt you to open the image and type the 6 digits
* 4. Submit GSTIN + captcha print the result
*/
import { chromium } from "playwright";
import * as fs from "fs";
import * as readline from "readline";
const GSTIN = (process.argv[2] ?? "").toUpperCase();
if (!GSTIN) { console.error("Usage: npx tsx scripts/test-gst-scrape.ts <GSTIN>"); process.exit(1); }
function ask(q: string): Promise<string> {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
return new Promise(resolve => rl.question(q, a => { rl.close(); resolve(a.trim()); }));
}
(async () => {
console.log(`\nGSTIN: ${GSTIN}\n`);
const browser = await chromium.launch({ headless: true });
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();
// ── Step 1: load search page ──────────────────────────────────────────────
process.stdout.write("Loading GST portal… ");
await page.goto("https://services.gst.gov.in/services/searchtp", {
waitUntil: "networkidle",
timeout: 30000,
});
console.log("done.");
// ── Step 2: fetch captcha from same origin (sets CaptchaCookie) ───────────
process.stdout.write("Fetching CAPTCHA… ");
const captchaB64: string = await page.evaluate(() =>
fetch("/services/captcha", { headers: { Accept: "image/png,image/*" } })
.then(r => r.blob())
.then(blob => new Promise<string>((res, rej) => {
const reader = new FileReader();
reader.onload = () => res((reader.result as string).split(",")[1]);
reader.onerror = rej;
reader.readAsDataURL(blob);
}))
);
const imgPath = "scripts/captcha.png";
fs.writeFileSync(imgPath, Buffer.from(captchaB64, "base64"));
console.log(`saved → ${imgPath}`);
const cookies = await ctx.cookies("https://services.gst.gov.in");
const capCookie = cookies.find(c => c.name === "CaptchaCookie");
console.log(`CaptchaCookie: ${capCookie?.value ?? "NOT SET"}`);
// ── Step 3: ask for captcha answer ────────────────────────────────────────
console.log("\nOpen scripts/captcha.png and read the 6-digit number.");
const captcha = await ask("Enter CAPTCHA (6 digits): ");
if (!/^\d{6}$/.test(captcha)) {
console.error("Expected exactly 6 digits. Got:", captcha);
await browser.close();
process.exit(1);
}
// ── Step 4: submit search ─────────────────────────────────────────────────
process.stdout.write(`\nSubmitting { gstin: "${GSTIN}", captcha: "${captcha}" }… `);
const result: { status: number; body: unknown } = await page.evaluate(
([gstin, cap]: [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, captcha: cap }),
})
.then(async r => ({ status: r.status, body: await r.json().catch(() => r.text()) }))
.catch((e: Error) => ({ status: 0, body: { error: e.message } })),
[GSTIN, captcha] as [string, string]
);
console.log("done.\n");
console.log("=== Response ===");
console.log(JSON.stringify(result.body, null, 2));
// If wrong captcha, SWEB_9034; if GSTIN not found, different code; on success → data
const body = result.body as Record<string, unknown>;
if (body.errorCode === "SWEB_9034") {
console.log("\n→ Wrong CAPTCHA. Re-run to get a fresh image.");
} else if (body.errorCode === "SWEB_9000") {
console.log("\n→ SWEB_9000 (session/auth issue — not a captcha problem).");
} else if (body.errorCode) {
console.log(`\n→ Error code: ${body.errorCode}`);
} else {
console.log("\n✅ Success — taxpayer data above.");
}
await browser.close();
})().catch(e => { console.error("\nError:", e.message); process.exit(1); });

12
GstService/tsconfig.json Normal file
View file

@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "node",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}