fix(fhir): stop advertising a credential no client can obtain

The capability descriptor named `fhir:read` as the scope a narrow token needs
for the REST face. No code path mints a token carrying it — every mint grants a
wildcard, the medication ingest scope, or an MCP scope — so the face is
reachable by the owner's cookie session or a wildcard device token and by
nothing else. Advertising an unobtainable credential invites a client to go
looking for one. The constant stays where it belongs, as the fail-closed check
each route makes; the descriptor now reports the fact instead.

End-to-end coverage for the scope picker: the first run opens it expanded with
the fenced tier rendered and no group control over it, a group row clears the
44 px floor at 390 px, and a leaf grid opens from its chevron. All on stable
test ids rather than text a breakpoint can hide.
This commit is contained in:
Marc-André Bombeck committed 2026-07-26 00:45:08 +02:00
1 parent f12290532f
commit ffe99caccd
5 files changed
+72 -12

No files matched your search

@@ -53,7 +53,6 @@ import {
GERMAN_ATC_DEFAULT_LOCALES,
} from "@/lib/fhir/build-bundle";
import {
FHIR_READ_SCOPE,
FHIR_REST_RESOURCE_TYPES,
FHIR_EVERYTHING_OPERATION,
FHIR_SEARCH_PARAMS,
@@ -87,7 +86,7 @@ type CapabilitiesBody = {
snomedRoute: string;
germanAtcDefaultLocales: string[];
restBaseUrl: string;
readScope: string;
scopeMintable: boolean;
resourceTypes: string[];
operations: string[];
searchParams: string[];
@@ -169,7 +168,10 @@ describe("GET /api/meta/capabilities — drift guards", () => {
const res = await call();
const body = (await res.json()) as CapabilitiesBody;
expect(body.data.fhir.restBaseUrl).toBe("/api/fhir");
expect(body.data.fhir.readScope).toBe(FHIR_READ_SCOPE);
// The face checks `fhir:read`, but nothing mints a token carrying it, so
// advertising it as an obtainable credential would be a claim the code
// cannot support.
expect(body.data.fhir.scopeMintable).toBe(false);
expect(body.data.fhir.resourceTypes).toEqual([...FHIR_REST_RESOURCE_TYPES]);
expect(body.data.fhir.operations).toEqual([FHIR_EVERYTHING_OPERATION]);
expect(body.data.fhir.searchParams).toEqual([...FHIR_SEARCH_PARAMS]);
+7 -2
View File
@@ -41,7 +41,6 @@ import {
GERMAN_ATC_DEFAULT_LOCALES,
} from "@/lib/fhir/build-bundle";
import {
FHIR_READ_SCOPE,
FHIR_REST_RESOURCE_TYPES,
FHIR_EVERYTHING_OPERATION,
FHIR_SEARCH_PARAMS,
@@ -111,7 +110,13 @@ export const GET = apiHandler(async () => {
// whole-record operation, the honoured search params and the narrow
// Bearer scope. All sourced from the canonical `rest.ts` constants.
restBaseUrl: "/api/fhir",
readScope: FHIR_READ_SCOPE,
// The face checks a narrow `fhir:read` scope, and NO code path mints a
// token carrying it — every mint grants a wildcard, the medication
// ingest scope, or an MCP scope. So the face is reachable by the owner's
// cookie session or a wildcard device token, and by nothing else. It is
// reported that way rather than advertising a credential a client cannot
// obtain.
scopeMintable: false,
resourceTypes: FHIR_REST_RESOURCE_TYPES,
operations: [FHIR_EVERYTHING_OPERATION],
searchParams: FHIR_SEARCH_PARAMS,
+5 -3
View File
@@ -70,9 +70,11 @@ const capabilitiesResponse = z
restBaseUrl: z
.string()
.describe("Base path of the read-only FHIR R4 REST face."),
readScope: z
.string()
.describe("Bearer scope a narrow token needs to read the FHIR face."),
scopeMintable: z
.boolean()
.describe(
"Whether a narrow Bearer token for the FHIR face can be obtained. Always false: no mint path grants the scope, so the face is reachable by the owner's cookie session or a wildcard device token only.",
),
resourceTypes: z
.array(z.string())
.describe(