@mparticle/web-sdk 2.50.1 → 2.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -252,15 +252,51 @@ export default class RoktManager {
252
252
  }
253
253
  }
254
254
 
255
- public hashAttributes(attributes: IRoktPartnerAttributes): Promise<IRoktPartnerAttributes> {
256
- if (!this.isReady()) {
257
- return this.deferredCall<IRoktPartnerAttributes>('hashAttributes', attributes);
258
- }
259
-
255
+ /**
256
+ * Hashes attributes and returns both original and hashed versions
257
+ * with Rokt-compatible key names (like emailsha256, mobilesha256)
258
+ *
259
+ *
260
+ * @param {IRoktPartnerAttributes} attributes - Attributes to hash
261
+ * @returns {Promise<IRoktPartnerAttributes>} Object with both original and hashed attributes
262
+ *
263
+ */
264
+ public async hashAttributes(attributes: IRoktPartnerAttributes): Promise<IRoktPartnerAttributes> {
260
265
  try {
261
- return this.kit.hashAttributes(attributes);
266
+ if (!attributes || typeof attributes !== 'object') {
267
+ return {};
268
+ }
269
+
270
+ // Get own property keys only
271
+ const keys = Object.keys(attributes);
272
+
273
+ if (keys.length === 0) {
274
+ return {};
275
+ }
276
+
277
+ // Hash all attributes in parallel
278
+ const hashPromises = keys.map(async (key) => {
279
+ const attributeValue = attributes[key];
280
+ const hashedValue = await this.hashSha256(attributeValue);
281
+ return { key, attributeValue, hashedValue };
282
+ });
283
+
284
+ const results = await Promise.all(hashPromises);
285
+
286
+ // Build the result object
287
+ const hashedAttributes: IRoktPartnerAttributes = {};
288
+ for (const { key, attributeValue, hashedValue } of results) {
289
+ hashedAttributes[key] = attributeValue;
290
+ if (hashedValue) {
291
+ hashedAttributes[`${key}sha256`] = hashedValue;
292
+ }
293
+ }
294
+ return hashedAttributes;
295
+
262
296
  } catch (error) {
263
- return Promise.reject(error instanceof Error ? error : new Error('Unknown error occurred'));
297
+ const errorMessage = error instanceof Error ? error.message : String(error);
298
+ this.logger.error(`Failed to hashAttributes, returning an empty object: ${errorMessage}`);
299
+ return {};
264
300
  }
265
301
  }
266
302
 
@@ -290,6 +326,29 @@ export default class RoktManager {
290
326
  }
291
327
  }
292
328
 
329
+ /**
330
+ * Hashes an attribute using SHA-256
331
+ *
332
+ * @param {string | number | boolean | undefined | null} attribute - The value to hash
333
+ * @returns {Promise<string | undefined | null>} SHA-256 hashed value or undefined/null
334
+ *
335
+ */
336
+ public async hashSha256(attribute: string | number | boolean | undefined | null): Promise<string | undefined | null> {
337
+ if (attribute === null || attribute === undefined) {
338
+ this.logger.warning(`hashSha256 received null/undefined as input`);
339
+ return attribute as null | undefined;
340
+ }
341
+
342
+ try {
343
+ const normalizedValue = String(attribute).trim().toLocaleLowerCase();
344
+ return await this.sha256Hex(normalizedValue);
345
+ } catch (error) {
346
+ const errorMessage = error instanceof Error ? error.message : String(error);
347
+ this.logger.error(`Failed to hashSha256, returning undefined: ${errorMessage}`);
348
+ return undefined;
349
+ }
350
+ }
351
+
293
352
  public getLocalSessionAttributes(): LocalSessionAttributes {
294
353
  return this.store.getLocalSessionAttributes();
295
354
  }
@@ -400,6 +459,37 @@ export default class RoktManager {
400
459
  this.messageQueue.delete(messageId);
401
460
  }
402
461
 
