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 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 15:45:24 -08:00
parent a133202e1b
commit 59e410b9c4
2 changed files with 80 additions and 50 deletions

View File

@@ -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 { getJobsByStatus, updateJobStatus, appendLog, loadConfig, isAlreadyApplied } from './lib/queue.mjs';
import { acquireLock } from './lib/lock.mjs'; import { acquireLock } from './lib/lock.mjs';
import { createBrowser } from './lib/browser.mjs'; import { createBrowser } from './lib/browser.mjs';
import { ensureAuth } from './lib/session.mjs';
import { FormFiller } from './lib/form_filler.mjs'; import { FormFiller } from './lib/form_filler.mjs';
import { applyToJob, supportedTypes } from './lib/apply/index.mjs'; import { applyToJob, supportedTypes } from './lib/apply/index.mjs';
import { sendTelegram, formatApplySummary } from './lib/notify.mjs'; import { sendTelegram, formatApplySummary } from './lib/notify.mjs';
@@ -135,6 +136,19 @@ async function main() {
if (platform === 'external') { if (platform === 'external') {
browser = await createBrowser(settings, null); // no profile needed browser = await createBrowser(settings, null); // no profile needed
} else { } 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); browser = await createBrowser(settings, platform);
console.log(' ✅ Logged in\n'); console.log(' ✅ Logged in\n');
} }

View File

@@ -1,67 +1,83 @@
/** /**
* session.mjs — Kernel Managed Auth session refresh * session.mjs — Kernel Managed Auth session management
* Call refreshSession() before creating a browser to ensure the profile is fresh * 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 { createRequire } from 'module';
import { import {
KERNEL_SDK_PATH, SESSION_REFRESH_POLL_TIMEOUT, SESSION_REFRESH_POLL_WAIT, KERNEL_SDK_PATH, SESSION_REFRESH_POLL_TIMEOUT, SESSION_REFRESH_POLL_WAIT,
SESSION_LOGIN_VERIFY_WAIT
} from './constants.mjs'; } from './constants.mjs';
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
export async function refreshSession(platform, apiKey, connectionIds = {}) { function getKernel(apiKey) {
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`);
const Kernel = require(KERNEL_SDK_PATH); const Kernel = require(KERNEL_SDK_PATH);
const kernel = new Kernel({ apiKey }); return 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;
} }
/** /**
* 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) { export async function ensureAuth(platform, apiKey, connectionIds = {}) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) { const connectionId = connectionIds[platform];
const loggedIn = await verifyFn(page); if (!connectionId) {
if (loggedIn) return true; return { ok: false, reason: `no connection ID configured for ${platform}` };
}
if (attempt < maxAttempts) { const kernel = getKernel(apiKey);
console.warn(` ⚠️ ${platform} not logged in (attempt ${attempt}), refreshing session...`);
await refreshSession(platform, apiKey, connectionIds); // Check current status
await page.reload({ waitUntil: 'domcontentloaded' }); let conn;
await page.waitForTimeout(SESSION_LOGIN_VERIFY_WAIT); 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` };
} }