{"version":3,"file":"uploadConsents-DIetBOK3.mjs","names":[],"sources":["../src/lib/consent-manager/createConsentToken.ts","../src/lib/consent-manager/uploadConsents.ts"],"sourcesContent":["import * as crypto from 'crypto';\n\nimport jwt from 'jsonwebtoken';\n\n/**\n * Function to create a consent manager token\n *\n * @see https://docs.transcend.io/docs/consent/reference/managed-consent-database\n * @param userId - User ID\n * @param base64EncryptionKey - Encryption key\n * @param base64SigningKey - Signing key\n * @returns Token\n */\nexport function createConsentToken(\n  userId: string,\n  base64EncryptionKey: string,\n  base64SigningKey: string,\n): string {\n  // Read on for where to find these keys\n  const signingKey = Buffer.from(base64SigningKey, 'base64');\n  const encryptionKey = Buffer.from(base64EncryptionKey, 'base64');\n\n  // NIST's AES-KWP implementation { aes 48 } - see https://tools.ietf.org/html/rfc5649\n  const encryptionAlgorithm = 'id-aes256-wrap-pad';\n  // Initial Value for AES-KWP integrity check - see https://tools.ietf.org/html/rfc5649#section-3\n  const iv = Buffer.from('A65959A6', 'hex');\n  // Set up encryption algorithm\n  const cipher = crypto.createCipheriv(encryptionAlgorithm, encryptionKey, iv);\n\n  // Encrypt the userId and base64-encode the result\n  const encryptedIdentifier = Buffer.concat([cipher.update(userId), cipher.final()]).toString(\n    'base64',\n  );\n\n  // Create the JWT content - jwt.sign will add a 'iat' (issued at) field to the payload\n  // If you wanted to add something manually, consider\n  // const issued: Date = new Date();\n  // const isoDate = issued.toISOString();\n  const jwtPayload = {\n    encryptedIdentifier,\n  };\n\n  // Create a JSON web token and HMAC it with SHA-384\n  const consentToken = jwt.sign(jwtPayload, signingKey, {\n    algorithm: 'HS384',\n  });\n\n  return consentToken;\n}\n","import { ConsentPreferencesBody } from '@transcend-io/airgap.js-types';\nimport { createTranscendConsentGotInstance } from '@transcend-io/sdk';\nimport { decodeCodec } from '@transcend-io/type-utils';\nimport { map } from '@transcend-io/utils';\nimport cliProgress from 'cli-progress';\nimport colors from 'colors';\nimport * as t from 'io-ts';\n\nimport { DEFAULT_TRANSCEND_CONSENT_API } from '../../constants.js';\nimport { logger } from '../../logger.js';\nimport { createConsentToken } from './createConsentToken.js';\nimport type { ConsentPreferenceUpload } from './types.js';\n\nexport const USP_STRING_REGEX = /^[0-9][Y|N]([Y|N])[Y|N]$/;\n\nexport const PurposeMap = t.record(t.string, t.union([t.boolean, t.literal('Auto')]));\n\n/**\n * Upload a set of consent preferences\n *\n * @param options - Options\n */\nexport async function uploadConsents({\n  base64EncryptionKey,\n  base64SigningKey,\n  preferences,\n  partition,\n  concurrency = 100,\n  transcendUrl = DEFAULT_TRANSCEND_CONSENT_API,\n}: {\n  /** base64 encryption key */\n  base64EncryptionKey: string;\n  /** base64 signing key */\n  base64SigningKey: string;\n  /** Partition key */\n  partition: string;\n  /** Sombra API key authentication */\n  preferences: ConsentPreferenceUpload[];\n  /** API URL for Transcend backend */\n  transcendUrl?: string;\n  /** Concurrency limit for approving */\n  concurrency?: number;\n}): Promise<void> {\n  // Create connection to API\n  const transcendConsentApi = createTranscendConsentGotInstance(transcendUrl);\n\n  // Ensure usp strings are valid\n  const invalidUspStrings = preferences.filter(\n    (pref) => pref.usp && !USP_STRING_REGEX.test(pref.usp),\n  );\n  if (invalidUspStrings.length > 0) {\n    throw new Error(`Received invalid usp strings: ${JSON.stringify(invalidUspStrings, null, 2)}`);\n  }\n\n  // Ensure purpose maps are valid\n  const invalidPurposeMaps = preferences\n    .map((pref, ind) => [pref, ind] as [ConsentPreferenceUpload, number])\n    .filter(([pref]) => {\n      if (!pref.purposes) {\n        return false;\n      }\n      try {\n        decodeCodec(PurposeMap, pref.purposes);\n        return false;\n      } catch {\n        return true;\n      }\n    });\n  if (invalidPurposeMaps.length > 0) {\n    throw new Error(\n      `Received invalid purpose maps: ${JSON.stringify(invalidPurposeMaps, null, 2)}`,\n    );\n  }\n\n  // Ensure usp or preferences are provided\n  const invalidInputs = preferences.filter((pref) => !pref.usp && !pref.purposes);\n  if (invalidInputs.length > 0) {\n    throw new Error(\n      `Received invalid inputs, expected either purposes or usp to be defined: ${JSON.stringify(\n        invalidInputs,\n        null,\n        2,\n      )}`,\n    );\n  }\n\n  logger.info(\n    colors.magenta(`Uploading ${preferences.length} user preferences to partition ${partition}`),\n  );\n\n  // Time duration\n  const t0 = new Date().getTime();\n  // create a new progress bar instance and use shades_classic theme\n  const progressBar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);\n\n  // Build a GraphQL client\n  let total = 0;\n  progressBar.start(preferences.length, 0);\n  await map(\n    preferences,\n    async ({ userId, confirmed = 'true', updated, prompted, purposes, ...consent }) => {\n      const token = createConsentToken(userId, base64EncryptionKey, base64SigningKey);\n\n      // parse usp string\n      const [, saleStatus] = consent.usp ? USP_STRING_REGEX.exec(consent.usp) || [] : [];\n\n      const input = {\n        token,\n        partition,\n        consent: {\n          confirmed: confirmed === 'true',\n          purposes: purposes\n            ? decodeCodec(PurposeMap, purposes)\n            : consent.usp\n              ? { SaleOfInfo: saleStatus === 'Y' }\n              : {},\n          ...(updated ? { updated: updated === 'true' } : {}),\n          ...(prompted ? { prompted: prompted === 'true' } : {}),\n          ...consent,\n        },\n      } as ConsentPreferencesBody;\n\n      // Make the request\n      try {\n        await transcendConsentApi\n          .post('sync', {\n            json: input,\n          })\n          .json();\n      } catch (err) {\n        try {\n          const parsed = JSON.parse(err?.response?.body || '{}');\n          if (parsed.error) {\n            logger.error(colors.red(`Error: ${parsed.error}`));\n          }\n        } catch {\n          // continue\n        }\n        throw new Error(`Received an error from server: ${err?.response?.body || err?.message}`);\n      }\n\n      total += 1;\n      progressBar.update(total);\n    },\n    { concurrency },\n  );\n\n  progressBar.stop();\n  const t1 = new Date().getTime();\n  const totalTime = t1 - t0;\n\n  logger.info(\n    colors.green(\n      `Successfully uploaded ${preferences.length} user preferences to partition ${partition} in \"${\n        totalTime / 1000\n      }\" seconds!`,\n    ),\n  );\n}\n"],"mappings":"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"}