gate-protect-widget 1.2.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 ADDED
@@ -0,0 +1,323 @@
1
+ # 🛡️ PaywallProtect Widget
2
+
3
+ JavaScript widget for protecting content from AI bots while allowing human visitors.
4
+
5
+ **Configured for:** acvetne@gmail.com
6
+
7
+ ## Quick Start
8
+
9
+ ### 1. Generate Your Credentials
10
+
11
+ ```bash
12
+ node setup.js
13
+ ```
14
+
15
+ This will create:
16
+ - A unique Site ID
17
+ - A unique API Key
18
+ - A configuration file (`widget-config.html`)
19
+
20
+ ### 2. Add Widget to Your Site
21
+
22
+ Copy this code and paste it before the closing `</body>` tag on your website:
23
+
24
+ ```html
25
+ <script
26
+ src="dist/paywall-widget.min.js"
27
+ data-site-id="YOUR_SITE_ID"
28
+ data-api-key="YOUR_API_KEY"
29
+ data-api-url="YOUR_SUPABASE_URL/functions/v1"
30
+ data-debug="true"
31
+ async
32
+ ></script>
33
+ ```
34
+
35
+ **Get your credentials from:**
36
+ - Run `node setup.js` to generate them
37
+ - OR create a site in the [Dashboard](http://localhost:5173/dashboard)
38
+
39
+ ### 3. That's It!
40
+
41
+ The widget will automatically:
42
+ - ✅ **Allow all humans** - No interruption for real visitors
43
+ - ❌ **Block AI bots** - GPTBot, ClaudeBot, CCBot, etc.
44
+ - ✅ **Allow SEO bots** - Google, Bing, etc. for search indexing
45
+ - 📊 **Log everything** - View traffic in your dashboard
46
+
47
+ ## Default Behavior
48
+
49
+ **The widget is pre-configured to ONLY block bots:**
50
+
51
+ | Visitor Type | Behavior |
52
+ |--------------|----------|
53
+ | 👤 Humans | ✅ Always allowed - no paywall |
54
+ | 🤖 AI Bots (GPTBot, ClaudeBot) | ❌ Blocked |
55
+ | 🔍 SEO Bots (Google, Bing) | ✅ Allowed |
56
+ | 🕷️ Scrapers | ❌ Blocked |
57
+
58
+ **You will NEVER see a paywall as a human visitor** - this is the default configuration.
59
+
60
+ ## Configuration Options
61
+
62
+ Customize widget behavior with data attributes:
63
+
64
+ ```html
65
+ <script
66
+ src="dist/paywall-widget.min.js"
67
+ data-site-id="site_xxx"
68
+ data-api-key="pk_live_xxx"
69
+ data-api-url="https://your-project.supabase.co/functions/v1"
70
+
71
+ <!-- Optional Configuration -->
72
+ data-debug="true" <!-- Enable debug logging -->
73
+ data-mode="auto" <!-- auto | always | never -->
74
+ data-seo-safe="true" <!-- Allow search engine bots -->
75
+ data-protect-body="true" <!-- Protect entire page -->
76
+ data-subscribe-url="/subscribe" <!-- Custom subscribe URL -->
77
+ data-login-url="/login" <!-- Custom login URL -->
78
+ async
79
+ ></script>
80
+ ```
81
+
82
+ ### Available Modes
83
+
84
+ - **`auto`** (default) - Automatically detect bots and block them
85
+ - **`always`** - Always show paywall (for testing)
86
+ - **`never`** - Disable widget entirely
87
+
88
+ ## Dashboard Setup
89
+
90
+ ### Option 1: Using the Dashboard (Recommended)
91
+
92
+ 1. Go to [http://localhost:5173/dashboard](http://localhost:5173/dashboard)
93
+ 2. Sign in with `acvetne@gmail.com`
94
+ 3. Click **"Add New Site"**
95
+ 4. Enter your site details:
96
+ - Site Name: "My Website"
97
+ - Domain: "example.com"
98
+ 5. **Copy the Site ID and API Key** shown
99
+ 6. Add the widget code to your site
100
+
101
+ ### Option 2: Automatic Setup Script
102
+
103
+ Run the setup script to generate credentials automatically:
104
+
105
+ ```bash
106
+ node setup.js
107
+ ```
108
+
109
+ This creates a `widget-config.html` file with your unique credentials and integration instructions.
110
+
111
+ ## Testing the Widget
112
+
113
+ ### View Demo Page
114
+
115
+ ```bash
116
+ open demo.html
117
+ ```
118
+
119
+ The demo page shows:
120
+ - How the widget integrates
121
+ - Debug controls
122
+ - Configuration examples
123
+
124
+ ### Debug in Browser Console
125
+
126
+ Once the widget is loaded, you can use these commands in the browser console:
127
+
128
+ ```javascript
129
+ // View current status
130
+ PaywallProtect.debug.status()
131
+
132
+ // View all logs
133
+ PaywallProtect.logs.getAll()
134
+
135
+ // Download logs as JSON
136
+ PaywallProtect.logs.download()
137
+
138
+ // Export logs to console
139
+ console.log(PaywallProtect.logs.export())
140
+
141
+ // Test paywall display
142
+ PaywallProtect.showPaywall({ type: 'hard' })
143
+
144
+ // Hide paywall
145
+ PaywallProtect.hidePaywall()
146
+
147
+ // Force reload/recheck
148
+ PaywallProtect.reload()
149
+ ```
150
+
151
+ ### Testing with Different User Agents
152
+
153
+ Test bot detection by changing your user agent:
154
+
155
+ ```bash
156
+ # Using curl (will be detected as bot)
157
+ curl http://localhost:5173
158
+
159
+ # Using a bot user agent
160
+ curl -A "GPTBot/1.0" http://localhost:5173
161
+ ```
162
+
163
+ ## Building the Widget
164
+
165
+ ### Development
166
+
167
+ ```bash
168
+ # Install dependencies
169
+ npm install
170
+
171
+ # Build the widget
172
+ npm run build
173
+ ```
174
+
175
+ This creates:
176
+ - `dist/paywall-widget.min.js` - Minified IIFE version
177
+ - `dist/paywall-widget.esm.js` - ESM module version
178
+
179
+ ### Distribution
180
+
181
+ The built files can be:
182
+ - Hosted on your CDN
183
+ - Included directly in your site
184
+ - Distributed via npm
185
+
186
+ ## How It Works
187
+
188
+ 1. **Widget loads** on your page
189
+ 2. **Fingerprints browser** - Collects canvas, WebGL, plugins, timing data
190
+ 3. **Checks user agent** - Identifies known bots
191
+ 4. **Calls API** - Sends fingerprint to your backend
192
+ 5. **Backend decides** - Bot detection + paywall logic
193
+ 6. **Widget responds**:
194
+ - **If bot:** Silently blocked
195
+ - **If human:** Full access (by default)
196
+ - **If paywall enabled for humans:** Shows paywall UI
197
+
198
+ ## Allowed Bots (SEO-Safe)
199
+
200
+ These bots are allowed by default for SEO:
201
+
202
+ - Googlebot (Google Search)
203
+ - Bingbot (Bing Search)
204
+ - DuckDuckBot (DuckDuckGo)
205
+ - Baiduspider (Baidu)
206
+ - Yandexbot (Yandex)
207
+ - FacebookExternalHit (Facebook previews)
208
+ - TwitterBot (Twitter cards)
209
+ - LinkedInBot (LinkedIn previews)
210
+ - SlackBot (Slack previews)
211
+
212
+ ## Blocked Bots
213
+
214
+ These bots are automatically blocked:
215
+
216
+ - **GPTBot** (OpenAI)
217
+ - **ClaudeBot** (Anthropic)
218
+ - **CCBot** (Common Crawl)
219
+ - **Cohere-AI** (Cohere)
220
+ - Generic scrapers (curl, wget, python-requests, scrapy)
221
+
222
+ ## API Reference
223
+
224
+ ### JavaScript API
225
+
226
+ ```javascript
227
+ // Global PaywallProtect object
228
+ window.PaywallProtect = {
229
+ version: '1.2.1',
230
+
231
+ // Manual controls
232
+ showPaywall: (config) => {},
233
+ hidePaywall: () => {},
234
+ checkAccess: () => Promise,
235
+ reload: () => {},
236
+
237
+ // Configuration
238
+ config: { ... },
239
+ allowedBots: [ ... ],
240
+
241
+ // Logging
242
+ logs: {
243
+ getAll: () => Array,
244
+ export: () => String,
245
+ clear: () => void,
246
+ download: () => void
247
+ },
248
+
249
+ // Debug helpers
250
+ debug: {
251
+ enable: () => void,
252
+ disable: () => void,
253
+ status: () => void
254
+ }
255
+ }
256
+ ```
257
+
258
+ ## Troubleshooting
259
+
260
+ ### Widget not loading?
261
+
262
+ Check browser console for errors:
263
+
264
+ ```javascript
265
+ // Expected output:
266
+ [PaywallProtect] Initializing PaywallProtect Widget
267
+ [PaywallProtect] ✓ Configuration validated
268
+ [PaywallProtect] ✓ Access granted, no protection applied
269
+ [PaywallProtect] ✓ PaywallProtect initialization complete
270
+ ```
271
+
272
+ ### Seeing "Missing required configuration"?
273
+
274
+ Make sure you have:
275
+ - `data-site-id="..."`
276
+ - `data-api-key="..."`
277
+ - `data-api-url="..."`
278
+
279
+ All three are required.
280
+
281
+ ### Getting "payment required" as a human?
282
+
283
+ This means your site configuration has `showPaywallToHumans: true`.
284
+
285
+ **To fix:**
286
+ 1. Go to your [Dashboard](http://localhost:5173/dashboard)
287
+ 2. Find your site and click "Configure"
288
+ 3. Ensure "Show Paywall to Humans" is **disabled**
289
+
290
+ The default is disabled - only bots are blocked.
291
+
292
+ ### Widget not blocking bots?
293
+
294
+ Check that your API endpoint is working:
295
+
296
+ ```javascript
297
+ // In browser console
298
+ fetch('YOUR_SUPABASE_URL/functions/v1/check-access', {
299
+ method: 'POST',
300
+ headers: { 'Content-Type': 'application/json' },
301
+ body: JSON.stringify({
302
+ siteId: 'YOUR_SITE_ID',
303
+ apiKey: 'YOUR_API_KEY',
304
+ page: '/',
305
+ userAgent: navigator.userAgent,
306
+ fingerprint: {}
307
+ })
308
+ }).then(r => r.json()).then(console.log)
309
+ ```
310
+
311
+ ## Support
312
+
313
+ - **Dashboard:** [http://localhost:5173/dashboard](http://localhost:5173/dashboard)
314
+ - **Demo:** [http://localhost:5173/demo](http://localhost:5173/demo)
315
+ - **Email:** acvetne@gmail.com
316
+
317
+ ## License
318
+
319
+ MIT License - See LICENSE file for details
320
+
321
+ ---
322
+
323
+ **Made with ❤️ for protecting content from AI bots**
@@ -0,0 +1,34 @@
1
+ var k=(s,y,a)=>new Promise((t,i)=>{var l=d=>{try{g(a.next(d))}catch(m){i(m)}},h=d=>{try{g(a.throw(d))}catch(m){i(m)}},g=d=>d.done?t(d.value):Promise.resolve(d.value).then(l,h);g((a=a.apply(s,y)).next())});(function(){"use strict";let s=document.currentScript||document.querySelector("script[data-site-id]");if(!s){console.error("[PaywallProtect] Script tag not found");return}let y=["googlebot","bingbot","slurp","duckduckbot","baiduspider","yandexbot","facebookexternalhit","twitterbot","linkedinbot","slackbot","telegrambot","whatsapp","discordbot","pinterestbot","redditbot","applebot"],a={siteId:s.dataset.siteId||s.getAttribute("data-site-id"),apiKey:s.dataset.apiKey||s.getAttribute("data-api-key"),apiUrl:s.dataset.apiUrl||s.getAttribute("data-api-url")||"https://bakzzkadgmyvvvnpuvki.supabase.co/functions/v1",subscribeUrl:s.dataset.subscribeUrl||"/subscribe",loginUrl:s.dataset.loginUrl||"/login",mode:s.dataset.mode||"auto",seoSafe:s.dataset.seoSafe!=="false",allowBots:s.dataset.allowBots||null,protectBody:s.dataset.protectBody!=="false",debug:s.dataset.debug==="true"||s.dataset.debug==="1"},t={sessionId:"session_"+Date.now()+"_"+Math.random().toString(36).substr(2,9),logs:[],startTime:performance.now(),log(e,o,r={}){let n=new Date().toISOString(),u=Math.round(performance.now()-this.startTime),f={timestamp:n,elapsed:u,level:e,message:o,data:r,sessionId:this.sessionId};this.logs.push(f);let p="[PaywallProtect]",c=a.debug?` [+${u}ms]`:"";switch(e){case"info":console.log(`%c${p}${c} ${o}`,"color: #3b82f6",r);break;case"success":console.log(`%c${p}${c} \u2713 ${o}`,"color: #10b981; font-weight: bold",r);break;case"warn":console.warn(`${p}${c} \u26A0 ${o}`,r);break;case"error":console.error(`${p}${c} \u2717 ${o}`,r);break;case"debug":a.debug&&console.log(`%c${p}${c} [DEBUG] ${o}`,"color: #8b5cf6",r);break}this.logs.length>100&&this.logs.shift()},info(e,o){this.log("info",e,o)},success(e,o){this.log("success",e,o)},warn(e,o){this.log("warn",e,o)},error(e,o){this.log("error",e,o)},debug(e,o){this.log("debug",e,o)},getAll(){return[...this.logs]},export(){return JSON.stringify({sessionId:this.sessionId,config:{siteId:a.siteId,mode:a.mode,seoSafe:a.seoSafe,protectBody:a.protectBody,debug:a.debug},userAgent:navigator.userAgent,page:window.location.href,logs:this.logs,performance:{totalTime:Math.round(performance.now()-this.startTime),memory:performance.memory?{used:Math.round(performance.memory.usedJSHeapSize/1024/1024)+"MB",total:Math.round(performance.memory.totalJSHeapSize/1024/1024)+"MB"}:"N/A"}},null,2)},clear(){this.logs=[],console.clear(),this.info("Logs cleared")}},i=[];if(a.seoSafe&&(i=[...y]),a.allowBots){let e=a.allowBots.split(",").map(o=>o.trim().toLowerCase());i=[...i,...e],t.info("Custom allowlist added",{bots:e})}if(t.info("Initializing PaywallProtect Widget",{version:"1.2.1",siteId:a.siteId,mode:a.mode,seoSafe:a.seoSafe,protectBody:a.protectBody,debug:a.debug,allowedBotsCount:i.length}),!a.siteId||!a.apiKey){t.error("Missing required configuration",{hasSiteId:!!a.siteId,hasApiKey:!!a.apiKey});return}if(!a.apiUrl){t.error("Missing API URL",{apiUrl:a.apiUrl});return}t.success("Configuration validated"),a.seoSafe&&t.info("SEO-safe mode enabled",{allowedBots:i.length});let l={marks:{},mark(e){this.marks[e]=performance.now(),t.debug(`Performance mark: ${e}`,{time:Math.round(this.marks[e])+"ms"})},measure(e,o){let r=Math.round(performance.now()-this.marks[o]);return t.debug(`Performance: ${e}`,{duration:r+"ms"}),r}};l.mark("init_start");function h(){l.mark("allowlist_check_start");let e=navigator.userAgent.toLowerCase();t.debug("Checking user agent against allowlist",{userAgent:navigator.userAgent,allowlistSize:i.length});for(let o of i)if(e.includes(o))return l.measure("Allowlist check complete","allowlist_check_start"),t.success("Allowed bot detected",{bot:o,userAgent:navigator.userAgent}),!0;return l.measure("Allowlist check complete","allowlist_check_start"),t.debug("Not in allowlist, will check with server"),!1}function g(){l.mark("fingerprint_start"),t.debug("Generating browser fingerprint");let e={userAgent:navigator.userAgent,language:navigator.language,platform:navigator.platform,hardwareConcurrency:navigator.hardwareConcurrency||0,deviceMemory:navigator.deviceMemory||0,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone,screen:{width:window.screen.width,height:window.screen.height,colorDepth:window.screen.colorDepth,pixelRatio:window.devicePixelRatio||1},timing:{pageLoadTime:Math.round(performance.now())},webdriver:navigator.webdriver||!1,canvas:d(),webgl:m(),touchSupport:"ontouchstart"in window,plugins:x()};return l.measure("Fingerprint generated","fingerprint_start"),t.debug("Fingerprint complete",{hasCanvas:!!e.canvas,hasWebGL:!!e.webgl,pluginCount:e.plugins.length,webdriver:e.webdriver}),e}function d(){try{let e=document.createElement("canvas"),o=e.getContext("2d");if(!o)return t.warn("Canvas context unavailable"),null;o.textBaseline="top",o.font="14px Arial",o.fillText("PaywallProtect",2,2);let r=e.toDataURL().substring(0,100);return t.debug("Canvas fingerprint generated",{length:r.length}),r}catch(e){return t.error("Canvas fingerprint failed",{error:e.message}),null}}function m(){try{let o=document.createElement("canvas").getContext("webgl");if(!o)return t.warn("WebGL context unavailable"),null;let r={renderer:o.getParameter(o.RENDERER),vendor:o.getParameter(o.VENDOR)};return t.debug("WebGL fingerprint generated",r),r}catch(e){return t.error("WebGL fingerprint failed",{error:e.message}),null}}function x(){try{let e=Array.from(navigator.plugins||[]).map(o=>o.name).slice(0,5);return t.debug("Plugins enumerated",{count:e.length}),e}catch(e){return t.error("Plugin enumeration failed",{error:e.message}),[]}}function P(){return k(this,null,function*(){l.mark("api_call_start"),t.info("Calling API for access check",{endpoint:a.apiUrl+"/check-access"});try{let e={siteId:a.siteId,apiKey:a.apiKey,page:window.location.pathname,userAgent:navigator.userAgent,fingerprint:g(),referrer:document.referrer,allowedBots:i};t.debug("API request payload",{siteId:e.siteId,page:e.page,hasFingerprint:!!e.fingerprint,allowedBotsCount:e.allowedBots.length});let o=yield fetch(a.apiUrl+"/check-access",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),r=l.measure("API call complete","api_call_start");if(!o.ok)return t.error("API returned error status",{status:o.status,statusText:o.statusText,duration:r+"ms"}),{allowed:!0,reason:"API error",showPaywall:!1};let n=yield o.json();return t.success("API response received",{allowed:n.allowed,status:n.status,showPaywall:n.showPaywall,reason:n.reason,duration:r+"ms"}),n}catch(e){let o=l.measure("API call failed","api_call_start");return t.error("Network error during API call",{error:e.message,stack:e.stack,duration:o+"ms"}),{allowed:!0,reason:"Network error",showPaywall:!1}}})}function w(e){if(document.getElementById("paywall-protect-modal")){t.warn("Paywall modal already exists, skipping creation");return}l.mark("modal_create_start"),t.info("Creating paywall modal",{type:e.type||"hard"});let o=e.type||"hard",r="";o==="hard"?r=`
2
+ <div class="paywall-icon">\u{1F512}</div>
3
+ <h2 id="paywall-title">${e.title||"Premium Content"}</h2>
4
+ <p>${e.message||"Subscribe to access this content."}</p>
5
+ <button id="paywall-subscribe" class="paywall-btn primary">Subscribe Now</button>
6
+ <p class="footer">Already a subscriber? <a href="${a.loginUrl}" id="paywall-login">Sign in</a></p>
7
+ `:o==="metered"?r=`
8
+ <div class="paywall-icon">\u{1F4CA}</div>
9
+ <h2 id="paywall-title">Free Article Limit Reached</h2>
10
+ <p>You've read <strong>${e.articlesRead||3} of ${e.freeLimit||3}</strong> free articles this month.</p>
11
+ <button id="paywall-subscribe" class="paywall-btn primary">Get Unlimited Access</button>
12
+ <button id="paywall-close" class="paywall-btn secondary">Maybe Later</button>
13
+ `:r=`
14
+ <div class="paywall-icon">\u{1F916}</div>
15
+ <h2 id="paywall-title">Access Denied</h2>
16
+ <p>${e.message||"Automated access is not permitted."}</p>
17
+ `;let n=document.createElement("div");if(n.id="paywall-protect-modal",n.innerHTML=`<div class="paywall-overlay"><div class="paywall-content">${r}</div></div>`,!document.getElementById("paywall-styles")){let c=document.createElement("style");c.id="paywall-styles",c.textContent=`
18
+ #paywall-protect-modal{position:fixed;top:0;left:0;width:100%;height:100%;z-index:999999;animation:fadeIn .3s}
19
+ .paywall-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.85);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;padding:20px}
20
+ .paywall-content{background:#fff;border-radius:16px;padding:48px 40px;max-width:500px;width:100%;text-align:center;box-shadow:0 20px 60px rgba(0,0,0,.3);animation:slideUp .3s}
21
+ .paywall-icon{font-size:64px;margin-bottom:24px}
22
+ #paywall-title{font-size:28px;font-weight:700;color:#1a1a1a;margin:0 0 16px 0}
23
+ .paywall-content p{font-size:16px;color:#666;line-height:1.6;margin:0 0 24px 0}
24
+ .paywall-btn{display:block;width:100%;padding:16px 32px;border:none;border-radius:8px;font-size:16px;font-weight:600;cursor:pointer;transition:all .2s;margin-bottom:12px}
25
+ .paywall-btn.primary{background:#3b82f6;color:#fff}
26
+ .paywall-btn.primary:hover{background:#2563eb;transform:translateY(-1px)}
27
+ .paywall-btn.secondary{background:transparent;color:#666;border:2px solid #e5e7eb}
28
+ .paywall-btn.secondary:hover{background:#f9fafb}
29
+ .footer{margin-top:24px;font-size:14px;color:#666}
30
+ .footer a{color:#3b82f6;text-decoration:none}
31
+ .footer a:hover{text-decoration:underline}
32
+ @keyframes fadeIn{from{opacity:0}to{opacity:1}}
33
+ @keyframes slideUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
34
+ `,document.head.appendChild(c),t.debug("Paywall styles injected")}document.body.appendChild(n);let u=n.querySelector("#paywall-subscribe");u&&(u.onclick=()=>{t.info("Subscribe button clicked",{url:e.subscribeUrl||a.subscribeUrl}),window.location.href=e.subscribeUrl||a.subscribeUrl});let f=n.querySelector("#paywall-close");f&&(f.onclick=()=>{t.info("Paywall closed by user"),n.remove()});let p=n.querySelector("#paywall-login");p&&(p.onclick=c=>{c.preventDefault(),t.info("Login link clicked",{url:a.loginUrl}),window.location.href=a.loginUrl}),l.measure("Modal created","modal_create_start"),t.success("Paywall modal displayed")}function b(){l.mark("blur_start");let e=[];if(a.protectBody)e=[document.body],t.info("Protecting entire page (body element)");else{let r=["main","article",'[role="main"]',".content",".post-content",".article-content",".entry-content","#content","#main-content"];t.debug("Searching for content elements",{selectors:r});for(let n of r){let u=document.querySelector(n);if(u){e=[u],t.info("Content element found",{selector:n});break}}e.length===0&&(e=[document.body],t.warn("No content elements found, protecting body as fallback"))}let o=0;e.forEach(r=>{r.style.filter="blur(8px)",r.style.userSelect="none",r.style.pointerEvents="none",r.addEventListener("copy",n=>{n.preventDefault(),t.debug("Copy attempt blocked")}),r.addEventListener("cut",n=>{n.preventDefault(),t.debug("Cut attempt blocked")}),r.addEventListener("contextmenu",n=>{n.preventDefault(),t.debug("Context menu blocked")}),o++}),l.measure("Content blurred","blur_start"),t.success("Content protection applied",{elementsProtected:o,protectionType:a.protectBody?"body":"selective"})}function v(){return k(this,null,function*(){if(document.readyState==="loading"){t.debug("DOM not ready, waiting for DOMContentLoaded"),document.addEventListener("DOMContentLoaded",v);return}if(t.info("DOM ready, starting protection sequence"),a.mode==="never"){t.warn("Widget disabled (mode=never)");return}if(h()){t.success("Allowed bot detected, skipping all protection");return}if(a.mode==="always"){t.info("Force mode enabled (mode=always)"),b(),w({type:"hard"});return}let e=yield P();t.info("Access check decision received",{allowed:e.allowed,status:e.status,showPaywall:e.showPaywall}),!e.allowed&&e.status==="payment_required"?(t.warn("Payment required for bot access",{paymentUrl:e.paymentUrl}),e.paymentUrl?window.location.href=e.paymentUrl:(b(),w({type:"bot-blocked",message:e.reason}))):!e.allowed&&e.status==="blocked"?(t.warn("Access blocked",{reason:e.reason}),b(),w({type:"bot-blocked",message:e.reason})):e.showPaywall&&e.paywallConfig?(t.info("Showing paywall to user",{type:e.paywallConfig.type}),b(),w(e.paywallConfig)):t.success("Access granted, no protection applied");let o=l.measure("Initialization complete","init_start");t.success("PaywallProtect initialization complete",{totalTime:o+"ms"})})}v(),window.PaywallProtect={version:"1.2.1",showPaywall:e=>{t.info("Manual showPaywall() called",e),b(),w(e||{type:"hard"})},hidePaywall:()=>{t.info("Manual hidePaywall() called");let e=document.getElementById("paywall-protect-modal");e?(e.remove(),t.success("Paywall hidden")):t.warn("No paywall to hide")},checkAccess:P,reload:()=>{t.info("Manual reload() called"),v()},config:a,allowedBots:i,logs:{getAll:()=>t.getAll(),export:()=>t.export(),clear:()=>t.clear(),download:()=>{let e=t.export(),o=new Blob([e],{type:"application/json"}),r=URL.createObjectURL(o),n=document.createElement("a");n.href=r,n.download=`paywall-logs-${t.sessionId}.json`,n.click(),URL.revokeObjectURL(r),t.info("Logs downloaded")}},debug:{enable:()=>{a.debug=!0,t.success("Debug mode enabled")},disable:()=>{a.debug=!1,t.info("Debug mode disabled")},status:()=>{console.table({Version:window.PaywallProtect.version,"Session ID":t.sessionId,"Site ID":a.siteId,Mode:a.mode,"SEO Safe":a.seoSafe,"Protect Body":a.protectBody,"Debug Mode":a.debug,"Allowed Bots":i.length,"Total Logs":t.logs.length,Uptime:Math.round(performance.now()-t.startTime)+"ms"})}}},t.success("Public API initialized",{methods:Object.keys(window.PaywallProtect)})})();
@@ -0,0 +1,34 @@
1
+ var PaywallProtect=(()=>{var k=(s,y,a)=>new Promise((t,i)=>{var l=d=>{try{g(a.next(d))}catch(m){i(m)}},h=d=>{try{g(a.throw(d))}catch(m){i(m)}},g=d=>d.done?t(d.value):Promise.resolve(d.value).then(l,h);g((a=a.apply(s,y)).next())});(function(){"use strict";let s=document.currentScript||document.querySelector("script[data-site-id]");if(!s){console.error("[PaywallProtect] Script tag not found");return}let y=["googlebot","bingbot","slurp","duckduckbot","baiduspider","yandexbot","facebookexternalhit","twitterbot","linkedinbot","slackbot","telegrambot","whatsapp","discordbot","pinterestbot","redditbot","applebot"],a={siteId:s.dataset.siteId||s.getAttribute("data-site-id"),apiKey:s.dataset.apiKey||s.getAttribute("data-api-key"),apiUrl:s.dataset.apiUrl||s.getAttribute("data-api-url")||"https://bakzzkadgmyvvvnpuvki.supabase.co/functions/v1",subscribeUrl:s.dataset.subscribeUrl||"/subscribe",loginUrl:s.dataset.loginUrl||"/login",mode:s.dataset.mode||"auto",seoSafe:s.dataset.seoSafe!=="false",allowBots:s.dataset.allowBots||null,protectBody:s.dataset.protectBody!=="false",debug:s.dataset.debug==="true"||s.dataset.debug==="1"},t={sessionId:"session_"+Date.now()+"_"+Math.random().toString(36).substr(2,9),logs:[],startTime:performance.now(),log(e,o,r={}){let n=new Date().toISOString(),u=Math.round(performance.now()-this.startTime),f={timestamp:n,elapsed:u,level:e,message:o,data:r,sessionId:this.sessionId};this.logs.push(f);let p="[PaywallProtect]",c=a.debug?` [+${u}ms]`:"";switch(e){case"info":console.log(`%c${p}${c} ${o}`,"color: #3b82f6",r);break;case"success":console.log(`%c${p}${c} \u2713 ${o}`,"color: #10b981; font-weight: bold",r);break;case"warn":console.warn(`${p}${c} \u26A0 ${o}`,r);break;case"error":console.error(`${p}${c} \u2717 ${o}`,r);break;case"debug":a.debug&&console.log(`%c${p}${c} [DEBUG] ${o}`,"color: #8b5cf6",r);break}this.logs.length>100&&this.logs.shift()},info(e,o){this.log("info",e,o)},success(e,o){this.log("success",e,o)},warn(e,o){this.log("warn",e,o)},error(e,o){this.log("error",e,o)},debug(e,o){this.log("debug",e,o)},getAll(){return[...this.logs]},export(){return JSON.stringify({sessionId:this.sessionId,config:{siteId:a.siteId,mode:a.mode,seoSafe:a.seoSafe,protectBody:a.protectBody,debug:a.debug},userAgent:navigator.userAgent,page:window.location.href,logs:this.logs,performance:{totalTime:Math.round(performance.now()-this.startTime),memory:performance.memory?{used:Math.round(performance.memory.usedJSHeapSize/1024/1024)+"MB",total:Math.round(performance.memory.totalJSHeapSize/1024/1024)+"MB"}:"N/A"}},null,2)},clear(){this.logs=[],console.clear(),this.info("Logs cleared")}},i=[];if(a.seoSafe&&(i=[...y]),a.allowBots){let e=a.allowBots.split(",").map(o=>o.trim().toLowerCase());i=[...i,...e],t.info("Custom allowlist added",{bots:e})}if(t.info("Initializing PaywallProtect Widget",{version:"1.2.1",siteId:a.siteId,mode:a.mode,seoSafe:a.seoSafe,protectBody:a.protectBody,debug:a.debug,allowedBotsCount:i.length}),!a.siteId||!a.apiKey){t.error("Missing required configuration",{hasSiteId:!!a.siteId,hasApiKey:!!a.apiKey});return}if(!a.apiUrl){t.error("Missing API URL",{apiUrl:a.apiUrl});return}t.success("Configuration validated"),a.seoSafe&&t.info("SEO-safe mode enabled",{allowedBots:i.length});let l={marks:{},mark(e){this.marks[e]=performance.now(),t.debug(`Performance mark: ${e}`,{time:Math.round(this.marks[e])+"ms"})},measure(e,o){let r=Math.round(performance.now()-this.marks[o]);return t.debug(`Performance: ${e}`,{duration:r+"ms"}),r}};l.mark("init_start");function h(){l.mark("allowlist_check_start");let e=navigator.userAgent.toLowerCase();t.debug("Checking user agent against allowlist",{userAgent:navigator.userAgent,allowlistSize:i.length});for(let o of i)if(e.includes(o))return l.measure("Allowlist check complete","allowlist_check_start"),t.success("Allowed bot detected",{bot:o,userAgent:navigator.userAgent}),!0;return l.measure("Allowlist check complete","allowlist_check_start"),t.debug("Not in allowlist, will check with server"),!1}function g(){l.mark("fingerprint_start"),t.debug("Generating browser fingerprint");let e={userAgent:navigator.userAgent,language:navigator.language,platform:navigator.platform,hardwareConcurrency:navigator.hardwareConcurrency||0,deviceMemory:navigator.deviceMemory||0,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone,screen:{width:window.screen.width,height:window.screen.height,colorDepth:window.screen.colorDepth,pixelRatio:window.devicePixelRatio||1},timing:{pageLoadTime:Math.round(performance.now())},webdriver:navigator.webdriver||!1,canvas:d(),webgl:m(),touchSupport:"ontouchstart"in window,plugins:x()};return l.measure("Fingerprint generated","fingerprint_start"),t.debug("Fingerprint complete",{hasCanvas:!!e.canvas,hasWebGL:!!e.webgl,pluginCount:e.plugins.length,webdriver:e.webdriver}),e}function d(){try{let e=document.createElement("canvas"),o=e.getContext("2d");if(!o)return t.warn("Canvas context unavailable"),null;o.textBaseline="top",o.font="14px Arial",o.fillText("PaywallProtect",2,2);let r=e.toDataURL().substring(0,100);return t.debug("Canvas fingerprint generated",{length:r.length}),r}catch(e){return t.error("Canvas fingerprint failed",{error:e.message}),null}}function m(){try{let o=document.createElement("canvas").getContext("webgl");if(!o)return t.warn("WebGL context unavailable"),null;let r={renderer:o.getParameter(o.RENDERER),vendor:o.getParameter(o.VENDOR)};return t.debug("WebGL fingerprint generated",r),r}catch(e){return t.error("WebGL fingerprint failed",{error:e.message}),null}}function x(){try{let e=Array.from(navigator.plugins||[]).map(o=>o.name).slice(0,5);return t.debug("Plugins enumerated",{count:e.length}),e}catch(e){return t.error("Plugin enumeration failed",{error:e.message}),[]}}function P(){return k(this,null,function*(){l.mark("api_call_start"),t.info("Calling API for access check",{endpoint:a.apiUrl+"/check-access"});try{let e={siteId:a.siteId,apiKey:a.apiKey,page:window.location.pathname,userAgent:navigator.userAgent,fingerprint:g(),referrer:document.referrer,allowedBots:i};t.debug("API request payload",{siteId:e.siteId,page:e.page,hasFingerprint:!!e.fingerprint,allowedBotsCount:e.allowedBots.length});let o=yield fetch(a.apiUrl+"/check-access",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),r=l.measure("API call complete","api_call_start");if(!o.ok)return t.error("API returned error status",{status:o.status,statusText:o.statusText,duration:r+"ms"}),{allowed:!0,reason:"API error",showPaywall:!1};let n=yield o.json();return t.success("API response received",{allowed:n.allowed,status:n.status,showPaywall:n.showPaywall,reason:n.reason,duration:r+"ms"}),n}catch(e){let o=l.measure("API call failed","api_call_start");return t.error("Network error during API call",{error:e.message,stack:e.stack,duration:o+"ms"}),{allowed:!0,reason:"Network error",showPaywall:!1}}})}function w(e){if(document.getElementById("paywall-protect-modal")){t.warn("Paywall modal already exists, skipping creation");return}l.mark("modal_create_start"),t.info("Creating paywall modal",{type:e.type||"hard"});let o=e.type||"hard",r="";o==="hard"?r=`
2
+ <div class="paywall-icon">\u{1F512}</div>
3
+ <h2 id="paywall-title">${e.title||"Premium Content"}</h2>
4
+ <p>${e.message||"Subscribe to access this content."}</p>
5
+ <button id="paywall-subscribe" class="paywall-btn primary">Subscribe Now</button>
6
+ <p class="footer">Already a subscriber? <a href="${a.loginUrl}" id="paywall-login">Sign in</a></p>
7
+ `:o==="metered"?r=`
8
+ <div class="paywall-icon">\u{1F4CA}</div>
9
+ <h2 id="paywall-title">Free Article Limit Reached</h2>
10
+ <p>You've read <strong>${e.articlesRead||3} of ${e.freeLimit||3}</strong> free articles this month.</p>
11
+ <button id="paywall-subscribe" class="paywall-btn primary">Get Unlimited Access</button>
12
+ <button id="paywall-close" class="paywall-btn secondary">Maybe Later</button>
13
+ `:r=`
14
+ <div class="paywall-icon">\u{1F916}</div>
15
+ <h2 id="paywall-title">Access Denied</h2>
16
+ <p>${e.message||"Automated access is not permitted."}</p>
17
+ `;let n=document.createElement("div");if(n.id="paywall-protect-modal",n.innerHTML=`<div class="paywall-overlay"><div class="paywall-content">${r}</div></div>`,!document.getElementById("paywall-styles")){let c=document.createElement("style");c.id="paywall-styles",c.textContent=`
18
+ #paywall-protect-modal{position:fixed;top:0;left:0;width:100%;height:100%;z-index:999999;animation:fadeIn .3s}
19
+ .paywall-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.85);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;padding:20px}
20
+ .paywall-content{background:#fff;border-radius:16px;padding:48px 40px;max-width:500px;width:100%;text-align:center;box-shadow:0 20px 60px rgba(0,0,0,.3);animation:slideUp .3s}
21
+ .paywall-icon{font-size:64px;margin-bottom:24px}
22
+ #paywall-title{font-size:28px;font-weight:700;color:#1a1a1a;margin:0 0 16px 0}
23
+ .paywall-content p{font-size:16px;color:#666;line-height:1.6;margin:0 0 24px 0}
24
+ .paywall-btn{display:block;width:100%;padding:16px 32px;border:none;border-radius:8px;font-size:16px;font-weight:600;cursor:pointer;transition:all .2s;margin-bottom:12px}
25
+ .paywall-btn.primary{background:#3b82f6;color:#fff}
26
+ .paywall-btn.primary:hover{background:#2563eb;transform:translateY(-1px)}
27
+ .paywall-btn.secondary{background:transparent;color:#666;border:2px solid #e5e7eb}
28
+ .paywall-btn.secondary:hover{background:#f9fafb}
29
+ .footer{margin-top:24px;font-size:14px;color:#666}
30
+ .footer a{color:#3b82f6;text-decoration:none}
31
+ .footer a:hover{text-decoration:underline}
32
+ @keyframes fadeIn{from{opacity:0}to{opacity:1}}
33
+ @keyframes slideUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
34
+ `,document.head.appendChild(c),t.debug("Paywall styles injected")}document.body.appendChild(n);let u=n.querySelector("#paywall-subscribe");u&&(u.onclick=()=>{t.info("Subscribe button clicked",{url:e.subscribeUrl||a.subscribeUrl}),window.location.href=e.subscribeUrl||a.subscribeUrl});let f=n.querySelector("#paywall-close");f&&(f.onclick=()=>{t.info("Paywall closed by user"),n.remove()});let p=n.querySelector("#paywall-login");p&&(p.onclick=c=>{c.preventDefault(),t.info("Login link clicked",{url:a.loginUrl}),window.location.href=a.loginUrl}),l.measure("Modal created","modal_create_start"),t.success("Paywall modal displayed")}function b(){l.mark("blur_start");let e=[];if(a.protectBody)e=[document.body],t.info("Protecting entire page (body element)");else{let r=["main","article",'[role="main"]',".content",".post-content",".article-content",".entry-content","#content","#main-content"];t.debug("Searching for content elements",{selectors:r});for(let n of r){let u=document.querySelector(n);if(u){e=[u],t.info("Content element found",{selector:n});break}}e.length===0&&(e=[document.body],t.warn("No content elements found, protecting body as fallback"))}let o=0;e.forEach(r=>{r.style.filter="blur(8px)",r.style.userSelect="none",r.style.pointerEvents="none",r.addEventListener("copy",n=>{n.preventDefault(),t.debug("Copy attempt blocked")}),r.addEventListener("cut",n=>{n.preventDefault(),t.debug("Cut attempt blocked")}),r.addEventListener("contextmenu",n=>{n.preventDefault(),t.debug("Context menu blocked")}),o++}),l.measure("Content blurred","blur_start"),t.success("Content protection applied",{elementsProtected:o,protectionType:a.protectBody?"body":"selective"})}function v(){return k(this,null,function*(){if(document.readyState==="loading"){t.debug("DOM not ready, waiting for DOMContentLoaded"),document.addEventListener("DOMContentLoaded",v);return}if(t.info("DOM ready, starting protection sequence"),a.mode==="never"){t.warn("Widget disabled (mode=never)");return}if(h()){t.success("Allowed bot detected, skipping all protection");return}if(a.mode==="always"){t.info("Force mode enabled (mode=always)"),b(),w({type:"hard"});return}let e=yield P();t.info("Access check decision received",{allowed:e.allowed,status:e.status,showPaywall:e.showPaywall}),!e.allowed&&e.status==="payment_required"?(t.warn("Payment required for bot access",{paymentUrl:e.paymentUrl}),e.paymentUrl?window.location.href=e.paymentUrl:(b(),w({type:"bot-blocked",message:e.reason}))):!e.allowed&&e.status==="blocked"?(t.warn("Access blocked",{reason:e.reason}),b(),w({type:"bot-blocked",message:e.reason})):e.showPaywall&&e.paywallConfig?(t.info("Showing paywall to user",{type:e.paywallConfig.type}),b(),w(e.paywallConfig)):t.success("Access granted, no protection applied");let o=l.measure("Initialization complete","init_start");t.success("PaywallProtect initialization complete",{totalTime:o+"ms"})})}v(),window.PaywallProtect={version:"1.2.1",showPaywall:e=>{t.info("Manual showPaywall() called",e),b(),w(e||{type:"hard"})},hidePaywall:()=>{t.info("Manual hidePaywall() called");let e=document.getElementById("paywall-protect-modal");e?(e.remove(),t.success("Paywall hidden")):t.warn("No paywall to hide")},checkAccess:P,reload:()=>{t.info("Manual reload() called"),v()},config:a,allowedBots:i,logs:{getAll:()=>t.getAll(),export:()=>t.export(),clear:()=>t.clear(),download:()=>{let e=t.export(),o=new Blob([e],{type:"application/json"}),r=URL.createObjectURL(o),n=document.createElement("a");n.href=r,n.download=`paywall-logs-${t.sessionId}.json`,n.click(),URL.revokeObjectURL(r),t.info("Logs downloaded")}},debug:{enable:()=>{a.debug=!0,t.success("Debug mode enabled")},disable:()=>{a.debug=!1,t.info("Debug mode disabled")},status:()=>{console.table({Version:window.PaywallProtect.version,"Session ID":t.sessionId,"Site ID":a.siteId,Mode:a.mode,"SEO Safe":a.seoSafe,"Protect Body":a.protectBody,"Debug Mode":a.debug,"Allowed Bots":i.length,"Total Logs":t.logs.length,Uptime:Math.round(performance.now()-t.startTime)+"ms"})}}},t.success("Public API initialized",{methods:Object.keys(window.PaywallProtect)})})();})();
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "gate-protect-widget",
3
+ "version": "1.2.0",
4
+ "description": "Gate widget for bot detection, content protection, and paywall management",
5
+ "main": "dist/paywall-widget.min.js",
6
+ "module": "dist/paywall-widget.esm.js",
7
+ "types": "types/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "types",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "npm run build:iife && npm run build:esm",
15
+ "build:iife": "esbuild src/index.js --bundle --minify --target=es2015 --format=iife --global-name=PaywallProtect --outfile=dist/paywall-widget.min.js",
16
+ "build:esm": "esbuild src/index.js --bundle --minify --target=es2015 --format=esm --outfile=dist/paywall-widget.esm.js",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/acvetne/gate-protect-widget.git"
22
+ },
23
+ "keywords": [
24
+ "paywall",
25
+ "bot-detection",
26
+ "content-protection",
27
+ "anti-scraping",
28
+ "ai-bots",
29
+ "gate",
30
+ "security"
31
+ ],
32
+ "author": "Gate Securities LLC",
33
+ "license": "MIT",
34
+ "bugs": {
35
+ "url": "https://github.com/acvetne/gate-protect-widget/issues"
36
+ },
37
+ "homepage": "https://github.com/acvetne/gate-protect-widget#readme",
38
+ "devDependencies": {
39
+ "esbuild": "^0.19.0"
40
+ }
41
+ }
@@ -0,0 +1,35 @@
1
+ export interface PaywallProtectConfig {
2
+ siteId: string;
3
+ apiKey: string;
4
+ apiUrl?: string;
5
+ subscribeUrl?: string;
6
+ loginUrl?: string;
7
+ mode?: 'auto' | 'always' | 'never';
8
+ }
9
+
10
+ export interface PaywallConfig {
11
+ type: 'hard' | 'metered' | 'bot-blocked';
12
+ title?: string;
13
+ message?: string;
14
+ articlesRead?: number;
15
+ freeLimit?: number;
16
+ subscribeUrl?: string;
17
+ loginUrl?: string;
18
+ }
19
+
20
+ export interface PaywallProtect {
21
+ version: string;
22
+ init(config: PaywallProtectConfig): void;
23
+ showPaywall(config?: PaywallConfig): void;
24
+ hidePaywall(): void;
25
+ reload(): void;
26
+ }
27
+
28
+ declare global {
29
+ interface Window {
30
+ PaywallProtect: PaywallProtect;
31
+ }
32
+ }
33
+
34
+ export const PaywallProtect: PaywallProtect;
35
+ export default PaywallProtect;