feat(chat): optional moderation-service bridge for free-form chat#40
Draft
ChronoFinale wants to merge 1 commit into
Draft
feat(chat): optional moderation-service bridge for free-form chat#40ChronoFinale wants to merge 1 commit into
ChronoFinale wants to merge 1 commit into
Conversation
ChronoFinale
force-pushed
the
feat/chat-moderation
branch
from
July 14, 2026 23:35
bda75bd to
ddd39a2
Compare
Adds a forward-only bridge to an external moderation service. With MODERATION_SERVICE_URL unset (the default) nothing changes — chat runs the existing local pipeline exactly as before. Layered per the repo structure: - infrastructure/gateways/moderation.gateway.ts — transport only: config from env.ts (parsed once at boot; timeout/URL/policy validated), POST /moderate, wire-body type guard. Any transport/protocol failure maps to `unreachable`. - features/chat/moderation-policy.ts — the pure decision: verdict/outage -> publish (with optional rewrite) or a typed rejection. ChatResult is a discriminated union with literal reasons, so the route mapping is compiler-checked. - chat.service.ts stays gather -> decide -> publish. The reported-lobby evidence buffer keeps the ORIGINAL text — a rewrite goes to MQTT and to the sender (publishText) but never launders the record moderators review. Behavior on the wire: - allow: publish as-is or the service's rewritten form; publishText returned to the sender so their client can show what was delivered - reject: 403 (moderated / muted with until-timestamp) or 429 with Retry-After (rate-limited and service load-shedding are one client contract) - unreachable: fail closed — 503 (cause logged, throttled), or allowlist-only when MODERATION_OUTAGE_POLICY=presets (curated presets are safe by construction) Tests: pure decision table (moderation-policy), gateway wire handling incl. malformed bodies and abort signal, env parsing (unset URL => null bridge, trailing slash, garbage timeout), service-level bridge behavior incl. evidence-buffer originals, and a supertest route suite pinning the HTTP mappings (Retry-After surviving the error middleware, muted message, 503). Config: MODERATION_SERVICE_URL, MODERATION_BEARER_TOKEN, MODERATION_TIMEOUT_MS (default 1500), MODERATION_OUTAGE_POLICY (off|presets).
ChronoFinale
force-pushed
the
feat/chat-moderation
branch
from
July 14, 2026 23:50
ddd39a2 to
197dcaa
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds a forward-only bridge from the chat path to an external moderation service. With
MODERATION_SERVICE_URLunset (the default), nothing changes — chat runs the existing local pipeline exactly as before. The bridge is dormant until env vars opt in.The v1 rollout, across repos
POST /moderatebefore publish; verdict → HTTP mappingapi.mod.balatroleague.com, tested end-to-end in-game against this exact bridgeNot-a-PR ops to go live here: set the four env vars below on the relay. Rollout can start in shadow mode service-side (logs would-blocks, blocks nothing) to eyeball calls on real traffic before enforcement.
Structure (follows the repo layering)
infrastructure/gateways/moderation.gateway.ts— transport only. Config comes fromenv.ts, parsed once at boot with validation (timeout guarded againstNumber('')→0 / NaN, trailing-slash URLs normalized, unknown outage policy warns + fails closed). Responses are validated with a type guard, never blind-cast. Owns the wire contract: the service's band vocabulary is documented here, and the two bands the relay treats specially are named constants (BAND_MUTED,BAND_RATE_LIMITED) — unknown future bands degrade to a generic block by design.features/chat/moderation-policy.ts— the pure decision: verdict/outage → publish (with optional rewrite) or a typed rejection.ChatResultis a discriminated union with literal reasons, so the route's mapping is compiler-checked.chat.service.ts— stays thin: gather → decide → publish.Behavior
publishText), which is also returned to the sender ("delivered as …"). The reported-lobby evidence buffer keeps the original text — a rewrite never launders the record moderators review.403(moderated / muted with until-timestamp) or429withRetry-After(per-player rate limiting and service load-shedding are one client contract)503(cause logged server-side, throttled), or allowlist-only whenMODERATION_OUTAGE_POLICY=presets(curated presets are safe by construction, so preset chat survives an outage)No automatic retries anywhere — the sender gets instant feedback, so the player is the retry loop; auto-retries would amplify load exactly when the service is struggling.
Config
MODERATION_SERVICE_URLMODERATION_BEARER_TOKENMODERATION_TIMEOUT_MS1500MODERATION_OUTAGE_POLICYoffoff= chat 503s on outage;presets= allowlist-onlyTesting
Retry-Aftersurviving the error middlewareFull suite: 415/415 green on top of current
mqtt.