feat(core-consent): add PayloadConsent, RecordingConsent and DI binders
Implements the Payload-backed IConsent that reads/writes users.consentState and emits CONSENT_GRANT/CONSENT_WITHDRAW audit entries via injected auditLog. Adds RecordingConsent test double in core-testing for unit-test injection. Adds bindProductionConsent/bindDevSeedConsent DI binders and InMemoryConsent for dev/seed contexts. Contract tests cover grant/withdraw/isGranted round-trip, audit entry shape, metadata persistence (bannerVersion/policyVersion/method), and getCategories reflection of state. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,3 +11,7 @@ export {
|
||||
type RecordedIdentify,
|
||||
type RecordedPageView,
|
||||
} from "./recording-analytics";
|
||||
export {
|
||||
RecordingConsent,
|
||||
type RecordedConsentGrant,
|
||||
} from "./recording-consent";
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// Local type aliases mirroring exports from `@repo/core-consent`.
|
||||
// Kept inline to avoid a build-graph cycle between core-testing (tooling)
|
||||
// and core-consent (optional core). Same pattern used by RecordingAuditLog,
|
||||
// RecordingEventBus, and other recording doubles.
|
||||
|
||||
type ConsentCategory = string;
|
||||
type ConsentState = "granted" | "denied" | "pending";
|
||||
|
||||
type ConsentGrantMeta = {
|
||||
bannerVersion?: string;
|
||||
policyVersion?: string;
|
||||
method?: string;
|
||||
};
|
||||
|
||||
type UserConsentState = {
|
||||
readonly category: ConsentCategory;
|
||||
readonly state: ConsentState;
|
||||
readonly grantedAt?: Date;
|
||||
readonly withdrawnAt?: Date;
|
||||
readonly bannerVersion?: string;
|
||||
readonly policyVersion?: string;
|
||||
readonly method?: string;
|
||||
};
|
||||
|
||||
/** Recorded grant call. */
|
||||
export type RecordedConsentGrant = {
|
||||
category: ConsentCategory;
|
||||
meta: ConsentGrantMeta | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Test-side recording double for IConsent. Records all grant/withdraw calls
|
||||
* for assertion while maintaining the same state semantics as the real impl
|
||||
* (isGranted returns true after grant, false after withdraw).
|
||||
*
|
||||
* Use directly via constructor injection:
|
||||
* const consent = new RecordingConsent();
|
||||
* const uc = myUseCase(consent);
|
||||
* await uc({ ... });
|
||||
* expect(consent.grants).toHaveLength(1);
|
||||
*/
|
||||
export class RecordingConsent {
|
||||
public grants: RecordedConsentGrant[] = [];
|
||||
public withdrawals: ConsentCategory[] = [];
|
||||
private state = new Map<ConsentCategory, UserConsentState>();
|
||||
|
||||
isGranted(category: ConsentCategory): boolean {
|
||||
return this.state.get(category)?.state === "granted";
|
||||
}
|
||||
|
||||
async grant(
|
||||
category: ConsentCategory,
|
||||
meta?: ConsentGrantMeta,
|
||||
): Promise<void> {
|
||||
this.grants.push({ category, meta });
|
||||
const existing = this.state.get(category);
|
||||
this.state.set(category, {
|
||||
category,
|
||||
state: "granted",
|
||||
grantedAt: new Date(),
|
||||
withdrawnAt: existing?.withdrawnAt,
|
||||
bannerVersion: meta?.bannerVersion,
|
||||
policyVersion: meta?.policyVersion,
|
||||
method: meta?.method,
|
||||
});
|
||||
}
|
||||
|
||||
async withdraw(category: ConsentCategory): Promise<void> {
|
||||
this.withdrawals.push(category);
|
||||
const existing = this.state.get(category);
|
||||
this.state.set(category, {
|
||||
...existing,
|
||||
category,
|
||||
state: "denied",
|
||||
withdrawnAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
getCategories(): UserConsentState[] {
|
||||
return Array.from(this.state.values());
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.grants = [];
|
||||
this.withdrawals = [];
|
||||
this.state.clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user