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

+18 -1
View File
@@ -394,7 +394,7 @@ export const metaPaths: NonNullable<ZodOpenApiObject["paths"]> = {
summary: "The operator's assistant switches (deprecated)",
deprecated: true,
description:
'Deprecated: read `ai` on GET /api/auth/me instead, which resolves each AI capability for the record from the switches, the record\'s modules, the provider, consent and who is asking. This route projects the operator switches alone and is removed in the first release after the native build that reads `ai` ships. Responses carry `Deprecation: true` and `Link: </api/auth/me>; rel="successor-version"`. The master is applied server-side, so every sub-switch is already false when it is off.\n\n' +
'Deprecated: read `ai` on GET /api/auth/me instead, which resolves each AI capability for the record from the switches, the record\'s modules, the provider, consent and who is asking. This route projects the operator switches alone and is removed in the first release after the native build that reads `ai` ships. Responses carry `Deprecation` (the date it was deprecated, as `@<unix seconds>`), `Sunset` (the first day it may stop answering) and `Link: </api/auth/me>; rel="successor-version"`. The master is applied server-side, so every sub-switch is already false when it is off.\n\n' +
"An ACTOR surface: it answers about the deployment, not about a record, so it keeps answering while the caller is acting on somebody else's — the Coach launcher and the assistant chrome are gated on it, and a refusal here would delete a piece of the shell rather than a piece of the data. A request that attaches the per-request account selector (the `AccountSelector` header parameter) is refused with 403 `sharing.not_permitted` rather than quietly answered.\n\n" +
"Served with `Cache-Control: private, max-age=60`: an operator's toggle propagates within a minute, and the flag read stays off the hot mount path in the meantime.",
responses: {
@@ -408,6 +408,23 @@ export const metaPaths: NonNullable<ZodOpenApiObject["paths"]> = {
),
},
},
headers: {
Deprecation: {
description:
"When this route was deprecated, as an RFC 9745 date: `@` and Unix seconds.",
schema: { type: "string" as const, pattern: "^@[0-9]+$" },
},
Sunset: {
description:
"The first day this route may stop answering (RFC 8594 HTTP-date).",
schema: { type: "string" as const },
},
Link: {
description:
'The successor, as `</api/auth/me>; rel="successor-version"`.',
schema: { type: "string" as const },
},
},
},
"403": {
description: