mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-25 15:37:12 +00:00
feat: remove stych, add prelude client (#3312)
This commit is contained in:
Generated
+31
-20
@@ -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",
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}` },
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user