@uservibesos/web-component 1.0.0 → 1.0.3
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 +52 -496
- package/dist/widget.d.ts +27 -0
- package/dist/widget.js +159 -12
- package/package.json +28 -4
- package/build.js +0 -47
- package/src/widget.ts +0 -244
package/README.md
CHANGED
|
@@ -1,533 +1,89 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @uservibesos/web-component
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
⚠️ **API KEY REQUIRED** - This widget requires authentication. Only compatible with server-rendered applications.
|
|
3
|
+
Feature request and bug report widget as a Web Component for UserVibesOS.
|
|
6
4
|
|
|
7
5
|
## Installation
|
|
8
6
|
|
|
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
7
|
```bash
|
|
20
|
-
npm install @
|
|
8
|
+
npm install @uservibesos/web-component
|
|
21
9
|
```
|
|
22
10
|
|
|
23
|
-
|
|
11
|
+
Or use via CDN:
|
|
24
12
|
|
|
25
|
-
```
|
|
26
|
-
|
|
13
|
+
```html
|
|
14
|
+
<script src="https://app.uservibesos.com/widget-assets/v1.0.0/widget.js"></script>
|
|
27
15
|
```
|
|
28
16
|
|
|
29
17
|
## Usage
|
|
30
18
|
|
|
31
|
-
###
|
|
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
|
|
19
|
+
### Basic Usage (with JWT)
|
|
65
20
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
widget
|
|
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
|
-
}
|
|
21
|
+
```html
|
|
22
|
+
<uservibes-widget
|
|
23
|
+
project="your-project-slug"
|
|
24
|
+
jwt="your-jwt-token"
|
|
25
|
+
theme="light"
|
|
26
|
+
></uservibes-widget>
|
|
110
27
|
```
|
|
111
28
|
|
|
112
|
-
### With Custom
|
|
29
|
+
### With Custom Height
|
|
113
30
|
|
|
114
|
-
```
|
|
115
|
-
<
|
|
116
|
-
project="
|
|
117
|
-
|
|
118
|
-
theme="dark"
|
|
31
|
+
```html
|
|
32
|
+
<uservibes-widget
|
|
33
|
+
project="your-project-slug"
|
|
34
|
+
jwt="your-jwt-token"
|
|
119
35
|
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
|
-
}
|
|
36
|
+
></uservibes-widget>
|
|
158
37
|
```
|
|
159
38
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
UserVibeOS offers two security tiers for widget authentication. Choose based on your security requirements.
|
|
163
|
-
|
|
164
|
-
---
|
|
39
|
+
### Attributes
|
|
165
40
|
|
|
166
|
-
|
|
41
|
+
- `project` (required): Your project slug or ID
|
|
42
|
+
- `jwt` (optional): JWT token for authenticated users
|
|
43
|
+
- `theme` (optional): `light` or `dark` (default: `light`)
|
|
44
|
+
- `height` (optional): Widget height (default: `90vh`)
|
|
45
|
+
- `mode` (optional): `feature-request` or `roadmap` (default: `feature-request`)
|
|
46
|
+
- `base-url` (optional): Custom API base URL (for development)
|
|
167
47
|
|
|
168
|
-
|
|
48
|
+
## Features
|
|
169
49
|
|
|
170
|
-
|
|
171
|
-
-
|
|
172
|
-
-
|
|
173
|
-
-
|
|
174
|
-
-
|
|
50
|
+
- ✅ Feature request submission with AI-powered chat extraction (max 4 questions)
|
|
51
|
+
- ✅ Bug report submission with structured field extraction (max 7 questions)
|
|
52
|
+
- ✅ Roadmap view
|
|
53
|
+
- ✅ Changelog view
|
|
54
|
+
- ✅ Voting and commenting
|
|
55
|
+
- ✅ JWT authentication support
|
|
56
|
+
- ✅ Dark mode support
|
|
57
|
+
- ✅ Responsive design
|
|
58
|
+
- ✅ Zero dependencies (vanilla web component)
|
|
175
59
|
|
|
176
|
-
|
|
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
|
|
60
|
+
## Development
|
|
182
61
|
|
|
183
|
-
**Setup:**
|
|
184
62
|
```bash
|
|
185
|
-
#
|
|
186
|
-
|
|
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();
|
|
63
|
+
# Install dependencies
|
|
64
|
+
npm install
|
|
281
65
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
token: data.token,
|
|
285
|
-
expiresAt: data.expiresAt,
|
|
286
|
-
};
|
|
66
|
+
# Build the widget
|
|
67
|
+
npm run build
|
|
287
68
|
|
|
288
|
-
|
|
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);
|
|
69
|
+
# Watch mode for development
|
|
70
|
+
npm run dev
|
|
376
71
|
```
|
|
377
72
|
|
|
378
|
-
|
|
73
|
+
## Publishing
|
|
379
74
|
|
|
380
|
-
|
|
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
|
|
75
|
+
The widget is automatically published to NPM when a new release is created on GitHub. You can also publish manually:
|
|
399
76
|
|
|
400
77
|
```bash
|
|
401
|
-
|
|
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
|
|
78
|
+
npm run publish:npm
|
|
411
79
|
```
|
|
412
80
|
|
|
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
81
|
## License
|
|
532
82
|
|
|
533
83
|
MIT
|
|
84
|
+
|
|
85
|
+
## Links
|
|
86
|
+
|
|
87
|
+
- [NPM Package](https://www.npmjs.com/package/@uservibesos/web-component)
|
|
88
|
+
- [GitHub Repository](https://github.com/harperaa/uservibesOS)
|
|
89
|
+
- [UserVibesOS Website](https://uservibesos.com)
|
package/dist/widget.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface InitConfig {
|
|
2
|
+
projectId: string;
|
|
3
|
+
jwt?: string;
|
|
4
|
+
container: string | HTMLElement;
|
|
5
|
+
theme?: string;
|
|
6
|
+
height?: string;
|
|
7
|
+
mode?: string;
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface WidgetInstance {
|
|
12
|
+
element: HTMLElement;
|
|
13
|
+
setJWT: (jwt: string) => void;
|
|
14
|
+
destroy: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export declare function init(config: InitConfig): WidgetInstance | null;
|
|
18
|
+
|
|
19
|
+
export declare class UserVibesWidget extends HTMLElement {
|
|
20
|
+
setJWT(jwt: string): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export declare const UserVibesOS: {
|
|
24
|
+
init: typeof init;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default UserVibesOS;
|
package/dist/widget.js
CHANGED
|
@@ -1,15 +1,117 @@
|
|
|
1
|
-
"
|
|
1
|
+
function g(n){if(typeof document>"u"||document.querySelector(`link[href="${n}"]`))return;let s=document.createElement("link");s.rel="preconnect",s.href=n,document.head.appendChild(s)}var m=class extends HTMLElement{constructor(){super();this.baseUrl="https://app.uservibesos.com";this.iframe=null;this.skeleton=null;this.loaded=!1;this.isFullscreen=!1;this.originalIframeStyles="";this.attachShadow({mode:"open"})}static get observedAttributes(){return["project","jwt","theme","height","mode","base-url"]}connectedCallback(){this.render()}attributeChangedCallback(e,t,r){t!==r&&this.render()}render(){let e=this.getAttribute("project"),t=this.getAttribute("jwt"),r=this.getAttribute("theme")||"light",a=this.getAttribute("height")||"90vh",l=this.getAttribute("mode")||"feature-request",d=this.getAttribute("base-url");if(!e){this.renderError('Missing required "project" attribute (project ID)');return}d?this.baseUrl=d:typeof window<"u"&&window.location.hostname==="localhost"&&(this.baseUrl=window.location.origin),g(this.baseUrl);let o=`${this.baseUrl}/embed?projectId=${encodeURIComponent(e)}&mode=${l}`;t&&(o+=`&jwt=${encodeURIComponent(t)}`);let i=`
|
|
2
2
|
<style>
|
|
3
3
|
:host {
|
|
4
4
|
display: block;
|
|
5
5
|
width: 100%;
|
|
6
6
|
}
|
|
7
|
+
.widget-container {
|
|
8
|
+
position: relative;
|
|
9
|
+
min-height: ${a};
|
|
10
|
+
}
|
|
7
11
|
iframe {
|
|
8
12
|
width: 100%;
|
|
9
|
-
height: ${
|
|
13
|
+
height: ${a};
|
|
10
14
|
border: none;
|
|
11
15
|
border-radius: 8px;
|
|
12
16
|
overflow: hidden;
|
|
17
|
+
opacity: 0;
|
|
18
|
+
transition: opacity 0.2s ease-in-out;
|
|
19
|
+
}
|
|
20
|
+
iframe.loaded {
|
|
21
|
+
opacity: 1;
|
|
22
|
+
}
|
|
23
|
+
iframe.fullscreen {
|
|
24
|
+
position: fixed !important;
|
|
25
|
+
top: 0 !important;
|
|
26
|
+
left: 0 !important;
|
|
27
|
+
right: 0 !important;
|
|
28
|
+
bottom: 0 !important;
|
|
29
|
+
width: 100vw !important;
|
|
30
|
+
height: 100vh !important;
|
|
31
|
+
max-width: 100vw !important;
|
|
32
|
+
max-height: 100vh !important;
|
|
33
|
+
border-radius: 0 !important;
|
|
34
|
+
z-index: 999999 !important;
|
|
35
|
+
margin: 0 !important;
|
|
36
|
+
padding: 0 !important;
|
|
37
|
+
}
|
|
38
|
+
.skeleton {
|
|
39
|
+
position: absolute;
|
|
40
|
+
top: 0;
|
|
41
|
+
left: 0;
|
|
42
|
+
right: 0;
|
|
43
|
+
padding: 1.5rem;
|
|
44
|
+
background: #f9fafb;
|
|
45
|
+
border-radius: 8px;
|
|
46
|
+
font-family: system-ui, sans-serif;
|
|
47
|
+
}
|
|
48
|
+
.skeleton.hidden {
|
|
49
|
+
display: none;
|
|
50
|
+
}
|
|
51
|
+
.skeleton-header {
|
|
52
|
+
display: flex;
|
|
53
|
+
justify-content: space-between;
|
|
54
|
+
align-items: center;
|
|
55
|
+
margin-bottom: 1.5rem;
|
|
56
|
+
padding-bottom: 1rem;
|
|
57
|
+
border-bottom: 1px solid #e5e7eb;
|
|
58
|
+
}
|
|
59
|
+
.skeleton-title {
|
|
60
|
+
height: 1.5rem;
|
|
61
|
+
width: 180px;
|
|
62
|
+
background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%);
|
|
63
|
+
background-size: 200% 100%;
|
|
64
|
+
animation: shimmer 1.5s infinite;
|
|
65
|
+
border-radius: 4px;
|
|
66
|
+
}
|
|
67
|
+
.skeleton-button {
|
|
68
|
+
height: 2.25rem;
|
|
69
|
+
width: 100px;
|
|
70
|
+
background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%);
|
|
71
|
+
background-size: 200% 100%;
|
|
72
|
+
animation: shimmer 1.5s infinite;
|
|
73
|
+
border-radius: 6px;
|
|
74
|
+
}
|
|
75
|
+
.skeleton-tabs {
|
|
76
|
+
display: flex;
|
|
77
|
+
gap: 0.5rem;
|
|
78
|
+
margin-bottom: 1rem;
|
|
79
|
+
}
|
|
80
|
+
.skeleton-tab {
|
|
81
|
+
height: 2rem;
|
|
82
|
+
width: 60px;
|
|
83
|
+
background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%);
|
|
84
|
+
background-size: 200% 100%;
|
|
85
|
+
animation: shimmer 1.5s infinite;
|
|
86
|
+
border-radius: 4px;
|
|
87
|
+
}
|
|
88
|
+
.skeleton-item {
|
|
89
|
+
padding: 1rem;
|
|
90
|
+
margin-bottom: 0.75rem;
|
|
91
|
+
background: white;
|
|
92
|
+
border: 1px solid #e5e7eb;
|
|
93
|
+
border-radius: 8px;
|
|
94
|
+
}
|
|
95
|
+
.skeleton-item-title {
|
|
96
|
+
height: 1rem;
|
|
97
|
+
width: 70%;
|
|
98
|
+
background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%);
|
|
99
|
+
background-size: 200% 100%;
|
|
100
|
+
animation: shimmer 1.5s infinite;
|
|
101
|
+
border-radius: 4px;
|
|
102
|
+
margin-bottom: 0.5rem;
|
|
103
|
+
}
|
|
104
|
+
.skeleton-item-desc {
|
|
105
|
+
height: 0.75rem;
|
|
106
|
+
width: 90%;
|
|
107
|
+
background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%);
|
|
108
|
+
background-size: 200% 100%;
|
|
109
|
+
animation: shimmer 1.5s infinite;
|
|
110
|
+
border-radius: 4px;
|
|
111
|
+
}
|
|
112
|
+
@keyframes shimmer {
|
|
113
|
+
0% { background-position: 200% 0; }
|
|
114
|
+
100% { background-position: -200% 0; }
|
|
13
115
|
}
|
|
14
116
|
.error {
|
|
15
117
|
padding: 2rem;
|
|
@@ -18,16 +120,44 @@
|
|
|
18
120
|
border-radius: 8px;
|
|
19
121
|
background: #fef2f2;
|
|
20
122
|
color: #991b1b;
|
|
123
|
+
font-family: system-ui, sans-serif;
|
|
21
124
|
}
|
|
22
125
|
</style>
|
|
23
|
-
`,
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
126
|
+
`,f=`
|
|
127
|
+
<div class="widget-container">
|
|
128
|
+
|
|
129
|
+
<div class="skeleton" id="skeleton">
|
|
130
|
+
<div class="skeleton-header">
|
|
131
|
+
<div class="skeleton-title"></div>
|
|
132
|
+
<div class="skeleton-button"></div>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="skeleton-tabs">
|
|
135
|
+
<div class="skeleton-tab"></div>
|
|
136
|
+
<div class="skeleton-tab"></div>
|
|
137
|
+
<div class="skeleton-tab"></div>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="skeleton-item">
|
|
140
|
+
<div class="skeleton-item-title"></div>
|
|
141
|
+
<div class="skeleton-item-desc"></div>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="skeleton-item">
|
|
144
|
+
<div class="skeleton-item-title"></div>
|
|
145
|
+
<div class="skeleton-item-desc"></div>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="skeleton-item">
|
|
148
|
+
<div class="skeleton-item-title"></div>
|
|
149
|
+
<div class="skeleton-item-desc"></div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<iframe
|
|
154
|
+
src="${o}"
|
|
155
|
+
title="Feature Requests"
|
|
156
|
+
loading="lazy"
|
|
157
|
+
allow="clipboard-write"
|
|
158
|
+
></iframe>
|
|
159
|
+
</div>
|
|
160
|
+
`;this.shadowRoot&&(this.shadowRoot.innerHTML=i+f,this.iframe=this.shadowRoot.querySelector("iframe"),this.skeleton=this.shadowRoot.querySelector("#skeleton"),this.setupLoadHandler(),this.setupMessageListener())}setupLoadHandler(){this.iframe&&(this.iframe.addEventListener("load",()=>{this.loaded=!0,this.iframe.classList.add("loaded"),this.skeleton&&this.skeleton.classList.add("hidden"),console.log("[Widget] DEBUG: Iframe loaded, sending USERVIBES_INIT to:",this.baseUrl),this.iframe.contentWindow?(this.iframe.contentWindow.postMessage({type:"USERVIBES_INIT"},this.baseUrl),console.log("[Widget] DEBUG: \u2705 USERVIBES_INIT message sent")):console.error("[Widget] DEBUG: \u274C No contentWindow, cannot send USERVIBES_INIT")}),this.iframe.addEventListener("error",()=>{this.renderError("Failed to load widget. Please try again later.")}),setTimeout(()=>{!this.loaded&&this.iframe&&(this.iframe.classList.add("loaded"),this.skeleton&&this.skeleton.classList.add("hidden"))},1e4))}renderError(e){let r=`
|
|
31
161
|
<style>
|
|
32
162
|
.error {
|
|
33
163
|
padding: 2rem;
|
|
@@ -36,9 +166,26 @@
|
|
|
36
166
|
border-radius: 8px;
|
|
37
167
|
background: #fef2f2;
|
|
38
168
|
color: #991b1b;
|
|
169
|
+
font-family: system-ui, sans-serif;
|
|
39
170
|
}
|
|
40
171
|
</style>
|
|
41
172
|
<div class="error">
|
|
42
|
-
<strong>Widget Error:</strong> ${e}
|
|
173
|
+
<strong>Widget Error:</strong> ${this.escapeHtml(e)}
|
|
43
174
|
</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}))
|
|
175
|
+
`;this.shadowRoot&&(this.shadowRoot.innerHTML=r)}escapeHtml(e){if(!e||typeof e!="string")return"";let t=document.createElement("div");return t.textContent=e,t.innerHTML}setupMessageListener(){window.addEventListener("message",e=>{console.log("[Widget] Received message:",e.data?.type,"from:",e.origin,"expected:",this.baseUrl),e.origin===this.baseUrl&&(e.data.type==="USERVIBES_READY"&&this.iframe?.contentWindow&&(console.log("[Widget] DEBUG: Received USERVIBES_READY, resending USERVIBES_INIT"),this.iframe.contentWindow.postMessage({type:"USERVIBES_INIT"},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})),e.data.type==="USERVIBES_ENTER_FULLSCREEN"&&(console.log("[Widget] Received ENTER_FULLSCREEN, applying fullscreen styles to iframe"),this.isFullscreen=!0,this.iframe&&(this.originalIframeStyles=this.iframe.getAttribute("style")||""),this.iframe&&(this.iframe.style.cssText=`
|
|
176
|
+
position: fixed !important;
|
|
177
|
+
top: 0 !important;
|
|
178
|
+
left: 0 !important;
|
|
179
|
+
right: 0 !important;
|
|
180
|
+
bottom: 0 !important;
|
|
181
|
+
width: 100vw !important;
|
|
182
|
+
height: 100vh !important;
|
|
183
|
+
max-width: 100vw !important;
|
|
184
|
+
max-height: 100vh !important;
|
|
185
|
+
border: none !important;
|
|
186
|
+
border-radius: 0 !important;
|
|
187
|
+
z-index: 999999 !important;
|
|
188
|
+
margin: 0 !important;
|
|
189
|
+
padding: 0 !important;
|
|
190
|
+
opacity: 1 !important;
|
|
191
|
+
`),document.body.style.overflow="hidden",console.log("[Widget] Applied fullscreen styles to iframe")),e.data.type==="USERVIBES_EXIT_FULLSCREEN"&&(console.log("[Widget] Received EXIT_FULLSCREEN, removing fullscreen styles"),this.iframe&&(this.originalIframeStyles?this.iframe.setAttribute("style",this.originalIframeStyles):this.iframe.removeAttribute("style")),document.body.style.overflow="",this.isFullscreen=!1,console.log("[Widget] Removed fullscreen styles")))})}setJWT(e){this.setAttribute("jwt",e)}};typeof window<"u"&&typeof customElements<"u"&&!customElements.get("uservibes-widget")&&customElements.define("uservibes-widget",m);function p(n){let{projectId:s,jwt:e,container:t,theme:r,height:a,mode:l,baseUrl:d}=n;if(!s)return console.error('[UserVibesOS] Missing required "projectId" in config'),null;if(!t)return console.error('[UserVibesOS] Missing required "container" in config'),null;let o=typeof t=="string"?document.querySelector(t):t;if(!o)return console.error(`[UserVibesOS] Container "${t}" not found`),null;let i=document.createElement("uservibes-widget");return i.setAttribute("project",s),e&&i.setAttribute("jwt",e),r&&i.setAttribute("theme",r),a&&i.setAttribute("height",a),l&&i.setAttribute("mode",l),d&&i.setAttribute("base-url",d),o.innerHTML="",o.appendChild(i),{element:i,setJWT:c=>i.setJWT(c),destroy:()=>o.removeChild(i)}}var h={init:p};var u=h;typeof window<"u"&&(window.UserVibesOS=h);export{h as UserVibesOS,m as UserVibesWidget,u as default,p as init};
|
package/package.json
CHANGED
|
@@ -1,21 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uservibesos/web-component",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Feature request widget as a Web Component",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"main": "dist/widget.js",
|
|
7
|
+
"module": "dist/widget.js",
|
|
8
|
+
"types": "dist/widget.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/widget.js",
|
|
12
|
+
"types": "./dist/widget.d.ts",
|
|
13
|
+
"default": "./dist/widget.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
6
19
|
"scripts": {
|
|
7
20
|
"build": "node build.js",
|
|
8
|
-
"dev": "node build.js --watch"
|
|
21
|
+
"dev": "node build.js --watch",
|
|
22
|
+
"prepublishOnly": "npm run build",
|
|
23
|
+
"publish:npm": "npm publish --access public"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/harperaa/uservibesOS.git",
|
|
28
|
+
"directory": "packages/web-component"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
9
32
|
},
|
|
10
33
|
"keywords": [
|
|
11
34
|
"feature-request",
|
|
12
35
|
"web-component",
|
|
13
36
|
"custom-element",
|
|
14
|
-
"widget"
|
|
37
|
+
"widget",
|
|
38
|
+
"uservibesos"
|
|
15
39
|
],
|
|
16
40
|
"author": "UserVibesOS",
|
|
17
41
|
"license": "MIT",
|
|
18
42
|
"devDependencies": {
|
|
19
|
-
"esbuild": "^0.
|
|
43
|
+
"esbuild": "^0.25.0"
|
|
20
44
|
}
|
|
21
45
|
}
|
package/build.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
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/src/widget.ts
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
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;
|