feat: remove stych, add prelude client (#3312)

This commit is contained in:
Daniel Salazar
2026-06-27 14:07:52 -07:00
committed by GitHub
parent 98461088f8
commit 0752ea761c
12 changed files with 138 additions and 157 deletions
+31 -20
View File
@@ -1516,18 +1516,6 @@
"sisteransi": "^1.0.5"
}
},
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
"version": "1.3.0",
"extraneous": true,
"inBundle": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@cloudflare/kv-asset-handler": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz",
@@ -4749,6 +4737,25 @@
"dev": true,
"license": "MIT"
},
"node_modules/@prelude.so/core": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@prelude.so/core/-/core-0.2.1.tgz",
"integrity": "sha512-rVMWSouNhQLvab+kDfhdZvTAB5rExO+801vI9IrR1IGczrtGn7LpJBBLMjLwIzUkb+ZJsSwJGkV6fTipyfRALA==",
"license": "Apache-2.0"
},
"node_modules/@prelude.so/js-sdk": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@prelude.so/js-sdk/-/js-sdk-0.12.0.tgz",
"integrity": "sha512-kGkB8uVl9Q5URE6n3BTVsOM+7hn6nVpiDy+07SKj5vAdmTuSfifiWJh4oDi1ll+M1tNM9qoLtqywoMYZ7lj2yg==",
"license": "Apache-2.0",
"dependencies": {
"@prelude.so/core": "^0.2.1",
"browser-tabs-lock": "^1.3.0"
},
"engines": {
"node": ">=22"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"license": "BSD-3-Clause"
@@ -7024,7 +7031,6 @@
},
"node_modules/assertion-error": {
"version": "1.1.0",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
@@ -7342,6 +7348,16 @@
"version": "2.1.1",
"license": "MIT"
},
"node_modules/browser-tabs-lock": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/browser-tabs-lock/-/browser-tabs-lock-1.3.0.tgz",
"integrity": "sha512-g6nHaobTiT0eMZ7jh16YpD2kcjAp+PInbiVq3M1x6KKaEIVhT4v9oURNIpZLOZ3LQbQ3XYfNhMAb/9hzNLIWrw==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"lodash": ">=4.17.21"
}
},
"node_modules/browserslist": {
"version": "4.28.2",
"dev": true,
@@ -7512,7 +7528,6 @@
},
"node_modules/chai": {
"version": "4.5.0",
"dev": true,
"license": "MIT",
"dependencies": {
"assertion-error": "^1.1.0",
@@ -8115,7 +8130,6 @@
},
"node_modules/deep-eql": {
"version": "4.1.4",
"dev": true,
"license": "MIT",
"dependencies": {
"type-detect": "^4.0.0"
@@ -11455,7 +11469,6 @@
},
"node_modules/lodash": {
"version": "4.18.1",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.camelcase": {
@@ -11530,7 +11543,6 @@
},
"node_modules/loupe": {
"version": "2.3.7",
"dev": true,
"license": "MIT",
"dependencies": {
"get-func-name": "^2.0.1"
@@ -12548,7 +12560,6 @@
},
"node_modules/pathval": {
"version": "1.1.1",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
@@ -15066,7 +15077,6 @@
},
"node_modules/type-detect": {
"version": "4.1.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -15110,7 +15120,7 @@
},
"node_modules/typescript": {
"version": "5.9.3",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -18121,6 +18131,7 @@
"dependencies": {
"@opentelemetry/auto-instrumentations-node": "0.77.0",
"@opentelemetry/sdk-node": "0.219.0",
"@prelude.so/js-sdk": "0.12.0",
"@thumbmarkjs/thumbmarkjs": "1.9.1",
"file-type": "21.3.3",
"json-colorizer": "^3.0.1",
+1 -2
View File
@@ -137,9 +137,8 @@ export type EventMap = {
* the request for log / support correlation.
*/
trail_id?: string;
/** Device signals forwarded verbatim from the signup request body. */
/** Device signal forwarded verbatim from the signup request body. */
fingerprint?: string | null;
dfp_telemetry_id?: string | null;
/** Set by the abuse harness — require SMS phone verification post-signup. */
requires_phone_verification?: boolean;
/** Set by the abuse harness — require card verification post-signup. */
@@ -115,6 +115,32 @@ describe('PreludeClient', () => {
);
});
it('forwards dispatch_id as a top-level field, not inside signals', async () => {
fetchMock.mockResolvedValue(okJson({ id: 'v', status: 'success' }));
const client = makeClient('sk_test');
await client.createVerification('+14155550123', {
ip: '203.0.113.7',
dispatch_id: 'd1f5e9a0-0000-4000-8000-000000000000',
});
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
expect(body.dispatch_id).toBe('d1f5e9a0-0000-4000-8000-000000000000');
expect(body.signals).toEqual({ ip: '203.0.113.7' });
expect(body.signals).not.toHaveProperty('dispatch_id');
});
it('omits dispatch_id when not supplied', async () => {
fetchMock.mockResolvedValue(okJson({ id: 'v', status: 'success' }));
const client = makeClient('sk_test');
await client.createVerification('+14155550123', { ip: '203.0.113.7' });
expect(JSON.parse(fetchMock.mock.calls[0][1].body)).not.toHaveProperty(
'dispatch_id',
);
});
it('includes a configured template_id + sender_id + preferred channel', async () => {
fetchMock.mockResolvedValue(okJson({ id: 'v', status: 'success' }));
const client = new PreludeClient({
+6 -2
View File
@@ -125,8 +125,9 @@ export class PreludeClient extends PuterClient {
* device + platform are the highest-value). We pass what the backend
* already has: the request `ip`, the client `device_id` (ThumbmarkJS
* hash), and the `user_agent` (Prelude infers platform/model/OS from it).
* Richer client-collected signals require Prelude's frontend SDK, which
* produces a `dispatch_id` the backend would forward here.
* `dispatch_id` carries the richer browser signals gathered by Prelude's
* frontend JS Signals SDK; it's a top-level field in the request, not part
* of the `signals` object, so it's pulled out below.
*/
async createVerification(
target: string,
@@ -134,6 +135,7 @@ export class PreludeClient extends PuterClient {
ip?: string;
device_id?: string;
user_agent?: string;
dispatch_id?: string;
} = {},
): Promise<{ id?: string; status: PreludeCreateStatus }> {
// Match the 6-box code UI (UIWindowPhoneVerificationRequired). Without
@@ -164,6 +166,8 @@ export class PreludeClient extends PuterClient {
if (signals.device_id) sig.device_id = signals.device_id;
if (signals.user_agent) sig.user_agent = signals.user_agent;
if (Object.keys(sig).length > 0) body.signals = sig;
// dispatch_id is a top-level field, not a member of `signals`.
if (signals.dispatch_id) body.dispatch_id = signals.dispatch_id;
return this.#post('/verification', body) as Promise<{
id?: string;
status: PreludeCreateStatus;
@@ -609,7 +609,7 @@ describe('AuthController.handleSignup', () => {
});
});
// -- Signup device signals (fingerprint + dfp_telemetry_id) --
// -- Signup device signal (fingerprint) --
describe('AuthController.handleSignup device signals', () => {
const uniq = () => Math.random().toString(36).slice(2, 10);
@@ -631,11 +631,10 @@ describe('AuthController.handleSignup device signals', () => {
(evt) => (evt as { username?: string }).username === username,
);
it('forwards fingerprint and dfp_telemetry_id verbatim to validate and success events', async () => {
it('forwards fingerprint verbatim to validate and success events', async () => {
const username = `fp_${uniq()}`;
const baseline = heardSignupSuccess.length;
const fingerprint = 'Fp_abc.123-XYZ';
const dfpTelemetryId = 'tel_id-456';
const seen = await captureValidateEvents(async () => {
const res = makeRes();
@@ -645,7 +644,6 @@ describe('AuthController.handleSignup device signals', () => {
email: `${username}@test.local`,
password: 'correct-horse-battery',
fingerprint,
dfp_telemetry_id: dfpTelemetryId,
}),
res,
);
@@ -654,7 +652,6 @@ describe('AuthController.handleSignup device signals', () => {
expect(seen).toHaveLength(1);
expect(seen[0].fingerprint).toBe(fingerprint);
expect(seen[0].dfp_telemetry_id).toBe(dfpTelemetryId);
const successes = successEventsFor(baseline, username);
expect(successes).toHaveLength(1);
@@ -662,7 +659,7 @@ describe('AuthController.handleSignup device signals', () => {
expect(successes[0].is_temp).toBe(false);
});
it('accepts boundary-length values (128-char fingerprint, 64-char dfp_telemetry_id)', async () => {
it('accepts a boundary-length 128-char fingerprint', async () => {
const username = `fp_${uniq()}`;
const res = makeRes();
await controller.handleSignup(
@@ -671,14 +668,13 @@ describe('AuthController.handleSignup device signals', () => {
email: `${username}@test.local`,
password: 'correct-horse-battery',
fingerprint: 'f'.repeat(128),
dfp_telemetry_id: 'd'.repeat(64),
}),
res,
);
expect(isCompleteLoginResponse(res.body)).toBe(true);
});
it('defaults both fields to null on the validate event when absent', async () => {
it('defaults the fingerprint to null on the validate event when absent', async () => {
const username = `fp_${uniq()}`;
const baseline = heardSignupSuccess.length;
@@ -699,7 +695,6 @@ describe('AuthController.handleSignup device signals', () => {
expect(seen).toHaveLength(1);
expect(seen[0].fingerprint).toBeNull();
expect(seen[0].dfp_telemetry_id).toBeNull();
const successes = successEventsFor(baseline, username);
expect(successes).toHaveLength(1);
@@ -707,7 +702,7 @@ describe('AuthController.handleSignup device signals', () => {
expect(successes[0].is_temp).toBe(false);
});
it('treats empty-string fingerprint and dfp_telemetry_id as absent', async () => {
it('treats an empty-string fingerprint as absent', async () => {
const username = `fp_${uniq()}`;
const seen = await captureValidateEvents(async () => {
@@ -718,7 +713,6 @@ describe('AuthController.handleSignup device signals', () => {
email: `${username}@test.local`,
password: 'correct-horse-battery',
fingerprint: '',
dfp_telemetry_id: '',
}),
res,
);
@@ -728,7 +722,6 @@ describe('AuthController.handleSignup device signals', () => {
expect(seen).toHaveLength(1);
expect(seen[0].fingerprint).toBeNull();
expect(seen[0].dfp_telemetry_id).toBeNull();
});
it('rejects a non-string fingerprint with 400 and fires no success event', async () => {
@@ -770,37 +763,6 @@ describe('AuthController.handleSignup device signals', () => {
expect(heardSignupSuccess.length).toBe(baseline);
});
it('rejects a dfp_telemetry_id longer than 64 characters with 400 and fires no success event', async () => {
const username = `fp_${uniq()}`;
const baseline = heardSignupSuccess.length;
await expect(
controller.handleSignup(
makeReq({
username,
email: `${username}@test.local`,
password: 'correct-horse-battery',
dfp_telemetry_id: 'd'.repeat(65),
}),
makeRes(),
),
).rejects.toMatchObject({ statusCode: 400 });
expect(heardSignupSuccess.length).toBe(baseline);
});
it('rejects a non-string dfp_telemetry_id with 400', async () => {
await expect(
controller.handleSignup(
makeReq({
username: `fp_${uniq()}`,
email: `${uniq()}@test.local`,
password: 'correct-horse-battery',
dfp_telemetry_id: { nested: true },
}),
makeRes(),
),
).rejects.toMatchObject({ statusCode: 400 });
});
it('temp-user signup reports is_temp true and carries the fingerprint on the success event', async () => {
const baseline = heardSignupSuccess.length;
const res = makeRes();
+18 -24
View File
@@ -59,7 +59,7 @@ import { PuterController } from '../types.js';
const USERNAME_REGEX = /^\w{1,}$/;
const USERNAME_MAX_LENGTH = 45;
const FINGERPRINT_MAX_LENGTH = 128;
const DFP_TELEMETRY_ID_MAX_LENGTH = 64;
const DISPATCH_ID_MAX_LENGTH = 128;
const RESERVED_USERNAMES = new Set([
'admin',
'administrator',
@@ -378,11 +378,10 @@ export class AuthController extends PuterController {
return;
}
// Optional device signals (browser fingerprint hash, DFP telemetry
// id). Core only enforces shape and forwards the values verbatim —
// signup-abuse policy built on them lives in extensions. Checked
// before the reauth short-circuit so a malformed value is rejected
// on every /signup path.
// Optional device signal (browser fingerprint hash). Core only enforces
// shape and forwards the value verbatim — signup-abuse policy built on it
// lives in extensions. Checked before the reauth short-circuit so a
// malformed value is rejected on every /signup path.
if (body.fingerprint !== undefined && body.fingerprint !== null) {
if (typeof body.fingerprint !== 'string')
throw new HttpError(400, 'fingerprint must be a string.', {
@@ -395,25 +394,9 @@ export class AuthController extends PuterController {
{ legacyCode: 'bad_request' },
);
}
if (
body.dfp_telemetry_id !== undefined &&
body.dfp_telemetry_id !== null
) {
if (typeof body.dfp_telemetry_id !== 'string')
throw new HttpError(400, 'dfp_telemetry_id must be a string.', {
legacyCode: 'bad_request',
});
if (body.dfp_telemetry_id.length > DFP_TELEMETRY_ID_MAX_LENGTH)
throw new HttpError(
400,
`dfp_telemetry_id cannot be longer than ${DFP_TELEMETRY_ID_MAX_LENGTH} characters.`,
{ legacyCode: 'bad_request' },
);
}
// Empty strings are treated as absent — a signal that wasn't
// collected, not a malformed request.
const fingerprint: string | null = body.fingerprint || null;
const dfp_telemetry_id: string | null = body.dfp_telemetry_id || null;
// Temp-user reauth short-circuit: when an existing temp user is
// forced through the reauth flow, the GUI re-submits /signup with
@@ -605,7 +588,6 @@ export class AuthController extends PuterController {
code: null,
user_agent: req?.headers?.['user-agent'] ?? null,
fingerprint,
dfp_telemetry_id,
// Populated by the abuse extension's v2 harness; persisted to the
// user row below so the signup-time reputation is referable later.
reputation: null as number | null,
@@ -746,7 +728,6 @@ export class AuthController extends PuterController {
user_agent: req.headers?.['user-agent'],
origin: req.headers?.origin,
fingerprint,
dfp_telemetry_id,
},
signup_ip: clientIp,
signup_ip_forwarded: proxyIpChain,
@@ -1076,6 +1057,18 @@ export class AuthController extends PuterController {
legacyCode: 'bad_request',
});
// Optional Prelude dispatch id (browser signals gathered by the JS
// Signals SDK on the number-entry page). Shape-checked and forwarded
// verbatim to Prelude; an empty / oversized / non-string value is just
// dropped so a bad client signal never blocks a real verification.
const rawDispatchId = req.body?.dispatch_id;
const dispatchId =
typeof rawDispatchId === 'string' &&
rawDispatchId.length > 0 &&
rawDispatchId.length <= DISPATCH_ID_MAX_LENGTH
? rawDispatchId
: undefined;
// Cost cap: skip countries with no SMS channel or rates above the cap
// (see PreludeClient / countries.ts). Avoids paying exorbitant per-SMS
// rates in low-revenue, high-fraud geographies.
@@ -1160,6 +1153,7 @@ export class AuthController extends PuterController {
ip,
device_id: req.deviceFingerprint ?? undefined,
user_agent: userAgent,
dispatch_id: dispatchId,
},
);
// Prelude rejected the attempt as abusive — surface as rate-limit.
+7
View File
@@ -397,6 +397,13 @@ export interface IGuiParams {
title?: string;
short_description?: string;
social_media_image?: string;
/**
* Public SDK key for Prelude's JS Signals SDK. When set, the phone-
* verification window collects a `dispatch_id` (browser signals) and
* forwards it to /send-confirm-phone, which passes it to Prelude's Verify
* API so its abuse model can weigh the device. Omit to disable collection.
*/
preludeSdkKey?: string;
[key: string]: unknown;
}
+1
View File
@@ -47,6 +47,7 @@
"dependencies": {
"@opentelemetry/auto-instrumentations-node": "0.77.0",
"@opentelemetry/sdk-node": "0.219.0",
"@prelude.so/js-sdk": "0.12.0",
"@thumbmarkjs/thumbmarkjs": "1.9.1",
"file-type": "21.3.3",
"json-colorizer": "^3.0.1",
@@ -717,20 +717,28 @@ function UIWindowPhoneVerificationRequired(options) {
.prop('disabled', true)
.html(spinner);
// Same device fingerprint signal signup sends, so the abuse
// extension can cap SMS sends per device across accounts. Best
// effort — a failure/timeout just omits it (the cap fails open).
// Two best-effort device signals, both gathered on this number-entry
// page. The fingerprint lets the abuse extension cap SMS sends per
// device across accounts; the Prelude dispatch id forwards browser
// signals into Prelude's own Verify abuse model. A failure/timeout
// on either just omits it (everything downstream fails open).
let fingerprint = null;
let dispatchId = null;
try {
fingerprint = await window.getDeviceFingerprint?.();
[fingerprint, dispatchId] = await Promise.all([
window.getDeviceFingerprint?.(),
window.getPreludeDispatchId?.(),
]);
} catch (_) {}
const sendData = { phone };
if ( fingerprint ) sendData.fingerprint = fingerprint;
if ( dispatchId ) sendData.dispatch_id = dispatchId;
$.ajax({
url: `${window.api_origin}/send-confirm-phone`,
type: 'POST',
data: JSON.stringify(
fingerprint ? { phone, fingerprint } : { phone },
),
data: JSON.stringify(sendData),
async: true,
contentType: 'application/json',
headers: { Authorization: `Bearer ${window.auth_token}` },
+2 -9
View File
@@ -353,14 +353,10 @@ function UIWindowSignup (options) {
headers = window.custom_headers;
}
// Device signals for abuse prevention; omitted when unavailable
// Device signal for abuse prevention; omitted when unavailable
let fingerprint = null;
let dfpTelemetryId = null;
try {
[fingerprint, dfpTelemetryId] = await Promise.all([
window.getDeviceFingerprint?.(),
window.getDfpTelemetryId?.(),
]);
fingerprint = await window.getDeviceFingerprint?.();
} catch (_) {
// signup must never block or fail because of device signals
}
@@ -378,9 +374,6 @@ function UIWindowSignup (options) {
if ( fingerprint ) {
requestData.fingerprint = fingerprint;
}
if ( dfpTelemetryId ) {
requestData.dfp_telemetry_id = dfpTelemetryId;
}
$.ajax({
url: `${window.gui_origin }/signup`,
+22 -40
View File
@@ -18,13 +18,16 @@
*/
import { Thumbmark } from '@thumbmarkjs/thumbmarkjs';
import { dispatchSignals } from '@prelude.so/js-sdk/signals';
const FINGERPRINT_TIMEOUT = 1500;
const DFP_TELEMETRY_TIMEOUT = 2500;
// Server-side caps for the matching /signup fields; values that would be
// rejected there are dropped client-side so signup can never 400 over them.
const PRELUDE_DISPATCH_TIMEOUT = 2500;
// Server-side cap for the matching /signup field; a value that would be
// rejected there is dropped client-side so signup can never 400 over it.
const FINGERPRINT_MAX_LENGTH = 128;
const DFP_TELEMETRY_ID_MAX_LENGTH = 64;
// Prelude dispatch ids are UUIDs; cap generously so a malformed value never
// inflates the verification request body.
const PRELUDE_DISPATCH_ID_MAX_LENGTH = 128;
// Resolves null on rejection or timeout so signup flows can await these
// signals unconditionally without ever blocking or failing on them.
@@ -61,41 +64,20 @@ const computeFingerprint = () => {
return fingerprint_promise;
};
let stytch_script_promise = null;
const loadStytchScript = () => {
if ( ! stytch_script_promise ) {
stytch_script_promise = window.loadScript('https://elements.stytch.com/telemetry.js');
stytch_script_promise.catch(error => {
// Don't cache the failure — a transient load error would
// otherwise disable DFP for the rest of the session.
stytch_script_promise = null;
console.debug('Stytch telemetry script unavailable:', error);
});
}
return stytch_script_promise;
};
// Telemetry ids are short-lived, so unlike the fingerprint this is fetched
// fresh on every call; only the script load itself is reused.
const fetchDfpTelemetryId = async () => {
await loadStytchScript();
if ( typeof window.GetTelemetryID !== 'function' ) {
return null;
}
const telemetry_id = await window.GetTelemetryID({
publicToken: window.gui_params.stytchPublicToken,
});
return telemetry_id || null;
// Dispatch ids are tied to a single verification attempt, so unlike the
// fingerprint this is fetched fresh on every call rather than cached.
const fetchPreludeDispatchId = async () => {
return dispatchSignals(window.gui_params.preludeSdkKey);
};
/**
* Installs window.getDeviceFingerprint() and window.getDfpTelemetryId(). Both
* getters resolve to string|null and never reject. Collection is lazy: no
* probing happens and no third-party script is loaded until a signup flow
* actually asks. The fingerprint needs no credentials so it's on by default
* (gui_params.thumbmarkEnabled = false is the kill switch); the Stytch
* telemetry id requires a public token and is collected only when one is
* configured.
* Installs window.getDeviceFingerprint() and window.getPreludeDispatchId().
* Both getters resolve to string|null and never reject. Collection is lazy: no
* probing or third-party request happens until a flow actually asks. The
* fingerprint needs no credentials so it's on by default (gui_params.
* thumbmarkEnabled = false is the kill switch); the Prelude dispatch id (browser
* signals forwarded to Prelude's Verify abuse model) is collected only when an
* SDK key is configured (gui_params.preludeSdkKey).
*/
const init_device_signals = () => {
window.getDeviceFingerprint = () => {
@@ -110,13 +92,13 @@ const init_device_signals = () => {
}
};
window.getDfpTelemetryId = () => {
window.getPreludeDispatchId = () => {
try {
if ( ! window.gui_params?.stytchPublicToken ) {
if ( ! window.gui_params?.preludeSdkKey ) {
return Promise.resolve(null);
}
return settleWithin(fetchDfpTelemetryId(), DFP_TELEMETRY_TIMEOUT)
.then(value => asPlausible(value, DFP_TELEMETRY_ID_MAX_LENGTH));
return settleWithin(fetchPreludeDispatchId(), PRELUDE_DISPATCH_TIMEOUT)
.then(value => asPlausible(value, PRELUDE_DISPATCH_ID_MAX_LENGTH));
} catch (e) {
return Promise.resolve(null);
}
+4 -10
View File
@@ -306,8 +306,8 @@ window.initgui = async function (options) {
window.url_paths = url_paths;
// Install device signal helpers; collection is lazy. The fingerprint is
// on by default (gui_params.thumbmarkEnabled = false kills it); the Stytch
// telemetry id needs gui_params.stytchPublicToken.
// on by default (gui_params.thumbmarkEnabled = false kills it); the Prelude
// dispatch id needs gui_params.preludeSdkKey.
init_device_signals();
let picked_a_user_for_sdk_login = false;
@@ -1325,18 +1325,12 @@ window.initgui = async function (options) {
requestData['cf-turnstile-response'] = turnstileToken;
}
// Device signals for abuse prevention; omitted when unavailable
// Device signal for abuse prevention; omitted when unavailable
try {
const [fingerprint, dfpTelemetryId] = await Promise.all([
window.getDeviceFingerprint?.(),
window.getDfpTelemetryId?.(),
]);
const fingerprint = await window.getDeviceFingerprint?.();
if ( fingerprint ) {
requestData.fingerprint = fingerprint;
}
if ( dfpTelemetryId ) {
requestData.dfp_telemetry_id = dfpTelemetryId;
}
} catch (e) {
// signup must never block or fail because of device signals
}