Skip to content

Commit

Permalink
Allow looking up a given tcfeu vendor id
Browse files Browse the repository at this point in the history
  • Loading branch information
zapo committed Dec 19, 2024
1 parent 93a4c66 commit 5e59a4d
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 27 deletions.
16 changes: 12 additions & 4 deletions lib/config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe("getConfig", () => {
site: "site",
cookies: false,
initPassport: false,
consent: { deviceAccess: true, reg: "us" },
consent: { static: { deviceAccess: true, reg: "us" } },
})
).toEqual({
host: "host",
Expand All @@ -30,12 +30,20 @@ describe("getConfig", () => {
});
});

it("resolves to globalConsent when using auto", () => {
it("infers regulation and gathers consent when using cmpapi", () => {
const spy = jest.spyOn(Intl, "DateTimeFormat").mockImplementation(() => ({
resolvedOptions: () => ({
timeZone: "America/New_York",
}),
}));

const config = getConfig({
host: "host",
site: "site",
consent: "auto",
consent: { cmpapi: {} },
});
expect(config.consent).toEqual(globalConsent);
expect(config.consent).toEqual({ deviceAccess: true, reg: "us" });

spy.mockRestore();
});
});
32 changes: 21 additions & 11 deletions lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,49 @@
import globalConsent, { Consent } from "./core/regs/consent";
import { getConsent, Consent, inferRegulation } from "./core/regs/consent";

type CMPApiConfig = {
tcfeuVendorID?: number;
};

type InitConsent = {
cmpapi?: CMPApiConfig;
static?: Consent;
};

type InitConfig = {
host: string;
site: string;
cookies?: boolean;
initPassport?: boolean;
consent?: Consent | "auto";
consent?: InitConsent;
};

