mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
feat(consent): CRUD helper for AI consent receipts
Adds the persistence layer for the AI consent audit trail required by App-Store Guideline 5.1.2(i) and GDPR Art. 7: - src/lib/validations/consent.ts — Zod schemas with a three-kind discriminator (ai_full / ai_insights_only / ai_coach) and a 64 KB cap on the opaque artefact so a misbehaving client can't fill the audit table with multi-megabyte rows. - src/lib/consent/receipts.ts — append-only helpers: createReceipt, latestActiveReceipt, latestActiveReceiptsByKind, revokeLatest. Revoke flips a single field on the existing row; a re-grant always mints a fresh row, keeping the historical chain intact for legal review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
55fefb1d72
commit
8692a97e3f
3 files changed
+288
No files matched your search
@@ -0,0 +1,60 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
/**
|
||||
* v1.4.40 SB-10 — AI consent receipts (App-Store Guideline 5.1.2(i) +
|
||||
* GDPR Art. 7 audit trail).
|
||||
*
|
||||
* The discriminator covers the three consent surfaces the iOS client
|
||||
* collects independently:
|
||||
* - `ai_full` full assistant + on-device personalisation
|
||||
* - `ai_insights_only` read-only Insights generation (no Coach)
|
||||
* - `ai_coach` chat-style Coach access
|
||||
*
|
||||
* Keeping each surface as its own row means the legal team can prove
|
||||
* which exact scope the user agreed to without parsing the signed
|
||||
* artefact server-side. The server stays opaque to the artefact's
|
||||
* format — we accept either a base64-encoded PDF or a signed JWT, the
|
||||
* audit trail *is* the storage plus the `signedAt` timestamp.
|
||||
*/
|
||||
export const consentKindEnum = z.enum([
|
||||
"ai_full",
|
||||
"ai_insights_only",
|
||||
"ai_coach",
|
||||
]);
|
||||
|
||||
export type ConsentKind = z.infer<typeof consentKindEnum>;
|
||||
|
||||
/**
|
||||
* 64 KB upper bound on the artefact. A signed PDF receipt sits well
|
||||
* under 32 KB; a signed JWT is a few hundred bytes. The cap exists so
|
||||
* a misbehaving client (or a malicious one) can't fill the audit table
|
||||
* with multi-megabyte rows.
|
||||
*/
|
||||
const ARTEFACT_MAX_BYTES = 64 * 1024;
|
||||
|
||||
export const consentPostBody = z.object({
|
||||
kind: consentKindEnum,
|
||||
artefact: z
|
||||
.string()
|
||||
.min(1, "artefact must not be empty")
|
||||
.max(ARTEFACT_MAX_BYTES, "artefact exceeds 64 KB cap"),
|
||||
signedAt: z.iso
|
||||
.datetime({ offset: true })
|
||||
.transform((s) => new Date(s)),
|
||||
});
|
||||
|
||||
export type ConsentPostBody = z.infer<typeof consentPostBody>;
|
||||
|
||||
/**
|
||||
* Query schema for both `/api/consent/ai/latest` GET and DELETE.
|
||||
*
|
||||
* Omitting `kind` on GET returns the latest active receipt per kind
|
||||
* (a `Record<ConsentKind, …>` shape). Omitting it on DELETE revokes
|
||||
* the latest receipt across all three kinds — handy for the iOS
|
||||
* "deaktivieren alle KI-Funktionen" master toggle.
|
||||
*/
|
||||
export const consentLatestQuery = z.object({
|
||||
kind: consentKindEnum.optional(),
|
||||
});
|
||||
|
||||
export type ConsentLatestQuery = z.infer<typeof consentLatestQuery>;
|
||||
Reference in new issue
Block a user