import { chromium } from "playwright"; import { writeFileSync, appendFileSync } from "fs"; const LOG_FILE = "flightics_explore.log"; writeFileSync(LOG_FILE, `=== Flightics Full Exploration - ${new Date().toISOString()} ===\n\n`); function log(text) { console.log(text); appendFileSync(LOG_FILE, text + "\n"); } const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36", }); const page = await context.newPage(); const allRequests = new Map(); page.on("request", (req) => { const url = req.url(); const method = req.method(); const rt = req.resourceType(); if ((rt === "xhr" || rt === "fetch") && url.includes("flightics.com")) { const key = `${method} ${url.split("?")[0]}`; if (!allRequests.has(key)) { allRequests.set(key, { method, url, headers: req.headers(), postData: req.postData() }); log(`\n>>> ${method} ${url}`); if (req.postData()) log(` BODY: ${req.postData()}`); } } }); page.on("response", async (res) => { const req = res.request(); const url = res.url(); if ((req.resourceType() === "xhr" || req.resourceType() === "fetch") && url.includes("flightics.com")) { try { const ct = res.headers()["content-type"] || ""; if (ct.includes("json")) { const body = await res.json(); log(`<<< ${res.status()} ${url}`); log(` RESPONSE: ${JSON.stringify(body, null, 2).substring(0, 3000)}`); } else if (ct.includes("grpc")) { log(`<<< ${res.status()} ${url} [grpc-web binary]`); const buf = await res.body(); log(` RESPONSE SIZE: ${buf.length} bytes`); } } catch {} } }); log("=== PHASE 1: Homepage ==="); await page.goto("https://www.flightics.com/en"); await page.waitForTimeout(3000); log("\n=== PHASE 2: Inspiration pages ==="); // Try different inspiration endpoints const airports = ["MAD", "BCN", "AGP", "GRX", "SVQ", "VLC", "PMI", "TFS", "LPA"]; for (const apt of airports.slice(0, 3)) { try { await page.goto(`https://www.flightics.com/en/from/${apt.toLowerCase()}`); await page.waitForTimeout(2000); } catch {} } log("\n=== PHASE 3: Search with different params ==="); // One-way search await page.goto("https://www.flightics.com/en/trip/1-0-0/20-05-2026/mad/nte/0-0"); await page.waitForTimeout(5000); // Multi-city / complex await page.goto("https://www.flightics.com/en/trip/2-0-0/01-06-2026_15-06-2026/mad/nte/3-7"); await page.waitForTimeout(5000); // Explore URL patterns await page.goto("https://www.flightics.com/en/trip/1-0-0/20-05-2026/bcn/cdg/0-0?maxStops=0"); await page.waitForTimeout(5000); log("\n=== PHASE 4: Probing API endpoints ==="); // Try common API patterns const probeEndpoints = [ { method: "GET", path: "/api/v1" }, { method: "GET", path: "/api/v1/airports" }, { method: "GET", path: "/api/v1/airlines" }, { method: "GET", path: "/api/v1/routes" }, { method: "GET", path: "/api/v1/countries" }, { method: "GET", path: "/api/v1/cities" }, { method: "GET", path: "/api/v1/cheapest" }, { method: "GET", path: "/api/v1/calendar" }, { method: "GET", path: "/api/v1/prices" }, { method: "GET", path: "/api/v1/inspirations" }, { method: "GET", path: "/api/v1/inspirations/search?from=MAD&take=100&locale=en" }, { method: "GET", path: "/api/v1/inspirations/search?from=BCN&take=100&locale=en" }, { method: "GET", path: "/api/v1/inspirations/search?from=MAD,BCN,AGP,GRX,SVQ,VLC&take=100&locale=en" }, { method: "GET", path: "/api/v2" }, { method: "GET", path: "/api/v2/trips/search" }, { method: "GET", path: "/api" }, { method: "GET", path: "/_configuration" }, { method: "GET", path: "/_blazor" }, { method: "POST", path: "/api/v1/trips/search/calendar" }, { method: "POST", path: "/api/v1/trips/cheapest" }, { method: "POST", path: "/api/v1/airports/search" }, { method: "POST", path: "/api/v1/routes/search" }, { method: "GET", path: "/api/v1/config" }, { method: "GET", path: "/api/v1/settings" }, { method: "GET", path: "/api/v1/meta" }, { method: "GET", path: "/api/v1/health" }, { method: "GET", path: "/api/v1/version" }, { method: "GET", path: "/api/v1/status" }, { method: "GET", path: "/api/v1/sitemap" }, ]; // Also probe gRPC services const grpcServices = [ "trips.v1.TripService/SearchTrips", "trips.v1.TripService/GetTripDetail", "trips.v1.TripService/CheckTrip", "locations.v1.LocationsService/GetLocationsV1", "locations.v1.LocationsService/SearchLocations", "inspirations.v1.InspirationService/GetInspirationsV1", "inspirations.v1.InspirationService/SearchInspirations", "airports.v1.AirportService/GetAirports", "airports.v1.AirportService/SearchAirports", "airlines.v1.AirlineService/GetAirlines", "routes.v1.RouteService/GetRoutes", "prices.v1.PriceService/GetPrices", "calendar.v1.CalendarService/GetCalendar", ]; for (const ep of probeEndpoints) { const url = `https://www.flightics.com${ep.path}`; try { const opts = { method: ep.method, headers: { "Content-Type": "application/json" } }; if (ep.method === "POST") opts.body = JSON.stringify({}); const res = await page.evaluate(async ({ url, opts }) => { try { const r = await fetch(url, opts); const text = await r.text(); return { status: r.status, body: text.substring(0, 2000), contentType: r.headers.get("content-type") }; } catch (e) { return { status: -1, body: e.message, contentType: null }; } }, { url, opts }); if (res.status !== 404 && res.status !== -1 && res.status !== 405) { log(`\n[PROBE] ${ep.method} ${ep.path} => ${res.status} (${res.contentType})`); log(` ${res.body.substring(0, 1500)}`); } else { log(`[PROBE] ${ep.method} ${ep.path} => ${res.status}`); } } catch (e) { log(`[PROBE] ${ep.method} ${ep.path} => ERROR: ${e.message}`); } } for (const svc of grpcServices) { const url = `https://www.flightics.com/${svc}`; try { const res = await page.evaluate(async ({ url }) => { try { const r = await fetch(url, { method: "POST", headers: { "Content-Type": "application/grpc-web", "x-user-agent": "grpc-dotnet/2.76.0", "grpc-accept-encoding": "identity,gzip,deflate", }, body: new Uint8Array([0, 0, 0, 0, 0]), }); return { status: r.status, headers: Object.fromEntries(r.headers.entries()) }; } catch (e) { return { status: -1, error: e.message }; } }, { url }); const grpcStatus = res.headers?.["grpc-status"]; if (res.status === 200 || grpcStatus !== undefined) { log(`\n[gRPC] ${svc} => HTTP ${res.status}, grpc-status: ${grpcStatus || "N/A"}`); log(` Headers: ${JSON.stringify(res.headers)}`); } else { log(`[gRPC] ${svc} => ${res.status}`); } } catch (e) { log(`[gRPC] ${svc} => ERROR: ${e.message}`); } } log("\n=== PHASE 5: Try search with POST body variations ==="); const searchVariations = [ // One-way { departures: ["MAD"], local: "en", departureDateInterval: { begin: "2026-05-20T00:00:00+00:00", end: "2026-05-20T00:00:00+00:00" }, stops: [{ locations: ["NTE"], stayRange: { min: 0, max: 0 }, stayDateRange: { begin: "0001-01-01T00:00:00", end: "0001-01-01T00:00:00" }, continueFromAny: false }], endInSameLocation: false, maxStops: null, fixStopsOrder: false, stopLength: { min: 0, max: 0, isSet: false }, maxResults: 10, passengersCount: { adult: 1, child: 0, infant: 0 } }, // Anywhere destination (empty locations) { departures: ["MAD"], local: "en", departureDateInterval: { begin: "2026-06-01T00:00:00+00:00", end: "2026-06-30T00:00:00+00:00" }, stops: [{ locations: [], stayRange: { min: 48, max: 168 }, stayDateRange: { begin: "0001-01-01T00:00:00", end: "0001-01-01T00:00:00" }, continueFromAny: true }, { locations: ["MAD"], stayRange: { min: 0, max: 0 }, stayDateRange: { begin: "0001-01-01T00:00:00", end: "0001-01-01T00:00:00" }, continueFromAny: false }], endInSameLocation: false, maxStops: null, fixStopsOrder: false, stopLength: { min: 0, max: 0, isSet: false }, maxResults: 10, passengersCount: { adult: 1, child: 0, infant: 0 } }, // Multi-stop (3 cities) { departures: ["MAD"], local: "en", departureDateInterval: { begin: "2026-06-01T00:00:00+00:00", end: "2026-06-30T00:00:00+00:00" }, stops: [ { locations: ["PAR"], stayRange: { min: 48, max: 72 }, stayDateRange: { begin: "0001-01-01T00:00:00", end: "0001-01-01T00:00:00" }, continueFromAny: true }, { locations: ["ROM"], stayRange: { min: 48, max: 72 }, stayDateRange: { begin: "0001-01-01T00:00:00", end: "0001-01-01T00:00:00" }, continueFromAny: true }, { locations: ["MAD"], stayRange: { min: 0, max: 0 }, stayDateRange: { begin: "0001-01-01T00:00:00", end: "0001-01-01T00:00:00" }, continueFromAny: false }, ], endInSameLocation: false, maxStops: null, fixStopsOrder: false, stopLength: { min: 0, max: 0, isSet: false }, maxResults: 10, passengersCount: { adult: 1, child: 0, infant: 0 } }, // Direct flights only { departures: ["BCN"], local: "en", departureDateInterval: { begin: "2026-06-01T00:00:00+00:00", end: "2026-06-15T00:00:00+00:00" }, stops: [{ locations: ["LHR"], stayRange: { min: 48, max: 120 }, stayDateRange: { begin: "0001-01-01T00:00:00", end: "0001-01-01T00:00:00" }, continueFromAny: true }, { locations: ["BCN"], stayRange: { min: 0, max: 0 }, stayDateRange: { begin: "0001-01-01T00:00:00", end: "0001-01-01T00:00:00" }, continueFromAny: false }], endInSameLocation: false, maxStops: 0, fixStopsOrder: false, stopLength: { min: 0, max: 0, isSet: false }, maxResults: 10, passengersCount: { adult: 1, child: 0, infant: 0 } }, ]; for (let i = 0; i < searchVariations.length; i++) { const variant = searchVariations[i]; log(`\n--- Search variation ${i + 1} ---`); try { const res = await page.evaluate(async ({ body }) => { const r = await fetch("https://www.flightics.com/api/v1/trips/search", { method: "POST", headers: { "Content-Type": "application/json; charset=utf-8" }, body: JSON.stringify(body), }); const data = await r.json(); return { status: r.status, tripCount: data.trips?.length || 0, sample: JSON.stringify(data).substring(0, 2000) }; }, { body: variant }); log(`[SEARCH] Status: ${res.status}, Trips: ${res.tripCount}`); log(` Body sent: ${JSON.stringify(variant).substring(0, 500)}`); if (res.tripCount > 0) { log(` Sample: ${res.sample.substring(0, 1000)}`); } } catch (e) { log(`[SEARCH] ERROR: ${e.message}`); } } log("\n=== PHASE 6: Explore sitemap and other pages ==="); const pagesToVisit = [ "/en/about", "/en/privacy", "/en/terms", "/en/faq", "/en/contact", "/en/blog", "/en/destinations", "/en/airlines", "/en/airports", "/en/cheap-flights", "/en/map", "/en/calendar", "/en/explore", "/en/anywhere", "/sitemap.xml", "/robots.txt", ]; for (const p of pagesToVisit) { try { const res = await page.evaluate(async ({ url }) => { const r = await fetch(url); return { status: r.status, contentType: r.headers.get("content-type"), size: (await r.text()).length }; }, { url: `https://www.flightics.com${p}` }); if (res.status !== 404) { log(`[PAGE] ${p} => ${res.status} (${res.contentType}, ${res.size} bytes)`); } } catch {} } // Try to get the blazor boot manifest to discover API surface log("\n=== PHASE 7: Blazor manifest ==="); try { const res = await page.evaluate(async () => { const r = await fetch("https://www.flightics.com/_framework/blazor.boot.json"); if (r.ok) return await r.text(); return `Status: ${r.status}`; }); if (res.length < 5000) { log(`[BLAZOR] boot.json: ${res}`); } else { log(`[BLAZOR] boot.json: ${res.length} bytes (too large, checking for API hints...)`); // Extract interesting parts const matches = res.match(/[A-Za-z]+\.(Api|Service|Grpc|Client)[A-Za-z.]*/g); if (matches) { log(` API-related assemblies: ${[...new Set(matches)].join(", ")}`); } } } catch (e) { log(`[BLAZOR] ERROR: ${e.message}`); } log("\n\n=== SUMMARY OF UNIQUE ENDPOINTS ==="); for (const [key, val] of allRequests) { log(` ${key}`); } log("\n=== DONE ==="); await browser.close();