A build your own ATProto adventure, OAuth already figured out for you. demo.atpoke.xyz
at main 81 lines 3.8 kB view raw
1// Loads all your OAuth settings from here and then uses the client everywhere else. metadata endpoint, jwks, etc 2 3import { atprotoLoopbackClientMetadata, Keyset, NodeOAuthClient } from '@atproto/oauth-client-node'; 4import { JoseKey } from '@atproto/jwk-jose'; 5import { db } from '$lib/server/db'; 6import { SessionStore, StateStore } from '$lib/server/atproto/storage'; 7import { env } from '$env/dynamic/private'; 8import type { OAuthClientMetadataInput } from '@atproto/oauth-types'; 9 10//You will need to change these if you are using another collection, can also change by setting the env OAUTH_SCOPES 11//For permission to all you can uncomment below 12// const DEFAULT_SCOPES = 'atproto transition:generic'; 13const DEFAULT_SCOPES = 'atproto repo:app.bsky.feed.post?action=create repo:xyz.atpoke.graph.poke'; 14const loadJwk = async () => { 15 const raw = env.OAUTH_JWK; 16 if (!raw) return undefined; 17 const json = JSON.parse(raw); 18 if (!json) return undefined; 19 const keys = await Promise.all( 20 json.map((jwk: string | Record<string, unknown>) => JoseKey.fromJWK(jwk)), 21 ); 22 return new Keyset(keys); 23}; 24 25 26let client: Promise<NodeOAuthClient> | null = null; 27 28export const atpOAuthClient = async () => { 29 if (!client) { 30 client = (async () => { 31 const rootDomain = env.OAUTH_DOMAIN ?? '127.0.0.1:5173'; 32 const dev = env.DEV !== undefined; 33 const isConfidential = env.OAUTH_JWK !== undefined; 34 35 if(!dev && env.OAUTH_DOMAIN === undefined){ 36 throw new Error('OAUTH_DOMAIN must be set in production'); 37 } 38 39 const keyset = env.OAUTH_JWK && env.OAUTH_DOMAIN 40 ? await loadJwk() 41 : undefined; 42 // @ts-expect-error I have no idea why it doesn't like use 43 const pk = keyset?.findPrivateKey({ use: 'sig' }); 44 const rootUrl = `https://${rootDomain}`; 45 const clientMetadata: OAuthClientMetadataInput = dev 46 ? atprotoLoopbackClientMetadata( 47 `http://localhost?${new URLSearchParams([ 48 ['redirect_uri', `http://${rootDomain}/oauth/callback`], 49 ['scope', env.OAUTH_SCOPES ?? DEFAULT_SCOPES], 50 ])}`, 51 ) : 52 { 53 client_name: env.OAUTH_CLIENT_NAME, 54 logo_uri: env.OAUTH_LOGO_URI, 55 client_id: `${rootUrl}/oauth-client-metadata.json`, 56 client_uri: rootUrl, 57 redirect_uris: [`${rootUrl}/oauth/callback`], 58 scope: env.OAUTH_SCOPES ?? DEFAULT_SCOPES, 59 grant_types: ['authorization_code', 'refresh_token'], 60 application_type: 'web', 61 token_endpoint_auth_method: isConfidential ? 'private_key_jwt' : 'none', 62 dpop_bound_access_tokens: true, 63 jwks_uri: isConfidential ? `${rootUrl}/.well-known/jwks.json` : undefined, 64 token_endpoint_auth_signing_alg: isConfidential ? pk?.alg : undefined, 65 tos_uri: env.OAUTH_TOS_URI, 66 policy_uri: env.OAUTH_POLICY_URI, 67 }; 68 69 return new NodeOAuthClient({ 70 stateStore: new StateStore(db), 71 sessionStore: new SessionStore(db), 72 keyset, 73 clientMetadata, 74 // Not needed since this all runs locally to one machine I believe. But if you do run multiple instances and change out the DB from sqlite may need this 75 // https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-node#requestlock 76 requestLock: undefined, 77 }); 78 })(); 79 } 80 return client; 81};