@uservibesos/web-component 1.0.1 → 1.0.4
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.js +38 -7
- package/package.json +13 -3
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.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function
|
|
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;
|
|
@@ -6,11 +6,11 @@ function f(o){if(typeof document>"u"||document.querySelector(`link[href="${o}"]`
|
|
|
6
6
|
}
|
|
7
7
|
.widget-container {
|
|
8
8
|
position: relative;
|
|
9
|
-
min-height: ${
|
|
9
|
+
min-height: ${a};
|
|
10
10
|
}
|
|
11
11
|
iframe {
|
|
12
12
|
width: 100%;
|
|
13
|
-
height: ${
|
|
13
|
+
height: ${a};
|
|
14
14
|
border: none;
|
|
15
15
|
border-radius: 8px;
|
|
16
16
|
overflow: hidden;
|
|
@@ -20,6 +20,21 @@ function f(o){if(typeof document>"u"||document.querySelector(`link[href="${o}"]`
|
|
|
20
20
|
iframe.loaded {
|
|
21
21
|
opacity: 1;
|
|
22
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
|
+
}
|
|
23
38
|
.skeleton {
|
|
24
39
|
position: absolute;
|
|
25
40
|
top: 0;
|
|
@@ -108,7 +123,7 @@ function f(o){if(typeof document>"u"||document.querySelector(`link[href="${o}"]`
|
|
|
108
123
|
font-family: system-ui, sans-serif;
|
|
109
124
|
}
|
|
110
125
|
</style>
|
|
111
|
-
`,
|
|
126
|
+
`,f=`
|
|
112
127
|
<div class="widget-container">
|
|
113
128
|
|
|
114
129
|
<div class="skeleton" id="skeleton">
|
|
@@ -136,13 +151,13 @@ function f(o){if(typeof document>"u"||document.querySelector(`link[href="${o}"]`
|
|
|
136
151
|
</div>
|
|
137
152
|
|
|
138
153
|
<iframe
|
|
139
|
-
src="${
|
|
154
|
+
src="${o}"
|
|
140
155
|
title="Feature Requests"
|
|
141
156
|
loading="lazy"
|
|
142
157
|
allow="clipboard-write"
|
|
143
158
|
></iframe>
|
|
144
159
|
</div>
|
|
145
|
-
`;this.shadowRoot&&(this.shadowRoot.innerHTML=i+
|
|
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=`
|
|
146
161
|
<style>
|
|
147
162
|
.error {
|
|
148
163
|
padding: 2rem;
|
|
@@ -157,4 +172,20 @@ function f(o){if(typeof document>"u"||document.querySelector(`link[href="${o}"]`
|
|
|
157
172
|
<div class="error">
|
|
158
173
|
<strong>Widget Error:</strong> ${this.escapeHtml(e)}
|
|
159
174
|
</div>
|
|
160
|
-
`;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=>{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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uservibesos/web-component",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Feature request widget as a Web Component",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/widget.js",
|
|
@@ -18,7 +18,17 @@
|
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "node build.js",
|
|
21
|
-
"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"
|
|
22
32
|
},
|
|
23
33
|
"keywords": [
|
|
24
34
|
"feature-request",
|
|
@@ -30,6 +40,6 @@
|
|
|
30
40
|
"author": "UserVibesOS",
|
|
31
41
|
"license": "MIT",
|
|
32
42
|
"devDependencies": {
|
|
33
|
-
"esbuild": "^0.
|
|
43
|
+
"esbuild": "^0.25.0"
|
|
34
44
|
}
|
|
35
45
|
}
|