462
+ /**
463
+ * Hashes a string input using SHA-256 and returns the hex digest
464
+ * Uses the Web Crypto API for secure hashing
465
+ *
466
+ * @param {string} input - The string to hash
467
+ * @returns {Promise<string>} The SHA-256 hash as a hexadecimal string
468
+ */
469
+ private async sha256Hex(input: string): Promise<string> {
470
+ const encoder = new TextEncoder();
471
+ const encodedInput = encoder.encode(input);
472
+ const digest = await crypto.subtle.digest('SHA-256', encodedInput);
473
+ return this.arrayBufferToHex(digest);
474
+ }
475
+
476
+ /**
477
+ * Converts an ArrayBuffer to a hexadecimal string representation
478
+ * Each byte is converted to a 2-character hex string with leading zeros
479
+ *
480
+ * @param {ArrayBuffer} buffer - The buffer to convert
481
+ * @returns {string} The hexadecimal string representation
482
+ */
483
+ private arrayBufferToHex(buffer: ArrayBuffer): string {
484
+ const bytes = new Uint8Array(buffer);
485
+ let hexString = '';
486
+ for (let i = 0; i < bytes.length; i++) {
487
+ const hexByte = bytes[i].toString(16).padStart(2, '0');
488
+ hexString += hexByte;
489
+ }
490
+ return hexString;
491
+ }
492
+
403
493
  /**
404
494
  * Checks if an identity value has changed by comparing current and new values
405
495
  *
@@ -33,31 +33,24 @@ export default function SessionManager(
33
33
 
34
34
  this.initialize = function (): void {
35
35
  if (mpInstance._Store.sessionId) {
36
- const sessionTimeoutInMilliseconds: number =
37
- mpInstance._Store.SDKConfig.sessionTimeout * 60000;
38
-
39
- if (
40
- new Date() >
41
- new Date(
42
- mpInstance._Store.dateLastEventSent.getTime() +
43
- sessionTimeoutInMilliseconds
44
- )
45
- ) {
36
+ const { dateLastEventSent, SDKConfig } = mpInstance._Store;
37
+ const { sessionTimeout } = SDKConfig;
38
+
39
+ if (hasSessionTimedOut(dateLastEventSent?.getTime(), sessionTimeout)) {
46
40
  self.endSession();
47
41
  self.startNewSession();
48
42
  } else {
49
43
  // https://go.mparticle.com/work/SQDSDKS-6045
50
44
  // https://go.mparticle.com/work/SQDSDKS-6323
51
45
  const currentUser = mpInstance.Identity.getCurrentUser();
52
- const sdkIdentityRequest =
53
- mpInstance._Store.SDKConfig.identifyRequest;
46
+ const sdkIdentityRequest = SDKConfig.identifyRequest;
54
47
 
55
48
  if (
56
49
  hasIdentityRequestChanged(currentUser, sdkIdentityRequest)
57
50
  ) {
58
51
  mpInstance.Identity.identify(
59
- mpInstance._Store.SDKConfig.identifyRequest,
60
- mpInstance._Store.SDKConfig.identityCallback
52
+ sdkIdentityRequest,
53
+ SDKConfig.identityCallback
61
54
  );
62
55
  mpInstance._Store.identifyCalled = true;
63
56
  mpInstance._Store.SDKConfig.identityCallback = null;
@@ -134,12 +127,7 @@ export default function SessionManager(
134
127
  );
135
128
 
136
129
  if (override) {
137
- mpInstance._Events.logEvent({
138
- messageType: Types.MessageType.SessionEnd,
139
- });
140
-
141
- mpInstance._Store.nullifySession();
142
- mpInstance._timeOnSiteTimer?.resetTimer();
130
+ performSessionEnd();
143
131
  return;
144
132
  }
145
133
 
@@ -152,13 +140,9 @@ export default function SessionManager(
152
140
  Messages.InformationMessages.AbandonEndSession
153
141
  );
154
142
  mpInstance._timeOnSiteTimer?.resetTimer();
155
-
156
143
  return;
157
144
  }
158
145
 
159
- let sessionTimeoutInMilliseconds: number;
160
- let timeSinceLastEventSent: number;
161
-
162
146
  const cookies: IPersistenceMinified =
163
147
  mpInstance._Persistence.getPersistence();
164
148
 
@@ -167,7 +151,6 @@ export default function SessionManager(
167
151
  Messages.InformationMessages.NoSessionToEnd
168
152
  );
169
153
  mpInstance._timeOnSiteTimer?.resetTimer();
170
-
171
154
  return;
172
155
  }
173
156
 
@@ -177,24 +160,15 @@ export default function SessionManager(
177
160
  }
178
161
 
179
162
  if (cookies?.gs?.les) {
180
- sessionTimeoutInMilliseconds =
181
- mpInstance._Store.SDKConfig.sessionTimeout * 60000;
182
- const newDate: number = new Date().getTime();
183
- timeSinceLastEventSent = newDate - cookies.gs.les;
184
-
185
- if (timeSinceLastEventSent < sessionTimeoutInMilliseconds) {
186
- self.setSessionTimer();
163
+ const sessionTimeout = mpInstance._Store.SDKConfig.sessionTimeout;
164
+
165
+ if (hasSessionTimedOut(cookies.gs.les, sessionTimeout)) {
166
+ performSessionEnd();
187
167
  } else {
188
- mpInstance._Events.logEvent({
189
- messageType: Types.MessageType.SessionEnd,
190
- });
191
-
192
- mpInstance._Store.sessionStartDate = null;
193
- mpInstance._Store.nullifySession();
168
+ self.setSessionTimer();
169
+ mpInstance._timeOnSiteTimer?.resetTimer();
194
170
  }
195
171
  }
196
-
197
- mpInstance._timeOnSiteTimer?.resetTimer();
198
172
  };
199
173
 
200
174
  this.setSessionTimer = function (): void {
@@ -234,4 +208,35 @@ export default function SessionManager(
234
208
  }
235
209
  }
236
210
  };
211
+
212
+ /**
213
+ * Checks if the session has expired based on the last event timestamp
214
+ * @param lastEventTimestamp - Unix timestamp in milliseconds of the last event
215
+ * @param sessionTimeout - Session timeout in minutes
216
+ * @returns true if the session has expired, false otherwise
217
+ */
218
+ function hasSessionTimedOut(lastEventTimestamp: number, sessionTimeout: number): boolean {
219
+ if (!lastEventTimestamp || !sessionTimeout || sessionTimeout <= 0) {
220
+ return false;
221
+ }
222
+
223
+ const sessionTimeoutInMilliseconds: number = sessionTimeout * 60000;
224
+ const timeSinceLastEvent: number = Date.now() - lastEventTimestamp;
225
+
226
+ return timeSinceLastEvent >= sessionTimeoutInMilliseconds;
227
+ }
228
+
229
+ /**
230
+ * Performs session end operations:
231
+ * - Logs a SessionEnd event
232
+ * - Nullifies the session ID and related data
233
+ * - Resets the time-on-site timer
234
+ */
235
+ function performSessionEnd(): void {
236
+ mpInstance._Events.logEvent({
237
+ messageType: Types.MessageType.SessionEnd,
238
+ });
239
+ mpInstance._Store.nullifySession();
240
+ mpInstance._timeOnSiteTimer?.resetTimer();
241
+ }
237
242
  }
