Merge managed-profile editing, sex at creation and module switches per record

This commit is contained in:
Marc-André Bombeck committed 2026-09-10 04:19:35 +02:00
commit 06d2104808
36 files changed
+2795 -147

No files matched your search

+190 -20
View File
@@ -18200,7 +18200,8 @@ paths:
summary: The signed-in account, and what sharing lets it do
description: "Keeps answering while the browser is acting on another record — the switcher, the banner naming whose
record is open, and the way back out all read it, so a refusal here would strand a switched session. It is
therefore an ACTOR surface: it always serves the caller's own rows, and a request that attaches
therefore an ACTOR surface: it serves the caller's own rows — apart from `modules` and `cycleTrackingEnabled`,
which answer for the record the session is inside, masked to what the grant opens — and a request that attaches
`X-HealthLog-Account` is refused with 403 `sharing.not_permitted` rather than quietly answered. A client that
wants a record's data sends the selector on the read that needs it, never on this call."
responses:
@@ -18504,6 +18505,89 @@ paths:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
/api/managed-profiles/{id}:
get:
tags:
- Account sharing
summary: Read a managed record's identity
description: "What the record is right now — the five fields its creation took. The read an edit form fills itself from,
and the reason it exists: `accountAccess.accounts` on the account payload carries the record's name and nothing
else, so a form offered from the Guardian's own panel had no way to show the timezone, the language or the date
of birth it was about to overwrite. Cookie transport only, an ACTOR surface (the profile is named in the path
and the caller acts as themselves, so it answers without switching into the record), and NOT step-up gated: this
is a read, it discloses to a Guardian only what they already administer, and the gate belongs to the acts."
parameters:
- in: path
name: id
schema:
type: string
required: true
responses:
"200":
description: The record as it stands.
content:
application/json:
schema:
$ref: "#/components/schemas/ManagedProfileEnvelope"
"401": *a1
"404":
description: "No such managed profile, or the caller is not one of its Guardians (`meta.errorCode:
managed_profile.not_found`). The two are byte-identical, so the refusal is not an enumeration oracle."
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a4
"429": *a5
patch:
tags:
- Account sharing
summary: Change a managed record's identity
description: "Edit the record after it exists — the name that was a placeholder, the timezone that moved with the
household, the date of birth somebody finally has, the sex that decides whether the cycle module is on. Any
active Guardian may do it, from a cookie session with a fresh second factor: the same gate creation and deletion
carry, because the three acts mint, change and end an account that can never prove anything about itself. Ten an
hour per caller, the ceiling creation carries. Answers with the whole record, so a client re-renders from the
server's version rather than from what it sent."
parameters:
- in: path
name: id
schema:
type: string
required: true
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateManagedProfileRequest"
responses:
"200":
description: The record as it now stands.
content:
application/json:
schema:
$ref: "#/components/schemas/ManagedProfileEnvelope"
"401":
description: No fresh second-factor proof, or none enrolled. Same unconditional gate as creation and deletion.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"404":
description: "No such managed profile, or the caller is not one of its Guardians (`meta.errorCode:
managed_profile.not_found`). The two are byte-identical."
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a4
"429":
description: More than ten edits from one caller within the hour — the ceiling creation carries. No error code; branch
on the status. Nothing was changed, and the refusal lands before the record named in the path is looked at.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
delete:
tags:
- Account sharing
@@ -28096,6 +28180,14 @@ components:
type: string
minLength: 1
maxLength: 64
gender:
anyOf:
- type: string
enum:
- MALE
- FEMALE
- OTHER
- type: "null"
required:
- displayName
- locale
@@ -28105,12 +28197,58 @@ components:
The caller becomes its first Guardian in the same transaction, through an ordinary MANAGE grant with the new
record as grantor, so the profile appears in `accountAccess.accounts` on the next `GET /api/auth/me` and is
switched into like any other shared record. `displayName` is the only required text; `dateOfBirth` is an
optional `YYYY-MM-DD` and is never synthesised from a year. `locale` and `timezone` belong to the RECORD and not
to the Guardian reading it: they decide how the profile's own reminders are worded and when its day starts.
Strict — an unexpected field is a 422 rather than an ignored key. Cookie transport only and step-up gated
optional `YYYY-MM-DD` and is never synthesised from a year. `gender` is optional and takes the same three values
`PATCH /api/auth/me` takes for an ordinary account (`MALE`, `FEMALE`, `OTHER`) or null; absent and null both
mean not recorded, and the value is what the cycle module derives its default from, so a record created for a
child who does not need it starts with it off. `locale` and `timezone` belong to the RECORD and not to the
Guardian reading it: they decide how the profile's own reminders are worded and when its day starts. Strict — an
unexpected field is a 422 rather than an ignored key. Cookie transport only and step-up gated
(`requireFreshMfa`, unconditional): a Bearer caller cannot mint a credential-less person and a permanent
management relationship, and an account with no second factor enrolled is refused rather than waved through.
Rate-limited to ten an hour per caller."
UpdateManagedProfileRequest:
type: object
properties:
displayName:
type: string
minLength: 1
maxLength: 80
dateOfBirth:
anyOf:
- type: string
format: date
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))$
- type: "null"
locale:
type: string
enum:
- de
- en
- es
- fr
- it
- pl
- ko
timezone:
type: string
minLength: 1
maxLength: 64
gender:
anyOf:
- type: string
enum:
- MALE
- FEMALE
- OTHER
- type: "null"
additionalProperties: false
description: "Change what a managed record is, from the Guardian's own panel. The same five fields creation takes, every
one of them optional, and at least one required — an empty body is a 422 rather than a no-op that audits a
change nobody made. Absence means leave the field alone; an explicit null on `dateOfBirth` or `gender` clears
it, and the two are different answers. Nothing else on the record is reachable here: credentials, provider
connections, notification routing and the health profile are not identity, and the wider configuration surface
is `PATCH /api/record-settings/{family}`, which is guardian-fenced and answers only while switched into the
record. Strict, cookie-only and step-up gated, like every act in this family."
InviteManagedProfileGuardianRequest:
type: object
properties:
@@ -28375,10 +28513,15 @@ components:
vaccinations:
type: boolean
additionalProperties: false
required:
- modulePreferences
cycleTrackingEnabled:
type: boolean
additionalProperties: false
description: Partial module toggle. Merged onto the record's current preferences; omitted keys keep their value.
description: "Partial module toggle. `modulePreferences` is merged onto the record's current preferences; omitted keys
keep their value. `cycleTrackingEnabled` is separate because `cycle` is a DELEGATED module: its state lives in
the record's own cycle profile and `modulePreferences.cycle` is refused, so this is the field that turns cycle
tracking on or off for the record — an explicit boolean overrides the default the record's sex would derive.
Both fields are optional and at least one is required; an empty body is a 422 rather than a save that changed
nothing."
RecordNotificationSettingsPatch:
type: object
properties:
@@ -43832,9 +43975,13 @@ components:
- nutrients
- vaccinations
additionalProperties: false
description: Fully-resolved per-user module enable/disable map. Every toggleable module key is present. `false` means
the surface should disappear end-to-end (nav, dashboard, insights, …). `cycle` mirrors `cycleTrackingEnabled`;
`coach` mirrors the resolved per-user opt-out + operator master flag.
description: Fully-resolved module enable/disable map. Every toggleable module key is present. `false` means the surface
should disappear end-to-end (nav, dashboard, insights, …). `cycle` mirrors `cycleTrackingEnabled`; `coach`
mirrors the resolved per-user opt-out + operator master flag. On `GET /api/auth/me` it answers for the RECORD
the session is inside rather than for the caller, because every surface it gates shows the record's data, and it
is masked to the sections the active grant opens — a module outside them reads `false` rather than the record's
true state. With no switch — which is every native request, since the Bearer transport carries none — the two
are the same account, nothing is masked and the payload is unchanged.
PatchModulesResponse:
type: object
properties:
@@ -65735,9 +65882,13 @@ components:
- recordSession
additionalProperties: {}
description: "The signed-in account: its identity, its preferences, and (since v1.36.0) what account sharing lets it do.
Additional properties are the preference fields this spec does not yet enumerate. Under an active switch this
payload still describes the CALLER — their preferences, their modules, their identity — because display
preferences belong to the person at the keyboard rather than to the record they are reading."
Additional properties are the preference fields this spec does not yet enumerate. Under an active switch the
identity and preference fields still describe the CALLER, because display preferences belong to the person at
the keyboard rather than to the record they are reading. Two fields are the exception (v1.38.14): `modules` and
`cycleTrackingEnabled` describe the ACTIVE RECORD, since every surface they gate shows the record's data — and
they are masked to the sections the active grant opens, so a scoped grant reads `false` for a module outside it
rather than the record's true state. With no switch — which is every native request, since the Bearer transport
carries none — the two are the same account and nothing is masked."
AccountAccess:
type: object
properties:
@@ -66360,13 +66511,31 @@ components:
description: The profile's account id — the value `POST /api/account/switch` and the `X-HealthLog-Account` selector
take, and the `{id}` in this family's paths.
displayName:
type: string
anyOf:
- type: string
- type: "null"
dateOfBirth:
anyOf:
- type: string
- type: "null"
description: "`YYYY-MM-DD`, or null. The same date-only shape the request takes — this is a calendar date, not an
instant, and publishing it with a time would invite a client to apply a zone to it."
gender:
anyOf:
- type: string
enum:
- MALE
- FEMALE
- OTHER
- type: "null"
description: "`User.gender` for this record. Null means not recorded, which is a real answer and not a missing one."
locale:
type: string
anyOf:
- type: string
- type: "null"
description: The record's own language. Every profile this family creates carries one, because creation requires it; the
column is nullable, so null means a row written before that and a client renders its own language rather
than the word.
timezone:
type: string
recordKind:
@@ -66378,15 +66547,16 @@ components:
- id
- displayName
- dateOfBirth
- gender
- locale
- timezone
- recordKind
additionalProperties: false
description: "A record somebody looks after, as its creation answers with it. There is deliberately no list endpoint:
the profiles a Guardian looks after are the `managed` entries of `accountAccess.accounts` on GET /api/auth/me,
because a Guardian's relationship to a profile IS a MANAGE grant. Re-read the account payload after any change
here; a client that refreshed only its own panel would leave the new record out of the switcher and the banner
until the next boot."
description: "A record somebody looks after, as its creation, its read and its edit all answer with it. There is
deliberately no list endpoint: the profiles a Guardian looks after are the `managed` entries of
`accountAccess.accounts` on GET /api/auth/me, because a Guardian's relationship to a profile IS a MANAGE grant.
Re-read the account payload after any change here; a client that refreshed only its own panel would leave the
new record out of the switcher and the banner until the next boot."
DeletedManagedProfileEnvelope:
type: object
properties: