Something went wrong. Try again.
[WIP] Matrix Client for web and desktop
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198import { createContext, createSignal } from "solid-js";import { createClient, OAuth2, TokenRefresher, type MatrixClient,} from "matrix-js-sdk";
/** One stored Matrix session. Indexed by (homeserverUrl, userId) pair. */export interface StoredSession { homeserverUrl: string; userId: string; deviceId: string; clientId: string; accessToken: string | undefined; refreshToken: string | undefined; /** Path-independent writable key for sorting / identity. */ id: string;}
/** Sessions index + active pointer. */const SESSIONS_KEY = "asterisk.matrix.sessions";const ACTIVE_KEY = "asterisk.matrix.active";
function keyParts(s: StoredSession): string { return `${s.homeserverUrl}/${s.userId}`;}
/** True if this session's id matches the current active account. */export function isActiveSession(session: StoredSession): boolean { return session.id === getActiveAccountId();}
export const MatrixContext = createContext<MatrixClient | undefined>();// Keep the client undefined until we have it authenticated.// Initialised asynchronously by `initMatrix()` in App.export const [matrixClient, setMatrixClient] = createSignal<MatrixClient>();
export function getSessions(): StoredSession[] { const raw = localStorage.getItem(SESSIONS_KEY); if (!raw) return []; try { const list = JSON.parse(raw) as StoredSession[]; return Array.isArray(list) ? list : []; } catch { return []; }}
function writeSessions(sessions: StoredSession[]): void { localStorage.setItem(SESSIONS_KEY, JSON.stringify(sessions));}
export function getActiveAccountId(): string | null { return localStorage.getItem(ACTIVE_KEY);}
export function setActiveAccountId(id: string | null): void { if (id === null) { localStorage.removeItem(ACTIVE_KEY); } else { localStorage.setItem(ACTIVE_KEY, id); }}
export function removeSession(session: StoredSession): void { writeSessions(getSessions().filter((s) => s.id !== session.id));
if (getActiveAccountId() === session.id) setActiveAccountId(null);}
/** Upsert one session; for the matching (homeserverUrl, userId) replace it. */export function upsertSession(session: StoredSession): StoredSession { const without = getSessions().filter( (s) => keyParts(s) !== keyParts(session), ); const next = [...without, session]; writeSessions(next); return session;}
/** Migrate legacy single-session storage to the multi-session index. */export function migrateLegacySingleSession(): void { const raw = localStorage.getItem("asterisk.matrix.session"); if (!raw) return;
try { const legacy = JSON.parse(raw) as { homeserverUrl: string; clientOpts: Record<string, unknown>; }; const opts = legacy.clientOpts ?? {}; const clientId = localStorage.getItem( `asterisk.oauth.client.${legacy.homeserverUrl}`, ); const session: StoredSession = { homeserverUrl: legacy.homeserverUrl ?? (opts.baseUrl as string) ?? "", userId: (opts.userId as string) ?? "", deviceId: (opts.deviceId as string) ?? "", clientId: clientId ?? "", accessToken: opts.accessToken as string | undefined, refreshToken: opts.refreshToken as string | undefined, id: `${legacy.homeserverUrl}/${(opts.userId as string) ?? ""}`, }; if (session.userId) upsertSession(session); } finally { localStorage.removeItem("asterisk.matrix.session"); }}
/** * Persist a session built explicitly (preferred source of truth over * inferring fields from the client, which can drift from storage). */export function adoptMatrixClient(client: MatrixClient): StoredSession { const homeserverUrl = client.getHomeserverUrl(); const userId = client.getUserId() ?? ""; const session: StoredSession = { homeserverUrl, userId, deviceId: client.getDeviceId() ?? "", clientId: "", // filled in by auth/callback after adoption accessToken: client.getAccessToken() ?? undefined, refreshToken: client.getRefreshToken() ?? undefined, id: `${homeserverUrl}/${userId}`, }; upsertSession(session); setMatrixClient(client); return session;}
/** * Recreate a client from the stored session identified by `session`, or by * the active account if `session` is omitted. Handles expiry by refreshing * and persists new tokens. */export async function loadMatrixSession( session?: StoredSession,): Promise<MatrixClient | null> { const target = session ?? getSessions().find((s) => isActiveSession(s)) ?? getSessions()[0]; if (!target?.accessToken) return null;
const oauthMetadata = await createClient({ baseUrl: target.homeserverUrl, }).getAuthMetadata();
const onRefresh = async (tokens: { accessToken: string; refreshToken?: string; }) => { // Persist rotated tokens. Grab the existing record to preserve fields // that only live in storage (clientId), not on the client itself. const existing = getSessions().find((s) => s.id === target.id); const updated: StoredSession = { ...(existing ?? target), accessToken: tokens.accessToken, refreshToken: tokens.refreshToken, }; upsertSession(updated); // Don't change active account here — that's user intent, not // token bookkeeping. };
const tokenRefreshFunction = new TokenRefresher( new OAuth2(oauthMetadata, { clientId: target.clientId, // performRefreshTokenGrant never touches redirectUri; satisfy the // Context shape with a sentinel. redirectUri: "urn:ietfa:oauth:2.0:oob", }), onRefresh, ).tokenRefreshFunction;
try { const client = createClient({ baseUrl: target.homeserverUrl, userId: target.userId, deviceId: target.deviceId, accessToken: target.accessToken, refreshToken: target.refreshToken, tokenRefreshFunction, });
// Eagerly kick a whoami to validate the tokens. If the access token is // stale this will refresh it via the SDK before it even requests. await client.whoami();
setMatrixClient(client); return client; } catch { return null; }}