Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
6.8 kB · 232 lines
TSX
at mistress
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233import { oauthClient } from "#/lib/atproto";import { Agent } from "@atproto/api";import { TokenRefreshError } from "@atproto/oauth-client-expo";import { Buffer } from "@craftzdog/react-native-buffer";import { deleteItemAsync, getItemAsync, setItemAsync } from "expo-secure-store";import { createContext, ReactNode, useCallback, useContext, useEffect, useState } from "react";import { NitroSecret, SecretSize } from "react-native-nitro-totp";
const DID_KEY = "club.touchfluffytail.did";const PRIVATE_KEY = "club.touchfluffytail.privkey";const PUBKEY_TID_KEY = "club.touchfluffytail.pubkey_tid";const SEED_KEY = "club.touchfluffytail.otp_seed";
const nitroSecret = new NitroSecret();
export type AuthRequiredReason = "no-session" | "unavailable";export class AuthRequiredError extends Error { readonly reason: AuthRequiredReason;
constructor(reason: AuthRequiredReason, message: string, options?: ErrorOptions) { super(message, options); this.name = "AuthRequiredError"; this.reason = reason; }}export type AuthStatus = "loading" | "anonymous" | "saved" | "authenticated";
type AuthContextType = { agent: Agent | null; did: string | null; status: AuthStatus; isLoading: boolean; isAuthed: boolean; hasSavedDid: boolean; seed: string | null; privateKey: Uint8Array<ArrayBufferLike> | null; pubkeyTid: string | null; signIn: (handleOrDid: string, privateKey: Uint8Array<ArrayBufferLike>) => Promise<Agent>; savePubkeyTid: (tid: string) => Promise<void>; signOut: () => Promise<void>; requireSession: () => Promise<Agent>;};
const AuthContext = createContext<AuthContextType | undefined>(undefined);
async function restoreAgent(sub: string): Promise<Agent> { const session = await oauthClient.restore(sub);
return new Agent(session);}
function toAuthRequiredError(error: unknown): AuthRequiredError { if (error instanceof TokenRefreshError && error.cause === undefined) { return new AuthRequiredError("no-session", "No usable session is stored on this device.", { cause: error, }); }
return new AuthRequiredError( "unavailable", error instanceof Error ? error.message : String(error), { cause: error }, );}
export function AuthProvider({ children }: { children: ReactNode }) { const [agent, setAgent] = useState<Agent | null>(null); const [did, setDid] = useState<string | null>(null); const [isLoading, setIsLoading] = useState(true); const [seed, setSeed] = useState<string | null>(null); const [privateKey, setPrivateKey] = useState<Uint8Array<ArrayBufferLike> | null>(null); const [pubkeyTid, setPubkeyTid] = useState<string | null>(null);
useEffect(() => { let active = true;
const loadSavedData = async () => { try { const savedDid = await getItemAsync(DID_KEY); if (active && savedDid) setDid(savedDid); if (active && savedDid) { const [privateKeyString, savedTid] = await Promise.all([ getItemAsync(PRIVATE_KEY), getItemAsync(PUBKEY_TID_KEY), ]);
if (active && savedTid) setPubkeyTid(savedTid);
if (!privateKeyString) { throw new Error("Unable to find privatekey"); } const pkey = new Uint8Array(Buffer.from(privateKeyString, "hex")); setPrivateKey(pkey); } } catch (error) { console.error("Failed to read the saved DID:", error); } finally { try { const savedSeed = await getItemAsync(SEED_KEY); const nextSeed = savedSeed ?? nitroSecret.generate({ size: SecretSize.EXTENDED });
if (!savedSeed) await setItemAsync(SEED_KEY, nextSeed);
if (active) setSeed(nextSeed); } catch (error) { console.error("Failed to initialize the OTP seed:", error); } }
if (active) setIsLoading(false); }; void loadSavedData();
return () => { active = false; }; }, []);
const forgetIdentity = useCallback(async () => { if (did) { try { await oauthClient.revoke(did); } catch (error) { console.warn("Failed to revoke the AT Protocol session:", error); } }
await deleteItemAsync(DID_KEY); await deleteItemAsync(PRIVATE_KEY); await deleteItemAsync(PUBKEY_TID_KEY); await deleteItemAsync(SEED_KEY); }, [did]);
const requireSession = useCallback(async (): Promise<Agent> => { if (agent) return agent;
if (!did) { throw new AuthRequiredError("no-session", "No account is saved on this device."); }
try { const nextAgent = await restoreAgent(did);
setAgent(nextAgent);
return nextAgent; } catch (error) { console.error("Failed to restore the AT Protocol session:", error);
throw toAuthRequiredError(error); } }, [agent, did]);
const signIn = useCallback( async (handleOrDid: string, privateKey: Uint8Array<ArrayBufferLike>): Promise<Agent> => { const session = await oauthClient.signIn(handleOrDid.trim()); console.log(`Logged in as ${session.did}`);
if (did && did !== session.did) await forgetIdentity();
await setItemAsync(DID_KEY, session.did); setDid(session.did);
const privateKeyString = Buffer.from(privateKey).toString("hex"); await setItemAsync(PRIVATE_KEY, privateKeyString); setPrivateKey(privateKey);
// The fresh key pair has no pubkey record yet, so any previously stored // TID would point at a key this device no longer holds. await deleteItemAsync(PUBKEY_TID_KEY); setPubkeyTid(null);
const nextAgent = new Agent(session); setAgent(nextAgent);
return nextAgent; }, [did, forgetIdentity], );
const savePubkeyTid = useCallback(async (tid: string) => { await setItemAsync(PUBKEY_TID_KEY, tid); setPubkeyTid(tid); }, []);
const signOut = useCallback(async () => { await forgetIdentity(); setAgent(null); setDid(null); setPrivateKey(null); setPubkeyTid(null); }, [forgetIdentity]);
const status: AuthStatus = isLoading ? "loading" : agent ? "authenticated" : did ? "saved" : "anonymous";
return ( <AuthContext.Provider value={{ agent, did, status, isLoading, isAuthed: agent !== null, hasSavedDid: did !== null, seed, privateKey, pubkeyTid, signIn, savePubkeyTid, signOut, requireSession, }}> {children} </AuthContext.Provider> );}
export function useAuth() { const context = useContext(AuthContext);
if (!context) { throw new Error("useAuth must be used inside AuthProvider"); }
return context;}