tangled
alpha
login
or
join now
vielle.dev
/
dnd-astral-powers
0
fork
atom
this repo has no description
0
fork
atom
overview
issues
pulls
pipelines
add oauth flow
vielle.dev
1 month ago
379ab65d
6cd5cd44
verified
This commit was signed with the committer's
known signature
.
vielle.dev
SSH Key Fingerprint:
SHA256:/4bvxqoEh9iMdjAPgcgAgXKZZQTROL3ULiPt6nH9RSs=
+532
-13
10 changed files
expand all
collapse all
unified
split
package.json
pnpm-lock.yaml
src
Base.astro
lib
auth.ts
oauth-client-metadata.json
pages
atproto
callback.astro
login.astro
logout.astro
index.astro
oauth-client-metadata.json.ts
+5
-1
package.json
···
9
9
"astro": "astro"
10
10
},
11
11
"dependencies": {
12
12
+
"@atcute/client": "^4.2.1",
13
13
+
"@atcute/identity-resolver": "^1.2.2",
14
14
+
"@atcute/oauth-browser-client": "^2.0.3",
15
15
+
"actor-typeahead": "^0.1.2",
12
16
"astro": "^5.16.6"
13
17
}
14
14
-
}
18
18
+
}
+123
pnpm-lock.yaml
···
8
8
9
9
.:
10
10
dependencies:
11
11
+
'@atcute/client':
12
12
+
specifier: ^4.2.1
13
13
+
version: 4.2.1
14
14
+
'@atcute/identity-resolver':
15
15
+
specifier: ^1.2.2
16
16
+
version: 1.2.2(@atcute/identity@1.1.3)
17
17
+
'@atcute/oauth-browser-client':
18
18
+
specifier: ^2.0.3
19
19
+
version: 2.0.3(@atcute/identity@1.1.3)
20
20
+
actor-typeahead:
21
21
+
specifier: ^0.1.2
22
22
+
version: 0.1.2
11
23
astro:
12
24
specifier: ^5.16.6
13
25
version: 5.16.6(@types/node@25.0.3)(rollup@4.55.1)(typescript@5.9.3)
···
31
43
resolution: {integrity: sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==}
32
44
engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0}
33
45
46
46
+
'@atcute/client@4.2.1':
47
47
+
resolution: {integrity: sha512-ZBFM2pW075JtgGFu5g7HHZBecrClhlcNH8GVP9Zz1aViWR+cjjBsTpeE63rJs+FCOHFYlirUyo5L8SGZ4kMINw==}
48
48
+
49
49
+
'@atcute/identity-resolver@1.2.2':
50
50
+
resolution: {integrity: sha512-eUh/UH4bFvuXS0X7epYCeJC/kj4rbBXfSRumLEH4smMVwNOgTo7cL/0Srty+P/qVPoZEyXdfEbS0PHJyzoXmHw==}
51
51
+
peerDependencies:
52
52
+
'@atcute/identity': ^1.0.0
53
53
+
54
54
+
'@atcute/identity@1.1.3':
55
55
+
resolution: {integrity: sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng==}
56
56
+
57
57
+
'@atcute/lexicons@1.2.6':
58
58
+
resolution: {integrity: sha512-s76UQd8D+XmHIzrjD9CJ9SOOeeLPHc+sMmcj7UFakAW/dDFXc579fcRdRfuUKvXBL5v1Gs2VgDdlh/IvvQZAwA==}
59
59
+
60
60
+
'@atcute/multibase@1.1.6':
61
61
+
resolution: {integrity: sha512-HBxuCgYLKPPxETV0Rot4VP9e24vKl8JdzGCZOVsDaOXJgbRZoRIF67Lp0H/OgnJeH/Xpva8Z5ReoTNJE5dn3kg==}
62
62
+
63
63
+
'@atcute/oauth-browser-client@2.0.3':
64
64
+
resolution: {integrity: sha512-rzUjwhjE4LRRKdQnCFQag/zXRZMEAB1hhBoLfnoQuHwWbmDUCL7fzwC3jRhDPp3om8XaYNDj8a/iqRip0wRqoQ==}
65
65
+
66
66
+
'@atcute/uint8array@1.0.6':
67
67
+
resolution: {integrity: sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A==}
68
68
+
69
69
+
'@atcute/util-fetch@1.0.5':
70
70
+
resolution: {integrity: sha512-qjHj01BGxjSjIFdPiAjSARnodJIIyKxnCMMEcXMESo9TAyND6XZQqrie5fia+LlYWVXdpsTds8uFQwc9jdKTig==}
71
71
+
72
72
+
'@atcute/util-text@0.0.1':
73
73
+
resolution: {integrity: sha512-t1KZqvn0AYy+h2KcJyHnKF9aEqfRfMUmyY8j1ELtAEIgqN9CxINAjxnoRCJIFUlvWzb+oY3uElQL/Vyk3yss0g==}
74
74
+
34
75
'@babel/helper-string-parser@7.27.1':
35
76
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
36
77
engines: {node: '>=6.9.0'}
···
47
88
'@babel/types@7.28.5':
48
89
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
49
90
engines: {node: '>=6.9.0'}
91
91
+
92
92
+
'@badrap/valita@0.4.6':
93
93
+
resolution: {integrity: sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==}
94
94
+
engines: {node: '>= 18'}
50
95
51
96
'@capsizecss/unpack@3.0.1':
52
97
resolution: {integrity: sha512-8XqW8xGn++Eqqbz3e9wKuK7mxryeRjs4LOHLxbh2lwKeSbuNR4NFifDZT4KzvjU6HMOPbiNTsWpniK5EJfTWkg==}
···
509
554
'@shikijs/vscode-textmate@10.0.2':
510
555
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
511
556
557
557
+
'@standard-schema/spec@1.1.0':
558
558
+
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
559
559
+
512
560
'@swc/helpers@0.5.18':
513
561
resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==}
514
562
···
546
594
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
547
595
engines: {node: '>=0.4.0'}
548
596
hasBin: true
597
597
+
598
598
+
actor-typeahead@0.1.2:
599
599
+
resolution: {integrity: sha512-I97YqqNl7Kar0J/bIJvgY/KmHpssHcDElhfwVTLP7wRFlkxso2ZLBqiS2zol5A8UVUJbQK2JXYaqNpZXz8Uk2A==}
549
600
550
601
ansi-align@3.0.1:
551
602
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
···
776
827
escape-string-regexp@5.0.0:
777
828
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
778
829
engines: {node: '>=12'}
830
830
+
831
831
+
esm-env@1.2.2:
832
832
+
resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
779
833
780
834
estree-walker@2.0.2:
781
835
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
···
1057
1111
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
1058
1112
hasBin: true
1059
1113
1114
1114
+
nanoid@5.1.6:
1115
1115
+
resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==}
1116
1116
+
engines: {node: ^18 || >=20}
1117
1117
+
hasBin: true
1118
1118
+
1060
1119
neotraverse@0.6.18:
1061
1120
resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==}
1062
1121
engines: {node: '>= 10'}
···
1312
1371
1313
1372
unicode-properties@1.4.1:
1314
1373
resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==}
1374
1374
+
1375
1375
+
unicode-segmenter@0.14.5:
1376
1376
+
resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==}
1315
1377
1316
1378
unicode-trie@2.0.0:
1317
1379
resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==}
···
1567
1629
transitivePeerDependencies:
1568
1630
- supports-color
1569
1631
1632
1632
+
'@atcute/client@4.2.1':
1633
1633
+
dependencies:
1634
1634
+
'@atcute/identity': 1.1.3
1635
1635
+
'@atcute/lexicons': 1.2.6
1636
1636
+
1637
1637
+
'@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.3)':
1638
1638
+
dependencies:
1639
1639
+
'@atcute/identity': 1.1.3
1640
1640
+
'@atcute/lexicons': 1.2.6
1641
1641
+
'@atcute/util-fetch': 1.0.5
1642
1642
+
'@badrap/valita': 0.4.6
1643
1643
+
1644
1644
+
'@atcute/identity@1.1.3':
1645
1645
+
dependencies:
1646
1646
+
'@atcute/lexicons': 1.2.6
1647
1647
+
'@badrap/valita': 0.4.6
1648
1648
+
1649
1649
+
'@atcute/lexicons@1.2.6':
1650
1650
+
dependencies:
1651
1651
+
'@atcute/uint8array': 1.0.6
1652
1652
+
'@atcute/util-text': 0.0.1
1653
1653
+
'@standard-schema/spec': 1.1.0
1654
1654
+
esm-env: 1.2.2
1655
1655
+
1656
1656
+
'@atcute/multibase@1.1.6':
1657
1657
+
dependencies:
1658
1658
+
'@atcute/uint8array': 1.0.6
1659
1659
+
1660
1660
+
'@atcute/oauth-browser-client@2.0.3(@atcute/identity@1.1.3)':
1661
1661
+
dependencies:
1662
1662
+
'@atcute/client': 4.2.1
1663
1663
+
'@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.3)
1664
1664
+
'@atcute/lexicons': 1.2.6
1665
1665
+
'@atcute/multibase': 1.1.6
1666
1666
+
'@atcute/uint8array': 1.0.6
1667
1667
+
nanoid: 5.1.6
1668
1668
+
transitivePeerDependencies:
1669
1669
+
- '@atcute/identity'
1670
1670
+
1671
1671
+
'@atcute/uint8array@1.0.6': {}
1672
1672
+
1673
1673
+
'@atcute/util-fetch@1.0.5':
1674
1674
+
dependencies:
1675
1675
+
'@badrap/valita': 0.4.6
1676
1676
+
1677
1677
+
'@atcute/util-text@0.0.1':
1678
1678
+
dependencies:
1679
1679
+
unicode-segmenter: 0.14.5
1680
1680
+
1570
1681
'@babel/helper-string-parser@7.27.1': {}
1571
1682
1572
1683
'@babel/helper-validator-identifier@7.28.5': {}
···
1579
1690
dependencies:
1580
1691
'@babel/helper-string-parser': 7.27.1
1581
1692
'@babel/helper-validator-identifier': 7.28.5
1693
1693
+
1694
1694
+
'@badrap/valita@0.4.6': {}
1582
1695
1583
1696
'@capsizecss/unpack@3.0.1':
1584
1697
dependencies:
···
1884
1997
1885
1998
'@shikijs/vscode-textmate@10.0.2': {}
1886
1999
2000
2000
+
'@standard-schema/spec@1.1.0': {}
2001
2001
+
1887
2002
'@swc/helpers@0.5.18':
1888
2003
dependencies:
1889
2004
tslib: 2.8.1
···
1922
2037
1923
2038
acorn@8.15.0: {}
1924
2039
2040
2040
+
actor-typeahead@0.1.2: {}
2041
2041
+
1925
2042
ansi-align@3.0.1:
1926
2043
dependencies:
1927
2044
string-width: 4.2.3
···
2227
2344
'@esbuild/win32-x64': 0.25.12
2228
2345
2229
2346
escape-string-regexp@5.0.0: {}
2347
2347
+
2348
2348
+
esm-env@1.2.2: {}
2230
2349
2231
2350
estree-walker@2.0.2: {}
2232
2351
···
2736
2855
2737
2856
nanoid@3.3.11: {}
2738
2857
2858
2858
+
nanoid@5.1.6: {}
2859
2859
+
2739
2860
neotraverse@0.6.18: {}
2740
2861
2741
2862
nlcst-to-string@4.0.0:
···
3082
3203
dependencies:
3083
3204
base64-js: 1.5.1
3084
3205
unicode-trie: 2.0.0
3206
3206
+
3207
3207
+
unicode-segmenter@0.14.5: {}
3085
3208
3086
3209
unicode-trie@2.0.0:
3087
3210
dependencies:
+25
src/Base.astro
···
1
1
+
---
2
2
+
interface Props {
3
3
+
title: string;
4
4
+
}
5
5
+
---
6
6
+
7
7
+
<html lang="en">
8
8
+
<head>
9
9
+
<meta charset="utf-8" />
10
10
+
<meta name="viewport" content="width=device-width" />
11
11
+
<meta name="generator" content={Astro.generator} />
12
12
+
<title>{Astro.props.title}</title>
13
13
+
<style is:inline>
14
14
+
@layer base {
15
15
+
:root {
16
16
+
color-scheme: dark;
17
17
+
}
18
18
+
}
19
19
+
</style>
20
20
+
<slot name="head" />
21
21
+
</head>
22
22
+
<body>
23
23
+
<slot />
24
24
+
</body>
25
25
+
</html>
+77
src/lib/auth.ts
···
1
1
+
import { Client } from "@atcute/client";
2
2
+
import { getSession, OAuthUserAgent } from "@atcute/oauth-browser-client";
3
3
+
import { configureOAuth } from "@atcute/oauth-browser-client";
4
4
+
import {
5
5
+
CompositeDidDocumentResolver,
6
6
+
LocalActorResolver,
7
7
+
PlcDidDocumentResolver,
8
8
+
WebDidDocumentResolver,
9
9
+
XrpcHandleResolver,
10
10
+
} from "@atcute/identity-resolver";
11
11
+
12
12
+
import metadata from "../oauth-client-metadata.json" with { type: "json" };
13
13
+
14
14
+
configureOAuth({
15
15
+
metadata: import.meta.env.PROD
16
16
+
? {
17
17
+
client_id: metadata.client_id,
18
18
+
redirect_uri: metadata.redirect_uris[0],
19
19
+
}
20
20
+
: {
21
21
+
client_id: `http://localhost?${new URLSearchParams({
22
22
+
redirect_uri: "http://127.0.0.1/atproto/callback",
23
23
+
scope: "atproto transition:generic",
24
24
+
}).toString()}`,
25
25
+
redirect_uri: "http://127.0.0.1/atproto/callback",
26
26
+
},
27
27
+
identityResolver: new LocalActorResolver({
28
28
+
handleResolver: new XrpcHandleResolver({
29
29
+
serviceUrl: "https://public.api.bsky.app",
30
30
+
}),
31
31
+
didDocumentResolver: new CompositeDidDocumentResolver({
32
32
+
methods: {
33
33
+
plc: new PlcDidDocumentResolver(),
34
34
+
web: new WebDidDocumentResolver(),
35
35
+
},
36
36
+
}),
37
37
+
}),
38
38
+
});
39
39
+
40
40
+
// overrides
41
41
+
export async function getAuth(
42
42
+
forceAuth: true
43
43
+
): Promise<[Client, OAuthUserAgent, `did:${string}:${string}`]>;
44
44
+
export async function getAuth(
45
45
+
forceAuth?: false
46
46
+
): Promise<
47
47
+
| [Client, OAuthUserAgent, `did:${string}:${string}`]
48
48
+
| [undefined, undefined, undefined]
49
49
+
>;
50
50
+
// main impl
51
51
+
export async function getAuth(
52
52
+
forceAuth?: boolean
53
53
+
): Promise<
54
54
+
| [Client, OAuthUserAgent, `did:${string}:${string}`]
55
55
+
| [undefined, undefined, undefined]
56
56
+
> {
57
57
+
const did = localStorage.getItem("did");
58
58
+
try {
59
59
+
if (did && did.match(/^did:.+:.*$/)) {
60
60
+
const session = await getSession(did as `did:${string}:${string}`, {});
61
61
+
const agent = new OAuthUserAgent(session);
62
62
+
const client = new Client({ handler: agent });
63
63
+
return [client, agent, did as `did:${string}:${string}`];
64
64
+
} else {
65
65
+
if (forceAuth) {
66
66
+
location.assign("/atproto/login");
67
67
+
return undefined as never;
68
68
+
} else return [undefined, undefined, undefined];
69
69
+
}
70
70
+
} catch (e) {
71
71
+
console.warn(e);
72
72
+
if (forceAuth) {
73
73
+
location.assign("/atproto/login");
74
74
+
return undefined as never;
75
75
+
} else return [undefined, undefined, undefined];
76
76
+
}
77
77
+
}
+12
src/oauth-client-metadata.json
···
1
1
+
{
2
2
+
"client_id": "http://localhost/oauth-client-metadata.json",
3
3
+
"application_type": "web",
4
4
+
"grant_types": ["authorization_code"],
5
5
+
"scope": "atproto transition:generic",
6
6
+
"response_types": ["code"],
7
7
+
"redirect_uris": ["http://127.0.0.1/atproto/callback"],
8
8
+
"token_endpoint_auth_method": "none",
9
9
+
"dpop_bound_access_tokens": true,
10
10
+
"client_name": "D&D Utils",
11
11
+
"client_uri": "https://dnd.vielle.dev"
12
12
+
}
+92
src/pages/atproto/callback.astro
···
1
1
+
---
2
2
+
import Base from "../../Base.astro";
3
3
+
---
4
4
+
5
5
+
<Base title="Authenticating...">
6
6
+
<style>
7
7
+
@keyframes --blink {
8
8
+
from,
9
9
+
to {
10
10
+
opacity: 0;
11
11
+
}
12
12
+
13
13
+
50% {
14
14
+
opacity: 1;
15
15
+
}
16
16
+
}
17
17
+
18
18
+
.blink {
19
19
+
animation: 2s linear var(--offset) infinite forwards --blink;
20
20
+
}
21
21
+
22
22
+
:global(body, html) {
23
23
+
margin: 0;
24
24
+
height: 100%;
25
25
+
width: 100%;
26
26
+
display: flex;
27
27
+
align-items: center;
28
28
+
justify-content: center;
29
29
+
}
30
30
+
</style>
31
31
+
<script>
32
32
+
import {
33
33
+
configureOAuth,
34
34
+
finalizeAuthorization,
35
35
+
} from "@atcute/oauth-browser-client";
36
36
+
import {
37
37
+
CompositeDidDocumentResolver,
38
38
+
LocalActorResolver,
39
39
+
PlcDidDocumentResolver,
40
40
+
WebDidDocumentResolver,
41
41
+
XrpcHandleResolver,
42
42
+
} from "@atcute/identity-resolver";
43
43
+
import metadata from "../../oauth-client-metadata.json" with { type: "json" };
44
44
+
import { getAuth } from "../../lib/auth";
45
45
+
46
46
+
configureOAuth({
47
47
+
metadata: import.meta.env.PROD
48
48
+
? {
49
49
+
client_id: metadata.client_id,
50
50
+
redirect_uri: metadata.redirect_uris[0],
51
51
+
}
52
52
+
: {
53
53
+
client_id: `http://localhost?${new URLSearchParams({
54
54
+
redirect_uri: "http://127.0.0.1/atproto/callback",
55
55
+
scope: "atproto transition:generic",
56
56
+
}).toString()}`,
57
57
+
redirect_uri: "http://127.0.0.1/atproto/callback",
58
58
+
},
59
59
+
identityResolver: new LocalActorResolver({
60
60
+
handleResolver: new XrpcHandleResolver({
61
61
+
serviceUrl: "https://public.api.bsky.app",
62
62
+
}),
63
63
+
didDocumentResolver: new CompositeDidDocumentResolver({
64
64
+
methods: {
65
65
+
plc: new PlcDidDocumentResolver(),
66
66
+
web: new WebDidDocumentResolver(),
67
67
+
},
68
68
+
}),
69
69
+
}),
70
70
+
});
71
71
+
72
72
+
const params = new URLSearchParams(location.hash.slice(1));
73
73
+
history.replaceState(null, "", window.location.pathname);
74
74
+
75
75
+
// if a did is already stored use existing auth
76
76
+
// if user wants to change account they need to sign out
77
77
+
// if restoring auth fails, finalize auth as usual
78
78
+
const session = !localStorage.getItem("did")
79
79
+
? (await finalizeAuthorization(params)).session
80
80
+
: ((await getAuth())[1]?.session ??
81
81
+
(await finalizeAuthorization(params)).session);
82
82
+
localStorage.setItem("did", session.info.sub);
83
83
+
84
84
+
window.location.assign("/");
85
85
+
</script>
86
86
+
<h1>
87
87
+
Authenticating <span class="blink" style="--offset: -0.2s">.</span><span
88
88
+
class="blink"
89
89
+
style="--offset: -0.1s">.</span
90
90
+
><span class="blink" style="--offset: 0s">.</span>
91
91
+
</h1>
92
92
+
</Base>
+121
src/pages/atproto/login.astro
···
1
1
+
---
2
2
+
import Base from "../../Base.astro";
3
3
+
---
4
4
+
5
5
+
<Base title="D&D Utils">
6
6
+
<script>
7
7
+
import "actor-typeahead";
8
8
+
import {
9
9
+
configureOAuth,
10
10
+
createAuthorizationUrl,
11
11
+
} from "@atcute/oauth-browser-client";
12
12
+
import {
13
13
+
CompositeDidDocumentResolver,
14
14
+
LocalActorResolver,
15
15
+
PlcDidDocumentResolver,
16
16
+
WebDidDocumentResolver,
17
17
+
XrpcHandleResolver,
18
18
+
} from "@atcute/identity-resolver";
19
19
+
import metadata from "../../oauth-client-metadata.json" with { type: "json" };
20
20
+
import { getAuth } from "../../lib/auth";
21
21
+
22
22
+
configureOAuth({
23
23
+
metadata: import.meta.env.PROD
24
24
+
? {
25
25
+
client_id: metadata.client_id,
26
26
+
redirect_uri: metadata.redirect_uris[0],
27
27
+
}
28
28
+
: {
29
29
+
client_id: `http://localhost?${new URLSearchParams({
30
30
+
redirect_uri: "http://127.0.0.1/atproto/callback",
31
31
+
scope: "atproto transition:generic",
32
32
+
}).toString()}`,
33
33
+
redirect_uri: "http://127.0.0.1/atproto/callback",
34
34
+
},
35
35
+
identityResolver: new LocalActorResolver({
36
36
+
handleResolver: new XrpcHandleResolver({
37
37
+
serviceUrl: "https://public.api.bsky.app",
38
38
+
}),
39
39
+
didDocumentResolver: new CompositeDidDocumentResolver({
40
40
+
methods: {
41
41
+
plc: new PlcDidDocumentResolver(),
42
42
+
web: new WebDidDocumentResolver(),
43
43
+
},
44
44
+
}),
45
45
+
}),
46
46
+
});
47
47
+
48
48
+
// if theres a did in storage and it restores, go to /
49
49
+
if (localStorage.getItem("did") && (await getAuth().then((x) => !!x[0])))
50
50
+
window.location.assign("/");
51
51
+
52
52
+
const submit = document.getElementById(
53
53
+
"submit"
54
54
+
) as HTMLButtonElement | null;
55
55
+
if (!submit) throw null;
56
56
+
submit.addEventListener("click", async (ev) => {
57
57
+
ev.preventDefault();
58
58
+
const unameEl = document.getElementById("username");
59
59
+
if (!(unameEl instanceof HTMLInputElement)) return;
60
60
+
if (!unameEl.validity.valid) {
61
61
+
unameEl.reportValidity();
62
62
+
return;
63
63
+
}
64
64
+
const handle = unameEl.value;
65
65
+
submit.disabled = true;
66
66
+
67
67
+
const authUrl = await createAuthorizationUrl({
68
68
+
target: {
69
69
+
type: "account",
70
70
+
identifier: handle as `${string}.${string}`,
71
71
+
},
72
72
+
scope: "atproto transition:generic",
73
73
+
});
74
74
+
75
75
+
await new Promise((res) => setTimeout(res, 200)); // let browser persist local storage
76
76
+
window.location.assign(authUrl);
77
77
+
});
78
78
+
</script>
79
79
+
<style>
80
80
+
:global(html, body) {
81
81
+
height: 100%;
82
82
+
margin: 0;
83
83
+
}
84
84
+
85
85
+
form {
86
86
+
height: calc(100% - 20px);
87
87
+
width: calc(100% - 20px);
88
88
+
max-width: 40ch;
89
89
+
padding: 10px;
90
90
+
margin: auto;
91
91
+
92
92
+
display: flex;
93
93
+
flex-direction: row;
94
94
+
align-items: center;
95
95
+
justify-content: center;
96
96
+
gap: 10px;
97
97
+
}
98
98
+
99
99
+
actor-typeahead {
100
100
+
--color-background: #2b2b2b;
101
101
+
--color-border: #ffffff22;
102
102
+
--color-shadow: #ffffff;
103
103
+
--color-hover: #ffffff11;
104
104
+
flex: 1;
105
105
+
}
106
106
+
107
107
+
input {
108
108
+
padding: 2px;
109
109
+
border: 2px inset ButtonBorder;
110
110
+
border-radius: 5px;
111
111
+
width: calc(100% - 4px - 2.5px);
112
112
+
}
113
113
+
</style>
114
114
+
115
115
+
<form>
116
116
+
<actor-typeahead>
117
117
+
<input id="username" required autocomplete="off" />
118
118
+
</actor-typeahead>
119
119
+
<button id="submit">Sign in</button>
120
120
+
</form>
121
121
+
</Base>
+51
src/pages/atproto/logout.astro
···
1
1
+
---
2
2
+
import Base from "../../Base.astro";
3
3
+
---
4
4
+
5
5
+
<Base title="Logging out">
6
6
+
<script>
7
7
+
import { getAuth } from "../../lib/auth";
8
8
+
const [_, agent, __] = await getAuth();
9
9
+
try {
10
10
+
await agent?.signOut();
11
11
+
localStorage.removeItem("did");
12
12
+
window.location.assign("/");
13
13
+
} catch (e) {
14
14
+
const h1 = document.querySelector("h1");
15
15
+
if (!h1) throw "no <h1>";
16
16
+
console.error(e);
17
17
+
h1.outerHTML = /*html*/ `<h1>Got Error</h1><code><pre>${e instanceof Error ? e.name + "\n" + e.message + (e.stack ? "\n" + e.stack : "") + (e.cause ? "\n" + e.cause : "") : e}</pre></code>`;
18
18
+
}
19
19
+
</script>
20
20
+
<style>
21
21
+
@keyframes --blink {
22
22
+
from,
23
23
+
to {
24
24
+
opacity: 0;
25
25
+
}
26
26
+
27
27
+
50% {
28
28
+
opacity: 1;
29
29
+
}
30
30
+
}
31
31
+
32
32
+
.blink {
33
33
+
animation: 2s linear var(--offset) infinite forwards --blink;
34
34
+
}
35
35
+
36
36
+
:global(body, html) {
37
37
+
margin: 0;
38
38
+
height: 100%;
39
39
+
width: 100%;
40
40
+
display: flex;
41
41
+
align-items: center;
42
42
+
justify-content: center;
43
43
+
}
44
44
+
</style>
45
45
+
<h1>
46
46
+
Logging out <span class="blink" style="--offset: -0.2s">.</span><span
47
47
+
class="blink"
48
48
+
style="--offset: -0.1s">.</span
49
49
+
><span class="blink" style="--offset: 0s">.</span>
50
50
+
</h1>
51
51
+
</Base>
+19
-12
src/pages/index.astro
···
1
1
---
2
2
-
2
2
+
import Base from "../Base.astro";
3
3
---
4
4
5
5
-
<html lang="en">
6
6
-
<head>
7
7
-
<meta charset="utf-8" />
8
8
-
<meta name="viewport" content="width=device-width" />
9
9
-
<meta name="generator" content={Astro.generator} />
10
10
-
<title>Astro</title>
11
11
-
</head>
12
12
-
<body>
13
13
-
<h1>Astro</h1>
14
14
-
</body>
15
15
-
</html>
5
5
+
<Base title="D&D Utils">
6
6
+
<script>
7
7
+
import { getAuth } from "../lib/auth";
8
8
+
const [client, agent, did] = await getAuth();
9
9
+
const login = document.getElementById("login");
10
10
+
if (!login) throw "no #login";
11
11
+
if (did)
12
12
+
login.outerHTML = `<a id="login" href="/atproto/logout">Logout</a>`;
13
13
+
else login.outerHTML = `<a id="login" href="/atproto/login">Login</a>`;
14
14
+
</script>
15
15
+
<h1>D&D Utils</h1>
16
16
+
<a id="login" href="#" style="display: none;"></a>
17
17
+
<p>Pages:</p>
18
18
+
<ul>
19
19
+
<li><a href="/astral">Astral Powers</a></li>
20
20
+
<li><a href="/chip">Chip</a></li>
21
21
+
</ul>
22
22
+
</Base>
+7
src/pages/oauth-client-metadata.json.ts
···
1
1
+
import metadata from "../oauth-client-metadata.json" with { type: "json" };
2
2
+
3
3
+
export function GET(): Response {
4
4
+
return new Response(JSON.stringify(metadata), {
5
5
+
headers: new Headers({ "content-type": "application/json" }),
6
6
+
});
7
7
+
}