Home Reference Source

src/token.js

/**
 * A consent web token represents the GDPR consents expressed by a user.
 * It can be used for storage or to be shared with third-parties.
 *
 * @example
 * const token = new CWT({
 *   issuer: 'issuer',
 *   user_id: 'user@domain.com',
 *   user_id_type: 'email'
 * });
 */
class CWT {
  /**
   * Create a new Consent web token
   *
   * @param {Object} tokenContent The content of the token
   * @param {string} tokenContent.issuer A unique ID identifying the issuer of the token
   * @param {string} [tokenContent.user_id] The ID of the user that owns the token
   * @param {string} [tokenContent.user_id_type] The type of ID (email, uuid, adid, etc.)
   * @param {string} [tokenContent.user_id_hash_method] If the user ID is hashed, this is the method used for generating the hash (md5, sha1, sha256)
   * @param {Object[]} [tokenContent.consents] A list of consents already given by the user
   */
  constructor(tokenContent) {
    tokenContent = tokenContent || {};

    /**
     * A unique ID identifying the issuer of the token
     *
     * @type {string}
     */
    this.issuer = tokenContent.issuer;

    /**
     * The ID of the user that owns the token
     *
     * @type {string}
     */
    this.user_id = tokenContent.user_id;

    /**
     * The type of ID (email, uuid, adid, etc.)
     *
     * @type {string}
     */
    this.user_id_type = tokenContent.user_id_type;

    /**
     * If the user ID is hashed, this is the method used for generating the hash (md5, sha1, sha256)
     *
     * @type {string}
     */
    this.user_id_hash_method = tokenContent.user_id_hash_method;

    /**
     * A list of consents already given by the user
     *
     * @type {Object[]}
     */
    this.consents = tokenContent.consents || [];

    /**
     * The CWT specification version
     *
     * @type {number}
     */
    this.version = 1;
  }

  /**
   * Export the token information as a plain JavaScript object
   *
   * @return {Object}
   */
  toObject() {
    return {
      issuer: this.issuer,
      user_id: this.user_id,
      user_id_type: this.user_id_type,
      user_id_hash_method: this.user_id_hash_method,
      consents: this.consents,
      version: this.version,
    };
  }

  /**
   * Generate a JSON-encoded version of the token
   *
   * @return {string}
   */
  toJSON() {
    return JSON.stringify(this.toObject());
  }

  /**
   * Generate a base64-encoded version of the token
   * It first encode the token as JSON then base64-encode it.
   *
   * @return {string}
   */
  toBase64() {
    return new Buffer(this.toJSON()).toString('base64');
  }

  /**
   * Set the consent status for the user
   *
   * @param {string} status The consent status (yes/no) of the user for the vendor
   * @param {string} purpose The purpose for which the user has given consent
   * @param {string} vendorId The unique vendor ID for which the user has given consent. Use `*` to indicate that the user has given consent for all vendors)
   *
   * @example
   * const token = new CWT('issuer');
   * token.setConsentStatus(
   *   true,
   *   CWT.Purposes.Cookies,
   *   'didomi'
   * );
   */
  setConsentStatus(status, purpose, vendorId) {
    // Check if we already have consent information for that purpose
    let consent = this.consents.find(c => c.purpose === purpose);

    if (!consent) {
      consent = {
        purpose,
        vendors: [],
      };

      this.consents.push(consent);
    }

    // Check if we already have the vendor for that consent
    let vendor = consent.vendors.find(v => v.id === vendorId);

    if (!vendor) {
      vendor = {
        id: vendorId,
        status: undefined,
      };

      consent.vendors.push(vendor);
    }

    vendor.status = status;
  }

