import { AtpAgent } from "@atproto/api"; import { Agent, setGlobalDispatcher } from "undici"; import { config } from "./config/index.js"; import { logger } from "./logger/index.js"; import { type SessionData, loadSession, saveSession } from "./session.js"; setGlobalDispatcher( new Agent({ connect: { timeout: 20_000 }, keepAliveTimeout: 10_000, keepAliveMaxTimeout: 20_000, }) ); export const agent = new AtpAgent({ service: config.ozone.pds, }); const JWT_LIFETIME_MS = 2 * 60 * 60 * 1000; // 2 hours (typical ATP JWT lifetime) const REFRESH_AT_PERCENT = 0.8; // Refresh at 80% of lifetime let refreshTimer: NodeJS.Timeout | null = null; async function refreshSession(): Promise { try { logger.info("Refreshing session tokens"); if (!agent.session) { throw new Error("No active session to refresh"); } await agent.resumeSession(agent.session); saveSession(agent.session as SessionData); scheduleSessionRefresh(); } catch (error: unknown) { logger.error({ error }, "Failed to refresh session, will re-authenticate"); await performLogin(); } } function scheduleSessionRefresh(): void { if (refreshTimer) { clearTimeout(refreshTimer); } const refreshIn = JWT_LIFETIME_MS * REFRESH_AT_PERCENT; logger.debug(`Scheduling session refresh in ${(refreshIn / 1000 / 60).toFixed(1)} minutes`); refreshTimer = setTimeout(() => { refreshSession().catch((error: unknown) => { logger.error({ error }, "Scheduled session refresh failed"); }); }, refreshIn); } async function performLogin(): Promise { try { logger.info("Performing fresh login"); const response = await agent.login({ identifier: config.labeler.handle, password: config.labeler.password, }); if (response.success && agent.session) { saveSession(agent.session as SessionData); scheduleSessionRefresh(); logger.info("Login successful, session saved"); return true; } logger.error("Login failed: no session returned"); return false; } catch (error) { logger.error({ error }, "Login failed"); return false; } } const MAX_LOGIN_RETRIES = 3; const RETRY_DELAY_MS = 2000; let loginPromise: Promise | null = null; async function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } async function authenticate(): Promise { const savedSession = loadSession(); if (savedSession) { try { logger.info("Attempting to resume saved session"); await agent.resumeSession(savedSession); // Verify session is still valid with a lightweight call await agent.getProfile({ actor: savedSession.did }); logger.info("Session resumed successfully"); scheduleSessionRefresh(); return true; } catch (error) { logger.warn({ error }, "Saved session invalid, will re-authenticate"); } } return performLogin(); } async function authenticateWithRetry(): Promise { // Reuse existing login attempt if one is in progress if (loginPromise) { return loginPromise; } loginPromise = (async () => { for (let attempt = 1; attempt <= MAX_LOGIN_RETRIES; attempt++) { logger.info({ attempt, maxRetries: MAX_LOGIN_RETRIES }, "Attempting login"); const success = await authenticate(); if (success) { logger.info("Authentication successful"); return; } if (attempt < MAX_LOGIN_RETRIES) { logger.warn( { attempt, maxRetries: MAX_LOGIN_RETRIES, retryInMs: RETRY_DELAY_MS }, "Login failed, retrying" ); await sleep(RETRY_DELAY_MS); } } logger.error({ maxRetries: MAX_LOGIN_RETRIES }, "All login attempts failed, aborting"); process.exit(1); })(); return loginPromise; } export const login = authenticateWithRetry; export const isLoggedIn = authenticateWithRetry().then(() => true);