type ResolvedConfig = Required<InitConfig> & {
type ResolvedConfig = Required<Omit<InitConfig, "consent">> & {
consent: Consent;
};

const DCN_DEFAULTS = {
cookies: true,
initPassport: true,
// Backwards compatibility, default to device access allowed
// Once we have measured the impact of automatic CMP integration in the wild
// we may move to "auto" default.
consent: { deviceAccess: true, reg: null } as Consent,
consent: { deviceAccess: true, reg: null },
};

function getConfig(init: InitConfig): ResolvedConfig {
const config = {
const config: ResolvedConfig = {
host: init.host,
site: init.site,
cookies: init.cookies ?? DCN_DEFAULTS.cookies,
initPassport: init.initPassport ?? DCN_DEFAULTS.initPassport,
consent: DCN_DEFAULTS.consent,
};

if (init.consent) {
config.consent = init.consent === "auto" ? globalConsent : init.consent;
if (init.consent?.static) {
config.consent = init.consent.static;
} else if (init.consent?.cmpapi) {
config.consent = getConsent(inferRegulation(), init.consent.cmpapi);
}

return config;
}

export { InitConfig, ResolvedConfig, getConfig };
export type { InitConsent, CMPApiConfig, InitConfig, ResolvedConfig };
export { getConfig };
35 changes: 34 additions & 1 deletion lib/core/regs/consent.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ describe("getConsent", () => {
it("updates consent based on gpp signals for gdpr", () => {
const signal = mockGPPSignal(windowSpy);

const consent = getConsent("gdpr");
let consent = getConsent("gdpr");
// By default device access is denied
expect(consent.deviceAccess).toBe(false);
expect(consent.reg).toBe("gdpr");
Expand Down Expand Up @@ -177,6 +177,39 @@ describe("getConsent", () => {
expect(consent.deviceAccess).toBe(false);
expect(consent.gpp).toBe("revoked");
expect(consent.gppSectionIDs).toEqual([gpp.tcfeuv2.SectionID]);

// Given a specific vendor ID
consent = getConsent("gdpr", { tcfeuVendorID: 42 });
signal({
parsedSections: {
[gpp.tcfeuv2.APIPrefix]: [
// Core segment
{ Version: 2, PurposeConsent: [1], VendorConsent: [42] },
{ SegmentType: 3, PubPurposesConsent: [] },
],
},
applicableSections: [gpp.tcfeuv2.SectionID],
gppString: "vendor42",
});
expect(consent.deviceAccess).toBe(true);
expect(consent.gpp).toBe("vendor42");
expect(consent.gppSectionIDs).toEqual([gpp.tcfeuv2.SectionID]);

signal({
parsedSections: {
[gpp.tcfeuv2.APIPrefix]: [
// Core segment
{ Version: 2, PurposeConsent: [1], VendorConsent: [] },
{ SegmentType: 3, PubPurposesConsent: [1] },
],
},
applicableSections: [gpp.tcfeuv2.SectionID],
gppString: "vendor42notgranted",
});

expect(consent.deviceAccess).toBe(false);
expect(consent.gpp).toBe("vendor42notgranted");
expect(consent.gppSectionIDs).toEqual([gpp.tcfeuv2.SectionID]);
});

it("updates consent based on gpp signals for can", () => {
Expand Down
44 changes: 34 additions & 10 deletions lib/core/regs/consent.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { inferRegulation, Regulation } from "./regulations";
import type { CMPApiConfig } from "config";
import type { Regulation } from "./regulations";
import { inferRegulation } from "./regulations";
import * as gpp from "./gpp";
import * as tcf from "./tcf";

type Consent = {
// Whether the device access is granted
deviceAccess: boolean;
// The regulation that was detected, null if unknown
reg: Regulation | null;
// The TCF string if available, only when reg is "gdpr"
tcf?: string;
// The GPP string if available
gpp?: string;
// The GPP section IDs that are applicable
gppSectionIDs?: number[];
};

Expand All @@ -33,19 +40,19 @@ const usSectionIDs = [
gpp.usva.SectionID,
];

function gdprConsent(): Consent {
function gdprConsent(tcfeuVendorID?: number): Consent {
const consent: Consent = { deviceAccess: false, reg: "gdpr" };

// Use TCF if available, otherwise use GPP,
// if none available assume device access is not allowed
if (hasTCF()) {
onTCFChange((data) => {
consent.deviceAccess = tcfDeviceAccess(data);
consent.deviceAccess = tcfDeviceAccess(data, tcfeuVendorID);
consent.tcf = data.tcString;
});
} else if (hasGPP()) {
onGPPSectionChange(gdprSectionIDs, (data) => {
consent.deviceAccess = gppEUDeviceAccess(data);
consent.deviceAccess = gppEUDeviceAccess(data, tcfeuVendorID);
consent.gpp = data.gppString;
consent.gppSectionIDs = data.applicableSections;
});
Expand Down Expand Up @@ -74,10 +81,10 @@ function canConsent(): Consent {
return consent;
}

function getConsent(reg: Regulation | null): Consent {
function getConsent(reg: Regulation | null, conf: CMPApiConfig = {}): Consent {
switch (reg) {
case "gdpr":
return gdprConsent();
return gdprConsent(conf.tcfeuVendorID);
case "us":
return usConsent();
case "can":
Expand All @@ -87,12 +94,24 @@ function getConsent(reg: Regulation | null): Consent {
}
}

function gppEUDeviceAccess(data: gpp.cmpapi.PingReturn): boolean {
function gppEUDeviceAccess(data: gpp.cmpapi.PingReturn, vendorID?: number): boolean {
if (!(gpp.tcfeuv2.APIPrefix in data.parsedSections)) {
return false;
}

const section = data.parsedSections[gpp.tcfeuv2.APIPrefix] || [];

if (typeof vendorID === "number") {
const coreSegment = section.find((s) => {
return "Version" in s;
});
if (!coreSegment) {
return false;
}

return coreSegment.PurposeConsent.includes(1) && coreSegment.VendorConsent.includes(vendorID);
}

const publisherSubsection = section.find((s) => {
return "SegmentType" in s && s.SegmentType === 3;
});
Expand All @@ -104,10 +123,15 @@ function gppEUDeviceAccess(data: gpp.cmpapi.PingReturn): boolean {
return publisherSubsection.PubPurposesConsent.includes(1);
}

function tcfDeviceAccess(data: tcf.cmpapi.TCData): boolean {
function tcfDeviceAccess(data: tcf.cmpapi.TCData, vendorID?: number): boolean {
if (!data.gdprApplies) {
return true;
}

if (vendorID) {
return data.purpose.consents["1"] && data.vendor.consents[vendorID];
}

return !!data.publisher.consents["1"];
}

Expand Down Expand Up @@ -162,5 +186,5 @@ function hasTCF(): boolean {
return typeof window.__tcfapi === "function";
}

export default getConsent(inferRegulation());
export { Consent, getConsent };
export { getConsent, inferRegulation };
export type { Consent };
3 changes: 2 additions & 1 deletion lib/core/regs/regulations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,5 @@ function inferRegulation(): Regulation | null {
return regulation ?? null;
}

export { inferRegulation, Regulation };
export { inferRegulation };
export type { Regulation };

0 comments on commit 5e59a4d

Please sign in to comment.