| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 |
- function resolveAppOrigin(apiBaseUrl: string): string {
- return apiBaseUrl.replace(/\/api\/actions\/?$/, "");
- }
- export function splitSetCookieHeader(combined: string): string[] {
- const cookies: string[] = [];
- let start = 0;
- let inExpires = false;
- let inQuotes = false;
- let escapeNext = false;
- function isExpiresStart(index: number): boolean {
- if (combined.slice(index, index + 8).toLowerCase() !== "expires=") return false;
- if (index === 0) return true;
- const prev = combined[index - 1];
- return prev === ";" || prev === " " || prev === "\t";
- }
- for (let i = 0; i < combined.length; i++) {
- const ch = combined[i];
- if (inQuotes) {
- if (escapeNext) {
- escapeNext = false;
- continue;
- }
- if (ch === "\\") {
- escapeNext = true;
- continue;
- }
- if (ch === '"') {
- inQuotes = false;
- }
- continue;
- }
- if (ch === '"') {
- inQuotes = true;
- continue;
- }
- if (!inExpires && isExpiresStart(i)) {
- inExpires = true;
- i += 7;
- continue;
- }
- if (inExpires && ch === ";") {
- inExpires = false;
- continue;
- }
- if (ch !== ",") continue;
- const next = combined.slice(i + 1);
- const looksLikeCookieStart = /^\s*[^;\s]+=/.test(next);
- if (!looksLikeCookieStart) {
- continue;
- }
- const part = combined.slice(start, i).trim();
- if (part) {
- cookies.push(part);
- }
- start = i + 1;
- inExpires = false;
- }
- const last = combined.slice(start).trim();
- if (last) {
- cookies.push(last);
- }
- return cookies;
- }
- function getSetCookieHeaders(response: Response): string[] {
- const headersWithGetSetCookie = response.headers as unknown as {
- getSetCookie?: () => string[];
- };
- const headerList = headersWithGetSetCookie.getSetCookie?.();
- if (Array.isArray(headerList) && headerList.length > 0) {
- return headerList;
- }
- const combined = response.headers.get("set-cookie");
- if (!combined) return [];
- return splitSetCookieHeader(combined);
- }
- function extractCookieValue(setCookieHeader: string, cookieName: string): string | null {
- const trimmed = setCookieHeader.trim();
- if (!trimmed) return null;
- const segments = trimmed.split(";");
- for (const segment of segments) {
- const part = segment.trim();
- if (!part) continue;
- if (part.startsWith(`${cookieName}=`)) {
- return part.slice(cookieName.length + 1) || null;
- }
- }
- return null;
- }
- async function sleep(ms: number): Promise<void> {
- await new Promise((resolve) => setTimeout(resolve, ms));
- }
- function shouldRetryFetchError(error: unknown): boolean {
- if (!(error instanceof Error)) return false;
- const retryCodes = new Set([
- "ECONNREFUSED",
- "ECONNRESET",
- "ETIMEDOUT",
- "EAI_AGAIN",
- "UND_ERR_CONNECT_TIMEOUT",
- "UND_ERR_HEADERS_TIMEOUT",
- "UND_ERR_BODY_TIMEOUT",
- "UND_ERR_SOCKET",
- ]);
- const errorWithCause = error as { cause?: unknown; code?: unknown };
- const maybeCodes: string[] = [];
- if (typeof errorWithCause.code === "string") {
- maybeCodes.push(errorWithCause.code);
- }
- const cause = errorWithCause.cause;
- if (cause && typeof cause === "object") {
- const causeCode = (cause as { code?: unknown }).code;
- if (typeof causeCode === "string") {
- maybeCodes.push(causeCode);
- }
- }
- if (maybeCodes.length > 0) {
- return maybeCodes.some((code) => retryCodes.has(code));
- }
- const message = error.message.toLowerCase();
- return message.includes("fetch failed");
- }
- export async function loginAndGetAuthToken(apiBaseUrl: string, key: string): Promise<string> {
- const origin = resolveAppOrigin(apiBaseUrl);
- const url = `${origin}/api/auth/login`;
- const maxAttempts = 10;
- let lastError: unknown;
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
- let response: Response;
- try {
- response = await fetch(url, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ key }),
- });
- } catch (error) {
- lastError = error;
- if (!shouldRetryFetchError(error)) {
- break;
- }
- if (attempt >= maxAttempts) {
- break;
- }
- await sleep(Math.min(1000, 100 * 2 ** (attempt - 1)));
- continue;
- }
- if (!response.ok) {
- const text = await response.text().catch(() => "");
- const errorCode = (() => {
- try {
- const parsed = JSON.parse(text) as { errorCode?: unknown };
- return typeof parsed?.errorCode === "string" ? parsed.errorCode : undefined;
- } catch {
- return undefined;
- }
- })();
- const error = new Error(`[e2e] login failed: ${response.status} ${text}`);
- const shouldRetry = response.status === 503 && errorCode === "SESSION_CREATE_FAILED";
- if (!shouldRetry) {
- throw error;
- }
- lastError = error;
- if (attempt >= maxAttempts) {
- break;
- }
- await sleep(Math.min(1000, 100 * 2 ** (attempt - 1)));
- continue;
- }
- const setCookieHeaders = getSetCookieHeaders(response);
- const authToken = setCookieHeaders
- .map((header) => extractCookieValue(header, "auth-token"))
- .find((value): value is string => Boolean(value));
- if (!authToken) {
- throw new Error("[e2e] login succeeded but auth-token cookie is missing");
- }
- return authToken;
- }
- throw lastError instanceof Error ? lastError : new Error("[e2e] login failed");
- }
|