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() {
41 user.isInitializing = true;
42
43 const clientId = 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 configureOAuth({
57 metadata: {
58 client_id: clientId,
59 redirect_uri: dev ? 'http://127.0.0.1:5179' + REDIRECT_PATH : metadata.redirect_uris[0]
60 },
61 identityResolver: new LocalActorResolver({
62 handleResolver: handleResolver,
63 didDocumentResolver: new CompositeDidDocumentResolver({
64 methods: {
65 plc: new PlcDidDocumentResolver(),
66 web: new WebDidDocumentResolver()
67 }
68 })
69 })
70 });
71
72 const params = new SvelteURLSearchParams(location.hash.slice(1));
73
74 const did = (localStorage.getItem('current-login') as Did) ?? undefined;
75
76 if (params.size > 0) {
77 await finalizeLogin(params, did);
78 } else if (did) {
79 await resumeSession(did);
80 }
81
82 user.isInitializing = false;
83}
84
85export async function login(handle: ActorIdentifier) {
86 console.log('login in with', handle);
87 if (handle.startsWith('did:')) {
88 if (handle.length < 6) throw new Error('DID must be at least 6 characters');
89
90 await startAuthorization(handle as ActorIdentifier);
91 } else if (handle.includes('.') && handle.length > 3) {
92 const processed = handle.startsWith('@') ? handle.slice(1) : handle;
93 if (processed.length < 4) throw new Error('Handle must be at least 4 characters');
94
95 await startAuthorization(processed as ActorIdentifier);
96 } else if (handle.length > 3) {
97 const processed = (handle.startsWith('@') ? handle.slice(1) : handle) + '.bsky.social';
98 await startAuthorization(processed as ActorIdentifier);
99 } else {
100 throw new Error('Please provide a valid handle or DID.');
101 }
102}
103
104export async function signup() {
105 await startAuthorization();
106}
107
108async function startAuthorization(identity?: ActorIdentifier) {
109 const authUrl = await createAuthorizationUrl({
110 target: identity
111 ? { type: 'account', identifier: identity }
112 : { type: 'pds', serviceUrl: signUpPDS },
113 // @ts-expect-error - new stuff
114 prompt: identity ? undefined : 'create',
115 scope: metadata.scope
116 });
117
118 // let browser persist local storage
119 await new Promise((resolve) => setTimeout(resolve, 200));
120
121 window.location.assign(authUrl);
122
123 await new Promise((_resolve, reject) => {
124 const listener = () => {
125 reject(new Error(`user aborted the login request`));
126 };
127
128 window.addEventListener('pageshow', listener, { once: true });
129 });
130}
131
132export async function logout() {
133 const currentAgent = user.agent;
134 if (currentAgent) {
135 const did = currentAgent.session.info.sub;
136
137 localStorage.removeItem('current-login');
138 localStorage.removeItem(`profile-${did}`);
139
140 try {
141 await currentAgent.signOut();
142 } catch {
143 deleteStoredSession(did);
144 }
145
146 user.agent = null;
147 user.profile = null;
148 user.isLoggedIn = false;
149 } else {
150 console.error('trying to logout, but user not signed in');
151 return false;
152 }
153}
154
155async function finalizeLogin(params: SvelteURLSearchParams, did?: Did) {
156 try {
157 const { session } = await finalizeAuthorization(params);
158 replaceState(location.pathname + location.search, {});
159
160 user.agent = new OAuthUserAgent(session);
161 user.did = session.info.sub;
162 user.client = new Client({ handler: user.agent });
163
164 localStorage.setItem('current-login', session.info.sub);
165
166 await loadProfile(session.info.sub);
167
168 user.isLoggedIn = true;
169
170 try {
171 if (!user.profile) return;
172 const recentLogins = JSON.parse(localStorage.getItem('recent-logins') || '{}');
173
174 recentLogins[session.info.sub] = user.profile;
175
176 localStorage.setItem('recent-logins', JSON.stringify(recentLogins));
177 } catch {
178 console.log('failed to save to recent logins');
179 }
180 } catch (error) {
181 console.error('error finalizing login', error);
182 if (did) {
183 await resumeSession(did);
184 }
185 }
186}
187
188async function resumeSession(did: Did) {
189 try {
190 const session = await getSession(did, { allowStale: true });
191
192 if (session.token.expires_at && session.token.expires_at < Date.now()) {
193 throw Error('session expired');
194 }
195
196 if (session.token.scope !== metadata.scope) {
197 throw Error('scope changed, signing out!');
198 }
199
200 user.agent = new OAuthUserAgent(session);
201 user.did = session.info.sub;
202 user.client = new Client({ handler: user.agent });
203
204 await loadProfile(session.info.sub);
205
206 user.isLoggedIn = true;
207 } catch (error) {
208 console.error('error resuming session', error);
209 deleteStoredSession(did);
210 }
211}
212
213async function loadProfile(actor: Did) {
214 // check if profile is already loaded in local storage
215 const profile = localStorage.getItem(`profile-${actor}`);
216 if (profile) {
217 try {
218 user.profile = JSON.parse(profile);
219 return;
220 } catch {
221 console.error('error loading profile from local storage');
222 }
223 }
224
225 const response = await getDetailedProfile();
226
227 if (!response || response.handle === 'handle.invalid') {
228 console.log('invalid handle or no profile from bsky, fetching from repo description');
229 const repo = await describeRepo({ did: actor });
230 user.profile = {
231 did: actor,
232 handle: repo?.handle || 'handle.invalid'
233 };
234 localStorage.setItem(`profile-${actor}`, JSON.stringify(user.profile));
235 } else {
236 user.profile = response;
237 localStorage.setItem(`profile-${actor}`, JSON.stringify(response));
238 }
239}