import fs from "node:fs/promises"; import path from "node:path"; function domainMatches(hostname, cookieDomain) { const normalized = String(cookieDomain || "").replace(/^\./, "").toLowerCase(); const host = String(hostname || "").toLowerCase(); if (!normalized || !host) return false; if (normalized === host) return true; if (normalized === "localhost") return host === "localhost"; return host.endsWith(`.${normalized}`); } export function normalizeSameSite(value) { const normalized = String(value || "").trim().toLowerCase(); if (normalized === "strict") return "Strict"; if (normalized === "none") return "None"; return "Lax"; } export function normalizeCookie(cookie) { if (!cookie?.name || !cookie?.domain) return null; return { path: "/", expires: -1, httpOnly: false, secure: false, sameSite: "Lax", ...cookie, domain: String(cookie.domain).replace(/^\./, "").toLowerCase(), path: cookie.path || "/", expires: Number.isFinite(cookie.expires) || cookie.expires === -1 ? cookie.expires : Number(cookie.expires) || -1, httpOnly: Boolean(cookie.httpOnly), secure: Boolean(cookie.secure), sameSite: normalizeSameSite(cookie.sameSite), }; } function findCookieIndex(cookies, candidate) { return cookies.findIndex( (cookie) => cookie.name === candidate.name && String(cookie.domain || "").replace(/^\./, "").toLowerCase() === candidate.domain && (cookie.path || "/") === candidate.path ); } export function cookieHeaderFor(urlString, cookies) { const url = new URL(urlString); const now = Math.floor(Date.now() / 1000); return cookies .map(normalizeCookie) .filter((cookie) => { if (!cookie) return false; if (!domainMatches(url.hostname, cookie.domain)) return false; if (!url.pathname.startsWith(cookie.path || "/")) return false; if (cookie.secure && url.protocol !== "https:") return false; if (cookie.expires && cookie.expires !== -1 && cookie.expires < now) { return false; } return true; }) .sort((left, right) => (right.path?.length || 0) - (left.path?.length || 0)) .map((cookie) => `${cookie.name}=${cookie.value}`) .join("; "); } export function splitSetCookieHeader(headerValue) { if (!headerValue) return []; const parts = []; let current = ""; let inExpires = false; for (let index = 0; index < headerValue.length; index += 1) { const char = headerValue[index]; const next = headerValue.slice(index); if (!inExpires && next.toLowerCase().startsWith("expires=")) { inExpires = true; } if (char === "," && !inExpires) { const trimmed = current.trim(); if (trimmed) parts.push(trimmed); current = ""; continue; } current += char; if (inExpires && char === ";") { inExpires = false; } } const trimmed = current.trim(); if (trimmed) parts.push(trimmed); return parts; } function defaultCookiePath(url) { const pathname = url.pathname || "/"; if (!pathname.startsWith("/") || pathname === "/") return "/"; const lastSlash = pathname.lastIndexOf("/"); return lastSlash <= 0 ? "/" : pathname.slice(0, lastSlash); } export function mergeCookiesFromHeader(state, urlString, headerValue) { const url = new URL(urlString); const parts = splitSetCookieHeader(headerValue); let changed = false; for (const part of parts) { const segments = part.split(";").map((segment) => segment.trim()).filter(Boolean); const [nameValue, ...attributes] = segments; const eqIndex = nameValue?.indexOf("=") ?? -1; if (eqIndex <= 0) continue; const name = nameValue.slice(0, eqIndex); const value = nameValue.slice(eqIndex + 1); const nextCookie = { name, value, domain: url.hostname.toLowerCase(), path: defaultCookiePath(url), expires: -1, httpOnly: false, secure: url.protocol === "https:", sameSite: "Lax", }; for (const attribute of attributes) { const separator = attribute.indexOf("="); const rawKey = (separator >= 0 ? attribute.slice(0, separator) : attribute).trim(); const rawValue = separator >= 0 ? attribute.slice(separator + 1).trim() : ""; const key = rawKey.toLowerCase(); if (key === "domain") nextCookie.domain = rawValue.replace(/^\./, "").toLowerCase() || url.hostname.toLowerCase(); if (key === "path") nextCookie.path = rawValue || "/"; if (key === "secure") nextCookie.secure = true; if (key === "httponly") nextCookie.httpOnly = true; if (key === "samesite") nextCookie.sameSite = normalizeSameSite(rawValue); if (key === "expires") { const seconds = Math.floor(new Date(rawValue).getTime() / 1000); nextCookie.expires = Number.isFinite(seconds) ? seconds : -1; } if (key === "max-age") { const maxAge = Number(rawValue); nextCookie.maxAge = maxAge; nextCookie.expires = Number.isFinite(maxAge) ? Math.floor(Date.now() / 1000) + maxAge : nextCookie.expires; } } const normalized = normalizeCookie(nextCookie); if (!normalized) continue; const existingIndex = findCookieIndex(state.cookies, normalized); if (normalized.expires === 0 || normalized.maxAge === 0) { if (existingIndex >= 0) { state.cookies.splice(existingIndex, 1); changed = true; } continue; } if (existingIndex >= 0) { const previous = JSON.stringify(normalizeCookie(state.cookies[existingIndex])); state.cookies[existingIndex] = { ...state.cookies[existingIndex], ...normalized }; if (previous !== JSON.stringify(normalizeCookie(state.cookies[existingIndex]))) { changed = true; } } else { state.cookies.push(normalized); changed = true; } } return changed; } function getHeaderValue(headers, name) { if (!headers) return null; if (typeof headers.get === "function") { return headers.get(name) || headers.get(name.toLowerCase()) || headers.get(name.toUpperCase()) || null; } return headers[name] || headers[name.toLowerCase()] || headers[name.toUpperCase()] || null; } export function extractClientBin(html, headers = null) { const fromHeader = getHeaderValue(headers, "x-bitech-svrbin"); if (fromHeader) return fromHeader; return String(html || "").match(/oracle-jet-bundles\/[^.]+\.([a-z0-9]+)\//i)?.[1] || null; } export function extractCsrfToken(value) { if (!value || typeof value !== "object") return null; for (const [key, candidate] of Object.entries(value)) { if (typeof candidate === "string" && key.toLowerCase() === "csrftoken") { return candidate; } const nested = extractCsrfToken(candidate); if (nested) return nested; } return null; } export async function readJson(filePath, label) { let raw; try { raw = await fs.readFile(filePath, "utf8"); } catch (error) { if (error?.code === "ENOENT") { throw new Error( `Could not find ${label} at ${filePath}. Run scripts/refresh-auth-direct.mjs first to create the session files.` ); } throw error; } try { return JSON.parse(raw); } catch (error) { throw new Error(`Could not parse ${label} at ${filePath}: ${error.message}`); } } export async function persistSessionArtifacts(statePath, metaPath, state, meta, { ensureParentDir } = {}) { if (typeof ensureParentDir === "function") { await ensureParentDir(statePath); await ensureParentDir(metaPath); } await fs.writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8"); await fs.writeFile(metaPath, `${JSON.stringify(meta, null, 2)}\n`, "utf8"); } export function replaceObjectContents(target, nextValue) { for (const key of Object.keys(target)) { delete target[key]; } Object.assign(target, nextValue); return target; } export function inferRuntimeHome(statePath, metaPath) { const stateDir = statePath ? path.dirname(statePath) : null; const metaDir = metaPath ? path.dirname(metaPath) : null; if (stateDir && metaDir && stateDir === metaDir) { return stateDir; } return stateDir || metaDir || null; } export function isAuthRefreshRequiredError(error) { const message = String(error?.message || ""); return [ "Refresh auth state first", "Request redirected", "Session redirected to Oracle sign-in", "Oracle DV session restore redirected to Oracle sign-in", "Request redirected while restoring Oracle DV session", "Oracle DV session is not valid anymore", "Could not find auth state", "Could not find session metadata", "Session metadata at", "Expected JSON", ].some((fragment) => message.includes(fragment)); }