@jeffusion/bungee 5.24.3 → 5.25.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 (2) hide show
  1. package/dist/index.js +179 -67
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -2146,7 +2146,7 @@ var {
2146
2146
  } = exports_commander;
2147
2147
 
2148
2148
  // src/commands/recover.ts
2149
- import { resolve as resolve4, join as join7 } from "node:path";
2149
+ import { resolve as resolve4, join as join8, dirname as dirname3 } from "node:path";
2150
2150
  import { spawn as spawn3 } from "node:child_process";
2151
2151
 
2152
2152
  // src/config/paths.ts
@@ -2178,10 +2178,10 @@ class ConfigPaths {
2178
2178
  // src/daemon/manager.ts
2179
2179
  import fs2 from "node:fs";
2180
2180
  import { readFile as readFile3 } from "node:fs/promises";
2181
- import { randomBytes as randomBytes2, randomUUID as randomUUID3 } from "node:crypto";
2181
+ import { randomBytes as randomBytes3, randomUUID as randomUUID3 } from "node:crypto";
2182
2182
  import { spawn as spawn2 } from "node:child_process";
2183
2183
  import { realpathSync as realpathSync2 } from "node:fs";
2184
- import { basename as basename2, isAbsolute as isAbsolute4, join as join6 } from "node:path";
2184
+ import { basename as basename2, isAbsolute as isAbsolute4, join as join7 } from "node:path";
2185
2185
  import { isIP } from "node:net";
2186
2186
 
2187
2187
  // ../types/dist/daemon-control.js
@@ -3249,6 +3249,10 @@ function defaultWindowsAclAdapter(deadlineMs = WINDOWS_ACL_DEADLINE_MS) {
3249
3249
  }
3250
3250
  };
3251
3251
  }
3252
+ async function secureWindowsOwnerOnlyFileAcl(path, adapter = defaultWindowsAclAdapter()) {
3253
+ await ensureWindowsAcl(path, adapter, "file");
3254
+ await assertWindowsOwnerOnlyFileAcl(path, adapter);
3255
+ }
3252
3256
  async function assertWindowsOwnerOnlyFileAcl(path, adapter = defaultWindowsAclAdapter()) {
3253
3257
  const snapshot = await adapter.read(path);
3254
3258
  if (!canonicalSid(snapshot.ownerSid) || snapshot.ownerSid !== snapshot.currentSid)
@@ -3388,16 +3392,117 @@ async function createLaunchingDaemonMetadataFile(path, metadata, options) {
3388
3392
  }
3389
3393
  }
3390
3394
 
