- Roll out platform-wide standard for unauthenticated binary access using '?key=[account_id]' query parameter. - Update API helpers (get, post, patch) to recognize 'key' bypass and strip account context headers accordingly. - Refactor IDAA Bulletin Board to restore inline image rendering and edit-mode previews. - Modernize Events Launcher (Layout, Sync, Session View) to use V3 Action URLs with verified auth. - Update HTML generators in 'ae_utils.ts' to support the new authenticated URL structure. - Harden 'ae_comp__event_file_obj_tbl' CSV export and clipboard links with V3 standard patterns.
351 lines
14 KiB
TypeScript
351 lines
14 KiB
TypeScript
import type { key_val } from '$lib/stores/ae_stores';
|
|
|
|
export let temp_get_blob_percent_completed = 0;
|
|
export const get_blob_percent_completed = temp_get_blob_percent_completed;
|
|
|
|
export const temp_get_object_percent_completed = 0;
|
|
export const get_object_percent_completed = temp_get_object_percent_completed;
|
|
|
|
export const get_object = async function get_object({
|
|
api_cfg = null,
|
|
endpoint = '',
|
|
headers = {},
|
|
params = {},
|
|
data = {},
|
|
timeout = 90000,
|
|
return_meta = false,
|
|
return_blob = false,
|
|
filename = '',
|
|
auto_download = false,
|
|
as_list = false, // Is this still really needed?
|
|
// The task_id value should be a random string that is unique to the task. This is used to identify the task in the message event.
|
|
task_id = crypto.randomUUID(),
|
|
log_lvl = 0,
|
|
retry_count = 5 // Number of retry attempts
|
|
}: {
|
|
api_cfg: any;
|
|
endpoint: string;
|
|
headers?: any;
|
|
params?: any;
|
|
data?: any;
|
|
timeout?: number;
|
|
return_meta?: boolean;
|
|
return_blob?: boolean;
|
|
filename?: null | string;
|
|
auto_download?: boolean;
|
|
as_list?: boolean;
|
|
task_id?: string;
|
|
log_lvl?: number;
|
|
retry_count?: number;
|
|
}) {
|
|
if (log_lvl) {
|
|
console.log(`*** get_object() *** Endpoint: ${endpoint} AE Task ID: ${task_id}`);
|
|
console.log('Params:', params);
|
|
if (log_lvl > 1) {
|
|
console.log('Data:', data);
|
|
}
|
|
}
|
|
|
|
if (!api_cfg) {
|
|
console.log('No API Config was provided. Returning false.');
|
|
return false;
|
|
}
|
|
|
|
// FAIL FAST: Check if we are explicitly offline to avoid long browser timeouts
|
|
if (typeof navigator !== 'undefined' && !navigator.onLine) {
|
|
if (log_lvl) console.log('get_object: Browser is offline. Failing fast to allow cache fallback.');
|
|
return false;
|
|
}
|
|
|
|
const url = new URL(endpoint, api_cfg['base_url']);
|
|
Object.keys(params).forEach((key) => url.searchParams.append(key, params[key]));
|
|
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
|
|
// Clean and merge headers without mutating the original api_cfg
|
|
const headers_cleaned: key_val = {};
|
|
const merged_headers = { ...api_cfg['headers'], ...headers };
|
|
|
|
// Auto-promote account_id from api_cfg to header if missing
|
|
if (!merged_headers['x-account-id'] && api_cfg['account_id']) {
|
|
merged_headers['x-account-id'] = api_cfg['account_id'];
|
|
}
|
|
|
|
// Handle "Bootstrap Paradox" for unauthenticated requests
|
|
const bypass_val = merged_headers['x-no-account-id'] || merged_headers['x_no_account_id'];
|
|
const is_valid_bypass = bypass_val === 'bypass' ||
|
|
bypass_val === 'Nothing to See Here' ||
|
|
params['key'] ||
|
|
bypass_val === 'direct-download';
|
|
|
|
if (is_valid_bypass) {
|
|
if (log_lvl > 1) console.log('api_get_object: Valid bypass detected. Stripping account ID context.');
|
|
delete merged_headers['x-account-id'];
|
|
delete merged_headers['x_account_id'];
|
|
} else {
|
|
// If it's a placeholder (like "No_Account_ID_Here"), just remove the bypass header
|
|
// but DO NOT strip the valid Account ID.
|
|
delete merged_headers['x-no-account-id'];
|
|
delete merged_headers['x_no_account_id'];
|
|
}
|
|
|
|
// Standardize all headers to kebab-case and ensure string values
|
|
for (const prop in merged_headers) {
|
|
const prop_cleaned = prop.replaceAll('_', '-');
|
|
let value = merged_headers[prop];
|
|
if (value === null || value === undefined) continue;
|
|
|
|
if (typeof value !== 'string') {
|
|
value = JSON.stringify(value);
|
|
}
|
|
headers_cleaned[prop_cleaned] = value;
|
|
}
|
|
|
|
// Auto-inject Authorization header if JWT is present but header is missing
|
|
let jwt = headers_cleaned['jwt'] ||
|
|
headers_cleaned['JWT'] ||
|
|
api_cfg['jwt'] ||
|
|
api_cfg['headers']?.['jwt'] ||
|
|
api_cfg['headers']?.['JWT'];
|
|
|
|
// Final Fallback: Direct check of primary ae_loc key
|
|
if (!jwt && typeof localStorage !== 'undefined') {
|
|
try {
|
|
const raw = localStorage.getItem('ae_loc');
|
|
if (raw) {
|
|
const json = JSON.parse(raw);
|
|
if (json.jwt) jwt = json.jwt;
|
|
}
|
|
} catch (e) {
|
|
// Silently fail on storage read
|
|
}
|
|
}
|
|
|
|
if (jwt && !headers_cleaned['Authorization'] && !headers_cleaned['authorization']) {
|
|
headers_cleaned['Authorization'] = `Bearer ${jwt}`;
|
|
}
|
|
|
|
if (log_lvl > 1) {
|
|
console.log('Final cleaned headers:', headers_cleaned);
|
|
}
|
|
|
|
const fetchOptions: RequestInit = {
|
|
method: 'GET',
|
|
headers: headers_cleaned,
|
|
signal: controller.signal
|
|
};
|
|
|
|
if (log_lvl > 1) {
|
|
console.log('Fetch options:', fetchOptions);
|
|
}
|
|
|
|
let fetch_method: any = fetch;
|
|
if (api_cfg.fetch) {
|
|
if (log_lvl > 1) {
|
|
console.log('Using custom fetch function from api_cfg!!!');
|
|
}
|
|
fetch_method = api_cfg.fetch;
|
|
}
|
|
|
|
for (let attempt = 1; attempt <= retry_count; attempt++) {
|
|
// FAIL FAST: Check if we are explicitly offline to avoid long browser timeouts
|
|
if (typeof navigator !== 'undefined' && !navigator.onLine) {
|
|
if (log_lvl) console.log(`get_object: Browser is offline (attempt ${attempt}). Failing fast to allow cache fallback.`);
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch_method(url.toString(), fetchOptions).catch(function (
|
|
error: any
|
|
) {
|
|
// SILENCE NOISE: Aborted requests (common in SWR/Background loads) shouldn't spam logs
|
|
if (error.name === 'AbortError' || error.message?.includes('aborted') || error.name === 'TypeError') {
|
|
if (log_lvl > 1) {
|
|
console.log('API GET: Request was aborted or terminated by browser. This is expected during navigation.', error);
|
|
}
|
|
return error; // Return error to be handled below
|
|
}
|
|
|
|
console.log(
|
|
'API GET Object *fetch* request failed in an unexpected way.',
|
|
error
|
|
);
|
|
return error;
|
|
});
|
|
clearTimeout(timeoutId);
|
|
|
|
// Check if we should stop due to abort or network failure
|
|
if (response instanceof Error || (response && (response.name === 'TypeError' || response.name === 'AbortError'))) {
|
|
// If it was an explicit abort, definitely stop
|
|
if (response.name === 'AbortError') return false;
|
|
|
|
if (log_lvl > 1) console.log('API GET Object: Detected NetworkError or TypeError. Failing fast.');
|
|
return false;
|
|
}
|
|
|
|
if (!response) {
|
|
if (log_lvl > 1) {
|
|
console.log(
|
|
'API GET Object: Something went wrong with *fetch* request. Returning false? Throwing an error!'
|
|
);
|
|
}
|
|
throw new Error(
|
|
`HTTP fetch request was aborted or failed in an unexpected way! URL = ${url.toString()}`
|
|
); // This will allow it to retry
|
|
// return false; // This will stop the retries
|
|
}
|
|
|
|
if (log_lvl) {
|
|
console.log(
|
|
`Response: status=${response.status} statusText=${response.statusText} url=${response.url} attempt=${attempt}`
|
|
);
|
|
}
|
|
if (log_lvl > 1) {
|
|
console.log('Response:', response);
|
|
}
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 404) {
|
|
if (log_lvl) {
|
|
console.log('The response was a 404 not found "error". Returning null.');
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// FAIL FAST (Section 2D): Do not retry on Auth or Client errors (400, 401, 403, 422)
|
|
if (response.status === 400 || response.status === 401 || response.status === 403 || response.status === 422) {
|
|
if (log_lvl) console.error(`API Client Failure (${response.status}). Failing fast.`);
|
|
|
|
if (response.status === 401 || response.status === 403) {
|
|
console.warn(`AUTH DIAGNOSTICS: Headers sent for ${endpoint}:`, {
|
|
has_auth: !!headers_cleaned['Authorization'],
|
|
has_api_key: !!headers_cleaned['x-aether-api-key'],
|
|
has_account_id: !!headers_cleaned['x-account-id'],
|
|
jwt_preview: jwt ? `${jwt.slice(0, 8)}...` : 'MISSING'
|
|
});
|
|
}
|
|
|
|
// Structured Error Handling (V3): Attempt to get rich error metadata
|
|
let error_json: any = null;
|
|
try {
|
|
error_json = await response.json();
|
|
} catch (e) {
|
|
// Not JSON
|
|
}
|
|
|
|
if (log_lvl) console.log('The response was not ok. Structured Error Check:', error_json);
|
|
|
|
if (error_json?.meta?.details) {
|
|
return error_json;
|
|
}
|
|
|
|
// Fallback for standard FastAPI "detail" errors
|
|
if (error_json?.detail) {
|
|
return {
|
|
meta: {
|
|
success: false,
|
|
status_code: response.status,
|
|
details: {
|
|
category: 'validation',
|
|
message: typeof error_json.detail === 'string' ? error_json.detail : JSON.stringify(error_json.detail),
|
|
raw: error_json.detail
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
if (!return_blob) {
|
|
const json = await response.json();
|
|
if (log_lvl > 1) {
|
|
console.log('Response JSON:', json);
|
|
}
|
|
if (!Array.isArray(json.data) && as_list) {
|
|
return [json.data];
|
|
}
|
|
return json.data || json;
|
|
} else {
|
|
const reader = response.body?.getReader();
|
|
const contentLength = +response.headers.get('Content-Length')!;
|
|
let receivedLength = 0;
|
|
const chunks = [];
|
|
|
|
while (true) {
|
|
const { done, value } = await reader!.read();
|
|
if (done) break;
|
|
chunks.push(value);
|
|
receivedLength += value.length;
|
|
|
|
const percent_completed = Math.round((receivedLength * 100) / contentLength);
|
|
if (log_lvl > 1) {
|
|
console.log(
|
|
'GET Blob Progress:',
|
|
percent_completed,
|
|
'Total:',
|
|
contentLength,
|
|
'Loaded:',
|
|
receivedLength,
|
|
'Percent Completed',
|
|
percent_completed
|
|
);
|
|
}
|
|
|
|
temp_get_blob_percent_completed = percent_completed;
|
|
|
|
try {
|
|
if (typeof window !== 'undefined') {
|
|
window.postMessage(
|
|
{
|
|
type: 'api_download_blob',
|
|
status: 'downloading',
|
|
task_id: task_id,
|
|
endpoint: endpoint,
|
|
filename: filename,
|
|
size_total: contentLength,
|
|
size_loaded: receivedLength,
|
|
percent_completed: percent_completed
|
|
},
|
|
'*'
|
|
);
|
|
}
|
|
} catch (e) {
|
|
console.error('Error posting message:', e);
|
|
}
|
|
}
|
|
|
|
const blob = new Blob(chunks);
|
|
if (auto_download) {
|
|
const downloadUrl = window.URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = downloadUrl;
|
|
link.setAttribute('download', filename || 'download');
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
link.remove();
|
|
return true;
|
|
} else {
|
|
return blob;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.log(`API GET object request *fetch* error on attempt ${attempt}:`, error);
|
|
|
|
if (attempt === retry_count) {
|
|
console.log('Max retry attempts reached. Returning false.');
|
|
return false;
|
|
}
|
|
|
|
// Log retry information
|
|
if (log_lvl) {
|
|
console.log(`Retrying... (${attempt}/${retry_count})`);
|
|
}
|
|
}
|
|
}
|
|
};
|