this repo has no description

add oauth flow

vielle.dev 379ab65d 6cd5cd44

verified
+532 -13
+5 -1
package.json
··· 9 9 "astro": "astro" 10 10 }, 11 11 "dependencies": { 12 + "@atcute/client": "^4.2.1", 13 + "@atcute/identity-resolver": "^1.2.2", 14 + "@atcute/oauth-browser-client": "^2.0.3", 15 + "actor-typeahead": "^0.1.2", 12 16 "astro": "^5.16.6" 13 17 } 14 - } 18 + }
+123
pnpm-lock.yaml
··· 8 8 9 9 .: 10 10 dependencies: 11 + '@atcute/client': 12 + specifier: ^4.2.1 13 + version: 4.2.1 14 + '@atcute/identity-resolver': 15 + specifier: ^1.2.2 16 + version: 1.2.2(@atcute/identity@1.1.3) 17 + '@atcute/oauth-browser-client': 18 + specifier: ^2.0.3 19 + version: 2.0.3(@atcute/identity@1.1.3) 20 + actor-typeahead: 21 + specifier: ^0.1.2 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 + '@atcute/client@4.2.1': 47 + resolution: {integrity: sha512-ZBFM2pW075JtgGFu5g7HHZBecrClhlcNH8GVP9Zz1aViWR+cjjBsTpeE63rJs+FCOHFYlirUyo5L8SGZ4kMINw==} 48 + 49 + '@atcute/identity-resolver@1.2.2': 50 + resolution: {integrity: sha512-eUh/UH4bFvuXS0X7epYCeJC/kj4rbBXfSRumLEH4smMVwNOgTo7cL/0Srty+P/qVPoZEyXdfEbS0PHJyzoXmHw==} 51 + peerDependencies: 52 + '@atcute/identity': ^1.0.0 53 + 54 + '@atcute/identity@1.1.3': 55 + resolution: {integrity: sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng==} 56 + 57 + '@atcute/lexicons@1.2.6': 58 + resolution: {integrity: sha512-s76UQd8D+XmHIzrjD9CJ9SOOeeLPHc+sMmcj7UFakAW/dDFXc579fcRdRfuUKvXBL5v1Gs2VgDdlh/IvvQZAwA==} 59 + 60 + '@atcute/multibase@1.1.6': 61 + resolution: {integrity: sha512-HBxuCgYLKPPxETV0Rot4VP9e24vKl8JdzGCZOVsDaOXJgbRZoRIF67Lp0H/OgnJeH/Xpva8Z5ReoTNJE5dn3kg==} 62 + 63 + '@atcute/oauth-browser-client@2.0.3': 64 + resolution: {integrity: sha512-rzUjwhjE4LRRKdQnCFQag/zXRZMEAB1hhBoLfnoQuHwWbmDUCL7fzwC3jRhDPp3om8XaYNDj8a/iqRip0wRqoQ==} 65 + 66 + '@atcute/uint8array@1.0.6': 67 + resolution: {integrity: sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A==} 68 + 69 + '@atcute/util-fetch@1.0.5': 70 + resolution: {integrity: sha512-qjHj01BGxjSjIFdPiAjSARnodJIIyKxnCMMEcXMESo9TAyND6XZQqrie5fia+LlYWVXdpsTds8uFQwc9jdKTig==} 71 + 72 + '@atcute/util-text@0.0.1': 73 + resolution: {integrity: sha512-t1KZqvn0AYy+h2KcJyHnKF9aEqfRfMUmyY8j1ELtAEIgqN9CxINAjxnoRCJIFUlvWzb+oY3uElQL/Vyk3yss0g==} 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 + 92 + '@badrap/valita@0.4.6': 93 + resolution: {integrity: sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==} 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 + '@standard-schema/spec@1.1.0': 558 + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} 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 + 598 + actor-typeahead@0.1.2: 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 + 831 + esm-env@1.2.2: 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 + nanoid@5.1.6: 1115 + resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} 1116 + engines: {node: ^18 || >=20} 1117 + hasBin: true 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 + 1375 + unicode-segmenter@0.14.5: 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 + '@atcute/client@4.2.1': 1633 + dependencies: 1634 + '@atcute/identity': 1.1.3 1635 + '@atcute/lexicons': 1.2.6 1636 + 1637 + '@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.3)': 1638 + dependencies: 1639 + '@atcute/identity': 1.1.3 1640 + '@atcute/lexicons': 1.2.6 1641 + '@atcute/util-fetch': 1.0.5 1642 + '@badrap/valita': 0.4.6 1643 + 1644 + '@atcute/identity@1.1.3': 1645 + dependencies: 1646 + '@atcute/lexicons': 1.2.6 1647 + '@badrap/valita': 0.4.6 1648 + 1649 + '@atcute/lexicons@1.2.6': 1650 + dependencies: 1651 + '@atcute/uint8array': 1.0.6 1652 + '@atcute/util-text': 0.0.1 1653 + '@standard-schema/spec': 1.1.0 1654 + esm-env: 1.2.2 1655 + 1656 + '@atcute/multibase@1.1.6': 1657 + dependencies: 1658 + '@atcute/uint8array': 1.0.6 1659 + 1660 + '@atcute/oauth-browser-client@2.0.3(@atcute/identity@1.1.3)': 1661 + dependencies: 1662 + '@atcute/client': 4.2.1 1663 + '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.3) 1664 + '@atcute/lexicons': 1.2.6 1665 + '@atcute/multibase': 1.1.6 1666 + '@atcute/uint8array': 1.0.6 1667 + nanoid: 5.1.6 1668 + transitivePeerDependencies: 1669 + - '@atcute/identity' 1670 + 1671 + '@atcute/uint8array@1.0.6': {} 1672 + 1673 + '@atcute/util-fetch@1.0.5': 1674 + dependencies: 1675 + '@badrap/valita': 0.4.6 1676 + 1677 + '@atcute/util-text@0.0.1': 1678 + dependencies: 1679 + unicode-segmenter: 0.14.5 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 + 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 + '@standard-schema/spec@1.1.0': {} 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 + actor-typeahead@0.1.2: {} 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 + 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 + nanoid@5.1.6: {} 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 + 3207 + unicode-segmenter@0.14.5: {} 3085 3208 3086 3209 unicode-trie@2.0.0: 3087 3210 dependencies:
+25
src/Base.astro
··· 1 + --- 2 + interface Props { 3 + title: string; 4 + } 5 + --- 6 + 7 + <html lang="en"> 8 + <head> 9 + <meta charset="utf-8" /> 10 + <meta name="viewport" content="width=device-width" /> 11 + <meta name="generator" content={Astro.generator} /> 12 + <title>{Astro.props.title}</title> 13 + <style is:inline> 14 + @layer base { 15 + :root { 16 + color-scheme: dark; 17 + } 18 + } 19 + </style> 20 + <slot name="head" /> 21 + </head> 22 + <body> 23 + <slot /> 24 + </body> 25 + </html>
+77
src/lib/auth.ts
··· 1 + import { Client } from "@atcute/client"; 2 + import { getSession, OAuthUserAgent } from "@atcute/oauth-browser-client"; 3 + import { configureOAuth } from "@atcute/oauth-browser-client"; 4 + import { 5 + CompositeDidDocumentResolver, 6 + LocalActorResolver, 7 + PlcDidDocumentResolver, 8 + WebDidDocumentResolver, 9 + XrpcHandleResolver, 10 + } from "@atcute/identity-resolver"; 11 + 12 + import metadata from "../oauth-client-metadata.json" with { type: "json" }; 13 + 14 + configureOAuth({ 15 + metadata: import.meta.env.PROD 16 + ? { 17 + client_id: metadata.client_id, 18 + redirect_uri: metadata.redirect_uris[0], 19 + } 20 + : { 21 + client_id: `http://localhost?${new URLSearchParams({ 22 + redirect_uri: "http://127.0.0.1/atproto/callback", 23 + scope: "atproto transition:generic", 24 + }).toString()}`, 25 + redirect_uri: "http://127.0.0.1/atproto/callback", 26 + }, 27 + identityResolver: new LocalActorResolver({ 28 + handleResolver: new XrpcHandleResolver({ 29 + serviceUrl: "https://public.api.bsky.app", 30 + }), 31 + didDocumentResolver: new CompositeDidDocumentResolver({ 32 + methods: { 33 + plc: new PlcDidDocumentResolver(), 34 + web: new WebDidDocumentResolver(), 35 + }, 36 + }), 37 + }), 38 + }); 39 + 40 + // overrides 41 + export async function getAuth( 42 + forceAuth: true 43 + ): Promise<[Client, OAuthUserAgent, `did:${string}:${string}`]>; 44 + export async function getAuth( 45 + forceAuth?: false 46 + ): Promise< 47 + | [Client, OAuthUserAgent, `did:${string}:${string}`] 48 + | [undefined, undefined, undefined] 49 + >; 50 + // main impl 51 + export async function getAuth( 52 + forceAuth?: boolean 53 + ): Promise< 54 + | [Client, OAuthUserAgent, `did:${string}:${string}`] 55 + | [undefined, undefined, undefined] 56 + > { 57 + const did = localStorage.getItem("did"); 58 + try { 59 + if (did && did.match(/^did:.+:.*$/)) { 60 + const session = await getSession(did as `did:${string}:${string}`, {}); 61 + const agent = new OAuthUserAgent(session); 62 + const client = new Client({ handler: agent }); 63 + return [client, agent, did as `did:${string}:${string}`]; 64 + } else { 65 + if (forceAuth) { 66 + location.assign("/atproto/login"); 67 + return undefined as never; 68 + } else return [undefined, undefined, undefined]; 69 + } 70 + } catch (e) { 71 + console.warn(e); 72 + if (forceAuth) { 73 + location.assign("/atproto/login"); 74 + return undefined as never; 75 + } else return [undefined, undefined, undefined]; 76 + } 77 + }
+12
src/oauth-client-metadata.json
··· 1 + { 2 + "client_id": "http://localhost/oauth-client-metadata.json", 3 + "application_type": "web", 4 + "grant_types": ["authorization_code"], 5 + "scope": "atproto transition:generic", 6 + "response_types": ["code"], 7 + "redirect_uris": ["http://127.0.0.1/atproto/callback"], 8 + "token_endpoint_auth_method": "none", 9 + "dpop_bound_access_tokens": true, 10 + "client_name": "D&D Utils", 11 + "client_uri": "https://dnd.vielle.dev" 12 + }
+92
src/pages/atproto/callback.astro
··· 1 + --- 2 + import Base from "../../Base.astro"; 3 + --- 4 + 5 + <Base title="Authenticating..."> 6 + <style> 7 + @keyframes --blink { 8 + from, 9 + to { 10 + opacity: 0; 11 + } 12 + 13 + 50% { 14 + opacity: 1; 15 + } 16 + } 17 + 18 + .blink { 19 + animation: 2s linear var(--offset) infinite forwards --blink; 20 + } 21 + 22 + :global(body, html) { 23 + margin: 0; 24 + height: 100%; 25 + width: 100%; 26 + display: flex; 27 + align-items: center; 28 + justify-content: center; 29 + } 30 + </style> 31 + <script> 32 + import { 33 + configureOAuth, 34 + finalizeAuthorization, 35 + } from "@atcute/oauth-browser-client"; 36 + import { 37 + CompositeDidDocumentResolver, 38 + LocalActorResolver, 39 + PlcDidDocumentResolver, 40 + WebDidDocumentResolver, 41 + XrpcHandleResolver, 42 + } from "@atcute/identity-resolver"; 43 + import metadata from "../../oauth-client-metadata.json" with { type: "json" }; 44 + import { getAuth } from "../../lib/auth"; 45 + 46 + configureOAuth({ 47 + metadata: import.meta.env.PROD 48 + ? { 49 + client_id: metadata.client_id, 50 + redirect_uri: metadata.redirect_uris[0], 51 + } 52 + : { 53 + client_id: `http://localhost?${new URLSearchParams({ 54 + redirect_uri: "http://127.0.0.1/atproto/callback", 55 + scope: "atproto transition:generic", 56 + }).toString()}`, 57 + redirect_uri: "http://127.0.0.1/atproto/callback", 58 + }, 59 + identityResolver: new LocalActorResolver({ 60 + handleResolver: new XrpcHandleResolver({ 61 + serviceUrl: "https://public.api.bsky.app", 62 + }), 63 + didDocumentResolver: new CompositeDidDocumentResolver({ 64 + methods: { 65 + plc: new PlcDidDocumentResolver(), 66 + web: new WebDidDocumentResolver(), 67 + }, 68 + }), 69 + }), 70 + }); 71 + 72 + const params = new URLSearchParams(location.hash.slice(1)); 73 + history.replaceState(null, "", window.location.pathname); 74 + 75 + // if a did is already stored use existing auth 76 + // if user wants to change account they need to sign out 77 + // if restoring auth fails, finalize auth as usual 78 + const session = !localStorage.getItem("did") 79 + ? (await finalizeAuthorization(params)).session 80 + : ((await getAuth())[1]?.session ?? 81 + (await finalizeAuthorization(params)).session); 82 + localStorage.setItem("did", session.info.sub); 83 + 84 + window.location.assign("/"); 85 + </script> 86 + <h1> 87 + Authenticating <span class="blink" style="--offset: -0.2s">.</span><span 88 + class="blink" 89 + style="--offset: -0.1s">.</span 90 + ><span class="blink" style="--offset: 0s">.</span> 91 + </h1> 92 + </Base>
+121
src/pages/atproto/login.astro
··· 1 + --- 2 + import Base from "../../Base.astro"; 3 + --- 4 + 5 + <Base title="D&D Utils"> 6 + <script> 7 + import "actor-typeahead"; 8 + import { 9 + configureOAuth, 10 + createAuthorizationUrl, 11 + } from "@atcute/oauth-browser-client"; 12 + import { 13 + CompositeDidDocumentResolver, 14 + LocalActorResolver, 15 + PlcDidDocumentResolver, 16 + WebDidDocumentResolver, 17 + XrpcHandleResolver, 18 + } from "@atcute/identity-resolver"; 19 + import metadata from "../../oauth-client-metadata.json" with { type: "json" }; 20 + import { getAuth } from "../../lib/auth"; 21 + 22 + configureOAuth({ 23 + metadata: import.meta.env.PROD 24 + ? { 25 + client_id: metadata.client_id, 26 + redirect_uri: metadata.redirect_uris[0], 27 + } 28 + : { 29 + client_id: `http://localhost?${new URLSearchParams({ 30 + redirect_uri: "http://127.0.0.1/atproto/callback", 31 + scope: "atproto transition:generic", 32 + }).toString()}`, 33 + redirect_uri: "http://127.0.0.1/atproto/callback", 34 + }, 35 + identityResolver: new LocalActorResolver({ 36 + handleResolver: new XrpcHandleResolver({ 37 + serviceUrl: "https://public.api.bsky.app", 38 + }), 39 + didDocumentResolver: new CompositeDidDocumentResolver({ 40 + methods: { 41 + plc: new PlcDidDocumentResolver(), 42 + web: new WebDidDocumentResolver(), 43 + }, 44 + }), 45 + }), 46 + }); 47 + 48 + // if theres a did in storage and it restores, go to / 49 + if (localStorage.getItem("did") && (await getAuth().then((x) => !!x[0]))) 50 + window.location.assign("/"); 51 + 52 + const submit = document.getElementById( 53 + "submit" 54 + ) as HTMLButtonElement | null; 55 + if (!submit) throw null; 56 + submit.addEventListener("click", async (ev) => { 57 + ev.preventDefault(); 58 + const unameEl = document.getElementById("username"); 59 + if (!(unameEl instanceof HTMLInputElement)) return; 60 + if (!unameEl.validity.valid) { 61 + unameEl.reportValidity(); 62 + return; 63 + } 64 + const handle = unameEl.value; 65 + submit.disabled = true; 66 + 67 + const authUrl = await createAuthorizationUrl({ 68 + target: { 69 + type: "account", 70 + identifier: handle as `${string}.${string}`, 71 + }, 72 + scope: "atproto transition:generic", 73 + }); 74 + 75 + await new Promise((res) => setTimeout(res, 200)); // let browser persist local storage 76 + window.location.assign(authUrl); 77 + }); 78 + </script> 79 + <style> 80 + :global(html, body) { 81 + height: 100%; 82 + margin: 0; 83 + } 84 + 85 + form { 86 + height: calc(100% - 20px); 87 + width: calc(100% - 20px); 88 + max-width: 40ch; 89 + padding: 10px; 90 + margin: auto; 91 + 92 + display: flex; 93 + flex-direction: row; 94 + align-items: center; 95 + justify-content: center; 96 + gap: 10px; 97 + } 98 + 99 + actor-typeahead { 100 + --color-background: #2b2b2b; 101 + --color-border: #ffffff22; 102 + --color-shadow: #ffffff; 103 + --color-hover: #ffffff11; 104 + flex: 1; 105 + } 106 + 107 + input { 108 + padding: 2px; 109 + border: 2px inset ButtonBorder; 110 + border-radius: 5px; 111 + width: calc(100% - 4px - 2.5px); 112 + } 113 + </style> 114 + 115 + <form> 116 + <actor-typeahead> 117 + <input id="username" required autocomplete="off" /> 118 + </actor-typeahead> 119 + <button id="submit">Sign in</button> 120 + </form> 121 + </Base>
+51
src/pages/atproto/logout.astro
··· 1 + --- 2 + import Base from "../../Base.astro"; 3 + --- 4 + 5 + <Base title="Logging out"> 6 + <script> 7 + import { getAuth } from "../../lib/auth"; 8 + const [_, agent, __] = await getAuth(); 9 + try { 10 + await agent?.signOut(); 11 + localStorage.removeItem("did"); 12 + window.location.assign("/"); 13 + } catch (e) { 14 + const h1 = document.querySelector("h1"); 15 + if (!h1) throw "no <h1>"; 16 + console.error(e); 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 + } 19 + </script> 20 + <style> 21 + @keyframes --blink { 22 + from, 23 + to { 24 + opacity: 0; 25 + } 26 + 27 + 50% { 28 + opacity: 1; 29 + } 30 + } 31 + 32 + .blink { 33 + animation: 2s linear var(--offset) infinite forwards --blink; 34 + } 35 + 36 + :global(body, html) { 37 + margin: 0; 38 + height: 100%; 39 + width: 100%; 40 + display: flex; 41 + align-items: center; 42 + justify-content: center; 43 + } 44 + </style> 45 + <h1> 46 + Logging out <span class="blink" style="--offset: -0.2s">.</span><span 47 + class="blink" 48 + style="--offset: -0.1s">.</span 49 + ><span class="blink" style="--offset: 0s">.</span> 50 + </h1> 51 + </Base>
+19 -12
src/pages/index.astro
··· 1 1 --- 2 - 2 + import Base from "../Base.astro"; 3 3 --- 4 4 5 - <html lang="en"> 6 - <head> 7 - <meta charset="utf-8" /> 8 - <meta name="viewport" content="width=device-width" /> 9 - <meta name="generator" content={Astro.generator} /> 10 - <title>Astro</title> 11 - </head> 12 - <body> 13 - <h1>Astro</h1> 14 - </body> 15 - </html> 5 + <Base title="D&D Utils"> 6 + <script> 7 + import { getAuth } from "../lib/auth"; 8 + const [client, agent, did] = await getAuth(); 9 + const login = document.getElementById("login"); 10 + if (!login) throw "no #login"; 11 + if (did) 12 + login.outerHTML = `<a id="login" href="/atproto/logout">Logout</a>`; 13 + else login.outerHTML = `<a id="login" href="/atproto/login">Login</a>`; 14 + </script> 15 + <h1>D&D Utils</h1> 16 + <a id="login" href="#" style="display: none;"></a> 17 + <p>Pages:</p> 18 + <ul> 19 + <li><a href="/astral">Astral Powers</a></li> 20 + <li><a href="/chip">Chip</a></li> 21 + </ul> 22 + </Base>
+7
src/pages/oauth-client-metadata.json.ts
··· 1 + import metadata from "../oauth-client-metadata.json" with { type: "json" }; 2 + 3 + export function GET(): Response { 4 + return new Response(JSON.stringify(metadata), { 5 + headers: new Headers({ "content-type": "application/json" }), 6 + }); 7 + }