@deflectbot/deflect-sdk 1.3.8 → 1.4.0

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 CHANGED
@@ -12,12 +12,19 @@ declare class Deflect {
12
12
  private scriptCache;
13
13
  private isWarmupInProgress;
14
14
  private hasWarmupError;
15
- private static readonly SDK_ERROR_MARKER;
15
+ private pulseReporter;
16
16
  constructor();
17
- private initializeSentry;
18
17
  private initializeGlobalState;
19
18
  private setupAutomaticWarmup;
19
+ private resolvePulseConfig;
20
+ private reportError;
20
21
  private tryWarmup;
22
+ /**
23
+ * @param actionId - The action ID to validate
24
+ * @returns The sanitized action ID
25
+ * @throws Error if actionId contains invalid characters
26
+ */
27
+ private validateActionId;
21
28
  private buildScriptUrl;
22
29
  private fetchScript;
23
30
  private executeScript;
@@ -27,18 +34,80 @@ declare class Deflect {
27
34
  private loadScriptElement;
28
35
  private getTokenFromScript;
29
36
  private cleanup;
37
+ /**
38
+ * Configures the Deflect SDK with the provided parameters.
39
+ * Must be called before using getToken() or other SDK methods.
40
+ *
41
+ * @param params - Configuration options for the SDK
42
+ * @param params.actionId - The unique action identifier from your Deflect dashboard (required)
43
+ * @param params.scriptUrl - Optional custom script URL (defaults to Deflect CDN)
44
+ * @throws Error if actionId is empty or contains invalid characters
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * Deflect.configure({ actionId: "your-action-id" });
49
+ * ```
50
+ */
30
51
  configure(params: DeflectConfig): void;
31
52
  private isTestMode;
53
+ /**
54
+ * @deprecated Use {@link getToken} instead. This method is kept for backward compatibility.
55
+ * @returns A promise that resolves to the Deflect token string
56
+ */
32
57
  solveChallenge(): Promise<string>;
58
+ /**
59
+ * Retrieves a Deflect token for the configured action.
60
+ * The token should be included in requests to your protected endpoints.
61
+ *
62
+ * @returns A promise that resolves to the Deflect token string
63
+ * @throws Error if configure() has not been called first
64
+ * @throws Error if the script fails to load or execute
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * const token = await Deflect.getToken();
69
+ * // Include token in your API request
70
+ * fetch('/api/protected', {
71
+ * headers: { 'X-Deflect-Token': token }
72
+ * });
73
+ * ```
74
+ */
33
75
  getToken(): Promise<string>;
76
+ /**
77
+ * Pre-fetches the challenge script to reduce latency on the first getToken() call.
78
+ * This is automatically called after configure(), but can be manually triggered.
79
+ *
80
+ * @returns A promise that resolves to true if warmup succeeded, false otherwise
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * Deflect.configure({ actionId: "your-action-id" });
85
+ * const warmedUp = await Deflect.warmup();
86
+ * console.log('Script pre-cached:', warmedUp);
87
+ * ```
88
+ */
34
89
  warmup(): Promise<boolean>;
90
+ /**
91
+ * Clears the cached challenge script.
92
+ * Useful when you need to force a fresh script fetch on the next getToken() call.
93
+ */
35
94
  clearCache(): void;
36
95
  /**
37
- * Inject a fresh token as a hidden input into a form. Only accepts a submit event from onsubmit.
38
- * Usage: <form ... onsubmit="return Deflect.injectToken(event)">
39
- * Returns false to prevent double submit.
96
+ * Injects a Deflect token as a hidden input into a form and submits it.
97
+ * Designed for use with form onsubmit handlers.
98
+ *
99
+ * @param event - The form submit event
100
+ * @throws Error if not called from a form submit event
101
+ *
102
+ * @example
103
+ * ```html
104
+ * <form action="/login" method="POST" onsubmit="Deflect.injectToken(event)">
105
+ * <input name="username" />
106
+ * <button type="submit">Login</button>
107
+ * </form>
108
+ * ```
40
109
  */
41
- injectToken(event: SubmitEvent): Promise<false>;
110
+ injectToken(event: SubmitEvent): Promise<void>;
42
111
  }
43
112
  declare const DeflectInstance: Deflect;
44
113
  export default DeflectInstance;
package/dist/index.esm.js CHANGED
@@ -1,58 +1,14 @@
1
- import * as Sentry from "@sentry/browser";
1
+ import PulseReporter from "./pulse";
2
2
  class Deflect {
3
3
  constructor() {
4
4
  this.config = null;
5
5
  this.scriptCache = null;
6
6
  this.isWarmupInProgress = false;
7
7
  this.hasWarmupError = false;
8
- this.initializeSentry();
9
8
  this.initializeGlobalState();
9
+ this.pulseReporter = new PulseReporter(this.resolvePulseConfig());
10
10
  this.setupAutomaticWarmup();
11
11
  }
12
- static {
13
- this.SDK_ERROR_MARKER = "__DEFLECT_SDK_ERROR__";
14
- }
15
- initializeSentry() {
16
- try {
17
- Sentry.init({
18
- dsn: "https://4a62ed75ab362d4694ae4215c2b4f621@o4509968565665792.ingest.de.sentry.io/4510257986469968",
19
- sendDefaultPii: true,
20
- // Enable to capture IP address
21
- environment: typeof window !== "undefined" ? window.location.hostname : "unknown",
22
- beforeSend(event) {
23
- const isSDKError = event.tags?.deflect_sdk_error === "true";
24
- if (!isSDKError) {
25
- return null;
26
- }
27
- if (typeof window !== "undefined" && typeof navigator !== "undefined") {
28
- event.contexts = event.contexts || {};
29
- event.contexts.browser = {
30
- name: navigator.userAgent,
31
- version: navigator.appVersion
32
- };
33
- event.request = event.request || {};
34
- event.request.headers = event.request.headers || {};
35
- event.request.headers["User-Agent"] = navigator.userAgent;
36
- event.contexts.page = {
37
- url: window.location.href,
38
- referrer: document.referrer,
39
- title: document.title
40
- };
41
- event.contexts.device = {
42
- screen_width: window.screen.width,
43
- screen_height: window.screen.height,
44
- viewport_width: window.innerWidth,
45
- viewport_height: window.innerHeight,
46
- language: navigator.language,
47
- platform: navigator.platform
48
- };
49
- }
50
- return event;
51
- }
52
- });
53
- } catch {
54
- }
55
- }
56
12
  initializeGlobalState() {
57
13
  if (typeof window === "undefined") return;
58
14
  window.Deflect = window.Deflect || {};
@@ -65,6 +21,16 @@ class Deflect {
65
21
  setTimeout(() => this.tryWarmup(), 100);
66
22
  }
67
23
  }
24
+ resolvePulseConfig() {
25
+ const runtimeConfig = typeof window !== "undefined" && typeof window.Deflect === "object" && window.Deflect?.pulseConfig && typeof window.Deflect.pulseConfig === "object" ? window.Deflect.pulseConfig : {};
26
+ return {
27
+ ...runtimeConfig,
28
+ environment: runtimeConfig.environment || (typeof window !== "undefined" ? window.location.hostname : void 0)
29
+ };
30
+ }
31
+ reportError(error, tags, context) {
32
+ this.pulseReporter.captureException(error, { tags, context });
33
+ }
68
34
  async tryWarmup() {
69
35
  if (!this.config?.actionId || this.isWarmupInProgress || this.scriptCache || this.hasWarmupError) {
70
36
  return;
@@ -79,55 +45,108 @@ class Deflect {
79
45
  this.isWarmupInProgress = false;
80
46
  }
81
47
  }
48
+ /**
49
+ * @param actionId - The action ID to validate
50
+ * @returns The sanitized action ID
51
+ * @throws Error if actionId contains invalid characters
52
+ */
53
+ validateActionId(actionId) {
54
+ const sanitized = actionId.trim();
55
+ const validPattern = /^[a-zA-Z0-9/_-]+$/;
56
+ if (!validPattern.test(sanitized)) {
57
+ throw new Error("Invalid actionId format: contains disallowed characters");
58
+ }
59
+ return encodeURIComponent(sanitized);
60
+ }
82
61
  buildScriptUrl(actionId) {
83
62
  const baseUrl = this.config?.scriptUrl || "https://js.deflect.bot/main.js";
63
+ const sanitizedActionId = this.validateActionId(actionId);
84
64
  const nonce = Date.now().toString();
85
- return `${baseUrl}?action_id=${actionId}&_=${nonce}`;
65
+ return `${baseUrl}?action_id=${sanitizedActionId}&_=${nonce}`;
86
66
  }
87
67
  async fetchScript() {
88
68
  if (!this.config?.actionId) {
89
69
  throw new Error("actionId is required");
90
70
  }
91
71
  const url = this.buildScriptUrl(this.config.actionId);
92
- const response = await fetch(url, { cache: "no-store" });
93
- if (!response.ok) {
94
- throw new Error(`Failed to fetch script: ${response.status}`);
95
- }
96
- const content = await response.text();
97
72
  try {
98
- const maybeError = JSON.parse(content);
99
- if (maybeError.success === false || maybeError.error) {
100
- const errorMessage = maybeError.error || "Script fetch failed";
101
- const error = new Error(errorMessage);
102
- if (errorMessage === "action_does_not_exist" || errorMessage.includes("invalid action")) {
103
- error.isUserError = true;
104
- }
105
- throw error;
73
+ const response = await fetch(url, {
74
+ cache: "no-store",
75
+ mode: "cors",
76
+ credentials: "omit"
77
+ });
78
+ if (!response.ok) {
79
+ throw new Error(`Failed to fetch script: ${response.status}`);
106
80
  }
107
- } catch (e) {
108
- if (e instanceof SyntaxError) {
109
- } else {
110
- throw e;
81
+ const content = await response.text();
82
+ try {
83
+ const maybeError = JSON.parse(content);
84
+ if (maybeError.success === false || maybeError.error) {
85
+ const errorMessage = maybeError.error || "Script fetch failed";
86
+ const error = new Error(errorMessage);
87
+ if (errorMessage === "action_does_not_exist" || errorMessage.includes("invalid action")) {
88
+ error.isUserError = true;
89
+ }
90
+ throw error;
91
+ }
92
+ } catch (e) {
93
+ if (!(e instanceof SyntaxError)) {
94
+ throw e;
95
+ }
111
96
  }
97
+ return {
98
+ content,
99
+ sessionId: response.headers.get("session_id") || void 0
100
+ };
101
+ } catch (error) {
102
+ this.reportError(
103
+ error,
104
+ {
105
+ deflect_sdk_error: "true",
106
+ method: "fetchScript",
107
+ action_id: this.config.actionId
108
+ },
109
+ {
110
+ actionId: this.config.actionId,
111
+ url
112
+ }
113
+ );
114
+ throw error;
112
115
  }
113
- return {
114
- content,
115
- sessionId: response.headers.get("session_id") || void 0
116
- };
117
116
  }
118
117
  async executeScript(script) {
119
118
  if (script.sessionId && typeof window !== "undefined") {
120
119
  window.Deflect.sessionId = script.sessionId;
121
120
  }
122
121
  const blobUrl = this.createScriptBlob(script.content);
123
- const scriptElement = await this.loadScriptElement(blobUrl);
122
+ let scriptElement = null;
124
123
  try {
124
+ scriptElement = await this.loadScriptElement(blobUrl);
125
125
  await this.waitForGetToken();
126
126
  const token = await this.getTokenFromScript();
127
127
  this.prefetchNextScript();
128
128
  return token;
129
+ } catch (error) {
130
+ this.reportError(
131
+ error,
132
+ {
133
+ deflect_sdk_error: "true",
134
+ method: "executeScript",
135
+ stage: "load_or_execute",
136
+ action_id: this.config?.actionId || "unknown"
137
+ },
138
+ {
139
+ hasCache: this.scriptCache !== null,
140
+ hasWarmupError: this.hasWarmupError
141
+ }
142
+ );
143
+ throw error;
129
144
  } finally {
130
- this.cleanup(blobUrl, scriptElement);
145
+ if (scriptElement) {
146
+ this.cleanup(blobUrl, scriptElement);
147
+ } else {
148
+ URL.revokeObjectURL(blobUrl);
149
+ }
131
150
  }
132
151
  }
133
152
  prefetchNextScript() {
@@ -137,16 +156,16 @@ class Deflect {
137
156
  }
138
157
  }
139
158
  async waitForGetToken() {
140
- return new Promise((resolve) => {
159
+ return new Promise((resolve, reject) => {
141
160
  const checkInterval = setInterval(() => {
142
161
  if (typeof window !== "undefined" && typeof window.Deflect?.getToken === "function") {
143
162
  clearInterval(checkInterval);
144
163
  resolve();
145
164
  }
146
- }, 10);
165
+ }, 50);
147
166
  setTimeout(() => {
148
167
  clearInterval(checkInterval);
149
- resolve();
168
+ reject(new Error("Timeout: getToken function did not become available within 10 seconds"));
150
169
  }, 1e4);
151
170
  });
152
171
  }
@@ -181,6 +200,20 @@ class Deflect {
181
200
  delete window.Deflect.getToken;
182
201
  }
183
202
  }
203
+ /**
204
+ * Configures the Deflect SDK with the provided parameters.
205
+ * Must be called before using getToken() or other SDK methods.
206
+ *
207
+ * @param params - Configuration options for the SDK
208
+ * @param params.actionId - The unique action identifier from your Deflect dashboard (required)
209
+ * @param params.scriptUrl - Optional custom script URL (defaults to Deflect CDN)
210
+ * @throws Error if actionId is empty or contains invalid characters
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * Deflect.configure({ actionId: "your-action-id" });
215
+ * ```
216
+ */
184
217
  configure(params) {
185
218
  try {
186
219
  if (!params.actionId?.trim()) {
@@ -200,25 +233,53 @@ class Deflect {
200
233
  }
201
234
  } catch (error) {
202
235
  const isUserError = error && typeof error === "object" && "isUserError" in error && error.isUserError === true;
203
- if (!isUserError) {
204
- Sentry.captureException(error, {
205
- tags: {
206
- deflect_sdk_error: "true",
207
- method: "configure",
208
- actionId: params?.actionId || "unknown"
209
- }
210
- });
211
- }
236
+ this.reportError(
237
+ error,
238
+ {
239
+ deflect_sdk_error: "true",
240
+ deflect_user_error: isUserError ? "true" : "false",
241
+ method: "configure",
242
+ action_id: params?.actionId || "unknown"
243
+ },
244
+ {
245
+ actionId: params?.actionId,
246
+ hasCache: this.scriptCache !== null,
247
+ hasWarmupError: this.hasWarmupError
248
+ }
249
+ );
212
250
  throw error;
213
251
  }
214
252
  }
215
253
  isTestMode() {
254
+ if (this.config?.actionId === "PULSE_TEST" || this.config?.actionId === "SENTRY_TEST") {
255
+ throw new Error("PULSE_TEST: This is a test error to verify Pulse integration is working");
256
+ }
216
257
  return this.config?.actionId === "t/FFFFFFFFFFFFF/111111111" || this.config?.actionId === "t/FFFFFFFFFFFFF/000000000";
217
258
  }
218
- // Deprecated name kept for backward compatibility
259
+ /**
260
+ * @deprecated Use {@link getToken} instead. This method is kept for backward compatibility.
261
+ * @returns A promise that resolves to the Deflect token string
262
+ */
219
263
  async solveChallenge() {
220
264
  return this.getToken();
221
265
  }
266
+ /**
267
+ * Retrieves a Deflect token for the configured action.
268
+ * The token should be included in requests to your protected endpoints.
269
+ *
270
+ * @returns A promise that resolves to the Deflect token string
271
+ * @throws Error if configure() has not been called first
272
+ * @throws Error if the script fails to load or execute
273
+ *
274
+ * @example
275
+ * ```typescript
276
+ * const token = await Deflect.getToken();
277
+ * // Include token in your API request
278
+ * fetch('/api/protected', {
279
+ * headers: { 'X-Deflect-Token': token }
280
+ * });
281
+ * ```
282
+ */
222
283
  async getToken() {
223
284
  try {
224
285
  if (!this.config?.actionId) {
@@ -227,9 +288,6 @@ class Deflect {
227
288
  if (this.isTestMode()) {
228
289
  return "TESTTOKEN";
229
290
  }
230
- if (this.config.actionId === "SENTRY_TEST") {
231
- throw new Error("SENTRY_TEST: This is a test error to verify Sentry integration is working");
232
- }
233
291
  let script;
234
292
  if (this.scriptCache && !this.isWarmupInProgress) {
235
293
  script = this.scriptCache;
@@ -240,20 +298,39 @@ class Deflect {
240
298
  return this.executeScript(script);
241
299
  } catch (error) {
242
300
  const isUserError = error && typeof error === "object" && "isUserError" in error && error.isUserError === true;
243
- if (!isUserError) {
244
- Sentry.captureException(error, {
245
- tags: {
246
- deflect_sdk_error: "true",
247
- method: "getToken",
248
- actionId: this.config?.actionId || "unknown",
249
- hasCache: this.scriptCache !== null,
250
- hasWarmupError: this.hasWarmupError
251
- }
252
- });
253
- }
301
+ this.reportError(
302
+ error,
303
+ {
304
+ deflect_sdk_error: "true",
305
+ deflect_user_error: isUserError ? "true" : "false",
306
+ method: "getToken",
307
+ action_id: this.config?.actionId || "unknown",
308
+ has_cache: this.scriptCache !== null ? "true" : "false",
309
+ has_warmup_error: this.hasWarmupError ? "true" : "false",
310
+ stage: "call"
311
+ },
312
+ {
313
+ actionId: this.config?.actionId,
314
+ hasCache: this.scriptCache !== null,
315
+ hasWarmupError: this.hasWarmupError
316
+ }
317
+ );
254
318
  throw error;
255
319
  }
256
320
  }
321
+ /**
322
+ * Pre-fetches the challenge script to reduce latency on the first getToken() call.
323
+ * This is automatically called after configure(), but can be manually triggered.
324
+ *
325
+ * @returns A promise that resolves to true if warmup succeeded, false otherwise
326
+ *
327
+ * @example
328
+ * ```typescript
329
+ * Deflect.configure({ actionId: "your-action-id" });
330
+ * const warmedUp = await Deflect.warmup();
331
+ * console.log('Script pre-cached:', warmedUp);
332
+ * ```
333
+ */
257
334
  async warmup() {
258
335
  if (!this.config?.actionId) {
259
336
  return false;
@@ -265,13 +342,27 @@ class Deflect {
265
342
  return false;
266
343
  }
267
344
  }
345
+ /**
346
+ * Clears the cached challenge script.
347
+ * Useful when you need to force a fresh script fetch on the next getToken() call.
348
+ */
268
349
  clearCache() {
269
350
  this.scriptCache = null;
270
351
  }
271
352
  /**
272
- * Inject a fresh token as a hidden input into a form. Only accepts a submit event from onsubmit.
273
- * Usage: <form ... onsubmit="return Deflect.injectToken(event)">
274
- * Returns false to prevent double submit.
353
+ * Injects a Deflect token as a hidden input into a form and submits it.
354
+ * Designed for use with form onsubmit handlers.
355
+ *
356
+ * @param event - The form submit event
357
+ * @throws Error if not called from a form submit event
358
+ *
359
+ * @example
360
+ * ```html
361
+ * <form action="/login" method="POST" onsubmit="Deflect.injectToken(event)">
362
+ * <input name="username" />
363
+ * <button type="submit">Login</button>
364
+ * </form>
365
+ * ```
275
366
  */
276
367
  async injectToken(event) {
277
368
  if (!event || !event.target || !(event.target instanceof HTMLFormElement)) {
@@ -289,7 +380,6 @@ class Deflect {
289
380
  hidden.value = token;
290
381
  form.appendChild(hidden);
291
382
  form.submit();
292
- return false;
293
383
  }
294
384
  }
295
385
  const DeflectInstance = new Deflect();