this repo has no description
1/** 2 * OAuth flow helpers for e2e tests 3 */ 4 5import { randomBytes } from 'node:crypto'; 6import { DpopClient } from './dpop.js'; 7 8const BASE = 'http://localhost:8787'; 9 10/** 11 * Fetch with retry for flaky wrangler dev 12 * @param {string} url 13 * @param {RequestInit} options 14 * @param {number} maxAttempts 15 * @returns {Promise<Response>} 16 */ 17async function fetchWithRetry(url, options, maxAttempts = 3) { 18 let lastError; 19 for (let attempt = 0; attempt < maxAttempts; attempt++) { 20 try { 21 const res = await fetch(url, options); 22 // Check if we got an HTML error page instead of expected response 23 const contentType = res.headers.get('content-type') || ''; 24 if (!res.ok && contentType.includes('text/html')) { 25 // Wrangler dev error page - retry 26 if (attempt < maxAttempts - 1) { 27 await new Promise((r) => setTimeout(r, 100 * (attempt + 1))); 28 continue; 29 } 30 } 31 return res; 32 } catch (err) { 33 lastError = err; 34 if (attempt < maxAttempts - 1) { 35 await new Promise((r) => setTimeout(r, 100 * (attempt + 1))); 36 } 37 } 38 } 39 throw lastError || new Error('Fetch failed after retries'); 40} 41 42/** 43 * Get an OAuth token with a specific scope via full PAR -> authorize -> token flow 44 * @param {string} scope - The scope to request 45 * @param {string} did - The DID to authenticate as 46 * @param {string} password - The password for authentication 47 * @returns {Promise<{accessToken: string, refreshToken: string, dpop: DpopClient}>} 48 */ 49export async function getOAuthTokenWithScope(scope, did, password) { 50 const dpop = await DpopClient.create(); 51 const clientId = 'http://localhost:3000'; 52 const redirectUri = 'http://localhost:3000/callback'; 53 const codeVerifier = randomBytes(32).toString('base64url'); 54 const challengeBuffer = await crypto.subtle.digest( 55 'SHA-256', 56 new TextEncoder().encode(codeVerifier), 57 ); 58 const codeChallenge = Buffer.from(challengeBuffer).toString('base64url'); 59 60 // PAR request (with retry for flaky wrangler dev) 61 let parData; 62 for (let attempt = 0; attempt < 3; attempt++) { 63 // Generate fresh DPoP proof for each attempt 64 const parProof = await dpop.createProof('POST', `${BASE}/oauth/par`); 65 const parRes = await fetchWithRetry(`${BASE}/oauth/par`, { 66 method: 'POST', 67 headers: { 68 'Content-Type': 'application/x-www-form-urlencoded', 69 DPoP: parProof, 70 }, 71 body: new URLSearchParams({ 72 client_id: clientId, 73 redirect_uri: redirectUri, 74 response_type: 'code', 75 scope: scope, 76 code_challenge: codeChallenge, 77 code_challenge_method: 'S256', 78 login_hint: did, 79 }).toString(), 80 }); 81 if (parRes.ok) { 82 parData = await parRes.json(); 83 break; 84 } 85 if (attempt < 2) { 86 await new Promise((r) => setTimeout(r, 100 * (attempt + 1))); 87 } else { 88 const text = await parRes.text(); 89 throw new Error( 90 `PAR request failed: ${parRes.status} - ${text.slice(0, 100)}`, 91 ); 92 } 93 } 94 95 // Authorize (with retry) 96 let authCode; 97 for (let attempt = 0; attempt < 3; attempt++) { 98 const authRes = await fetchWithRetry(`${BASE}/oauth/authorize`, { 99 method: 'POST', 100 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 101 body: new URLSearchParams({ 102 request_uri: parData.request_uri, 103 client_id: clientId, 104 password: password, 105 }).toString(), 106 redirect: 'manual', 107 }); 108 const location = authRes.headers.get('location'); 109 if (location) { 110 authCode = new URL(location).searchParams.get('code'); 111 if (authCode) break; 112 } 113 if (attempt < 2) { 114 await new Promise((r) => setTimeout(r, 100 * (attempt + 1))); 115 } else { 116 throw new Error('Authorize request failed to return code'); 117 } 118 } 119 120 // Token exchange (with retry and fresh DPoP proof) 121 let tokenData; 122 for (let attempt = 0; attempt < 3; attempt++) { 123 const tokenProof = await dpop.createProof('POST', `${BASE}/oauth/token`); 124 const tokenRes = await fetchWithRetry(`${BASE}/oauth/token`, { 125 method: 'POST', 126 headers: { 127 'Content-Type': 'application/x-www-form-urlencoded', 128 DPoP: tokenProof, 129 }, 130 body: new URLSearchParams({ 131 grant_type: 'authorization_code', 132 code: authCode, 133 client_id: clientId, 134 redirect_uri: redirectUri, 135 code_verifier: codeVerifier, 136 }).toString(), 137 }); 138 if (tokenRes.ok) { 139 tokenData = await tokenRes.json(); 140 break; 141 } 142 if (attempt < 2) { 143 await new Promise((r) => setTimeout(r, 100 * (attempt + 1))); 144 } else { 145 const text = await tokenRes.text(); 146 throw new Error( 147 `Token request failed: ${tokenRes.status} - ${text.slice(0, 100)}`, 148 ); 149 } 150 } 151 152 return { 153 accessToken: tokenData.access_token, 154 refreshToken: tokenData.refresh_token, 155 dpop, 156 }; 157}