From c9d5479b0d1d4fd30bf08dacd8c9c38ee7f2ef78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 21 May 2026 11:34:04 +0200 Subject: [PATCH] feat(consent-receipts): schema scaffolding for AI consent audit trail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prereq for SB-10 (POST /api/consent/ai). Append-only per-user audit trail of AI-processing consent grants. The iOS app posts a fresh row whenever the user opts in to off-device AI flows during onboarding or via Settings; a revoke + re-grant cycle mints fresh rows so the timeline reads chronologically and the latest non-revoked row per (user, kind) is the source of truth for "is AI active right now". Backs both App-Store Guideline 5.1.2(i) ("data-collection consent must be verifiable") and the GDPR Art. 7 documentation burden. The server persists the raw signed artefact (PDF receipt or signed JWT — caller's choice) without parsing it; the audit trail is the storage itself plus the `signed_at` wall-clock the client recorded. Migration 0074 is additive only; IF NOT EXISTS guards mirror 0067 / 0070 / 0071. No table rewrite, no breaking change. --- .../0074_v1440_consent_receipts/migration.sql | 34 +++++++++++++++ prisma/schema.prisma | 43 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 prisma/migrations/0074_v1440_consent_receipts/migration.sql diff --git a/prisma/migrations/0074_v1440_consent_receipts/migration.sql b/prisma/migrations/0074_v1440_consent_receipts/migration.sql new file mode 100644 index 000000000..73f9d3d6f --- /dev/null +++ b/prisma/migrations/0074_v1440_consent_receipts/migration.sql @@ -0,0 +1,34 @@ +-- v1.4.40 SB-10 — per-user AI consent receipts. +-- +-- Append-only audit trail backing the iOS onboarding consent flow plus +-- the operator's GDPR Art. 7 + App-Store Guideline 5.1.2(i) burden of +-- proof for "data-collection consent is verifiable". A user revoking + +-- re-granting consent mints a fresh row; the latest non-revoked row +-- per (user, kind) is the source of truth for "is AI active right +-- now". +-- +-- Additive only. Idempotent guards mirror 0067 / 0070 / 0071. + +CREATE TABLE IF NOT EXISTS "consent_receipts" ( + "id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "kind" TEXT NOT NULL, + "artefact" TEXT NOT NULL, + "signed_at" TIMESTAMP(3) NOT NULL, + "revoked_at" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "consent_receipts_pkey" PRIMARY KEY ("id") +); + +CREATE INDEX IF NOT EXISTS "consent_receipts_user_id_created_at_idx" + ON "consent_receipts" ("user_id", "created_at" DESC); + +DO $$ BEGIN + ALTER TABLE "consent_receipts" + ADD CONSTRAINT "consent_receipts_user_id_fkey" + FOREIGN KEY ("user_id") REFERENCES "users"("id") + ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION + WHEN duplicate_object THEN NULL; +END $$; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b0509aee8..9e31aab28 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -295,6 +295,13 @@ model User { // keep the row in sync so `/api/medications/intake?scope=compliance` // and `computeUserHealthScoreFastPath` skip the per-event findMany. medicationComplianceRollups MedicationComplianceRollup[] + // v1.4.40 SB-10 — AI consent receipts the iOS app POSTs when the user + // grants AI processing during onboarding. Persisted audit trail + // covers App-Store Guideline 5.1.2(i) ("data-collection consent must + // be verifiable") plus the operator's GDPR Art. 7 documentation + // burden. Append-only — a user revoking + re-granting consent mints + // a fresh row so the timeline reads chronologically. + consentReceipts ConsentReceipt[] @@map("users") } @@ -1917,3 +1924,39 @@ model CoachUsage { @@index([userId, dateKey]) @@map("coach_usage") } + +// ─── Consent Receipts (v1.4.40 SB-10) ───────────────────────── +// +// Per-user audit trail of AI-processing consent grants. The iOS app +// posts a fresh row whenever the user opts in to off-device AI flows +// during onboarding or via Settings; append-only so a revoke + re-grant +// cycle reads chronologically. Persists the raw signed artefact (PDF +// receipt OR signed JWT — caller's choice) the App-Store Guideline +// 5.1.2(i) audit needs to be verifiable. The latest row per user is +// the source of truth for "is AI active right now" decisions; older +// rows are kept for the GDPR Art. 7 burden of proof. +// +// `kind` is a free-text discriminator the iOS / web client uses to +// distinguish "ai_full" / "ai_insights_only" / future scopes without +// requiring a schema migration per new tier. +model ConsentReceipt { + id String @id @default(cuid()) + userId String @map("user_id") + kind String + /// Raw consent artefact — either a base64-encoded PDF or a signed + /// JWT. The server does not parse this payload; the audit trail is + /// the storage itself plus the `signedAt` timestamp. + artefact String @db.Text + /// Wall-clock the client recorded the user's tap. UTC ISO-8601. + signedAt DateTime @map("signed_at") + /// Optional revocation marker — set when a follow-up "revoke" row + /// is minted so the latest-per-user lookup can shortcut to "no + /// active consent" without diffing the kind discriminator. + revokedAt DateTime? @map("revoked_at") + createdAt DateTime @default(now()) @map("created_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId, createdAt(sort: Desc)]) + @@map("consent_receipts") +}