feat(teams): the slash-command seam, and the first command through it

Phase 7 of TEAMS.md. `api.registerSlashCommands` stops throwing: a module
registers a command's DEFINITION and its HANDLER together, the bot pulls the
definitions over the internal listener and runs none of our code, and the
handler executes here — forced by the bot container having no `modules` volume,
and the right boundary anyway.

Registration validates what Discord would reject as a batch (names, description
lengths, the four option types, required-before-optional), because the bot
registers the whole set in one PUT and a single bad entry costs every command
including the bot's own. Commands are not namespaced under their owner — there
is no dot in Discord's name grammar — so collisions are first-come with the
holder named.

The dispatcher is the access boundary: `linked` has no Discord equivalent, so
the platform-side permission default can only ever be advertising. It resolves
the actor by `auth_providers.kind` rather than the id slug, treats a banned
account as unlinked, bounds a handler under the bot's own timeout, and keeps
`ok` outside the envelope so a handler cannot forge it.

Liveness is asked at both the pull and the dispatch. The registries have no
removal path, so a module an operator disables at runtime would otherwise keep
a live handler behind a command Discord still advertises.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-18 18:53:34 -05:00
parent b1d3b87cd6
commit cecd72915f
21 changed files with 1459 additions and 27 deletions

View File

@@ -72,4 +72,16 @@ function reverseModAction({ discordUserId, actionType, appealId }) {
})
}
module.exports = { pushConfig, getStatus, announce, reverseModAction }
// Site -> bot: the registered slash-command set has moved, re-pull it
// (TEAMS.md §7.1). Best-effort like everything else here — a bot that is down
// re-pulls on its next `ready` anyway, so a missed nudge costs nothing but the
// delay until the bot reconnects.
//
// Its own endpoint rather than a field on pushConfig, whose body carries the
// DECRYPTED bot token: telling the bot that a module changed should not require
// reading a secret out of the database.
function refreshCommands() {
return call('/internal/refresh-commands', { method: 'POST', body: {} })
}
module.exports = { pushConfig, getStatus, announce, reverseModAction, refreshCommands }