From c3fd18e1a2e7a2a897f296d4d827ea99582a6dbf Mon Sep 17 00:00:00 2001 From: Heinz Baumann Date: Mon, 2 Jun 2025 11:43:37 -0930 Subject: [PATCH 1/3] initial code to support TCF 2.2 --- .../src/cmpapi/command/GetFieldCommand.ts | 7 ++- .../src/cmpapi/command/GetSectionCommand.ts | 8 ++- modules/cmpapi/src/encoder/section/TcfEuV2.ts | 34 ++++++++++-- .../src/encoder/segment/TcfEuV2CoreSegment.ts | 2 +- modules/cmpapi/test/GppModel.test.ts | 55 ++++++++++++++++--- .../test/encoder/section/TcfEuV2.test.ts | 6 +- 6 files changed, 91 insertions(+), 21 deletions(-) diff --git a/modules/cmpapi/src/cmpapi/command/GetFieldCommand.ts b/modules/cmpapi/src/cmpapi/command/GetFieldCommand.ts index 062cd34..7d58a4f 100644 --- a/modules/cmpapi/src/cmpapi/command/GetFieldCommand.ts +++ b/modules/cmpapi/src/cmpapi/command/GetFieldCommand.ts @@ -13,7 +13,12 @@ export class GetFieldCommand extends Command { let sectionName = parts[0]; let fieldName = parts[1]; - let fieldValue = this.cmpApiContext.gppModel.getFieldValue(sectionName, fieldName); + let fieldValue = null; + // Since TCF 2.2 no fields are allowed to be called directly. Data always needs to be retrieved using the + // AddEventListener callback + if (this.parameter != "tcfeuv2") { + fieldValue = this.cmpApiContext.gppModel.getFieldValue(sectionName, fieldName); + } this.invokeCallback(fieldValue); } } diff --git a/modules/cmpapi/src/cmpapi/command/GetSectionCommand.ts b/modules/cmpapi/src/cmpapi/command/GetSectionCommand.ts index 81d0c9f..77ef551 100644 --- a/modules/cmpapi/src/cmpapi/command/GetSectionCommand.ts +++ b/modules/cmpapi/src/cmpapi/command/GetSectionCommand.ts @@ -7,8 +7,12 @@ export class GetSectionCommand extends Command { } let section = null; - if (this.cmpApiContext.gppModel.hasSection(this.parameter)) { - section = this.cmpApiContext.gppModel.getSection(this.parameter); + // Since TCF 2.2 no fields are allowed to be called directly. Data always needs to be retrieved using the + // AddEventListener callback + if (this.parameter != "tcfeuv2") { + if (this.cmpApiContext.gppModel.hasSection(this.parameter)) { + section = this.cmpApiContext.gppModel.getSection(this.parameter); + } } this.invokeCallback(section); } diff --git a/modules/cmpapi/src/encoder/section/TcfEuV2.ts b/modules/cmpapi/src/encoder/section/TcfEuV2.ts index 08f2ad7..ca1915b 100644 --- a/modules/cmpapi/src/encoder/section/TcfEuV2.ts +++ b/modules/cmpapi/src/encoder/section/TcfEuV2.ts @@ -113,13 +113,35 @@ export class TcfEuV2 extends AbstractLazilyEncodableSection { //Overriden public setFieldValue(fieldName: string, value: any): void { - super.setFieldValue(fieldName, value); + // + // special handling for exceptions based on TCF policy and specification + // + // purpose 1, 3 - 6 legitimate interest is always false + if (fieldName === TcfEuV2Field.PURPOSE_LEGITIMATE_INTERESTS) { + value[0] = false; + value[2] = value[3] = value[4] = value[5] = false; + } + + // create and last update will need to stay in sync + if (fieldName === TcfEuV2Field.CREATED || fieldName === TcfEuV2Field.LAST_UPDATED) { + if (fieldName === TcfEuV2Field.CREATED) { + super.setFieldValue(TcfEuV2Field.LAST_UPDATED, value); + } else { + super.setFieldValue(TcfEuV2Field.CREATED, value); + } + } else + // always update the date stamp with a change occurs + this.updateDateStamp(); - if (fieldName !== TcfEuV2Field.CREATED && fieldName !== TcfEuV2Field.LAST_UPDATED) { - let date = new Date(); + // update the actual given fields + super.setFieldValue(fieldName, value); + } - super.setFieldValue(TcfEuV2Field.CREATED, date); - super.setFieldValue(TcfEuV2Field.LAST_UPDATED, date); - } + // update last_update and create time stamps when a change occurs + private updateDateStamp(): void { + const date = new Date(); + const utcDate = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); + super.setFieldValue(TcfEuV2Field.CREATED, utcDate); + super.setFieldValue(TcfEuV2Field.LAST_UPDATED, utcDate); } } diff --git a/modules/cmpapi/src/encoder/segment/TcfEuV2CoreSegment.ts b/modules/cmpapi/src/encoder/segment/TcfEuV2CoreSegment.ts index a165bc1..7f9ccdc 100644 --- a/modules/cmpapi/src/encoder/segment/TcfEuV2CoreSegment.ts +++ b/modules/cmpapi/src/encoder/segment/TcfEuV2CoreSegment.ts @@ -44,7 +44,7 @@ export class TcfEuV2CoreSegment extends AbstractLazilyEncodableSegment { expect(gppModel.hasSection("ustn")).to.eql(false); gppModel.setFieldValue("tcfeuv2", "Version", 2); + gppModel.setFieldValue("tcfeuv2", "CmpId", 880); + gppModel.setFieldValue("tcfcav1", "Version", 1); gppModel.setFieldValue("tcfeuv2", "Created", utcDateTime); gppModel.setFieldValue("tcfeuv2", "LastUpdated", utcDateTime); - gppModel.setFieldValue("tcfcav1", "Version", 1); gppModel.setFieldValue("tcfcav1", "Created", utcDateTime); gppModel.setFieldValue("tcfcav1", "LastUpdated", utcDateTime); gppModel.setFieldValue("uspv1", "Version", 1); @@ -106,7 +107,7 @@ describe("manifest.GppModel", (): void => { let gppString = gppModel.encode(); expect(gppString).to.eql( - "DBACOdM~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAAAA.QAAA.IAAA~BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA~1---~BAAAAAAAAABA.QA~BAAAAABA.QA~BAAAABA~BAAAAEA.QA~BAAAAAQA~BAAAAAEA.QA~BAAAAABA~BAAAAABA.QA~BAAAAAABAA.QA~BAAAAAQA.QA~BAAAAAABAA.QA~BAAAAAQA.QA~BAAAAAQA.QA~BAAAAABA.QA~BAAAAAAAQA.QA~BAAAAAQA.QA" + "DBACOdM~CPSG_8APSG_8ANwAAAENAAFAAAAAAAAAAAAAAAAAAAAA.QAAA.IAAA~BPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAA~1---~BAAAAAAAAABA.QA~BAAAAABA.QA~BAAAABA~BAAAAEA.QA~BAAAAAQA~BAAAAAEA.QA~BAAAAABA~BAAAAABA.QA~BAAAAAABAA.QA~BAAAAAQA.QA~BAAAAAABAA.QA~BAAAAAQA.QA~BAAAAAQA.QA~BAAAAABA.QA~BAAAAAAAQA.QA~BAAAAAQA.QA" ); }); @@ -665,7 +666,7 @@ describe("manifest.GppModel", (): void => { gppModel.setFieldValue("tcfeuv2", "LastUpdated", utcDateTime); let gppString = gppModel.encode(); - expect(gppString).to.eql("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAOAAAABAAAAA.QAAA.IAAA"); + expect(gppString).to.eql("DBABMA~CPSG_8APSG_8AAAAAAENAAFAAAAAAAAAAAAAAOAAAABAAAAA.QAAA.IAAA"); }); it("should encode tcfeuv2 vendor consents [29]", (): void => { @@ -675,7 +676,7 @@ describe("manifest.GppModel", (): void => { gppModel.setFieldValue("tcfeuv2", "LastUpdated", utcDateTime); let gppString = gppModel.encode(); - expect(gppString).to.eql("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAOwAQAOgAAAA.QAAA.IAAA"); + expect(gppString).to.eql("DBABMA~CPSG_8APSG_8AAAAAAENAAFAAAAAAAAAAAAAAOwAQAOgAAAA.QAAA.IAAA"); }); it("should encode tcfeuv2 vendor consents [1, 173, 722]", (): void => { @@ -685,7 +686,7 @@ describe("manifest.GppModel", (): void => { gppModel.setFieldValue("tcfeuv2", "LastUpdated", utcDateTime); let gppString = gppModel.encode(); - expect(gppString).to.eql("DBABMA~CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAFpQAwAAgCtAWkAAAAAAA.QAAA.IAAA"); + expect(gppString).to.eql("DBABMA~CPSG_8APSG_8AAAAAAENAAFAAAAAAAAAAAAAFpQAwAAgCtAWkAAAAAAA.QAAA.IAAA"); }); it("should decode tcfeuv2 vendor consents [28]", (): void => { @@ -765,15 +766,28 @@ describe("manifest.GppModel", (): void => { let fromObjectModel = new GppModel(); fromObjectModel.setFieldValue("tcfeuv2", "PurposeConsents", [ true, + false, true, true, + false, + false, true, true, true, true, true, + ]); + fromObjectModel.setFieldValue("tcfeuv2", "PurposeLegitimateInterests", [ + true, + true, + false, + false, true, true, + false, + false, + false, + false, ]); fromObjectModel.setFieldValue("tcfeuv2", "VendorConsents", [32, 128, 81, 210, 755, 21, 173, 238]); @@ -783,14 +797,32 @@ describe("manifest.GppModel", (): void => { expect(decodedModel.getFieldValue("tcfeuv2", "PurposeConsents")).to.eql([ true, + false, true, true, + false, + false, true, true, true, true, true, - true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + ]); + expect(decodedModel.getFieldValue("tcfeuv2", "PurposeLegitimateInterests")).to.eql([ + false, true, false, false, @@ -806,8 +838,15 @@ describe("manifest.GppModel", (): void => { false, false, false, - ]); - + false, + false, + false, + false, + false, + false, + false, + false + ]) expect(decodedModel.getFieldValue("tcfeuv2", "VendorConsents")).to.eql([21, 32, 81, 128, 173, 210, 238, 755]); }); diff --git a/modules/cmpapi/test/encoder/section/TcfEuV2.test.ts b/modules/cmpapi/test/encoder/section/TcfEuV2.test.ts index bc19132..7a96b21 100644 --- a/modules/cmpapi/test/encoder/section/TcfEuV2.test.ts +++ b/modules/cmpapi/test/encoder/section/TcfEuV2.test.ts @@ -7,15 +7,15 @@ describe("manifest.section.TcfEuV2", (): void => { let tcfEuV2 = new TcfEuV2(); tcfEuV2.setFieldValue(TcfEuV2Field.CREATED, new Date("2022-01-01T00:00:00Z")); tcfEuV2.setFieldValue(TcfEuV2Field.LAST_UPDATED, new Date("2022-01-01T00:00:00Z")); - expect(tcfEuV2.encode()).to.eql("CPSG_8APSG_8AAAAAAENAACAAAAAAAAAAAAAAAAAAAAA.QAAA.IAAA"); + expect(tcfEuV2.encode()).to.eql("CPSG_8APSG_8AAAAAAENAAFAAAAAAAAAAAAAAAAAAAAA.QAAA.IAAA"); }); it("encode with service specific", (): void => { let tcfEuV2 = new TcfEuV2(); tcfEuV2.setFieldValue("IsServiceSpecific", true); tcfEuV2.setFieldValue("Created", new Date("2022-01-01T00:00:00Z")); - tcfEuV2.setFieldValue("LastUpdated", new Date("2022-01-01T00:00:00Z")); - expect(tcfEuV2.encode()).to.eql("CPSG_8APSG_8AAAAAAENAACgAAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAAA"); + tcfEuV2.setFieldValue("LastUpdated", new Date("2022-02-01T00:00:00Z")); + expect(tcfEuV2.encode()).to.eql("CPTtLAAPTtLAAAAAAAENAAFgAAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAAA"); }); it("decode defaults", (): void => { From dde8dcbef49ff5806d2d4ab0883cfbb5e3335f7c Mon Sep 17 00:00:00 2001 From: Heinz Baumann Date: Mon, 2 Jun 2025 12:15:31 -0930 Subject: [PATCH 2/3] added new langauge --- modules/cmpapi/src/gvl/gvlmodel/ConsentLanguages.ts | 2 ++ modules/cmpapi/test/GVLV2_2.test.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/cmpapi/src/gvl/gvlmodel/ConsentLanguages.ts b/modules/cmpapi/src/gvl/gvlmodel/ConsentLanguages.ts index c6bf220..0c4107f 100644 --- a/modules/cmpapi/src/gvl/gvlmodel/ConsentLanguages.ts +++ b/modules/cmpapi/src/gvl/gvlmodel/ConsentLanguages.ts @@ -17,6 +17,7 @@ export class ConsentLanguages { 'FR', 'GL', 'HE', + 'HI', 'HR', 'HU', 'ID', @@ -49,6 +50,7 @@ export class ConsentLanguages { 'UK', 'VI', 'ZH', + 'ZH-HANT', ]); public has(key: string): boolean { diff --git a/modules/cmpapi/test/GVLV2_2.test.ts b/modules/cmpapi/test/GVLV2_2.test.ts index f39b9e3..e46f44a 100644 --- a/modules/cmpapi/test/GVLV2_2.test.ts +++ b/modules/cmpapi/test/GVLV2_2.test.ts @@ -210,7 +210,7 @@ describe("GVL", (): void => { it("number of language translations should match", (): void => { const langSet = new ConsentLanguages(); - expect(langSet.size).equal(49); + expect(langSet.size).equal(51); expect(langSet.has("VI")).equal(true); }); From 728ed32d31163ac5b1c4a280215bacf6f45cacbc Mon Sep 17 00:00:00 2001 From: Heinz Baumann Date: Mon, 2 Jun 2025 15:04:08 -0930 Subject: [PATCH 3/3] Added support for TCF 2.3 --- modules/cmpapi/src/encoder/section/TcfEuV2.ts | 3 +++ modules/cmpapi/test/GppModel.test.ts | 11 ++++++++--- modules/cmpapi/test/encoder/section/TcfEuV2.test.ts | 7 ++++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/modules/cmpapi/src/encoder/section/TcfEuV2.ts b/modules/cmpapi/src/encoder/section/TcfEuV2.ts index ca1915b..cd5060e 100644 --- a/modules/cmpapi/src/encoder/section/TcfEuV2.ts +++ b/modules/cmpapi/src/encoder/section/TcfEuV2.ts @@ -98,6 +98,9 @@ export class TcfEuV2 extends AbstractLazilyEncodableSection { if (segments.length >= 2) { encodedSegments.push(segments[1].encode()); } + if (segments.length >= 3) { + encodedSegments.push(segments[3].encode()); + } } else { if (segments.length >= 2) { encodedSegments.push(segments[2].encode()); diff --git a/modules/cmpapi/test/GppModel.test.ts b/modules/cmpapi/test/GppModel.test.ts index e182bed..db093e1 100644 --- a/modules/cmpapi/test/GppModel.test.ts +++ b/modules/cmpapi/test/GppModel.test.ts @@ -146,12 +146,17 @@ describe("manifest.GppModel", (): void => { gppModel.setFieldValue("tcfeuv2", "ConsentLanguage", "EN"); gppModel.setFieldValue("tcfeuv2", "VendorListVersion", 48); gppModel.setFieldValue("tcfeuv2", "PolicyVersion", 2); - gppModel.setFieldValue("tcfeuv2", "IsServiceSpecific", false); + gppModel.setFieldValue("tcfeuv2", "IsServiceSpecific", true); gppModel.setFieldValue("tcfeuv2", "UseNonStandardStacks", false); gppModel.setFieldValue("tcfeuv2", "PurposeOneTreatment", false); - gppModel.setFieldValue("tcfeuv2", "PublisherCountryCode", "AA"); + gppModel.setFieldValue("tcfeuv2", "PublisherCountryCode", "DE"); gppModel.setFieldValue("tcfeuv2", "Created", utcDateTime); gppModel.setFieldValue("tcfeuv2", "LastUpdated", utcDateTime); + + // set a few vendors + gppModel.setFieldValue("tcfeuv2", "VendorConsents", [1, 2, 3, 4]); + gppModel.setFieldValue("tcfeuv2", "VendorLegitimateInterests", []); + gppModel.setFieldValue("tcfeuv2", "VendorsDisclosed", [1, 2, 3, 4, 5, 100, 404]); expect(gppModel.getSectionIds()).to.eql([2]); expect(gppModel.hasSection("uspv1")).to.eql(false); @@ -159,7 +164,7 @@ describe("manifest.GppModel", (): void => { expect(gppModel.hasSection("tcfcav1")).to.eql(false); let gppString = gppModel.encode(); - expect(gppString).to.eql("DBABMA~CPSG_8APSG_8ANwAAAENAwCAAAAAAAAAAAAAAAAAAAAA.QAAA.IAAA"); + expect(gppString).to.eql("DBABMA~CQSbk4AQSbk4ANwAAAENAwCgAAAAAAAAAAYgACPAAAAA.YAAAAAAAAAAA.IDKQA4AAgAKAGQAygAAA"); expect(gppString.split("~").length).to.eql(2); diff --git a/modules/cmpapi/test/encoder/section/TcfEuV2.test.ts b/modules/cmpapi/test/encoder/section/TcfEuV2.test.ts index 7a96b21..04c7560 100644 --- a/modules/cmpapi/test/encoder/section/TcfEuV2.test.ts +++ b/modules/cmpapi/test/encoder/section/TcfEuV2.test.ts @@ -15,7 +15,7 @@ describe("manifest.section.TcfEuV2", (): void => { tcfEuV2.setFieldValue("IsServiceSpecific", true); tcfEuV2.setFieldValue("Created", new Date("2022-01-01T00:00:00Z")); tcfEuV2.setFieldValue("LastUpdated", new Date("2022-02-01T00:00:00Z")); - expect(tcfEuV2.encode()).to.eql("CPTtLAAPTtLAAAAAAAENAAFgAAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAAA"); + expect(tcfEuV2.encode()).to.eql("CPTtLAAPTtLAAAAAAAENAAFgAAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAAA.IAAA"); }); it("decode defaults", (): void => { @@ -165,7 +165,7 @@ describe("manifest.section.TcfEuV2", (): void => { }); it("decode service specific", (): void => { - let tcfEuV2 = new TcfEuV2("CPSG_8APSG_8AAAAAAENAACgAAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAAA"); + let tcfEuV2 = new TcfEuV2("CPSG_8APSG_8AAAAAAENAACgAAAAAAAAAAAAAAAAAAAA.YAAAAAAAAAAA.IAAA"); expect(tcfEuV2.getFieldValue("Version")).to.eql(2); expect(tcfEuV2.getFieldValue("Created")).to.eql(new Date("2022-01-01T00:00:00Z")); expect(tcfEuV2.getFieldValue("LastUpdated")).to.eql(new Date("2022-01-01T00:00:00Z")); @@ -557,7 +557,7 @@ describe("manifest.section.TcfEuV2", (): void => { }); it("decode 4", (): void => { - let tcfEuV2 = new TcfEuV2("COv_eg6Ov_eg6AOADBENAaCgAP_AAH_AACiQAVEUQQoAIQAqIoghAAQgAA.YAAAAAAAAAAAAAAAAAA"); + let tcfEuV2 = new TcfEuV2("COv_eg6Ov_eg6AOADBENAaCgAP_AAH_AACiQAVEUQQoAIQAqIoghAAQgAA.YAAAAAAAAAAAAAAAAAA.IGLtV_T9fb2vj-_Z99_tkeYwf95y3p-wzhheMs-8NyZeH_B4Wv2MyvBX4JiQKGRgksjLBAQdtHGlcTQgBwIlViTLMYk2MjzNKJrJEilsbO2dYGD9Pn8HT3ZCY70-vv__7v3ff_3g"); expect(tcfEuV2.getFieldValue("Version")).to.eql(2); expect(tcfEuV2.getFieldValue("CmpId")).to.eql(14); @@ -642,6 +642,7 @@ describe("manifest.section.TcfEuV2", (): void => { expect(tcfEuV2.getFieldValue("VendorConsents")).to.eql([2, 6, 8, 12, 18, 23, 25, 37, 42]); expect(tcfEuV2.getFieldValue("VendorLegitimateInterests")).to.eql([2, 6, 8, 12, 18, 23, 37, 42]); + expect(tcfEuV2.getFieldValue("VendorsDisclosed").length).to.eql(434); }); it("should throw Error on garbage 1", (): void => {