Primeiro Commit v0.beta1
This commit is contained in:
256
scripts/lib/auth-shared.mjs
Normal file
256
scripts/lib/auth-shared.mjs
Normal file
@@ -0,0 +1,256 @@
|
||||
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));
|
||||
}
|
||||
Reference in New Issue
Block a user