Fix Auth Issue #3

merged
opened by skywatch.blue targeting main from fix/login-bug

Fixes #1

+2 -1
.claude/settings.local.json
··· 12 12 "mcp__git-mcp-server__git_set_working_dir", 13 13 "Bash(npm run test:run:*)", 14 14 "Bash(bunx eslint:*)", 15 - "Bash(bun test:run:*)" 15 + "Bash(bun test:run:*)", 16 + "Bash(bun run type-check:*)" 16 17 ], 17 18 "deny": [], 18 19 "ask": []
+1
.gitignore
··· 5 5 labels.db* 6 6 .DS_Store 7 7 coverage/ 8 + .session
-10
.session
··· 1 - { 2 - "accessJwt": "eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJFUzI1NksifQ.eyJzY29wZSI6ImNvbS5hdHByb3RvLmFwcFBhc3MiLCJzdWIiOiJkaWQ6cGxjOmpzamxoajM1NzRvZHRmcWF6cXhuNG9uZCIsImlhdCI6MTc2MTA3OTUzMSwiZXhwIjoxNzYxMDg2NzMxLCJhdWQiOiJkaWQ6d2ViOm95c3RlcmxpbmcudXMtd2VzdC5ob3N0LmJza3kubmV0d29yayJ9.2EPsA8yDLvngSPzOu-DHy-2SQCjgzk4wFxgsOL7BXq1gwmRkoJy_Poykjb8m9JeYt9_s08-VCM_h1C43FOVosg", 3 - "refreshJwt": "eyJ0eXAiOiJyZWZyZXNoK2p3dCIsImFsZyI6IkVTMjU2SyJ9.eyJzY29wZSI6ImNvbS5hdHByb3RvLnJlZnJlc2giLCJzdWIiOiJkaWQ6cGxjOmpzamxoajM1NzRvZHRmcWF6cXhuNG9uZCIsImF1ZCI6ImRpZDp3ZWI6YnNreS5zb2NpYWwiLCJqdGkiOiJpOWVKcTVHa0VQeS9ISVV0YWtUb0dqMW55Mllzb25PK0VGMHUySGRoNFNFIiwiaWF0IjoxNzYxMDc5NTMxLCJleHAiOjE3Njg4NTU1MzF9.5YawggT0amOGgZryO5h2kJ11ePimtc0YMqs8W-ZzxPkU8aymD0m29w4_wZXeyoK4vclU-YvlUc9iDr5SgrqJ2w", 4 - "handle": "automod.skywatch.blue", 5 - "did": "did:plc:jsjlhj3574odtfqazqxn4ond", 6 - "email": "bsky.duration409@passmail.net", 7 - "emailConfirmed": true, 8 - "emailAuthFactor": true, 9 - "active": true 10 - }
+50 -4
src/agent.ts
··· 99 99 } 100 100 } 101 101 102 + const MAX_LOGIN_RETRIES = 3; 103 + const RETRY_DELAY_MS = 2000; 104 + 105 + let loginPromise: Promise<void> | null = null; 106 + 107 + async function sleep(ms: number): Promise<void> { 108 + return new Promise((resolve) => setTimeout(resolve, ms)); 109 + } 110 + 102 111 async function authenticate(): Promise<boolean> { 103 112 const savedSession = loadSession(); 104 113 ··· 121 130 return performLogin(); 122 131 } 123 132 124 - export const login = authenticate; 125 - export const isLoggedIn = authenticate() 126 - .then((success) => success) 127 - .catch(() => false); 133 + async function authenticateWithRetry(): Promise<void> { 134 + // Reuse existing login attempt if one is in progress 135 + if (loginPromise) { 136 + return loginPromise; 137 + } 138 + 139 + loginPromise = (async () => { 140 + for (let attempt = 1; attempt <= MAX_LOGIN_RETRIES; attempt++) { 141 + logger.info( 142 + { attempt, maxRetries: MAX_LOGIN_RETRIES }, 143 + "Attempting login", 144 + ); 145 + 146 + const success = await authenticate(); 147 + 148 + if (success) { 149 + logger.info("Authentication successful"); 150 + return; 151 + } 152 + 153 + if (attempt < MAX_LOGIN_RETRIES) { 154 + logger.warn( 155 + { attempt, maxRetries: MAX_LOGIN_RETRIES, retryInMs: RETRY_DELAY_MS }, 156 + "Login failed, retrying", 157 + ); 158 + await sleep(RETRY_DELAY_MS); 159 + } 160 + } 161 + 162 + logger.error( 163 + { maxRetries: MAX_LOGIN_RETRIES }, 164 + "All login attempts failed, aborting", 165 + ); 166 + process.exit(1); 167 + })(); 168 + 169 + return loginPromise; 170 + } 171 + 172 + export const login = authenticateWithRetry; 173 + export const isLoggedIn = authenticateWithRetry().then(() => true);
+5
src/main.ts
··· 5 5 IdentityEvent, 6 6 } from "@skyware/jetstream"; 7 7 import { Jetstream } from "@skyware/jetstream"; 8 + import { login } from "./agent.js"; 8 9 import { 9 10 CURSOR_UPDATE_INTERVAL, 10 11 FIREHOSE_URL, ··· 344 345 logger.info({ process: "MAIN" }, "Connecting to Redis"); 345 346 await connectRedis(); 346 347 348 + logger.info({ process: "MAIN" }, "Authenticating with Bluesky"); 349 + await login(); 350 + logger.info({ process: "MAIN" }, "Authentication complete, starting Jetstream"); 351 + 347 352 jetstream.start(); 348 353 349 354 async function shutdown() {
+12 -1
src/tests/agent.test.ts
··· 13 13 OZONE_PDS: "pds.test.com", 14 14 })); 15 15 16 + // Mock session 17 + const mockSession = { 18 + did: "did:plc:test123", 19 + handle: "test.bsky.social", 20 + accessJwt: "test-access-jwt", 21 + refreshJwt: "test-refresh-jwt", 22 + }; 23 + 16 24 // Mock the AtpAgent 17 - const mockLogin = vi.fn(() => Promise.resolve()); 25 + const mockLogin = vi.fn(() => 26 + Promise.resolve({ success: true, data: mockSession }), 27 + ); 18 28 const mockConstructor = vi.fn(); 19 29 vi.doMock("@atproto/api", () => ({ 20 30 AtpAgent: class { 21 31 login = mockLogin; 22 32 service: URL; 33 + session = mockSession; 23 34 constructor(options: { service: string }) { 24 35 mockConstructor(options); 25 36 this.service = new URL(options.service);