From 59e410b9c4edb247212d6bdeeb1dfe7b3c7742a5 Mon Sep 17 00:00:00 2001 From: Matthew Jackson Date: Fri, 6 Mar 2026 15:45:24 -0800 Subject: [PATCH] Add auth health check before browser creation - Rewrite session.mjs: check connection status via SDK before creating browser. If NEEDS_AUTH + can_reauth, auto re-auth with stored creds. If can't re-auth, send Telegram alert and skip platform. - Wire ensureAuth() into job_applier.mjs before createBrowser() - Jobs are returned to queue (not failed) when auth is down Co-Authored-By: Claude Opus 4.6 --- job_applier.mjs | 14 ++++++ lib/session.mjs | 116 +++++++++++++++++++++++++++--------------------- 2 files changed, 80 insertions(+), 50 deletions(-) diff --git a/job_applier.mjs b/job_applier.mjs index 59f4d44..752c0bf 100644 --- a/job_applier.mjs +++ b/job_applier.mjs @@ -22,6 +22,7 @@ process.stderr.write = (chunk, ...args) => { logStream.write(chunk); return orig import { getJobsByStatus, updateJobStatus, appendLog, loadConfig, isAlreadyApplied } from './lib/queue.mjs'; import { acquireLock } from './lib/lock.mjs'; import { createBrowser } from './lib/browser.mjs'; +import { ensureAuth } from './lib/session.mjs'; import { FormFiller } from './lib/form_filler.mjs'; import { applyToJob, supportedTypes } from './lib/apply/index.mjs'; import { sendTelegram, formatApplySummary } from './lib/notify.mjs'; @@ -135,6 +136,19 @@ async function main() { if (platform === 'external') { browser = await createBrowser(settings, null); // no profile needed } else { + // Check auth status before creating browser + const connectionIds = settings.kernel?.connection_ids || {}; + const kernelApiKey = process.env.KERNEL_API_KEY || settings.kernel_api_key; + const authResult = await ensureAuth(platform, kernelApiKey, connectionIds); + if (!authResult.ok) { + console.error(` ❌ ${platform} auth failed: ${authResult.reason}`); + await sendTelegram(settings, `⚠️ *${platform}* auth failed — ${authResult.reason}`).catch(() => {}); + // Mark all jobs for this platform as needing retry + for (const job of platformJobs) { + updateJobStatus(job.id, 'new', { retry_reason: 'auth_failed' }); + } + continue; + } browser = await createBrowser(settings, platform); console.log(' ✅ Logged in\n'); } diff --git a/lib/session.mjs b/lib/session.mjs index ded6911..3f4bdc9 100644 --- a/lib/session.mjs +++ b/lib/session.mjs @@ -1,67 +1,83 @@ /** - * session.mjs — Kernel Managed Auth session refresh - * Call refreshSession() before creating a browser to ensure the profile is fresh + * session.mjs — Kernel Managed Auth session management + * Checks auth status before browser creation, triggers re-auth when needed. + * + * Flow: + * 1. Check connection status via SDK + * 2. If AUTHENTICATED → good to go + * 3. If NEEDS_AUTH + can_reauth → trigger login(), poll until done + * 4. If NEEDS_AUTH + !can_reauth → return false (caller should alert + skip) */ import { createRequire } from 'module'; import { KERNEL_SDK_PATH, SESSION_REFRESH_POLL_TIMEOUT, SESSION_REFRESH_POLL_WAIT, - SESSION_LOGIN_VERIFY_WAIT } from './constants.mjs'; const require = createRequire(import.meta.url); -export async function refreshSession(platform, apiKey, connectionIds = {}) { - const connectionId = connectionIds[platform]; - if (!connectionId) throw new Error(`No Kernel connection ID configured for platform: ${platform} — add it to settings.json under kernel.connection_ids`); - +function getKernel(apiKey) { const Kernel = require(KERNEL_SDK_PATH); - const kernel = new Kernel({ apiKey }); - - console.log(` 🔄 Refreshing ${platform} session...`); - - // Trigger re-auth (uses stored credentials automatically) - const loginResp = await kernel.auth.connections.login(connectionId); - - if (loginResp.status === 'SUCCESS') { - console.log(` ✅ ${platform} session refreshed`); - return true; - } - - // If not immediately successful, poll for up to 30s - console.log(` ⏳ ${platform} session pending (status: ${loginResp.status}), polling...`); - const start = Date.now(); - let pollCount = 0; - while (Date.now() - start < SESSION_REFRESH_POLL_TIMEOUT) { - await new Promise(r => setTimeout(r, SESSION_REFRESH_POLL_WAIT)); - pollCount++; - const conn = await kernel.auth.connections.retrieve(connectionId); - if (conn.status === 'SUCCESS') { - console.log(` ✅ ${platform} session refreshed (after ${pollCount} polls)`); - return true; - } - if (['FAILED', 'EXPIRED', 'CANCELED'].includes(conn.status)) { - console.warn(` ⚠️ ${platform} session refresh failed: ${conn.status} (after ${pollCount} polls)`); - return false; - } - } - - console.warn(` ⚠️ ${platform} session refresh timed out`); - return false; + return new Kernel({ apiKey }); } /** - * Verify login after browser connects — if not logged in, trigger refresh and retry + * Check auth connection status and re-auth if needed. + * Returns { ok: true } or { ok: false, reason: string } */ -export async function ensureLoggedIn(page, verifyFn, platform, apiKey, connectionIds = {}, maxAttempts = 2) { - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - const loggedIn = await verifyFn(page); - if (loggedIn) return true; +export async function ensureAuth(platform, apiKey, connectionIds = {}) { + const connectionId = connectionIds[platform]; + if (!connectionId) { + return { ok: false, reason: `no connection ID configured for ${platform}` }; + } - if (attempt < maxAttempts) { - console.warn(` ⚠️ ${platform} not logged in (attempt ${attempt}), refreshing session...`); - await refreshSession(platform, apiKey, connectionIds); - await page.reload({ waitUntil: 'domcontentloaded' }); - await page.waitForTimeout(SESSION_LOGIN_VERIFY_WAIT); + const kernel = getKernel(apiKey); + + // Check current status + let conn; + try { + conn = await kernel.auth.connections.retrieve(connectionId); + } catch (e) { + return { ok: false, reason: `connection ${connectionId} not found: ${e.message}` }; + } + + if (conn.status === 'AUTHENTICATED') { + return { ok: true }; + } + + // NEEDS_AUTH — can we auto re-auth? + if (!conn.can_reauth) { + return { ok: false, reason: `${platform} needs manual re-login (can_reauth=false). Go to Kernel dashboard or run: kernel auth connections login ${connectionId}` }; + } + + // Trigger re-auth with stored credentials + console.log(` 🔄 ${platform} session expired — re-authenticating...`); + try { + await kernel.auth.connections.login(connectionId); + } catch (e) { + return { ok: false, reason: `re-auth login() failed: ${e.message}` }; + } + + // Poll until complete + const start = Date.now(); + while (Date.now() - start < SESSION_REFRESH_POLL_TIMEOUT) { + await new Promise(r => setTimeout(r, SESSION_REFRESH_POLL_WAIT)); + try { + conn = await kernel.auth.connections.retrieve(connectionId); + } catch (e) { + return { ok: false, reason: `polling failed: ${e.message}` }; + } + + if (conn.status === 'AUTHENTICATED') { + console.log(` ✅ ${platform} re-authenticated`); + return { ok: true }; + } + + if (conn.flow_status === 'FAILED') { + return { ok: false, reason: `re-auth failed: ${conn.error_message || conn.error_code || 'unknown'}` }; + } + if (conn.flow_status === 'EXPIRED' || conn.flow_status === 'CANCELED') { + return { ok: false, reason: `re-auth ${conn.flow_status.toLowerCase()}` }; } } - return false; + + return { ok: false, reason: `re-auth timed out after ${SESSION_REFRESH_POLL_TIMEOUT / 1000}s` }; }