feat(api): gate assistant endpoints on the feature-flag matrix

Every LLM-driven endpoint now reads the operator flag set near the
top of the handler and throws `AssistantDisabledError` when the
relevant surface is off. The api-handler catches the typed error
and returns the 403 envelope:

  { data: null, error: "...", meta: { errorCode: "assistant.disabled.<surface>" } }

Gates per the v1.4.31 brief:

  /api/insights/chat            → coach            (POST + GET rail)
  /api/insights/generate        → coach
  /api/insights/comprehensive   → coach
  /api/insights/cards           → insightStatus
  /api/insights/correlations    → correlations
  /api/insights/{weight,bmi,blood-pressure,pulse,mood,
                 medication-compliance}-status → insightStatus

Master `assistantEnabled = false` forces every sub-flag false in
the resolver, so flipping the master 403s every endpoint at once.

Older iOS clients see a generic 403 (their existing 403-handler
covers the case); v1.4.31+ clients read the `errorCode` to render
the `<AssistantDisabledNotice>` empty state. Locked per
`.planning/RESPONSE-TO-IOS-TEAM-2026-05-16.md` §3 R5.
This commit is contained in:
Marc-André Bombeck committed 2026-05-16 13:46:17 +02:00
1 parent 61c9a1e792
commit aeff47afde
16 files changed
+239 -2

No files matched your search

+19 -1
View File
@@ -9,6 +9,7 @@ import { getSession } from "./auth/session";
import { hashToken } from "./auth/hmac";
import { prisma } from "./db";
import { auditLog } from "./auth/audit";
import { AssistantDisabledError } from "./feature-flags";
/**
* Custom error class for HTTP errors with status codes.
@@ -162,7 +163,24 @@ export function apiHandler<T extends (...args: any[]) => Promise<Response>>(
try {
response = await handler(...args);
} catch (error) {
if (error instanceof HttpError) {
if (error instanceof AssistantDisabledError) {
// v1.4.31 — operator has disabled the assistant surface.
// The 403 + `errorCode: "assistant.disabled.<surface>"`
// envelope is locked per
// `.planning/RESPONSE-TO-IOS-TEAM-2026-05-16.md` §3 R5.
// Older iOS clients that don't know the errorCode surface
// this as a generic 403; v1.4.31+ clients render the
// `<AssistantDisabledNotice>` empty state.
evt.setError(error);
response = NextResponse.json(
{
data: null,
error: error.message,
meta: { errorCode: error.errorCode },
},
{ status: 403 },
);
} else if (error instanceof HttpError) {
evt.setError(error);
response = NextResponse.json(
{ data: null, error: error.message },