@deflectbot/deflect-sdk 1.3.8 → 1.4.1-beta.1

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,33 +12,22 @@ 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;
21
- private buildScriptUrl;
22
- private fetchScript;
23
- private executeScript;
22
+ private validateActionId;
24
23
  private prefetchNextScript;
25
- private waitForGetToken;
26
- private createScriptBlob;
27
- private loadScriptElement;
28
- private getTokenFromScript;
29
- private cleanup;
30
- configure(params: DeflectConfig): void;
31
24
  private isTestMode;
32
- solveChallenge(): Promise<string>;
25
+ configure(params: DeflectConfig): void;
33
26
  getToken(): Promise<string>;
27
+ private executeScript;
34
28
  warmup(): Promise<boolean>;
35
29
  clearCache(): void;
36
- /**
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.
40
- */
41
- injectToken(event: SubmitEvent): Promise<false>;
30
+ injectToken(event: SubmitEvent): Promise<void>;
42
31
  }
43
32
  declare const DeflectInstance: Deflect;
44
33
  export default DeflectInstance;
package/dist/index.esm.js CHANGED
@@ -1,58 +1,16 @@
1
- import * as Sentry from "@sentry/browser";
1
+ import PulseReporter from "./pulse";
2
+ import { fetchScript } from "./scriptClient";
3
+ import { createBlobUrl, loadModuleScript, waitForGlobalFunction, getTokenFromGlobal, cleanup } from "./scriptRunner";
2
4
  class Deflect {
3
5
  constructor() {
4
6
  this.config = null;
5
7
  this.scriptCache = null;
6
8
  this.isWarmupInProgress = false;
7
9
  this.hasWarmupError = false;
8
- this.initializeSentry();
9
10
  this.initializeGlobalState();
11
+ this.pulseReporter = new PulseReporter(this.resolvePulseConfig());
10
12
  this.setupAutomaticWarmup();
11
13
  }
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
14
  initializeGlobalState() {
57
15
  if (typeof window === "undefined") return;
58
16
  window.Deflect = window.Deflect || {};
@@ -65,13 +23,24 @@ class Deflect {
65
23
  setTimeout(() => this.tryWarmup(), 100);
66
24
  }
67
25
  }
26
+ resolvePulseConfig() {
27
+ const runtimeConfig = typeof window !== "undefined" && typeof window.Deflect === "object" && window.Deflect?.pulseConfig && typeof window.Deflect.pulseConfig === "object" ? window.Deflect.pulseConfig : {};
28
+ return {
29
+ ...runtimeConfig,
30
+ environment: runtimeConfig.environment || (typeof window !== "undefined" ? window.location.hostname : void 0)
31
+ };
32
+ }
33
+ reportError(error, tags, context) {
34
+ this.pulseReporter.captureException(error, { tags, context });
35
+ }
68
36
  async tryWarmup() {
69
37
  if (!this.config?.actionId || this.isWarmupInProgress || this.scriptCache || this.hasWarmupError) {
70
38
  return;
71
39
  }
40
+ this.validateActionId(this.config.actionId);
72
41
  this.isWarmupInProgress = true;
73
42
  try {
74
- this.scriptCache = await this.fetchScript();
43
+ this.scriptCache = await fetchScript(this.config.actionId, this.config.scriptUrl);
75
44
  this.hasWarmupError = false;
76
45
  } catch {
77
46
  this.hasWarmupError = true;
@@ -79,56 +48,13 @@ class Deflect {
79
48
  this.isWarmupInProgress = false;
80
49
  }
81
50
  }
82
- buildScriptUrl(actionId) {
83
- const baseUrl = this.config?.scriptUrl || "https://js.deflect.bot/main.js";
84
- const nonce = Date.now().toString();
85
- return `${baseUrl}?action_id=${actionId}&_=${nonce}`;
86
- }
87
- async fetchScript() {
88
- if (!this.config?.actionId) {
89
- throw new Error("actionId is required");
90
- }
91
- 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
- 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;
106
- }
107
- } catch (e) {
108
- if (e instanceof SyntaxError) {
109
- } else {
110
- throw e;
111
- }
112
- }
113
- return {
114
- content,
115
- sessionId: response.headers.get("session_id") || void 0
116
- };
117
- }
118
- async executeScript(script) {
119
- if (script.sessionId && typeof window !== "undefined") {
120
- window.Deflect.sessionId = script.sessionId;
121
- }
122
- const blobUrl = this.createScriptBlob(script.content);
123
- const scriptElement = await this.loadScriptElement(blobUrl);
124
- try {
125
- await this.waitForGetToken();
126
- const token = await this.getTokenFromScript();
127
- this.prefetchNextScript();
128
- return token;
129
- } finally {
130
- this.cleanup(blobUrl, scriptElement);
51
+ validateActionId(actionId) {
52
+ const sanitized = actionId.trim();
53
+ const validPattern = /^[a-zA-Z0-9/_-]+$/;
54
+ if (!validPattern.test(sanitized)) {
55
+ throw new Error("Invalid actionId format: contains disallowed characters");
131
56
  }
57
+ return encodeURIComponent(sanitized);
132
58
  }
133
59
  prefetchNextScript() {
134
60
  if (!this.hasWarmupError) {
@@ -136,56 +62,18 @@ class Deflect {
136
62
  });
137
63
  }
138
64
  }
139
- async waitForGetToken() {
140
- return new Promise((resolve) => {
141
- const checkInterval = setInterval(() => {
142
- if (typeof window !== "undefined" && typeof window.Deflect?.getToken === "function") {
143
- clearInterval(checkInterval);
144
- resolve();
145
- }
146
- }, 10);
147
- setTimeout(() => {
148
- clearInterval(checkInterval);
149
- resolve();
150
- }, 1e4);
151
- });
152
- }
153
- createScriptBlob(content) {
154
- const blob = new Blob([content], { type: "text/javascript" });
155
- return URL.createObjectURL(blob);
156
- }
157
- async loadScriptElement(blobUrl) {
158
- return new Promise((resolve, reject) => {
159
- const script = document.createElement("script");
160
- script.type = "module";
161
- script.src = blobUrl;
162
- script.onload = () => resolve(script);
163
- script.onerror = () => reject(new Error("Script failed to load"));
164
- document.head.appendChild(script);
165
- });
166
- }
167
- async getTokenFromScript() {
168
- if (typeof window === "undefined" || typeof window.Deflect?.getToken !== "function") {
169
- throw new Error("Script did not load properly - getToken not available");
170
- }
171
- try {
172
- return await window.Deflect.getToken();
173
- } catch (error) {
174
- throw new Error(`Script execution failed: ${error}`);
175
- }
176
- }
177
- cleanup(blobUrl, scriptElement) {
178
- URL.revokeObjectURL(blobUrl);
179
- scriptElement.remove();
180
- if (typeof window !== "undefined" && window.Deflect?.getToken) {
181
- delete window.Deflect.getToken;
65
+ isTestMode() {
66
+ if (this.config?.actionId === "PULSE_TEST" || this.config?.actionId === "SENTRY_TEST") {
67
+ throw new Error("PULSE_TEST: This is a test error to verify Pulse integration is working");
182
68
  }
69
+ return this.config?.actionId === "t/FFFFFFFFFFFFF/111111111" || this.config?.actionId === "t/FFFFFFFFFFFFF/000000000";
183
70
  }
184
71
  configure(params) {
185
72
  try {
186
73
  if (!params.actionId?.trim()) {
187
74
  throw new Error("actionId is required and cannot be empty");
188
75
  }
76
+ this.validateActionId(params.actionId);
189
77
  if (this.config?.actionId === params.actionId) {
190
78
  return;
191
79
  }
@@ -200,58 +88,95 @@ class Deflect {
200
88
  }
201
89
  } catch (error) {
202
90
  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
- }
91
+ this.reportError(
92
+ error,
93
+ {
94
+ deflect_sdk_error: "true",
95
+ deflect_user_error: isUserError ? "true" : "false",
96
+ method: "configure",
97
+ action_id: params?.actionId || "unknown"
98
+ },
99
+ {
100
+ actionId: params?.actionId,
101
+ hasCache: this.scriptCache !== null,
102
+ hasWarmupError: this.hasWarmupError
103
+ }
104
+ );
212
105
  throw error;
213
106
  }
214
107
  }
215
- isTestMode() {
216
- return this.config?.actionId === "t/FFFFFFFFFFFFF/111111111" || this.config?.actionId === "t/FFFFFFFFFFFFF/000000000";
217
- }
218
- // Deprecated name kept for backward compatibility
219
- async solveChallenge() {
220
- return this.getToken();
221
- }
222
108
  async getToken() {
223
109
  try {
224
110
  if (!this.config?.actionId) {
225
- throw new Error("Must call configure() before solveChallenge()");
111
+ throw new Error("Must call configure() before getToken()");
226
112
  }
113
+ this.validateActionId(this.config.actionId);
227
114
  if (this.isTestMode()) {
228
115
  return "TESTTOKEN";
229
116
  }
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
117
  let script;
234
118
  if (this.scriptCache && !this.isWarmupInProgress) {
235
119
  script = this.scriptCache;
236
120
  this.scriptCache = null;
237
121
  } else {
238
- script = await this.fetchScript();
122
+ script = await fetchScript(this.config.actionId, this.config.scriptUrl);
239
123
  }
240
124
  return this.executeScript(script);
241
125
  } catch (error) {
242
126
  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
- }
127
+ this.reportError(
128
+ error,
129
+ {
130
+ deflect_sdk_error: "true",
131
+ deflect_user_error: isUserError ? "true" : "false",
132
+ method: "getToken",
133
+ action_id: this.config?.actionId || "unknown",
134
+ has_cache: this.scriptCache !== null ? "true" : "false",
135
+ has_warmup_error: this.hasWarmupError ? "true" : "false",
136
+ stage: "call"
137
+ },
138
+ {
139
+ actionId: this.config?.actionId,
140
+ hasCache: this.scriptCache !== null,
141
+ hasWarmupError: this.hasWarmupError
142
+ }
143
+ );
144
+ throw error;
145
+ }
146
+ }
147
+ async executeScript(script) {
148
+ if (script.sessionId && typeof window !== "undefined") {
149
+ window.Deflect.sessionId = script.sessionId;
150
+ }
151
+ const blobUrl = createBlobUrl(script.content);
152
+ let scriptElement = null;
153
+ try {
154
+ scriptElement = await loadModuleScript(blobUrl);
155
+ await waitForGlobalFunction("getChallengeToken");
156
+ const token = await getTokenFromGlobal("getChallengeToken");
157
+ this.prefetchNextScript();
158
+ return token;
159
+ } catch (error) {
160
+ this.reportError(
161
+ error,
162
+ {
163
+ deflect_sdk_error: "true",
164
+ method: "executeScript",
165
+ stage: "load_or_execute",
166
+ action_id: this.config?.actionId || "unknown"
167
+ },
168
+ {
169
+ hasCache: this.scriptCache !== null,
170
+ hasWarmupError: this.hasWarmupError
171
+ }
172
+ );
254
173
  throw error;
174
+ } finally {
175
+ if (scriptElement) {
176
+ cleanup(blobUrl, scriptElement, "getChallengeToken");
177
+ } else {
178
+ URL.revokeObjectURL(blobUrl);
179
+ }
255
180
  }
256
181
  }
257
182
  async warmup() {
@@ -268,11 +193,6 @@ class Deflect {
268
193
  clearCache() {
269
194
  this.scriptCache = null;
270
195
  }
271
- /**
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.
275
- */
276
196
  async injectToken(event) {
277
197
  if (!event || !event.target || !(event.target instanceof HTMLFormElement)) {
278
198
  throw new Error("injectToken: must be called from a form submit event");
@@ -289,7 +209,6 @@ class Deflect {
289
209
  hidden.value = token;
290
210
  form.appendChild(hidden);
291
211
  form.submit();
292
- return false;
293
212
  }
294
213
  }
295
214
  const DeflectInstance = new Deflect();