Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
1import { NodeOAuthClient, type ClientMetadata } from "@atproto/oauth-client-node"; 2import { JoseKey } from "@atproto/jwk-jose"; 3import { db } from "./db"; 4 5const stateStore = { 6 async set(key: string, data: any) { 7 console.debug('[stateStore] set', key) 8 await db` 9 INSERT INTO oauth_states (key, data) 10 VALUES (${key}, ${JSON.stringify(data)}) 11 ON CONFLICT (key) DO UPDATE SET data = EXCLUDED.data 12 `; 13 }, 14 async get(key: string) { 15 console.debug('[stateStore] get', key) 16 const result = await db`SELECT data FROM oauth_states WHERE key = ${key}`; 17 return result[0] ? JSON.parse(result[0].data) : undefined; 18 }, 19 async del(key: string) { 20 console.debug('[stateStore] del', key) 21 await db`DELETE FROM oauth_states WHERE key = ${key}`; 22 } 23}; 24 25const sessionStore = { 26 async set(sub: string, data: any) { 27 console.debug('[sessionStore] set', sub) 28 await db` 29 INSERT INTO oauth_sessions (sub, data) 30 VALUES (${sub}, ${JSON.stringify(data)}) 31 ON CONFLICT (sub) DO UPDATE SET data = EXCLUDED.data, updated_at = EXTRACT(EPOCH FROM NOW()) 32 `; 33 }, 34 async get(sub: string) { 35 console.debug('[sessionStore] get', sub) 36 const result = await db`SELECT data FROM oauth_sessions WHERE sub = ${sub}`; 37 return result[0] ? JSON.parse(result[0].data) : undefined; 38 }, 39 async del(sub: string) { 40 console.debug('[sessionStore] del', sub) 41 await db`DELETE FROM oauth_sessions WHERE sub = ${sub}`; 42 } 43}; 44 45export { sessionStore }; 46 47export const createClientMetadata = (config: { domain: `https://${string}`, clientName: string }): ClientMetadata => { 48 // Use editor.wisp.place for OAuth endpoints since that's where the API routes live 49 return { 50 client_id: `${config.domain}/client-metadata.json`, 51 client_name: config.clientName, 52 client_uri: `https://wisp.place`, 53 logo_uri: `${config.domain}/logo.png`, 54 tos_uri: `${config.domain}/tos`, 55 policy_uri: `${config.domain}/policy`, 56 redirect_uris: [`${config.domain}/api/auth/callback`], 57 grant_types: ['authorization_code', 'refresh_token'], 58 response_types: ['code'], 59 application_type: 'web', 60 token_endpoint_auth_method: 'private_key_jwt', 61 token_endpoint_auth_signing_alg: "ES256", 62 scope: "atproto transition:generic", 63 dpop_bound_access_tokens: true, 64 jwks_uri: `${config.domain}/jwks.json`, 65 subject_type: 'public', 66 authorization_signed_response_alg: 'ES256' 67 }; 68}; 69 70const persistKey = async (key: JoseKey) => { 71 const priv = key.privateJwk; 72 if (!priv) return; 73 const kid = key.kid ?? crypto.randomUUID(); 74 await db` 75 INSERT INTO oauth_keys (kid, jwk) 76 VALUES (${kid}, ${JSON.stringify(priv)}) 77 ON CONFLICT (kid) DO UPDATE SET jwk = EXCLUDED.jwk 78 `; 79}; 80 81const loadPersistedKeys = async (): Promise<JoseKey[]> => { 82 const rows = await db`SELECT kid, jwk FROM oauth_keys ORDER BY kid`; 83 const keys: JoseKey[] = []; 84 for (const row of rows) { 85 try { 86 const obj = JSON.parse(row.jwk); 87 const key = await JoseKey.fromImportable(obj as any, (obj as any).kid); 88 keys.push(key); 89 } catch (err) { 90 console.error('Could not parse stored JWK', err); 91 } 92 } 93 return keys; 94}; 95 96const ensureKeys = async (): Promise<JoseKey[]> => { 97 let keys = await loadPersistedKeys(); 98 const needed: string[] = []; 99 for (let i = 1; i <= 3; i++) { 100 const kid = `key${i}`; 101 if (!keys.some(k => k.kid === kid)) needed.push(kid); 102 } 103 for (const kid of needed) { 104 const newKey = await JoseKey.generate(['ES256'], kid); 105 await persistKey(newKey); 106 keys.push(newKey); 107 } 108 keys.sort((a, b) => (a.kid ?? '').localeCompare(b.kid ?? '')); 109 return keys; 110}; 111 112let currentKeys: JoseKey[] = []; 113 114export const getCurrentKeys = () => currentKeys; 115 116export const getOAuthClient = async (config: { domain: `https://${string}`, clientName: string }) => { 117 if (currentKeys.length === 0) { 118 currentKeys = await ensureKeys(); 119 } 120 121 return new NodeOAuthClient({ 122 clientMetadata: createClientMetadata(config), 123 keyset: currentKeys, 124 stateStore, 125 sessionStore 126 }); 127};