@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.
@@ -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
+ };