diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index 7c2f44f7..6aa69ace 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -83,7 +83,7 @@ try { context = await browser.newContext({ viewport, ...(playwrightProxy === null ? {} : { proxy: playwrightProxy }) }); page = await context.newPage(); attachPassiveListeners(page, "control", pageId); - auth = await runControlCommand({ id: "startup-login", type: "login", createdAt: startedAt, source: "startup" }, async () => authenticate(context, page)); + auth = await runControlCommand({ id: "startup-login", type: "login", createdAt: startedAt, source: "startup" }, async () => authenticate(context)); await runControlCommand({ id: "startup-goto", type: "goto", path: targetPath, createdAt: new Date().toISOString(), source: "startup" }, async () => gotoTarget(targetPath)); observerPage = await context.newPage(); attachPassiveListeners(observerPage, "observer", observerPageId); @@ -340,7 +340,7 @@ async function processCommand(command) { await writeHeartbeat({ status: "running", activeCommandId }); await appendJsonl(files.control, controlRecord(command, "started", commandInputSummary(command))); switch (command.type) { - case "login": return authenticate(context, page); + case "login": return authenticate(context); case "loginAccount": return withObserverSync(await loginAccount(command), "loginAccount"); case "logout": return withObserverSync(await logoutAccount(command), "logout"); case "listSessions": return withObserverSync(await listSessions(command), "listSessions"); @@ -490,87 +490,77 @@ async function runControlCommand(command, fn) { } } -async function authenticate(browserContext, authPage) { +async function authenticate(browserContext) { const loginUrl = new URL("/auth/login", baseUrl).toString(); - let activeAuthPage = authPage && !authPage.isClosed() ? authPage : null; - let closeAuthPage = false; - if (!activeAuthPage) { - activeAuthPage = await browserContext.newPage(); - closeAuthPage = true; - } const attempts = []; const maxAttempts = 5; const initialDelayMs = 250; const maxDelayMs = 5000; - try { - for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { - const retryDelayMs = attempt < maxAttempts ? Math.min(maxDelayMs, initialDelayMs * (2 ** (attempt - 1))) : 0; - const retryLabel = attempt + "/" + maxAttempts; - await writeHeartbeat({ status: terminalStatus, auth: { phase: "api-login", retryAttempt: attempt, retryMaxAttempts: maxAttempts, lastRetryLabel: retryLabel, retryDelayMs: 0, retryExhausted: false, valuesRedacted: true } }).catch(() => {}); - try { - const response = await pageAuthLogin(activeAuthPage, loginUrl); - const cookieState = await readAuthCookieState(browserContext); - const retryable = isRetryableAuthStatus(response.status); - const item = { - attempt, - retryAttempt: attempt, - retryMaxAttempts: maxAttempts, - retryLabel, - retryDelayMs: retryable && attempt < maxAttempts ? retryDelayMs : 0, + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + const retryDelayMs = attempt < maxAttempts ? Math.min(maxDelayMs, initialDelayMs * (2 ** (attempt - 1))) : 0; + const retryLabel = attempt + "/" + maxAttempts; + await writeHeartbeat({ status: terminalStatus, auth: { phase: "api-login", retryAttempt: attempt, retryMaxAttempts: maxAttempts, lastRetryLabel: retryLabel, retryDelayMs: 0, retryExhausted: false, valuesRedacted: true } }).catch(() => {}); + try { + const response = await pageAuthLogin(browserContext, loginUrl); + const cookieState = await readAuthCookieState(browserContext); + const retryable = isRetryableAuthStatus(response.status); + const item = { + attempt, + retryAttempt: attempt, + retryMaxAttempts: maxAttempts, + retryLabel, + retryDelayMs: retryable && attempt < maxAttempts ? retryDelayMs : 0, + method: "api", + status: response.status, + statusText: response.statusText, + retryable, + cookiePresent: cookieState.cookiePresent, + cookieNames: cookieState.cookieNames, + valuesRedacted: true, + }; + attempts.push(item); + await writeHeartbeat({ status: terminalStatus, auth: { phase: "api-login", lastRetryLabel: item.retryLabel, retryAttempt: item.retryAttempt, retryMaxAttempts: item.retryMaxAttempts, retryDelayMs: item.retryDelayMs, lastStatus: item.status, lastStatusText: item.statusText, retryable: item.retryable, cookiePresent: item.cookiePresent, retryExhausted: false, valuesRedacted: true } }).catch(() => {}); + if (response.ok && cookieState.cookiePresent) { + return { + ok: true, method: "api", + loginPath: new URL(loginUrl).pathname, status: response.status, statusText: response.statusText, - retryable, - cookiePresent: cookieState.cookiePresent, + cookiePresent: true, cookieNames: cookieState.cookieNames, + attempts, + retryCount: attempt - 1, + retryMaxAttempts: maxAttempts, + lastRetryLabel: attempt + "/" + maxAttempts, + retryExhausted: false, + retryable: false, valuesRedacted: true, }; - attempts.push(item); - await writeHeartbeat({ status: terminalStatus, auth: { phase: "api-login", lastRetryLabel: item.retryLabel, retryAttempt: item.retryAttempt, retryMaxAttempts: item.retryMaxAttempts, retryDelayMs: item.retryDelayMs, lastStatus: item.status, lastStatusText: item.statusText, retryable: item.retryable, cookiePresent: item.cookiePresent, retryExhausted: false, valuesRedacted: true } }).catch(() => {}); - if (response.ok && cookieState.cookiePresent) { - return { - ok: true, - method: "api", - loginPath: new URL(loginUrl).pathname, - status: response.status, - statusText: response.statusText, - cookiePresent: true, - cookieNames: cookieState.cookieNames, - attempts, - retryCount: attempt - 1, - retryMaxAttempts: maxAttempts, - lastRetryLabel: attempt + "/" + maxAttempts, - retryExhausted: false, - retryable: false, - valuesRedacted: true, - }; - } - if (!retryable) break; - } catch (error) { - const retryable = isRetryableAuthError(error); - attempts.push({ - attempt, - retryAttempt: attempt, - retryMaxAttempts: maxAttempts, - retryLabel, - retryDelayMs: retryable && attempt < maxAttempts ? retryDelayMs : 0, - method: "api", - status: 0, - statusText: "request-error", - retryable, - error: error && error.message ? truncate(error.message, 500) : truncate(String(error), 500), - cookiePresent: false, - cookieNames: [], - valuesRedacted: true, - }); - const item = attempts[attempts.length - 1] || null; - await writeHeartbeat({ status: terminalStatus, auth: { phase: "api-login", lastRetryLabel: item?.retryLabel || retryLabel, retryAttempt: attempt, retryMaxAttempts: maxAttempts, retryDelayMs: item?.retryDelayMs ?? 0, lastStatus: item?.status ?? 0, lastStatusText: item?.statusText ?? "request-error", retryable, cookiePresent: false, retryExhausted: false, lastError: item?.error || null, valuesRedacted: true } }).catch(() => {}); - if (!retryable) break; } - if (attempt < maxAttempts && attempts[attempts.length - 1]?.retryable === true) await sleep(retryDelayMs); + if (!retryable) break; + } catch (error) { + const retryable = isRetryableAuthError(error); + attempts.push({ + attempt, + retryAttempt: attempt, + retryMaxAttempts: maxAttempts, + retryLabel, + retryDelayMs: retryable && attempt < maxAttempts ? retryDelayMs : 0, + method: "api", + status: 0, + statusText: "request-error", + retryable, + error: error && error.message ? truncate(error.message, 500) : truncate(String(error), 500), + cookiePresent: false, + cookieNames: [], + valuesRedacted: true, + }); + const item = attempts[attempts.length - 1] || null; + await writeHeartbeat({ status: terminalStatus, auth: { phase: "api-login", lastRetryLabel: item?.retryLabel || retryLabel, retryAttempt: attempt, retryMaxAttempts: maxAttempts, retryDelayMs: item?.retryDelayMs ?? 0, lastStatus: item?.status ?? 0, lastStatusText: item?.statusText ?? "request-error", retryable, cookiePresent: false, retryExhausted: false, lastError: item?.error || null, valuesRedacted: true } }).catch(() => {}); + if (!retryable) break; } - } finally { - if (closeAuthPage && activeAuthPage && !activeAuthPage.isClosed()) await activeAuthPage.close().catch(() => {}); + if (attempt < maxAttempts && attempts[attempts.length - 1]?.retryable === true) await sleep(retryDelayMs); } const cookieState = await readAuthCookieState(browserContext); const last = attempts[attempts.length - 1] || null; @@ -598,23 +588,19 @@ async function authenticate(browserContext, authPage) { throw error; } -async function pageAuthLogin(authPage, loginUrl, credential = { username, password }) { - if (!authPage) throw new Error("auth page is not ready"); - await authPage.goto(new URL("/assets/favicon.svg", baseUrl).toString(), { waitUntil: "domcontentloaded", timeout: 12000 }); - return authPage.evaluate(async (input) => { - const response = await fetch(input.loginUrl, { - method: "POST", - headers: { accept: "application/json", "content-type": "application/json" }, - body: JSON.stringify({ username: input.username, password: input.password }), - credentials: "include", - }); - await response.text().catch(() => ""); - return { - ok: response.ok, - status: response.status, - statusText: response.statusText || "", - }; - }, { username: credential.username, password: credential.password, loginUrl }); +async function pageAuthLogin(browserContext, loginUrl, credential = { username, password }) { + if (!browserContext?.request) throw new Error("auth browser context request is not ready"); + const response = await browserContext.request.post(loginUrl, { + data: { username: credential.username, password: credential.password }, + headers: { accept: "application/json", "content-type": "application/json" }, + timeout: 12000, + }); + await response.text().catch(() => ""); + return { + ok: response.ok(), + status: response.status(), + statusText: response.statusText() || "", + }; } async function loginAccount(command) { @@ -622,7 +608,7 @@ async function loginAccount(command) { const credential = credentialForAccount(accountId); const loginUrl = new URL("/auth/login", baseUrl).toString(); const before = await accountSessionSnapshot(); - const response = await pageAuthLogin(page, loginUrl, credential); + const response = await pageAuthLogin(context, loginUrl, credential); const cookieState = await readAuthCookieState(context); if (!response.ok || !cookieState.cookiePresent) { const error = new Error("loginAccount failed for accountId=" + accountId + " status=" + response.status + " " + (response.statusText || ""));