/** * Call a JSON-in JSON-out API endpoint * Data is automatically serialized * @param {string} endpoint - The API endpoint to call * @param {any} data - The data to send to the API * @returns {Promise} The JSON response from the API */ export async function callJsonApi(endpoint, data) { const response = await fetchApi(endpoint, { method: "POST", headers: { "Content-Type": "application/json", }, credentials: "same-origin", body: JSON.stringify(data), }); if (!response.ok) { const error = await response.text(); throw new Error(error); } const jsonResponse = await response.json(); return jsonResponse; } /** * Fetch wrapper for A0 APIs that ensures token exchange * Automatically adds CSRF token to request headers * @param {string} url - The URL to fetch * @param {Object} [request] - The fetch request options * @returns {Promise} The fetch response */ export async function fetchApi(url, request) { async function _wrap(canRetry, forceFreshToken) { // get the CSRF token const token = await getCsrfToken(forceFreshToken); // create a new request object if none was provided const finalRequest = request || {}; // ensure headers object exists finalRequest.headers = finalRequest.headers || {}; // add the CSRF token to the headers finalRequest.headers["X-CSRF-Token"] = token; // perform the fetch with the updated request const response = await fetch(url, finalRequest); // check if there was an CSRF error if (response.status === 403 && canRetry) { // retry the request with a freshly-fetched token. forceFreshToken=true is required here: // getCsrfToken() would otherwise re-read the tag, which is baked into the page at // load time and never updates -- so a stale meta-tag token (e.g. after the server // restarted and rotated its CSRF secret) would fail the exact same way forever, since // this "retry" kept re-reading the identical stale value instead of ever hitting // /csrf_token for a token signed by the server's current secret. csrfToken = null; return await _wrap(false, true); } // return the response return response; } // perform the request const response = await _wrap(true, false); // return the response return response; } // csrf token stored locally let csrfToken = null; /** * Get the CSRF token for API requests * Caches the token after first request * @param {boolean} [forceRefresh] - Skip the cache and the page's tag, always fetch a * live token from /csrf_token. Use this when retrying after a 403. * @returns {Promise} The CSRF token */ async function getCsrfToken(forceRefresh = false) { if (!forceRefresh && csrfToken) return csrfToken; if (!forceRefresh) { const tokenElement = document.querySelector('meta[name="csrf-token"]'); if (tokenElement) { csrfToken = tokenElement.content; return csrfToken; } } // live fetch: reached on first load with no tag, or whenever a fresh token was // explicitly requested (forceRefresh) after the cached/meta-tag token was rejected const response = await fetch("/csrf_token", { credentials: "same-origin", }).then((r) => r.json()); csrfToken = response.token; document.cookie = `csrf_token_${response.runtime_id}=${csrfToken}; SameSite=Strict; Path=/`; return csrfToken; }