···11+import {
22+ NodeOAuthClient,
33+ atprotoLoopbackClientMetadata,
44+} from "@atproto/oauth-client-node";
55+import { JoseKey } from "@atproto/jwk-jose";
66+import { stateStore, sessionStore } from "./oauth-stores-db";
77+import { getOAuthConfig } from "./oauth-config";
88+99+function normalizePrivateKey(key: string): string {
1010+ if (!key.includes("\n") && key.includes("\\n")) {
1111+ return key.replace(/\\n/g, "\n");
1212+ }
1313+ return key;
1414+}
1515+1616+/**
1717+ * Creates and returns a configured OAuth client based on environment
1818+ * Centralizes the client creation logic used across all endpoints
1919+ */
2020+export async function createOAuthClient(): Promise<NodeOAuthClient> {
2121+ const config = getOAuthConfig();
2222+ const isDev = config.clientType === "loopback";
2323+2424+ if (isDev) {
2525+ // Loopback mode for local development
2626+ console.log("[oauth-client] Creating loopback OAuth client");
2727+ const clientMetadata = atprotoLoopbackClientMetadata(config.clientId);
2828+2929+ return new NodeOAuthClient({
3030+ clientMetadata: clientMetadata,
3131+ stateStore: stateStore as any,
3232+ sessionStore: sessionStore as any,
3333+ });
3434+ } else {
3535+ // Production mode with private key
3636+ console.log("[oauth-client] Creating production OAuth client");
3737+3838+ if (!process.env.OAUTH_PRIVATE_KEY) {
3939+ throw new Error("OAUTH_PRIVATE_KEY is required for production");
4040+ }
4141+4242+ const normalizedKey = normalizePrivateKey(process.env.OAUTH_PRIVATE_KEY);
4343+ const privateKey = await JoseKey.fromImportable(normalizedKey, "main-key");
4444+4545+ return new NodeOAuthClient({
4646+ clientMetadata: {
4747+ client_id: config.clientId,
4848+ client_name: "ATlast",
4949+ client_uri: config.clientId.replace("/oauth-client-metadata.json", ""),
5050+ redirect_uris: [config.redirectUri],
5151+ scope: "atproto transition:generic",
5252+ grant_types: ["authorization_code", "refresh_token"],
5353+ response_types: ["code"],
5454+ application_type: "web",
5555+ token_endpoint_auth_method: "private_key_jwt",
5656+ token_endpoint_auth_signing_alg: "ES256",
5757+ dpop_bound_access_tokens: true,
5858+ jwks_uri: config.jwksUri,
5959+ },
6060+ keyset: [privateKey],
6161+ stateStore: stateStore as any,
6262+ sessionStore: sessionStore as any,
6363+ });
6464+ }
6565+}
+4-8
netlify/functions/logout.ts
···11import { Handler, HandlerEvent, HandlerResponse } from "@netlify/functions";
22-import { userSessions } from "./oauth-stores-db";
22+import { SessionManager } from "./session-manager";
33import { getOAuthConfig } from "./oauth-config";
44import cookie from "cookie";
55···2727 console.log("[logout] Session ID from cookie:", sessionId);
28282929 if (sessionId) {
3030- // Get the DID before deleting
3131- const userSession = await userSessions.get(sessionId);
3232- const did = userSession?.did;
3333-3434- // Delete session from database
3535- await userSessions.del(sessionId);
3636- console.log("[logout] Deleted session from database");
3030+ // Use SessionManager to properly clean up both user and OAuth sessions
3131+ await SessionManager.deleteSession(sessionId);
3232+ console.log("[logout] Successfully deleted session:", sessionId);
3733 }
38343935 // Clear the session cookie with matching flags from when it was set