this repo has no description
1const OAUTH_STATE_KEY = 'tranquil_pds_oauth_state' 2const OAUTH_VERIFIER_KEY = 'tranquil_pds_oauth_verifier' 3 4interface OAuthState { 5 state: string 6 codeVerifier: string 7 returnTo?: string 8} 9 10function generateRandomString(length: number): string { 11 const array = new Uint8Array(length) 12 crypto.getRandomValues(array) 13 return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join('') 14} 15 16async function sha256(plain: string): Promise<ArrayBuffer> { 17 const encoder = new TextEncoder() 18 const data = encoder.encode(plain) 19 return crypto.subtle.digest('SHA-256', data) 20} 21 22function base64UrlEncode(buffer: ArrayBuffer): string { 23 const bytes = new Uint8Array(buffer) 24 let binary = '' 25 for (const byte of bytes) { 26 binary += String.fromCharCode(byte) 27 } 28 return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 29} 30 31async function generateCodeChallenge(verifier: string): Promise<string> { 32 const hash = await sha256(verifier) 33 return base64UrlEncode(hash) 34} 35 36function generateState(): string { 37 return generateRandomString(32) 38} 39 40function generateCodeVerifier(): string { 41 return generateRandomString(32) 42} 43 44function saveOAuthState(state: OAuthState): void { 45 sessionStorage.setItem(OAUTH_STATE_KEY, state.state) 46 sessionStorage.setItem(OAUTH_VERIFIER_KEY, state.codeVerifier) 47} 48 49function getOAuthState(): OAuthState | null { 50 const state = sessionStorage.getItem(OAUTH_STATE_KEY) 51 const codeVerifier = sessionStorage.getItem(OAUTH_VERIFIER_KEY) 52 if (!state || !codeVerifier) return null 53 return { state, codeVerifier } 54} 55 56function clearOAuthState(): void { 57 sessionStorage.removeItem(OAUTH_STATE_KEY) 58 sessionStorage.removeItem(OAUTH_VERIFIER_KEY) 59} 60 61export async function startOAuthLogin(): Promise<void> { 62 const state = generateState() 63 const codeVerifier = generateCodeVerifier() 64 const codeChallenge = await generateCodeChallenge(codeVerifier) 65 66 saveOAuthState({ state, codeVerifier }) 67 68 const clientId = `${window.location.origin}/oauth/client-metadata.json` 69 const redirectUri = `${window.location.origin}/` 70 71 const parResponse = await fetch('/oauth/par', { 72 method: 'POST', 73 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 74 body: new URLSearchParams({ 75 client_id: clientId, 76 redirect_uri: redirectUri, 77 response_type: 'code', 78 scope: [ 79 'atproto', 80 'repo:*?action=create', 81 'repo:*?action=update', 82 'repo:*?action=delete', 83 'blob:*/*', 84 ].join(' '), 85 state: state, 86 code_challenge: codeChallenge, 87 code_challenge_method: 'S256', 88 }), 89 }) 90 91 if (!parResponse.ok) { 92 const error = await parResponse.json().catch(() => ({ error: 'Unknown error' })) 93 throw new Error(error.error_description || error.error || 'Failed to start OAuth flow') 94 } 95 96 const { request_uri } = await parResponse.json() 97 98 const authorizeUrl = new URL('/oauth/authorize', window.location.origin) 99 authorizeUrl.searchParams.set('client_id', clientId) 100 authorizeUrl.searchParams.set('request_uri', request_uri) 101 102 window.location.href = authorizeUrl.toString() 103} 104 105export interface OAuthTokens { 106 access_token: string 107 refresh_token?: string 108 token_type: string 109 expires_in?: number 110 scope?: string 111 sub: string 112} 113 114export async function handleOAuthCallback(code: string, state: string): Promise<OAuthTokens> { 115 const savedState = getOAuthState() 116 if (!savedState) { 117 throw new Error('No OAuth state found. Please try logging in again.') 118 } 119 120 if (savedState.state !== state) { 121 clearOAuthState() 122 throw new Error('OAuth state mismatch. Please try logging in again.') 123 } 124 125 const clientId = `${window.location.origin}/oauth/client-metadata.json` 126 const redirectUri = `${window.location.origin}/` 127 128 const tokenResponse = await fetch('/oauth/token', { 129 method: 'POST', 130 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 131 body: new URLSearchParams({ 132 grant_type: 'authorization_code', 133 client_id: clientId, 134 code: code, 135 redirect_uri: redirectUri, 136 code_verifier: savedState.codeVerifier, 137 }), 138 }) 139 140 clearOAuthState() 141 142 if (!tokenResponse.ok) { 143 const error = await tokenResponse.json().catch(() => ({ error: 'Unknown error' })) 144 throw new Error(error.error_description || error.error || 'Failed to exchange code for tokens') 145 } 146 147 return tokenResponse.json() 148} 149 150export async function refreshOAuthToken(refreshToken: string): Promise<OAuthTokens> { 151 const clientId = `${window.location.origin}/oauth/client-metadata.json` 152 153 const tokenResponse = await fetch('/oauth/token', { 154 method: 'POST', 155 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 156 body: new URLSearchParams({ 157 grant_type: 'refresh_token', 158 client_id: clientId, 159 refresh_token: refreshToken, 160 }), 161 }) 162 163 if (!tokenResponse.ok) { 164 const error = await tokenResponse.json().catch(() => ({ error: 'Unknown error' })) 165 throw new Error(error.error_description || error.error || 'Failed to refresh token') 166 } 167 168 return tokenResponse.json() 169} 170 171export function checkForOAuthCallback(): { code: string; state: string } | null { 172 const params = new URLSearchParams(window.location.search) 173 const code = params.get('code') 174 const state = params.get('state') 175 176 if (code && state) { 177 return { code, state } 178 } 179 180 return null 181} 182 183export function clearOAuthCallbackParams(): void { 184 const url = new URL(window.location.href) 185 url.search = '' 186 window.history.replaceState({}, '', url.toString()) 187}