mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
Two downloads under the token — the FHIR document Bundle and the clinical PDF —
behind the same unlock cookie the document route enforces, serving exactly the
link's frozen selection. That is what the removed FHIR toggle promised, in the
form a practice actually uses: open the link, unlock it, save the file. No new
credential type, no new scope, no mint path that does not exist.
The gate is the document route's, step for step: the same live check, the same
token-scoped cookie, the same blunt 404 for every miss class so a probe cannot
tell "no such link" from "revoked" from "documents-only". It differs in one
number, because generating a bundle is far more expensive than serving a stored
blob and must not share a budget with it. Each download writes its own audit
row. The scope cannot exceed the page's: both resolve the same frozen leaf list
through the same resolver, and the insurance leaf is refused at create, so the
insurance number cannot reach this path at all.
The MCP doctor-visit summary stops claiming presence it cannot support. An
account whose owner never saved a report selection resolves to the empty one on
that surface, and an empty scope produces an empty payload — which now reads as
{ present: false, reason }, the same sentinel the other reads use, instead of a
record with every section quietly missing.
Mutation-checked: removing the unlock check, the rate limit, the documents-only
guard or the audit row reddens its own case; pointing the bucket at the document
route's key reddens the bucket assertion; making presence unconditional reddens
the absence case.
The saved profile's shape moves out of the module that reads the row. The panel
is a client component and parses the blob from the /me payload; importing the
reader dragged the database client into the browser bundle, which typecheck and
the unit suite both pass and only `next build` catches.
44 lines
1.6 KiB
TypeScript
44 lines
1.6 KiB
TypeScript
/**
|
|
* The shape of the owner's saved report profile, without the read that
|
|
* fetches it.
|
|
*
|
|
* The export panel is a client component and needs to parse the blob that
|
|
* rides the `/api/auth/me` payload. Keeping the schema here means it can do
|
|
* that without pulling the Prisma client into the browser bundle — the same
|
|
* separation `saved-profile.ts` documents on the server side.
|
|
*/
|
|
import { z } from "zod/v4";
|
|
|
|
import { reportSelectionSchema } from "./selection";
|
|
|
|
/**
|
|
* The stored profile: the leaf list plus the render choices the panel
|
|
* restores. `format` / `rangeDays` / `includeCharts` are presentation, not
|
|
* scope — no gate reads them — so they ride alongside rather than inside the
|
|
* selection.
|
|
*/
|
|
export const savedReportProfileSchema = reportSelectionSchema.extend({
|
|
format: z.enum(["pdf", "fhir", "package"]),
|
|
rangeDays: z.number().int().min(1).max(365),
|
|
includeCharts: z.boolean(),
|
|
});
|
|
|
|
export type SavedReportProfile = z.infer<typeof savedReportProfileSchema>;
|
|
|
|
/**
|
|
* The render choices the panel starts from when the account has never saved a
|
|
* profile. Presentation only — this is deliberately NOT a scope fallback;
|
|
* there is no such thing here, because a scope nobody chose is the defect.
|
|
*/
|
|
export const SAVED_PROFILE_FALLBACK = {
|
|
format: "pdf",
|
|
rangeDays: 90,
|
|
includeCharts: true,
|
|
} as const;
|
|
|
|
/** Parse a stored profile blob, or `null` when there is none this build reads. */
|
|
export function parseSavedProfile(raw: unknown): SavedReportProfile | null {
|
|
const parsed = savedReportProfileSchema.safeParse(raw);
|
|
return parsed.success ? parsed.data : null;
|
|
}
|