├── .gitattributes ├── .jsdoc.js ├── .travis.yml ├── .editorconfig ├── src ├── index.js ├── decode.js ├── index.d.ts ├── utils │ ├── definitions.js │ └── bits.js ├── encode.js └── consent-string.js ├── .eslintrc.js ├── .babelrc ├── test ├── .eslintrc.js ├── decode.test.js ├── encode.test.js ├── consent-string.test.js ├── utils │ └── bits.test.js └── vendors.json ├── LICENSE ├── consent_string_use_cases.md ├── package.json ├── .gitignore ├── README.md └── consent_string_methods.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js -crlf 2 | -------------------------------------------------------------------------------- /.jsdoc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | source: { 3 | include: [ 4 | 'README.md', 5 | 'src/consent-string.js', 6 | ], 7 | }, 8 | opts: { 9 | destination: 'docs/', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - stable 5 | 6 | install: 7 | - npm install 8 | 9 | script: 10 | - npm run coverage 11 | 12 | # Send coverage data to Coveralls 13 | after_script: "cat coverage/lcov.info | node_modules/coveralls/bin/coveralls.js" 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { ConsentString } = require('./consent-string'); 2 | const { decodeConsentString } = require('./decode'); 3 | const { encodeConsentString } = require('./encode'); 4 | 5 | module.exports = { 6 | ConsentString, 7 | decodeConsentString, 8 | encodeConsentString, 9 | }; 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb-base", 3 | "plugins": [ 4 | "import" 5 | ], 6 | "env": { 7 | "node": true 8 | }, 9 | "rules": { 10 | "no-console": "error", 11 | "no-prototype-builtins": "off", 12 | "max-len": ["error", 100, 2, { 13 | "ignoreUrls": true, 14 | "ignoreComments": true, 15 | "ignoreRegExpLiterals": true, 16 | "ignoreStrings": true, 17 | "ignoreTemplateLiterals": true, 18 | }], 19 | 'no-param-reassign': 'off', 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "browsers": ["last 2 versions", "> 5% in BE"], 6 | "node": "6.10" 7 | }, 8 | "modules": false 9 | }] 10 | ], 11 | "plugins": [ 12 | "transform-object-rest-spread", 13 | "transform-es2015-arrow-functions", 14 | "transform-es2015-classes", 15 | "transform-es2015-destructuring", 16 | "transform-es2015-modules-commonjs", 17 | "transform-es2015-object-super", 18 | "transform-class-properties" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'plugins': [ 3 | 'mocha' 4 | ], 5 | 'env': { 6 | 'mocha': true 7 | }, 8 | 'rules': { 9 | 'prefer-arrow-callback': 'off', 10 | 'func-names': 'off', 11 | 'no-unused-expressions': 'off', 12 | 13 | 'mocha/handle-done-callback': 'error', 14 | 'mocha/no-exclusive-tests': 'error', 15 | 'mocha/no-global-tests': 'error', 16 | 'mocha/no-identical-title': 'error', 17 | 'mocha/no-mocha-arrows': 'error', 18 | 'mocha/no-nested-tests': 'error', 19 | 'mocha/no-return-and-callback': 'error', 20 | 'mocha/no-skipped-tests': 'error', 21 | 'mocha/no-top-level-hooks': 'error', 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /consent_string_use_cases.md: -------------------------------------------------------------------------------- 1 | ## Consent String Use cases 2 | 3 | ### Vendors 4 | 5 | Vendors that receive a consent string encoded by a Consent Management Platform, on a webpage or in a mobile application, can decode the string and determine if they the user has given consent to their specific purpose and vendor IDs. 6 | 7 | **Example:** 8 | 9 | Assuming you are the vendor with ID 1 and want to check that the user has given consent to access her device (purpose 1) and personalize advertizing (purpose 2): 10 | 11 | ```javascript 12 | const { ConsentString } = require('consent-string'); 13 | 14 | const consentData = new ConsentString('encoded base64 consent string received'); 15 | 16 | if ( 17 | consentData.isVendorAllowed(1) 18 | && consentData.isPurposeAllowed(1) 19 | && consentData.isPurposeAllowed(2) 20 | ) { 21 | // The vendor ID and the purposes are all allowed 22 | // Process with your data collection / processing 23 | } else { 24 | // Either the vendor or one of the purposes is not allowed by the user 25 | // Do not collect or process personal data 26 | } 27 | ``` 28 | 29 | ### Consent management platforms 30 | 31 | CMPs can read a cookie, modify its content then update the cookie value with the correct encoding. 32 | 33 | ```javascript 34 | const { ConsentString } = require('consent-string'); 35 | 36 | // Decode the base64-encoded consent string contained in the cookie 37 | const consentData = new ConsentString(readCookieValue()); 38 | 39 | // Modify the consent data 40 | consentData.setCmpId(1); 41 | consentData.setConsentScreen(1); 42 | consentData.setPurposeAllowed(12, true); 43 | 44 | // Update the cookie value 45 | writeCookieValue(consentData.getConsentString()); 46 | ``` 47 | 48 | **Note:** CMPs need to manage the cookie operations (reading and writing) themselves. 49 | 50 | -------------------------------------------------------------------------------- /src/decode.js: -------------------------------------------------------------------------------- 1 | const { 2 | decodeBitsToIds, 3 | decodeFromBase64, 4 | } = require('./utils/bits'); 5 | 6 | /** 7 | * Decode consent data from a web-safe base64-encoded string 8 | * 9 | * @param {string} consentString 10 | */ 11 | function decodeConsentString(consentString) { 12 | const { 13 | version, 14 | cmpId, 15 | vendorListVersion, 16 | purposeIdBitString, 17 | maxVendorId, 18 | created, 19 | lastUpdated, 20 | isRange, 21 | defaultConsent, 22 | vendorIdBitString, 23 | vendorRangeList, 24 | cmpVersion, 25 | consentScreen, 26 | consentLanguage, 27 | } = decodeFromBase64(consentString); 28 | 29 | const consentStringData = { 30 | version, 31 | cmpId, 32 | vendorListVersion, 33 | allowedPurposeIds: decodeBitsToIds(purposeIdBitString), 34 | maxVendorId, 35 | created, 36 | lastUpdated, 37 | cmpVersion, 38 | consentScreen, 39 | consentLanguage, 40 | }; 41 | 42 | if (isRange) { 43 | /* eslint no-shadow: off */ 44 | const idMap = vendorRangeList.reduce((acc, { isRange, startVendorId, endVendorId }) => { 45 | const lastVendorId = isRange ? endVendorId : startVendorId; 46 | 47 | for (let i = startVendorId; i <= lastVendorId; i += 1) { 48 | acc[i] = true; 49 | } 50 | 51 | return acc; 52 | }, {}); 53 | 54 | consentStringData.allowedVendorIds = []; 55 | 56 | for (let i = 1; i <= maxVendorId; i += 1) { 57 | if ( 58 | (defaultConsent && !idMap[i]) || 59 | (!defaultConsent && idMap[i]) 60 | ) { 61 | if (consentStringData.allowedVendorIds.indexOf(i) === -1) { 62 | consentStringData.allowedVendorIds.push(i); 63 | } 64 | } 65 | } 66 | } else { 67 | consentStringData.allowedVendorIds = decodeBitsToIds(vendorIdBitString); 68 | } 69 | 70 | return consentStringData; 71 | } 72 | 73 | module.exports = { 74 | decodeConsentString, 75 | }; 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "consent-string", 3 | "version": "1.5.2", 4 | "description": "Encode and decode web-safe base64 consent information with the IAB EU's GDPR Transparency and Consent Framework", 5 | "homepage": "https://github.com/InteractiveAdvertisingBureau/Consent-String-SDK-JS", 6 | "repository": "https://github.com/InteractiveAdvertisingBureau/Consent-String-SDK-JS", 7 | "keywords": [ 8 | "consent", 9 | "gdpr", 10 | "iab" 11 | ], 12 | "license": "MIT", 13 | "main": "dist/index.js", 14 | "types": "dist/index.d.ts", 15 | "files": [ 16 | "dist" 17 | ], 18 | "scripts": { 19 | "test": "mocha test/ --recursive", 20 | "debug": "ndb npm test", 21 | "coverage": "nyc --reporter=html --reporter=text-summary --reporter=lcov --check-coverage --lines 60 --functions 60 --branches 60 mocha test/ --recursive", 22 | "lint": "eslint src/. test/.", 23 | "docs": "jsdoc -c .jsdoc.js -r", 24 | "build": "babel src --out-dir dist && cp src/index.d.ts dist/", 25 | "prepublishOnly":"npm run build" 26 | }, 27 | "dependencies": { 28 | "base-64": "^0.1.0" 29 | }, 30 | "devDependencies": { 31 | "babel-cli": "^6.26.0", 32 | "babel-plugin-transform-class-properties": "^6.24.1", 33 | "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", 34 | "babel-plugin-transform-es2015-classes": "^6.24.1", 35 | "babel-plugin-transform-es2015-destructuring": "^6.23.0", 36 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", 37 | "babel-plugin-transform-es2015-object-super": "^6.24.1", 38 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 39 | "babel-preset-env": "^1.6.1", 40 | "chai": "^4.0.2", 41 | "coveralls": "^3.0.0", 42 | "eslint": "^4.0.0", 43 | "eslint-config-airbnb-base": "^11.2.0", 44 | "eslint-plugin-import": "^2.3.0", 45 | "eslint-plugin-mocha": "^4.11.0", 46 | "jsdoc": "^3.5.5", 47 | "mocha": "^3.4.2", 48 | "ndb": "^1.1.4", 49 | "nyc": "^11.0.2", 50 | "sinon": "^4.5.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for consent-string 2 | 3 | export interface Purpose { 4 | id: number; 5 | name: string; 6 | description: string; 7 | } 8 | 9 | export interface Feature { 10 | id: number; 11 | name: string; 12 | description: string; 13 | } 14 | 15 | export interface Vendor { 16 | deletedDate?: string; 17 | id: number; 18 | featureIds: number[]; 19 | legIntPurposeIds: number[]; 20 | name: string; 21 | policyUrl: string; 22 | purposeIds: number[]; 23 | } 24 | 25 | export interface VendorList { 26 | features: Feature[]; 27 | purposes: Purpose[]; 28 | vendorListVersion: number; 29 | vendors: Vendor[]; 30 | } 31 | 32 | export class ConsentString { 33 | constructor(baseString?: string); 34 | 35 | private created: Date; 36 | private lastUpdated: Date; 37 | private version: number; 38 | private vendorList: VendorList; 39 | private vendorListVersion: number; 40 | private cmpId: number; 41 | private cmpVersion: number; 42 | private consentScreen: number; 43 | private consentLanguage: string; 44 | private allowedPurposeIds: number[]; 45 | private allowedVendorIds: number[]; 46 | 47 | public getConsentString(updateDate?:boolean): string; 48 | public getVersion(): number; 49 | public getVendorListVersion(): number; 50 | public setGlobalVendorList(vendorList: VendorList): void; 51 | public setCmpId(cmpId: number): void; 52 | public getCmpId(): number; 53 | public setCmpVersion(version: number): void; 54 | public getCmpVersion(): number; 55 | public setConsentScreen(screenId: number): void; 56 | public getConsentScreen(): number; 57 | public setConsentLanguage(language: string): void; 58 | public getConsentLanguage(): string; 59 | public setPurposesAllowed(purposeIds: number[]): void; 60 | public getPurposesAllowed(): number[]; 61 | public setPurposeAllowed(purposeId: number, value: boolean): void; 62 | public isPurposeAllowed(purposeId: number): boolean; 63 | public setVendorsAllowed(vendorIds: number[]): void; 64 | public getVendorsAllowed(): number[]; 65 | public setVendorAllowed(vendorId: number, value: boolean): void; 66 | public isVendorAllowed(vendorId: number): boolean; 67 | } 68 | 69 | export function decodeConsentString(consentString: string): ConsentString; 70 | 71 | export function encodeConsentString(consentData: ConsentString): string; 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | .nyc_output 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # Compiled binary addons (http://nodejs.org/api/addons.html) 21 | build/Release 22 | 23 | # Dependency directory 24 | # Commenting this out is preferred by some people, see 25 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 26 | node_modules 27 | 28 | # Users Environment Variables 29 | .lock-wscript 30 | 31 | # IDEs and editors (shamelessly copied from @angular/cli's .gitignore) 32 | /.idea 33 | .project 34 | .classpath 35 | .c9/ 36 | *.launch 37 | .settings/ 38 | *.sublime-workspace 39 | *.swp 40 | *.swo 41 | 42 | # IDE - VSCode 43 | .vscode/* 44 | !.vscode/settings.json 45 | !.vscode/tasks.json 46 | !.vscode/launch.json 47 | !.vscode/extensions.json 48 | 49 | ### Linux ### 50 | *~ 51 | 52 | # temporary files which can be created if a process still has a handle open of a deleted file 53 | .fuse_hidden* 54 | 55 | # KDE directory preferences 56 | .directory 57 | 58 | # Linux trash folder which might appear on any partition or disk 59 | .Trash-* 60 | 61 | # .nfs files are created when an open file is removed but is still being accessed 62 | .nfs* 63 | 64 | ### OSX ### 65 | *.DS_Store 66 | .AppleDouble 67 | .LSOverride 68 | 69 | # Icon must end with two \r 70 | Icon 71 | 72 | 73 | # Thumbnails 74 | ._* 75 | 76 | # Files that might appear in the root of a volume 77 | .DocumentRevisions-V100 78 | .fseventsd 79 | .Spotlight-V100 80 | .TemporaryItems 81 | .Trashes 82 | .VolumeIcon.icns 83 | .com.apple.timemachine.donotpresent 84 | 85 | # Directories potentially created on remote AFP share 86 | .AppleDB 87 | .AppleDesktop 88 | Network Trash Folder 89 | Temporary Items 90 | .apdisk 91 | 92 | ### Windows ### 93 | # Windows thumbnail cache files 94 | Thumbs.db 95 | ehthumbs.db 96 | ehthumbs_vista.db 97 | 98 | # Folder config file 99 | Desktop.ini 100 | 101 | # Recycle Bin used on file shares 102 | $RECYCLE.BIN/ 103 | 104 | # Windows Installer files 105 | *.cab 106 | *.msi 107 | *.msm 108 | *.msp 109 | 110 | # Windows shortcuts 111 | *.lnk 112 | 113 | # Others 114 | ./data/ 115 | build/ 116 | *.pyc 117 | *.zip 118 | dist/ 119 | test.js 120 | *.tgz 121 | -------------------------------------------------------------------------------- /src/utils/definitions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Number of bits for encoding the version integer 3 | * Expected to be the same across versions 4 | */ 5 | const versionNumBits = 6; 6 | 7 | /** 8 | * Definition of the consent string encoded format 9 | * 10 | * From https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/Draft_for_Public_Comment_Transparency%20%26%20Consent%20Framework%20-%20cookie%20and%20vendor%20list%20format%20specification%20v1.0a.pdf 11 | */ 12 | const vendorVersionMap = { 13 | /** 14 | * Version 1 15 | */ 16 | 1: { 17 | version: 1, 18 | metadataFields: ['version', 'created', 'lastUpdated', 'cmpId', 19 | 'cmpVersion', 'consentScreen', 'vendorListVersion'], 20 | fields: [ 21 | { name: 'version', type: 'int', numBits: 6 }, 22 | { name: 'created', type: 'date', numBits: 36 }, 23 | { name: 'lastUpdated', type: 'date', numBits: 36 }, 24 | { name: 'cmpId', type: 'int', numBits: 12 }, 25 | { name: 'cmpVersion', type: 'int', numBits: 12 }, 26 | { name: 'consentScreen', type: 'int', numBits: 6 }, 27 | { name: 'consentLanguage', type: 'language', numBits: 12 }, 28 | { name: 'vendorListVersion', type: 'int', numBits: 12 }, 29 | { name: 'purposeIdBitString', type: 'bits', numBits: 24 }, 30 | { name: 'maxVendorId', type: 'int', numBits: 16 }, 31 | { name: 'isRange', type: 'bool', numBits: 1 }, 32 | { 33 | name: 'vendorIdBitString', 34 | type: 'bits', 35 | numBits: decodedObject => decodedObject.maxVendorId, 36 | validator: decodedObject => !decodedObject.isRange, 37 | }, 38 | { 39 | name: 'defaultConsent', 40 | type: 'bool', 41 | numBits: 1, 42 | validator: decodedObject => decodedObject.isRange, 43 | }, 44 | { 45 | name: 'numEntries', 46 | numBits: 12, 47 | type: 'int', 48 | validator: decodedObject => decodedObject.isRange, 49 | }, 50 | { 51 | name: 'vendorRangeList', 52 | type: 'list', 53 | listCount: decodedObject => decodedObject.numEntries, 54 | validator: decodedObject => decodedObject.isRange, 55 | fields: [ 56 | { 57 | name: 'isRange', 58 | type: 'bool', 59 | numBits: 1, 60 | }, 61 | { 62 | name: 'startVendorId', 63 | type: 'int', 64 | numBits: 16, 65 | }, 66 | { 67 | name: 'endVendorId', 68 | type: 'int', 69 | numBits: 16, 70 | validator: decodedObject => decodedObject.isRange, 71 | }, 72 | ], 73 | }, 74 | ], 75 | }, 76 | }; 77 | 78 | module.exports = { 79 | versionNumBits, 80 | vendorVersionMap, 81 | }; 82 | -------------------------------------------------------------------------------- /test/decode.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | 3 | const { decodeConsentString } = require('../src/decode'); 4 | 5 | describe('decode', function () { 6 | const aDate = new Date('2018-07-15 PDT'); 7 | 8 | it('decodes the consent data from a base64-encoded string', function () { 9 | const consentData = decodeConsentString('BOQ7WlgOQ7WlgABACDENABwAAABJOACgACAAQABA'); 10 | 11 | expect(consentData).to.deep.equal({ 12 | version: 1, 13 | cmpId: 1, 14 | cmpVersion: 2, 15 | consentScreen: 3, 16 | consentLanguage: 'en', 17 | vendorListVersion: 1, 18 | maxVendorId: 1171, 19 | created: aDate, 20 | lastUpdated: aDate, 21 | allowedPurposeIds: [1, 2], 22 | allowedVendorIds: [1, 2, 4], 23 | }); 24 | }); 25 | 26 | it('decodes the consent data from another base64-encoded string', function () { 27 | // those two consents represent the same data, but with a different encoding: 28 | // DefaultConsent of Range = true 29 | const consentData = decodeConsentString('BOOMzbgOOQww_AtABAFRAb-AAAsvPA2AAKACwAF4ANgAgABTADAAGMAM8AagBrgDoAOoAdwA8gB7gEMAQ4AiQBFgCPAEkAJQASwAmABQwClAKaAVYBWQCwALIAWoAuIBdAF2AL8AYgAx4BkgGUAMyAZwBngDUAGsANiAbQBvgDkgHMAc4A6QB2QDuAO-AeQB5wD3APiAfQB-gEBAIHAQUBDICHAIgAROAioCLQEZsvI'); 30 | // DefaultConsent of Range = false 31 | const consentData2 = decodeConsentString('BOOMzbgOOQww_AtABAFRAb-AAAsvOA3gACAAkABgArgBaAF0AMAA1gBuAH8AQQBSgCoAL8AYQBigDIAM0AaABpgDYAOYAdgA8AB6gD4AQoAiABFQCMAI6ASABIgCTAEqAJeATIBQQCiAKSAU4BVQCtAK-AWYBaQC2ALcAXMAvAC-gGAAYcAxQDGAGQAMsAZsA0ADTAGqANcAbMA4ADjAHKAOiAdQB1gDtgHgAeMA9AD2AHzAP4BAACBAEEAIbAREBEgCKQEXARhZeYA'); 32 | 33 | const toCompareWith = { 34 | created: new Date('2018-05-23T07:58:14.400Z'), 35 | lastUpdated: new Date('2018-05-24T12:47:40.700Z'), 36 | version: 1, 37 | vendorListVersion: 27, 38 | cmpId: 45, 39 | cmpVersion: 1, 40 | consentScreen: 0, 41 | consentLanguage: 'fr', 42 | allowedPurposeIds: [1, 2, 3, 4, 5], 43 | allowedVendorIds: [1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 45, 46, 48, 49, 50, 51, 52, 53, 55, 56, 57, 58, 59, 60, 61, 62, 63, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 97, 98, 100, 101, 102, 104, 105, 108, 109, 110, 111, 112, 113, 114, 115, 118, 120, 122, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 136, 138, 140, 141, 142, 144, 145, 147, 149, 151, 153, 154, 155, 156, 157, 158, 159, 160, 162, 163, 164, 167, 168, 169, 170, 173, 174, 175, 179, 180, 182, 183, 185, 188, 189, 190, 192, 193, 194, 195, 197, 198, 200, 203, 205, 208, 209, 210, 211, 213, 215, 217, 224, 225, 226, 227, 229, 232, 234, 235, 237, 240, 241, 244, 245, 246, 249, 254, 255, 256, 258, 260, 269, 273, 274, 276, 279, 280, 45811], // eslint-disable-line 44 | maxVendorId: 45811, 45 | }; 46 | expect(consentData).to.deep.equal(toCompareWith); 47 | expect(consentData2).to.deep.equal(toCompareWith); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/encode.js: -------------------------------------------------------------------------------- 1 | const { 2 | encodeToBase64, 3 | padRight, 4 | } = require('./utils/bits'); 5 | 6 | /** 7 | * Encode a list of vendor IDs into bits 8 | * 9 | * @param {integer} maxVendorId Highest vendor ID in the vendor list 10 | * @param {integer[]} allowedVendorIds Vendors that the user has given consent to 11 | */ 12 | function encodeVendorIdsToBits(maxVendorId, allowedVendorIds = []) { 13 | let vendorString = ''; 14 | 15 | for (let id = 1; id <= maxVendorId; id += 1) { 16 | vendorString += (allowedVendorIds.indexOf(id) !== -1 ? '1' : '0'); 17 | } 18 | 19 | return padRight(vendorString, Math.max(0, maxVendorId - vendorString.length)); 20 | } 21 | 22 | /** 23 | * Encode a list of purpose IDs into bits 24 | * 25 | * @param {*} purposes List of purposes from the vendor list 26 | * @param {*} allowedPurposeIds List of purpose IDs that the user has given consent to 27 | */ 28 | function encodePurposeIdsToBits(purposes, allowedPurposeIds = new Set()) { 29 | let maxPurposeId = 0; 30 | for (let i = 0; i < purposes.length; i += 1) { 31 | maxPurposeId = Math.max(maxPurposeId, purposes[i].id); 32 | } 33 | for (let i = 0; i < allowedPurposeIds.length; i += 1) { 34 | maxPurposeId = Math.max(maxPurposeId, allowedPurposeIds[i]); 35 | } 36 | 37 | let purposeString = ''; 38 | for (let id = 1; id <= maxPurposeId; id += 1) { 39 | purposeString += (allowedPurposeIds.indexOf(id) !== -1 ? '1' : '0'); 40 | } 41 | 42 | return purposeString; 43 | } 44 | 45 | /** 46 | * Convert a list of vendor IDs to ranges 47 | * 48 | * @param {object[]} vendors List of vendors from the vendor list (important: this list must to be sorted by ID) 49 | * @param {integer[]} allowedVendorIds List of vendor IDs that the user has given consent to 50 | */ 51 | function convertVendorsToRanges(vendors, allowedVendorIds) { 52 | let range = []; 53 | const ranges = []; 54 | 55 | const idsInList = vendors.map(vendor => vendor.id); 56 | 57 | for (let index = 0; index < vendors.length; index += 1) { 58 | const { id } = vendors[index]; 59 | if (allowedVendorIds.indexOf(id) !== -1) { 60 | range.push(id); 61 | } 62 | 63 | // Do we need to close the current range? 64 | if ( 65 | ( 66 | allowedVendorIds.indexOf(id) === -1 // The vendor we are evaluating is not allowed 67 | || index === vendors.length - 1 // There is no more vendor to evaluate 68 | || idsInList.indexOf(id + 1) === -1 // There is no vendor after this one (ie there is a gap in the vendor IDs) ; we need to stop here to avoid including vendors that do not have consent 69 | ) 70 | && range.length 71 | ) { 72 | const startVendorId = range.shift(); 73 | const endVendorId = range.pop(); 74 | 75 | range = []; 76 | 77 | ranges.push({ 78 | isRange: typeof endVendorId === 'number', 79 | startVendorId, 80 | endVendorId, 81 | }); 82 | } 83 | } 84 | 85 | return ranges; 86 | } 87 | 88 | /** 89 | * Get maxVendorId from the list of vendors and return that id 90 | * 91 | * @param {object} vendors 92 | */ 93 | function getMaxVendorId(vendors) { 94 | // Find the max vendor ID from the vendor list 95 | let maxVendorId = 0; 96 | 97 | vendors.forEach((vendor) => { 98 | if (vendor.id > maxVendorId) { 99 | maxVendorId = vendor.id; 100 | } 101 | }); 102 | return maxVendorId; 103 | } 104 | /** 105 | * Encode consent data into a web-safe base64-encoded string 106 | * 107 | * @param {object} consentData Data to include in the string (see `utils/definitions.js` for the list of fields) 108 | */ 109 | function encodeConsentString(consentData) { 110 | let { maxVendorId } = consentData; 111 | const { vendorList = {}, allowedPurposeIds, allowedVendorIds } = consentData; 112 | const { vendors = [], purposes = [] } = vendorList; 113 | 114 | // if no maxVendorId is in the ConsentData, get it 115 | if (!maxVendorId) { 116 | maxVendorId = getMaxVendorId(vendors); 117 | } 118 | 119 | // Encode the data with and without ranges and return the smallest encoded payload 120 | const noRangesData = encodeToBase64({ 121 | ...consentData, 122 | maxVendorId, 123 | purposeIdBitString: encodePurposeIdsToBits(purposes, allowedPurposeIds), 124 | isRange: false, 125 | vendorIdBitString: encodeVendorIdsToBits(maxVendorId, allowedVendorIds), 126 | }); 127 | 128 | const vendorRangeList = convertVendorsToRanges(vendors, allowedVendorIds); 129 | 130 | const rangesData = encodeToBase64({ 131 | ...consentData, 132 | maxVendorId, 133 | purposeIdBitString: encodePurposeIdsToBits(purposes, allowedPurposeIds), 134 | isRange: true, 135 | defaultConsent: false, 136 | numEntries: vendorRangeList.length, 137 | vendorRangeList, 138 | }); 139 | 140 | return noRangesData.length < rangesData.length ? noRangesData : rangesData; 141 | } 142 | 143 | module.exports = { 144 | convertVendorsToRanges, 145 | encodeConsentString, 146 | getMaxVendorId, 147 | encodeVendorIdsToBits, 148 | encodePurposeIdsToBits, 149 | }; 150 | -------------------------------------------------------------------------------- /test/encode.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | 3 | const vendorList = require('./vendors.json'); 4 | 5 | const { 6 | convertVendorsToRanges, 7 | encodeConsentString, 8 | getMaxVendorId, 9 | encodeVendorIdsToBits, 10 | encodePurposeIdsToBits, 11 | } = require('../src/encode'); 12 | 13 | describe('convertVendorsToRanges', function () { 14 | it('converts a list of vendors to a full range', function () { 15 | expect(convertVendorsToRanges( 16 | [ 17 | { id: 1 }, 18 | { id: 2 }, 19 | { id: 3 }, 20 | { id: 4 }, 21 | { id: 5 }, 22 | ], 23 | [1, 2, 3, 4, 5], 24 | )).to.deep.equal([ 25 | { isRange: true, startVendorId: 1, endVendorId: 5 }, 26 | ]); 27 | }); 28 | 29 | it('converts a list of vendors to a multiple ranges as needed', function () { 30 | expect(convertVendorsToRanges( 31 | [ 32 | { id: 1 }, 33 | { id: 2 }, 34 | { id: 3 }, 35 | { id: 4 }, 36 | { id: 5 }, 37 | ], 38 | [1, 2, 3, 5], 39 | )).to.deep.equal([ 40 | { isRange: true, startVendorId: 1, endVendorId: 3 }, 41 | { isRange: false, startVendorId: 5, endVendorId: undefined }, 42 | ]); 43 | }); 44 | 45 | it('ignores missing vendors when creating ranges', function () { 46 | expect(convertVendorsToRanges( 47 | [ 48 | { id: 1 }, 49 | { id: 2 }, 50 | { id: 3 }, 51 | { id: 7 }, 52 | ], 53 | [1, 2, 3, 7], 54 | )).to.deep.equal([ 55 | { isRange: true, startVendorId: 1, endVendorId: 3 }, 56 | { isRange: false, startVendorId: 7, endVendorId: undefined }, 57 | ]); 58 | 59 | expect(convertVendorsToRanges( 60 | [ 61 | { id: 1 }, 62 | { id: 3 }, 63 | { id: 7 }, 64 | ], 65 | [1, 3, 7], 66 | )).to.deep.equal([ 67 | { isRange: false, startVendorId: 1, endVendorId: undefined }, 68 | { isRange: false, startVendorId: 3, endVendorId: undefined }, 69 | { isRange: false, startVendorId: 7, endVendorId: undefined }, 70 | ]); 71 | }); 72 | }); 73 | 74 | describe('encode', function () { 75 | const aDate = new Date('2018-07-15 PDT'); 76 | 77 | it('encodes the consent data into a base64-encoded string', function () { 78 | const consentData = { 79 | version: 1, 80 | cmpId: 1, 81 | cmpVersion: 2, 82 | consentScreen: 3, 83 | consentLanguage: 'en', 84 | created: aDate, 85 | lastUpdated: aDate, 86 | allowedPurposeIds: [1, 2], 87 | allowedVendorIds: [1, 2, 4], 88 | vendorList, 89 | vendorListVersion: vendorList.vendorListVersion, 90 | }; 91 | 92 | const encodedString = encodeConsentString(consentData); 93 | expect(encodedString).to.equal('BOQ7WlgOQ7WlgABACDENAOwAAAAHCADAACAAQAAQ'); 94 | }); 95 | }); 96 | 97 | describe('maxVendorId', function () { 98 | it('gets the max vendor id from the vendorList.vendors', function () { 99 | const maxVendorId = getMaxVendorId(vendorList.vendors); 100 | 101 | expect(maxVendorId).to.equal(112); 102 | }); 103 | }); 104 | 105 | describe('encodeVendorIdsToBits', function () { 106 | it('encodes vendor id values to bits and turns on the one I tell it to', function () { 107 | const setBit = 5; 108 | const maxVendorId = getMaxVendorId(vendorList.vendors); 109 | const bitString = encodeVendorIdsToBits(maxVendorId, [setBit]); 110 | 111 | expect(bitString.length).to.equal(112); 112 | for (let i = 0; i < maxVendorId; i += 1) { 113 | if (i === (setBit - 1)) { 114 | expect(bitString[i]).to.equal('1'); 115 | } else { 116 | expect(bitString[i]).to.equal('0'); 117 | } 118 | } 119 | }); 120 | it('encodes vendor id values to bits and turns on the two I tell it to', function () { 121 | const setBit1 = 5; 122 | const setBit2 = 9; 123 | const maxVendorId = getMaxVendorId(vendorList.vendors); 124 | const bitString = encodeVendorIdsToBits(maxVendorId, [setBit1, setBit2]); 125 | 126 | expect(bitString.length).to.equal(112); 127 | for (let i = 0; i < maxVendorId; i += 1) { 128 | if (i === (setBit1 - 1) || i === (setBit2 - 1)) { 129 | expect(bitString[i]).to.equal('1'); 130 | } else { 131 | expect(bitString[i]).to.equal('0'); 132 | } 133 | } 134 | }); 135 | }); 136 | 137 | describe('encodePurposeIdsToBits', function () { 138 | it('encodes purpose id values to bits and turns on the one I tell it to', function () { 139 | const setBit = 4; 140 | const purposes = vendorList.purposes; 141 | const bitString = encodePurposeIdsToBits(purposes, [setBit]); 142 | 143 | expect(bitString.length).to.equal(purposes.length); 144 | for (let i = 0; i < purposes.length; i += 1) { 145 | if (i === (setBit - 1)) { 146 | expect(bitString[i]).to.equal('1'); 147 | } else { 148 | expect(bitString[i]).to.equal('0'); 149 | } 150 | } 151 | }); 152 | it('encodes purpose id values to bits and turns on the two I tell it to', function () { 153 | const setBit1 = 2; 154 | const setBit2 = 4; 155 | const purposes = vendorList.purposes; 156 | const bitString = encodePurposeIdsToBits(purposes, [setBit1, setBit2]); 157 | 158 | expect(bitString.length).to.equal(purposes.length); 159 | for (let i = 0; i < purposes.length; i += 1) { 160 | if (i === (setBit1 - 1) || i === (setBit2 - 1)) { 161 | expect(bitString[i]).to.equal('1'); 162 | } else { 163 | expect(bitString[i]).to.equal('0'); 164 | } 165 | } 166 | }); 167 | }); 168 | 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Transparency and Consent Framework: Consent String SDK (JavaScript) 2 | 3 | [![Build Status](https://travis-ci.org/didomi/consent-string.svg?branch=master)](https://travis-ci.org/didomi/consent-string) 4 | [![Coverage Status](https://coveralls.io/repos/github/didomi/consent-string/badge.svg?branch=master)](https://coveralls.io/github/didomi/consent-string?branch=master) 5 | 6 | Encode and decode web-safe base64 consent information with the IAB EU's GDPR Transparency and Consent Framework. 7 | 8 | This library is a JavaScript reference implementation for dealing with consent strings in the IAB EU's GDPR Transparency and Consent Framework. 9 | It should be used by anyone who receives or sends consent information like vendors that receive consent data from a partner, or consent management platforms that need to encode/decode the global cookie. 10 | 11 | The IAB specification for the consent string format is available on the [IAB Github](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/Consent%20string%20and%20vendor%20list%20formats%20v1.1%20Final.md) (section "Vendor Consent Cookie Format"). 12 | 13 | **This library fully supports the version v1.1 of the specification. It can encode and decode consent strings with version bit 1.** 14 | 15 | #### IAB Europe Transparency and Consent Framework 16 | 17 | In November 2017, IAB Europe and a cross-section of the publishing and advertising industry, announced a new Transparency & Consent Framework to help publishers, advertisers and technology companies comply with key elements of GDPR. The Framework will give the publishing and advertising industries a common language with which to communicate consumer consent for the delivery of relevant online advertising and content. 18 | 19 | Framework Technical specifications available at: https://raw.githubusercontent.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework. 20 | 21 | --- 22 | 23 | **Table of Contents** 24 | 25 | - [Installation](#installation) 26 | - [Usage](#usage) 27 | - [Documentation](#documentation) 28 | 29 | ## Terms 30 | 31 | | Term | Meaning | 32 | | --- | --- | 33 | | IAB | Interactive Advertising Bureau | 34 | | TCF | Transparency and Consent Framework | 35 | | Vendor ID | Refers to IAB EU hosted Global Vendor List id defined by the TCF | 36 | | Consent String | Refers to IAB EU Base64 encoded bit string representing user preference in the TCF | 37 | | CMP | "Consent Management Provider" as specified by the TCF -- ie. a javascript widget that captures users consent preferences and displays advertising information and vendors. | 38 | | Consent Screen | CMP Screen in which consent was confirmed. A proprietary number to each CMP that is arbitrary. | 39 | 40 | ## Installation 41 | 42 | ### For a browser application 43 | 44 | The `consent-string` library is designed to be as lightweight as possible and has no external dependency when used in a client-side application. 45 | 46 | You can install it as a standard `npm` library: 47 | 48 | ```bash 49 | npm install --save consent-string 50 | ``` 51 | 52 | **Note:** You will need webpack or a similar module bundler to correctly pack the library for use in a browser. 53 | 54 | ### For Node.js 55 | 56 | You can install it as a standard `npm` library: 57 | 58 | ```bash 59 | npm install --save consent-string 60 | ``` 61 | 62 | ## Usage 63 | 64 | ### Decode a consent string 65 | 66 | You can decode a base64-encoded consent string by passing it as a parameter to the `ConsentString` constructor: 67 | 68 | ```javascript 69 | const { ConsentString } = require('consent-string'); 70 | 71 | const consentData = new ConsentString('BOQ7WlgOQ7WlgABABwAAABJOACgACAAQABA'); 72 | 73 | // `consentData` contains the decoded consent information 74 | ``` 75 | 76 | **Note:** You do not need the IAB global vendor list for decoding a consent string as long as you know the purpose and vendor IDs you are looking for. 77 | 78 | ### Encode consent data 79 | 80 | ```javascript 81 | const { ConsentString } = require('consent-string'); 82 | 83 | const consentData = new ConsentString(); 84 | 85 | // Set the global vendor list 86 | // You need to download and provide the vendor list yourself 87 | // It can be found here - https://vendorlist.consensu.org/vendorlist.json 88 | consentData.setGlobalVendorList(vendorList); 89 | 90 | // Set the consent data 91 | consentData.setCmpId(1); 92 | consentData.setCmpVersion(1); 93 | consentData.setConsentScreen(1); 94 | consentData.setConsentLanguage('en'); 95 | consentData.setPurposesAllowed([1, 2, 4]); 96 | consentData.setVendorsAllowed([1, 24, 245]); 97 | 98 | // Encode the data into a web-safe base64 string 99 | consentData.getConsentString(); 100 | ``` 101 | 102 | ## Documentation 103 | 104 | #### Consent String 105 | [Methods](consent_string_methods.md) 106 | 107 | [Use Cases](consent_string_use_cases.md) 108 | 109 | ## About 110 | 111 | #### About IAB Tech Lab 112 | 113 | The IAB Technology Laboratory (?Tech Lab?) is a non-profit research and development consortium that produces and provides standards, software, and services to drive growth of an effective and sustainable global digital media ecosystem. Comprised of digital publishers and ad technology firms, as well as marketers, agencies, and other companies with interests in the interactive marketing arena, IAB Tech Lab aims to enable brand and media growth via a transparent, safe, effective supply chain, simpler and more consistent measurement, and better advertising experiences for consumers, with a focus on mobile and ?TV?/digital video channel enablement. The IAB Tech Lab portfolio includes the DigiTrust real-time standardized identity service designed to improve the digital experience for consumers, publishers, advertisers, and third-party platforms. Board members include AppNexus, ExtremeReach, Google, GroupM, Hearst Digital Media, Integral Ad Science, Index Exchange, LinkedIn, MediaMath, Microsoft, Moat, Pandora, PubMatic, Quantcast, Telaria, The Trade Desk, and Yahoo! Japan. Established in 2014, the IAB Tech Lab is headquartered in New York City with an office in San Francisco and representation in Seattle and London. 114 | 115 | Learn more about IAB Tech Lab here: [https://www.iabtechlab.com/](https://www.iabtechlab.com/) 116 | 117 | #### About IAB Europe 118 | 119 | IAB Europe is the voice of digital business and the leading European-level industry association for the interactive advertising ecosystem. Its mission is to promote the development of this innovative sector by shaping the regulatory environment, investing in research and education, and developing and facilitating the uptake of business standards. 120 | 121 | Learn more about IAB Europe here: [https://www.iabeurope.eu/](https://www.iabeurope.eu/) 122 | 123 | 124 | #### Contributors and Technical Governance 125 | 126 | GDPR Technical Working Group members provide contributions to this repository. Participants in the GDPR Technical Working group must be members of IAB Tech Lab. Technical Governance for the project is provided by the IAB Tech Lab GDPR Commit Group. 127 | -------------------------------------------------------------------------------- /consent_string_methods.md: -------------------------------------------------------------------------------- 1 | ## Consent String Methods 2 | 3 | #### constructor 4 | Constructs new object 5 | 6 | ```javascript 7 | // @param str is the web-safe base64 encoded consent string or null (no parameter) 8 | constructor( str = null ) 9 | ``` 10 | --- 11 | 12 | #### getConsentString() 13 | Gets the consent string either new one created or one passed in on construction. 14 | ```javascript 15 | // @return web-safe base64 encoded consent string 16 | getConsentString() 17 | ``` 18 | 19 | --- 20 | 21 | #### getLastUpdated() 22 | Gets the `Date` of last updated 23 | ```javascript 24 | // @return Date 25 | getLastUpdated() 26 | ``` 27 | 28 | --- 29 | 30 | #### setLastUpdated(date) 31 | Sets the `Date` of last updated 32 | ```javascript 33 | setLastUpdated(new Date()); 34 | ``` 35 | 36 | --- 37 | 38 | #### getCreatedDate() 39 | Gets the `Date` created 40 | ```javascript 41 | // @return Date 42 | getCreatedDate() 43 | ``` 44 | 45 | --- 46 | 47 | #### setCreatedDate(date) 48 | Sets the `Date` created 49 | ```javascript 50 | setCreatedDate(new Date()); 51 | ``` 52 | --- 53 | #### getMaxVendorId() 54 | Gets the maxVendorId from the vendor list. 55 | ```javascript 56 | // @return number 57 | getMaxVendorId() 58 | ``` 59 | --- 60 | #### getParsedVendorConsents() 61 | Gets the binary string format of the vendor consents. 1 being has consent 0 being no consent. 62 | ```javascript 63 | // @return string (binary) 64 | getParsedVendorConsents() 65 | ``` 66 | --- 67 | #### getParsedPurposeConsents() 68 | Gets the binary string format of the purpose consents. 1 being has consent 0 being no consent. 69 | ```javascript 70 | // @return string (binary) 71 | getParsedPurposeConsents() 72 | ``` 73 | --- 74 | #### getMetadataString() 75 | Gets the metadata string as defined in the response to the getVendorConsents method (ie binary string that includes only header information like consent string version, timestamps, cmp ID, etc. but no purposes/consents information). 76 | 77 | ```javascript 78 | // @return web-safe base64 encoded metadata string 79 | getMetadataString() 80 | ``` 81 | --- 82 | 83 | #### ConsentString.decodeMetadataString() (Static Method) 84 | Decodes the metadata string. 85 | 86 | ```javascript 87 | // @return object with metadata fields 88 | ConsentString.decodeMetadataString(encodedMetadataString) 89 | ``` 90 | --- 91 | #### getVersion() 92 | The version number in which this consent string specification adheres to 93 | 94 | ```javascript 95 | // @return integer version number of consent string specification 96 | getVersion() 97 | ``` 98 | 99 | --- 100 | #### getVendorListVersion() 101 | Returns either the vendor list version set by `setGlobalVendorList` or whatever was previously set as the consent string when the object was created 102 | 103 | ```javascript 104 | // @return integer version number of vendor list version 105 | getVendorListVersion() 106 | ``` 107 | --- 108 | #### setGlobalVendorList( gvlObject ) 109 | Sets the global vendor list object. Generally this would be the parsed JSON that comes from the IAB hosted Global Vendor List. 110 | 111 | ```javascript 112 | // @param gvlObject is the parsed JSON that conforms to the IAB EU TCF Vendor List Specification 113 | setGlobalVendorList( gvlObject ) 114 | ``` 115 | --- 116 | #### getGlobalVendorList() 117 | Gets the global vendor list object. 118 | 119 | ```javascript 120 | // @param gvlObject is the parsed JSON that conforms to the IAB EU TCF Vendor List Specification 121 | getGlobalVendorList(); 122 | ``` 123 | --- 124 | #### setCmpId( cmpId ) 125 | Sets CMP ID number that is assigned to your CMP from the IAB EU. A unique ID will be assigned to each Consent Manager Provider 126 | 127 | ```javascript 128 | // @param cmpId the id for the cmp setting the consent string values. 129 | setCmpId( cmpId ) 130 | ``` 131 | --- 132 | #### getCmpId() 133 | Get the ID of the CMP from the consent string 134 | 135 | ```javascript 136 | // @return CMP id 137 | getCmpId() 138 | ``` 139 | --- 140 | #### setCmpVersion( version ) 141 | Sets the version of the CMP code that created or updated the consent string 142 | 143 | ```javascript 144 | // @param version - CMP version 145 | setCmpVersion( version ) 146 | ``` 147 | --- 148 | #### getCmpVersion() 149 | The version of the CMP code that created or updated the consent string 150 | 151 | ```javascript 152 | // @return version CMP version 153 | getCmpVersion() 154 | ``` 155 | --- 156 | #### setConsentScreen( screenId ) 157 | Sets the consent screen id. The screen number is CMP and CMP Version specific, and is for logging proof of consent 158 | 159 | ```javascript 160 | // @param screenId id for the screen in which the consent values were confirmed 161 | setConsentScreen( screenId ) 162 | ``` 163 | --- 164 | #### getConsentScreen() 165 | The screen number is CMP and CmpVersion specific, and is for logging proof of consent 166 | 167 | ```javascript 168 | // @return screenId id for the screen in which the consent values were confirmed 169 | getConsentScreen() 170 | ``` 171 | --- 172 | #### setConsentLanguage( language ) 173 | Sets consent language. Two-letter ISO639-1 language code that CMP asked for consent in 174 | 175 | ```javascript 176 | // @param language two character ISO639-1 language code 177 | setConsentLanguage( language ) 178 | ``` 179 | --- 180 | #### getConsentLanguage() 181 | gets consent language. Two-letter ISO639-1 language code that CMP asked for consent in 182 | 183 | ```javascript 184 | // @return language two character ISO639-1 language code 185 | getConsentLanguage() 186 | ``` 187 | --- 188 | #### setPurposesAllowed( purposeIdArray) 189 | Sets the allowed purposes as an array of purpose ids 190 | 191 | ```javascript 192 | // @param purposeIdArray variable length array of integers setting which purposes are allowed. If the id is in the array it’s allowed. 193 | setPurposesAllowed( purposeIdArray) 194 | ``` 195 | --- 196 | #### getPurposesAllowed() 197 | Gets an array of purposes allowed either set by `setPurposesAllowed` or whatever was previously set by the initializing consent string 198 | 199 | ```javascript 200 | // @return variable length array of integers setting which purposes are allowed. If the id is in the array it’s allowed. 201 | getPurposesAllowed() 202 | ``` 203 | --- 204 | #### setPurposeAllowed( purposeId, value ) 205 | Sets a single purpose by id and boolean value 206 | 207 | ```javascript 208 | // @param purposeId the purpose id 209 | // @param value the boolean value to set it to true for allowed false for not allowed 210 | setPurposeAllowed( purposeId, value ) 211 | ``` 212 | --- 213 | #### isPurposeAllowed( purposeId ) 214 | Gets a single purpose by id and returns boolean value 215 | 216 | ```javascript 217 | // @param purposeId the purpose id 218 | // @return boolean value true for allowed false for not allowed 219 | isPurposeAllowed( purposeId ) 220 | ``` 221 | --- 222 | #### setVendorAllowed( vendorId, valueBool ) 223 | Sets consent value for a vendor id 224 | 225 | ```javascript 226 | // @param vendorId - vendor id to set consent value for 227 | // @param value - the boolean value to set the consent to 228 | setVendorAllowed( vendorId, valueBool ) 229 | ``` 230 | --- 231 | #### isVendorAllowed( vendorId ) 232 | For determining if the vendor consent value bit is turned on or off for a particular vendor id. 233 | 234 | ```javascript 235 | // @param vendorId vendor id to see if consent is allowed for 236 | // @return boolean value of consent for that vendor id 237 | 238 | isVendorAllowed( vendorId ) 239 | ``` 240 | -------------------------------------------------------------------------------- /test/consent-string.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const vendorList = require('./vendors.json'); 3 | const { ConsentString } = require('../src/consent-string'); 4 | 5 | describe('ConsentString', function () { 6 | const aDate = new Date('2018-07-15 PDT'); 7 | 8 | const consentData = { 9 | version: 1, 10 | cmpId: 1, 11 | cmpVersion: 2, 12 | consentScreen: 3, 13 | consentLanguage: 'en', 14 | vendorListVersion: 1, 15 | maxVendorId: Math.max(...vendorList.vendors.map(vendor => vendor.id)), 16 | created: aDate, 17 | lastUpdated: aDate, 18 | allowedPurposeIds: [1, 2], 19 | allowedVendorIds: [1, 2, 4], 20 | }; 21 | 22 | const resultingMetadaString = 'BOQ7WlgOQ7WlgABACDAAABAAAAAAAA'; 23 | 24 | it('gives the same result when encoding then decoding data', function () { 25 | const consentString = new ConsentString(); 26 | consentString.setGlobalVendorList(vendorList); 27 | 28 | Object.assign(consentString, consentData); 29 | 30 | expect(new ConsentString(consentString.getConsentString(false))).to.deep.include(consentData); 31 | }); 32 | 33 | it('gets the max vendor id as expected', function () { 34 | const consentString = new ConsentString(); 35 | consentString.setGlobalVendorList(vendorList); 36 | Object.assign(consentString, consentData); 37 | expect(consentString.getMaxVendorId()).to.equal(112); 38 | }); 39 | 40 | it('Should decode without having a vendorlist and return the string I set', function () { 41 | const encoded = 'BOhs9bQOjjnLQAXABBENCfeAAAApmABgAYADoA'; 42 | expect(() => { 43 | const consentString = new ConsentString(encoded); 44 | expect(consentString.isPurposeAllowed(2)).to.be.true; 45 | expect(consentString.isVendorAllowed(12)).to.be.true; 46 | expect(consentString.isVendorAllowed(11)).to.be.false; 47 | expect(consentString.getConsentString(false)).to.equal(encoded); 48 | }).not.to.throw(); 49 | }); 50 | 51 | it('gets the parsed vendor consents', function () { 52 | const consentString = new ConsentString(); 53 | consentString.setGlobalVendorList(vendorList); 54 | Object.assign(consentString, consentData); 55 | 56 | const parsedVendors = consentString.getParsedVendorConsents(); 57 | expect(parsedVendors.length).to.equal(112); 58 | for (let i = 0; i < consentString.getMaxVendorId(); i += 1) { 59 | if (consentData.allowedVendorIds.includes(i + 1)) { 60 | expect(parsedVendors[i]).to.equal('1'); 61 | } else { 62 | expect(parsedVendors[i]).to.equal('0'); 63 | } 64 | } 65 | }); 66 | 67 | it('gets the parsed purpose consents', function () { 68 | const consentString = new ConsentString(); 69 | consentString.setGlobalVendorList(vendorList); 70 | Object.assign(consentString, consentData); 71 | 72 | const parsedPurposes = consentString.getParsedPurposeConsents(); 73 | expect(parsedPurposes.length).to.equal(vendorList.purposes.length); 74 | for (let i = 0; i < vendorList.purposes.length; i += 1) { 75 | if (consentData.allowedPurposeIds.includes(i + 1)) { 76 | expect(parsedPurposes[i]).to.equal('1'); 77 | } else { 78 | expect(parsedPurposes[i]).to.equal('0'); 79 | } 80 | } 81 | }); 82 | 83 | it('encodes the Metadata String as expected', function () { 84 | const consentString = new ConsentString(); 85 | consentString.setGlobalVendorList(vendorList); 86 | Object.assign(consentString, consentData); 87 | expect(consentString.getMetadataString()).to.equal(resultingMetadaString); 88 | }); 89 | 90 | it('decodes the Metadata String as expected', function () { 91 | const result = ConsentString.decodeMetadataString(resultingMetadaString); 92 | expect(result.cmpId).to.equal(consentData.cmpId); 93 | expect(result.cmpVersion).to.equal(consentData.cmpVersion); 94 | expect(result.version).to.equal(consentData.version); 95 | expect(result.vendorListVersion).to.equal(consentData.vendorListVersion); 96 | expect(result.created).to.deep.equal(consentData.created); 97 | expect(result.lastUpdated).to.deep.equal(consentData.lastUpdated); 98 | expect(result.consentScreen).to.equal(consentData.consentScreen); 99 | }); 100 | 101 | describe('vendorPermissions', function () { 102 | it('can manipulate one vendor permission without affecting the others', function () { 103 | const consentString = new ConsentString(); 104 | consentString.setGlobalVendorList(vendorList); 105 | Object.assign(consentString, consentData); 106 | 107 | const allowedVendorsBefore = consentString.allowedVendorIds.slice(); 108 | 109 | consentString.setVendorAllowed(2, false); 110 | 111 | expect(allowedVendorsBefore.length - 1).to.equal(consentString.allowedVendorIds.length); 112 | }); 113 | }); 114 | 115 | describe('purposePermissions', function () { 116 | it('can manipulate one purpose permission without affecting the others', function () { 117 | const consentString = new ConsentString(); 118 | consentString.setGlobalVendorList(vendorList); 119 | Object.assign(consentString, consentData); 120 | 121 | const allowedPurposes = consentString.allowedPurposeIds.slice(); 122 | 123 | consentString.setPurposeAllowed(1, false); 124 | 125 | expect(allowedPurposes.length - 1).to.equal(consentString.allowedPurposeIds.length); 126 | }); 127 | 128 | it("shouldn't throw an error when calling isPurposeAllowed", function () { 129 | const consentString = new ConsentString(); 130 | consentString.setGlobalVendorList(vendorList); 131 | Object.assign(consentString, consentData); 132 | 133 | consentString.setPurposeAllowed(1, true); 134 | 135 | expect(consentString.isPurposeAllowed(1)).to.be.true; 136 | }); 137 | }); 138 | 139 | describe('setGlobalVendorList', function () { 140 | it('throws an error if the provided vendor list does not respect the IAB format', function () { 141 | expect(() => (new ConsentString()).setGlobalVendorList()).to.throw; 142 | expect(() => (new ConsentString()).setGlobalVendorList({})).to.throw; 143 | expect(() => (new ConsentString()).setGlobalVendorList({ 144 | vendorListVersion: 1, 145 | })).to.throw; 146 | expect(() => (new ConsentString()).setGlobalVendorList({ 147 | vendorListVersion: 1, 148 | purposes: [], 149 | })).to.throw; 150 | expect(() => (new ConsentString()).setGlobalVendorList({ 151 | vendorListVersion: 1, 152 | vendors: [], 153 | })).to.throw; 154 | expect(() => (new ConsentString()).setGlobalVendorList({ 155 | vendorListVersion: 1, 156 | purposes: {}, 157 | })).to.throw; 158 | expect(() => (new ConsentString()).setGlobalVendorList({ 159 | purposes: [], 160 | vendors: [], 161 | })).to.throw; 162 | expect(() => (new ConsentString()).setGlobalVendorList({ 163 | version: 1, 164 | purposes: [], 165 | vendors: [], 166 | })).to.throw; 167 | }); 168 | 169 | it('does not throw an error if the provided vendor list does respect the IAB format', function () { 170 | const consent = new ConsentString(); 171 | 172 | expect(consent.setGlobalVendorList({ 173 | vendorListVersion: 1, 174 | purposes: [], 175 | vendors: [], 176 | })).to.be.undefined; 177 | 178 | expect(consent.vendorListVersion).to.equal(1); 179 | }); 180 | 181 | it('sorts the vendor list by ID', function () { 182 | const consent = new ConsentString(); 183 | 184 | consent.setGlobalVendorList({ 185 | vendorListVersion: 1, 186 | purposes: [], 187 | vendors: [ 188 | { id: 2 }, 189 | { id: 1 }, 190 | ], 191 | }); 192 | 193 | expect(consent.getGlobalVendorList().vendors).to.deep.equal([ 194 | { id: 1 }, 195 | { id: 2 }, 196 | ]); 197 | }); 198 | }); 199 | }); 200 | -------------------------------------------------------------------------------- /src/consent-string.js: -------------------------------------------------------------------------------- 1 | const { encodeConsentString, getMaxVendorId, encodeVendorIdsToBits, encodePurposeIdsToBits } = require('./encode'); 2 | const { decodeConsentString } = require('./decode'); 3 | const { vendorVersionMap } = require('./utils/definitions'); 4 | /** 5 | * Regular expression for validating 6 | */ 7 | const consentLanguageRegexp = /^[a-z]{2}$/; 8 | let cachedString; 9 | 10 | class ConsentString { 11 | constructor(baseString = null) { 12 | this.maxVendorId = 0; 13 | this.created = new Date(); 14 | this.lastUpdated = new Date(); 15 | this.version = 1; 16 | this.vendorList = null; 17 | this.vendorListVersion = null; 18 | this.cmpId = null; 19 | this.cmpVersion = null; 20 | this.consentScreen = null; 21 | this.consentLanguage = null; 22 | this.allowedPurposeIds = []; 23 | this.allowedVendorIds = []; 24 | 25 | // Decode the base string 26 | if (baseString) { 27 | cachedString = baseString; 28 | Object.assign(this, decodeConsentString(baseString)); 29 | } 30 | } 31 | 32 | getConsentString(updateDate = true) { 33 | let retr; 34 | 35 | /** 36 | * check for cached string that was passed in. This avoids having to 37 | * decode the consent string and even to have a vendorlist 38 | */ 39 | if (cachedString && !updateDate) { 40 | retr = cachedString; 41 | } else { 42 | if (!this.vendorList) { 43 | throw new Error('ConsentString - A vendor list is required to encode a consent string'); 44 | } 45 | 46 | if (updateDate === true) { 47 | this.lastUpdated = new Date(); 48 | } 49 | 50 | retr = encodeConsentString({ 51 | version: this.getVersion(), 52 | vendorList: this.vendorList, 53 | allowedPurposeIds: this.allowedPurposeIds, 54 | allowedVendorIds: this.allowedVendorIds, 55 | created: this.created, 56 | lastUpdated: this.lastUpdated, 57 | cmpId: this.cmpId, 58 | cmpVersion: this.cmpVersion, 59 | consentScreen: this.consentScreen, 60 | consentLanguage: this.consentLanguage, 61 | vendorListVersion: this.vendorListVersion, 62 | }); 63 | 64 | cachedString = retr; 65 | } 66 | return retr; 67 | } 68 | getLastUpdated() { 69 | return this.lastUpdated; 70 | } 71 | setLastUpdated(date = null) { 72 | cachedString = ''; 73 | if (date) { 74 | this.lastUpdated = new Date(date); 75 | } else { 76 | this.lastUpdated = new Date(); 77 | } 78 | } 79 | getCreated() { 80 | return this.created; 81 | } 82 | setCreated(date = null) { 83 | cachedString = ''; 84 | if (date) { 85 | this.created = new Date(date); 86 | } else { 87 | this.created = new Date(); 88 | } 89 | } 90 | getMaxVendorId() { 91 | if (!this.maxVendorId) { 92 | if (this.vendorList) { 93 | this.maxVendorId = getMaxVendorId(this.vendorList.vendors); 94 | } 95 | } 96 | return this.maxVendorId; 97 | } 98 | getParsedVendorConsents() { 99 | return encodeVendorIdsToBits(getMaxVendorId(this.vendorList.vendors), this.allowedVendorIds); 100 | } 101 | getParsedPurposeConsents() { 102 | return encodePurposeIdsToBits(this.vendorList.purposes, this.allowedPurposeIds); 103 | } 104 | getMetadataString() { 105 | return encodeConsentString({ 106 | version: this.getVersion(), 107 | created: this.created, 108 | lastUpdated: this.lastUpdated, 109 | cmpId: this.cmpId, 110 | cmpVersion: this.cmpVersion, 111 | consentScreen: this.consentScreen, 112 | vendorListVersion: this.vendorListVersion, 113 | }); 114 | } 115 | static decodeMetadataString(encodedMetadata) { 116 | const decodedString = decodeConsentString(encodedMetadata); 117 | const metadata = {}; 118 | vendorVersionMap[decodedString.version] 119 | .metadataFields.forEach((field) => { 120 | metadata[field] = decodedString[field]; 121 | }); 122 | return metadata; 123 | } 124 | getVersion() { 125 | return this.version; 126 | } 127 | getVendorListVersion() { 128 | return this.vendorListVersion; 129 | } 130 | setGlobalVendorList(vendorList) { 131 | if (typeof vendorList !== 'object') { 132 | throw new Error('ConsentString - You must provide an object when setting the global vendor list'); 133 | } 134 | 135 | if ( 136 | !vendorList.vendorListVersion 137 | || !Array.isArray(vendorList.purposes) 138 | || !Array.isArray(vendorList.vendors) 139 | ) { 140 | // The provided vendor list does not look valid 141 | throw new Error('ConsentString - The provided vendor list does not respect the schema from the IAB EU’s GDPR Consent and Transparency Framework'); 142 | } 143 | 144 | // does a vendorList already exist and is it a different version 145 | if (!this.vendorList || this.vendorListVersion !== vendorList.vendorListVersion) { 146 | cachedString = ''; 147 | // Cloning the GVL 148 | // It's important as we might transform it and don't want to modify objects that we do not own 149 | this.vendorList = { 150 | vendorListVersion: vendorList.vendorListVersion, 151 | lastUpdated: vendorList.lastUpdated, 152 | purposes: vendorList.purposes, 153 | features: vendorList.features, 154 | 155 | // Clone the list and sort the vendors by ID (it breaks our range generation algorithm if they are not sorted) 156 | vendors: vendorList.vendors 157 | .slice(0) 158 | .sort((firstVendor, secondVendor) => (firstVendor.id < secondVendor.id ? -1 : 1)), 159 | }; 160 | this.vendorListVersion = vendorList.vendorListVersion; 161 | } 162 | } 163 | 164 | getGlobalVendorList() { 165 | return this.vendorList; 166 | } 167 | setCmpId(id) { 168 | if (id !== this.cmpId) { 169 | cachedString = ''; 170 | this.cmpId = id; 171 | } 172 | } 173 | getCmpId() { 174 | return this.cmpId; 175 | } 176 | setCmpVersion(version) { 177 | if (version !== this.cmpVersion) { 178 | cachedString = ''; 179 | this.cmpVersion = version; 180 | } 181 | } 182 | getCmpVersion() { 183 | return this.cmpVersion; 184 | } 185 | setConsentScreen(screenId) { 186 | if (screenId !== this.consentScreen) { 187 | cachedString = ''; 188 | this.consentScreen = screenId; 189 | } 190 | } 191 | getConsentScreen() { 192 | return this.consentScreen; 193 | } 194 | setConsentLanguage(language) { 195 | if (consentLanguageRegexp.test(language) === false) { 196 | throw new Error('ConsentString - The consent language must be a two-letter ISO639-1 code (en, fr, de, etc.)'); 197 | } 198 | 199 | if (language !== this.consentLanguage) { 200 | cachedString = ''; 201 | this.consentLanguage = language; 202 | } 203 | } 204 | getConsentLanguage() { 205 | return this.consentLanguage; 206 | } 207 | setPurposesAllowed(purposeIds) { 208 | cachedString = ''; 209 | this.allowedPurposeIds = purposeIds; 210 | } 211 | getPurposesAllowed() { 212 | return this.allowedPurposeIds; 213 | } 214 | setPurposeAllowed(purposeId, value) { 215 | const purposeIndex = this.allowedPurposeIds.indexOf(purposeId); 216 | 217 | cachedString = ''; 218 | 219 | if (value === true) { 220 | if (purposeIndex === -1) { 221 | this.allowedPurposeIds.push(purposeId); 222 | } 223 | } else if (value === false) { 224 | if (purposeIndex !== -1) { 225 | this.allowedPurposeIds.splice(purposeIndex, 1); 226 | } 227 | } 228 | } 229 | isPurposeAllowed(purposeId) { 230 | return this.allowedPurposeIds.indexOf(purposeId) !== -1; 231 | } 232 | setVendorsAllowed(vendorIds) { 233 | cachedString = ''; 234 | this.allowedVendorIds = vendorIds; 235 | } 236 | getVendorsAllowed() { 237 | return this.allowedVendorIds; 238 | } 239 | setVendorAllowed(vendorId, value) { 240 | const vendorIndex = this.allowedVendorIds.indexOf(vendorId); 241 | 242 | cachedString = ''; 243 | if (value === true) { 244 | if (vendorIndex === -1) { 245 | this.allowedVendorIds.push(vendorId); 246 | } 247 | } else if (value === false) { 248 | if (vendorIndex !== -1) { 249 | this.allowedVendorIds.splice(vendorIndex, 1); 250 | } 251 | } 252 | } 253 | isVendorAllowed(vendorId) { 254 | return this.allowedVendorIds.indexOf(vendorId) !== -1; 255 | } 256 | } 257 | 258 | module.exports = { 259 | ConsentString, 260 | }; 261 | -------------------------------------------------------------------------------- /test/utils/bits.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | 3 | const { 4 | encodeIntToBits, 5 | encodeBoolToBits, 6 | encodeDateToBits, 7 | encodeLetterToBits, 8 | encodeLanguageToBits, 9 | encodeToBase64, 10 | decodeBitsToInt, 11 | decodeBitsToDate, 12 | decodeBitsToBool, 13 | decodeBitsToLanguage, 14 | decodeBitsToLetter, 15 | decodeFromBase64, 16 | } = require('../../src/utils/bits'); 17 | 18 | describe('bits', function () { 19 | describe('encodeIntToBits', function () { 20 | it('encodes an integer to a bit string', function () { 21 | const bitString = encodeIntToBits(123); 22 | expect(bitString).to.equal('1111011'); 23 | }); 24 | 25 | it('encodes an integer to a bit string with padding', function () { 26 | const bitString = encodeIntToBits(123, 12); 27 | expect(bitString).to.equal('000001111011'); 28 | }); 29 | }); 30 | 31 | describe('encodeBoolToBits', function () { 32 | it('encodes a "true" boolean to a bit string', function () { 33 | const bitString = encodeBoolToBits(true); 34 | expect(bitString).to.equal('1'); 35 | }); 36 | it('encode a "false" boolean to a bit string', function () { 37 | const bitString = encodeBoolToBits(false); 38 | expect(bitString).to.equal('0'); 39 | }); 40 | }); 41 | 42 | describe('encodeDateToBits', function () { 43 | it('encode a date to a bit string', function () { 44 | const date = new Date(1512661975200); 45 | const bitString = encodeDateToBits(date); 46 | expect(bitString).to.equal('1110000101100111011110011001101000'); 47 | }); 48 | it('encode a date to a bit string with padding', function () { 49 | const date = new Date(1512661975200); 50 | const bitString = encodeDateToBits(date, 36); 51 | expect(bitString).to.equal('001110000101100111011110011001101000'); 52 | }); 53 | }); 54 | 55 | describe('encodeLetterToBits', function () { 56 | it('encodes a letter to a bit string', function () { 57 | expect(encodeLetterToBits('a')).to.equal('0'); 58 | expect(encodeLetterToBits('K')).to.equal('1010'); 59 | expect(encodeLetterToBits('z')).to.equal('11001'); 60 | }); 61 | 62 | it('encodes a letter to a bit string with padding', function () { 63 | expect(encodeLetterToBits('a', 6)).to.equal('000000'); 64 | expect(encodeLetterToBits('K', 6)).to.equal('001010'); 65 | expect(encodeLetterToBits('z', 6)).to.equal('011001'); 66 | }); 67 | }); 68 | 69 | describe('encodeLanguageToBits', function () { 70 | it('encodes a language code to a bit string', function () { 71 | expect(encodeLanguageToBits('en', 12)).to.equal('000100001101'); 72 | expect(encodeLanguageToBits('EN', 12)).to.equal('000100001101'); 73 | expect(encodeLanguageToBits('fr', 12)).to.equal('000101010001'); 74 | expect(encodeLanguageToBits('FR', 12)).to.equal('000101010001'); 75 | }); 76 | }); 77 | 78 | describe('decodeBitsToInt', function () { 79 | it('decodes a bit string to original encoded value', function () { 80 | const bitString = encodeIntToBits(123); 81 | const decoded = decodeBitsToInt(bitString, 0, bitString.length); 82 | expect(decoded).to.equal(123); 83 | }); 84 | }); 85 | 86 | describe('decodeBitsToDate', function () { 87 | it('decodes a bit string to original encoded value', function () { 88 | const now = new Date('2018-07-15 PDT'); 89 | const bitString = encodeDateToBits(now); 90 | const decoded = decodeBitsToDate(bitString, 0, bitString.length); 91 | expect(decoded.getTime()).to.equal(now.getTime()); 92 | }); 93 | }); 94 | 95 | describe('decodeBitsToBool', function () { 96 | it('decodes a bit string to original encoded "true" value', function () { 97 | const bitString = encodeBoolToBits(true); 98 | const decoded = decodeBitsToBool(bitString, 0, bitString.length); 99 | expect(decoded).to.equal(true); 100 | }); 101 | it('decodes a bit string to original encoded "false" value', function () { 102 | const bitString = encodeBoolToBits(false); 103 | const decoded = decodeBitsToBool(bitString, 0, bitString.length); 104 | expect(decoded).to.equal(false); 105 | }); 106 | }); 107 | 108 | describe('decodeBitsToLetter', function () { 109 | it('decodes a bit string to a letter', function () { 110 | expect(decodeBitsToLetter('000000'), 'a'); 111 | expect(decodeBitsToLetter('001010'), 'k'); 112 | expect(decodeBitsToLetter('011001'), 'z'); 113 | }); 114 | 115 | it('decodes a bit string to its original value', function () { 116 | const bitString = encodeLetterToBits('z', 6); 117 | const decoded = decodeBitsToLetter(bitString); 118 | expect(decoded).to.equal('z'); 119 | }); 120 | }); 121 | 122 | describe('decodeBitsToLanguage', function () { 123 | it('decodes a bit string to a language code', function () { 124 | expect(decodeBitsToLanguage('000100001101', 0, 12)).to.equal('en'); 125 | expect(decodeBitsToLanguage('000101010001', 0, 12)).to.equal('fr'); 126 | }); 127 | 128 | it('decodes a bit string to its original value', function () { 129 | const bitString = encodeLanguageToBits('en', 12); 130 | const decoded = decodeBitsToLanguage(bitString, 0, 12); 131 | expect(decoded).to.equal('en'); 132 | }); 133 | }); 134 | 135 | it('fails to encode a version that does not exist', function () { 136 | const aDate = new Date('2018-07-15 PDT'); 137 | 138 | const consentData = { 139 | version: 999, 140 | created: aDate, 141 | lastUpdated: aDate, 142 | cmpId: 1, 143 | vendorListVersion: 1, 144 | }; 145 | 146 | expect(() => encodeToBase64(consentData)).to.throw; 147 | }); 148 | 149 | it('fails to encode an invalid version', function () { 150 | const aDate = new Date('2018-07-15 PDT'); 151 | 152 | const consentData = { 153 | version: 'hello', 154 | created: aDate, 155 | lastUpdated: aDate, 156 | cmpId: 1, 157 | vendorListVersion: 1, 158 | }; 159 | 160 | expect(() => encodeToBase64(consentData)).to.throw; 161 | }); 162 | 163 | it('fails to decode an invalid version', function () { 164 | const bitString = encodeIntToBits(999, 6); 165 | expect(() => decodeFromBase64(bitString)).to.throw; 166 | }); 167 | 168 | it('encodes and decodes the vendor value with ranges back to original value', function () { 169 | const aDate = new Date('2018-07-15 PDT'); 170 | 171 | const consentData = { 172 | version: 1, 173 | created: aDate, 174 | lastUpdated: aDate, 175 | cmpId: 1, 176 | cmpVersion: 2, 177 | consentScreen: 3, 178 | consentLanguage: 'en', 179 | vendorListVersion: 1, 180 | purposeIdBitString: '111000001010101010001101', 181 | maxVendorId: 5, 182 | isRange: true, 183 | defaultConsent: false, 184 | numEntries: 2, 185 | vendorRangeList: [ 186 | { 187 | isRange: true, 188 | startVendorId: 2, 189 | endVendorId: 4, 190 | }, 191 | { 192 | isRange: false, 193 | startVendorId: 1, 194 | }, 195 | ], 196 | }; 197 | 198 | const bitString = encodeToBase64(consentData); 199 | const decoded = decodeFromBase64(bitString); 200 | 201 | expect(decoded).to.deep.equal(consentData); 202 | }); 203 | 204 | it('encodes and decodes the vendor value with range ranges back to original value', function () { 205 | const aDate = new Date('2018-07-15 PDT'); 206 | 207 | const consentData = { 208 | version: 1, 209 | created: aDate, 210 | lastUpdated: aDate, 211 | cmpId: 1, 212 | cmpVersion: 2, 213 | consentScreen: 3, 214 | consentLanguage: 'en', 215 | vendorListVersion: 1, 216 | purposeIdBitString: '111000001010101010001101', 217 | maxVendorId: 5, 218 | isRange: true, 219 | defaultConsent: false, 220 | numEntries: 2, 221 | vendorRangeList: [ 222 | { 223 | isRange: false, 224 | startVendorId: 2, 225 | }, 226 | { 227 | isRange: false, 228 | startVendorId: 1, 229 | }, 230 | ], 231 | }; 232 | 233 | const bitString = encodeToBase64(consentData); 234 | const decoded = decodeFromBase64(bitString); 235 | 236 | expect(decoded).to.deep.equal(consentData); 237 | }); 238 | 239 | it('encodes and decodes the vendor value without ranges back to original value', function () { 240 | const aDate = new Date('2018-07-15 PDT'); 241 | 242 | const consentData = { 243 | version: 1, 244 | created: aDate, 245 | lastUpdated: aDate, 246 | cmpId: 1, 247 | cmpVersion: 2, 248 | consentScreen: 3, 249 | consentLanguage: 'en', 250 | vendorListVersion: 1, 251 | purposeIdBitString: '000000001010101010001100', 252 | maxVendorId: 5, 253 | isRange: false, 254 | vendorIdBitString: '10011', 255 | }; 256 | 257 | const bitString = encodeToBase64(consentData); 258 | const decoded = decodeFromBase64(bitString); 259 | 260 | expect(decoded).to.deep.equal(consentData); 261 | }); 262 | }); 263 | -------------------------------------------------------------------------------- /src/utils/bits.js: -------------------------------------------------------------------------------- 1 | /* eslint no-use-before-define: off */ 2 | 3 | const base64 = require('base-64'); 4 | const { 5 | versionNumBits, 6 | vendorVersionMap, 7 | } = require('./definitions'); 8 | 9 | function repeat(count, string = '0') { 10 | let padString = ''; 11 | 12 | for (let i = 0; i < count; i += 1) { 13 | padString += string; 14 | } 15 | 16 | return padString; 17 | } 18 | 19 | function padLeft(string, padding) { 20 | return repeat(Math.max(0, padding)) + string; 21 | } 22 | 23 | function padRight(string, padding) { 24 | return string + repeat(Math.max(0, padding)); 25 | } 26 | 27 | function encodeIntToBits(number, numBits) { 28 | let bitString = ''; 29 | 30 | if (typeof number === 'number' && !isNaN(number)) { 31 | bitString = parseInt(number, 10).toString(2); 32 | } 33 | 34 | // Pad the string if not filling all bits 35 | if (numBits >= bitString.length) { 36 | bitString = padLeft(bitString, numBits - bitString.length); 37 | } 38 | 39 | // Truncate the string if longer than the number of bits 40 | if (bitString.length > numBits) { 41 | bitString = bitString.substring(0, numBits); 42 | } 43 | 44 | return bitString; 45 | } 46 | 47 | function encodeBoolToBits(value) { 48 | return encodeIntToBits(value === true ? 1 : 0, 1); 49 | } 50 | 51 | function encodeDateToBits(date, numBits) { 52 | if (date instanceof Date) { 53 | return encodeIntToBits(date.getTime() / 100, numBits); 54 | } 55 | return encodeIntToBits(date, numBits); 56 | } 57 | 58 | function encodeLetterToBits(letter, numBits) { 59 | return encodeIntToBits(letter.toUpperCase().charCodeAt(0) - 65, numBits); 60 | } 61 | 62 | function encodeLanguageToBits(language, numBits = 12) { 63 | return encodeLetterToBits(language.slice(0, 1), numBits / 2) 64 | + encodeLetterToBits(language.slice(1), numBits / 2); 65 | } 66 | 67 | function decodeBitsToInt(bitString, start, length) { 68 | return parseInt(bitString.substr(start, length), 2); 69 | } 70 | 71 | function decodeBitsToDate(bitString, start, length) { 72 | return new Date(decodeBitsToInt(bitString, start, length) * 100); 73 | } 74 | 75 | function decodeBitsToBool(bitString, start) { 76 | return parseInt(bitString.substr(start, 1), 2) === 1; 77 | } 78 | 79 | function decodeBitsToLetter(bitString) { 80 | const letterCode = decodeBitsToInt(bitString); 81 | return String.fromCharCode(letterCode + 65).toLowerCase(); 82 | } 83 | 84 | function decodeBitsToLanguage(bitString, start, length) { 85 | const languageBitString = bitString.substr(start, length); 86 | 87 | return decodeBitsToLetter(languageBitString.slice(0, length / 2)) 88 | + decodeBitsToLetter(languageBitString.slice(length / 2)); 89 | } 90 | 91 | function encodeField({ input, field }) { 92 | const { name, type, numBits, encoder, validator } = field; 93 | 94 | if (typeof validator === 'function') { 95 | if (!validator(input)) { 96 | return ''; 97 | } 98 | } 99 | if (typeof encoder === 'function') { 100 | return encoder(input); 101 | } 102 | 103 | const bitCount = typeof numBits === 'function' ? numBits(input) : numBits; 104 | 105 | const inputValue = input[name]; 106 | const fieldValue = inputValue === null || inputValue === undefined ? '' : inputValue; 107 | 108 | switch (type) { 109 | case 'int': 110 | return encodeIntToBits(fieldValue, bitCount); 111 | case 'bool': 112 | return encodeBoolToBits(fieldValue); 113 | case 'date': 114 | return encodeDateToBits(fieldValue, bitCount); 115 | case 'bits': 116 | return padRight(fieldValue, bitCount - fieldValue.length).substring(0, bitCount); 117 | case 'list': 118 | return fieldValue.reduce((acc, listValue) => acc + encodeFields({ 119 | input: listValue, 120 | fields: field.fields, 121 | }), ''); 122 | case 'language': 123 | return encodeLanguageToBits(fieldValue, bitCount); 124 | default: 125 | throw new Error(`ConsentString - Unknown field type ${type} for encoding`); 126 | } 127 | } 128 | 129 | function encodeFields({ input, fields }) { 130 | return fields.reduce((acc, field) => { 131 | acc += encodeField({ input, field }); 132 | 133 | return acc; 134 | }, ''); 135 | } 136 | 137 | function decodeField({ input, output, startPosition, field }) { 138 | const { type, numBits, decoder, validator, listCount } = field; 139 | 140 | if (typeof validator === 'function') { 141 | if (!validator(output)) { 142 | // Not decoding this field so make sure we start parsing the next field at 143 | // the same point 144 | return { newPosition: startPosition }; 145 | } 146 | } 147 | 148 | if (typeof decoder === 'function') { 149 | return decoder(input, output, startPosition); 150 | } 151 | 152 | const bitCount = typeof numBits === 'function' ? numBits(output) : numBits; 153 | 154 | switch (type) { 155 | case 'int': 156 | return { fieldValue: decodeBitsToInt(input, startPosition, bitCount) }; 157 | case 'bool': 158 | return { fieldValue: decodeBitsToBool(input, startPosition) }; 159 | case 'date': 160 | return { fieldValue: decodeBitsToDate(input, startPosition, bitCount) }; 161 | case 'bits': 162 | return { fieldValue: input.substr(startPosition, bitCount) }; 163 | case 'list': 164 | return decodeList(input, output, startPosition, field, listCount); 165 | case 'language': 166 | return { fieldValue: decodeBitsToLanguage(input, startPosition, bitCount) }; 167 | default: 168 | throw new Error(`ConsentString - Unknown field type ${type} for decoding`); 169 | } 170 | } 171 | 172 | function decodeList(input, output, startPosition, field, listCount) { 173 | let listEntryCount = 0; 174 | 175 | if (typeof listCount === 'function') { 176 | listEntryCount = listCount(output); 177 | } else if (typeof listCount === 'number') { 178 | listEntryCount = listCount; 179 | } 180 | 181 | let newPosition = startPosition; 182 | const fieldValue = []; 183 | 184 | for (let i = 0; i < listEntryCount; i += 1) { 185 | const decodedFields = decodeFields({ 186 | input, 187 | fields: field.fields, 188 | startPosition: newPosition, 189 | }); 190 | 191 | newPosition = decodedFields.newPosition; 192 | fieldValue.push(decodedFields.decodedObject); 193 | } 194 | 195 | return { fieldValue, newPosition }; 196 | } 197 | 198 | function decodeFields({ input, fields, startPosition = 0 }) { 199 | let position = startPosition; 200 | 201 | const decodedObject = fields.reduce((acc, field) => { 202 | const { name, numBits } = field; 203 | const { fieldValue, newPosition } = decodeField({ 204 | input, 205 | output: acc, 206 | startPosition: position, 207 | field, 208 | }); 209 | 210 | if (fieldValue !== undefined) { 211 | acc[name] = fieldValue; 212 | } 213 | 214 | if (newPosition !== undefined) { 215 | position = newPosition; 216 | } else if (typeof numBits === 'number') { 217 | position += numBits; 218 | } 219 | 220 | return acc; 221 | }, {}); 222 | 223 | return { 224 | decodedObject, 225 | newPosition: position, 226 | }; 227 | } 228 | 229 | /** 230 | * Encode the data properties to a bit string. Encoding will encode 231 | * either `selectedVendorIds` or the `vendorRangeList` depending on 232 | * the value of the `isRange` flag. 233 | */ 234 | function encodeDataToBits(data, definitionMap) { 235 | const { version } = data; 236 | 237 | if (typeof version !== 'number') { 238 | throw new Error('ConsentString - No version field to encode'); 239 | } else if (!definitionMap[version]) { 240 | throw new Error(`ConsentString - No definition for version ${version}`); 241 | } else { 242 | const fields = definitionMap[version].fields; 243 | return encodeFields({ input: data, fields }); 244 | } 245 | } 246 | 247 | /** 248 | * Take all fields required to encode the consent string and produce the URL safe Base64 encoded value 249 | */ 250 | function encodeToBase64(data, definitionMap = vendorVersionMap) { 251 | const binaryValue = encodeDataToBits(data, definitionMap); 252 | 253 | if (binaryValue) { 254 | // Pad length to multiple of 8 255 | const paddedBinaryValue = padRight(binaryValue, 7 - ((binaryValue.length + 7) % 8)); 256 | 257 | // Encode to bytes 258 | let bytes = ''; 259 | for (let i = 0; i < paddedBinaryValue.length; i += 8) { 260 | bytes += String.fromCharCode(parseInt(paddedBinaryValue.substr(i, 8), 2)); 261 | } 262 | 263 | // Make base64 string URL friendly 264 | return base64.encode(bytes) 265 | .replace(/\+/g, '-') 266 | .replace(/\//g, '_') 267 | .replace(/=+$/, ''); 268 | } 269 | 270 | return null; 271 | } 272 | 273 | function decodeConsentStringBitValue(bitString, definitionMap = vendorVersionMap) { 274 | const version = decodeBitsToInt(bitString, 0, versionNumBits); 275 | 276 | if (typeof version !== 'number') { 277 | throw new Error('ConsentString - Unknown version number in the string to decode'); 278 | } else if (!vendorVersionMap[version]) { 279 | throw new Error(`ConsentString - Unsupported version ${version} in the string to decode`); 280 | } 281 | 282 | const fields = definitionMap[version].fields; 283 | const { decodedObject } = decodeFields({ input: bitString, fields }); 284 | 285 | return decodedObject; 286 | } 287 | 288 | /** 289 | * Decode the (URL safe Base64) value of a consent string into an object. 290 | */ 291 | function decodeFromBase64(consentString, definitionMap) { 292 | // Add padding 293 | let unsafe = consentString; 294 | while (unsafe.length % 4 !== 0) { 295 | unsafe += '='; 296 | } 297 | 298 | // Replace safe characters 299 | unsafe = unsafe 300 | .replace(/-/g, '+') 301 | .replace(/_/g, '/'); 302 | 303 | const bytes = base64.decode(unsafe); 304 | 305 | let inputBits = ''; 306 | for (let i = 0; i < bytes.length; i += 1) { 307 | const bitString = bytes.charCodeAt(i).toString(2); 308 | inputBits += padLeft(bitString, 8 - bitString.length); 309 | } 310 | 311 | return decodeConsentStringBitValue(inputBits, definitionMap); 312 | } 313 | 314 | function decodeBitsToIds(bitString) { 315 | return bitString.split('').reduce((acc, bit, index) => { 316 | if (bit === '1') { 317 | if (acc.indexOf(index + 1) === -1) { 318 | acc.push(index + 1); 319 | } 320 | } 321 | return acc; 322 | }, []); 323 | } 324 | 325 | module.exports = { 326 | padRight, 327 | padLeft, 328 | encodeField, 329 | encodeDataToBits, 330 | encodeIntToBits, 331 | encodeBoolToBits, 332 | encodeDateToBits, 333 | encodeLanguageToBits, 334 | encodeLetterToBits, 335 | encodeToBase64, 336 | decodeBitsToIds, 337 | decodeBitsToInt, 338 | decodeBitsToDate, 339 | decodeBitsToBool, 340 | decodeBitsToLanguage, 341 | decodeBitsToLetter, 342 | decodeFromBase64, 343 | }; 344 | -------------------------------------------------------------------------------- /test/vendors.json: -------------------------------------------------------------------------------- 1 | { 2 | "vendorListVersion": 14, 3 | "lastUpdated": "2018-05-04T15:56:34Z", 4 | "purposes": [ 5 | { 6 | "id": 1, 7 | "name": "Information storage and access", 8 | "description": "The storage of information, or access to information that is already stored, on your device such as advertising identifiers, device identifiers, cookies, and similar technologies." 9 | }, 10 | { 11 | "id": 2, 12 | "name": "Personalisation", 13 | "description": "The collection and processing of information about your use of this service to subsequently personalise advertising and/or content for you in other contexts, such as on other websites or apps, over time. Typically, the content of the site or app is used to make inferences about your interests, which inform future selection of advertising and/or content." 14 | }, 15 | { 16 | "id": 3, 17 | "name": "Ad selection, delivery, reporting", 18 | "description": "The collection of information, and combination with previously collected information, to select and deliver advertisements for you, and to measure the delivery and effectiveness of such advertisements. This includes using previously collected information about your interests to select ads, processing data about what advertisements were shown, how often they were shown, when and where they were shown, and whether you took any action related to the advertisement, including for example clicking an ad or making a purchase. This does not include personalisation, which is the collection and processing of information about your use of this service to subsequently personalise advertising and/or content for you in other contexts, such as websites or apps, over time." 19 | }, 20 | { 21 | "id": 4, 22 | "name": "Content selection, delivery, reporting", 23 | "description": "The collection of information, and combination with previously collected information, to select and deliver content for you, and to measure the delivery and effectiveness of such content. This includes using previously collected information about your interests to select content, processing data about what content was shown, how often or how long it was shown, when and where it was shown, and whether the you took any action related to the content, including for example clicking on content. This does not include personalisation, which is the collection and processing of information about your use of this service to subsequently personalise content and/or advertising for you in other contexts, such as websites or apps, over time." 24 | }, 25 | { 26 | "id": 5, 27 | "name": "Measurement", 28 | "description": "The collection of information about your use of the content, and combination with previously collected information, used to measure, understand, and report on your usage of the service. This does not include personalisation, the collection of information about your use of this service to subsequently personalise content and/or advertising for you in other contexts, i.e. on other service, such as websites or apps, over time." 29 | } 30 | ], 31 | "features": [ 32 | { 33 | "id": 1, 34 | "name": "Matching Data to Offline Sources", 35 | "description": "Combining data from offline sources that were initially collected in other contexts." 36 | }, 37 | { 38 | "id": 2, 39 | "name": "Linking Devices", 40 | "description": "Allow processing of a user's data to connect such user across multiple devices." 41 | }, 42 | { 43 | "id": 3, 44 | "name": "Precise Geographic Location Data", 45 | "description": "Allow processing of a user's precise geographic location data in support of a purpose for which that certain third party has consent." 46 | } 47 | ], 48 | "vendors": [ 49 | { 50 | "id": 8, 51 | "name": "Emerse Sverige AB", 52 | "policyUrl": "https://www.emerse.com/privacy-policy/", 53 | "purposeIds": [ 54 | 1, 55 | 2, 56 | 4 57 | ], 58 | "legIntPurposeIds": [ 59 | 3, 60 | 5 61 | ], 62 | "featureIds": [ 63 | 1, 64 | 2 65 | ] 66 | }, 67 | { 68 | "id": 12, 69 | "name": "BeeswaxIO Corporation", 70 | "policyUrl": "https://www.beeswax.com/privacy.html", 71 | "purposeIds": [ 72 | 1, 73 | 3, 74 | 5 75 | ], 76 | "legIntPurposeIds": [], 77 | "featureIds": [ 78 | 3 79 | ] 80 | }, 81 | { 82 | "id": 28, 83 | "name": "TripleLift, Inc.", 84 | "policyUrl": "https://triplelift.com/privacy/", 85 | "purposeIds": [ 86 | 1, 87 | 3 88 | ], 89 | "legIntPurposeIds": [], 90 | "featureIds": [ 91 | 3 92 | ] 93 | }, 94 | { 95 | "id": 9, 96 | "name": "AdMaxim Inc.", 97 | "policyUrl": "http://www.admaxim.com/privacy/", 98 | "purposeIds": [ 99 | 1, 100 | 2, 101 | 3, 102 | 4, 103 | 5 104 | ], 105 | "legIntPurposeIds": [], 106 | "featureIds": [ 107 | 1, 108 | 2, 109 | 3 110 | ] 111 | }, 112 | { 113 | "id": 27, 114 | "name": "ADventori SAS", 115 | "policyUrl": "https://www.adventori.com/with-us/legal-notice/", 116 | "purposeIds": [ 117 | 2 118 | ], 119 | "legIntPurposeIds": [ 120 | 1, 121 | 3, 122 | 4, 123 | 5 124 | ], 125 | "featureIds": [] 126 | }, 127 | { 128 | "id": 25, 129 | "name": "Oath (EMEA) Limited", 130 | "policyUrl": "https://policies.oath.com/ie/en/oath/privacy/index.html", 131 | "purposeIds": [ 132 | 1, 133 | 2 134 | ], 135 | "legIntPurposeIds": [ 136 | 3, 137 | 5 138 | ], 139 | "featureIds": [ 140 | 1, 141 | 2, 142 | 3 143 | ] 144 | }, 145 | { 146 | "id": 26, 147 | "name": "Venatus Media Limited", 148 | "policyUrl": "https://www.venatusmedia.com/privacy/", 149 | "purposeIds": [ 150 | 1, 151 | 2, 152 | 3, 153 | 4, 154 | 5 155 | ], 156 | "legIntPurposeIds": [], 157 | "featureIds": [] 158 | }, 159 | { 160 | "id": 1, 161 | "name": "Exponential Interactive, Inc", 162 | "policyUrl": "http://exponential.com/privacy", 163 | "purposeIds": [ 164 | 1, 165 | 2, 166 | 3, 167 | 4, 168 | 5 169 | ], 170 | "legIntPurposeIds": [], 171 | "featureIds": [] 172 | }, 173 | { 174 | "id": 6, 175 | "name": "AdSpirit GmbH", 176 | "policyUrl": "http://www.adspirit.de/privacy", 177 | "purposeIds": [ 178 | 1, 179 | 2, 180 | 3, 181 | 4, 182 | 5 183 | ], 184 | "legIntPurposeIds": [], 185 | "featureIds": [] 186 | }, 187 | { 188 | "id": 30, 189 | "name": "BidTheatre AB", 190 | "policyUrl": "https://www.bidtheatre.com/privacy-policy", 191 | "purposeIds": [ 192 | 1, 193 | 2, 194 | 3 195 | ], 196 | "legIntPurposeIds": [], 197 | "featureIds": [ 198 | 2, 199 | 3 200 | ] 201 | }, 202 | { 203 | "id": 24, 204 | "name": "Conversant Europe Ltd.", 205 | "policyUrl": "https://www.conversantmedia.eu/legal/privacy-policy", 206 | "purposeIds": [ 207 | 1 208 | ], 209 | "legIntPurposeIds": [ 210 | 2, 211 | 3, 212 | 4, 213 | 5 214 | ], 215 | "featureIds": [ 216 | 1, 217 | 2, 218 | 3 219 | ] 220 | }, 221 | { 222 | "id": 29, 223 | "name": "Etarget SE", 224 | "policyUrl": "https://www.etarget.sk/privacy.php", 225 | "purposeIds": [ 226 | 1, 227 | 2, 228 | 3, 229 | 4, 230 | 5 231 | ], 232 | "legIntPurposeIds": [], 233 | "featureIds": [ 234 | 1 235 | ] 236 | }, 237 | { 238 | "id": 39, 239 | "name": "ADITION technologies AG", 240 | "policyUrl": "adition.com/datenschutz", 241 | "purposeIds": [], 242 | "legIntPurposeIds": [ 243 | 1, 244 | 2, 245 | 3, 246 | 4, 247 | 5 248 | ], 249 | "featureIds": [ 250 | 1, 251 | 2, 252 | 3 253 | ] 254 | }, 255 | { 256 | "id": 11, 257 | "name": "Quantcast International Limited", 258 | "policyUrl": "https://www.quantcast.com/privacy/", 259 | "purposeIds": [ 260 | 1 261 | ], 262 | "legIntPurposeIds": [ 263 | 2, 264 | 3, 265 | 4, 266 | 5 267 | ], 268 | "featureIds": [ 269 | 1 270 | ] 271 | }, 272 | { 273 | "id": 15, 274 | "name": "Adikteev", 275 | "policyUrl": "https://www.adikteev.com/eu/privacy/", 276 | "purposeIds": [ 277 | 1, 278 | 2 279 | ], 280 | "legIntPurposeIds": [], 281 | "featureIds": [] 282 | }, 283 | { 284 | "id": 4, 285 | "name": "Roq.ad GmbH", 286 | "policyUrl": "https://www.roq.ad/privacy-policy", 287 | "purposeIds": [ 288 | 1, 289 | 2, 290 | 3, 291 | 4, 292 | 5 293 | ], 294 | "legIntPurposeIds": [], 295 | "featureIds": [ 296 | 2, 297 | 3 298 | ] 299 | }, 300 | { 301 | "id": 7, 302 | "name": "Vibrant Media Limited", 303 | "policyUrl": "https://www.vibrantmedia.com/en/privacy-policy/", 304 | "purposeIds": [ 305 | 2, 306 | 3, 307 | 4, 308 | 5 309 | ], 310 | "legIntPurposeIds": [ 311 | 1 312 | ], 313 | "featureIds": [] 314 | }, 315 | { 316 | "id": 2, 317 | "name": "Captify Technologies Limited", 318 | "policyUrl": "http://www.captify.co.uk/privacy-policy/", 319 | "purposeIds": [ 320 | 2, 321 | 3, 322 | 5 323 | ], 324 | "legIntPurposeIds": [ 325 | 1 326 | ], 327 | "featureIds": [ 328 | 2 329 | ] 330 | }, 331 | { 332 | "id": 37, 333 | "name": "NEURAL.ONE", 334 | "policyUrl": "https://web.neural.one/privacy-policy/", 335 | "purposeIds": [ 336 | 1, 337 | 2, 338 | 3, 339 | 5 340 | ], 341 | "legIntPurposeIds": [], 342 | "featureIds": [ 343 | 1, 344 | 2 345 | ] 346 | }, 347 | { 348 | "id": 13, 349 | "name": "Sovrn Holdings Inc", 350 | "policyUrl": "https://www.sovrn.com/sovrn-privacy/", 351 | "purposeIds": [ 352 | 1, 353 | 2, 354 | 3 355 | ], 356 | "legIntPurposeIds": [], 357 | "featureIds": [ 358 | 2, 359 | 3 360 | ] 361 | }, 362 | { 363 | "id": 34, 364 | "name": "NEORY GmbH", 365 | "policyUrl": "https://www.neory.com/privacy.html", 366 | "purposeIds": [ 367 | 1, 368 | 2, 369 | 4, 370 | 5 371 | ], 372 | "legIntPurposeIds": [ 373 | 3 374 | ], 375 | "featureIds": [] 376 | }, 377 | { 378 | "id": 32, 379 | "name": "AppNexus Inc.", 380 | "policyUrl": "https://www.appnexus.com/en/company/platform-privacy-policy", 381 | "purposeIds": [ 382 | 1 383 | ], 384 | "legIntPurposeIds": [ 385 | 3 386 | ], 387 | "featureIds": [ 388 | 2, 389 | 3 390 | ] 391 | }, 392 | { 393 | "id": 10, 394 | "name": "Index Exchange, Inc. ", 395 | "policyUrl": "www.indexexchange.com/privacy", 396 | "purposeIds": [ 397 | 1 398 | ], 399 | "legIntPurposeIds": [], 400 | "featureIds": [ 401 | 2, 402 | 3 403 | ] 404 | }, 405 | { 406 | "id": 57, 407 | "name": "ADARA MEDIA UNLIMITED", 408 | "policyUrl": "https://adara.com/2018/04/10/adara-gdpr-faq/", 409 | "purposeIds": [ 410 | 1, 411 | 2, 412 | 3, 413 | 4, 414 | 5 415 | ], 416 | "legIntPurposeIds": [], 417 | "featureIds": [ 418 | 1, 419 | 2 420 | ] 421 | }, 422 | { 423 | "id": 63, 424 | "name": "Avocet Systems Limited", 425 | "policyUrl": "http://www.avocet.io/privacy-policy", 426 | "purposeIds": [], 427 | "legIntPurposeIds": [ 428 | 1, 429 | 3 430 | ], 431 | "featureIds": [] 432 | }, 433 | { 434 | "id": 51, 435 | "name": "xAd, Inc. dba GroundTruth", 436 | "policyUrl": "https://www.groundtruth.com/privacy-policy/", 437 | "purposeIds": [ 438 | 1, 439 | 2, 440 | 3, 441 | 4, 442 | 5 443 | ], 444 | "legIntPurposeIds": [], 445 | "featureIds": [ 446 | 1, 447 | 2, 448 | 3 449 | ] 450 | }, 451 | { 452 | "id": 49, 453 | "name": "Tradelab, SAS", 454 | "policyUrl": "http://tradelab.com/en/privacy/", 455 | "purposeIds": [ 456 | 1, 457 | 2, 458 | 3 459 | ], 460 | "legIntPurposeIds": [ 461 | 5 462 | ], 463 | "featureIds": [ 464 | 1, 465 | 2, 466 | 3 467 | ] 468 | }, 469 | { 470 | "id": 45, 471 | "name": "Smart Adserver", 472 | "policyUrl": "http://smartadserver.com/company/privacy-policy/", 473 | "purposeIds": [ 474 | 1, 475 | 2 476 | ], 477 | "legIntPurposeIds": [ 478 | 3, 479 | 5 480 | ], 481 | "featureIds": [ 482 | 3 483 | ] 484 | }, 485 | { 486 | "id": 52, 487 | "name": "The Rubicon Project, Limited", 488 | "policyUrl": "http://rubiconproject.com/rubicon-project-yield-optimization-privacy-policy/", 489 | "purposeIds": [ 490 | 1 491 | ], 492 | "legIntPurposeIds": [ 493 | 2, 494 | 3, 495 | 4, 496 | 5 497 | ], 498 | "featureIds": [ 499 | 3 500 | ] 501 | }, 502 | { 503 | "id": 35, 504 | "name": "Purch Group, Inc.", 505 | "policyUrl": "http://www.purch.com/privacy-policy/", 506 | "purposeIds": [ 507 | 1 508 | ], 509 | "legIntPurposeIds": [ 510 | 3, 511 | 5 512 | ], 513 | "featureIds": [] 514 | }, 515 | { 516 | "id": 71, 517 | "name": "Dataxu, Inc. ", 518 | "policyUrl": "https://www.dataxu.com/about-us/privacy/data-collection-platform/", 519 | "purposeIds": [ 520 | 1, 521 | 2, 522 | 3 523 | ], 524 | "legIntPurposeIds": [], 525 | "featureIds": [ 526 | 1, 527 | 2, 528 | 3 529 | ] 530 | }, 531 | { 532 | "id": 79, 533 | "name": "MediaMath, Inc.", 534 | "policyUrl": "http://www.mediamath.com/privacy-policy/", 535 | "purposeIds": [ 536 | 1 537 | ], 538 | "legIntPurposeIds": [ 539 | 2, 540 | 3, 541 | 4, 542 | 5 543 | ], 544 | "featureIds": [ 545 | 1, 546 | 2, 547 | 3 548 | ] 549 | }, 550 | { 551 | "id": 91, 552 | "name": "Criteo SA", 553 | "policyUrl": "https://www.criteo.com/privacy/", 554 | "purposeIds": [ 555 | 1, 556 | 2, 557 | 3 558 | ], 559 | "legIntPurposeIds": [], 560 | "featureIds": [ 561 | 1, 562 | 2 563 | ] 564 | }, 565 | { 566 | "id": 85, 567 | "name": "Crimtan Holdings Limited", 568 | "policyUrl": "https://crimtan.com/privacy/", 569 | "purposeIds": [ 570 | 1, 571 | 2, 572 | 3, 573 | 4, 574 | 5 575 | ], 576 | "legIntPurposeIds": [], 577 | "featureIds": [ 578 | 1, 579 | 2, 580 | 3 581 | ] 582 | }, 583 | { 584 | "id": 16, 585 | "name": "RTB House S.A.", 586 | "policyUrl": "https://www.rtbhouse.com/privacy/", 587 | "purposeIds": [ 588 | 1, 589 | 2, 590 | 3, 591 | 4, 592 | 5 593 | ], 594 | "legIntPurposeIds": [], 595 | "featureIds": [] 596 | }, 597 | { 598 | "id": 86, 599 | "name": "Scene Stealer Limited", 600 | "policyUrl": "http://scenestealer.tv/privacy-policy/", 601 | "purposeIds": [ 602 | 1, 603 | 2, 604 | 3, 605 | 4, 606 | 5 607 | ], 608 | "legIntPurposeIds": [], 609 | "featureIds": [ 610 | 3 611 | ] 612 | }, 613 | { 614 | "id": 94, 615 | "name": "Blis Media Limited", 616 | "policyUrl": "http://www.blis.com/privacy/", 617 | "purposeIds": [ 618 | 1, 619 | 2, 620 | 3, 621 | 4, 622 | 5 623 | ], 624 | "legIntPurposeIds": [], 625 | "featureIds": [ 626 | 1, 627 | 2, 628 | 3 629 | ] 630 | }, 631 | { 632 | "id": 73, 633 | "name": "Simplifi Holdings Inc.", 634 | "policyUrl": "https://www.simpli.fi/site-privacy-policy2/", 635 | "purposeIds": [ 636 | 2, 637 | 3, 638 | 4, 639 | 5 640 | ], 641 | "legIntPurposeIds": [ 642 | 1 643 | ], 644 | "featureIds": [ 645 | 2, 646 | 3 647 | ] 648 | }, 649 | { 650 | "id": 67, 651 | "name": "LifeStreet Corporation", 652 | "policyUrl": "http://www.lifestreet.com/privacy/", 653 | "purposeIds": [ 654 | 1, 655 | 2, 656 | 3, 657 | 4, 658 | 5 659 | ], 660 | "legIntPurposeIds": [], 661 | "featureIds": [] 662 | }, 663 | { 664 | "id": 33, 665 | "name": "ShareThis, Inc.", 666 | "policyUrl": "http://www.sharethis.com/privacy/", 667 | "purposeIds": [ 668 | 3, 669 | 4 670 | ], 671 | "legIntPurposeIds": [ 672 | 1, 673 | 5 674 | ], 675 | "featureIds": [] 676 | }, 677 | { 678 | "id": 20, 679 | "name": "N Technologies Inc.", 680 | "policyUrl": "https://n.rich/privacy-notice", 681 | "purposeIds": [], 682 | "legIntPurposeIds": [ 683 | 1, 684 | 2, 685 | 3, 686 | 4, 687 | 5 688 | ], 689 | "featureIds": [ 690 | 2 691 | ] 692 | }, 693 | { 694 | "id": 55, 695 | "name": "Madison Logic, Inc.", 696 | "policyUrl": "https://www.madisonlogic.com/privacy/", 697 | "purposeIds": [], 698 | "legIntPurposeIds": [ 699 | 1, 700 | 2, 701 | 3, 702 | 4, 703 | 5 704 | ], 705 | "featureIds": [ 706 | 1, 707 | 2, 708 | 3 709 | ] 710 | }, 711 | { 712 | "id": 53, 713 | "name": "Sirdata", 714 | "policyUrl": "https://www.sirdata.com/privacy/", 715 | "purposeIds": [], 716 | "legIntPurposeIds": [ 717 | 1, 718 | 2, 719 | 3, 720 | 4, 721 | 5 722 | ], 723 | "featureIds": [ 724 | 1, 725 | 2 726 | ] 727 | }, 728 | { 729 | "id": 69, 730 | "name": "OpenX Software Ltd. and its affiliates", 731 | "policyUrl": "https://www.openx.com/legal/privacy-policy/", 732 | "purposeIds": [ 733 | 1, 734 | 2, 735 | 3 736 | ], 737 | "legIntPurposeIds": [], 738 | "featureIds": [ 739 | 1, 740 | 2, 741 | 3 742 | ] 743 | }, 744 | { 745 | "id": 98, 746 | "name": "mPlatform", 747 | "policyUrl": "https://www.groupm.com/mplatform-privacy-policy", 748 | "purposeIds": [ 749 | 1, 750 | 2, 751 | 3, 752 | 4 753 | ], 754 | "legIntPurposeIds": [ 755 | 5 756 | ], 757 | "featureIds": [ 758 | 1, 759 | 2, 760 | 3 761 | ] 762 | }, 763 | { 764 | "id": 62, 765 | "name": "Justpremium BV", 766 | "policyUrl": "http://justpremium.com/privacy-policy/", 767 | "purposeIds": [ 768 | 1, 769 | 3 770 | ], 771 | "legIntPurposeIds": [], 772 | "featureIds": [] 773 | }, 774 | { 775 | "id": 19, 776 | "name": "Intent Media, Inc.", 777 | "policyUrl": "https://intentmedia.com/privacy-policy/", 778 | "purposeIds": [ 779 | 1 780 | ], 781 | "legIntPurposeIds": [ 782 | 2, 783 | 3, 784 | 4, 785 | 5 786 | ], 787 | "featureIds": [ 788 | 2 789 | ] 790 | }, 791 | { 792 | "id": 43, 793 | "name": "Vdopia DBA Chocolate Platform", 794 | "policyUrl": "https://chocolateplatform.com/privacy-policy/", 795 | "purposeIds": [ 796 | 1, 797 | 3 798 | ], 799 | "legIntPurposeIds": [], 800 | "featureIds": [ 801 | 3 802 | ] 803 | }, 804 | { 805 | "id": 36, 806 | "name": "RhythmOne, LLC", 807 | "policyUrl": "https://www.rhythmone.com/privacy-policy", 808 | "purposeIds": [], 809 | "legIntPurposeIds": [ 810 | 1, 811 | 2, 812 | 3, 813 | 4, 814 | 5 815 | ], 816 | "featureIds": [ 817 | 1, 818 | 2, 819 | 3 820 | ] 821 | }, 822 | { 823 | "id": 80, 824 | "name": "Sharethrough, Inc", 825 | "policyUrl": "https://platform-cdn.sharethrough.com/privacy-policy", 826 | "purposeIds": [ 827 | 3, 828 | 5 829 | ], 830 | "legIntPurposeIds": [ 831 | 1 832 | ], 833 | "featureIds": [] 834 | }, 835 | { 836 | "id": 81, 837 | "name": "PulsePoint, Inc.", 838 | "policyUrl": "https://www.pulsepoint.com/privacy-policy", 839 | "purposeIds": [ 840 | 1, 841 | 2, 842 | 3, 843 | 4, 844 | 5 845 | ], 846 | "legIntPurposeIds": [], 847 | "featureIds": [ 848 | 1, 849 | 2, 850 | 3 851 | ] 852 | }, 853 | { 854 | "id": 23, 855 | "name": "Amobee, Inc. ", 856 | "policyUrl": "https://www.amobee.com/trust/privacy-guidelines", 857 | "purposeIds": [], 858 | "legIntPurposeIds": [ 859 | 1, 860 | 2, 861 | 3, 862 | 4, 863 | 5 864 | ], 865 | "featureIds": [ 866 | 1, 867 | 2, 868 | 3 869 | ] 870 | }, 871 | { 872 | "id": 75, 873 | "name": "M32 Media Inc", 874 | "policyUrl": "https://m32.media/privacy-cookie-policy/", 875 | "purposeIds": [ 876 | 1, 877 | 2, 878 | 3, 879 | 4, 880 | 5 881 | ], 882 | "legIntPurposeIds": [], 883 | "featureIds": [ 884 | 3 885 | ] 886 | }, 887 | { 888 | "id": 17, 889 | "name": "Greenhouse Group BV (with its trademark LemonPI)", 890 | "policyUrl": "https://www.lemonpi.io/privacy-policy/", 891 | "purposeIds": [ 892 | 1, 893 | 2, 894 | 3, 895 | 4, 896 | 5 897 | ], 898 | "legIntPurposeIds": [], 899 | "featureIds": [ 900 | 2 901 | ] 902 | }, 903 | { 904 | "id": 61, 905 | "name": "GumGum, Inc.", 906 | "policyUrl": "https://gumgum.com/privacy-policy", 907 | "purposeIds": [ 908 | 1, 909 | 2, 910 | 3, 911 | 5 912 | ], 913 | "legIntPurposeIds": [], 914 | "featureIds": [ 915 | 3 916 | ] 917 | }, 918 | { 919 | "id": 40, 920 | "name": "Active Agent AG", 921 | "policyUrl": "http://www.active-agent.com/de/unternehmen/datenschutzerklaerung/", 922 | "purposeIds": [], 923 | "legIntPurposeIds": [ 924 | 1, 925 | 2, 926 | 3, 927 | 5 928 | ], 929 | "featureIds": [ 930 | 1, 931 | 2, 932 | 3 933 | ] 934 | }, 935 | { 936 | "id": 76, 937 | "name": "PubMatic, Inc.", 938 | "policyUrl": "https://pubmatic.com/privacy-policy/", 939 | "purposeIds": [ 940 | 1, 941 | 2 942 | ], 943 | "legIntPurposeIds": [ 944 | 3, 945 | 4, 946 | 5 947 | ], 948 | "featureIds": [] 949 | }, 950 | { 951 | "id": 89, 952 | "name": "Tapad, Inc. ", 953 | "policyUrl": "https://www.tapad.com/privacy", 954 | "purposeIds": [ 955 | 1 956 | ], 957 | "legIntPurposeIds": [ 958 | 2, 959 | 3, 960 | 5 961 | ], 962 | "featureIds": [ 963 | 2 964 | ] 965 | }, 966 | { 967 | "id": 46, 968 | "name": "Skimbit Ltd", 969 | "policyUrl": "https://skimlinks.com/pages/privacy-policy", 970 | "purposeIds": [ 971 | 1, 972 | 2, 973 | 3 974 | ], 975 | "legIntPurposeIds": [ 976 | 5 977 | ], 978 | "featureIds": [] 979 | }, 980 | { 981 | "id": 66, 982 | "name": "adsquare GmbH", 983 | "policyUrl": "www.adsquare.com/privacy", 984 | "purposeIds": [ 985 | 1, 986 | 2, 987 | 3, 988 | 5 989 | ], 990 | "legIntPurposeIds": [], 991 | "featureIds": [ 992 | 1, 993 | 2, 994 | 3 995 | ] 996 | }, 997 | { 998 | "id": 105, 999 | "name": "Impression Desk Technologies Limited", 1000 | "policyUrl": "impressiondesk.com", 1001 | "purposeIds": [ 1002 | 1, 1003 | 2, 1004 | 3, 1005 | 4, 1006 | 5 1007 | ], 1008 | "legIntPurposeIds": [], 1009 | "featureIds": [ 1010 | 2, 1011 | 3 1012 | ] 1013 | }, 1014 | { 1015 | "id": 41, 1016 | "name": "Adverline", 1017 | "policyUrl": "https://www.adverline.com/privacy/", 1018 | "purposeIds": [ 1019 | 2 1020 | ], 1021 | "legIntPurposeIds": [ 1022 | 1, 1023 | 3 1024 | ], 1025 | "featureIds": [] 1026 | }, 1027 | { 1028 | "id": 3, 1029 | "name": "affilinet", 1030 | "policyUrl": "https://www.affili.net/de/footeritem/datenschutz", 1031 | "purposeIds": [ 1032 | 2, 1033 | 3, 1034 | 4, 1035 | 5 1036 | ], 1037 | "legIntPurposeIds": [], 1038 | "featureIds": [] 1039 | }, 1040 | { 1041 | "id": 82, 1042 | "name": "Smaato, Inc.", 1043 | "policyUrl": "https://www.smaato.com/privacy/", 1044 | "purposeIds": [ 1045 | 1, 1046 | 2, 1047 | 3, 1048 | 4, 1049 | 5 1050 | ], 1051 | "legIntPurposeIds": [], 1052 | "featureIds": [ 1053 | 3 1054 | ] 1055 | }, 1056 | { 1057 | "id": 60, 1058 | "name": "Rakuten Marketing LLC", 1059 | "policyUrl": "https://rakutenmarketing.com/legal-notices/services-privacy-policy", 1060 | "purposeIds": [ 1061 | 1 1062 | ], 1063 | "legIntPurposeIds": [ 1064 | 2, 1065 | 3, 1066 | 4, 1067 | 5 1068 | ], 1069 | "featureIds": [ 1070 | 1, 1071 | 2, 1072 | 3 1073 | ] 1074 | }, 1075 | { 1076 | "id": 70, 1077 | "name": "Yieldlab AG", 1078 | "policyUrl": "http://www.yieldlab.de/meta-navigation/datenschutz/", 1079 | "purposeIds": [], 1080 | "legIntPurposeIds": [ 1081 | 1, 1082 | 3 1083 | ], 1084 | "featureIds": [ 1085 | 3 1086 | ] 1087 | }, 1088 | { 1089 | "id": 50, 1090 | "name": "Adform A/S", 1091 | "policyUrl": "https://site.adform.com/privacy-policy-opt-out/", 1092 | "purposeIds": [ 1093 | 1 1094 | ], 1095 | "legIntPurposeIds": [ 1096 | 2, 1097 | 3, 1098 | 4, 1099 | 5 1100 | ], 1101 | "featureIds": [ 1102 | 1, 1103 | 2 1104 | ] 1105 | }, 1106 | { 1107 | "id": 48, 1108 | "name": "NetSuccess, s.r.o.", 1109 | "policyUrl": "https://www.inres.sk/pp/", 1110 | "purposeIds": [ 1111 | 1, 1112 | 2, 1113 | 3, 1114 | 4, 1115 | 5 1116 | ], 1117 | "legIntPurposeIds": [], 1118 | "featureIds": [ 1119 | 1, 1120 | 2, 1121 | 3 1122 | ] 1123 | }, 1124 | { 1125 | "id": 100, 1126 | "name": "Fifty Technology Limited", 1127 | "policyUrl": "https://fiftymedia.com/privacy-policy/", 1128 | "purposeIds": [ 1129 | 2, 1130 | 3, 1131 | 5 1132 | ], 1133 | "legIntPurposeIds": [ 1134 | 1 1135 | ], 1136 | "featureIds": [] 1137 | }, 1138 | { 1139 | "id": 21, 1140 | "name": "The Trade Desk, Inc and affiliated companies", 1141 | "policyUrl": "https://www.thetradedesk.com/general/privacy-policy", 1142 | "purposeIds": [ 1143 | 1, 1144 | 2 1145 | ], 1146 | "legIntPurposeIds": [ 1147 | 3 1148 | ], 1149 | "featureIds": [ 1150 | 1, 1151 | 2, 1152 | 3 1153 | ] 1154 | }, 1155 | { 1156 | "id": 110, 1157 | "name": "Hottraffic BV (DMA Institute)", 1158 | "policyUrl": "https://www.dma-institute.com/additional-information-for-data-subjects/", 1159 | "purposeIds": [], 1160 | "legIntPurposeIds": [ 1161 | 1, 1162 | 2, 1163 | 3, 1164 | 4, 1165 | 5 1166 | ], 1167 | "featureIds": [] 1168 | }, 1169 | { 1170 | "id": 42, 1171 | "name": "Taboola Europe Limited", 1172 | "policyUrl": "https://www.taboola.com/privacy-policy", 1173 | "purposeIds": [ 1174 | 1 1175 | ], 1176 | "legIntPurposeIds": [ 1177 | 2, 1178 | 3, 1179 | 4, 1180 | 5 1181 | ], 1182 | "featureIds": [ 1183 | 1, 1184 | 2 1185 | ] 1186 | }, 1187 | { 1188 | "id": 112, 1189 | "name": "Maytrics GmbH", 1190 | "policyUrl": "https://maytrics.com/node/2", 1191 | "purposeIds": [ 1192 | 1, 1193 | 2, 1194 | 3, 1195 | 4, 1196 | 5 1197 | ], 1198 | "legIntPurposeIds": [], 1199 | "featureIds": [] 1200 | }, 1201 | { 1202 | "id": 77, 1203 | "name": "comScore, Inc.", 1204 | "policyUrl": "https://www.comscore.com/About-comScore/Privacy-Policy", 1205 | "purposeIds": [ 1206 | 1, 1207 | 5 1208 | ], 1209 | "legIntPurposeIds": [], 1210 | "featureIds": [ 1211 | 2 1212 | ] 1213 | }, 1214 | { 1215 | "id": 109, 1216 | "name": "LoopMe Ltd", 1217 | "policyUrl": "https://loopme.com/privacy/", 1218 | "purposeIds": [ 1219 | 1, 1220 | 2, 1221 | 3, 1222 | 5 1223 | ], 1224 | "legIntPurposeIds": [], 1225 | "featureIds": [ 1226 | 1, 1227 | 2, 1228 | 3 1229 | ] 1230 | } 1231 | ] 1232 | } 1233 | --------------------------------------------------------------------------------