your personal website on atproto - mirror blento.app
at update-docs 250 lines 6.9 kB view raw
1import { 2 configureOAuth, 3 createAuthorizationUrl, 4 finalizeAuthorization, 5 OAuthUserAgent, 6 getSession, 7 deleteStoredSession 8} from '@atcute/oauth-browser-client'; 9import { AppBskyActorDefs } from '@atcute/bluesky'; 10import { 11 CompositeDidDocumentResolver, 12 CompositeHandleResolver, 13 DohJsonHandleResolver, 14 LocalActorResolver, 15 PlcDidDocumentResolver, 16 WebDidDocumentResolver, 17 WellKnownHandleResolver 18} from '@atcute/identity-resolver'; 19import { Client } from '@atcute/client'; 20 21import { dev } from '$app/environment'; 22import { replaceState } from '$app/navigation'; 23 24import { metadata } from './metadata'; 25import { describeRepo, getDetailedProfile } from './methods'; 26import { DOH_RESOLVER, REDIRECT_PATH, signUpPDS } from './settings'; 27import { SvelteURLSearchParams } from 'svelte/reactivity'; 28 29import type { ActorIdentifier, Did } from '@atcute/lexicons'; 30 31export const user = $state({ 32 agent: null as OAuthUserAgent | null, 33 client: null as Client | null, 34 profile: null as AppBskyActorDefs.ProfileViewDetailed | null | undefined, 35 isInitializing: true, 36 isLoggedIn: false, 37 did: undefined as Did | undefined 38}); 39 40export async function initClient(options?: { customDomain?: string }) { 41 user.isInitializing = true; 42 43 let client_id = dev 44 ? `http://localhost` + 45 `?redirect_uri=${encodeURIComponent('http://127.0.0.1:5179' + REDIRECT_PATH)}` + 46 `&scope=${encodeURIComponent(metadata.scope)}` 47 : metadata.client_id; 48 49 const handleResolver = new CompositeHandleResolver({ 50 methods: { 51 dns: new DohJsonHandleResolver({ dohUrl: DOH_RESOLVER }), 52 http: new WellKnownHandleResolver() 53 } 54 }); 55 56 let redirect_uri = dev ? 'http://127.0.0.1:5179' + REDIRECT_PATH : metadata.redirect_uris[0]; 57 58 if (options?.customDomain) { 59 client_id = client_id.replace('blento.app', options.customDomain); 60 redirect_uri = redirect_uri.replace('blento.app', options.customDomain); 61 62 console.log(client_id, redirect_uri); 63 } else { 64 console.log('no custom domain'); 65 } 66 67 configureOAuth({ 68 metadata: { 69 client_id, 70 redirect_uri 71 }, 72 identityResolver: new LocalActorResolver({ 73 handleResolver: handleResolver, 74 didDocumentResolver: new CompositeDidDocumentResolver({ 75 methods: { 76 plc: new PlcDidDocumentResolver(), 77 web: new WebDidDocumentResolver() 78 } 79 }) 80 }) 81 }); 82 83 const params = new SvelteURLSearchParams(location.hash.slice(1)); 84 85 const did = (localStorage.getItem('current-login') as Did) ?? undefined; 86 87 if (params.size > 0) { 88 await finalizeLogin(params, did); 89 } else if (did) { 90 await resumeSession(did); 91 } 92 93 user.isInitializing = false; 94} 95 96export async function login(handle: ActorIdentifier) { 97 console.log('login in with', handle); 98 if (handle.startsWith('did:')) { 99 if (handle.length < 6) throw new Error('DID must be at least 6 characters'); 100 101 await startAuthorization(handle as ActorIdentifier); 102 } else if (handle.includes('.') && handle.length > 3) { 103 const processed = handle.startsWith('@') ? handle.slice(1) : handle; 104 if (processed.length < 4) throw new Error('Handle must be at least 4 characters'); 105 106 await startAuthorization(processed as ActorIdentifier); 107 } else if (handle.length > 3) { 108 const processed = (handle.startsWith('@') ? handle.slice(1) : handle) + '.bsky.social'; 109 await startAuthorization(processed as ActorIdentifier); 110 } else { 111 throw new Error('Please provide a valid handle or DID.'); 112 } 113} 114 115export async function signup() { 116 await startAuthorization(); 117} 118 119async function startAuthorization(identity?: ActorIdentifier) { 120 const authUrl = await createAuthorizationUrl({ 121 target: identity 122 ? { type: 'account', identifier: identity } 123 : { type: 'pds', serviceUrl: signUpPDS }, 124 // @ts-expect-error - new stuff 125 prompt: identity ? undefined : 'create', 126 scope: metadata.scope 127 }); 128 129 // let browser persist local storage 130 await new Promise((resolve) => setTimeout(resolve, 200)); 131 132 window.location.assign(authUrl); 133 134 await new Promise((_resolve, reject) => { 135 const listener = () => { 136 reject(new Error(`user aborted the login request`)); 137 }; 138 139 window.addEventListener('pageshow', listener, { once: true }); 140 }); 141} 142 143export async function logout() { 144 const currentAgent = user.agent; 145 if (currentAgent) { 146 const did = currentAgent.session.info.sub; 147 148 localStorage.removeItem('current-login'); 149 localStorage.removeItem(`profile-${did}`); 150 151 try { 152 await currentAgent.signOut(); 153 } catch { 154 deleteStoredSession(did); 155 } 156 157 user.agent = null; 158 user.profile = null; 159 user.isLoggedIn = false; 160 } else { 161 console.error('trying to logout, but user not signed in'); 162 return false; 163 } 164} 165 166async function finalizeLogin(params: SvelteURLSearchParams, did?: Did) { 167 try { 168 const { session } = await finalizeAuthorization(params); 169 replaceState(location.pathname + location.search, {}); 170 171 user.agent = new OAuthUserAgent(session); 172 user.did = session.info.sub; 173 user.client = new Client({ handler: user.agent }); 174 175 localStorage.setItem('current-login', session.info.sub); 176 177 await loadProfile(session.info.sub); 178 179 user.isLoggedIn = true; 180 181 try { 182 if (!user.profile) return; 183 const recentLogins = JSON.parse(localStorage.getItem('recent-logins') || '{}'); 184 185 recentLogins[session.info.sub] = user.profile; 186 187 localStorage.setItem('recent-logins', JSON.stringify(recentLogins)); 188 } catch { 189 console.log('failed to save to recent logins'); 190 } 191 } catch (error) { 192 console.error('error finalizing login', error); 193 if (did) { 194 await resumeSession(did); 195 } 196 } 197} 198 199async function resumeSession(did: Did) { 200 try { 201 const session = await getSession(did, { allowStale: true }); 202 203 if (session.token.expires_at && session.token.expires_at < Date.now()) { 204 throw Error('session expired'); 205 } 206 207 if (session.token.scope !== metadata.scope) { 208 throw Error('scope changed, signing out!'); 209 } 210 211 user.agent = new OAuthUserAgent(session); 212 user.did = session.info.sub; 213 user.client = new Client({ handler: user.agent }); 214 215 await loadProfile(session.info.sub); 216 217 user.isLoggedIn = true; 218 } catch (error) { 219 console.error('error resuming session', error); 220 deleteStoredSession(did); 221 } 222} 223 224async function loadProfile(actor: Did) { 225 // check if profile is already loaded in local storage 226 const profile = localStorage.getItem(`profile-${actor}`); 227 if (profile) { 228 try { 229 user.profile = JSON.parse(profile); 230 return; 231 } catch { 232 console.error('error loading profile from local storage'); 233 } 234 } 235 236 const response = await getDetailedProfile(); 237 238 if (!response || response.handle === 'handle.invalid') { 239 console.log('invalid handle or no profile from bsky, fetching from repo description'); 240 const repo = await describeRepo({ did: actor }); 241 user.profile = { 242 did: actor, 243 handle: repo?.handle || 'handle.invalid' 244 }; 245 localStorage.setItem(`profile-${actor}`, JSON.stringify(user.profile)); 246 } else { 247 user.profile = response; 248 localStorage.setItem(`profile-${actor}`, JSON.stringify(response)); 249 } 250}