experimental bluesky client
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Get auth flow working

+240 -67
+60
src/lib/oauth-client.ts
··· 1 + import { JoseKey, NodeOAuthClient, type NodeSavedState, type NodeSavedSession } from '@atproto/oauth-client-node' 2 + 3 + interface SessionStore { 4 + [index: string]: NodeSavedSession 5 + } 6 + 7 + interface StateStore { 8 + [index: string]: NodeSavedState 9 + } 10 + 11 + const sessionStore: SessionStore = {} 12 + const stateStore: StateStore = {} 13 + 14 + const rootUrl = process.env.VITE_APP_URL 15 + 16 + export const client = new NodeOAuthClient({ 17 + clientMetadata: { 18 + client_id: `${rootUrl}/client-metadata`, 19 + client_name: "Dudesky", 20 + client_uri: rootUrl, 21 + redirect_uris: [`${rootUrl}/callback`], 22 + grant_types: ["authorization_code", "refresh_token"], 23 + scope: "atproto transition:generic", 24 + application_type: "web", 25 + token_endpoint_auth_method: "private_key_jwt", 26 + token_endpoint_auth_signing_alg: "ES256", 27 + dpop_bound_access_tokens: true, 28 + jwks_uri: `${rootUrl}/jwks`, 29 + }, 30 + 31 + keyset: await Promise.all([ 32 + JoseKey.fromImportable(process.env.PRIVATE_KEY_0!, 'key1'), 33 + JoseKey.fromImportable(process.env.PRIVATE_KEY_1!, 'key2'), 34 + JoseKey.fromImportable(process.env.PRIVATE_KEY_2!, 'key3'), 35 + ]), 36 + 37 + stateStore: { 38 + async set(key: string, internalState: NodeSavedState): Promise<void> { 39 + stateStore[key] = internalState 40 + }, 41 + async get(key: string): Promise<NodeSavedState | undefined> { 42 + return stateStore[key] 43 + }, 44 + async del(key: string): Promise<void> { 45 + delete stateStore[key] 46 + }, 47 + }, 48 + 49 + sessionStore: { 50 + async set(sub: string, session: NodeSavedSession): Promise<void> { 51 + sessionStore[sub] = session 52 + }, 53 + async get(sub: string): Promise<NodeSavedSession | undefined> { 54 + return sessionStore[sub] 55 + }, 56 + async del(sub: string): Promise<void> { 57 + delete sessionStore[sub] 58 + }, 59 + }, 60 + })
+81 -4
src/routeTree.gen.ts
··· 9 9 // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. 10 10 11 11 import { Route as rootRouteImport } from './routes/__root' 12 + import { Route as LoginRouteImport } from './routes/login' 13 + import { Route as JwksRouteImport } from './routes/jwks' 12 14 import { Route as FeedRouteImport } from './routes/feed' 13 15 import { Route as ClientMetadataRouteImport } from './routes/client-metadata' 16 + import { Route as CallbackRouteImport } from './routes/callback' 14 17 import { Route as AboutRouteImport } from './routes/about' 15 18 import { Route as IndexRouteImport } from './routes/index' 16 19 20 + const LoginRoute = LoginRouteImport.update({ 21 + id: '/login', 22 + path: '/login', 23 + getParentRoute: () => rootRouteImport, 24 + } as any) 25 + const JwksRoute = JwksRouteImport.update({ 26 + id: '/jwks', 27 + path: '/jwks', 28 + getParentRoute: () => rootRouteImport, 29 + } as any) 17 30 const FeedRoute = FeedRouteImport.update({ 18 31 id: '/feed', 19 32 path: '/feed', ··· 24 37 path: '/client-metadata', 25 38 getParentRoute: () => rootRouteImport, 26 39 } as any) 40 + const CallbackRoute = CallbackRouteImport.update({ 41 + id: '/callback', 42 + path: '/callback', 43 + getParentRoute: () => rootRouteImport, 44 + } as any) 27 45 const AboutRoute = AboutRouteImport.update({ 28 46 id: '/about', 29 47 path: '/about', ··· 38 56 export interface FileRoutesByFullPath { 39 57 '/': typeof IndexRoute 40 58 '/about': typeof AboutRoute 59 + '/callback': typeof CallbackRoute 41 60 '/client-metadata': typeof ClientMetadataRoute 42 61 '/feed': typeof FeedRoute 62 + '/jwks': typeof JwksRoute 63 + '/login': typeof LoginRoute 43 64 } 44 65 export interface FileRoutesByTo { 45 66 '/': typeof IndexRoute 46 67 '/about': typeof AboutRoute 68 + '/callback': typeof CallbackRoute 47 69 '/client-metadata': typeof ClientMetadataRoute 48 70 '/feed': typeof FeedRoute 71 + '/jwks': typeof JwksRoute 72 + '/login': typeof LoginRoute 49 73 } 50 74 export interface FileRoutesById { 51 75 __root__: typeof rootRouteImport 52 76 '/': typeof IndexRoute 53 77 '/about': typeof AboutRoute 78 + '/callback': typeof CallbackRoute 54 79 '/client-metadata': typeof ClientMetadataRoute 55 80 '/feed': typeof FeedRoute 81 + '/jwks': typeof JwksRoute 82 + '/login': typeof LoginRoute 56 83 } 57 84 export interface FileRouteTypes { 58 85 fileRoutesByFullPath: FileRoutesByFullPath 59 - fullPaths: '/' | '/about' | '/client-metadata' | '/feed' 86 + fullPaths: 87 + | '/' 88 + | '/about' 89 + | '/callback' 90 + | '/client-metadata' 91 + | '/feed' 92 + | '/jwks' 93 + | '/login' 60 94 fileRoutesByTo: FileRoutesByTo 61 - to: '/' | '/about' | '/client-metadata' | '/feed' 62 - id: '__root__' | '/' | '/about' | '/client-metadata' | '/feed' 95 + to: 96 + | '/' 97 + | '/about' 98 + | '/callback' 99 + | '/client-metadata' 100 + | '/feed' 101 + | '/jwks' 102 + | '/login' 103 + id: 104 + | '__root__' 105 + | '/' 106 + | '/about' 107 + | '/callback' 108 + | '/client-metadata' 109 + | '/feed' 110 + | '/jwks' 111 + | '/login' 63 112 fileRoutesById: FileRoutesById 64 113 } 65 114 export interface RootRouteChildren { 66 115 IndexRoute: typeof IndexRoute 67 116 AboutRoute: typeof AboutRoute 117 + CallbackRoute: typeof CallbackRoute 68 118 ClientMetadataRoute: typeof ClientMetadataRoute 69 119 FeedRoute: typeof FeedRoute 120 + JwksRoute: typeof JwksRoute 121 + LoginRoute: typeof LoginRoute 70 122 } 71 123 72 124 declare module '@tanstack/react-router' { 73 125 interface FileRoutesByPath { 126 + '/login': { 127 + id: '/login' 128 + path: '/login' 129 + fullPath: '/login' 130 + preLoaderRoute: typeof LoginRouteImport 131 + parentRoute: typeof rootRouteImport 132 + } 133 + '/jwks': { 134 + id: '/jwks' 135 + path: '/jwks' 136 + fullPath: '/jwks' 137 + preLoaderRoute: typeof JwksRouteImport 138 + parentRoute: typeof rootRouteImport 139 + } 74 140 '/feed': { 75 141 id: '/feed' 76 142 path: '/feed' ··· 85 151 preLoaderRoute: typeof ClientMetadataRouteImport 86 152 parentRoute: typeof rootRouteImport 87 153 } 154 + '/callback': { 155 + id: '/callback' 156 + path: '/callback' 157 + fullPath: '/callback' 158 + preLoaderRoute: typeof CallbackRouteImport 159 + parentRoute: typeof rootRouteImport 160 + } 88 161 '/about': { 89 162 id: '/about' 90 163 path: '/about' ··· 105 178 const rootRouteChildren: RootRouteChildren = { 106 179 IndexRoute: IndexRoute, 107 180 AboutRoute: AboutRoute, 181 + CallbackRoute: CallbackRoute, 108 182 ClientMetadataRoute: ClientMetadataRoute, 109 183 FeedRoute: FeedRoute, 184 + JwksRoute: JwksRoute, 185 + LoginRoute: LoginRoute, 110 186 } 111 187 export const routeTree = rootRouteImport 112 188 ._addFileChildren(rootRouteChildren) 113 189 ._addFileTypes<FileRouteTypes>() 114 190 115 191 import type { getRouter } from './router.tsx' 116 - import type { createStart } from '@tanstack/react-start' 192 + import type { startInstance } from './start.ts' 117 193 declare module '@tanstack/react-start' { 118 194 interface Register { 119 195 ssr: true 120 196 router: Awaited<ReturnType<typeof getRouter>> 197 + config: Awaited<ReturnType<typeof startInstance.getOptions>> 121 198 } 122 199 }
+21
src/routes/callback.tsx
··· 1 + import { createFileRoute } from '@tanstack/react-router' 2 + import { client } from '#/lib/oauth-client' 3 + 4 + export const Route = createFileRoute('/callback')({ 5 + server: { 6 + handlers: { 7 + GET: async ({ request }) => { 8 + try { 9 + const params = new URL(request.url).searchParams 10 + const { session } = await client.callback(params) 11 + // session.sub is the user's DID 12 + console.log('[/callback] authenticated:', session.sub) 13 + return Response.redirect(new URL('/feed', request.url).toString(), 302) 14 + } catch (e) { 15 + console.error('[/callback]', e) 16 + return new Response(String(e), { status: 500 }) 17 + } 18 + } 19 + } 20 + } 21 + })
+5 -63
src/routes/client-metadata.tsx
··· 1 1 import { createFileRoute } from '@tanstack/react-router' 2 - import { JoseKey, NodeOAuthClient, type NodeSavedState, type NodeSavedSession } from '@atproto/oauth-client-node' 3 - 4 - interface SessionStore { 5 - [index: string]: NodeSavedSession; 6 - } 7 - 8 - interface StateStore { 9 - [index: string]: NodeSavedState; 10 - } 11 - 12 - let sessionStore: SessionStore = {}; 13 - let stateStore: StateStore = {}; 14 - 15 - let rootUrl = process.env.VITE_APP_URL; 16 - console.log(rootUrl); 17 - 18 - const client = new NodeOAuthClient({ 19 - clientMetadata: { 20 - client_id: `${rootUrl}/client-metadata`, 21 - client_name: "Dudesky", 22 - client_uri: rootUrl, 23 - redirect_uris: [`${rootUrl}/callback`], 24 - grant_types: ["authorization_code", "refresh_token"], 25 - scope: "atproto transition:generic", 26 - application_type: "web", 27 - token_endpoint_auth_method: "private_key_jwt", 28 - token_endpoint_auth_signing_alg: "ES256", 29 - dpop_bound_access_tokens: true, 30 - jwks_uri: `${rootUrl}/jwks`, 31 - }, 32 - 33 - keyset: await Promise.all([ 34 - JoseKey.fromImportable(process.env.PRIVATE_KEY_0!, 'key1'), 35 - JoseKey.fromImportable(process.env.PRIVATE_KEY_1!, 'key2'), 36 - JoseKey.fromImportable(process.env.PRIVATE_KEY_2!, 'key3'), 37 - ]), 38 - 39 - stateStore: { 40 - async set(key: string, internalState: NodeSavedState): Promise<void> { 41 - stateStore[key] = internalState; 42 - }, 43 - async get(key: string): Promise<NodeSavedState | undefined> { 44 - return stateStore[key]; 45 - }, 46 - async del(key: string): Promise<void> { 47 - delete stateStore[key]; 48 - }, 49 - }, 50 - 51 - sessionStore: { 52 - async set(sub: string, session: NodeSavedSession): Promise<void> { 53 - sessionStore[sub] = session; 54 - }, 55 - async get(sub: string): Promise<NodeSavedSession | undefined> { 56 - return sessionStore[sub]; 57 - }, 58 - async del(sub: string): Promise<void> { 59 - delete sessionStore[sub]; 60 - }, 61 - } 62 - }) 2 + import { client } from '#/lib/oauth-client' 63 3 64 4 export const Route = createFileRoute('/client-metadata')({ 65 5 server: { 66 6 handlers: { 67 - GET: async({ request }) => { 68 - return new Response(JSON.stringify(client.clientMetadata)); 7 + GET: async() => { 8 + return new Response(JSON.stringify(client.clientMetadata), { 9 + headers: { 'Content-Type': 'application/json' }, 10 + }) 69 11 } 70 12 } 71 13 }
+14
src/routes/jwks.tsx
··· 1 + import { createFileRoute } from '@tanstack/react-router' 2 + import { client } from '#/lib/oauth-client' 3 + 4 + export const Route = createFileRoute('/jwks')({ 5 + server: { 6 + handlers: { 7 + GET: async () => { 8 + return new Response(JSON.stringify(client.jwks), { 9 + headers: { 'Content-Type': 'application/json' }, 10 + }) 11 + } 12 + } 13 + } 14 + })
+32
src/routes/login.tsx
··· 1 + import { createFileRoute } from '@tanstack/react-router' 2 + import { client } from '#/lib/oauth-client' 3 + 4 + function LoginPage() { 5 + return ( 6 + <form method="POST" action="/login"> 7 + <input name="handle" placeholder="you.bsky.social" /> 8 + <button type="submit">Login with Bluesky</button> 9 + </form> 10 + ) 11 + } 12 + 13 + export const Route = createFileRoute('/login')({ 14 + component: LoginPage, 15 + server: { 16 + handlers: { 17 + POST: async ({ request }) => { 18 + try { 19 + const body = await request.formData() 20 + const handle = body.get('handle') as string 21 + const url = await client.authorize(handle, { 22 + scope: 'atproto transition:generic', 23 + }) 24 + return Response.redirect(url.toString(), 302) 25 + } catch (e) { 26 + console.error('[/login POST]', e) 27 + return new Response(String(e), { status: 500 }) 28 + } 29 + } 30 + } 31 + } 32 + })
+27
src/start.ts
··· 1 + import { createStart, createMiddleware } from '@tanstack/react-start' 2 + 3 + const isProd = process.env.NODE_ENV === 'production' 4 + 5 + const requestLogger = createMiddleware({ type: 'request' }).server( 6 + async ({ next, request }) => { 7 + const { method, url } = request 8 + const pathname = new URL(url).pathname 9 + const start = Date.now() 10 + const result = await next() 11 + const status = result.response.status 12 + const ms = Date.now() - start 13 + 14 + if (isProd) { 15 + console.log(JSON.stringify({ method, pathname, status, ms })) 16 + } else { 17 + console.log(`req: ${method} ${pathname}`) 18 + console.log(`res: ${method} ${pathname} ${status} (${ms}ms)`) 19 + } 20 + 21 + return result 22 + } 23 + ) 24 + 25 + export const startInstance = createStart(() => ({ 26 + requestMiddleware: [requestLogger], 27 + }))