mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
Merge branch 'main' into fix/v1396-small-fixes
This commit is contained in:
39 files changed
+2981
-155
No files matched your search
+7
-1
@@ -66,7 +66,13 @@ before treating a warning as a regression.
|
||||
`health:write`), audience-bound to `/mcp` and refused on REST writes;
|
||||
- `POST /api/tokens/measurements` mints `measurements:write`, accepted by
|
||||
`POST /api/measurements` and `POST /api/measurements/batch` only, on the
|
||||
holder's own record only.
|
||||
holder's own record only;
|
||||
- `POST /api/tokens/workouts` mints `workouts:write`, accepted by
|
||||
`POST /api/workouts/batch` only, on the holder's own record only; the
|
||||
workouts it writes carry `source: EXTERNAL`;
|
||||
- `POST /api/tokens/documents` mints `documents:write`, accepted by
|
||||
`POST /api/documents/inbound` and `GET /api/documents/inbound/source`
|
||||
only.
|
||||
|
||||
`POST /api/tokens` is **not** among them — that generic mint was removed in
|
||||
v1.30.17. The path still lists (`GET`) and revokes (`DELETE /api/tokens/{id}`)
|
||||
|
||||
+175
-18
@@ -2134,10 +2134,10 @@ paths:
|
||||
description: >-
|
||||
Stamps the calling browser session as recently re-proved, so for the next five minutes it may run the actions
|
||||
that ask for a fresh proof: GET /api/export/full-backup, POST /api/export/encrypted, POST /api/share-links, POST
|
||||
/api/mcp/tokens, POST /api/tokens/measurements, POST /api/tokens/documents, and the admin backup download,
|
||||
upload and restore, the admin data wipe and the admin password reset. Those answer 401 `auth.reproof.required`
|
||||
with `meta.methods` (the proofs this account can give) when the session signed in or re-proved more than five
|
||||
minutes ago.
|
||||
/api/mcp/tokens, POST /api/tokens/measurements, POST /api/tokens/workouts, POST /api/tokens/documents, and the
|
||||
admin backup download, upload and restore, the admin data wipe and the admin password reset. Those answer 401
|
||||
`auth.reproof.required` with `meta.methods` (the proofs this account can give) when the session signed in or
|
||||
re-proved more than five minutes ago.
|
||||
|
||||
|
||||
The body takes the same shapes as POST /api/auth/step-up. On an account with a second factor only `totp`,
|
||||
@@ -2977,10 +2977,10 @@ paths:
|
||||
because that scope reached no ingest route while the pre-fail-closed default let it reach everything else. The
|
||||
credentials that work are minted where they are scoped: the per-medication API-endpoint toggle, `POST
|
||||
/api/mcp/tokens` for a connector, the sibling `POST /api/tokens/measurements` for third-party measurement
|
||||
ingest, and `POST /api/tokens/documents` for document upload from another system. And unlike the revoke, this
|
||||
read is NOT gated on the operator's instance-wide API switch: that switch governs the surfaces a token is for,
|
||||
not a token's ability to authenticate, so tokens stay live while it is off and their owner has to be able to see
|
||||
them.
|
||||
ingest, `POST /api/tokens/workouts` for workout ingest from a bridge, and `POST /api/tokens/documents` for
|
||||
document upload from another system. And unlike the revoke, this read is NOT gated on the operator's
|
||||
instance-wide API switch: that switch governs the surfaces a token is for, not a token's ability to
|
||||
authenticate, so tokens stay live while it is off and their owner has to be able to see them.
|
||||
responses:
|
||||
"200":
|
||||
description: The caller's tokens, newest first.
|
||||
@@ -3088,6 +3088,93 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
/api/tokens/workouts:
|
||||
post:
|
||||
tags:
|
||||
- Auth
|
||||
summary: Mint a workout-ingest token
|
||||
description: >-
|
||||
Mints a Bearer scoped to exactly `workouts:write` and audits the mint. THE RESPONSE CARRIES THE RAW TOKEN, once;
|
||||
it is stored as an HMAC and no path re-reveals it.
|
||||
|
||||
|
||||
**What it can do.** `POST /api/workouts/batch`, on its owner's own record, and nothing else. It is the door a
|
||||
workout bridge pushes through: a watch vendor's sync relayed by a small service, or a script replaying an
|
||||
exported history. Workouts it writes carry `source: EXTERNAL`, resolved from the credential rather than taken
|
||||
from the body, and a batch in which any entry names a source is refused 422
|
||||
(`workout.batch.source_not_permitted`) rather than relabelled. `EXTERNAL` is on no default source ladder, so
|
||||
when the phone also recorded the same session the phone's copy is the one the workout list shows.
|
||||
|
||||
|
||||
**What it cannot do.** List or read a workout, reach the measurement or document ingest routes, act on a shared
|
||||
record, or mint another token. It is a separate scope from `measurements:write` on purpose: a token already
|
||||
handed to a scale does not start accepting workouts, and a `measurements:write` token is refused 403 on the
|
||||
workout batch. Every other route names no scope and refuses it 403.
|
||||
|
||||
|
||||
Minting requires a COOKIE SESSION with a fresh proof, as for the measurement mint, for the reason it gives: a
|
||||
day-lived native access token must not be able to leave behind a year-lived credential. Gated by the operator's
|
||||
instance-wide API switch. Body capped at 16 KiB; 10 mints per user per minute, and at most 10 live workout
|
||||
tokens held at once. Tokens appear in `GET /api/tokens` and are revoked at `DELETE /api/tokens/{id}`.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CreateWorkoutTokenRequest"
|
||||
responses:
|
||||
"201":
|
||||
description: Token minted. The `token` field is the only copy of the secret.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CreateWorkoutTokenEnvelope"
|
||||
"400":
|
||||
description: Body is not parseable JSON.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"401": *a1
|
||||
"403":
|
||||
description: The operator has switched the API off instance-wide. Nothing was minted. A Bearer caller is refused 401
|
||||
before the switch is read.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"409":
|
||||
description: The account already holds 10 live workout tokens (`meta.errorCode` = `tokens.workouts.ceiling_reached`).
|
||||
Nothing was minted. Revoke one at `DELETE /api/tokens/{id}`; only live tokens carrying `workouts:write`
|
||||
count.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"413":
|
||||
description: Body exceeds 16 KiB.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"415":
|
||||
description: Content-Type is not `application/json`.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"422":
|
||||
description: Validation failed. The envelope carries every offending issue.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"429":
|
||||
description: More than 10 mints from this account in a minute. Nothing was minted.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
/api/tokens/documents:
|
||||
post:
|
||||
tags:
|
||||
@@ -4579,16 +4666,26 @@ paths:
|
||||
tags:
|
||||
- Measurements
|
||||
summary: Typed workout batch ingest (v1.4.25 W16b)
|
||||
description: 'Server-side ingest endpoint for HKWorkout records (iOS) and Withings activity rows (server-to-server). Up
|
||||
to 100 workouts per call; nested route geometry (GeoJSON LineString) is capped at 20 000 points and stored in a
|
||||
1:1 `WorkoutRoute` row keyed by `workoutId`. Idempotent via the `Idempotency-Key` header (replay window 24h).
|
||||
description: >-
|
||||
Server-side ingest endpoint for HKWorkout records (iOS) and Withings activity rows (server-to-server). Up to 100
|
||||
workouts per call; nested route geometry (GeoJSON LineString) is capped at 20 000 points and stored in a 1:1
|
||||
`WorkoutRoute` row keyed by `workoutId`. Idempotent via the `Idempotency-Key` header (replay window 24h).
|
||||
Per-entry status (`inserted | duplicate | enriched | skipped`) lets the iOS sync cursor checkpoint accurately.
|
||||
An entry matching a stored workout is first-write-wins for the workout row; when it carries a `samples` array
|
||||
and the stored workout has no heart-rate series, the series is attached and the entry reports `enriched`
|
||||
(repeatable: a workout that already has a series is a no-op). An entry whose `samples` array fails validation is
|
||||
refused on its own with `reason: "invalid_samples"` — the rest of the batch still lands. The request body
|
||||
ceiling is 5 MB enforced at the HTTP layer — clients above the ceiling receive a 413 with
|
||||
`workout.batch.payload_too_large`.'
|
||||
`workout.batch.payload_too_large`.
|
||||
|
||||
|
||||
**Narrow token.** Also reachable with a `workouts:write` Bearer (minted at `POST /api/tokens/workouts`), the
|
||||
supported way to push workouts from a bridge that is not the phone. This route is the scope's only one. Such a
|
||||
credential writes only on its owner's own record; its workouts are attributed `EXTERNAL`, resolved from the
|
||||
credential, and it may not name a source itself, so a batch in which any entry carries one, `MANUAL` and
|
||||
`APPLE_HEALTH` alike, is refused 422 (`workout.batch.source_not_permitted`) and nothing is written. `EXTERNAL`
|
||||
gives those writes their own `(userId, source, externalId)` namespace, so a bridge's ids can never collide with
|
||||
the phone's. Every other narrow token, `measurements:write` included, is refused 403.
|
||||
parameters:
|
||||
- *a6
|
||||
requestBody:
|
||||
@@ -4618,7 +4715,14 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"422": *a4
|
||||
"422":
|
||||
description: A narrow `workouts:write` credential sent a batch in which an entry names a source, any source
|
||||
(`workout.batch.source_not_permitted`). Nothing was written; resending the batch unchanged is refused the
|
||||
same way. Omit `source` and the server attributes the rows `EXTERNAL`.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"429": *a5
|
||||
/api/devices:
|
||||
post:
|
||||
@@ -22919,6 +23023,21 @@ components:
|
||||
permission array is a literal, so no request can widen it. `name` is what the token is listed under — name it
|
||||
after the thing you are pasting it into, because that is what you will be reading when you decide whether to
|
||||
revoke it. `expiresInDays` defaults to 365."
|
||||
CreateWorkoutTokenRequest:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 100
|
||||
expiresInDays:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 365
|
||||
required:
|
||||
- name
|
||||
description: "Mint a workout-ingest Bearer. No `scope` field: the endpoint mints one shape. `name` is what the token is
|
||||
listed under; name it after the bridge it is pasted into. `expiresInDays` defaults to 365."
|
||||
CreateDocumentTokenRequest:
|
||||
type: object
|
||||
properties:
|
||||
@@ -31636,10 +31755,11 @@ components:
|
||||
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; tokens — tokens.documents.ceiling_reached,
|
||||
tokens.measurements.ceiling_reached; vaccination — vaccination.booster-invalid,
|
||||
vaccination.booster-no-antigen, vaccination.booster-out-of-scope, 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,
|
||||
tokens.measurements.ceiling_reached, tokens.workouts.ceiling_reached; vaccination —
|
||||
vaccination.booster-invalid, vaccination.booster-no-antigen, vaccination.booster-out-of-scope,
|
||||
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; (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,
|
||||
@@ -39650,6 +39770,42 @@ components:
|
||||
- expiresAt
|
||||
additionalProperties: false
|
||||
description: The one response that carries a usable measurement-ingest Bearer value.
|
||||
CreateWorkoutTokenEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/CreateWorkoutTokenResponse"
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
CreateWorkoutTokenResponse:
|
||||
type: object
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
description: The raw `hlk_` Bearer, RETURNED EXACTLY ONCE. It is stored only as an HMAC-SHA256 hash and cannot be
|
||||
retrieved later. Treat it as a secret.
|
||||
name:
|
||||
type: string
|
||||
expiresAt:
|
||||
type: string
|
||||
format: date-time
|
||||
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])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
|
||||
required:
|
||||
- token
|
||||
- name
|
||||
- expiresAt
|
||||
additionalProperties: false
|
||||
description: The one response that carries a usable workout-ingest Bearer value.
|
||||
CreateDocumentTokenEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
@@ -40443,7 +40599,8 @@ components:
|
||||
- EXTERNAL
|
||||
description: Origin of the measurement. v1.4.23 added APPLE_HEALTH for the iOS HealthKit batch ingest path. v1.38.x
|
||||
added EXTERNAL for rows pushed in under a narrow `measurements:write` Bearer — a scale, a watch bridge, a
|
||||
home-automation rule. No client may name that value on a write; the server resolves it from the credential.
|
||||
home-automation rule — and workouts pushed in under a narrow `workouts:write` Bearer carry it too. No client may
|
||||
name that value on a write; the server resolves it from the credential.
|
||||
SyncMeasurementTombstone:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
Reference in new issue
Block a user