• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

prebid / Prebid.js / 20526850826

26 Dec 2025 05:52PM UTC coverage: 96.214% (-0.002%) from 96.216%
20526850826

push

github

60baec
web-flow
Uid2 library: BugFix for Refresh so id always shows latest UID2/EUID token (#14290)

* added log errors to catch the errors

* fixed lint errors

* making stored optout consistent between uid2 and euid

* remove log error

* add empty catch statements

* made code more consistent

* background refresh needs to update submdoule idobj

* updated uid2 testing

* updated uid2 testing

---------

Co-authored-by: Patrick McCann <patmmccann@gmail.com>

41481 of 51000 branches covered (81.34%)

4 of 8 new or added lines in 1 file covered. (50.0%)

2 existing lines in 1 file now uncovered.

207629 of 215800 relevant lines covered (96.21%)

71.24 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.73
/libraries/uid2IdSystemShared/uid2IdSystem_shared.js
1
import { ajax } from '../../src/ajax.js'
2
import { cyrb53Hash, logError } from '../../src/utils.js';
3

4
export const Uid2CodeVersion = '1.1';
2✔
5

6
function isValidIdentity(identity) {
7
  return !!(typeof identity === 'object' && identity !== null && identity.advertising_token && identity.identity_expires && identity.refresh_from && identity.refresh_token && identity.refresh_expires);
33✔
8
}
9

10
// Helper function to prepend message
11
function prependMessage(message) {
12
  return `UID2 shared library - ${message}`;
1,235✔
13
}
14

15
// Wrapper function for logInfo
16
function logInfoWrapper(logInfo, ...args) {
2,408!
17
  logInfo(prependMessage(args[0]), ...args.slice(1));
1,204✔
18
}
19

20
// This is extracted from an in-progress API client. Once it's available via NPM, this class should be replaced with the NPM package.
21
export class Uid2ApiClient {
22
  constructor(opts, clientId, logInfo, logWarn) {
23
    this._baseUrl = opts.baseUrl;
58✔
24
    this._clientVersion = clientId;
58✔
25
    this._logInfo = (...args) => logInfoWrapper(logInfo, ...args);
258✔
26
    this._logWarn = logWarn;
58✔
27
  }
28

29
  createArrayBuffer(text) {
30
    const arrayBuffer = new Uint8Array(text.length);
36✔
31
    for (let i = 0; i < text.length; i++) {
36✔
32
      arrayBuffer[i] = text.charCodeAt(i);
4,968✔
33
    }
34
    return arrayBuffer;
36✔
35
  }
36
  hasStatusResponse(response) {
37
    return typeof (response) === 'object' && response && response.status;
18✔
38
  }
39
  isValidRefreshResponse(response) {
40
    return this.hasStatusResponse(response) && (
18✔
41
      response.status === 'optout' || response.status === 'expired_token' || (response.status === 'success' && response.body && isValidIdentity(response.body))
42
    );
43
  }
44
  ResponseToRefreshResult(response) {
45
    if (this.isValidRefreshResponse(response)) {
18!
46
      if (response.status === 'success') { return { status: response.status, identity: response.body }; }
18✔
47
      if (response.status === 'optout') { return { status: response.status, identity: 'optout' }; }
×
48
      return response;
×
49
    } else { return prependMessage(`Response didn't contain a valid status`); }
×
50
  }
51
  callRefreshApi(refreshDetails) {
52
    const url = this._baseUrl + '/v2/token/refresh';
58✔
53
    let resolvePromise;
54
    let rejectPromise;
55
    const promise = new Promise((resolve, reject) => {
58✔
56
      resolvePromise = resolve;
58✔
57
      rejectPromise = reject;
58✔
58
    });
59
    this._logInfo('Sending refresh request', refreshDetails);
58✔
60
    ajax(url, {
58✔
61
      success: (responseText) => {
62
        try {
18✔
63
          if (!refreshDetails.refresh_response_key) {
18!
64
            this._logInfo('No response decryption key available, assuming unencrypted JSON');
×
65
            const response = JSON.parse(responseText);
×
66
            const result = this.ResponseToRefreshResult(response);
×
67
            if (typeof result === 'string') { rejectPromise(prependMessage(result)); } else { resolvePromise(result); }
×
68
          } else {
69
            this._logInfo('Decrypting refresh API response');
18✔
70
            const encodeResp = this.createArrayBuffer(atob(responseText));
18✔
71
            window.crypto.subtle.importKey('raw', this.createArrayBuffer(atob(refreshDetails.refresh_response_key)), { name: 'AES-GCM' }, false, ['decrypt']).then((key) => {
18✔
72
              this._logInfo('Imported decryption key')
18✔
73
              // returns the symmetric key
74
              window.crypto.subtle.decrypt({
18✔
75
                name: 'AES-GCM',
76
                iv: encodeResp.slice(0, 12),
77
                tagLength: 128, // The tagLength you used to encrypt (if any)
78
              }, key, encodeResp.slice(12)).then((decrypted) => {
79
                const decryptedResponse = String.fromCharCode(...new Uint8Array(decrypted));
18✔
80
                this._logInfo('Decrypted to:', decryptedResponse);
18✔
81
                const response = JSON.parse(decryptedResponse);
18✔
82
                const result = this.ResponseToRefreshResult(response);
18✔
83
                if (typeof result === 'string') { rejectPromise(prependMessage(result)); } else { resolvePromise(result); }
18!
84
              }, (reason) => this._logWarn(prependMessage(`Call to UID2 API failed`), reason));
×
85
            }, (reason) => this._logWarn(prependMessage(`Call to UID2 API failed`), reason));
×
86
          }
87
        } catch (_err) {
88
          rejectPromise(prependMessage(responseText));
×
89
        }
90
      },
91
      error: (error, xhr) => {
92
        try {
17✔
93
          this._logInfo('Error status, assuming unencrypted JSON');
17✔
94
          const response = JSON.parse(xhr.responseText);
17✔
95
          const result = this.ResponseToRefreshResult(response);
×
96
          if (typeof result === 'string') { rejectPromise(prependMessage(result)); } else { resolvePromise(result); }
×
97
        } catch (_e) {
98
          rejectPromise(prependMessage(error));
17✔
99
        }
100
      }
101
    }, refreshDetails.refresh_token, { method: 'POST',
102
      customHeaders: {
103
        'X-UID2-Client-Version': this._clientVersion
104
      } });
105
    return promise;
58✔
106
  }
107
}
108
export class Uid2StorageManager {
109
  constructor(storage, preferLocalStorage, storageName, logInfo) {
110
    this._storage = storage;
134✔
111
    this._preferLocalStorage = preferLocalStorage;
134✔
112
    this._storageName = storageName;
134✔
113
    this._logInfo = (...args) => logInfoWrapper(logInfo, ...args);
134✔
114
  }
115
  readCookie(cookieName) {
116
    return this._storage.cookiesAreEnabled() ? this._storage.getCookie(cookieName) : null;
184!
117
  }
118
  readLocalStorage(key) {
119
    return this._storage.localStorageIsEnabled() ? this._storage.getDataFromLocalStorage(key) : null;
137!
120
  }
121
  readModuleCookie() {
122
    return this.parseIfContainsBraces(this.readCookie(this._storageName));
150✔
123
  }
124
  writeModuleCookie(value) {
125
    this._storage.setCookie(this._storageName, JSON.stringify(value), Date.now() + 60 * 60 * 24 * 1000);
44✔
126
  }
127
  readModuleStorage() {
128
    return this.parseIfContainsBraces(this.readLocalStorage(this._storageName));
137✔
129
  }
130
  writeModuleStorage(value) {
131
    this._storage.setDataInLocalStorage(this._storageName, JSON.stringify(value));
29✔
132
  }
133
  readProvidedCookie(cookieName) {
134
    return JSON.parse(this.readCookie(cookieName));
34✔
135
  }
136
  parseIfContainsBraces(value) {
137
    return (value?.includes('{')) ? JSON.parse(value) : value;
287✔
138
  }
139
  storeValue(value) {
140
    if (this._preferLocalStorage) {
71✔
141
      this.writeModuleStorage(value);
27✔
142
    } else {
143
      this.writeModuleCookie(value);
44✔
144
    }
145
  }
146

147
  getStoredValueWithFallback() {
148
    const preferredStorageLabel = this._preferLocalStorage ? 'local storage' : 'cookie';
152✔
149
    const preferredStorageGet = (this._preferLocalStorage ? this.readModuleStorage : this.readModuleCookie).bind(this);
152✔
150
    const preferredStorageSet = (this._preferLocalStorage ? this.writeModuleStorage : this.writeModuleCookie).bind(this);
152✔
151
    const fallbackStorageGet = (this._preferLocalStorage ? this.readModuleCookie : this.readModuleStorage).bind(this);
152✔
152

153
    const storedValue = preferredStorageGet();
152✔
154

155
    if (!storedValue) {
152✔
156
      const fallbackValue = fallbackStorageGet();
131✔
157
      if (fallbackValue) {
131✔
158
        this._logInfo(`${preferredStorageLabel} was empty, but found a fallback value.`)
6✔
159
        if (typeof fallbackValue === 'object') {
6✔
160
          this._logInfo(`Copying the fallback value to ${preferredStorageLabel}.`);
2✔
161
          preferredStorageSet(fallbackValue);
2✔
162
        }
163
        return fallbackValue;
6✔
164
      }
165
    } else if (typeof storedValue === 'string') {
21✔
166
      const fallbackValue = fallbackStorageGet();
4✔
167
      if (fallbackValue && typeof fallbackValue === 'object') {
4!
168
        this._logInfo(`${preferredStorageLabel} contained a basic token, but found a refreshable token fallback. Copying the fallback value to ${preferredStorageLabel}.`);
×
169
        preferredStorageSet(fallbackValue);
×
170
        return fallbackValue;
×
171
      }
172
    }
173
    return storedValue;
146✔
174
  }
175
}
176

177
function refreshTokenAndStore(baseUrl, token, clientId, storageManager, _logInfo, _logWarn) {
178
  _logInfo('UID2 base url provided: ', baseUrl);
58✔
179
  const client = new Uid2ApiClient({baseUrl}, clientId, _logInfo, _logWarn);
58✔
180
  return client.callRefreshApi(token).then((response) => {
58✔
181
    _logInfo('Refresh endpoint responded with:', response);
18✔
182
    const tokens = {
18✔
183
      originalToken: token,
184
      latestToken: response.identity,
185
    };
186
    const storedTokens = storageManager.getStoredValueWithFallback();
18✔
187
    if (storedTokens?.originalIdentity) tokens.originalIdentity = storedTokens.originalIdentity;
18✔
188
    storageManager.storeValue(tokens);
18✔
189
    return tokens;
18✔
190
  });
191
}
192

193
let clientSideTokenGenerator;
194
if (FEATURES.UID2_CSTG) {
2✔
195
  const SERVER_PUBLIC_KEY_PREFIX_LENGTH = 9;
2✔
196

197
  clientSideTokenGenerator = {
2✔
198
    isCSTGOptionsValid(maybeOpts, _logWarn) {
199
      if (typeof maybeOpts !== 'object' || maybeOpts === null) {
134!
200
        _logWarn('CSTG is not being used, but is included in the Prebid.js bundle. You can reduce the bundle size by passing "--disable UID2_CSTG" to the Prebid.js build.');
×
201
        return false;
×
202
      }
203

204
      const opts = maybeOpts;
134✔
205
      if (!opts.serverPublicKey && !opts.subscriptionId) {
134✔
206
        _logWarn('CSTG has been enabled but its parameters have not been set.');
81✔
207
        return false;
81✔
208
      }
209
      if (typeof opts.serverPublicKey !== 'string') {
53✔
210
        _logWarn('CSTG opts.serverPublicKey must be a string');
1✔
211
        return false;
1✔
212
      }
213
      const serverPublicKeyPrefix = /^(UID2|EUID)-X-[A-Z]-.+/;
52✔
214
      if (!serverPublicKeyPrefix.test(opts.serverPublicKey)) {
52✔
215
        _logWarn(
1✔
216
          `CSTG opts.serverPublicKey must match the regular expression ${serverPublicKeyPrefix}`
217
        );
218
        return false;
1✔
219
      }
220
      // We don't do any further validation of the public key, as we will find out
221
      // later if it's valid by using importKey.
222

223
      if (typeof opts.subscriptionId !== 'string') {
51✔
224
        _logWarn('CSTG opts.subscriptionId must be a string');
1✔
225
        return false;
1✔
226
      }
227
      if (opts.subscriptionId.length === 0) {
50✔
228
        _logWarn('CSTG opts.subscriptionId is empty');
1✔
229
        return false;
1✔
230
      }
231
      return true;
49✔
232
    },
233

234
    getValidIdentity(opts, _logWarn) {
235
      if (opts.emailHash) {
49✔
236
        if (!UID2DiiNormalization.isBase64Hash(opts.emailHash)) {
7✔
237
          _logWarn('CSTG opts.emailHash is invalid');
1✔
238
          return;
1✔
239
        }
240
        return { email_hash: opts.emailHash };
6✔
241
      }
242

243
      if (opts.phoneHash) {
42✔
244
        if (!UID2DiiNormalization.isBase64Hash(opts.phoneHash)) {
7✔
245
          _logWarn('CSTG opts.phoneHash is invalid');
1✔
246
          return;
1✔
247
        }
248
        return { phone_hash: opts.phoneHash };
6✔
249
      }
250

251
      if (opts.email) {
35✔
252
        const normalizedEmail = UID2DiiNormalization.normalizeEmail(opts.email);
28✔
253
        if (normalizedEmail === undefined) {
28✔
254
          _logWarn('CSTG opts.email is invalid');
1✔
255
          return;
1✔
256
        }
257
        return { email: normalizedEmail };
27✔
258
      }
259

260
      if (opts.phone) {
7✔
261
        if (!UID2DiiNormalization.isNormalizedPhone(opts.phone)) {
7✔
262
          _logWarn('CSTG opts.phone is invalid');
1✔
263
          return;
1✔
264
        }
265
        return { phone: opts.phone };
6✔
266
      }
267
    },
268

269
    isStoredTokenInvalid(cstgIdentity, storedTokens, _logInfo, _logWarn) {
270
      if (storedTokens) {
6✔
271
        if (storedTokens.latestToken === 'optout') {
6!
272
          return true;
×
273
        }
274
        const identity = Object.values(cstgIdentity)[0];
6✔
275
        if (!this.isStoredTokenFromSameIdentity(storedTokens, identity)) {
6✔
276
          _logInfo(
1✔
277
            'CSTG supplied new identity - ignoring stored value.',
278
            storedTokens.originalIdentity,
279
            cstgIdentity
280
          );
281
          // Stored token wasn't originally sourced from the provided identity - ignore the stored value. A new user has logged in?
282
          return true;
1✔
283
        }
284
      }
285
      return false;
5✔
286
    },
287

288
    async generateTokenAndStore(
289
      baseUrl,
290
      cstgOpts,
291
      cstgIdentity,
292
      storageManager,
293
      _logInfo,
294
      _logWarn
295
    ) {
296
      _logInfo('UID2 cstg opts provided: ', JSON.stringify(cstgOpts));
42✔
297
      const client = new UID2CstgApiClient(
42✔
298
        { baseUrl, cstg: cstgOpts },
299
        _logInfo,
300
        _logWarn
301
      );
302
      const response = await client.generateToken(cstgIdentity);
42✔
303
      _logInfo('CSTG endpoint responded with:', response);
17✔
304
      const tokens = {
17✔
305
        originalIdentity: this.encodeOriginalIdentity(cstgIdentity),
306
        latestToken: response.identity,
307
      };
308
      storageManager.storeValue(tokens);
17✔
309
      return tokens;
17✔
310
    },
311

312
    isStoredTokenFromSameIdentity(storedTokens, identity) {
313
      if (!storedTokens.originalIdentity) return false;
6!
314
      return (
6✔
315
        cyrb53Hash(identity, storedTokens.originalIdentity.salt) ===
316
        storedTokens.originalIdentity.identity
317
      );
318
    },
319

320
    encodeOriginalIdentity(identity) {
321
      const identityValue = Object.values(identity)[0];
17✔
322
      const salt = Math.floor(Math.random() * Math.pow(2, 32));
17✔
323
      return {
17✔
324
        identity: cyrb53Hash(identityValue, salt),
325
        salt,
326
      };
327
    },
328
  };
329

330
  class UID2DiiNormalization {
331
    static EMAIL_EXTENSION_SYMBOL = '+';
2✔
332
    static EMAIL_DOT = '.';
2✔
333
    static GMAIL_DOMAIN = 'gmail.com';
2✔
334

335
    static isBase64Hash(value) {
336
      if (!(value && value.length === 44)) {
14✔
337
        return false;
2✔
338
      }
339

340
      try {
12✔
341
        return btoa(atob(value)) === value;
12✔
342
      } catch (err) {
343
        return false;
×
344
      }
345
    }
346

347
    static isNormalizedPhone(phone) {
348
      return /^\+[0-9]{10,15}$/.test(phone);
7✔
349
    }
350

351
    static normalizeEmail(email) {
352
      if (!email || !email.length) return;
28!
353

354
      const parsedEmail = email.trim().toLowerCase();
28✔
355
      if (parsedEmail.indexOf(' ') > 0) return;
28✔
356

357
      const emailParts = this.splitEmailIntoAddressAndDomain(parsedEmail);
27✔
358
      if (!emailParts) return;
27!
359

360
      const { address, domain } = emailParts;
27✔
361

362
      const emailIsGmail = this.isGmail(domain);
27✔
363
      const parsedAddress = this.normalizeAddressPart(
27✔
364
        address,
365
        emailIsGmail,
366
        emailIsGmail
367
      );
368

369
      return parsedAddress ? `${parsedAddress}@${domain}` : undefined;
27!
370
    }
371

372
    static splitEmailIntoAddressAndDomain(email) {
373
      const parts = email.split('@');
27✔
374
      if (
27!
375
        parts.length !== 2 ||
54✔
376
        parts.some((part) => part === '')
54✔
377
      ) { return; }
×
378

379
      return {
27✔
380
        address: parts[0],
381
        domain: parts[1],
382
      };
383
    }
384

385
    static isGmail(domain) {
386
      return domain === this.GMAIL_DOMAIN;
27✔
387
    }
388

389
    static dropExtension(address, extensionSymbol = this.EMAIL_EXTENSION_SYMBOL) {
2!
390
      return address.split(extensionSymbol)[0];
2✔
391
    }
392

393
    static normalizeAddressPart(address, shouldRemoveDot, shouldDropExtension) {
394
      let parsedAddress = address;
27✔
395
      if (shouldRemoveDot) { parsedAddress = parsedAddress.replaceAll(this.EMAIL_DOT, ''); }
27✔
396
      if (shouldDropExtension) parsedAddress = this.dropExtension(parsedAddress);
27✔
397
      return parsedAddress;
27✔
398
    }
399
  }
400

401
  class UID2CstgApiClient {
402
    constructor(opts, logInfo, logWarn) {
403
      this._baseUrl = opts.baseUrl;
42✔
404
      this._serverPublicKey = opts.cstg.serverPublicKey;
42✔
405
      this._subscriptionId = opts.cstg.subscriptionId;
42✔
406
      this._logInfo = (...args) => logInfoWrapper(logInfo, ...args);
168✔
407
      this._logWarn = logWarn;
42✔
408
    }
409

410
    hasStatusResponse(response) {
411
      return typeof response === 'object' && response && response.status;
19✔
412
    }
413

414
    isCstgApiSuccessResponse(response) {
415
      return (
17✔
416
        this.hasStatusResponse(response) &&
49✔
417
        response.status === 'success' &&
418
        isValidIdentity(response.body)
419
      );
420
    }
421

422
    isCstgApiOptoutResponse(response) {
423
      return (
2✔
424
        this.hasStatusResponse(response) &&
4✔
425
        response.status === 'optout');
426
    }
427

428
    isCstgApiClientErrorResponse(response) {
429
      return (
×
430
        this.hasStatusResponse(response) &&
×
431
        response.status === 'client_error' &&
432
        typeof response.message === 'string'
433
      );
434
    }
435

436
    isCstgApiForbiddenResponse(response) {
437
      return (
×
438
        this.hasStatusResponse(response) &&
×
439
        response.status === 'invalid_http_origin' &&
440
        typeof response.message === 'string'
441
      );
442
    }
443

444
    stripPublicKeyPrefix(serverPublicKey) {
445
      return serverPublicKey.substring(SERVER_PUBLIC_KEY_PREFIX_LENGTH);
42✔
446
    }
447

448
    async generateCstgRequest(cstgIdentity) {
449
      if ('email_hash' in cstgIdentity || 'phone_hash' in cstgIdentity) {
42✔
450
        return cstgIdentity;
12✔
451
      }
452
      if ('email' in cstgIdentity) {
30✔
453
        const emailHash = await UID2CstgCrypto.hash(cstgIdentity.email);
24✔
454
        return { email_hash: emailHash };
24✔
455
      }
456
      if ('phone' in cstgIdentity) {
6✔
457
        const phoneHash = await UID2CstgCrypto.hash(cstgIdentity.phone);
6✔
458
        return { phone_hash: phoneHash };
6✔
459
      }
460
    }
461

462
    async generateToken(cstgIdentity) {
463
      const request = await this.generateCstgRequest(cstgIdentity);
42✔
464
      this._logInfo('Building CSTG request for', request);
42✔
465
      const box = await UID2CstgBox.build(
42✔
466
        this.stripPublicKeyPrefix(this._serverPublicKey)
467
      );
468
      const encoder = new TextEncoder();
42✔
469
      const now = Date.now();
42✔
470
      const { iv, ciphertext } = await box.encrypt(
42✔
471
        encoder.encode(JSON.stringify(request)),
472
        encoder.encode(JSON.stringify([now]))
473
      );
474

475
      const exportedPublicKey = await UID2CstgCrypto.exportPublicKey(
42✔
476
        box.clientPublicKey
477
      );
478
      const requestBody = {
42✔
479
        payload: UID2CstgCrypto.bytesToBase64(new Uint8Array(ciphertext)),
480
        iv: UID2CstgCrypto.bytesToBase64(new Uint8Array(iv)),
481
        public_key: UID2CstgCrypto.bytesToBase64(
482
          new Uint8Array(exportedPublicKey)
483
        ),
484
        timestamp: now,
485
        subscription_id: this._subscriptionId,
486
      };
487
      return this.callCstgApi(requestBody, box);
42✔
488
    }
489

490
    async callCstgApi(requestBody, box) {
491
      const url = this._baseUrl + '/v2/token/client-generate';
42✔
492
      let resolvePromise;
493
      let rejectPromise;
494
      const promise = new Promise((resolve, reject) => {
42✔
495
        resolvePromise = resolve;
42✔
496
        rejectPromise = reject;
42✔
497
      });
498

499
      this._logInfo('Sending CSTG request', requestBody);
42✔
500
      ajax(
42✔
501
        url,
502
        {
503
          success: async (responseText, xhr) => {
504
            try {
17✔
505
              const encodedResp = UID2CstgCrypto.base64ToBytes(responseText);
17✔
506
              const decrypted = await box.decrypt(
17✔
507
                encodedResp.slice(0, 12),
508
                encodedResp.slice(12)
509
              );
510
              const decryptedResponse = new TextDecoder().decode(decrypted);
17✔
511
              const response = JSON.parse(decryptedResponse);
17✔
512
              if (this.isCstgApiSuccessResponse(response)) {
17✔
513
                resolvePromise({
15✔
514
                  status: 'success',
515
                  identity: response.body,
516
                });
517
              } else if (this.isCstgApiOptoutResponse(response)) {
2!
518
                resolvePromise({
2✔
519
                  status: 'optout',
520
                  identity: 'optout',
521
                });
522
              } else {
523
                // A 200 should always be a success response.
524
                // Something has gone wrong.
525
                rejectPromise(
×
526
                  prependMessage(`API error: Response body was invalid for HTTP status 200: ${decryptedResponse}`)
527
                );
528
              }
529
            } catch (err) {
530
              rejectPromise(prependMessage(err));
×
531
            }
532
          },
533
          error: (error, xhr) => {
534
            try {
14✔
535
              if (xhr.status === 400) {
14!
536
                const response = JSON.parse(xhr.responseText);
×
537
                if (this.isCstgApiClientErrorResponse(response)) {
×
538
                  rejectPromise(prependMessage(`Client error: ${response.message}`));
×
539
                } else {
540
                  // A 400 should always be a client error.
541
                  // Something has gone wrong.
542
                  rejectPromise(
×
543
                    prependMessage(`UID2 API error: Response body was invalid for HTTP status 400: ${xhr.responseText}`)
544
                  );
545
                }
546
              } else if (xhr.status === 403) {
14!
547
                const response = JSON.parse(xhr.responseText);
×
548
                if (this.isCstgApiForbiddenResponse(xhr)) {
×
549
                  rejectPromise(prependMessage(`Forbidden: ${response.message}`));
×
550
                } else {
551
                  // A 403 should always be a forbidden response.
552
                  // Something has gone wrong.
553
                  rejectPromise(
×
554
                    prependMessage(`UID2 API error: Response body was invalid for HTTP status 403: ${xhr.responseText}`)
555
                  );
556
                }
557
              } else {
558
                rejectPromise(
14✔
559
                  prependMessage(`UID2 API error: Unexpected HTTP status ${xhr.status}: ${error}`)
560
                );
561
              }
562
            } catch (_e) {
563
              rejectPromise(prependMessage(error));
×
564
            }
565
          },
566
        },
567
        JSON.stringify(requestBody),
568
        { method: 'POST' }
569
      );
570
      return promise;
42✔
571
    }
572
  }
573

574
  class UID2CstgBox {
575
    static _namedCurve = 'P-256';
2✔
576
    constructor(clientPublicKey, sharedKey) {
577
      this._clientPublicKey = clientPublicKey;
42✔
578
      this._sharedKey = sharedKey;
42✔
579
    }
580

581
    static async build(serverPublicKey) {
582
      const clientKeyPair = await UID2CstgCrypto.generateKeyPair(
42✔
583
        UID2CstgBox._namedCurve
584
      );
585
      const importedServerPublicKey = await UID2CstgCrypto.importPublicKey(
42✔
586
        serverPublicKey,
587
        this._namedCurve
588
      );
589
      const sharedKey = await UID2CstgCrypto.deriveKey(
42✔
590
        importedServerPublicKey,
591
        clientKeyPair.privateKey
592
      );
593
      return new UID2CstgBox(clientKeyPair.publicKey, sharedKey);
42✔
594
    }
595

596
    async encrypt(plaintext, additionalData) {
597
      const iv = window.crypto.getRandomValues(new Uint8Array(12));
42✔
598
      const ciphertext = await window.crypto.subtle.encrypt(
42✔
599
        {
600
          name: 'AES-GCM',
601
          iv,
602
          additionalData,
603
        },
604
        this._sharedKey,
605
        plaintext
606
      );
607
      return { iv, ciphertext };
42✔
608
    }
609

610
    async decrypt(iv, ciphertext) {
611
      return window.crypto.subtle.decrypt(
17✔
612
        {
613
          name: 'AES-GCM',
614
          iv,
615
        },
616
        this._sharedKey,
617
        ciphertext
618
      );
619
    }
620

621
    get clientPublicKey() {
622
      return this._clientPublicKey;
42✔
623
    }
624
  }
625

626
  class UID2CstgCrypto {
627
    static base64ToBytes(base64) {
628
      const binString = atob(base64);
59✔
629
      return Uint8Array.from(binString, (m) => m.codePointAt(0));
7,256✔
630
    }
631

632
    static bytesToBase64(bytes) {
633
      const binString = Array.from(bytes, (x) => String.fromCodePoint(x)).join(
504✔
634
        ''
635
      );
636
      return btoa(binString);
156✔
637
    }
638

639
    static async generateKeyPair(namedCurve) {
640
      const params = {
42✔
641
        name: 'ECDH',
642
        namedCurve: namedCurve,
643
      };
644
      return window.crypto.subtle.generateKey(params, false, ['deriveKey']);
42✔
645
    }
646

647
    static async importPublicKey(publicKey, namedCurve) {
648
      const params = {
42✔
649
        name: 'ECDH',
650
        namedCurve: namedCurve,
651
      };
652
      return window.crypto.subtle.importKey(
42✔
653
        'spki',
654
        this.base64ToBytes(publicKey),
655
        params,
656
        false,
657
        []
658
      );
659
    }
660

661
    static exportPublicKey(publicKey) {
662
      return window.crypto.subtle.exportKey('spki', publicKey);
42✔
663
    }
664

665
    static async deriveKey(serverPublicKey, clientPrivateKey) {
666
      return window.crypto.subtle.deriveKey(
42✔
667
        {
668
          name: 'ECDH',
669
          public: serverPublicKey,
670
        },
671
        clientPrivateKey,
672
        {
673
          name: 'AES-GCM',
674
          length: 256,
675
        },
676
        false,
677
        ['encrypt', 'decrypt']
678
      );
679
    }
680

681
    static async hash(value) {
682
      const hash = await window.crypto.subtle.digest(
30✔
683
        'SHA-256',
684
        new TextEncoder().encode(value)
685
      );
686
      return this.bytesToBase64(new Uint8Array(hash));
30✔
687
    }
688
  }
689
}
690

691
export function Uid2GetId(config, prebidStorageManager, _logInfo, _logWarn) {
692
  // eslint-disable-next-line no-restricted-syntax
693
  const logInfo = (...args) => logInfoWrapper(_logInfo, ...args);
1,966✔
694

695
  let suppliedToken = null;
134✔
696
  const preferLocalStorage = (config.storage !== 'cookie');
134✔
697
  const storageManager = new Uid2StorageManager(prebidStorageManager, preferLocalStorage, config.internalStorage, logInfo);
134✔
698
  logInfo(`Module is using ${preferLocalStorage ? 'local storage' : 'cookies'} for internal storage.`);
134✔
699

700
  const isCstgEnabled =
701
  clientSideTokenGenerator &&
134✔
702
  clientSideTokenGenerator.isCSTGOptionsValid(config.cstg, _logWarn);
703
  if (isCstgEnabled) {
134✔
704
    logInfo(`Module is using client-side token generation.`);
49✔
705
    // Ignores config.paramToken and config.serverCookieName if any is provided
706
    suppliedToken = null;
49✔
707
  } else if (config.paramToken) {
85✔
708
    suppliedToken = config.paramToken;
36✔
709
    logInfo('Read token from params', suppliedToken);
36✔
710
  } else if (config.serverCookieName) {
49✔
711
    suppliedToken = storageManager.readProvidedCookie(config.serverCookieName);
34✔
712
    logInfo('Read token from server-supplied cookie', suppliedToken);
34✔
713
  }
714
  let storedTokens = storageManager.getStoredValueWithFallback();
134✔
715
  logInfo('Loaded module-stored tokens:', storedTokens);
134✔
716

717
  if (storedTokens && typeof storedTokens === 'string') {
134✔
718
    // Stored value is a plain token - if no token is supplied, just use the stored value.
719

720
    if (!suppliedToken && !isCstgEnabled) {
8✔
721
      logInfo('Returning legacy cookie value.');
5✔
722
      return { id: storedTokens };
5✔
723
    }
724
    // Otherwise, ignore the legacy value - it should get over-written later anyway.
725
    logInfo('Discarding superseded legacy cookie.');
3✔
726
    storedTokens = null;
3✔
727
  }
728

729
  if (suppliedToken && storedTokens) {
129✔
730
    if (storedTokens.originalToken?.advertising_token !== suppliedToken.advertising_token) {
4!
731
      logInfo('Server supplied new token - ignoring stored value.', storedTokens.originalToken?.advertising_token, suppliedToken.advertising_token);
×
732
      // Stored token wasn't originally sourced from the provided token - ignore the stored value. A new user has logged in?
733
      storedTokens = null;
×
734
    }
735
  }
736

737
  if (FEATURES.UID2_CSTG && isCstgEnabled) {
129✔
738
    const cstgIdentity = clientSideTokenGenerator.getValidIdentity(config.cstg, _logWarn);
49✔
739
    if (cstgIdentity) {
49✔
740
      if (storedTokens && clientSideTokenGenerator.isStoredTokenInvalid(cstgIdentity, storedTokens, logInfo, _logWarn)) {
45✔
741
        storedTokens = null;
1✔
742
      }
743

744
      if (!storedTokens || Date.now() > storedTokens.latestToken.refresh_expires) {
45✔
745
        const promise = clientSideTokenGenerator.generateTokenAndStore(config.apiBaseUrl, config.cstg, cstgIdentity, storageManager, logInfo, _logWarn);
42✔
746
        logInfo('Generate token using CSTG');
42✔
747
        return { callback: (cb) => {
42✔
748
          promise.then((result) => {
34✔
749
            logInfo('Token generation responded, passing the new token on.', result);
17✔
750
            cb(result);
17✔
751
          }).catch((e) => { logError('error generating token: ', e); });
14✔
752
        } };
753
      }
754
    }
755
  }
756

757
  const useSuppliedToken = !(storedTokens?.latestToken) || (suppliedToken && suppliedToken.identity_expires > storedTokens.latestToken.identity_expires);
87✔
758
  const newestAvailableToken = useSuppliedToken ? suppliedToken : storedTokens.latestToken;
87✔
759
  logInfo('UID2 module selected latest token', useSuppliedToken, newestAvailableToken);
87✔
760
  if ((!newestAvailableToken || Date.now() > newestAvailableToken.refresh_expires)) {
87✔
761
    logInfo('Newest available token is expired and not refreshable.');
13✔
762
    return { id: null };
13✔
763
  }
764
  if (Date.now() > newestAvailableToken.identity_expires) {
74✔
765
    const promise = refreshTokenAndStore(config.apiBaseUrl, newestAvailableToken, config.clientId, storageManager, logInfo, _logWarn);
38✔
766
    logInfo('Token is expired but can be refreshed, attempting refresh.');
38✔
767
    return { callback: (cb) => {
38✔
768
      promise.then((result) => {
38✔
769
        logInfo('Refresh reponded, passing the updated token on.', result);
14✔
770
        cb(result);
14✔
771
      }).catch((e) => { logError('error refreshing token: ', e); });
13✔
772
    } };
773
  }
774
  const tokens = {
36✔
775
    originalToken: suppliedToken ?? storedTokens?.originalToken,
39✔
776
    latestToken: newestAvailableToken,
777
  };
778
  if (FEATURES.UID2_CSTG && isCstgEnabled) {
36✔
779
    tokens.originalIdentity = storedTokens?.originalIdentity;
1✔
780
  }
781
  storageManager.storeValue(tokens);
36✔
782

783
  // If should refresh (but don't need to), refresh in the background.
784
  // Return both immediate id and callback so idObj gets updated when refresh completes.
785
  if (Date.now() > newestAvailableToken.refresh_from) {
36✔
786
    logInfo(`Refreshing token in background with low priority.`);
20✔
787
    const refreshPromise = refreshTokenAndStore(config.apiBaseUrl, newestAvailableToken, config.clientId, storageManager, logInfo, _logWarn);
20✔
788
    return {
20✔
789
      id: tokens,
790
      callback: (cb) => {
NEW
791
        refreshPromise.then((refreshedTokens) => {
×
NEW
792
          logInfo('Background token refresh completed, updating ID.', refreshedTokens);
×
NEW
793
          cb(refreshedTokens);
×
NEW
794
        }).catch((e) => { logError('error refreshing token in background: ', e); });
×
795
      }
796
    };
797
  }
798

799
  return { id: tokens };
16✔
800
}
801

802
export function extractIdentityFromParams(params) {
803
  const keysToCheck = ['emailHash', 'phoneHash', 'email', 'phone'];
134✔
804

805
  for (const key of keysToCheck) {
134✔
806
    if (params.hasOwnProperty(key)) {
467✔
807
      return { [key]: params[key] };
55✔
808
    }
809
  }
810

811
  return {};
79✔
812
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc