@uservibesos/web-component 1.0.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,533 @@
1
+ # @uservibeos/web-component
2
+
3
+ **Authenticated** Feature Request Widget as a Web Component (Custom Element).
4
+
5
+ ⚠️ **API KEY REQUIRED** - This widget requires authentication. Only compatible with server-rendered applications.
6
+
7
+ ## Installation
8
+
9
+ ### Via CDN (Recommended)
10
+
11
+ Add the script tag to your HTML:
12
+
13
+ ```html
14
+ <script src="https://app.uservibeos.com/widget-assets/v1.0.0/widget.js"></script>
15
+ ```
16
+
17
+ ### Via NPM
18
+
19
+ ```bash
20
+ npm install @uservibeos/web-component
21
+ ```
22
+
23
+ Then import in your JavaScript:
24
+
25
+ ```javascript
26
+ import '@uservibeos/web-component';
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ### Prerequisites
32
+ - Server-rendered application (Next.js, Remix, SvelteKit, etc.)
33
+ - UserVibeOS API key stored in environment variables
34
+
35
+ **NOT compatible with:**
36
+ - Static HTML sites
37
+ - Client-only React/Vue apps
38
+ - WordPress (unless using custom server integration)
39
+
40
+ ### Required Attributes
41
+
42
+ | Attribute | Type | Required | Description |
43
+ |-----------|------|----------|-------------|
44
+ | `project` | string | **YES** | Your project slug from UserVibeOS dashboard |
45
+ | `api-key` | string | **YES** | Your UserVibeOS API key from environment variables |
46
+
47
+ ### Optional Attributes
48
+
49
+ | Attribute | Type | Default | Description |
50
+ |-----------|------|---------|-------------|
51
+ | `theme` | `'light' \| 'dark'` | `'light'` | Color theme |
52
+ | `height` | string | `'600px'` | Widget height (CSS value) |
53
+ | `base-url` | string | Auto-detected | Custom base URL (for development/self-hosting) |
54
+
55
+ ### Security Requirements 🔒
56
+
57
+ **API keys are MANDATORY:**
58
+ - ✅ Widgets will NOT load without a valid API key
59
+ - ✅ API key must come from environment variables
60
+ - ✅ Only use in server-rendered applications
61
+ - ❌ Never hardcode API keys in HTML or JavaScript
62
+ - ❌ Not compatible with static sites or client-only apps
63
+
64
+ ### Events
65
+
66
+ The widget emits custom events you can listen to:
67
+
68
+ ```javascript
69
+ const widget = document.querySelector('uservibe-widget');
70
+
71
+ widget.addEventListener('request-submitted', (event) => {
72
+ console.log('New request submitted:', event.detail);
73
+ });
74
+
75
+ widget.addEventListener('vote-added', (event) => {
76
+ console.log('Vote added:', event.detail);
77
+ });
78
+ ```
79
+
80
+ ## Examples
81
+
82
+ ### Next.js App Router (Recommended)
83
+
84
+ **Step 1:** Add your API key to `.env.local`:
85
+ ```bash
86
+ USERVIBE_API_KEY=uv_live_your_key_here
87
+ ```
88
+
89
+ **Step 2:** Add to `.gitignore`:
90
+ ```
91
+ .env.local
92
+ ```
93
+
94
+ **Step 3:** Use in your server component:
95
+ ```tsx
96
+ // app/feedback/page.tsx (Next.js App Router Server Component)
97
+ export default function FeedbackPage() {
98
+ return (
99
+ <div>
100
+ <h1>Feature Requests</h1>
101
+ <script src="https://app.uservibeos.com/widget-assets/v1.0.0/widget.js"></script>
102
+ <uservibe-widget
103
+ project="my-app"
104
+ api-key={process.env.USERVIBE_API_KEY}
105
+ theme="light"
106
+ />
107
+ </div>
108
+ );
109
+ }
110
+ ```
111
+
112
+ ### With Custom Styling
113
+
114
+ ```tsx
115
+ <uservibe-widget
116
+ project="my-app"
117
+ api-key={process.env.USERVIBE_API_KEY}
118
+ theme="dark"
119
+ height="800px"
120
+ />
121
+
122
+ ### Development/Local Testing
123
+
124
+ When testing locally with UserVibeOS running on a different port:
125
+
126
+ ```tsx
127
+ // Test app on localhost:3002, UserVibeOS on localhost:3000
128
+ export default function TestPage() {
129
+ return (
130
+ <div>
131
+ <script src="http://localhost:3000/widget-assets/v1.0.0/widget.js"></script>
132
+ <uservibe-widget
133
+ project="my-app"
134
+ base-url="http://localhost:3000"
135
+ api-key={process.env.USERVIBE_API_KEY}
136
+ />
137
+ </div>
138
+ );
139
+ }
140
+ ```
141
+
142
+ Don't forget to add TypeScript types:
143
+
144
+ ```typescript
145
+ declare global {
146
+ namespace JSX {
147
+ interface IntrinsicElements {
148
+ 'uservibe-widget': {
149
+ project: string;
150
+ 'api-key': string; // REQUIRED
151
+ theme?: 'light' | 'dark';
152
+ height?: string;
153
+ 'base-url'?: string;
154
+ };
155
+ }
156
+ }
157
+ }
158
+ ```
159
+
160
+ ## 🔐 Two-Tier Security Model
161
+
162
+ UserVibeOS offers two security tiers for widget authentication. Choose based on your security requirements.
163
+
164
+ ---
165
+
166
+ ### TIER 1: Public Keys (PK) - Recommended for Most Users
167
+
168
+ **Best for:** Standard web applications with good security needs.
169
+
170
+ **How it works:**
171
+ - Create a Public Key (PK) in the API Keys dashboard
172
+ - Configure allowed origins (up to 5 domains)
173
+ - Use server-side rendering to inject the key
174
+ - Widget validates origin against your whitelist
175
+
176
+ **Security features:**
177
+ - ✅ Origin/referrer enforcement
178
+ - ✅ Rate limiting (5 req/min per IP/fingerprint)
179
+ - ✅ Advanced browser fingerprinting
180
+ - ✅ Behavioral anomaly detection
181
+ - ✅ Real-time security event logging
182
+
183
+ **Setup:**
184
+ ```bash
185
+ # .env.local
186
+ USERVIBE_PUBLIC_KEY=pk_live_your_key_here
187
+ ```
188
+
189
+ ```tsx
190
+ // app/feedback/page.tsx (Next.js Server Component)
191
+ export default function FeedbackPage() {
192
+ return (
193
+ <div>
194
+ <script src="https://app.uservibeos.com/widget-assets/v1.0.0/widget.js"></script>
195
+ <uservibe-widget
196
+ project="my-app"
197
+ api-key={process.env.USERVIBE_PUBLIC_KEY}
198
+ theme="light"
199
+ />
200
+ </div>
201
+ );
202
+ }
203
+ ```
204
+
205
+ **Limitations:**
206
+ - Keys visible in browser's view source
207
+ - Relies on origin validation (can be bypassed by determined attackers)
208
+ - Not suitable for high-security applications
209
+
210
+ ---
211
+
212
+ ### TIER 2: JWT Proxy - Maximum Security
213
+
214
+ **Best for:** Financial apps, healthcare, or maximum security requirements.
215
+
216
+ **How it works:**
217
+ 1. Your backend securely holds your Secret Key (SK)
218
+ 2. Widget calls YOUR backend proxy endpoint (not UserVibeOS directly)
219
+ 3. Your backend exchanges SK for a short-lived JWT token (5 min expiry)
220
+ 4. Backend forwards request to UserVibeOS with JWT
221
+ 5. **Secret Key never leaves your server**
222
+
223
+ **Security features:**
224
+ - ✅ All Tier 1 features PLUS:
225
+ - ✅ Secret Key (SK) never exposed to client
226
+ - ✅ JWT tokens expire in 5 minutes
227
+ - ✅ One-time use tokens (replay protection)
228
+ - ✅ Backend request validation
229
+ - ✅ Full cryptographic signing
230
+ - ✅ Complete audit trail
231
+
232
+ ---
233
+
234
+ ### Tier 2 Implementation Guide
235
+
236
+ #### Step 1: Enable JWT in Project Settings
237
+
238
+ 1. Go to your UserVibeOS Dashboard → Projects → [Your Project]
239
+ 2. Navigate to "Project Settings"
240
+ 3. Enable "JWT Authentication (Tier 2)"
241
+ 4. Select or create a Secret Key (SK)
242
+ 5. Set token expiry (default: 5 minutes)
243
+
244
+ ---
245
+
246
+ #### Step 2: Create Backend Proxy
247
+
248
+ Choose your framework:
249
+
250
+ **Next.js App Router:**
251
+
252
+ ```typescript
253
+ // app/api/uservibe-proxy/route.ts
254
+ import { NextRequest, NextResponse } from 'next/server';
255
+
256
+ // Token cache (use Redis in production for multi-instance)
257
+ let tokenCache: { token: string; expiresAt: number } | null = null;
258
+
259
+ async function getValidToken(projectId: string): Promise<string> {
260
+ // Check cache
261
+ if (tokenCache && tokenCache.expiresAt > Date.now() + 60000) {
262
+ return tokenCache.token;
263
+ }
264
+
265
+ // Exchange Secret Key for JWT token
266
+ const response = await fetch('https://app.uservibeos.com/api/token/exchange', {
267
+ method: 'POST',
268
+ headers: { 'Content-Type': 'application/json' },
269
+ body: JSON.stringify({
270
+ secretKey: process.env.USERVIBE_SECRET_KEY, // SK never exposed
271
+ projectId,
272
+ origin: process.env.NEXT_PUBLIC_SITE_URL || 'https://yourapp.com',
273
+ }),
274
+ });
275
+
276
+ if (!response.ok) {
277
+ throw new Error('Token exchange failed');
278
+ }
279
+
280
+ const data = await response.json();
281
+
282
+ // Cache token
283
+ tokenCache = {
284
+ token: data.token,
285
+ expiresAt: data.expiresAt,
286
+ };
287
+
288
+ return data.token;
289
+ }
290
+
291
+ export async function POST(req: NextRequest) {
292
+ try {
293
+ const body = await req.json();
294
+ const { projectId, action, ...payload } = body;
295
+
296
+ // Get valid JWT token
297
+ const jwtToken = await getValidToken(projectId);
298
+
299
+ // Forward to Convex with JWT
300
+ const convexUrl = process.env.CONVEX_URL || 'https://your-deployment.convex.cloud';
301
+ const response = await fetch(`${convexUrl}/api/function/${action}`, {
302
+ method: 'POST',
303
+ headers: {
304
+ 'Content-Type': 'application/json',
305
+ 'Authorization': `Bearer ${jwtToken}`,
306
+ },
307
+ body: JSON.stringify(payload),
308
+ });
309
+
310
+ const data = await response.json();
311
+ return NextResponse.json(data);
312
+
313
+ } catch (error) {
314
+ console.error('Proxy error:', error);
315
+ return NextResponse.json(
316
+ { error: 'Proxy request failed' },
317
+ { status: 500 }
318
+ );
319
+ }
320
+ }
321
+ ```
322
+
323
+ **Express.js:**
324
+
325
+ ```javascript
326
+ // server.js
327
+ const express = require('express');
328
+ const app = express();
329
+
330
+ let tokenCache = null;
331
+
332
+ async function getValidToken(projectId) {
333
+ if (tokenCache && tokenCache.expiresAt > Date.now() + 60000) {
334
+ return tokenCache.token;
335
+ }
336
+
337
+ const response = await fetch('https://app.uservibeos.com/api/token/exchange', {
338
+ method: 'POST',
339
+ headers: { 'Content-Type': 'application/json' },
340
+ body: JSON.stringify({
341
+ secretKey: process.env.USERVIBE_SECRET_KEY,
342
+ projectId,
343
+ origin: process.env.SITE_URL || 'https://yourapp.com',
344
+ }),
345
+ });
346
+
347
+ const data = await response.json();
348
+ tokenCache = { token: data.token, expiresAt: data.expiresAt };
349
+ return data.token;
350
+ }
351
+
352
+ app.post('/api/uservibe-proxy', async (req, res) => {
353
+ try {
354
+ const { projectId, action, ...payload } = req.body;
355
+ const token = await getValidToken(projectId);
356
+
357
+ const convexUrl = process.env.CONVEX_URL;
358
+ const response = await fetch(`${convexUrl}/api/function/${action}`, {
359
+ method: 'POST',
360
+ headers: {
361
+ 'Content-Type': 'application/json',
362
+ 'Authorization': `Bearer ${token}`,
363
+ },
364
+ body: JSON.stringify(payload),
365
+ });
366
+
367
+ const data = await response.json();
368
+ res.json(data);
369
+ } catch (error) {
370
+ console.error('Proxy error:', error);
371
+ res.status(500).json({ error: 'Proxy request failed' });
372
+ }
373
+ });
374
+
375
+ app.listen(3000);
376
+ ```
377
+
378
+ ---
379
+
380
+ #### Step 3: Configure Widget for JWT Mode
381
+
382
+ ```html
383
+ <script src="https://app.uservibeos.com/widget-assets/v1.0.0/widget.js"></script>
384
+ <uservibe-widget
385
+ project="my-app"
386
+ jwt-mode="true"
387
+ proxy-url="/api/uservibe-proxy"
388
+ theme="light"
389
+ ></uservibe-widget>
390
+ ```
391
+
392
+ **Key attributes for Tier 2:**
393
+ - `jwt-mode="true"` - Enables JWT authentication mode
394
+ - `proxy-url="/api/uservibe-proxy"` - Your backend proxy endpoint
395
+
396
+ ---
397
+
398
+ #### Step 4: Environment Variables
399
+
400
+ ```bash
401
+ # .env.local (Backend only - NEVER commit)
402
+
403
+ # Tier 2: Secret Key (SK) for JWT signing
404
+ USERVIBE_SECRET_KEY=sk_live_your_secret_key_here
405
+
406
+ # Your Convex deployment URL
407
+ CONVEX_URL=https://your-deployment.convex.cloud
408
+
409
+ # Your site URL (for origin validation)
410
+ NEXT_PUBLIC_SITE_URL=https://yourapp.com
411
+ ```
412
+
413
+ ---
414
+
415
+ ### Production Considerations for Tier 2
416
+
417
+ #### Token Caching
418
+
419
+ **Single-instance deployments** (Vercel Hobby, single server):
420
+ - Use in-memory caching (as shown above)
421
+
422
+ **Multi-instance deployments** (Vercel Pro, load-balanced):
423
+ - Use Redis or similar distributed cache
424
+ - Share tokens across instances
425
+
426
+ ```typescript
427
+ // Example with Upstash Redis
428
+ import { Redis } from '@upstash/redis';
429
+
430
+ const redis = new Redis({
431
+ url: process.env.UPSTASH_REDIS_URL!,
432
+ token: process.env.UPSTASH_REDIS_TOKEN!,
433
+ });
434
+
435
+ async function getValidToken(projectId: string): Promise<string> {
436
+ const cacheKey = `uservibe:jwt:${projectId}`;
437
+
438
+ // Check cache
439
+ const cached = await redis.get<{ token: string; expiresAt: number }>(cacheKey);
440
+ if (cached && cached.expiresAt > Date.now() + 60000) {
441
+ return cached.token;
442
+ }
443
+
444
+ // Exchange for new token
445
+ const token = await exchangeToken(projectId);
446
+
447
+ // Cache with TTL (4 min, tokens expire in 5)
448
+ await redis.setex(cacheKey, 240, token);
449
+
450
+ return token.token;
451
+ }
452
+ ```
453
+
454
+ #### Error Handling
455
+
456
+ Always implement graceful error handling:
457
+
458
+ ```typescript
459
+ try {
460
+ const token = await getValidToken(projectId);
461
+ // Use token...
462
+ } catch (error) {
463
+ console.error('JWT token exchange failed:', error);
464
+
465
+ // Return user-friendly error
466
+ return NextResponse.json(
467
+ { error: 'Authentication failed. Please try again.' },
468
+ { status: 503 }
469
+ );
470
+ }
471
+ ```
472
+
473
+ ---
474
+
475
+ ### Complete Implementation Guides
476
+
477
+ For detailed, framework-specific guides:
478
+
479
+ - 📘 [Next.js App Router Proxy Guide](https://github.com/uservibeOS/uservibeOS/blob/main/docs/jwt-proxy-nextjs.md)
480
+ - 📗 [React + Express Proxy Guide](https://github.com/uservibeOS/uservibeOS/blob/main/docs/jwt-proxy-react-express.md)
481
+ - 📕 [Standalone Express Proxy Guide](https://github.com/uservibeOS/uservibeOS/blob/main/docs/jwt-proxy-express.md)
482
+ - 📙 [Migration Guide: Upgrading to Two-Tier Security](https://github.com/uservibeOS/uservibeOS/blob/main/docs/migration-guide-two-tier-security.md)
483
+
484
+ ---
485
+
486
+ ## API Key Security Best Practices 🔒
487
+
488
+ ### Required Security Practices (Both Tiers)
489
+
490
+ ✅ **MUST DO:**
491
+ - Store API keys in `.env.local` (never in code)
492
+ - Add `.env.local` to `.gitignore`
493
+ - Use server-side rendering only (Next.js Server Components, Remix loaders, etc.)
494
+ - Use different API keys for development and production
495
+ - Rotate API keys if compromised
496
+
497
+ ❌ **NEVER DO:**
498
+ - Hardcode API keys in HTML, JavaScript, or TSX files
499
+ - Commit API keys to version control (GitHub, GitLab, etc.)
500
+ - Use this widget in static HTML sites
501
+ - Use this widget in client-only React/Vue apps
502
+ - Expose API keys in client-side environment variables (NEXT_PUBLIC_, VITE_, etc.)
503
+ - Share API keys in public repositories or screenshots
504
+
505
+ ### Tier 1 vs Tier 2: When to Use
506
+
507
+ | Use Case | Recommended Tier | Reason |
508
+ |----------|-----------------|---------|
509
+ | Standard web app | Tier 1 (PK) | Good security, easier setup |
510
+ | Financial services | Tier 2 (JWT) | Regulatory compliance |
511
+ | Healthcare (HIPAA) | Tier 2 (JWT) | Maximum security required |
512
+ | E-commerce | Tier 1 or 2 | Depends on sensitivity |
513
+ | Internal tools | Tier 1 (PK) | Lower risk, simpler |
514
+ | Public-facing SaaS | Tier 2 (JWT) | Higher attack surface |
515
+
516
+ ### Important Notes
517
+
518
+ ⚠️ **Tier 1 Limitation**: Public Keys visible in browser's view source (but origin-restricted)
519
+
520
+ ⚠️ **Tier 2 Requirement**: Requires backend infrastructure to run proxy
521
+
522
+ ⚠️ **Not for Static Sites**: Both tiers require server-side rendering
523
+
524
+ ## Browser Support
525
+
526
+ - Chrome/Edge: ✅
527
+ - Firefox: ✅
528
+ - Safari: ✅
529
+ - IE11: ❌ (Custom Elements not supported)
530
+
531
+ ## License
532
+
533
+ MIT
package/build.js ADDED
@@ -0,0 +1,47 @@
1
+ const esbuild = require('esbuild');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const isWatch = process.argv.includes('--watch');
6
+
7
+ const buildOptions = {
8
+ entryPoints: ['src/widget.ts'],
9
+ bundle: true,
10
+ minify: !isWatch,
11
+ sourcemap: isWatch,
12
+ format: 'iife',
13
+ target: ['es2020'],
14
+ outfile: 'dist/widget.js',
15
+ platform: 'browser',
16
+ logLevel: 'info',
17
+ };
18
+
19
+ async function build() {
20
+ try {
21
+ if (isWatch) {
22
+ const ctx = await esbuild.context(buildOptions);
23
+ await ctx.watch();
24
+ console.log('👀 Watching for changes...');
25
+ } else {
26
+ await esbuild.build(buildOptions);
27
+
28
+ // Also copy to public directory for CDN
29
+ const publicDir = path.join(__dirname, '../../public/widget-assets/v1.0.0');
30
+ if (!fs.existsSync(publicDir)) {
31
+ fs.mkdirSync(publicDir, { recursive: true });
32
+ }
33
+ fs.copyFileSync(
34
+ path.join(__dirname, 'dist/widget.js'),
35
+ path.join(publicDir, 'widget.js')
36
+ );
37
+
38
+ console.log('✅ Build complete!');
39
+ console.log('📦 Copied to public/widget-assets/v1.0.0/widget.js');
40
+ }
41
+ } catch (error) {
42
+ console.error('❌ Build failed:', error);
43
+ process.exit(1);
44
+ }
45
+ }
46
+
47
+ build();
package/dist/widget.js ADDED
@@ -0,0 +1,44 @@
1
+ "use strict";(()=>{var i=class extends HTMLElement{constructor(){super();this.iframe=null;this.baseUrl="https://app.uservibesos.com";this.attachShadow({mode:"open"})}static get observedAttributes(){return["project","theme","height","mode","base-url","api-key","jwt-mode","proxy-url"]}connectedCallback(){this.render()}attributeChangedCallback(e,t,o){t!==o&&this.render()}render(){let e=this.getAttribute("project"),t=this.getAttribute("theme")||"light",o=this.getAttribute("height")||"600px",s=this.getAttribute("mode")||"feature",d=this.getAttribute("base-url"),n=this.getAttribute("api-key"),l=this.getAttribute("jwt-mode")==="true",a=this.getAttribute("proxy-url");if(!e){this.renderError('Missing required "project" attribute');return}d?this.baseUrl=d:typeof window<"u"&&window.location.hostname==="localhost"&&(this.baseUrl=window.location.origin);let r=`${this.baseUrl}/widget/${e}`;if(!n&&!l){let h="/api/uservibes-proxy",u=typeof window<"u"?`${window.location.origin}${h}`:h;r+=`?tier=2&proxyUrl=${encodeURIComponent(u)}&mode=${s}`,console.log("[Web Component] TIER 2 auto-detected (no api-key) - using proxy:",u)}else if(l&&a)r+=`?tier=2&proxyUrl=${encodeURIComponent(a)}&mode=${s}`,console.log("[Web Component] TIER 2 legacy mode - using custom proxy:",a);else if(n)r+=`?publicKey=${encodeURIComponent(n)}&mode=${s}`,console.log("[Web Component] TIER 1 mode - using public key");else{this.renderError('Widget requires either an "api-key" attribute for Tier 1 security, or no api-key for Tier 2 security (proxy mode). For Tier 2, ensure you have implemented the proxy at /api/uservibes-proxy.');return}let p=`
2
+ <style>
3
+ :host {
4
+ display: block;
5
+ width: 100%;
6
+ }
7
+ iframe {
8
+ width: 100%;
9
+ height: ${o};
10
+ border: none;
11
+ border-radius: 8px;
12
+ overflow: hidden;
13
+ }
14
+ .error {
15
+ padding: 2rem;
16
+ text-align: center;
17
+ border: 1px solid #ef4444;
18
+ border-radius: 8px;
19
+ background: #fef2f2;
20
+ color: #991b1b;
21
+ }
22
+ </style>
23
+ `,c=`
24
+ <iframe
25
+ src="${r}"
26
+ title="${e} Feature Requests"
27
+ loading="lazy"
28
+ allow="clipboard-write"
29
+ ></iframe>
30
+ `;this.shadowRoot&&(this.shadowRoot.innerHTML=p+c,this.iframe=this.shadowRoot.querySelector("iframe"),this.setupMessageListener())}renderError(e){let t=`
31
+ <style>
32
+ .error {
33
+ padding: 2rem;
34
+ text-align: center;
35
+ border: 1px solid #ef4444;
36
+ border-radius: 8px;
37
+ background: #fef2f2;
38
+ color: #991b1b;
39
+ }
40
+ </style>
41
+ <div class="error">
42
+ <strong>Widget Error:</strong> ${e}
43
+ </div>
44
+ `;this.shadowRoot&&(this.shadowRoot.innerHTML=t)}setupMessageListener(){window.addEventListener("message",e=>{e.origin===this.baseUrl&&(e.data.type==="USERVIBES_HEIGHT_UPDATE"&&this.iframe&&(this.iframe.style.height=`${e.data.height}px`),e.data.type==="USERVIBES_REQUEST_SUBMITTED"&&this.dispatchEvent(new CustomEvent("request-submitted",{detail:e.data.payload})),e.data.type==="USERVIBES_VOTE_ADDED"&&this.dispatchEvent(new CustomEvent("vote-added",{detail:e.data.payload})))})}};typeof window<"u"&&!customElements.get("uservibes-widget")&&customElements.define("uservibes-widget",i);var b=i;})();
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@uservibesos/web-component",
3
+ "version": "1.0.0",
4
+ "description": "Feature request widget as a Web Component",
5
+ "main": "dist/widget.js",
6
+ "scripts": {
7
+ "build": "node build.js",
8
+ "dev": "node build.js --watch"
9
+ },
10
+ "keywords": [
11
+ "feature-request",
12
+ "web-component",
13
+ "custom-element",
14
+ "widget"
15
+ ],
16
+ "author": "UserVibesOS",
17
+ "license": "MIT",
18
+ "devDependencies": {
19
+ "esbuild": "^0.24.0"
20
+ }
21
+ }
package/src/widget.ts ADDED
@@ -0,0 +1,244 @@
1
+ /**
2
+ * UserVibesOS Feature Request Widget - Web Component
3
+ *
4
+ * A custom element that embeds authenticated feature request widgets.
5
+ *
6
+ * TWO-TIER SECURITY MODEL:
7
+ *
8
+ * TIER 1: Public Keys (PK) - Recommended for most users
9
+ * - Origin enforcement + Rate limiting + Fingerprinting
10
+ * - Safe to use with server-side rendering
11
+ *
12
+ * TIER 2: JWT Tokens - Maximum security
13
+ * - Short-lived tokens (5min) + Backend proxy required
14
+ * - One-time use + Request signing
15
+ * - Auto-detected when no api-key is present
16
+ *
17
+ * ========================================
18
+ * TIER 1 USAGE (Public Keys - Recommended)
19
+ * ========================================
20
+ *
21
+ * Step 1: Create Public Key (PK) in UserVibesOS dashboard with allowed origins
22
+ * Step 2: Add to .env.local
23
+ * USERVIBES_PUBLIC_KEY=pk_live_your_public_key_here
24
+ *
25
+ * Step 3: Use in server component
26
+ * <script src="https://app.uservibesos.com/widget-assets/v1.0.0/widget.js"></script>
27
+ * <uservibes-widget
28
+ * project="my-project-slug"
29
+ * api-key={process.env.USERVIBES_PUBLIC_KEY}
30
+ * ></uservibes-widget>
31
+ *
32
+ * ========================================
33
+ * TIER 2 USAGE (Maximum Security - Auto-detected)
34
+ * ========================================
35
+ *
36
+ * Step 1: Enable Tier 2 mode in project settings
37
+ * Step 2: Implement backend proxy at /api/uservibes-proxy
38
+ * Step 3: Use widget WITHOUT api-key (auto-detects Tier 2)
39
+ *
40
+ * <script src="https://app.uservibesos.com/widget-assets/v1.0.0/widget.js"></script>
41
+ * <uservibes-widget
42
+ * project="my-project-slug"
43
+ * ></uservibes-widget>
44
+ *
45
+ * The widget automatically detects Tier 2 mode when:
46
+ * - No api-key attribute is present
47
+ * - Makes requests to /api/uservibes-proxy (standard path)
48
+ *
49
+ * Attributes:
50
+ * - project (required): Your project slug
51
+ * - api-key (Tier 1 only): Public Key from environment variables
52
+ * - mode (optional): "feature" or "roadmap" (default: "feature")
53
+ * - theme (optional): "light" or "dark" (default: "light")
54
+ * - height (optional): Height of the widget (default: "600px")
55
+ * - base-url (optional): Custom base URL (for development/self-hosting)
56
+ *
57
+ * DEPRECATED (for backward compatibility only):
58
+ * - jwt-mode: No longer needed, auto-detected
59
+ * - proxy-url: Uses standard /api/uservibes-proxy path
60
+ *
61
+ * SECURITY:
62
+ * - TIER 1: Store Public Key in environment variables
63
+ * - TIER 2: No keys in client code, all handled by proxy
64
+ * - NEVER commit .env files to version control
65
+ */
66
+
67
+ class UserVibesWidget extends HTMLElement {
68
+ private iframe: HTMLIFrameElement | null = null;
69
+ private baseUrl: string = 'https://app.uservibesos.com';
70
+
71
+ constructor() {
72
+ super();
73
+
74
+ // Use Shadow DOM for style isolation
75
+ this.attachShadow({ mode: 'open' });
76
+ }
77
+
78
+ static get observedAttributes() {
79
+ return ['project', 'theme', 'height', 'mode', 'base-url', 'api-key', 'jwt-mode', 'proxy-url'];
80
+ }
81
+
82
+ connectedCallback() {
83
+ this.render();
84
+ }
85
+
86
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
87
+ if (oldValue !== newValue) {
88
+ this.render();
89
+ }
90
+ }
91
+
92
+ private render() {
93
+ const project = this.getAttribute('project');
94
+ const theme = this.getAttribute('theme') || 'light';
95
+ const height = this.getAttribute('height') || '600px';
96
+ const mode = this.getAttribute('mode') || 'feature';
97
+ const customBaseUrl = this.getAttribute('base-url');
98
+ const apiKey = this.getAttribute('api-key');
99
+
100
+ // For backward compatibility, check legacy jwt-mode and proxy-url attributes
101
+ const legacyJwtMode = this.getAttribute('jwt-mode') === 'true';
102
+ const legacyProxyUrl = this.getAttribute('proxy-url');
103
+
104
+ if (!project) {
105
+ this.renderError('Missing required "project" attribute');
106
+ return;
107
+ }
108
+
109
+ // Use custom base URL if provided, otherwise use local URL in development
110
+ if (customBaseUrl) {
111
+ this.baseUrl = customBaseUrl;
112
+ } else if (typeof window !== 'undefined' && window.location.hostname === 'localhost') {
113
+ this.baseUrl = window.location.origin;
114
+ }
115
+
116
+ // Build widget URL - ALWAYS points to UserVibesOS
117
+ let widgetUrl = `${this.baseUrl}/widget/${project}`;
118
+
119
+ // AUTO-DETECT SECURITY TIER
120
+ if (!apiKey && !legacyJwtMode) {
121
+ // TIER 2: Auto-detected - No api-key means proxy mode
122
+ // Build absolute proxy URL from current page origin
123
+ const standardProxyPath = '/api/uservibes-proxy';
124
+ const absoluteProxyUrl = typeof window !== 'undefined'
125
+ ? `${window.location.origin}${standardProxyPath}`
126
+ : standardProxyPath;
127
+ widgetUrl += `?tier=2&proxyUrl=${encodeURIComponent(absoluteProxyUrl)}&mode=${mode}`;
128
+ console.log('[Web Component] TIER 2 auto-detected (no api-key) - using proxy:', absoluteProxyUrl);
129
+ } else if (legacyJwtMode && legacyProxyUrl) {
130
+ // LEGACY: Explicit JWT mode with custom proxy URL (backward compatibility)
131
+ widgetUrl += `?tier=2&proxyUrl=${encodeURIComponent(legacyProxyUrl)}&mode=${mode}`;
132
+ console.log('[Web Component] TIER 2 legacy mode - using custom proxy:', legacyProxyUrl);
133
+ } else if (apiKey) {
134
+ // TIER 1: Public key mode - pass key directly
135
+ widgetUrl += `?publicKey=${encodeURIComponent(apiKey)}&mode=${mode}`;
136
+ console.log('[Web Component] TIER 1 mode - using public key');
137
+ } else {
138
+ // No valid configuration found
139
+ this.renderError('Widget requires either an "api-key" attribute for Tier 1 security, or no api-key for Tier 2 security (proxy mode). For Tier 2, ensure you have implemented the proxy at /api/uservibes-proxy.');
140
+ return;
141
+ }
142
+
143
+ const styles = `
144
+ <style>
145
+ :host {
146
+ display: block;
147
+ width: 100%;
148
+ }
149
+ iframe {
150
+ width: 100%;
151
+ height: ${height};
152
+ border: none;
153
+ border-radius: 8px;
154
+ overflow: hidden;
155
+ }
156
+ .error {
157
+ padding: 2rem;
158
+ text-align: center;
159
+ border: 1px solid #ef4444;
160
+ border-radius: 8px;
161
+ background: #fef2f2;
162
+ color: #991b1b;
163
+ }
164
+ </style>
165
+ `;
166
+
167
+ const iframe = `
168
+ <iframe
169
+ src="${widgetUrl}"
170
+ title="${project} Feature Requests"
171
+ loading="lazy"
172
+ allow="clipboard-write"
173
+ ></iframe>
174
+ `;
175
+
176
+ if (this.shadowRoot) {
177
+ this.shadowRoot.innerHTML = styles + iframe;
178
+ this.iframe = this.shadowRoot.querySelector('iframe');
179
+
180
+ // Listen for messages from iframe (for height adjustment)
181
+ this.setupMessageListener();
182
+ }
183
+ }
184
+
185
+ private renderError(message: string) {
186
+ const html = `
187
+ <style>
188
+ .error {
189
+ padding: 2rem;
190
+ text-align: center;
191
+ border: 1px solid #ef4444;
192
+ border-radius: 8px;
193
+ background: #fef2f2;
194
+ color: #991b1b;
195
+ }
196
+ </style>
197
+ <div class="error">
198
+ <strong>Widget Error:</strong> ${message}
199
+ </div>
200
+ `;
201
+
202
+ if (this.shadowRoot) {
203
+ this.shadowRoot.innerHTML = html;
204
+ }
205
+ }
206
+
207
+ private setupMessageListener() {
208
+ window.addEventListener('message', (event) => {
209
+ // Verify origin for security
210
+ if (event.origin !== this.baseUrl) {
211
+ return;
212
+ }
213
+
214
+ // Handle height updates from iframe
215
+ if (event.data.type === 'USERVIBES_HEIGHT_UPDATE' && this.iframe) {
216
+ this.iframe.style.height = `${event.data.height}px`;
217
+ }
218
+
219
+ // Dispatch custom events for parent page
220
+ if (event.data.type === 'USERVIBES_REQUEST_SUBMITTED') {
221
+ this.dispatchEvent(
222
+ new CustomEvent('request-submitted', {
223
+ detail: event.data.payload,
224
+ })
225
+ );
226
+ }
227
+
228
+ if (event.data.type === 'USERVIBES_VOTE_ADDED') {
229
+ this.dispatchEvent(
230
+ new CustomEvent('vote-added', {
231
+ detail: event.data.payload,
232
+ })
233
+ );
234
+ }
235
+ });
236
+ }
237
+ }
238
+
239
+ // Register the custom element
240
+ if (typeof window !== 'undefined' && !customElements.get('uservibes-widget')) {
241
+ customElements.define('uservibes-widget', UserVibesWidget);
242
+ }
243
+
244
+ export default UserVibesWidget;