bluesky client without react native baggage written in sveltekit
1import { Client, ok, simpleFetchHandler } from '@atcute/client';
2import { AppBskyActorDefs } from '@atcute/bluesky';
3import {
4 configureOAuth,
5 createAuthorizationUrl,
6 finalizeAuthorization,
7 getSession,
8 listStoredSessions,
9 OAuthUserAgent,
10 type Session
11} from '@atcute/oauth-browser-client';
12import {
13 CompositeDidDocumentResolver,
14 LocalActorResolver,
15 PlcDidDocumentResolver,
16 WebDidDocumentResolver,
17 XrpcHandleResolver
18} from '@atcute/identity-resolver';
19
20configureOAuth({
21 metadata: {
22 client_id: import.meta.env.VITE_OAUTH_CLIENT_ID,
23 redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URI
24 },
25 identityResolver: new LocalActorResolver({
26 handleResolver: new XrpcHandleResolver({
27 serviceUrl: 'https://public.api.bsky.app'
28 }),
29 didDocumentResolver: new CompositeDidDocumentResolver({
30 methods: {
31 plc: new PlcDidDocumentResolver(),
32 web: new WebDidDocumentResolver()
33 }
34 })
35 })
36});
37
38/** Public (unauthenticated) RPC client — always available */
39export const publicClient = new Client({
40 handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' })
41});
42
43/** Authenticated RPC client — set after login, null otherwise */
44export let rpc: Client | null = null;
45
46/** Current OAuth session */
47let currentSession: Session | null = null;
48
49export async function getClient(): Promise<Client> {
50 if (rpc === null) {
51 await resumeSession();
52 }
53 return rpc ?? publicClient;
54}
55
56/** Try to resume an existing session from localStorage */
57export async function resumeSession(): Promise<boolean> {
58 const dids = listStoredSessions();
59 if (dids.length === 0) return false;
60
61 try {
62 const session = await getSession(dids[0], { allowStale: true });
63 setSession(session);
64 return true;
65 } catch {
66 return false;
67 }
68}
69
70/** Handle the OAuth callback — call this from the callback route */
71export async function handleCallback(params: URLSearchParams): Promise<void> {
72 const { session } = await finalizeAuthorization(params);
73 setSession(session);
74}
75
76/** Start the login flow */
77export async function login(handle: string): Promise<void> {
78 const authUrl = await createAuthorizationUrl({
79 target: { type: 'account', identifier: handle },
80 scope: import.meta.env.VITE_OAUTH_SCOPE
81 });
82 await new Promise((r) => setTimeout(r, 200));
83 window.location.assign(authUrl);
84}
85
86/** Check if a session is active */
87export function isLoggedIn(): boolean {
88 return rpc !== null;
89}
90
91/** Fetch the logged-in user's profile */
92export async function getProfile(): Promise<AppBskyActorDefs.ProfileViewDetailed | null> {
93 if (!rpc || !currentSession) return null;
94 const data = await ok(rpc.get('app.bsky.actor.getProfile', {
95 params: { actor: currentSession.info.sub }
96 }));
97 return data;
98}
99
100function setSession(session: Session): void {
101 currentSession = session;
102 const agent = new OAuthUserAgent(session);
103 rpc = new Client({ handler: agent });
104}