From 0752ea761cabb36e50ac47d67115d2b026e1a49b Mon Sep 17 00:00:00 2001 From: Daniel Salazar Date: Sat, 27 Jun 2026 17:07:52 -0400 Subject: [PATCH] feat: remove stych, add prelude client (#3312) --- package-lock.json | 51 +++++++++------ src/backend/clients/event/types.ts | 3 +- .../clients/prelude/PreludeClient.test.ts | 26 ++++++++ src/backend/clients/prelude/PreludeClient.ts | 8 ++- .../controllers/auth/AuthController.test.ts | 48 ++------------ .../controllers/auth/AuthController.ts | 42 ++++++------- src/backend/types.ts | 7 +++ src/gui/package.json | 1 + .../UI/UIWindowPhoneVerificationRequired.js | 22 ++++--- src/gui/src/UI/UIWindowSignup.js | 11 +--- src/gui/src/helpers/device_signals.js | 62 +++++++------------ src/gui/src/initgui.js | 14 ++--- 12 files changed, 138 insertions(+), 157 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1f2de8f4f..64e5b1c2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/src/backend/clients/event/types.ts b/src/backend/clients/event/types.ts index bbfd46a05..0ec09cd01 100644 --- a/src/backend/clients/event/types.ts +++ b/src/backend/clients/event/types.ts @@ -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. */ diff --git a/src/backend/clients/prelude/PreludeClient.test.ts b/src/backend/clients/prelude/PreludeClient.test.ts index f03d6d424..b6edd66dc 100644 --- a/src/backend/clients/prelude/PreludeClient.test.ts +++ b/src/backend/clients/prelude/PreludeClient.test.ts @@ -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({ diff --git a/src/backend/clients/prelude/PreludeClient.ts b/src/backend/clients/prelude/PreludeClient.ts index 716f06313..c59a0b272 100644 --- a/src/backend/clients/prelude/PreludeClient.ts +++ b/src/backend/clients/prelude/PreludeClient.ts @@ -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; diff --git a/src/backend/controllers/auth/AuthController.test.ts b/src/backend/controllers/auth/AuthController.test.ts index eec8ac876..9ea325463 100644 --- a/src/backend/controllers/auth/AuthController.test.ts +++ b/src/backend/controllers/auth/AuthController.test.ts @@ -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(); diff --git a/src/backend/controllers/auth/AuthController.ts b/src/backend/controllers/auth/AuthController.ts index 5824f8cfc..f2d4a2cae 100644 --- a/src/backend/controllers/auth/AuthController.ts +++ b/src/backend/controllers/auth/AuthController.ts @@ -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. diff --git a/src/backend/types.ts b/src/backend/types.ts index 8d15c3982..29ab59d00 100644 --- a/src/backend/types.ts +++ b/src/backend/types.ts @@ -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; } diff --git a/src/gui/package.json b/src/gui/package.json index 94b4afe3b..48824f5a3 100644 --- a/src/gui/package.json +++ b/src/gui/package.json @@ -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", diff --git a/src/gui/src/UI/UIWindowPhoneVerificationRequired.js b/src/gui/src/UI/UIWindowPhoneVerificationRequired.js index 665bbf608..35b4c7466 100644 --- a/src/gui/src/UI/UIWindowPhoneVerificationRequired.js +++ b/src/gui/src/UI/UIWindowPhoneVerificationRequired.js @@ -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}` }, diff --git a/src/gui/src/UI/UIWindowSignup.js b/src/gui/src/UI/UIWindowSignup.js index d72808542..db2707af0 100644 --- a/src/gui/src/UI/UIWindowSignup.js +++ b/src/gui/src/UI/UIWindowSignup.js @@ -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`, diff --git a/src/gui/src/helpers/device_signals.js b/src/gui/src/helpers/device_signals.js index 80824b7c1..56dd86671 100644 --- a/src/gui/src/helpers/device_signals.js +++ b/src/gui/src/helpers/device_signals.js @@ -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); } diff --git a/src/gui/src/initgui.js b/src/gui/src/initgui.js index db867eddb..3614f057f 100644 --- a/src/gui/src/initgui.js +++ b/src/gui/src/initgui.js @@ -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 }