#!/usr/bin/env node import fs from "node:fs/promises"; import path from "node:path"; import vm from "node:vm"; import { createInterface } from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; import { DISPLAY_META_PATH, DISPLAY_RUNTIME_HOME, DISPLAY_STATE_PATH, ensureRuntimeTree, resolveMetaWritePath, resolveRuntimeHome, resolveStateWritePath, } from "./lib/runtime-paths.mjs"; import { parseArgs } from "./lib/cli-shared.mjs"; import { extractClientBin, extractCsrfToken, normalizeSameSite, splitSetCookieHeader, } from "./lib/auth-shared.mjs"; const SIGNON_URL = "https://signon-int.oracle.com/signin"; const SIGNON_ORIGIN = "https://signon-int.oracle.com"; const SIGNON_PUBLIC_URL = "https://signon.oracle.com/signin"; const SIGNON_PUBLIC_ORIGIN = "https://signon.oracle.com"; const TARGET_URL = "https://salesintelligence-dv.oracle.com/ui/dv/ui/project.jsp?pageid=visualAnalyzer&reportmode=presentation&reportpath=%2F%40Catalog%2Fshared%2FTeam%20Sharing%20Area%2FLAD%2FLAD%20Sales%20Operations%2FLAD%20Production%2FLAD%20Knowledge%20One%20View"; const SESSION_INFO_PATH = "/ui/dv/ui/api/v1/sessioninfo/ext"; const SUPPORTED_FACTORS = new Set([ "PUSH", "SMS", "EMAIL", "PHONE_CALL", "TOTP", "BYPASSCODE", "PASSWORD", "FIDO_PASSKEY", ]); function usage() { return `Direct Oracle Sales Intelligence authentication without browser. Usage: node refresh-auth-direct.mjs [options] Options: --runtime-home Override workspace runtime root (default: ${DISPLAY_RUNTIME_HOME}) --state-path Override auth-state.json output path (default: ${DISPLAY_STATE_PATH}) --meta-path Override session-meta.json output path (default: ${DISPLAY_META_PATH}) --username Username to use (prompted if omitted; also supports ORACLE_SI_USERNAME) --factor Prefer one MFA factor (for example: BYPASSCODE, TOTP, PASSWORD) --url Target Oracle DV workbook URL --help Show this help `; } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function decodeHtml(value) { return String(value || "") .replace(/&/g, "&") .replace(/"/g, '"') .replace(/'/g, "'") .replace(/</g, "<") .replace(/>/g, ">"); } function defaultCookiePath(url) { const pathname = url.pathname || "/"; if (!pathname.startsWith("/") || pathname === "/") return "/"; const lastSlash = pathname.lastIndexOf("/"); return lastSlash <= 0 ? "/" : pathname.slice(0, lastSlash); } function domainMatches(hostname, cookieDomain) { const normalized = String(cookieDomain || "").replace(/^\./, "").toLowerCase(); const host = String(hostname || "").toLowerCase(); return host === normalized || host.endsWith(`.${normalized}`); } function normalizeFactor(value) { const source = String(value || "").trim().toUpperCase().replace(/[\s-]+/g, "_"); if (!source) return null; if (source.includes("PHONE")) return "PHONE_CALL"; if (source.includes("BYPASS")) return "BYPASSCODE"; if (source.includes("FIDO") || source.includes("PASSKEY") || source.includes("WEBAUTHN")) { return "FIDO_PASSKEY"; } if (source === "PASSWORD" || source === "USERNAME_PASSWORD") return "PASSWORD"; if (source.includes("PUSH")) return "PUSH"; if (source.includes("TOTP")) return "TOTP"; if (source.includes("SMS")) return "SMS"; if (source.includes("EMAIL")) return "EMAIL"; return SUPPORTED_FACTORS.has(source) ? source : null; } function uniqBy(items, keyFn) { const seen = new Set(); const result = []; for (const item of items) { const key = keyFn(item); if (seen.has(key)) continue; seen.add(key); result.push(item); } return result; } function mergeFactorChoices(...groups) { return uniqBy( groups.flat().filter(Boolean), (item) => `${item.factor}|${item.deviceId || ""}|${item.label || ""}` ); } function rankFactorChoice(choice, preferredFactor) { if (preferredFactor && choice.factor === preferredFactor) return 0; const order = { BYPASSCODE: 1, TOTP: 2, PUSH: 3, SMS: 4, EMAIL: 5, PHONE_CALL: 6, PASSWORD: 7, FIDO_PASSKEY: 8, }; return order[choice.factor] ?? 99; } function prioritizeFactorChoices(choices, preferredFactor) { return [...choices].sort((left, right) => { const rankDiff = rankFactorChoice(left, preferredFactor) - rankFactorChoice(right, preferredFactor); if (rankDiff !== 0) return rankDiff; return `${left.factor}|${left.label || ""}|${left.deviceId || ""}`.localeCompare( `${right.factor}|${right.label || ""}|${right.deviceId || ""}` ); }); } function formatFactorChoice(choice, duplicateFactors) { const label = String(choice.label || "").trim(); if (!label) return choice.factor; if (choice.deviceId || duplicateFactors.has(choice.factor) || label !== choice.factor) { return `${choice.factor} - ${label}`; } return choice.factor; } function attachChoiceState(choices, state, source) { return (choices || []).map((choice) => ({ ...choice, state, source, requestState: extractTopLevelRequestState(state), })); } class CookieJar { constructor(initialCookies = []) { this.cookies = []; for (const cookie of initialCookies) { this.upsert(cookie); } } upsert(cookie) { if (!cookie?.name || !cookie?.domain) return; const normalized = { path: "/", expires: -1, httpOnly: false, secure: false, sameSite: "Lax", ...cookie, domain: String(cookie.domain).toLowerCase(), path: cookie.path || "/", expires: Number.isFinite(cookie.expires) || cookie.expires === -1 ? cookie.expires : Number(cookie.expires) || -1, sameSite: normalizeSameSite(cookie.sameSite), }; const key = `${normalized.name}|${normalized.domain}|${normalized.path}`; const index = this.cookies.findIndex( (item) => `${item.name}|${item.domain}|${item.path}` === key ); if (normalized.expires === 0 || normalized.maxAge === 0) { if (index >= 0) this.cookies.splice(index, 1); return; } if (index >= 0) { this.cookies[index] = normalized; return; } this.cookies.push(normalized); } storeFromResponse(response, requestUrl) { const url = new URL(requestUrl); const rawSetCookies = typeof response.headers.getSetCookie === "function" ? response.headers.getSetCookie() : splitSetCookieHeader(response.headers.get("set-cookie")); for (const header of rawSetCookies) { const cookie = this.parseSetCookie(header, url); if (cookie) this.upsert(cookie); } } parseSetCookie(header, url) { if (!header) return null; const segments = header.split(";").map((part) => part.trim()).filter(Boolean); const [nameValue, ...attributes] = segments; const separator = nameValue.indexOf("="); if (separator <= 0) return null; const cookie = { name: nameValue.slice(0, separator), value: nameValue.slice(separator + 1), domain: url.hostname.toLowerCase(), path: defaultCookiePath(url), expires: -1, httpOnly: false, secure: false, sameSite: "Lax", }; for (const attribute of attributes) { const eq = attribute.indexOf("="); const key = (eq >= 0 ? attribute.slice(0, eq) : attribute).trim().toLowerCase(); const value = eq >= 0 ? attribute.slice(eq + 1).trim() : ""; if (key === "domain") { cookie.domain = value.toLowerCase(); } else if (key === "path") { cookie.path = value || "/"; } else if (key === "expires") { const seconds = Math.floor(new Date(value).getTime() / 1000); cookie.expires = Number.isFinite(seconds) ? seconds : -1; } else if (key === "max-age") { const maxAge = Number(value); cookie.expires = Number.isFinite(maxAge) ? Math.floor(Date.now() / 1000) + maxAge : cookie.expires; cookie.maxAge = maxAge; } else if (key === "httponly") { cookie.httpOnly = true; } else if (key === "secure") { cookie.secure = true; } else if (key === "samesite") { cookie.sameSite = normalizeSameSite(value); } } return cookie; } headerFor(urlString) { const url = new URL(urlString); const now = Math.floor(Date.now() / 1000); return this.cookies .filter((cookie) => { 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("; "); } toStorageState(username = "") { return { cookies: this.cookies .map((cookie) => ({ name: cookie.name, value: cookie.value, domain: cookie.domain, path: cookie.path, expires: cookie.expires ?? -1, httpOnly: Boolean(cookie.httpOnly), secure: Boolean(cookie.secure), sameSite: normalizeSameSite(cookie.sameSite), })) .sort((left, right) => `${left.domain}|${left.path}|${left.name}`.localeCompare( `${right.domain}|${right.path}|${right.name}` ) ), origins: [ { origin: "https://signon.oracle.com", localStorage: [{ name: "idcs-clp-username", value: username }], }, { origin: "https://signon-int.oracle.com", localStorage: [{ name: "idcs-clp-username", value: username }], }, ], }; } } async function requestWithJar(jar, url, options = {}) { let currentUrl = url; let method = options.method || "GET"; let body = options.body; let headers = { ...(options.headers || {}) }; const maxRedirects = options.maxRedirects ?? 10; for (let redirectCount = 0; redirectCount <= maxRedirects; redirectCount += 1) { const requestHeaders = { ...headers }; const cookieHeader = jar.headerFor(currentUrl); if (cookieHeader) { requestHeaders.cookie = cookieHeader; } const response = await fetch(currentUrl, { method, headers: requestHeaders, body, redirect: "manual", }); jar.storeFromResponse(response, currentUrl); if ([301, 302, 303, 307, 308].includes(response.status)) { const location = response.headers.get("location"); if (!location) { return { url: currentUrl, response, text: await response.text(), }; } currentUrl = new URL(location, currentUrl).toString(); if (response.status === 303 || ((response.status === 301 || response.status === 302) && method === "POST")) { method = "GET"; body = undefined; headers = Object.fromEntries( Object.entries(headers).filter(([key]) => !/^content-(type|length)$/i.test(key)) ); } continue; } return { url: currentUrl, response, text: await response.text(), }; } throw new Error(`Too many redirects while requesting ${url}`); } async function requestJson(jar, url, payload, extraHeaders = {}) { const { response, text } = await requestWithJar(jar, url, { method: "POST", headers: { accept: "application/json, text/plain, */*", "content-type": "application/json; charset=UTF-8", origin: SIGNON_ORIGIN, referer: SIGNON_URL, "x-requested-with": "XMLHttpRequest", ...extraHeaders, }, body: JSON.stringify(payload), }); if (!response.ok) { throw new Error(`Auth request failed (${response.status}) ${url}: ${text.slice(0, 500)}`); } try { return JSON.parse(text); } catch (error) { throw new Error(`Could not parse JSON from ${url}: ${error.message}`); } } function parseHiddenInputs(html) { const formMatch = html.match(/]*action="([^"]+)"[\s\S]*?<\/form>/i); if (!formMatch) { throw new Error("Could not find Oracle redirect form in sign-in response."); } const inputRegex = /]*name="([^"]+)"[^>]*value="([^"]*)"[^>]*>/gi; const fields = {}; let match = inputRegex.exec(html); while (match) { fields[match[1]] = decodeHtml(match[2]); match = inputRegex.exec(html); } return { action: decodeHtml(formMatch[1]), fields, }; } function tryParseHiddenInputs(html) { try { return parseHiddenInputs(html); } catch { return null; } } function isPostFormRedirectPage(html) { return /PostFormRedirect|submitFormOnLoad\(/i.test(String(html || "")); } function parseGlobalConfig(html) { const match = html.match(/(?:var|window\.)\s*GlobalConfig\s*=\s*({[\s\S]*?});/i); if (match) { try { return vm.runInNewContext(`(${match[1]})`); } catch { // fall through to regex extraction } } const accessToken = html.match(/["']accessToken["']\s*:\s*["']([^"']+)["']/i)?.[1]; const idcsURL = html.match(/["']idcsURL["']\s*:\s*["']([^"']+)["']/i)?.[1]; const loginCtx = html.match(/["']loginCtx["']\s*:\s*["']([^"']+)["']/i)?.[1]; if (!accessToken || !idcsURL || !loginCtx) { throw new Error("Could not extract GlobalConfig from Oracle sign-in page."); } return { accessToken, idcsURL, loginCtx }; } function walk(value, visitor, context = {}) { if (Array.isArray(value)) { for (const item of value) { walk(item, visitor, context); } return; } if (!value || typeof value !== "object") { return; } visitor(value, context); for (const [key, child] of Object.entries(value)) { const nextContext = { ...context }; if (key.toLowerCase().includes("factor")) { nextContext.factor = normalizeFactor( typeof child === "string" ? child : value.authFactor || value.type || value.factor ); } walk(child, visitor, nextContext); } } function findFirstString(node, keys) { let result = null; walk(node, (value) => { if (result) return; for (const key of keys) { if (typeof value[key] === "string" && value[key]) { result = value[key]; return; } } }); return result; } function extractRequestState(value) { if (!value) return null; if (typeof value === "string") { try { const decoded = Buffer.from(value, "base64").toString("utf8"); const parsed = JSON.parse(decoded); return ( parsed.requestState || parsed.nextRequestState || findFirstString(parsed, ["requestState", "nextRequestState", "state"]) ); } catch { return null; } } return findFirstString(value, ["requestState", "nextRequestState", "state"]); } function extractTopLevelRequestState(value) { if (value && typeof value === "object" && typeof value.requestState === "string" && value.requestState) { return value.requestState; } return extractRequestState(value); } function extractAuthnToken(value) { return findFirstString(value, ["authnToken"]); } function extractMessage(value) { return ( findFirstString(value, ["displayMessage", "message", "errorMessage", "localizedMessage"]) || null ); } function extractFactorChoices(value) { const choices = []; if (Array.isArray(value?.nextAuthFactors)) { for (const rawFactor of value.nextAuthFactors) { const factor = normalizeFactor(rawFactor); if (!factor || !SUPPORTED_FACTORS.has(factor)) { continue; } choices.push({ factor, deviceId: null, label: factor === "FIDO_PASSKEY" ? value.displayName || "Windows Hello / passkey" : value.displayName || factor, }); } } walk( value, (node, context) => { const factor = normalizeFactor( node.authFactor || node.factorName || node.factorType || node.type || context.factor ); if (!factor || !SUPPORTED_FACTORS.has(factor)) { return; } if (Array.isArray(node.devices)) { for (const device of node.devices) { choices.push({ factor, deviceId: device.deviceId || device.id || null, label: device.displayName || device.deviceName || device.name || device.email || device.phoneNumber || device.mobileNumber || "default device", }); } return; } const deviceId = node.deviceId || node.id || null; const label = node.displayName || node.deviceName || node.name || node.email || node.phoneNumber || node.mobileNumber || (deviceId ? "selected device" : factor); if (deviceId || factor === "BYPASSCODE") { choices.push({ factor, deviceId, label }); } }, {} ); if (choices.length > 0) { return uniqBy(choices, (item) => `${item.factor}|${item.deviceId || ""}|${item.label}`); } const factors = []; walk(value, (node) => { const factor = normalizeFactor(node.authFactor || node.factorName || node.factorType || node.type); if (factor && SUPPORTED_FACTORS.has(factor)) { factors.push({ factor, deviceId: null, label: factor }); } }); return uniqBy(factors, (item) => `${item.factor}|${item.deviceId || ""}`); } function toFormBody(values) { const form = new URLSearchParams(); for (const [key, value] of Object.entries(values)) { if (value === undefined || value === null) continue; form.set(key, String(value)); } return form.toString(); } async function promptLine(rl, message) { return rl.question(message); } async function promptSecret(message) { const secretRl = createInterface({ input, output, terminal: true, historySize: 0 }); const originalWriteToOutput = typeof secretRl._writeToOutput === "function" ? secretRl._writeToOutput.bind(secretRl) : null; secretRl._writeToOutput = (chunk) => { const text = String(chunk); if (text.includes("\n") || text.includes("\r")) { output.write(text); return; } if (/^\x1B\[[0-9;]*[A-Za-z]$/.test(text)) { output.write(text); return; } output.write("*"); }; try { return await secretRl.question(message); } finally { if (originalWriteToOutput) { secretRl._writeToOutput = originalWriteToOutput; } secretRl.close(); output.write("\n"); } } async function authenticateUsername({ jar, authEndpoint, requestState, username, accessToken }) { return requestJson(jar, authEndpoint, { op: "credSubmit", origin: SIGNON_ORIGIN, credentials: { username, keepMeSignedIn: false, }, authFactor: "USERNAME", isFidoSupportedPlatform: true, requestState, }, { authorization: `Bearer ${accessToken}`, }); } async function authenticatePrimary({ jar, authEndpoint, requestState, username, password, accessToken }) { return requestJson(jar, authEndpoint, { op: "credSubmit", origin: SIGNON_ORIGIN, credentials: { username, keepMeSignedIn: false, password, browserFingerPrintParameters: "", }, authFactor: "USERNAME_PASSWORD", requestState, }, { authorization: `Bearer ${accessToken}`, }); } async function submitPasswordCredential({ jar, authEndpoint, currentState, username, password, accessToken }) { return authenticatePrimary({ jar, authEndpoint, requestState: extractRequestState(currentState), username, password, accessToken, }); } async function submitFactorSelection({ jar, authEndpoint, requestState, factor, deviceId, accessToken }) { const credentials = {}; if (deviceId) credentials.deviceId = deviceId; return requestJson(jar, authEndpoint, { op: "credSubmit", origin: SIGNON_ORIGIN, authFactor: factor, requestState, credentials, }, { authorization: `Bearer ${accessToken}`, }); } async function submitOtpCode({ jar, authEndpoint, requestState, factor, code, deviceId, accessToken }) { const payload = { op: "credSubmit", origin: SIGNON_ORIGIN, requestState, credentials: { otpCode: code, }, }; if (factor) payload.authFactor = factor; if (deviceId) payload.credentials.deviceId = deviceId; return requestJson(jar, authEndpoint, payload, { authorization: `Bearer ${accessToken}`, }); } async function resendOtp({ jar, authEndpoint, requestState, accessToken }) { return requestJson(jar, authEndpoint, { op: "resendCode", requestState, }, { authorization: `Bearer ${accessToken}`, }); } async function fetchBackupFactors({ jar, authEndpoint, requestState, accessToken }) { return requestJson( jar, authEndpoint, { op: "getBackupFactors", origin: SIGNON_ORIGIN, requestState, }, { authorization: `Bearer ${accessToken}`, } ); } async function handleOtpFlow({ rl, jar, authEndpoint, currentState, factor, deviceId, accessToken }) { let state = currentState; if (["SMS", "EMAIL", "PHONE_CALL"].includes(factor)) { state = await submitFactorSelection({ jar, authEndpoint, requestState: extractRequestState(state), factor, deviceId, accessToken, }); } while (true) { const code = (await promptLine(rl, `Codigo ${factor}: `)).trim(); if (!code) continue; state = await submitOtpCode({ jar, authEndpoint, requestState: extractRequestState(state), factor: factor === "SMS" || factor === "EMAIL" || factor === "PHONE_CALL" ? undefined : factor, code, deviceId, accessToken, }); if (extractAuthnToken(state)) { return state; } const message = extractMessage(state); if (message) { console.log(message); } const nextAction = ( await promptLine(rl, "Codigo invalido. Pressione Enter para tentar de novo ou digite resend: ") ) .trim() .toLowerCase(); if (nextAction === "resend" && ["SMS", "EMAIL", "PHONE_CALL"].includes(factor)) { state = await resendOtp({ jar, authEndpoint, requestState: extractRequestState(state), accessToken, }); console.log("Novo codigo solicitado."); } } } async function handlePushFlow({ jar, authEndpoint, currentState, deviceId, accessToken }) { let state = await submitFactorSelection({ jar, authEndpoint, requestState: extractRequestState(currentState), factor: "PUSH", deviceId, accessToken, }); console.log("Aprove a solicitacao de push no seu dispositivo."); for (let attempt = 1; attempt <= 60; attempt += 1) { if (extractAuthnToken(state)) { return state; } await sleep(5000); state = await requestJson(jar, authEndpoint, { op: "credSubmit", origin: SIGNON_ORIGIN, requestState: extractRequestState(state), }, { authorization: `Bearer ${accessToken}`, }); const message = extractMessage(state); if (message && attempt % 3 === 0) { console.log(message); } } throw new Error("Timeout aguardando aprovacao do push MFA."); } async function handleMfa({ rl, jar, authEndpoint, response, username, accessToken, preferredFactor }) { let choices = attachChoiceState(extractFactorChoices(response), response, "primary"); let backupChoices = []; if (Array.isArray(response?.nextOp) && response.nextOp.includes("getBackupFactors")) { try { const backupResponse = await fetchBackupFactors({ jar, authEndpoint, requestState: extractRequestState(response), accessToken, }); backupChoices = attachChoiceState(extractFactorChoices(backupResponse), backupResponse, "backup"); choices = mergeFactorChoices( choices, backupChoices ); } catch (error) { console.log(`Nao foi possivel carregar fatores alternativos: ${error.message}`); } } choices = prioritizeFactorChoices(choices, preferredFactor); if (choices.length === 0) { throw new Error( `MFA exigido, mas o script nao conseguiu identificar os fatores disponiveis. Resposta: ${JSON.stringify( response, null, 2 ).slice(0, 2000)}` ); } let selected = choices[0]; const duplicateFactors = new Set( choices .map((choice) => choice.factor) .filter((factor, index, factors) => factors.indexOf(factor) !== index) ); const preferredChoice = preferredFactor ? backupChoices.find((choice) => choice.factor === preferredFactor) || choices.find((choice) => choice.factor === preferredFactor) : null; if (preferredChoice) { selected = preferredChoice; console.log(`Usando MFA ${formatFactorChoice(selected, duplicateFactors)} (preferido por argumento).`); } else if (choices.length > 1) { console.log("Fatores MFA disponiveis:"); choices.forEach((choice, index) => { console.log(` ${index + 1}. ${formatFactorChoice(choice, duplicateFactors)}`); }); while (true) { const raw = (await promptLine(rl, `Escolha um fator [1-${choices.length}]: `)).trim(); const index = Number(raw) - 1; if (Number.isInteger(index) && index >= 0 && index < choices.length) { selected = choices[index]; break; } } } else { console.log(`Usando MFA ${formatFactorChoice(selected, duplicateFactors)}.`); } if (selected.factor === "PUSH") { return handlePushFlow({ jar, authEndpoint, currentState: selected.requestState ? { requestState: selected.requestState } : selected.state || response, deviceId: selected.deviceId, accessToken, }); } if (selected.factor === "TOTP" || selected.factor === "BYPASSCODE") { return handleOtpFlow({ rl, jar, authEndpoint, currentState: selected.requestState ? { requestState: selected.requestState } : selected.state || response, factor: selected.factor, deviceId: selected.deviceId, accessToken, }); } if (["SMS", "EMAIL", "PHONE_CALL"].includes(selected.factor)) { return handleOtpFlow({ rl, jar, authEndpoint, currentState: selected.requestState ? { requestState: selected.requestState } : selected.state || response, factor: selected.factor, deviceId: selected.deviceId, accessToken, }); } if (selected.factor === "PASSWORD") { const password = await promptSecret("Senha Oracle: "); if (!password) { throw new Error("Senha vazia."); } return submitPasswordCredential({ jar, authEndpoint, currentState: selected.requestState ? { requestState: selected.requestState } : selected.state || response, username, password, accessToken, }); } if (selected.factor === "FIDO_PASSKEY") { throw new Error( "Este tenant exige passkey/FIDO com Windows Hello. O fluxo direto por terminal ainda nao consegue acionar a janela nativa do Windows Hello; use refresh-auth-state.mjs no browser para concluir a autenticacao." ); } throw new Error(`Fator MFA nao suportado automaticamente: ${selected.factor}`); } async function createSecureSession({ jar, idcsUrl, accessToken, authnToken }) { const targets = uniqBy( [ { url: new URL("/sso/v1/sdk/secure/session", idcsUrl).toString(), origin: new URL(idcsUrl).origin }, { url: new URL("/sso/v1/sdk/secure/session", SIGNON_ORIGIN).toString(), origin: SIGNON_ORIGIN }, { url: new URL("/sso/v1/sdk/secure/session", SIGNON_PUBLIC_ORIGIN).toString(), origin: SIGNON_PUBLIC_ORIGIN }, ], (entry) => entry.url ); const errors = []; let successCount = 0; for (const target of targets) { const { response, text } = await requestWithJar(jar, target.url, { method: "POST", headers: { accept: "*/*", "content-type": "application/x-www-form-urlencoded; charset=UTF-8", origin: target.origin, referer: `${target.origin}/signin`, }, body: toFormBody({ authnToken, authorization: accessToken, }), }); if (response.ok) { successCount += 1; continue; } errors.push(`${target.url} -> ${response.status}: ${text.slice(0, 200)}`); } if (successCount === 0) { throw new Error(`Could not establish Oracle secure session: ${errors.join(" | ")}`); } } async function loadBrowserLikePage({ jar, targetUrl, maxSteps = 12 }) { let nextRequest = { url: targetUrl, options: { headers: { accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", }, maxRedirects: 12, }, }; for (let step = 1; step <= maxSteps; step += 1) { const page = await requestWithJar(jar, nextRequest.url, nextRequest.options); const url = page.url; if ( (url.includes("signon.oracle.com") || url.includes("signon-int.oracle.com")) && !isPostFormRedirectPage(page.text) ) { throw new Error("Oracle DV redirectou de volta para a pagina de login. A autenticacao direta nao fechou a sessao."); } const form = tryParseHiddenInputs(page.text); if (form && isPostFormRedirectPage(page.text)) { const formUrl = new URL(form.action, url).toString(); nextRequest = { url: formUrl, options: { method: "POST", headers: { accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "content-type": "application/x-www-form-urlencoded; charset=UTF-8", origin: new URL(formUrl).origin, referer: url, }, body: toFormBody(form.fields), maxRedirects: 12, }, }; continue; } return page; } throw new Error("Oracle DV browserless bootstrap exceeded the maximum number of redirects/forms."); } async function loadDvSession({ jar, targetUrl }) { const page = await loadBrowserLikePage({ jar, targetUrl }); if (page.url.includes("signon.oracle.com") || page.url.includes("signon-int.oracle.com")) { throw new Error("Oracle DV redirectou de volta para o sign-in. A autenticacao direta nao fechou a sessao."); } if (!page.response.ok) { throw new Error(`Could not load Oracle DV workbook (${page.response.status}).`); } let clientBin = extractClientBin(page.text, page.response.headers); const sessionInfoUrl = new URL(SESSION_INFO_PATH, targetUrl).toString(); const sessionInfo = await requestWithJar(jar, sessionInfoUrl, { headers: { accept: "application/json, text/plain, */*", authorization: "session", "x-requested-with": "XMLHttpRequest", referer: targetUrl, ...(clientBin ? { "x-bitech-clientbin": clientBin } : {}), }, }); if (sessionInfo.url.includes("signon.oracle.com") || sessionInfo.url.includes("signon-int.oracle.com")) { throw new Error("Oracle DV session info redirecionou para o sign-in."); } if (!sessionInfo.response.ok) { throw new Error( `Could not read Oracle DV session info (${sessionInfo.response.status}): ${sessionInfo.text.slice(0, 500)}` ); } let sessionInfoJson; try { sessionInfoJson = JSON.parse(sessionInfo.text); } catch (error) { throw new Error(`Could not parse Oracle DV session info JSON: ${error.message}`); } const csrfToken = extractCsrfToken(sessionInfoJson); clientBin ||= extractClientBin(sessionInfo.text, sessionInfo.response.headers); if (!csrfToken) { throw new Error("Could not extract csrftoken from Oracle DV session info."); } if (!clientBin) { throw new Error("Could not extract x-bitech-clientbin from Oracle DV responses."); } return { pageUrl: page.url, xCsrfToken: csrfToken, xBitechClientbin: clientBin, }; } async function writeJson(filePath, value) { await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); } async function main() { const args = parseArgs(process.argv.slice(2)); if (args.help === "true") { console.log(usage()); return; } const configuredUsername = args.username || process.env.ORACLE_SI_USERNAME || ""; const runtimeHome = resolveRuntimeHome(args["runtime-home"]); await ensureRuntimeTree(runtimeHome); const statePath = resolveStateWritePath({ explicitPath: args["state-path"], runtimeHome }); const metaPath = resolveMetaWritePath({ explicitPath: args["meta-path"], runtimeHome }); const targetUrl = args.url || process.env.ORACLE_SI_TARGET_URL || TARGET_URL; const preferredFactor = args.factor ? normalizeFactor(args.factor) : null; const jar = new CookieJar(); const startedAt = Date.now(); let rl = null; try { rl = createInterface({ input, output }); const username = configuredUsername.trim() || (await promptLine(rl, "Oracle username/email: ")).trim(); if (!username) { throw new Error("Oracle username/email is required. Pass --username or set ORACLE_SI_USERNAME."); } console.log(`Autenticando diretamente no Oracle IDCS para ${username}.`); console.log("O fluxo nao abre browser. Ele tenta primeiro o caminho passwordless do tenant e so pede senha se o Oracle realmente solicitar."); const initial = await requestWithJar(jar, SIGNON_URL, { maxRedirects: 8 }); const redirectForm = parseHiddenInputs(initial.text); const signInApp = await requestWithJar(jar, redirectForm.action, { method: "POST", headers: { accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "content-type": "application/x-www-form-urlencoded; charset=UTF-8", origin: SIGNON_ORIGIN, referer: SIGNON_URL, }, body: toFormBody(redirectForm.fields), maxRedirects: 8, }); const globalConfig = parseGlobalConfig(signInApp.text); const requestState = extractRequestState(globalConfig.loginCtx); if (!requestState) { throw new Error("Could not extract requestState from GlobalConfig loginCtx."); } const authEndpoint = new URL("/sso/v1/sdk/authenticate", globalConfig.idcsURL).toString(); let authResponse = await authenticateUsername({ jar, authEndpoint, requestState, username, accessToken: globalConfig.accessToken, }); if (!extractAuthnToken(authResponse)) { authResponse = await handleMfa({ rl, jar, authEndpoint, response: authResponse, username, accessToken: globalConfig.accessToken, preferredFactor, }); } const authnToken = extractAuthnToken(authResponse); if (!authnToken) { throw new Error( `Oracle login nao retornou authnToken. Resposta: ${JSON.stringify(authResponse, null, 2).slice(0, 2000)}` ); } await createSecureSession({ jar, idcsUrl: globalConfig.idcsURL, accessToken: globalConfig.accessToken, authnToken, }); const dvSession = await loadDvSession({ jar, targetUrl, }); const storageState = jar.toStorageState(username); const sessionMeta = { xCsrfToken: dvSession.xCsrfToken, xBitechClientbin: dvSession.xBitechClientbin, referer: targetUrl, targetUrl, capturedAt: new Date().toISOString(), }; await writeJson(statePath, storageState); await writeJson(metaPath, sessionMeta); console.log( JSON.stringify( { ok: true, username, statePath, metaPath, cookies: storageState.cookies.length, xBitechClientbin: sessionMeta.xBitechClientbin, elapsedMs: Date.now() - startedAt, }, null, 2 ) ); } finally { rl?.close(); } } main().catch((error) => { console.error(error.message); process.exitCode = 1; });