@purespectrum1/purescore-client 0.7.0 → 0.9.8

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.
Files changed (47) hide show
  1. package/README.md +135 -3
  2. package/lib/application/auto-init.d.ts +6 -3
  3. package/lib/application/auto-init.js +29 -20
  4. package/lib/application/device-identifier.d.ts +18 -1
  5. package/lib/application/device-identifier.js +43 -5
  6. package/lib/domain/errors/index.d.ts +1 -0
  7. package/lib/domain/errors/index.js +17 -0
  8. package/lib/domain/errors/transaction-rejected-error.d.ts +7 -0
  9. package/lib/domain/errors/transaction-rejected-error.js +14 -0
  10. package/lib/domain/models/crosscutting/device-intelligence.d.ts +3 -3
  11. package/lib/domain/models/crosscutting/device-intelligence.js +3 -3
  12. package/lib/domain/models/crosscutting/index.d.ts +1 -0
  13. package/lib/domain/models/crosscutting/index.js +1 -0
  14. package/lib/domain/models/crosscutting/verisoul.d.ts +19 -0
  15. package/lib/domain/models/crosscutting/verisoul.js +25 -0
  16. package/lib/domain/protocols/services/device-fingerprint/navigator.d.ts +1 -0
  17. package/lib/domain/protocols/services/device-identifier.d.ts +6 -6
  18. package/lib/domain/protocols/services/interaction-activity.d.ts +25 -0
  19. package/lib/domain/protocols/services/interaction-activity.js +2 -0
  20. package/lib/domain/protocols/services/single-script-injection/auto-init.d.ts +8 -8
  21. package/lib/domain/protocols/services/verisoul.d.ts +23 -0
  22. package/lib/domain/protocols/services/verisoul.js +2 -0
  23. package/lib/domain/services/device-fingerprint/index.d.ts +1 -0
  24. package/lib/domain/services/device-fingerprint/index.js +1 -0
  25. package/lib/domain/services/device-fingerprint/navigator.d.ts +2 -0
  26. package/lib/domain/services/device-fingerprint/navigator.js +7 -0
  27. package/lib/domain/services/device-fingerprint/webgl.d.ts +5 -0
  28. package/lib/domain/services/device-fingerprint/webgl.js +29 -0
  29. package/lib/domain/services/dom-mutation-tracker.d.ts +19 -0
  30. package/lib/domain/services/dom-mutation-tracker.js +47 -0
  31. package/lib/domain/services/interaction-activity.d.ts +43 -0
  32. package/lib/domain/services/interaction-activity.js +189 -0
  33. package/lib/domain/services/single-script-injection/checkpoint-scheduler.d.ts +1 -1
  34. package/lib/domain/services/single-script-injection/checkpoint-scheduler.js +3 -3
  35. package/lib/domain/services/single-script-injection/response-delivery.d.ts +3 -3
  36. package/lib/domain/services/single-script-injection/response-delivery.js +3 -3
  37. package/lib/domain/services/single-script-injection/schedule-store.d.ts +17 -7
  38. package/lib/domain/services/single-script-injection/schedule-store.js +11 -10
  39. package/lib/domain/services/snapshot-store.d.ts +1 -0
  40. package/lib/domain/services/snapshot-store.js +3 -0
  41. package/lib/domain/services/translation-detector.d.ts +20 -0
  42. package/lib/domain/services/translation-detector.js +119 -0
  43. package/lib/domain/services/verisoul.d.ts +27 -0
  44. package/lib/domain/services/verisoul.js +96 -0
  45. package/lib/index.d.ts +2 -1
  46. package/lib/index.js +2 -2
  47. package/package.json +1 -1
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
- # Purescore Identifier
1
+ # Purescore Client
2
2
 
3
3
  `@purespectrum1/purescore-client` generates a stable, unique device ID from
4
4
  device- and browser-level signals (fingerprinting). It ships as both a Node/TypeScript
5
5
  library and a standalone UMD browser bundle, so the same identification logic can run
6
6
  in a service or be dropped into a page with a `<script>` tag.
7
7
 
