@jeffusion/bungee-core 3.3.1 → 4.0.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 +383 -83
- package/dist/worker.js +521 -210
- package/package.json +2 -2
package/dist/master.js
CHANGED
|
@@ -16046,9 +16046,379 @@ var init_migrations = __esm(() => {
|
|
|
16046
16046
|
];
|
|
16047
16047
|
});
|
|
16048
16048
|
|
|
16049
|
+
// src/config-migrations/types.ts
|
|
16050
|
+
var LATEST_CONFIG_VERSION = 2;
|
|
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;
|
|
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: LATEST_CONFIG_VERSION,
|
|
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 = LATEST_CONFIG_VERSION;
|
|
16188
|
+
return {
|
|
16189
|
+
config: cleanupEmptyObjects(config),
|
|
16190
|
+
changes,
|
|
16191
|
+
warnings
|
|
16192
|
+
};
|
|
16193
|
+
}
|
|
16194
|
+
};
|
|
16195
|
+
});
|
|
16196
|
+
|
|
16197
|
+
// src/config-migrations/index.ts
|
|
16198
|
+
var CONFIG_MIGRATIONS;
|
|
16199
|
+
var init_config_migrations = __esm(() => {
|
|
16200
|
+
init_v1_to_v2();
|
|
16201
|
+
CONFIG_MIGRATIONS = [
|
|
16202
|
+
v1ToV2Migration
|
|
16203
|
+
];
|
|
16204
|
+
});
|
|
16205
|
+
|
|
16206
|
+
// src/config-migrations/migrate-config.ts
|
|
16207
|
+
function buildMigrationMap(migrations2) {
|
|
16208
|
+
return new Map(migrations2.map((migration4) => [migration4.fromVersion, migration4]));
|
|
16209
|
+
}
|
|
16210
|
+
function migrateConfigToLatest(rawConfig) {
|
|
16211
|
+
const migrationMap = buildMigrationMap(CONFIG_MIGRATIONS);
|
|
16212
|
+
const originalVersion = detectConfigVersion(rawConfig);
|
|
16213
|
+
let currentVersion = originalVersion;
|
|
16214
|
+
let workingConfig = cloneJson(rawConfig);
|
|
16215
|
+
const changes = [];
|
|
16216
|
+
const warnings = [];
|
|
16217
|
+
while (currentVersion < LATEST_CONFIG_VERSION) {
|
|
16218
|
+
const migration4 = migrationMap.get(currentVersion);
|
|
16219
|
+
if (!migration4) {
|
|
16220
|
+
throw new Error(`No config migration registered from version ${currentVersion}`);
|
|
16221
|
+
}
|
|
16222
|
+
const result = migration4.migrate(workingConfig);
|
|
16223
|
+
workingConfig = result.config;
|
|
16224
|
+
changes.push(...result.changes);
|
|
16225
|
+
warnings.push(...result.warnings);
|
|
16226
|
+
currentVersion = migration4.toVersion;
|
|
16227
|
+
}
|
|
16228
|
+
if (!isPlainRecord(workingConfig)) {
|
|
16229
|
+
throw new Error("Migrated config must be an object");
|
|
16230
|
+
}
|
|
16231
|
+
workingConfig.configVersion = LATEST_CONFIG_VERSION;
|
|
16232
|
+
return {
|
|
16233
|
+
config: workingConfig,
|
|
16234
|
+
originalVersion,
|
|
16235
|
+
finalVersion: LATEST_CONFIG_VERSION,
|
|
16236
|
+
changes,
|
|
16237
|
+
warnings
|
|
16238
|
+
};
|
|
16239
|
+
}
|
|
16240
|
+
var init_migrate_config = __esm(() => {
|
|
16241
|
+
init_config_migrations();
|
|
16242
|
+
});
|
|
16243
|
+
|
|
16049
16244
|
// src/config.ts
|
|
16050
16245
|
import fs6 from "fs";
|
|
16051
16246
|
import path6 from "path";
|
|
16247
|
+
function ensurePositiveNumber(value, field, context) {
|
|
16248
|
+
if (typeof value !== "number" || Number.isNaN(value) || value <= 0) {
|
|
16249
|
+
logger.error(`${field} in ${context} must be a positive number.`);
|
|
16250
|
+
process.exit(1);
|
|
16251
|
+
}
|
|
16252
|
+
return value;
|
|
16253
|
+
}
|
|
16254
|
+
function normalizeRouteConfig(route) {
|
|
16255
|
+
route.timeouts ??= {};
|
|
16256
|
+
route.failover ??= undefined;
|
|
16257
|
+
for (const upstream of route.upstreams) {
|
|
16258
|
+
if (upstream.weight === undefined) {
|
|
16259
|
+
upstream.weight = 100;
|
|
16260
|
+
}
|
|
16261
|
+
if (upstream.priority === undefined) {
|
|
16262
|
+
upstream.priority = 1;
|
|
16263
|
+
}
|
|
16264
|
+
}
|
|
16265
|
+
return route;
|
|
16266
|
+
}
|
|
16267
|
+
function validateStickySession(route) {
|
|
16268
|
+
if (route.stickySession === undefined) {
|
|
16269
|
+
return;
|
|
16270
|
+
}
|
|
16271
|
+
if (typeof route.stickySession !== "object" || route.stickySession === null || Array.isArray(route.stickySession)) {
|
|
16272
|
+
logger.error(`Invalid stickySession config in route "${route.path}". stickySession must be an object.`);
|
|
16273
|
+
process.exit(1);
|
|
16274
|
+
}
|
|
16275
|
+
if (route.stickySession.enabled !== undefined && typeof route.stickySession.enabled !== "boolean") {
|
|
16276
|
+
logger.error(`Invalid stickySession.enabled in route "${route.path}". stickySession.enabled must be a boolean.`);
|
|
16277
|
+
process.exit(1);
|
|
16278
|
+
}
|
|
16279
|
+
if (route.stickySession.keyExpression !== undefined && typeof route.stickySession.keyExpression !== "string") {
|
|
16280
|
+
logger.error(`Invalid stickySession.keyExpression in route "${route.path}". stickySession.keyExpression must be a string.`);
|
|
16281
|
+
process.exit(1);
|
|
16282
|
+
}
|
|
16283
|
+
}
|
|
16284
|
+
function validateFailoverConfig(route) {
|
|
16285
|
+
const failover = route.failover;
|
|
16286
|
+
if (!failover) {
|
|
16287
|
+
return;
|
|
16288
|
+
}
|
|
16289
|
+
if (typeof failover.enabled !== "boolean") {
|
|
16290
|
+
logger.error(`Invalid failover.enabled in route "${route.path}". failover.enabled must be a boolean.`);
|
|
16291
|
+
process.exit(1);
|
|
16292
|
+
}
|
|
16293
|
+
if (route.upstreams.length < 2 && failover.enabled) {
|
|
16294
|
+
logger.warn(`Route for path "${route.path}" has failover enabled but less than 2 upstreams. Failover will not be active.`);
|
|
16295
|
+
}
|
|
16296
|
+
if (route.timeouts?.connectMs !== undefined) {
|
|
16297
|
+
ensurePositiveNumber(route.timeouts.connectMs, "timeouts.connectMs", `route "${route.path}"`);
|
|
16298
|
+
}
|
|
16299
|
+
if (route.timeouts?.requestMs !== undefined) {
|
|
16300
|
+
ensurePositiveNumber(route.timeouts.requestMs, "timeouts.requestMs", `route "${route.path}"`);
|
|
16301
|
+
}
|
|
16302
|
+
if (failover.recovery?.probeIntervalMs !== undefined) {
|
|
16303
|
+
ensurePositiveNumber(failover.recovery.probeIntervalMs, "failover.recovery.probeIntervalMs", `route "${route.path}"`);
|
|
16304
|
+
}
|
|
16305
|
+
if (failover.recovery?.probeTimeoutMs !== undefined) {
|
|
16306
|
+
ensurePositiveNumber(failover.recovery.probeTimeoutMs, "failover.recovery.probeTimeoutMs", `route "${route.path}"`);
|
|
16307
|
+
}
|
|
16308
|
+
if (failover.passiveHealth?.consecutiveFailures !== undefined) {
|
|
16309
|
+
ensurePositiveNumber(failover.passiveHealth.consecutiveFailures, "failover.passiveHealth.consecutiveFailures", `route "${route.path}"`);
|
|
16310
|
+
}
|
|
16311
|
+
if (failover.passiveHealth?.healthySuccesses !== undefined) {
|
|
16312
|
+
ensurePositiveNumber(failover.passiveHealth.healthySuccesses, "failover.passiveHealth.healthySuccesses", `route "${route.path}"`);
|
|
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.`);
|
|
16319
|
+
process.exit(1);
|
|
16320
|
+
}
|
|
16321
|
+
if (failover.slowStart) {
|
|
16322
|
+
if (typeof failover.slowStart.enabled !== "boolean") {
|
|
16323
|
+
logger.error(`Invalid failover.slowStart.enabled in route "${route.path}". It must be a boolean.`);
|
|
16324
|
+
process.exit(1);
|
|
16325
|
+
}
|
|
16326
|
+
if (failover.slowStart.durationMs !== undefined) {
|
|
16327
|
+
ensurePositiveNumber(failover.slowStart.durationMs, "failover.slowStart.durationMs", `route "${route.path}"`);
|
|
16328
|
+
}
|
|
16329
|
+
if (failover.slowStart.initialWeightFactor !== undefined) {
|
|
16330
|
+
const factor = failover.slowStart.initialWeightFactor;
|
|
16331
|
+
if (typeof factor !== "number" || Number.isNaN(factor) || factor <= 0 || factor > 1) {
|
|
16332
|
+
logger.error(`failover.slowStart.initialWeightFactor in route "${route.path}" must be between 0 and 1.`);
|
|
16333
|
+
process.exit(1);
|
|
16334
|
+
}
|
|
16335
|
+
}
|
|
16336
|
+
}
|
|
16337
|
+
if (failover.healthCheck) {
|
|
16338
|
+
if (typeof failover.healthCheck.enabled !== "boolean") {
|
|
16339
|
+
logger.error(`Invalid failover.healthCheck.enabled in route "${route.path}". It must be a boolean.`);
|
|
16340
|
+
process.exit(1);
|
|
16341
|
+
}
|
|
16342
|
+
if (failover.healthCheck.intervalMs !== undefined) {
|
|
16343
|
+
ensurePositiveNumber(failover.healthCheck.intervalMs, "failover.healthCheck.intervalMs", `route "${route.path}"`);
|
|
16344
|
+
}
|
|
16345
|
+
if (failover.healthCheck.timeoutMs !== undefined) {
|
|
16346
|
+
ensurePositiveNumber(failover.healthCheck.timeoutMs, "failover.healthCheck.timeoutMs", `route "${route.path}"`);
|
|
16347
|
+
}
|
|
16348
|
+
if (failover.healthCheck.unhealthyThreshold !== undefined) {
|
|
16349
|
+
ensurePositiveNumber(failover.healthCheck.unhealthyThreshold, "failover.healthCheck.unhealthyThreshold", `route "${route.path}"`);
|
|
16350
|
+
}
|
|
16351
|
+
if (failover.healthCheck.healthyThreshold !== undefined) {
|
|
16352
|
+
ensurePositiveNumber(failover.healthCheck.healthyThreshold, "failover.healthCheck.healthyThreshold", `route "${route.path}"`);
|
|
16353
|
+
}
|
|
16354
|
+
}
|
|
16355
|
+
}
|
|
16356
|
+
function validateAuthConfig(authConfig, context) {
|
|
16357
|
+
if (authConfig.enabled === undefined) {
|
|
16358
|
+
logger.error(`Auth config in ${context} must have an "enabled" field.`);
|
|
16359
|
+
process.exit(1);
|
|
16360
|
+
}
|
|
16361
|
+
if (!authConfig.enabled) {
|
|
16362
|
+
return;
|
|
16363
|
+
}
|
|
16364
|
+
if (!authConfig.tokens || !Array.isArray(authConfig.tokens)) {
|
|
16365
|
+
logger.error(`Auth config in ${context} must have a "tokens" array when enabled.`);
|
|
16366
|
+
process.exit(1);
|
|
16367
|
+
}
|
|
16368
|
+
if (authConfig.tokens.length === 0) {
|
|
16369
|
+
logger.error(`Auth config in ${context} must have at least one token in the "tokens" array.`);
|
|
16370
|
+
process.exit(1);
|
|
16371
|
+
}
|
|
16372
|
+
for (let i = 0;i < authConfig.tokens.length; i++) {
|
|
16373
|
+
if (typeof authConfig.tokens[i] !== "string") {
|
|
16374
|
+
logger.error(`Token at index ${i} in ${context} auth config must be a string.`);
|
|
16375
|
+
process.exit(1);
|
|
16376
|
+
}
|
|
16377
|
+
}
|
|
16378
|
+
logger.debug(`Auth config validated successfully for ${context}`);
|
|
16379
|
+
}
|
|
16380
|
+
function validateAndNormalizeConfig(config) {
|
|
16381
|
+
if (!config.routes || !Array.isArray(config.routes)) {
|
|
16382
|
+
logger.error('Error: "routes" is not defined or not an array in config.json.');
|
|
16383
|
+
process.exit(1);
|
|
16384
|
+
}
|
|
16385
|
+
if (config.auth) {
|
|
16386
|
+
validateAuthConfig(config.auth, "global");
|
|
16387
|
+
}
|
|
16388
|
+
for (const route of config.routes) {
|
|
16389
|
+
if (!route.upstreams || route.upstreams.length === 0) {
|
|
16390
|
+
logger.error(`Route for path "${route.path}" must have a non-empty "upstreams" array.`);
|
|
16391
|
+
process.exit(1);
|
|
16392
|
+
}
|
|
16393
|
+
if (route.auth) {
|
|
16394
|
+
validateAuthConfig(route.auth, `route "${route.path}"`);
|
|
16395
|
+
}
|
|
16396
|
+
validateStickySession(route);
|
|
16397
|
+
validateFailoverConfig(route);
|
|
16398
|
+
normalizeRouteConfig(route);
|
|
16399
|
+
let totalWeight = 0;
|
|
16400
|
+
for (const upstream of route.upstreams) {
|
|
16401
|
+
if (typeof upstream.target !== "string") {
|
|
16402
|
+
logger.error(`Invalid upstream in route for path "${route.path}". Each upstream must have a string "target".`);
|
|
16403
|
+
process.exit(1);
|
|
16404
|
+
}
|
|
16405
|
+
if (typeof upstream.weight !== "number" || upstream.weight <= 0) {
|
|
16406
|
+
logger.error(`Invalid weight in route for path "${route.path}". Weight must be a positive number.`);
|
|
16407
|
+
process.exit(1);
|
|
16408
|
+
}
|
|
16409
|
+
if (typeof upstream.priority !== "number" || upstream.priority <= 0) {
|
|
16410
|
+
logger.error(`Invalid priority in route for path "${route.path}". Priority must be a positive number.`);
|
|
16411
|
+
process.exit(1);
|
|
16412
|
+
}
|
|
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
|
+
}
|
|
16419
|
+
}
|
|
16420
|
+
return config;
|
|
16421
|
+
}
|
|
16052
16422
|
function preloadGlobalConfig() {
|
|
16053
16423
|
try {
|
|
16054
16424
|
const configPath = process.env.CONFIG_PATH || path6.resolve(process.cwd(), "config.json");
|
|
@@ -16057,7 +16427,8 @@ function preloadGlobalConfig() {
|
|
|
16057
16427
|
return;
|
|
16058
16428
|
}
|
|
16059
16429
|
const configContent = fs6.readFileSync(configPath, "utf-8");
|
|
16060
|
-
const
|
|
16430
|
+
const rawConfig = JSON.parse(configContent);
|
|
16431
|
+
const config = migrateConfigToLatest(rawConfig).config;
|
|
16061
16432
|
const configMapping = [
|
|
16062
16433
|
{
|
|
16063
16434
|
jsonKey: "logLevel",
|
|
@@ -16109,36 +16480,13 @@ function preloadGlobalConfig() {
|
|
|
16109
16480
|
console.log("Falling back to environment variables and defaults.");
|
|
16110
16481
|
}
|
|
16111
16482
|
}
|
|
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
16483
|
async function loadConfig(configPath) {
|
|
16137
16484
|
try {
|
|
16138
16485
|
const configFilePath = configPath || process.env.CONFIG_PATH || "config.json";
|
|
16139
16486
|
if (!fs6.existsSync(configFilePath)) {
|
|
16140
16487
|
logger.info(`Config file not found at ${configFilePath}, creating empty configuration`);
|
|
16141
16488
|
const minimalConfig = {
|
|
16489
|
+
configVersion: 2,
|
|
16142
16490
|
routes: []
|
|
16143
16491
|
};
|
|
16144
16492
|
fs6.writeFileSync(configFilePath, JSON.stringify(minimalConfig, null, 2), "utf-8");
|
|
@@ -16146,65 +16494,16 @@ async function loadConfig(configPath) {
|
|
|
16146
16494
|
logger.warn(`\u26A0\uFE0F No routes configured. Please add routes via the web UI at http://localhost:${process.env.PORT || 8088}`);
|
|
16147
16495
|
logger.info(`\uD83D\uDCDD Built-in endpoints: /health (health check), / (web UI)`);
|
|
16148
16496
|
}
|
|
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
|
-
}
|
|
16497
|
+
const rawConfig = await Bun.file(configFilePath).json();
|
|
16498
|
+
const migrated = migrateConfigToLatest(rawConfig);
|
|
16499
|
+
if (migrated.originalVersion !== migrated.finalVersion) {
|
|
16500
|
+
logger.info({
|
|
16501
|
+
fromVersion: migrated.originalVersion,
|
|
16502
|
+
toVersion: migrated.finalVersion,
|
|
16503
|
+
changeCount: migrated.changes.length
|
|
16504
|
+
}, "Config migrated to latest version in memory");
|
|
16206
16505
|
}
|
|
16207
|
-
return config;
|
|
16506
|
+
return validateAndNormalizeConfig(migrated.config);
|
|
16208
16507
|
} catch (error) {
|
|
16209
16508
|
logger.fatal({ error }, "Failed to load or parse config.json. Please ensure it exists and is valid JSON.");
|
|
16210
16509
|
process.exit(1);
|
|
@@ -16212,6 +16511,7 @@ async function loadConfig(configPath) {
|
|
|
16212
16511
|
}
|
|
16213
16512
|
var init_config = __esm(() => {
|
|
16214
16513
|
init_logger();
|
|
16514
|
+
init_migrate_config();
|
|
16215
16515
|
});
|
|
16216
16516
|
|
|
16217
16517
|
// src/plugin-storage-cleanup.ts
|