Meal planning and recipes sorted
1import {
2 JoseKey,
3 Keyset,
4 NodeOAuthClient,
5 buildAtprotoLoopbackClientMetadata,
6} from "@atproto/oauth-client-node";
7import type {
8 NodeSavedSession,
9 NodeSavedState,
10 OAuthClientMetadataInput,
11} from "@atproto/oauth-client-node";
12import { getDb } from "../db";
13
14export const SCOPE = "atproto account:email repo:com.example.record";
15
16let client: NodeOAuthClient | null = null;
17
18const PUBLIC_URL = process.env.PUBLIC_URL;
19const PRIVATE_KEY = process.env.PRIVATE_KEY;
20
21function getClientMetadata(): OAuthClientMetadataInput {
22 if (PUBLIC_URL) {
23 return {
24 client_id: `${PUBLIC_URL}/oauth-client-metadata.json`,
25 client_name: "OAuth Tutorial",
26 client_uri: PUBLIC_URL,
27 redirect_uris: [`${PUBLIC_URL}/oauth/callback`],
28 grant_types: ["authorization_code", "refresh_token"],
29 response_types: ["code"],
30 scope: SCOPE,
31 token_endpoint_auth_method: "private_key_jwt" as const,
32 token_endpoint_auth_signing_alg: "ES256" as const, // must match the alg in scripts/gen-key.ts
33 jwks_uri: `${PUBLIC_URL}/.well-known/jwks.json`,
34 dpop_bound_access_tokens: true,
35 };
36 } else {
37 return buildAtprotoLoopbackClientMetadata({
38 scope: SCOPE,
39 redirect_uris: ["http://127.0.0.1:3000/oauth/callback"],
40 });
41 }
42}
43
44async function getKeyset(): Promise<Keyset | undefined> {
45 if (PUBLIC_URL && PRIVATE_KEY) {
46 return new Keyset([await JoseKey.fromJWK(JSON.parse(PRIVATE_KEY))]);
47 } else {
48 return undefined;
49 }
50}
51
52export async function getOAuthClient(): Promise<NodeOAuthClient> {
53 if (client) return client;
54
55 client = new NodeOAuthClient({
56 clientMetadata: getClientMetadata(),
57 keyset: await getKeyset(),
58
59 stateStore: {
60 async get(key: string) {
61 const db = getDb();
62 const row = await db
63 .selectFrom("auth_state")
64 .select("value")
65 .where("key", "=", key)
66 .executeTakeFirst();
67 return row ? JSON.parse(row.value) : undefined;
68 },
69 async set(key: string, value: NodeSavedState) {
70 const db = getDb();
71 const valueJson = JSON.stringify(value);
72 await db
73 .insertInto("auth_state")
74 .values({ key, value: valueJson })
75 .onConflict((oc) =>
76 oc.column("key").doUpdateSet({ value: valueJson }),
77 )
78 .execute();
79 },
80 async del(key: string) {
81 const db = getDb();
82 await db.deleteFrom("auth_state").where("key", "=", key).execute();
83 },
84 },
85
86 sessionStore: {
87 async get(key: string) {
88 const db = getDb();
89 const row = await db
90 .selectFrom("auth_session")
91 .select("value")
92 .where("key", "=", key)
93 .executeTakeFirst();
94 return row ? JSON.parse(row.value) : undefined;
95 },
96 async set(key: string, value: NodeSavedSession) {
97 const db = getDb();
98 const valueJson = JSON.stringify(value);
99 await db
100 .insertInto("auth_session")
101 .values({ key, value: valueJson })
102 .onConflict((oc) =>
103 oc.column("key").doUpdateSet({ value: valueJson }),
104 )
105 .execute();
106 },
107 async del(key: string) {
108 const db = getDb();
109 await db.deleteFrom("auth_session").where("key", "=", key).execute();
110 },
111 },
112 });
113
114 return client;
115}