// Shared-secret client for the APP's internal listener (port 3001) — the // bot→app direction of the channel `botInternalClient.js` runs app→bot. // // Two callers, both slash-command plumbing (TEAMS.md §7.1): pull the registered // command definitions, and dispatch one that a member has just run. Distinct // from siteApiClient.js, which reads the site's PUBLIC API with no secret at all. // // **The base URL is derived from `SITE_INTERNAL_URL`'s origin, not configured // separately.** That variable already points at the app's internal listener — // `http://app:3001/internal/bot-config` — and adding a second variable naming the // same host would be one more thing an operator can get half-right. Deriving it // means every existing deployment gains these endpoints with no compose change. const createLogger = require('../utils/logger') const log = createLogger('app-internal') const KEY = process.env.BOT_INTERNAL_KEY || '' // §7.1's budget, and the same 4s `botInternalClient` uses in the other // direction. The app bounds its own handlers UNDER this (3s), so a timeout here // normally means the app itself is unreachable rather than a module being slow. const TIMEOUT_MS = 4000 function baseUrl() { const configured = process.env.SITE_INTERNAL_URL if (!configured) return null try { return new URL(configured).origin } catch { log.error('SITE_INTERNAL_URL is not a URL — slash-command registration is off', { configured }) return null } } async function call(path, { method = 'GET', body } = {}) { const base = baseUrl() if (!base || !KEY) return { ok: false, error: 'SITE_INTERNAL_URL or BOT_INTERNAL_KEY not set' } const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS) try { const res = await fetch(`${base}${path}`, { method, headers: { 'Content-Type': 'application/json', 'X-Internal-Key': KEY }, body: body ? JSON.stringify(body) : undefined, signal: controller.signal, }) if (!res.ok) return { ok: false, status: res.status, error: `app responded ${res.status}` } return { ok: true, status: res.status, data: await res.json() } } catch (err) { log.warn('app internal call failed', { path, message: err.message }) return { ok: false, status: 0, error: err.message } } finally { clearTimeout(timeout) } } /** The registered slash-command definitions, plus the version they belong to. */ function fetchCommands() { return call('/internal/commands') } /** * Run one command in the app and get the response envelope back. * * The bot has already deferred by the time this is called, so the only deadline * that matters is Discord's 15-minute follow-up window — TIMEOUT_MS is about not * holding an interaction open on a wedged app, not about the 3-second ack. */ function dispatchCommand({ command, options, platformUserId, guildId }) { return call('/internal/commands/dispatch', { method: 'POST', body: { command, options, platform: 'discord', platformUserId, guildId }, }) } module.exports = { fetchCommands, dispatchCommand }