tangled
alpha
login
or
join now
flo-bit.dev
/
svelte-atproto-client-oauth
6
fork
atom
simple atproto oauth for static svelte apps
flo-bit.dev/svelte-atproto-client-oauth/
6
fork
atom
overview
issues
pulls
pipelines
switch to atcute
Florian
1 year ago
ea5b5ad8
6b551cdf
+207
-55
6 changed files
expand all
collapse all
unified
split
package-lock.json
package.json
src
lib
auth.svelte.ts
client-metadata.ts
routes
+page.svelte
test
+page.svelte
+14
package-lock.json
···
8
8
"name": "svelte-atproto-client-oauth",
9
9
"version": "0.0.1",
10
10
"dependencies": {
11
11
+
"@atcute/oauth-browser-client": "^1.0.13",
11
12
"@atproto/api": "^0.14.4",
12
13
"@atproto/oauth-client-browser": "^0.3.10",
13
14
"@sveltejs/adapter-static": "^3.0.8",
···
45
46
},
46
47
"engines": {
47
48
"node": ">=6.0.0"
49
49
+
}
50
50
+
},
51
51
+
"node_modules/@atcute/client": {
52
52
+
"version": "2.0.8",
53
53
+
"resolved": "https://registry.npmjs.org/@atcute/client/-/client-2.0.8.tgz",
54
54
+
"integrity": "sha512-OTfiWwjB4mOTlp2InGStvoQ+PIA5lvih9cTYU8BvOhzNcCBUpt4l860MKZExHjvQ9Tt1kjq/ED9zRiUjsAgIxw=="
55
55
+
},
56
56
+
"node_modules/@atcute/oauth-browser-client": {
57
57
+
"version": "1.0.13",
58
58
+
"resolved": "https://registry.npmjs.org/@atcute/oauth-browser-client/-/oauth-browser-client-1.0.13.tgz",
59
59
+
"integrity": "sha512-JxQKl9Vo1V8poxvR9uKS8bkBv8t53DIH4lCbaih6yn9u7fM62ZC/0x/9KoGWSNqpp3R3U0y/DOQQfdC9Y4GEGQ==",
60
60
+
"dependencies": {
61
61
+
"@atcute/client": "^2.0.7"
48
62
}
49
63
},
50
64
"node_modules/@atproto-labs/did-resolver": {
+1
package.json
···
35
35
"vite": "^6.0.0"
36
36
},
37
37
"dependencies": {
38
38
+
"@atcute/oauth-browser-client": "^1.0.13",
38
39
"@atproto/api": "^0.14.4",
39
40
"@atproto/oauth-client-browser": "^0.3.10",
40
41
"@sveltejs/adapter-static": "^3.0.8",
+70
-45
src/lib/auth.svelte.ts
···
1
1
-
import type { BrowserOAuthClient, OAuthSession } from '@atproto/oauth-client-browser';
2
2
-
import type { Agent } from '@atproto/api';
3
3
-
import { metadata } from './client-metadata';
4
4
-
5
5
-
const { MODE } = import.meta.env;
1
1
+
import type { BrowserOAuthClient } from '@atproto/oauth-client-browser';
6
2
7
3
export const HANDLE_RESOLVER_URL = 'https://bsky.social';
8
4
export const PLC_DIRECTORY_URL = undefined;
···
10
6
export const BASE_PATH = '/svelte-atproto-client-oauth';
11
7
12
8
export const data = $state({
13
13
-
agent: null as Agent | null,
14
14
-
session: null as OAuthSession | null,
9
9
+
agent: null as OAuthUserAgent | null,
10
10
+
session: null as Session | null,
15
11
client: null as BrowserOAuthClient | null,
16
12
isInitializing: true
17
13
});
18
14
19
19
-
let oauthClient: BrowserOAuthClient | null = null;
15
15
+
import {
16
16
+
configureOAuth,
17
17
+
createAuthorizationUrl,
18
18
+
finalizeAuthorization,
19
19
+
resolveFromIdentity,
20
20
+
type Session,
21
21
+
OAuthUserAgent,
22
22
+
getSession
23
23
+
} from '@atcute/oauth-browser-client';
20
24
21
25
export async function initOAuthClient() {
22
22
-
// Dynamically import the module on the client side
23
23
-
const { BrowserOAuthClient } = await import('@atproto/oauth-client-browser');
24
24
-
const { Agent } = await import('@atproto/api');
26
26
+
data.isInitializing = true;
25
27
26
28
const clientId = `${window.location.origin}/svelte-atproto-client-oauth/client-metadata.json`;
27
29
28
28
-
console.log(clientId);
29
29
-
30
30
-
oauthClient = new BrowserOAuthClient({
31
31
-
clientMetadata: metadata,
32
32
-
handleResolver: HANDLE_RESOLVER_URL,
33
33
-
allowHttp: MODE === 'development' || MODE === 'test'
30
30
+
configureOAuth({
31
31
+
metadata: {
32
32
+
client_id: clientId,
33
33
+
redirect_uri: `${window.location.origin}/svelte-atproto-client-oauth`
34
34
+
}
34
35
});
35
36
36
36
-
data.client = oauthClient;
37
37
+
const params = new URLSearchParams(location.hash.slice(1));
38
38
+
39
39
+
const did = localStorage.getItem('last-login');
40
40
+
41
41
+
if (params.size > 0) {
42
42
+
history.replaceState(null, '', location.pathname + location.search);
43
43
+
44
44
+
// you'd be given a session object that you can then pass to OAuthUserAgent!
45
45
+
const session = await finalizeAuthorization(params);
46
46
+
47
47
+
data.session = session;
48
48
+
console.log(session);
49
49
+
50
50
+
data.agent = new OAuthUserAgent(session);
51
51
+
52
52
+
// save did to local storage
53
53
+
localStorage.setItem('last-login', session.info.sub);
54
54
+
} else if (did) {
55
55
+
try {
56
56
+
const session = await getSession(did as `did:${string}`, { allowStale: true });
57
57
+
data.session = session;
58
58
+
59
59
+
data.agent = new OAuthUserAgent(session);
37
60
38
38
-
try {
39
39
-
const initResult = await oauthClient.init();
40
40
-
console.log(initResult);
41
41
-
if (initResult) {
42
42
-
data.session = initResult.session;
43
43
-
data.agent = new Agent(initResult.session);
61
61
+
console.log('resuming session', session);
62
62
+
} catch (error) {
63
63
+
console.error('error resuming session', error);
44
64
}
45
45
-
} catch (err) {
46
46
-
console.error('Failed to initialize OAuth client:', err);
47
47
-
} finally {
48
48
-
data.isInitializing = false;
49
65
}
66
66
+
67
67
+
data.isInitializing = false;
50
68
}
51
69
52
70
export async function trySignIn(value: string) {
53
71
if (value.startsWith('did:')) {
54
72
if (value.length > 5) await signIn(value);
55
73
else throw new Error('DID must be at least 6 characters');
56
56
-
} else if (value.startsWith('https://') || value.startsWith('http://')) {
57
57
-
const url = new URL(value);
58
58
-
if (value !== url.origin) throw new Error('PDS URL must be an origin');
59
59
-
await signIn(value);
60
74
} else if (value.includes('.') && value.length > 3) {
61
75
const handle = value.startsWith('@') ? value.slice(1) : value;
62
76
if (handle.length > 3) await signIn(handle);
···
70
84
}
71
85
72
86
export async function signIn(input: string) {
73
73
-
if (!oauthClient) throw new Error('OAuth client not initialized');
87
87
+
const { identity, metadata } = await resolveFromIdentity(input);
88
88
+
89
89
+
const authUrl = await createAuthorizationUrl({
90
90
+
metadata: metadata,
91
91
+
identity: identity,
92
92
+
scope: 'atproto transition:generic'
93
93
+
});
94
94
+
95
95
+
// recommended to wait for the browser to persist local storage before proceeding
96
96
+
await new Promise((resolve) => setTimeout(resolve, 200));
74
97
75
75
-
try {
76
76
-
const userSession = await oauthClient.signIn(input);
77
77
-
data.session = userSession;
98
98
+
// redirect the user to sign in and authorize the app
99
99
+
window.location.assign(authUrl);
78
100
79
79
-
const { Agent } = await import('@atproto/api');
80
80
-
data.agent = new Agent(userSession);
81
81
-
} catch (err) {
82
82
-
console.error('Sign-in error:', err);
83
83
-
throw err;
84
84
-
}
101
101
+
await new Promise((_resolve, reject) => {
102
102
+
const listener = () => {
103
103
+
reject(new Error(`user aborted the login request`));
104
104
+
};
105
105
+
106
106
+
window.addEventListener('pageshow', listener, { once: true });
107
107
+
});
85
108
}
86
109
87
110
export async function signOut() {
88
88
-
const currentSession = data.session;
89
89
-
if (currentSession) {
90
90
-
await currentSession.signOut();
111
111
+
const currentAgent = data.agent;
112
112
+
if (currentAgent) {
113
113
+
await currentAgent.signOut();
91
114
data.session = null;
92
115
data.agent = null;
116
116
+
117
117
+
localStorage.removeItem('last-login');
93
118
}
94
119
}
+1
-1
src/lib/client-metadata.ts
···
7
7
8
8
redirect_uris: [url],
9
9
10
10
-
scope: 'atproto',
10
10
+
scope: 'atproto transition:generic',
11
11
grant_types: ['authorization_code', 'refresh_token'],
12
12
response_types: ['code'],
13
13
token_endpoint_auth_method: 'none',
+41
-9
src/routes/+page.svelte
···
11
11
let likes = $state([]);
12
12
13
13
async function getLikes() {
14
14
-
if (data.agent?.did) {
15
15
-
const likesData = await data.agent.getActorLikes({ actor: data.agent.did, limit: 10 });
16
16
-
console.log(likesData);
17
17
-
likes = likesData.data.feed;
14
14
+
if (data.agent?.session.info.sub) {
15
15
+
const response = await data.agent.handle(
16
16
+
'/xrpc/app.bsky.feed.getActorLikes?actor=' + data.agent.session.info.sub + '&limit=10'
17
17
+
);
18
18
+
19
19
+
const json = await response.json();
20
20
+
console.log(json);
21
21
+
likes = json.feed;
22
22
+
}
23
23
+
}
24
24
+
25
25
+
import { XRPC } from '@atcute/client';
26
26
+
27
27
+
async function putRecord() {
28
28
+
if (data.agent?.session.info.sub) {
29
29
+
try {
30
30
+
31
31
+
const rpc = new XRPC({ handler: data.agent });
32
32
+
33
33
+
const hello = await rpc.call('com.atproto.repo.createRecord', {
34
34
+
data: {
35
35
+
collection: 'com.atproto.test',
36
36
+
repo: data.agent.session.info.sub,
37
37
+
record: {
38
38
+
text: 'hello there'
39
39
+
}
40
40
+
}
41
41
+
});
42
42
+
console.log(hello);
43
43
+
} catch (error) {
44
44
+
console.log('hello', error);
45
45
+
}
18
46
}
19
47
}
20
48
</script>
···
22
50
<div class="mx-auto my-16 max-w-3xl px-2">
23
51
<h1 class="text-3xl font-bold">svelte atproto client oauth demo</h1>
24
52
25
25
-
{#if !data.client}
26
26
-
<div class="mt-8 text-sm">client not loaded</div>
53
53
+
{#if data.isInitializing}
54
54
+
<div class="mt-8 text-sm">loading...</div>
27
55
{/if}
28
56
29
29
-
{#if data.client && !data.agent}
57
57
+
{#if !data.isInitializing && !data.agent}
30
58
<div class="mt-8 text-sm">not signed in</div>
31
59
<Button class="mt-4" onclick={() => (showLoginModal.visible = true)}>Sign In</Button>
32
60
{/if}
33
61
34
62
{#if data.agent}
35
35
-
<div class="mt-8 text-sm">signed in with {data.agent.did}</div>
63
63
+
<div class="mt-8 text-sm">signed in with {data.agent.session.info.sub}</div>
36
64
37
65
<Button class="mt-4" onclick={() => signOut()}>Sign Out</Button>
38
66
39
67
<Button class="mt-4" onclick={getLikes}>Get recent likes</Button>
40
68
69
69
+
<Button class="mt-4" onclick={putRecord}>Put record</Button>
70
70
+
41
71
{#if likes.length > 0}
42
72
<div class="mt-8 text-sm">recent likes</div>
43
73
<ul class="mt-4 flex flex-col gap-2 text-sm">
···
47
77
</ul>
48
78
{/if}
49
79
{/if}
50
50
-
</div>
80
80
+
81
81
+
<a href="/svelte-atproto-client-oauth/test">test</a>
82
82
+
</div>
+80
src/routes/test/+page.svelte
···
1
1
+
<script>
2
2
+
import { data, signOut } from '$lib/auth.svelte';
3
3
+
import Button from '$lib/UI/Button.svelte';
4
4
+
import { showLoginModal } from '$lib/state.svelte';
5
5
+
import { onMount } from 'svelte';
6
6
+
7
7
+
onMount(() => {
8
8
+
console.log(data.agent);
9
9
+
});
10
10
+
11
11
+
let likes = $state([]);
12
12
+
13
13
+
async function getLikes() {
14
14
+
if (data.agent?.session.info.sub) {
15
15
+
const response = await data.agent.handle(
16
16
+
'/xrpc/app.bsky.feed.getActorLikes?actor=' + data.agent.session.info.sub + '&limit=10'
17
17
+
);
18
18
+
19
19
+
const json = await response.json();
20
20
+
console.log(json);
21
21
+
likes = json.feed;
22
22
+
}
23
23
+
}
24
24
+
25
25
+
import { XRPC } from '@atcute/client';
26
26
+
27
27
+
async function putRecord() {
28
28
+
if (data.agent?.session.info.sub) {
29
29
+
try {
30
30
+
31
31
+
const rpc = new XRPC({ handler: data.agent });
32
32
+
33
33
+
const hello = await rpc.call('com.atproto.repo.createRecord', {
34
34
+
data: {
35
35
+
collection: 'com.atproto.test',
36
36
+
repo: data.agent.session.info.sub,
37
37
+
record: {
38
38
+
text: 'hello there'
39
39
+
}
40
40
+
}
41
41
+
});
42
42
+
console.log(hello);
43
43
+
} catch (error) {
44
44
+
console.log('hello', error);
45
45
+
}
46
46
+
}
47
47
+
}
48
48
+
</script>
49
49
+
50
50
+
<div class="mx-auto my-16 max-w-3xl px-2">
51
51
+
<h1 class="text-3xl font-bold">svelte atproto client oauth demo</h1>
52
52
+
53
53
+
{#if data.isInitializing}
54
54
+
<div class="mt-8 text-sm">loading...</div>
55
55
+
{/if}
56
56
+
57
57
+
{#if !data.isInitializing && !data.agent}
58
58
+
<div class="mt-8 text-sm">not signed in</div>
59
59
+
<Button class="mt-4" onclick={() => (showLoginModal.visible = true)}>Sign In</Button>
60
60
+
{/if}
61
61
+
62
62
+
{#if data.agent}
63
63
+
<div class="mt-8 text-sm">signed in with {data.agent.session.info.sub}</div>
64
64
+
65
65
+
<Button class="mt-4" onclick={() => signOut()}>Sign Out</Button>
66
66
+
67
67
+
<Button class="mt-4" onclick={getLikes}>Get recent likes</Button>
68
68
+
69
69
+
<Button class="mt-4" onclick={putRecord}>Put record</Button>
70
70
+
71
71
+
{#if likes.length > 0}
72
72
+
<div class="mt-8 text-sm">recent likes</div>
73
73
+
<ul class="mt-4 flex flex-col gap-2 text-sm">
74
74
+
{#each likes as like}
75
75
+
<li>{like.post.record.text}</li>
76
76
+
{/each}
77
77
+
</ul>
78
78
+
{/if}
79
79
+
{/if}
80
80
+
</div>