  /**
   * Get the consent status of the user for a specific purpose/vendor
   *
   * Returns true if consent has been given, false if consent has been denied and undefined if no consent information is available
   *
   * @param {string} purpose Purpose
   * @param {string} vendorId Unique ID of the vendor to check consent for
   * @return {boolean}
   *
   * @example
   * const token = new CWT('issuer');
   * token.setConsentStatus(CWT.Purposes.Cookies, 'didomi');
   * token.getConsentStatus(CWT.Purposes.Cookies, 'didomi');
   */
  getConsentStatus(purpose, vendorId) {
    const consent = this.consents.find(c => c.purpose === purpose);

    if (consent) {
      // Consent information found for that purpose

      // Check if we have consent information for the specific vendor
      const vendor = consent.vendors.find(v => v.id === vendorId);
      if (vendor) {
        return vendor.status;
      }

      // We do not have consent information for that vendor, check if we have some for '*' (all vendors)
      const vendorCatchAll = consent.vendors.find(v => v.id === '*');
      if (vendorCatchAll) {
        return vendorCatchAll.status;
      }
    }

    return undefined;
  }
}

/**
 * Parse a JSON string into a CWT object
 *
 * @param {string} jsonString
 * @return {CWT|null} Return a CWT object or null if the JSON does not represent a valid Consent Web Token
 */
function CWTFromJSON(jsonString) {
  if (!jsonString) {
    return null;
  }

  let object;

  try {
    object = JSON.parse(jsonString);
  } catch (error) {
    return null;
  }

  return new CWT(object);
}

/**
 * Parse a base64-encoded JSON string into a CWT object
 *
 * @param {string} base64String
 * @return {CWT|null} Return a CWT object or null if the string does not represent a valid Consent Web Token
 */
function CWTFromBase64(base64String) {
  if (!base64String) {
    return null;
  }

  return CWTFromJSON(new Buffer(base64String, 'base64').toString());
}

/**
 * List of standard GDPR/ePrivacy purposes
 *
 * This list is provided as part of the CWT specification but is not intended to be a complete/restrictive list. You are free to specify your own purposes. If you share tokens with third-parties though, they will need to know how to interpret your purpose IDs whereas you can expect them to know how to deal with the standard purposes provided here.
 */
const Purposes = {
  Cookies: 'cookies',
  CookiesAnalytics: 'cookies_analytics',
  CookiesMarketing: 'cookies_marketing',
  CookiesSocial: 'cookies_social',

  /**
   * Purposes from the IAB GDPR Transparency and consent framework
   * From http://advertisingconsent.eu/wp-content/uploads/2018/03/Transparency_Consent_Framework_FAQ_Formatted_v1_8-March-2018.pdf
   * Subject to change
   */

  // Advertising personalisation allow processing of a user’s data to provide and inform personalised advertising (including delivery, measurement, and reporting) based on a user’s preferences or interests known or inferred from data collected across multiple sites, apps, or devices; and/or accessing or storing information on devices for that purpose
  AdvertisingPersonalization: 'advertising_personalization',

  // Analytics allow processing of a user’s data to deliver content or advertisements and measure the delivery of such content or advertisements, extract insights and generate reports to understand service usage; and/or accessing or storing information on devices for that purpose
  Analytics: 'analytics',

  // Content personalisation allow processing of a user’s data to provide and inform personalised content (including delivery, measurement, and reporting) based on a user’s preferences or interests known or inferred from data collected across multiple sites, apps, or devices; and/or accessing or storing information on devices for that purpose.
  ContentPersonalization: 'content_personalization',

  // Accessing a device allow storing or accessing information on a user’s device
  DeviceAccess: 'device_access',

  // Matching data to offline sources combining data from offline sources that were initially collected in other contexts
  OfflineMatch: 'offline_match',

  // Linking devices allow processing of a user’s data to connect such user across multiple devices
  LinkDevices: 'link_devices',

  // Precise geographic location data allow processing of a user’s precise geographic location data in support of a purpose for which that certain third party has consent
  PreciseGeo: 'precise_geo',
};

module.exports = {
  CWT,
  CWTFromBase64,
  CWTFromJSON,
  Purposes,
};