your personal website on atproto - mirror blento.app
at fix-cached-posts 254 lines 7.1 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 localStorage.setItem('login-redirect', location.pathname + location.search); 130 131 // let browser persist local storage 132 await new Promise((resolve) => setTimeout(resolve, 200)); 133 134 window.location.assign(authUrl); 135 136 await new Promise((_resolve, reject) => { 137 const listener = () => { 138 reject(new Error(`user aborted the login request`)); 139 }; 140 141 window.addEventListener('pageshow', listener, { once: true }); 142 }); 143} 144 145export async function logout() { 146 const currentAgent = user.agent; 147 if (currentAgent) { 148 const did = currentAgent.session.info.sub; 149 150 localStorage.removeItem('current-login'); 151 localStorage.removeItem(`profile-${did}`); 152 153 try { 154 await currentAgent.signOut(); 155 } catch { 156 deleteStoredSession(did); 157 } 158 159 user.agent = null; 160 user.profile = null; 161 user.isLoggedIn = false; 162 } else { 163 console.error('trying to logout, but user not signed in'); 164 return false; 165 } 166} 167 168async function finalizeLogin(params: SvelteURLSearchParams, did?: Did) { 169 try { 170 const { session } = await finalizeAuthorization(params); 171 replaceState(location.pathname + location.search, {}); 172 173 user.agent = new OAuthUserAgent(session); 174 user.did = session.info.sub; 175 user.client = new Client({ handler: user.agent }); 176 177 localStorage.setItem('current-login', session.info.sub); 178 179 await loadProfile(session.info.sub); 180 181 user.isLoggedIn = true; 182 183 try { 184 if (!user.profile) return; 185 const recentLogins = JSON.parse(localStorage.getItem('recent-logins') || '{}'); 186 187 recentLogins[session.info.sub] = user.profile; 188 189 localStorage.setItem('recent-logins', JSON.stringify(recentLogins)); 190 } catch { 191 console.log('failed to save to recent logins'); 192 } 193 } catch (error) { 194 console.error('error finalizing login', error); 195 if (did) { 196 await resumeSession(did); 197 } 198 } 199} 200 201async function resumeSession(did: Did) { 202 try { 203 const session = await getSession(did); 204 205 if (session.token.expires_at && session.token.expires_at < Date.now()) { 206 throw Error('session expired'); 207 } 208 209 const requestedScopes = metadata.scope.split(' ').filter((s) => !s.startsWith('include:')); 210 const tokenScopes = new Set(session.token.scope?.split(' ')); 211 if (!requestedScopes.every((s) => tokenScopes.has(s))) { 212 throw Error('scope changed, signing out!'); 213 } 214 215 user.agent = new OAuthUserAgent(session); 216 user.did = session.info.sub; 217 user.client = new Client({ handler: user.agent }); 218 219 await loadProfile(session.info.sub); 220 221 user.isLoggedIn = true; 222 } catch (error) { 223 console.error('error resuming session', error); 224 deleteStoredSession(did); 225 } 226} 227 228async function loadProfile(actor: Did) { 229 // check if profile is already loaded in local storage 230 const profile = localStorage.getItem(`profile-${actor}`); 231 if (profile) { 232 try { 233 user.profile = JSON.parse(profile); 234 return; 235 } catch { 236 console.error('error loading profile from local storage'); 237 } 238 } 239 240 const response = await getDetailedProfile(); 241 242 if (!response || response.handle === 'handle.invalid') { 243 console.log('invalid handle or no profile from bsky, fetching from repo description'); 244 const repo = await describeRepo({ did: actor }); 245 user.profile = { 246 did: actor, 247 handle: repo?.handle || 'handle.invalid' 248 }; 249 localStorage.setItem(`profile-${actor}`, JSON.stringify(user.profile)); 250 } else { 251 user.profile = response; 252 localStorage.setItem(`profile-${actor}`, JSON.stringify(response)); 253 } 254}