@jeffusion/bungee-core 3.3.1 → 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 +703 -85
- package/dist/worker.js +1496 -473
- package/package.json +2 -2
package/dist/master.js
CHANGED
|
@@ -16046,9 +16046,697 @@ var init_migrations = __esm(() => {
|
|
|
16046
16046
|
];
|
|
16047
16047
|
});
|
|
16048
16048
|
|
|
16049
|
+
// src/config-migrations/types.ts
|
|
16050
|
+
var LATEST_CONFIG_VERSION = 4;
|
|
16051
|
+
|
|
16052
|
+
// src/config-migrations/utils.ts
|
|
16053
|
+
function isPlainObject(value) {
|
|
16054
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16055
|
+
}
|
|
16056
|
+
function cloneJson(value) {
|
|
16057
|
+
return JSON.parse(JSON.stringify(value));
|
|
16058
|
+
}
|
|
16059
|
+
function getAtPath(obj, path6) {
|
|
16060
|
+
if (!path6)
|
|
16061
|
+
return obj;
|
|
16062
|
+
return path6.split(".").reduce((current, segment) => {
|
|
16063
|
+
if (!isPlainObject(current) && !Array.isArray(current)) {
|
|
16064
|
+
return;
|
|
16065
|
+
}
|
|
16066
|
+
return current[segment];
|
|
16067
|
+
}, obj);
|
|
16068
|
+
}
|
|
16069
|
+
function setAtPath(obj, path6, value) {
|
|
16070
|
+
const segments = path6.split(".");
|
|
16071
|
+
let current = obj;
|
|
16072
|
+
for (let i = 0;i < segments.length - 1; i++) {
|
|
16073
|
+
const segment = segments[i];
|
|
16074
|
+
if (!isPlainObject(current[segment])) {
|
|
16075
|
+
current[segment] = {};
|
|
16076
|
+
}
|
|
16077
|
+
current = current[segment];
|
|
16078
|
+
}
|
|
16079
|
+
current[segments[segments.length - 1]] = value;
|
|
16080
|
+
}
|
|
16081
|
+
function deleteAtPath(obj, path6) {
|
|
16082
|
+
const segments = path6.split(".");
|
|
16083
|
+
let current = obj;
|
|
16084
|
+
for (let i = 0;i < segments.length - 1; i++) {
|
|
16085
|
+
const segment = segments[i];
|
|
16086
|
+
if (!isPlainObject(current[segment])) {
|
|
16087
|
+
return;
|
|
16088
|
+
}
|
|
16089
|
+
current = current[segment];
|
|
16090
|
+
}
|
|
16091
|
+
delete current[segments[segments.length - 1]];
|
|
16092
|
+
}
|
|
16093
|
+
function cleanupEmptyObjects(value) {
|
|
16094
|
+
if (Array.isArray(value)) {
|
|
16095
|
+
return value.map(cleanupEmptyObjects);
|
|
16096
|
+
}
|
|
16097
|
+
if (!isPlainObject(value)) {
|
|
16098
|
+
return value;
|
|
16099
|
+
}
|
|
16100
|
+
const cleanedEntries = Object.entries(value).map(([key, child]) => [key, cleanupEmptyObjects(child)]).filter(([, child]) => {
|
|
16101
|
+
if (child === undefined)
|
|
16102
|
+
return false;
|
|
16103
|
+
if (isPlainObject(child) && Object.keys(child).length === 0)
|
|
16104
|
+
return false;
|
|
16105
|
+
return true;
|
|
16106
|
+
});
|
|
16107
|
+
return Object.fromEntries(cleanedEntries);
|
|
16108
|
+
}
|
|
16109
|
+
function detectConfigVersion(rawConfig) {
|
|
16110
|
+
if (!isPlainObject(rawConfig)) {
|
|
16111
|
+
return 1;
|
|
16112
|
+
}
|
|
16113
|
+
const version = rawConfig.configVersion ?? rawConfig.config_version;
|
|
16114
|
+
return typeof version === "number" && Number.isInteger(version) && version > 0 ? version : 1;
|
|
16115
|
+
}
|
|
16116
|
+
function isPlainRecord(value) {
|
|
16117
|
+
return isPlainObject(value);
|
|
16118
|
+
}
|
|
16119
|
+
|
|
16120
|
+
// src/config-migrations/versions/v1-to-v2.ts
|
|
16121
|
+
function hasAnyValue(route, paths) {
|
|
16122
|
+
return paths.some((path6) => getAtPath(route, path6) !== undefined);
|
|
16123
|
+
}
|
|
16124
|
+
function moveLegacyField(route, fromPath, toPath, changes) {
|
|
16125
|
+
const currentValue = getAtPath(route, fromPath);
|
|
16126
|
+
if (currentValue === undefined) {
|
|
16127
|
+
return;
|
|
16128
|
+
}
|
|
16129
|
+
setAtPath(route, toPath, currentValue);
|
|
16130
|
+
deleteAtPath(route, fromPath);
|
|
16131
|
+
changes.push({
|
|
16132
|
+
type: "move",
|
|
16133
|
+
path: fromPath,
|
|
16134
|
+
from: currentValue,
|
|
16135
|
+
to: toPath,
|
|
16136
|
+
message: `Moved ${fromPath} to ${toPath}`
|
|
16137
|
+
});
|
|
16138
|
+
}
|
|
16139
|
+
var LEGACY_FIELD_MAPPINGS, v1ToV2Migration;
|
|
16140
|
+
var init_v1_to_v2 = __esm(() => {
|
|
16141
|
+
LEGACY_FIELD_MAPPINGS = [
|
|
16142
|
+
["failover.requestTimeoutMs", "timeouts.requestMs"],
|
|
16143
|
+
["failover.connectTimeoutMs", "timeouts.connectMs"],
|
|
16144
|
+
["failover.retryableStatusCodes", "failover.retryOn"],
|
|
16145
|
+
["failover.consecutiveFailuresThreshold", "failover.passiveHealth.consecutiveFailures"],
|
|
16146
|
+
["failover.healthyThreshold", "failover.passiveHealth.healthySuccesses"],
|
|
16147
|
+
["failover.autoDisableThreshold", "failover.passiveHealth.autoDisableThreshold"],
|
|
16148
|
+
["failover.autoEnableOnHealthCheck", "failover.passiveHealth.autoEnableOnActiveHealthCheck"],
|
|
16149
|
+
["failover.recoveryIntervalMs", "failover.recovery.probeIntervalMs"],
|
|
16150
|
+
["failover.recoveryTimeoutMs", "failover.recovery.probeTimeoutMs"]
|
|
16151
|
+
];
|
|
16152
|
+
v1ToV2Migration = {
|
|
16153
|
+
fromVersion: 1,
|
|
16154
|
+
toVersion: 2,
|
|
16155
|
+
description: "Migrate legacy failover route fields to config model v2",
|
|
16156
|
+
migrate(input) {
|
|
16157
|
+
const config = cloneJson(input);
|
|
16158
|
+
if (!isPlainRecord(config)) {
|
|
16159
|
+
throw new Error("Config must be an object");
|
|
16160
|
+
}
|
|
16161
|
+
if (!Array.isArray(config.routes)) {
|
|
16162
|
+
throw new Error("Config routes must be an array");
|
|
16163
|
+
}
|
|
16164
|
+
const changes = [];
|
|
16165
|
+
const warnings = [];
|
|
16166
|
+
const newFieldPaths = [
|
|
16167
|
+
"timeouts.requestMs",
|
|
16168
|
+
"timeouts.connectMs",
|
|
16169
|
+
"failover.retryOn",
|
|
16170
|
+
"failover.passiveHealth",
|
|
16171
|
+
"failover.recovery"
|
|
16172
|
+
];
|
|
16173
|
+
const legacyFieldPaths = LEGACY_FIELD_MAPPINGS.map(([fromPath]) => fromPath);
|
|
16174
|
+
config.routes = config.routes.map((route, routeIndex) => {
|
|
16175
|
+
if (!isPlainRecord(route)) {
|
|
16176
|
+
throw new Error(`Route #${routeIndex + 1} must be an object`);
|
|
16177
|
+
}
|
|
16178
|
+
const migratedRoute = cloneJson(route);
|
|
16179
|
+
if (hasAnyValue(migratedRoute, legacyFieldPaths) && hasAnyValue(migratedRoute, newFieldPaths)) {
|
|
16180
|
+
throw new Error(`Route "${migratedRoute.path || routeIndex}" mixes legacy and v2 failover fields`);
|
|
16181
|
+
}
|
|
16182
|
+
for (const [fromPath, toPath] of LEGACY_FIELD_MAPPINGS) {
|
|
16183
|
+
moveLegacyField(migratedRoute, fromPath, toPath, changes);
|
|
16184
|
+
}
|
|
16185
|
+
return cleanupEmptyObjects(migratedRoute);
|
|
16186
|
+
});
|
|
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;
|
|
16440
|
+
return {
|
|
16441
|
+
config: cleanupEmptyObjects(config),
|
|
16442
|
+
changes,
|
|
16443
|
+
warnings
|
|
16444
|
+
};
|
|
16445
|
+
}
|
|
16446
|
+
};
|
|
16447
|
+
});
|
|
16448
|
+
|
|
16449
|
+
// src/config-migrations/index.ts
|
|
16450
|
+
var CONFIG_MIGRATIONS;
|
|
16451
|
+
var init_config_migrations = __esm(() => {
|
|
16452
|
+
init_v1_to_v2();
|
|
16453
|
+
init_v2_to_v3();
|
|
16454
|
+
init_v3_to_v4();
|
|
16455
|
+
CONFIG_MIGRATIONS = [
|
|
16456
|
+
v1ToV2Migration,
|
|
16457
|
+
v2ToV3Migration,
|
|
16458
|
+
v3ToV4Migration
|
|
16459
|
+
];
|
|
16460
|
+
});
|
|
16461
|
+
|
|
16462
|
+
// src/config-migrations/migrate-config.ts
|
|
16463
|
+
function buildMigrationMap(migrations2) {
|
|
16464
|
+
return new Map(migrations2.map((migration4) => [migration4.fromVersion, migration4]));
|
|
16465
|
+
}
|
|
16466
|
+
function migrateConfigToLatest(rawConfig) {
|
|
16467
|
+
const migrationMap = buildMigrationMap(CONFIG_MIGRATIONS);
|
|
16468
|
+
const originalVersion = detectConfigVersion(rawConfig);
|
|
16469
|
+
let currentVersion = originalVersion;
|
|
16470
|
+
let workingConfig = cloneJson(rawConfig);
|
|
16471
|
+
const changes = [];
|
|
16472
|
+
const warnings = [];
|
|
16473
|
+
while (currentVersion < LATEST_CONFIG_VERSION) {
|
|
16474
|
+
const migration4 = migrationMap.get(currentVersion);
|
|
16475
|
+
if (!migration4) {
|
|
16476
|
+
throw new Error(`No config migration registered from version ${currentVersion}`);
|
|
16477
|
+
}
|
|
16478
|
+
const result = migration4.migrate(workingConfig);
|
|
16479
|
+
workingConfig = result.config;
|
|
16480
|
+
changes.push(...result.changes);
|
|
16481
|
+
warnings.push(...result.warnings);
|
|
16482
|
+
currentVersion = migration4.toVersion;
|
|
16483
|
+
}
|
|
16484
|
+
if (!isPlainRecord(workingConfig)) {
|
|
16485
|
+
throw new Error("Migrated config must be an object");
|
|
16486
|
+
}
|
|
16487
|
+
workingConfig.config_version = LATEST_CONFIG_VERSION;
|
|
16488
|
+
return {
|
|
16489
|
+
config: workingConfig,
|
|
16490
|
+
originalVersion,
|
|
16491
|
+
finalVersion: LATEST_CONFIG_VERSION,
|
|
16492
|
+
changes,
|
|
16493
|
+
warnings
|
|
16494
|
+
};
|
|
16495
|
+
}
|
|
16496
|
+
var init_migrate_config = __esm(() => {
|
|
16497
|
+
init_config_migrations();
|
|
16498
|
+
});
|
|
16499
|
+
|
|
16049
16500
|
// src/config.ts
|
|
16050
16501
|
import fs6 from "fs";
|
|
16051
16502
|
import path6 from "path";
|
|
16503
|
+
function ensurePositiveNumber(value, field, context) {
|
|
16504
|
+
if (typeof value !== "number" || Number.isNaN(value) || value <= 0) {
|
|
16505
|
+
logger.error(`${field} in ${context} must be a positive number.`);
|
|
16506
|
+
process.exit(1);
|
|
16507
|
+
}
|
|
16508
|
+
return value;
|
|
16509
|
+
}
|
|
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) {
|
|
16530
|
+
route.timeouts ??= {};
|
|
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;
|
|
16536
|
+
}
|
|
16537
|
+
if (endpoint.priority === undefined) {
|
|
16538
|
+
endpoint.priority = 1;
|
|
16539
|
+
}
|
|
16540
|
+
}
|
|
16541
|
+
return route;
|
|
16542
|
+
}
|
|
16543
|
+
function validateStickySession(service) {
|
|
16544
|
+
if (service.sticky_session === undefined) {
|
|
16545
|
+
return;
|
|
16546
|
+
}
|
|
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.`);
|
|
16549
|
+
process.exit(1);
|
|
16550
|
+
}
|
|
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.`);
|
|
16553
|
+
process.exit(1);
|
|
16554
|
+
}
|
|
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.`);
|
|
16557
|
+
process.exit(1);
|
|
16558
|
+
}
|
|
16559
|
+
}
|
|
16560
|
+
function validateFailoverConfig(service) {
|
|
16561
|
+
const failover = service.failover;
|
|
16562
|
+
if (!failover) {
|
|
16563
|
+
return;
|
|
16564
|
+
}
|
|
16565
|
+
if (typeof failover.enabled !== "boolean") {
|
|
16566
|
+
logger.error(`Invalid failover.enabled in service "${service.name}". failover.enabled must be a boolean.`);
|
|
16567
|
+
process.exit(1);
|
|
16568
|
+
}
|
|
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.`);
|
|
16571
|
+
}
|
|
16572
|
+
if (failover.recovery?.probe_interval_ms !== undefined) {
|
|
16573
|
+
ensurePositiveNumber(failover.recovery.probe_interval_ms, "failover.recovery.probe_interval_ms", `service "${service.name}"`);
|
|
16574
|
+
}
|
|
16575
|
+
if (failover.recovery?.probe_timeout_ms !== undefined) {
|
|
16576
|
+
ensurePositiveNumber(failover.recovery.probe_timeout_ms, "failover.recovery.probe_timeout_ms", `service "${service.name}"`);
|
|
16577
|
+
}
|
|
16578
|
+
if (failover.passive_health?.consecutive_failures !== undefined) {
|
|
16579
|
+
ensurePositiveNumber(failover.passive_health.consecutive_failures, "failover.passive_health.consecutive_failures", `service "${service.name}"`);
|
|
16580
|
+
}
|
|
16581
|
+
if (failover.passive_health?.healthy_successes !== undefined) {
|
|
16582
|
+
ensurePositiveNumber(failover.passive_health.healthy_successes, "failover.passive_health.healthy_successes", `service "${service.name}"`);
|
|
16583
|
+
}
|
|
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}"`);
|
|
16586
|
+
}
|
|
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.`);
|
|
16589
|
+
process.exit(1);
|
|
16590
|
+
}
|
|
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.`);
|
|
16594
|
+
process.exit(1);
|
|
16595
|
+
}
|
|
16596
|
+
if (failover.slow_start.duration_ms !== undefined) {
|
|
16597
|
+
ensurePositiveNumber(failover.slow_start.duration_ms, "failover.slow_start.duration_ms", `service "${service.name}"`);
|
|
16598
|
+
}
|
|
16599
|
+
if (failover.slow_start.initial_weight_factor !== undefined) {
|
|
16600
|
+
const factor = failover.slow_start.initial_weight_factor;
|
|
16601
|
+
if (typeof factor !== "number" || Number.isNaN(factor) || factor <= 0 || factor > 1) {
|
|
16602
|
+
logger.error(`failover.slow_start.initial_weight_factor in service "${service.name}" must be between 0 and 1.`);
|
|
16603
|
+
process.exit(1);
|
|
16604
|
+
}
|
|
16605
|
+
}
|
|
16606
|
+
}
|
|
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.`);
|
|
16610
|
+
process.exit(1);
|
|
16611
|
+
}
|
|
16612
|
+
if (failover.health_check.interval_ms !== undefined) {
|
|
16613
|
+
ensurePositiveNumber(failover.health_check.interval_ms, "failover.health_check.interval_ms", `service "${service.name}"`);
|
|
16614
|
+
}
|
|
16615
|
+
if (failover.health_check.timeout_ms !== undefined) {
|
|
16616
|
+
ensurePositiveNumber(failover.health_check.timeout_ms, "failover.health_check.timeout_ms", `service "${service.name}"`);
|
|
16617
|
+
}
|
|
16618
|
+
if (failover.health_check.unhealthy_threshold !== undefined) {
|
|
16619
|
+
ensurePositiveNumber(failover.health_check.unhealthy_threshold, "failover.health_check.unhealthy_threshold", `service "${service.name}"`);
|
|
16620
|
+
}
|
|
16621
|
+
if (failover.health_check.healthy_threshold !== undefined) {
|
|
16622
|
+
ensurePositiveNumber(failover.health_check.healthy_threshold, "failover.health_check.healthy_threshold", `service "${service.name}"`);
|
|
16623
|
+
}
|
|
16624
|
+
}
|
|
16625
|
+
}
|
|
16626
|
+
function validateAuthConfig(authConfig, context) {
|
|
16627
|
+
if (authConfig.enabled === undefined) {
|
|
16628
|
+
logger.error(`Auth config in ${context} must have an "enabled" field.`);
|
|
16629
|
+
process.exit(1);
|
|
16630
|
+
}
|
|
16631
|
+
if (!authConfig.enabled) {
|
|
16632
|
+
return;
|
|
16633
|
+
}
|
|
16634
|
+
if (!authConfig.tokens || !Array.isArray(authConfig.tokens)) {
|
|
16635
|
+
logger.error(`Auth config in ${context} must have a "tokens" array when enabled.`);
|
|
16636
|
+
process.exit(1);
|
|
16637
|
+
}
|
|
16638
|
+
if (authConfig.tokens.length === 0) {
|
|
16639
|
+
logger.error(`Auth config in ${context} must have at least one token in the "tokens" array.`);
|
|
16640
|
+
process.exit(1);
|
|
16641
|
+
}
|
|
16642
|
+
for (let i = 0;i < authConfig.tokens.length; i++) {
|
|
16643
|
+
if (typeof authConfig.tokens[i] !== "string") {
|
|
16644
|
+
logger.error(`Token at index ${i} in ${context} auth config must be a string.`);
|
|
16645
|
+
process.exit(1);
|
|
16646
|
+
}
|
|
16647
|
+
}
|
|
16648
|
+
logger.debug(`Auth config validated successfully for ${context}`);
|
|
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
|
+
}
|
|
16676
|
+
function validateAndNormalizeConfig(config) {
|
|
16677
|
+
if (!config.routes || !Array.isArray(config.routes)) {
|
|
16678
|
+
logger.error('Error: "routes" is not defined or not an array in config.json.');
|
|
16679
|
+
process.exit(1);
|
|
16680
|
+
}
|
|
16681
|
+
if (config.services && Array.isArray(config.services)) {
|
|
16682
|
+
validateServices(config.services);
|
|
16683
|
+
}
|
|
16684
|
+
if (config.auth) {
|
|
16685
|
+
validateAuthConfig(config.auth, "global");
|
|
16686
|
+
}
|
|
16687
|
+
for (const route of config.routes) {
|
|
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.`);
|
|
16695
|
+
process.exit(1);
|
|
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
|
+
}
|
|
16704
|
+
if (route.auth) {
|
|
16705
|
+
validateAuthConfig(route.auth, `route "${route.path}"`);
|
|
16706
|
+
}
|
|
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;
|
|
16731
|
+
}
|
|
16732
|
+
if (total_weight === 0) {
|
|
16733
|
+
logger.error(`Total weight for endpoints in route "${route.path}" cannot be zero.`);
|
|
16734
|
+
process.exit(1);
|
|
16735
|
+
}
|
|
16736
|
+
}
|
|
16737
|
+
}
|
|
16738
|
+
return config;
|
|
16739
|
+
}
|
|
16052
16740
|
function preloadGlobalConfig() {
|
|
16053
16741
|
try {
|
|
16054
16742
|
const configPath = process.env.CONFIG_PATH || path6.resolve(process.cwd(), "config.json");
|
|
@@ -16057,10 +16745,11 @@ function preloadGlobalConfig() {
|
|
|
16057
16745
|
return;
|
|
16058
16746
|
}
|
|
16059
16747
|
const configContent = fs6.readFileSync(configPath, "utf-8");
|
|
16060
|
-
const
|
|
16748
|
+
const rawConfig = JSON.parse(configContent);
|
|
16749
|
+
const config = migrateConfigToLatest(rawConfig).config;
|
|
16061
16750
|
const configMapping = [
|
|
16062
16751
|
{
|
|
16063
|
-
jsonKey: "
|
|
16752
|
+
jsonKey: "log_level",
|
|
16064
16753
|
envKey: "LOG_LEVEL",
|
|
16065
16754
|
default: "info",
|
|
16066
16755
|
validate: (value) => ["trace", "debug", "info", "warn", "error", "fatal"].includes(value.toLowerCase())
|
|
@@ -16084,7 +16773,7 @@ function preloadGlobalConfig() {
|
|
|
16084
16773
|
}
|
|
16085
16774
|
},
|
|
16086
16775
|
{
|
|
16087
|
-
jsonKey: "
|
|
16776
|
+
jsonKey: "body_parser_limit",
|
|
16088
16777
|
envKey: "BODY_PARSER_LIMIT",
|
|
16089
16778
|
default: "50mb"
|
|
16090
16779
|
}
|
|
@@ -16109,36 +16798,13 @@ function preloadGlobalConfig() {
|
|
|
16109
16798
|
console.log("Falling back to environment variables and defaults.");
|
|
16110
16799
|
}
|
|
16111
16800
|
}
|
|
16112
|
-
function validateAuthConfig(authConfig, context) {
|
|
16113
|
-
if (authConfig.enabled === undefined) {
|
|
16114
|
-
logger.error(`Auth config in ${context} must have an "enabled" field.`);
|
|
16115
|
-
process.exit(1);
|
|
16116
|
-
}
|
|
16117
|
-
if (!authConfig.enabled) {
|
|
16118
|
-
return;
|
|
16119
|
-
}
|
|
16120
|
-
if (!authConfig.tokens || !Array.isArray(authConfig.tokens)) {
|
|
16121
|
-
logger.error(`Auth config in ${context} must have a "tokens" array when enabled.`);
|
|
16122
|
-
process.exit(1);
|
|
16123
|
-
}
|
|
16124
|
-
if (authConfig.tokens.length === 0) {
|
|
16125
|
-
logger.error(`Auth config in ${context} must have at least one token in the "tokens" array.`);
|
|
16126
|
-
process.exit(1);
|
|
16127
|
-
}
|
|
16128
|
-
for (let i = 0;i < authConfig.tokens.length; i++) {
|
|
16129
|
-
if (typeof authConfig.tokens[i] !== "string") {
|
|
16130
|
-
logger.error(`Token at index ${i} in ${context} auth config must be a string.`);
|
|
16131
|
-
process.exit(1);
|
|
16132
|
-
}
|
|
16133
|
-
}
|
|
16134
|
-
logger.debug(`Auth config validated successfully for ${context}`);
|
|
16135
|
-
}
|
|
16136
16801
|
async function loadConfig(configPath) {
|
|
16137
16802
|
try {
|
|
16138
16803
|
const configFilePath = configPath || process.env.CONFIG_PATH || "config.json";
|
|
16139
16804
|
if (!fs6.existsSync(configFilePath)) {
|
|
16140
16805
|
logger.info(`Config file not found at ${configFilePath}, creating empty configuration`);
|
|
16141
16806
|
const minimalConfig = {
|
|
16807
|
+
config_version: 4,
|
|
16142
16808
|
routes: []
|
|
16143
16809
|
};
|
|
16144
16810
|
fs6.writeFileSync(configFilePath, JSON.stringify(minimalConfig, null, 2), "utf-8");
|
|
@@ -16146,65 +16812,16 @@ async function loadConfig(configPath) {
|
|
|
16146
16812
|
logger.warn(`\u26A0\uFE0F No routes configured. Please add routes via the web UI at http://localhost:${process.env.PORT || 8088}`);
|
|
16147
16813
|
logger.info(`\uD83D\uDCDD Built-in endpoints: /health (health check), / (web UI)`);
|
|
16148
16814
|
}
|
|
16149
|
-
const
|
|
16150
|
-
|
|
16151
|
-
|
|
16152
|
-
|
|
16153
|
-
|
|
16154
|
-
|
|
16155
|
-
|
|
16156
|
-
|
|
16157
|
-
for (const route of config.routes) {
|
|
16158
|
-
if (!route.upstreams || route.upstreams.length === 0) {
|
|
16159
|
-
logger.error(`Route for path "${route.path}" must have a non-empty "upstreams" array.`);
|
|
16160
|
-
process.exit(1);
|
|
16161
|
-
}
|
|
16162
|
-
if (route.upstreams.length < 2 && route.failover?.enabled) {
|
|
16163
|
-
logger.warn(`Route for path "${route.path}" has failover enabled but less than 2 upstreams. Failover will not be active.`);
|
|
16164
|
-
}
|
|
16165
|
-
if (route.auth) {
|
|
16166
|
-
validateAuthConfig(route.auth, `route "${route.path}"`);
|
|
16167
|
-
}
|
|
16168
|
-
if (route.stickySession !== undefined) {
|
|
16169
|
-
if (typeof route.stickySession !== "object" || route.stickySession === null || Array.isArray(route.stickySession)) {
|
|
16170
|
-
logger.error(`Invalid stickySession config in route "${route.path}". stickySession must be an object.`);
|
|
16171
|
-
process.exit(1);
|
|
16172
|
-
}
|
|
16173
|
-
if (route.stickySession.enabled !== undefined && typeof route.stickySession.enabled !== "boolean") {
|
|
16174
|
-
logger.error(`Invalid stickySession.enabled in route "${route.path}". stickySession.enabled must be a boolean.`);
|
|
16175
|
-
process.exit(1);
|
|
16176
|
-
}
|
|
16177
|
-
if (route.stickySession.keyExpression !== undefined && typeof route.stickySession.keyExpression !== "string") {
|
|
16178
|
-
logger.error(`Invalid stickySession.keyExpression in route "${route.path}". stickySession.keyExpression must be a string.`);
|
|
16179
|
-
process.exit(1);
|
|
16180
|
-
}
|
|
16181
|
-
}
|
|
16182
|
-
let totalWeight = 0;
|
|
16183
|
-
for (const upstream of route.upstreams) {
|
|
16184
|
-
if (typeof upstream.target !== "string") {
|
|
16185
|
-
logger.error(`Invalid upstream in route for path "${route.path}". Each upstream must have a string "target".`);
|
|
16186
|
-
process.exit(1);
|
|
16187
|
-
}
|
|
16188
|
-
if (upstream.weight === undefined) {
|
|
16189
|
-
upstream.weight = 100;
|
|
16190
|
-
} else if (typeof upstream.weight !== "number" || upstream.weight <= 0) {
|
|
16191
|
-
logger.error(`Invalid weight in route for path "${route.path}". Weight must be a positive number.`);
|
|
16192
|
-
process.exit(1);
|
|
16193
|
-
}
|
|
16194
|
-
if (upstream.priority === undefined) {
|
|
16195
|
-
upstream.priority = 1;
|
|
16196
|
-
} else if (typeof upstream.priority !== "number" || upstream.priority <= 0) {
|
|
16197
|
-
logger.error(`Invalid priority in route for path "${route.path}". Priority must be a positive number.`);
|
|
16198
|
-
process.exit(1);
|
|
16199
|
-
}
|
|
16200
|
-
totalWeight += upstream.weight;
|
|
16201
|
-
}
|
|
16202
|
-
if (totalWeight === 0) {
|
|
16203
|
-
logger.error(`Total weight for upstreams in route "${route.path}" cannot be zero.`);
|
|
16204
|
-
process.exit(1);
|
|
16205
|
-
}
|
|
16815
|
+
const rawConfig = await Bun.file(configFilePath).json();
|
|
16816
|
+
const migrated = migrateConfigToLatest(rawConfig);
|
|
16817
|
+
if (migrated.originalVersion !== migrated.finalVersion) {
|
|
16818
|
+
logger.info({
|
|
16819
|
+
fromVersion: migrated.originalVersion,
|
|
16820
|
+
toVersion: migrated.finalVersion,
|
|
16821
|
+
changeCount: migrated.changes.length
|
|
16822
|
+
}, "Config migrated to latest version in memory");
|
|
16206
16823
|
}
|
|
16207
|
-
return config;
|
|
16824
|
+
return validateAndNormalizeConfig(migrated.config);
|
|
16208
16825
|
} catch (error) {
|
|
16209
16826
|
logger.fatal({ error }, "Failed to load or parse config.json. Please ensure it exists and is valid JSON.");
|
|
16210
16827
|
process.exit(1);
|
|
@@ -16212,6 +16829,7 @@ async function loadConfig(configPath) {
|
|
|
16212
16829
|
}
|
|
16213
16830
|
var init_config = __esm(() => {
|
|
16214
16831
|
init_logger();
|
|
16832
|
+
init_migrate_config();
|
|
16215
16833
|
});
|
|
16216
16834
|
|
|
16217
16835
|
// src/plugin-storage-cleanup.ts
|