Spaces:
Sleeping
Sleeping
| /** | |
| * 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<any>} 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<Response>} 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 <meta> 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 <meta> tag, always fetch a | |
| * live token from /csrf_token. Use this when retrying after a 403. | |
| * @returns {Promise<string>} 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 <meta> 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; | |
| } | |