feat: self-refresh Matrix tokens from the account browser session

Expired Matrix bearers no longer pause an account until a new extension
dump is uploaded. BrowserMatrixTokenRefresher loads chat.reddit.com in
the account's persistent browser profile (whose Reddit cookies stay
valid), lifts the fresh bearer off the first authenticated Matrix
request, verifies it via whoami, swaps it into the live MatrixClient
and persists it to the account row.

Wired into the per-account runtime at boot verification, the auth-gate
authenticator, and ChatService's 401 retry path. A dump is now only
needed when the Reddit cookies themselves no longer authenticate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jamie BruntonandClaude Fable 5 committed 2026-08-10 08:56:17 +01:00
1 parent f34129666e
commit 82be197b79
7 files changed
+426 -8

No files matched your search

+29 -6
View File
@@ -12,7 +12,7 @@ import {
ActivityLogRepository,
RecipientHistoryRepository,
} from '../database/repositories.js';
import { AccountContentRepository } from '../database/account-repositories.js';
import { AccountContentRepository, RedditAccountsRepository } from '../database/account-repositories.js';
import { RedditApiClient, RedditRateLimitError, type RedditPost } from '../reddit/api.js';
import { RedditRequestCoordinator } from '../reddit/request-coordinator.js';
import { OpenRouterClient } from '../ai/openrouter.js';
@@ -28,8 +28,9 @@ import {
} from './account-auth-context.js';
import { ChatCache } from './chat/cache.js';
import {
ChatService, MatrixClient, MatrixMediaDownloader, MatrixTokenRefresher,
MatrixUsernameResolver, type UsernameResolver,
BrowserMatrixTokenRefresher, ChatService, MatrixAuthError, MatrixClient,
MatrixMediaDownloader, MatrixUsernameResolver,
type MatrixCredentials, type UsernameResolver,
} from './chat/index.js';
import { MediaService } from './media.js';
import { MediaSyncService } from './media-sync.js';
@@ -116,7 +117,29 @@ async function buildRuntime(
}
const matrixClient = new MatrixClient(session.matrixCredentials, logger);
if (await matrixClient.whoami() !== session.matrixUserId) {
const accountsRepo = new RedditAccountsRepository(db);
const tokenRefresher = new BrowserMatrixTokenRefresher({
context: browserClient.getContext()!,
expectedUserId: session.matrixUserId,
verifyCredentials: (credentials) => new MatrixClient(credentials, logger).whoami(),
applyCredentials: (credentials) => matrixClient.updateCredentials(credentials),
persistCredentials: (credentials: MatrixCredentials) => {
session.matrixCredentials = credentials;
accountsRepo.updateMatrixCredentials(session.accountId, JSON.stringify(credentials));
},
logger,
});
const verifyMatrixSession = async (refreshOnAuthError: boolean): Promise<boolean> => {
try {
return await matrixClient.whoami() === session.matrixUserId;
} catch (error) {
if (!refreshOnAuthError || !(error instanceof MatrixAuthError)) throw error;
logger.warn(`Stored Matrix token for u/${session.redditUsername} was rejected — refreshing from the browser session`);
await tokenRefresher.refresh();
return await matrixClient.whoami() === session.matrixUserId;
}
};
if (!(await verifyMatrixSession(true))) {
throw new Error(`Stored Matrix session no longer verifies for u/${session.redditUsername}`);
}
@@ -125,7 +148,7 @@ async function buildRuntime(
accountAuth.setAuthenticator(async () => {
const current = await browserClient.getAuthenticatedIdentity();
if (!current || matrixRedditId(session.matrixUserId) !== current.accountId.toLowerCase().replace(/^t2_/, '')) return null;
return await matrixClient.whoami() === session.matrixUserId
return await verifyMatrixSession(true)
? runtimeAuthenticatedAccount(session, current)
: null;
});
@@ -182,7 +205,7 @@ async function buildRuntime(
const chatCache = new ChatCache(logger, undefined, accountContentRepo, session.accountId);
const chatService = new ChatService(
matrixClient, logger, successfulMessagesRepo, mediaService,
new MatrixTokenRefresher(), usernameResolver, chatCache,
tokenRefresher, usernameResolver, chatCache,
messagesRepo, redditUsersRepo, accountAuth, conversationOutcomesRepo, conversationStateRepo,
);
await chatService.loadCache();