@deflectbot/deflect-sdk 1.4.4 → 1.4.6
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.
- package/dist/index.d.ts +140 -0
- package/dist/index.esm.js +692 -0
- package/dist/index.js +939 -0
- package/dist/index.min.js +2 -0
- package/dist/pulse.d.ts +99 -0
- package/dist/scriptClient.d.ts +8 -0
- package/dist/scriptRunner.d.ts +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
import PulseReporter from "./pulse";
|
|
2
|
+
import { fetchScript } from "./scriptClient";
|
|
3
|
+
import { createBlobUrl, loadModuleScript, waitForGlobalFunction, getTokenFromGlobal, cleanup } from "./scriptRunner";
|
|
4
|
+
const ERROR_COOLDOWN_MS = 2e3;
|
|
5
|
+
const MAX_CONSECUTIVE_ERRORS = 5;
|
|
6
|
+
const MAX_FETCHES_PER_WINDOW = 15;
|
|
7
|
+
const FETCH_WINDOW_MS = 6e4;
|
|
8
|
+
class DeflectClient {
|
|
9
|
+
constructor(config, pulseReporter) {
|
|
10
|
+
this.scriptCache = null;
|
|
11
|
+
this.isWarmupInProgress = false;
|
|
12
|
+
this.hasWarmupError = false;
|
|
13
|
+
this.warmupPromise = null;
|
|
14
|
+
this.scriptFetchPromise = null;
|
|
15
|
+
// Guards against infinite loading
|
|
16
|
+
this.getTokenPromise = null;
|
|
17
|
+
this.lastErrorTime = 0;
|
|
18
|
+
this.consecutiveErrorCount = 0;
|
|
19
|
+
this.fetchCountInWindow = 0;
|
|
20
|
+
this.fetchWindowStart = 0;
|
|
21
|
+
if (!config.actionId?.trim()) {
|
|
22
|
+
throw new Error("actionId is required and cannot be empty");
|
|
23
|
+
}
|
|
24
|
+
this.validateActionId(config.actionId);
|
|
25
|
+
this.config = { prefetch: true, ...config };
|
|
26
|
+
this.pulseReporter = pulseReporter || new PulseReporter(DeflectClient.resolvePulseConfig());
|
|
27
|
+
if (this.config.prefetch !== false && !this.isTestMode()) {
|
|
28
|
+
this.scheduleWarmup();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Get the actionId this client is configured for */
|
|
32
|
+
getActionId() {
|
|
33
|
+
return this.config.actionId;
|
|
34
|
+
}
|
|
35
|
+
scheduleWarmup() {
|
|
36
|
+
if (typeof window === "undefined") return;
|
|
37
|
+
if (document.readyState === "loading") {
|
|
38
|
+
document.addEventListener("DOMContentLoaded", () => this.tryWarmup(), { once: true });
|
|
39
|
+
} else {
|
|
40
|
+
setTimeout(() => this.tryWarmup(), 100);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
static resolvePulseConfig() {
|
|
44
|
+
const runtimeConfig = typeof window !== "undefined" && typeof window.Deflect === "object" && window.Deflect?.pulseConfig && typeof window.Deflect.pulseConfig === "object" ? window.Deflect.pulseConfig : {};
|
|
45
|
+
return {
|
|
46
|
+
...runtimeConfig,
|
|
47
|
+
environment: runtimeConfig.environment || (typeof window !== "undefined" ? window.location.hostname : void 0)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
reportError(error, tags, context) {
|
|
51
|
+
this.pulseReporter.captureException(error, { tags, context });
|
|
52
|
+
}
|
|
53
|
+
isInErrorCooldown() {
|
|
54
|
+
if (this.consecutiveErrorCount === 0) return false;
|
|
55
|
+
const backoffMs = Math.min(
|
|
56
|
+
ERROR_COOLDOWN_MS * Math.pow(2, this.consecutiveErrorCount - 1),
|
|
57
|
+
3e4
|
|
58
|
+
);
|
|
59
|
+
return Date.now() - this.lastErrorTime < backoffMs;
|
|
60
|
+
}
|
|
61
|
+
isRateLimited() {
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
if (now - this.fetchWindowStart > FETCH_WINDOW_MS) {
|
|
64
|
+
this.fetchCountInWindow = 0;
|
|
65
|
+
this.fetchWindowStart = now;
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return this.fetchCountInWindow >= MAX_FETCHES_PER_WINDOW;
|
|
69
|
+
}
|
|
70
|
+
hasExceededMaxErrors() {
|
|
71
|
+
return this.consecutiveErrorCount >= MAX_CONSECUTIVE_ERRORS;
|
|
72
|
+
}
|
|
73
|
+
recordFetchSuccess() {
|
|
74
|
+
this.consecutiveErrorCount = 0;
|
|
75
|
+
this.lastErrorTime = 0;
|
|
76
|
+
}
|
|
77
|
+
recordFetchError() {
|
|
78
|
+
this.consecutiveErrorCount++;
|
|
79
|
+
this.lastErrorTime = Date.now();
|
|
80
|
+
}
|
|
81
|
+
incrementFetchCount() {
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
if (now - this.fetchWindowStart > FETCH_WINDOW_MS) {
|
|
84
|
+
this.fetchCountInWindow = 0;
|
|
85
|
+
this.fetchWindowStart = now;
|
|
86
|
+
}
|
|
87
|
+
this.fetchCountInWindow++;
|
|
88
|
+
}
|
|
89
|
+
async tryWarmup() {
|
|
90
|
+
if (this.config.prefetch === false || this.isWarmupInProgress || this.scriptCache || this.hasWarmupError) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (this.isRateLimited() || this.isInErrorCooldown() || this.hasExceededMaxErrors()) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
this.isWarmupInProgress = true;
|
|
97
|
+
const fetchPromise = this.fetchScriptOnce();
|
|
98
|
+
this.warmupPromise = (async () => {
|
|
99
|
+
try {
|
|
100
|
+
const script = await fetchPromise;
|
|
101
|
+
this.scriptCache = script;
|
|
102
|
+
this.hasWarmupError = false;
|
|
103
|
+
this.recordFetchSuccess();
|
|
104
|
+
} catch {
|
|
105
|
+
this.hasWarmupError = true;
|
|
106
|
+
this.recordFetchError();
|
|
107
|
+
} finally {
|
|
108
|
+
this.isWarmupInProgress = false;
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
111
|
+
await this.warmupPromise.catch(() => {
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
validateActionId(actionId) {
|
|
115
|
+
const sanitized = actionId.trim();
|
|
116
|
+
const validPattern = /^[a-zA-Z0-9/_-]+$/;
|
|
117
|
+
if (!validPattern.test(sanitized)) {
|
|
118
|
+
throw new Error("Invalid actionId format: contains disallowed characters");
|
|
119
|
+
}
|
|
120
|
+
return encodeURIComponent(sanitized);
|
|
121
|
+
}
|
|
122
|
+
prefetchNextScript() {
|
|
123
|
+
if (!this.hasWarmupError && this.config.prefetch !== false) {
|
|
124
|
+
this.tryWarmup().catch(() => {
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
fetchScriptOnce() {
|
|
129
|
+
if (!this.scriptFetchPromise) {
|
|
130
|
+
this.incrementFetchCount();
|
|
131
|
+
this.scriptFetchPromise = fetchScript(
|
|
132
|
+
this.config.actionId,
|
|
133
|
+
this.config.scriptUrl,
|
|
134
|
+
this.config.extraArgs
|
|
135
|
+
).then((script) => {
|
|
136
|
+
this.scriptFetchPromise = null;
|
|
137
|
+
return script;
|
|
138
|
+
}).catch((error) => {
|
|
139
|
+
this.scriptFetchPromise = null;
|
|
140
|
+
throw error;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return this.scriptFetchPromise;
|
|
144
|
+
}
|
|
145
|
+
isTestMode() {
|
|
146
|
+
if (this.config.actionId === "PULSE_TEST" || this.config.actionId === "SENTRY_TEST") {
|
|
147
|
+
throw new Error("PULSE_TEST: This is a test error to verify Pulse integration is working");
|
|
148
|
+
}
|
|
149
|
+
return this.config.actionId === "t/FFFFFFFFFFFFF/111111111" || this.config.actionId === "t/FFFFFFFFFFFFF/000000000";
|
|
150
|
+
}
|
|
151
|
+
/** Get a challenge token for this client's actionId */
|
|
152
|
+
async getToken() {
|
|
153
|
+
if (this.getTokenPromise) {
|
|
154
|
+
return this.getTokenPromise;
|
|
155
|
+
}
|
|
156
|
+
this.getTokenPromise = this.getTokenInternal();
|
|
157
|
+
try {
|
|
158
|
+
return await this.getTokenPromise;
|
|
159
|
+
} finally {
|
|
160
|
+
this.getTokenPromise = null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async getTokenInternal() {
|
|
164
|
+
try {
|
|
165
|
+
if (this.isTestMode()) {
|
|
166
|
+
return "TESTTOKEN";
|
|
167
|
+
}
|
|
168
|
+
if (this.hasExceededMaxErrors()) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Too many consecutive errors (${this.consecutiveErrorCount}). Create a new client or wait before retrying.`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
if (this.isRateLimited()) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Rate limit exceeded: ${MAX_FETCHES_PER_WINDOW} requests per minute. Please wait before retrying.`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
if (this.isInErrorCooldown()) {
|
|
179
|
+
const backoffMs = Math.min(
|
|
180
|
+
ERROR_COOLDOWN_MS * Math.pow(2, this.consecutiveErrorCount - 1),
|
|
181
|
+
3e4
|
|
182
|
+
);
|
|
183
|
+
const remainingMs = backoffMs - (Date.now() - this.lastErrorTime);
|
|
184
|
+
throw new Error(
|
|
185
|
+
`In error cooldown. Please wait ${Math.ceil(remainingMs / 1e3)} seconds before retrying.`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
let script;
|
|
189
|
+
if (this.warmupPromise) {
|
|
190
|
+
await this.warmupPromise.catch(() => {
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (this.scriptCache && !this.isWarmupInProgress) {
|
|
194
|
+
script = this.scriptCache;
|
|
195
|
+
this.scriptCache = null;
|
|
196
|
+
} else {
|
|
197
|
+
script = await this.fetchScriptOnce();
|
|
198
|
+
}
|
|
199
|
+
return this.executeScript(script);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
const errorMessage = error instanceof Error ? error.message : "";
|
|
202
|
+
const isGuardError = errorMessage.includes("cooldown") || errorMessage.includes("Rate limit") || errorMessage.includes("Too many consecutive");
|
|
203
|
+
if (!isGuardError) {
|
|
204
|
+
this.recordFetchError();
|
|
205
|
+
}
|
|
206
|
+
const isUserError = error && typeof error === "object" && "isUserError" in error && error.isUserError === true;
|
|
207
|
+
this.reportError(
|
|
208
|
+
error,
|
|
209
|
+
{
|
|
210
|
+
deflect_sdk_error: "true",
|
|
211
|
+
deflect_user_error: isUserError ? "true" : "false",
|
|
212
|
+
method: "getToken",
|
|
213
|
+
action_id: this.config.actionId,
|
|
214
|
+
has_cache: this.scriptCache !== null ? "true" : "false",
|
|
215
|
+
has_warmup_error: this.hasWarmupError ? "true" : "false",
|
|
216
|
+
consecutive_errors: this.consecutiveErrorCount.toString(),
|
|
217
|
+
stage: "call"
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
actionId: this.config.actionId,
|
|
221
|
+
hasCache: this.scriptCache !== null,
|
|
222
|
+
hasWarmupError: this.hasWarmupError,
|
|
223
|
+
consecutiveErrorCount: this.consecutiveErrorCount,
|
|
224
|
+
fetchCountInWindow: this.fetchCountInWindow
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async executeScript(script) {
|
|
231
|
+
if (script.sessionId && typeof window !== "undefined") {
|
|
232
|
+
window.Deflect = window.Deflect || {};
|
|
233
|
+
window.Deflect.sessionId = script.sessionId;
|
|
234
|
+
}
|
|
235
|
+
const blobUrl = createBlobUrl(script.content);
|
|
236
|
+
let scriptElement = null;
|
|
237
|
+
try {
|
|
238
|
+
scriptElement = await loadModuleScript(blobUrl);
|
|
239
|
+
await waitForGlobalFunction("getChallengeToken");
|
|
240
|
+
const token = await getTokenFromGlobal("getChallengeToken");
|
|
241
|
+
this.recordFetchSuccess();
|
|
242
|
+
this.prefetchNextScript();
|
|
243
|
+
return token;
|
|
244
|
+
} catch (error) {
|
|
245
|
+
this.reportError(
|
|
246
|
+
error,
|
|
247
|
+
{
|
|
248
|
+
deflect_sdk_error: "true",
|
|
249
|
+
method: "executeScript",
|
|
250
|
+
stage: "load_or_execute",
|
|
251
|
+
action_id: this.config.actionId
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
hasCache: this.scriptCache !== null,
|
|
255
|
+
hasWarmupError: this.hasWarmupError
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
throw error;
|
|
259
|
+
} finally {
|
|
260
|
+
if (scriptElement) {
|
|
261
|
+
cleanup(blobUrl, scriptElement, "getChallengeToken");
|
|
262
|
+
} else {
|
|
263
|
+
URL.revokeObjectURL(blobUrl);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/** Manually trigger warmup/prefetch */
|
|
268
|
+
async warmup() {
|
|
269
|
+
try {
|
|
270
|
+
if (this.config.prefetch === false) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
await this.tryWarmup();
|
|
274
|
+
return this.scriptCache !== null;
|
|
275
|
+
} catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/** Clear the cached script */
|
|
280
|
+
clearCache() {
|
|
281
|
+
this.scriptCache = null;
|
|
282
|
+
}
|
|
283
|
+
/** Reset error state to allow retries */
|
|
284
|
+
resetErrorState() {
|
|
285
|
+
this.hasWarmupError = false;
|
|
286
|
+
this.consecutiveErrorCount = 0;
|
|
287
|
+
this.lastErrorTime = 0;
|
|
288
|
+
this.getTokenPromise = null;
|
|
289
|
+
}
|
|
290
|
+
/** Get current rate limit and error status for debugging */
|
|
291
|
+
getStatus() {
|
|
292
|
+
return {
|
|
293
|
+
actionId: this.config.actionId,
|
|
294
|
+
isRateLimited: this.isRateLimited(),
|
|
295
|
+
isInCooldown: this.isInErrorCooldown(),
|
|
296
|
+
consecutiveErrors: this.consecutiveErrorCount,
|
|
297
|
+
fetchesInWindow: this.fetchCountInWindow,
|
|
298
|
+
canFetch: !this.isRateLimited() && !this.isInErrorCooldown() && !this.hasExceededMaxErrors()
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
/** Inject token into a form and submit */
|
|
302
|
+
async injectToken(event) {
|
|
303
|
+
if (!event || !event.target || !(event.target instanceof HTMLFormElement)) {
|
|
304
|
+
throw new Error("injectToken: must be called from a form submit event");
|
|
305
|
+
}
|
|
306
|
+
event.preventDefault();
|
|
307
|
+
const form = event.target;
|
|
308
|
+
const token = await this.getToken();
|
|
309
|
+
Array.from(form.querySelectorAll('input[name="deflect_token"]')).forEach(
|
|
310
|
+
(el) => el.remove()
|
|
311
|
+
);
|
|
312
|
+
const hidden = document.createElement("input");
|
|
313
|
+
hidden.type = "hidden";
|
|
314
|
+
hidden.name = "deflect_token";
|
|
315
|
+
hidden.value = token;
|
|
316
|
+
form.appendChild(hidden);
|
|
317
|
+
form.submit();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
class Deflect {
|
|
321
|
+
constructor() {
|
|
322
|
+
this.config = null;
|
|
323
|
+
this.scriptCache = null;
|
|
324
|
+
this.isWarmupInProgress = false;
|
|
325
|
+
this.hasWarmupError = false;
|
|
326
|
+
this.warmupPromise = null;
|
|
327
|
+
// Guards against infinite loading
|
|
328
|
+
this.getTokenPromise = null;
|
|
329
|
+
this.lastErrorTime = 0;
|
|
330
|
+
this.consecutiveErrorCount = 0;
|
|
331
|
+
this.fetchCountInWindow = 0;
|
|
332
|
+
this.fetchWindowStart = 0;
|
|
333
|
+
this.initializeGlobalState();
|
|
334
|
+
this.pulseReporter = new PulseReporter(this.resolvePulseConfig());
|
|
335
|
+
this.setupAutomaticWarmup();
|
|
336
|
+
}
|
|
337
|
+
initializeGlobalState() {
|
|
338
|
+
if (typeof window === "undefined") return;
|
|
339
|
+
window.Deflect = window.Deflect || {};
|
|
340
|
+
}
|
|
341
|
+
setupAutomaticWarmup() {
|
|
342
|
+
if (typeof window === "undefined") return;
|
|
343
|
+
if (document.readyState === "loading") {
|
|
344
|
+
document.addEventListener("DOMContentLoaded", () => this.tryWarmup());
|
|
345
|
+
} else {
|
|
346
|
+
setTimeout(() => this.tryWarmup(), 100);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
resolvePulseConfig() {
|
|
350
|
+
const runtimeConfig = typeof window !== "undefined" && typeof window.Deflect === "object" && window.Deflect?.pulseConfig && typeof window.Deflect.pulseConfig === "object" ? window.Deflect.pulseConfig : {};
|
|
351
|
+
return {
|
|
352
|
+
...runtimeConfig,
|
|
353
|
+
environment: runtimeConfig.environment || (typeof window !== "undefined" ? window.location.hostname : void 0)
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
reportError(error, tags, context) {
|
|
357
|
+
this.pulseReporter.captureException(error, { tags, context });
|
|
358
|
+
}
|
|
359
|
+
/** Check if we're in error cooldown period */
|
|
360
|
+
isInErrorCooldown() {
|
|
361
|
+
if (this.consecutiveErrorCount === 0) return false;
|
|
362
|
+
const backoffMs = Math.min(
|
|
363
|
+
ERROR_COOLDOWN_MS * Math.pow(2, this.consecutiveErrorCount - 1),
|
|
364
|
+
3e4
|
|
365
|
+
// Max 30 second backoff
|
|
366
|
+
);
|
|
367
|
+
return Date.now() - this.lastErrorTime < backoffMs;
|
|
368
|
+
}
|
|
369
|
+
/** Check if we've exceeded the rate limit */
|
|
370
|
+
isRateLimited() {
|
|
371
|
+
const now = Date.now();
|
|
372
|
+
if (now - this.fetchWindowStart > FETCH_WINDOW_MS) {
|
|
373
|
+
this.fetchCountInWindow = 0;
|
|
374
|
+
this.fetchWindowStart = now;
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
return this.fetchCountInWindow >= MAX_FETCHES_PER_WINDOW;
|
|
378
|
+
}
|
|
379
|
+
/** Check if we've hit max consecutive errors */
|
|
380
|
+
hasExceededMaxErrors() {
|
|
381
|
+
return this.consecutiveErrorCount >= MAX_CONSECUTIVE_ERRORS;
|
|
382
|
+
}
|
|
383
|
+
/** Record a successful fetch - resets error state */
|
|
384
|
+
recordFetchSuccess() {
|
|
385
|
+
this.consecutiveErrorCount = 0;
|
|
386
|
+
this.lastErrorTime = 0;
|
|
387
|
+
}
|
|
388
|
+
/** Record a failed fetch - increments error tracking */
|
|
389
|
+
recordFetchError() {
|
|
390
|
+
this.consecutiveErrorCount++;
|
|
391
|
+
this.lastErrorTime = Date.now();
|
|
392
|
+
}
|
|
393
|
+
/** Increment fetch counter for rate limiting */
|
|
394
|
+
incrementFetchCount() {
|
|
395
|
+
const now = Date.now();
|
|
396
|
+
if (now - this.fetchWindowStart > FETCH_WINDOW_MS) {
|
|
397
|
+
this.fetchCountInWindow = 0;
|
|
398
|
+
this.fetchWindowStart = now;
|
|
399
|
+
}
|
|
400
|
+
this.fetchCountInWindow++;
|
|
401
|
+
}
|
|
402
|
+
async tryWarmup() {
|
|
403
|
+
if (!this.config?.actionId || this.config.prefetch === false || this.isWarmupInProgress || this.scriptCache || this.hasWarmupError) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (this.isRateLimited() || this.isInErrorCooldown() || this.hasExceededMaxErrors()) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
this.validateActionId(this.config.actionId);
|
|
410
|
+
this.isWarmupInProgress = true;
|
|
411
|
+
this.incrementFetchCount();
|
|
412
|
+
this.warmupPromise = (async () => {
|
|
413
|
+
try {
|
|
414
|
+
this.scriptCache = await fetchScript(
|
|
415
|
+
this.config.actionId,
|
|
416
|
+
this.config.scriptUrl,
|
|
417
|
+
this.config.extraArgs
|
|
418
|
+
);
|
|
419
|
+
this.hasWarmupError = false;
|
|
420
|
+
this.recordFetchSuccess();
|
|
421
|
+
} catch {
|
|
422
|
+
this.hasWarmupError = true;
|
|
423
|
+
this.recordFetchError();
|
|
424
|
+
} finally {
|
|
425
|
+
this.isWarmupInProgress = false;
|
|
426
|
+
}
|
|
427
|
+
})();
|
|
428
|
+
await this.warmupPromise.catch(() => {
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
validateActionId(actionId) {
|
|
432
|
+
const sanitized = actionId.trim();
|
|
433
|
+
const validPattern = /^[a-zA-Z0-9/_-]+$/;
|
|
434
|
+
if (!validPattern.test(sanitized)) {
|
|
435
|
+
throw new Error("Invalid actionId format: contains disallowed characters");
|
|
436
|
+
}
|
|
437
|
+
return encodeURIComponent(sanitized);
|
|
438
|
+
}
|
|
439
|
+
prefetchNextScript() {
|
|
440
|
+
if (!this.hasWarmupError && this.config?.prefetch !== false) {
|
|
441
|
+
this.tryWarmup().catch(() => {
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
isTestMode() {
|
|
446
|
+
if (this.config?.actionId === "PULSE_TEST" || this.config?.actionId === "SENTRY_TEST") {
|
|
447
|
+
throw new Error("PULSE_TEST: This is a test error to verify Pulse integration is working");
|
|
448
|
+
}
|
|
449
|
+
return this.config?.actionId === "t/FFFFFFFFFFFFF/111111111" || this.config?.actionId === "t/FFFFFFFFFFFFF/000000000";
|
|
450
|
+
}
|
|
451
|
+
configure(params) {
|
|
452
|
+
try {
|
|
453
|
+
if (!params.actionId?.trim()) {
|
|
454
|
+
throw new Error("actionId is required and cannot be empty");
|
|
455
|
+
}
|
|
456
|
+
this.validateActionId(params.actionId);
|
|
457
|
+
if (this.config?.actionId === params.actionId && this.config.prefetch === params.prefetch && this.config.scriptUrl === params.scriptUrl) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const actionIdChanged = this.config?.actionId !== params.actionId;
|
|
461
|
+
if (actionIdChanged) {
|
|
462
|
+
this.hasWarmupError = false;
|
|
463
|
+
this.consecutiveErrorCount = 0;
|
|
464
|
+
this.lastErrorTime = 0;
|
|
465
|
+
}
|
|
466
|
+
this.scriptCache = null;
|
|
467
|
+
this.getTokenPromise = null;
|
|
468
|
+
this.config = { prefetch: true, ...params };
|
|
469
|
+
if (typeof window !== "undefined") {
|
|
470
|
+
window.Deflect.actionId = params.actionId;
|
|
471
|
+
}
|
|
472
|
+
if (!this.isTestMode() && this.config.prefetch !== false) {
|
|
473
|
+
this.tryWarmup();
|
|
474
|
+
}
|
|
475
|
+
} catch (error) {
|
|
476
|
+
const isUserError = error && typeof error === "object" && "isUserError" in error && error.isUserError === true;
|
|
477
|
+
this.reportError(
|
|
478
|
+
error,
|
|
479
|
+
{
|
|
480
|
+
deflect_sdk_error: "true",
|
|
481
|
+
deflect_user_error: isUserError ? "true" : "false",
|
|
482
|
+
method: "configure",
|
|
483
|
+
action_id: params?.actionId || "unknown"
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
actionId: params?.actionId,
|
|
487
|
+
hasCache: this.scriptCache !== null,
|
|
488
|
+
hasWarmupError: this.hasWarmupError
|
|
489
|
+
}
|
|
490
|
+
);
|
|
491
|
+
throw error;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
async getToken() {
|
|
495
|
+
if (this.getTokenPromise) {
|
|
496
|
+
return this.getTokenPromise;
|
|
497
|
+
}
|
|
498
|
+
this.getTokenPromise = this.getTokenInternal();
|
|
499
|
+
try {
|
|
500
|
+
return await this.getTokenPromise;
|
|
501
|
+
} finally {
|
|
502
|
+
this.getTokenPromise = null;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async getTokenInternal() {
|
|
506
|
+
try {
|
|
507
|
+
if (!this.config?.actionId) {
|
|
508
|
+
throw new Error("Must call configure() before getToken()");
|
|
509
|
+
}
|
|
510
|
+
this.validateActionId(this.config.actionId);
|
|
511
|
+
if (this.isTestMode()) {
|
|
512
|
+
return "TESTTOKEN";
|
|
513
|
+
}
|
|
514
|
+
if (this.hasExceededMaxErrors()) {
|
|
515
|
+
throw new Error(
|
|
516
|
+
`Too many consecutive errors (${this.consecutiveErrorCount}). Call configure() with a new actionId or wait before retrying.`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
if (this.isRateLimited()) {
|
|
520
|
+
throw new Error(
|
|
521
|
+
`Rate limit exceeded: ${MAX_FETCHES_PER_WINDOW} requests per minute. Please wait before retrying.`
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
if (this.isInErrorCooldown()) {
|
|
525
|
+
const backoffMs = Math.min(
|
|
526
|
+
ERROR_COOLDOWN_MS * Math.pow(2, this.consecutiveErrorCount - 1),
|
|
527
|
+
3e4
|
|
528
|
+
);
|
|
529
|
+
const remainingMs = backoffMs - (Date.now() - this.lastErrorTime);
|
|
530
|
+
throw new Error(
|
|
531
|
+
`In error cooldown. Please wait ${Math.ceil(remainingMs / 1e3)} seconds before retrying.`
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
let script;
|
|
535
|
+
if (this.warmupPromise) {
|
|
536
|
+
await this.warmupPromise.catch(() => {
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
if (this.scriptCache && !this.isWarmupInProgress) {
|
|
540
|
+
script = this.scriptCache;
|
|
541
|
+
this.scriptCache = null;
|
|
542
|
+
} else {
|
|
543
|
+
this.incrementFetchCount();
|
|
544
|
+
script = await fetchScript(
|
|
545
|
+
this.config.actionId,
|
|
546
|
+
this.config.scriptUrl,
|
|
547
|
+
this.config.extraArgs
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
return this.executeScript(script);
|
|
551
|
+
} catch (error) {
|
|
552
|
+
const errorMessage = error instanceof Error ? error.message : "";
|
|
553
|
+
const isGuardError = errorMessage.includes("cooldown") || errorMessage.includes("Rate limit") || errorMessage.includes("Too many consecutive");
|
|
554
|
+
if (!isGuardError) {
|
|
555
|
+
this.recordFetchError();
|
|
556
|
+
}
|
|
557
|
+
const isUserError = error && typeof error === "object" && "isUserError" in error && error.isUserError === true;
|
|
558
|
+
this.reportError(
|
|
559
|
+
error,
|
|
560
|
+
{
|
|
561
|
+
deflect_sdk_error: "true",
|
|
562
|
+
deflect_user_error: isUserError ? "true" : "false",
|
|
563
|
+
method: "getToken",
|
|
564
|
+
action_id: this.config?.actionId || "unknown",
|
|
565
|
+
has_cache: this.scriptCache !== null ? "true" : "false",
|
|
566
|
+
has_warmup_error: this.hasWarmupError ? "true" : "false",
|
|
567
|
+
consecutive_errors: this.consecutiveErrorCount.toString(),
|
|
568
|
+
stage: "call"
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
actionId: this.config?.actionId,
|
|
572
|
+
hasCache: this.scriptCache !== null,
|
|
573
|
+
hasWarmupError: this.hasWarmupError,
|
|
574
|
+
consecutiveErrorCount: this.consecutiveErrorCount,
|
|
575
|
+
fetchCountInWindow: this.fetchCountInWindow
|
|
576
|
+
}
|
|
577
|
+
);
|
|
578
|
+
throw error;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async executeScript(script) {
|
|
582
|
+
if (script.sessionId && typeof window !== "undefined") {
|
|
583
|
+
window.Deflect.sessionId = script.sessionId;
|
|
584
|
+
}
|
|
585
|
+
const blobUrl = createBlobUrl(script.content);
|
|
586
|
+
let scriptElement = null;
|
|
587
|
+
try {
|
|
588
|
+
scriptElement = await loadModuleScript(blobUrl);
|
|
589
|
+
await waitForGlobalFunction("getChallengeToken");
|
|
590
|
+
const token = await getTokenFromGlobal("getChallengeToken");
|
|
591
|
+
this.recordFetchSuccess();
|
|
592
|
+
this.prefetchNextScript();
|
|
593
|
+
return token;
|
|
594
|
+
} catch (error) {
|
|
595
|
+
this.reportError(
|
|
596
|
+
error,
|
|
597
|
+
{
|
|
598
|
+
deflect_sdk_error: "true",
|
|
599
|
+
method: "executeScript",
|
|
600
|
+
stage: "load_or_execute",
|
|
601
|
+
action_id: this.config?.actionId || "unknown"
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
hasCache: this.scriptCache !== null,
|
|
605
|
+
hasWarmupError: this.hasWarmupError
|
|
606
|
+
}
|
|
607
|
+
);
|
|
608
|
+
throw error;
|
|
609
|
+
} finally {
|
|
610
|
+
if (scriptElement) {
|
|
611
|
+
cleanup(blobUrl, scriptElement, "getChallengeToken");
|
|
612
|
+
} else {
|
|
613
|
+
URL.revokeObjectURL(blobUrl);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
async warmup() {
|
|
618
|
+
if (!this.config?.actionId) {
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
if (this.config.prefetch === false) {
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
await this.tryWarmup();
|
|
626
|
+
return this.scriptCache !== null;
|
|
627
|
+
} catch {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
clearCache() {
|
|
632
|
+
this.scriptCache = null;
|
|
633
|
+
}
|
|
634
|
+
/** Reset error state to allow retries. Use after fixing configuration issues. */
|
|
635
|
+
resetErrorState() {
|
|
636
|
+
this.hasWarmupError = false;
|
|
637
|
+
this.consecutiveErrorCount = 0;
|
|
638
|
+
this.lastErrorTime = 0;
|
|
639
|
+
this.getTokenPromise = null;
|
|
640
|
+
}
|
|
641
|
+
/** Get current rate limit and error status for debugging */
|
|
642
|
+
getStatus() {
|
|
643
|
+
return {
|
|
644
|
+
isRateLimited: this.isRateLimited(),
|
|
645
|
+
isInCooldown: this.isInErrorCooldown(),
|
|
646
|
+
consecutiveErrors: this.consecutiveErrorCount,
|
|
647
|
+
fetchesInWindow: this.fetchCountInWindow,
|
|
648
|
+
canFetch: !this.isRateLimited() && !this.isInErrorCooldown() && !this.hasExceededMaxErrors()
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
async injectToken(event) {
|
|
652
|
+
if (!event || !event.target || !(event.target instanceof HTMLFormElement)) {
|
|
653
|
+
throw new Error("injectToken: must be called from a form submit event");
|
|
654
|
+
}
|
|
655
|
+
event.preventDefault();
|
|
656
|
+
const form = event.target;
|
|
657
|
+
const token = await this.getToken();
|
|
658
|
+
Array.from(form.querySelectorAll('input[name="deflect_token"]')).forEach(
|
|
659
|
+
(el) => el.remove()
|
|
660
|
+
);
|
|
661
|
+
const hidden = document.createElement("input");
|
|
662
|
+
hidden.type = "hidden";
|
|
663
|
+
hidden.name = "deflect_token";
|
|
664
|
+
hidden.value = token;
|
|
665
|
+
form.appendChild(hidden);
|
|
666
|
+
form.submit();
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Create a new Deflect client instance with its own configuration.
|
|
670
|
+
* Use this when you need multiple actionIds on the same page.
|
|
671
|
+
*
|
|
672
|
+
* @example
|
|
673
|
+
* const loginClient = Deflect.createClient({ actionId: 'login-form' });
|
|
674
|
+
* const signupClient = Deflect.createClient({ actionId: 'signup-form' });
|
|
675
|
+
*
|
|
676
|
+
* // Each client manages its own state
|
|
677
|
+
* const loginToken = await loginClient.getToken();
|
|
678
|
+
* const signupToken = await signupClient.getToken();
|
|
679
|
+
*/
|
|
680
|
+
createClient(config) {
|
|
681
|
+
return new DeflectClient(config, this.pulseReporter);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
const DeflectInstance = new Deflect();
|
|
685
|
+
if (typeof window !== "undefined") {
|
|
686
|
+
window.Deflect = DeflectInstance;
|
|
687
|
+
}
|
|
688
|
+
var src_default = DeflectInstance;
|
|
689
|
+
export {
|
|
690
|
+
DeflectClient,
|
|
691
|
+
src_default as default
|
|
692
|
+
};
|