From 8ab666c8998356834d074895e2bd8487a2630717 Mon Sep 17 00:00:00 2001 From: Innes Date: Fri, 16 Oct 2026 22:01:37 -0700 Subject: [PATCH 1/6] Add authenticated Mythic bot API event client --- .../mythic/BotApiBridge.java | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 src/main/java/com/innygr/icreateordernetworking/mythic/BotApiBridge.java diff --git a/src/main/java/com/innygr/icreateordernetworking/mythic/BotApiBridge.java b/src/main/java/com/innygr/icreateordernetworking/mythic/BotApiBridge.java new file mode 100644 index 0000000..c7f2aa3 --- /dev/null +++ b/src/main/java/com/innygr/icreateordernetworking/mythic/BotApiBridge.java @@ -0,0 +1,104 @@ +package com.innygr.icreateordernetworking.mythic; + +import com.google.gson.JsonObject; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Optional authenticated outbound event client for the companion Discord bot. + * The bot must expose POST /api/icreate/events and validate the bearer token. + * Credentials remain in the dedicated server's config and are never sent to clients. + */ +public final class BotApiBridge { + private static final Logger LOGGER = LoggerFactory.getLogger("iCreateOrderNetworking/BotApi"); + private static final HttpClient HTTP = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(4)) + .build(); + private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(task -> { + Thread thread = new Thread(task, "icreate-mythic-bot-api"); + thread.setDaemon(true); + return thread; + }); + + private static volatile URI endpoint; + private static volatile String token = ""; + private static volatile boolean enabled; + + private BotApiBridge() {} + + static void configure(String baseUrl, String sharedToken) { + enabled = false; + endpoint = null; + token = sharedToken == null ? "" : sharedToken.strip(); + if (baseUrl == null || baseUrl.isBlank() || token.isBlank()) return; + try { + URI base = URI.create(baseUrl.strip()); + if (!"https".equalsIgnoreCase(base.getScheme()) || base.getHost() == null + || base.getUserInfo() != null || base.getQuery() != null || base.getFragment() != null) { + LOGGER.warn("Mythic bot API disabled: apiBaseUrl must be an HTTPS base URL without credentials, query, or fragment."); + return; + } + String path = base.getPath() == null ? "" : base.getPath().replaceAll("/+$", ""); + endpoint = URI.create(base.getScheme() + "://" + base.getAuthority() + path + "/api/icreate/events"); + enabled = true; + } catch (IllegalArgumentException exception) { + LOGGER.warn("Mythic bot API disabled: invalid apiBaseUrl."); + } + } + + static void sendEvent(String eventType, String details) { + URI target = endpoint; + String secret = token; + if (!enabled || target == null || secret.isBlank()) return; + + JsonObject body = new JsonObject(); + body.addProperty("source", "icreate-order-networking"); + body.addProperty("event", safe(eventType, 80)); + body.addProperty("details", safe(details, 1800)); + body.addProperty("timestamp", java.time.Instant.now().toString()); + + final HttpRequest request; + try { + request = HttpRequest.newBuilder(target) + .timeout(Duration.ofSeconds(6)) + .header("Authorization", "Bearer " + secret) + .header("Content-Type", "application/json; charset=utf-8") + .POST(HttpRequest.BodyPublishers.ofString(body.toString(), StandardCharsets.UTF_8)) + .build(); + } catch (IllegalArgumentException exception) { + LOGGER.warn("Could not create Mythic bot API request."); + return; + } + + EXECUTOR.submit(() -> { + try { + HttpResponse response = HTTP.send(request, HttpResponse.BodyHandlers.discarding()); + if (response.statusCode() < 200 || response.statusCode() >= 300) { + LOGGER.warn("Mythic bot API returned HTTP {}; event was not accepted.", response.statusCode()); + } + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + } catch (Exception exception) { + LOGGER.warn("Unable to deliver event to Mythic bot API: {}", exception.getMessage()); + } + }); + } + + private static String safe(String value, int maxLength) { + if (value == null) return "unknown"; + String clean = value.replace("@everyone", "@\\u200beveryone") + .replace("@here", "@\\u200bhere") + .replace("\\r", " ").strip(); + return clean.length() > maxLength ? clean.substring(0, maxLength) : clean; + } +} From de8333239669b62e8d3df9d25b3ce38f532f32ae Mon Sep 17 00:00:00 2001 From: Innes Date: Fri, 16 Oct 2026 22:01:47 -0700 Subject: [PATCH 2/6] Route Mythic events to bot API as well as webhook --- .../mythic/DiscordBridge.java | 40 ++++++++----------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/innygr/icreateordernetworking/mythic/DiscordBridge.java b/src/main/java/com/innygr/icreateordernetworking/mythic/DiscordBridge.java index fcdef75..0323662 100644 --- a/src/main/java/com/innygr/icreateordernetworking/mythic/DiscordBridge.java +++ b/src/main/java/com/innygr/icreateordernetworking/mythic/DiscordBridge.java @@ -1,7 +1,6 @@ package com.innygr.icreateordernetworking.mythic; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import net.neoforged.fml.loading.FMLPaths; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,20 +17,12 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -/** - * Server-only Discord notifier for the optional Mythic build. - * - * A Discord webhook is used instead of a bot token: it is configured only on - * the dedicated server and is never sent to clients or embedded in the JAR. - */ +/** Server-only Discord webhook transport for the optional Mythic build. */ public final class DiscordBridge { private static final Logger LOGGER = LoggerFactory.getLogger("iCreateOrderNetworking/MythicDiscord"); - private static final Path CONFIG_PATH = FMLPaths.CONFIGDIR.get().resolve("icreate-order-networking-mythic.json"); - private static final HttpClient HTTP = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(5)) - .build(); - private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(runnable -> { - Thread thread = new Thread(runnable, "icreate-mythic-discord"); + private static final HttpClient HTTP = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build(); + private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(task -> { + Thread thread = new Thread(task, "icreate-mythic-discord"); thread.setDaemon(true); return thread; }); @@ -56,16 +47,21 @@ public static boolean isConfigured() { } public static void sendEvent(String eventType, String details) { - if (stopped || !isConfigured()) return; + if (stopped) return; String safeType = sanitize(eventType, 80); String safeDetails = sanitize(details, 1500); + + // The companion bot receives events independently of the optional webhook. + BotApiBridge.sendEvent(safeType, safeDetails); + if (!isConfigured()) return; + JsonObject payload = new JsonObject(); - payload.addProperty("content", "**[" + serverName + "] " + safeType + "**\n" + safeDetails); + payload.addProperty("content", "**[" + sanitize(serverName, 100) + "] " + safeType + "**\n" + safeDetails); JsonObject allowedMentions = new JsonObject(); allowedMentions.add("parse", new com.google.gson.JsonArray()); payload.add("allowed_mentions", allowedMentions); - HttpRequest request; + final HttpRequest request; try { request = HttpRequest.newBuilder(URI.create(webhookUrl)) .timeout(Duration.ofSeconds(8)) @@ -79,7 +75,7 @@ public static void sendEvent(String eventType, String details) { EXECUTOR.submit(() -> { try { - HttpResponse response = HTTP.send(request, HttpResponse.BodyHandlers.ofString()); + HttpResponse response = HTTP.send(request, HttpResponse.BodyHandlers.discarding()); if (response.statusCode() < 200 || response.statusCode() >= 300) { LOGGER.warn("Mythic Discord webhook returned HTTP {}; event was not delivered.", response.statusCode()); } @@ -91,9 +87,7 @@ public static void sendEvent(String eventType, String details) { }); } - static void shutdown() { - stopped = true; - } + static void shutdown() { stopped = true; } private static boolean isHttpsWebhook(String value) { if (value == null || value.isBlank()) return false; @@ -111,9 +105,9 @@ private static boolean isHttpsWebhook(String value) { private static String sanitize(String value, int maxLength) { if (value == null) return "n/a"; - String normalized = value.replace("@everyone", "@\u200beveryone") - .replace("@here", "@\u200bhere") - .replace("\r", " ") + String normalized = value.replace("@everyone", "@\\u200beveryone") + .replace("@here", "@\\u200bhere") + .replace("\\r", " ") .strip(); return normalized.length() > maxLength ? normalized.substring(0, maxLength) : normalized; } From b020b1650b8755c962ccacbf803ee897357193b2 Mon Sep 17 00:00:00 2001 From: Innes Date: Fri, 16 Oct 2026 22:01:59 -0700 Subject: [PATCH 3/6] Enable authenticated bot API configuration in Mythic integration --- .../mythic/MythicIntegration.java | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/innygr/icreateordernetworking/mythic/MythicIntegration.java b/src/main/java/com/innygr/icreateordernetworking/mythic/MythicIntegration.java index c2e4393..cbd7de0 100644 --- a/src/main/java/com/innygr/icreateordernetworking/mythic/MythicIntegration.java +++ b/src/main/java/com/innygr/icreateordernetworking/mythic/MythicIntegration.java @@ -12,10 +12,7 @@ import java.nio.file.Path; import java.util.UUID; -/** - * Optional server-only Mythic integration. This class is compiled into the - * Mythic variant only; the regular public build excludes this package. - */ +/** Optional server-only Mythic integration, compiled only in the Mythic build. */ public final class MythicIntegration { private static final Logger LOGGER = LoggerFactory.getLogger("iCreateOrderNetworking/Mythic"); private static final Path CONFIG_PATH = net.neoforged.fml.loading.FMLPaths.CONFIGDIR.get() @@ -24,6 +21,11 @@ public final class MythicIntegration { private boolean enabled; private boolean registered; + private String webhookUrl = ""; + private String serverName = "Minecraft server"; + private String apiBaseUrl = ""; + private String apiToken = ""; + private MythicIntegration() {} public static void onServerStarted(MinecraftServer server) { @@ -31,53 +33,48 @@ public static void onServerStarted(MinecraftServer server) { integration.loadConfig(); FtbTeamsSync.load(); active = integration; + BotApiBridge.configure(integration.apiBaseUrl, integration.apiToken); if (integration.enabled) { NeoForge.EVENT_BUS.register(integration); integration.registered = true; DiscordBridge.configure(true, integration.webhookUrl, integration.serverName); - DiscordBridge.sendEvent("Server online", "Mythic Discord integration connected."); + DiscordBridge.sendEvent("Server online", "Mythic integration connected."); LOGGER.info("Mythic server integration enabled."); } else { DiscordBridge.configure(false, "", integration.serverName); - LOGGER.info("Mythic server integration is disabled in config."); + LOGGER.info("Mythic server integration disabled; configure a valid webhook or bot API."); } } public static void onServerStopped(MinecraftServer server) { MythicIntegration integration = active; - if (integration != null && integration.registered) { - NeoForge.EVENT_BUS.unregister(integration); - } + if (integration != null && integration.registered) NeoForge.EVENT_BUS.unregister(integration); DiscordBridge.sendEvent("Server offline", "The Minecraft server is shutting down."); DiscordBridge.shutdown(); active = null; } - /** Hook for the missile assembly/launch implementation to report a launch. */ + /** Call when the missile assembly is successfully launched. */ public static void missileLaunched(UUID missileId, String teamId, String teamName, String target) { - String resolvedTeam = FtbTeamsSync.displayName(teamId, teamName); DiscordBridge.sendEvent("Missile launched", - "Missile: " + safeId(missileId) + "\nTeam: " + resolvedTeam + "\nTarget: " + safe(target)); + "Missile: " + safeId(missileId) + "\nTeam: " + FtbTeamsSync.displayName(teamId, teamName) + + "\nTarget: " + safe(target)); } - /** Hook for the missile impact handler to report a resolved impact. */ + /** Call once when a missile impact is resolved. */ public static void missileImpacted(UUID missileId, String teamId, String teamName, String result, String location) { - String resolvedTeam = FtbTeamsSync.displayName(teamId, teamName); DiscordBridge.sendEvent("Missile impact", - "Missile: " + safeId(missileId) + "\nTeam: " + resolvedTeam + "\nResult: " - + safe(result) + "\nLocation: " + safe(location)); + "Missile: " + safeId(missileId) + "\nTeam: " + FtbTeamsSync.displayName(teamId, teamName) + + "\nResult: " + safe(result) + "\nLocation: " + safe(location)); } - /** Hook for interception/defence code to report a successful interception. */ + /** Call once when defence/interception code confirms an interception. */ public static void missileIntercepted(UUID missileId, String interceptor, String location) { DiscordBridge.sendEvent("Missile intercepted", "Missile: " + safeId(missileId) + "\nIntercepted by: " + safe(interceptor) + "\nLocation: " + safe(location)); } - private String webhookUrl = ""; - private String serverName = "Minecraft server"; - private void loadConfig() { try { if (Files.notExists(CONFIG_PATH)) { @@ -86,7 +83,9 @@ private void loadConfig() { { "enabled": false, "serverName": "Minecraft server", - "discordWebhookUrl": "" + "discordWebhookUrl": "", + "apiBaseUrl": "", + "apiToken": "" } """, StandardCharsets.UTF_8); } @@ -95,10 +94,16 @@ private void loadConfig() { enabled = config.has("enabled") && config.get("enabled").getAsBoolean(); serverName = config.has("serverName") ? config.get("serverName").getAsString() : "Minecraft server"; webhookUrl = config.has("discordWebhookUrl") ? config.get("discordWebhookUrl").getAsString() : ""; - if (enabled && !isValidWebhook(webhookUrl)) { + apiBaseUrl = config.has("apiBaseUrl") ? config.get("apiBaseUrl").getAsString() : ""; + apiToken = config.has("apiToken") ? config.get("apiToken").getAsString() : ""; + + boolean webhookReady = isValidWebhook(webhookUrl); + boolean apiReady = apiBaseUrl != null && !apiBaseUrl.isBlank() && apiToken != null && !apiToken.isBlank(); + if (enabled && !webhookReady && !apiReady) { enabled = false; - LOGGER.warn("Mythic integration disabled: discordWebhookUrl must be a Discord HTTPS webhook URL."); + LOGGER.warn("Mythic integration disabled: configure a valid Discord webhook or HTTPS bot API URL plus apiToken."); } + if (!webhookReady) webhookUrl = ""; } catch (Exception exception) { enabled = false; LOGGER.warn("Could not load Mythic integration config from {}: {}", CONFIG_PATH, exception.getMessage()); @@ -107,28 +112,23 @@ private void loadConfig() { @net.neoforged.bus.api.SubscribeEvent public void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) { - if (event.getEntity() instanceof ServerPlayer player) { + if (event.getEntity() instanceof ServerPlayer player) DiscordBridge.sendEvent("Player joined", player.getGameProfile().getName()); - } } @net.neoforged.bus.api.SubscribeEvent public void onPlayerLogout(PlayerEvent.PlayerLoggedOutEvent event) { - if (event.getEntity() instanceof ServerPlayer player) { + if (event.getEntity() instanceof ServerPlayer player) DiscordBridge.sendEvent("Player left", player.getGameProfile().getName()); - } } private static boolean isValidWebhook(String value) { try { java.net.URI uri = java.net.URI.create(value); - return "https".equalsIgnoreCase(uri.getScheme()) - && uri.getHost() != null - && uri.getHost().equalsIgnoreCase("discord.com") - && uri.getPath() != null && uri.getPath().startsWith("/api/webhooks/"); - } catch (IllegalArgumentException exception) { - return false; - } + return "https".equalsIgnoreCase(uri.getScheme()) && uri.getHost() != null + && uri.getHost().equalsIgnoreCase("discord.com") && uri.getPath() != null + && uri.getPath().startsWith("/api/webhooks/"); + } catch (IllegalArgumentException exception) { return false; } } private static String safeId(UUID id) { return id == null ? "unknown" : id.toString(); } From 5ff815deac30ff1d9e21b6c5819cd6ccfef1df53 Mon Sep 17 00:00:00 2001 From: Innes Date: Fri, 16 Oct 2026 22:02:17 -0700 Subject: [PATCH 4/6] Document authenticated Mythic bot API contract --- docs/MYTHIC_BOT_API.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docs/MYTHIC_BOT_API.md diff --git a/docs/MYTHIC_BOT_API.md b/docs/MYTHIC_BOT_API.md new file mode 100644 index 0000000..1fab0fa --- /dev/null +++ b/docs/MYTHIC_BOT_API.md @@ -0,0 +1,9 @@ +# Mythic companion bot API + +The Mythic build can post events to a companion bot over HTTPS. Configure `apiBaseUrl` and `apiToken` in the dedicated server's `config/icreate-order-networking-mythic.json`. Keep the token private; never commit the server config or place credentials in client-side files. + +The mod posts to `POST /api/icreate/events` with `Authorization: Bearer ` and a JSON body containing `source`, `event`, `details`, and an ISO-8601 `timestamp`. The bot should return a 2xx status after accepting an event and 401/403 for an invalid token. The mod validates HTTPS URLs and sends requests asynchronously away from the Minecraft server tick. + +Events include server lifecycle and player join/leave, plus missile launch/impact/interception when the relevant gameplay handlers call the hooks. + +**Important:** this repository contains the Minecraft mod, not the companion bot's `index.js`. The bot host still needs to implement and mount the authenticated HTTP endpoint. The existing `/mc-link` and team-sync approval queue are not connected by this outbound event client alone. \ No newline at end of file From 7b401c859b772f00f5cc424568a1493fdc2af1aa Mon Sep 17 00:00:00 2001 From: Innes Date: Fri, 16 Oct 2026 22:02:51 -0700 Subject: [PATCH 5/6] Add companion Discord bot Mythic event receiver --- bot-integration/mythic-api.js | 67 +++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 bot-integration/mythic-api.js diff --git a/bot-integration/mythic-api.js b/bot-integration/mythic-api.js new file mode 100644 index 0000000..f486cff --- /dev/null +++ b/bot-integration/mythic-api.js @@ -0,0 +1,67 @@ +const http = require("node:http"); +const crypto = require("node:crypto"); + +const MAX_BODY_BYTES = 8192; + +// Configure ICREATE_MYTHIC_API_TOKEN and ICREATE_MYTHIC_EVENTS_CHANNEL_ID. +// Mount behind an HTTPS reverse proxy if the Minecraft server is remote. +// Usage: const { startMythicApi } = require("./mythic-api"); startMythicApi(client); +function startMythicApi(client, options = {}) { + const token = process.env.ICREATE_MYTHIC_API_TOKEN; + const channelId = process.env.ICREATE_MYTHIC_EVENTS_CHANNEL_ID; + const host = options.host || process.env.ICREATE_MYTHIC_API_HOST || "127.0.0.1"; + const port = Number(options.port || process.env.ICREATE_MYTHIC_API_PORT || 8787); + + if (!token || token.length < 32) throw new Error("ICREATE_MYTHIC_API_TOKEN must be at least 32 characters."); + if (!channelId) throw new Error("ICREATE_MYTHIC_EVENTS_CHANNEL_ID must identify the events channel."); + if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Invalid ICREATE_MYTHIC_API_PORT."); + + const expected = Buffer.from(token, "utf8"); + const server = http.createServer(async (req, res) => { + res.setHeader("Cache-Control", "no-store"); + res.setHeader("X-Content-Type-Options", "nosniff"); + if (req.method !== "POST" || req.url !== "/api/icreate/events") return res.writeHead(404).end(); + + const auth = req.headers.authorization || ""; + const supplied = Buffer.from(auth.startsWith("Bearer ") ? auth.slice(7) : "", "utf8"); + if (supplied.length !== expected.length || !crypto.timingSafeEqual(supplied, expected)) { + return res.writeHead(401, { "Content-Type": "application/json" }).end(JSON.stringify({ error: "unauthorized" })); + } + if (!String(req.headers["content-type"] || "").toLowerCase().startsWith("application/json")) return res.writeHead(415).end(); + + try { + const chunks = []; + let size = 0; + for await (const chunk of req) { + size += chunk.length; + if (size > MAX_BODY_BYTES) { + res.writeHead(413).end(); + req.destroy(); + return; + } + chunks.push(chunk); + } + const body = JSON.parse(Buffer.concat(chunks).toString("utf8")); + if (!body || body.source !== "icreate-order-networking" || typeof body.event !== "string" || + typeof body.details !== "string" || typeof body.timestamp !== "string") { + return res.writeHead(400, { "Content-Type": "application/json" }).end(JSON.stringify({ error: "invalid event payload" })); + } + + const eventName = body.event.replace(/[\r\n\x00-\x1f]/g, " ").slice(0, 80); + const details = body.details.replace(/@everyone/g, "@\u200beveryone") + .replace(/@here/g, "@\u200bhere").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "").slice(0, 1800); + const channel = await client.channels.fetch(channelId); + if (!channel || !channel.isTextBased() || typeof channel.send !== "function") throw new Error("Configured events channel is not a text channel."); + await channel.send({ content: "**[iCreate Mythic] " + eventName + "**\n" + details, allowedMentions: { parse: [] } }); + res.writeHead(202, { "Content-Type": "application/json" }).end(JSON.stringify({ accepted: true })); + } catch (error) { + console.error("[Mythic API] Event rejected:", error.message); + if (!res.headersSent) res.writeHead(400, { "Content-Type": "application/json" }).end(JSON.stringify({ error: "event could not be processed" })); + } + }); + + server.listen(port, host, () => console.log("[Mythic API] Listening on " + host + ":" + port + "/api/icreate/events")); + return server; +} + +module.exports = { startMythicApi }; From ce284c080884cc26aec65d2153d0229d320300c0 Mon Sep 17 00:00:00 2001 From: Innes Date: Fri, 16 Oct 2026 22:02:58 -0700 Subject: [PATCH 6/6] Document included Mythic bot event receiver setup --- docs/MYTHIC_BOT_API.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/MYTHIC_BOT_API.md b/docs/MYTHIC_BOT_API.md index 1fab0fa..7140b87 100644 --- a/docs/MYTHIC_BOT_API.md +++ b/docs/MYTHIC_BOT_API.md @@ -2,8 +2,19 @@ The Mythic build can post events to a companion bot over HTTPS. Configure `apiBaseUrl` and `apiToken` in the dedicated server's `config/icreate-order-networking-mythic.json`. Keep the token private; never commit the server config or place credentials in client-side files. -The mod posts to `POST /api/icreate/events` with `Authorization: Bearer ` and a JSON body containing `source`, `event`, `details`, and an ISO-8601 `timestamp`. The bot should return a 2xx status after accepting an event and 401/403 for an invalid token. The mod validates HTTPS URLs and sends requests asynchronously away from the Minecraft server tick. +The mod posts to `POST /api/icreate/events` with `Authorization: Bearer ` and a JSON body containing `source`, `event`, `details`, and an ISO-8601 `timestamp`. Requests run asynchronously away from the Minecraft server tick. + +## Bot-side receiver + +A drop-in Node.js receiver is included at `bot-integration/mythic-api.js`. It uses Node's built-in HTTP and crypto modules and the existing Discord.js client. In the bot's entrypoint, add: + +```js +const { startMythicApi } = require("./mythic-api"); +startMythicApi(client); +``` + +Set `ICREATE_MYTHIC_API_TOKEN` (at least 32 random characters) and `ICREATE_MYTHIC_EVENTS_CHANNEL_ID` in the bot host environment. The receiver binds to `127.0.0.1` by default; use an HTTPS reverse proxy for remote Minecraft servers and set the mod's `apiBaseUrl` to that HTTPS origin. The receiver validates bearer credentials, limits request size, rejects malformed payloads, sanitizes mentions, and posts accepted events to the configured Discord channel. Events include server lifecycle and player join/leave, plus missile launch/impact/interception when the relevant gameplay handlers call the hooks. -**Important:** this repository contains the Minecraft mod, not the companion bot's `index.js`. The bot host still needs to implement and mount the authenticated HTTP endpoint. The existing `/mc-link` and team-sync approval queue are not connected by this outbound event client alone. \ No newline at end of file +**Still outstanding:** this event receiver does not implement `/mc-link` or the team-sync approval queue, and the current mod repository does not yet contain a working missile flight/impact/defence entity that can call those hooks. Those require separate integration work. \ No newline at end of file