Fix Auth Issue #3

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

Fixes #1

+2 -1
.claude/settings.local.json
···
"mcp__git-mcp-server__git_set_working_dir",
"Bash(npm run test:run:*)",
"Bash(bunx eslint:*)",
-
"Bash(bun test:run:*)"
+
"Bash(bun test:run:*)",
+
"Bash(bun run type-check:*)"
],
"deny": [],
"ask": []
+1
.gitignore
···
labels.db*
.DS_Store
coverage/
+
.session
-10
.session
···
-
{
-
"accessJwt": "eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJFUzI1NksifQ.eyJzY29wZSI6ImNvbS5hdHByb3RvLmFwcFBhc3MiLCJzdWIiOiJkaWQ6cGxjOmpzamxoajM1NzRvZHRmcWF6cXhuNG9uZCIsImlhdCI6MTc2MTA3OTUzMSwiZXhwIjoxNzYxMDg2NzMxLCJhdWQiOiJkaWQ6d2ViOm95c3RlcmxpbmcudXMtd2VzdC5ob3N0LmJza3kubmV0d29yayJ9.2EPsA8yDLvngSPzOu-DHy-2SQCjgzk4wFxgsOL7BXq1gwmRkoJy_Poykjb8m9JeYt9_s08-VCM_h1C43FOVosg",
-
"refreshJwt": "eyJ0eXAiOiJyZWZyZXNoK2p3dCIsImFsZyI6IkVTMjU2SyJ9.eyJzY29wZSI6ImNvbS5hdHByb3RvLnJlZnJlc2giLCJzdWIiOiJkaWQ6cGxjOmpzamxoajM1NzRvZHRmcWF6cXhuNG9uZCIsImF1ZCI6ImRpZDp3ZWI6YnNreS5zb2NpYWwiLCJqdGkiOiJpOWVKcTVHa0VQeS9ISVV0YWtUb0dqMW55Mllzb25PK0VGMHUySGRoNFNFIiwiaWF0IjoxNzYxMDc5NTMxLCJleHAiOjE3Njg4NTU1MzF9.5YawggT0amOGgZryO5h2kJ11ePimtc0YMqs8W-ZzxPkU8aymD0m29w4_wZXeyoK4vclU-YvlUc9iDr5SgrqJ2w",
-
"handle": "automod.skywatch.blue",
-
"did": "did:plc:jsjlhj3574odtfqazqxn4ond",
-
"email": "bsky.duration409@passmail.net",
-
"emailConfirmed": true,
-
"emailAuthFactor": true,
-
"active": true
-
}
+1 -1
package.json
···
{
"name": "skywatch-tools",
-
"version": "1.3.0",
+
"version": "2.0.1",
"type": "module",
"scripts": {
"start": "npx tsx src/main.ts",
+50 -4
src/agent.ts
···
}
}
+
const MAX_LOGIN_RETRIES = 3;
+
const RETRY_DELAY_MS = 2000;
+
+
let loginPromise: Promise<void> | null = null;
+
+
async function sleep(ms: number): Promise<void> {
+
return new Promise((resolve) => setTimeout(resolve, ms));
+
}
+
async function authenticate(): Promise<boolean> {
const savedSession = loadSession();
···
return performLogin();
}
-
export const login = authenticate;
-
export const isLoggedIn = authenticate()
-
.then((success) => success)
-
.catch(() => false);
+
async function authenticateWithRetry(): Promise<void> {
+
// 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);
+5
src/main.ts
···
IdentityEvent,
} from "@skyware/jetstream";
import { Jetstream } from "@skyware/jetstream";
+
import { login } from "./agent.js";
import {
CURSOR_UPDATE_INTERVAL,
FIREHOSE_URL,
···
logger.info({ process: "MAIN" }, "Connecting to Redis");
await connectRedis();
+
+
logger.info({ process: "MAIN" }, "Authenticating with Bluesky");
+
await login();
+
logger.info({ process: "MAIN" }, "Authentication complete, starting Jetstream");
jetstream.start();
+12 -1
src/tests/agent.test.ts
···
OZONE_PDS: "pds.test.com",
}));
+
// Mock session
+
const mockSession = {
+
did: "did:plc:test123",
+
handle: "test.bsky.social",
+
accessJwt: "test-access-jwt",
+
refreshJwt: "test-refresh-jwt",
+
};
+
// Mock the AtpAgent
-
const mockLogin = vi.fn(() => Promise.resolve());
+
const mockLogin = vi.fn(() =>
+
Promise.resolve({ success: true, data: mockSession }),
+
);
const mockConstructor = vi.fn();
vi.doMock("@atproto/api", () => ({
AtpAgent: class {
login = mockLogin;
service: URL;
+
session = mockSession;
constructor(options: { service: string }) {
mockConstructor(options);
this.service = new URL(options.service);