mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
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:
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user