From 094248f428f94105e0aad04a401fe412a62538c7 Mon Sep 17 00:00:00 2001 From: Michael Farrell Date: Thu, 21 Mar 2024 17:50:18 -0700 Subject: [PATCH] Fixes cli command for consent upload and adds example (#316) * Fixes filter by * Consent push fix --- README.md | 5 +- examples/preference-upload.csv | 2 + package.json | 2 +- .../uploadConsentPreferences.ts | 100 +++++++++++++++--- 4 files changed, 91 insertions(+), 18 deletions(-) create mode 100644 examples/preference-upload.csv diff --git a/README.md b/README.md index f796feee..005ac22a 100644 --- a/README.md +++ b/README.md @@ -2079,9 +2079,12 @@ Each row in the CSV must include: | timestamp | Timestamp for when consent was collected for that user | string - timestamp | N/A | true | | purposes | JSON map from consent purpose name -> boolean indicating whether user has opted in or out of that purpose | {[k in string]: boolean } | {} | false | | confirmed | Whether consent preferences have been explicitly confirmed or inferred | boolean | true | false | -| updated | Time consent preferences were last updated | string - timestamp | N/A | false | +| updated | Has the consent been updated (including no-change confirmation) since default resolution | boolean | N/A | false | +| prompted | Whether or not the UI has been shown to the end-user (undefined in older versions of airgap.js) | boolean | N/A | false | | usp | US Privacy string | string - USP | N/A | false | +An sample CSV can be found [here](./examples/preference-upload.csv). + #### Authentication In order to use this cli, you will first need to follow [this guide](https://docs.transcend.io/docs/consent/reference/managed-consent-database#authenticate-a-user's-consent) in order diff --git a/examples/preference-upload.csv b/examples/preference-upload.csv new file mode 100644 index 00000000..642af59d --- /dev/null +++ b/examples/preference-upload.csv @@ -0,0 +1,2 @@ +userId,timestamp,purposes,confirmed,updated,usp +frederick@transcend.io,2024-03-11T19:32:31.707Z,"{""Analytics"":true,""SaleOfInfo"":true,""Advertising"":true,""Essential"":true,""Functional"":true,""TestConsent"":true,""VisibleNotConfigurable"":true}",true,true,"1YYY" diff --git a/package.json b/package.json index bf9ee218..976f8147 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Transcend Inc.", "name": "@transcend-io/cli", "description": "Small package containing useful typescript utilities.", - "version": "4.132.2", + "version": "4.132.3", "homepage": "https://github.com/transcend-io/cli", "repository": { "type": "git", diff --git a/src/consent-manager/uploadConsentPreferences.ts b/src/consent-manager/uploadConsentPreferences.ts index f5f11a02..9353de75 100644 --- a/src/consent-manager/uploadConsentPreferences.ts +++ b/src/consent-manager/uploadConsentPreferences.ts @@ -6,9 +6,15 @@ import { map } from 'bluebird'; import { createConsentToken } from './createConsentToken'; import { logger } from '../logger'; import cliProgress from 'cli-progress'; +import { decodeCodec } from '@transcend-io/type-utils'; export const USP_STRING_REGEX = /^[0-9][Y|N]([Y|N])[Y|N]$/; +export const PurposeMap = t.record( + t.string, + t.union([t.boolean, t.literal('Auto')]), +); + export const ManagedConsentDatabaseConsentPreference = t.intersection([ t.type({ /** User ID */ @@ -17,14 +23,21 @@ export const ManagedConsentDatabaseConsentPreference = t.intersection([ timestamp: t.string, }), t.partial({ - /** Purpose map */ - purposes: t.record(t.string, t.union([t.boolean, t.literal('Auto')])), + /** + * Purpose map + * This is a JSON object with keys as purpose names and values as booleans or 'Auto' + */ + purposes: t.string, /** Was tracking consent confirmed by the user? If this is false, the consent was resolved from defaults & is not yet confirmed */ - confirmed: t.boolean, - /** Time updated */ - updated: t.boolean, - /** Whether or not the UI has been shown to the end-user (undefined in older versions of airgap.js) */ - prompted: t.boolean, + confirmed: t.union([t.literal('true'), t.literal('false')]), + /** + * Has the consent been updated (including no-change confirmation) since default resolution + */ + updated: t.union([t.literal('true'), t.literal('false')]), + /** + * Whether or not the UI has been shown to the end-user (undefined in older versions of airgap.js) + */ + prompted: t.union([t.literal('true'), t.literal('false')]), /** US Privacy (USP) String */ usp: t.string, }), @@ -78,6 +91,33 @@ export async function uploadConsentPreferences({ ); } + // Ensure purpose maps are valid + const invalidPurposeMaps = preferences + .map( + (pref, ind) => + [pref, ind] as [ManagedConsentDatabaseConsentPreference, number], + ) + .filter(([pref]) => { + if (!pref.purposes) { + return false; + } + try { + decodeCodec(PurposeMap, pref.purposes); + return false; + } catch { + return true; + } + }); + if (invalidPurposeMaps.length > 0) { + throw new Error( + `Received invalid purpose maps: ${JSON.stringify( + invalidPurposeMaps, + null, + 2, + )}`, + ); + } + // Ensure usp or preferences are provided const invalidInputs = preferences.filter( (pref) => !pref.usp && !pref.purposes, @@ -111,7 +151,14 @@ export async function uploadConsentPreferences({ progressBar.start(preferences.length, 0); await map( preferences, - async ({ userId, confirmed = true, purposes, ...consent }) => { + async ({ + userId, + confirmed = 'true', + updated, + prompted, + purposes, + ...consent + }) => { const token = createConsentToken( userId, base64EncryptionKey, @@ -127,19 +174,40 @@ export async function uploadConsentPreferences({ token, partition, consent: { - confirmed, - purposes: - purposes || (consent.usp ? { SaleOfInfo: saleStatus === 'Y' } : {}), + confirmed: confirmed === 'true', + purposes: purposes + ? decodeCodec(PurposeMap, purposes) + : consent.usp + ? { SaleOfInfo: saleStatus === 'Y' } + : {}, + ...(updated ? { updated: updated === 'true' } : {}), + ...(prompted ? { prompted: prompted === 'true' } : {}), ...consent, }, }; // Make the request - await transcendConsentApi - .post('sync', { - json: input, - }) - .json(); + try { + await transcendConsentApi + .post('sync', { + json: input, + }) + .json(); + } catch (err) { + try { + const parsed = JSON.parse(err?.response?.body || '{}'); + if (parsed.error) { + logger.error(colors.red(`Error: ${parsed.error}`)); + } + } catch (e) { + // continue + } + throw new Error( + `Received an error from server: ${ + err?.response?.body || err?.message + }`, + ); + } total += 1; progressBar.update(total);