---
id: 04-core-consent-implementation
epic: dsr-consent-and-cookie-banner
title: core-consent implementation — Payload impl, DI binders, migration helpers, tRPC router
type: technical-story
status: done
feature: core-consent
depends-on: [03-core-consent-foundation]
blocks:
  [
    05-core-consent-react,
    07-core-api-router-composition,
    10-auth-signup-migration,
  ]
created: 2026-05-19T12:00:00Z
updated: 2026-05-19T13:27:09.005Z
---

## Goal

Implement the runtime layer of `@repo/core-consent`: the Payload-backed `PayloadConsent` impl, the `RecordingConsent` test double, DI binders, the anonymous → authenticated consent migration helpers, protocol-agnostic handlers, and the `consentRouter` tRPC router.

## Why

Story 03 established the type surface and structural enforcement. This story delivers the working machinery: a Payload impl that reads/writes `users.consentState` and emits `CONSENT_GRANT`/`CONSENT_WITHDRAW` audit entries, migration helpers that consumers call in `signUp`, and a tRPC router that `core-api` can compose. Without this story, no downstream use case can actually check or record consent.

## Done when

- `PayloadConsent` in `core-consent/` reads `users.consentState` for fast `isGranted` reads and writes both the cache field and a `CONSENT_GRANT`/`CONSENT_WITHDRAW` audit entry via injected `core-audit` on grant/withdraw.
- `RecordingConsent` test double in `core-testing` records calls and payloads; unit tests assert captured calls match invocations.
- DI binders `core-consent/di/bind-production.ts` + `core-consent/di/bind-dev-seed.ts` exist; `assertFeatureConformance` passes at boot.
- Contract tests cover: `isGranted` returns `false` before grant, `true` after; `grant` writes both cache + audit entry with correct shape; `withdraw` clears cache + emits `CONSENT_WITHDRAW`; `getCategories` returns all categories with `granted: true`; consent state round-trip including `bannerVersion`, `policyVersion`, `method`.
- `extractAnonymousConsent(cookieHeader: string)` + `migrateAnonymousConsent({ userId, cookieState, bannerVersion, policyVersion })` exist in `core-consent/` with tests covering the happy path and absent-cookie no-op.
- Protocol-agnostic handlers in `core-consent/handlers/` return normalized `{ status, body, headers }`.
- `consentRouter` in `core-consent/consent.router.ts` exports `grant`, `withdraw`, `isGranted`, `getCategories` procedures with auth checks via `defineErrorMiddleware` pattern; integration tests assert response shapes and auth error passthrough.
- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass after each task.

## In scope

- `PayloadConsent` Payload-backed implementation (reads/writes `users.consentState`, emits consent audit entries via injected `core-audit`).
- `RecordingConsent` test double in `packages/core-testing/`.
- DI binders (`bind-production`, `bind-dev-seed`) in `core-consent/di/`.
- `extractAnonymousConsent` + `migrateAnonymousConsent` migration helpers in `core-consent/`.
- Protocol-agnostic handlers in `core-consent/handlers/`.
- `consentRouter` tRPC router in `core-consent/consent.router.ts`.

## Out of scope

- React subpath `<ConsentProvider>` + `useConsent()` (Story 05).
- `core-api` appRouter composition (Story 07).
- `auth.signUp` integration (Story 10).

## Tasks

- [x] Add `PayloadConsent` Payload-backed implementation in `core-consent/` reading/writing `users.consentState` and emitting `CONSENT_GRANT`/`CONSENT_WITHDRAW` audit entries via injected `core-audit` + `RecordingConsent` test double in `packages/core-testing/` + DI binders `core-consent/di/bind-production.ts` and `core-consent/di/bind-dev-seed.ts`; contract tests covering grant/withdraw/isGranted round-trip, audit entry shape, and `getCategories`; all gates pass.
- [x] Add `extractAnonymousConsent(cookieHeader: string)` + `migrateAnonymousConsent({ userId, cookieState, bannerVersion, policyVersion })` helpers in `core-consent/` with tests covering the happy path (cookie present → calls `IConsent.grant` with `method: "signup-migration"`) and the absent-cookie no-op; all gates pass.
- [x] Add protocol-agnostic handlers in `core-consent/handlers/` + `consentRouter` tRPC router in `core-consent/consent.router.ts` exporting `grant`, `withdraw`, `isGranted`, `getCategories` procedures with auth checks; integration tests asserting response shapes and auth error passthrough; all gates pass.
