@deflectbot/deflect-sdk 1.2.2 → 1.3.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/README.md CHANGED
@@ -1,50 +1,78 @@
1
- # Deflect -
1
+ # Deflect SDK (Client-side)
2
2
 
3
3
  ![npm](https://img.shields.io/npm/v/@deflectbot/deflect-sdk)
4
4
  ![downloads](https://img.shields.io/npm/dm/@deflectbot/deflect-sdk)
5
5
 
6
- Deflect is an antibot solution that works with Vue, React, NextJS, and other JavaScript frameworks.
6
+ [Deflect](https://deflect.bot) is an advanced antibot engine that protects your site from bots on any scale.
7
7
 
8
- ---
8
+ ## Usage
9
9
 
10
- ## CDN Install (Auto-Updates)
10
+ See the [documentation](https://docs.deflect.bot/) for how to use this SDK with the Deflect APIs.
11
+
12
+ ## Installation
13
+
14
+ #### yarn
11
15
 
12
16
  ```bash
13
- Include in your project:
14
- <script src="https://cdn.jsdelivr.net/npm/@deflectbot/deflect-sdk/dist/index.min.js"></script>
17
+ yarn add @castleio/sdk
15
18
  ```
16
19
 
17
- ---
18
-
19
- ## NPM Install (Requires Manual Updates)
20
+ #### npm
20
21
 
21
22
  ```bash
22
- npm install deflect
23
+ npm install --save @castleio/sdk
23
24
  ```
24
25
 
25
- ---
26
+ #### CDN
26
27
 
27
- ## Setup
28
+ Use `@deflect-sdk@latest` to always use the latest version, or specify a version with `@deflect-sdk@x.x.x`.
28
29
 
29
- ```ts
30
- import Deflect from '@deflectbot/deflect-sdk';
30
+ ```html
31
+ <script src="https://cdn.jsdelivr.net/npm/@deflectbot/deflect-sdk@latest/dist/index.min.js" />
32
+ ```
31
33
 
34
+ ## Configuration
35
+
36
+ Deflect works through Defense Actions™, which allows you to either setup a global configuration one-time to work across your entire application, or individual and more customized Defense Actions™ for each protected endpoint.
37
+
38
+ #### One-time global initialization.
39
+
40
+ ```ts
32
41
  Deflect.configure({
33
- actionId: 'YOUR_ACTION_ID',
34
- extraArgs: { mode: 'strict' }, // Optional
35
- forceRefresh: true, // Optional
42
+ actionId: "<ACTION_ID>",
36
43
  });
37
44
  ```
38
45
 
39
- ---
40
-
41
- ## Solve Challenge
46
+ #### To get a token to pass to a protected endpoint
42
47
 
43
48
  ```ts
44
- try {
45
- const token = await Deflect.solveChallenge();
46
- console.log('✅ Token received:', token);
47
- } catch (error) {
48
- console.error('❌ Failed to solve challenge:', error);
49
- }
49
+ const token = await Deflect.getToken(); // returns token as string
50
+ ```
51
+
52
+ #### Helper for form submit
53
+
54
+ If you're using forms like this:
55
+
56
+ ```html
57
+ <form action="/login" method="POST">
58
+ <input name="username" />
59
+ <input name="password" type="password" />
60
+ <button type="submit">Login</button>
61
+ </form>
50
62
  ```
63
+
64
+ You can use the helper as shown:
65
+
66
+ ```html
67
+ <form
68
+ action="/login"
69
+ method="POST"
70
+ onsubmit="return Deflect.injectToken(event)"
71
+ >
72
+ <input name="username" />
73
+ <input name="password" type="password" />
74
+ <button type="submit">Login</button>
75
+ </form>
76
+ ```
77
+
78
+ `injectToken()` will fetch a token and insert it as a hidden input named `deflect_token` into your form. Use it directly in your form's onsubmit attribute:
@@ -0,0 +1 @@
1
+ "use strict";var Deflect=(()=>{var c=Object.defineProperty;var l=Object.getOwnPropertyDescriptor;var d=Object.getOwnPropertyNames;var p=Object.prototype.hasOwnProperty;var f=(n,e)=>{for(var t in e)c(n,t,{get:e[t],enumerable:!0})},u=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of d(e))!p.call(n,i)&&i!==t&&c(n,i,{get:()=>e[i],enumerable:!(r=l(e,i))||r.enumerable});return n};var h=n=>u(c({},"__esModule",{value:!0}),n);var m={};f(m,{default:()=>w});var s=class{constructor(){this.config=null;this.scriptCache=null;this.isWarmupInProgress=!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.isWarmupInProgress=!0;try{this.scriptCache=await this.fetchScript()}catch{}finally{this.isWarmupInProgress=!1}}}buildScriptUrl(e){let t=this.config?.scriptUrl||"https://js.deflect.bot/main.js";if(this.config?.scriptUrl)return t;let r=Date.now().toString();return`${t}?action_id=${e}&_=${r}`}async fetchScript(){if(!this.config?.actionId)throw new Error("actionId is required");let e=this.buildScriptUrl(this.config.actionId),t=await fetch(e,{cache:"no-store"});if(!t.ok)throw new Error(`Failed to fetch script: ${t.status}`);return{content:await t.text(),sessionId:t.headers.get("session_id")||void 0}}async executeScript(e){e.sessionId&&typeof window<"u"&&(window.Deflect.sessionId=e.sessionId);let t=this.createReadyPromise(),r=this.createScriptBlob(e.content),i=await this.loadScriptElement(r);try{await t;let o=await this.getTokenFromScript();return this.prefetchNextScript(),o}finally{this.cleanup(r,i)}}prefetchNextScript(){this.tryWarmup().catch(()=>{})}createReadyPromise(){return new Promise(e=>{typeof window<"u"&&(window.Deflect.ready={promise:new Promise(t=>{window.Deflect.ready.resolve=t}),resolve:()=>e()})})}createScriptBlob(e){let t=new Blob([e],{type:"text/javascript"});return URL.createObjectURL(t)}async loadScriptElement(e){return new Promise((t,r)=>{let i=document.createElement("script");i.type="module",i.src=e,i.onload=()=>t(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(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){if(!e.actionId?.trim())throw new Error("actionId is required and cannot be empty");this.config={...e},typeof window<"u"&&(window.Deflect.actionId=e.actionId),this.tryWarmup()}async solveChallenge(){return this.getToken()}async getToken(){if(!this.config?.actionId)throw new Error("Must call configure() before solveChallenge()");let e;return this.scriptCache&&!this.isWarmupInProgress?(e=this.scriptCache,this.scriptCache=null):e=await this.fetchScript(),this.executeScript(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,r=await this.getToken();Array.from(t.querySelectorAll('input[name="deflect_token"]')).forEach(o=>o.remove());let i=document.createElement("input");return i.type="hidden",i.name="deflect_token",i.value=r,t.appendChild(i),t.submit(),!1}},a=new s;typeof window<"u"&&(window.Deflect=a);var w=a;return h(m);})();
package/dist/global.js ADDED
@@ -0,0 +1,7 @@
1
+ // Re-export the default instance while also assigning it to window.Deflect for non-module usage.
2
+ import DeflectInstance from "./index";
3
+ if (typeof window !== "undefined") {
4
+ // Avoid overwriting if a user already attached something custom.
5
+ window.Deflect = DeflectInstance;
6
+ }
7
+ export default DeflectInstance;
package/dist/index.js CHANGED
@@ -1,108 +1,226 @@
1
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
- return new (P || (P = Promise))(function (resolve, reject) {
4
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
- step((generator = generator.apply(thisArg, _arguments || [])).next());
8
- });
9
- };
10
- class Deflect {
11
- constructor() {
12
- this._prefetchedScriptText = null;
13
- this._hasUsedPrefetch = false;
14
- window.Deflect = window.Deflect || {};
15
- window.Deflect.extraArgs = window.Deflect.extraArgs || {};
16
- if (typeof window !== "undefined") {
17
- window.addEventListener("load", () => {
18
- setTimeout(() => {
19
- this._warmupScript();
20
- }, 500);
21
- });
22
- }
23
- }
24
- _warmupScript() {
25
- if (!window.Deflect.actionId)
26
- return;
27
- const nonce = Date.now().toString();
28
- const scriptUrl = `https://js.deflect.bot/main.js?action_id=${window.Deflect.actionId}&_=${nonce}`;
29
- fetch(scriptUrl, { cache: "no-store" })
30
- .then((res) => __awaiter(this, void 0, void 0, function* () {
31
- if (!res.ok)
32
- return;
33
- this._prefetchedScriptText = yield res.text();
34
- }))
35
- .catch(() => { });
36
- }
37
- setupReady() {
38
- let resolveFn;
39
- const promise = new Promise((resolve) => {
40
- resolveFn = resolve;
41
- });
42
- window.Deflect.ready = {
43
- promise,
44
- resolve: () => resolveFn(),
45
- };
46
- }
47
- configure(params) {
48
- if (!params.actionId) {
49
- throw new Error("actionId is required in configuration");
50
- }
51
- window.Deflect.actionId = params.actionId;
52
- }
53
- solveChallenge() {
54
- return __awaiter(this, void 0, void 0, function* () {
55
- var _a;
56
- if (!window.Deflect.actionId) {
57
- throw new Error("actionId is missing in configuration");
58
- }
59
- this.setupReady();
60
- let scriptText;
61
- let sessionId = null;
62
- if (this._prefetchedScriptText && !this._hasUsedPrefetch) {
63
- scriptText = this._prefetchedScriptText;
64
- this._hasUsedPrefetch = true;
65
- }
66
- else {
67
- const nonce = Date.now().toString();
68
- const scriptUrl = `https://js.deflect.bot/main.js?action_id=${window.Deflect.actionId}&_=${nonce}`;
69
- const response = yield fetch(scriptUrl, { cache: "no-store" });
70
- if (!response.ok) {
71
- throw new Error("Failed to fetch the Deflect script");
72
- }
73
- sessionId = response.headers.get("session_id");
74
- scriptText = yield response.text();
75
- }
76
- if (sessionId) {
77
- window.Deflect.sessionId = sessionId;
78
- }
79
- const blob = new Blob([scriptText], { type: "text/javascript" });
80
- const blobUrl = URL.createObjectURL(blob);
81
- const scriptEl = document.createElement("script");
82
- scriptEl.type = "module";
83
- scriptEl.src = blobUrl;
84
- document.head.appendChild(scriptEl);
85
- yield new Promise((resolve, reject) => {
86
- scriptEl.onload = () => resolve();
87
- scriptEl.onerror = () => reject("Failed to load the Deflect script");
88
- });
89
- yield ((_a = window.Deflect.ready) === null || _a === void 0 ? void 0 : _a.promise);
90
- if (typeof window.Deflect === "undefined" ||
91
- typeof window.Deflect.getToken !== "function") {
92
- throw new Error("Deflect script did not load properly");
93
- }
94
- let token;
95
- try {
96
- token = yield window.Deflect.getToken();
97
- }
98
- catch (error) {
99
- throw new Error(`Deflect script execution failed: ${error}`);
100
- }
101
- URL.revokeObjectURL(blobUrl);
102
- scriptEl.remove();
103
- delete window.Deflect.getToken;
104
- return token;
105
- });
106
- }
107
- }
108
- export default new Deflect();
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ class Deflect {
11
+ constructor() {
12
+ this.config = null;
13
+ this.scriptCache = null;
14
+ this.isWarmupInProgress = false;
15
+ this.initializeGlobalState();
16
+ this.setupAutomaticWarmup();
17
+ }
18
+ initializeGlobalState() {
19
+ if (typeof window === "undefined")
20
+ return;
21
+ window.Deflect = window.Deflect || {};
22
+ }
23
+ setupAutomaticWarmup() {
24
+ if (typeof window === "undefined")
25
+ return;
26
+ if (document.readyState === "loading") {
27
+ document.addEventListener("DOMContentLoaded", () => this.tryWarmup());
28
+ }
29
+ else {
30
+ setTimeout(() => this.tryWarmup(), 100);
31
+ }
32
+ }
33
+ tryWarmup() {
34
+ return __awaiter(this, void 0, void 0, function* () {
35
+ var _a;
36
+ if (!((_a = this.config) === null || _a === void 0 ? void 0 : _a.actionId) || this.isWarmupInProgress || this.scriptCache) {
37
+ return;
38
+ }
39
+ this.isWarmupInProgress = true;
40
+ try {
41
+ this.scriptCache = yield this.fetchScript();
42
+ }
43
+ catch (_b) {
44
+ /* empty */
45
+ }
46
+ finally {
47
+ this.isWarmupInProgress = false;
48
+ }
49
+ });
50
+ }
51
+ buildScriptUrl(actionId) {
52
+ var _a, _b;
53
+ const baseUrl = ((_a = this.config) === null || _a === void 0 ? void 0 : _a.scriptUrl) || "https://js.deflect.bot/main.js";
54
+ if ((_b = this.config) === null || _b === void 0 ? void 0 : _b.scriptUrl) {
55
+ return baseUrl;
56
+ }
57
+ const nonce = Date.now().toString();
58
+ return `${baseUrl}?action_id=${actionId}&_=${nonce}`;
59
+ }
60
+ fetchScript() {
61
+ return __awaiter(this, void 0, void 0, function* () {
62
+ var _a;
63
+ if (!((_a = this.config) === null || _a === void 0 ? void 0 : _a.actionId)) {
64
+ throw new Error("actionId is required");
65
+ }
66
+ const url = this.buildScriptUrl(this.config.actionId);
67
+ const response = yield fetch(url, { cache: "no-store" });
68
+ if (!response.ok) {
69
+ throw new Error(`Failed to fetch script: ${response.status}`);
70
+ }
71
+ return {
72
+ content: yield response.text(),
73
+ sessionId: response.headers.get("session_id") || undefined,
74
+ };
75
+ });
76
+ }
77
+ executeScript(script) {
78
+ return __awaiter(this, void 0, void 0, function* () {
79
+ if (script.sessionId && typeof window !== "undefined") {
80
+ window.Deflect.sessionId = script.sessionId;
81
+ }
82
+ const readyPromise = this.createReadyPromise();
83
+ const blobUrl = this.createScriptBlob(script.content);
84
+ const scriptElement = yield this.loadScriptElement(blobUrl);
85
+ try {
86
+ yield readyPromise;
87
+ const token = yield this.getTokenFromScript();
88
+ this.prefetchNextScript();
89
+ return token;
90
+ }
91
+ finally {
92
+ this.cleanup(blobUrl, scriptElement);
93
+ }
94
+ });
95
+ }
96
+ prefetchNextScript() {
97
+ this.tryWarmup().catch(() => { });
98
+ }
99
+ createReadyPromise() {
100
+ return new Promise((resolve) => {
101
+ if (typeof window !== "undefined") {
102
+ window.Deflect.ready = {
103
+ promise: new Promise((innerResolve) => {
104
+ window.Deflect.ready.resolve = innerResolve;
105
+ }),
106
+ resolve: () => resolve(),
107
+ };
108
+ }
109
+ });
110
+ }
111
+ createScriptBlob(content) {
112
+ const blob = new Blob([content], { type: "text/javascript" });
113
+ return URL.createObjectURL(blob);
114
+ }
115
+ loadScriptElement(blobUrl) {
116
+ return __awaiter(this, void 0, void 0, function* () {
117
+ return new Promise((resolve, reject) => {
118
+ const script = document.createElement("script");
119
+ script.type = "module";
120
+ script.src = blobUrl;
121
+ script.onload = () => resolve(script);
122
+ script.onerror = () => reject(new Error("Script failed to load"));
123
+ document.head.appendChild(script);
124
+ });
125
+ });
126
+ }
127
+ getTokenFromScript() {
128
+ return __awaiter(this, void 0, void 0, function* () {
129
+ var _a;
130
+ if (typeof window === "undefined" ||
131
+ typeof ((_a = window.Deflect) === null || _a === void 0 ? void 0 : _a.getToken) !== "function") {
132
+ throw new Error("Script did not load properly - getToken not available");
133
+ }
134
+ try {
135
+ return yield window.Deflect.getToken();
136
+ }
137
+ catch (error) {
138
+ throw new Error(`Script execution failed: ${error}`);
139
+ }
140
+ });
141
+ }
142
+ cleanup(blobUrl, scriptElement) {
143
+ var _a;
144
+ URL.revokeObjectURL(blobUrl);
145
+ scriptElement.remove();
146
+ if (typeof window !== "undefined" && ((_a = window.Deflect) === null || _a === void 0 ? void 0 : _a.getToken)) {
147
+ delete window.Deflect.getToken;
148
+ }
149
+ }
150
+ configure(params) {
151
+ var _a;
152
+ if (!((_a = params.actionId) === null || _a === void 0 ? void 0 : _a.trim())) {
153
+ throw new Error("actionId is required and cannot be empty");
154
+ }
155
+ this.config = Object.assign({}, params);
156
+ if (typeof window !== "undefined") {
157
+ window.Deflect.actionId = params.actionId;
158
+ }
159
+ this.tryWarmup();
160
+ }
161
+ // Deprecated name kept for backward compatibility
162
+ solveChallenge() {
163
+ return __awaiter(this, void 0, void 0, function* () {
164
+ return this.getToken();
165
+ });
166
+ }
167
+ getToken() {
168
+ return __awaiter(this, void 0, void 0, function* () {
169
+ var _a;
170
+ if (!((_a = this.config) === null || _a === void 0 ? void 0 : _a.actionId)) {
171
+ throw new Error("Must call configure() before solveChallenge()");
172
+ }
173
+ let script;
174
+ if (this.scriptCache && !this.isWarmupInProgress) {
175
+ script = this.scriptCache;
176
+ this.scriptCache = null;
177
+ }
178
+ else {
179
+ script = yield this.fetchScript();
180
+ }
181
+ return this.executeScript(script);
182
+ });
183
+ }
184
+ warmup() {
185
+ return __awaiter(this, void 0, void 0, function* () {
186
+ var _a;
187
+ if (!((_a = this.config) === null || _a === void 0 ? void 0 : _a.actionId)) {
188
+ return false;
189
+ }
190
+ try {
191
+ yield this.tryWarmup();
192
+ return this.scriptCache !== null;
193
+ }
194
+ catch (_b) {
195
+ return false;
196
+ }
197
+ });
198
+ }
199
+ clearCache() {
200
+ this.scriptCache = null;
201
+ }
202
+ /**
203
+ * Inject a fresh token as a hidden input into a form. Only accepts a submit event from onsubmit.
204
+ * Usage: <form ... onsubmit="return Deflect.injectToken(event)">
205
+ * Returns false to prevent double submit.
206
+ */
207
+ injectToken(event) {
208
+ return __awaiter(this, void 0, void 0, function* () {
209
+ if (!event || !event.target || !(event.target instanceof HTMLFormElement)) {
210
+ throw new Error("injectToken: must be called from a form submit event");
211
+ }
212
+ event.preventDefault();
213
+ const form = event.target;
214
+ const token = yield this.getToken();
215
+ Array.from(form.querySelectorAll('input[name="deflect_token"]')).forEach((el) => el.remove());
216
+ const hidden = document.createElement("input");
217
+ hidden.type = "hidden";
218
+ hidden.name = "deflect_token";
219
+ hidden.value = token;
220
+ form.appendChild(hidden);
221
+ form.submit();
222
+ return false;
223
+ });
224
+ }
225
+ }
226
+ export default new Deflect();
@@ -0,0 +1,7 @@
1
+ import DeflectInstance from "./index";
2
+ declare global {
3
+ interface Window {
4
+ Deflect: any;
5
+ }
6
+ }
7
+ export default DeflectInstance;
@@ -1,17 +1,35 @@
1
- interface DeflectConfig {
2
- actionId: string;
3
- extraArgs?: {
4
- [key: string]: string;
5
- };
6
- }
7
- declare class Deflect {
8
- private _prefetchedScriptText;
9
- private _hasUsedPrefetch;
10
- constructor();
11
- private _warmupScript;
12
- private setupReady;
13
- configure(params: DeflectConfig): void;
14
- solveChallenge(): Promise<string>;
15
- }
16
- declare const _default: Deflect;
17
- export default _default;
1
+ interface DeflectConfig {
2
+ actionId: string;
3
+ scriptUrl?: string;
4
+ }
5
+ declare class Deflect {
6
+ private config;
7
+ private scriptCache;
8
+ private isWarmupInProgress;
9
+ constructor();
10
+ private initializeGlobalState;
11
+ private setupAutomaticWarmup;
12
+ private tryWarmup;
13
+ private buildScriptUrl;
14
+ private fetchScript;
15
+ private executeScript;
16
+ private prefetchNextScript;
17
+ private createReadyPromise;
18
+ private createScriptBlob;
19
+ private loadScriptElement;
20
+ private getTokenFromScript;
21
+ private cleanup;
22
+ configure(params: DeflectConfig): void;
23
+ solveChallenge(): Promise<string>;
24
+ getToken(): Promise<string>;
25
+ warmup(): Promise<boolean>;
26
+ clearCache(): void;
27
+ /**
28
+ * Inject a fresh token as a hidden input into a form. Only accepts a submit event from onsubmit.
29
+ * Usage: <form ... onsubmit="return Deflect.injectToken(event)">
30
+ * Returns false to prevent double submit.
31
+ */
32
+ injectToken(event: SubmitEvent): Promise<false>;
33
+ }
34
+ declare const _default: Deflect;
35
+ export default _default;
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@deflectbot/deflect-sdk",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "SDK for deflect.bot - Use it for seamless captcha integration on any website.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/types/index.d.ts",
7
7
  "scripts": {
8
- "build": "tsc"
8
+ "build": "tsc && npm run build:global",
9
+ "build:global": "esbuild src/global.ts --bundle --format=iife --global-name=Deflect --minify --outfile=dist/deflect.global.min.js"
9
10
  },
10
11
  "author": "Deflect",
11
12
  "license": "MIT",
@@ -14,12 +15,11 @@
14
15
  "eslint": "^9.22.0",
15
16
  "globals": "^16.0.0",
16
17
  "typescript": "^5.7.3",
17
- "typescript-eslint": "^8.27.0"
18
+ "typescript-eslint": "^8.27.0",
19
+ "esbuild": "^0.23.0"
18
20
  },
19
21
  "publishConfig": {
20
22
  "access": "public"
21
23
  },
22
- "dependencies": {
23
- "@deflectbot/deflect-sdk": "file:"
24
- }
24
+ "dependencies": {}
25
25
  }
