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