mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
Merge remote-tracking branch 'origin/main' into feat/onboarding-wizard
# Conflicts: # docs/api/openapi.yaml # src/lib/openapi/error-codes.ts
This commit is contained in:
commit
55ff6df205
87 files changed
+2080
-399
No files matched your search
@@ -4,10 +4,11 @@
|
||||
* `meta.errorCode` is the mechanism this API chose for machine-readable errors
|
||||
* — the envelope's own description tells a client to branch on it rather than
|
||||
* on the prose — and until this file existed a client could not enumerate it.
|
||||
* Sixteen of the two hundred and forty-two codes appeared somewhere in
|
||||
* `docs/api/openapi.yaml`; the rest existed only in server source, so the only
|
||||
* way to learn one was to trigger the error in testing and the only way to
|
||||
* learn a new one had appeared was to trigger that too.
|
||||
* When this file was written, sixteen of its two hundred and forty-two codes
|
||||
* appeared somewhere in `docs/api/openapi.yaml`; the rest existed only in
|
||||
* server source, so the only way to learn one was to trigger the error in
|
||||
* testing and the only way to learn a new one had appeared was to trigger
|
||||
* that too.
|
||||
*
|
||||
* The list is generated from the code and then committed as a literal, so it is
|
||||
* reviewable in a diff and stable across a release. `error-code-catalogue.test.ts`
|
||||
@@ -297,6 +298,7 @@ export const ERROR_CODE_CATALOGUE: Readonly<Record<string, readonly string[]>> =
|
||||
vaccination: [
|
||||
"vaccination.booster-invalid",
|
||||
"vaccination.booster-no-antigen",
|
||||
"vaccination.booster-out-of-scope",
|
||||
"vaccination.encounter-not-found",
|
||||
"vaccination.identity-required",
|
||||
"vaccination.invalid",
|
||||
|
||||
@@ -874,7 +874,7 @@ export const cyclePaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
tags: ["Cycle"],
|
||||
summary: "Cycle-phase correlation insights (v1.15.0)",
|
||||
description:
|
||||
"FDR-guarded luteal-vs-follicular phase contrast per outcome metric (RHR / HRV / sleep / steps / weight / temperatures), plus the single headline finding (resting-heart-rate-by-phase, falling back to HRV). The same Welch t-test + Benjamini-Hochberg machinery the mood-factor crosstab runs; only rows with p < 0.05 AND q ≤ 0.10 surface. Strictly gender-gated — phase never appears on the general `/api/insights/correlations` route. Observational only, never causal. Gated: `cycle.disabled` 403 when the feature is off.",
|
||||
"FDR-guarded luteal-vs-follicular phase contrast per outcome metric (RHR / HRV / sleep / steps / weight / temperatures), plus the single headline finding (resting-heart-rate-by-phase, falling back to HRV). The same Welch t-test + Benjamini-Hochberg machinery the mood-factor crosstab runs; only rows with p < 0.05 AND q ≤ 0.10 surface. Strictly gender-gated — phase never appears on the general `/api/insights/correlations` route. Observational only, never causal. Gated: `cycle.disabled` 403 when the feature is off. Declared for the `cycle` section; the outcome columns are fenced per section at the query, so a grant scoped to the cycle alone receives the phase rows with no `lutealAvg` / `follicularAvg` / `delta` for measurement or mood outcomes and no lagged discovery, exactly as a record with nothing logged would read. The owner and a whole-record grant are unchanged.",
|
||||
responses: {
|
||||
"200": {
|
||||
description: "Phase-correlation rows + headline.",
|
||||
|
||||
@@ -20,7 +20,11 @@
|
||||
import { z } from "zod/v4";
|
||||
import type { ZodOpenApiObject } from "zod-openapi";
|
||||
import { backupPayloadSchema } from "@/lib/validations/backup";
|
||||
import { errorEnvelope, stdResponses } from "./shared";
|
||||
import {
|
||||
errorEnvelope,
|
||||
SHARING_NOT_PERMITTED_DESCRIPTION,
|
||||
stdResponses,
|
||||
} from "./shared";
|
||||
|
||||
const exportQuery = z
|
||||
.object({
|
||||
@@ -305,6 +309,12 @@ export const exportPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"403": {
|
||||
description:
|
||||
"The Bearer token carries a narrow scope. The encrypted archive needs a cookie session or a full-access token; the refusal happens before any second-factor proof is looked at.\n\n" +
|
||||
SHARING_NOT_PERMITTED_DESCRIPTION,
|
||||
content: { "application/json": { schema: errorEnvelope } },
|
||||
},
|
||||
...stdResponses,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -135,7 +135,8 @@ export const mentalHealthPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
tags: ["Mental health"],
|
||||
summary: "Record a completed screener",
|
||||
description:
|
||||
"Stores one administration (item answers encrypted) and writes the derived total as a server-owned PHQ9_SCORE / GAD7_SCORE / WHO5_SCORE / SCI_SCORE measurement (source COMPUTED — never client-writable). WHO-5 and SCI totals run HIGHER = better; their follow-up thresholds point downwards (≤ 50 / ≤ 16). On a positive PHQ-9 item-9 the response carries the locale-aware crisis-resource set.",
|
||||
"Stores one administration (item answers encrypted) and writes the derived total as a server-owned PHQ9_SCORE / GAD7_SCORE / WHO5_SCORE / SCI_SCORE measurement (source COMPUTED — never client-writable). WHO-5 and SCI totals run HIGHER = better; their follow-up thresholds point downwards (≤ 50 / ≤ 16). On a positive PHQ-9 item-9 the response carries the locale-aware crisis-resource set.\n\n" +
|
||||
"The projection row is the one write this operation makes outside its declared section: the operation is fenced on `mind`, and the measurement it derives lands in `measurements`. It is deliberate and unconditional — a screener whose own total is missing from the record would be an inconsistency inside the section the grant does open. The consent copy for the mood-and-mind section names it.",
|
||||
parameters: [idempotencyKeyParameter],
|
||||
requestBody: {
|
||||
content: {
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
dataEnvelope,
|
||||
errorEnvelope,
|
||||
recordRefusal,
|
||||
SHARING_NOT_PERMITTED_DESCRIPTION,
|
||||
stdResponses,
|
||||
} from "./shared";
|
||||
|
||||
@@ -615,7 +616,8 @@ export const settingsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
"403": {
|
||||
description:
|
||||
"The Bearer token carries a narrow scope. Erasure needs a cookie session or a full-access token; the refusal happens before any second-factor proof is looked at.",
|
||||
"The Bearer token carries a narrow scope. Erasure needs a cookie session or a full-access token; the refusal happens before any second-factor proof is looked at.\n\n" +
|
||||
SHARING_NOT_PERMITTED_DESCRIPTION,
|
||||
content: { "application/json": { schema: errorEnvelope } },
|
||||
},
|
||||
},
|
||||
@@ -801,7 +803,8 @@ export const settingsPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
},
|
||||
"403": {
|
||||
description:
|
||||
"The Bearer token carries a narrow scope. Erasure needs a cookie session or a full-access token; the refusal happens before any second-factor proof is looked at.",
|
||||
"The Bearer token carries a narrow scope. Erasure needs a cookie session or a full-access token; the refusal happens before any second-factor proof is looked at.\n\n" +
|
||||
SHARING_NOT_PERMITTED_DESCRIPTION,
|
||||
content: { "application/json": { schema: errorEnvelope } },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -431,6 +431,25 @@ export function idempotentWrite(): IdempotentConflictResponse {
|
||||
export const SHARING_ACCESS_DENIED_DESCRIPTION =
|
||||
"Refused: this request named a record the caller may not act on (`meta.errorCode` = `sharing.access.denied`). Byte-identical for an account that does not exist, an account that granted nothing, a grant whose sections do not reach this surface, and a read grant on a request that writes — the response carries no reason, so it is not an account-enumeration oracle. The reason reaches the record owner's activity feed and the operator's audit trail instead. On the cookie transport the same code answers one further case: a session that has entered a shared record and sends a request without its record-context assertion, which is a client older than the fence — leave the record and reload. A request that names no record but the caller's own never reaches this response, so a client that never switches and never sends the per-request account selector will not see it.";
|
||||
|
||||
/**
|
||||
* The OTHER sharing refusal, for a route that names no record at all.
|
||||
*
|
||||
* A route resolving through `requireAuth` serves the caller and only the
|
||||
* caller, and refuses outright while the browser is inside somebody else's
|
||||
* record rather than quietly answering with the caller's own rows. That
|
||||
* posture matters most on the routes where being wrong is unrecoverable — the
|
||||
* record wipe, the account deletion, the encrypted archive — and it was
|
||||
* documented nowhere on them. A client that switches records has to know which
|
||||
* of its calls stop working, and finding out by wiping the wrong record is not
|
||||
* a contract.
|
||||
*
|
||||
* One sentence, spliced onto whatever else the operation's 403 already says,
|
||||
* for the same reason the delegable refusal is one string: a paraphrase per
|
||||
* path invites a client to tell them apart.
|
||||
*/
|
||||
export const SHARING_NOT_PERMITTED_DESCRIPTION =
|
||||
"Refused: the request was made while the session is acting on another account (`meta.errorCode` = `sharing.not_permitted`). This operation resolves the CALLER and never a named record, so under a switch it refuses instead of quietly answering with the caller's own — leave the shared record first. No grant at any level opens it.";
|
||||
|
||||
/**
|
||||
* The 403 above, optionally sharing the status with the reasons a route already
|
||||
* refuses for.
|
||||
|
||||
@@ -364,14 +364,17 @@ export const vaccinationPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
tags: ["Records"],
|
||||
summary: "Plan a booster reminder from a dose",
|
||||
description:
|
||||
"Mints (or re-anchors) the booster reminder a logged dose suggests. The minted row is an ordinary `origin: VORSORGE` measurement reminder carrying the dose's primary antigen as a server-side match key; it lists on the preventive-care surface and rings through the same engine as every other checkup. Re-confirming re-anchors the existing reminder instead of minting a second one: 201 carries a fresh mint (`minted: true`), 200 a re-anchor. A free-text-only dose has no antigen to remind on and is refused with 422. Audits as `vaccination.booster.planned`.",
|
||||
"Mints (or re-anchors) the booster reminder a logged dose suggests. The minted row is an ordinary `origin: VORSORGE` measurement reminder carrying the dose's primary antigen as a server-side match key; it lists on the preventive-care surface and rings through the same engine as every other checkup. Re-confirming re-anchors the existing reminder instead of minting a second one: 201 carries a fresh mint (`minted: true`), 200 a re-anchor. A free-text-only dose has no antigen to remind on and is refused with 422. Audits as `vaccination.booster.planned`.\n\n" +
|
||||
"The operation is declared under the health background and the row it mints belongs to the measurements section, so it crosses a section boundary: a delegate whose grant does not also open `measurements` is refused with 403 `vaccination.booster-out-of-scope` and nothing is written. The dose itself still logs — only the booster does not.",
|
||||
requestParams: idPath,
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: { "application/json": { schema: vaccinationBoosterRequest } },
|
||||
},
|
||||
responses: {
|
||||
...recordRefusal(),
|
||||
...recordRefusal(
|
||||
"Refused: the caller's grant does not open this record's measurements, and the booster reminder lives there (`meta.errorCode` = `vaccination.booster-out-of-scope`). Nothing was written.",
|
||||
),
|
||||
"200": {
|
||||
description: "Existing booster reminder re-anchored.",
|
||||
content: {
|
||||
|
||||
Reference in new issue
Block a user