Skip to content

v1.38.13 — a guardian sees the controls the routes accept - #942

Merged
MBombeck merged 9 commits into
mainfrom
fix/managed-record-capabilities
Sep 9, 2026
Merged

v1.38.13 — a guardian sees the controls the routes accept#942
MBombeck merged 9 commits into
mainfrom
fix/managed-record-capabilities

Conversation

@MBombeck

@MBombeck MBombeck commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Refs #939. A person acting on a managed profile, or on a record shared with them at the manage level, saw no add, edit or delete controls on Mood, Mental wellbeing, Visits, Profile, Labs and others. The server accepted those writes; the client hard-wired canManage to false for every shared record, a hold-back from v1.37.0 "until the routes answer" that never got lifted. Lifting it globally would have painted controls that 403 in the documents surfaces, so the fix is per domain and server-authoritative.

One source of truth. src/lib/sharing/domain-write-support.ts records, per share domain, whether any route accepts delegated writes at write and at manage. A structural test scans every src/app/api/**/route.ts for requireRecordAuth("write"|"manage", domain) and fails on drift in either direction. Derived table: measurements, medications, labs, profile and illness accept write and manage; mind and cycle accept manage only; documents accept neither.

Server. Every accountAccess entry on GET /api/auth/me gains writableDomains and manageableDomains: the sections the grant opens, intersected with the table at the grant's level. Additive; OpenAPI regenerated; the account-payload consumer guard covers the two fields.

Client. useRecordCapabilities gains canWriteDomain and canManageDomain; canManage now means "at least one manageable domain" and only the shared-record banner and the navigation still use that coarse form. Every former consumer names its domain. Owner-only by decision, with the reason at each site: the layout wrenches, chart overlay controls, custom metrics, the today hero, the efficacy tab, the labs OCR probe, and the medication detail tabs that mix caller routes. The Documents page shows a guardian a sentence that only the record owner can add documents, in all seven locales, instead of an empty slot.

Tests. Hook and affordance unit tests replace the hold-back pin with the new contract; the structural guard above; an integration test where a guardian reads the two lists on /api/auth/me, creates a mood entry and an allergy on the managed profile, and is refused a document write; an e2e spec where the guardian adds a mood entry and sees the owner-only sentence on Documents.

Gates on the final commit: typecheck, lint (three baseline warnings), format, openapi:check, unit suite 22 893 passed, full integration suite 215 files / 1930 tests, production build, e2e spec locally.

Issues 2 to 4 of #939 (module switches per managed profile, editing a managed profile, sex at creation) are the management surface and follow separately; the issue stays open.

Add the one table of which record sections have a route declaring
requireRecordAuth("write") or ("manage"), derived from the route files,
and a structural guard that re-derives it from src/app/api on every run
and fails on a disagreement in either direction. The matcher tolerates
the line break a formatter puts after the opening parenthesis and
asserts a non-zero match count.

delegatedDomains() turns a grant's level and scope into the sections it
can add to and the sections it can change, keeping write within manage.
… payload

Every accountAccess entry on GET /api/auth/me carries writableDomains
and manageableDomains: the sections the grant opens, intersected with
the sections whose routes accept a delegated write at that level. A
READ grant gets two empty lists, a WRITE grant gets writable sections
only, a MANAGE grant gets both, and the vault appears in neither.

The client parser checks the structural invariants (no repeats, within
the grant's sections, manageable within writable, nothing below the
level) and requires the two published views of the active record to
agree on the lists. Regenerate the OpenAPI document and extend the
payload consumer guard with the active-record readers.
useRecordCapabilities answers per section now. canWriteDomain and
canManageDomain read the two lists the account payload publishes; in
one's own record they are always true, and under a refused, pending or
unproven context always false. canManage becomes the coarse switch,
true when any section is manageable, and is what the banner and the
navigation read.

Every add, edit and delete control names its own section: labs, mind,
profile, illness, cycle, medications, measurements, documents. Creates
that a WRITE route answers (visits, practitioners) ask canWriteDomain.
The generic delete button, the selection bar, the capture picker, the
dashboard quick entry and the bottom nav take the section from their
callers. Controls whose routes resolve the caller rather than the
record (settings shortcuts, chart overlay preferences, custom metrics,
the Today rail, OCR, efficacy targets, credential tabs) ask for the
caller's own record instead, and the customise-shortcut guard now
requires that shape.

The vault has no delegated write route, so its controls stay withheld
under every grant, and the documents page says so in one sentence
where the upload control would be, in all seven locales.
…and the browser

The integration leg reads GET /api/auth/me as a guardian acting on a
managed profile and asserts both lists, lands a mood entry and an
allergy on the profile's record, is refused a vault write, and shows a
WRITE delegate writable sections without manageable ones together with
the refused mood entry the list withheld.

The browser leg opens the managed profile, adds a mood entry through
the control that used to be hidden and asserts the POST's status, and
finds the owner-only sentence and no upload control on the documents
page. The spec gets its own session jar and runs in the desktop
project only, like every other spec that moves the record selector.
…contract

The account-access integration contract compares whole entries, so the
two lists the payload now publishes belong in its expected shapes. The
managed-record leg posts the mood entry with the logged-at instant the
create schema requires.
…cept

Version anchors, the OpenAPI document and the changelog entry. Reported in #939.
@MBombeck MBombeck changed the title Show a guardian the controls the routes accept v1.38.13 — a guardian sees the controls the routes accept Sep 9, 2026
pnpm-workspace.yaml carried two overrides for the same packages: an older floor (hono < 4.12.25, js-yaml >= 4.0.0 < 4.3.1) and the newer one added for the scanner findings. The older selector won the resolution, so the lockfile kept hono 4.13.3 and js-yaml 4.3.1 and four of the seventeen findings stayed open after v1.38.12. The stale bounds are gone; the lockfile now resolves hono 4.13.7 and js-yaml 4.3.2.
The release commit carried the version anchors and the OpenAPI document but not the changelog entry; it is added here.
@MBombeck
MBombeck merged commit 1d372df into main Sep 9, 2026
24 checks passed
@MBombeck
MBombeck deleted the fix/managed-record-capabilities branch September 9, 2026 20:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant