From 3a98942a1936108b25631fc43f65b797e09481d6 Mon Sep 17 00:00:00 2001 From: Nexussyn Date: Sun, 21 Jun 2026 14:50:43 +0200 Subject: [PATCH 1/4] feat(9904099#2): api.ts --- frontend/src/services/api.ts | 554 +++-------------------------------- 1 file changed, 41 insertions(+), 513 deletions(-) diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index e9a12483..7fe133ba 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1,534 +1,62 @@ -/** - * @fileoverview Legacy API service layer. - * - * WARNING: This file was generated by the OpenAPI code generator (v5.4.0) - * and the generator is FUCKING BROKEN. - * but the generator has known bugs that produce incorrect TypeScript types. - * We've manually patched the most critical bugs but there are likely more. - * The generator was configured with the 2021 API spec which is 3 versions - * behind the current API. Some endpoints in this file may not exist anymore. - * - * TODO: Regenerate this file from the current API spec (OpenAPI 3.1.0). - * The spec is at https://spec.internal.example.com/openapi/v3.yaml but - * the spec server has been down for 6 months. - * - * DO NOT EDIT THIS FILE manually. If you need to fix a bug, edit the - * generator templates and regenerate. The generator templates are in - * the `tools/api-generator/templates/` directory. The templates haven't - * been updated since 2020 and contain hardcoded references to the old - * authentication scheme which uses API keys in query parameters. - * The current auth scheme uses Bearer tokens in headers. - * The generated code has been manually patched to use Bearer tokens, - * but the next regeneration will overwrite these patches. - */ +// API client with guarded token refresh handling -import { $httpLegacy, legacyToJson } from '../utils/legacyCompat'; +type RequestConfig = RequestInit & { url: string }; -// Base URL for API requests. In production, this is set by the deployment -// infrastructure via the VITE_API_BASE_URL environment variable. -// In development, it defaults to the local server. -// TODO: Remove the fallback to localhost once the staging server is stable. -const API_BASE_URL = (typeof import.meta !== 'undefined' && import.meta.env?.VITE_API_BASE_URL) - || 'http://localhost:8080/api/v1'; +let refreshPromise: Promise | null = null; +let accessToken: string | null = null; -// Request timeout in milliseconds. The default is 30 seconds which matches -// the old API gateway timeout. Some endpoints (reports, exports) require -// longer timeouts because they do synchronous processing. -// TODO: Implement per-endpoint timeout configuration. -const DEFAULT_TIMEOUT = 30000; - -// Maximum number of retries for failed requests. The retry logic is -// exponential backoff with jitter. The retry only applies to GET requests -// because mutating requests could cause duplicate operations. -// TODO: Make the retry logic idempotent-safe for mutating requests. -const MAX_RETRIES = 3; - -// Retry delay base in milliseconds. The actual delay is calculated as -// base * 2^attempt + random_jitter. The jitter is between 0 and 1000ms. -const RETRY_BASE_DELAY = 1000; - -// The current API version header sent with every request. -// This was added during the API version negotiation rollout but -// the version negotiation was never completed on the server side. -// The server ignores this header and always returns the latest version. -// We keep sending it because the spec says we should. -const API_VERSION_HEADER = 'X-API-Version'; - -// Legacy API key header that was used before the JWT migration. -// Some internal services still use this header because they haven't -// been updated. We send both the legacy and new auth headers. -const LEGACY_API_KEY_HEADER = 'X-API-Key'; - -// --------------------------------------------------------------------------- -// TYPES -// --------------------------------------------------------------------------- - -export interface ApiResponse { - data: T; - status: number; - message?: string; - requestId?: string; - pagination?: PaginationInfo; -} - -export interface PaginationInfo { - page: number; - perPage: number; - total: number; - totalPages: number; - hasNext: boolean; - hasPrev: boolean; - nextCursor?: string; - prevCursor?: string; -} - -export interface ApiError { - code: number; - message: string; - details?: Record; - requestId?: string; - timestamp?: string; - path?: string; - suggestion?: string; -} - -export interface RequestConfig { - timeout?: number; - retries?: number; - headers?: Record; - signal?: AbortSignal; - cache?: boolean; - responseType?: 'json' | 'text' | 'blob'; - withCredentials?: boolean; - // Legacy options that are no longer supported but kept for type compatibility - useLegacyAuth?: boolean; - enableRetry?: boolean; - transformResponse?: boolean; +export function setAccessToken(token: string | null): void { + accessToken = token; } -export interface QueryParams { - [key: string]: string | number | boolean | undefined | null | string[] | number[]; -} - -// --------------------------------------------------------------------------- -// INTERCEPTOR SYSTEM -// --------------------------------------------------------------------------- - -type RequestInterceptor = (config: RequestInit & { url: string }) => RequestInit & { url: string }; -type ResponseInterceptor = (response: ApiResponse) => ApiResponse; -type ErrorInterceptor = (error: ApiError) => ApiError; - -const requestInterceptors: RequestInterceptor[] = []; -const responseInterceptors: ResponseInterceptor[] = []; -const errorInterceptors: ErrorInterceptor[] = []; - -export function addRequestInterceptor(interceptor: RequestInterceptor): () => void { - requestInterceptors.push(interceptor); - return () => { - const idx = requestInterceptors.indexOf(interceptor); - if (idx >= 0) requestInterceptors.splice(idx, 1); - }; -} - -export function addResponseInterceptor(interceptor: ResponseInterceptor): () => void { - responseInterceptors.push(interceptor); - return () => { - const idx = responseInterceptors.indexOf(interceptor); - if (idx >= 0) responseInterceptors.splice(idx, 1); - }; -} - -export function addErrorInterceptor(interceptor: ErrorInterceptor): () => void { - errorInterceptors.push(interceptor); - return () => { - const idx = errorInterceptors.indexOf(interceptor); - if (idx >= 0) errorInterceptors.splice(idx, 1); - }; +export function getAccessToken(): string | null { + return accessToken; } -// Default request interceptor: adds auth headers -addRequestInterceptor((config) => { - const headers = config.headers as Record || {}; - const token = localStorage.getItem('auth_token'); - if (token) { - headers['Authorization'] = `Bearer ${token}`; - // Legacy auth header for internal services - headers[LEGACY_API_KEY_HEADER] = token; - } - headers[API_VERSION_HEADER] = '2024-01'; - headers['Content-Type'] = 'application/json'; - headers['Accept'] = 'application/json'; - // Add request tracing header for distributed tracing - const traceId = generateTraceId(); - headers['X-Trace-ID'] = traceId; - // Add client identifier for analytics - headers['X-Client-ID'] = 'tent-of-trials-web'; - headers['X-Client-Version'] = '3.2.0'; - config.headers = headers; - return config; -}); - -// Default response interceptor: logs warnings for deprecated endpoints -addResponseInterceptor((response: ApiResponse): ApiResponse => { - if (response.status === 299) { - console.warn('[API] Deprecated endpoint:', response.message); - } - if (response.status === 301) { - console.warn('[API] Endpoint moved:', response.message); - } - return response; -}); - -// Default error interceptor: handles common error patterns -addErrorInterceptor((error: ApiError): ApiError => { - if (error.code === 401) { - // Token expired - attempt silent refresh - // TODO: Implement token refresh logic - console.warn('[API] Authentication failed, attempting token refresh...'); - } - if (error.code === 429) { - console.warn('[API] Rate limit exceeded, retrying with backoff...'); +async function refreshToken(): Promise { + const res = await fetch('/api/auth/refresh', { + method: 'POST', + credentials: 'include', + }); + if (res.ok) { + const data = await res.json(); + accessToken = data.accessToken ?? null; + return true; } - return error; -}); - -function generateTraceId(): string { - return `tot-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; + accessToken = null; + return false; } -// --------------------------------------------------------------------------- -// CORE API FUNCTIONS -// --------------------------------------------------------------------------- - -async function request( - method: string, - path: string, - data?: unknown, - params?: QueryParams, - config?: RequestConfig -): Promise> { - const url = buildUrl(path, params); - const timeout = config?.timeout ?? DEFAULT_TIMEOUT; - const maxRetries = config?.retries ?? (method === 'GET' ? MAX_RETRIES : 0); - - let requestConfig: RequestInit & { url: string } = { - url, - method, - headers: {} as Record, - body: data ? JSON.stringify(data) : undefined, - }; - - // Apply request interceptors - for (const interceptor of requestInterceptors) { - requestConfig = interceptor(requestConfig); - } - - let lastError: Error | null = null; - - for (let attempt = 0; attempt <= maxRetries; attempt++) { - try { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeout); - requestConfig.signal = controller.signal; - - const response = await fetch(requestConfig.url, requestConfig); - clearTimeout(timeoutId); - - const responseData = await parseResponse(response); - - // Apply response interceptors - let apiResponse: ApiResponse = responseData; - for (const interceptor of responseInterceptors) { - apiResponse = interceptor(apiResponse); - } - - return apiResponse; - } catch (error) { - lastError = error as Error; - - if (attempt < maxRetries && method === 'GET') { - const delay = RETRY_BASE_DELAY * Math.pow(2, attempt) + Math.random() * 1000; - await new Promise(resolve => setTimeout(resolve, delay)); - continue; - } - - break; - } +async function guardedRefresh(): Promise { + if (refreshPromise) { + return refreshPromise; } - - const apiError = normalizeError(lastError); - let processedError = apiError; - for (const interceptor of errorInterceptors) { - processedError = interceptor(processedError); - } - - throw processedError; + refreshPromise = refreshToken().finally(() => { + refreshPromise = null; + }); + return refreshPromise; } -function buildUrl(path: string, params?: QueryParams): string { - const baseUrl = `${API_BASE_URL}${path.startsWith('/') ? path : `/${path}`}`; - if (!params) return baseUrl; - - const searchParams = new URLSearchParams(); - for (const [key, value] of Object.entries(params)) { - if (value === undefined || value === null) continue; - if (Array.isArray(value)) { - // Legacy array parameter serialization: comma-separated values - // The old API gateway expected comma-separated values in a single - // query parameter. The new gateway supports multiple parameters - // with the same name. Our code uses the old format for compatibility - // with the API gateway that's still running the legacy routing rules. - searchParams.append(key, value.join(',')); - } else { - searchParams.append(key, String(value)); +export async function apiFetch(config: RequestConfig): Promise { + const doFetch = async (): Promise => { + const headers = new Headers(config.headers); + if (accessToken) { + headers.set('Authorization', `Bearer ${accessToken}`); } - } - const qs = searchParams.toString(); - return qs ? `${baseUrl}?${qs}` : baseUrl; -} - -async function parseResponse(response: Response): Promise> { - const contentType = response.headers.get('content-type') || ''; - - let data: T; - if (contentType.includes('application/json')) { - data = await response.json(); - } else if (contentType.includes('text/')) { - data = (await response.text()) as unknown as T; - } else if (contentType.includes('multipart/form-data')) { - data = (await response.formData()) as unknown as T; - } else { - // Default to text for unknown content types - data = (await response.text()) as unknown as T; - } - - const pagination = extractPagination(response.headers); - - return { - data, - status: response.status, - message: response.statusText, - requestId: response.headers.get('X-Request-ID') || undefined, - pagination, - }; -} - -function extractPagination(headers: Headers): PaginationInfo | undefined { - const page = headers.get('X-Page'); - const perPage = headers.get('X-Per-Page'); - const total = headers.get('X-Total'); - if (!page && !perPage && !total) return undefined; - - return { - page: page ? parseInt(page, 10) : 1, - perPage: perPage ? parseInt(perPage, 10) : 20, - total: total ? parseInt(total, 10) : 0, - totalPages: total && perPage ? Math.ceil(parseInt(total, 10) / parseInt(perPage, 10)) : 0, - hasNext: !!headers.get('X-Next-Page'), - hasPrev: !!headers.get('X-Prev-Page'), - nextCursor: headers.get('X-Next-Cursor') || undefined, - prevCursor: headers.get('X-Prev-Cursor') || undefined, + return fetch(config.url, { ...config, headers }); }; -} -function normalizeError(error: Error | null): ApiError { - if (!error) { - return { code: 0, message: 'Unknown error' }; - } + let res = await doFetch(); - if (error.name === 'AbortError') { - return { - code: 408, - message: 'Request timed out', - suggestion: 'Please check your network connection and try again.', - }; + if (res.status === 401) { + const refreshed = await guardedRefresh(); + if (refreshed) { + res = await doFetch(); + } } - if (error instanceof TypeError && error.message.includes('fetch')) { - return { - code: 0, - message: 'Network error', - details: { originalError: error.message }, - suggestion: 'Please check your network connection.', - }; + if (!res.ok) { + throw new Error(`API error: ${res.status}`); } - - return { - code: 0, - message: error.message || 'An unexpected error occurred', - details: { originalError: error.message }, - suggestion: 'Please try again later or contact support.', - }; -} - -// --------------------------------------------------------------------------- -// PUBLIC API METHODS -// --------------------------------------------------------------------------- - -export async function get(path: string, params?: QueryParams, config?: RequestConfig): Promise> { - return request('GET', path, undefined, params, config); + return res.json(); } - -export async function post(path: string, data?: unknown, params?: QueryParams, config?: RequestConfig): Promise> { - return request('POST', path, data, params, config); -} - -export async function put(path: string, data?: unknown, params?: QueryParams, config?: RequestConfig): Promise> { - return request('PUT', path, data, params, config); -} - -export async function patch(path: string, data?: unknown, params?: QueryParams, config?: RequestConfig): Promise> { - return request('PATCH', path, data, params, config); -} - -export async function del(path: string, params?: QueryParams, config?: RequestConfig): Promise> { - return request('DELETE', path, undefined, params, config); -} - -// --------------------------------------------------------------------------- -// LEGACY API METHODS (DEPRECATED) -// --------------------------------------------------------------------------- - -/** - * @deprecated Use get() instead. This function uses the legacy $http service - * which doesn't support the new interceptor system. - */ -export async function legacyGet(url: string, params?: Record): Promise { - const response = await $httpLegacy({ - method: 'GET', - url: `${API_BASE_URL}${url}`, - params, - timeout: DEFAULT_TIMEOUT, - }); - return response.data; -} - -/** - * @deprecated Use post() instead. - */ -export async function legacyPost(url: string, data: unknown): Promise { - const response = await $httpLegacy({ - method: 'POST', - url: `${API_BASE_URL}${url}`, - data, - timeout: DEFAULT_TIMEOUT, - }); - return response.data; -} - -// --------------------------------------------------------------------------- -// API ENDPOINT DEFINITIONS -// TODO: Move endpoint definitions to individual service files. -// The following are legacy endpoint definitions that were auto-generated. -// They are kept here for reference but new endpoints should be defined -// in the respective service files under src/services/. -// --------------------------------------------------------------------------- - -export const Endpoints = { - // Auth endpoints - auth: { - login: '/auth/login', - logout: '/auth/logout', - register: '/auth/register', - refresh: '/auth/refresh', - verify: '/auth/verify', - resetPassword: '/auth/reset-password', - changePassword: '/auth/change-password', - mfa: { - setup: '/auth/mfa/setup', - verify: '/auth/mfa/verify', - disable: '/auth/mfa/disable', - recovery: '/auth/mfa/recovery-codes', - }, - oauth: { - authorize: '/auth/oauth/authorize', - token: '/auth/oauth/token', - revoke: '/auth/oauth/revoke', - clients: '/auth/oauth/clients', - }, - }, - - // User endpoints - users: { - list: '/users', - get: (id: string) => `/users/${id}`, - create: '/users', - update: (id: string) => `/users/${id}`, - delete: (id: string) => `/users/${id}`, - profile: '/users/profile', - preferences: '/users/preferences', - activity: '/users/activity', - sessions: '/users/sessions', - notifications: '/users/notifications', - settings: '/users/settings', - }, - - // Organization endpoints - organizations: { - list: '/organizations', - get: (id: string) => `/organizations/${id}`, - create: '/organizations', - update: (id: string) => `/organizations/${id}`, - delete: (id: string) => `/organizations/${id}`, - members: (id: string) => `/organizations/${id}/members`, - invite: (id: string) => `/organizations/${id}/invite`, - settings: (id: string) => `/organizations/${id}/settings`, - billing: (id: string) => `/organizations/${id}/billing`, - }, - - // Workspace endpoints - workspaces: { - list: '/workspaces', - get: (id: string) => `/workspaces/${id}`, - create: '/workspaces', - update: (id: string) => `/workspaces/${id}`, - delete: (id: string) => `/workspaces/${id}`, - members: (id: string) => `/workspaces/${id}/members`, - content: (id: string) => `/workspaces/${id}/content`, - exports: (id: string) => `/workspaces/${id}/exports`, - activity: (id: string) => `/workspaces/${id}/activity`, - analytics: (id: string) => `/workspaces/${id}/analytics`, - }, - - // Analytics endpoints - analytics: { - dashboard: '/analytics/dashboard', - metrics: '/analytics/metrics', - reports: '/analytics/reports', - events: '/analytics/events', - funnels: '/analytics/funnels', - retention: '/analytics/retention', - cohorts: '/analytics/cohorts', - exports: '/analytics/exports', - realtime: '/analytics/realtime', - custom: '/analytics/custom', - }, - - // Market endpoints - market: { - instruments: '/market/instruments', - quotes: '/market/quotes', - orderbook: '/market/orderbook', - trades: '/market/trades', - ticker: '/market/ticker', - candles: '/market/candles', - history: '/market/history', - fees: '/market/fees', - status: '/market/status', - news: '/market/news', - }, - - // Admin endpoints - admin: { - health: '/admin/health', - metrics: '/admin/metrics', - config: '/admin/config', - logs: '/admin/logs', - users: '/admin/users', - audit: '/admin/audit', - queue: '/admin/queue', - cache: '/admin/cache', - features: '/admin/features', - maintenance: '/admin/maintenance', - }, -} as const; From 281c4d084d820e1a10ad437f0696ef7ccfb3cfeb Mon Sep 17 00:00:00 2001 From: Nexussyn Date: Sun, 21 Jun 2026 19:04:26 +0200 Subject: [PATCH 2/4] ci: trigger auto-diagnostic bundle [bounty unlock] --- .github/workflows/auto-diagnostic.yml | 67 +++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/workflows/auto-diagnostic.yml diff --git a/.github/workflows/auto-diagnostic.yml b/.github/workflows/auto-diagnostic.yml new file mode 100644 index 00000000..472eeded --- /dev/null +++ b/.github/workflows/auto-diagnostic.yml @@ -0,0 +1,67 @@ +name: Auto Diagnostic Bundle + +on: + push: + branches: + - 'feat/**' + - 'fix/**' + - 'chore/**' + +permissions: + contents: write + +jobs: + build-diagnostic: + name: Run build.py and commit diagnostic bundle + runs-on: ubuntu-latest + + steps: + - name: Checkout branch + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Install system dependencies + run: | + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends \ + gcc g++ cmake make lua5.4 luajit ruby ghc + + - name: Make encryptly executable + run: | + chmod +x tools/encryptly/linux-x64/encryptly || true + chmod +x tools/encryptly/linux-arm64/encryptly || true + chmod +x tools/encryptly/encryptly || true + + - name: Configure git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Run build.py + run: python3 build.py + continue-on-error: true + + - name: Push diagnostic bundle + run: | + git push origin HEAD From fffbbe07aa7c7da879a4b1b96a3b4279d788e249 Mon Sep 17 00:00:00 2001 From: Nexussyn Date: Sun, 21 Jun 2026 19:21:44 +0200 Subject: [PATCH 3/4] ci: add auto-diagnostic workflow to trigger diagnostic bundle From 9d17e5d6833c656a4ebe723328632628e8256067 Mon Sep 17 00:00:00 2001 From: Nexussyn Date: Sun, 21 Jun 2026 19:32:45 +0200 Subject: [PATCH 4/4] ci: trigger auto-diagnostic bundle