mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
fix(consent): a withdrawn AI consent stays withdrawn, and the web can withdraw
The settings mount fires an idempotent consent heal so an account that predates the gate does not fall into the no-key state. It looked only for an active receipt, so a deliberate withdrawal — which sets revokedAt — read the same as never having consented, and the next visit to the AI settings granted again, writing an audit row for an act the user never performed. GDPR Art. 7 (3) puts withdrawal on the same footing as the grant; one undone by a page load is not a withdrawal. The mint now distinguishes the two callers. The mount heal grants only to an account with no consent history at all and leaves a revocation standing; switching document auto-read on is the consent act itself and may supersede an earlier revocation. The revocation is re-checked inside the transaction so a withdrawal landing mid-mint is not overwritten. The revoke endpoint has existed since v1.4.40 but only the native client called it: on the web, consent could be given and never taken back. The AI settings now carry the standing decision and the control that changes it, in all six languages.
This commit is contained in:
1 parent
851a82cc79
commit
1c2cfffdb8
18 files changed
+473
-18
No files matched your search
@@ -107,7 +107,7 @@ export const PATCH = apiHandler(async (req: Request) => {
|
||||
// `ai_full` receipt (idempotent) so the durable audit trail records it, in
|
||||
// addition to the runtime short-circuit the document consent gate reads.
|
||||
if (next) {
|
||||
await ensureWebAiConsentReceipt(user.id);
|
||||
await ensureWebAiConsentReceipt(user.id, "affirmative");
|
||||
}
|
||||
|
||||
await auditLog("user.documentsAutoAiRead.update", {
|
||||
|
||||
@@ -90,7 +90,10 @@ describe("POST /api/consent/ai/web", () => {
|
||||
|
||||
it("is a no-op when an active receipt already exists (minted:false)", async () => {
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
vi.mocked(ensureWebAiConsentReceipt).mockResolvedValue({ minted: false });
|
||||
vi.mocked(ensureWebAiConsentReceipt).mockResolvedValue({
|
||||
minted: false,
|
||||
reason: "already_active",
|
||||
});
|
||||
|
||||
const res = await POST();
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
@@ -37,7 +37,7 @@ export const POST = apiHandler(async () => {
|
||||
});
|
||||
}
|
||||
|
||||
const result = await ensureWebAiConsentReceipt(user.id);
|
||||
const result = await ensureWebAiConsentReceipt(user.id, "heal");
|
||||
|
||||
if (result.minted) {
|
||||
// Audit trail parity with the iOS-driven POST /api/consent/ai grant so
|
||||
|
||||
Reference in new issue
Block a user