Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
3e4516c
feat: backends report their server software to the proxy
objz Oct 26, 2026
83b29a6
feat: live web editor for scripts (/cb editor)
objz Oct 26, 2026
cd31359
feat(core): save a config back to config.yml
objz Oct 26, 2026
5c175c1
feat(core): pairing codes, channel ids and sealing
objz Oct 26, 2026
e6f10a8
feat(core): read the certificate pin of a keystore
objz Oct 26, 2026
61313c4
feat(relay): pairing channel for /cbc pair
objz Oct 26, 2026
ae1048b
refactor(velocity): relay connection shared by editor and setup
objz Oct 26, 2026
8300ea3
feat(velocity): add servers to velocity.toml in place
objz Oct 26, 2026
9a2933a
feat(velocity): setup plan and applying it
objz Oct 26, 2026
400e529
feat(velocity): /cb setup and /cb setup confirm
objz Oct 26, 2026
263756c
feat(backends): /cbc pair writes the config from the setup
objz Oct 26, 2026
7761e6c
fix(velocity): setup state and fresh-install hint with an unset keyst…
objz Oct 26, 2026
53186e5
fix(velocity): pair servers with the proxy's TOFU certificate pin
objz Oct 26, 2026
bc25bc3
feat: open the setup on a fresh install, confirm before setting up again
objz Oct 26, 2026
3918ac5
feat(relay): tell a page why its session ended, let a reload take over
objz Oct 26, 2026
8f4796e
fix(relay): clearer reason for an ended link
objz Oct 26, 2026
189c078
feat: setup applies without a proxy restart, console output through t…
objz Oct 26, 2026
c242dc4
feat: a finished setup closes when its page comes back
objz Oct 26, 2026
b2867b0
fix(velocity): ignore everything but auth from connections that haven…
objz Oct 26, 2026
830234d
fix: encrypt the editor and setup messages so the relay can't read them
objz Oct 26, 2026
5da27b0
test(core): config profile normalization
objz Oct 26, 2026
133322e
test(core): ConfigManager load
objz Oct 26, 2026
c6600f8
test(core): TLS trust manager pinning
objz Oct 26, 2026
e3a589c
test(core): script loader end to end and list/string adapters
objz Oct 26, 2026
e5fbf56
test(core): response awaiter timeout and plugin message channel
objz Oct 26, 2026
9a6da7d
fix(velocity): setup codes and the secret only after the console conf…
objz Oct 26, 2026
e8e28b0
fix(backends): /cbc pair shows where the server would connect and wai…
objz Oct 26, 2026
3cc9897
test(velocity): auth handler handshake
objz Oct 26, 2026
751d948
test(velocity): command dispatcher target routing
objz Oct 26, 2026
26a41be
fix(velocity): safer restart, no paths or causes sent to the page, ed…
objz Oct 26, 2026
9e8b60b
test(velocity): user cache persistence and lookup order
objz Oct 26, 2026
1ce68a3
test(velocity): dump sanitizer redaction
objz Oct 26, 2026
a481cbc
test(velocity): plugin message queue cap, ttl and drain
objz Oct 26, 2026
44e4246
test(velocity): command entry cooldown, delay, schedule-online and pl…
objz Oct 26, 2026
60e602d
fix(velocity): a time argument too large is refused instead of wrappi…
objz Oct 26, 2026
fd79c9a
fix(core): migration keeps an existing key and the file's line endings
objz Oct 26, 2026
7ffdec5
fix: a server whose last script was removed is told so and drops the …
objz Oct 26, 2026
7ca6fde
test(backends): auth request outcomes
objz Oct 26, 2026
0978ab6
test(backends): invoked command argument mapping
objz Oct 26, 2026
279579f
test(backends): execute command and registration handlers
objz Oct 26, 2026
af4d753
fix(backends): a reconnect that ran out without a result no longer bl…
objz Oct 26, 2026
d93c532
fix(velocity): an unreadable tasks.json is kept aside instead of over…
objz Oct 26, 2026
ead07cc
Merge tests from the test-coverage pass
objz Oct 26, 2026
638e3c1
fix: bugs the new tests found (config defaults, list errors, example …
objz Oct 26, 2026
a4ef0df
chore: tidy comments
objz Oct 26, 2026
7fa52c9
fix: complete security and bug audit across proxy, dispatcher, editor…
objz Oct 26, 2026
6eefcc1
fix(live): make isolated Velocity config valid
objz Oct 26, 2026
9e9c270
test: add bounded sustained command-delivery checks
objz Oct 26, 2026
a7f7c1b
test(live): count reload executions rather than debug messages
objz Oct 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ dist/bin/
*.tmp
*.bak
*.swp

# wrangler dev
node_modules/
.wrangler/
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,11 +234,12 @@ api.onConnectionStateChanged(state -> {
- [x] player locator service
- [ ] more channel types and lifecycle hooks

- [ ] **web interface**
- [ ] ...

- [ ] **admin GUI**
- [ ] ...
- [ ] **web editor (`/cb editor`)**
- [x] live session over a Cloudflare relay (no open ports, no self-hosting)
- [x] signed messages, secret only in the editor link
- [x] create, edit and delete scripts with inline validation errors
- [ ] config editing (without secrets and transport settings)
- [ ] backend config editing

see the [open issues](https://github.com/objz/CommandBridge/issues) for bugs and feature requests.

Expand Down
3 changes: 3 additions & 0 deletions backends/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ tasks.jar {

dependencies {
testImplementation(libs.junit.jupiter)
testImplementation("org.spongepowered:configurate-yaml:4.2.0")
testImplementation("org.snakeyaml:snakeyaml-engine:2.10")
testImplementation("dev.jorel:commandapi-spigot-core:11.2.0")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import dev.objz.commandbridge.backends.platform.ScheduleHandle;
import dev.objz.commandbridge.backends.platform.cmd.ClientCommands;
import dev.objz.commandbridge.backends.platform.cmd.CommandExecutor;
import dev.objz.commandbridge.backends.platform.cmd.PairCommand;
import dev.objz.commandbridge.backends.setup.PairClient;
import dev.objz.commandbridge.config.ConfigManager;
import dev.objz.commandbridge.config.model.BackendsConfig;
import dev.objz.commandbridge.config.model.EndpointType;
Expand All @@ -38,6 +40,7 @@

public final class Adapter implements PlatformAdapter<JavaPlugin> {
private BackendClient client;
private RegistrationHandler registrationHandler;
private BackendsConfig cfg;
private Path dataDir;
private JavaPlugin plugin;
Expand All @@ -53,6 +56,13 @@ public void load(PlatformEnv env, JavaPlugin plugin) throws Exception {
this.cfg = cfgMgr.current();
if (!ok || cfg == null) {
configOk = false;
Log.info(PairClient.UNPAIRED_HINT);
return;
}
// the shipped config can't authenticate anywhere, so don't keep trying
if (PairClient.isUnpaired(cfg)) {
configOk = false;
Log.info(PairClient.UNPAIRED_HINT);
return;
}

Expand All @@ -66,11 +76,14 @@ public void load(PlatformEnv env, JavaPlugin plugin) throws Exception {

@Override
public void start(PlatformEnv env) throws Exception {
if (this.dataDir == null)
this.dataDir = PathsUtil.normalizeDataDir(env.dataDir());
// register /cbc pair even when the config is fresh or broken, since that's when it's needed
ClientCommands.register(() -> client,
PairCommand.subcommand(this, dataDir, Bukkit.getBukkitVersion().split("-")[0]));
if (!configOk) {
return;
}
if (this.dataDir == null)
this.dataDir = PathsUtil.normalizeDataDir(env.dataDir());
if (this.cfg == null) {
var cfgMgr = new ConfigManager<>(dataDir, BackendsConfig.class);
if (!cfgMgr.load()) {
Expand All @@ -89,6 +102,11 @@ public void start(PlatformEnv env) throws Exception {
Bukkit::isPrimaryThread,
task -> Bukkit.getScheduler().runTask(plugin, task));

startClient();
Bukkit.getPluginManager().registerEvents(new PlayerListener(), plugin);
}

private void startClient() {
this.client = cfg.endpointType() == EndpointType.REDIS
? new RedisClient(cfg, dataDir, this)
: new WsClient(cfg, dataDir, this);
Expand All @@ -100,9 +118,7 @@ public void start(PlatformEnv env) throws Exception {
client.scheduleReconnection();
}

ClientCommands.register(client);

RegistrationHandler registrationHandler = new RegistrationHandler(client);
registrationHandler = new RegistrationHandler(client);
client.inboundRouter().register(MessageType.REGISTER_COMMANDS, registrationHandler);
client.inboundRouter().register(MessageType.EXECUTE_COMMAND,
new ExecuteCommandHandler(commandExecutor));
Expand All @@ -115,7 +131,37 @@ public void start(PlatformEnv env) throws Exception {
Location.BACKEND));

client.onAuthenticated(this::sendPlayerList);
Bukkit.getPluginManager().registerEvents(new PlayerListener(), plugin);
}

@Override
public void applyNewConfig() {
Bukkit.getScheduler().runTask(plugin, () -> {
try {
if (registrationHandler != null)
registrationHandler.unregisterAll();
if (client != null)
client.close();
var cfgMgr = new ConfigManager<>(dataDir, BackendsConfig.class);
if (!cfgMgr.load()) {
Log.error("The paired config.yml is not valid");
return;
}
this.cfg = cfgMgr.current();
Log.setDebug(cfg.debug());
if (commandExecutor == null)
commandExecutor = new BukkitExecutor(plugin);
if (!configOk) {
configOk = true;
Log.installThreadMarshalling(
Bukkit::isPrimaryThread,
task -> Bukkit.getScheduler().runTask(plugin, task));
Bukkit.getPluginManager().registerEvents(new PlayerListener(), plugin);
}
startClient();
} catch (Exception e) {
Log.error(e, "Could not connect with the new config: {}", e.getMessage());
}
});
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import dev.objz.commandbridge.backends.platform.ScheduleHandle;
import dev.objz.commandbridge.backends.platform.cmd.ClientCommands;
import dev.objz.commandbridge.backends.platform.cmd.CommandExecutor;
import dev.objz.commandbridge.backends.platform.cmd.PairCommand;
import dev.objz.commandbridge.backends.setup.PairClient;
import dev.objz.commandbridge.config.ConfigManager;
import dev.objz.commandbridge.config.model.BackendsConfig;
import dev.objz.commandbridge.config.model.EndpointType;
Expand All @@ -39,6 +41,7 @@

public final class Adapter implements PlatformAdapter<JavaPlugin> {
private BackendClient client;
private RegistrationHandler registrationHandler;
private BackendsConfig cfg;
private Path dataDir;
private JavaPlugin plugin;
Expand All @@ -54,6 +57,13 @@ public void load(PlatformEnv env, JavaPlugin plugin) throws Exception {
this.cfg = cfgMgr.current();
if (!ok || cfg == null) {
configOk = false;
Log.info(PairClient.UNPAIRED_HINT);
return;
}
// the shipped config can't authenticate anywhere, so don't keep trying
if (PairClient.isUnpaired(cfg)) {
configOk = false;
Log.info(PairClient.UNPAIRED_HINT);
return;
}

Expand All @@ -67,11 +77,14 @@ public void load(PlatformEnv env, JavaPlugin plugin) throws Exception {

@Override
public void start(PlatformEnv env) throws Exception {
if (this.dataDir == null)
this.dataDir = PathsUtil.normalizeDataDir(env.dataDir());
// register /cbc pair even when the config is fresh or broken, since that's when it's needed
ClientCommands.register(() -> client,
PairCommand.subcommand(this, dataDir, Bukkit.getBukkitVersion().split("-")[0]));
if (!configOk) {
return;
}
if (this.dataDir == null)
this.dataDir = PathsUtil.normalizeDataDir(env.dataDir());
if (this.cfg == null) {
var cfgMgr = new ConfigManager<>(dataDir, BackendsConfig.class);
if (!cfgMgr.load()) {
Expand All @@ -86,6 +99,11 @@ public void start(PlatformEnv env) throws Exception {
this.commandExecutor = new FoliaExecutor(plugin);
}

startClient();
Bukkit.getPluginManager().registerEvents(new PlayerListener(), plugin);
}

private void startClient() {
this.client = cfg.endpointType() == EndpointType.REDIS
? new RedisClient(cfg, dataDir, this)
: new WsClient(cfg, dataDir, this);
Expand All @@ -97,9 +115,7 @@ public void start(PlatformEnv env) throws Exception {
client.scheduleReconnection();
}

ClientCommands.register(client);

RegistrationHandler registrationHandler = new RegistrationHandler(client);
registrationHandler = new RegistrationHandler(client);
client.inboundRouter().register(MessageType.REGISTER_COMMANDS, registrationHandler);
client.inboundRouter().register(MessageType.EXECUTE_COMMAND,
new ExecuteCommandHandler(commandExecutor));
Expand All @@ -112,7 +128,34 @@ public void start(PlatformEnv env) throws Exception {
Location.BACKEND));

client.onAuthenticated(this::sendPlayerList);
Bukkit.getPluginManager().registerEvents(new PlayerListener(), plugin);
}

@Override
public void applyNewConfig() {
Bukkit.getGlobalRegionScheduler().execute(plugin, () -> {
try {
if (registrationHandler != null)
registrationHandler.unregisterAll();
if (client != null)
client.close();
var cfgMgr = new ConfigManager<>(dataDir, BackendsConfig.class);
if (!cfgMgr.load()) {
Log.error("The paired config.yml is not valid");
return;
}
this.cfg = cfgMgr.current();
Log.setDebug(cfg.debug());
if (commandExecutor == null)
commandExecutor = new FoliaExecutor(plugin);
if (!configOk) {
configOk = true;
Bukkit.getPluginManager().registerEvents(new PlayerListener(), plugin);
}
startClient();
} catch (Exception e) {
Log.error(e, "Could not connect with the new config: {}", e.getMessage());
}
});
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import dev.objz.commandbridge.backends.platform.ScheduleHandle;
import dev.objz.commandbridge.backends.platform.cmd.ClientCommands;
import dev.objz.commandbridge.backends.platform.cmd.CommandExecutor;
import dev.objz.commandbridge.backends.platform.cmd.PairCommand;
import dev.objz.commandbridge.backends.setup.PairClient;
import dev.objz.commandbridge.config.ConfigManager;
import dev.objz.commandbridge.config.model.BackendsConfig;
import dev.objz.commandbridge.config.model.EndpointType;
Expand All @@ -38,6 +40,7 @@

public final class Adapter implements PlatformAdapter<JavaPlugin> {
private BackendClient client;
private RegistrationHandler registrationHandler;
private BackendsConfig cfg;
private Path dataDir;
private JavaPlugin plugin;
Expand All @@ -53,6 +56,13 @@ public void load(PlatformEnv env, JavaPlugin plugin) throws Exception {
this.cfg = cfgMgr.current();
if (!ok || cfg == null) {
configOk = false;
Log.info(PairClient.UNPAIRED_HINT);
return;
}
// the shipped config can't authenticate anywhere, so don't keep trying
if (PairClient.isUnpaired(cfg)) {
configOk = false;
Log.info(PairClient.UNPAIRED_HINT);
return;
}

Expand All @@ -66,11 +76,14 @@ public void load(PlatformEnv env, JavaPlugin plugin) throws Exception {

@Override
public void start(PlatformEnv env) throws Exception {
if (this.dataDir == null)
this.dataDir = PathsUtil.normalizeDataDir(env.dataDir());
// register /cbc pair even when the config is fresh or broken, since that's when it's needed
ClientCommands.register(() -> client,
PairCommand.subcommand(this, dataDir, Bukkit.getBukkitVersion().split("-")[0]));
if (!configOk) {
return;
}
if (this.dataDir == null)
this.dataDir = PathsUtil.normalizeDataDir(env.dataDir());
if (this.cfg == null) {
var cfgMgr = new ConfigManager<>(dataDir, BackendsConfig.class);
if (!cfgMgr.load()) {
Expand All @@ -89,6 +102,11 @@ public void start(PlatformEnv env) throws Exception {
Bukkit::isPrimaryThread,
task -> Bukkit.getScheduler().runTask(plugin, task));

startClient();
Bukkit.getPluginManager().registerEvents(new PlayerListener(), plugin);
}

private void startClient() {
this.client = cfg.endpointType() == EndpointType.REDIS
? new RedisClient(cfg, dataDir, this)
: new WsClient(cfg, dataDir, this);
Expand All @@ -100,9 +118,7 @@ public void start(PlatformEnv env) throws Exception {
client.scheduleReconnection();
}

ClientCommands.register(client);

RegistrationHandler registrationHandler = new RegistrationHandler(client);
registrationHandler = new RegistrationHandler(client);
client.inboundRouter().register(MessageType.REGISTER_COMMANDS, registrationHandler);
client.inboundRouter().register(MessageType.EXECUTE_COMMAND,
new ExecuteCommandHandler(commandExecutor));
Expand All @@ -115,7 +131,37 @@ public void start(PlatformEnv env) throws Exception {
Location.BACKEND));

client.onAuthenticated(this::sendPlayerList);
Bukkit.getPluginManager().registerEvents(new PlayerListener(), plugin);
}

@Override
public void applyNewConfig() {
Bukkit.getScheduler().runTask(plugin, () -> {
try {
if (registrationHandler != null)
registrationHandler.unregisterAll();
if (client != null)
client.close();
var cfgMgr = new ConfigManager<>(dataDir, BackendsConfig.class);
if (!cfgMgr.load()) {
Log.error("The paired config.yml is not valid");
return;
}
this.cfg = cfgMgr.current();
Log.setDebug(cfg.debug());
if (commandExecutor == null)
commandExecutor = new PaperExecutor(plugin);
if (!configOk) {
configOk = true;
Log.installThreadMarshalling(
Bukkit::isPrimaryThread,
task -> Bukkit.getScheduler().runTask(plugin, task));
Bukkit.getPluginManager().registerEvents(new PlayerListener(), plugin);
}
startClient();
} catch (Exception e) {
Log.error(e, "Could not connect with the new config: {}", e.getMessage());
}
});
}

@Override
Expand Down
Loading
Loading