Bind Reddit and Matrix session identities

This commit is contained in:
jjbrunton committed 2026-08-08 19:43:35 +01:00
1 parent 34ae532cdd
commit 492efe0f8a
12 files changed
+234 -55

No files matched your search

+39 -10
View File
@@ -6,11 +6,18 @@ export type AccountAuthStatus =
| 'authenticated';
export interface AuthenticatedAccount {
/** Reddit account fullname, e.g. `t2_abc123`. */
accountId: string;
redditUsername: string;
matrixUserId: string;
}
export interface RedditAccountIdentity {
username: string;
/** Reddit's account ID, with or without the `t2_` prefix. */
accountId: string;
}
export interface AccountAuthSnapshot {
status: AccountAuthStatus;
accountId: string | null;
@@ -34,21 +41,43 @@ export interface AccountAuthContextOptions {
}
export interface CurrentAccountAuthentication {
getRedditUsername(): Promise<string | null>;
getRedditIdentity(): Promise<RedditAccountIdentity | null>;
refreshMatrixCredentials(): Promise<{ userId: string } | null>;
}
export function normalizeRedditAccountId(accountId: string): string {
const trimmed = accountId.trim();
const normalized = trimmed.startsWith('t2_') ? trimmed : `t2_${trimmed}`;
if (!/^t2_[a-z0-9]+$/i.test(normalized)) {
throw new Error('Reddit returned an invalid account ID');
}
return normalized;
}
export function expectedMatrixUserId(accountId: string): string {
return `@${normalizeRedditAccountId(accountId)}:reddit.com`;
}
export function assertMatchingAccountIdentity(account: AuthenticatedAccount): void {
const expected = expectedMatrixUserId(account.accountId);
if (account.matrixUserId !== expected) {
throw new Error(
`Reddit account u/${account.redditUsername} (${normalizeRedditAccountId(account.accountId)}) does not match Matrix account ${account.matrixUserId}`,
);
}
}
export async function authenticateCurrentAccount(
authentication: CurrentAccountAuthentication,
): Promise<AuthenticatedAccount | null> {
const redditUsername = await authentication.getRedditUsername();
if (!redditUsername) return null;
const redditIdentity = await authentication.getRedditIdentity();
if (!redditIdentity) return null;
const credentials = await authentication.refreshMatrixCredentials();
if (!credentials?.userId) return null;
return {
accountId: credentials.userId,
redditUsername,
accountId: normalizeRedditAccountId(redditIdentity.accountId),
redditUsername: redditIdentity.username,
matrixUserId: credentials.userId,
};
}
@@ -104,15 +133,15 @@ export class AccountAuthContext implements AccountAuthGate {
if (!account.accountId || !account.redditUsername || !account.matrixUserId) {
throw new Error('A fully authenticated account requires Reddit and Matrix identities');
}
if (account.accountId !== account.matrixUserId) {
throw new Error('Authenticated account ID must match the Matrix user ID');
}
assertMatchingAccountIdentity(account);
const changedAccount = this.snapshot.accountId !== account.accountId;
const accountId = normalizeRedditAccountId(account.accountId);
const changedAccount = this.snapshot.accountId !== accountId;
const wasAuthenticated = this.isFullyAuthenticated();
this.snapshot = {
status: 'authenticated',
accountId: account.accountId,
accountId,
redditUsername: account.redditUsername,
matrixUserId: account.matrixUserId,
reason: null,
+24 -7
View File
@@ -2,11 +2,16 @@ import type { Cookie } from 'playwright';
import type { MatrixClient } from './chat/matrix/client.js';
import type { MatrixCredentials } from './chat/matrix/types.js';
import type { AccountAuthContext } from './account-auth-context.js';
import {
assertMatchingAccountIdentity,
normalizeRedditAccountId,
type RedditAccountIdentity,
} from './account-auth-context.js';
interface BrowserSessionTarget {
importRedditCookies(cookies: Cookie[]): Promise<void>;
isSessionValid(): Promise<boolean>;
getAuthenticatedUsername(): Promise<string | null>;
getAuthenticatedIdentity(): Promise<RedditAccountIdentity | null>;
}
interface ChatSessionTarget {
@@ -41,17 +46,29 @@ export class RedditSessionImporter {
}
await this.browser.importRedditCookies(cookies);
if (!(await this.browser.isSessionValid())) {
throw new Error('Imported cookies did not authenticate with Reddit');
let redditIdentity: RedditAccountIdentity;
try {
if (!(await this.browser.isSessionValid())) {
throw new Error('Imported cookies did not authenticate with Reddit');
}
const identity = await this.browser.getAuthenticatedIdentity();
if (!identity) throw new Error('Reddit did not return an authenticated account identity');
redditIdentity = identity;
assertMatchingAccountIdentity({
accountId: identity.accountId,
redditUsername: identity.username,
matrixUserId: userId,
});
} catch (error) {
this.accountAuth.invalidate(error instanceof Error ? error.message : String(error));
throw error;
}
const redditUsername = await this.browser.getAuthenticatedUsername();
if (!redditUsername) throw new Error('Reddit did not return an authenticated account identity');
this.saveCredentials(credentials);
this.chat.setMatrixClient(matrixClient);
this.accountAuth.markAuthenticated({
accountId: userId,
redditUsername,
accountId: normalizeRedditAccountId(redditIdentity.accountId),
redditUsername: redditIdentity.username,
matrixUserId: userId,
});
}