feat(resilience): add service proto and stub handlers (#2657)

* feat(resilience): add service proto and stub handlers

Add the worldmonitor.resilience.v1 proto package, generated client/server artifacts, edge routing, and zero-state handler stubs so the domain is deployable before the seed and scoring layers land.

Validation:
- PATH="/Users/lucaspassos/go/bin:/Users/lucaspassos/.codex/tmp/arg0/codex-arg06nbVvG:/Users/lucaspassos/.antigravity/antigravity/bin:/Users/lucaspassos/.local/bin:/Users/lucaspassos/.codeium/windsurf/bin:/Users/lucaspassos/Library/Python/3.12/bin:/Library/Frameworks/Python.framework/Versions/3.12/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/opt/pkg/env/active/bin:/opt/pmk/env/global/bin:/Library/Apple/usr/bin:/opt/homebrew/bin:/Applications/Codex.app/Contents/Resources" make generate
- PATH="/Users/lucaspassos/go/bin:/Users/lucaspassos/.codex/tmp/arg0/codex-arg06nbVvG:/Users/lucaspassos/.antigravity/antigravity/bin:/Users/lucaspassos/.local/bin:/Users/lucaspassos/.codeium/windsurf/bin:/Users/lucaspassos/Library/Python/3.12/bin:/Library/Frameworks/Python.framework/Versions/3.12/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/opt/pkg/env/active/bin:/opt/pmk/env/global/bin:/Library/Apple/usr/bin:/opt/homebrew/bin:/Applications/Codex.app/Contents/Resources" npx tsx --test tests/route-cache-tier.test.mjs tests/edge-functions.test.mjs
- npm run typecheck (fails on upstream Dodo/Clerk baseline)
- npm run typecheck:api (fails on upstream vitest baseline)
- npm run test:data (fails on upstream dodopayments-checkout baseline via tests/runtime-config-panel-visibility.test.mjs)

* fix(resilience): add countryCode validation to get-resilience-score

Throw ValidationError when countryCode is missing instead of silently
returning a zero-state response with an empty string country code.

* fix(resilience): validate countryCode format and mark required in spec

- Trim whitespace and reject non-ISO-3166-1 alpha-2 codes to prevent
  cache pollution from malformed aliases (e.g. 'USA', '  us  ', 'foobar')
- Add required: true to proto QueryConfig so generated OpenAPI spec
  matches runtime validation behavior
- Regenerated OpenAPI artifacts via make generate

---------

Co-authored-by: Elie Habib <elie.habib@gmail.com>
This commit is contained in:
Lucas Passos
2026-04-04 05:04:46 +01:00
committed by GitHub
parent e0fc8bc136
commit 4b67012260
14 changed files with 660 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
export const config = { runtime: 'edge' };
import { createDomainGateway, serverOptions } from '../../../server/gateway';
import { createResilienceServiceRoutes } from '../../../src/generated/server/worldmonitor/resilience/v1/service_server';
import { resilienceHandler } from '../../../server/worldmonitor/resilience/v1/handler';
export default createDomainGateway(
createResilienceServiceRoutes(resilienceHandler, serverOptions),
);

View File

@@ -0,0 +1 @@
{"components":{"schemas":{"Error":{"description":"Error is returned when a handler encounters an error. It contains a simple error message that the developer can customize.","properties":{"message":{"description":"Error message (e.g., 'user not found', 'database connection failed')","type":"string"}},"type":"object"},"FieldViolation":{"description":"FieldViolation describes a single validation error for a specific field.","properties":{"description":{"description":"Human-readable description of the validation violation (e.g., 'must be a valid email address', 'required field missing')","type":"string"},"field":{"description":"The field path that failed validation (e.g., 'user.email' for nested fields). For header validation, this will be the header name (e.g., 'X-API-Key')","type":"string"}},"required":["field","description"],"type":"object"},"GetResilienceRankingRequest":{"type":"object"},"GetResilienceRankingResponse":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ResilienceRankingItem"},"type":"array"}},"type":"object"},"GetResilienceScoreRequest":{"properties":{"countryCode":{"type":"string"}},"type":"object"},"GetResilienceScoreResponse":{"properties":{"change30d":{"format":"double","type":"number"},"countryCode":{"type":"string"},"cronbachAlpha":{"format":"double","type":"number"},"domains":{"items":{"$ref":"#/components/schemas/ResilienceDomain"},"type":"array"},"level":{"type":"string"},"lowConfidence":{"type":"boolean"},"overallScore":{"format":"double","type":"number"},"trend":{"type":"string"}},"type":"object"},"ResilienceDimension":{"properties":{"coverage":{"format":"double","type":"number"},"id":{"type":"string"},"score":{"format":"double","type":"number"}},"type":"object"},"ResilienceDomain":{"properties":{"dimensions":{"items":{"$ref":"#/components/schemas/ResilienceDimension"},"type":"array"},"id":{"type":"string"},"score":{"format":"double","type":"number"},"weight":{"format":"double","type":"number"}},"type":"object"},"ResilienceRankingItem":{"properties":{"countryCode":{"type":"string"},"level":{"type":"string"},"lowConfidence":{"type":"boolean"},"overallScore":{"format":"double","type":"number"}},"type":"object"},"ValidationError":{"description":"ValidationError is returned when request validation fails. It contains a list of field violations describing what went wrong.","properties":{"violations":{"description":"List of validation violations","items":{"$ref":"#/components/schemas/FieldViolation"},"type":"array"}},"required":["violations"],"type":"object"}}},"info":{"title":"ResilienceService API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/api/resilience/v1/get-resilience-ranking":{"get":{"operationId":"GetResilienceRanking","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetResilienceRankingResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetResilienceRanking","tags":["ResilienceService"]}},"/api/resilience/v1/get-resilience-score":{"get":{"operationId":"GetResilienceScore","parameters":[{"in":"query","name":"countryCode","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetResilienceScoreResponse"}}},"description":"Successful response"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}},"description":"Validation error"},"default":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Error response"}},"summary":"GetResilienceScore","tags":["ResilienceService"]}}}}

View File

@@ -0,0 +1,170 @@
openapi: 3.1.0
info:
title: ResilienceService API
version: 1.0.0
paths:
/api/resilience/v1/get-resilience-score:
get:
tags:
- ResilienceService
summary: GetResilienceScore
operationId: GetResilienceScore
parameters:
- name: countryCode
in: query
required: true
schema:
type: string
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetResilienceScoreResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/resilience/v1/get-resilience-ranking:
get:
tags:
- ResilienceService
summary: GetResilienceRanking
operationId: GetResilienceRanking
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/GetResilienceRankingResponse'
"400":
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
default:
description: Error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Error:
type: object
properties:
message:
type: string
description: Error message (e.g., 'user not found', 'database connection failed')
description: Error is returned when a handler encounters an error. It contains a simple error message that the developer can customize.
FieldViolation:
type: object
properties:
field:
type: string
description: The field path that failed validation (e.g., 'user.email' for nested fields). For header validation, this will be the header name (e.g., 'X-API-Key')
description:
type: string
description: Human-readable description of the validation violation (e.g., 'must be a valid email address', 'required field missing')
required:
- field
- description
description: FieldViolation describes a single validation error for a specific field.
ValidationError:
type: object
properties:
violations:
type: array
items:
$ref: '#/components/schemas/FieldViolation'
description: List of validation violations
required:
- violations
description: ValidationError is returned when request validation fails. It contains a list of field violations describing what went wrong.
GetResilienceScoreRequest:
type: object
properties:
countryCode:
type: string
GetResilienceScoreResponse:
type: object
properties:
countryCode:
type: string
overallScore:
type: number
format: double
level:
type: string
domains:
type: array
items:
$ref: '#/components/schemas/ResilienceDomain'
cronbachAlpha:
type: number
format: double
trend:
type: string
change30d:
type: number
format: double
lowConfidence:
type: boolean
ResilienceDomain:
type: object
properties:
id:
type: string
score:
type: number
format: double
weight:
type: number
format: double
dimensions:
type: array
items:
$ref: '#/components/schemas/ResilienceDimension'
ResilienceDimension:
type: object
properties:
id:
type: string
score:
type: number
format: double
coverage:
type: number
format: double
GetResilienceRankingRequest:
type: object
GetResilienceRankingResponse:
type: object
properties:
items:
type: array
items:
$ref: '#/components/schemas/ResilienceRankingItem'
ResilienceRankingItem:
type: object
properties:
countryCode:
type: string
overallScore:
type: number
format: double
level:
type: string
lowConfidence:
type: boolean

View File

@@ -0,0 +1,11 @@
syntax = "proto3";
package worldmonitor.resilience.v1;
import "worldmonitor/resilience/v1/resilience.proto";
message GetResilienceRankingRequest {}
message GetResilienceRankingResponse {
repeated ResilienceRankingItem items = 1;
}

View File

@@ -0,0 +1,21 @@
syntax = "proto3";
package worldmonitor.resilience.v1;
import "sebuf/http/annotations.proto";
import "worldmonitor/resilience/v1/resilience.proto";
message GetResilienceScoreRequest {
string country_code = 1 [(sebuf.http.query) = { name: "countryCode", required: true }];
}
message GetResilienceScoreResponse {
string country_code = 1;
double overall_score = 2;
string level = 3;
repeated ResilienceDomain domains = 4;
double cronbach_alpha = 5;
string trend = 6;
double change_30d = 7;
bool low_confidence = 8;
}

View File

@@ -0,0 +1,23 @@
syntax = "proto3";
package worldmonitor.resilience.v1;
message ResilienceDimension {
string id = 1;
double score = 2;
double coverage = 3;
}
message ResilienceDomain {
string id = 1;
double score = 2;
double weight = 3;
repeated ResilienceDimension dimensions = 4;
}
message ResilienceRankingItem {
string country_code = 1;
double overall_score = 2;
string level = 3;
bool low_confidence = 4;
}

View File

@@ -0,0 +1,19 @@
syntax = "proto3";
package worldmonitor.resilience.v1;
import "sebuf/http/annotations.proto";
import "worldmonitor/resilience/v1/get_resilience_ranking.proto";
import "worldmonitor/resilience/v1/get_resilience_score.proto";
service ResilienceService {
option (sebuf.http.service_config) = {base_path: "/api/resilience/v1"};
rpc GetResilienceScore(GetResilienceScoreRequest) returns (GetResilienceScoreResponse) {
option (sebuf.http.config) = {path: "/get-resilience-score", method: HTTP_METHOD_GET};
}
rpc GetResilienceRanking(GetResilienceRankingRequest) returns (GetResilienceRankingResponse) {
option (sebuf.http.config) = {path: "/get-resilience-ranking", method: HTTP_METHOD_GET};
}
}

View File

@@ -208,6 +208,8 @@ const RPC_CACHE_TIER: Record<string, CacheTier> = {
'/api/health/v1/list-disease-outbreaks': 'slow',
'/api/health/v1/list-air-quality-alerts': 'fast',
'/api/intelligence/v1/get-social-velocity': 'fast',
'/api/resilience/v1/get-resilience-score': 'slow',
'/api/resilience/v1/get-resilience-ranking': 'slow',
};
import { PREMIUM_RPC_PATHS } from '../src/shared/premium-paths';

View File

@@ -0,0 +1,15 @@
import type {
ResilienceServiceHandler,
ServerContext,
GetResilienceRankingRequest,
GetResilienceRankingResponse,
} from '../../../../src/generated/server/worldmonitor/resilience/v1/service_server';
export const getResilienceRanking: ResilienceServiceHandler['getResilienceRanking'] = async (
_ctx: ServerContext,
_req: GetResilienceRankingRequest,
): Promise<GetResilienceRankingResponse> => {
return {
items: [],
};
};

View File

@@ -0,0 +1,31 @@
import type {
ResilienceServiceHandler,
ServerContext,
GetResilienceScoreRequest,
GetResilienceScoreResponse,
} from '../../../../src/generated/server/worldmonitor/resilience/v1/service_server';
import { ValidationError } from '../../../../src/generated/server/worldmonitor/resilience/v1/service_server';
export const getResilienceScore: ResilienceServiceHandler['getResilienceScore'] = async (
_ctx: ServerContext,
req: GetResilienceScoreRequest,
): Promise<GetResilienceScoreResponse> => {
const countryCode = String(req.countryCode || '').trim().toUpperCase();
if (!countryCode) {
throw new ValidationError([{ field: 'countryCode', description: 'countryCode is required' }]);
}
if (!/^[A-Z]{2}$/.test(countryCode)) {
throw new ValidationError([{ field: 'countryCode', description: 'countryCode must be a 2-letter ISO 3166-1 alpha-2 code' }]);
}
return {
countryCode,
overallScore: 0,
level: 'unknown',
domains: [],
cronbachAlpha: 0,
trend: 'stable',
change30d: 0,
lowConfidence: true,
};
};

View File

@@ -0,0 +1,9 @@
import type { ResilienceServiceHandler } from '../../../../src/generated/server/worldmonitor/resilience/v1/service_server';
import { getResilienceRanking } from './get-resilience-ranking';
import { getResilienceScore } from './get-resilience-score';
export const resilienceHandler: ResilienceServiceHandler = {
getResilienceScore,
getResilienceRanking,
};

View File

@@ -0,0 +1,158 @@
// @ts-nocheck
// Code generated by protoc-gen-ts-client. DO NOT EDIT.
// source: worldmonitor/resilience/v1/service.proto
export interface GetResilienceScoreRequest {
countryCode: string;
}
export interface GetResilienceScoreResponse {
countryCode: string;
overallScore: number;
level: string;
domains: ResilienceDomain[];
cronbachAlpha: number;
trend: string;
change30d: number;
lowConfidence: boolean;
}
export interface ResilienceDomain {
id: string;
score: number;
weight: number;
dimensions: ResilienceDimension[];
}
export interface ResilienceDimension {
id: string;
score: number;
coverage: number;
}
export interface GetResilienceRankingRequest {
}
export interface GetResilienceRankingResponse {
items: ResilienceRankingItem[];
}
export interface ResilienceRankingItem {
countryCode: string;
overallScore: number;
level: string;
lowConfidence: boolean;
}
export interface FieldViolation {
field: string;
description: string;
}
export class ValidationError extends Error {
violations: FieldViolation[];
constructor(violations: FieldViolation[]) {
super("Validation failed");
this.name = "ValidationError";
this.violations = violations;
}
}
export class ApiError extends Error {
statusCode: number;
body: string;
constructor(statusCode: number, message: string, body: string) {
super(message);
this.name = "ApiError";
this.statusCode = statusCode;
this.body = body;
}
}
export interface ResilienceServiceClientOptions {
fetch?: typeof fetch;
defaultHeaders?: Record<string, string>;
}
export interface ResilienceServiceCallOptions {
headers?: Record<string, string>;
signal?: AbortSignal;
}
export class ResilienceServiceClient {
private baseURL: string;
private fetchFn: typeof fetch;
private defaultHeaders: Record<string, string>;
constructor(baseURL: string, options?: ResilienceServiceClientOptions) {
this.baseURL = baseURL.replace(/\/+$/, "");
this.fetchFn = options?.fetch ?? globalThis.fetch;
this.defaultHeaders = { ...options?.defaultHeaders };
}
async getResilienceScore(req: GetResilienceScoreRequest, options?: ResilienceServiceCallOptions): Promise<GetResilienceScoreResponse> {
let path = "/api/resilience/v1/get-resilience-score";
const params = new URLSearchParams();
if (req.countryCode != null && req.countryCode !== "") params.set("countryCode", String(req.countryCode));
const url = this.baseURL + path + (params.toString() ? "?" + params.toString() : "");
const headers: Record<string, string> = {
"Content-Type": "application/json",
...this.defaultHeaders,
...options?.headers,
};
const resp = await this.fetchFn(url, {
method: "GET",
headers,
signal: options?.signal,
});
if (!resp.ok) {
return this.handleError(resp);
}
return await resp.json() as GetResilienceScoreResponse;
}
async getResilienceRanking(req: GetResilienceRankingRequest, options?: ResilienceServiceCallOptions): Promise<GetResilienceRankingResponse> {
let path = "/api/resilience/v1/get-resilience-ranking";
const url = this.baseURL + path;
const headers: Record<string, string> = {
"Content-Type": "application/json",
...this.defaultHeaders,
...options?.headers,
};
const resp = await this.fetchFn(url, {
method: "GET",
headers,
signal: options?.signal,
});
if (!resp.ok) {
return this.handleError(resp);
}
return await resp.json() as GetResilienceRankingResponse;
}
private async handleError(resp: Response): Promise<never> {
const body = await resp.text();
if (resp.status === 400) {
try {
const parsed = JSON.parse(body);
if (parsed.violations) {
throw new ValidationError(parsed.violations);
}
} catch (e) {
if (e instanceof ValidationError) throw e;
}
}
throw new ApiError(resp.status, `Request failed with status ${resp.status}`, body);
}
}

View File

@@ -0,0 +1,187 @@
// @ts-nocheck
// Code generated by protoc-gen-ts-server. DO NOT EDIT.
// source: worldmonitor/resilience/v1/service.proto
export interface GetResilienceScoreRequest {
countryCode: string;
}
export interface GetResilienceScoreResponse {
countryCode: string;
overallScore: number;
level: string;
domains: ResilienceDomain[];
cronbachAlpha: number;
trend: string;
change30d: number;
lowConfidence: boolean;
}
export interface ResilienceDomain {
id: string;
score: number;
weight: number;
dimensions: ResilienceDimension[];
}
export interface ResilienceDimension {
id: string;
score: number;
coverage: number;
}
export interface GetResilienceRankingRequest {
}
export interface GetResilienceRankingResponse {
items: ResilienceRankingItem[];
}
export interface ResilienceRankingItem {
countryCode: string;
overallScore: number;
level: string;
lowConfidence: boolean;
}
export interface FieldViolation {
field: string;
description: string;
}
export class ValidationError extends Error {
violations: FieldViolation[];
constructor(violations: FieldViolation[]) {
super("Validation failed");
this.name = "ValidationError";
this.violations = violations;
}
}
export class ApiError extends Error {
statusCode: number;
body: string;
constructor(statusCode: number, message: string, body: string) {
super(message);
this.name = "ApiError";
this.statusCode = statusCode;
this.body = body;
}
}
export interface ServerContext {
request: Request;
pathParams: Record<string, string>;
headers: Record<string, string>;
}
export interface ServerOptions {
onError?: (error: unknown, req: Request) => Response | Promise<Response>;
validateRequest?: (methodName: string, body: unknown) => FieldViolation[] | undefined;
}
export interface RouteDescriptor {
method: string;
path: string;
handler: (req: Request) => Promise<Response>;
}
export interface ResilienceServiceHandler {
getResilienceScore(ctx: ServerContext, req: GetResilienceScoreRequest): Promise<GetResilienceScoreResponse>;
getResilienceRanking(ctx: ServerContext, req: GetResilienceRankingRequest): Promise<GetResilienceRankingResponse>;
}
export function createResilienceServiceRoutes(
handler: ResilienceServiceHandler,
options?: ServerOptions,
): RouteDescriptor[] {
return [
{
method: "GET",
path: "/api/resilience/v1/get-resilience-score",
handler: async (req: Request): Promise<Response> => {
try {
const pathParams: Record<string, string> = {};
const url = new URL(req.url, "http://localhost");
const params = url.searchParams;
const body: GetResilienceScoreRequest = {
countryCode: params.get("countryCode") ?? "",
};
if (options?.validateRequest) {
const bodyViolations = options.validateRequest("getResilienceScore", body);
if (bodyViolations) {
throw new ValidationError(bodyViolations);
}
}
const ctx: ServerContext = {
request: req,
pathParams,
headers: Object.fromEntries(req.headers.entries()),
};
const result = await handler.getResilienceScore(ctx, body);
return new Response(JSON.stringify(result as GetResilienceScoreResponse), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (err: unknown) {
if (err instanceof ValidationError) {
return new Response(JSON.stringify({ violations: err.violations }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
if (options?.onError) {
return options.onError(err, req);
}
const message = err instanceof Error ? err.message : String(err);
return new Response(JSON.stringify({ message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
},
},
{
method: "GET",
path: "/api/resilience/v1/get-resilience-ranking",
handler: async (req: Request): Promise<Response> => {
try {
const pathParams: Record<string, string> = {};
const body = {} as GetResilienceRankingRequest;
const ctx: ServerContext = {
request: req,
pathParams,
headers: Object.fromEntries(req.headers.entries()),
};
const result = await handler.getResilienceRanking(ctx, body);
return new Response(JSON.stringify(result as GetResilienceRankingResponse), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (err: unknown) {
if (err instanceof ValidationError) {
return new Response(JSON.stringify({ violations: err.violations }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
if (options?.onError) {
return options.onError(err, req);
}
const message = err instanceof Error ? err.message : String(err);
return new Response(JSON.stringify({ message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
},
},
];
}

View File

@@ -194,6 +194,7 @@ function sebufApiPlugin(): Plugin {
tradeServerMod, tradeHandlerMod,
supplyChainServerMod, supplyChainHandlerMod,
naturalServerMod, naturalHandlerMod,
resilienceServerMod, resilienceHandlerMod,
] = await Promise.all([
import('./server/router'),
import('./server/cors'),
@@ -242,6 +243,8 @@ function sebufApiPlugin(): Plugin {
import('./server/worldmonitor/supply-chain/v1/handler'),
import('./src/generated/server/worldmonitor/natural/v1/service_server'),
import('./server/worldmonitor/natural/v1/handler'),
import('./src/generated/server/worldmonitor/resilience/v1/service_server'),
import('./server/worldmonitor/resilience/v1/handler'),
]);
const serverOptions = { onError: errorMod.mapErrorToResponse };
@@ -268,6 +271,7 @@ function sebufApiPlugin(): Plugin {
...tradeServerMod.createTradeServiceRoutes(tradeHandlerMod.tradeHandler, serverOptions),
...supplyChainServerMod.createSupplyChainServiceRoutes(supplyChainHandlerMod.supplyChainHandler, serverOptions),
...naturalServerMod.createNaturalServiceRoutes(naturalHandlerMod.naturalHandler, serverOptions),
...resilienceServerMod.createResilienceServiceRoutes(resilienceHandlerMod.resilienceHandler, serverOptions),
];
cachedCorsMod = corsMod;
return routerMod.createRouter(allRoutes);