Scratch space for learning atproto app development
1import {
2 Keyset,
3 JoseKey,
4 atprotoLoopbackClientMetadata,
5 NodeOAuthClient,
6 OAuthClientMetadataInput,
7} from '@atproto/oauth-client-node'
8import assert from 'node:assert'
9
10import type { Database } from '#/db'
11import { env } from '#/env'
12import { SessionStore, StateStore } from './storage'
13
14export async function createOAuthClient(db: Database) {
15 assert(
16 !env.PUBLIC_URL || env.PRIVATE_JWKS,
17 'ATProto requires backend clients to be confidential',
18 )
19
20 // Confidential client require a keyset accessible on the internet. Non
21 // internet clients (e.g. development) cannot expose a keyset on the internet
22 // so they can't be private..
23 const keyset =
24 env.PUBLIC_URL && env.PRIVATE_JWKS
25 ? new Keyset(
26 await Promise.all(
27 env.PRIVATE_JWKS.map((jwk) => JoseKey.fromJWK(jwk)),
28 ),
29 )
30 : undefined
31
32 // If a keyset is defined (meaning the client is confidential). Let's make
33 // sure it has a private key for signing. Note: findPrivateKey will throw if
34 // the keyset does no contain a suitable private key.
35 const pk = keyset?.findPrivateKey({ use: 'sig' })
36
37 const clientMetadata: OAuthClientMetadataInput = env.PUBLIC_URL
38 ? {
39 client_name: 'Statusphere Example App',
40 client_id: `${env.PUBLIC_URL}/oauth-client-metadata.json`,
41 jwks_uri: `${env.PUBLIC_URL}/.well-known/jwks.json`,
42 redirect_uris: [`${env.PUBLIC_URL}/oauth/callback`],
43 scope: 'atproto transition:generic',
44 grant_types: ['authorization_code', 'refresh_token'],
45 response_types: ['code'],
46 application_type: 'web',
47 token_endpoint_auth_method: pk ? 'private_key_jwt' : 'none',
48 token_endpoint_auth_signing_alg: pk ? pk.alg : undefined,
49 dpop_bound_access_tokens: true,
50 }
51 : atprotoLoopbackClientMetadata(
52 `http://localhost?${new URLSearchParams([
53 ['redirect_uri', `http://127.0.0.1:${env.PORT}/oauth/callback`],
54 ['scope', `atproto transition:generic`],
55 ])}`,
56 )
57
58 return new NodeOAuthClient({
59 keyset,
60 clientMetadata,
61 stateStore: new StateStore(db),
62 sessionStore: new SessionStore(db),
63
64 // XXX Staging
65 plcDirectoryUrl: 'https://plc.staging.bsky.dev',
66 handleResolver: 'https://staging.bsky.dev',
67 })
68}