From 2b5974cf4ad0564fc22fb262c5f35d64eca06e5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 16 Jun 2026 01:24:20 +0200 Subject: [PATCH] fix(coach): enforce the module gate on coach chat and list routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The streaming POST and the conversation-list GET enforced only the legacy operator assistant flag (requireAssistantSurface("coach")), missing the per-user disableCoach opt-out and the operator module-availability layer. Both handlers now run the two-layer requireModuleEnabled(userId, "coach") gate right after auth, returning 403 module.disabled when the account has Coach turned off — even with a valid Bearer token. The legacy assistant-flag check stays in place. --- src/app/api/insights/chat/route.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/app/api/insights/chat/route.ts b/src/app/api/insights/chat/route.ts index 73bf1c728..df06d2c80 100644 --- a/src/app/api/insights/chat/route.ts +++ b/src/app/api/insights/chat/route.ts @@ -35,6 +35,7 @@ import { auditLog } from "@/lib/auth/audit"; import { prisma } from "@/lib/db"; import { checkRateLimit } from "@/lib/rate-limit"; import { requireAssistantSurface } from "@/lib/feature-flags"; +import { requireModuleEnabled } from "@/lib/modules/gate"; import { resolveServerLocale } from "@/lib/i18n/server-locale"; import { @@ -151,6 +152,10 @@ function buildHistoryWindow( async function handleChatRequest(request: NextRequest): Promise { const auth = await requireAuth(); + // v1.18.0 — two-layer module gate (operator availability + per-user + // disableCoach) on top of the legacy assistant flag. 403 module.disabled. + const gate = await requireModuleEnabled(auth.user.id, "coach"); + if (!gate.enabled) return gate.response; // v1.4.31 — operator can disable the Coach surface app-wide. // Throws AssistantDisabledError → apiHandler returns 403 + // `errorCode: "assistant.disabled.coach"` per the iOS contract. @@ -687,6 +692,9 @@ export const POST = apiHandler(handleChatRequest); */ export const GET = apiHandler(async (request: NextRequest) => { const auth = await requireAuth(); + // v1.18.0 — same two-layer module gate as the SSE POST. + const gate = await requireModuleEnabled(auth.user.id, "coach"); + if (!gate.enabled) return gate.response; // v1.4.31 — same gate as the SSE POST. Hiding the rail when // the operator has disabled Coach matches the FAB suppression // on the client.