annotate-kit 0.1.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/LICENSE +21 -0
- package/README.md +307 -0
- package/backend/firebase/firestore.indexes.json +22 -0
- package/backend/firebase/firestore.rules +84 -0
- package/backend/firebase/storage.rules +37 -0
- package/backend/supabase/migration.sql +245 -0
- package/dist/adapter-Cyqfwj6F.d.ts +40 -0
- package/dist/annotate-kit.css +2 -0
- package/dist/chunk-G44KPHPN.js +107 -0
- package/dist/delivery.d.ts +31 -0
- package/dist/delivery.js +48 -0
- package/dist/firebase.d.ts +34 -0
- package/dist/firebase.js +200 -0
- package/dist/idb.d.ts +18 -0
- package/dist/idb.js +202 -0
- package/dist/index.d.ts +290 -0
- package/dist/index.js +1856 -0
- package/dist/local.d.ts +18 -0
- package/dist/local.js +115 -0
- package/dist/prompt.d.ts +54 -0
- package/dist/prompt.js +8 -0
- package/dist/rest.d.ts +27 -0
- package/dist/rest.js +63 -0
- package/dist/supabase.d.ts +31 -0
- package/dist/supabase.js +143 -0
- package/dist/types-BLXmj4Oi.d.ts +93 -0
- package/package.json +143 -0
- package/src/Annotate.tsx +1043 -0
- package/src/ScreenshotEditor.tsx +195 -0
- package/src/adapter.ts +31 -0
- package/src/adapters/firebase.ts +197 -0
- package/src/adapters/idb.ts +181 -0
- package/src/adapters/local.ts +114 -0
- package/src/adapters/rest.ts +98 -0
- package/src/adapters/supabase.ts +185 -0
- package/src/constants.ts +35 -0
- package/src/delivery.ts +63 -0
- package/src/index.ts +20 -0
- package/src/prompt.ts +109 -0
- package/src/snapshot.ts +21 -0
- package/src/styles/annotate.css +211 -0
- package/src/types.ts +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lavish-mitra
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# ๐๏ธ annotate-kit
|
|
4
|
+
|
|
5
|
+
**A drop-in, in-app annotation & visual-feedback overlay for React.**
|
|
6
|
+
|
|
7
|
+
Pin and draw directly on your live site โ capture the element, screenshot, console & network โ
|
|
8
|
+
export one clean prompt for your developer or any AI assistant.
|
|
9
|
+
|
|
10
|
+
Backend-agnostic. Safe by default. Free, and works with **zero sign-up**.
|
|
11
|
+
|
|
12
|
+
[](https://www.npmjs.com/package/annotate-kit)
|
|
13
|
+
[](https://github.com/lavish-mitra/annotate-kit/actions/workflows/ci.yml)
|
|
14
|
+
[](LICENSE)
|
|
15
|
+
[](CONTRIBUTING.md)
|
|
16
|
+
[](https://react.dev)
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Why annotate-kit?
|
|
23
|
+
|
|
24
|
+
Getting precise visual feedback usually means blurry screenshots and vague messages like
|
|
25
|
+
_"the button looks off."_ annotate-kit lets anyone mark **exactly** what they mean, right on the live
|
|
26
|
+
page, and hands your developer (or any AI coding assistant) a structured, pinpointed change list โ
|
|
27
|
+
with the element selector, a screenshot, and the console/network context already attached.
|
|
28
|
+
|
|
29
|
+
- ๐ **Pin & draw** โ box, arrow, circle, freehand โ with colours, type, priority, status & replies
|
|
30
|
+
- ๐ผ๏ธ **Screenshots** โ per-element, full-page, or viewport, with a built-in **markup editor** (draw arrows/boxes/pen); password fields blurred automatically
|
|
31
|
+
- ๐ **Reproducible context** โ browser/OS/viewport + opt-in console and network capture
|
|
32
|
+
- ๐ค **Tool-agnostic export** โ clean markdown *or* structured JSON for any dev or AI assistant
|
|
33
|
+
- ๐ **Any backend** โ zero-backend, your REST API, Supabase, Firebase, or write your own (9 methods)
|
|
34
|
+
- โก **Realtime** โ live updates on Supabase & Firebase (falls back to polling elsewhere)
|
|
35
|
+
- ๐ค **Deliver anywhere** โ optional one-click GitHub issue / webhook / Slack from the Send panel
|
|
36
|
+
- ๐ **Safe by default** โ ships admin-only, marks private, capture opt-in, secrets redacted
|
|
37
|
+
- ๐จ **Themeable** โ light / dark / auto + your brand accent colour, self-contained styles
|
|
38
|
+
- โฟ **Accessible** โ dialog semantics, labelled controls, full keyboard support
|
|
39
|
+
|
|
40
|
+
## How it works
|
|
41
|
+
|
|
42
|
+
```mermaid
|
|
43
|
+
flowchart LR
|
|
44
|
+
U([Your users]) -->|pin and draw| A["Annotate overlay"]
|
|
45
|
+
A -->|9-method contract| AD[StorageAdapter]
|
|
46
|
+
AD --> LB["local / idb (zero backend)"]
|
|
47
|
+
AD --> RB["your REST API"]
|
|
48
|
+
AD --> SB[Supabase]
|
|
49
|
+
AD --> FB[Firebase]
|
|
50
|
+
A -->|export| P["buildDevPrompt / buildDevContext"]
|
|
51
|
+
P --> DEV([Developer or AI assistant])
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## ๐ 30-second start (zero backend, no sign-up)
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm install annotate-kit
|
|
60
|
+
# or straight from GitHub: npm install github:lavish-mitra/annotate-kit
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { Annotate } from 'annotate-kit'
|
|
65
|
+
import { createLocalAdapter } from 'annotate-kit/local'
|
|
66
|
+
import 'annotate-kit/styles.css'
|
|
67
|
+
|
|
68
|
+
export function AnnotateMount() {
|
|
69
|
+
return <Annotate adapter={createLocalAdapter()} />
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Render `<AnnotateMount />` once near your app root. That's it โ a floating tool appears, and marks are
|
|
74
|
+
saved in the browser. Ready for a demo or local dev without any server.
|
|
75
|
+
|
|
76
|
+
> Ships prebuilt (`dist/` is committed), so there's no build step on install. Peer deps: `react` and
|
|
77
|
+
> `react-dom` (already in your app).
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Pick a backend
|
|
82
|
+
|
|
83
|
+
The overlay talks to storage through **one small adapter** โ choose what fits. **None require a
|
|
84
|
+
third-party sign-up.**
|
|
85
|
+
|
|
86
|
+
| Adapter | Import | Best for | Multi-user? |
|
|
87
|
+
|---|---|---|---|
|
|
88
|
+
| **local** | `annotate-kit/local` | Demos, local dev, single user | โ single browser |
|
|
89
|
+
| **idb** | `annotate-kit/idb` | Same, but many/large screenshots (IndexedDB) | โ single browser |
|
|
90
|
+
| **rest** | `annotate-kit/rest` | Your own API on any platform | โ
your server enforces |
|
|
91
|
+
| **supabase** | `annotate-kit/supabase` | Supabase projects | โ
RLS enforces |
|
|
92
|
+
| **firebase** | `annotate-kit/firebase` | Firebase projects | โ
rules enforce |
|
|
93
|
+
|
|
94
|
+
**Your own API (any platform):**
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import { createRestAdapter } from 'annotate-kit/rest'
|
|
98
|
+
|
|
99
|
+
const adapter = createRestAdapter({
|
|
100
|
+
baseUrl: '/api/annotations',
|
|
101
|
+
headers: () => ({ Authorization: `Bearer ${getToken()}` }), // optional
|
|
102
|
+
})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
See [`src/adapters/rest.ts`](src/adapters/rest.ts) for the exact 9-endpoint contract your API implements.
|
|
106
|
+
|
|
107
|
+
<details>
|
|
108
|
+
<summary><b>Quick start โ Supabase</b></summary>
|
|
109
|
+
|
|
110
|
+
**1. Apply the backend** (once): run [`backend/supabase/migration.sql`](backend/supabase/migration.sql)
|
|
111
|
+
via `supabase db push` or the SQL editor. It creates the table, a global toggle, a private
|
|
112
|
+
`annotations` bucket, an author-stamping trigger, and RLS policies safe for the anon key.
|
|
113
|
+
|
|
114
|
+
**2. Mark your admins**: in Supabase Auth, give admin users
|
|
115
|
+
`app_metadata.role = "admin"` (or `app_metadata.annotate_admin = true`).
|
|
116
|
+
|
|
117
|
+
**3. Mount it:**
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import { Annotate } from 'annotate-kit'
|
|
121
|
+
import { createSupabaseAdapter } from 'annotate-kit/supabase'
|
|
122
|
+
import { useMemo } from 'react'
|
|
123
|
+
import { useLocation, useNavigate } from 'react-router-dom'
|
|
124
|
+
import { supabase } from './lib/supabase'
|
|
125
|
+
import 'annotate-kit/styles.css'
|
|
126
|
+
|
|
127
|
+
export function AnnotateMount() {
|
|
128
|
+
const adapter = useMemo(() => createSupabaseAdapter({ client: supabase }), [])
|
|
129
|
+
const { pathname } = useLocation()
|
|
130
|
+
const navigate = useNavigate()
|
|
131
|
+
return <Annotate adapter={adapter} route={pathname} onNavigate={navigate} />
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The tool ships **admin-only** โ an admin enables it for everyone from the panel when ready.
|
|
136
|
+
|
|
137
|
+
</details>
|
|
138
|
+
|
|
139
|
+
<details>
|
|
140
|
+
<summary><b>Quick start โ Firebase</b></summary>
|
|
141
|
+
|
|
142
|
+
**1. Apply the rules + indexes** (once): merge [`backend/firebase/firestore.rules`](backend/firebase/firestore.rules)
|
|
143
|
+
and [`backend/firebase/storage.rules`](backend/firebase/storage.rules) into your rules, add the composite
|
|
144
|
+
indexes from [`backend/firebase/firestore.indexes.json`](backend/firebase/firestore.indexes.json), and deploy:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
firebase deploy --only firestore:rules,firestore:indexes,storage
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**2. Mark your admins** via a custom claim (Admin SDK):
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
await admin.auth().setCustomUserClaims(uid, { admin: true })
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**3. Mount it:**
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
import { Annotate } from 'annotate-kit'
|
|
160
|
+
import { createFirebaseAdapter } from 'annotate-kit/firebase'
|
|
161
|
+
import { useMemo } from 'react'
|
|
162
|
+
import { db, storage, auth } from './lib/firebase'
|
|
163
|
+
import 'annotate-kit/styles.css'
|
|
164
|
+
|
|
165
|
+
export function AnnotateMount() {
|
|
166
|
+
const adapter = useMemo(() => createFirebaseAdapter({ firestore: db, storage, auth }), [])
|
|
167
|
+
return <Annotate adapter={adapter} route={window.location.pathname} />
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
</details>
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## `<Annotate>` props
|
|
176
|
+
|
|
177
|
+
| Prop | Type | Default | Notes |
|
|
178
|
+
|---|---|---|---|
|
|
179
|
+
| `adapter` | `StorageAdapter` | โ | **Required.** From `annotate-kit/{local,idb,rest,supabase,firebase}`, or your own. |
|
|
180
|
+
| `route` | `string` | `location.pathname` | Pass your router's pathname so marks are page-aware in an SPA. |
|
|
181
|
+
| `onNavigate` | `(path) => void` | full-page load | Your router's `navigate`, for smooth cross-page "find on page". |
|
|
182
|
+
| `user` | `{ email; name; role }` | from adapter | Override author attribution. |
|
|
183
|
+
| `onToast` | `(kind, msg) => void` | built-in toast | Hook into your app's notifications. |
|
|
184
|
+
| `pollMs` | `number` | `15000` | Refresh cadence while the tool/panel is open. |
|
|
185
|
+
| `theme` | `'light' \| 'dark' \| 'auto'` | `'light'` | Colour theme for the widget's own panels (not the host page). |
|
|
186
|
+
| `accent` | `string` | brand indigo | Brand accent colour (any valid CSS colour, e.g. `#0ea5e9`). Invalid values ignored. |
|
|
187
|
+
| `captureConsole` | `boolean` | `false` | Attach a rolling console/JS-error buffer to each mark (may contain secrets). |
|
|
188
|
+
| `captureNetwork` | `boolean` | `false` | Attach failed/slow fetch+XHR calls; URLs trimmed + redacted. |
|
|
189
|
+
| `captureDomSnapshot` | `boolean` | `false` | Attach a sanitised HTML snapshot of the marked element (scripts/handlers stripped, passwords blanked). |
|
|
190
|
+
| `captureText` | `boolean` | `true` | Capture on-screen element text. Turn off for sensitive apps. |
|
|
191
|
+
| `captureFullUrl` | `boolean` | `false` | Store the full URL (query + hash) instead of just `origin + pathname`. |
|
|
192
|
+
| `redactScreenshots` | `boolean` | `true` | Blur password fields and `[data-annot-redact]` elements before capture. |
|
|
193
|
+
| `redact` | `(text) => string` | built-in | Redact secrets/PII from captured text + console. Default masks JWTs, tokens, emails, long digit runs. |
|
|
194
|
+
| `singleKeyShortcuts` | `boolean` | `true` | Bare-letter tool hotkeys (P/B/A/C/D). Turn off to avoid host-app key collisions. |
|
|
195
|
+
| `deliveries` | `Delivery[]` | `[]` | Extra "send toโฆ" buttons in the Send panel โ GitHub issue / webhook / Slack / your own. |
|
|
196
|
+
|
|
197
|
+
> If your adapter supports realtime (Supabase & Firebase do), the overlay updates **live** โ you don't
|
|
198
|
+
> need to tune `pollMs`.
|
|
199
|
+
|
|
200
|
+
The tool only renders for users who may use it, and it ships **admin-only** with marks defaulting to
|
|
201
|
+
**private** โ so it's safe to mount unconditionally: end users see nothing until an admin enables it.
|
|
202
|
+
|
|
203
|
+
**Keyboard:** `Shift+A` pin ยท `P/B/A/C/D` tools ยท `Esc` cancel/close ยท `โ/Ctrl+Enter` save ยท
|
|
204
|
+
`Ctrl+Z` undo ยท `Shift+H` hide/show ยท `?` help.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Sending feedback
|
|
209
|
+
|
|
210
|
+
Open the dock โ mark things โ **Send**. You get a structured markdown prompt (grouped by page, ordered
|
|
211
|
+
by priority, each item naming the exact element, request, environment, and console). Copy it into your
|
|
212
|
+
dev chat or any AI assistant. As items get built, flip them to **Done** in the panel.
|
|
213
|
+
|
|
214
|
+
Two exports are available for your own tooling:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import { buildDevPrompt, buildDevContext } from 'annotate-kit'
|
|
218
|
+
|
|
219
|
+
const markdown = buildDevPrompt(items, { includeConsole: true, includeNetwork: true })
|
|
220
|
+
const json = buildDevContext(items) // { total, items: [{ element, note, console, network, โฆ }] }
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Both are pure functions. The export treats all annotation text as **untrusted data** (a stored
|
|
224
|
+
prompt-injection defence) and omits console/network unless you opt in.
|
|
225
|
+
|
|
226
|
+
### Send straight to your tools (optional)
|
|
227
|
+
|
|
228
|
+
Add one-click targets to the Send panel. `githubIssueDelivery` needs **no token**; webhook/Slack use a
|
|
229
|
+
URL you provide:
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
import { githubIssueDelivery, createWebhookDelivery, createSlackDelivery } from 'annotate-kit'
|
|
233
|
+
|
|
234
|
+
<Annotate adapter={adapter} deliveries={[
|
|
235
|
+
githubIssueDelivery({ repo: 'you/your-repo' }), // opens a prefilled issue (no token)
|
|
236
|
+
createWebhookDelivery({ url: 'https://api.you.dev/feedback' }), // POSTs { prompt, context }
|
|
237
|
+
createSlackDelivery({ webhookUrl: process.env.SLACK_HOOK! }), // posts to Slack
|
|
238
|
+
]} />
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
> **Client-side note:** webhook/Slack targets run in the browser โ their URL/headers are visible to end
|
|
244
|
+
> users, so use non-secret endpoints or proxy secrets through your own server (`githubIssueDelivery`
|
|
245
|
+
> needs no token). Also memoize your `adapter` (`useMemo`) so realtime/polling don't re-subscribe each render.
|
|
246
|
+
|
|
247
|
+
## ๐ Security & privacy
|
|
248
|
+
|
|
249
|
+
annotate-kit captures live-app context, so the defaults are conservative โ please review before
|
|
250
|
+
shipping. See [SECURITY.md](SECURITY.md) for the full policy and how to report issues.
|
|
251
|
+
|
|
252
|
+
- **Admin-only until enabled**, new marks **private** by default.
|
|
253
|
+
- **Screenshots are access-controlled** (Supabase/Firebase): private-mark shots are readable only by
|
|
254
|
+
their owner or an admin; uploads are capped to 5 MB images and can't overwrite existing files.
|
|
255
|
+
- **Console & network capture are opt-in** and only hook while the tool is active โ no global patching
|
|
256
|
+
at import, and host wrappers (Sentry etc.) are never clobbered.
|
|
257
|
+
- **Redaction**: captured text/console is scrubbed (JWTs, tokens, emails, long digit runs); screenshots
|
|
258
|
+
blur password fields and any `data-annot-redact` element. URLs are stored as `origin + pathname`
|
|
259
|
+
unless you opt into `captureFullUrl`.
|
|
260
|
+
- **Server-verified identity**: Supabase stamps `created_by`/role from the JWT and pins it immutable;
|
|
261
|
+
Firestore pins `created_by` on update. Authorization is enforced by your backend, not the client.
|
|
262
|
+
- **Local / idb adapters** are single-browser only โ no multi-user boundary. Use a server-backed
|
|
263
|
+
adapter for shared data.
|
|
264
|
+
|
|
265
|
+
> โ ๏ธ The `local` and `idb` adapters store everything in one browser with no access control โ great for
|
|
266
|
+
> demos and single-user, never for shared/multi-tenant data.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Write your own adapter
|
|
271
|
+
|
|
272
|
+
Implement 9 methods against any store and the whole UI just works:
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
import type { StorageAdapter } from 'annotate-kit'
|
|
276
|
+
|
|
277
|
+
const myAdapter: StorageAdapter = {
|
|
278
|
+
getAccess, // who is looking + what they may do
|
|
279
|
+
setAccess, // flip the global toggle (admin)
|
|
280
|
+
list, // all visible marks, with screenshot_url resolved
|
|
281
|
+
save, // persist a new mark โ { id }
|
|
282
|
+
update, // patch status/note/type/priority by id
|
|
283
|
+
remove, // delete a mark (+ its screenshot)
|
|
284
|
+
addReply, // append a reply
|
|
285
|
+
clearResolved, // delete done/dismissed
|
|
286
|
+
uploadScreenshot, // (File, { visibility }) โ { path }
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
See [`src/adapter.ts`](src/adapter.ts) for the exact contract.
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Contributing
|
|
295
|
+
|
|
296
|
+
Contributions are welcome โ bug fixes, adapters, docs, and ideas. Please read
|
|
297
|
+
[CONTRIBUTING.md](CONTRIBUTING.md), and note the [Code of Conduct](CODE_OF_CONDUCT.md).
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
git clone https://github.com/lavish-mitra/annotate-kit.git
|
|
301
|
+
cd annotate-kit && npm install
|
|
302
|
+
npm run typecheck && npm test && npm run build
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## License
|
|
306
|
+
|
|
307
|
+
[MIT](LICENSE) ยฉ annotate-kit contributors โ free to use, modify, and distribute.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"//": "annotate-kit โ composite indexes so non-admin list() can order newest-first while filtering. Deploy with: firebase deploy --only firestore:indexes (rename 'annotations' if you set collectionPath).",
|
|
3
|
+
"indexes": [
|
|
4
|
+
{
|
|
5
|
+
"collectionGroup": "annotations",
|
|
6
|
+
"queryScope": "COLLECTION",
|
|
7
|
+
"fields": [
|
|
8
|
+
{ "fieldPath": "visibility", "order": "ASCENDING" },
|
|
9
|
+
{ "fieldPath": "id", "order": "DESCENDING" }
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"collectionGroup": "annotations",
|
|
14
|
+
"queryScope": "COLLECTION",
|
|
15
|
+
"fields": [
|
|
16
|
+
{ "fieldPath": "created_by", "order": "ASCENDING" },
|
|
17
|
+
{ "fieldPath": "id", "order": "DESCENDING" }
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"fieldOverrides": []
|
|
22
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
rules_version = '2';
|
|
2
|
+
|
|
3
|
+
// annotate-kit โ Firestore rules. Merge these into your firestore.rules (inside the
|
|
4
|
+
// existing `match /databases/{database}/documents { ... }` block) and deploy:
|
|
5
|
+
// firebase deploy --only firestore:rules
|
|
6
|
+
//
|
|
7
|
+
// Admin model: a user is an admin when their ID-token has custom claim
|
|
8
|
+
// admin == true OR role == 'admin'
|
|
9
|
+
// (set via the Admin SDK: admin.auth().setCustomUserClaims(uid, { admin: true })).
|
|
10
|
+
// The client adapter's isAdmin default matches this.
|
|
11
|
+
//
|
|
12
|
+
// Security notes (kept at parity with the Supabase backend):
|
|
13
|
+
// * Ships admin-only: non-admin read/create is gated on the global toggle at
|
|
14
|
+
// annotate_settings/global (enabled == true). New marks default to visibility:'private'.
|
|
15
|
+
// * created_by is pinned to your own email on create and IMMUTABLE on update โ no author forgery.
|
|
16
|
+
// * Anyone who can SEE a mark (public, or owner, or admin) may append a reply.
|
|
17
|
+
// * Payload sizes are capped to blunt storage/cost abuse.
|
|
18
|
+
// * NOTE (created_by_name/role): these are client-written display values on Firebase and are not
|
|
19
|
+
// server-verified (unlike Supabase's trigger). Treat only created_by (email) as trustworthy.
|
|
20
|
+
// * NOTE (screenshots): getDownloadURL tokens are bearer links; a private screenshot's URL is only
|
|
21
|
+
// stored in owner/admin-readable docs, but the link itself bypasses Storage rules once known.
|
|
22
|
+
//
|
|
23
|
+
// NOTE: if you set collectionPath / settingsPath on the adapter, rename the match/settings paths below.
|
|
24
|
+
|
|
25
|
+
service cloud.firestore {
|
|
26
|
+
match /databases/{database}/documents {
|
|
27
|
+
|
|
28
|
+
function annotSignedIn() { return request.auth != null; }
|
|
29
|
+
function annotIsAdmin() {
|
|
30
|
+
return annotSignedIn() && (request.auth.token.admin == true || request.auth.token.role == 'admin');
|
|
31
|
+
}
|
|
32
|
+
function annotMyEmail() { return request.auth.token.email; }
|
|
33
|
+
// Global on/off toggle โ mirrors Supabase's annotate_enabled(). Reads the settings doc.
|
|
34
|
+
function annotEnabled() {
|
|
35
|
+
return exists(/databases/$(database)/documents/annotate_settings/global)
|
|
36
|
+
&& get(/databases/$(database)/documents/annotate_settings/global).data.enabled == true;
|
|
37
|
+
}
|
|
38
|
+
function annotSizeOk() {
|
|
39
|
+
let d = request.resource.data;
|
|
40
|
+
return (!('note' in d) || d.note.size() <= 5000)
|
|
41
|
+
&& (!('target_text' in d) || d.target_text.size() <= 2000)
|
|
42
|
+
&& (!('target_selector' in d) || d.target_selector.size() <= 1000)
|
|
43
|
+
&& (!('points' in d) || d.points == null || d.points.size() <= 4000)
|
|
44
|
+
&& (!('console_logs' in d) || d.console_logs == null || d.console_logs.size() <= 50)
|
|
45
|
+
&& (!('network_logs' in d) || d.network_logs == null || d.network_logs.size() <= 100)
|
|
46
|
+
&& (!('replies' in d) || d.replies == null || d.replies.size() <= 200)
|
|
47
|
+
&& (!('dom_snapshot' in d) || d.dom_snapshot == null || d.dom_snapshot.size() <= 20000);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
match /annotations/{id} {
|
|
51
|
+
// Admins see everything; everyone else sees their own + public marks โ but only while enabled.
|
|
52
|
+
allow read: if annotIsAdmin()
|
|
53
|
+
|| (annotEnabled() && annotSignedIn()
|
|
54
|
+
&& (resource.data.created_by == annotMyEmail() || resource.data.visibility == 'public'));
|
|
55
|
+
// Create only as yourself, only while enabled (or admin), with valid visibility + bounded size.
|
|
56
|
+
allow create: if (annotIsAdmin() || annotEnabled())
|
|
57
|
+
&& annotSignedIn()
|
|
58
|
+
&& request.resource.data.created_by == annotMyEmail()
|
|
59
|
+
&& request.resource.data.visibility in ['public', 'private']
|
|
60
|
+
&& annotSizeOk();
|
|
61
|
+
// Edit your own (or admin) with created_by immutable, OR append a reply to any PUBLIC mark you can see.
|
|
62
|
+
allow update: if (
|
|
63
|
+
(annotIsAdmin() || (annotSignedIn() && resource.data.created_by == annotMyEmail()))
|
|
64
|
+
&& request.resource.data.created_by == resource.data.created_by
|
|
65
|
+
&& request.resource.data.visibility in ['public', 'private']
|
|
66
|
+
&& annotSizeOk()
|
|
67
|
+
) || (
|
|
68
|
+
annotSignedIn()
|
|
69
|
+
&& resource.data.visibility == 'public'
|
|
70
|
+
&& request.resource.data.diff(resource.data).affectedKeys().hasOnly(['replies', 'updated_at'])
|
|
71
|
+
&& request.resource.data.created_by == resource.data.created_by
|
|
72
|
+
&& annotSizeOk()
|
|
73
|
+
);
|
|
74
|
+
// Delete your own, or admin.
|
|
75
|
+
allow delete: if annotIsAdmin()
|
|
76
|
+
|| (annotSignedIn() && resource.data.created_by == annotMyEmail());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
match /annotate_settings/{doc} {
|
|
80
|
+
allow read: if annotSignedIn(); // everyone can read the on/off toggle
|
|
81
|
+
allow write: if annotIsAdmin(); // only admins flip it
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
rules_version = '2';
|
|
2
|
+
|
|
3
|
+
// annotate-kit โ Cloud Storage rules for annotation screenshots. Merge into your storage.rules and deploy:
|
|
4
|
+
// firebase deploy --only storage
|
|
5
|
+
//
|
|
6
|
+
// Screenshots are stored under annotations/<visibility>/<uid>/<file> (the adapter builds this path):
|
|
7
|
+
// * public/<uid>/โฆ readable by any signed-in user (these back 'public' marks)
|
|
8
|
+
// * private/<uid>/โฆ readable ONLY by the owner (uid) or an admin
|
|
9
|
+
// Uploads may only go into the caller's OWN uid folder, are limited to <5MB images, and existing
|
|
10
|
+
// objects cannot be overwritten. Delete is restricted to the owner or an admin.
|
|
11
|
+
//
|
|
12
|
+
// NOTE: if you set storagePath on the adapter, rename the 'annotations' segment below to match.
|
|
13
|
+
|
|
14
|
+
service firebase.storage {
|
|
15
|
+
match /b/{bucket}/o {
|
|
16
|
+
|
|
17
|
+
function annotSignedIn() { return request.auth != null; }
|
|
18
|
+
function annotIsAdmin() {
|
|
19
|
+
return annotSignedIn() && (request.auth.token.admin == true || request.auth.token.role == 'admin');
|
|
20
|
+
}
|
|
21
|
+
function annotImageOk() {
|
|
22
|
+
return request.resource.size < 5 * 1024 * 1024
|
|
23
|
+
&& request.resource.contentType.matches('image/.*');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
match /annotations/{visibility}/{uid}/{allPaths=**} {
|
|
27
|
+
allow read: if annotSignedIn()
|
|
28
|
+
&& (visibility == 'public' || request.auth.uid == uid || annotIsAdmin());
|
|
29
|
+
allow create: if annotSignedIn()
|
|
30
|
+
&& request.auth.uid == uid
|
|
31
|
+
&& (visibility == 'public' || visibility == 'private')
|
|
32
|
+
&& annotImageOk();
|
|
33
|
+
allow delete: if annotSignedIn() && (request.auth.uid == uid || annotIsAdmin());
|
|
34
|
+
allow update: if false; // no overwriting an existing object
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|