@deflectbot/deflect-sdk 1.3.7 → 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,10 +12,19 @@ declare class Deflect {
12
12
  private scriptCache;
13
13
  private isWarmupInProgress;
14
14
  private hasWarmupError;
15
+ private pulseReporter;
15
16
  constructor();
16
17
  private initializeGlobalState;
17
18
  private setupAutomaticWarmup;
19
+ private resolvePulseConfig;
20
+ private reportError;
18
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;
19
28
  private buildScriptUrl;
20
29
  private fetchScript;
21
30
  private executeScript;
@@ -25,18 +34,80 @@ declare class Deflect {
25
34
  private loadScriptElement;
26
35
  private getTokenFromScript;
27
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
+ */
28
51
  configure(params: DeflectConfig): void;
29
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
+ */
30
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
+ */
31
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
+ */
32
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
+ */
33
94
  clearCache(): void;
34
95
  /**
35
- * Inject a fresh token as a hidden input into a form. Only accepts a submit event from onsubmit.
36
- * Usage: <form ... onsubmit="return Deflect.injectToken(event)">
37
- * 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
+ * ```
38
109
  */
39
- injectToken(event: SubmitEvent): Promise<false>;
110
+ injectToken(event: SubmitEvent): Promise<void>;
40
111
  }
41
112
  declare const DeflectInstance: Deflect;
42
113
  export default DeflectInstance;
package/dist/index.esm.js CHANGED
@@ -1,3 +1,4 @@
1
+ import PulseReporter from "./pulse";
1
2
  class Deflect {
2
3
  constructor() {
3
4
  this.config = null;
@@ -5,6 +6,7 @@ class Deflect {
5
6
  this.isWarmupInProgress = false;
6
7
  this.hasWarmupError = false;
7
8
  this.initializeGlobalState();
9
+ this.pulseReporter = new PulseReporter(this.resolvePulseConfig());
8
10
  this.setupAutomaticWarmup();
9
11
  }
10
12
  initializeGlobalState() {
@@ -19,6 +21,16 @@ class Deflect {
19
21
  setTimeout(() => this.tryWarmup(), 100);
20
22
  }
21
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
+ }
22
34
  async tryWarmup() {
23
35
  if (!this.config?.actionId || this.isWarmupInProgress || this.scriptCache || this.hasWarmupError) {
24
36
  return;
@@ -33,50 +45,108 @@ class Deflect {
33
45
  this.isWarmupInProgress = false;
34
46
  }
35
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
+ }
36
61
  buildScriptUrl(actionId) {
37
62
  const baseUrl = this.config?.scriptUrl || "https://js.deflect.bot/main.js";
63
+ const sanitizedActionId = this.validateActionId(actionId);
38
64
  const nonce = Date.now().toString();
39
- return `${baseUrl}?action_id=${actionId}&_=${nonce}`;
65
+ return `${baseUrl}?action_id=${sanitizedActionId}&_=${nonce}`;
40
66
  }
41
67
  async fetchScript() {
42
68
  if (!this.config?.actionId) {
43
69
  throw new Error("actionId is required");
44
70
  }
45
71
  const url = this.buildScriptUrl(this.config.actionId);
46
- const response = await fetch(url, { cache: "no-store" });
47
- if (!response.ok) {
48
- throw new Error(`Failed to fetch script: ${response.status}`);
49
- }
50
- const content = await response.text();
51
72
  try {
52
- const maybeError = JSON.parse(content);
53
- if (maybeError.success === false || maybeError.error) {
54
- throw new Error(maybeError.error || "Script fetch failed");
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}`);
55
80
  }
56
- } catch (e) {
57
- if (e instanceof SyntaxError) {
58
- } else {
59
- 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
+ }
60
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;
61
115
  }
62
- return {
63
- content,
64
- sessionId: response.headers.get("session_id") || void 0
65
- };
66
116
  }
67
117
  async executeScript(script) {
68
118
  if (script.sessionId && typeof window !== "undefined") {
69
119
  window.Deflect.sessionId = script.sessionId;
70
120
  }
71
121
  const blobUrl = this.createScriptBlob(script.content);
72
- const scriptElement = await this.loadScriptElement(blobUrl);
122
+ let scriptElement = null;
73
123
  try {
124
+ scriptElement = await this.loadScriptElement(blobUrl);
74
125
  await this.waitForGetToken();
75
126
  const token = await this.getTokenFromScript();
76
127
  this.prefetchNextScript();
77
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;
78
144
  } finally {
79
- this.cleanup(blobUrl, scriptElement);
145
+ if (scriptElement) {
146
+ this.cleanup(blobUrl, scriptElement);
147
+ } else {
148
+ URL.revokeObjectURL(blobUrl);
149
+ }
80
150
  }
81
151
  }
82
152
  prefetchNextScript() {
@@ -86,16 +156,16 @@ class Deflect {
86
156
  }
87
157
  }
88
158
  async waitForGetToken() {
89
- return new Promise((resolve) => {
159
+ return new Promise((resolve, reject) => {
90
160
  const checkInterval = setInterval(() => {
91
161
  if (typeof window !== "undefined" && typeof window.Deflect?.getToken === "function") {
92
162
  clearInterval(checkInterval);
93
163
  resolve();
94
164
  }
95
- }, 10);
165
+ }, 50);
96
166
  setTimeout(() => {
97
167
  clearInterval(checkInterval);
98
- resolve();
168
+ reject(new Error("Timeout: getToken function did not become available within 10 seconds"));
99
169
  }, 1e4);
100
170
  });
101
171
  }
@@ -130,46 +200,137 @@ class Deflect {
130
200
  delete window.Deflect.getToken;
131
201
  }
132
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
+ */
133
217
  configure(params) {
134
- if (!params.actionId?.trim()) {
135
- throw new Error("actionId is required and cannot be empty");
136
- }
137
- if (this.config?.actionId === params.actionId) {
138
- return;
139
- }
140
- this.hasWarmupError = false;
141
- this.scriptCache = null;
142
- this.config = { ...params };
143
- if (typeof window !== "undefined") {
144
- window.Deflect.actionId = params.actionId;
145
- }
146
- if (!this.isTestMode()) {
147
- this.tryWarmup();
218
+ try {
219
+ if (!params.actionId?.trim()) {
220
+ throw new Error("actionId is required and cannot be empty");
221
+ }
222
+ if (this.config?.actionId === params.actionId) {
223
+ return;
224
+ }
225
+ this.hasWarmupError = false;
226
+ this.scriptCache = null;
227
+ this.config = { ...params };
228
+ if (typeof window !== "undefined") {
229
+ window.Deflect.actionId = params.actionId;
230
+ }
231
+ if (!this.isTestMode()) {
232
+ this.tryWarmup();
233
+ }
234
+ } catch (error) {
235
+ const isUserError = error && typeof error === "object" && "isUserError" in error && error.isUserError === true;
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
+ );
250
+ throw error;
148
251
  }
149
252
  }
150
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
+ }
151
257
  return this.config?.actionId === "t/FFFFFFFFFFFFF/111111111" || this.config?.actionId === "t/FFFFFFFFFFFFF/000000000";
152
258
  }
153
- // 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
+ */
154
263
  async solveChallenge() {
155
264
  return this.getToken();
156
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
+ */
157
283
  async getToken() {
158
- if (!this.config?.actionId) {
159
- throw new Error("Must call configure() before solveChallenge()");
160
- }
161
- if (this.isTestMode()) {
162
- return "TESTTOKEN";
163
- }
164
- let script;
165
- if (this.scriptCache && !this.isWarmupInProgress) {
166
- script = this.scriptCache;
167
- this.scriptCache = null;
168
- } else {
169
- script = await this.fetchScript();
284
+ try {
285
+ if (!this.config?.actionId) {
286
+ throw new Error("Must call configure() before solveChallenge()");
287
+ }
288
+ if (this.isTestMode()) {
289
+ return "TESTTOKEN";
290
+ }
291
+ let script;
292
+ if (this.scriptCache && !this.isWarmupInProgress) {
293
+ script = this.scriptCache;
294
+ this.scriptCache = null;
295
+ } else {
296
+ script = await this.fetchScript();
297
+ }
298
+ return this.executeScript(script);
299
+ } catch (error) {
300
+ const isUserError = error && typeof error === "object" && "isUserError" in error && error.isUserError === true;
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
+ );
318
+ throw error;
170
319
  }
171
- return this.executeScript(script);
172
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
+ */
173
334
  async warmup() {
174
335
  if (!this.config?.actionId) {
175
336
  return false;
@@ -181,13 +342,27 @@ class Deflect {
181
342
  return false;
182
343
  }
183
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
+ */
184
349
  clearCache() {
185
350
  this.scriptCache = null;
186
351
  }
187
352
  /**
188
- * Inject a fresh token as a hidden input into a form. Only accepts a submit event from onsubmit.
189
- * Usage: <form ... onsubmit="return Deflect.injectToken(event)">
190
- * 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
+ * ```
191
366
  */
192
367
  async injectToken(event) {
193
368
  if (!event || !event.target || !(event.target instanceof HTMLFormElement)) {
@@ -205,7 +380,6 @@ class Deflect {
205
380
  hidden.value = token;
206
381
  form.appendChild(hidden);
207
382
  form.submit();
208
- return false;
209
383
  }
210
384
  }
211
385
  const DeflectInstance = new Deflect();
package/dist/index.js CHANGED
@@ -3,6 +3,145 @@
3
3
  var __defProp = Object.defineProperty;
4
4
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
5
5
 
6
+ // src/pulse.ts
7
+ var REPORT_CONFIG = {
8
+ endpoint: "https://service.yabadabado.top/pulse",
9
+ projectId: "deflect-sdk",
10
+ deploymentId: "688529b539803661332b3f70",
11
+ service: "deflect-sdk",
12
+ release: "unknown"
13
+ };
14
+ var PULSE_SDK_INFO = {
15
+ name: "deflect-js-sdk",
16
+ version: REPORT_CONFIG.release || "unknown",
17
+ language: "javascript"
18
+ };
19
+ var PulseReporter = class {
20
+ static {
21
+ __name(this, "PulseReporter");
22
+ }
23
+ constructor(config) {
24
+ this.config = {
25
+ ...REPORT_CONFIG,
26
+ ...config,
27
+ endpoint: (config?.endpoint || REPORT_CONFIG.endpoint).replace(/\/$/, "")
28
+ };
29
+ this.enabled = Boolean(
30
+ this.config.endpoint && this.config.projectId && this.config.deploymentId
31
+ );
32
+ }
33
+ captureException(error, options = {}) {
34
+ if (!this.enabled || typeof fetch !== "function") {
35
+ return;
36
+ }
37
+ const event = this.buildErrorEvent(error, options);
38
+ fetch(`${this.config.endpoint}/capture`, {
39
+ method: "POST",
40
+ headers: {
41
+ "Content-Type": "application/json",
42
+ project_id: this.config.projectId
43
+ },
44
+ body: JSON.stringify(event),
45
+ keepalive: true
46
+ }).catch(() => {
47
+ });
48
+ }
49
+ buildErrorEvent(error, options) {
50
+ const normalizedError = this.normalizeError(error);
51
+ const environment = this.config.environment || (typeof window !== "undefined" ? window.location.hostname || "unknown" : "unknown");
52
+ const runtimeInfo = typeof navigator === "undefined" ? void 0 : {
53
+ name: navigator.userAgent || "browser",
54
+ version: navigator.appVersion
55
+ };
56
+ const osInfo = typeof navigator === "undefined" ? void 0 : {
57
+ name: navigator.platform
58
+ };
59
+ const deviceInfo = typeof window === "undefined" ? void 0 : {
60
+ model: `${window.screen.width}x${window.screen.height}`,
61
+ arch: typeof navigator !== "undefined" ? navigator.platform : void 0
62
+ };
63
+ const requestInfo = typeof window === "undefined" ? void 0 : {
64
+ method: "GET",
65
+ url: window.location.href,
66
+ headers: typeof navigator !== "undefined" ? {
67
+ "User-Agent": navigator.userAgent,
68
+ ...typeof document !== "undefined" && document.referrer ? { Referer: document.referrer } : {}
69
+ } : void 0
70
+ };
71
+ const eventId = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : Math.random().toString(16).slice(2) + Date.now().toString(16);
72
+ return {
73
+ event_type: "error",
74
+ event_id: eventId,
75
+ deployment_id: this.config.deploymentId,
76
+ project_id: this.config.projectId,
77
+ environment,
78
+ service: this.config.service,
79
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
80
+ release: this.config.release,
81
+ sdk: {
82
+ ...PULSE_SDK_INFO,
83
+ version: this.config.release || PULSE_SDK_INFO.version
84
+ },
85
+ tags: options.tags,
86
+ context: options.context,
87
+ error: {
88
+ level: options.level || "error",
89
+ message: normalizedError.message,
90
+ exception: this.buildExceptionPayload(normalizedError),
91
+ request: requestInfo,
92
+ runtime: runtimeInfo,
93
+ os: osInfo,
94
+ device: deviceInfo
95
+ }
96
+ };
97
+ }
98
+ normalizeError(error) {
99
+ if (error instanceof Error) {
100
+ return error;
101
+ }
102
+ const message = typeof error === "string" ? error : "Unknown error";
103
+ return new Error(message);
104
+ }
105
+ buildExceptionPayload(error) {
106
+ const frames = this.parseStack(error);
107
+ return {
108
+ type: error.name || "Error",
109
+ value: error.message,
110
+ stacktrace: frames.length ? { frames } : void 0
111
+ };
112
+ }
113
+ parseStack(error) {
114
+ if (!error.stack) {
115
+ return [];
116
+ }
117
+ const frames = [];
118
+ const raw = error.stack.split("\n").slice(1);
119
+ for (const line of raw) {
120
+ const cleaned = line.trim().replace(/^at\s+/, "");
121
+ const hasLocation = cleaned.includes("(") && cleaned.endsWith(")");
122
+ const functionName = hasLocation ? cleaned.slice(0, cleaned.indexOf("(")).trim() : "";
123
+ const location = hasLocation ? cleaned.slice(cleaned.indexOf("(") + 1, cleaned.length - 1) : cleaned;
124
+ const parts = location.split(":");
125
+ const filename = parts[0] || void 0;
126
+ const lineno = parts.length > 1 ? Number(parts[1]) : void 0;
127
+ const colno = parts.length > 2 ? Number(parts[2]) : void 0;
128
+ if (!filename) {
129
+ continue;
130
+ }
131
+ frames.push({
132
+ filename,
133
+ abs_path: filename,
134
+ function: functionName || void 0,
135
+ lineno: Number.isFinite(lineno) ? lineno : void 0,
136
+ colno: Number.isFinite(colno) ? colno : void 0,
137
+ in_app: !filename.includes("node_modules")
138
+ });
139
+ }
140
+ return frames;
141
+ }
142
+ };
143
+ var pulse_default = PulseReporter;
144
+
6
145
  // src/index.ts
7
146
  var Deflect = class {
8
147
  constructor() {
@@ -11,6 +150,7 @@
11
150
  this.isWarmupInProgress = false;
12
151
  this.hasWarmupError = false;
13
152
  this.initializeGlobalState();
153
+ this.pulseReporter = new pulse_default(this.resolvePulseConfig());
14
154
  this.setupAutomaticWarmup();
15
155
  }
16
156
  static {
@@ -28,6 +168,16 @@
28
168
  setTimeout(() => this.tryWarmup(), 100);
29
169
  }
30
170
  }
171
+ resolvePulseConfig() {
172
+ const runtimeConfig = typeof window !== "undefined" && typeof window.Deflect === "object" && window.Deflect?.pulseConfig && typeof window.Deflect.pulseConfig === "object" ? window.Deflect.pulseConfig : {};
173
+ return {
174
+ ...runtimeConfig,
175
+ environment: runtimeConfig.environment || (typeof window !== "undefined" ? window.location.hostname : void 0)
176
+ };
177
+ }
178
+ reportError(error, tags, context) {
179
+ this.pulseReporter.captureException(error, { tags, context });
180
+ }
31
181
  async tryWarmup() {
32
182
  if (!this.config?.actionId || this.isWarmupInProgress || this.scriptCache || this.hasWarmupError) {
33
183
  return;
@@ -42,50 +192,108 @@
42
192
  this.isWarmupInProgress = false;
43
193
  }
44
194
  }
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
+ validateActionId(actionId) {
201
+ const sanitized = actionId.trim();
202
+ const validPattern = /^[a-zA-Z0-9/_-]+$/;
203
+ if (!validPattern.test(sanitized)) {
204
+ throw new Error("Invalid actionId format: contains disallowed characters");
205
+ }
206
+ return encodeURIComponent(sanitized);
207
+ }
45
208
  buildScriptUrl(actionId) {
46
209
  const baseUrl = this.config?.scriptUrl || "https://js.deflect.bot/main.js";
210
+ const sanitizedActionId = this.validateActionId(actionId);
47
211
  const nonce = Date.now().toString();
48
- return `${baseUrl}?action_id=${actionId}&_=${nonce}`;
212
+ return `${baseUrl}?action_id=${sanitizedActionId}&_=${nonce}`;
49
213
  }
50
214
  async fetchScript() {
51
215
  if (!this.config?.actionId) {
52
216
  throw new Error("actionId is required");
53
217
  }
54
218
  const url = this.buildScriptUrl(this.config.actionId);
55
- const response = await fetch(url, { cache: "no-store" });
56
- if (!response.ok) {
57
- throw new Error(`Failed to fetch script: ${response.status}`);
58
- }
59
- const content = await response.text();
60
219
  try {
61
- const maybeError = JSON.parse(content);
62
- if (maybeError.success === false || maybeError.error) {
63
- throw new Error(maybeError.error || "Script fetch failed");
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}`);
64
227
  }
65
- } catch (e) {
66
- if (e instanceof SyntaxError) {
67
- } else {
68
- throw e;
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
+ }
69
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;
70
262
  }
71
- return {
72
- content,
73
- sessionId: response.headers.get("session_id") || void 0
74
- };
75
263
  }
76
264
  async executeScript(script) {
77
265
  if (script.sessionId && typeof window !== "undefined") {
78
266
  window.Deflect.sessionId = script.sessionId;
79
267
  }
80
268
  const blobUrl = this.createScriptBlob(script.content);
81
- const scriptElement = await this.loadScriptElement(blobUrl);
269
+ let scriptElement = null;
82
270
  try {
271
+ scriptElement = await this.loadScriptElement(blobUrl);
83
272
  await this.waitForGetToken();
84
273
  const token = await this.getTokenFromScript();
85
274
  this.prefetchNextScript();
86
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;
87
291
  } finally {
88
- this.cleanup(blobUrl, scriptElement);
292
+ if (scriptElement) {
293
+ this.cleanup(blobUrl, scriptElement);
294
+ } else {
295
+ URL.revokeObjectURL(blobUrl);
296
+ }
89
297
  }
90
298
  }
91
299
  prefetchNextScript() {
@@ -95,16 +303,16 @@
95
303
  }
96
304
  }
97
305
  async waitForGetToken() {
98
- return new Promise((resolve) => {
306
+ return new Promise((resolve, reject) => {
99
307
  const checkInterval = setInterval(() => {
100
308
  if (typeof window !== "undefined" && typeof window.Deflect?.getToken === "function") {
101
309
  clearInterval(checkInterval);
102
310
  resolve();
103
311
  }
104
- }, 10);
312
+ }, 50);
105
313
  setTimeout(() => {
106
314
  clearInterval(checkInterval);
107
- resolve();
315
+ reject(new Error("Timeout: getToken function did not become available within 10 seconds"));
108
316
  }, 1e4);
109
317
  });
110
318
  }
@@ -139,46 +347,137 @@
139
347
  delete window.Deflect.getToken;
140
348
  }
141
349
  }
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
+ */
142
364
  configure(params) {
143
- if (!params.actionId?.trim()) {
144
- throw new Error("actionId is required and cannot be empty");
145
- }
146
- if (this.config?.actionId === params.actionId) {
147
- return;
148
- }
149
- this.hasWarmupError = false;
150
- this.scriptCache = null;
151
- this.config = { ...params };
152
- if (typeof window !== "undefined") {
153
- window.Deflect.actionId = params.actionId;
154
- }
155
- if (!this.isTestMode()) {
156
- this.tryWarmup();
365
+ try {
366
+ if (!params.actionId?.trim()) {
367
+ throw new Error("actionId is required and cannot be empty");
368
+ }
369
+ if (this.config?.actionId === params.actionId) {
370
+ return;
371
+ }
372
+ this.hasWarmupError = false;
373
+ this.scriptCache = null;
374
+ this.config = { ...params };
375
+ if (typeof window !== "undefined") {
376
+ window.Deflect.actionId = params.actionId;
377
+ }
378
+ if (!this.isTestMode()) {
379
+ this.tryWarmup();
380
+ }
381
+ } catch (error) {
382
+ const isUserError = error && typeof error === "object" && "isUserError" in error && error.isUserError === true;
383
+ this.reportError(
384
+ error,
385
+ {
386
+ deflect_sdk_error: "true",
387
+ deflect_user_error: isUserError ? "true" : "false",
388
+ method: "configure",
389
+ action_id: params?.actionId || "unknown"
390
+ },
391
+ {
392
+ actionId: params?.actionId,
393
+ hasCache: this.scriptCache !== null,
394
+ hasWarmupError: this.hasWarmupError
395
+ }
396
+ );
397
+ throw error;
157
398
  }
158
399
  }
159
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
+ }
160
404
  return this.config?.actionId === "t/FFFFFFFFFFFFF/111111111" || this.config?.actionId === "t/FFFFFFFFFFFFF/000000000";
161
405
  }
162
- // Deprecated name kept for backward compatibility
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
+ */
163
410
  async solveChallenge() {
164
411
  return this.getToken();
165
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
+ */
166
430
  async getToken() {
167
- if (!this.config?.actionId) {
168
- throw new Error("Must call configure() before solveChallenge()");
169
- }
170
- if (this.isTestMode()) {
171
- return "TESTTOKEN";
172
- }
173
- let script;
174
- if (this.scriptCache && !this.isWarmupInProgress) {
175
- script = this.scriptCache;
176
- this.scriptCache = null;
177
- } else {
178
- script = await this.fetchScript();
431
+ try {
432
+ if (!this.config?.actionId) {
433
+ throw new Error("Must call configure() before solveChallenge()");
434
+ }
435
+ if (this.isTestMode()) {
436
+ return "TESTTOKEN";
437
+ }
438
+ let script;
439
+ if (this.scriptCache && !this.isWarmupInProgress) {
440
+ script = this.scriptCache;
441
+ this.scriptCache = null;
442
+ } else {
443
+ script = await this.fetchScript();
444
+ }
445
+ return this.executeScript(script);
446
+ } catch (error) {
447
+ const isUserError = error && typeof error === "object" && "isUserError" in error && error.isUserError === true;
448
+ this.reportError(
449
+ error,
450
+ {
451
+ deflect_sdk_error: "true",
452
+ deflect_user_error: isUserError ? "true" : "false",
453
+ method: "getToken",
454
+ action_id: this.config?.actionId || "unknown",
455
+ has_cache: this.scriptCache !== null ? "true" : "false",
456
+ has_warmup_error: this.hasWarmupError ? "true" : "false",
457
+ stage: "call"
458
+ },
459
+ {
460
+ actionId: this.config?.actionId,
461
+ hasCache: this.scriptCache !== null,
462
+ hasWarmupError: this.hasWarmupError
463
+ }
464
+ );
465
+ throw error;
179
466
  }
180
- return this.executeScript(script);
181
467
  }
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
+ */
182
481
  async warmup() {
183
482
  if (!this.config?.actionId) {
184
483
  return false;
@@ -190,13 +489,27 @@
190
489
  return false;
191
490
  }
192
491
  }
492
+ /**
493
+ * Clears the cached challenge script.
494
+ * Useful when you need to force a fresh script fetch on the next getToken() call.
495
+ */
193
496
  clearCache() {
194
497
  this.scriptCache = null;
195
498
  }
196
499
  /**
197
- * Inject a fresh token as a hidden input into a form. Only accepts a submit event from onsubmit.
198
- * Usage: <form ... onsubmit="return Deflect.injectToken(event)">
199
- * Returns false to prevent double submit.
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
+ * ```
200
513
  */
201
514
  async injectToken(event) {
202
515
  if (!event || !event.target || !(event.target instanceof HTMLFormElement)) {
@@ -214,7 +527,6 @@
214
527
  hidden.value = token;
215
528
  form.appendChild(hidden);
216
529
  form.submit();
217
- return false;
218
530
  }
219
531
  };
220
532
  var DeflectInstance = new Deflect();
package/dist/index.min.js CHANGED
@@ -1 +1,2 @@
1
- "use strict";(()=>{var n=class{constructor(){this.config=null;this.scriptCache=null;this.isWarmupInProgress=!1;this.hasWarmupError=!1;this.initializeGlobalState(),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))}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}}}buildScriptUrl(t){let e=this.config?.scriptUrl||"https://js.deflect.bot/main.js",r=Date.now().toString();return`${e}?action_id=${t}&_=${r}`}async fetchScript(){if(!this.config?.actionId)throw new Error("actionId is required");let t=this.buildScriptUrl(this.config.actionId),e=await fetch(t,{cache:"no-store"});if(!e.ok)throw new Error(`Failed to fetch script: ${e.status}`);let r=await e.text();try{let i=JSON.parse(r);if(i.success===!1||i.error)throw new Error(i.error||"Script fetch failed")}catch(i){if(!(i instanceof SyntaxError))throw i}return{content:r,sessionId:e.headers.get("session_id")||void 0}}async executeScript(t){t.sessionId&&typeof window<"u"&&(window.Deflect.sessionId=t.sessionId);let e=this.createScriptBlob(t.content),r=await this.loadScriptElement(e);try{await this.waitForGetToken();let i=await this.getTokenFromScript();return this.prefetchNextScript(),i}finally{this.cleanup(e,r)}}prefetchNextScript(){this.hasWarmupError||this.tryWarmup().catch(()=>{})}async waitForGetToken(){return new Promise(t=>{let e=setInterval(()=>{typeof window<"u"&&typeof window.Deflect?.getToken=="function"&&(clearInterval(e),t())},10);setTimeout(()=>{clearInterval(e),t()},1e4)})}createScriptBlob(t){let e=new Blob([t],{type:"text/javascript"});return URL.createObjectURL(e)}async loadScriptElement(t){return new Promise((e,r)=>{let i=document.createElement("script");i.type="module",i.src=t,i.onload=()=>e(i),i.onerror=()=>r(new Error("Script failed to load")),document.head.appendChild(i)})}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(t){throw new Error(`Script execution failed: ${t}`)}}cleanup(t,e){URL.revokeObjectURL(t),e.remove(),typeof window<"u"&&window.Deflect?.getToken&&delete window.Deflect.getToken}configure(t){if(!t.actionId?.trim())throw new Error("actionId is required and cannot be empty");this.config?.actionId!==t.actionId&&(this.hasWarmupError=!1,this.scriptCache=null,this.config={...t},typeof window<"u"&&(window.Deflect.actionId=t.actionId),this.isTestMode()||this.tryWarmup())}isTestMode(){return this.config?.actionId==="t/FFFFFFFFFFFFF/111111111"||this.config?.actionId==="t/FFFFFFFFFFFFF/000000000"}async solveChallenge(){return this.getToken()}async getToken(){if(!this.config?.actionId)throw new Error("Must call configure() before solveChallenge()");if(this.isTestMode())return"TESTTOKEN";let t;return this.scriptCache&&!this.isWarmupInProgress?(t=this.scriptCache,this.scriptCache=null):t=await this.fetchScript(),this.executeScript(t)}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(t){if(!t||!t.target||!(t.target instanceof HTMLFormElement))throw new Error("injectToken: must be called from a form submit event");t.preventDefault();let e=t.target,r=await this.getToken();Array.from(e.querySelectorAll('input[name="deflect_token"]')).forEach(s=>s.remove());let i=document.createElement("input");return i.type="hidden",i.name="deflect_token",i.value=r,e.appendChild(i),e.submit(),!1}},o=new n;typeof window<"u"&&(window.Deflect=o);var a=o;})();
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()}},w=new u;typeof window<"u"&&(window.Deflect=w);var P=w;})();
@@ -0,0 +1,99 @@
1
+ export type PulseEventType = "error" | "span" | "transaction" | "log" | "metric";
2
+ export type PulseLevel = "fatal" | "error" | "warning" | "info" | "debug";
3
+ export interface PulseReporterConfig {
4
+ endpoint: string;
5
+ projectId: string;
6
+ deploymentId: string;
7
+ environment?: string;
8
+ service?: string;
9
+ release?: string;
10
+ }
11
+ export interface PulseSDKInfo {
12
+ name: string;
13
+ version: string;
14
+ language: string;
15
+ integrations?: string[];
16
+ }
17
+ export interface PulseStackFrame {
18
+ filename?: string;
19
+ abs_path?: string;
20
+ function?: string;
21
+ lineno?: number;
22
+ colno?: number;
23
+ in_app?: boolean;
24
+ context_line?: string;
25
+ }
26
+ export interface PulseExceptionPayload {
27
+ type?: string;
28
+ value?: string;
29
+ stacktrace?: {
30
+ frames: PulseStackFrame[];
31
+ };
32
+ }
33
+ export interface PulseRequestInfo {
34
+ method?: string;
35
+ url?: string;
36
+ headers?: Record<string, string>;
37
+ route?: string;
38
+ status_code?: number;
39
+ }
40
+ export interface PulseRuntimeInfo {
41
+ name?: string;
42
+ version?: string;
43
+ }
44
+ export interface PulseOSInfo {
45
+ name?: string;
46
+ version?: string;
47
+ }
48
+ export interface PulseDeviceInfo {
49
+ model?: string;
50
+ arch?: string;
51
+ }
52
+ export interface PulseErrorPayload {
53
+ level: PulseLevel;
54
+ message?: string;
55
+ exception?: PulseExceptionPayload;
56
+ culprit?: string;
57
+ request?: PulseRequestInfo;
58
+ runtime?: PulseRuntimeInfo;
59
+ os?: PulseOSInfo;
60
+ device?: PulseDeviceInfo;
61
+ }
62
+ export interface PulseEvent {
63
+ event_type: PulseEventType;
64
+ event_id: string;
65
+ deployment_id: string;
66
+ project_id: string;
67
+ environment?: string;
68
+ service?: string;
69
+ host?: string;
70
+ sdk?: PulseSDKInfo;
71
+ timestamp: string;
72
+ release?: string;
73
+ dist?: string;
74
+ deployment?: string;
75
+ tags?: Record<string, string>;
76
+ context?: Record<string, unknown>;
77
+ sample_rate?: number;
78
+ fingerprint?: string[];
79
+ ingest_key_id?: string;
80
+ payload_version?: number;
81
+ content_encoding?: string;
82
+ error?: PulseErrorPayload;
83
+ }
84
+ export interface PulseCaptureOptions {
85
+ tags?: Record<string, string>;
86
+ context?: Record<string, unknown>;
87
+ level?: PulseLevel;
88
+ }
89
+ export declare class PulseReporter {
90
+ private readonly config;
91
+ private readonly enabled;
92
+ constructor(config?: Partial<PulseReporterConfig>);
93
+ captureException(error: unknown, options?: PulseCaptureOptions): void;
94
+ private buildErrorEvent;
95
+ private normalizeError;
96
+ private buildExceptionPayload;
97
+ private parseStack;
98
+ }
99
+ export default PulseReporter;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deflectbot/deflect-sdk",
3
- "version": "1.3.7",
3
+ "version": "1.4.0",
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",
@@ -24,13 +24,16 @@
24
24
  "license": "MIT",
25
25
  "devDependencies": {
26
26
  "@eslint/js": "^9.22.0",
27
+ "esbuild": "^0.23.0",
27
28
  "eslint": "^9.22.0",
28
29
  "globals": "^16.0.0",
29
30
  "typescript": "^5.7.3",
30
- "typescript-eslint": "^8.27.0",
31
- "esbuild": "^0.23.0"
31
+ "typescript-eslint": "^8.27.0"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"
35
+ },
36
+ "dependencies": {
37
+ "@sentry/browser": "^10.22.0"
35
38
  }
36
39
  }