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; /** * 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; /** * Query schema for both `/api/consent/ai/latest` GET and DELETE. * * Omitting `kind` on GET returns the latest active receipt per kind * (a `Record` 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;