Bind Reddit and Matrix session identities
This commit is contained in:
1 parent
34ae532cdd
commit
492efe0f8a
12 files changed
+234
-55
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user