@mindbowser_inc/ui-feedback-plugin 0.2.0 → 0.3.1

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.
Files changed (32) hide show
  1. package/README.md +310 -61
  2. package/dist/aws-CkWYvUhs.mjs +134 -0
  3. package/dist/aws-CkWYvUhs.mjs.map +1 -0
  4. package/dist/html2canvas-pro.esm-QETbiPYt.mjs +7587 -0
  5. package/dist/html2canvas-pro.esm-QETbiPYt.mjs.map +1 -0
  6. package/dist/index-B-1NC_TX.mjs +12047 -0
  7. package/dist/index-B-1NC_TX.mjs.map +1 -0
  8. package/dist/index-ClYqXfVs.mjs +183 -0
  9. package/dist/index-ClYqXfVs.mjs.map +1 -0
  10. package/dist/{index-BcqBbswO.mjs → index-D6dCjvzy.mjs} +147 -181
  11. package/dist/index-D6dCjvzy.mjs.map +1 -0
  12. package/dist/index.browser-CKeO7JYa.mjs +190 -0
  13. package/dist/index.browser-CKeO7JYa.mjs.map +1 -0
  14. package/dist/index.d.ts +27 -23
  15. package/dist/loadCognitoIdentity-V6H3EoBY.mjs +957 -0
  16. package/dist/loadCognitoIdentity-V6H3EoBY.mjs.map +1 -0
  17. package/dist/stream-collector-mh16BUWl.mjs +4032 -0
  18. package/dist/stream-collector-mh16BUWl.mjs.map +1 -0
  19. package/dist/{supabase-BUHP2OkK.mjs → supabase-CbB80UVb.mjs} +2 -2
  20. package/dist/{supabase-BUHP2OkK.mjs.map → supabase-CbB80UVb.mjs.map} +1 -1
  21. package/dist/tslib.es6-BNkLP3y-.mjs +112 -0
  22. package/dist/tslib.es6-BNkLP3y-.mjs.map +1 -0
  23. package/dist/ui-feedback-plugin.es.js +4 -4
  24. package/dist/ui-feedback-plugin.es.js.map +1 -1
  25. package/dist/ui-feedback-plugin.umd.js +253 -243
  26. package/dist/ui-feedback-plugin.umd.js.map +1 -1
  27. package/package.json +9 -5
  28. package/dist/html2canvas.esm-d2sM-0Wm.mjs +0 -4871
  29. package/dist/html2canvas.esm-d2sM-0Wm.mjs.map +0 -1
  30. package/dist/index-BcqBbswO.mjs.map +0 -1
  31. package/dist/s3-4V57aJiZ.mjs +0 -66
  32. package/dist/s3-4V57aJiZ.mjs.map +0 -1
package/README.md CHANGED
@@ -8,7 +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
- - Three fully independent backend options
11
+ - Three fully independent backend options — no lock-in
12
12
 
13
13
  ---
14
14
 
@@ -22,44 +22,84 @@ npm install @mindbowser_inc/ui-feedback-plugin
22
22
 
23
23
  ## Backends
24
24
 
25
- Choose **one** backend. They are fully independent — no mixing required.
25
+ Choose **one** backend. They are fully independent — switch any time by changing the `backend` config.
26
26
 
27
- | Backend | Database | Screenshot storage | Real-time |
27
+ | Backend | Database | Screenshots | Real-time |
28
28
  |---|---|---|---|
29
29
  | **Firebase** | Firestore | Firebase Storage | Yes (onSnapshot) |
30
30
  | **Supabase** | Postgres | Supabase Storage | Yes (Realtime) |
31
- | **AWS S3** | JSON files in S3 | S3 | Polling (5s default) |
31
+ | **AWS** | JSON files in S3 | S3 | Polling (5 s default) |
32
32
 
33
33
  ---
34
34
 
35
35
  ## Firebase
36
36
 
37
- Firestore stores comments and replies. Firebase Storage holds screenshots.
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
38
40
 
