@jeffusion/bungee-core 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/master.js +405 -87
- package/dist/worker.js +1086 -374
- package/package.json +2 -2
package/dist/master.js
CHANGED
|
@@ -16047,7 +16047,7 @@ var init_migrations = __esm(() => {
|
|
|
16047
16047
|
});
|
|
16048
16048
|
|
|
16049
16049
|
// src/config-migrations/types.ts
|
|
16050
|
-
var LATEST_CONFIG_VERSION =
|
|
16050
|
+
var LATEST_CONFIG_VERSION = 4;
|
|
16051
16051
|
|
|
16052
16052
|
// src/config-migrations/utils.ts
|
|
16053
16053
|
function isPlainObject(value) {
|
|
@@ -16110,7 +16110,7 @@ function detectConfigVersion(rawConfig) {
|
|
|
16110
16110
|
if (!isPlainObject(rawConfig)) {
|
|
16111
16111
|
return 1;
|
|
16112
16112
|
}
|
|
16113
|
-
const version = rawConfig.configVersion;
|
|
16113
|
+
const version = rawConfig.configVersion ?? rawConfig.config_version;
|
|
16114
16114
|
return typeof version === "number" && Number.isInteger(version) && version > 0 ? version : 1;
|
|
16115
16115
|
}
|
|
16116
16116
|
function isPlainRecord(value) {
|
|
@@ -16151,7 +16151,7 @@ var init_v1_to_v2 = __esm(() => {
|
|
|
16151
16151
|
];
|
|
16152
16152
|
v1ToV2Migration = {
|
|
16153
16153
|
fromVersion: 1,
|
|
16154
|
-
toVersion:
|
|
16154
|
+
toVersion: 2,
|
|
16155
16155
|
description: "Migrate legacy failover route fields to config model v2",
|
|
16156
16156
|
migrate(input) {
|
|
16157
16157
|
const config = cloneJson(input);
|
|
@@ -16184,7 +16184,259 @@ var init_v1_to_v2 = __esm(() => {
|
|
|
16184
16184
|
}
|
|
16185
16185
|
return cleanupEmptyObjects(migratedRoute);
|
|
16186
16186
|
});
|
|
16187
|
-
config.configVersion =
|
|
16187
|
+
config.configVersion = 2;
|
|
16188
|
+
return {
|
|
16189
|
+
config: cleanupEmptyObjects(config),
|
|
16190
|
+
changes,
|
|
16191
|
+
warnings
|
|
16192
|
+
};
|
|
16193
|
+
}
|
|
16194
|
+
};
|
|
16195
|
+
});
|
|
16196
|
+
|
|
16197
|
+
// src/config-migrations/versions/v2-to-v3.ts
|
|
16198
|
+
function renameKeysRecursive(obj, mappings, changes) {
|
|
16199
|
+
if (Array.isArray(obj)) {
|
|
16200
|
+
return obj.map((item) => renameKeysRecursive(item, mappings, changes));
|
|
16201
|
+
}
|
|
16202
|
+
if (!isPlainRecord(obj)) {
|
|
16203
|
+
return obj;
|
|
16204
|
+
}
|
|
16205
|
+
const result = {};
|
|
16206
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
16207
|
+
const mapping = mappings.find(([from]) => from === key);
|
|
16208
|
+
const newKey = mapping ? mapping[1] : key;
|
|
16209
|
+
if (mapping) {
|
|
16210
|
+
changes.push({
|
|
16211
|
+
type: "rename",
|
|
16212
|
+
path: key,
|
|
16213
|
+
from: key,
|
|
16214
|
+
to: newKey,
|
|
16215
|
+
message: `Renamed ${key} to ${newKey}`
|
|
16216
|
+
});
|
|
16217
|
+
}
|
|
16218
|
+
result[newKey] = renameKeysRecursive(value, mappings, changes);
|
|
16219
|
+
}
|
|
16220
|
+
return result;
|
|
16221
|
+
}
|
|
16222
|
+
function migrateRoutesToServiceRef(config, changes, warnings) {
|
|
16223
|
+
if (!Array.isArray(config.routes))
|
|
16224
|
+
return;
|
|
16225
|
+
const upstreamSignatureMap = new Map;
|
|
16226
|
+
const services = [];
|
|
16227
|
+
for (const route of config.routes) {
|
|
16228
|
+
if (!isPlainRecord(route))
|
|
16229
|
+
continue;
|
|
16230
|
+
const endpoints = route.endpoints;
|
|
16231
|
+
if (!Array.isArray(endpoints) || endpoints.length === 0) {
|
|
16232
|
+
warnings.push({
|
|
16233
|
+
path: `route "${route.path || "unknown"}"`,
|
|
16234
|
+
message: "Route has no endpoints; skipping service extraction"
|
|
16235
|
+
});
|
|
16236
|
+
continue;
|
|
16237
|
+
}
|
|
16238
|
+
const signature = JSON.stringify(endpoints.map((u) => ({
|
|
16239
|
+
target: u.target,
|
|
16240
|
+
weight: u.weight,
|
|
16241
|
+
priority: u.priority
|
|
16242
|
+
})));
|
|
16243
|
+
let serviceName = upstreamSignatureMap.get(signature);
|
|
16244
|
+
if (!serviceName) {
|
|
16245
|
+
serviceName = `service-${services.length + 1}`;
|
|
16246
|
+
upstreamSignatureMap.set(signature, serviceName);
|
|
16247
|
+
const service = {
|
|
16248
|
+
name: serviceName,
|
|
16249
|
+
endpoints
|
|
16250
|
+
};
|
|
16251
|
+
if (isPlainRecord(route.health_check)) {
|
|
16252
|
+
service.health_check = route.health_check;
|
|
16253
|
+
}
|
|
16254
|
+
services.push(service);
|
|
16255
|
+
changes.push({
|
|
16256
|
+
type: "move",
|
|
16257
|
+
path: `route "${route.path}".endpoints`,
|
|
16258
|
+
from: "route.endpoints",
|
|
16259
|
+
to: `services.${serviceName}.endpoints`,
|
|
16260
|
+
message: `Extracted endpoints to service "${serviceName}"`
|
|
16261
|
+
});
|
|
16262
|
+
}
|
|
16263
|
+
route.service = serviceName;
|
|
16264
|
+
delete route.endpoints;
|
|
16265
|
+
}
|
|
16266
|
+
config.services = services;
|
|
16267
|
+
}
|
|
16268
|
+
var CAMEL_TO_SNAKE_MAPPINGS, v2ToV3Migration;
|
|
16269
|
+
var init_v2_to_v3 = __esm(() => {
|
|
16270
|
+
CAMEL_TO_SNAKE_MAPPINGS = [
|
|
16271
|
+
["configVersion", "config_version"],
|
|
16272
|
+
["logLevel", "log_level"],
|
|
16273
|
+
["bodyParserLimit", "body_parser_limit"],
|
|
16274
|
+
["pathRewrite", "path_rewrite"],
|
|
16275
|
+
["stickySession", "sticky_session"],
|
|
16276
|
+
["keyExpression", "key_expression"],
|
|
16277
|
+
["connectMs", "connect_ms"],
|
|
16278
|
+
["requestMs", "request_ms"],
|
|
16279
|
+
["retryOn", "retry_on"],
|
|
16280
|
+
["passiveHealth", "passive_health"],
|
|
16281
|
+
["consecutiveFailures", "consecutive_failures"],
|
|
16282
|
+
["healthySuccesses", "healthy_successes"],
|
|
16283
|
+
["autoDisableThreshold", "auto_disable_threshold"],
|
|
16284
|
+
["autoEnableOnActiveHealthCheck", "auto_enable_on_active_health_check"],
|
|
16285
|
+
["probeIntervalMs", "probe_interval_ms"],
|
|
16286
|
+
["probeTimeoutMs", "probe_timeout_ms"],
|
|
16287
|
+
["slowStart", "slow_start"],
|
|
16288
|
+
["durationMs", "duration_ms"],
|
|
16289
|
+
["initialWeightFactor", "initial_weight_factor"],
|
|
16290
|
+
["healthCheck", "health_check"],
|
|
16291
|
+
["intervalMs", "interval_ms"],
|
|
16292
|
+
["timeoutMs", "timeout_ms"],
|
|
16293
|
+
["expectedStatus", "expected_status"],
|
|
16294
|
+
["unhealthyThreshold", "unhealthy_threshold"],
|
|
16295
|
+
["healthyThreshold", "healthy_threshold"],
|
|
16296
|
+
["contentType", "content_type"],
|
|
16297
|
+
["maxSize", "max_size"],
|
|
16298
|
+
["retentionDays", "retention_days"],
|
|
16299
|
+
["upstreams", "endpoints"]
|
|
16300
|
+
];
|
|
16301
|
+
v2ToV3Migration = {
|
|
16302
|
+
fromVersion: 2,
|
|
16303
|
+
toVersion: 3,
|
|
16304
|
+
description: "Migrate camelCase to snake_case and extract upstreams into services",
|
|
16305
|
+
migrate(input) {
|
|
16306
|
+
const config = cloneJson(input);
|
|
16307
|
+
if (!isPlainRecord(config)) {
|
|
16308
|
+
throw new Error("Config must be an object");
|
|
16309
|
+
}
|
|
16310
|
+
const changes = [];
|
|
16311
|
+
const warnings = [];
|
|
16312
|
+
const renamedConfig = renameKeysRecursive(config, CAMEL_TO_SNAKE_MAPPINGS, changes);
|
|
16313
|
+
migrateRoutesToServiceRef(renamedConfig, changes, warnings);
|
|
16314
|
+
renamedConfig.config_version = 3;
|
|
16315
|
+
return {
|
|
16316
|
+
config: cleanupEmptyObjects(renamedConfig),
|
|
16317
|
+
changes,
|
|
16318
|
+
warnings
|
|
16319
|
+
};
|
|
16320
|
+
}
|
|
16321
|
+
};
|
|
16322
|
+
});
|
|
16323
|
+
|
|
16324
|
+
// src/config-migrations/versions/v3-to-v4.ts
|
|
16325
|
+
function serviceNameFromPath(path6) {
|
|
16326
|
+
const base = path6.replace(/^\/+/, "").replace(/\/+$/, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
|
|
16327
|
+
return `${base || "root"}-service`;
|
|
16328
|
+
}
|
|
16329
|
+
function uniqueServiceName(preferredName, existingNames) {
|
|
16330
|
+
if (!existingNames.has(preferredName)) {
|
|
16331
|
+
existingNames.add(preferredName);
|
|
16332
|
+
return preferredName;
|
|
16333
|
+
}
|
|
16334
|
+
let suffix = 2;
|
|
16335
|
+
while (existingNames.has(`${preferredName}-${suffix}`)) {
|
|
16336
|
+
suffix++;
|
|
16337
|
+
}
|
|
16338
|
+
const name = `${preferredName}-${suffix}`;
|
|
16339
|
+
existingNames.add(name);
|
|
16340
|
+
return name;
|
|
16341
|
+
}
|
|
16342
|
+
function moveRouteConfigToService(route, service, routeIndex, changes) {
|
|
16343
|
+
if (route.failover && !service.failover) {
|
|
16344
|
+
service.failover = route.failover;
|
|
16345
|
+
changes.push({
|
|
16346
|
+
type: "move",
|
|
16347
|
+
path: `routes.${routeIndex}.failover`,
|
|
16348
|
+
from: `routes.${routeIndex}.failover`,
|
|
16349
|
+
to: `services.${service.name}.failover`,
|
|
16350
|
+
message: `Moved route failover to service "${service.name}"`
|
|
16351
|
+
});
|
|
16352
|
+
}
|
|
16353
|
+
if (route.sticky_session && !service.sticky_session) {
|
|
16354
|
+
service.sticky_session = route.sticky_session;
|
|
16355
|
+
changes.push({
|
|
16356
|
+
type: "move",
|
|
16357
|
+
path: `routes.${routeIndex}.sticky_session`,
|
|
16358
|
+
from: `routes.${routeIndex}.sticky_session`,
|
|
16359
|
+
to: `services.${service.name}.sticky_session`,
|
|
16360
|
+
message: `Moved route sticky_session to service "${service.name}"`
|
|
16361
|
+
});
|
|
16362
|
+
}
|
|
16363
|
+
if (route.failover) {
|
|
16364
|
+
delete route.failover;
|
|
16365
|
+
}
|
|
16366
|
+
if (route.sticky_session) {
|
|
16367
|
+
delete route.sticky_session;
|
|
16368
|
+
}
|
|
16369
|
+
}
|
|
16370
|
+
var v3ToV4Migration;
|
|
16371
|
+
var init_v3_to_v4 = __esm(() => {
|
|
16372
|
+
v3ToV4Migration = {
|
|
16373
|
+
fromVersion: 3,
|
|
16374
|
+
toVersion: 4,
|
|
16375
|
+
description: "Move failover and sticky_session from routes to services",
|
|
16376
|
+
migrate(input) {
|
|
16377
|
+
const config = cloneJson(input);
|
|
16378
|
+
if (!isPlainRecord(config)) {
|
|
16379
|
+
throw new Error("Config must be an object");
|
|
16380
|
+
}
|
|
16381
|
+
const changes = [];
|
|
16382
|
+
const warnings = [];
|
|
16383
|
+
const routes = Array.isArray(config.routes) ? config.routes : [];
|
|
16384
|
+
const services = Array.isArray(config.services) ? config.services : [];
|
|
16385
|
+
const serviceNames = new Set(services.map((service) => service.name));
|
|
16386
|
+
for (let routeIndex = 0;routeIndex < routes.length; routeIndex++) {
|
|
16387
|
+
const route = routes[routeIndex];
|
|
16388
|
+
if (!isPlainRecord(route)) {
|
|
16389
|
+
continue;
|
|
16390
|
+
}
|
|
16391
|
+
const hasRouteFailover = route.failover !== undefined;
|
|
16392
|
+
const hasRouteStickySession = route.sticky_session !== undefined;
|
|
16393
|
+
if (!hasRouteFailover && !hasRouteStickySession) {
|
|
16394
|
+
continue;
|
|
16395
|
+
}
|
|
16396
|
+
if (route.service) {
|
|
16397
|
+
let service2 = services.find((candidate) => candidate.name === route.service);
|
|
16398
|
+
if (!service2) {
|
|
16399
|
+
const routeEndpoints2 = route.endpoints ?? [];
|
|
16400
|
+
service2 = {
|
|
16401
|
+
name: route.service,
|
|
16402
|
+
endpoints: routeEndpoints2
|
|
16403
|
+
};
|
|
16404
|
+
services.push(service2);
|
|
16405
|
+
serviceNames.add(service2.name);
|
|
16406
|
+
changes.push({
|
|
16407
|
+
type: "move",
|
|
16408
|
+
path: `routes.${routeIndex}.service`,
|
|
16409
|
+
to: `services.${service2.name}`,
|
|
16410
|
+
message: `Created missing service "${service2.name}" for route "${route.path}"`
|
|
16411
|
+
});
|
|
16412
|
+
}
|
|
16413
|
+
moveRouteConfigToService(route, service2, routeIndex, changes);
|
|
16414
|
+
continue;
|
|
16415
|
+
}
|
|
16416
|
+
const serviceName = uniqueServiceName(serviceNameFromPath(route.path), serviceNames);
|
|
16417
|
+
const routeEndpoints = route.endpoints ?? [];
|
|
16418
|
+
const service = {
|
|
16419
|
+
name: serviceName,
|
|
16420
|
+
endpoints: routeEndpoints,
|
|
16421
|
+
...route.failover && { failover: route.failover },
|
|
16422
|
+
...route.sticky_session && { sticky_session: route.sticky_session }
|
|
16423
|
+
};
|
|
16424
|
+
services.push(service);
|
|
16425
|
+
route.service = serviceName;
|
|
16426
|
+
delete route.endpoints;
|
|
16427
|
+
changes.push({
|
|
16428
|
+
type: "move",
|
|
16429
|
+
path: `routes.${routeIndex}`,
|
|
16430
|
+
from: `routes.${routeIndex}.endpoints/failover/sticky_session`,
|
|
16431
|
+
to: `services.${serviceName}`,
|
|
16432
|
+
message: `Created service "${serviceName}" from route "${route.path}"`
|
|
16433
|
+
});
|
|
16434
|
+
moveRouteConfigToService(route, service, routeIndex, changes);
|
|
16435
|
+
}
|
|
16436
|
+
if (services.length > 0) {
|
|
16437
|
+
config.services = services;
|
|
16438
|
+
}
|
|
16439
|
+
config.config_version = 4;
|
|
16188
16440
|
return {
|
|
16189
16441
|
config: cleanupEmptyObjects(config),
|
|
16190
16442
|
changes,
|
|
@@ -16198,8 +16450,12 @@ var init_v1_to_v2 = __esm(() => {
|
|
|
16198
16450
|
var CONFIG_MIGRATIONS;
|
|
16199
16451
|
var init_config_migrations = __esm(() => {
|
|
16200
16452
|
init_v1_to_v2();
|
|
16453
|
+
init_v2_to_v3();
|
|
16454
|
+
init_v3_to_v4();
|
|
16201
16455
|
CONFIG_MIGRATIONS = [
|
|
16202
|
-
v1ToV2Migration
|
|
16456
|
+
v1ToV2Migration,
|
|
16457
|
+
v2ToV3Migration,
|
|
16458
|
+
v3ToV4Migration
|
|
16203
16459
|
];
|
|
16204
16460
|
});
|
|
16205
16461
|
|
|
@@ -16228,7 +16484,7 @@ function migrateConfigToLatest(rawConfig) {
|
|
|
16228
16484
|
if (!isPlainRecord(workingConfig)) {
|
|
16229
16485
|
throw new Error("Migrated config must be an object");
|
|
16230
16486
|
}
|
|
16231
|
-
workingConfig.
|
|
16487
|
+
workingConfig.config_version = LATEST_CONFIG_VERSION;
|
|
16232
16488
|
return {
|
|
16233
16489
|
config: workingConfig,
|
|
16234
16490
|
originalVersion,
|
|
@@ -16251,105 +16507,119 @@ function ensurePositiveNumber(value, field, context) {
|
|
|
16251
16507
|
}
|
|
16252
16508
|
return value;
|
|
16253
16509
|
}
|
|
16254
|
-
function
|
|
16510
|
+
function resolveRouteEndpoints(route, services) {
|
|
16511
|
+
if (route.service) {
|
|
16512
|
+
const svc = services?.find((s) => s.name === route.service);
|
|
16513
|
+
if (!svc) {
|
|
16514
|
+
logger.error(`Route "${route.path}" references unknown service "${route.service}".`);
|
|
16515
|
+
process.exit(1);
|
|
16516
|
+
}
|
|
16517
|
+
return svc.endpoints;
|
|
16518
|
+
}
|
|
16519
|
+
const routeEndpoints = route.endpoints ?? [];
|
|
16520
|
+
if (routeEndpoints.length > 0) {
|
|
16521
|
+
return routeEndpoints;
|
|
16522
|
+
}
|
|
16523
|
+
if (route.direct_response?.enabled || route.redirect?.enabled || route.response_rules?.some((rule) => rule.enabled)) {
|
|
16524
|
+
return [];
|
|
16525
|
+
}
|
|
16526
|
+
logger.error(`Route "${route.path}" must have "endpoints", "service", "response_rules", "direct_response", or "redirect" defined.`);
|
|
16527
|
+
process.exit(1);
|
|
16528
|
+
}
|
|
16529
|
+
function normalizeRouteConfig(route, services) {
|
|
16255
16530
|
route.timeouts ??= {};
|
|
16256
|
-
route
|
|
16257
|
-
|
|
16258
|
-
|
|
16259
|
-
|
|
16531
|
+
const endpoints = resolveRouteEndpoints(route, services);
|
|
16532
|
+
route.endpoints = endpoints;
|
|
16533
|
+
for (const endpoint of endpoints) {
|
|
16534
|
+
if (endpoint.weight === undefined) {
|
|
16535
|
+
endpoint.weight = 100;
|
|
16260
16536
|
}
|
|
16261
|
-
if (
|
|
16262
|
-
|
|
16537
|
+
if (endpoint.priority === undefined) {
|
|
16538
|
+
endpoint.priority = 1;
|
|
16263
16539
|
}
|
|
16264
16540
|
}
|
|
16265
16541
|
return route;
|
|
16266
16542
|
}
|
|
16267
|
-
function validateStickySession(
|
|
16268
|
-
if (
|
|
16543
|
+
function validateStickySession(service) {
|
|
16544
|
+
if (service.sticky_session === undefined) {
|
|
16269
16545
|
return;
|
|
16270
16546
|
}
|
|
16271
|
-
if (typeof
|
|
16272
|
-
logger.error(`Invalid
|
|
16547
|
+
if (typeof service.sticky_session !== "object" || service.sticky_session === null || Array.isArray(service.sticky_session)) {
|
|
16548
|
+
logger.error(`Invalid sticky_session config in service "${service.name}". sticky_session must be an object.`);
|
|
16273
16549
|
process.exit(1);
|
|
16274
16550
|
}
|
|
16275
|
-
if (
|
|
16276
|
-
logger.error(`Invalid
|
|
16551
|
+
if (service.sticky_session.enabled !== undefined && typeof service.sticky_session.enabled !== "boolean") {
|
|
16552
|
+
logger.error(`Invalid sticky_session.enabled in service "${service.name}". sticky_session.enabled must be a boolean.`);
|
|
16277
16553
|
process.exit(1);
|
|
16278
16554
|
}
|
|
16279
|
-
if (
|
|
16280
|
-
logger.error(`Invalid
|
|
16555
|
+
if (service.sticky_session.key_expression !== undefined && typeof service.sticky_session.key_expression !== "string") {
|
|
16556
|
+
logger.error(`Invalid sticky_session.key_expression in service "${service.name}". sticky_session.key_expression must be a string.`);
|
|
16281
16557
|
process.exit(1);
|
|
16282
16558
|
}
|
|
16283
16559
|
}
|
|
16284
|
-
function validateFailoverConfig(
|
|
16285
|
-
const failover =
|
|
16560
|
+
function validateFailoverConfig(service) {
|
|
16561
|
+
const failover = service.failover;
|
|
16286
16562
|
if (!failover) {
|
|
16287
16563
|
return;
|
|
16288
16564
|
}
|
|
16289
16565
|
if (typeof failover.enabled !== "boolean") {
|
|
16290
|
-
logger.error(`Invalid failover.enabled in
|
|
16566
|
+
logger.error(`Invalid failover.enabled in service "${service.name}". failover.enabled must be a boolean.`);
|
|
16291
16567
|
process.exit(1);
|
|
16292
16568
|
}
|
|
16293
|
-
if (
|
|
16294
|
-
logger.warn(`
|
|
16569
|
+
if (service.endpoints.length < 2 && failover.enabled) {
|
|
16570
|
+
logger.warn(`Service "${service.name}" has failover enabled but less than 2 endpoints. Failover will not be active.`);
|
|
16295
16571
|
}
|
|
16296
|
-
if (
|
|
16297
|
-
ensurePositiveNumber(
|
|
16572
|
+
if (failover.recovery?.probe_interval_ms !== undefined) {
|
|
16573
|
+
ensurePositiveNumber(failover.recovery.probe_interval_ms, "failover.recovery.probe_interval_ms", `service "${service.name}"`);
|
|
16298
16574
|
}
|
|
16299
|
-
if (
|
|
16300
|
-
ensurePositiveNumber(
|
|
16575
|
+
if (failover.recovery?.probe_timeout_ms !== undefined) {
|
|
16576
|
+
ensurePositiveNumber(failover.recovery.probe_timeout_ms, "failover.recovery.probe_timeout_ms", `service "${service.name}"`);
|
|
16301
16577
|
}
|
|
16302
|
-
if (failover.
|
|
16303
|
-
ensurePositiveNumber(failover.
|
|
16578
|
+
if (failover.passive_health?.consecutive_failures !== undefined) {
|
|
16579
|
+
ensurePositiveNumber(failover.passive_health.consecutive_failures, "failover.passive_health.consecutive_failures", `service "${service.name}"`);
|
|
16304
16580
|
}
|
|
16305
|
-
if (failover.
|
|
16306
|
-
ensurePositiveNumber(failover.
|
|
16581
|
+
if (failover.passive_health?.healthy_successes !== undefined) {
|
|
16582
|
+
ensurePositiveNumber(failover.passive_health.healthy_successes, "failover.passive_health.healthy_successes", `service "${service.name}"`);
|
|
16307
16583
|
}
|
|
16308
|
-
if (failover.
|
|
16309
|
-
ensurePositiveNumber(failover.
|
|
16584
|
+
if (failover.passive_health?.auto_disable_threshold !== undefined) {
|
|
16585
|
+
ensurePositiveNumber(failover.passive_health.auto_disable_threshold, "failover.passive_health.auto_disable_threshold", `service "${service.name}"`);
|
|
16310
16586
|
}
|
|
16311
|
-
if (failover.
|
|
16312
|
-
|
|
16313
|
-
}
|
|
16314
|
-
if (failover.passiveHealth?.autoDisableThreshold !== undefined) {
|
|
16315
|
-
ensurePositiveNumber(failover.passiveHealth.autoDisableThreshold, "failover.passiveHealth.autoDisableThreshold", `route "${route.path}"`);
|
|
16316
|
-
}
|
|
16317
|
-
if (failover.passiveHealth?.autoEnableOnActiveHealthCheck !== undefined && typeof failover.passiveHealth.autoEnableOnActiveHealthCheck !== "boolean") {
|
|
16318
|
-
logger.error(`Invalid failover.passiveHealth.autoEnableOnActiveHealthCheck in route "${route.path}". It must be a boolean.`);
|
|
16587
|
+
if (failover.passive_health?.auto_enable_on_active_health_check !== undefined && typeof failover.passive_health.auto_enable_on_active_health_check !== "boolean") {
|
|
16588
|
+
logger.error(`Invalid failover.passive_health.auto_enable_on_active_health_check in service "${service.name}". It must be a boolean.`);
|
|
16319
16589
|
process.exit(1);
|
|
16320
16590
|
}
|
|
16321
|
-
if (failover.
|
|
16322
|
-
if (typeof failover.
|
|
16323
|
-
logger.error(`Invalid failover.
|
|
16591
|
+
if (failover.slow_start) {
|
|
16592
|
+
if (typeof failover.slow_start.enabled !== "boolean") {
|
|
16593
|
+
logger.error(`Invalid failover.slow_start.enabled in service "${service.name}". It must be a boolean.`);
|
|
16324
16594
|
process.exit(1);
|
|
16325
16595
|
}
|
|
16326
|
-
if (failover.
|
|
16327
|
-
ensurePositiveNumber(failover.
|
|
16596
|
+
if (failover.slow_start.duration_ms !== undefined) {
|
|
16597
|
+
ensurePositiveNumber(failover.slow_start.duration_ms, "failover.slow_start.duration_ms", `service "${service.name}"`);
|
|
16328
16598
|
}
|
|
16329
|
-
if (failover.
|
|
16330
|
-
const factor = failover.
|
|
16599
|
+
if (failover.slow_start.initial_weight_factor !== undefined) {
|
|
16600
|
+
const factor = failover.slow_start.initial_weight_factor;
|
|
16331
16601
|
if (typeof factor !== "number" || Number.isNaN(factor) || factor <= 0 || factor > 1) {
|
|
16332
|
-
logger.error(`failover.
|
|
16602
|
+
logger.error(`failover.slow_start.initial_weight_factor in service "${service.name}" must be between 0 and 1.`);
|
|
16333
16603
|
process.exit(1);
|
|
16334
16604
|
}
|
|
16335
16605
|
}
|
|
16336
16606
|
}
|
|
16337
|
-
if (failover.
|
|
16338
|
-
if (typeof failover.
|
|
16339
|
-
logger.error(`Invalid failover.
|
|
16607
|
+
if (failover.health_check) {
|
|
16608
|
+
if (typeof failover.health_check.enabled !== "boolean") {
|
|
16609
|
+
logger.error(`Invalid failover.health_check.enabled in service "${service.name}". It must be a boolean.`);
|
|
16340
16610
|
process.exit(1);
|
|
16341
16611
|
}
|
|
16342
|
-
if (failover.
|
|
16343
|
-
ensurePositiveNumber(failover.
|
|
16612
|
+
if (failover.health_check.interval_ms !== undefined) {
|
|
16613
|
+
ensurePositiveNumber(failover.health_check.interval_ms, "failover.health_check.interval_ms", `service "${service.name}"`);
|
|
16344
16614
|
}
|
|
16345
|
-
if (failover.
|
|
16346
|
-
ensurePositiveNumber(failover.
|
|
16615
|
+
if (failover.health_check.timeout_ms !== undefined) {
|
|
16616
|
+
ensurePositiveNumber(failover.health_check.timeout_ms, "failover.health_check.timeout_ms", `service "${service.name}"`);
|
|
16347
16617
|
}
|
|
16348
|
-
if (failover.
|
|
16349
|
-
ensurePositiveNumber(failover.
|
|
16618
|
+
if (failover.health_check.unhealthy_threshold !== undefined) {
|
|
16619
|
+
ensurePositiveNumber(failover.health_check.unhealthy_threshold, "failover.health_check.unhealthy_threshold", `service "${service.name}"`);
|
|
16350
16620
|
}
|
|
16351
|
-
if (failover.
|
|
16352
|
-
ensurePositiveNumber(failover.
|
|
16621
|
+
if (failover.health_check.healthy_threshold !== undefined) {
|
|
16622
|
+
ensurePositiveNumber(failover.health_check.healthy_threshold, "failover.health_check.healthy_threshold", `service "${service.name}"`);
|
|
16353
16623
|
}
|
|
16354
16624
|
}
|
|
16355
16625
|
}
|
|
@@ -16377,44 +16647,92 @@ function validateAuthConfig(authConfig, context) {
|
|
|
16377
16647
|
}
|
|
16378
16648
|
logger.debug(`Auth config validated successfully for ${context}`);
|
|
16379
16649
|
}
|
|
16650
|
+
function validateServices(services) {
|
|
16651
|
+
const names = new Set;
|
|
16652
|
+
for (const service of services) {
|
|
16653
|
+
if (!service.name || typeof service.name !== "string") {
|
|
16654
|
+
logger.error('Each service must have a non-empty "name" string.');
|
|
16655
|
+
process.exit(1);
|
|
16656
|
+
}
|
|
16657
|
+
if (names.has(service.name)) {
|
|
16658
|
+
logger.error(`Duplicate service name: "${service.name}". Service names must be unique.`);
|
|
16659
|
+
process.exit(1);
|
|
16660
|
+
}
|
|
16661
|
+
names.add(service.name);
|
|
16662
|
+
if (!service.endpoints || !Array.isArray(service.endpoints) || service.endpoints.length === 0) {
|
|
16663
|
+
logger.error(`Service "${service.name}" must have a non-empty "endpoints" array.`);
|
|
16664
|
+
process.exit(1);
|
|
16665
|
+
}
|
|
16666
|
+
for (const endpoint of service.endpoints) {
|
|
16667
|
+
if (typeof endpoint.target !== "string") {
|
|
16668
|
+
logger.error(`Invalid endpoint in service "${service.name}". Each endpoint must have a string "target".`);
|
|
16669
|
+
process.exit(1);
|
|
16670
|
+
}
|
|
16671
|
+
}
|
|
16672
|
+
validateStickySession(service);
|
|
16673
|
+
validateFailoverConfig(service);
|
|
16674
|
+
}
|
|
16675
|
+
}
|
|
16380
16676
|
function validateAndNormalizeConfig(config) {
|
|
16381
16677
|
if (!config.routes || !Array.isArray(config.routes)) {
|
|
16382
16678
|
logger.error('Error: "routes" is not defined or not an array in config.json.');
|
|
16383
16679
|
process.exit(1);
|
|
16384
16680
|
}
|
|
16681
|
+
if (config.services && Array.isArray(config.services)) {
|
|
16682
|
+
validateServices(config.services);
|
|
16683
|
+
}
|
|
16385
16684
|
if (config.auth) {
|
|
16386
16685
|
validateAuthConfig(config.auth, "global");
|
|
16387
16686
|
}
|
|
16388
16687
|
for (const route of config.routes) {
|
|
16389
|
-
|
|
16390
|
-
|
|
16688
|
+
const hasEndpoints = route.endpoints && route.endpoints.length > 0;
|
|
16689
|
+
const hasServiceRef = !!route.service;
|
|
16690
|
+
const hasDirectResponse = !!route.direct_response?.enabled;
|
|
16691
|
+
const hasRedirect = !!route.redirect?.enabled;
|
|
16692
|
+
const hasResponseRules = !!route.response_rules?.some((rule) => rule.enabled);
|
|
16693
|
+
if (!hasEndpoints && !hasServiceRef && !hasDirectResponse && !hasRedirect && !hasResponseRules) {
|
|
16694
|
+
logger.error(`Route for path "${route.path}" must have "endpoints", "service", "response_rules", "direct_response", or "redirect" defined.`);
|
|
16391
16695
|
process.exit(1);
|
|
16392
16696
|
}
|
|
16697
|
+
if (hasServiceRef && config.services) {
|
|
16698
|
+
const svc = config.services.find((s) => s.name === route.service);
|
|
16699
|
+
if (!svc) {
|
|
16700
|
+
logger.error(`Route "${route.path}" references unknown service "${route.service}".`);
|
|
16701
|
+
process.exit(1);
|
|
16702
|
+
}
|
|
16703
|
+
}
|
|
16393
16704
|
if (route.auth) {
|
|
16394
16705
|
validateAuthConfig(route.auth, `route "${route.path}"`);
|
|
16395
16706
|
}
|
|
16396
|
-
|
|
16397
|
-
|
|
16398
|
-
|
|
16399
|
-
|
|
16400
|
-
|
|
16401
|
-
|
|
16402
|
-
|
|
16403
|
-
|
|
16404
|
-
|
|
16405
|
-
|
|
16406
|
-
|
|
16407
|
-
|
|
16707
|
+
normalizeRouteConfig(route, config.services);
|
|
16708
|
+
if (route.timeouts?.connect_ms !== undefined) {
|
|
16709
|
+
ensurePositiveNumber(route.timeouts.connect_ms, "timeouts.connect_ms", `route "${route.path}"`);
|
|
16710
|
+
}
|
|
16711
|
+
if (route.timeouts?.request_ms !== undefined) {
|
|
16712
|
+
ensurePositiveNumber(route.timeouts.request_ms, "timeouts.request_ms", `route "${route.path}"`);
|
|
16713
|
+
}
|
|
16714
|
+
const endpoints = resolveRouteEndpoints(route, config.services);
|
|
16715
|
+
if (endpoints.length > 0) {
|
|
16716
|
+
let total_weight = 0;
|
|
16717
|
+
for (const endpoint of endpoints) {
|
|
16718
|
+
if (typeof endpoint.target !== "string") {
|
|
16719
|
+
logger.error(`Invalid endpoint in route for path "${route.path}". Each endpoint must have a string "target".`);
|
|
16720
|
+
process.exit(1);
|
|
16721
|
+
}
|
|
16722
|
+
if (typeof endpoint.weight !== "number" || endpoint.weight <= 0) {
|
|
16723
|
+
logger.error(`Invalid weight in route for path "${route.path}". Weight must be a positive number.`);
|
|
16724
|
+
process.exit(1);
|
|
16725
|
+
}
|
|
16726
|
+
if (typeof endpoint.priority !== "number" || endpoint.priority <= 0) {
|
|
16727
|
+
logger.error(`Invalid priority in route for path "${route.path}". Priority must be a positive number.`);
|
|
16728
|
+
process.exit(1);
|
|
16729
|
+
}
|
|
16730
|
+
total_weight += endpoint.weight;
|
|
16408
16731
|
}
|
|
16409
|
-
if (
|
|
16410
|
-
logger.error(`
|
|
16732
|
+
if (total_weight === 0) {
|
|
16733
|
+
logger.error(`Total weight for endpoints in route "${route.path}" cannot be zero.`);
|
|
16411
16734
|
process.exit(1);
|
|
16412
16735
|
}
|
|
16413
|
-
totalWeight += upstream.weight;
|
|
16414
|
-
}
|
|
16415
|
-
if (totalWeight === 0) {
|
|
16416
|
-
logger.error(`Total weight for upstreams in route "${route.path}" cannot be zero.`);
|
|
16417
|
-
process.exit(1);
|
|
16418
16736
|
}
|
|
16419
16737
|
}
|
|
16420
16738
|
return config;
|
|
@@ -16431,7 +16749,7 @@ function preloadGlobalConfig() {
|
|
|
16431
16749
|
const config = migrateConfigToLatest(rawConfig).config;
|
|
16432
16750
|
const configMapping = [
|
|
16433
16751
|
{
|
|
16434
|
-
jsonKey: "
|
|
16752
|
+
jsonKey: "log_level",
|
|
16435
16753
|
envKey: "LOG_LEVEL",
|
|
16436
16754
|
default: "info",
|
|
16437
16755
|
validate: (value) => ["trace", "debug", "info", "warn", "error", "fatal"].includes(value.toLowerCase())
|
|
@@ -16455,7 +16773,7 @@ function preloadGlobalConfig() {
|
|
|
16455
16773
|
}
|
|
16456
16774
|
},
|
|
16457
16775
|
{
|
|
16458
|
-
jsonKey: "
|
|
16776
|
+
jsonKey: "body_parser_limit",
|
|
16459
16777
|
envKey: "BODY_PARSER_LIMIT",
|
|
16460
16778
|
default: "50mb"
|
|
16461
16779
|
}
|
|
@@ -16486,7 +16804,7 @@ async function loadConfig(configPath) {
|
|
|
16486
16804
|
if (!fs6.existsSync(configFilePath)) {
|
|
16487
16805
|
logger.info(`Config file not found at ${configFilePath}, creating empty configuration`);
|
|
16488
16806
|
const minimalConfig = {
|
|
16489
|
-
|
|
16807
|
+
config_version: 4,
|
|
16490
16808
|
routes: []
|
|
16491
16809
|
};
|
|
16492
16810
|
fs6.writeFileSync(configFilePath, JSON.stringify(minimalConfig, null, 2), "utf-8");
|