mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
fix(runtime): inject auth headers for premium RPCs in all web deployments
Two bugs in installWebApiRedirect(): 1. Early bail-out when no API base URL was configured (the default for standard worldmonitor.app deployment using relative URLs). The entire enrichInitForPremium logic was skipped, so Pro users never got their Clerk session token injected. 2. enrichInitForPremium only tried Clerk + tester keys, never WORLDMONITOR_API_KEY from runtime-config. Users with an API key configured via env got 401 because no X-WorldMonitor-Key was sent. Fix: - Remove the early !apiBase return; always patch window.fetch for auth injection regardless of whether an API base redirect is configured. - Add WORLDMONITOR_API_KEY injection (highest priority after existing headers) matching the desktop cloud-fallback pattern. - When no redirect is configured, install a slim fetch wrapper that only runs enrichInitForPremium for /api/ paths. Auth priority: existing headers → WORLDMONITOR_API_KEY → Clerk Bearer → tester key. Fixes: Premium Stock Analysis and Premium Backtesting 401s on web.
This commit is contained in:
@@ -748,52 +748,42 @@ function isAllowedRedirectTarget(url: string): boolean {
|
||||
|
||||
export function installWebApiRedirect(): void {
|
||||
if (isDesktopRuntime() || typeof window === 'undefined') return;
|
||||
const apiBase = getConfiguredWebApiBaseUrl();
|
||||
if (!apiBase) return;
|
||||
if (!isAllowedRedirectTarget(apiBase)) {
|
||||
console.warn('[runtime] web API base blocked — not in hostname allowlist:', apiBase);
|
||||
return;
|
||||
}
|
||||
if ((window as unknown as Record<string, unknown>).__wmWebRedirectPatched) return;
|
||||
|
||||
const apiBase = getConfiguredWebApiBaseUrl();
|
||||
const hasRedirect = !!apiBase && isAllowedRedirectTarget(apiBase);
|
||||
if (apiBase && !hasRedirect) {
|
||||
console.warn('[runtime] web API base blocked — not in hostname allowlist:', apiBase);
|
||||
}
|
||||
|
||||
const nativeFetch = window.fetch.bind(window);
|
||||
const API_BASE = apiBase;
|
||||
const shouldRedirectPath = (pathWithQuery: string): boolean => pathWithQuery.startsWith('/api/');
|
||||
const shouldFallbackToOrigin = (status: number): boolean => (
|
||||
status === 404 || status === 405 || status === 501 || status === 502 || status === 503
|
||||
);
|
||||
const fetchWithRedirectFallback = async (
|
||||
redirectedInput: RequestInfo | URL,
|
||||
originalInput: RequestInfo | URL,
|
||||
originalInit?: RequestInit,
|
||||
): Promise<Response> => {
|
||||
try {
|
||||
const redirectedResponse = await nativeFetch(redirectedInput, originalInit);
|
||||
if (!shouldFallbackToOrigin(redirectedResponse.status)) return redirectedResponse;
|
||||
return nativeFetch(originalInput, originalInit);
|
||||
} catch (error) {
|
||||
try {
|
||||
return await nativeFetch(originalInput, originalInit);
|
||||
} catch {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* For premium API paths, inject auth when the user has premium access but no
|
||||
* existing auth header is present. Handles three paths:
|
||||
* 1. Clerk Pro: Authorization: Bearer <token>
|
||||
* 2. Tester key (wm-pro-key / wm-widget-key): X-WorldMonitor-Key: <key>
|
||||
* 3. API key users: already set X-WorldMonitor-Key — left unchanged
|
||||
* existing auth header is present. Priority order:
|
||||
* 1. Existing auth headers — left unchanged (API key users keep their flow)
|
||||
* 2. WORLDMONITOR_API_KEY from runtime config → X-WorldMonitor-Key
|
||||
* 3. Clerk Pro session → Authorization: Bearer <token>
|
||||
* 4. Tester key (wm-pro-key / wm-widget-key) → X-WorldMonitor-Key
|
||||
* Runs on every web deployment (with or without API base redirect).
|
||||
* Returns the original init unchanged for non-premium paths (zero overhead).
|
||||
*/
|
||||
const enrichInitForPremium = async (pathWithQuery: string, init?: RequestInit): Promise<RequestInit | undefined> => {
|
||||
const path = pathWithQuery.split('?')[0] ?? pathWithQuery;
|
||||
if (!WEB_PREMIUM_API_PATHS.has(path)) return init;
|
||||
const headers = new Headers(init?.headers);
|
||||
// Don't overwrite existing auth headers (API key users keep their flow)
|
||||
// Don't overwrite existing auth headers
|
||||
if (headers.has('Authorization') || headers.has('X-WorldMonitor-Key')) return init;
|
||||
// WORLDMONITOR_API_KEY from env or runtime config
|
||||
try {
|
||||
const { getRuntimeConfigSnapshot } = await import('@/services/runtime-config');
|
||||
const wmKey = getRuntimeConfigSnapshot().secrets['WORLDMONITOR_API_KEY']?.value;
|
||||
if (wmKey) {
|
||||
headers.set('X-WorldMonitor-Key', wmKey);
|
||||
return { ...init, headers };
|
||||
}
|
||||
} catch { /* runtime-config unavailable — fall through */ }
|
||||
// Clerk Pro: inject Bearer token
|
||||
const token = await getClerkToken();
|
||||
if (token) {
|
||||
@@ -810,30 +800,74 @@ export function installWebApiRedirect(): void {
|
||||
return init;
|
||||
};
|
||||
|
||||
window.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||
if (typeof input === 'string' && shouldRedirectPath(input)) {
|
||||
const enriched = await enrichInitForPremium(input, init);
|
||||
return fetchWithRedirectFallback(`${API_BASE}${input}`, input, enriched);
|
||||
}
|
||||
if (input instanceof URL && input.origin === window.location.origin && shouldRedirectPath(`${input.pathname}${input.search}`)) {
|
||||
const pathAndSearch = `${input.pathname}${input.search}`;
|
||||
const enriched = await enrichInitForPremium(pathAndSearch, init);
|
||||
return fetchWithRedirectFallback(new URL(`${API_BASE}${pathAndSearch}`), input, enriched);
|
||||
}
|
||||
if (input instanceof Request) {
|
||||
const u = new URL(input.url);
|
||||
if (u.origin === window.location.origin && shouldRedirectPath(`${u.pathname}${u.search}`)) {
|
||||
const pathAndSearch = `${u.pathname}${u.search}`;
|
||||
const enriched = await enrichInitForPremium(pathAndSearch, init);
|
||||
return fetchWithRedirectFallback(
|
||||
new Request(`${API_BASE}${pathAndSearch}`, input),
|
||||
input.clone(),
|
||||
enriched,
|
||||
);
|
||||
if (hasRedirect) {
|
||||
const API_BASE = apiBase;
|
||||
const shouldFallbackToOrigin = (status: number): boolean => (
|
||||
status === 404 || status === 405 || status === 501 || status === 502 || status === 503
|
||||
);
|
||||
const fetchWithRedirectFallback = async (
|
||||
redirectedInput: RequestInfo | URL,
|
||||
originalInput: RequestInfo | URL,
|
||||
originalInit?: RequestInit,
|
||||
): Promise<Response> => {
|
||||
try {
|
||||
const redirectedResponse = await nativeFetch(redirectedInput, originalInit);
|
||||
if (!shouldFallbackToOrigin(redirectedResponse.status)) return redirectedResponse;
|
||||
return nativeFetch(originalInput, originalInit);
|
||||
} catch (error) {
|
||||
try {
|
||||
return await nativeFetch(originalInput, originalInit);
|
||||
} catch {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nativeFetch(input, init);
|
||||
};
|
||||
};
|
||||
|
||||
window.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||
if (typeof input === 'string' && shouldRedirectPath(input)) {
|
||||
const enriched = await enrichInitForPremium(input, init);
|
||||
return fetchWithRedirectFallback(`${API_BASE}${input}`, input, enriched);
|
||||
}
|
||||
if (input instanceof URL && input.origin === window.location.origin && shouldRedirectPath(`${input.pathname}${input.search}`)) {
|
||||
const pathAndSearch = `${input.pathname}${input.search}`;
|
||||
const enriched = await enrichInitForPremium(pathAndSearch, init);
|
||||
return fetchWithRedirectFallback(new URL(`${API_BASE}${pathAndSearch}`), input, enriched);
|
||||
}
|
||||
if (input instanceof Request) {
|
||||
const u = new URL(input.url);
|
||||
if (u.origin === window.location.origin && shouldRedirectPath(`${u.pathname}${u.search}`)) {
|
||||
const pathAndSearch = `${u.pathname}${u.search}`;
|
||||
const enriched = await enrichInitForPremium(pathAndSearch, init);
|
||||
return fetchWithRedirectFallback(
|
||||
new Request(`${API_BASE}${pathAndSearch}`, input),
|
||||
input.clone(),
|
||||
enriched,
|
||||
);
|
||||
}
|
||||
}
|
||||
return nativeFetch(input, init);
|
||||
};
|
||||
} else {
|
||||
// No API base redirect — only inject auth headers for premium paths.
|
||||
window.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||
if (typeof input === 'string' && shouldRedirectPath(input)) {
|
||||
const enriched = await enrichInitForPremium(input, init);
|
||||
return nativeFetch(input, enriched ?? init);
|
||||
}
|
||||
if (input instanceof URL && input.origin === window.location.origin && shouldRedirectPath(`${input.pathname}${input.search}`)) {
|
||||
const enriched = await enrichInitForPremium(`${input.pathname}${input.search}`, init);
|
||||
return nativeFetch(input, enriched ?? init);
|
||||
}
|
||||
if (input instanceof Request) {
|
||||
const u = new URL(input.url);
|
||||
if (u.origin === window.location.origin && shouldRedirectPath(`${u.pathname}${u.search}`)) {
|
||||
const enriched = await enrichInitForPremium(`${u.pathname}${u.search}`, init);
|
||||
if (enriched) return nativeFetch(new Request(input, enriched));
|
||||
}
|
||||
}
|
||||
return nativeFetch(input, init);
|
||||
};
|
||||
}
|
||||
|
||||
(window as unknown as Record<string, unknown>).__wmWebRedirectPatched = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user