package/src/store.ts CHANGED
@@ -683,6 +683,7 @@ export default function Store(
683
683
  this.nullifySession = (): void => {
684
684
  this.sessionId = null;
685
685
  this.dateLastEventSent = null;
686
+ this.sessionStartDate = null;
686
687
  this.sessionAttributes = {};
687
688
  this.localSessionAttributes = {};
688
689
  mpInstance._Persistence.update();
@@ -166,6 +166,7 @@ function createAliasRequest() {
166
166
  destinationMpid: 'b',
167
167
  startTime: new Date().getTime(),
168
168
  endTime: new Date().getTime(),
169
+ scope: 'device',
169
170
  };
170
171
  }
171
172
 
package/src/utils.ts CHANGED
@@ -206,18 +206,28 @@ const replaceAmpWithAmpersand = (value: string): string => value.replace(/&amp;/
206
206
  const createCookieSyncUrl = (
207
207
  mpid: MPID,
208
208
  pixelUrl: string,
209
- redirectUrl?: string
209
+ redirectUrl?: string,
210
+ domain?: string
210
211
  ): string => {
211
212
  const modifiedPixelUrl = replaceAmpWithAmpersand(pixelUrl);
212
213
  const modifiedDirectUrl = redirectUrl
213
214
  ? replaceAmpWithAmpersand(redirectUrl)
214
215
  : null;
215
216
 
216
- const url = replaceMPID(modifiedPixelUrl, mpid);
217
+ let url = replaceMPID(modifiedPixelUrl, mpid);
218
+
217
219
  const redirect = modifiedDirectUrl
218
220
  ? replaceMPID(modifiedDirectUrl, mpid)
219
221
  : '';
220
- return url + encodeURIComponent(redirect);
222
+
223
+ let fullUrl = url + encodeURIComponent(redirect);
224
+
225
+ if (domain) {
226
+ const separator = fullUrl.includes('?') ? '&' : '?';
227
+ fullUrl += `${separator}domain=${domain}`;
228
+ }
229
+
230
+ return fullUrl;
221
231
  };
222
232
 
223
233
  // FIXME: REFACTOR for V3