3395
+ // src/config/plugin-secrets-key.ts
3396
+ import { randomBytes } from "node:crypto";
3397
+ import { constants } from "node:fs";
3398
+ import { lstat as lstat2, mkdir as mkdir2, open as open2, unlink as unlink2 } from "node:fs/promises";
3399
+ import { dirname, join as join2 } from "node:path";
3400
+ var UNSAFE_FILE = "Plugin secrets key must be an owner-only regular file containing a canonical Base64 key for 32 bytes";
3401
+ function pluginSecretsKeyPath(configDbPath) {
3402
+ return join2(dirname(configDbPath), "plugin-secrets.key");
3403
+ }
3404
+ function withPluginSecretsKey(environment, key) {
3405
+ return {
3406
+ ...Object.fromEntries(Object.entries(environment).filter(([name]) => name.toLowerCase() !== "bungee_plugin_secrets_key")),
3407
+ BUNGEE_PLUGIN_SECRETS_KEY: key
3408
+ };
3409
+ }
3410
+ function validateKey(value) {
3411
+ const bytes = Buffer.from(value, "base64");
3412
+ if (bytes.length !== 32 || bytes.toString("base64") !== value) {
3413
+ throw new Error("Plugin secrets key must be canonical Base64 for 32 bytes");
3414
+ }
3415
+ return value;
3416
+ }
3417
+ async function readKeyFile(path, options) {
3418
+ const platform = options.platform ?? process.platform;
3419
+ const before = await lstat2(path);
3420
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1)
3421
+ throw new Error(UNSAFE_FILE);
3422
+ const file = await open2(path, constants.O_RDONLY | (platform === "win32" ? 0 : constants.O_NOFOLLOW | constants.O_NONBLOCK));
3423
+ try {
3424
+ const opened = await file.stat();
3425
+ const verify = (item) => {
3426
+ if (!item.isFile() || item.nlink !== 1 || item.dev !== before.dev || item.ino !== before.ino || item.size !== 44 || platform !== "win32" && ((item.mode & 511) !== 384 || process.geteuid && item.uid !== process.geteuid()))
3427
+ throw new Error(UNSAFE_FILE);
3428
+ };
3429
+ verify(opened);
3430
+ if (platform === "win32")
3431
+ await assertWindowsOwnerOnlyFileAcl(path, options.windowsAcl);
3432
+ const value = await file.readFile("utf8");
3433
+ verify(await file.stat());
3434
+ const after = await lstat2(path);
3435
+ verify(after);
3436
+ return validateKey(value);
3437
+ } finally {
3438
+ await file.close();
3439
+ }
3440
+ }
3441
+ async function readPluginSecretsKey(configDbPath, options = {}) {
3442
+ try {
3443
+ return await readKeyFile(pluginSecretsKeyPath(configDbPath), options);
3444
+ } catch (error) {
3445
+ if (error.code === "ENOENT")
3446
+ throw new Error("Plugin secrets key is missing. Run bungee init first.");
3447
+ throw error;
3448
+ }
3449
+ }
3450
+ async function initializePluginSecretsKey(configDbPath, suppliedKey, options = {}) {
3451
+ if (suppliedKey !== undefined)
3452
+ validateKey(suppliedKey);
3453
+ const path = pluginSecretsKeyPath(configDbPath);
3454
+ try {
3455
+ const existing = await readKeyFile(path, options);
3456
+ if (suppliedKey !== undefined && suppliedKey !== existing)
3457
+ throw new Error("Plugin secrets key is already initialized with a different key");
3458
+ return existing;
3459
+ } catch (error) {
3460
+ if (error.code !== "ENOENT")
3461
+ throw error;
3462
+ }
3463
+ if (suppliedKey === undefined) {
3464
+ for (const database of [configDbPath, join2(dirname(configDbPath), "plugin-state.db")]) {
3465
+ try {
3466
+ await lstat2(database);
3467
+ throw new Error("An existing database requires its original plugin secrets key when running bungee init");
3468
+ } catch (error) {
3469
+ if (error.code !== "ENOENT")
3470
+ throw error;
3471
+ }
3472
+ }
3473
+ }
3474
+ const key = suppliedKey ?? randomBytes(32).toString("base64");
3475
+ await mkdir2(dirname(path), { recursive: true, mode: 448 });
3476
+ const file = await open2(path, "wx", 384);
3477
+ const created = await file.stat();
3478
+ try {
3479
+ if ((options.platform ?? process.platform) === "win32")
3480
+ await secureWindowsOwnerOnlyFileAcl(path, options.windowsAcl);
3481
+ else
3482
+ await file.chmod(384);
3483
+ await file.writeFile(key);
3484
+ await file.sync();
3485
+ } catch (error) {
3486
+ const current = await lstat2(path);
3487
+ if (current.dev === created.dev && current.ino === created.ino)
3488
+ await unlink2(path);
3489
+ throw error;
3490
+ } finally {
3491
+ await file.close();
3492
+ }
3493
+ return readKeyFile(path, options);
3494
+ }
3495
+
3391
3496
  // src/binary/manager.ts
3392
3497
  import { randomUUID as randomUUID2 } from "node:crypto";
3393
3498
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, rmSync as rmSync2, writeFileSync } from "node:fs";
3394
3499
  import { homedir } from "node:os";
3395
- import { join as join3 } from "node:path";
3500
+ import { join as join4 } from "node:path";
3396
3501
  import { createInterface } from "node:readline";
3397
3502
  // package.json