package/src/global.ts ADDED
@@ -0,0 +1,18 @@
1
+ // Re-export the default instance while also assigning it to window.Deflect for non-module usage.
2
+ import DeflectInstance from "./index";
3
+
4
+ // Ensure side effects (constructor) already ran on import.
5
+ // Attach to window for classic script tag usage.
6
+ declare global {
7
+ interface Window {
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK attaches dynamic shape at runtime
9
+ Deflect: any;
10
+ }
11
+ }
12
+
13
+ if (typeof window !== "undefined") {
14
+ // Avoid overwriting if a user already attached something custom.
15
+ window.Deflect = DeflectInstance;
16
+ }
17
+
18
+ export default DeflectInstance;
package/src/index.ts CHANGED
@@ -1,147 +1,237 @@
1
1
  interface DeflectConfig {
2
2
  actionId: string;
3
3
  scriptUrl?: string;
4
- extraArgs?: { [key: string]: string };
4
+ }
5
+
6
+ interface DeflectScript {
7
+ sessionId?: string;
8
+ content: string;
5
9
  }
6
10
 
7
11
  class Deflect {
8
- private _prefetchedScriptText: string | null = null;
9
- private _hasUsedPrefetch = false;
10
- private _customScriptUrl: string | null = null;
12
+ private config: DeflectConfig | null = null;
13
+ private scriptCache: DeflectScript | null = null;
14
+ private isWarmupInProgress = false;
11
15
 
12
16
  constructor() {
17
+ this.initializeGlobalState();
18
+ this.setupAutomaticWarmup();
19
+ }
20
+
21
+ private initializeGlobalState(): void {
22
+ if (typeof window === "undefined") return;
13
23
  window.Deflect = window.Deflect || {};
14
- window.Deflect.extraArgs = window.Deflect.extraArgs || {};
24
+ }
15
25
 
16
- if (typeof window !== "undefined") {
17
- window.addEventListener("load", () => {
18
- setTimeout(() => {
19
- this._warmupScript();
20
- }, 500);
21
- });
26
+ private setupAutomaticWarmup(): void {
27
+ if (typeof window === "undefined") return;
28
+
29
+ if (document.readyState === "loading") {
30
+ document.addEventListener("DOMContentLoaded", () => this.tryWarmup());
31
+ } else {
32
+ setTimeout(() => this.tryWarmup(), 100);
22
33
  }
23
34
  }
24
35
 
25
- private _getScriptUrl(): string {
26
- const nonce = Date.now().toString();
27
- if (this._customScriptUrl) {
28
- const separator = this._customScriptUrl.includes("?") ? "&" : "?";
29
- return `${this._customScriptUrl}${separator}action_id=${window.Deflect.actionId}&_=${nonce}`;
36
+ private async tryWarmup(): Promise<void> {
37
+ if (!this.config?.actionId || this.isWarmupInProgress || this.scriptCache) {
38
+ return;
30
39
  }
31
- return `https://js.deflect.bot/main.js?action_id=${window.Deflect.actionId}&_=${nonce}`;
32
- }
33
40
 
34
- private _warmupScript(): void {
35
- if (!window.Deflect.actionId) return;
41
+ this.isWarmupInProgress = true;
36
42
 
37
- const scriptUrl = this._getScriptUrl();
38
- fetch(scriptUrl, { cache: "no-store" })
39
- .then(async (res) => {
40
- if (!res.ok) return;
41
- this._prefetchedScriptText = await res.text();
42
- })
43
- .catch(() => {});
43
+ try {
44
+ this.scriptCache = await this.fetchScript();
45
+ } catch {
46
+ /* empty */
47
+ } finally {
48
+ this.isWarmupInProgress = false;
49
+ }
44
50
  }
45
51
 
46
- private _refetchScript(): void {
47
- this._hasUsedPrefetch = false;
48
- this._prefetchedScriptText = null;
52
+ private buildScriptUrl(actionId: string): string {
53
+ const baseUrl = this.config?.scriptUrl || "https://js.deflect.bot/main.js";
49
54
 
50
- setTimeout(() => {
51
- this._warmupScript();
52
- }, 100);
53
- }
55
+ if (this.config?.scriptUrl) {
56
+ return baseUrl;
57
+ }
54
58
 
55
- private setupReady(): void {
56
- let resolveFn: () => void;
57
- const promise = new Promise<void>((resolve) => {
58
- resolveFn = resolve;
59
- });
60
- window.Deflect.ready = {
61
- promise,
62
- resolve: () => resolveFn(),
63
- };
59
+ const nonce = Date.now().toString();
60
+ return `${baseUrl}?action_id=${actionId}&_=${nonce}`;
64
61
  }
65
62
 
66
- configure(params: DeflectConfig): void {
67
- if (!params.actionId) {
68
- throw new Error("actionId is required in configuration");
63
+ private async fetchScript(): Promise<DeflectScript> {
64
+ if (!this.config?.actionId) {
65
+ throw new Error("actionId is required");
69
66
  }
70
- window.Deflect.actionId = params.actionId;
71
67
 
72
- if (params.scriptUrl) {
73
- this._customScriptUrl = params.scriptUrl;
68
+ const url = this.buildScriptUrl(this.config.actionId);
69
+ const response = await fetch(url, { cache: "no-store" });
70
+
71
+ if (!response.ok) {
72
+ throw new Error(`Failed to fetch script: ${response.status}`);
74
73
  }
74
+
75
+ return {
76
+ content: await response.text(),
77
+ sessionId: response.headers.get("session_id") || undefined,
78
+ };
75
79
  }
76
80
 
77
- async solveChallenge(): Promise<string> {
78
- if (!window.Deflect.actionId) {
79
- throw new Error("actionId is missing in configuration");
81
+ private async executeScript(script: DeflectScript): Promise<string> {
82
+ if (script.sessionId && typeof window !== "undefined") {
83
+ window.Deflect.sessionId = script.sessionId;
80
84
  }
81
85
 
82
- this.setupReady();
86
+ const readyPromise = this.createReadyPromise();
83
87
 
84
- let scriptText: string;
85
- let sessionId: string | null = null;
88
+ const blobUrl = this.createScriptBlob(script.content);
89
+ const scriptElement = await this.loadScriptElement(blobUrl);
86
90
 
87
- if (this._prefetchedScriptText && !this._hasUsedPrefetch) {
88
- scriptText = this._prefetchedScriptText;
89
- this._hasUsedPrefetch = true;
90
- } else {
91
- const scriptUrl = this._getScriptUrl();
92
- const response = await fetch(scriptUrl, { cache: "no-store" });
91
+ try {
92
+ await readyPromise;
93
93
 
94
- if (!response.ok) {
95
- throw new Error("Failed to fetch the Deflect script");
96
- }
94
+ const token = await this.getTokenFromScript();
97
95
 
98
- sessionId = response.headers.get("session_id");
99
- scriptText = await response.text();
100
- }
96
+ this.prefetchNextScript();
101
97
 
102
- if (sessionId) {
103
- window.Deflect.sessionId = sessionId;
98
+ return token;
99
+ } finally {
100
+ this.cleanup(blobUrl, scriptElement);
104
101
  }
102
+ }
105
103
 
106
- const blob = new Blob([scriptText], { type: "text/javascript" });
107
- const blobUrl = URL.createObjectURL(blob);
104
+ private prefetchNextScript(): void {
105
+ this.tryWarmup().catch(() => {});
106
+ }
108
107
 
109
- const scriptEl = document.createElement("script");
110
- scriptEl.type = "module";
111
- scriptEl.src = blobUrl;
108
+ private createReadyPromise(): Promise<void> {
109
+ return new Promise<void>((resolve) => {
110
+ if (typeof window !== "undefined") {
111
+ window.Deflect.ready = {
112
+ promise: new Promise<void>((innerResolve) => {
113
+ window.Deflect.ready.resolve = innerResolve;
114
+ }),
115
+ resolve: () => resolve(),
116
+ };
117
+ }
118
+ });
119
+ }
112
120
 
113
- document.head.appendChild(scriptEl);
121
+ private createScriptBlob(content: string): string {
122
+ const blob = new Blob([content], { type: "text/javascript" });
123
+ return URL.createObjectURL(blob);
124
+ }
114
125
 
115
- await new Promise<void>((resolve, reject) => {
116
- scriptEl.onload = () => resolve();
117
- scriptEl.onerror = () => reject("Failed to load the Deflect script");
118
- });
126
+ private async loadScriptElement(blobUrl: string): Promise<HTMLScriptElement> {
127
+ return new Promise((resolve, reject) => {
128
+ const script = document.createElement("script");
129
+ script.type = "module";
130
+ script.src = blobUrl;
119
131
 
120
- await window.Deflect.ready?.promise;
132
+ script.onload = () => resolve(script);
133
+ script.onerror = () => reject(new Error("Script failed to load"));
121
134
 
135
+ document.head.appendChild(script);
136
+ });
137
+ }
138
+
139
+ private async getTokenFromScript(): Promise<string> {
122
140
  if (
123
- typeof window.Deflect === "undefined" ||
124
- typeof window.Deflect.getToken !== "function"
141
+ typeof window === "undefined" ||
142
+ typeof window.Deflect?.getToken !== "function"
125
143
  ) {
126
- throw new Error("Deflect script did not load properly");
144
+ throw new Error("Script did not load properly - getToken not available");
127
145
  }
128
146
 
129
- let token: string;
130
-
131
147
  try {
132
- token = await window.Deflect.getToken();
148
+ return await window.Deflect.getToken();
133
149
  } catch (error) {
134
- throw new Error(`Deflect script execution failed: ${error}`);
150
+ throw new Error(`Script execution failed: ${error}`);
135
151
  }
152
+ }
136
153
 
137
- this._refetchScript();
138
-
154
+ private cleanup(blobUrl: string, scriptElement: HTMLScriptElement): void {
139
155
  URL.revokeObjectURL(blobUrl);
140
- scriptEl.remove();
156
+ scriptElement.remove();
157
+
158
+ if (typeof window !== "undefined" && window.Deflect?.getToken) {
159
+ delete window.Deflect.getToken;
160
+ }
161
+ }
141
162
 
142
- delete window.Deflect.getToken;
163
+ public configure(params: DeflectConfig): void {
164
+ if (!params.actionId?.trim()) {
165
+ throw new Error("actionId is required and cannot be empty");
166
+ }
167
+ this.config = { ...params };
168
+ if (typeof window !== "undefined") {
169
+ window.Deflect.actionId = params.actionId;
170
+ }
171
+ this.tryWarmup();
172
+ }
173
+
174
+ // Deprecated name kept for backward compatibility
175
+ public async solveChallenge(): Promise<string> {
176
+ return this.getToken();
177
+ }
178
+
179
+ public async getToken(): Promise<string> {
180
+ if (!this.config?.actionId) {
181
+ throw new Error("Must call configure() before solveChallenge()");
182
+ }
143
183
 
144
- return token;
184
+ let script: DeflectScript;
185
+
186
+ if (this.scriptCache && !this.isWarmupInProgress) {
187
+ script = this.scriptCache;
188
+ this.scriptCache = null;
189
+ } else {
190
+ script = await this.fetchScript();
191
+ }
192
+
193
+ return this.executeScript(script);
194
+ }
195
+
196
+ public async warmup(): Promise<boolean> {
197
+ if (!this.config?.actionId) {
198
+ return false;
199
+ }
200
+
201
+ try {
202
+ await this.tryWarmup();
203
+ return this.scriptCache !== null;
204
+ } catch {
205
+ return false;
206
+ }
207
+ }
208
+
209
+ public clearCache(): void {
210
+ this.scriptCache = null;
211
+ }
212
+
213
+ /**
214
+ * Inject a fresh token as a hidden input into a form. Only accepts a submit event from onsubmit.
215
+ * Usage: <form ... onsubmit="return Deflect.injectToken(event)">
216
+ * Returns false to prevent double submit.
217
+ */
218
+ public async injectToken(event: SubmitEvent): Promise<false> {
219
+ if (!event || !event.target || !(event.target instanceof HTMLFormElement)) {
220
+ throw new Error("injectToken: must be called from a form submit event");
221
+ }
222
+ event.preventDefault();
223
+ const form = event.target;
224
+ const token = await this.getToken();
225
+ Array.from(form.querySelectorAll('input[name="deflect_token"]')).forEach(
226
+ (el) => el.remove()
227
+ );
228
+ const hidden = document.createElement("input");
229
+ hidden.type = "hidden";
230
+ hidden.name = "deflect_token";
231
+ hidden.value = token;
232
+ form.appendChild(hidden);
233
+ form.submit();
234
+ return false;
145
235
  }
146
236
  }
147
237
 
@@ -1,3 +1,6 @@
1
+ // Global window augmentation for Deflect SDK.
2
+ // Using 'any' here keeps backward compatibility and avoids circular type issues.
1
3
  interface Window {
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK attaches dynamic shape at runtime
2
5
  Deflect: any;
3
6
  }