fix(insights): let a read grant read the record's targets

The weight band and the BMI chart read `/api/insights/targets` inside a
shared record, and the route asked for a manage grant, so a read or
write delegate got a 403 and saw no band and no BMI. The targets are
the bands the dashboard snapshot already paints on a whole-record read
grant, computed with no provider, so the read now asks for the same
level. A section-scoped grant is still refused.
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 22:54:34 +01:00
1 parent ce01d455be
commit f76e5cdafd
3 files changed
+7 -11

No files matched your search

+2 -7
View File
@@ -1553,7 +1553,7 @@ const DELEGABLE_ROUTES: Record<string, DelegableEntry> = {
},
"app/api/insights/targets/route.ts": {
domain: "record",
why: "The record's resolved targets. Reached only through this module's MANAGE arm; the argument for admitting it is the manage literal's reason line, and the entry here is what leg (a) freezes and leg (f) checks the section against.",
why: "The record's resolved targets: the bands and reference ranges the dashboard snapshot already paints on the whole-record grant, read the same way, with no provider on the path. GET only; nothing is written here.",
},
"app/api/insights/ecg/route.ts": {
domain: "record",
@@ -2099,11 +2099,6 @@ const DELEGABLE_MANAGE_ROUTES: Record<string, ManageEntry> = {
conditions: [],
why: "A score's daily course, read from the same rows the score pages and the day view read. Deterministic, no provider, nothing enqueued.",
},
"app/api/insights/targets/route.ts": {
domain: "record",
conditions: [],
why: "The record's resolved targets.",
},
"app/api/insights/ecg/route.ts": {
domain: "record",
conditions: [],
@@ -2305,7 +2300,7 @@ const ACTOR_ROUTES: Record<string, string> = {
* make, or a managed profile's guardian's, never an adult record's delegate's.
* 276 -> 278.
*/
const FROZEN_ENTRY_COUNT = 278;
const FROZEN_ENTRY_COUNT = 277;
/**
* The two surfaces that authenticate a Bearer token outside `requireAuth` —
+4 -3
View File
@@ -15,9 +15,10 @@ import { buildTargetsResponse } from "@/lib/targets/build-response";
export const dynamic = "force-dynamic";
export const GET = apiHandler(async () => {
// v1.37.0 — MANAGE-level read: computed over the whole record, with no
// provider anywhere on the path.
const { user } = await requireRecordAuth("manage", "record");
// A read of the whole record, with no provider anywhere on the path, at the
// level the dashboard snapshot answers: the weight band and the BMI chart
// read it inside a shared record, and a read or write grant got a 403.
const { user } = await requireRecordAuth("read", "record");
// v1.16.8 — stale-while-revalidate read. The cold build is the multi-query
// walk in `buildTargetsResponse`; past the 60 s fresh TTL the prior body
// serves immediately while ONE background rebuild warms the cell. Writes
+1 -1
View File
@@ -541,7 +541,7 @@ export const insightsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
tags: ["Insights"],
summary: "Per-metric target tiles + consistency strips",
description:
"The whole targets page in one read: a tile per metric with its band, its current and 30-day-average values, and the seven-day consistency strip behind it, plus the page summary, the diastolic companion figures and the profile facts every band was resolved from. Pure compute over the record's own data — no provider anywhere on the path. Stale-while-revalidate: past the fresh window the prior body serves immediately while one background rebuild warms the cell, and a write hard-evicts the bucket so the account's own action lands on the next read. Delegable at MANAGE level over the whole record: the walk spans vitals, sleep, medications, mood and glucose, so a section-scoped grant is refused rather than served a filtered page. Not module-gated as a whole; a tile whose data does not exist is simply absent. Cookie or Bearer auth.",
"The whole targets page in one read: a tile per metric with its band, its current and 30-day-average values, and the seven-day consistency strip behind it, plus the page summary, the diastolic companion figures and the profile facts every band was resolved from. Pure compute over the record's own data — no provider anywhere on the path. Stale-while-revalidate: past the fresh window the prior body serves immediately while one background rebuild warms the cell, and a write hard-evicts the bucket so the account's own action lands on the next read. Delegable at READ level over the whole record, like the dashboard snapshot: the walk spans vitals, sleep, medications, mood and glucose, so a section-scoped grant is refused rather than served a filtered page. Not module-gated as a whole; a tile whose data does not exist is simply absent. Cookie or Bearer auth.",
responses: {
...recordRefusal(),
"200": {