39
41
  ```js
40
42
  import { initFeedbackPlugin } from '@mindbowser_inc/ui-feedback-plugin'
41
43
 
42
44
  initFeedbackPlugin({
43
45
  backend: {
44
- provider: 'firebase',
45
- apiKey: process.env.FIREBASE_API_KEY,
46
- authDomain: process.env.FIREBASE_AUTH_DOMAIN,
47
- projectId: process.env.FIREBASE_PROJECT_ID,
48
- storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
49
- messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
50
- appId: process.env.FIREBASE_APP_ID,
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',
51
53
  },
52
54
  projectKey: 'my-project',
53
55
  })
54
56
  ```
55
57
 
56
- > The Firebase API key is a **public identifier**, not a secret. Secure your data with [Firebase Security Rules](https://firebase.google.com/docs/rules).
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
+ ```
57
95
 
58
96
  ---
59
97
 
60
98
  ## Supabase
61
99
 
62
- Postgres stores comments and replies. Supabase Storage holds screenshots.
100
+ Postgres stores comments and replies. Supabase Storage holds screenshots. No backend required — the `anonKey` is a public key secured by Row Level Security.
101
+
102
+ ### Quick start
63
103
 
64
104
  ```js
65
105
  import { initFeedbackPlugin } from '@mindbowser_inc/ui-feedback-plugin'
@@ -67,8 +107,8 @@ import { initFeedbackPlugin } from '@mindbowser_inc/ui-feedback-plugin'
67
107
  initFeedbackPlugin({
68
108
  backend: {
69
109
  provider: 'supabase',
70
- url: process.env.SUPABASE_URL,
71
- anonKey: process.env.SUPABASE_ANON_KEY,
110
+ url: 'https://xxxx.supabase.co',
111
+ anonKey: 'eyJhbGci...',
72
112
  },
73
113
  projectKey: 'my-project',
74
114
  })
@@ -76,88 +116,275 @@ initFeedbackPlugin({
76
116
 
77
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).
78
118
 
79
- **Required SQL** — run this in your Supabase SQL editor once:
119
+ ### Supabase setup
120
+
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**.
80
126
 
127
+ **Database SQL** (run once in SQL Editor):
81
128
  ```sql
82
129
  create table if not exists ufp_comments (
83
- id uuid primary key default gen_random_uuid(),
84
- page_url text not null,
85
- project_key text not null default 'default',
86
- author_name text not null,
87
- text text not null,
88
- screenshot_url text default '',
89
- annotation jsonb not null default '{}',
90
- resolved boolean default false,
91
- created_at bigint not null
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
92
139
  );
140
+
93
141
  create table if not exists ufp_replies (
94
- id uuid primary key default gen_random_uuid(),
95
- comment_id uuid not null references ufp_comments(id) on delete cascade,
142
+ id uuid primary key default gen_random_uuid(),
143
+ comment_id uuid not null references ufp_comments(id) on delete cascade,
96
144
  author_name text not null,
97
- text text not null,
98
- created_at bigint not null
145
+ text text not null,
146
+ created_at bigint not null
99
147
  );
148
+
149
+ -- Enable Row Level Security
100
150
  alter table ufp_comments enable row level security;
101
- alter table ufp_replies enable row level security;
151
+ alter table ufp_replies enable row level security;
152
+
153
+ -- Allow all operations (restrict per your auth strategy in production)
102
154
  create policy "ufp_comments_all" on ufp_comments for all using (true) with check (true);
103
- create policy "ufp_replies_all" on ufp_replies for all using (true) with check (true);
155
+ create policy "ufp_replies_all" on ufp_replies for all using (true) with check (true);
156
+
157
+ -- Grant table access to the anon role (RLS policies alone are not enough)
104
158
  grant select, insert, update, delete on ufp_comments to anon;
105
- grant select, insert, update, delete on ufp_replies to anon;
159
+ grant select, insert, update, delete on ufp_replies to anon;
160
+
161
+ -- Enable real-time for comments
106
162
  alter publication supabase_realtime add table ufp_comments;
107
163
  ```
108
164
 
109
- Then in **Storage**, create a public bucket named `ufp-screenshots` and add:
110
-
165
+ **Storage policy SQL** (run after creating the `ufp-screenshots` bucket):
111
166
  ```sql
112
- create policy "ufp_storage_insert" on storage.objects for insert to anon with check (bucket_id = 'ufp-screenshots');
113
- create policy "ufp_storage_select" on storage.objects for select to anon using (bucket_id = 'ufp-screenshots');
114
- create policy "ufp_storage_update" on storage.objects for update to anon using (bucket_id = 'ufp-screenshots');
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');
115
175
  ```
116
176
 
117
177
  ---
118
178
 
119
- ## AWS S3
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.
120
182
 
121
- Everything — comments, replies, and screenshots — lives in S3 as JSON/image files. No Firebase or Supabase needed.
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).
122
184
 
123
- **AWS credentials must never appear in frontend code.** This adapter calls your own backend API, which holds the credentials and talks to S3.
185
+ ### Quick start
124
186
 
125
187
  ```js
126
188
  import { initFeedbackPlugin } from '@mindbowser_inc/ui-feedback-plugin'
127
189
 
128
190
  initFeedbackPlugin({
129
191
  backend: {
130
- provider: 's3',
131
- apiUrl: 'https://your-api.com/ufp', // your backend base URL
132
- headers: { Authorization: 'Bearer your-internal-token' },
133
- pollInterval: 5000, // ms between comment refreshes (default: 5000)
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
134
197
  },
135
198
  projectKey: 'my-project',
136
199
  })
137
200
  ```
138
201
 
139
- **Your backend must expose these endpoints:**
202
+ ### AWS setup (step by step)
140
203
 
141
- | Method | Path | Body | Returns |
142
- |---|---|---|---|
143
- | GET | `/comments?pageUrl=...&projectKey=...` | — | `FeedbackComment[]` |
144
- | POST | `/comments` | `FeedbackComment` (no id) | `{ id: string }` |
145
- | PUT | `/comments/:id` | `Partial<FeedbackComment>` | `200` |
146
- | DELETE | `/comments/:id` | — | `200` |
147
- | GET | `/replies?commentId=...` | — | `Reply[]` |
148
- | POST | `/replies` | `Reply` (no id) | `{ id: string }` |
149
- | POST | `/screenshots` | `{ commentId, imageData }` (base64) | `{ url: string }` |
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.
150
212
 
151
- A full Node.js/Express reference server is included as comments inside [`src/adapters/s3.ts`](src/adapters/s3.ts).
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',
348
+ },
349
+ projectKey: 'my-project',
350
+ })
351
+ ```
352
+
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 |
152
379
 
153
380
  ---
154
381
 
155
- ## Configuration
382
+ ## Configuration reference
156
383
 
157
384
  ```ts
