@mindbowser_inc/ui-feedback-plugin 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +330 -60
- package/dist/aws-CkWYvUhs.mjs +134 -0
- package/dist/aws-CkWYvUhs.mjs.map +1 -0
- package/dist/index-B-1NC_TX.mjs +12047 -0
- package/dist/index-B-1NC_TX.mjs.map +1 -0
- package/dist/index-ClYqXfVs.mjs +183 -0
- package/dist/index-ClYqXfVs.mjs.map +1 -0
- package/dist/{index-BcqBbswO.mjs → index-D6dCjvzy.mjs} +147 -181
- package/dist/index-D6dCjvzy.mjs.map +1 -0
- package/dist/index.browser-CKeO7JYa.mjs +190 -0
- package/dist/index.browser-CKeO7JYa.mjs.map +1 -0
- package/dist/index.d.ts +40 -41
- package/dist/loadCognitoIdentity-V6H3EoBY.mjs +957 -0
- package/dist/loadCognitoIdentity-V6H3EoBY.mjs.map +1 -0
- package/dist/stream-collector-mh16BUWl.mjs +4032 -0
- package/dist/stream-collector-mh16BUWl.mjs.map +1 -0
- package/dist/{supabase-BUHP2OkK.mjs → supabase-CbB80UVb.mjs} +2 -2
- package/dist/{supabase-BUHP2OkK.mjs.map → supabase-CbB80UVb.mjs.map} +1 -1
- package/dist/tslib.es6-BNkLP3y-.mjs +112 -0
- package/dist/tslib.es6-BNkLP3y-.mjs.map +1 -0
- package/dist/ui-feedback-plugin.es.js +158 -173
- package/dist/ui-feedback-plugin.es.js.map +1 -1
- package/dist/ui-feedback-plugin.umd.js +250 -228
- package/dist/ui-feedback-plugin.umd.js.map +1 -1
- package/package.json +8 -4
- package/dist/index-BcqBbswO.mjs.map +0 -1
- package/dist/s3-DkzKoz0A.mjs +0 -32
- package/dist/s3-DkzKoz0A.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -8,9 +8,7 @@ A drop-in floating widget for collecting annotated UI feedback on web prototypes
|
|
|
8
8
|
- Full-page screenshots with annotations
|
|
9
9
|
- Comment threads with replies
|
|
10
10
|
- Open / Resolved filter
|
|
11
|
-
-
|
|
12
|
-
- Firebase **or** Supabase backend
|
|
13
|
-
- S3 screenshot storage via a backend proxy
|
|
11
|
+
- Three fully independent backend options — no lock-in
|
|
14
12
|
|
|
15
13
|
---
|
|
16
14
|
|
|
@@ -22,140 +20,412 @@ npm install @mindbowser_inc/ui-feedback-plugin
|
|
|
22
20
|
|
|
23
21
|
---
|
|
24
22
|
|
|
25
|
-
##
|
|
23
|
+
## Backends
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
Choose **one** backend. They are fully independent — switch any time by changing the `backend` config.
|
|
26
|
+
|
|
27
|
+
| Backend | Database | Screenshots | Real-time |
|
|
28
|
+
|---|---|---|---|
|
|
29
|
+
| **Firebase** | Firestore | Firebase Storage | Yes (onSnapshot) |
|
|
30
|
+
| **Supabase** | Postgres | Supabase Storage | Yes (Realtime) |
|
|
31
|
+
| **AWS** | JSON files in S3 | S3 | Polling (5 s default) |
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Firebase
|
|
36
|
+
|
|
37
|
+
Firestore stores comments and replies. Firebase Storage holds screenshots. No backend required — Firebase keys are public identifiers, secured by Security Rules.
|
|
38
|
+
|
|
39
|
+
### Quick start
|
|
28
40
|
|
|
29
41
|
```js
|
|
30
42
|
import { initFeedbackPlugin } from '@mindbowser_inc/ui-feedback-plugin'
|
|
31
43
|
|
|
32
44
|
initFeedbackPlugin({
|
|
33
|
-
|
|
34
|
-
provider:
|
|
35
|
-
apiKey:
|
|
36
|
-
authDomain:
|
|
37
|
-
projectId:
|
|
38
|
-
storageBucket:
|
|
39
|
-
messagingSenderId:
|
|
40
|
-
appId:
|
|
45
|
+
backend: {
|
|
46
|
+
provider: 'firebase',
|
|
47
|
+
apiKey: 'AIzaSy...',
|
|
48
|
+
authDomain: 'your-project.firebaseapp.com',
|
|
49
|
+
projectId: 'your-project',
|
|
50
|
+
storageBucket: 'your-project.appspot.com',
|
|
51
|
+
messagingSenderId: '123456789',
|
|
52
|
+
appId: '1:123456789:web:abc123',
|
|
41
53
|
},
|
|
42
54
|
projectKey: 'my-project',
|
|
43
55
|
})
|
|
44
56
|
```
|
|
45
57
|
|
|
46
|
-
> The Firebase
|
|
58
|
+
> The Firebase `apiKey` is a **public identifier**, not a secret. Secure your data with [Firebase Security Rules](https://firebase.google.com/docs/rules).
|
|
59
|
+
|
|
60
|
+
### Firebase console setup
|
|
61
|
+
|
|
62
|
+
1. Go to [console.firebase.google.com](https://console.firebase.google.com) and create a project.
|
|
63
|
+
2. **Firestore** → Create database → Start in test mode (then lock it down with rules below).
|
|
64
|
+
3. **Storage** → Get started → set rules below.
|
|
65
|
+
4. **Project Settings** → Your apps → Add a web app → copy the config values above.
|
|
66
|
+
|
|
67
|
+
### Recommended Security Rules
|
|
68
|
+
|
|
69
|
+
**Firestore** (`firestore.rules`):
|
|
70
|
+
```
|
|
71
|
+
rules_version = '2';
|
|
72
|
+
service cloud.firestore {
|
|
73
|
+
match /databases/{database}/documents {
|
|
74
|
+
match /ufp_comments/{doc} {
|
|
75
|
+
allow read, write: if true; // Replace with auth checks for production
|
|
76
|
+
}
|
|
77
|
+
match /ufp_replies/{doc} {
|
|
78
|
+
allow read, write: if true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Storage** (`storage.rules`):
|
|
85
|
+
```
|
|
86
|
+
rules_version = '2';
|
|
87
|
+
service firebase.storage {
|
|
88
|
+
match /b/{bucket}/o {
|
|
89
|
+
match /ufp/{allPaths=**} {
|
|
90
|
+
allow read, write: if true; // Replace with auth checks for production
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Supabase
|
|
99
|
+
|
|
100
|
+
Postgres stores comments and replies. Supabase Storage holds screenshots. No backend required — the `anonKey` is a public key secured by Row Level Security.
|
|
47
101
|
|
|
48
|
-
###
|
|
102
|
+
### Quick start
|
|
49
103
|
|
|
50
104
|
```js
|
|
51
105
|
import { initFeedbackPlugin } from '@mindbowser_inc/ui-feedback-plugin'
|
|
52
106
|
|
|
53
107
|
initFeedbackPlugin({
|
|
54
|
-
|
|
108
|
+
backend: {
|
|
55
109
|
provider: 'supabase',
|
|
56
|
-
url:
|
|
57
|
-
anonKey:
|
|
110
|
+
url: 'https://xxxx.supabase.co',
|
|
111
|
+
anonKey: 'eyJhbGci...',
|
|
58
112
|
},
|
|
59
113
|
projectKey: 'my-project',
|
|
60
114
|
})
|
|
61
115
|
```
|
|
62
116
|
|
|
63
|
-
> The Supabase `anonKey` is a **public key by design**. Secure your data with [Row Level Security (RLS)](https://supabase.com/docs/guides/auth/row-level-security)
|
|
117
|
+
> The Supabase `anonKey` is a **public key by design**. Secure your data with [Row Level Security (RLS)](https://supabase.com/docs/guides/auth/row-level-security).
|
|
118
|
+
|
|
119
|
+
### Supabase setup
|
|
64
120
|
|
|
65
|
-
|
|
121
|
+
1. Create a project at [supabase.com](https://supabase.com).
|
|
122
|
+
2. Go to **SQL Editor** and run the SQL below.
|
|
123
|
+
3. Go to **Storage** → New bucket → name it `ufp-screenshots` → set it to **Public**.
|
|
124
|
+
4. Run the storage policy SQL below.
|
|
125
|
+
5. Find your URL and anon key in **Project Settings → API**.
|
|
66
126
|
|
|
127
|
+
**Database SQL** (run once in SQL Editor):
|
|
67
128
|
```sql
|
|
68
129
|
create table if not exists ufp_comments (
|
|
69
|
-
id
|
|
70
|
-
page_url
|
|
71
|
-
project_key
|
|
72
|
-
author_name
|
|
73
|
-
text
|
|
74
|
-
screenshot_url text
|
|
75
|
-
annotation
|
|
76
|
-
resolved
|
|
77
|
-
created_at
|
|
130
|
+
id uuid primary key default gen_random_uuid(),
|
|
131
|
+
page_url text not null,
|
|
132
|
+
project_key text not null default 'default',
|
|
133
|
+
author_name text not null,
|
|
134
|
+
text text not null,
|
|
135
|
+
screenshot_url text default '',
|
|
136
|
+
annotation jsonb not null default '{}',
|
|
137
|
+
resolved boolean default false,
|
|
138
|
+
created_at bigint not null
|
|
78
139
|
);
|
|
79
140
|
|
|
80
141
|
create table if not exists ufp_replies (
|
|
81
|
-
id
|
|
82
|
-
comment_id
|
|
142
|
+
id uuid primary key default gen_random_uuid(),
|
|
143
|
+
comment_id uuid not null references ufp_comments(id) on delete cascade,
|
|
83
144
|
author_name text not null,
|
|
84
|
-
text
|
|
85
|
-
created_at
|
|
145
|
+
text text not null,
|
|
146
|
+
created_at bigint not null
|
|
86
147
|
);
|
|
87
148
|
|
|
149
|
+
-- Enable Row Level Security
|
|
88
150
|
alter table ufp_comments enable row level security;
|
|
89
|
-
alter table ufp_replies
|
|
151
|
+
alter table ufp_replies enable row level security;
|
|
152
|
+
|
|
153
|
+
-- Allow all operations (restrict per your auth strategy in production)
|
|
90
154
|
create policy "ufp_comments_all" on ufp_comments for all using (true) with check (true);
|
|
91
|
-
create policy "ufp_replies_all"
|
|
155
|
+
create policy "ufp_replies_all" on ufp_replies for all using (true) with check (true);
|
|
92
156
|
|
|
157
|
+
-- Grant table access to the anon role (RLS policies alone are not enough)
|
|
93
158
|
grant select, insert, update, delete on ufp_comments to anon;
|
|
94
|
-
grant select, insert, update, delete on ufp_replies
|
|
159
|
+
grant select, insert, update, delete on ufp_replies to anon;
|
|
95
160
|
|
|
161
|
+
-- Enable real-time for comments
|
|
96
162
|
alter publication supabase_realtime add table ufp_comments;
|
|
97
163
|
```
|
|
98
164
|
|
|
99
|
-
|
|
100
|
-
|
|
165
|
+
**Storage policy SQL** (run after creating the `ufp-screenshots` bucket):
|
|
101
166
|
```sql
|
|
102
|
-
create policy "ufp_storage_insert" on storage.objects
|
|
103
|
-
|
|
104
|
-
|
|
167
|
+
create policy "ufp_storage_insert" on storage.objects
|
|
168
|
+
for insert to anon with check (bucket_id = 'ufp-screenshots');
|
|
169
|
+
|
|
170
|
+
create policy "ufp_storage_select" on storage.objects
|
|
171
|
+
for select to anon using (bucket_id = 'ufp-screenshots');
|
|
172
|
+
|
|
173
|
+
create policy "ufp_storage_update" on storage.objects
|
|
174
|
+
for update to anon using (bucket_id = 'ufp-screenshots');
|
|
105
175
|
```
|
|
106
176
|
|
|
107
177
|
---
|
|
108
178
|
|
|
109
|
-
##
|
|
179
|
+
## AWS (Cognito Identity Pool)
|
|
180
|
+
|
|
181
|
+
Everything — comments, replies, and screenshots — is stored directly in S3 as JSON and PNG files. No backend server required. No Firebase or Supabase needed.
|
|
110
182
|
|
|
111
|
-
|
|
183
|
+
The browser obtains **temporary, rotating credentials** from a Cognito Identity Pool. No long-lived AWS keys ever touch the browser. The IAM role follows the least-privilege principle (file-type restrictions, size limits, no delete permission).
|
|
184
|
+
|
|
185
|
+
### Quick start
|
|
112
186
|
|
|
113
187
|
```js
|
|
188
|
+
import { initFeedbackPlugin } from '@mindbowser_inc/ui-feedback-plugin'
|
|
189
|
+
|
|
114
190
|
initFeedbackPlugin({
|
|
115
|
-
|
|
116
|
-
provider:
|
|
117
|
-
|
|
118
|
-
|
|
191
|
+
backend: {
|
|
192
|
+
provider: 'aws',
|
|
193
|
+
region: 'us-east-1',
|
|
194
|
+
bucket: 'your-bucket-name',
|
|
195
|
+
identityPoolId: 'us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
|
|
196
|
+
pollInterval: 5000, // optional, default 5000 ms
|
|
119
197
|
},
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
198
|
+
projectKey: 'my-project',
|
|
199
|
+
})
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### AWS setup (step by step)
|
|
203
|
+
|
|
204
|
+
#### Step 1 — Create an S3 bucket
|
|
205
|
+
|
|
206
|
+
1. Open the [S3 console](https://s3.console.aws.amazon.com/s3).
|
|
207
|
+
2. Click **Create bucket**.
|
|
208
|
+
3. Choose a name and region (remember these — you'll need them in the plugin config).
|
|
209
|
+
4. Leave **Block all public access** ON (you'll open just the screenshots prefix below).
|
|
210
|
+
5. Enable **Versioning** (optional, but useful for recovery).
|
|
211
|
+
6. Create the bucket.
|
|
212
|
+
|
|
213
|
+
#### Step 2 — Configure CORS
|
|
214
|
+
|
|
215
|
+
Go to your bucket → **Permissions** → **Cross-origin resource sharing (CORS)** → Edit, and paste:
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
[
|
|
219
|
+
{
|
|
220
|
+
"AllowedOrigins": ["https://your-domain.com"],
|
|
221
|
+
"AllowedMethods": ["GET", "PUT", "HEAD"],
|
|
222
|
+
"AllowedHeaders": ["*"],
|
|
223
|
+
"ExposeHeaders": ["ETag"],
|
|
224
|
+
"MaxAgeSeconds": 3000
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
> Use `"*"` for `AllowedOrigins` during local development. Restrict to real domains in production.
|
|
230
|
+
|
|
231
|
+
#### Step 3 — Bucket policy (public read for screenshots only)
|
|
232
|
+
|
|
233
|
+
Go to bucket → **Permissions** → **Bucket policy** → Edit, and paste (replace `YOUR_BUCKET`):
|
|
234
|
+
|
|
235
|
+
```json
|
|
236
|
+
{
|
|
237
|
+
"Version": "2012-10-17",
|
|
238
|
+
"Statement": [
|
|
239
|
+
{
|
|
240
|
+
"Sid": "PublicReadScreenshots",
|
|
241
|
+
"Effect": "Allow",
|
|
242
|
+
"Principal": "*",
|
|
243
|
+
"Action": "s3:GetObject",
|
|
244
|
+
"Resource": "arn:aws:s3:::YOUR_BUCKET/ufp/*/screenshots/*"
|
|
245
|
+
}
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
This makes only screenshots publicly readable so they can display in `<img>` tags. Comments and reply JSON files remain private.
|
|
251
|
+
|
|
252
|
+
#### Step 4 — Create an IAM policy
|
|
253
|
+
|
|
254
|
+
Go to **IAM** → **Policies** → **Create policy** → JSON tab, and paste (replace `YOUR_BUCKET`):
|
|
255
|
+
|
|
256
|
+
```json
|
|
257
|
+
{
|
|
258
|
+
"Version": "2012-10-17",
|
|
259
|
+
"Statement": [
|
|
260
|
+
{
|
|
261
|
+
"Sid": "WriteJsonFiles",
|
|
262
|
+
"Effect": "Allow",
|
|
263
|
+
"Action": "s3:PutObject",
|
|
264
|
+
"Resource": "arn:aws:s3:::YOUR_BUCKET/ufp/*",
|
|
265
|
+
"Condition": {
|
|
266
|
+
"StringLike": {
|
|
267
|
+
"s3:RequestObjectKey": [
|
|
268
|
+
"ufp/*/comments/*.json",
|
|
269
|
+
"ufp/replies/*/*.json",
|
|
270
|
+
"ufp/_index/*.json"
|
|
271
|
+
]
|
|
272
|
+
},
|
|
273
|
+
"NumericLessThanEquals": {
|
|
274
|
+
"s3:content-length": "51200"
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"Sid": "WriteScreenshots",
|
|
280
|
+
"Effect": "Allow",
|
|
281
|
+
"Action": "s3:PutObject",
|
|
282
|
+
"Resource": "arn:aws:s3:::YOUR_BUCKET/ufp/*",
|
|
283
|
+
"Condition": {
|
|
284
|
+
"StringLike": {
|
|
285
|
+
"s3:RequestObjectKey": "ufp/*/screenshots/*.png"
|
|
286
|
+
},
|
|
287
|
+
"NumericLessThanEquals": {
|
|
288
|
+
"s3:content-length": "3145728"
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"Sid": "ReadFiles",
|
|
294
|
+
"Effect": "Allow",
|
|
295
|
+
"Action": "s3:GetObject",
|
|
296
|
+
"Resource": "arn:aws:s3:::YOUR_BUCKET/ufp/*"
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
"Sid": "ListFiles",
|
|
300
|
+
"Effect": "Allow",
|
|
301
|
+
"Action": "s3:ListBucket",
|
|
302
|
+
"Resource": "arn:aws:s3:::YOUR_BUCKET",
|
|
303
|
+
"Condition": {
|
|
304
|
+
"StringLike": {
|
|
305
|
+
"s3:prefix": "ufp/*"
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
]
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
What this enforces:
|
|
314
|
+
- Only `.json` files under the correct paths can be written (no arbitrary uploads)
|
|
315
|
+
- JSON files are capped at **50 KB** per file
|
|
316
|
+
- Screenshot `.png` files are capped at **3 MB** per file
|
|
317
|
+
- All access is scoped to the `ufp/` prefix
|
|
318
|
+
- **`s3:DeleteObject` is absent** — the browser can never delete files (soft delete is used instead)
|
|
319
|
+
|
|
320
|
+
Name the policy (e.g. `ufp-plugin-policy`) and save it.
|
|
321
|
+
|
|
322
|
+
#### Step 5 — Create an IAM role
|
|
323
|
+
|
|
324
|
+
1. Go to **IAM** → **Roles** → **Create role**.
|
|
325
|
+
2. Trusted entity type: **Web identity**.
|
|
326
|
+
3. Identity provider: **Amazon Cognito**.
|
|
327
|
+
4. Leave the audience field blank for now (Cognito will configure it).
|
|
328
|
+
5. Click **Next**, attach the policy you created in Step 4.
|
|
329
|
+
6. Name the role (e.g. `ufp-plugin-cognito-unauth`) and create it.
|
|
330
|
+
|
|
331
|
+
#### Step 6 — Create a Cognito Identity Pool
|
|
332
|
+
|
|
333
|
+
1. Go to **Cognito** → **Identity pools** → **Create identity pool**.
|
|
334
|
+
2. **User access**: check **Guest access** (unauthenticated identities).
|
|
335
|
+
3. **Guest role**: select **Use an existing IAM role** → choose the role from Step 5.
|
|
336
|
+
4. Give the pool a name (e.g. `ufp-plugin-pool`) and create it.
|
|
337
|
+
5. On the pool detail page, copy the **Identity pool ID** — it looks like `us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`.
|
|
338
|
+
|
|
339
|
+
#### Step 7 — Configure the plugin
|
|
340
|
+
|
|
341
|
+
```js
|
|
342
|
+
initFeedbackPlugin({
|
|
343
|
+
backend: {
|
|
344
|
+
provider: 'aws',
|
|
345
|
+
region: 'us-east-1', // same region as your bucket and Identity Pool
|
|
346
|
+
bucket: 'your-bucket-name',
|
|
347
|
+
identityPoolId: 'us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
|
|
124
348
|
},
|
|
125
349
|
projectKey: 'my-project',
|
|
126
350
|
})
|
|
127
351
|
```
|
|
128
352
|
|
|
129
|
-
|
|
353
|
+
### How data is stored
|
|
354
|
+
|
|
355
|
+
```
|
|
356
|
+
your-bucket/
|
|
357
|
+
ufp/
|
|
358
|
+
{projectKey}/
|
|
359
|
+
{sha256(pageUrl)[0..15]}/
|
|
360
|
+
comments/
|
|
361
|
+
{commentId}.json ← comment data
|
|
362
|
+
screenshots/
|
|
363
|
+
{commentId}.png ← publicly readable
|
|
364
|
+
replies/
|
|
365
|
+
{commentId}/
|
|
366
|
+
{replyId}.json ← reply data
|
|
367
|
+
_index/
|
|
368
|
+
{commentId}.json ← { "p": projectKey, "h": pageHash }
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Limitations
|
|
372
|
+
|
|
373
|
+
| Limitation | Detail |
|
|
374
|
+
|---|---|
|
|
375
|
+
| No real-time | New comments from other users appear after the next poll (default 5 s) |
|
|
376
|
+
| Soft delete only | Deleted comments are hidden but remain in S3 (good for audit trail) |
|
|
377
|
+
| N+1 reads | Loading comments = 1 LIST + N GET requests (fine for typical 10–30 comments) |
|
|
378
|
+
| CORS required | Your bucket must whitelist the domains the plugin runs on |
|
|
130
379
|
|
|
131
380
|
---
|
|
132
381
|
|
|
133
|
-
## Configuration
|
|
382
|
+
## Configuration reference
|
|
134
383
|
|
|
135
384
|
```ts
|
|
136
385
|
initFeedbackPlugin({
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
theme?: { primaryColor?: string } // default: '#6366f1'
|
|
386
|
+
backend: FirebaseProviderConfig | SupabaseProviderConfig | AWSProviderConfig // required
|
|
387
|
+
projectKey?: string // namespace comments per project (default: 'default')
|
|
388
|
+
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
|
|
389
|
+
theme?: { primaryColor?: string } // default: '#6366f1'
|
|
142
390
|
})
|
|
143
391
|
```
|
|
144
392
|
|
|
145
393
|
---
|
|
146
394
|
|
|
147
|
-
##
|
|
395
|
+
## Plain HTML / CDN (UMD)
|
|
396
|
+
|
|
397
|
+
No build step needed — load directly from a CDN:
|
|
148
398
|
|
|
149
399
|
```html
|
|
150
400
|
<script src="https://unpkg.com/@mindbowser_inc/ui-feedback-plugin/dist/ui-feedback-plugin.umd.js"></script>
|
|
151
401
|
<script>
|
|
152
402
|
UIFeedbackPlugin.initFeedbackPlugin({
|
|
153
|
-
|
|
403
|
+
backend: {
|
|
404
|
+
provider: 'supabase',
|
|
405
|
+
url: 'https://xxxx.supabase.co',
|
|
406
|
+
anonKey: 'eyJhbGci...',
|
|
407
|
+
},
|
|
154
408
|
projectKey: 'my-project',
|
|
155
409
|
})
|
|
156
410
|
</script>
|
|
157
411
|
```
|
|
158
412
|
|
|
413
|
+
Replace the `backend` block with any of the three provider configs shown above.
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## Local development
|
|
418
|
+
|
|
419
|
+
```bash
|
|
420
|
+
git clone https://github.com/vishmindbowser/ui-feedback-plugin.git
|
|
421
|
+
cd ui-feedback-plugin
|
|
422
|
+
npm install
|
|
423
|
+
cp .env.local.example .env.local # fill in your credentials
|
|
424
|
+
npm run dev
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
Set `VITE_PROVIDER` in `.env.local` to `supabase`, `firebase`, or `aws` to switch backends.
|
|
428
|
+
|
|
159
429
|
---
|
|
160
430
|
|
|
161
431
|
## License
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
var h = Object.defineProperty;
|
|
2
|
+
var u = (c, t, e) => t in c ? h(c, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : c[t] = e;
|
|
3
|
+
var l = (c, t, e) => u(c, typeof t != "symbol" ? t + "" : t, e);
|
|
4
|
+
class g {
|
|
5
|
+
constructor(t) {
|
|
6
|
+
l(this, "cfg");
|
|
7
|
+
l(this, "_s3", null);
|
|
8
|
+
this.cfg = t;
|
|
9
|
+
}
|
|
10
|
+
async s3() {
|
|
11
|
+
if (this._s3) return this._s3;
|
|
12
|
+
const [{ S3Client: t }, { fromCognitoIdentityPool: e }] = await Promise.all([
|
|
13
|
+
import("./index-B-1NC_TX.mjs").then((s) => s.j),
|
|
14
|
+
import("./index-ClYqXfVs.mjs")
|
|
15
|
+
]);
|
|
16
|
+
return this._s3 = new t({
|
|
17
|
+
region: this.cfg.region,
|
|
18
|
+
credentials: e({
|
|
19
|
+
identityPoolId: this.cfg.identityPoolId,
|
|
20
|
+
clientConfig: { region: this.cfg.region }
|
|
21
|
+
})
|
|
22
|
+
}), this._s3;
|
|
23
|
+
}
|
|
24
|
+
async pageHash(t) {
|
|
25
|
+
const e = new TextEncoder().encode(t), s = await crypto.subtle.digest("SHA-256", e);
|
|
26
|
+
return Array.from(new Uint8Array(s)).map((n) => n.toString(16).padStart(2, "0")).join("").slice(0, 16);
|
|
27
|
+
}
|
|
28
|
+
async put(t, e, s) {
|
|
29
|
+
const { PutObjectCommand: n } = await import("./index-B-1NC_TX.mjs").then((i) => i.j);
|
|
30
|
+
await (await this.s3()).send(new n({
|
|
31
|
+
Bucket: this.cfg.bucket,
|
|
32
|
+
Key: t,
|
|
33
|
+
Body: e,
|
|
34
|
+
ContentType: s
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
async getJson(t) {
|
|
38
|
+
const { GetObjectCommand: e } = await import("./index-B-1NC_TX.mjs").then((i) => i.j), o = await (await (await this.s3()).send(new e({ Bucket: this.cfg.bucket, Key: t }))).Body.transformToString();
|
|
39
|
+
return JSON.parse(o);
|
|
40
|
+
}
|
|
41
|
+
async listKeys(t) {
|
|
42
|
+
const { ListObjectsV2Command: e } = await import("./index-B-1NC_TX.mjs").then((i) => i.j), s = await this.s3(), n = [];
|
|
43
|
+
let o;
|
|
44
|
+
do {
|
|
45
|
+
const i = await s.send(new e({
|
|
46
|
+
Bucket: this.cfg.bucket,
|
|
47
|
+
Prefix: t,
|
|
48
|
+
ContinuationToken: o
|
|
49
|
+
}));
|
|
50
|
+
for (const a of i.Contents ?? [])
|
|
51
|
+
a.Key && n.push(a.Key);
|
|
52
|
+
o = i.NextContinuationToken;
|
|
53
|
+
} while (o);
|
|
54
|
+
return n;
|
|
55
|
+
}
|
|
56
|
+
indexKey(t) {
|
|
57
|
+
return `ufp/_index/${t}.json`;
|
|
58
|
+
}
|
|
59
|
+
async lookupComment(t) {
|
|
60
|
+
const { p: e, h: s } = await this.getJson(this.indexKey(t));
|
|
61
|
+
return { key: `ufp/${e}/${s}/comments/${t}.json` };
|
|
62
|
+
}
|
|
63
|
+
async addComment(t) {
|
|
64
|
+
const e = await this.pageHash(t.pageUrl), s = crypto.randomUUID();
|
|
65
|
+
return await Promise.all([
|
|
66
|
+
this.put(
|
|
67
|
+
`ufp/${t.projectKey}/${e}/comments/${s}.json`,
|
|
68
|
+
JSON.stringify({ id: s, ...t }),
|
|
69
|
+
"application/json"
|
|
70
|
+
),
|
|
71
|
+
this.put(
|
|
72
|
+
this.indexKey(s),
|
|
73
|
+
JSON.stringify({ p: t.projectKey, h: e }),
|
|
74
|
+
"application/json"
|
|
75
|
+
)
|
|
76
|
+
]), s;
|
|
77
|
+
}
|
|
78
|
+
async getComments(t, e) {
|
|
79
|
+
const s = await this.pageHash(t), n = await this.listKeys(`ufp/${e}/${s}/comments/`);
|
|
80
|
+
return (await Promise.all(
|
|
81
|
+
n.map(
|
|
82
|
+
(i) => this.getJson(i).catch(() => null)
|
|
83
|
+
)
|
|
84
|
+
)).filter((i) => i !== null && !i.deleted).sort((i, a) => a.createdAt - i.createdAt);
|
|
85
|
+
}
|
|
86
|
+
async updateComment(t, e) {
|
|
87
|
+
const { key: s } = await this.lookupComment(t), n = await this.getJson(s);
|
|
88
|
+
await this.put(s, JSON.stringify({ ...n, ...e }), "application/json");
|
|
89
|
+
}
|
|
90
|
+
async deleteComment(t) {
|
|
91
|
+
const { key: e } = await this.lookupComment(t), s = await this.getJson(e);
|
|
92
|
+
await this.put(e, JSON.stringify({ ...s, deleted: !0 }), "application/json");
|
|
93
|
+
}
|
|
94
|
+
async addReply(t) {
|
|
95
|
+
const e = crypto.randomUUID();
|
|
96
|
+
return await this.put(
|
|
97
|
+
`ufp/replies/${t.commentId}/${e}.json`,
|
|
98
|
+
JSON.stringify({ id: e, ...t }),
|
|
99
|
+
"application/json"
|
|
100
|
+
), e;
|
|
101
|
+
}
|
|
102
|
+
async getReplies(t) {
|
|
103
|
+
const e = await this.listKeys(`ufp/replies/${t}/`);
|
|
104
|
+
return (await Promise.all(
|
|
105
|
+
e.map((n) => this.getJson(n).catch(() => null))
|
|
106
|
+
)).filter((n) => n !== null).sort((n, o) => n.createdAt - o.createdAt);
|
|
107
|
+
}
|
|
108
|
+
subscribeToComments(t, e, s) {
|
|
109
|
+
let n = !0;
|
|
110
|
+
const o = this.cfg.pollInterval ?? 5e3, i = async () => {
|
|
111
|
+
if (n) {
|
|
112
|
+
try {
|
|
113
|
+
const a = await this.getComments(t, e);
|
|
114
|
+
n && s(a);
|
|
115
|
+
} catch (a) {
|
|
116
|
+
console.error("[ui-feedback-plugin] AWS poll error:", a);
|
|
117
|
+
}
|
|
118
|
+
n && setTimeout(i, o);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
return i(), () => {
|
|
122
|
+
n = !1;
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async uploadScreenshot(t, e) {
|
|
126
|
+
const { p: s, h: n } = await this.getJson(this.indexKey(t)), o = `ufp/${s}/${n}/screenshots/${t}.png`, i = e.split(",")[1], a = atob(i), p = new Uint8Array(a.length);
|
|
127
|
+
for (let r = 0; r < a.length; r++) p[r] = a.charCodeAt(r);
|
|
128
|
+
return await this.put(o, p, "image/png"), `https://${this.cfg.bucket}.s3.${this.cfg.region}.amazonaws.com/${o}`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export {
|
|
132
|
+
g as AWSAdapter
|
|
133
|
+
};
|
|
134
|
+
//# sourceMappingURL=aws-CkWYvUhs.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aws-CkWYvUhs.mjs","sources":["../src/adapters/aws.ts"],"sourcesContent":["// AWS Cognito Identity Pool adapter — browser talks directly to S3, no backend required.\n// Requires @aws-sdk/client-s3 and @aws-sdk/credential-providers (both in devDependencies).\n//\n// ── AWS SETUP ────────────────────────────────────────────────────────────────────\n//\n// 1. CREATE S3 BUCKET\n// - Keep \"Block all public access\" ON (we'll add a narrow public policy for screenshots).\n// - Enable versioning (optional, helpful for recovery).\n//\n// 2. CONFIGURE CORS (required for browser access)\n// Go to Bucket → Permissions → CORS and paste:\n// [\n// {\n// \"AllowedOrigins\": [\"https://your-domain.com\"],\n// \"AllowedMethods\": [\"GET\", \"PUT\", \"HEAD\"],\n// \"AllowedHeaders\": [\"*\"],\n// \"ExposeHeaders\": [\"ETag\"],\n// \"MaxAgeSeconds\": 3000\n// }\n// ]\n// Use \"*\" for AllowedOrigins during local dev, restrict to real domains in production.\n//\n// 3. ADD BUCKET POLICY (public read for screenshots only)\n// Go to Bucket → Permissions → Bucket policy and paste (replace YOUR_BUCKET):\n// {\n// \"Version\": \"2012-10-17\",\n// \"Statement\": [{\n// \"Sid\": \"PublicReadScreenshots\",\n// \"Effect\": \"Allow\",\n// \"Principal\": \"*\",\n// \"Action\": \"s3:GetObject\",\n// \"Resource\": \"arn:aws:s3:::YOUR_BUCKET/ufp/*/screenshots/*\"\n// }]\n// }\n// Comments and replies remain private (only accessible via Cognito credentials).\n//\n// 4. CREATE IAM POLICY (least privilege — see README for full JSON)\n// Allows:\n// - s3:PutObject on ufp/*/comments/*.json (max 50 KB)\n// - s3:PutObject on ufp/replies/*/*.json (max 50 KB)\n// - s3:PutObject on ufp/_index/*.json (max 50 KB)\n// - s3:PutObject on ufp/*/screenshots/*.png (max 3 MB)\n// - s3:GetObject on ufp/*\n// - s3:ListBucket on the bucket with prefix ufp/*\n// NO s3:DeleteObject — soft delete is used instead.\n//\n// 5. CREATE IAM ROLE\n// - Trusted entity: Web Identity\n// - Identity provider: Amazon Cognito\n// - Attach the policy from step 4\n//\n// 6. CREATE COGNITO IDENTITY POOL\n// - Enable unauthenticated identities\n// - Unauthenticated role: the IAM role from step 5\n// - Authenticated role: none (or a different role)\n// - Copy the Identity Pool ID (format: us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)\n//\n// 7. CONFIGURE THE PLUGIN\n// initFeedbackPlugin({\n// backend: {\n// provider: 'aws',\n// region: 'us-east-1',\n// bucket: 'your-bucket-name',\n// identityPoolId: 'us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',\n// },\n// projectKey: 'my-project',\n// })\n//\n// ── DATA LAYOUT IN S3 ────────────────────────────────────────────────────────────\n//\n// ufp/{projectKey}/{pageHash}/comments/{commentId}.json\n// ufp/{projectKey}/{pageHash}/screenshots/{commentId}.png ← publicly readable\n// ufp/replies/{commentId}/{replyId}.json\n// ufp/_index/{commentId}.json ← { \"p\": projectKey, \"h\": pageHash }\n//\n// ── SOFT DELETE ──────────────────────────────────────────────────────────────────\n// deleteComment overwrites the comment JSON with { ...comment, deleted: true }.\n// The file stays in S3 but is filtered out on read. No s3:DeleteObject needed.\n//\n// ── REAL-TIME ────────────────────────────────────────────────────────────────────\n// S3 has no push. subscribeToComments polls on a configurable interval (default 5s).\n\nimport type { FeedbackComment, Reply, DatabaseAdapter, ScreenshotAdapter, AWSProviderConfig } from '../core/types'\nimport type { S3Client as S3ClientType } from '@aws-sdk/client-s3'\n\nexport class AWSAdapter implements DatabaseAdapter, ScreenshotAdapter {\n private cfg: AWSProviderConfig\n private _s3: S3ClientType | null = null\n\n constructor(config: AWSProviderConfig) {\n this.cfg = config\n }\n\n private async s3(): Promise<S3ClientType> {\n if (this._s3) return this._s3\n const [{ S3Client }, { fromCognitoIdentityPool }] = await Promise.all([\n import('@aws-sdk/client-s3'),\n import('@aws-sdk/credential-provider-cognito-identity'),\n ])\n this._s3 = new S3Client({\n region: this.cfg.region,\n credentials: fromCognitoIdentityPool({\n identityPoolId: this.cfg.identityPoolId,\n clientConfig: { region: this.cfg.region },\n }),\n })\n return this._s3\n }\n\n private async pageHash(url: string): Promise<string> {\n const data = new TextEncoder().encode(url)\n const buf = await crypto.subtle.digest('SHA-256', data)\n return Array.from(new Uint8Array(buf))\n .map(b => b.toString(16).padStart(2, '0'))\n .join('')\n .slice(0, 16)\n }\n\n private async put(key: string, body: string | Uint8Array, contentType: string): Promise<void> {\n const { PutObjectCommand } = await import('@aws-sdk/client-s3')\n const client = await this.s3()\n await client.send(new PutObjectCommand({\n Bucket: this.cfg.bucket,\n Key: key,\n Body: body,\n ContentType: contentType,\n }))\n }\n\n private async getJson<T>(key: string): Promise<T> {\n const { GetObjectCommand } = await import('@aws-sdk/client-s3')\n const client = await this.s3()\n const res = await client.send(new GetObjectCommand({ Bucket: this.cfg.bucket, Key: key }))\n const text = await res.Body!.transformToString()\n return JSON.parse(text) as T\n }\n\n private async listKeys(prefix: string): Promise<string[]> {\n const { ListObjectsV2Command } = await import('@aws-sdk/client-s3')\n const client = await this.s3()\n const keys: string[] = []\n let token: string | undefined\n do {\n const res = await client.send(new ListObjectsV2Command({\n Bucket: this.cfg.bucket,\n Prefix: prefix,\n ContinuationToken: token,\n }))\n for (const obj of res.Contents ?? []) {\n if (obj.Key) keys.push(obj.Key)\n }\n token = res.NextContinuationToken\n } while (token)\n return keys\n }\n\n private indexKey(id: string) {\n return `ufp/_index/${id}.json`\n }\n\n private async lookupComment(id: string): Promise<{ key: string }> {\n const { p, h } = await this.getJson<{ p: string; h: string }>(this.indexKey(id))\n return { key: `ufp/${p}/${h}/comments/${id}.json` }\n }\n\n async addComment(comment: Omit<FeedbackComment, 'id'>): Promise<string> {\n const hash = await this.pageHash(comment.pageUrl)\n const id = crypto.randomUUID()\n await Promise.all([\n this.put(\n `ufp/${comment.projectKey}/${hash}/comments/${id}.json`,\n JSON.stringify({ id, ...comment }),\n 'application/json'\n ),\n this.put(\n this.indexKey(id),\n JSON.stringify({ p: comment.projectKey, h: hash }),\n 'application/json'\n ),\n ])\n return id\n }\n\n async getComments(pageUrl: string, projectKey: string): Promise<FeedbackComment[]> {\n const hash = await this.pageHash(pageUrl)\n const keys = await this.listKeys(`ufp/${projectKey}/${hash}/comments/`)\n const rows = await Promise.all(\n keys.map(k =>\n this.getJson<FeedbackComment & { deleted?: boolean }>(k).catch(() => null)\n )\n )\n return rows\n .filter((r): r is FeedbackComment => r !== null && !r.deleted)\n .sort((a, b) => b.createdAt - a.createdAt)\n }\n\n async updateComment(id: string, data: Partial<FeedbackComment>): Promise<void> {\n const { key } = await this.lookupComment(id)\n const existing = await this.getJson<FeedbackComment>(key)\n await this.put(key, JSON.stringify({ ...existing, ...data }), 'application/json')\n }\n\n async deleteComment(id: string): Promise<void> {\n const { key } = await this.lookupComment(id)\n const existing = await this.getJson<FeedbackComment>(key)\n // Soft delete — overwrite with deleted flag. No s3:DeleteObject permission needed.\n await this.put(key, JSON.stringify({ ...existing, deleted: true }), 'application/json')\n }\n\n async addReply(reply: Omit<Reply, 'id'>): Promise<string> {\n const id = crypto.randomUUID()\n await this.put(\n `ufp/replies/${reply.commentId}/${id}.json`,\n JSON.stringify({ id, ...reply }),\n 'application/json'\n )\n return id\n }\n\n async getReplies(commentId: string): Promise<Reply[]> {\n const keys = await this.listKeys(`ufp/replies/${commentId}/`)\n const rows = await Promise.all(\n keys.map(k => this.getJson<Reply>(k).catch(() => null))\n )\n return rows\n .filter((r): r is Reply => r !== null)\n .sort((a, b) => a.createdAt - b.createdAt)\n }\n\n subscribeToComments(\n pageUrl: string,\n projectKey: string,\n callback: (comments: FeedbackComment[]) => void\n ): () => void {\n let active = true\n const interval = this.cfg.pollInterval ?? 5000\n\n const poll = async () => {\n if (!active) return\n try {\n const comments = await this.getComments(pageUrl, projectKey)\n if (active) callback(comments)\n } catch (err) {\n console.error('[ui-feedback-plugin] AWS poll error:', err)\n }\n if (active) setTimeout(poll, interval)\n }\n\n poll()\n return () => { active = false }\n }\n\n async uploadScreenshot(commentId: string, dataUrl: string): Promise<string> {\n const { p, h } = await this.getJson<{ p: string; h: string }>(this.indexKey(commentId))\n const key = `ufp/${p}/${h}/screenshots/${commentId}.png`\n\n const base64 = dataUrl.split(',')[1]\n const binary = atob(base64)\n const bytes = new Uint8Array(binary.length)\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)\n\n await this.put(key, bytes, 'image/png')\n return `https://${this.cfg.bucket}.s3.${this.cfg.region}.amazonaws.com/${key}`\n }\n}\n"],"names":["AWSAdapter","config","__publicField","S3Client","fromCognitoIdentityPool","n","url","data","buf","b","key","body","contentType","PutObjectCommand","GetObjectCommand","text","prefix","ListObjectsV2Command","client","keys","token","res","obj","id","p","h","comment","hash","pageUrl","projectKey","k","r","a","existing","reply","commentId","callback","active","interval","poll","comments","err","dataUrl","base64","binary","bytes","i"],"mappings":"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"}
|