mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
fix(premium-fetch): exhaust tester keys before Clerk (#2347)
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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)));
|
||||
|
||||
Reference in New Issue
Block a user