Something went wrong. Try again.
leaflet fork for copy edit / bug fixes forked from leaflet.pub/leaflet
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192import { NodeOAuthClient, NodeSavedSession, NodeSavedState, RuntimeLock, OAuthSession, DidCache,} from "@atproto/oauth-client-node";import { JoseKey } from "@atproto/jwk-jose";import { oauth_metadata, oauth_store_key_prefix,} from "app/api/oauth/[route]/oauth-metadata";import { supabaseServerClient } from "supabase/serverClient";
import Client from "ioredis";import Redlock from "redlock";import { Result, Ok, Err } from "./result";
// Module-scoped singleton: NodeOAuthClient, ioredis connection, and Redlock// have no per-request state — keys/stores live above the user — so building// them once per Node instance avoids reconnect + keyset re-import on every call.// Stashed on globalThis so Next.js dev hot-reload doesn't leak Redis sockets.const globalForOauth = globalThis as unknown as { __oauthClient?: Promise<NodeOAuthClient>;};
export function createOauthClient(): Promise<NodeOAuthClient> { if (!globalForOauth.__oauthClient) { globalForOauth.__oauthClient = buildOauthClient(); } return globalForOauth.__oauthClient;}
// The client's DID→document cache maps a DID to its PDS, and its only reader// is authorize() — token refresh re-resolves with noCache — so caching here// buys nothing but the risk of building a login redirect against a PDS the// user has migrated away from (the library default caches for 1h in memory).// Logins are rare; resolve fresh every time.const noDidCache: DidCache = { get: () => undefined, set: () => {}, del: () => {},};
async function buildOauthClient(): Promise<NodeOAuthClient> { let keyset = process.env.NODE_ENV === "production" ? await Promise.all([ JoseKey.fromImportable(process.env.JOSE_PRIVATE_KEY_1!), ]) : undefined; let requestLock: RuntimeLock | undefined; if (process.env.NODE_ENV === "production" && process.env.REDIS_URL) { const client = new Client(process.env.REDIS_URL); const redlock = new Redlock([client]); requestLock = async (key, fn) => { // 30 seconds should be enough. Since we will be using one lock per user id // we can be quite liberal with the lock duration here. const lock = await redlock.acquire([key], 45e3); try { return await fn(); } finally { await lock.release(); } }; } return new NodeOAuthClient({ // This object will be used to build the payload of the /client-metadata.json // endpoint metadata, exposing the client metadata to the OAuth server. clientMetadata: oauth_metadata,
// Used to authenticate the client to the token endpoint. Will be used to // build the jwks object to be exposed on the "jwks_uri" endpoint. keyset,
// Interface to store authorization state data (during authorization flows) stateStore, // Interface to store authenticated session data sessionStore, requestLock,
didCache: noDidCache, });}
const storeKey = (key: string) => oauth_store_key_prefix + key;
let stateStore = { async set(key: string, state: NodeSavedState): Promise<void> { await supabaseServerClient .from("oauth_state_store") .upsert({ key: storeKey(key), state }); }, async get(key: string): Promise<NodeSavedState | undefined> { let { data } = await supabaseServerClient .from("oauth_state_store") .select("state") .eq("key", storeKey(key)) .single(); return (data?.state as NodeSavedState) || undefined; }, async del(key: string): Promise<void> { await supabaseServerClient .from("oauth_state_store") .delete() .eq("key", storeKey(key)); },};
let sessionStore = { async set(key: string, session: NodeSavedSession): Promise<void> { await supabaseServerClient .from("oauth_session_store") .upsert({ key: storeKey(key), session }); }, async get(key: string): Promise<NodeSavedSession | undefined> { let { data } = await supabaseServerClient .from("oauth_session_store") .select("session") .eq("key", storeKey(key)) .single(); return (data?.session as NodeSavedSession) || undefined; }, async del(key: string): Promise<void> { await supabaseServerClient .from("oauth_session_store") .delete() .eq("key", storeKey(key)); },};
export type OAuthSessionError = { type: "oauth_session_expired"; message: string; did: string;};
// In-process dedupe: collapse concurrent restore() calls for the same DID into// one underlying restore + Redlock acquisition. Successful entries linger// briefly so a burst of requests (e.g. hover-fired ProfilePopovers) share one// result; rejected promises evict immediately so a transient failure doesn't// stick around poisoning subsequent calls.const RESTORE_DEDUPE_TTL_MS = 5_000;const inFlightRestores = new Map<string, Promise<OAuthSession>>();
function dedupedRestore(did: string): Promise<OAuthSession> { let existing = inFlightRestores.get(did); if (existing) return existing;
const promise = (async () => { const oauthClient = await createOauthClient(); return oauthClient.restore(did); })(); inFlightRestores.set(did, promise);
promise.then( () => { setTimeout(() => { if (inFlightRestores.get(did) === promise) { inFlightRestores.delete(did); } }, RESTORE_DEDUPE_TTL_MS); }, () => { if (inFlightRestores.get(did) === promise) { inFlightRestores.delete(did); } }, );
return promise;}
export async function restoreOAuthSession( did: string): Promise<Result<OAuthSession, OAuthSessionError>> { try { const session = await dedupedRestore(did); return Ok(session); } catch (error) { return Err({ type: "oauth_session_expired", message: error instanceof Error ? error.message : "OAuth session expired or invalid", did, }); }}