mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
Merge branch 'fix/step-up-and-session-revocation' into release/v1.39.3
# Conflicts: # messages/de.json # messages/en.json # messages/es.json # messages/fr.json # messages/it.json # messages/ko.json # messages/pl.json
This commit is contained in:
commit
03ccb21227
96 files changed
+3238
-495
No files matched your search
+205
-30
@@ -917,7 +917,11 @@ paths:
|
||||
`INSURANCE` leaf is refused with a 422 because the share view never decrypts the insurance number. An optional
|
||||
`documentIds` array (bounded ≤50) is validated as the caller's own live documents and frozen onto the link; a
|
||||
documents-only share (no report scope, non-empty `documentIds`) is valid. `expiresAt` is required and capped at
|
||||
90 days. Auth via cookie or Bearer; rate-limited (`share-link:<userId>`, 20/h). Strict: unknown keys 422."
|
||||
90 days. Auth via cookie or Bearer; rate-limited (`share-link:<userId>`, 20/h). Strict: unknown keys 422. A
|
||||
browser session needs a fresh proof as well as a live one: a sign-in or `POST /api/auth/reproof` within the last
|
||||
five minutes (with a second factor, on an account that has one). Without it the answer is 401
|
||||
`auth.reproof.required`, with `meta.methods` naming the proofs the account can give. A Bearer caller is not
|
||||
asked for more than its token yet."
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -1587,7 +1591,11 @@ paths:
|
||||
`Content-Disposition: attachment` with `healthlog-backup-<userId>-<YYYY-MM-DD>.json`, `Cache-Control: no-store`.
|
||||
Shares the `export:<userId>` bucket (10 per hour) with every other export route. Audits as
|
||||
`user.export.full-backup` with the row counts. Cookie or Bearer auth; the caller is always resolved as
|
||||
themselves, so this download cannot be delegated to a shared record."
|
||||
themselves, so this download cannot be delegated to a shared record. A browser session needs a fresh proof as
|
||||
well as a live one: a sign-in or `POST /api/auth/reproof` within the last five minutes (with a second factor, on
|
||||
an account that has one). Without it the answer is 401 `auth.reproof.required`, with `meta.methods` naming the
|
||||
proofs the account can give. A Bearer caller presents an `X-Step-Up` elevation from `POST /api/auth/step-up` (a
|
||||
second-factor one on an account with a second factor) and receives 401 `auth.stepup.required` without it."
|
||||
responses:
|
||||
"200":
|
||||
description: The backup payload. `application/json; charset=utf-8`, served as an attachment.
|
||||
@@ -1788,12 +1796,15 @@ paths:
|
||||
tags:
|
||||
- Export
|
||||
summary: Download the full backup as a passphrase-encrypted archive
|
||||
description: v1.23. Same restore-compatible payload as the plaintext full backup, sealed into an `HLX1` archive
|
||||
description: "v1.23. Same restore-compatible payload as the plaintext full backup, sealed into an `HLX1` archive
|
||||
(Argon2id-derived key + AES-256-GCM) under the supplied `passphrase`. Returns `application/octet-stream` (a
|
||||
`.hlx` file). When the account has a second factor enrolled, the call is step-up gated (`requireFreshMfa`,
|
||||
cookie-only) and returns 401 `auth.stepup.required` without a fresh factor; single-factor accounts use a normal
|
||||
session or Bearer. Shared `export:<userId>` rate bucket (10/h). The passphrase is never stored — a forgotten
|
||||
passphrase makes the archive unrecoverable.
|
||||
`.hlx` file). Asks for a fresh proof, like the plaintext full backup. A browser session needs a fresh proof as
|
||||
well as a live one: a sign-in or `POST /api/auth/reproof` within the last five minutes (with a second factor, on
|
||||
an account that has one). Without it the answer is 401 `auth.reproof.required`, with `meta.methods` naming the
|
||||
proofs the account can give. On Bearer, an account without a second factor passes on its token, and one with a
|
||||
second factor presents a second-factor `X-Step-Up` elevation (401 `auth.stepup.required` without it). Shared
|
||||
`export:<userId>` rate bucket (10/h). The passphrase is never stored — a forgotten passphrase makes the archive
|
||||
unrecoverable."
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -2063,9 +2074,12 @@ paths:
|
||||
Accepting routes (the complete set): POST /api/auth/me/mfa/totp/setup; POST /api/auth/me/mfa/totp/confirm; POST
|
||||
/api/auth/me/mfa/disable; POST /api/auth/me/mfa/recovery-codes/regenerate; POST
|
||||
/api/auth/me/mfa/webauthn/register/options; POST /api/auth/me/mfa/webauthn/register/verify; PATCH and DELETE
|
||||
/api/auth/me/mfa/webauthn/{id}; DELETE /api/settings/account; DELETE /api/settings/data. The last three MFA
|
||||
routes, disable, and both erasure routes require a fresh-factor proof. GET /api/auth/me/mfa needs no elevation
|
||||
at all. Nothing else accepts one — admin endpoints stay cookie-only.
|
||||
/api/auth/me/mfa/webauthn/{id}; DELETE /api/settings/account; DELETE /api/settings/data; GET
|
||||
/api/export/full-backup; POST /api/export/encrypted. The last three MFA routes, disable, and both erasure routes
|
||||
require a fresh-factor proof. The two whole-record exports take any elevation on an account without a second
|
||||
factor and a fresh-factor one on an account with one; the full backup requires an elevation on every Bearer
|
||||
call, the encrypted export only on an account with a second factor. GET /api/auth/me/mfa needs no elevation at
|
||||
all. Nothing else accepts one — admin endpoints stay cookie-only.
|
||||
|
||||
|
||||
The two erasure routes joined the set so an account with a second factor can be deleted from a native app: their
|
||||
@@ -2088,6 +2102,64 @@ paths:
|
||||
"401": *a1
|
||||
"422": *a4
|
||||
"429": *a5
|
||||
/api/auth/reproof:
|
||||
post:
|
||||
tags:
|
||||
- Auth
|
||||
summary: Re-prove a credential on a browser session (cookie only)
|
||||
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.
|
||||
|
||||
|
||||
The body takes the same shapes as POST /api/auth/step-up. On an account with a second factor only `totp`,
|
||||
`webauthn` (a security key) or `passkey` are accepted, because a password has never stood in for the second
|
||||
factor; on one without, `password` or `passkey`. A method the account cannot use here is refused with 422
|
||||
`auth.reproof.too_weak` and `meta.methods`, before any guess is spent. A second-factor or passkey proof also
|
||||
refreshes the session's second-factor stamp, exactly as signing in again would. Assertions begin at POST
|
||||
/api/auth/passkey/register-options with `{ method: "passkey" | "webauthn" }`.
|
||||
|
||||
|
||||
Proofs draw on the account's shared re-proof budget (five per fifteen minutes, 429 once spent). A proof that
|
||||
does not verify is 401 `auth.reproof.failed` and is audited. Bearer callers use POST /api/auth/step-up instead.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/StepUpMintRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Proof accepted; the session is stamped.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ReproofEnvelope"
|
||||
"401": *a1
|
||||
"422": *a4
|
||||
"429": *a5
|
||||
get:
|
||||
tags:
|
||||
- Auth
|
||||
summary: Ask whether this browser session's proof is recent
|
||||
description: "The question the gated routes ask, without acting: 200 `{ recent: true }` when the session signed in or
|
||||
re-proved within five minutes (a second factor on an account that has one), otherwise 401
|
||||
`auth.reproof.required` with `meta.methods`. Lets a client re-prove before sending a large upload rather than
|
||||
after it was refused."
|
||||
responses:
|
||||
"200":
|
||||
description: The session's proof is recent.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ReproofStatusEnvelope"
|
||||
"401": *a1
|
||||
"422": *a4
|
||||
"429": *a5
|
||||
/api/auth/me/mfa/totp/setup:
|
||||
post:
|
||||
tags:
|
||||
@@ -2523,8 +2595,21 @@ paths:
|
||||
tags:
|
||||
- Auth
|
||||
summary: Sign out everywhere else
|
||||
description: v1.23 — revokes every OTHER web session plus all native refresh tokens, keeping the caller's current
|
||||
session. API tokens are not touched (manage those under /settings/api-tokens).
|
||||
description: "Ends every credential except the caller's own: every OTHER web session, every other device login (refresh
|
||||
token and its paired access token), every trusted device and step-up elevation, every connected AI assistant
|
||||
(the OAuth connection, so it cannot mint a new access token), every programmatic token (connector, measurement
|
||||
and document tokens), and every clinician share link. Share links are revoked by default because a link made by
|
||||
whoever held a lost session or device would otherwise keep opening the record; send `keepShareLinks=1` to keep
|
||||
them. A Bearer caller keeps its own device login."
|
||||
parameters:
|
||||
- in: query
|
||||
name: keepShareLinks
|
||||
schema:
|
||||
description: "`1` keeps the clinician share links. Anything else, or nothing, revokes them."
|
||||
type: string
|
||||
enum:
|
||||
- "1"
|
||||
description: "`1` keeps the clinician share links. Anything else, or nothing, revokes them."
|
||||
responses:
|
||||
"200":
|
||||
description: Other sessions revoked.
|
||||
@@ -2898,12 +2983,14 @@ paths:
|
||||
holder actually has. And it cannot mint another token, this endpoint included.
|
||||
|
||||
|
||||
Minting requires a COOKIE SESSION. No Bearer credential reaches this endpoint at any scope, wildcard included —
|
||||
not because a wildcard lacks the reach, but because of the lifetimes involved: a native access token lives a day
|
||||
and what it could mint here lives a year, so admitting one would let a short-lived compromise leave behind a
|
||||
credential that outlives revoking it. 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 tokens held at once. Tokens appear in `GET /api/tokens` and
|
||||
are revoked at `DELETE /api/tokens/{id}` like any other — revoking frees a slot against the ceiling.
|
||||
Minting requires a COOKIE SESSION with a fresh proof: a sign-in or `POST /api/auth/reproof` within the last five
|
||||
minutes (with a second factor, on an account that has one), otherwise 401 `auth.reproof.required` with
|
||||
`meta.methods`. No Bearer credential reaches this endpoint at any scope, wildcard included — not because a
|
||||
wildcard lacks the reach, but because of the lifetimes involved: a native access token lives a day and what it
|
||||
could mint here lives a year, so admitting one would let a short-lived compromise leave behind a credential that
|
||||
outlives revoking it. 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 tokens held at once. Tokens appear in `GET /api/tokens` and are revoked at
|
||||
`DELETE /api/tokens/{id}` like any other — revoking frees a slot against the ceiling.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -2990,10 +3077,10 @@ paths:
|
||||
it 403.
|
||||
|
||||
|
||||
Minting requires a COOKIE SESSION, for the reason the measurement mint 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 document tokens held at once. Tokens appear
|
||||
in `GET /api/tokens` and are revoked at `DELETE /api/tokens/{id}`.
|
||||
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 document
|
||||
tokens held at once. Tokens appear in `GET /api/tokens` and are revoked at `DELETE /api/tokens/{id}`.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -20205,9 +20292,12 @@ paths:
|
||||
tags:
|
||||
- MCP
|
||||
summary: Mint an MCP connector token
|
||||
description: Mints a `health:read` (optionally read+write) Bearer for the manual / stdio path and audits the mint. THE
|
||||
description: "Mints a `health:read` (optionally read+write) Bearer for the manual / stdio path and audits the mint. THE
|
||||
RESPONSE CARRIES THE RAW TOKEN — this is the only place it ever exists, and it cannot be retrieved again. Body
|
||||
capped at 16 KiB. Auth via cookie or Bearer.
|
||||
capped at 16 KiB. Auth via cookie or Bearer. A browser session needs a fresh proof as well as a live one: a
|
||||
sign-in or `POST /api/auth/reproof` within the last five minutes (with a second factor, on an account that has
|
||||
one). Without it the answer is 401 `auth.reproof.required`, with `meta.methods` naming the proofs the account
|
||||
can give. A Bearer caller is not asked for more than its token yet."
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -20892,7 +20982,9 @@ paths:
|
||||
description: "Decrypts one stored copy and returns the backup document itself, with `Content-Disposition: attachment`
|
||||
and `Cache-Control: no-store`. The response is the raw document, not the standard envelope — it is the artefact
|
||||
an operator keeps. The ciphertext stays in the database. Admin session cookie required; Bearer tokens cannot
|
||||
reach admin endpoints."
|
||||
reach admin endpoints. A browser session needs a fresh proof as well as a live one: a sign-in or `POST
|
||||
/api/auth/reproof` within the last five minutes (with a second factor, on an account that has one). Without it
|
||||
the answer is 401 `auth.reproof.required`, with `meta.methods` naming the proofs the account can give."
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@@ -20956,7 +21048,10 @@ paths:
|
||||
snapshot was taken for, never the admin running it. The request refuses a missing confirmation, an unknown
|
||||
backup and a copy that cannot be decrypted; the file's own checks (schema, declared owner, manifest, documents)
|
||||
run in the job before anything is deleted, and a refusal there ends the job as `failed` with the same reason.
|
||||
One restore per account at a time. Admin session cookie required; Bearer tokens cannot reach admin endpoints."
|
||||
One restore per account at a time. Admin session cookie required; Bearer tokens cannot reach admin endpoints. A
|
||||
browser session needs a fresh proof as well as a live one: a sign-in or `POST /api/auth/reproof` within the last
|
||||
five minutes (with a second factor, on an account that has one). Without it the answer is 401
|
||||
`auth.reproof.required`, with `meta.methods` naming the proofs the account can give."
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
@@ -31293,10 +31388,10 @@ components:
|
||||
analytics.invalid_query; anamnesis — anamnesis.fact.conflict, anamnesis.fact.currentExists,
|
||||
anamnesis.fact.invalidValue; auth — auth.admin.required, auth.already_authenticated,
|
||||
auth.mfa.code_invalid, auth.missing, auth.refresh.invalid, auth.refresh.reuse, auth.refresh.revoked,
|
||||
auth.reproof.failed, auth.reproof.required, auth.scope.insufficient, auth.stepup.required,
|
||||
auth.token.expired, auth.token.invalid; backup — backup.payload.undecryptable, backup.restore.active,
|
||||
backup.section.missing, backup.upload.too_large; body-site — body-site.invalid; coach —
|
||||
coach.budget.exceeded, coach.conversation.invalidTitle, coach.conversation.notFound,
|
||||
auth.reproof.failed, auth.reproof.required, auth.reproof.too_weak, auth.scope.insufficient,
|
||||
auth.stepup.required, auth.token.expired, auth.token.invalid; backup — backup.payload.undecryptable,
|
||||
backup.restore.active, backup.section.missing, backup.upload.too_large; body-site — body-site.invalid;
|
||||
coach — coach.budget.exceeded, coach.conversation.invalidTitle, coach.conversation.notFound,
|
||||
coach.fenced.attachmentConflict, coach.fenced.attachmentLimit, coach.fenced.attachmentNotFound,
|
||||
coach.fenced.attachmentNotIndexed, coach.fenced.attachmentRequired, coach.fenced.attachmentUnavailable,
|
||||
coach.fenced.invalid, coach.request.invalid; coach-prefs — coach-prefs.body.invalid_json,
|
||||
@@ -38292,6 +38387,67 @@ components:
|
||||
- method
|
||||
- satisfiesFreshFactor
|
||||
additionalProperties: false
|
||||
ReproofEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/ReproofResponse"
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
ReproofResponse:
|
||||
type: object
|
||||
properties:
|
||||
method:
|
||||
type: string
|
||||
enum:
|
||||
- password
|
||||
- totp
|
||||
- webauthn
|
||||
- passkey
|
||||
verifiedAt:
|
||||
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))$
|
||||
required:
|
||||
- method
|
||||
- verifiedAt
|
||||
additionalProperties: false
|
||||
ReproofStatusEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/ReproofStatus"
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
ReproofStatus:
|
||||
type: object
|
||||
properties:
|
||||
recent:
|
||||
type: boolean
|
||||
const: true
|
||||
required:
|
||||
- recent
|
||||
additionalProperties: false
|
||||
TotpSetupEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
@@ -38798,8 +38954,27 @@ components:
|
||||
minimum: -9007199254740991
|
||||
maximum: 9007199254740991
|
||||
description: Number of OTHER sessions removed (the current one is kept).
|
||||
accessTokensRevoked:
|
||||
type: integer
|
||||
minimum: -9007199254740991
|
||||
maximum: 9007199254740991
|
||||
description: "Tokens revoked: the access tokens of other device logins plus every programmatic token (connector,
|
||||
measurement, document), never the caller's own."
|
||||
connectorsRevoked:
|
||||
type: integer
|
||||
minimum: -9007199254740991
|
||||
maximum: 9007199254740991
|
||||
description: Connected AI-assistant (MCP OAuth) connections revoked.
|
||||
shareLinksRevoked:
|
||||
type: integer
|
||||
minimum: -9007199254740991
|
||||
maximum: 9007199254740991
|
||||
description: Clinician share links revoked; 0 when `keepShareLinks=1` was sent.
|
||||
required:
|
||||
- sessionsRevoked
|
||||
- accessTokensRevoked
|
||||
- connectorsRevoked
|
||||
- shareLinksRevoked
|
||||
additionalProperties: false
|
||||
SessionRevokeEnvelope:
|
||||
type: object
|
||||
|
||||
Reference in new issue
Block a user