Improve remote Matrix token refresh
This commit is contained in:
1 parent
b9df8459e3
commit
bffda95251
6 files changed
+226
-14
No files matched your search
@@ -20,6 +20,11 @@ export type { MatrixCredentials } from './matrix/types.js';
|
||||
export {
|
||||
extractMatrixCredentials,
|
||||
loadCachedCredentials,
|
||||
MatrixBrowserLoginRequiredError,
|
||||
saveCredentials,
|
||||
} from './matrix/token-extractor.js';
|
||||
export {
|
||||
MatrixRemoteLoginRequiredError,
|
||||
MatrixTokenRefresher,
|
||||
} from './matrix/token-refresher.js';
|
||||
export { MatrixMediaDownloader } from './matrix/http-media-downloader.js';
|
||||
@@ -16,6 +16,16 @@ export interface TokenExtractorOptions {
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export class MatrixBrowserLoginRequiredError extends Error {
|
||||
constructor(loginUrl: string) {
|
||||
super(
|
||||
`Reddit redirected Matrix token capture to the login page (${loginUrl}). ` +
|
||||
'Restore the browser session before retrying.',
|
||||
);
|
||||
this.name = 'MatrixBrowserLoginRequiredError';
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadCachedCredentials(
|
||||
cachePath = DEFAULT_CACHE_PATH,
|
||||
): Promise<MatrixCredentials | null> {
|
||||
@@ -101,6 +111,20 @@ export async function extractMatrixCredentials(
|
||||
};
|
||||
|
||||
page.on('request', onRequest);
|
||||
page.on('framenavigated', (frame) => {
|
||||
if (done || frame !== page.mainFrame()) return;
|
||||
try {
|
||||
const url = new URL(frame.url());
|
||||
const isRedditHost = url.hostname === 'reddit.com' || url.hostname === 'www.reddit.com';
|
||||
if (isRedditHost && url.pathname.startsWith('/login')) {
|
||||
done = true;
|
||||
logger.warn('[matrix] chat page redirected to Reddit login; token capture aborted');
|
||||
rejectCreds(new MatrixBrowserLoginRequiredError(url.toString()));
|
||||
}
|
||||
} catch {
|
||||
// Ignore transient or malformed navigation URLs.
|
||||
}
|
||||
});
|
||||
|
||||
// Reddit now redirects `chat.reddit.com` → `www.reddit.com/chat/`. Going
|
||||
// direct to the canonical URL avoids one redirect hop and means the
|
||||
@@ -124,17 +148,19 @@ export async function extractMatrixCredentials(
|
||||
);
|
||||
});
|
||||
|
||||
const timeout = new Promise<MatrixCredentials>((_, reject) =>
|
||||
setTimeout(
|
||||
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
||||
const timeout = new Promise<MatrixCredentials>((_, reject) => {
|
||||
timeoutHandle = setTimeout(
|
||||
() => reject(new Error(`Matrix token capture timed out after ${timeoutMs}ms`)),
|
||||
timeoutMs,
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
let captured: MatrixCredentials;
|
||||
try {
|
||||
captured = await Promise.race([credsPromise, timeout]);
|
||||
} catch (err) {
|
||||
if (err instanceof MatrixBrowserLoginRequiredError) throw err;
|
||||
// On timeout, surface enough info to tell the three failure modes apart:
|
||||
// (a) chat.reddit.com never loaded → page.url() still about:blank, almost no hosts
|
||||
// (b) page loaded, no Matrix host hit → reddit hosts present, no matrix.*
|
||||
@@ -156,6 +182,8 @@ export async function extractMatrixCredentials(
|
||||
logger.warn('[matrix] failed to collect timeout diagnostics:', diagErr);
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
if (timeoutHandle) clearTimeout(timeoutHandle);
|
||||
}
|
||||
|
||||
// Resolve the user_id by calling /whoami so the caller knows who we are.
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { BrowserContext } from 'playwright';
|
||||
import type { Logger } from '../../../utils/logger.js';
|
||||
import type { TokenRefresher } from '../service.js';
|
||||
import type { MatrixCredentials } from './types.js';
|
||||
import { extractMatrixCredentials } from './token-extractor.js';
|
||||
|
||||
export interface MatrixSessionBrowser {
|
||||
getContext(): BrowserContext | null;
|
||||
isSessionValid(): Promise<boolean>;
|
||||
login(): Promise<boolean>;
|
||||
}
|
||||
|
||||
export type MatrixCredentialExtractor = (
|
||||
context: BrowserContext,
|
||||
logger: Logger,
|
||||
) => Promise<MatrixCredentials>;
|
||||
|
||||
export class MatrixRemoteLoginRequiredError extends Error {
|
||||
constructor() {
|
||||
super(
|
||||
'Reddit browser session has expired and automatic login could not restore it. ' +
|
||||
'Open the dashboard and use the noVNC console to log in to Reddit, then retry chat sync.',
|
||||
);
|
||||
this.name = 'MatrixRemoteLoginRequiredError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the browser login when needed, then captures and applies one fresh
|
||||
* Matrix credential set. Concurrent callers share the same browser operation.
|
||||
*/
|
||||
export class MatrixTokenRefresher implements TokenRefresher {
|
||||
private refreshInFlight: Promise<void> | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly browser: MatrixSessionBrowser,
|
||||
private readonly logger: Logger,
|
||||
private readonly applyCredentials: (credentials: MatrixCredentials) => void,
|
||||
private readonly extract: MatrixCredentialExtractor = extractMatrixCredentials,
|
||||
) {}
|
||||
|
||||
refresh(): Promise<void> {
|
||||
if (this.refreshInFlight) return this.refreshInFlight;
|
||||
|
||||
this.refreshInFlight = this.performRefresh().finally(() => {
|
||||
this.refreshInFlight = null;
|
||||
});
|
||||
return this.refreshInFlight;
|
||||
}
|
||||
|
||||
private async performRefresh(): Promise<void> {
|
||||
const sessionValid = await this.browser.isSessionValid();
|
||||
if (!sessionValid) {
|
||||
this.logger.warn('[matrix] Reddit browser session expired; attempting login recovery');
|
||||
if (!(await this.browser.login())) {
|
||||
throw new MatrixRemoteLoginRequiredError();
|
||||
}
|
||||
this.logger.info('[matrix] Reddit browser session restored');
|
||||
}
|
||||
|
||||
const context = this.browser.getContext();
|
||||
if (!context) {
|
||||
throw new Error('Matrix token refresh requires an initialized browser context');
|
||||
}
|
||||
|
||||
const credentials = await this.extract(context, this.logger);
|
||||
this.applyCredentials(credentials);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user