Atproto AMA app
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 125 lines 4.1 kB view raw
1import { 2 NodeOAuthClient, 3 type NodeSavedSession, 4 type NodeSavedState, 5 type OAuthClientMetadataInput, 6} from "@atproto/oauth-client-node"; 7import { eq } from "drizzle-orm"; 8 9import { db } from "~/lib/db"; 10import { oauthSessions, oauthStates } from "~/lib/schema"; 11 12function resolveUrl(): string { 13 const publicUrl = process.env.PUBLIC_URL; 14 if (publicUrl) return publicUrl.replace(/\/$/, ""); 15 // APP_URL is used in dev to set a loopback-compatible origin (e.g. http://[::1]:3000) 16 // so the app and the PDS are cross-site in the browser (required by oauth-provider). 17 const appUrl = process.env.APP_URL; 18 if (appUrl) return appUrl.replace(/\/$/, ""); 19 const port = process.env.PORT || 3000; 20 return `http://127.0.0.1:${port}`; 21} 22 23export function getOAuthClientMetadata(): OAuthClientMetadataInput { 24 const publicUrl = process.env.PUBLIC_URL; 25 const url = resolveUrl(); 26 const enc = encodeURIComponent; 27 28 return { 29 client_name: "Askimut", 30 client_id: publicUrl 31 ? `${url}/client-metadata.json` 32 : `http://localhost?redirect_uri=${enc(`${url}/oauth/callback`)}&scope=${enc("atproto transition:generic")}`, 33 client_uri: url, 34 redirect_uris: [`${url}/oauth/callback`], 35 scope: "atproto transition:generic", 36 grant_types: ["authorization_code", "refresh_token"], 37 response_types: ["code"], 38 application_type: "web", 39 token_endpoint_auth_method: "none", 40 dpop_bound_access_tokens: true, 41 }; 42} 43 44function drizzleStateStore() { 45 return { 46 async set(key: string, val: NodeSavedState): Promise<void> { 47 const state = JSON.stringify(val); 48 await db 49 .insert(oauthStates) 50 .values({ key, state, createdAt: new Date() }) 51 .onConflictDoUpdate({ 52 target: oauthStates.key, 53 set: { state, createdAt: new Date() }, 54 }); 55 }, 56 async get(key: string): Promise<NodeSavedState | undefined> { 57 const row = await db.query.oauthStates.findFirst({ 58 where: eq(oauthStates.key, key), 59 }); 60 if (!row) return undefined; 61 return JSON.parse(row.state) as NodeSavedState; 62 }, 63 async del(key: string): Promise<void> { 64 await db.delete(oauthStates).where(eq(oauthStates.key, key)); 65 }, 66 }; 67} 68 69function drizzleSessionStore() { 70 return { 71 async set(key: string, val: NodeSavedSession): Promise<void> { 72 const session = JSON.stringify(val); 73 await db 74 .insert(oauthSessions) 75 .values({ key, session, createdAt: new Date() }) 76 .onConflictDoUpdate({ 77 target: oauthSessions.key, 78 set: { session, createdAt: new Date() }, 79 }); 80 }, 81 async get(key: string): Promise<NodeSavedSession | undefined> { 82 const row = await db.query.oauthSessions.findFirst({ 83 where: eq(oauthSessions.key, key), 84 }); 85 if (!row) return undefined; 86 return JSON.parse(row.session) as NodeSavedSession; 87 }, 88 async del(key: string): Promise<void> { 89 await db.delete(oauthSessions).where(eq(oauthSessions.key, key)); 90 }, 91 }; 92} 93 94function oauthAllowHttp(): boolean { 95 // Without PUBLIC_URL we already use http://127.0.0.1 for the app → allow http PDS. 96 if (!process.env.PUBLIC_URL) return true; 97 // .env often sets PUBLIC_URL for redirects while DEV_PDS_URL is still http (dev-network). 98 const pds = process.env.DEV_PDS_URL; 99 try { 100 return Boolean(pds && new URL(pds).protocol === "http:"); 101 } catch { 102 return false; 103 } 104} 105 106export async function createOAuthClient(): Promise<NodeOAuthClient> { 107 const devPdsUrl = process.env.DEV_PDS_URL; 108 const devPlcUrl = process.env.DEV_PLC_URL; 109 110 return new NodeOAuthClient({ 111 clientMetadata: getOAuthClientMetadata(), 112 stateStore: drizzleStateStore(), 113 sessionStore: drizzleSessionStore(), 114 allowHttp: oauthAllowHttp(), 115 ...(devPdsUrl && { handleResolver: devPdsUrl }), 116 ...(devPlcUrl && { plcDirectoryUrl: devPlcUrl }), 117 }); 118} 119 120let clientPromise: Promise<NodeOAuthClient> | undefined; 121 122export function getOAuthClient(): Promise<NodeOAuthClient> { 123 clientPromise ??= createOAuthClient(); 124 return clientPromise; 125}