158
385
  initFeedbackPlugin({
159
- backend: FirebaseProviderConfig | SupabaseProviderConfig | S3ProviderConfig // required
160
- projectKey?: string // namespace comments per project (default: 'default')
386
+ backend: FirebaseProviderConfig | SupabaseProviderConfig | AWSProviderConfig // required
387
+ projectKey?: string // namespace comments per project (default: 'default')
161
388
  position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
162
389
  theme?: { primaryColor?: string } // default: '#6366f1'
163
390
  })
@@ -165,18 +392,40 @@ initFeedbackPlugin({
165
392
 
166
393
  ---
167
394
 
168
- ## Plain HTML (UMD / CDN)
395
+ ## Plain HTML / CDN (UMD)
396
+
397
+ No build step needed — load directly from a CDN:
169
398
 
170
399
  ```html
171
400
  <script src="https://unpkg.com/@mindbowser_inc/ui-feedback-plugin/dist/ui-feedback-plugin.umd.js"></script>
172
401
  <script>
173
402
  UIFeedbackPlugin.initFeedbackPlugin({
174
- backend: { provider: 'supabase', url: '...', anonKey: '...' },
403
+ backend: {
404
+ provider: 'supabase',
405
+ url: 'https://xxxx.supabase.co',
406
+ anonKey: 'eyJhbGci...',
407
+ },
175
408
  projectKey: 'my-project',
176
409
  })
177
410
  </script>
178
411
  ```
179
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
+
180
429
  ---
181
430
 
182
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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"}