@jeffusion/bungee-core 3.3.0 → 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.
Files changed (3) hide show
  1. package/dist/master.js +383 -83
  2. package/dist/worker.js +521 -210
  3. 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 config = JSON.parse(configContent);
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 config = await Bun.file(configFilePath).json();
16150
- if (!config.routes || !Array.isArray(config.routes)) {
16151
- logger.error('Error: "routes" is not defined or not an array in config.json.');
16152
- process.exit(1);
16153
- }
16154
- if (config.auth) {
16155
- validateAuthConfig(config.auth, "global");
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