your personal website on atproto - mirror
blento.app
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}