3398
3503
  var package_default = {
3399
3504
  name: "@jeffusion/bungee",
3400
- version: "5.24.3",
3505
+ version: "5.25.0",
3401
3506
  description: "High-performance reverse proxy server CLI",
3402
3507
  type: "module",
3403
3508
  bin: {
@@ -3453,7 +3558,7 @@ import {
3453
3558
  renameSync,
3454
3559
  rmSync
3455
3560
  } from "node:fs";
3456
- import { basename, join as join2 } from "node:path";
3561
+ import { basename, join as join3 } from "node:path";
3457
3562
  function runTar(args) {
3458
3563
  const result = spawnSync("tar", args, { encoding: "utf8" });
3459
3564
  if (result.status !== 0) {
@@ -3483,8 +3588,8 @@ function validateListing(listing, binaryName) {
3483
3588
  }
3484
3589
  }
3485
3590
  function validateExtractedTree(directory, binaryName) {
3486
- const executable = join2(directory, binaryName);
3487
- const plugins = join2(directory, "plugins");
3591
+ const executable = join3(directory, binaryName);
3592
+ const plugins = join3(directory, "plugins");
3488
3593
  if (!lstatSync(executable).isFile() || !lstatSync(plugins).isDirectory()) {
3489
3594
  throw new Error("Binary archive has an invalid root layout");
3490
3595
  }
@@ -3495,7 +3600,7 @@ function validateExtractedTree(directory, binaryName) {
3495
3600
  }
3496
3601
  const visit = (current) => {
3497
3602
  for (const entry of readdirSync(current, { withFileTypes: true })) {
3498
- const target = join2(current, entry.name);
3603
+ const target = join3(current, entry.name);
3499
3604
  if (entry.isSymbolicLink())
3500
3605
  throw new Error(`Binary archive contains a symlink: ${entry.name}`);
3501
3606
  if (entry.isDirectory())
@@ -3506,8 +3611,8 @@ function validateExtractedTree(directory, binaryName) {
3506
3611
  for (const plugin of readdirSync(plugins, { withFileTypes: true })) {
3507
3612
  if (!plugin.isDirectory())
3508
3613
  throw new Error(`Invalid plugin artifact: ${plugin.name}`);
3509
- const pluginDirectory = join2(plugins, plugin.name);
3510
- if (!existsSync2(join2(pluginDirectory, "manifest.json")) || !existsSync2(join2(pluginDirectory, "index.js"))) {
3614
+ const pluginDirectory = join3(plugins, plugin.name);
3615
+ if (!existsSync2(join3(pluginDirectory, "manifest.json")) || !existsSync2(join3(pluginDirectory, "index.js"))) {
3511
3616
  throw new Error(`Incomplete plugin artifact: ${plugin.name}`);
3512
3617
  }
3513
3618
  }
@@ -3517,15 +3622,15 @@ function installBinaryArchive(options) {
3517
3622
  throw new Error("Invalid binary install identity");
3518
3623
  }
3519
3624
  mkdirSync(options.installRoot, { recursive: true });
3520
- const target = join2(options.installRoot, options.version);
3521
- const staging = mkdtempSync(join2(options.installRoot, `${options.version}.staging-`));
3625
+ const target = join3(options.installRoot, options.version);
3626
+ const staging = mkdtempSync(join3(options.installRoot, `${options.version}.staging-`));
3522
3627
  const backup = `${target}.backup-${randomUUID()}`;
3523
3628
  let backedUp = false;
3524
3629
  try {
3525
3630
  validateListing(runTar(["-tzf", options.archivePath]), options.binaryName);
3526
3631
  runTar(["-xzf", options.archivePath, "-C", staging]);
3527
3632
  validateExtractedTree(staging, options.binaryName);
3528
- chmodSync(join2(staging, options.binaryName), 493);
3633
+ chmodSync(join3(staging, options.binaryName), 493);
3529
3634
  if (existsSync2(target)) {
3530
3635
  renameSync(target, backup);
3531
3636
  backedUp = true;
@@ -3539,7 +3644,7 @@ function installBinaryArchive(options) {
3539
3644
  }
3540
3645
  if (backedUp)
3541
3646
  rmSync(backup, { recursive: true, force: true });
3542
- return join2(target, options.binaryName);
3647
+ return join3(target, options.binaryName);
3543
3648
  } finally {
3544
3649
  rmSync(staging, { recursive: true, force: true });
3545
3650
  }
@@ -3573,8 +3678,8 @@ function getAssetName(binaryName = getBinaryName()) {
3573
3678
 
3574
3679
  // src/binary/manager.ts
3575
3680
  class BinaryManager {
3576
- static BINARY_DIR = join3(homedir(), ".bungee", "bin");
3577
- static VERSION_FILE = join3(homedir(), ".bungee", "version.txt");
3681
+ static BINARY_DIR = join4(homedir(), ".bungee", "bin");
3682
+ static VERSION_FILE = join4(homedir(), ".bungee", "version.txt");
3578
3683
  static GITHUB_REPO = "jeffusion/bungee";
3579
3684
  static getBinaryName() {
3580
3685
  return getBinaryName();
@@ -3583,7 +3688,7 @@ class BinaryManager {
3583
3688
  return getAssetName(this.getBinaryName());
3584
3689
  }
3585
3690
  static getLocalBinaryPath() {
3586
- return join3(this.BINARY_DIR, package_default.version, this.getBinaryName());
3691
+ return join4(this.BINARY_DIR, package_default.version, this.getBinaryName());
3587
3692
  }
3588
3693
  static isBinaryInstalled() {
3589
3694
  return existsSync3(this.getLocalBinaryPath());
@@ -3596,7 +3701,7 @@ class BinaryManager {
3596
3701
  }
3597
3702
  }
3598
3703
  static saveVersion(version) {
3599
- mkdirSync2(join3(homedir(), ".bungee"), { recursive: true });
3704
+ mkdirSync2(join4(homedir(), ".bungee"), { recursive: true });
3600
3705
  writeFileSync(this.VERSION_FILE, version);
3601
3706
  }
3602
3707
  static checkVersion() {
@@ -3617,7 +3722,7 @@ class BinaryManager {
3617
3722
  static async downloadBinary(options = {}) {
3618
3723
  const url = options.assetUrl ?? this.getDownloadUrl();
3619
3724
  mkdirSync2(this.BINARY_DIR, { recursive: true });
3620
- const archivePath = join3(this.BINARY_DIR, `.download-${randomUUID2()}.tar.gz`);
3725
+ const archivePath = join4(this.BINARY_DIR, `.download-${randomUUID2()}.tar.gz`);
3621
3726
  if (!options.silent) {
3622
3727
  console.log(`Downloading Bungee v${package_default.version} from ${url}`);
3623
3728
  }
@@ -3679,7 +3784,7 @@ class BinaryManager {
3679
3784
  }
3680
3785
 
3681
3786
  // src/daemon/runtime.ts
3682
- import { isAbsolute as isAbsolute2, join as join4, resolve as resolve2 } from "node:path";
3787
+ import { isAbsolute as isAbsolute2, join as join5, resolve as resolve2 } from "node:path";
3683
3788
  function createDaemonRuntime(options) {
3684
3789
  if (!isAbsolute2(options.dataDirectory))
3685
3790
  throw new Error("Daemon data directory must be absolute");
@@ -3696,8 +3801,8 @@ function createDaemonRuntime(options) {
3696
3801
  cwd: options.dataDirectory,
3697
3802
  env: {
3698
3803
  ...inheritedEnvironment,
3699
- BUNGEE_CONFIG_DB_PATH: options.inheritedEnvironment?.BUNGEE_CONFIG_DB_PATH ? resolve2(options.inheritedEnvironment.BUNGEE_CONFIG_DB_PATH) : join4(options.dataDirectory, "bungee.db"),
3700
- BUNGEE_ACCESS_DB_PATH: join4(options.logsDirectory, "access.db"),
3804
+ BUNGEE_CONFIG_DB_PATH: options.inheritedEnvironment?.BUNGEE_CONFIG_DB_PATH ? resolve2(options.inheritedEnvironment.BUNGEE_CONFIG_DB_PATH) : join5(options.dataDirectory, "bungee.db"),
3805
+ BUNGEE_ACCESS_DB_PATH: join5(options.logsDirectory, "access.db"),
3701
3806
  WORKER_COUNT: options.workers ?? "2",
3702
3807
  DAEMON_MODE: "true",
3703
3808
  ...options.port === undefined ? {} : { PORT: options.port }
@@ -3707,9 +3812,9 @@ function createDaemonRuntime(options) {
3707
3812
 
3708
3813
  // src/daemon/pid-mirror.ts
3709
3814
  import { constants as fsConstants2 } from "node:fs";
3710
- import { lstat as lstat2, open as open2, rename as rename2, unlink as unlink2 } from "node:fs/promises";
3711
- import { dirname, isAbsolute as isAbsolute3, parse as parse2, resolve as resolve3 } from "node:path";
3712
- import { randomBytes } from "node:crypto";
3815
+ import { lstat as lstat3, open as open3, rename as rename2, unlink as unlink3 } from "node:fs/promises";
3816
+ import { dirname as dirname2, isAbsolute as isAbsolute3, parse as parse2, resolve as resolve3 } from "node:path";
3817
+ import { randomBytes as randomBytes2 } from "node:crypto";
3713
3818
  var MAX_PID_BYTES = 64;
3714
3819
  var UNSUPPORTED_DIR_FSYNC = new Set(["EINVAL", "ENOTSUP", "EOPNOTSUPP", "ENOSYS", "EPERM", "EISDIR"]);
3715
3820
  function identity2(stat) {
@@ -3728,7 +3833,7 @@ async function rejectSymlinkComponents2(path) {
3728
3833
  for (const part of absolute.slice(root.length).split(/[\\/]+/).filter(Boolean)) {
3729
3834
  current = resolve3(current, part);
3730
3835
  try {
3731
- if ((await lstat2(current)).isSymbolicLink())
3836
+ if ((await lstat3(current)).isSymbolicLink())
3732
3837
  throw new Error("PID mirror path contains a symlink");
3733
3838
  } catch (error) {
3734
3839
  if (error.code === "ENOENT")
@@ -3742,7 +3847,7 @@ async function syncDirectory(directory, platform) {
3742
3847
  return;
3743
3848
  let handle;
3744
3849
  try {
3745
- handle = await open2(directory, fsConstants2.O_RDONLY | (fsConstants2.O_DIRECTORY ?? 0));
3850
+ handle = await open3(directory, fsConstants2.O_RDONLY | (fsConstants2.O_DIRECTORY ?? 0));
3746
3851
  await handle.sync();
3747
3852
  } catch (error) {
3748
3853
  if (!UNSUPPORTED_DIR_FSYNC.has(error.code ?? ""))
@@ -3761,7 +3866,7 @@ async function syncDirectory(directory, platform) {
3761
3866
  async function readLegacyPidFile(path) {
3762
3867
  let listed;
3763
3868
  try {
3764
- listed = await lstat2(path);
3869
+ listed = await lstat3(path);
3765
3870
  } catch (error) {
3766
3871
  const code = error.code;
3767
3872
  return code === "ENOENT" ? { kind: "absent" } : { kind: "unknown" };
@@ -3772,7 +3877,7 @@ async function readLegacyPidFile(path) {
3772
3877
  const noFollow = process.platform === "win32" ? 0 : fsConstants2.O_NOFOLLOW;
3773
3878
  let handle;
3774
3879
  try {
3775
- handle = await open2(path, fsConstants2.O_RDONLY | noFollow);
3880
+ handle = await open3(path, fsConstants2.O_RDONLY | noFollow);
3776
3881
  const opened = await handle.stat();
3777
3882
  if (!regular(opened) || !sameIdentity2(expected, identity2(opened)))
3778
3883
  return { kind: "unsafe" };
@@ -3797,10 +3902,10 @@ async function readLegacyPidFile(path) {
3797
3902
  }
3798
3903
  async function deleteLegacyPidFile(path, expected) {
3799
3904
  try {
3800
- const current = await lstat2(path);
3905
+ const current = await lstat3(path);
3801
3906
  if (!regular(current) || current.isSymbolicLink() || !sameIdentity2(identity2(current), expected))
3802
3907
  return false;
3803
- await unlink2(path);
3908
+ await unlink3(path);
3804
3909
  return true;
3805
3910
  } catch {
3806
3911
  return false;
@@ -3810,10 +3915,10 @@ async function writeLegacyPidMirror(path, pid, options = {}) {
3810
3915
  if (!Number.isSafeInteger(pid) || pid <= 0 || !isAbsolute3(path) || path.includes("\x00"))
3811
3916
  throw new Error("invalid PID mirror target");
3812
3917
  const target = resolve3(path);
3813
- const directory = dirname(target);
3918
+ const directory = dirname2(target);
3814
3919
  await rejectSymlinkComponents2(directory);
3815
- const temporary = `${target}.${process.pid}.${randomBytes(16).toString("hex")}.tmp`;
3816
- const handle = await open2(temporary, fsConstants2.O_WRONLY | fsConstants2.O_CREAT | fsConstants2.O_EXCL, 384);
3920
+ const temporary = `${target}.${process.pid}.${randomBytes2(16).toString("hex")}.tmp`;
3921
+ const handle = await open3(temporary, fsConstants2.O_WRONLY | fsConstants2.O_CREAT | fsConstants2.O_EXCL, 384);
3817
3922
  try {
3818
3923
  await handle.writeFile(String(pid));
3819
3924
  await handle.chmod(384);
@@ -3829,7 +3934,7 @@ async function writeLegacyPidMirror(path, pid, options = {}) {
3829
3934
  await handle.close();
3830
3935
  } catch {}
3831
3936
  try {
3832
- await unlink2(temporary);
3937
+ await unlink3(temporary);
3833
3938
  } catch {}
3834
3939
  throw error;
3835
3940
  }
@@ -3837,7 +3942,7 @@ async function writeLegacyPidMirror(path, pid, options = {}) {
3837
3942
 
3838
3943
  // src/daemon/process-identity.ts
3839
3944
  import { existsSync as existsSync4 } from "node:fs";
3840
- import { lstat as lstat3, realpath as realpath2 } from "node:fs/promises";
3945
+ import { lstat as lstat4, realpath as realpath2 } from "node:fs/promises";
3841
3946
  import { readdir, readFile } from "node:fs/promises";
3842
3947
  import { execFile as nodeExecFile } from "node:child_process";
3843
3948
  import { promisify } from "node:util";
@@ -4108,7 +4213,7 @@ async function probeProcessAlive(pid, options = {}) {
4108
4213
  return "dead";
4109
4214
  if ((options.platform ?? process.platform) === "linux") {
4110
4215
  try {
4111
- await lstat3(`/proc/${pid}`);
4216
+ await lstat4(`/proc/${pid}`);
4112
4217
  return "alive";
4113
4218
  } catch (error) {
4114
4219
  return error.code === "ENOENT" ? "dead" : "unknown";
@@ -4229,7 +4334,7 @@ async function findExactDaemonProcessDetailed(bootNonce, expectedOrOptions, opti
4229
4334
  // src/daemon/force-stop.ts
4230
4335
  import { execFile as nodeExecFile3 } from "node:child_process";
4231
4336
  import { realpathSync } from "node:fs";
4232
- import { join as join5, win32 as win323 } from "node:path";
4337
+ import { join as join6, win32 as win323 } from "node:path";
4233
4338
  import { promisify as promisify3 } from "node:util";
4234
4339
 
4235
4340
  // src/daemon/process-tree.ts
@@ -4540,7 +4645,7 @@ function canonicalTaskkillPath() {
4540
4645
  throw new Error("Windows SystemRoot is unavailable");
4541
4646
  }
4542
4647
  })();
4543
- const expected = join5(canonicalRoot, "System32", "taskkill.exe");
4648
+ const expected = join6(canonicalRoot, "System32", "taskkill.exe");
4544
4649
  let actual;
4545
4650
  try {
4546
4651
  actual = realpathSync.native(expected);
@@ -5031,6 +5136,7 @@ class DaemonManager {
5031
5136
  findProcessDetailed;
5032
5137
  probePid;
5033
5138
  injectedLaunch;
5139
+ readSecretsKey;
5034
5140
  writePidMirror;
5035
5141
  httpRequest;
5036
5142
  forceStop;
@@ -5052,7 +5158,8 @@ class DaemonManager {
5052
5158
  this.dataDirectory = dependencies.dataDirectory ?? ConfigPaths.DATA_DIR;
5053
5159
  this.logsDirectory = dependencies.logsDirectory ?? ConfigPaths.LOGS_DIR;
5054
5160
  this.inheritedEnvironment = dependencies.inheritedEnvironment ?? process.env;
5055
- this.metadataFile = join6(this.runtimeDirectory, "daemon.json");
5161
+ this.readSecretsKey = dependencies.readPluginSecretsKey ?? readPluginSecretsKey;
5162
+ this.metadataFile = join7(this.runtimeDirectory, "daemon.json");
5056
5163
  this.now = dependencies.now ?? (() => performance.now());
5057
5164
  this.sleep = dependencies.sleep ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
5058
5165
  this.currentPid = dependencies.currentPid ?? (() => process.pid);
@@ -5315,9 +5422,17 @@ class DaemonManager {
5315
5422
  }
5316
5423
  async start(options = {}) {
5317
5424
  await this.inspectExisting();
5425
+ const runtime = createDaemonRuntime({
5426
+ dataDirectory: this.dataDirectory,
5427
+ logsDirectory: this.logsDirectory,
5428
+ workers: options.workers,
5429
+ port: options.port,
5430
+ inheritedEnvironment: this.inheritedEnvironment
5431
+ });
5432
+ const key = await this.readSecretsKey(runtime.env.BUNGEE_CONFIG_DB_PATH);
5318
5433
  const descriptor = await this.launchDescriptor(options);
5319
5434
  const bootNonce = randomUUID3().toLowerCase();
5320
- const shutdownSecret = encodeDaemonShutdownSecret(randomBytes2(32));
5435
+ const shutdownSecret = encodeDaemonShutdownSecret(randomBytes3(32));
5321
5436
  const launching = {
5322
5437
  schema: "bungee-daemon-metadata-v1",
5323
5438
  state: "launching",
@@ -5340,15 +5455,8 @@ class DaemonManager {
5340
5455
  }
5341
5456
  throw error;
5342
5457
  }
5343
- const runtime = createDaemonRuntime({
5344
- dataDirectory: this.dataDirectory,
5345
- logsDirectory: this.logsDirectory,
5346
- workers: options.workers,
5347
- port: options.port,
5348
- inheritedEnvironment: this.inheritedEnvironment
5349
- });
5350
5458
  const env = {
5351
- ...runtime.env,
5459
+ ...withPluginSecretsKey(runtime.env, key),
5352
5460
  BUNGEE_ROLE: "master",
5353
5461
  BUNGEE_DAEMON_METADATA_PATH: this.metadataFile,
5354
5462
  BUNGEE_DAEMON_BOOT_NONCE: bootNonce,
@@ -5770,15 +5878,15 @@ class DaemonManager {
5770
5878
  }
5771
5879
 
5772
5880
  // src/commands/recovery-file.ts
5773
- import { constants } from "node:fs";
5774
- import { lstat as lstat4, open as open3 } from "node:fs/promises";
5881
+ import { constants as constants2 } from "node:fs";
5882
+ import { lstat as lstat5, open as open4 } from "node:fs/promises";
5775
5883
  var UNSAFE_INPUT = "Recovery input must be an owner-only regular file of at most 8192 bytes";
5776
5884
  async function openRecoveryInputFile(path, options = {}) {
5777
5885
  const platform = options.platform ?? process.platform;
5778
- const before = await lstat4(path);
5886
+ const before = await lstat5(path);
5779
5887
  if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1)
5780
5888
  throw new Error(UNSAFE_INPUT);
5781
- const file = await open3(path, constants.O_RDONLY | (platform === "win32" ? 0 : constants.O_NOFOLLOW | constants.O_NONBLOCK));
5889
+ const file = await open4(path, constants2.O_RDONLY | (platform === "win32" ? 0 : constants2.O_NOFOLLOW | constants2.O_NONBLOCK));
5782
5890
  try {
5783
5891
  const opened = await file.stat();
5784
5892
  if (!opened.isFile() || opened.nlink !== 1 || opened.dev !== before.dev || opened.ino !== before.ino || opened.size > 8192)
@@ -5787,7 +5895,7 @@ async function openRecoveryInputFile(path, options = {}) {
5787
5895
  await assertWindowsOwnerOnlyFileAcl(path, options.windowsAcl);
5788
5896
  else if ((opened.mode & 511) !== 384 || process.getuid && opened.uid !== process.getuid())
5789
5897
  throw new Error(UNSAFE_INPUT);
5790
- const after = await lstat4(path);
5898
+ const after = await lstat5(path);
5791
5899
  const final = await file.stat();
5792
5900
  if (!after.isFile() || after.isSymbolicLink() || after.nlink !== 1 || after.dev !== opened.dev || after.ino !== opened.ino || final.nlink !== 1 || final.size > 8192 || final.size !== opened.size || platform !== "win32" && ((final.mode & 511) !== 384 || process.getuid && final.uid !== process.getuid()))
5793
5901
  throw new Error(UNSAFE_INPUT);
@@ -5800,15 +5908,16 @@ async function openRecoveryInputFile(path, options = {}) {
5800
5908
 
5801
5909
  // src/commands/recover.ts
5802
5910
  async function recoverCommand(options = {}) {
5803
- const launch = await resolveCoreLaunchDescriptor(options);
5804
- const db = resolve4(process.env.BUNGEE_CONFIG_DB_PATH ?? join7(ConfigPaths.DATA_DIR, "bungee.db"));
5911
+ const db = resolve4(process.env.BUNGEE_CONFIG_DB_PATH ?? join8(ConfigPaths.DATA_DIR, "bungee.db"));
5805
5912
  let file;
5806
5913
  try {
5807
5914
  if (options.file) {
5808
5915
  file = await openRecoveryInputFile(options.file);
5809
5916
  }
5917
+ const key = await readPluginSecretsKey(db);
5918
+ const launch = await resolveCoreLaunchDescriptor(options);
5810
5919
  await new Promise((accept, reject) => {
5811
- const child = spawn3(launch.executable, [...launch.entrypoint === null ? [] : [launch.entrypoint], "--recover", db], { cwd: ConfigPaths.DATA_DIR, stdio: [file?.fd ?? "inherit", "inherit", "inherit"], env: { ...process.env, BUNGEE_ROLE: "master", BUNGEE_ACCESS_DB_PATH: process.env.BUNGEE_ACCESS_DB_PATH ?? join7(ConfigPaths.LOGS_DIR, "access.db") } });
5920
+ const child = spawn3(launch.executable, [...launch.entrypoint === null ? [] : [launch.entrypoint], "--recover", db], { cwd: dirname3(db), stdio: [file?.fd ?? "inherit", "inherit", "inherit"], env: { ...withPluginSecretsKey(process.env, key), BUNGEE_ROLE: "master", BUNGEE_ACCESS_DB_PATH: process.env.BUNGEE_ACCESS_DB_PATH ?? join8(ConfigPaths.LOGS_DIR, "access.db") } });
5812
5921
  child.once("error", reject);
5813
5922
  child.once("exit", (code) => code === 0 ? accept() : reject(new Error("Offline recovery failed")));
5814
5923
  });
@@ -5818,22 +5927,25 @@ async function recoverCommand(options = {}) {
5818
5927
  }
5819
5928
 
5820
5929
  // src/commands/init.ts
5821
- import { resolve as resolve5, join as join8, dirname as dirname2 } from "node:path";
5930
+ import { resolve as resolve5, join as join9, dirname as dirname4 } from "node:path";
5822
5931
  import { spawn as spawn4 } from "node:child_process";
5932
+ import { mkdir as mkdir3 } from "node:fs/promises";
5823
5933
  async function initCommand(options = {}) {
5824
- ConfigPaths.ensureDataDir();
5825
- ConfigPaths.ensureLogsDir();
5826
- const configDbPath = resolve5(process.env.BUNGEE_CONFIG_DB_PATH ?? join8(ConfigPaths.DATA_DIR, "bungee.db"));
5934
+ const configDbPath = resolve5(process.env.BUNGEE_CONFIG_DB_PATH ?? join9(ConfigPaths.DATA_DIR, "bungee.db"));
5935
+ const accessDbPath = resolve5(process.env.BUNGEE_ACCESS_DB_PATH ?? join9(ConfigPaths.LOGS_DIR, "access.db"));
5936
+ const key = await initializePluginSecretsKey(configDbPath, options.pluginSecretsKey ?? process.env.BUNGEE_PLUGIN_SECRETS_KEY);
5937
+ await mkdir3(dirname4(accessDbPath), { recursive: true });
5827
5938
  const launch = await resolveCoreLaunchDescriptor(options);
5828
5939
  await new Promise((accept, reject) => {
5829
- const child = spawn4(launch.executable, [...launch.entrypoint === null ? [] : [launch.entrypoint], "--initialize-config", configDbPath], { cwd: ConfigPaths.DATA_DIR, stdio: "inherit", env: { ...process.env, BUNGEE_ROLE: "master", BUNGEE_ACCESS_DB_PATH: process.env.BUNGEE_ACCESS_DB_PATH ?? join8(ConfigPaths.LOGS_DIR, "access.db") } });
5940
+ const child = spawn4(launch.executable, [...launch.entrypoint === null ? [] : [launch.entrypoint], "--initialize-config", configDbPath], { cwd: dirname4(configDbPath), stdio: "inherit", env: { ...withPluginSecretsKey(process.env, key), BUNGEE_ROLE: "master", BUNGEE_ACCESS_DB_PATH: accessDbPath } });
5830
5941
  child.once("error", reject);
5831
5942
  child.once("exit", (code) => code === 0 ? accept() : reject(new Error(`Configuration initialization failed (${code})`)));
5832
5943
  });
5833
- console.log(`Bungee data directory initialized at: ${ConfigPaths.DATA_DIR}`);
5944
+ console.log(`Bungee data directory initialized at: ${dirname4(configDbPath)}`);
5834
5945
  console.log(`Configuration database: ${configDbPath}`);
5835
- console.log(`Plugin state database: ${join8(dirname2(configDbPath), "plugin-state.db")}`);
5836
- console.log(`Telemetry database: ${ConfigPaths.LOGS_DIR}/access.db`);
5946
+ console.log(`Plugin state database: ${join9(dirname4(configDbPath), "plugin-state.db")}`);
5947
+ console.log(`Plugin secrets key file: ${pluginSecretsKeyPath(configDbPath)}`);
5948
+ console.log(`Telemetry database: ${accessDbPath}`);
5837
5949
  console.log("Start Bungee with: bungee start");
5838
5950
  }
5839
5951
 
@@ -6154,7 +6266,7 @@ function createProgram() {
6154
6266
  process.exitCode = 1;
6155
6267
  }
6156
6268
  });
6157
- program.command("init").description("Initialize the Bungee SQLite data directory").action(initCommand);
6269
+ program.command("init").description("Initialize the Bungee SQLite data directory and persistent plugin secrets key").option("--plugin-secrets-key <base64>", "Initialize with a canonical Base64 key for 32 bytes (otherwise use BUNGEE_PLUGIN_SECRETS_KEY or generate one)").action(initCommand);
6158
6270
  program.command("recover").description("Recover a stopped instance using bounded JSON from stdin or a 0600 file").option("--file <path>", "Owner-only 0600 recovery JSON input file").addHelpText("after", `
6159
6271
  Stop the instance first. Supply one JSON object via stdin or bungee recover --file recovery.json (chmod 600).
6160
6272
  Administrator identity: {"kind":"identity","plugin":"local-accounts","payload":{"username":"admin","password":"<new password, 15-128 characters>","reason":"Lost credentials"}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffusion/bungee",
3
- "version": "5.24.3",
3
+ "version": "5.25.0",
4
4
  "description": "High-performance reverse proxy server CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,8 +13,8 @@
13
13
  "test": "bun test"
14
14
  },
15
15
  "dependencies": {
16
- "@jeffusion/bungee-core": "^5.24.3",
17
- "@jeffusion/bungee-types": "^5.24.3",
16
+ "@jeffusion/bungee-core": "^5.25.0",
17
+ "@jeffusion/bungee-types": "^5.25.0",
18
18
  "cli-progress": "^3.12.0",
19
19
  "commander": "^14.0.1"
20
20
  },