ATProto Social Bookmark
1import { NodeOAuthClient, NodeSavedState, NodeSavedSession } from '@atproto/oauth-client-node'
2import { JoseKey } from '@atproto/jwk-jose'
3import { prisma } from "../db.js";
4
5export const SCOPE = [
6 "atproto",
7 "include:blue.rito.permissionSet",
8 "repo:app.bsky.feed.post",
9 "rpc:app.bsky.actor.getProfile?aud=did:web:api.bsky.app%23bsky_appview",
10 "blob:*/*",
11];
12
13const stateStore = {
14 async set(key: string, internalState: NodeSavedState): Promise<void> {
15 await prisma.nodeOAuthState.upsert({
16 where: { key },
17 update: { state: JSON.stringify(internalState) },
18 create: { key, state: JSON.stringify(internalState) },
19 })
20 },
21
22 async get(key: string): Promise<NodeSavedState | undefined> {
23 const record = await prisma.nodeOAuthState.findUnique({ where: { key } })
24 if (!record) return undefined
25
26 try {
27 return JSON.parse(record.state) as NodeSavedState
28 } catch (err) {
29 console.error('Invalid NodeOAuthState JSON:', err)
30 return undefined
31 }
32 },
33
34 async del(key: string): Promise<void> {
35 await prisma.nodeOAuthState.delete({ where: { key } }).catch(() => { })
36 },
37}
38
39const sessionStore = {
40 async set(sub: string, session: NodeSavedSession): Promise<void> {
41 await prisma.nodeOAuthSession.upsert({
42 where: { key: sub },
43 update: { session: JSON.stringify(session) },
44 create: { key: sub, session: JSON.stringify(session) },
45 })
46 },
47
48 async get(sub: string): Promise<NodeSavedSession | undefined> {
49 const record = await prisma.nodeOAuthSession.findUnique({ where: { key: sub } })
50 if (!record) return undefined
51
52 try {
53 return JSON.parse(record.session) as NodeSavedSession
54 } catch (err) {
55 console.error('Invalid NodeOAuthSession JSON:', err)
56 return undefined
57 }
58 },
59
60 async del(sub: string): Promise<void> {
61 await prisma.nodeOAuthSession.delete({ where: { key: sub } }).catch(() => { })
62 },
63}
64
65// JoseKey を生成
66const key1 = await JoseKey.fromImportable(process.env.OAUTH_PRIVATE_JWK || '', 'key1')
67
68export const client = new NodeOAuthClient({
69 // This object will be used to build the payload of the /client-metadata.json
70 // endpoint metadata, exposing the client metadata to the OAuth server.
71 clientMetadata: {
72 // Must be a URL that will be exposing this metadata
73 client_id: `${process.env.NEXT_PUBLIC_URL}/api/client-metadata.json`,
74 client_name: 'Rito',
75 client_uri: `${process.env.NEXT_PUBLIC_URL}`,
76 logo_uri: `${process.env.NEXT_PUBLIC_URL}/favicon.ico`,
77 tos_uri: `${process.env.NEXT_PUBLIC_URL}/tos`,
78 policy_uri: `${process.env.NEXT_PUBLIC_URL}/privacy`,
79 redirect_uris: [`${process.env.NEXT_PUBLIC_URL}/api/oauth/callback`],
80 grant_types: ['authorization_code', 'refresh_token'],
81 scope: SCOPE.join(" "),
82 response_types: ['code'],
83 application_type: 'web',
84 token_endpoint_auth_method: 'private_key_jwt',
85 token_endpoint_auth_signing_alg: 'RS256',
86 dpop_bound_access_tokens: true,
87 jwks_uri: `${process.env.NEXT_PUBLIC_URL}/api/jwks.json`,
88 },
89
90 // Used to authenticate the client to the token endpoint. Will be used to
91 // build the jwks object to be exposed on the "jwks_uri" endpoint.
92 keyset: [key1],
93
94 // Interface to store authorization state data (during authorization flows)
95 stateStore,
96
97 // Interface to store authenticated session data
98 sessionStore,
99
100 // A lock to prevent concurrent access to the session store. Optional if only one instance is running.
101
102 requestLock: async (_key, fn) => await fn(),
103})