feat(consent): mint AI consent receipt from the web client

The server-side gate requires an active ConsentReceipt before any health
snapshot egresses via the operator's server-managed key, but the web
client never minted one — so web users on a shared-key deployment hit the
fail-closed no-key fallback on every AI surface.

Add ensureWebAiConsentReceipt() and POST /api/consent/ai/web: an
idempotent ai_full grant mirroring the iOS master grant (a user with an
active receipt is a no-op). The AI-settings card calls it on mount — the
web equivalent of the iOS shell-mount heal — so existing accounts gain a
receipt without a re-consent step.

Surface an honest signal: resolveReadOnlyStatusMiss now returns a
consent-missing outcome when a provider is configured but the gate would
block egress, distinct from no-provider, and skips the wasted enqueue.

Document the receipt precondition + the consent surface in the OpenAPI
contract (it shipped without a bump): the /api/consent/ai family, a
Consent tag, and a 403 consent.ai.required response on the AI-egress
route.
This commit is contained in:
Marc-André Bombeck committed 2026-06-14 10:25:18 +02:00
1 parent 7fa10b43a0
commit f9876f9492
19 files changed
+563 -12

No files matched your search

+181 -1
View File
@@ -32,6 +32,7 @@ tags:
- name: Mood
- name: Analytics
- name: Insights
- name: Consent
- name: Dashboard
- name: Notifications
- name: Devices
@@ -3979,7 +3980,8 @@ paths:
- Insights
summary: Comprehensive AI insights bundle
description: Full Insights surface — daily briefing, recommendations with rationale, optional weekly report + storyboard
annotations. Strict-schema validated server-side.
annotations. Strict-schema validated server-side. Requires an active ConsentReceipt when the resolved provider
chain egresses via the operator's server-managed key (see POST /api/consent/ai).
responses:
"200":
description: Insights bundle.
@@ -3988,6 +3990,13 @@ paths:
schema:
$ref: "#/components/schemas/InsightsComprehensiveResponseEnvelope"
"401": *a1
"403":
description: "AI consent required: no active ConsentReceipt for the server-managed provider. `meta.errorCode` =
`consent.ai.required`. Mint a receipt via POST /api/consent/ai before retrying."
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a3
"429": *a4
/api/insights/pregenerate:
@@ -4866,6 +4875,72 @@ paths:
"401": *a1
"422": *a3
"429": *a4
/api/consent/ai:
post:
tags:
- Consent
summary: Grant AI consent (signed artefact)
description: Persist a fresh AI-consent receipt. Required before server-managed AI egress (the operator's global LLM
key); BYOK / local / ChatGPT-OAuth chains are the user's own egress and are not gated. Append-only audit trail.
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
kind:
type: string
enum:
- ai_full
- ai_insights_only
- ai_coach
artefact:
type: string
minLength: 1
signedAt:
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:
- kind
- artefact
- signedAt
responses:
"200":
description: The minted receipt.
content:
application/json:
schema:
$ref: "#/components/schemas/ConsentReceiptResponseEnvelope"
"400":
description: Request validation failed (consent routes use 400).
content:
application/json:
schema:
$ref: "#/components/schemas/ConsentValidationErrorEnvelope"
"401": *a1
"422": *a3
"429": *a4
/api/consent/ai/web:
post:
tags:
- Consent
summary: Grant web AI consent (idempotent ai_full)
description: Mint an `ai_full` consent receipt for the calling web user if none is active; a no-op when one already
exists. Mirrors the iOS master grant; the web AI-settings surface calls this on mount so existing web accounts
on a shared-key deployment gain a receipt without a re-consent step. Revocation flows through DELETE
/api/consent/ai/latest.
responses:
"200":
description: Grant outcome.
content:
application/json:
schema:
$ref: "#/components/schemas/WebConsentGrantResponseEnvelope"
"401": *a1
"422": *a3
"429": *a4
components:
schemas:
PasskeyLoginVerifyRequest:
@@ -14317,6 +14392,111 @@ components:
additionalProperties: false
description: "Medication reminder thresholds: the operator-level late/missed minute marks plus the calling user's
low-stock runway threshold (v1.16.11)."
ConsentReceiptResponseEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/ConsentReceiptResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
ConsentReceiptResponse:
type: object
properties:
id:
type: string
receipt:
type: object
properties:
id:
type: string
userId:
type: string
kind:
type: string
enum:
- ai_full
- ai_insights_only
- ai_coach
signedAt:
type: string
revokedAt:
anyOf:
- type: string
- type: "null"
createdAt:
type: string
required:
- id
- userId
- kind
- signedAt
- revokedAt
- createdAt
additionalProperties: false
required:
- id
- receipt
additionalProperties: false
description: The minted receipt. `artefact` is deliberately stripped from the response — it is opaque and only read at
audit time directly from the DB.
ConsentValidationErrorEnvelope:
type: object
properties:
data:
type: "null"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
WebConsentGrantResponseEnvelope:
type: object
properties:
data:
$ref: "#/components/schemas/WebConsentGrantResponse"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
WebConsentGrantResponse:
type: object
properties:
minted:
type: boolean
description: True when a new `ai_full` receipt was minted; false when an active one already existed (idempotent no-op).
kind:
type: string
const: ai_full
required:
- minted
- kind
additionalProperties: false
description: Outcome of the idempotent web `ai_full` grant. Safe to call on every AI-settings mount.
MedicationScheduleOutput:
type: object
properties: