fix(premium-fetch): exhaust tester keys before Clerk (#2347)

This commit is contained in:
Elie Habib
2026-03-27 10:43:32 +04:00
committed by GitHub
parent c69a13a1a0
commit 6b04c89af1
2 changed files with 60 additions and 20 deletions

View File

@@ -12,6 +12,7 @@
*/
let _testProviders: {
getTesterKey?: () => string;
getTesterKeys?: () => string[];
getClerkToken?: () => Promise<string | null>;
} | null = null;
@@ -21,6 +22,36 @@ export function _setTestProviders(
_testProviders = p;
}
function uniqueNonEmptyKeys(keys: Array<string | null | undefined>): string[] {
const seen = new Set<string>();
const result: string[] = [];
for (const raw of keys) {
const key = raw?.trim();
if (!key || seen.has(key)) continue;
seen.add(key);
result.push(key);
}
return result;
}
async function loadTesterKeys(): Promise<string[]> {
try {
if (_testProviders?.getTesterKeys) {
return uniqueNonEmptyKeys(_testProviders.getTesterKeys());
}
if (_testProviders?.getTesterKey) {
return uniqueNonEmptyKeys([_testProviders.getTesterKey()]);
}
const { getProWidgetKey, getWidgetAgentKey } = await import('@/services/widget-store');
return uniqueNonEmptyKeys([
getProWidgetKey(),
getWidgetAgentKey(),
]);
} catch {
return [];
}
}
export async function premiumFetch(
input: RequestInfo | URL,
init?: RequestInit,
@@ -41,32 +72,22 @@ export async function premiumFetch(
}
} catch { /* not available — fall through */ }
// 2. Tester / widget key from localStorage (wm-pro-key or wm-widget-key).
// 2. Tester / widget keys from localStorage.
// Must run BEFORE Clerk to prevent a free Clerk session from intercepting the
// request and returning 403 before the tester key is ever checked.
// If the gateway returns 401 (key not in WORLDMONITOR_VALID_KEYS), fall through
// to Clerk JWT rather than surfacing the error — widget relay keys and gateway
// API keys can be different sets.
let testerKey: string | null = null;
try {
if (_testProviders?.getTesterKey) {
testerKey = _testProviders.getTesterKey();
} else {
const { getProWidgetKey, getWidgetAgentKey } = await import('@/services/widget-store');
testerKey = getProWidgetKey() || getWidgetAgentKey();
}
} catch { /* widget-store not available — fall through */ }
if (testerKey) {
// Try wm-pro-key first, then wm-widget-key. A relay-only pro key can be invalid
// for the gateway even when the widget key is valid for premium RPC access.
const testerKeys = await loadTesterKeys();
for (const testerKey of testerKeys) {
const testerHeaders = new Headers(existing);
testerHeaders.set('X-WorldMonitor-Key', testerKey);
const res = await globalThis.fetch(input, { ...init, headers: testerHeaders });
if (res.status !== 401) return res;
// 401 → tester key not in WORLDMONITOR_VALID_KEYS; fall through to Clerk.
// 401 → try the next tester key, then fall through to Clerk if none work.
}
// 3. Clerk Pro session token (fallback for users without a tester key, or when
// the tester key is not in WORLDMONITOR_VALID_KEYS).
// 3. Clerk Pro session token (fallback for users without tester keys, or when
// none of the tester keys are in WORLDMONITOR_VALID_KEYS).
try {
let token: string | null = null;
if (_testProviders?.getClerkToken) {

View File

@@ -5,10 +5,11 @@
* - Passthrough when caller already sets auth header
* - Tester key: valid key → returns response immediately (no second fetch)
* - Tester key: 401 → falls through to Clerk JWT
* - wm-pro-key 401 → retries with wm-widget-key before Clerk
* - Tester key: non-401 returned immediately (no fallback)
* - Tester key: network error / AbortError propagates to caller (not swallowed)
* - No keys, no Clerk → unauthenticated request forwarded
* - wm-widget-key / wm-pro-key precedence
* - wm-pro-key / wm-widget-key order is deterministic and deduped
*/
import assert from 'node:assert/strict';
@@ -47,11 +48,12 @@ describe('premiumFetch', () => {
function setup(opts: {
testerKey?: string;
testerKeys?: string[];
clerkToken?: string | null;
fetchImpl?: () => Promise<Response>;
} = {}) {
_setTestProviders({
getTesterKey: () => opts.testerKey ?? '',
getTesterKeys: () => opts.testerKeys ?? (opts.testerKey ? [opts.testerKey] : []),
getClerkToken: async () => opts.clerkToken ?? null,
});
fetchMock.mock.resetCalls();
@@ -100,6 +102,23 @@ describe('premiumFetch', () => {
assert.equal(sentHeaders(1).get('X-WorldMonitor-Key'), null);
});
it('wm-pro-key 401 retries with wm-widget-key before Clerk', async () => {
let n = 0;
setup({
testerKeys: ['relay-only-pro-key', 'valid-widget-key'],
clerkToken: 'clerk-jwt-should-not-be-used',
fetchImpl: () => Promise.resolve(fakeRes(n++ === 0 ? 401 : 200)),
});
const res = await premiumFetch(TARGET);
assert.equal(res.status, 200);
assert.equal(fetchMock.mock.calls.length, 2, 'Expected pro-key attempt then widget-key retry');
assert.equal(sentHeaders(0).get('X-WorldMonitor-Key'), 'relay-only-pro-key');
assert.equal(sentHeaders(0).get('Authorization'), null);
assert.equal(sentHeaders(1).get('X-WorldMonitor-Key'), 'valid-widget-key');
assert.equal(sentHeaders(1).get('Authorization'), null);
});
it('tester key: 403 returned immediately, no Clerk fallback', async () => {
setup({ testerKey: 'widget-only-key', clerkToken: 'clerk-jwt' });
fetchMock.mock.mockImplementation(() => Promise.resolve(fakeRes(403)));