@deflectbot/deflect-sdk 1.4.1 → 1.4.2

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
@@ -1,6 +1,10 @@
1
1
  interface DeflectConfig {
2
+ /** The action ID to fetch the script for. */
2
3
  actionId: string;
4
+ /** Override URL for the script. For staging/enterprise customers */
3
5
  scriptUrl?: string;
6
+ /** Enable/disable automatic warmup/prefetch. Defaults to true. */
7
+ prefetch?: boolean;
4
8
  }
5
9
  declare global {
6
10
  interface Window {
@@ -12,6 +16,7 @@ declare class Deflect {
12
16
  private scriptCache;
13
17
  private isWarmupInProgress;
14
18
  private hasWarmupError;
19
+ private warmupPromise;
15
20
  private pulseReporter;
16
21
  constructor();
17
22
  private initializeGlobalState;
@@ -19,95 +24,15 @@ declare class Deflect {
19
24
  private resolvePulseConfig;
20
25
  private reportError;
21
26
  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
27
  private validateActionId;
28
- private buildScriptUrl;
29
- private fetchScript;
30
- private executeScript;
31
28
  private prefetchNextScript;
32
- private waitForGetToken;
33
- private createScriptBlob;
34
- private loadScriptElement;
35
- private getTokenFromScript;
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
- */
51
- configure(params: DeflectConfig): void;
52
29
  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
- */
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
- */
30
+ configure(params: DeflectConfig): void;
75
31
  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
- */
32
+ private executeScript;
89
33
  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
- */
94
34
  clearCache(): void;
95
- /**
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
- * ```
109
- */
110
35
  injectToken(event: SubmitEvent): Promise<void>;
111
36
  }
112
- declare const _default: Deflect;
113
- export default _default;
37
+ declare const DeflectInstance: Deflect;
38
+ export default DeflectInstance;
package/dist/index.esm.js CHANGED
@@ -1,10 +1,13 @@
1
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;
10
+ this.warmupPromise = null;
8
11
  this.initializeGlobalState();
9
12
  this.pulseReporter = new PulseReporter(this.resolvePulseConfig());
10
13
  this.setupAutomaticWarmup();
@@ -32,24 +35,24 @@ class Deflect {
32
35
  this.pulseReporter.captureException(error, { tags, context });
33
36
  }
34
37
  async tryWarmup() {
35
- if (!this.config?.actionId || this.isWarmupInProgress || this.scriptCache || this.hasWarmupError) {
38
+ if (!this.config?.actionId || this.config.prefetch === false || this.isWarmupInProgress || this.scriptCache || this.hasWarmupError) {
36
39
  return;
37
40
  }
41
+ this.validateActionId(this.config.actionId);
38
42
  this.isWarmupInProgress = true;
39
- try {
40
- this.scriptCache = await this.fetchScript();
41
- this.hasWarmupError = false;
42
- } catch {
43
- this.hasWarmupError = true;
44
- } finally {
45
- this.isWarmupInProgress = false;
46
- }
43
+ this.warmupPromise = (async () => {
44
+ try {
45
+ this.scriptCache = await fetchScript(this.config.actionId, this.config.scriptUrl);
46
+ this.hasWarmupError = false;
47
+ } catch {
48
+ this.hasWarmupError = true;
49
+ } finally {
50
+ this.isWarmupInProgress = false;
51
+ }
52
+ })();
53
+ await this.warmupPromise.catch(() => {
54
+ });
47
55
  }
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
56
  validateActionId(actionId) {
54
57
  const sanitized = actionId.trim();
55
58
  const validPattern = /^[a-zA-Z0-9/_-]+$/;
@@ -58,177 +61,34 @@ class Deflect {
58
61
  }
59
62
  return encodeURIComponent(sanitized);
60
63
  }
61
- buildScriptUrl(actionId) {
62
- const baseUrl = this.config?.scriptUrl || "https://js.deflect.bot/main.js";
63
- const sanitizedActionId = this.validateActionId(actionId);
64
- const nonce = Date.now().toString();
65
- return `${baseUrl}?action_id=${sanitizedActionId}&_=${nonce}`;
66
- }
67
- async fetchScript() {
68
- if (!this.config?.actionId) {
69
- throw new Error("actionId is required");
70
- }
71
- const url = this.buildScriptUrl(this.config.actionId);
72
- try {
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}`);
80
- }
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
- }
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;
115
- }
116
- }
117
- async executeScript(script) {
118
- if (script.sessionId && typeof window !== "undefined") {
119
- window.Deflect.sessionId = script.sessionId;
120
- }
121
- const blobUrl = this.createScriptBlob(script.content);
122
- let scriptElement = null;
123
- try {
124
- scriptElement = await this.loadScriptElement(blobUrl);
125
- await this.waitForGetToken();
126
- const token = await this.getTokenFromScript();
127
- this.prefetchNextScript();
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;
144
- } finally {
145
- if (scriptElement) {
146
- this.cleanup(blobUrl, scriptElement);
147
- } else {
148
- URL.revokeObjectURL(blobUrl);
149
- }
150
- }
151
- }
152
64
  prefetchNextScript() {
153
- if (!this.hasWarmupError) {
65
+ if (!this.hasWarmupError && this.config?.prefetch !== false) {
154
66
  this.tryWarmup().catch(() => {
155
67
  });
156
68
  }
157
69
  }
158
- async waitForGetToken() {
159
- return new Promise((resolve, reject) => {
160
- const checkInterval = setInterval(() => {
161
- if (typeof window !== "undefined" && typeof window.Deflect?.getToken === "function") {
162
- clearInterval(checkInterval);
163
- resolve();
164
- }
165
- }, 50);
166
- setTimeout(() => {
167
- clearInterval(checkInterval);
168
- reject(new Error("Timeout: getToken function did not become available within 10 seconds"));
169
- }, 1e4);
170
- });
171
- }
172
- createScriptBlob(content) {
173
- const blob = new Blob([content], { type: "text/javascript" });
174
- return URL.createObjectURL(blob);
175
- }
176
- async loadScriptElement(blobUrl) {
177
- return new Promise((resolve, reject) => {
178
- const script = document.createElement("script");
179
- script.type = "module";
180
- script.src = blobUrl;
181
- script.onload = () => resolve(script);
182
- script.onerror = () => reject(new Error("Script failed to load"));
183
- document.head.appendChild(script);
184
- });
185
- }
186
- async getTokenFromScript() {
187
- if (typeof window === "undefined" || typeof window.Deflect?.getToken !== "function") {
188
- throw new Error("Script did not load properly - getToken not available");
189
- }
190
- try {
191
- return await window.Deflect.getToken();
192
- } catch (error) {
193
- throw new Error(`Script execution failed: ${error}`);
194
- }
195
- }
196
- cleanup(blobUrl, scriptElement) {
197
- URL.revokeObjectURL(blobUrl);
198
- scriptElement.remove();
199
- if (typeof window !== "undefined" && window.Deflect?.getToken) {
200
- delete window.Deflect.getToken;
70
+ isTestMode() {
71
+ if (this.config?.actionId === "PULSE_TEST" || this.config?.actionId === "SENTRY_TEST") {
72
+ throw new Error("PULSE_TEST: This is a test error to verify Pulse integration is working");
201
73
  }
74
+ return this.config?.actionId === "t/FFFFFFFFFFFFF/111111111" || this.config?.actionId === "t/FFFFFFFFFFFFF/000000000";
202
75
  }
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
- */
217
76
  configure(params) {
218
77
  try {
219
78
  if (!params.actionId?.trim()) {
220
79
  throw new Error("actionId is required and cannot be empty");
221
80
  }
222
- if (this.config?.actionId === params.actionId) {
81
+ this.validateActionId(params.actionId);
82
+ if (this.config?.actionId === params.actionId && this.config.prefetch === params.prefetch) {
223
83
  return;
224
84
  }
225
85
  this.hasWarmupError = false;
226
86
  this.scriptCache = null;
227
- this.config = { ...params };
87
+ this.config = { prefetch: true, ...params };
228
88
  if (typeof window !== "undefined") {
229
89
  window.Deflect.actionId = params.actionId;
230
90
  }
231
- if (!this.isTestMode()) {
91
+ if (!this.isTestMode() && this.config.prefetch !== false) {
232
92
  this.tryWarmup();
233
93
  }
234
94
  } catch (error) {
@@ -250,50 +110,25 @@ class Deflect {
250
110
  throw error;
251
111
  }
252
112
  }
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
- }
257
- return this.config?.actionId === "t/FFFFFFFFFFFFF/111111111" || this.config?.actionId === "t/FFFFFFFFFFFFF/000000000";
258
- }
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
- */
263
- async solveChallenge() {
264
- return this.getToken();
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
- */
283
113
  async getToken() {
284
114
  try {
285
115
  if (!this.config?.actionId) {
286
- throw new Error("Must call configure() before solveChallenge()");
116
+ throw new Error("Must call configure() before getToken()");
287
117
  }
118
+ this.validateActionId(this.config.actionId);
288
119
  if (this.isTestMode()) {
289
120
  return "TESTTOKEN";
290
121
  }
291
122
  let script;
123
+ if (this.warmupPromise) {
124
+ await this.warmupPromise.catch(() => {
125
+ });
126
+ }
292
127
  if (this.scriptCache && !this.isWarmupInProgress) {
293
128
  script = this.scriptCache;
294
129
  this.scriptCache = null;
295
130
  } else {
296
- script = await this.fetchScript();
131
+ script = await fetchScript(this.config.actionId, this.config.scriptUrl);
297
132
  }
298
133
  return this.executeScript(script);
299
134
  } catch (error) {
@@ -318,52 +153,58 @@ class Deflect {
318
153
  throw error;
319
154
  }
320
155
  }
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
- */
156
+ async executeScript(script) {
157
+ if (script.sessionId && typeof window !== "undefined") {
158
+ window.Deflect.sessionId = script.sessionId;
159
+ }
160
+ const blobUrl = createBlobUrl(script.content);
161
+ let scriptElement = null;
162
+ try {
163
+ scriptElement = await loadModuleScript(blobUrl);
164
+ await waitForGlobalFunction("getChallengeToken");
165
+ const token = await getTokenFromGlobal("getChallengeToken");
166
+ this.prefetchNextScript();
167
+ return token;
168
+ } catch (error) {
169
+ this.reportError(
170
+ error,
171
+ {
172
+ deflect_sdk_error: "true",
173
+ method: "executeScript",
174
+ stage: "load_or_execute",
175
+ action_id: this.config?.actionId || "unknown"
176
+ },
177
+ {
178
+ hasCache: this.scriptCache !== null,
179
+ hasWarmupError: this.hasWarmupError
180
+ }
181
+ );
182
+ throw error;
183
+ } finally {
184
+ if (scriptElement) {
185
+ cleanup(blobUrl, scriptElement, "getChallengeToken");
186
+ } else {
187
+ URL.revokeObjectURL(blobUrl);
188
+ }
189
+ }
190
+ }
334
191
  async warmup() {
335
192
  if (!this.config?.actionId) {
336
193
  return false;
337
194
  }
338
195
  try {
196
+ if (this.config.prefetch === false) {
197
+ return false;
198
+ }
339
199
  await this.tryWarmup();
340
200
  return this.scriptCache !== null;
341
201
  } catch {
342
202
  return false;
343
203
  }
344
204
  }
345
- /**
346
- * Clears the cached challenge script.
347
- * Useful when you need to force a fresh script fetch on the next getToken() call.
348
- */
349
205
  clearCache() {
350
206
  this.scriptCache = null;
351
207
  }
352
- /**
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
- * ```
366
- */
367
208
  async injectToken(event) {
368
209
  if (!event || !event.target || !(event.target instanceof HTMLFormElement)) {
369
210
  throw new Error("injectToken: must be called from a form submit event");
@@ -382,7 +223,11 @@ class Deflect {
382
223
  form.submit();
383
224
  }
384
225
  }
385
- var src_default = new Deflect();
226
+ const DeflectInstance = new Deflect();
227
+ if (typeof window !== "undefined") {
228
+ window.Deflect = DeflectInstance;
229
+ }
230
+ var src_default = DeflectInstance;
386
231
  export {
387
232
  src_default as default
388
233
  };
package/dist/index.js CHANGED
@@ -142,6 +142,97 @@
142
142
  };
143
143
  var pulse_default = PulseReporter;
144
144
 
145
+ // src/scriptClient.ts
146
+ function buildScriptUrl(actionId, scriptUrl) {
147
+ const baseUrl = scriptUrl || "https://js.deflect.bot/main.js";
148
+ const nonce = Date.now().toString();
149
+ return `${baseUrl}?action_id=${encodeURIComponent(actionId)}&_=${nonce}`;
150
+ }
151
+ __name(buildScriptUrl, "buildScriptUrl");
152
+ async function fetchScript(actionId, scriptUrl) {
153
+ const url = buildScriptUrl(actionId, scriptUrl);
154
+ const response = await fetch(url, {
155
+ cache: "no-store",
156
+ mode: "cors",
157
+ credentials: "omit"
158
+ });
159
+ const content = await response.text();
160
+ const error = parseErrorJson(content);
161
+ if (error) throw error;
162
+ return {
163
+ content,
164
+ sessionId: response.headers.get("session_id") || void 0
165
+ };
166
+ }
167
+ __name(fetchScript, "fetchScript");
168
+ function parseErrorJson(content) {
169
+ try {
170
+ const maybeError = JSON.parse(content);
171
+ if (maybeError.success === false || maybeError.error) {
172
+ const errorMessage = maybeError.error || "Script fetch failed";
173
+ const error = new Error(errorMessage);
174
+ if (errorMessage === "action_does_not_exist" || errorMessage.includes("invalid action")) {
175
+ error.isUserError = true;
176
+ }
177
+ return error;
178
+ }
179
+ } catch (err) {
180
+ if (!(err instanceof SyntaxError)) {
181
+ throw err;
182
+ }
183
+ }
184
+ return null;
185
+ }
186
+ __name(parseErrorJson, "parseErrorJson");
187
+
188
+ // src/scriptRunner.ts
189
+ function createBlobUrl(content) {
190
+ const blob = new Blob([content], { type: "text/javascript" });
191
+ return URL.createObjectURL(blob);
192
+ }
193
+ __name(createBlobUrl, "createBlobUrl");
194
+ function loadModuleScript(blobUrl) {
195
+ return new Promise((resolve, reject) => {
196
+ const script = document.createElement("script");
197
+ script.type = "module";
198
+ script.src = blobUrl;
199
+ script.onload = () => resolve(script);
200
+ script.onerror = () => reject(new Error("Script failed to load"));
201
+ document.head.appendChild(script);
202
+ });
203
+ }
204
+ __name(loadModuleScript, "loadModuleScript");
205
+ function waitForGlobalFunction(fnName, timeout = 1e4) {
206
+ return new Promise((resolve, reject) => {
207
+ const checkInterval = setInterval(() => {
208
+ if (typeof window !== "undefined" && typeof window.Deflect?.[fnName] === "function") {
209
+ clearInterval(checkInterval);
210
+ resolve();
211
+ }
212
+ }, 50);
213
+ setTimeout(() => {
214
+ clearInterval(checkInterval);
215
+ reject(new Error(`Timeout: ${fnName} did not become available within ${timeout / 1e3} seconds`));
216
+ }, timeout);
217
+ });
218
+ }
219
+ __name(waitForGlobalFunction, "waitForGlobalFunction");
220
+ async function getTokenFromGlobal(fnName) {
221
+ if (typeof window === "undefined" || typeof window.Deflect?.[fnName] !== "function") {
222
+ throw new Error(`${fnName} not available on window.Deflect`);
223
+ }
224
+ return window.Deflect[fnName]();
225
+ }
226
+ __name(getTokenFromGlobal, "getTokenFromGlobal");
227
+ function cleanup(blobUrl, scriptElement, fnName) {
228
+ URL.revokeObjectURL(blobUrl);
229
+ scriptElement.remove();
230
+ if (typeof window !== "undefined" && window.Deflect?.[fnName]) {
231
+ delete window.Deflect[fnName];
232
+ }
233
+ }
234
+ __name(cleanup, "cleanup");
235
+
145
236
  // src/index.ts
146
237
  var Deflect = class {
147
238
  constructor() {
@@ -149,6 +240,7 @@
149
240
  this.scriptCache = null;
150
241
  this.isWarmupInProgress = false;
151
242
  this.hasWarmupError = false;
243
+ this.warmupPromise = null;
152
244
  this.initializeGlobalState();
153
245
  this.pulseReporter = new pulse_default(this.resolvePulseConfig());
154
246
  this.setupAutomaticWarmup();
@@ -179,24 +271,24 @@
179
271
  this.pulseReporter.captureException(error, { tags, context });
180
272
  }
181
273
  async tryWarmup() {
182
- if (!this.config?.actionId || this.isWarmupInProgress || this.scriptCache || this.hasWarmupError) {
274
+ if (!this.config?.actionId || this.config.prefetch === false || this.isWarmupInProgress || this.scriptCache || this.hasWarmupError) {
183
275
  return;
184
276
  }
277
+ this.validateActionId(this.config.actionId);
185
278
  this.isWarmupInProgress = true;
186
- try {
187
- this.scriptCache = await this.fetchScript();
188
- this.hasWarmupError = false;
189
- } catch {
190
- this.hasWarmupError = true;
191
- } finally {
192
- this.isWarmupInProgress = false;
193
- }
279
+ this.warmupPromise = (async () => {
280
+ try {
281
+ this.scriptCache = await fetchScript(this.config.actionId, this.config.scriptUrl);
282
+ this.hasWarmupError = false;
283
+ } catch {
284
+ this.hasWarmupError = true;
285
+ } finally {
286
+ this.isWarmupInProgress = false;
287
+ }
288
+ })();
289
+ await this.warmupPromise.catch(() => {
290
+ });
194
291
  }
195
- /**
196
- * @param actionId - The action ID to validate
197
- * @returns The sanitized action ID
198
- * @throws Error if actionId contains invalid characters
199
- */
200
292
  validateActionId(actionId) {
201
293
  const sanitized = actionId.trim();
202
294
  const validPattern = /^[a-zA-Z0-9/_-]+$/;
@@ -205,177 +297,34 @@
205
297
  }
206
298
  return encodeURIComponent(sanitized);
207
299
  }
208
- buildScriptUrl(actionId) {
209
- const baseUrl = this.config?.scriptUrl || "https://js.deflect.bot/main.js";
210
- const sanitizedActionId = this.validateActionId(actionId);
211
- const nonce = Date.now().toString();
212
- return `${baseUrl}?action_id=${sanitizedActionId}&_=${nonce}`;
213
- }
214
- async fetchScript() {
215
- if (!this.config?.actionId) {
216
- throw new Error("actionId is required");
217
- }
218
- const url = this.buildScriptUrl(this.config.actionId);
219
- try {
220
- const response = await fetch(url, {
221
- cache: "no-store",
222
- mode: "cors",
223
- credentials: "omit"
224
- });
225
- if (!response.ok) {
226
- throw new Error(`Failed to fetch script: ${response.status}`);
227
- }
228
- const content = await response.text();
229
- try {
230
- const maybeError = JSON.parse(content);
231
- if (maybeError.success === false || maybeError.error) {
232
- const errorMessage = maybeError.error || "Script fetch failed";
233
- const error = new Error(errorMessage);
234
- if (errorMessage === "action_does_not_exist" || errorMessage.includes("invalid action")) {
235
- error.isUserError = true;
236
- }
237
- throw error;
238
- }
239
- } catch (e) {
240
- if (!(e instanceof SyntaxError)) {
241
- throw e;
242
- }
243
- }
244
- return {
245
- content,
246
- sessionId: response.headers.get("session_id") || void 0
247
- };
248
- } catch (error) {
249
- this.reportError(
250
- error,
251
- {
252
- deflect_sdk_error: "true",
253
- method: "fetchScript",
254
- action_id: this.config.actionId
255
- },
256
- {
257
- actionId: this.config.actionId,
258
- url
259
- }
260
- );
261
- throw error;
262
- }
263
- }
264
- async executeScript(script) {
265
- if (script.sessionId && typeof window !== "undefined") {
266
- window.Deflect.sessionId = script.sessionId;
267
- }
268
- const blobUrl = this.createScriptBlob(script.content);
269
- let scriptElement = null;
270
- try {
271
- scriptElement = await this.loadScriptElement(blobUrl);
272
- await this.waitForGetToken();
273
- const token = await this.getTokenFromScript();
274
- this.prefetchNextScript();
275
- return token;
276
- } catch (error) {
277
- this.reportError(
278
- error,
279
- {
280
- deflect_sdk_error: "true",
281
- method: "executeScript",
282
- stage: "load_or_execute",
283
- action_id: this.config?.actionId || "unknown"
284
- },
285
- {
286
- hasCache: this.scriptCache !== null,
287
- hasWarmupError: this.hasWarmupError
288
- }
289
- );
290
- throw error;
291
- } finally {
292
- if (scriptElement) {
293
- this.cleanup(blobUrl, scriptElement);
294
- } else {
295
- URL.revokeObjectURL(blobUrl);
296
- }
297
- }
298
- }
299
300
  prefetchNextScript() {
300
- if (!this.hasWarmupError) {
301
+ if (!this.hasWarmupError && this.config?.prefetch !== false) {
301
302
  this.tryWarmup().catch(() => {
302
303
  });
303
304
  }
304
305
  }
305
- async waitForGetToken() {
306
- return new Promise((resolve, reject) => {
307
- const checkInterval = setInterval(() => {
308
- if (typeof window !== "undefined" && typeof window.Deflect?.getToken === "function") {
309
- clearInterval(checkInterval);
310
- resolve();
311
- }
312
- }, 50);
313
- setTimeout(() => {
314
- clearInterval(checkInterval);
315
- reject(new Error("Timeout: getToken function did not become available within 10 seconds"));
316
- }, 1e4);
317
- });
318
- }
319
- createScriptBlob(content) {
320
- const blob = new Blob([content], { type: "text/javascript" });
321
- return URL.createObjectURL(blob);
322
- }
323
- async loadScriptElement(blobUrl) {
324
- return new Promise((resolve, reject) => {
325
- const script = document.createElement("script");
326
- script.type = "module";
327
- script.src = blobUrl;
328
- script.onload = () => resolve(script);
329
- script.onerror = () => reject(new Error("Script failed to load"));
330
- document.head.appendChild(script);
331
- });
332
- }
333
- async getTokenFromScript() {
334
- if (typeof window === "undefined" || typeof window.Deflect?.getToken !== "function") {
335
- throw new Error("Script did not load properly - getToken not available");
336
- }
337
- try {
338
- return await window.Deflect.getToken();
339
- } catch (error) {
340
- throw new Error(`Script execution failed: ${error}`);
341
- }
342
- }
343
- cleanup(blobUrl, scriptElement) {
344
- URL.revokeObjectURL(blobUrl);
345
- scriptElement.remove();
346
- if (typeof window !== "undefined" && window.Deflect?.getToken) {
347
- delete window.Deflect.getToken;
306
+ isTestMode() {
307
+ if (this.config?.actionId === "PULSE_TEST" || this.config?.actionId === "SENTRY_TEST") {
308
+ throw new Error("PULSE_TEST: This is a test error to verify Pulse integration is working");
348
309
  }
310
+ return this.config?.actionId === "t/FFFFFFFFFFFFF/111111111" || this.config?.actionId === "t/FFFFFFFFFFFFF/000000000";
349
311
  }
350
- /**
351
- * Configures the Deflect SDK with the provided parameters.
352
- * Must be called before using getToken() or other SDK methods.
353
- *
354
- * @param params - Configuration options for the SDK
355
- * @param params.actionId - The unique action identifier from your Deflect dashboard (required)
356
- * @param params.scriptUrl - Optional custom script URL (defaults to Deflect CDN)
357
- * @throws Error if actionId is empty or contains invalid characters
358
- *
359
- * @example
360
- * ```typescript
361
- * Deflect.configure({ actionId: "your-action-id" });
362
- * ```
363
- */
364
312
  configure(params) {
365
313
  try {
366
314
  if (!params.actionId?.trim()) {
367
315
  throw new Error("actionId is required and cannot be empty");
368
316
  }
369
- if (this.config?.actionId === params.actionId) {
317
+ this.validateActionId(params.actionId);
318
+ if (this.config?.actionId === params.actionId && this.config.prefetch === params.prefetch) {
370
319
  return;
371
320
  }
372
321
  this.hasWarmupError = false;
373
322
  this.scriptCache = null;
374
- this.config = { ...params };
323
+ this.config = { prefetch: true, ...params };
375
324
  if (typeof window !== "undefined") {
376
325
  window.Deflect.actionId = params.actionId;
377
326
  }
378
- if (!this.isTestMode()) {
327
+ if (!this.isTestMode() && this.config.prefetch !== false) {
379
328
  this.tryWarmup();
380
329
  }
381
330
  } catch (error) {
@@ -397,50 +346,25 @@
397
346
  throw error;
398
347
  }
399
348
  }
400
- isTestMode() {
401
- if (this.config?.actionId === "PULSE_TEST" || this.config?.actionId === "SENTRY_TEST") {
402
- throw new Error("PULSE_TEST: This is a test error to verify Pulse integration is working");
403
- }
404
- return this.config?.actionId === "t/FFFFFFFFFFFFF/111111111" || this.config?.actionId === "t/FFFFFFFFFFFFF/000000000";
405
- }
406
- /**
407
- * @deprecated Use {@link getToken} instead. This method is kept for backward compatibility.
408
- * @returns A promise that resolves to the Deflect token string
409
- */
410
- async solveChallenge() {
411
- return this.getToken();
412
- }
413
- /**
414
- * Retrieves a Deflect token for the configured action.
415
- * The token should be included in requests to your protected endpoints.
416
- *
417
- * @returns A promise that resolves to the Deflect token string
418
- * @throws Error if configure() has not been called first
419
- * @throws Error if the script fails to load or execute
420
- *
421
- * @example
422
- * ```typescript
423
- * const token = await Deflect.getToken();
424
- * // Include token in your API request
425
- * fetch('/api/protected', {
426
- * headers: { 'X-Deflect-Token': token }
427
- * });
428
- * ```
429
- */
430
349
  async getToken() {
431
350
  try {
432
351
  if (!this.config?.actionId) {
433
- throw new Error("Must call configure() before solveChallenge()");
352
+ throw new Error("Must call configure() before getToken()");
434
353
  }
354
+ this.validateActionId(this.config.actionId);
435
355
  if (this.isTestMode()) {
436
356
  return "TESTTOKEN";
437
357
  }
438
358
  let script;
359
+ if (this.warmupPromise) {
360
+ await this.warmupPromise.catch(() => {
361
+ });
362
+ }
439
363
  if (this.scriptCache && !this.isWarmupInProgress) {
440
364
  script = this.scriptCache;
441
365
  this.scriptCache = null;
442
366
  } else {
443
- script = await this.fetchScript();
367
+ script = await fetchScript(this.config.actionId, this.config.scriptUrl);
444
368
  }
445
369
  return this.executeScript(script);
446
370
  } catch (error) {
@@ -465,52 +389,58 @@
465
389
  throw error;
466
390
  }
467
391
  }
468
- /**
469
- * Pre-fetches the challenge script to reduce latency on the first getToken() call.
470
- * This is automatically called after configure(), but can be manually triggered.
471
- *
472
- * @returns A promise that resolves to true if warmup succeeded, false otherwise
473
- *
474
- * @example
475
- * ```typescript
476
- * Deflect.configure({ actionId: "your-action-id" });
477
- * const warmedUp = await Deflect.warmup();
478
- * console.log('Script pre-cached:', warmedUp);
479
- * ```
480
- */
392
+ async executeScript(script) {
393
+ if (script.sessionId && typeof window !== "undefined") {
394
+ window.Deflect.sessionId = script.sessionId;
395
+ }
396
+ const blobUrl = createBlobUrl(script.content);
397
+ let scriptElement = null;
398
+ try {
399
+ scriptElement = await loadModuleScript(blobUrl);
400
+ await waitForGlobalFunction("getChallengeToken");
401
+ const token = await getTokenFromGlobal("getChallengeToken");
402
+ this.prefetchNextScript();
403
+ return token;
404
+ } catch (error) {
405
+ this.reportError(
406
+ error,
407
+ {
408
+ deflect_sdk_error: "true",
409
+ method: "executeScript",
410
+ stage: "load_or_execute",
411
+ action_id: this.config?.actionId || "unknown"
412
+ },
413
+ {
414
+ hasCache: this.scriptCache !== null,
415
+ hasWarmupError: this.hasWarmupError
416
+ }
417
+ );
418
+ throw error;
419
+ } finally {
420
+ if (scriptElement) {
421
+ cleanup(blobUrl, scriptElement, "getChallengeToken");
422
+ } else {
423
+ URL.revokeObjectURL(blobUrl);
424
+ }
425
+ }
426
+ }
481
427
  async warmup() {
482
428
  if (!this.config?.actionId) {
483
429
  return false;
484
430
  }
485
431
  try {
432
+ if (this.config.prefetch === false) {
433
+ return false;
434
+ }
486
435
  await this.tryWarmup();
487
436
  return this.scriptCache !== null;
488
437
  } catch {
489
438
  return false;
490
439
  }
491
440
  }
492
- /**
493
- * Clears the cached challenge script.
494
- * Useful when you need to force a fresh script fetch on the next getToken() call.
495
- */
496
441
  clearCache() {
497
442
  this.scriptCache = null;
498
443
  }
499
- /**
500
- * Injects a Deflect token as a hidden input into a form and submits it.
501
- * Designed for use with form onsubmit handlers.
502
- *
503
- * @param event - The form submit event
504
- * @throws Error if not called from a form submit event
505
- *
506
- * @example
507
- * ```html
508
- * <form action="/login" method="POST" onsubmit="Deflect.injectToken(event)">
509
- * <input name="username" />
510
- * <button type="submit">Login</button>
511
- * </form>
512
- * ```
513
- */
514
444
  async injectToken(event) {
515
445
  if (!event || !event.target || !(event.target instanceof HTMLFormElement)) {
516
446
  throw new Error("injectToken: must be called from a form submit event");
@@ -529,5 +459,9 @@
529
459
  form.submit();
530
460
  }
531
461
  };
532
- var src_default = new Deflect();
462
+ var DeflectInstance = new Deflect();
463
+ if (typeof window !== "undefined") {
464
+ window.Deflect = DeflectInstance;
465
+ }
466
+ var src_default = DeflectInstance;
533
467
  })();
package/dist/index.min.js CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";(()=>{var d={endpoint:"https://service.yabadabado.top/pulse",projectId:"deflect-sdk",deploymentId:"688529b539803661332b3f70",service:"deflect-sdk",release:"unknown"},g={name:"deflect-js-sdk",version:d.release||"unknown",language:"javascript"},l=class{constructor(e){this.config={...d,...e,endpoint:(e?.endpoint||d.endpoint).replace(/\/$/,"")},this.enabled=!!(this.config.endpoint&&this.config.projectId&&this.config.deploymentId)}captureException(e,t={}){if(!this.enabled||typeof fetch!="function")return;let n=this.buildErrorEvent(e,t);fetch(`${this.config.endpoint}/capture`,{method:"POST",headers:{"Content-Type":"application/json",project_id:this.config.projectId},body:JSON.stringify(n),keepalive:!0}).catch(()=>{})}buildErrorEvent(e,t){let n=this.normalizeError(e),r=this.config.environment||typeof window<"u"&&window.location.hostname||"unknown",i=typeof navigator>"u"?void 0:{name:navigator.userAgent||"browser",version:navigator.appVersion},o=typeof navigator>"u"?void 0:{name:navigator.platform},c=typeof window>"u"?void 0:{model:`${window.screen.width}x${window.screen.height}`,arch:typeof navigator<"u"?navigator.platform:void 0},f=typeof window>"u"?void 0:{method:"GET",url:window.location.href,headers:typeof navigator<"u"?{"User-Agent":navigator.userAgent,...typeof document<"u"&&document.referrer?{Referer:document.referrer}:{}}:void 0};return{event_type:"error",event_id:typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():Math.random().toString(16).slice(2)+Date.now().toString(16),deployment_id:this.config.deploymentId,project_id:this.config.projectId,environment:r,service:this.config.service,timestamp:new Date().toISOString(),release:this.config.release,sdk:{...g,version:this.config.release||g.version},tags:t.tags,context:t.context,error:{level:t.level||"error",message:n.message,exception:this.buildExceptionPayload(n),request:f,runtime:i,os:o,device:c}}}normalizeError(e){if(e instanceof Error)return e;let t=typeof e=="string"?e:"Unknown error";return new Error(t)}buildExceptionPayload(e){let t=this.parseStack(e);return{type:e.name||"Error",value:e.message,stacktrace:t.length?{frames:t}:void 0}}parseStack(e){if(!e.stack)return[];let t=[],n=e.stack.split(`
2
- `).slice(1);for(let r of n){let i=r.trim().replace(/^at\s+/,""),o=i.includes("(")&&i.endsWith(")"),c=o?i.slice(0,i.indexOf("(")).trim():"",s=(o?i.slice(i.indexOf("(")+1,i.length-1):i).split(":"),a=s[0]||void 0,p=s.length>1?Number(s[1]):void 0,h=s.length>2?Number(s[2]):void 0;a&&t.push({filename:a,abs_path:a,function:c||void 0,lineno:Number.isFinite(p)?p:void 0,colno:Number.isFinite(h)?h:void 0,in_app:!a.includes("node_modules")})}return t}},m=l;var u=class{constructor(){this.config=null;this.scriptCache=null;this.isWarmupInProgress=!1;this.hasWarmupError=!1;this.initializeGlobalState(),this.pulseReporter=new m(this.resolvePulseConfig()),this.setupAutomaticWarmup()}initializeGlobalState(){typeof window>"u"||(window.Deflect=window.Deflect||{})}setupAutomaticWarmup(){typeof window>"u"||(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>this.tryWarmup()):setTimeout(()=>this.tryWarmup(),100))}resolvePulseConfig(){let e=typeof window<"u"&&typeof window.Deflect=="object"&&window.Deflect?.pulseConfig&&typeof window.Deflect.pulseConfig=="object"?window.Deflect.pulseConfig:{};return{...e,environment:e.environment||(typeof window<"u"?window.location.hostname:void 0)}}reportError(e,t,n){this.pulseReporter.captureException(e,{tags:t,context:n})}async tryWarmup(){if(!(!this.config?.actionId||this.isWarmupInProgress||this.scriptCache||this.hasWarmupError)){this.isWarmupInProgress=!0;try{this.scriptCache=await this.fetchScript(),this.hasWarmupError=!1}catch{this.hasWarmupError=!0}finally{this.isWarmupInProgress=!1}}}validateActionId(e){let t=e.trim();if(!/^[a-zA-Z0-9/_-]+$/.test(t))throw new Error("Invalid actionId format: contains disallowed characters");return encodeURIComponent(t)}buildScriptUrl(e){let t=this.config?.scriptUrl||"https://js.deflect.bot/main.js",n=this.validateActionId(e),r=Date.now().toString();return`${t}?action_id=${n}&_=${r}`}async fetchScript(){if(!this.config?.actionId)throw new Error("actionId is required");let e=this.buildScriptUrl(this.config.actionId);try{let t=await fetch(e,{cache:"no-store",mode:"cors",credentials:"omit"});if(!t.ok)throw new Error(`Failed to fetch script: ${t.status}`);let n=await t.text();try{let r=JSON.parse(n);if(r.success===!1||r.error){let i=r.error||"Script fetch failed",o=new Error(i);throw(i==="action_does_not_exist"||i.includes("invalid action"))&&(o.isUserError=!0),o}}catch(r){if(!(r instanceof SyntaxError))throw r}return{content:n,sessionId:t.headers.get("session_id")||void 0}}catch(t){throw this.reportError(t,{deflect_sdk_error:"true",method:"fetchScript",action_id:this.config.actionId},{actionId:this.config.actionId,url:e}),t}}async executeScript(e){e.sessionId&&typeof window<"u"&&(window.Deflect.sessionId=e.sessionId);let t=this.createScriptBlob(e.content),n=null;try{n=await this.loadScriptElement(t),await this.waitForGetToken();let r=await this.getTokenFromScript();return this.prefetchNextScript(),r}catch(r){throw this.reportError(r,{deflect_sdk_error:"true",method:"executeScript",stage:"load_or_execute",action_id:this.config?.actionId||"unknown"},{hasCache:this.scriptCache!==null,hasWarmupError:this.hasWarmupError}),r}finally{n?this.cleanup(t,n):URL.revokeObjectURL(t)}}prefetchNextScript(){this.hasWarmupError||this.tryWarmup().catch(()=>{})}async waitForGetToken(){return new Promise((e,t)=>{let n=setInterval(()=>{typeof window<"u"&&typeof window.Deflect?.getToken=="function"&&(clearInterval(n),e())},50);setTimeout(()=>{clearInterval(n),t(new Error("Timeout: getToken function did not become available within 10 seconds"))},1e4)})}createScriptBlob(e){let t=new Blob([e],{type:"text/javascript"});return URL.createObjectURL(t)}async loadScriptElement(e){return new Promise((t,n)=>{let r=document.createElement("script");r.type="module",r.src=e,r.onload=()=>t(r),r.onerror=()=>n(new Error("Script failed to load")),document.head.appendChild(r)})}async getTokenFromScript(){if(typeof window>"u"||typeof window.Deflect?.getToken!="function")throw new Error("Script did not load properly - getToken not available");try{return await window.Deflect.getToken()}catch(e){throw new Error(`Script execution failed: ${e}`)}}cleanup(e,t){URL.revokeObjectURL(e),t.remove(),typeof window<"u"&&window.Deflect?.getToken&&delete window.Deflect.getToken}configure(e){try{if(!e.actionId?.trim())throw new Error("actionId is required and cannot be empty");if(this.config?.actionId===e.actionId)return;this.hasWarmupError=!1,this.scriptCache=null,this.config={...e},typeof window<"u"&&(window.Deflect.actionId=e.actionId),this.isTestMode()||this.tryWarmup()}catch(t){let n=t&&typeof t=="object"&&"isUserError"in t&&t.isUserError===!0;throw this.reportError(t,{deflect_sdk_error:"true",deflect_user_error:n?"true":"false",method:"configure",action_id:e?.actionId||"unknown"},{actionId:e?.actionId,hasCache:this.scriptCache!==null,hasWarmupError:this.hasWarmupError}),t}}isTestMode(){if(this.config?.actionId==="PULSE_TEST"||this.config?.actionId==="SENTRY_TEST")throw new Error("PULSE_TEST: This is a test error to verify Pulse integration is working");return this.config?.actionId==="t/FFFFFFFFFFFFF/111111111"||this.config?.actionId==="t/FFFFFFFFFFFFF/000000000"}async solveChallenge(){return this.getToken()}async getToken(){try{if(!this.config?.actionId)throw new Error("Must call configure() before solveChallenge()");if(this.isTestMode())return"TESTTOKEN";let e;return this.scriptCache&&!this.isWarmupInProgress?(e=this.scriptCache,this.scriptCache=null):e=await this.fetchScript(),this.executeScript(e)}catch(e){let t=e&&typeof e=="object"&&"isUserError"in e&&e.isUserError===!0;throw this.reportError(e,{deflect_sdk_error:"true",deflect_user_error:t?"true":"false",method:"getToken",action_id:this.config?.actionId||"unknown",has_cache:this.scriptCache!==null?"true":"false",has_warmup_error:this.hasWarmupError?"true":"false",stage:"call"},{actionId:this.config?.actionId,hasCache:this.scriptCache!==null,hasWarmupError:this.hasWarmupError}),e}}async warmup(){if(!this.config?.actionId)return!1;try{return await this.tryWarmup(),this.scriptCache!==null}catch{return!1}}clearCache(){this.scriptCache=null}async injectToken(e){if(!e||!e.target||!(e.target instanceof HTMLFormElement))throw new Error("injectToken: must be called from a form submit event");e.preventDefault();let t=e.target,n=await this.getToken();Array.from(t.querySelectorAll('input[name="deflect_token"]')).forEach(i=>i.remove());let r=document.createElement("input");r.type="hidden",r.name="deflect_token",r.value=n,t.appendChild(r),t.submit()}},I=new u;})();
1
+ "use strict";(()=>{var d={endpoint:"https://service.yabadabado.top/pulse",projectId:"deflect-sdk",deploymentId:"688529b539803661332b3f70",service:"deflect-sdk",release:"unknown"},w={name:"deflect-js-sdk",version:d.release||"unknown",language:"javascript"},u=class{constructor(e){this.config={...d,...e,endpoint:(e?.endpoint||d.endpoint).replace(/\/$/,"")},this.enabled=!!(this.config.endpoint&&this.config.projectId&&this.config.deploymentId)}captureException(e,t={}){if(!this.enabled||typeof fetch!="function")return;let n=this.buildErrorEvent(e,t);fetch(`${this.config.endpoint}/capture`,{method:"POST",headers:{"Content-Type":"application/json",project_id:this.config.projectId},body:JSON.stringify(n),keepalive:!0}).catch(()=>{})}buildErrorEvent(e,t){let n=this.normalizeError(e),i=this.config.environment||typeof window<"u"&&window.location.hostname||"unknown",o=typeof navigator>"u"?void 0:{name:navigator.userAgent||"browser",version:navigator.appVersion},a=typeof navigator>"u"?void 0:{name:navigator.platform},l=typeof window>"u"?void 0:{model:`${window.screen.width}x${window.screen.height}`,arch:typeof navigator<"u"?navigator.platform:void 0},h=typeof window>"u"?void 0:{method:"GET",url:window.location.href,headers:typeof navigator<"u"?{"User-Agent":navigator.userAgent,...typeof document<"u"&&document.referrer?{Referer:document.referrer}:{}}:void 0};return{event_type:"error",event_id:typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():Math.random().toString(16).slice(2)+Date.now().toString(16),deployment_id:this.config.deploymentId,project_id:this.config.projectId,environment:i,service:this.config.service,timestamp:new Date().toISOString(),release:this.config.release,sdk:{...w,version:this.config.release||w.version},tags:t.tags,context:t.context,error:{level:t.level||"error",message:n.message,exception:this.buildExceptionPayload(n),request:h,runtime:o,os:a,device:l}}}normalizeError(e){if(e instanceof Error)return e;let t=typeof e=="string"?e:"Unknown error";return new Error(t)}buildExceptionPayload(e){let t=this.parseStack(e);return{type:e.name||"Error",value:e.message,stacktrace:t.length?{frames:t}:void 0}}parseStack(e){if(!e.stack)return[];let t=[],n=e.stack.split(`
2
+ `).slice(1);for(let i of n){let o=i.trim().replace(/^at\s+/,""),a=o.includes("(")&&o.endsWith(")"),l=a?o.slice(0,o.indexOf("(")).trim():"",s=(a?o.slice(o.indexOf("(")+1,o.length-1):o).split(":"),c=s[0]||void 0,g=s.length>1?Number(s[1]):void 0,m=s.length>2?Number(s[2]):void 0;c&&t.push({filename:c,abs_path:c,function:l||void 0,lineno:Number.isFinite(g)?g:void 0,colno:Number.isFinite(m)?m:void 0,in_app:!c.includes("node_modules")})}return t}},v=u;function k(r,e){let t=e||"https://js.deflect.bot/main.js",n=Date.now().toString();return`${t}?action_id=${encodeURIComponent(r)}&_=${n}`}async function f(r,e){let t=k(r,e),n=await fetch(t,{cache:"no-store",mode:"cors",credentials:"omit"}),i=await n.text(),o=_(i);if(o)throw o;return{content:i,sessionId:n.headers.get("session_id")||void 0}}function _(r){try{let e=JSON.parse(r);if(e.success===!1||e.error){let t=e.error||"Script fetch failed",n=new Error(t);return(t==="action_does_not_exist"||t.includes("invalid action"))&&(n.isUserError=!0),n}}catch(e){if(!(e instanceof SyntaxError))throw e}return null}function y(r){let e=new Blob([r],{type:"text/javascript"});return URL.createObjectURL(e)}function E(r){return new Promise((e,t)=>{let n=document.createElement("script");n.type="module",n.src=r,n.onload=()=>e(n),n.onerror=()=>t(new Error("Script failed to load")),document.head.appendChild(n)})}function I(r,e=1e4){return new Promise((t,n)=>{let i=setInterval(()=>{typeof window<"u"&&typeof window.Deflect?.[r]=="function"&&(clearInterval(i),t())},50);setTimeout(()=>{clearInterval(i),n(new Error(`Timeout: ${r} did not become available within ${e/1e3} seconds`))},e)})}async function P(r){if(typeof window>"u"||typeof window.Deflect?.[r]!="function")throw new Error(`${r} not available on window.Deflect`);return window.Deflect[r]()}function b(r,e,t){URL.revokeObjectURL(r),e.remove(),typeof window<"u"&&window.Deflect?.[t]&&delete window.Deflect[t]}var p=class{constructor(){this.config=null;this.scriptCache=null;this.isWarmupInProgress=!1;this.hasWarmupError=!1;this.warmupPromise=null;this.initializeGlobalState(),this.pulseReporter=new v(this.resolvePulseConfig()),this.setupAutomaticWarmup()}initializeGlobalState(){typeof window>"u"||(window.Deflect=window.Deflect||{})}setupAutomaticWarmup(){typeof window>"u"||(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>this.tryWarmup()):setTimeout(()=>this.tryWarmup(),100))}resolvePulseConfig(){let e=typeof window<"u"&&typeof window.Deflect=="object"&&window.Deflect?.pulseConfig&&typeof window.Deflect.pulseConfig=="object"?window.Deflect.pulseConfig:{};return{...e,environment:e.environment||(typeof window<"u"?window.location.hostname:void 0)}}reportError(e,t,n){this.pulseReporter.captureException(e,{tags:t,context:n})}async tryWarmup(){!this.config?.actionId||this.config.prefetch===!1||this.isWarmupInProgress||this.scriptCache||this.hasWarmupError||(this.validateActionId(this.config.actionId),this.isWarmupInProgress=!0,this.warmupPromise=(async()=>{try{this.scriptCache=await f(this.config.actionId,this.config.scriptUrl),this.hasWarmupError=!1}catch{this.hasWarmupError=!0}finally{this.isWarmupInProgress=!1}})(),await this.warmupPromise.catch(()=>{}))}validateActionId(e){let t=e.trim();if(!/^[a-zA-Z0-9/_-]+$/.test(t))throw new Error("Invalid actionId format: contains disallowed characters");return encodeURIComponent(t)}prefetchNextScript(){!this.hasWarmupError&&this.config?.prefetch!==!1&&this.tryWarmup().catch(()=>{})}isTestMode(){if(this.config?.actionId==="PULSE_TEST"||this.config?.actionId==="SENTRY_TEST")throw new Error("PULSE_TEST: This is a test error to verify Pulse integration is working");return this.config?.actionId==="t/FFFFFFFFFFFFF/111111111"||this.config?.actionId==="t/FFFFFFFFFFFFF/000000000"}configure(e){try{if(!e.actionId?.trim())throw new Error("actionId is required and cannot be empty");if(this.validateActionId(e.actionId),this.config?.actionId===e.actionId&&this.config.prefetch===e.prefetch)return;this.hasWarmupError=!1,this.scriptCache=null,this.config={prefetch:!0,...e},typeof window<"u"&&(window.Deflect.actionId=e.actionId),!this.isTestMode()&&this.config.prefetch!==!1&&this.tryWarmup()}catch(t){let n=t&&typeof t=="object"&&"isUserError"in t&&t.isUserError===!0;throw this.reportError(t,{deflect_sdk_error:"true",deflect_user_error:n?"true":"false",method:"configure",action_id:e?.actionId||"unknown"},{actionId:e?.actionId,hasCache:this.scriptCache!==null,hasWarmupError:this.hasWarmupError}),t}}async getToken(){try{if(!this.config?.actionId)throw new Error("Must call configure() before getToken()");if(this.validateActionId(this.config.actionId),this.isTestMode())return"TESTTOKEN";let e;return this.warmupPromise&&await this.warmupPromise.catch(()=>{}),this.scriptCache&&!this.isWarmupInProgress?(e=this.scriptCache,this.scriptCache=null):e=await f(this.config.actionId,this.config.scriptUrl),this.executeScript(e)}catch(e){let t=e&&typeof e=="object"&&"isUserError"in e&&e.isUserError===!0;throw this.reportError(e,{deflect_sdk_error:"true",deflect_user_error:t?"true":"false",method:"getToken",action_id:this.config?.actionId||"unknown",has_cache:this.scriptCache!==null?"true":"false",has_warmup_error:this.hasWarmupError?"true":"false",stage:"call"},{actionId:this.config?.actionId,hasCache:this.scriptCache!==null,hasWarmupError:this.hasWarmupError}),e}}async executeScript(e){e.sessionId&&typeof window<"u"&&(window.Deflect.sessionId=e.sessionId);let t=y(e.content),n=null;try{n=await E(t),await I("getChallengeToken");let i=await P("getChallengeToken");return this.prefetchNextScript(),i}catch(i){throw this.reportError(i,{deflect_sdk_error:"true",method:"executeScript",stage:"load_or_execute",action_id:this.config?.actionId||"unknown"},{hasCache:this.scriptCache!==null,hasWarmupError:this.hasWarmupError}),i}finally{n?b(t,n,"getChallengeToken"):URL.revokeObjectURL(t)}}async warmup(){if(!this.config?.actionId)return!1;try{return this.config.prefetch===!1?!1:(await this.tryWarmup(),this.scriptCache!==null)}catch{return!1}}clearCache(){this.scriptCache=null}async injectToken(e){if(!e||!e.target||!(e.target instanceof HTMLFormElement))throw new Error("injectToken: must be called from a form submit event");e.preventDefault();let t=e.target,n=await this.getToken();Array.from(t.querySelectorAll('input[name="deflect_token"]')).forEach(o=>o.remove());let i=document.createElement("input");i.type="hidden",i.name="deflect_token",i.value=n,t.appendChild(i),t.submit()}},S=new p;typeof window<"u"&&(window.Deflect=S);var j=S;})();
@@ -0,0 +1,7 @@
1
+ export interface DeflectScript {
2
+ sessionId?: string;
3
+ content: string;
4
+ }
5
+ export declare function buildScriptUrl(actionId: string, scriptUrl?: string): string;
6
+ export declare function fetchScript(actionId: string, scriptUrl?: string): Promise<DeflectScript>;
7
+ export declare function parseErrorJson(content: string): Error | null;
@@ -0,0 +1,5 @@
1
+ export declare function createBlobUrl(content: string): string;
2
+ export declare function loadModuleScript(blobUrl: string): Promise<HTMLScriptElement>;
3
+ export declare function waitForGlobalFunction(fnName: string, timeout?: number): Promise<void>;
4
+ export declare function getTokenFromGlobal(fnName: string): Promise<string>;
5
+ export declare function cleanup(blobUrl: string, scriptElement: HTMLScriptElement, fnName: string): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deflectbot/deflect-sdk",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "description": "SDK for deflect.bot - Use it for seamless captcha integration on any website.",
5
5
  "main": "dist/index.min.js",
6
6
  "module": "dist/index.esm.js",