1import { BASE_URL } from './config.js'; 2import { state } from './state.js'; 3 4async function makeRequest(endpoint, options = {}) { 5 const url = `${BASE_URL}${endpoint}`; 6 const config = { 7 headers: { 8 'Content-Type': 'application/json', 9 ...options.headers 10 }, 11 ...options 12 }; 13 14 if (state.accessToken) { 15 config.headers['Authorization'] = `Bearer ${state.accessToken}`; 16 } 17 18 try { 19 const response = await fetch(url, config); 20 21 if (!response.ok) { 22 let detail = response.statusText; 23 try { 24 const errorBody = await response.json(); 25 detail = errorBody.detail || detail; 26 } catch (_) { 27 // Keep the HTTP status text when the response is not JSON. 28 } 29 throw new Error(detail || `HTTP ${response.status}`); 30 } 31 32 const contentType = response.headers.get('content-type'); 33 if (contentType && contentType.includes('application/json')) { 34 return await response.json(); 35 } else { 36 return await response.text(); 37 } 38 } catch (error) { 39 if (error.name === 'TypeError' && error.message.includes('fetch')) { 40 throw new Error('Network error: Please make sure you\'re running this app through a web server (not file://) to avoid CORS issues. Try running: python -m http.server 8000'); 41 } 42 43 throw error; 44 } 45} 46 47export async function checkAccountAuthorization() { 48 try { 49 const response = await makeRequest('/account'); 50 51 if (!response || typeof response !== 'object' || !response.email_address || !response.expires_at) { 52 return false; 53 } 54 55 const expiresAt = new Date(response.expires_at); 56 if (isNaN(expiresAt.getTime())) { 57 return false; 58 } 59 60 const isExpired = response.is_expired === true; 61 const daysUntilExpiry = response.days_until_expiry || 0; 62 const renewalEligible = response.renewal_eligible !== false; 63 64 // If expired and not eligible for renewal, reject 65 if (isExpired && !renewalEligible) { 66 return false; 67 } 68 69 state.accountInfo = { 70 email_address: response.email_address, 71 expires_at: response.expires_at, 72 expires_date: expiresAt, 73 is_expired: isExpired, 74 days_until_expiry: daysUntilExpiry, 75 renewal_eligible: renewalEligible, 76 free_send_available: response.free_send_available === true 77 }; 78 79 return true; 80 } catch (error) { 81 return false; 82 } 83} 84 85export async function fetchEmails() { 86 const response = await makeRequest('/emails'); 87 return response.emails || []; 88} 89 90export async function fetchEmailContent(emailId) { 91 return await makeRequest(`/emails/${emailId}`); 92} 93 94export async function markAllEmailsRead() { 95 return await makeRequest('/emails/read-all', { method: 'POST' }); 96} 97 98export async function fetchPasskeys() { 99 return await makeRequest('/passkeys'); 100} 101 102export async function beginPasskeyRegistration() { 103 return await makeRequest('/passkeys/registration/options', { method: 'POST' }); 104} 105 106export async function finishPasskeyRegistration(ceremonyId, credential, name = 'Passkey') { 107 return await makeRequest('/passkeys/registration', { 108 method: 'POST', 109 body: JSON.stringify({ ceremony_id: ceremonyId, credential, name }) 110 }); 111} 112 113export async function beginPasskeyAuthentication() { 114 return await makeRequest('/passkeys/authentication/options', { method: 'POST' }); 115} 116 117export async function finishPasskeyAuthentication(ceremonyId, credential) { 118 return await makeRequest('/passkeys/authentication', { 119 method: 'POST', 120 body: JSON.stringify({ ceremony_id: ceremonyId, credential }) 121 }); 122} 123 124export async function removePasskey(credentialId) { 125 return await makeRequest(`/passkeys/${encodeURIComponent(credentialId)}`, { 126 method: 'DELETE' 127 }); 128} 129 130export async function sendEmail(recipient, subject, body, inReplyTo = null, references = null, attachments = []) { 131 const payload = { recipient, subject, body }; 132 if (inReplyTo) payload.in_reply_to = inReplyTo; 133 if (references) payload.references = references; 134 if (attachments.length > 0) payload.attachments = attachments; 135
136 return await makeRequest('/email/send', { 137 method: 'POST', 138 body: JSON.stringify(payload) 139 }); 140} 141 142export async function checkPaymentStatus(paymentHash) { 143 return await makeRequest(`/email/send/status/${paymentHash}`, { 144 method: 'GET' 145 }); 146} 147 148export async function checkApiHealth() { 149 try { 150 const controller = new AbortController(); 151 const timeoutId = setTimeout(() => controller.abort(), 10000); 152 const response = await makeRequest('/health', { signal: controller.signal }); 153 clearTimeout(timeoutId); 154 return { 155 success: true, 156 data: response 157 }; 158 } catch (error) { 159 return { 160 success: false, 161 error: error.name === 'AbortError' ? 'Health check timed out after 10 seconds' : error.message 162 }; 163 } 164} 165 166export async function deleteEmails(emailIds) { 167 try { 168 const response = await makeRequest('/emails', { 169 method: 'DELETE', 170 body: JSON.stringify({ email_ids: emailIds }) 171 }); 172 return { 173 success: true, 174 data: response 175 }; 176 } catch (error) { 177 return { 178 success: false, 179 error: error.message 180 }; 181 } 182} 183 184export async function createEmailAccount(options = {}) { 185 return await makeRequest('/email', { 186 method: 'POST', 187 body: JSON.stringify(options) 188 }); 189} 190 191export async function checkAccountPaymentStatus(paymentHash) { 192 return await makeRequest(`/payment/${paymentHash}`, { 193 method: 'GET' 194 }); 195} 196 197export async function fetchRecentSends() { 198 return await makeRequest('/email/sends/recent'); 199} 200 201export async function renewAccount(years = 1) { 202 return await makeRequest('/account/renew', { 203 method: 'POST', 204 body: JSON.stringify({ years }) 205 }); 206} 207 208export async function checkRenewalStatus(paymentHash) { 209 return await makeRequest(`/account/renew/status/${paymentHash}`, { 210 method: 'GET' 211 }); 212} 213 214// Re-issue an invoice from a different payment provider (when configured). 215// excludeProvider is the provider that issued the current, unpayable invoice. 216export async function newAccountInvoice(paymentHash, excludeProvider = null) { 217 return await makeRequest(`/email/${paymentHash}/new-invoice`, { 218 method: 'POST', 219 body: JSON.stringify({ exclude_provider: excludeProvider }) 220 }); 221} 222 223export async function newSendInvoice(paymentHash, excludeProvider = null) { 224 return await makeRequest(`/email/send/${paymentHash}/new-invoice`, { 225 method: 'POST', 226 body: JSON.stringify({ exclude_provider: excludeProvider }) 227 }); 228} 229 230export async function newRenewalInvoice(paymentHash, excludeProvider = null, years = 1) { 231 return await makeRequest(`/account/renew/${paymentHash}/new-invoice`, { 232 method: 'POST', 233 body: JSON.stringify({ exclude_provider: excludeProvider, years }) 234 }); 235}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.