this repo has no description
1const OAUTH_STATE_KEY = 'bspds_oauth_state' 2const OAUTH_VERIFIER_KEY = 'bspds_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: 'atproto transition:generic', 79 state: state, 80 code_challenge: codeChallenge, 81 code_challenge_method: 'S256', 82 }), 83 }) 84 85 if (!parResponse.ok) { 86 const error = await parResponse.json().catch(() => ({ error: 'Unknown error' })) 87 throw new Error(error.error_description || error.error || 'Failed to start OAuth flow') 88 } 89 90 const { request_uri } = await parResponse.json() 91 92 const authorizeUrl = new URL('/oauth/authorize', window.location.origin) 93 authorizeUrl.searchParams.set('client_id', clientId) 94 authorizeUrl.searchParams.set('request_uri', request_uri) 95 96 window.location.href = authorizeUrl.toString() 97} 98 99export interface OAuthTokens { 100 access_token: string 101 refresh_token?: string 102 token_type: string 103 expires_in?: number 104 scope?: string 105 sub: string 106} 107 108export async function handleOAuthCallback(code: string, state: string): Promise<OAuthTokens> { 109 const savedState = getOAuthState() 110 if (!savedState) { 111 throw new Error('No OAuth state found. Please try logging in again.') 112 } 113 114 if (savedState.state !== state) { 115 clearOAuthState() 116 throw new Error('OAuth state mismatch. Please try logging in again.') 117 } 118 119 const clientId = `${window.location.origin}/oauth/client-metadata.json` 120 const redirectUri = `${window.location.origin}/` 121 122 const tokenResponse = await fetch('/oauth/token', { 123 method: 'POST', 124 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 125 body: new URLSearchParams({ 126 grant_type: 'authorization_code', 127 client_id: clientId, 128 code: code, 129 redirect_uri: redirectUri, 130 code_verifier: savedState.codeVerifier, 131 }), 132 }) 133 134 clearOAuthState() 135 136 if (!tokenResponse.ok) { 137 const error = await tokenResponse.json().catch(() => ({ error: 'Unknown error' })) 138 throw new Error(error.error_description || error.error || 'Failed to exchange code for tokens') 139 } 140 141 return tokenResponse.json() 142} 143 144export async function refreshOAuthToken(refreshToken: string): Promise<OAuthTokens> { 145 const clientId = `${window.location.origin}/oauth/client-metadata.json` 146 147 const tokenResponse = await fetch('/oauth/token', { 148 method: 'POST', 149 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 150 body: new URLSearchParams({ 151 grant_type: 'refresh_token', 152 client_id: clientId, 153 refresh_token: refreshToken, 154 }), 155 }) 156 157 if (!tokenResponse.ok) { 158 const error = await tokenResponse.json().catch(() => ({ error: 'Unknown error' })) 159 throw new Error(error.error_description || error.error || 'Failed to refresh token') 160 } 161 162 return tokenResponse.json() 163} 164 165export function checkForOAuthCallback(): { code: string; state: string } | null { 166 const params = new URLSearchParams(window.location.search) 167 const code = params.get('code') 168 const state = params.get('state') 169 170 if (code && state) { 171 return { code, state } 172 } 173 174 return null 175} 176 177export function clearOAuthCallbackParams(): void { 178 const url = new URL(window.location.href) 179 url.search = '' 180 window.history.replaceState({}, '', url.toString()) 181}