Your music, beautifully tracked. All yours. (coming soon) teal.fm
teal-fm atproto

log in via non-bsky PDSes and fix did:web login

+94 -75
+5 -2
apps/amethyst/app/auth/signup.tsx
··· 4 4 import { Text } from "@/components/ui/text"; 5 5 import { Button } from "@/components/ui/button"; 6 6 import { Icon } from "@/lib/icons/iconWithClassName"; 7 - import { ArrowRight } from "lucide-react-native"; 7 + import { ArrowRight, Info } from "lucide-react-native"; 8 8 9 9 import { Stack, router } from "expo-router"; 10 10 import { FontAwesome6 } from "@expo/vector-icons"; ··· 34 34 <Text className="text-foreground text-lg"> 35 35 No account? That's fine. 36 36 </Text> 37 - <Text className="text-foreground mb-4 text-center text-lg"> 37 + <Text className="text-foreground text-center text-lg"> 38 38 Sign up for Bluesky, then return here to sign in. 39 + </Text> 40 + <Text className="text-muted-foreground mt-2 mb-4 text-center text-xs"> 41 + You'll need a PDS to use teal.fm. Bluesky is a good way to get one. 39 42 </Text> 40 43 {/* on click, open tab, then in the background navigate to /login */} 41 44 <Button
+3 -2
apps/amethyst/lib/atp/oauth.tsx
··· 12 12 >; 13 13 14 14 export default function createOAuthClient( 15 - baseUrl: string 15 + baseUrl: string, 16 + pdsBaseUrl: string, 16 17 ): AquareumOAuthClient { 17 18 if (!baseUrl) { 18 19 throw new Error("baseUrl is required"); ··· 60 61 }; 61 62 clientMetadataSchema.parse(meta); 62 63 return new ReactNativeOAuthClient({ 63 - handleResolver: "https://bsky.social", // backend instances should use a DNS based resolver 64 + handleResolver: "https://" + pdsBaseUrl, // backend instances should use a DNS based resolver 64 65 responseMode: "query", // or "fragment" (frontend only) or "form_post" (backend only) 65 66 66 67 // These must be the same metadata as the one exposed on the
+70 -62
apps/amethyst/lib/atp/pid.ts
··· 5 5 6 6 // resolve pid 7 7 export const isDid = (did: string) => { 8 - // is this a did? regex 9 - return did.match(/^did:[a-z]+:[\S\s]+/) 10 - } 8 + // is this a did? regex 9 + return did.match(/^did:[a-z]+:[\S\s]+/); 10 + }; 11 11 12 - export const resolveHandle = async (handle: string, resolverAppViewUrl: string = "https://public.api.bsky.app"): Promise<string> => { 13 - const url = resolverAppViewUrl + `/xrpc/com.atproto.identity.resolveHandle` + `?handle=${handle}`; 12 + export const resolveHandle = async ( 13 + handle: string, 14 + resolverAppViewUrl: string = "https://public.api.bsky.app", 15 + ): Promise<string> => { 16 + const url = 17 + resolverAppViewUrl + 18 + `/xrpc/com.atproto.identity.resolveHandle` + 19 + `?handle=${handle}`; 14 20 15 - const response = await fetch(url); 16 - if (response.status === 400) { 17 - throw new Error(`domain handle not found`); 18 - } else if (!response.ok) { 19 - throw new Error(`directory is unreachable`); 20 - } 21 + const response = await fetch(url); 22 + if (response.status === 400) { 23 + throw new Error(`domain handle not found`); 24 + } else if (!response.ok) { 25 + throw new Error(`directory is unreachable`); 26 + } 21 27 22 - const json = (await response.json()) 23 - return json.did; 28 + const json = await response.json(); 29 + return json.did; 24 30 }; 25 31 26 - 27 - 28 32 export const getDidDocument = async (did: string) => { 29 - const colon_index = did.indexOf(':', 4); 33 + const colon_index = did.indexOf(":", 4); 30 34 31 - const type = did.slice(4, colon_index); 32 - const ident = did.slice(colon_index + 1); 35 + const type = did.slice(4, colon_index); 36 + const ident = did.slice(colon_index + 1); 33 37 34 - // get a did:plc 35 - if (type === 'plc') { 36 - const res = await fetch("https://plc.directory/" + did) 38 + // get a did:plc 39 + if (type === "plc") { 40 + const res = await fetch("https://plc.directory/" + did); 37 41 38 - if (res.status === 400) { 39 - throw new Error(`domain handle not found`); 40 - } else if (!res.ok) { 41 - throw new Error(`directory is unreachable`); 42 - } 43 - 44 - const json = (await res.json()) 45 - return json; 46 - } else if (type === "web") { 47 - if (ident.match(/^([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*(?:\.[a-zA-Z]{2,}))$/)) { 48 - throw new Error(`invalid domain handle`); 49 - } 50 - const res = await fetch(`https://${ident}/.well-known/did.json`); 42 + if (res.status === 400) { 43 + throw new Error(`domain handle not found`); 44 + } else if (!res.ok) { 45 + throw new Error(`directory is unreachable`); 46 + } 51 47 52 - if (res.status === 400) { 53 - throw new Error(`domain handle not found`); 54 - } 55 - else if (!res.ok) { 56 - throw new Error(`directory is unreachable`); 57 - } 48 + const json = await res.json(); 49 + return json; 50 + } else if (type === "web") { 51 + if ( 52 + !ident.match(/^([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*(?:\.[a-zA-Z]{2,}))$/) 53 + ) { 54 + throw new Error(`invalid domain handle`); 55 + } 56 + const res = await fetch(`https://${ident}/.well-known/did.json`); 58 57 59 - const json = await res.json(); 60 - return json; 58 + if (res.status === 400) { 59 + throw new Error(`domain handle not found`); 60 + } else if (!res.ok) { 61 + throw new Error(`directory is unreachable`); 61 62 } 62 - 63 - 63 + 64 + const json = await res.json(); 65 + return json; 66 + } 64 67 }; 65 68 66 - export const resolveFromIdentity = async(identity: string, resolverAppViewUrl: string = "https://public.api.bsky.app") => { 67 - let did: string 68 - // is this a did? regex 69 - if (isDid(identity)) { 70 - did = identity 71 - } else { 72 - did = await resolveHandle(identity, resolverAppViewUrl) 73 - } 69 + export const resolveFromIdentity = async ( 70 + identity: string, 71 + resolverAppViewUrl: string = "https://public.api.bsky.app", 72 + ) => { 73 + let did: string; 74 + // is this a did? regex 75 + if (isDid(identity)) { 76 + did = identity; 77 + } else { 78 + did = await resolveHandle(identity, resolverAppViewUrl); 79 + } 74 80 75 - let doc = await getDidDocument(did) 76 - let pds = getPdsEndpoint(doc) 81 + let doc = await getDidDocument(did); 82 + let pds = getPdsEndpoint(doc); 77 83 78 - if (!pds) { 79 - throw new Error("account doesn't have PDS endpoint?") 80 - } 84 + if (!pds) { 85 + throw new Error("account doesn't have PDS endpoint?"); 86 + } 81 87 82 - return { 83 - did,doc,identity, 84 - pds: new URL(pds), 85 - } 86 - } 88 + return { 89 + did, 90 + doc, 91 + identity, 92 + pds: new URL(pds), 93 + }; 94 + };
+16 -9
apps/amethyst/stores/authenticationSlice.tsx
··· 1 - import create from "zustand"; 2 1 import { StateCreator } from "./mainStore"; 3 2 import createOAuthClient, { AquareumOAuthClient } from "../lib/atp/oauth"; 4 3 import { OAuthSession } from "@atproto/oauth-client"; 5 4 import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 6 5 import { Agent } from "@atproto/api"; 7 6 import * as Lexicons from "@teal/lexicons/src/lexicons"; 7 + import { resolveFromIdentity } from "@/lib/atp/pid"; 8 8 9 9 export interface AuthenticationSlice { 10 10 auth: AquareumOAuthClient; ··· 19 19 loading: boolean; 20 20 error: null | string; 21 21 }; 22 - pds: { 22 + pds: null | { 23 23 url: string; 24 24 loading: boolean; 25 25 error: null | string; ··· 37 37 // check if we have CF_PAGES_URL set. if not, use localhost 38 38 const baseUrl = process.env.EXPO_PUBLIC_BASE_URL || "http://localhost:8081"; 39 39 console.log("Using base URL:", baseUrl); 40 - const initialAuth = createOAuthClient(baseUrl); 40 + const initialAuth = createOAuthClient(baseUrl, "bsky.social"); 41 41 42 42 console.log("Auth client created!"); 43 43 ··· 54 54 loading: false, 55 55 error: null, 56 56 }, 57 - pds: { 58 - url: "bsky.social", 59 - loading: false, 60 - error: null, 61 - }, 57 + pds: null, 62 58 63 59 getLoginUrl: async (handle: string) => { 64 60 try { 65 - const url = await initialAuth.authorize(handle); 61 + // resolve the handle to a PDS URL 62 + const r = resolveFromIdentity(handle); 63 + let auth = createOAuthClient(baseUrl, (await r).pds.hostname); 64 + const url = await auth.authorize(handle); 65 + set({ 66 + auth, 67 + pds: { 68 + url: url.toString(), 69 + loading: false, 70 + error: null, 71 + }, 72 + }); 66 73 return url; 67 74 } catch (error) { 68 75 console.error("Failed to get login URL:", error);