feat(auth): migrate anonymous consent on signUp when cc_consent cookie present
Adds ConsentFactoryProtocol / ConsentGrantMeta / ConsentProtocol to core-shared/di/bind-protocols so feature binders can wire per-user consent without a hard dep on the optional @repo/core-consent package. BindContext gains an optional consentFactory? field following the same pattern as bus?, auditLog?, etc. signUpUseCase gains a 4th optional dep (consentFactory). When present and the input includes a cookieHeader containing cc_consent=<categories>, the use case calls consent.grant for each category with method:"signup-migration" and returns a clearCookie payload (Max-Age:0) so the anonymous cookie is cleared on the HTTP response. Tests use RecordingConsent from @repo/core-testing to assert migration call shape and cookie-clear; no-cookie and no-factory branches are also covered. All coverage bands hold at 100% for use-cases. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1,33 +1,33 @@
|
||||
{
|
||||
"generatedAt": "2026-05-19T21:36:08.521Z",
|
||||
"commit": "cbe7412",
|
||||
"generatedAt": "2026-05-19T21:51:40.371Z",
|
||||
"commit": "5151454",
|
||||
"repo": {
|
||||
"statements": 97.38,
|
||||
"branches": 92.08,
|
||||
"functions": 97.09,
|
||||
"branches": 92.19,
|
||||
"functions": 97.1,
|
||||
"lines": 97.38,
|
||||
"counts": {
|
||||
"lf": 5758,
|
||||
"lh": 5607,
|
||||
"brf": 1149,
|
||||
"brh": 1058,
|
||||
"fnf": 344,
|
||||
"fnh": 334
|
||||
"lf": 5801,
|
||||
"lh": 5649,
|
||||
"brf": 1165,
|
||||
"brh": 1074,
|
||||
"fnf": 345,
|
||||
"fnh": 335
|
||||
}
|
||||
},
|
||||
"byPackage": {
|
||||
"@repo/auth": {
|
||||
"statements": 93.81,
|
||||
"branches": 90.82,
|
||||
"statements": 94,
|
||||
"branches": 92.11,
|
||||
"functions": 100,
|
||||
"lines": 93.81,
|
||||
"lines": 94,
|
||||
"counts": {
|
||||
"lf": 791,
|
||||
"lh": 742,
|
||||
"brf": 98,
|
||||
"brh": 89,
|
||||
"fnf": 44,
|
||||
"fnh": 44
|
||||
"lf": 834,
|
||||
"lh": 784,
|
||||
"brf": 114,
|
||||
"brh": 105,
|
||||
"fnf": 45,
|
||||
"fnh": 45
|
||||
}
|
||||
},
|
||||
"@repo/blog": {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { ZodError } from "zod";
|
||||
import { RecordingEventBus } from "@repo/core-testing/instrumentation";
|
||||
import {
|
||||
RecordingEventBus,
|
||||
RecordingConsent,
|
||||
} from "@repo/core-testing/instrumentation";
|
||||
import {
|
||||
signUpUseCase,
|
||||
signUpOutputSchema,
|
||||
@@ -16,7 +19,7 @@ describe("signUpUseCase", () => {
|
||||
const users = new MockUsersRepository([]);
|
||||
const auth = new MockAuthenticationService(users);
|
||||
const bus = new RecordingEventBus();
|
||||
const useCase = signUpUseCase(users, auth, bus);
|
||||
const useCase = signUpUseCase(users, auth, bus, undefined);
|
||||
|
||||
const result = await useCase({
|
||||
username: "carol",
|
||||
@@ -34,7 +37,7 @@ describe("signUpUseCase", () => {
|
||||
const bus = new RecordingEventBus();
|
||||
await users.createUser(userFactory.build({ username: "alice" }));
|
||||
|
||||
const useCase = signUpUseCase(users, auth, bus);
|
||||
const useCase = signUpUseCase(users, auth, bus, undefined);
|
||||
await expect(
|
||||
useCase({
|
||||
username: "alice",
|
||||
@@ -48,7 +51,7 @@ describe("signUpUseCase", () => {
|
||||
const users = new MockUsersRepository([]);
|
||||
const auth = new MockAuthenticationService(users);
|
||||
const bus = new RecordingEventBus();
|
||||
const useCase = signUpUseCase(users, auth, bus);
|
||||
const useCase = signUpUseCase(users, auth, bus, undefined);
|
||||
|
||||
await useCase({
|
||||
username: "dave",
|
||||
@@ -70,7 +73,7 @@ describe("signUpUseCase", () => {
|
||||
it("works without an event bus (welcome email skipped silently)", async () => {
|
||||
const users = new MockUsersRepository([]);
|
||||
const auth = new MockAuthenticationService(users);
|
||||
const useCase = signUpUseCase(users, auth, undefined);
|
||||
const useCase = signUpUseCase(users, auth, undefined, undefined);
|
||||
|
||||
const result = await useCase({
|
||||
username: "frank",
|
||||
@@ -88,7 +91,7 @@ describe("signUpUseCase", () => {
|
||||
const bus = new RecordingEventBus();
|
||||
await users.createUser(userFactory.build({ username: "eve" }));
|
||||
|
||||
const useCase = signUpUseCase(users, auth, bus);
|
||||
const useCase = signUpUseCase(users, auth, bus, undefined);
|
||||
await expect(
|
||||
useCase({
|
||||
username: "eve",
|
||||
@@ -98,6 +101,108 @@ describe("signUpUseCase", () => {
|
||||
).rejects.toBeInstanceOf(AuthenticationError);
|
||||
expect(bus.published).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("migrates anonymous consent when cc_consent cookie is present", async () => {
|
||||
const users = new MockUsersRepository([]);
|
||||
const auth = new MockAuthenticationService(users);
|
||||
const bus = new RecordingEventBus();
|
||||
const consent = new RecordingConsent();
|
||||
const consentFactory = (_userId: string) => Promise.resolve(consent);
|
||||
const useCase = signUpUseCase(users, auth, bus, consentFactory);
|
||||
|
||||
const result = await useCase({
|
||||
username: "grace",
|
||||
password: "secret_password",
|
||||
confirmPassword: "secret_password",
|
||||
cookieHeader: "cc_consent=necessary,analytics; session=xyz",
|
||||
});
|
||||
|
||||
expect(consent.grants).toHaveLength(2);
|
||||
expect(consent.grants[0]).toEqual({
|
||||
category: "necessary",
|
||||
meta: { method: "signup-migration" },
|
||||
});
|
||||
expect(consent.grants[1]).toEqual({
|
||||
category: "analytics",
|
||||
meta: { method: "signup-migration" },
|
||||
});
|
||||
expect(result.clearCookie).toBeDefined();
|
||||
expect(result.clearCookie?.name).toBe("cc_consent");
|
||||
expect(result.clearCookie?.attributes.maxAge).toBe(0);
|
||||
});
|
||||
|
||||
it("does not migrate consent when no cc_consent cookie is present", async () => {
|
||||
const users = new MockUsersRepository([]);
|
||||
const auth = new MockAuthenticationService(users);
|
||||
const bus = new RecordingEventBus();
|
||||
const consent = new RecordingConsent();
|
||||
const consentFactory = (_userId: string) => Promise.resolve(consent);
|
||||
const useCase = signUpUseCase(users, auth, bus, consentFactory);
|
||||
|
||||
const result = await useCase({
|
||||
username: "henry",
|
||||
password: "secret_password",
|
||||
confirmPassword: "secret_password",
|
||||
cookieHeader: "session=xyz",
|
||||
});
|
||||
|
||||
expect(consent.grants).toHaveLength(0);
|
||||
expect(result.clearCookie).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not migrate consent when consentFactory is absent", async () => {
|
||||
const users = new MockUsersRepository([]);
|
||||
const auth = new MockAuthenticationService(users);
|
||||
const bus = new RecordingEventBus();
|
||||
const useCase = signUpUseCase(users, auth, bus, undefined);
|
||||
|
||||
const result = await useCase({
|
||||
username: "iris",
|
||||
password: "secret_password",
|
||||
confirmPassword: "secret_password",
|
||||
cookieHeader: "cc_consent=analytics",
|
||||
});
|
||||
|
||||
expect(result.clearCookie).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not migrate consent when cc_consent cookie has no value", async () => {
|
||||
const users = new MockUsersRepository([]);
|
||||
const auth = new MockAuthenticationService(users);
|
||||
const bus = new RecordingEventBus();
|
||||
const consent = new RecordingConsent();
|
||||
const consentFactory = (_userId: string) => Promise.resolve(consent);
|
||||
const useCase = signUpUseCase(users, auth, bus, consentFactory);
|
||||
|
||||
const result = await useCase({
|
||||
username: "jake",
|
||||
password: "secret_password",
|
||||
confirmPassword: "secret_password",
|
||||
cookieHeader: "cc_consent=",
|
||||
});
|
||||
|
||||
expect(consent.grants).toHaveLength(0);
|
||||
expect(result.clearCookie).toBeUndefined();
|
||||
});
|
||||
|
||||
it("parses cookie header with malformed parts (no = sign)", async () => {
|
||||
const users = new MockUsersRepository([]);
|
||||
const auth = new MockAuthenticationService(users);
|
||||
const bus = new RecordingEventBus();
|
||||
const consent = new RecordingConsent();
|
||||
const consentFactory = (_userId: string) => Promise.resolve(consent);
|
||||
const useCase = signUpUseCase(users, auth, bus, consentFactory);
|
||||
|
||||
const result = await useCase({
|
||||
username: "kate",
|
||||
password: "secret_password",
|
||||
confirmPassword: "secret_password",
|
||||
cookieHeader: "malformedcookie; cc_consent=necessary",
|
||||
});
|
||||
|
||||
expect(consent.grants).toHaveLength(1);
|
||||
expect(result.clearCookie).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("signUpUseCase output validation", () => {
|
||||
@@ -112,7 +217,7 @@ describe("signUpUseCase output validation", () => {
|
||||
} as unknown as IAuthenticationService;
|
||||
|
||||
const bus = new RecordingEventBus();
|
||||
const useCase = signUpUseCase(users, auth, bus);
|
||||
const useCase = signUpUseCase(users, auth, bus, undefined);
|
||||
await expect(
|
||||
useCase({
|
||||
username: "carol",
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import type { EventBusProtocol } from "@repo/core-shared/di";
|
||||
import type {
|
||||
EventBusProtocol,
|
||||
ConsentFactoryProtocol,
|
||||
} from "@repo/core-shared/di";
|
||||
import { userSignedUpEvent } from "../../events/user-signed-up.event";
|
||||
import { AuthenticationError } from "../../entities/errors/auth";
|
||||
import { cookieSchema } from "../../entities/models/cookie";
|
||||
@@ -8,12 +11,32 @@ import { sessionSchema } from "../../entities/models/session";
|
||||
import type { IUsersRepository } from "../repositories/users.repository.interface";
|
||||
import type { IAuthenticationService } from "../services/authentication.service.interface";
|
||||
|
||||
// Cookie name written by the anonymous consent banner (mirrors CONSENT_COOKIE_NAME in @repo/core-consent).
|
||||
const ANONYMOUS_CONSENT_COOKIE = "cc_consent";
|
||||
|
||||
function extractConsentFromCookieHeader(cookieHeader: string): string[] | null {
|
||||
for (const part of cookieHeader.split(";")) {
|
||||
const eqIdx = part.indexOf("=");
|
||||
if (eqIdx === -1) continue;
|
||||
const name = part.slice(0, eqIdx).trim();
|
||||
if (name !== ANONYMOUS_CONSENT_COOKIE) continue;
|
||||
const value = part.slice(eqIdx + 1).trim();
|
||||
const cats = value
|
||||
.split(",")
|
||||
.map((c) => c.trim())
|
||||
.filter(Boolean);
|
||||
return cats.length > 0 ? cats : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Input ────────────────────────────────────────────────────────────────
|
||||
export const signUpInputSchema = z
|
||||
.object({
|
||||
username: z.string().min(3).max(31),
|
||||
password: z.string().min(6).max(255),
|
||||
confirmPassword: z.string().min(6).max(255),
|
||||
cookieHeader: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.refine((d) => d.password === d.confirmPassword, {
|
||||
@@ -26,6 +49,7 @@ export type SignUpInput = z.infer<typeof signUpInputSchema>;
|
||||
export const signUpOutputSchema = z.object({
|
||||
session: sessionSchema,
|
||||
cookie: cookieSchema,
|
||||
clearCookie: cookieSchema.optional(),
|
||||
});
|
||||
export type SignUpOutput = z.infer<typeof signUpOutputSchema>;
|
||||
|
||||
@@ -37,6 +61,7 @@ export const signUpUseCase =
|
||||
usersRepository: IUsersRepository,
|
||||
authenticationService: IAuthenticationService,
|
||||
bus: EventBusProtocol | undefined,
|
||||
consentFactory: ConsentFactoryProtocol | undefined,
|
||||
) =>
|
||||
async (input: SignUpInput): Promise<SignUpOutput> => {
|
||||
const existingUser = await usersRepository.getUserByUsername(
|
||||
@@ -71,5 +96,24 @@ export const signUpUseCase =
|
||||
});
|
||||
}
|
||||
|
||||
return signUpOutputSchema.parse({ session, cookie });
|
||||
// Migrate anonymous consent when both a cookie header and a consent factory
|
||||
// are present. consentFactory is optional: absent when core-consent is not wired.
|
||||
const cookieState = input.cookieHeader
|
||||
? extractConsentFromCookieHeader(input.cookieHeader)
|
||||
: null;
|
||||
|
||||
let clearCookie: z.infer<typeof cookieSchema> | undefined;
|
||||
if (cookieState && consentFactory) {
|
||||
const consent = await consentFactory(newUser.id);
|
||||
for (const category of cookieState) {
|
||||
await consent.grant(category, { method: "signup-migration" });
|
||||
}
|
||||
clearCookie = {
|
||||
name: ANONYMOUS_CONSENT_COOKIE,
|
||||
value: "",
|
||||
attributes: { maxAge: 0, path: "/" },
|
||||
};
|
||||
}
|
||||
|
||||
return signUpOutputSchema.parse({ session, cookie, clearCookie });
|
||||
};
|
||||
|
||||
@@ -38,7 +38,15 @@ import type { IAuthenticationService } from "../application/services/authenticat
|
||||
* populated repo and rebinds the symbol.
|
||||
*/
|
||||
export async function bindDevSeedAuth(ctx: BindContext): Promise<void> {
|
||||
const { tracer, logger, bus, queue, realtime, realtimeRegistry } = ctx;
|
||||
const {
|
||||
tracer,
|
||||
logger,
|
||||
bus,
|
||||
queue,
|
||||
realtime,
|
||||
realtimeRegistry,
|
||||
consentFactory,
|
||||
} = ctx;
|
||||
|
||||
// Bind shared instrumentation into feature container
|
||||
if (authContainer.isBound(INSTRUMENTATION_SYMBOLS.TRACER)) {
|
||||
@@ -88,7 +96,7 @@ export async function bindDevSeedAuth(ctx: BindContext): Promise<void> {
|
||||
container: authContainer,
|
||||
symbol: AUTH_SYMBOLS.ISignUpUseCase,
|
||||
factory: signUpUseCase,
|
||||
deps: [repo, authService, bus],
|
||||
deps: [repo, authService, bus, consentFactory],
|
||||
feature: "auth",
|
||||
layer: "use-case",
|
||||
name: "signUp",
|
||||
|
||||
@@ -30,8 +30,16 @@ export function bindProductionAuth(ctx: BindProductionContext): void {
|
||||
if (bound) return;
|
||||
bound = true;
|
||||
|
||||
const { config, tracer, logger, bus, queue, realtime, realtimeRegistry } =
|
||||
ctx;
|
||||
const {
|
||||
config,
|
||||
tracer,
|
||||
logger,
|
||||
bus,
|
||||
queue,
|
||||
realtime,
|
||||
realtimeRegistry,
|
||||
consentFactory,
|
||||
} = ctx;
|
||||
|
||||
// Bind shared instrumentation into feature container
|
||||
if (authContainer.isBound(INSTRUMENTATION_SYMBOLS.TRACER)) {
|
||||
@@ -80,7 +88,7 @@ export function bindProductionAuth(ctx: BindProductionContext): void {
|
||||
container: authContainer,
|
||||
symbol: AUTH_SYMBOLS.ISignUpUseCase,
|
||||
factory: signUpUseCase,
|
||||
deps: [repo, authService, bus],
|
||||
deps: [repo, authService, bus, consentFactory],
|
||||
feature: "auth",
|
||||
layer: "use-case",
|
||||
name: "signUp",
|
||||
|
||||
@@ -39,7 +39,9 @@ export const AuthModule = new ContainerModule((bind: interfaces.Bind) => {
|
||||
bind<ISignInUseCase>(AUTH_SYMBOLS.ISignInUseCase).toDynamicValue((ctx) =>
|
||||
signInUseCase(
|
||||
ctx.container.get<IUsersRepository>(AUTH_SYMBOLS.IUsersRepository),
|
||||
ctx.container.get<IAuthenticationService>(AUTH_SYMBOLS.IAuthenticationService),
|
||||
ctx.container.get<IAuthenticationService>(
|
||||
AUTH_SYMBOLS.IAuthenticationService,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -49,32 +51,40 @@ export const AuthModule = new ContainerModule((bind: interfaces.Bind) => {
|
||||
// bus instance when @repo/core-events is scaffolded.
|
||||
signUpUseCase(
|
||||
ctx.container.get<IUsersRepository>(AUTH_SYMBOLS.IUsersRepository),
|
||||
ctx.container.get<IAuthenticationService>(AUTH_SYMBOLS.IAuthenticationService),
|
||||
ctx.container.get<IAuthenticationService>(
|
||||
AUTH_SYMBOLS.IAuthenticationService,
|
||||
),
|
||||
undefined,
|
||||
undefined,
|
||||
),
|
||||
);
|
||||
|
||||
bind<ISignOutUseCase>(AUTH_SYMBOLS.ISignOutUseCase).toDynamicValue((ctx) =>
|
||||
signOutUseCase(
|
||||
ctx.container.get<IAuthenticationService>(AUTH_SYMBOLS.IAuthenticationService),
|
||||
ctx.container.get<IAuthenticationService>(
|
||||
AUTH_SYMBOLS.IAuthenticationService,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
bind<ISignInController>(AUTH_SYMBOLS.ISignInController).toDynamicValue((ctx) =>
|
||||
signInController(
|
||||
ctx.container.get<ISignInUseCase>(AUTH_SYMBOLS.ISignInUseCase),
|
||||
),
|
||||
bind<ISignInController>(AUTH_SYMBOLS.ISignInController).toDynamicValue(
|
||||
(ctx) =>
|
||||
signInController(
|
||||
ctx.container.get<ISignInUseCase>(AUTH_SYMBOLS.ISignInUseCase),
|
||||
),
|
||||
);
|
||||
|
||||
bind<ISignUpController>(AUTH_SYMBOLS.ISignUpController).toDynamicValue((ctx) =>
|
||||
signUpController(
|
||||
ctx.container.get<ISignUpUseCase>(AUTH_SYMBOLS.ISignUpUseCase),
|
||||
),
|
||||
bind<ISignUpController>(AUTH_SYMBOLS.ISignUpController).toDynamicValue(
|
||||
(ctx) =>
|
||||
signUpController(
|
||||
ctx.container.get<ISignUpUseCase>(AUTH_SYMBOLS.ISignUpUseCase),
|
||||
),
|
||||
);
|
||||
|
||||
bind<ISignOutController>(AUTH_SYMBOLS.ISignOutController).toDynamicValue((ctx) =>
|
||||
signOutController(
|
||||
ctx.container.get<ISignOutUseCase>(AUTH_SYMBOLS.ISignOutUseCase),
|
||||
),
|
||||
bind<ISignOutController>(AUTH_SYMBOLS.ISignOutController).toDynamicValue(
|
||||
(ctx) =>
|
||||
signOutController(
|
||||
ctx.container.get<ISignOutUseCase>(AUTH_SYMBOLS.ISignOutUseCase),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -11,7 +11,12 @@ describe("signUpController", () => {
|
||||
it("returns a cookie on successful sign-up", async () => {
|
||||
const users = new MockUsersRepository([]);
|
||||
const auth = new MockAuthenticationService(users);
|
||||
const useCase = signUpUseCase(users, auth, new RecordingEventBus());
|
||||
const useCase = signUpUseCase(
|
||||
users,
|
||||
auth,
|
||||
new RecordingEventBus(),
|
||||
undefined,
|
||||
);
|
||||
const controller = signUpController(useCase);
|
||||
|
||||
const result = await controller({
|
||||
@@ -26,7 +31,12 @@ describe("signUpController", () => {
|
||||
it("throws InputParseError when passwords do not match", async () => {
|
||||
const users = new MockUsersRepository([]);
|
||||
const auth = new MockAuthenticationService(users);
|
||||
const useCase = signUpUseCase(users, auth, new RecordingEventBus());
|
||||
const useCase = signUpUseCase(
|
||||
users,
|
||||
auth,
|
||||
new RecordingEventBus(),
|
||||
undefined,
|
||||
);
|
||||
const controller = signUpController(useCase);
|
||||
|
||||
await expect(
|
||||
@@ -42,11 +52,20 @@ describe("signUpController", () => {
|
||||
const users = new MockUsersRepository([]);
|
||||
const auth = new MockAuthenticationService(users);
|
||||
await users.createUser(userFactory.build({ username: "alice" }));
|
||||
const useCase = signUpUseCase(users, auth, new RecordingEventBus());
|
||||
const useCase = signUpUseCase(
|
||||
users,
|
||||
auth,
|
||||
new RecordingEventBus(),
|
||||
undefined,
|
||||
);
|
||||
const controller = signUpController(useCase);
|
||||
|
||||
await expect(
|
||||
controller({ username: "ab", password: "secret_password", confirmPassword: "secret_password" }),
|
||||
controller({
|
||||
username: "ab",
|
||||
password: "secret_password",
|
||||
confirmPassword: "secret_password",
|
||||
}),
|
||||
).rejects.toBeInstanceOf(InputParseError);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,9 @@ describe("auth feature: sign-up → sign-in → sign-out", () => {
|
||||
const auth = new MockAuthenticationService(users);
|
||||
|
||||
const signIn = signInController(signInUseCase(users, auth));
|
||||
const signUp = signUpController(signUpUseCase(users, auth, new RecordingEventBus()));
|
||||
const signUp = signUpController(
|
||||
signUpUseCase(users, auth, new RecordingEventBus(), undefined),
|
||||
);
|
||||
const signOut = signOutController(signOutUseCase(auth));
|
||||
|
||||
// signUp returns a cookie (presenter shape)
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
MetricsProtocol,
|
||||
AuditLogProtocol,
|
||||
AnalyticsProtocol,
|
||||
ConsentFactoryProtocol,
|
||||
} from "./bind-protocols";
|
||||
|
||||
/** Always-present fields. Feature binders rely on these unconditionally. */
|
||||
@@ -43,6 +44,7 @@ export type BindContext<
|
||||
metrics?: Metrics;
|
||||
auditLog?: Audit;
|
||||
analytics?: Analytics;
|
||||
consentFactory?: ConsentFactoryProtocol;
|
||||
};
|
||||
|
||||
/** Production binders also receive the resolved Payload config. */
|
||||
|
||||
@@ -80,3 +80,23 @@ export type AnalyticsProtocol = {
|
||||
attributes?: Record<string, string | number | boolean>,
|
||||
): void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Minimal consent protocol surface. `IConsent` (in optional `@repo/core-consent`)
|
||||
* extends this — typechecks fail if narrowed below. Feature binders that
|
||||
* receive `ctx.consentFactory` see only this protocol type.
|
||||
*/
|
||||
export type ConsentGrantMeta = {
|
||||
method?: string;
|
||||
bannerVersion?: string;
|
||||
policyVersion?: string;
|
||||
};
|
||||
|
||||
export type ConsentProtocol = {
|
||||
grant(category: string, meta?: ConsentGrantMeta): Promise<void>;
|
||||
};
|
||||
|
||||
/** Factory that creates a per-user consent instance. Mirrors ConsentFactory in `@repo/core-consent`. */
|
||||
export type ConsentFactoryProtocol = (
|
||||
userId: string,
|
||||
) => Promise<ConsentProtocol>;
|
||||
|
||||
Reference in New Issue
Block a user