feat(api): date deprecations and log which builds still call them

/api/feature-flags answered Deprecation: true and nothing else: a client
learned the route was going, never when, and the log could not tell which
builds still called it. deprecated() (src/lib/http/deprecation.ts) applies
one reviewed notice per route: Deprecation as an RFC 9745 date, Sunset as
an RFC 8594 HTTP-date, the successor Link, and a wide-event annotation
with the client build read from the User-Agent.

feature-flags was deprecated on 2026-09-24 (v1.39.0); its sunset is
2027-03-31. The headers are declared on the operation, and a test holds
every notice to a deprecated operation that answers through the helper.
This commit is contained in:
Marc-André Bombeck committed 2026-10-20 13:57:11 +02:00
1 parent 39d23dbb7d
commit 9fda88586c
6 files changed
+237 -10

No files matched your search

@@ -150,7 +150,10 @@ describe("GET /api/feature-flags", () => {
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
FIND.mockResolvedValue(null);
const res = await GET(req());
expect(res.headers.get("Deprecation")).toBe("true");
// RFC 9745: the deprecation date as `@` and Unix seconds — 2026-09-24.
expect(res.headers.get("Deprecation")).toBe("@1790208000");
// RFC 8594: the first day it may stop answering.
expect(res.headers.get("Sunset")).toBe("Wed, 31 Mar 2027 00:00:00 GMT");
expect(res.headers.get("Link")).toBe(
'</api/auth/me>; rel="successor-version"',
);
+7 -6
View File
@@ -12,8 +12,10 @@
* "insightStatus", "documentAi" } } }
*
* The master is applied (every sub-switch reads false when it is off), and a
* switch read that fails answers every switch off. The response carries
* `Deprecation: true` and a `Link` to its successor.
* switch read that fails answers every switch off. The response carries the
* deprecation date, the sunset and a `Link` to its successor
* (`src/lib/http/deprecation.ts`), and the wide event names the client build
* that still asked.
*
* `Cache-Control: private, max-age=60`: switches flip rarely.
*/
@@ -22,9 +24,10 @@ import type { NextRequest } from "next/server";
import { apiHandler, requireActorAuth } from "@/lib/api-handler";
import { apiSuccess } from "@/lib/api-response";
import { getAssistantFlags } from "@/lib/feature-flags";
import { deprecated } from "@/lib/http/deprecation";
import { annotate } from "@/lib/logging/context";
export const GET = apiHandler(async (_request: NextRequest) => {
export const GET = apiHandler(async (request: NextRequest) => {
// An actor surface, and the easiest call in the set: the answer comes off
// the `AppSettings` singleton and reads no user row at all, so there is no
// record for a switch to substitute. It stays reachable while a switch is on
@@ -45,7 +48,5 @@ export const GET = apiHandler(async (_request: NextRequest) => {
const response = apiSuccess({ assistant });
response.headers.set("Cache-Control", "private, max-age=60");
response.headers.set("Deprecation", "true");
response.headers.set("Link", '</api/auth/me>; rel="successor-version"');
return response;
return deprecated(response, "/api/feature-flags", request);
});