import * as Crypto from 'expo-crypto'; import * as WebBrowser from 'expo-web-browser'; import { makeRedirectUri } from 'expo-auth-session'; import { setSetting } from '@/db/queries'; import { type SQLiteDatabase } from 'expo-sqlite'; const ISSUER = 'https://bincio.org'; const CLIENT_ID = 'bincio-autarchive'; const REDIRECT_URI = makeRedirectUri({ scheme: 'bincio', path: 'oauth' }); async function generateVerifier(): Promise { const bytes = await Crypto.getRandomBytesAsync(32); return btoa(String.fromCharCode(...bytes)) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); } async function deriveChallenge(verifier: string): Promise { const digest = await Crypto.digestStringAsync( Crypto.CryptoDigestAlgorithm.SHA256, verifier, { encoding: Crypto.CryptoEncoding.BASE64 }, ); return digest.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); } export interface LoginResult { ok: boolean; displayName?: string; error?: string; } export async function login(db: SQLiteDatabase): Promise { const verifier = await generateVerifier(); const challenge = await deriveChallenge(verifier); const authUrl = `${ISSUER}/oauth2/authorize?` + new URLSearchParams({ client_id: CLIENT_ID, redirect_uri: REDIRECT_URI, response_type: 'code', scope: 'openid profile', code_challenge: challenge, code_challenge_method: 'S256', }); const result = await WebBrowser.openAuthSessionAsync(authUrl, REDIRECT_URI); if (result.type !== 'success') { return { ok: false, error: result.type === 'cancel' ? 'Cancelled' : 'Browser error' }; } const code = new URL(result.url).searchParams.get('code'); if (!code) return { ok: false, error: 'No code in redirect' }; const tokenResp = await fetch(`${ISSUER}/oauth2/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: REDIRECT_URI, client_id: CLIENT_ID, code_verifier: verifier, }).toString(), }); if (!tokenResp.ok) { const text = await tokenResp.text().catch(() => ''); return { ok: false, error: `Token exchange failed: ${text}` }; } const tokenData = await tokenResp.json(); const idToken: string = tokenData.id_token; if (!idToken) return { ok: false, error: 'No id_token in response' }; const payload = JSON.parse(atob(idToken.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))); await Promise.all([ setSetting(db, 'instance_url', ISSUER), setSetting(db, 'handle', payload.sub ?? ''), setSetting(db, 'api_token', idToken), ]); return { ok: true, displayName: payload.name ?? payload.preferred_username }; } export async function logout(db: SQLiteDatabase): Promise { await Promise.all([ setSetting(db, 'instance_url', ''), setSetting(db, 'handle', ''), setSetting(db, 'api_token', ''), ]); }