From 2c533fa52a6df146d2e9acb7a265552e129ee0ef Mon Sep 17 00:00:00 2001 From: Jamie Brunton Date: Sat, 8 Aug 2026 16:03:54 +0100 Subject: [PATCH] Verify Reddit identity before Matrix capture --- src/bootstrap.ts | 26 +++++++++++++------------- src/services/account-auth-context.ts | 20 ++++++++++++++++++++ tests/account-auth-context.test.ts | 16 +++++++++++++++- 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/bootstrap.ts b/src/bootstrap.ts index 42db106..94b676f 100644 --- a/src/bootstrap.ts +++ b/src/bootstrap.ts @@ -31,7 +31,11 @@ import { MediaSyncService } from './services/media-sync.js'; import { RateLimiter } from './utils/rate-limiter.js'; import { fetchWithTimeout } from './utils/fetch-with-timeout.js'; import { WebServer } from './web/server.js'; -import { AccountAuthContext, type AuthenticatedAccount } from './services/account-auth-context.js'; +import { + AccountAuthContext, + authenticateCurrentAccount, + type AuthenticatedAccount, +} from './services/account-auth-context.js'; export interface AppContainer { config: Config; @@ -184,18 +188,14 @@ export async function bootstrap(config: Config, logger: Logger): Promise => { - credentialState.latest = null; - await tokenRefresher.refresh(); - const redditUsername = await browserClient.getAuthenticatedUsername(); - const freshCredentials = credentialState.latest as MatrixCredentials | null; - if (!redditUsername || !freshCredentials?.userId) return null; - return { - accountId: freshCredentials.userId, - redditUsername, - matrixUserId: freshCredentials.userId, - }; - }); + accountAuth.setAuthenticator(() => authenticateCurrentAccount({ + getRedditUsername: () => browserClient.getAuthenticatedUsername(), + refreshMatrixCredentials: async () => { + credentialState.latest = null; + await tokenRefresher.refresh(); + return credentialState.latest as MatrixCredentials | null; + }, + })); if (loggedIn) { logger.info('Verifying full Reddit and Matrix account authentication...'); diff --git a/src/services/account-auth-context.ts b/src/services/account-auth-context.ts index 8384f0a..fe9fbfa 100644 --- a/src/services/account-auth-context.ts +++ b/src/services/account-auth-context.ts @@ -33,6 +33,26 @@ export interface AccountAuthContextOptions { now?: () => number; } +export interface CurrentAccountAuthentication { + getRedditUsername(): Promise; + refreshMatrixCredentials(): Promise<{ userId: string } | null>; +} + +export async function authenticateCurrentAccount( + authentication: CurrentAccountAuthentication, +): Promise { + const redditUsername = await authentication.getRedditUsername(); + if (!redditUsername) return null; + + const credentials = await authentication.refreshMatrixCredentials(); + if (!credentials?.userId) return null; + return { + accountId: credentials.userId, + redditUsername, + matrixUserId: credentials.userId, + }; +} + export const denyAccountAuth: AccountAuthGate = { isFullyAuthenticated: () => false, invalidate: () => undefined, diff --git a/tests/account-auth-context.test.ts b/tests/account-auth-context.test.ts index 4ef3399..f158eed 100644 --- a/tests/account-auth-context.test.ts +++ b/tests/account-auth-context.test.ts @@ -1,8 +1,22 @@ import { describe, expect, it, vi } from 'vitest'; -import { AccountAuthContext } from '../src/services/account-auth-context.js'; +import { + AccountAuthContext, + authenticateCurrentAccount, +} from '../src/services/account-auth-context.js'; import { createMockLogger } from './mockLogger.js'; describe('AccountAuthContext', () => { + it('does not attempt Matrix capture until Reddit identity is authoritative', async () => { + const refreshMatrixCredentials = vi.fn(); + + await expect(authenticateCurrentAccount({ + getRedditUsername: vi.fn().mockResolvedValue(null), + refreshMatrixCredentials, + })).resolves.toBeNull(); + + expect(refreshMatrixCredentials).not.toHaveBeenCalled(); + }); + it('fails closed until Reddit and Matrix resolve to one authenticated account', async () => { const auth = new AccountAuthContext(createMockLogger()); const authenticate = vi.fn().mockResolvedValue({