| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { createRouter, type RouteDescriptor } from './router'; |
| import { getCorsHeaders, isDisallowedOrigin, isAllowedOrigin } from './cors'; |
| import { isPublicSharedRpcRequest } from '../src/shared/public-rpc-cache'; |
| import { PRO_FRESH_CACHE_RPC_PATHS } from '../src/shared/pro-fresh-rpc'; |
| |
| import { USER_API_KEY_GATEWAY_VALIDATION_ERROR, validateApiKey } from '../api/_api-key.js'; |
| |
| import { timingSafeEqualSecret } from '../api/_crypto.js'; |
| |
| import { captureSilentError } from '../api/_sentry-edge.js'; |
| import { mapErrorToResponse } from './error-mapper'; |
| import { |
| checkRateLimit, |
| checkEndpointRateLimit, |
| checkFailClosedScopedIpRateLimit, |
| hasEndpointRatePolicy, |
| } from './_shared/rate-limit'; |
| import { |
| drainResponseHeaders, |
| drainRetryableResponse, |
| drainSuccessStatusOverride, |
| } from './_shared/response-headers'; |
| import { projectJsonResponse } from './_shared/response-projection'; |
| import { getRpcNoStoreReasonFromJson } from './_shared/cache-contract'; |
| import { |
| checkEntitlementDetailed, |
| getBillingVerificationDenial, |
| getRequiredTier, |
| getEntitlements, |
| isEntitlementBackendConfigured, |
| type CachedEntitlements, |
| } from './_shared/entitlement-check'; |
| import { checkProMcpAccess } from './_shared/pro-mcp-gate'; |
| import { resolveClerkSession } from './_shared/auth-session'; |
| import { |
| INTERNAL_MCP_SIG_HEADER, |
| INTERNAL_MCP_USER_ID_HEADER, |
| INTERNAL_MCP_NONCE_HEADER, |
| INTERNAL_MCP_VERIFIED_HEADER, |
| TRUSTED_USER_ID_HEADER, |
| INTERNAL_MCP_REPLAY_CACHE_TTL_SECONDS, |
| getInternalMcpVerifiedNonce, |
| sha256Hex, |
| verifyInternalMcpRequest, |
| } from './_shared/mcp-internal-hmac'; |
| import { buildUsageIdentity, hashKeySync, type UsageIdentityInput } from './_shared/usage-identity'; |
| import { runRedisPipeline } from './_shared/redis'; |
| import { |
| beginIdempotency, |
| peekIdempotency, |
| IDEMPOTENCY_HEADER, |
| IDEMPOTENT_REPLAYED_HEADER, |
| type IdempotencyOutcome, |
| } from './_shared/idempotency'; |
| import { |
| checkBurst, |
| reserveDailyMeter, |
| rateLimitHeaders, |
| ENTERPRISE_API_RATE_LIMIT, |
| } from './_shared/api-key-rate-limit'; |
| import { |
| DIRECT_LLM_DAILY_QUOTA_LIMIT, |
| DIRECT_LLM_GATEWAY_QUOTA_PATHS, |
| reserveDirectLlmQuota, |
| } from './_shared/direct-llm-quota'; |
| import { |
| deliverUsageEvents, |
| buildRequestEvent, |
| deriveRequestId, |
| deriveExecutionRegion, |
| deriveCountry, |
| deriveIpCity, |
| deriveIpRegion, |
| deriveReqBytes, |
| deriveSentryTraceId, |
| deriveOriginKind, |
| deriveUaHash, |
| deriveIp, |
| deriveUserAgent, |
| deriveReferer, |
| deriveAcceptLanguage, |
| deriveHost, |
| maybeAttachDevHealthHeader, |
| runWithUsageScope, |
| type CacheTier as UsageCacheTier, |
| type RequestReason, |
| } from './_shared/usage'; |
| import { timingSafeEqual } from './_shared/internal-auth'; |
| import type { ServerOptions } from '../src/generated/server/worldmonitor/seismology/v1/service_server'; |
|
|
| export const serverOptions: ServerOptions = { onError: mapErrorToResponse }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const MAX_INTERNAL_MCP_BODY = 256 * 1024; |
|
|
| type InternalMcpReplayClaim = 'fresh' | 'replay' | 'unavailable'; |
|
|
| function getRateLimitTelemetryReason( |
| response: Response, |
| rejectedReason: RequestReason, |
| ): RequestReason { |
| return response.status === 503 && |
| response.headers.get('X-RateLimit-Mode') === 'degraded' |
| ? 'rate_limit_degraded' |
| : rejectedReason; |
| } |
|
|
| async function claimInternalMcpReplayNonce(userId: string, nonce: string): Promise<InternalMcpReplayClaim> { |
| const digest = await sha256Hex(`${userId}:${nonce}`); |
| const key = `internal-mcp-replay:v1:${digest}`; |
| const result = await runRedisPipeline([ |
| ['SET', key, '1', 'EX', INTERNAL_MCP_REPLAY_CACHE_TTL_SECONDS, 'NX'], |
| ]); |
| if (result.length === 0) return 'unavailable'; |
| const claim = result[0] as { result?: unknown; error?: unknown } | undefined; |
| if (claim?.error) return 'unavailable'; |
| return claim?.result === 'OK' ? 'fresh' : 'replay'; |
| } |
|
|
| |
| |
| |
|
|
| type CacheTier = 'fast' | 'medium' | 'slow' | 'slow-browser' | 'live-browser' | 'static' | 'daily' | 'no-store' | 'live'; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| const TIER_HEADERS: Record<CacheTier, string> = { |
| fast: 'public, max-age=60, s-maxage=300, stale-while-revalidate=60, stale-if-error=600', |
| medium: 'public, max-age=120, s-maxage=600, stale-while-revalidate=120, stale-if-error=900', |
| slow: 'public, max-age=300, s-maxage=1800, stale-while-revalidate=300, stale-if-error=3600', |
| 'slow-browser': 'max-age=300, stale-while-revalidate=60, stale-if-error=1800', |
| 'live-browser': 'private, max-age=30, stale-while-revalidate=60, stale-if-error=300', |
| static: 'public, max-age=600, s-maxage=3600, stale-while-revalidate=600, stale-if-error=14400', |
| daily: 'public, max-age=3600, s-maxage=14400, stale-while-revalidate=7200, stale-if-error=172800', |
| 'no-store': 'no-store', |
| live: 'public, max-age=30, s-maxage=60, stale-while-revalidate=60, stale-if-error=300', |
| }; |
|
|
| |
| |
| |
| const TIER_CDN_CACHE: Record<CacheTier, string | null> = { |
| fast: 'public, s-maxage=600, stale-while-revalidate=300, stale-if-error=1200', |
| medium: 'public, s-maxage=1200, stale-while-revalidate=600, stale-if-error=1800', |
| slow: 'public, s-maxage=3600, stale-while-revalidate=900, stale-if-error=7200', |
| 'slow-browser': 'public, s-maxage=900, stale-while-revalidate=60, stale-if-error=1800', |
| 'live-browser': null, |
| static: 'public, s-maxage=14400, stale-while-revalidate=3600, stale-if-error=28800', |
| daily: 'public, s-maxage=86400, stale-while-revalidate=14400, stale-if-error=172800', |
| 'no-store': null, |
| live: 'public, s-maxage=60, stale-while-revalidate=60, stale-if-error=300', |
| }; |
|
|
| const RPC_CACHE_TIER: Record<string, CacheTier> = { |
| |
| |
| |
| '/api/maritime/v1/get-vessel-snapshot': 'live', |
|
|
| '/api/market/v1/list-market-quotes': 'medium', |
| '/api/market/v1/list-crypto-quotes': 'medium', |
| '/api/market/v1/list-crypto-sectors': 'slow', |
| '/api/market/v1/list-defi-tokens': 'slow', |
| '/api/market/v1/list-ai-tokens': 'slow', |
| '/api/market/v1/list-other-tokens': 'slow', |
| '/api/market/v1/list-commodity-quotes': 'medium', |
| '/api/market/v1/list-stablecoin-markets': 'medium', |
| '/api/market/v1/get-sector-summary': 'medium', |
| '/api/market/v1/get-fear-greed-index': 'slow', |
| '/api/market/v1/get-market-breadth-history': 'daily', |
| '/api/market/v1/list-gulf-quotes': 'medium', |
| '/api/market/v1/analyze-stock': 'slow', |
| '/api/market/v1/get-stock-analysis-history': 'medium', |
| '/api/market/v1/backtest-stock': 'slow', |
| '/api/market/v1/list-stored-stock-backtests': 'medium', |
| '/api/infrastructure/v1/list-service-statuses': 'slow', |
| '/api/seismology/v1/list-earthquakes': 'slow', |
| '/api/infrastructure/v1/list-internet-outages': 'slow', |
| '/api/infrastructure/v1/list-internet-ddos-attacks': 'slow', |
| '/api/infrastructure/v1/list-internet-traffic-anomalies': 'slow', |
| '/api/forecast/v1/get-forecast-scorecard': 'fast', |
|
|
| '/api/unrest/v1/list-unrest-events': 'slow', |
| '/api/cyber/v1/list-cyber-threats': 'static', |
| '/api/conflict/v1/list-acled-events': 'slow', |
| '/api/military/v1/get-theater-posture': 'slow', |
| '/api/infrastructure/v1/get-temporal-baseline': 'slow', |
| '/api/aviation/v1/list-airport-delays': 'static', |
| '/api/aviation/v1/get-airport-ops-summary': 'static', |
| '/api/aviation/v1/list-airport-flights': 'static', |
| '/api/aviation/v1/get-carrier-ops': 'slow', |
| '/api/aviation/v1/get-flight-status': 'fast', |
| '/api/aviation/v1/track-aircraft': 'no-store', |
| '/api/aviation/v1/search-flight-prices': 'medium', |
| '/api/aviation/v1/search-google-flights': 'no-store', |
| '/api/aviation/v1/search-google-dates': 'medium', |
| '/api/aviation/v1/list-aviation-news': 'slow', |
| '/api/market/v1/get-country-stock-index': 'slow', |
|
|
| '/api/natural/v1/list-natural-events': 'slow', |
| '/api/wildfire/v1/list-fire-detections': 'static', |
| '/api/maritime/v1/list-navigational-warnings': 'static', |
| '/api/supply-chain/v1/get-china-corridor-control-towers': 'medium', |
| '/api/supply-chain/v1/get-shipping-rates': 'daily', |
| '/api/supply-chain/v1/list-pipelines': 'static', |
| '/api/supply-chain/v1/get-pipeline-detail': 'static', |
| '/api/supply-chain/v1/list-storage-facilities': 'static', |
| '/api/supply-chain/v1/get-storage-facility-detail': 'static', |
| '/api/supply-chain/v1/list-fuel-shortages': 'medium', |
| '/api/supply-chain/v1/get-fuel-shortage-detail': 'medium', |
| '/api/supply-chain/v1/list-energy-disruptions': 'medium', |
| '/api/economic/v1/get-fred-series': 'static', |
| '/api/economic/v1/get-bls-series': 'daily', |
| '/api/economic/v1/get-energy-prices': 'static', |
| '/api/research/v1/list-arxiv-papers': 'static', |
| '/api/research/v1/list-trending-repos': 'static', |
| '/api/giving/v1/get-giving-summary': 'static', |
| '/api/intelligence/v1/get-country-intel-brief': 'static', |
| |
| |
| |
| '/api/intelligence/v1/get-china-decision-signals': 'fast', |
| '/api/intelligence/v1/get-gdelt-topic-timeline': 'medium', |
| '/api/climate/v1/list-climate-anomalies': 'daily', |
| '/api/climate/v1/list-climate-disasters': 'daily', |
| '/api/climate/v1/get-co2-monitoring': 'daily', |
| '/api/climate/v1/get-ocean-ice-data': 'daily', |
| '/api/climate/v1/list-air-quality-data': 'fast', |
| '/api/climate/v1/list-climate-news': 'slow', |
| '/api/sanctions/v1/list-sanctions-pressure': 'daily', |
| '/api/sanctions/v1/lookup-sanction-entity': 'no-store', |
| '/api/radiation/v1/list-radiation-observations': 'slow', |
| '/api/thermal/v1/list-thermal-escalations': 'slow', |
| '/api/research/v1/list-tech-events': 'daily', |
| '/api/military/v1/get-usni-fleet-report': 'daily', |
| '/api/military/v1/list-defense-patents': 'daily', |
| '/api/conflict/v1/list-ucdp-events': 'daily', |
| '/api/conflict/v1/get-humanitarian-summary': 'daily', |
| '/api/conflict/v1/list-iran-events': 'slow', |
| '/api/displacement/v1/get-displacement-summary': 'daily', |
| '/api/displacement/v1/get-population-exposure': 'daily', |
| '/api/economic/v1/get-bis-policy-rates': 'daily', |
| '/api/economic/v1/get-bis-exchange-rates': 'daily', |
| '/api/economic/v1/get-bis-credit': 'daily', |
| '/api/trade/v1/get-tariff-trends': 'daily', |
| '/api/trade/v1/get-trade-flows': 'daily', |
| '/api/trade/v1/get-trade-barriers': 'daily', |
| '/api/trade/v1/get-trade-restrictions': 'daily', |
| '/api/trade/v1/get-customs-revenue': 'daily', |
| '/api/trade/v1/list-comtrade-flows': 'daily', |
| '/api/economic/v1/list-world-bank-indicators': 'daily', |
| '/api/economic/v1/get-energy-capacity': 'daily', |
| '/api/economic/v1/list-grocery-basket-prices': 'daily', |
| '/api/economic/v1/list-bigmac-prices': 'daily', |
| '/api/economic/v1/list-fuel-prices': 'daily', |
| '/api/economic/v1/get-fao-food-price-index': 'daily', |
| '/api/economic/v1/get-crude-inventories': 'daily', |
| '/api/economic/v1/get-nat-gas-storage': 'daily', |
| '/api/economic/v1/get-eu-yield-curve': 'daily', |
| '/api/supply-chain/v1/get-critical-minerals': 'daily', |
| '/api/military/v1/get-aircraft-details': 'static', |
| '/api/military/v1/get-wingbits-status': 'static', |
| '/api/military/v1/get-wingbits-live-flight': 'no-store', |
|
|
| '/api/military/v1/list-military-flights': 'slow', |
| '/api/market/v1/list-etf-flows': 'slow', |
| '/api/research/v1/list-hackernews-items': 'slow', |
| '/api/intelligence/v1/get-country-risk': 'slow', |
| '/api/intelligence/v1/get-risk-scores': 'slow', |
| '/api/intelligence/v1/get-pizzint-status': 'slow', |
| '/api/intelligence/v1/classify-event': 'static', |
| '/api/intelligence/v1/search-gdelt-documents': 'slow', |
| '/api/infrastructure/v1/get-cable-health': 'slow', |
| '/api/positive-events/v1/list-positive-geo-events': 'slow', |
|
|
| '/api/military/v1/list-military-bases': 'daily', |
| '/api/economic/v1/get-macro-signals': 'medium', |
| '/api/economic/v1/get-national-debt': 'daily', |
| '/api/prediction/v1/list-prediction-markets': 'medium', |
| '/api/forecast/v1/get-forecasts': 'medium', |
| '/api/forecast/v1/get-simulation-package': 'slow', |
| '/api/forecast/v1/get-simulation-outcome': 'slow', |
| '/api/supply-chain/v1/get-chokepoint-status': 'medium', |
| '/api/supply-chain/v1/get-chokepoint-history': 'slow', |
| '/api/news/v1/list-feed-digest': 'slow', |
| '/api/intelligence/v1/get-country-facts': 'daily', |
| '/api/intelligence/v1/list-security-advisories': 'slow', |
| '/api/intelligence/v1/list-satellites': 'static', |
| '/api/intelligence/v1/list-gps-interference': 'slow', |
| '/api/intelligence/v1/list-cross-source-signals': 'medium', |
| '/api/intelligence/v1/list-oref-alerts': 'fast', |
| '/api/intelligence/v1/list-telegram-feed': 'fast', |
| '/api/intelligence/v1/get-company-enrichment': 'slow', |
| '/api/intelligence/v1/list-company-signals': 'slow', |
| '/api/intelligence/v1/search-sec-filings': 'medium', |
| '/api/intelligence/v1/list-material-events': 'medium', |
| '/api/news/v1/summarize-article-cache': 'slow', |
|
|
| '/api/imagery/v1/search-imagery': 'static', |
|
|
| '/api/infrastructure/v1/list-temporal-anomalies': 'medium', |
| '/api/infrastructure/v1/get-ip-geo': 'no-store', |
| '/api/infrastructure/v1/reverse-geocode': 'slow', |
| '/api/infrastructure/v1/get-bootstrap-data': 'no-store', |
| '/api/webcam/v1/get-webcam-image': 'no-store', |
| '/api/webcam/v1/list-webcams': 'no-store', |
|
|
| '/api/consumer-prices/v1/get-consumer-price-overview': 'slow', |
| '/api/consumer-prices/v1/get-consumer-price-basket-series': 'slow', |
| '/api/consumer-prices/v1/list-consumer-price-categories': 'slow', |
| '/api/consumer-prices/v1/list-consumer-price-movers': 'slow', |
| '/api/consumer-prices/v1/list-retailer-price-spreads': 'slow', |
| '/api/consumer-prices/v1/get-consumer-price-freshness': 'slow', |
|
|
| '/api/aviation/v1/get-youtube-live-stream-info': 'fast', |
|
|
| '/api/market/v1/list-earnings-calendar': 'slow', |
| '/api/market/v1/get-cot-positioning': 'slow', |
| '/api/market/v1/get-gold-intelligence': 'slow', |
| '/api/market/v1/get-hyperliquid-flow': 'medium', |
| '/api/market/v1/get-insider-transactions': 'slow', |
| '/api/economic/v1/get-economic-calendar': 'slow', |
| '/api/economic/v1/get-china-macro-snapshot': 'slow', |
| '/api/economic/v1/get-china-activity-nowcast': 'medium', |
| '/api/intelligence/v1/list-market-implications': 'slow', |
| '/api/economic/v1/get-ecb-fx-rates': 'slow', |
| '/api/economic/v1/get-eurostat-country-data': 'slow', |
| '/api/economic/v1/get-eu-gas-storage': 'slow', |
| '/api/economic/v1/get-oil-stocks-analysis': 'static', |
| '/api/economic/v1/get-oil-inventories': 'slow', |
| '/api/economic/v1/get-energy-crisis-policies': 'static', |
| '/api/economic/v1/list-global-tenders': 'medium', |
| '/api/economic/v1/get-eu-fsi': 'slow', |
| '/api/economic/v1/get-economic-stress': 'slow', |
| '/api/supply-chain/v1/get-shipping-stress': 'medium', |
| '/api/supply-chain/v1/get-country-chokepoint-index': 'slow-browser', |
| '/api/supply-chain/v1/get-bypass-options': 'slow-browser', |
| '/api/supply-chain/v1/get-country-cost-shock': 'slow-browser', |
| '/api/supply-chain/v1/get-country-products': 'slow-browser', |
| '/api/supply-chain/v1/get-multi-sector-cost-shock': 'slow-browser', |
| '/api/supply-chain/v1/get-sector-dependency': 'slow-browser', |
| '/api/supply-chain/v1/get-route-explorer-lane': 'slow-browser', |
| '/api/supply-chain/v1/get-route-impact': 'slow-browser', |
| |
| |
| |
| |
| '/api/scenario/v1/list-scenario-templates': 'daily', |
| '/api/scenario/v1/get-scenario-status': 'slow-browser', |
| '/api/health/v1/list-disease-outbreaks': 'slow', |
| '/api/health/v1/list-air-quality-alerts': 'fast', |
| '/api/intelligence/v1/get-social-velocity': 'fast', |
| '/api/intelligence/v1/get-country-energy-profile': 'slow', |
| '/api/intelligence/v1/compute-energy-shock': 'fast', |
| '/api/intelligence/v1/get-country-port-activity': 'slow', |
| |
| |
| |
| |
| |
| '/api/intelligence/v1/get-regional-snapshot': 'slow', |
| |
| |
| |
| '/api/intelligence/v1/get-regime-history': 'slow', |
| |
| '/api/intelligence/v1/get-regional-brief': 'slow', |
| |
| |
| |
| '/api/intelligence/v1/get-intel-timeline': 'slow', |
| '/api/resilience/v1/get-resilience-score': 'slow', |
| '/api/resilience/v1/get-resilience-ranking': 'slow', |
| '/api/resilience/v1/get-runtime-manifest': 'no-store', |
|
|
| |
| |
| '/api/v2/shipping/route-intelligence': 'slow-browser', |
| |
| |
| '/api/v2/shipping/webhooks': 'slow-browser', |
| }; |
|
|
| import { PREMIUM_RPC_PATHS } from '../src/shared/premium-paths'; |
|
|
| export const PUBLIC_NO_AUTH_RPC_PATHS = new Set<string>([ |
| '/api/conflict/v1/list-acled-events', |
| '/api/natural/v1/list-natural-events', |
| '/api/intelligence/v1/get-china-decision-signals', |
| '/api/resilience/v1/get-runtime-manifest', |
| '/api/seismology/v1/list-earthquakes', |
| '/api/unrest/v1/list-unrest-events', |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| '/api/leads/v1/submit-contact', |
| '/api/leads/v1/register-interest', |
| ]); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export const RELAY_WARM_PING_PATHS = new Set<string>([ |
| '/api/infrastructure/v1/list-service-statuses', |
| '/api/infrastructure/v1/get-cable-health', |
| '/api/infrastructure/v1/list-temporal-anomalies', |
| '/api/intelligence/v1/get-risk-scores', |
| '/api/supply-chain/v1/get-chokepoint-status', |
| ]); |
|
|
| |
| |
| |
| |
| |
| |
| export type GatewayCtx = { waitUntil: (p: Promise<unknown>) => void }; |
|
|
| const POST_TO_GET_MAX_BODY_BYTES = 1_048_576; |
| const POST_TO_GET_MAX_ARRAY_VALUES_PER_KEY = 200; |
|
|
| export const REQUIRED_BBOX_QUERY_PARAMS = ['sw_lat', 'sw_lon', 'ne_lat', 'ne_lon'] as const; |
|
|
| |
| |
| export const REQUIRED_BBOX_RPC_PATHS = [ |
| '/api/military/v1/list-military-bases', |
| '/api/military/v1/list-military-flights', |
| ] as const; |
|
|
| const REQUIRED_BBOX_RPC_PATH_SET = new Set<string>(REQUIRED_BBOX_RPC_PATHS); |
| const MILITARY_BBOX_DIAGNOSTIC_PATH_SET = new Set<string>(REQUIRED_BBOX_RPC_PATHS); |
|
|
| function isPostToGetCompatibleBodySize(headers: Headers): boolean { |
| const rawContentLength = headers.get('Content-Length'); |
| if (rawContentLength === null || !/^\d+$/.test(rawContentLength)) return false; |
|
|
| const contentLength = Number(rawContentLength); |
| return Number.isSafeInteger(contentLength) && contentLength < POST_TO_GET_MAX_BODY_BYTES; |
| } |
|
|
| function getRequiredBboxQueryProblems(searchParams: URLSearchParams): { missing: string[]; invalid: string[]; allZero: boolean } { |
| const absent: string[] = []; |
| const invalid: string[] = []; |
| const values: number[] = []; |
|
|
| for (const param of REQUIRED_BBOX_QUERY_PARAMS) { |
| const raw = searchParams.get(param); |
| if (raw == null) { |
| absent.push(param); |
| continue; |
| } |
| if (raw.trim() === '') { |
| invalid.push(param); |
| continue; |
| } |
| const value = Number(raw); |
| if (!Number.isFinite(value)) { |
| invalid.push(param); |
| continue; |
| } |
| values.push(value); |
| } |
|
|
| const missing = absent.length === REQUIRED_BBOX_QUERY_PARAMS.length ? [...REQUIRED_BBOX_QUERY_PARAMS] : []; |
| return { |
| missing, |
| invalid, |
| allZero: absent.length === 0 && invalid.length === 0 && values.every((value) => value === 0), |
| }; |
| } |
|
|
| type RequiredBboxDiagnostic = { |
| status: 'missing' | 'invalid'; |
| missing: string[]; |
| invalid: string[]; |
| }; |
|
|
| function getRequiredBboxDiagnostic(request: Request, pathname: string): RequiredBboxDiagnostic | null { |
| if (!REQUIRED_BBOX_RPC_PATH_SET.has(pathname)) return null; |
|
|
| const { searchParams } = new URL(request.url); |
| const { missing, invalid, allZero } = getRequiredBboxQueryProblems(searchParams); |
| if (missing.length === 0 && invalid.length === 0 && !allZero) return null; |
|
|
| return { |
| status: missing.length > 0 ? 'missing' : 'invalid', |
| missing, |
| invalid: allZero ? [...REQUIRED_BBOX_QUERY_PARAMS] : invalid, |
| }; |
| } |
|
|
| function attachRequiredBboxDiagnosticHeaders( |
| headers: Headers, |
| pathname: string, |
| diagnostic: RequiredBboxDiagnostic | null, |
| ): void { |
| if (!diagnostic) return; |
| headers.set('X-WorldMonitor-Bbox', diagnostic.status); |
| if (diagnostic.missing.length > 0) headers.set('X-WorldMonitor-Bbox-Missing', diagnostic.missing.join(',')); |
| if (diagnostic.invalid.length > 0) headers.set('X-WorldMonitor-Bbox-Invalid', diagnostic.invalid.join(',')); |
| if (MILITARY_BBOX_DIAGNOSTIC_PATH_SET.has(pathname)) { |
| |
| headers.set('X-Military-Bbox', diagnostic.status); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| function cloneRequestWithHeaders(request: Request, headers: Headers): Request { |
| return new Request(request, { headers }); |
| } |
|
|
| function stripClientUserIdHeader(request: Request): Request { |
| if (!request.headers.has(TRUSTED_USER_ID_HEADER)) return request; |
| const headers = new Headers(request.headers); |
| headers.delete(TRUSTED_USER_ID_HEADER); |
| return cloneRequestWithHeaders(request, headers); |
| } |
|
|
| function withAuthenticatedUserId(request: Request, userId: string): Request { |
| const headers = new Headers(request.headers); |
| headers.set(TRUSTED_USER_ID_HEADER, userId); |
| return cloneRequestWithHeaders(request, headers); |
| } |
|
|
| function normalizeAuthError(error: string | undefined): string { |
| if (!error || error === USER_API_KEY_GATEWAY_VALIDATION_ERROR) return 'Invalid API key'; |
| return error; |
| } |
|
|
| function createGatewayAuthErrorResponse( |
| status: 401 | 403, |
| error: string | undefined, |
| corsHeaders: Record<string, string>, |
| ): Response { |
| return new Response(JSON.stringify({ error: normalizeAuthError(error) }), { |
| status, |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Cache-Control': 'no-store', |
| ...corsHeaders, |
| }, |
| }); |
| } |
|
|
| const GATEWAY_DIRECT_LLM_QUOTA_METHODS: Record<string, string> = { |
| '/api/intelligence/v1/classify-event': 'GET', |
| '/api/intelligence/v1/deduct-situation': 'POST', |
| '/api/intelligence/v1/get-country-intel-brief': 'GET', |
| '/api/market/v1/analyze-stock': 'GET', |
| '/api/news/v1/summarize-article': 'POST', |
| }; |
|
|
| async function shouldReserveGatewayDirectLlmQuota(request: Request, pathname: string): Promise<boolean> { |
| if (!DIRECT_LLM_GATEWAY_QUOTA_PATHS.has(pathname)) return false; |
| if (GATEWAY_DIRECT_LLM_QUOTA_METHODS[pathname] !== request.method) return false; |
| if (pathname !== '/api/news/v1/summarize-article') return true; |
|
|
| const contentLength = Number(request.headers.get('Content-Length') ?? '0'); |
| if (Number.isFinite(contentLength) && contentLength >= POST_TO_GET_MAX_BODY_BYTES) { |
| return true; |
| } |
| try { |
| const body = await request.clone().json() as { mode?: unknown }; |
| return body.mode !== 'translate'; |
| } catch { |
| |
| |
| return false; |
| } |
| } |
|
|
| function createDirectLlmQuotaFailureResponse( |
| reservation: Awaited<ReturnType<typeof reserveDirectLlmQuota>>, |
| corsHeaders: Record<string, string>, |
| ): Response { |
| if (reservation.ok) { |
| throw new Error('createDirectLlmQuotaFailureResponse called for successful reservation'); |
| } |
|
|
| if (reservation.reason === 'cap-exceeded') { |
| return new Response(JSON.stringify({ |
| error: 'Direct LLM daily quota exceeded', |
| limit: DIRECT_LLM_DAILY_QUOTA_LIMIT, |
| resetsAt: 'next UTC midnight', |
| }), { |
| status: 429, |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Cache-Control': 'no-store', |
| 'Retry-After': String(reservation.retryAfterSec), |
| ...corsHeaders, |
| }, |
| }); |
| } |
|
|
| return new Response(JSON.stringify({ error: 'Direct LLM quota unavailable' }), { |
| status: 503, |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Cache-Control': 'no-store', |
| 'Retry-After': String(reservation.retryAfterSec), |
| ...corsHeaders, |
| }, |
| }); |
| } |
|
|
| function markAuthErrorNoStore(response: Response): Response { |
| response.headers.set('Cache-Control', 'no-store'); |
| response.headers.delete('CDN-Cache-Control'); |
| response.headers.delete('Vercel-CDN-Cache-Control'); |
| return response; |
| } |
|
|
| function hasCredentialBearingHeader(request: Request): boolean { |
| return Boolean( |
| request.headers.get('Authorization') || |
| request.headers.get('X-WorldMonitor-Key') || |
| request.headers.get('X-Api-Key') || |
| request.headers.get('Cookie'), |
| ); |
| } |
|
|
| async function isResilienceRankingSeedRefreshRequest(request: Request, pathname: string): Promise<boolean> { |
| if (pathname !== '/api/resilience/v1/get-resilience-ranking') return false; |
| const expected = process.env.WORLDMONITOR_SEED_REFRESH_KEY?.trim() ?? ''; |
| if (!expected) return false; |
| try { |
| const url = new URL(request.url); |
| if (url.searchParams.get('refresh') !== '1') return false; |
| } catch { |
| return false; |
| } |
| const candidate = request.headers.get('X-WorldMonitor-Key') ?? ''; |
| return timingSafeEqual(candidate, expected); |
| } |
|
|
| |
| |
| |
| |
| |
| export async function isRelayWarmPingRequest(request: Request, pathname: string): Promise<boolean> { |
| if (!RELAY_WARM_PING_PATHS.has(pathname)) return false; |
| const expected = process.env.WORLDMONITOR_RELAY_KEY?.trim() ?? ''; |
| if (!expected) return false; |
| const candidate = request.headers.get('X-WorldMonitor-Key') ?? ''; |
| return timingSafeEqual(candidate, expected); |
| } |
|
|
| function assertProMcpGatewayHmacConfig(): void { |
| const proGrantSecret = process.env.MCP_PRO_GRANT_HMAC_SECRET?.trim() ?? ''; |
| const internalSecret = process.env.MCP_INTERNAL_HMAC_SECRET?.trim() ?? ''; |
| if (proGrantSecret && !internalSecret) { |
| throw new Error('MCP_INTERNAL_HMAC_SECRET must be configured when MCP_PRO_GRANT_HMAC_SECRET is set'); |
| } |
| } |
|
|
| export function createDomainGateway( |
| routes: RouteDescriptor[], |
| ): (req: Request, ctx?: GatewayCtx) => Promise<Response> { |
| assertProMcpGatewayHmacConfig(); |
| const router = createRouter(routes); |
|
|
| return async function handler(originalRequest: Request, ctx?: GatewayCtx): Promise<Response> { |
| let request = stripClientUserIdHeader(originalRequest); |
| const rawPathname = new URL(request.url).pathname; |
| const pathname = rawPathname.length > 1 ? rawPathname.replace(/\/+$/, '') : rawPathname; |
| const t0 = Date.now(); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| const rawWidgetKey = request.headers.get('x-widget-key') ?? null; |
| const widgetAgentKey = process.env.WIDGET_AGENT_KEY ?? ''; |
| const validatedWidgetKey = |
| await timingSafeEqualSecret(rawWidgetKey, widgetAgentKey) ? rawWidgetKey : null; |
| const usage: UsageIdentityInput = { |
| sessionUserId: null, |
| isUserApiKey: false, |
| enterpriseApiKey: null, |
| widgetKey: validatedWidgetKey, |
| clerkOrgId: null, |
| userApiKeyCustomerRef: null, |
| tier: null, |
| planKey: null, |
| }; |
| function recordUsageEntitlement(ent: CachedEntitlements | null): void { |
| if (!ent) return; |
| |
| |
| |
| |
| |
| |
| |
| if (ent.verificationUnavailable) return; |
| usage.tier = typeof ent.features.tier === 'number' ? ent.features.tier : 0; |
| usage.planKey = ent.planKey; |
| } |
| |
| |
| |
| const _parts = pathname.split('/'); |
| const domain = (/^v\d+$/.test(_parts[2] ?? '') ? _parts[3] : _parts[2]) ?? ''; |
| const reqBytes = deriveReqBytes(request); |
|
|
| |
| |
| |
| |
| let pendingShadowReason: RequestReason | null = null; |
| |
| |
| function denyForBillingVerification( |
| ent: CachedEntitlements | null | undefined, |
| cors: Record<string, string>, |
| capabilityCovered = false, |
| ): Response | null { |
| if (capabilityCovered) return null; |
| const billingDenial = getBillingVerificationDenial(ent, cors); |
| if (!billingDenial) return null; |
| emitRequest( |
| billingDenial.status, |
| billingDenial.status === 503 ? 'billing_verification_503' : 'tier_403', |
| null, |
| ); |
| return billingDenial; |
| } |
| function emitRequest(status: number, reason: RequestReason, cacheTier: UsageCacheTier | null, resBytes = 0): void { |
| if (!ctx?.waitUntil) return; |
| const effectiveReason: RequestReason = |
| pendingShadowReason && status < 400 ? pendingShadowReason : reason; |
| const identity = buildUsageIdentity(usage); |
| |
| |
| |
| |
| ctx.waitUntil((async () => { |
| const uaHash = await deriveUaHash(originalRequest); |
| await deliverUsageEvents([ |
| buildRequestEvent({ |
| requestId: deriveRequestId(originalRequest), |
| domain, |
| route: pathname, |
| method: originalRequest.method, |
| status, |
| durationMs: Date.now() - t0, |
| reqBytes, |
| resBytes, |
| customerId: identity.customer_id, |
| principalId: identity.principal_id, |
| authKind: identity.auth_kind, |
| tier: identity.tier, |
| planKey: identity.plan_key, |
| country: deriveCountry(originalRequest), |
| ipCity: deriveIpCity(originalRequest), |
| ipRegion: deriveIpRegion(originalRequest), |
| executionRegion: deriveExecutionRegion(originalRequest), |
| executionPlane: 'vercel-edge', |
| originKind: deriveOriginKind(originalRequest), |
| cacheTier, |
| ip: deriveIp(originalRequest), |
| userAgent: deriveUserAgent(originalRequest), |
| uaHash, |
| referer: deriveReferer(originalRequest), |
| acceptLanguage: deriveAcceptLanguage(originalRequest), |
| host: deriveHost(originalRequest), |
| sentryTraceId: deriveSentryTraceId(originalRequest), |
| reason: effectiveReason, |
| }), |
| ]); |
| })()); |
| } |
|
|
| |
| if (isDisallowedOrigin(request)) { |
| emitRequest(403, 'origin_403', null); |
| return new Response(JSON.stringify({ error: 'Origin not allowed' }), { |
| status: 403, |
| headers: { 'Content-Type': 'application/json' }, |
| }); |
| } |
|
|
| |
| |
| |
| |
| let corsHeaders: Record<string, string>; |
| try { |
| corsHeaders = getCorsHeaders(request); |
| } catch (err) { |
| |
| |
| |
| |
| |
| const captured = captureSilentError(err, { |
| tags: { route: 'gateway', step: 'cors_headers' }, |
| }); |
| ctx?.waitUntil(captured); |
| emitRequest(500, 'cors_error', null); |
| return new Response(JSON.stringify({ error: 'Internal server error' }), { |
| status: 500, |
| headers: { |
| 'Content-Type': 'application/json', |
| |
| |
| 'Cache-Control': 'no-store', |
| }, |
| }); |
| } |
|
|
| |
| if (request.method === 'OPTIONS') { |
| emitRequest(204, 'preflight', null); |
| return new Response(null, { status: 204, headers: corsHeaders }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| { |
| const inboundHeaders = request.headers; |
| if ( |
| inboundHeaders.has(INTERNAL_MCP_VERIFIED_HEADER) || |
| inboundHeaders.has(TRUSTED_USER_ID_HEADER) |
| ) { |
| const stripped = new Headers(inboundHeaders); |
| stripped.delete(INTERNAL_MCP_VERIFIED_HEADER); |
| stripped.delete(TRUSTED_USER_ID_HEADER); |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const reInit: RequestInit = { method: request.method, headers: stripped }; |
| if (request.method !== 'GET' && request.method !== 'HEAD') { |
| const contentLen = parseInt(request.headers.get('Content-Length') ?? '0', 10); |
| if (Number.isFinite(contentLen) && contentLen > MAX_INTERNAL_MCP_BODY) { |
| |
| |
| emitRequest(413, 'malformed_request', null); |
| return new Response(JSON.stringify({ error: 'payload_too_large' }), { |
| status: 413, |
| headers: { 'Content-Type': 'application/json', ...corsHeaders }, |
| }); |
| } |
| try { |
| const bytes = await request.clone().arrayBuffer(); |
| |
| |
| |
| if (bytes.byteLength > MAX_INTERNAL_MCP_BODY) { |
| emitRequest(413, 'malformed_request', null); |
| return new Response(JSON.stringify({ error: 'payload_too_large' }), { |
| status: 413, |
| headers: { 'Content-Type': 'application/json', ...corsHeaders }, |
| }); |
| } |
| reInit.body = bytes; |
| } catch { |
| |
| |
| |
| |
| |
| emitRequest(400, 'malformed_request', null); |
| return new Response(JSON.stringify({ error: 'malformed_request' }), { |
| status: 400, |
| headers: { 'Content-Type': 'application/json', ...corsHeaders }, |
| }); |
| } |
| } |
| request = new Request(request.url, reInit); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let internalMcpVerified = false; |
| if (request.headers.has(INTERNAL_MCP_SIG_HEADER)) { |
| const hmacSecret = process.env.MCP_INTERNAL_HMAC_SECRET ?? ''; |
| if (!hmacSecret) { |
| |
| |
| |
| |
| emitRequest(500, 'auth_401', null); |
| return new Response( |
| JSON.stringify({ error: 'CONFIGURATION', detail: 'MCP_INTERNAL_HMAC_SECRET not configured' }), |
| { status: 500, headers: { 'Content-Type': 'application/json', ...corsHeaders } }, |
| ); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let bodyBytes: ArrayBuffer | null = null; |
| if (request.method !== 'GET' && request.method !== 'HEAD') { |
| |
| |
| const contentLen = parseInt(request.headers.get('Content-Length') ?? '0', 10); |
| if (Number.isFinite(contentLen) && contentLen > MAX_INTERNAL_MCP_BODY) { |
| emitRequest(413, 'malformed_request', null); |
| return new Response(JSON.stringify({ error: 'payload_too_large' }), { |
| status: 413, |
| headers: { 'Content-Type': 'application/json', ...corsHeaders }, |
| }); |
| } |
| try { |
| bodyBytes = await request.clone().arrayBuffer(); |
| } catch { |
| emitRequest(401, 'auth_401', null); |
| return new Response( |
| JSON.stringify({ error: 'invalid_internal_mcp_signature' }), |
| { status: 401, headers: { 'Content-Type': 'application/json', ...corsHeaders } }, |
| ); |
| } |
| if (bodyBytes.byteLength > MAX_INTERNAL_MCP_BODY) { |
| emitRequest(413, 'malformed_request', null); |
| return new Response(JSON.stringify({ error: 'payload_too_large' }), { |
| status: 413, |
| headers: { 'Content-Type': 'application/json', ...corsHeaders }, |
| }); |
| } |
| |
| |
| request = new Request(request.url, { |
| method: request.method, |
| headers: request.headers, |
| body: bodyBytes, |
| }); |
| } |
| |
| |
| |
| |
| |
| const verified = await verifyInternalMcpRequest(request, hmacSecret); |
| if (!verified) { |
| emitRequest(401, 'auth_401', null); |
| return new Response( |
| JSON.stringify({ error: 'invalid_internal_mcp_signature' }), |
| { status: 401, headers: { 'Content-Type': 'application/json', ...corsHeaders } }, |
| ); |
| } |
| const replayClaim = await claimInternalMcpReplayNonce(verified.userId, verified.nonce); |
| if (replayClaim === 'unavailable') { |
| |
| |
| emitRequest(503, 'replay_cache_unavailable', null); |
| return new Response( |
| JSON.stringify({ error: 'internal_mcp_replay_cache_unavailable' }), |
| { status: 503, headers: { 'Content-Type': 'application/json', ...corsHeaders } }, |
| ); |
| } |
| if (replayClaim === 'replay') { |
| emitRequest(401, 'auth_401', null); |
| return new Response( |
| JSON.stringify({ error: 'invalid_internal_mcp_signature' }), |
| { status: 401, headers: { 'Content-Type': 'application/json', ...corsHeaders } }, |
| ); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const ent = await getEntitlements(verified.userId); |
| |
| |
| const gate = checkProMcpAccess(ent, Date.now()); |
| const mcpCovered = gate === null; |
| const billingDenial = denyForBillingVerification( |
| ent, |
| corsHeaders, |
| mcpCovered, |
| ); |
| if (billingDenial) return billingDenial; |
| if (!mcpCovered) { |
| emitRequest(401, 'auth_401', null); |
| return new Response( |
| JSON.stringify({ error: 'insufficient_entitlement' }), |
| { status: 401, headers: { 'Content-Type': 'application/json', ...corsHeaders } }, |
| ); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const trusted = new Headers(request.headers); |
| trusted.delete(INTERNAL_MCP_SIG_HEADER); |
| trusted.delete(INTERNAL_MCP_USER_ID_HEADER); |
| trusted.delete(INTERNAL_MCP_NONCE_HEADER); |
| trusted.set(INTERNAL_MCP_VERIFIED_HEADER, getInternalMcpVerifiedNonce()); |
| trusted.set(TRUSTED_USER_ID_HEADER, verified.userId); |
| const rebuildInit: RequestInit = { method: request.method, headers: trusted }; |
| if (bodyBytes !== null) rebuildInit.body = bodyBytes; |
| request = new Request(request.url, rebuildInit); |
| usage.sessionUserId = verified.userId; |
| recordUsageEntitlement(ent); |
| internalMcpVerified = true; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const isPublicNoAuthRpc = PUBLIC_NO_AUTH_RPC_PATHS.has(pathname) |
| || isPublicSharedRpcRequest(request.url, request.method); |
| const seedRefreshVerified = await isResilienceRankingSeedRefreshRequest(request, pathname); |
| const relayWarmPingVerified = await isRelayWarmPingRequest(request, pathname); |
| const requiresDirectLlmQuota = !internalMcpVerified && await shouldReserveGatewayDirectLlmQuota(request, pathname); |
| const isTierGated = !internalMcpVerified && !isPublicNoAuthRpc && !seedRefreshVerified && !relayWarmPingVerified && getRequiredTier(pathname) !== null; |
| const needsLegacyProBearerGate = !internalMcpVerified && !isPublicNoAuthRpc && PREMIUM_RPC_PATHS.has(pathname) && !isTierGated; |
| const isProFreshCacheRpc = PRO_FRESH_CACHE_RPC_PATHS.has(pathname); |
| const needsProFreshnessResolution = |
| !internalMcpVerified && |
| !isPublicNoAuthRpc && |
| isProFreshCacheRpc && |
| request.headers.get('Authorization')?.startsWith('Bearer ') === true; |
| let rateLimitPrincipalUserId: string | undefined; |
|
|
| |
| |
| |
| let sessionUserId: string | null = null; |
| let sessionRole: 'free' | 'pro' | null = null; |
| if (isTierGated || requiresDirectLlmQuota || needsProFreshnessResolution) { |
| const session = await resolveClerkSession(request); |
| sessionUserId = session?.userId ?? null; |
| sessionRole = session?.role ?? null; |
| usage.sessionUserId = sessionUserId; |
| usage.clerkOrgId = session?.orgId ?? null; |
| if (sessionUserId) { |
| request = withAuthenticatedUserId(request, sessionUserId); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let keyCheck: { valid: boolean; required: boolean; error?: string; kind?: 'enterprise' | 'session' | 'user' } = internalMcpVerified || isPublicNoAuthRpc || seedRefreshVerified || relayWarmPingVerified |
| ? { valid: true, required: false } |
| : ((await validateApiKey(request, { |
| forceKey: (isTierGated && !sessionUserId) || needsLegacyProBearerGate, |
| })) as { valid: boolean; required: boolean; error?: string; kind?: 'enterprise' | 'session' | 'user' }); |
|
|
| |
| |
| |
| |
| |
| |
| |
| let isUserApiKey = false; |
| const wmKey = |
| request.headers.get('X-WorldMonitor-Key') ?? |
| request.headers.get('X-Api-Key') ?? |
| ''; |
| if (keyCheck.required && !keyCheck.valid && wmKey.startsWith('wm_')) { |
| |
| |
| |
| |
| |
| |
| |
| const validationGuardResponse = await checkFailClosedScopedIpRateLimit( |
| request, |
| 'user-api-key:pre-auth-validation', |
| 600, |
| '60 s', |
| corsHeaders, |
| ); |
| if (validationGuardResponse) { |
| const reason = getRateLimitTelemetryReason( |
| validationGuardResponse, |
| 'rate_limit_429', |
| ); |
| emitRequest(validationGuardResponse.status, reason, null); |
| return validationGuardResponse; |
| } |
|
|
| |
| |
| |
| |
| const { validateUserApiKey } = await import('./_shared/user-api-key'); |
| try { |
| const userKeyResult = await validateUserApiKey(wmKey); |
| if (userKeyResult) { |
| isUserApiKey = true; |
| usage.isUserApiKey = true; |
| usage.userApiKeyCustomerRef = userKeyResult.userId; |
| keyCheck = { valid: true, required: true }; |
| |
| |
| |
| |
| |
| sessionUserId = userKeyResult.userId; |
| |
| |
| |
| sessionRole = null; |
| usage.sessionUserId = sessionUserId; |
| usage.clerkOrgId = null; |
| request = withAuthenticatedUserId(request, sessionUserId); |
| } |
| } catch (err) { |
| |
| |
| |
| |
| const code = |
| typeof err === 'object' && err !== null |
| ? (err as { code?: unknown }).code |
| : undefined; |
| if (code === 'validation_unavailable') { |
| emitRequest(503, 'validation_unavailable', null); |
| return new Response(JSON.stringify({ error: 'Service temporarily unavailable' }), { |
| status: 503, |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Cache-Control': 'no-store', |
| 'Retry-After': '5', |
| 'X-Validation-Mode': 'degraded', |
| ...corsHeaders, |
| }, |
| }); |
| } |
| throw err; |
| } |
| } |
|
|
| |
| |
| |
| |
| if ( |
| (isTierGated || requiresDirectLlmQuota || needsProFreshnessResolution) && |
| sessionUserId && |
| keyCheck.required && |
| !keyCheck.valid |
| ) { |
| keyCheck = { valid: true, required: false }; |
| } |
|
|
| |
| |
| |
| |
| |
| if (keyCheck.valid && wmKey && !isUserApiKey && keyCheck.kind === 'enterprise') { |
| usage.enterpriseApiKey = wmKey; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let userKeyEntitlement: CachedEntitlements | null | undefined; |
| if (isUserApiKey && sessionUserId) { |
| userKeyEntitlement = await getEntitlements(sessionUserId); |
| recordUsageEntitlement(userKeyEntitlement); |
| const apiAccessCovered = !!userKeyEntitlement && |
| userKeyEntitlement.features.apiAccess && |
| (userKeyEntitlement.validUntil ?? 0) >= Date.now(); |
| const billingDenial = denyForBillingVerification( |
| userKeyEntitlement, |
| corsHeaders, |
| apiAccessCovered, |
| ); |
| if (billingDenial) return billingDenial; |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if (!userKeyEntitlement) { |
| if (isEntitlementBackendConfigured()) { |
| emitRequest(503, 'billing_verification_503', null); |
| return new Response( |
| JSON.stringify({ |
| error: 'Unable to verify API access', |
| code: 'entitlement_verification_unavailable', |
| }), |
| { |
| status: 503, |
| headers: { |
| ...corsHeaders, |
| 'Content-Type': 'application/json', |
| 'Cache-Control': 'no-store', |
| 'Retry-After': '5', |
| 'X-Billing-Verification': 'entitlement_verification_unavailable', |
| }, |
| }, |
| ); |
| } |
| console.error( |
| '[gateway] entitlement backend unconfigured (CONVEX_SITE_URL / shared secret missing) — serving wm_-key request fail-open', |
| ); |
| } else if ( |
| !userKeyEntitlement.features.apiAccess || |
| (userKeyEntitlement.validUntil ?? 0) < Date.now() |
| ) { |
| emitRequest(403, 'tier_403', null); |
| return createGatewayAuthErrorResponse( |
| 403, |
| 'API access requires an active subscription', |
| corsHeaders, |
| ); |
| } else { |
| |
| |
| rateLimitPrincipalUserId = sessionUserId; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| let hasProFreshCacheAccess = internalMcpVerified && isProFreshCacheRpc; |
| if (!hasProFreshCacheAccess && isProFreshCacheRpc && sessionUserId) { |
| const ent = |
| userKeyEntitlement !== undefined |
| ? userKeyEntitlement |
| : await getEntitlements(sessionUserId); |
| recordUsageEntitlement(ent); |
| hasProFreshCacheAccess = |
| !!ent && |
| ent.features.tier >= 1 && |
| ent.validUntil >= Date.now(); |
| if (hasProFreshCacheAccess) { |
| rateLimitPrincipalUserId = sessionUserId; |
| } |
| } |
|
|
| if (keyCheck.required && !keyCheck.valid) { |
| if (needsLegacyProBearerGate) { |
| const authHeader = request.headers.get('Authorization'); |
| if (authHeader?.startsWith('Bearer ')) { |
| const { validateBearerToken } = await import('./auth-session'); |
| const session = await validateBearerToken(authHeader.slice(7)); |
| if (!session.valid) { |
| emitRequest(401, 'auth_401', null); |
| return createGatewayAuthErrorResponse(401, 'Invalid or expired session', corsHeaders); |
| } |
| |
| |
| |
| if (session.userId) { |
| sessionUserId = session.userId; |
| usage.sessionUserId = session.userId; |
| request = withAuthenticatedUserId(request, session.userId); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let allowed = session.role === 'pro'; |
| if (!allowed && session.userId) { |
| const ent = await getEntitlements(session.userId); |
| recordUsageEntitlement(ent); |
| const proCovered = !!ent && |
| ent.features.tier >= 1 && |
| ent.validUntil >= Date.now(); |
| const billingDenial = denyForBillingVerification( |
| ent, |
| corsHeaders, |
| proCovered, |
| ); |
| if (billingDenial) return billingDenial; |
| allowed = !!ent && ent.features.tier >= 1 && ent.validUntil >= Date.now(); |
| } |
| if (!allowed) { |
| emitRequest(403, 'tier_403', null); |
| return createGatewayAuthErrorResponse(403, 'Pro subscription required', corsHeaders); |
| } |
| rateLimitPrincipalUserId = session.userId; |
| |
| } else { |
| emitRequest(401, 'auth_401', null); |
| return createGatewayAuthErrorResponse(401, keyCheck.error, corsHeaders); |
| } |
| } else { |
| emitRequest(401, 'auth_401', null); |
| return createGatewayAuthErrorResponse(401, keyCheck.error, corsHeaders); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const isEnterpriseAuth = keyCheck.valid && wmKey && !isUserApiKey && keyCheck.kind === 'enterprise'; |
| if (!isEnterpriseAuth && !internalMcpVerified && !seedRefreshVerified && !relayWarmPingVerified) { |
| const entitlementCheck = await checkEntitlementDetailed(sessionUserId, pathname, corsHeaders, { |
| clerkRole: sessionRole, |
| }); |
| recordUsageEntitlement(entitlementCheck.entitlements); |
| const entitlementResponse = entitlementCheck.response; |
| if (entitlementResponse) { |
| const entReason: RequestReason = |
| entitlementResponse.status === 401 ? 'auth_401' |
| : entitlementResponse.status === 403 ? 'tier_403' |
| : entitlementResponse.status === 503 ? 'billing_verification_503' |
| : 'ok'; |
| emitRequest(entitlementResponse.status, entReason, null); |
| return entitlementResponse.status === 401 || entitlementResponse.status === 403 |
| ? markAuthErrorNoStore(entitlementResponse) |
| : entitlementResponse; |
| } |
|
|
| |
| |
| |
| |
| if (sessionUserId && isTierGated) { |
| rateLimitPrincipalUserId = sessionUserId; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if ( |
| pathname === '/api/news/v1/summarize-article' && |
| requiresDirectLlmQuota && |
| sessionUserId |
| ) { |
| |
| |
| |
| |
| |
| const attributionGuardResponse = await checkFailClosedScopedIpRateLimit( |
| request, |
| 'summarize-article:principal-attribution', |
| 600, |
| '60 s', |
| corsHeaders, |
| ); |
| if (attributionGuardResponse) { |
| const reason = getRateLimitTelemetryReason( |
| attributionGuardResponse, |
| 'rate_limit_429', |
| ); |
| emitRequest(attributionGuardResponse.status, reason, null); |
| return attributionGuardResponse; |
| } |
|
|
| const ent = entitlementCheck.entitlements ?? ( |
| userKeyEntitlement !== undefined |
| ? userKeyEntitlement |
| : await getEntitlements(sessionUserId) |
| ); |
| recordUsageEntitlement(ent); |
| if (ent && ent.features.tier >= 1 && ent.validUntil >= Date.now()) { |
| rateLimitPrincipalUserId = sessionUserId; |
| } |
| } |
| } |
|
|
| |
| let matchedHandler = router.match(request); |
| if (!matchedHandler && request.method === 'POST') { |
| if (isPostToGetCompatibleBodySize(request.headers)) { |
| const url = new URL(request.url); |
| let oversizedKey: string | null = null; |
| try { |
| const bodyText = await request.clone().text(); |
| if (new TextEncoder().encode(bodyText).byteLength >= POST_TO_GET_MAX_BODY_BYTES) { |
| emitRequest(400, 'malformed_request', null); |
| return new Response(JSON.stringify({ error: 'malformed_request' }), { |
| status: 400, |
| headers: { 'Content-Type': 'application/json', ...corsHeaders }, |
| }); |
| } |
| const body = JSON.parse(bodyText); |
| const isScalar = (x: unknown): x is string | number | boolean => |
| typeof x === 'string' || typeof x === 'number' || typeof x === 'boolean'; |
| for (const [k, v] of Object.entries(body as Record<string, unknown>)) { |
| if (Array.isArray(v)) { |
| if (v.length > POST_TO_GET_MAX_ARRAY_VALUES_PER_KEY) { |
| oversizedKey = k; |
| break; |
| } |
| v.forEach((item) => { if (isScalar(item)) url.searchParams.append(k, String(item)); }); |
| } else if (isScalar(v)) url.searchParams.set(k, String(v)); |
| } |
| } catch { } |
| if (oversizedKey !== null) { |
| emitRequest(400, 'malformed_request', null); |
| return new Response(JSON.stringify({ |
| error: 'Too many values for POST compatibility parameter', |
| parameter: oversizedKey, |
| maxValues: POST_TO_GET_MAX_ARRAY_VALUES_PER_KEY, |
| }), { |
| status: 400, |
| headers: { 'Content-Type': 'application/json', ...corsHeaders }, |
| }); |
| } |
| const getReq = new Request(url.toString(), { method: 'GET', headers: request.headers }); |
| matchedHandler = router.match(getReq); |
| if (matchedHandler) request = getReq; |
| } |
| } |
| if (!matchedHandler) { |
| const allowed = router.allowedMethods(new URL(request.url).pathname); |
| if (allowed.length > 0) { |
| emitRequest(405, 'method_not_allowed', null); |
| return new Response(JSON.stringify({ error: 'Method not allowed' }), { |
| status: 405, |
| headers: { 'Content-Type': 'application/json', Allow: allowed.join(', '), ...corsHeaders }, |
| }); |
| } |
| emitRequest(404, 'unknown_route', null); |
| return new Response(JSON.stringify({ error: 'Not found' }), { |
| status: 404, |
| headers: { 'Content-Type': 'application/json', ...corsHeaders }, |
| }); |
| } |
|
|
| const requiredBboxDiagnostic = getRequiredBboxDiagnostic(request, pathname); |
| const identityForScope = buildUsageIdentity(usage); |
|
|
| |
| |
| |
| |
| |
| let idempotency: IdempotencyOutcome | null = null; |
| const hasIdempotencyKey = request.method === 'POST' && request.headers.has(IDEMPOTENCY_HEADER); |
| const idScope = identityForScope.principal_id ?? identityForScope.customer_id; |
| const idempotencyScope = idScope ? `${identityForScope.auth_kind}:${idScope}` : null; |
|
|
| |
| |
| |
| |
| if (hasIdempotencyKey) { |
| const peek = await peekIdempotency({ |
| request, |
| pathname, |
| scope: idempotencyScope, |
| idempotencyKey: request.headers.get(IDEMPOTENCY_HEADER) ?? '', |
| corsHeaders, |
| }); |
| switch (peek.kind) { |
| case 'invalid': |
| emitRequest(400, 'idempotency_invalid', null); |
| return peek.response; |
| case 'replay': |
| emitRequest(peek.response.status, 'idempotent_replay', null); |
| return peek.response; |
| case 'conflict': |
| emitRequest(409, 'idempotency_conflict', null); |
| return peek.response; |
| case 'mismatch': |
| emitRequest(422, 'idempotency_mismatch', null); |
| return peek.response; |
| |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| if (!internalMcpVerified) { |
| const endpointRlResponse = rateLimitPrincipalUserId |
| ? await checkEndpointRateLimit(request, pathname, corsHeaders, { |
| principalUserId: rateLimitPrincipalUserId, |
| }) |
| : await checkEndpointRateLimit(request, pathname, corsHeaders); |
| if (endpointRlResponse) { |
| const reason = getRateLimitTelemetryReason( |
| endpointRlResponse, |
| 'rate_limit_429_endpoint', |
| ); |
| emitRequest(endpointRlResponse.status, reason, null); |
| return endpointRlResponse; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let governedByApiKeyLayer = false; |
| if (keyCheck.valid && (isUserApiKey || isEnterpriseAuth)) { |
| const enforce = process.env.API_RATE_LIMIT_ENFORCE === 'true'; |
| let perMinute = 0; |
| let allowance = -1; |
| let identity = ''; |
| let planKey = ''; |
| if (isEnterpriseAuth) { |
| perMinute = ENTERPRISE_API_RATE_LIMIT; |
| allowance = -1; |
| planKey = 'enterprise'; |
| usage.tier = 3; |
| |
| |
| |
| |
| |
| |
| |
| identity = wmKey ? hashKeySync(wmKey) : ''; |
| } else if (sessionUserId) { |
| |
| |
| |
| |
| const ent = |
| userKeyEntitlement !== undefined |
| ? userKeyEntitlement |
| : await getEntitlements(sessionUserId); |
| if (ent) { |
| |
| |
| |
| recordUsageEntitlement(ent); |
| } |
| if (ent && ent.features.apiAccess && ent.features.apiRateLimit > 0) { |
| perMinute = ent.features.apiRateLimit; |
| |
| allowance = |
| typeof ent.features.apiDailyAllowance === 'number' |
| ? ent.features.apiDailyAllowance |
| : -1; |
| planKey = ent.planKey; |
| identity = sessionUserId; |
| } |
| |
| |
| } |
|
|
| if (perMinute > 0 && identity) { |
| |
| const upgradeUrl = |
| planKey && planKey !== 'enterprise' ? 'https://worldmonitor.app/' : undefined; |
| |
| const burst = await checkBurst(perMinute, identity); |
| if (!burst.ok) { |
| if (enforce) { |
| const retryAfterSec = Math.max(1, Math.ceil((burst.reset - Date.now()) / 1000)); |
| emitRequest(429, 'rl_min_429', null); |
| return new Response(JSON.stringify({ |
| error: 'Too many requests', |
| plan: planKey || undefined, |
| limit: burst.limit, |
| limit_type: 'per_minute', |
| reset: new Date(burst.reset).toISOString(), |
| upgrade_url: upgradeUrl, |
| }), { |
| status: 429, |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Cache-Control': 'no-store', |
| ...rateLimitHeaders({ limit: burst.limit, remaining: 0, resetMs: burst.reset, retryAfterSec, windowSec: 60 }), |
| ...corsHeaders, |
| }, |
| }); |
| } |
| pendingShadowReason = 'rl_min_shadow'; |
| } else if (allowance >= 0) { |
| |
| |
| const meter = await reserveDailyMeter({ |
| userId: identity, |
| allowance, |
| pipeline: (cmds) => runRedisPipeline(cmds), |
| }); |
| if (meter.overLimit) { |
| if (enforce) { |
| await meter.rollback(); |
| emitRequest(429, 'rl_ceiling_429', null); |
| return new Response(JSON.stringify({ |
| error: 'Daily request limit reached', |
| plan: planKey || undefined, |
| limit: allowance, |
| limit_type: 'daily', |
| reset: new Date(Date.now() + meter.retryAfterSec * 1000).toISOString(), |
| upgrade_url: upgradeUrl, |
| }), { |
| status: 429, |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Cache-Control': 'no-store', |
| ...rateLimitHeaders({ |
| limit: allowance, |
| remaining: 0, |
| resetMs: Date.now() + meter.retryAfterSec * 1000, |
| retryAfterSec: meter.retryAfterSec, |
| |
| windowSec: 86_400, |
| }), |
| ...corsHeaders, |
| }, |
| }); |
| } |
| pendingShadowReason = 'rl_ceiling_shadow'; |
| } |
| } |
| |
| |
| |
| |
| if (enforce) governedByApiKeyLayer = true; |
| } |
| } |
|
|
| if (!governedByApiKeyLayer && !hasEndpointRatePolicy(pathname)) { |
| const rateLimitResponse = rateLimitPrincipalUserId |
| ? await checkRateLimit(request, corsHeaders, { |
| principalUserId: rateLimitPrincipalUserId, |
| }) |
| : await checkRateLimit(request, corsHeaders); |
| if (rateLimitResponse) { |
| const reason = getRateLimitTelemetryReason( |
| rateLimitResponse, |
| 'rate_limit_429_global', |
| ); |
| emitRequest(rateLimitResponse.status, reason, null); |
| return rateLimitResponse; |
| } |
| } |
| } |
|
|
| if (requiresDirectLlmQuota && !isEnterpriseAuth) { |
| if (!sessionUserId) { |
| emitRequest(401, 'auth_401', null); |
| return createGatewayAuthErrorResponse(401, 'Pro authentication required', corsHeaders); |
| } |
|
|
| const reservation = await reserveDirectLlmQuota({ |
| userId: sessionUserId, |
| pipeline: (cmds) => runRedisPipeline(cmds, true), |
| }); |
| if (!reservation.ok) { |
| const response = createDirectLlmQuotaFailureResponse(reservation, corsHeaders); |
| emitRequest( |
| response.status, |
| response.status === 429 ? 'rate_limit_429_direct_llm' : 'rate_limit_degraded', |
| null, |
| ); |
| return response; |
| } |
| } |
|
|
| |
| |
| if (hasIdempotencyKey) { |
| idempotency = await beginIdempotency({ |
| request, |
| pathname, |
| |
| |
| scope: idempotencyScope, |
| idempotencyKey: request.headers.get(IDEMPOTENCY_HEADER) ?? '', |
| corsHeaders, |
| }); |
| switch (idempotency.kind) { |
| case 'invalid': |
| emitRequest(400, 'idempotency_invalid', null); |
| return idempotency.response; |
| case 'replay': |
| emitRequest(idempotency.response.status, 'idempotent_replay', null); |
| return idempotency.response; |
| case 'conflict': |
| emitRequest(409, 'idempotency_conflict', null); |
| return idempotency.response; |
| case 'mismatch': |
| emitRequest(422, 'idempotency_mismatch', null); |
| return idempotency.response; |
| |
| } |
| } |
|
|
| |
| |
| |
| |
| let response: Response; |
| const handlerCall = matchedHandler; |
| const requestForHandler = request; |
| try { |
| response = await runWithUsageScope( |
| { |
| ctx: ctx ?? { waitUntil: () => {} }, |
| requestId: deriveRequestId(originalRequest), |
| customerId: identityForScope.customer_id, |
| route: pathname, |
| tier: identityForScope.tier, |
| }, |
| () => handlerCall(requestForHandler), |
| ); |
| } catch (err) { |
| console.error('[gateway] Unhandled handler error:', err); |
| response = new Response(JSON.stringify({ message: 'Internal server error' }), { |
| status: 500, |
| headers: { 'Content-Type': 'application/json' }, |
| }); |
| } |
|
|
| |
| const mergedHeaders = new Headers(response.headers); |
| for (const [key, value] of Object.entries(corsHeaders)) { |
| mergedHeaders.set(key, value); |
| } |
| const extraHeaders = drainResponseHeaders(request); |
| if (extraHeaders) { |
| for (const [key, value] of Object.entries(extraHeaders)) { |
| mergedHeaders.set(key, value); |
| } |
| } |
| const retryableResponse = drainRetryableResponse(request); |
| attachRequiredBboxDiagnosticHeaders(mergedHeaders, pathname, requiredBboxDiagnostic); |
|
|
| |
| |
| |
| |
| |
| |
| const statusOverride = drainSuccessStatusOverride(request); |
| const finalStatus = |
| statusOverride !== undefined && request.method === 'POST' && response.status === 200 |
| ? statusOverride |
| : response.status; |
|
|
| |
| let resolvedCacheTier: CacheTier | null = null; |
| if (response.status === 200 && request.method === 'GET' && response.body) { |
| const bodyBytes = await response.arrayBuffer(); |
|
|
| const bodyStr = new TextDecoder().decode(bodyBytes); |
| const noStoreReason = getRpcNoStoreReasonFromJson(bodyStr, { pathname }); |
|
|
| if (mergedHeaders.get('X-No-Cache') || noStoreReason) { |
| mergedHeaders.set('Cache-Control', 'no-store'); |
| mergedHeaders.delete('CDN-Cache-Control'); |
| mergedHeaders.delete('Vercel-CDN-Cache-Control'); |
| mergedHeaders.set('X-Cache-Tier', 'no-store'); |
| resolvedCacheTier = 'no-store'; |
| } else { |
| const rpcName = pathname.split('/').pop() ?? ''; |
| const envOverride = process.env[`CACHE_TIER_OVERRIDE_${rpcName.replace(/-/g, '_').toUpperCase()}`] as CacheTier | undefined; |
| const isPremium = PREMIUM_RPC_PATHS.has(pathname) || getRequiredTier(pathname) !== null; |
| const hasCredentialedNonPublicGet = !isPublicNoAuthRpc && hasCredentialBearingHeader(request); |
| const tier = hasProFreshCacheAccess ? 'live-browser' as CacheTier |
| : isPremium || hasCredentialedNonPublicGet ? 'slow-browser' as CacheTier |
| : (envOverride && envOverride in TIER_HEADERS ? envOverride : null) ?? RPC_CACHE_TIER[pathname] ?? 'medium'; |
| resolvedCacheTier = tier; |
| mergedHeaders.set('Cache-Control', TIER_HEADERS[tier]); |
| |
| |
| |
| |
| |
| const reqOrigin = request.headers.get('origin') || ''; |
| const cdnCache = !hasProFreshCacheAccess && !isPremium && !hasCredentialedNonPublicGet && isAllowedOrigin(reqOrigin) |
| ? TIER_CDN_CACHE[tier] |
| : null; |
| mergedHeaders.delete('CDN-Cache-Control'); |
| mergedHeaders.delete('Vercel-CDN-Cache-Control'); |
| if (cdnCache) mergedHeaders.set('CDN-Cache-Control', cdnCache); |
| mergedHeaders.set('X-Cache-Tier', tier); |
|
|
| |
| |
| |
| |
| } |
| mergedHeaders.delete('X-No-Cache'); |
| if (!new URL(request.url).searchParams.has('_debug')) { |
| mergedHeaders.delete('X-Cache-Tier'); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| let responseView = new Uint8Array(bodyBytes); |
| const jmespathExpr = new URL(request.url).searchParams.get('jmespath'); |
| if (jmespathExpr && (mergedHeaders.get('Content-Type') ?? '').includes('application/json')) { |
| const projection = projectJsonResponse(bodyStr, jmespathExpr); |
| if (!projection.ok) { |
| const errorBody = JSON.stringify(projection.envelope); |
| emitRequest(400, 'malformed_request', null, errorBody.length); |
| maybeAttachDevHealthHeader(mergedHeaders); |
| return new Response(errorBody, { |
| status: 400, |
| headers: { |
| ...corsHeaders, |
| 'Content-Type': 'application/json; charset=utf-8', |
| 'X-Content-Type-Options': 'nosniff', |
| 'Cache-Control': 'no-store', |
| }, |
| }); |
| } |
| responseView = new TextEncoder().encode(projection.body); |
| |
| |
| |
| mergedHeaders.delete('Content-Length'); |
| } |
|
|
| |
| let hash = 2166136261; |
| const view = responseView; |
| for (let i = 0; i < view.length; i++) { |
| hash ^= view[i]!; |
| hash = Math.imul(hash, 16777619); |
| } |
| const etag = `"${(hash >>> 0).toString(36)}-${view.length.toString(36)}"`; |
| mergedHeaders.set('ETag', etag); |
|
|
| const ifNoneMatch = request.headers.get('If-None-Match'); |
| if (ifNoneMatch === etag) { |
| emitRequest(304, 'ok', resolvedCacheTier, 0); |
| maybeAttachDevHealthHeader(mergedHeaders); |
| return new Response(null, { status: 304, headers: mergedHeaders }); |
| } |
|
|
| emitRequest(response.status, 'ok', resolvedCacheTier, view.length); |
| maybeAttachDevHealthHeader(mergedHeaders); |
| return new Response(responseView, { |
| status: response.status, |
| statusText: response.statusText, |
| headers: mergedHeaders, |
| }); |
| } |
|
|
| if (response.status === 200 && request.method === 'GET') { |
| if (mergedHeaders.get('X-No-Cache')) { |
| mergedHeaders.set('Cache-Control', 'no-store'); |
| } |
| mergedHeaders.delete('X-No-Cache'); |
| } |
|
|
| |
| |
| |
| |
| if (idempotency?.kind === 'proceed') { |
| const bodyBytes = response.body ? await response.arrayBuffer() : new ArrayBuffer(0); |
| mergedHeaders.set(IDEMPOTENCY_HEADER, idempotency.key); |
| mergedHeaders.set(IDEMPOTENT_REPLAYED_HEADER, 'false'); |
| |
| |
| |
| |
| |
| |
| |
| await idempotency.store( |
| retryableResponse ? 503 : finalStatus, |
| bodyBytes, |
| response.headers.get('content-type'), |
| ); |
| emitRequest(finalStatus, 'ok', resolvedCacheTier, bodyBytes.byteLength); |
| maybeAttachDevHealthHeader(mergedHeaders); |
| return new Response(bodyBytes, { |
| status: finalStatus, |
| statusText: response.statusText, |
| headers: mergedHeaders, |
| }); |
| } |
|
|
| |
| |
| const finalContentLen = response.headers.get('content-length'); |
| const finalResBytes = finalContentLen ? Number(finalContentLen) || 0 : 0; |
| emitRequest(finalStatus, 'ok', resolvedCacheTier, finalResBytes); |
| maybeAttachDevHealthHeader(mergedHeaders); |
| return new Response(response.body, { |
| status: finalStatus, |
| statusText: response.statusText, |
| headers: mergedHeaders, |
| }); |
| }; |
| } |
|
|