fix(resilience): gate premium RPCs through the current gateway path

Root cause: resilience RPCs added only to PREMIUM_RPC_PATHS would still be reachable from trusted browser origins because the gateway only forced API-key enforcement for tier-gated endpoints.\n\nAdd the resilience score/ranking routes to the shared premium path set, force legacy premium paths through the API-key-or-bearer gate, and extend gateway tests to cover both resilience endpoints for API-key and bearer flows.
This commit is contained in:
lspassos1
2026-04-03 22:46:52 +01:00
parent 94e2f545c6
commit 067df53b22
3 changed files with 71 additions and 3 deletions

View File

@@ -276,7 +276,7 @@ export function createDomainGateway(
// API key validation — tier-gated endpoints require EITHER an API key OR a valid bearer token.
// Authenticated users (sessionUserId present) bypass the API key requirement.
const keyCheck = validateApiKey(request, {
forceKey: isTierGated && !sessionUserId,
forceKey: (isTierGated && !sessionUserId) || needsLegacyProBearerGate,
});
if (keyCheck.required && !keyCheck.valid) {
if (needsLegacyProBearerGate) {

View File

@@ -10,4 +10,6 @@ export const PREMIUM_RPC_PATHS = new Set<string>([
'/api/market/v1/backtest-stock',
'/api/market/v1/list-stored-stock-backtests',
'/api/intelligence/v1/deduct-situation',
'/api/resilience/v1/get-resilience-score',
'/api/resilience/v1/get-resilience-ranking',
]);

View File

@@ -12,7 +12,7 @@ afterEach(() => {
else process.env.WORLDMONITOR_VALID_KEYS = originalKeys;
});
describe('premium stock gateway enforcement', () => {
describe('premium gateway API key enforcement', () => {
it('requires credentials for premium endpoints regardless of origin', async () => {
const handler = createDomainGateway([
{
@@ -20,6 +20,16 @@ describe('premium stock gateway enforcement', () => {
path: '/api/market/v1/analyze-stock',
handler: async () => new Response(JSON.stringify({ ok: true }), { status: 200 }),
},
{
method: 'GET',
path: '/api/resilience/v1/get-resilience-score',
handler: async () => new Response(JSON.stringify({ ok: true }), { status: 200 }),
},
{
method: 'GET',
path: '/api/resilience/v1/get-resilience-ranking',
handler: async () => new Response(JSON.stringify({ ok: true }), { status: 200 }),
},
{
method: 'GET',
path: '/api/market/v1/list-market-quotes',
@@ -35,6 +45,16 @@ describe('premium stock gateway enforcement', () => {
}));
assert.equal(browserNoKey.status, 401);
const resilienceScoreNoKey = await handler(new Request('https://worldmonitor.app/api/resilience/v1/get-resilience-score?countryCode=US', {
headers: { Origin: 'https://worldmonitor.app' },
}));
assert.equal(resilienceScoreNoKey.status, 401);
const resilienceRankingNoKey = await handler(new Request('https://worldmonitor.app/api/resilience/v1/get-resilience-ranking', {
headers: { Origin: 'https://worldmonitor.app' },
}));
assert.equal(resilienceRankingNoKey.status, 401);
// Trusted browser origin with valid API key — 200 (API-key holders bypass entitlement check)
const browserWithKey = await handler(new Request('https://worldmonitor.app/api/market/v1/analyze-stock?symbol=AAPL', {
headers: {
@@ -44,6 +64,22 @@ describe('premium stock gateway enforcement', () => {
}));
assert.equal(browserWithKey.status, 200);
const resilienceScoreWithKey = await handler(new Request('https://worldmonitor.app/api/resilience/v1/get-resilience-score?countryCode=US', {
headers: {
Origin: 'https://worldmonitor.app',
'X-WorldMonitor-Key': 'real-key-123',
},
}));
assert.equal(resilienceScoreWithKey.status, 200);
const resilienceRankingWithKey = await handler(new Request('https://worldmonitor.app/api/resilience/v1/get-resilience-ranking', {
headers: {
Origin: 'https://worldmonitor.app',
'X-WorldMonitor-Key': 'real-key-123',
},
}));
assert.equal(resilienceRankingWithKey.status, 200);
// Unknown origin — blocked (403 from isDisallowedOrigin before key check)
const unknownNoKey = await handler(new Request('https://external.example.com/api/market/v1/analyze-stock?symbol=AAPL', {
headers: { Origin: 'https://external.example.com' },
@@ -62,7 +98,7 @@ describe('premium stock gateway enforcement', () => {
// Bearer token auth path for premium endpoints
// ---------------------------------------------------------------------------
describe('premium stock gateway bearer token auth', () => {
describe('premium gateway bearer token auth', () => {
let privateKey: CryptoKey;
let wrongPrivateKey: CryptoKey;
let jwksServer: Server;
@@ -107,6 +143,16 @@ describe('premium stock gateway bearer token auth', () => {
path: '/api/market/v1/analyze-stock',
handler: async () => new Response(JSON.stringify({ ok: true }), { status: 200 }),
},
{
method: 'GET',
path: '/api/resilience/v1/get-resilience-score',
handler: async () => new Response(JSON.stringify({ ok: true }), { status: 200 }),
},
{
method: 'GET',
path: '/api/resilience/v1/get-resilience-ranking',
handler: async () => new Response(JSON.stringify({ ok: true }), { status: 200 }),
},
{
method: 'GET',
path: '/api/market/v1/list-market-quotes',
@@ -176,4 +222,24 @@ describe('premium stock gateway bearer token auth', () => {
}));
assert.equal(res.status, 200);
});
it('accepts valid Pro bearer token on resilience premium endpoints → 200', async () => {
const token = await signToken({ sub: 'user_pro', plan: 'pro' });
const scoreRes = await handler(new Request('https://worldmonitor.app/api/resilience/v1/get-resilience-score?countryCode=US', {
headers: {
Origin: 'https://worldmonitor.app',
Authorization: `Bearer ${token}`,
},
}));
assert.equal(scoreRes.status, 200);
const rankingRes = await handler(new Request('https://worldmonitor.app/api/resilience/v1/get-resilience-ranking', {
headers: {
Origin: 'https://worldmonitor.app',
Authorization: `Bearer ${token}`,
},
}));
assert.equal(rankingRes.status, 200);
});
});