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:
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
116
lib/session.mjs
116
lib/session.mjs
@@ -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` };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user