8
- > **Status:** project scaffolding. Build, test, lint and release pipelines are in place;
9
- > the `src/` implementation is still being added, so the public API below is not final.
8
+ For survey pages, the recommended integration is the self-initializing script tag
9
+ described in [Auto-init](#auto-init-single-script-tag).
10
10
 
11
11
  ## Requirements
12
12
 
@@ -41,6 +41,138 @@ Each release also uploads a version-pinned file (`bundle.<version>.js`) alongsid
41
41
  floating `bundle.js`. Pin the versioned file in production so a new release cannot
42
42
  change behaviour under you.
43
43
 
44
+ ## Auto-init (single script tag)
45
+
46
+ The browser bundle can set itself up from its own `<script>` tag, so the survey page
47
+ needs no bootstrap code and no `trackDetails` calls. Add `data-auto-init="true"` and
48
+ the attributes below, and the bundle will:
49
+
50
+ 1. create the transaction (`POST /v1/transaction`), retrying up to 3 times;
51
+ 2. send the device and behavioural signals (`PATCH /v1/respondent/:transactionId`) on
52
+ a schedule derived from the survey's expected length of interview (LOI);
53
+ 3. hand every response back to the page (see [Receiving the result](#receiving-the-result)).
54
+
55
+ Without `data-auto-init="true"` the bundle does nothing on load, so existing
56
+ integrations are unaffected.
57
+
58
+ ### Example
59
+
60
+ ```html
61
+ <script>
62
+ // Must be a global function, declared before the bundle tag (see data-callback).
63
+ function handlePureScore(result) {
64
+ if (result.continue === 0) {
65
+ // terminate the respondent
66
+ }
67
+ }
68
+ </script>
69
+ <script src="https://<cdn-host>/dist/bundle.<version>.js"
70
+ data-auto-init="true"
71
+ data-token="<device-intelligence-token>"
72
+ data-env="production"
73
+ data-callback="handlePureScore"
74
+
75
+ data-project-id="${project_id}"
76
+ data-expected-survey-loi="10"
77
+ data-survey-country="US"
78
+ data-survey-language="en"
79
+
80
+ data-buyer-id="456"
81
+ data-metadata='{"sup_resp_id":"24d373e4"}'
82
+ data-supplier-id="607"
83
+ data-supplier-member-id="m-24d373e4"
84
+ data-email-hash="<sha256-of-the-email>"
85
+ data-demographics-age="30"
86
+ data-demographics-gender="male"
87
+ data-demographics-zip="02134"></script>
88
+ ```
89
+
90
+ `${project_id}` stands for whatever placeholder syntax your survey platform uses. It
91
+ has to be replaced with the real value before the page is served (see
92
+ [Validation](#validation)).
93
+
94
+ ### Attributes
95
+
96
+ **Required.** If any of these is missing or invalid, auto-init logs an error to the
97
+ console and does not start.
98
+
99
+ | Attribute | Description |
100
+ | --- | --- |
101
+ | `data-auto-init` | Must be `"true"` to enable auto-init. |
102
+ | `data-token` | Purescore bearer token. |
103
+ | `data-project-id` | Project identifier. |
104
+ | `data-expected-survey-loi` | Expected length of interview, **in minutes**. A positive number (decimals allowed). |
105
+ | `data-survey-country` | ISO 3166-1 alpha-2 country code, e.g. `US`, `GB`. Converted to upper case. |
106
+ | `data-survey-language` | ISO 639-1 language code, e.g. `en`, `fr`. Converted to lower case. Use the language only: `en-US` is rejected. |
107
+
108
+ **Optional.** If one of these is set but invalid, it is logged and ignored, and
109
+ auto-init still runs.
110
+
111
+ | Attribute | Description |
112
+ | --- | --- |
113
+ | `data-env` | `production` or `staging`. Any other value, or none, means `staging`. **Set `production` on live surveys.** |
114
+ | `data-callback` | Name of a **global** function to call with each response. |
115
+ | `data-metadata` | A JSON **object** sent as `metadata` with every checkpoint. Use single quotes around the attribute so the JSON can keep its double quotes. |
116
+ | `data-buyer-id` | Added to the metadata as `buyer_id`. |
117
+ | `data-supplier-id` | Supplier identifier, sent when the transaction is created. |
118
+ | `data-supplier-member-id` | The supplier's respondent identifier, sent when the transaction is created. |
119
+ | `data-email-hash` | Hash of the respondent's email, sent when the transaction is created. Never send the plain email address. The value is not validated yet. |
120
+ | `data-demographics-age` | Respondent age. A positive number. |
121
+ | `data-demographics-gender` | Respondent gender. |
122
+ | `data-demographics-zip` | Respondent postal code. Keep it as a string so leading zeros are preserved. |
123
+
124
+ ### Validation
125
+
126
+ - **Uninterpolated placeholders are rejected.** If a value still contains `${`, `{{`,
127
+ `{%` or `<%`, the survey platform didn't replace it. A literal like
128
+ `${transaction_id}` would otherwise be shared by every respondent. For a required
129
+ attribute this stops auto-init; for an optional one, that attribute is dropped.
130
+ - **Errors go to the browser console** with the prefix `Purescore-Client auto-init:`.
131
+ Check the console first when nothing seems to happen.
132
+ - **Only the first tag on a page runs.** If the bundle is loaded a second time, it
133
+ does nothing.
134
+
135
+ ### Checkpoint schedule
136
+
137
+ The transaction makes at most 5 calls, timed from the respondent's first page load:
138
+
139
+ | Checkpoint | Due at | Example: LOI = 6 min |
140
+ | --- | --- | --- |
141
+ | 1 | 10 seconds | 0:10 |
142
+ | 2 | ⅓ × LOI | 2:00 |
143
+ | 3 | ⅔ × LOI | 4:00 |
144
+ | 4 | 1 × LOI | 6:00 |
145
+ | 5 | 2 × LOI | 12:00 |
146
+
147
+ ### Receiving the result
148
+
149
+ Each response is delivered through three channels at once. Use whichever suits your
150
+ platform:
151
+
152
+ ```js
153
+ // 1. The global function named in data-callback
154
+ function handlePureScore(result) { /* ... */ }
155
+
156
+ // 2. A DOM event on window (any number of listeners)
157
+ window.addEventListener('ps:purescore:result', function (event) {
158
+ var result = event.detail;
159
+ });
160
+
161
+ // 3. A global you can read at any time
162
+ window.psPureScore && window.psPureScore.lastResult;
163
+ ```
164
+
165
+ - **`data-callback` must name a function on `window`.** Declare it with a plain
166
+ `function` statement in a classic `<script>`, before the bundle tag. A function
167
+ declared inside an IIFE, an ES module or `jQuery(document).ready(...)` can't be
168
+ found, and the console will report `"<name>" is not a global function`.
169
+ - **Use the `continue` field to decide what to do.** `continue: 0` means the
170
+ respondent should be terminated.
171
+ - **On a later page, the latest response is delivered again.**
172
+ `window.psPureScore.lastResult` is set immediately. The event and the callback fire
173
+ once the document has finished parsing, so listeners declared further down the page
174
+ still receive it.
175
+
44
176
  ## Usage
45
177
 
46
178
  ```ts
@@ -16,11 +16,15 @@ export declare const TRANSACTION_RETRY_DELAY_MS = 1000;
16
16
  export declare function resolveScriptTag(): HTMLScriptElement | null;
17
17
  /**
18
18
  * Reads the bootstrap configuration off the tag's `data-*` attributes, or
19
- * returns null when a required one is missing or unusable.
19
+ * returns null when the token is missing.
20
+ *
21
+ * A missing or unusable survey attribute is reported but not fatal: the
22
+ * transaction is still created with what the tag holds, so the service's own
23
+ * validation error shows up in the network response next to the console one.
20
24
  */
21
25
  export declare function readAutoInitConfig(tag: HTMLScriptElement): AutoInitConfig | null;
22
26
  /**
23
- * Bootstraps the Device-Identifier straight from the script tag: no constructor
27
+ * Bootstraps the Purescore-Client straight from the script tag: no constructor
24
28
  * call and no `trackDetails` call on the buyer side.
25
29
  *
26
30
  * Strictly opt-in — it does nothing unless `data-auto-init="true"` is on the tag
@@ -38,7 +42,6 @@ export declare class AutoInitService {
38
42
  private resolveTransactionId;
39
43
  /**
40
44
  * Registers the transaction, trying up to `TRANSACTION_CREATION_ATTEMPTS` times.
41
- * Resolves with undefined when every attempt failed.
42
45
  */
43
46
  private createTransaction;
44
47
  }
@@ -15,6 +15,7 @@ exports.readAutoInitConfig = readAutoInitConfig;
15
15
  const validation_1 = require("../validation");
16
16
  const single_script_injection_1 = require("../domain/services/single-script-injection");
17
17
  const cache_1 = require("../infra/cache");
18
+ const errors_1 = require("../domain/errors");
18
19
  /**
19
20
  * Guard flag on `window`: a second tag on the same page is a no-op.
20
21
  */
@@ -39,15 +40,18 @@ function resolveScriptTag() {
39
40
  }
40
41
  /**
41
42
  * Reads the bootstrap configuration off the tag's `data-*` attributes, or
42
- * returns null when a required one is missing or unusable.
43
+ * returns null when the token is missing.
44
+ *
45
+ * A missing or unusable survey attribute is reported but not fatal: the
46
+ * transaction is still created with what the tag holds, so the service's own
47
+ * validation error shows up in the network response next to the console one.
43
48
  */
44
49
  function readAutoInitConfig(tag) {
45
50
  var _a, _b, _c, _d, _e, _f, _g;
46
51
  const data = tag.dataset;
52
+ const tokenError = validation_1.ValidationBuilder.of({ value: data.token, fieldName: 'data-token' }).required().validate();
47
53
  const errors = [
48
- validation_1.ValidationBuilder.of({ value: data.token, fieldName: 'data-token' })
49
- .required(),
50
- validation_1.ValidationBuilder.of({ value: data.surveyId, fieldName: 'data-survey-id' })
54
+ validation_1.ValidationBuilder.of({ value: data.projectId, fieldName: 'data-project-id' })
51
55
  .required()
52
56
  .interpolated(),
53
57
  validation_1.ValidationBuilder.of({ value: data.expectedSurveyLoi, fieldName: 'data-expected-survey-loi' })
@@ -62,17 +66,19 @@ function readAutoInitConfig(tag) {
62
66
  .required()
63
67
  .interpolated()
64
68
  .languageCode(),
65
- ].map(rules => rules.validate()).filter((error) => error !== undefined);
66
- if (errors.length) {
67
- console.error(`Device-Identifier auto-init: ${errors.map(e => e.message).join('; ')}`);
68
- return null;
69
+ ].map(rules => rules.validate());
70
+ const reported = [tokenError, ...errors].filter((error) => error !== undefined);
71
+ if (reported.length) {
72
+ console.error(`Purescore-Client auto-init: ${reported.map(e => e.message).join('; ')}`);
69
73
  }
74
+ if (tokenError)
75
+ return null;
70
76
  const config = {
71
77
  token: ((_a = data.token) !== null && _a !== void 0 ? _a : '').trim(),
72
78
  env: ((_b = data.env) === null || _b === void 0 ? void 0 : _b.trim().toLowerCase()) === 'production' ? 'production' : 'staging',
73
79
  callbackName: ((_c = data.callback) === null || _c === void 0 ? void 0 : _c.trim()) || undefined,
74
80
  metadata: parseMetadataAttribute(data.metadata),
75
- surveyId: ((_d = data.surveyId) !== null && _d !== void 0 ? _d : '').trim(),
81
+ projectId: ((_d = data.projectId) !== null && _d !== void 0 ? _d : '').trim(),
76
82
  expectedSurveyLOI: Number(data.expectedSurveyLoi),
77
83
  surveyCountry: ((_e = data.surveyCountry) !== null && _e !== void 0 ? _e : '').trim().toUpperCase(),
78
84
  surveyLanguage: ((_f = data.surveyLanguage) !== null && _f !== void 0 ? _f : '').trim().toLowerCase(),
@@ -103,7 +109,7 @@ function readAutoInitConfig(tag) {
103
109
  function optionalField(raw, fieldName, rules = builder => builder, convert = value => value) {
104
110
  const error = rules(validation_1.ValidationBuilder.of({ value: raw, fieldName }).optional().interpolated()).validate();
105
111
  if (error) {
106
- console.error(`Device-Identifier auto-init: ${error.message}, ignoring it`);
112
+ console.error(`Purescore-Client auto-init: ${error.message}, ignoring it`);
107
113
  return undefined;
108
114
  }
109
115
  const value = raw === null || raw === void 0 ? void 0 : raw.trim();
@@ -117,7 +123,7 @@ function optionalField(raw, fieldName, rules = builder => builder, convert = val
117
123
  function parseMetadataAttribute(raw) {
118
124
  const error = validation_1.ValidationBuilder.of({ value: raw, fieldName: 'data-metadata' }).jsonObject().validate();
119
125
  if (error) {
120
- console.error(`Device-Identifier auto-init: ${error.message}, ignoring it`);
126
+ console.error(`Purescore-Client auto-init: ${error.message}, ignoring it`);
121
127
  return {};
122
128
  }
123
129
  return (raw === null || raw === void 0 ? void 0 : raw.trim()) ? JSON.parse(raw) : {};
@@ -130,7 +136,7 @@ function assignDefined(target, values) {
130
136
  });
131
137
  }
132
138
  /**
133
- * Bootstraps the Device-Identifier straight from the script tag: no constructor
139
+ * Bootstraps the Purescore-Client straight from the script tag: no constructor
134
140
  * call and no `trackDetails` call on the buyer side.
135
141
  *
136
142
  * Strictly opt-in — it does nothing unless `data-auto-init="true"` is on the tag
@@ -166,7 +172,7 @@ class AutoInitService {
166
172
  var _a;
167
173
  const deviceIdentifier = this.createDeviceIdentifier(config);
168
174
  const delivery = new single_script_injection_1.ResponseDeliveryService(config.callbackName);
169
- const store = new single_script_injection_1.ScheduleStore(config.surveyId, new cache_1.LocalStorageAdapter());
175
+ const store = new single_script_injection_1.ScheduleStore(config, new cache_1.LocalStorageAdapter());
170
176
  const credentials = { token: config.token, env: config.env };
171
177
  // Re-emit the verdict from an earlier page before the schedule resumes, so a
172
178
  // page that only now registers a listener still receives the current one.
@@ -176,13 +182,15 @@ class AutoInitService {
176
182
  // The LOI comes off the tag, so every checkpoint is known up front. On a
177
183
  // later page the record already carries it, and `applyLoi` leaves it alone.
178
184
  store.resumeOrCreate();
179
- store.applyLoi(config.expectedSurveyLOI);
185
+ if (Number.isFinite(config.expectedSurveyLOI) && config.expectedSurveyLOI > 0) {
186
+ store.applyLoi(config.expectedSurveyLOI);
187
+ }
180
188
  const transactionReady = this.resolveTransactionId(store, deviceIdentifier, config, credentials);
181
189
  const scheduler = new single_script_injection_1.CheckpointScheduler(store, () => __awaiter(this, void 0, void 0, function* () {
182
190
  const transactionId = yield transactionReady;
183
191
  if (!transactionId)
184
192
  throw new Error('no transaction id to send the checkpoint for');
185
- const response = yield deviceIdentifier.trackDetails(transactionId, config.metadata, credentials, { surveyId: config.surveyId });
193
+ const response = yield deviceIdentifier.trackDetails(transactionId, config.metadata, credentials, { projectId: config.projectId });
186
194
  store.storeResponse(response);
187
195
  delivery.deliver(response);
188
196
  }));
@@ -206,7 +214,6 @@ class AutoInitService {
206
214
  }
207
215
  /**
208
216
  * Registers the transaction, trying up to `TRANSACTION_CREATION_ATTEMPTS` times.
209
- * Resolves with undefined when every attempt failed.
210
217
  */
211
218
  createTransaction(deviceIdentifier, config, credentials) {
212
219
  return __awaiter(this, void 0, void 0, function* () {
@@ -214,7 +221,7 @@ class AutoInitService {
214
221
  return new Promise(resolve => setTimeout(resolve, ms));
215
222
  };
216
223
  const details = {
217
- surveyId: config.surveyId,
224
+ projectId: config.projectId,
218
225
  expectedSurveyLOI: config.expectedSurveyLOI,
219
226
  surveyCountry: config.surveyCountry,
220
227
  surveyLanguage: config.surveyLanguage,
@@ -227,17 +234,19 @@ class AutoInitService {
227
234
  });
228
235
  for (let attempt = 1; attempt <= exports.TRANSACTION_CREATION_ATTEMPTS; attempt++) {
229
236
  try {
230
- const transactionId = yield deviceIdentifier.createTransaction(details, credentials);
237
+ const transactionId = yield deviceIdentifier.createTransaction(details, credentials, config.metadata);
231
238
  if (transactionId)
232
239
  return transactionId;
233
240
  }
234
241
  catch (error) {
235
- console.error(`Error creating Device-Intelligence transaction (attempt ${attempt}):`, error);
242
+ if (error instanceof errors_1.TransactionRejectedError)
243
+ return undefined;
244
+ console.error(`Error creating PureScore-Intelligence transaction (attempt ${attempt}):`, error);
236
245
  }
237
246
  if (attempt < exports.TRANSACTION_CREATION_ATTEMPTS)
238
247
  yield delay(exports.TRANSACTION_RETRY_DELAY_MS);
239
248
  }
240
- console.error(`Device-Identifier auto-init: could not create the transaction after ${exports.TRANSACTION_CREATION_ATTEMPTS} attempts`);
249
+ console.error(`Purescore-Client auto-init: could not create the transaction after ${exports.TRANSACTION_CREATION_ATTEMPTS} attempts`);
241
250
  return undefined;
242
251
  });
243
252
  }
@@ -26,6 +26,11 @@ export declare class DeviceIdentifier implements TrackDetails, CreateTransaction
26
26
  * Service responsible for performing API requests to the Purescore Intelligence service.
27
27
  */
28
28
  private deviceIntelligenceService;
29
+ /**
30
+ * Source of the Verisoul session id sent with each respondent PATCH. Only
31
+ * set under the auto-init bootstrap; not configurable from the outside.
32
+ */
33
+ private verisoul?;
29
34
  private singleScriptInjection?;
30
35
  private screen_resolution?;
31
36
  private screen_orientation?;
@@ -61,6 +66,9 @@ export declare class DeviceIdentifier implements TrackDetails, CreateTransaction
61
66
  };
62
67
  webdriverEnabled: boolean;
63
68
  estimatedRoundTripTime: number;
69
+ cpuCores: number | null;
70
+ deviceMemory: number | null;
71
+ gpu: string | null;
64
72
  ua: string;
65
73
  browser: {
66
74
  name?: string;
@@ -110,7 +118,16 @@ export declare class DeviceIdentifier implements TrackDetails, CreateTransaction
110
118
  * as inactive.
111
119
  */
112
120
  deactivateBehavioralBiometricsDetection(): void;
113
- createTransaction(details: TransactionDetails, config?: DeviceIdentifierConfig): Promise<string | undefined>;
121
+ /**
122
+ * The device details of a session
123
+ */
124
+ private collectSessionDetails;
125
+ /**
126
+ * Registers the transaction together with session 0: the device details
127
+ * collected right now, so the transaction can be evaluated as soon as it
128
+ * exists.
129
+ */
130
+ createTransaction(details: TransactionDetails, config?: DeviceIdentifierConfig, metadata?: DeviceIdentifierMetadata): Promise<string | undefined>;
114
131
  trackDetails(transactionId: string, metadata?: DeviceIdentifierMetadata, config?: DeviceIdentifierConfig, context?: TrackDetailsContext): Promise<Record<string, any>>;
115
132
  sendBehaviorBiometrics(transactionId: string, params: BehaviorBiometricsParams, config?: DeviceIdentifierConfig): Promise<any>;
116
133
  }
@@ -12,9 +12,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.DeviceIdentifier = void 0;
13
13
  const device_fingerprint_1 = require("../domain/services/device-fingerprint");
14
14
  const behavioural_biometrics_1 = require("../domain/services/behavioural-biometrics");
15
+ const interaction_activity_1 = require("../domain/services/interaction-activity");
16
+ const errors_1 = require("../domain/errors");
15
17
  const device_intelligence_1 = require("../domain/services/device-intelligence");
16
18
  const crosscutting_1 = require("../domain/models/crosscutting");
19
+ const verisoul_1 = require("../domain/services/verisoul");
17
20
  const spa_navigation_1 = require("../domain/services/spa-navigation");
21
+ const dom_mutation_tracker_1 = require("../domain/services/dom-mutation-tracker");
18
22
  const axios_http_client_1 = require("../infra/http/axios-http-client");
19
23
  const cache_1 = require("../infra/cache");
20
24
  const hash = new device_fingerprint_1.Hash();
@@ -24,7 +28,10 @@ const navigatior = new device_fingerprint_1.Navigatior();
24
28
  const canvas = new device_fingerprint_1.Canvas();
25
29
  const utils = new device_fingerprint_1.Utils();
26
30
  const audioFingerprint = new device_fingerprint_1.AudioFingerprint();
31
+ const webgl = new device_fingerprint_1.WebGL();
27
32
  const pointerCapability = new device_fingerprint_1.PointerCapabilityDetector();
33
+ const domMutationTracker = new dom_mutation_tracker_1.DomMutationTracker(new cache_1.SessionStorageAdapter());
34
+ const interactionActivity = new interaction_activity_1.InteractionActivityTracker(new cache_1.SessionStorageAdapter());
28
35
  // One collector shared for the lifetime of the page. It owns the trackers, which
29
36
  // is what makes behavioural data survive a DeviceIdentifier being re-instantiated.
30
37
  const behaviouralBiometrics = new behavioural_biometrics_1.BehaviouralBiometricsCollector(new behavioural_biometrics_1.TouchBehavior(), new behavioural_biometrics_1.CursorBehavior(), new behavioural_biometrics_1.KeyboardMetrics(), new cache_1.SessionStorageAdapter(), new spa_navigation_1.SpaNavigationDetector());
@@ -52,7 +59,14 @@ class DeviceIdentifier {
52
59
  this.token = token;
53
60
  this.baseUrl = (0, crosscutting_1.resolveDeviceIntelligenceBaseUrl)(env);
54
61
  this.deviceIntelligenceService = new device_intelligence_1.DeviceIntelligenceService(new axios_http_client_1.AxiosHttpClient(), this.baseUrl, this.token);
62
+ if (this.singleScriptInjection && typeof window !== 'undefined') {
63
+ // Load the SDK now so its session is ready by the first checkpoint.
64
+ this.verisoul = new verisoul_1.VerisoulService((0, crosscutting_1.resolveVerisoulConfig)(env));
65
+ this.verisoul.load();
66
+ }
55
67
  this.activateBehavioralBiometricsDetection();
68
+ domMutationTracker.start();
69
+ interactionActivity.startTracking();
56
70
  }
57
71
  /**
58
72
  * Generates the fingerprint data for the browser.
@@ -70,7 +84,7 @@ class DeviceIdentifier {
70
84
  const hash = this.getIdentity(canvasFingerprint);
71
85
  return Object.assign(Object.assign({}, parsedData), { hash, canvas: canvasFingerprint, cookieEnabled: navigatior.isCookieEnabled(), display: display.getDisplayDetails(), javaEnabled: navigatior.isJavaEnabled(), tzOffset: utils.getTimezoneOffset(), touchEnabled: utils.getTouchSupport() || pointerCapability.hasTouchOrPen(), userAgent: navigatior.getUserAgent(), platform: navigatior.getPlatform(), languages: navigatior.getLanguages(), displayChecks: display.displayChecks(), networkPerformance: utils.getNetworkTime(), isWebCrawler: navigatior.isCrawler(), audioFingerprint: audioFingerprintResponse, userAgentClientHints: {
72
86
  model: highEntropyClientHints === null || highEntropyClientHints === void 0 ? void 0 : highEntropyClientHints.model
73
- }, webdriverEnabled: navigatior.isWebDriverEnabled(), estimatedRoundTripTime: navigatior.getEstimatedRoundTripTime() });
87
+ }, webdriverEnabled: navigatior.isWebDriverEnabled(), estimatedRoundTripTime: navigatior.getEstimatedRoundTripTime(), cpuCores: navigatior.getCpuCores(), deviceMemory: navigatior.getDeviceMemory(), gpu: webgl.getGpuRenderer() });
74
88
  });
75
89
  }
76
90
  /**
@@ -127,10 +141,34 @@ class DeviceIdentifier {
127
141
  deactivateBehavioralBiometricsDetection() {
128
142
  behaviouralBiometrics.deactivate();
129
143
  }
144
+ /**
145
+ * The device details of a session
146
+ */
147
+ collectSessionDetails(metadata) {
148
+ return __awaiter(this, void 0, void 0, function* () {
149
+ var _a;
150
+ const [details, verisoulSessionId] = yield Promise.all([
151
+ this.getDetails(),
152
+ (_a = this.verisoul) === null || _a === void 0 ? void 0 : _a.getSessionId(),
153
+ ]);
154
+ return Object.assign(Object.assign(Object.assign({ metadata }, details), (verisoulSessionId ? { verisoulSessionId } : {})), { domElementsMutationCount: domMutationTracker.domElementsMutationCount, interactionActivity: interactionActivity.getInteractionActivity() });
155
+ });
156
+ }
157
+ /**
158
+ * Registers the transaction together with session 0: the device details
159
+ * collected right now, so the transaction can be evaluated as soon as it
160
+ * exists.
161
+ */
130
162
  createTransaction(details_1) {
131
- return __awaiter(this, arguments, void 0, function* (details, config = {}) {
163
+ return __awaiter(this, arguments, void 0, function* (details, config = {}, metadata = {}) {
132
164
  var _a;
133
- const response = yield this.deviceIntelligenceService.performRequest('/v1/transaction', 'post', config, details);
165
+ const session = yield this.collectSessionDetails(metadata).catch(error => {
166
+ console.error('Error collecting the PureScore-Intelligence Session 0 details:', error);
167
+ return undefined;
168
+ });
169
+ const response = yield this.deviceIntelligenceService.performRequest('/v1/transaction', 'post', config, session ? Object.assign(Object.assign({}, details), { session }) : details);
170
+ if ((response === null || response === void 0 ? void 0 : response.statusCode) === 422)
171
+ throw new errors_1.TransactionRejectedError(response.body);
134
172
  const transactionId = (_a = response === null || response === void 0 ? void 0 : response.body) === null || _a === void 0 ? void 0 : _a.transactionId;
135
173
  const succeeded = typeof (response === null || response === void 0 ? void 0 : response.statusCode) === 'number'
136
174
  && response.statusCode < 400
@@ -143,7 +181,7 @@ class DeviceIdentifier {
143
181
  return __awaiter(this, arguments, void 0, function* (transactionId, metadata = {}, config = {}, context = {}) {
144
182
  var _a;
145
183
  behaviouralBiometrics.pauseWindow();
146
- const bbParams = context.surveyId ? Object.assign(Object.assign({}, metadata), { survey_id: context.surveyId }) : metadata;
184
+ const bbParams = context.projectId ? Object.assign(Object.assign({}, metadata), { survey_id: context.projectId }) : metadata;
147
185
  const bbResponse = yield this.sendBehaviorBiometrics(transactionId, bbParams, config);
148
186
  if (bbResponse.statusCode >= 400) {
149
187
  this.deactivateBehavioralBiometricsDetection();
@@ -151,7 +189,7 @@ class DeviceIdentifier {
151
189
  else {
152
190
  behaviouralBiometrics.startCycle(this.behaviouralBiometricsOptions);
153
191
  }
154
- const payload = Object.assign(Object.assign({ metadata }, yield this.getDetails()), { transactionId });
192
+ const payload = Object.assign(Object.assign({}, yield this.collectSessionDetails(metadata)), { transactionId });
155
193
  const response = yield this.deviceIntelligenceService.performRequest(`/v1/respondent/${transactionId}`, 'patch', config, payload);
156
194
  return (_a = response.body) !== null && _a !== void 0 ? _a : response;
157
195
  });
@@ -0,0 +1 @@
1
+ export * from './transaction-rejected-error';
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./transaction-rejected-error"), exports);
@@ -0,0 +1,7 @@
1
+ export declare class TransactionRejectedError extends Error {
2
+ readonly body: unknown;
3
+ /**
4
+ * @param body - the service's response body, kept so the caller can surface its validation message.
5
+ */
6
+ constructor(body: unknown);
7
+ }
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TransactionRejectedError = void 0;
4
+ class TransactionRejectedError extends Error {
5
+ /**
6
+ * @param body - the service's response body, kept so the caller can surface its validation message.
7
+ */
8
+ constructor(body) {
9
+ super('PureScore-Intelligence rejected the transaction details (HTTP 422)');
10
+ this.body = body;
11
+ this.name = 'TransactionRejectedError';
12
+ }
13
+ }
14
+ exports.TransactionRejectedError = TransactionRejectedError;
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * NOTE: these endpoints are going to be changed to a new domain
3
3
  */
4
- /** Device-Intelligence API, per environment. */
4
+ /** PureScore-Intelligence API, per environment. */
5
5
  export declare const PURESCORE_INTELLIGENCE_BASE_URL: {
6
- readonly production: "https://purescore.spectrumsurveys.com";
7
- readonly staging: "https://purescore-staging.spectrumsurveys.com";
6
+ readonly production: "https://purescore-intelligence.spectrumsurveys.com";
7
+ readonly staging: "https://purescore-intelligence-uat.spectrumsurveys.com";
8
8
  };
9
9
  export type DeviceIntelligenceEnv = keyof typeof PURESCORE_INTELLIGENCE_BASE_URL;
10
10
  /** Anything other than an explicit `production` resolves to staging. */
@@ -5,10 +5,10 @@ exports.resolveDeviceIntelligenceBaseUrl = resolveDeviceIntelligenceBaseUrl;
5
5
  /**
6
6
  * NOTE: these endpoints are going to be changed to a new domain
7
7
  */
8
- /** Device-Intelligence API, per environment. */
8
+ /** PureScore-Intelligence API, per environment. */
9
9
  exports.PURESCORE_INTELLIGENCE_BASE_URL = {
10
- production: 'https://purescore.spectrumsurveys.com',
11
- staging: 'https://purescore-staging.spectrumsurveys.com',
10
+ production: 'https://purescore-intelligence.spectrumsurveys.com',
11
+ staging: 'https://purescore-intelligence-uat.spectrumsurveys.com',
12
12
  };
13
13
  /** Anything other than an explicit `production` resolves to staging. */
14
14
  function resolveDeviceIntelligenceBaseUrl(env) {
@@ -1 +1,2 @@
1
1
  export * from './device-intelligence';
2
+ export * from './verisoul';
@@ -15,3 +15,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./device-intelligence"), exports);
18
+ __exportStar(require("./verisoul"), exports);
@@ -0,0 +1,19 @@
1
+ import { DeviceIntelligenceEnv } from './device-intelligence';
2
+ /**
3
+ * Verisoul SDK, per environment. The bundle is served through our own proxy
4
+ * (same hosts as ps-screener) so ad-blockers keyed on the vendor domain don't
5
+ * drop it; the project id is what ties the session to our Verisoul account.
6
+ */
7
+ export declare const VERISOUL_CONFIG: {
8
+ readonly production: {
9
+ readonly sdkUrl: "https://vs.spectrumsurveys.com/bundle.js";
10
+ readonly projectId: "7a994dae-58a8-413f-a59a-655a7adbfb2d";
11
+ };
12
+ readonly staging: {
13
+ readonly sdkUrl: "https://sandbox.vs.spectrumsurveys.com/bundle.js";
14
+ readonly projectId: "6e7419d0-3be6-43d7-a4a4-9d44828d9f5e";
15
+ };
16
+ };
17
+ export type VerisoulEnvConfig = (typeof VERISOUL_CONFIG)[DeviceIntelligenceEnv];
18
+ /** Anything other than an explicit `production` resolves to the sandbox project. */
19
+ export declare function resolveVerisoulConfig(env?: string): VerisoulEnvConfig;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VERISOUL_CONFIG = void 0;
4
+ exports.resolveVerisoulConfig = resolveVerisoulConfig;
5
+ /**
6
+ * Verisoul SDK, per environment. The bundle is served through our own proxy
7
+ * (same hosts as ps-screener) so ad-blockers keyed on the vendor domain don't
8
+ * drop it; the project id is what ties the session to our Verisoul account.
9
+ */
10
+ exports.VERISOUL_CONFIG = {
11
+ production: {
12
+ sdkUrl: 'https://vs.spectrumsurveys.com/bundle.js',
13
+ projectId: '7a994dae-58a8-413f-a59a-655a7adbfb2d',
14
+ },
15
+ staging: {
16
+ sdkUrl: 'https://sandbox.vs.spectrumsurveys.com/bundle.js',
17
+ projectId: '6e7419d0-3be6-43d7-a4a4-9d44828d9f5e',
18
+ },
19
+ };
20
+ /** Anything other than an explicit `production` resolves to the sandbox project. */
21
+ function resolveVerisoulConfig(env) {
22
+ return (env === null || env === void 0 ? void 0 : env.toLowerCase()) === 'production'
23
+ ? exports.VERISOUL_CONFIG.production
24
+ : exports.VERISOUL_CONFIG.staging;
25
+ }
@@ -32,6 +32,7 @@ declare global {
32
32
  interface Navigator {
33
33
  userAgentData?: NavigatorUAData;
34
34
  connection?: NetworkInformation;
35
+ deviceMemory?: number;
35
36
  }
36
37
  }
37
38
  export interface UAParserDeviceType {