Let a delegate read the dashboard they land on

Ten routes on `/` refused with `undeclared_mode`, which made the first page
of account sharing the only page in it that does not work. Each one now
declares a mode, per route rather than as a class.

Eight are record reads: the dashboard snapshot, the Today digest, the badge
grid, the Coach nudge signal and reminder ledger, the medication low-stock
thresholds, and the two presentation blobs. The old objection to admitting an
aggregate — a summary can carry a finding from a module the grant does not
cover — does not hold while a grant is whole-record and all-or-nothing. Every
admission site names that, so the day per-module scope lands the aggregates
are findable.

The layout routes split their verbs. Reading the owner's arrangement is right
and, for the dashboard, forced: the snapshot already carries the layout and
the client seeds the same cache cell from it, so an actor answer would put two
people's layouts in one key. Writing it is not a caregiver's job, so the PUT
and DELETE stay bare and refuse. Same on the medication list, where the stored
order names the owner's medication ids and means nothing anywhere else.

Two are actor surfaces: the UI language belongs to the person reading the
screen and must not be transplanted onto the record, and the feature-flag
matrix reads the settings singleton and no user row at all.

Also fixes a second instance of the delegate-data-under-owner-banner defect,
on the same page: the dashboard's client-side band fallback derived blood-
pressure targets and the weight range from `/api/auth/me`, which answers about
the caller, and painted them over the record's charts. Under a switch it now
derives nothing and waits for the snapshot's own bands.

The integration suite drives every admitted route with both accounts seeded,
proves the two answers differ, and then demands the owner's back.
This commit is contained in:
Marc-André Bombeck committed 2026-08-03 21:12:35 +02:00
1 parent a0fc404fb8
commit 508cc8a60a
17 files changed
+1025 -49

No files matched your search

+16 -5
View File
@@ -19,9 +19,10 @@
* "error": null
* }
*
* - `requireAuth()` — any logged-in user. Per-request flag fetches
* from the iOS native client always arrive after auth, so the
* gate matches the rest of the read-only profile surface.
* - `requireActorAuth()` — any logged-in user, including one acting on
* somebody else's record. Per-request flag fetches from the iOS native
* client always arrive after auth, so the gate matches the rest of the
* read-only profile surface; the mode is argued at the call site.
* - Master kills every sub-flag in the resolver before the shape
* leaves the handler, so callers never have to compose
* `master && sub`.
@@ -35,13 +36,23 @@
*/
import type { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { apiHandler, requireActorAuth } from "@/lib/api-handler";
import { apiSuccess } from "@/lib/api-response";
import { getAssistantFlags } from "@/lib/feature-flags";
import { annotate } from "@/lib/logging/context";
export const GET = apiHandler(async (_request: NextRequest) => {
await requireAuth();
// 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
// because the shell needs it on every page — the Coach launcher and the
// assistant surfaces are gated on it, and a 403 here is a piece of chrome
// that decides it does not exist.
//
// Declared rather than left bare so the reasoning is recorded: it answers
// about the DEPLOYMENT, which for this purpose is the caller's side of the
// request, not the record's.
await requireActorAuth();
annotate({ action: { name: "feature-flags.read" } });
const assistant = await getAssistantFlags();