Merge the release with the security fixes into the backup health work

This commit is contained in:
Marc-André Bombeck committed 2026-10-25 15:03:17 +01:00
commit da9d941a24
99 files changed
+3700 -1073

No files matched your search

+40 -30
View File
@@ -24863,9 +24863,9 @@ paths:
**Auth is Bearer only.** A cookie session does not authenticate here: the route reads the `Authorization` header
itself, hashes it and looks the token up, so a browser with a valid login and no token gets 401. The token must
carry either the `*` wildcard or the `medication:ingest` scope — AND, once the named medication resolves, either
`*` or that medication's own `medication:{id}:ingest` scope. The second check runs after the lookup, which is
why a token scoped to one medication gets 404 for a name it may not touch only when the name does not exist, and
403 when it does.
`*` or that medication's own `medication:{id}:ingest` scope. The second check runs after the lookup. A token
scoped to one medication gets the same 403 for a name that does not exist as for one outside its scope, so it
cannot be used to learn which medications exist; only a `*` token gets 404 for an unknown name.
**The operator can switch this off.** With the global API kill switch disabled every request is refused with 403
@@ -24873,8 +24873,9 @@ paths:
**Replay is by `idempotencyKey`, and the status says which happened.** A key already stored for this token's
owner returns the existing row with **200**; a fresh dose returns **201**. Nothing else distinguishes them —
same body shape, same fields — so branch on the status code.
owner and the same medication returns the existing row with **200**; a fresh dose returns **201**; a key already
used for another medication returns **409** without its row. Replay is answered only after both scope checks.
Nothing else distinguishes them — same body shape, same fields — so branch on the status code.
**A dose is attributed to its scheduled slot, not stamped ad-hoc.** When the take falls inside a schedule's
@@ -24918,14 +24919,22 @@ paths:
$ref: "#/components/schemas/ErrorEnvelope"
"403":
description: Refused. Either the operator has the global API switch off (the message says so), or the token lacks
`medication:ingest` / `*`, or it lacks the resolved medication's own `medication:{id}:ingest` / `*` scope.
`medication:ingest` / `*`, or it lacks the resolved medication's own `medication:{id}:ingest` / `*` scope. A
token without `*` also gets this answer for a name that matches no active medication.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"404":
description: No ACTIVE medication of the token owner's matches `medicationName`. An inactive medication of the right
name is also a 404 — the name is only searched among active ones.
description: "Only for a `*` token: no ACTIVE medication of the token owner's matches `medicationName`. An inactive
medication of the right name is also a 404 — the name is only searched among active ones."
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"409":
description: The `idempotencyKey` is already stored for a dose of another medication. Nothing was written and that dose
is not returned.
content:
application/json:
schema:
@@ -38583,28 +38592,29 @@ components:
profile_claim.oidc_only_unsupported, profile_claim.sso_unavailable, profile_claim.taken; record_write —
record_write.rate_limited; recovery — recovery.kit.stale; report-selection —
report-selection.body.invalid_json, report-selection.body.invalid_shape,
report-selection.leaves.unknown; route — route.retired; search — search.invalid; security_activity —
security_activity.invalid; share-link — share-link.document.decryptFailed, share-link.document.notFound,
share-link.document.rateLimited, share-link.documents.invalid, share-link.report.notFound,
share-link.report.rateLimited, share-link.selection.forbidden_leaf, share-link.selection.unknown_leaf;
sharing — sharing.accept.expired, sharing.accept.not_found, sharing.accept.not_pending,
sharing.accept.revoked, sharing.access.denied, sharing.invite.duplicate, sharing.invite.invalid,
sharing.invite.invalid_scope, sharing.invite.manage_browser_only, sharing.invite.no_account,
sharing.invite.refused, sharing.invite.self, sharing.not_permitted, sharing.renounce.already_ended,
sharing.renounce.not_found, sharing.revoke.already_ended, sharing.revoke.not_found,
sharing.session.changed, sharing.switch.invalid, sharing.switch.wrong_transport; source-priority —
source-priority.body.invalid_json, source-priority.body.invalid_shape; symptoms —
symptoms.definition.limitReached, symptoms.definition.notFound, symptoms.episode.notOpen,
symptoms.event.notFound, symptoms.invalid; timeline — timeline.invalid; tokens —
tokens.documents.ceiling_reached, tokens.measurements.ceiling_reached,
tokens.mirror.not_measurement_token, tokens.workouts.ceiling_reached; vaccination —
vaccination.booster-invalid, vaccination.booster-no-antigen, vaccination.booster-out-of-scope,
vaccination.custom-vaccine-not-found, vaccination.custom.invalid, vaccination.custom.name-taken,
vaccination.custom.not-found, vaccination.encounter-not-found, vaccination.identity-required,
vaccination.invalid, vaccination.link.invalid, vaccination.not-found,
vaccination.practitioner-not-found; workout — workout.batch.invalid, workout.batch.payload_too_large,
workout.batch.source_not_permitted, workout.batch.too_large, workout.delete.synced_source; (unprefixed)
— about_me_conflict, ai_response_truncated, coach_prefs_conflict, dashboard_layout_conflict, empty_file,
report-selection.leaves.unknown; request — request.cross_site; route — route.retired; search —
search.invalid; security_activity — security_activity.invalid; share-link —
share-link.document.decryptFailed, share-link.document.notFound, share-link.document.rateLimited,
share-link.documents.invalid, share-link.report.notFound, share-link.report.rateLimited,
share-link.selection.forbidden_leaf, share-link.selection.unknown_leaf; sharing —
sharing.accept.expired, sharing.accept.not_found, sharing.accept.not_pending, sharing.accept.revoked,
sharing.access.denied, sharing.invite.duplicate, sharing.invite.invalid, sharing.invite.invalid_scope,
sharing.invite.manage_browser_only, sharing.invite.no_account, sharing.invite.refused,
sharing.invite.self, sharing.not_permitted, sharing.renounce.already_ended, sharing.renounce.not_found,
sharing.revoke.already_ended, sharing.revoke.not_found, sharing.session.changed, sharing.switch.invalid,
sharing.switch.wrong_transport; source-priority — source-priority.body.invalid_json,
source-priority.body.invalid_shape; symptoms — symptoms.definition.limitReached,
symptoms.definition.notFound, symptoms.episode.notOpen, symptoms.event.notFound, symptoms.invalid;
timeline — timeline.invalid; tokens — tokens.documents.ceiling_reached,
tokens.measurements.ceiling_reached, tokens.mirror.not_measurement_token,
tokens.workouts.ceiling_reached; vaccination — vaccination.booster-invalid,
vaccination.booster-no-antigen, vaccination.booster-out-of-scope, vaccination.custom-vaccine-not-found,
vaccination.custom.invalid, vaccination.custom.name-taken, vaccination.custom.not-found,
vaccination.encounter-not-found, vaccination.identity-required, vaccination.invalid,
vaccination.link.invalid, vaccination.not-found, vaccination.practitioner-not-found; workout —
workout.batch.invalid, workout.batch.payload_too_large, workout.batch.source_not_permitted,
workout.batch.too_large, workout.delete.synced_source; (unprefixed) — about_me_conflict,
ai_response_truncated, coach_prefs_conflict, dashboard_layout_conflict, empty_file,
health_score_config_conflict, insights_layout_conflict, invalid_base_updated_at,
json_carries_no_intake_time, json_not_an_array, medication_layout_conflict, missing_required_columns,
modules_conflict, mood_tag_layout_conflict, not_configured, notification_prefs_conflict, oidc_only,