@satanwagen/reviewkit 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 +284 -0
- package/cli/emblema-sync.mjs +636 -0
- package/dist/chunk-4YNLMSCK.js +234 -0
- package/dist/chunk-4YNLMSCK.js.map +1 -0
- package/dist/chunk-YWBFAV57.js +8057 -0
- package/dist/chunk-YWBFAV57.js.map +1 -0
- package/dist/client/index.cjs +8311 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +28 -0
- package/dist/client/index.d.ts +28 -0
- package/dist/client/index.js +4 -0
- package/dist/client/index.js.map +1 -0
- package/dist/index-BbucFgZi.d.ts +49 -0
- package/dist/index-CBlJrKkm.d.cts +49 -0
- package/dist/index.cjs +8302 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/schema.cjs +130 -0
- package/dist/schema.cjs.map +1 -0
- package/dist/schema.d.cts +226 -0
- package/dist/schema.d.ts +226 -0
- package/dist/schema.js +3 -0
- package/dist/schema.js.map +1 -0
- package/package.json +72 -0
- package/schema/review-session.schema.json +210 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dominik Majo
|
|
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,284 @@
|
|
|
1
|
+
# reviewkit
|
|
2
|
+
|
|
3
|
+
Drop-in visual review & feedback layer for React sites. A reviewer opens the live page, clicks elements and proposes changes — rewrite copy, swap an image, leave a comment, mark for deletion, request a move or a style tweak. **Nothing is ever applied to the site.** Every proposal is recorded as a portable session (JSON) that you hand to a coding agent or developer, who applies the changes in source.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @satanwagen/reviewkit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`react` and `react-dom` (18 or 19) are peer dependencies.
|
|
12
|
+
|
|
13
|
+
## Two-line integration
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { ReviewKit } from '@satanwagen/reviewkit/client';
|
|
17
|
+
|
|
18
|
+
// anywhere in your tree, rendered once:
|
|
19
|
+
<ReviewKit />
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
That's it. The component renders `null` unless review mode is active, and all UI lives in a shadow root appended to `document.body` — your CSS never leaks in, reviewkit's CSS never leaks out. Safe to ship in production builds.
|
|
23
|
+
|
|
24
|
+
### Props
|
|
25
|
+
|
|
26
|
+
| Prop | Type | Description |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| `token` | `string` | Restricts URL activation to `?review=<token>`. Without it, any `?review=x` activates (with a console warning). |
|
|
29
|
+
| `syncToken` | `string` | Enables live collaboration via the realtime sync server (`realtime/`): presence avatars, live cursors, remote selections and instantly shared items. Unset = solo mode, no network. The value is public in the browser bundle by nature — treat it as a gate against casual visitors, not a secret. Keep it in the lazy review chunk of the host app, never in the main bundle. |
|
|
30
|
+
| `defaultAuthor` | `string` | Pre-fills the reviewer name. |
|
|
31
|
+
| `position` | `'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right'` | Initial corner of the floating badge (default `bottom-right`; draggable, persisted). |
|
|
32
|
+
| `storageKey` | `string` | Persistence key (default: page origin). |
|
|
33
|
+
| `onSessionChange` | `(session: ReviewSession) => void` | Called (debounced) whenever the session changes. |
|
|
34
|
+
|
|
35
|
+
## How the layer activates
|
|
36
|
+
|
|
37
|
+
**With a `token` configured (the production setup) the rules are strict:**
|
|
38
|
+
|
|
39
|
+
1. `?review=<token>` in the URL — the only automatic way in.
|
|
40
|
+
2. The reviewer's explicit **“Keep on in this tab”** toggle in the panel — a per-tab, token-bound opt-in that expires after ~4 hours. Nothing persists silently.
|
|
41
|
+
|
|
42
|
+
Anything else shows nothing: no badge, no listeners, no WebSocket. **“End review”** in the panel (or `?review=off`) clears all persisted state and returns the clean site. Leftover activation keys from pre-strict versions (`review-kit:active`, `reviewkit`) are silently removed on the first token-less load.
|
|
43
|
+
|
|
44
|
+
**Without a token** (zero-config/dev) the conveniences remain: any `?review=x` value (console warning), the in-tab opt-in, a `reviewkit` localStorage flag, or a dev build (`NODE_ENV === 'development'`).
|
|
45
|
+
|
|
46
|
+
## Using it (reviewer flow)
|
|
47
|
+
|
|
48
|
+
A **Review** badge floats in a corner (drag it to another corner; click it to open the panel).
|
|
49
|
+
|
|
50
|
+
1. Click **Pick an element**, then click anything on the page.
|
|
51
|
+
2. A toolbar appears on the selection: **Rewrite** (edit the text in place — the DOM is restored afterwards, only the diff is recorded), **Image** (propose a replacement image), **Comment**, **Style**, **Move**, **Delete**.
|
|
52
|
+
3. Each item takes an optional note and a **Must / Nice to have** priority.
|
|
53
|
+
4. The panel lists all items grouped by route, with search, type filters, inline editing, status (`open / accepted / rejected / applied`), and jump-to-element. Items whose element no longer exists are flagged **unanchored**.
|
|
54
|
+
|
|
55
|
+
Sessions autosave locally (IndexedDB, falling back to `localStorage`), keyed per origin — a reviewer can close the tab and continue later.
|
|
56
|
+
|
|
57
|
+
### Keyboard shortcuts
|
|
58
|
+
|
|
59
|
+
| Keys | Action |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| `?` | Shortcut cheatsheet |
|
|
62
|
+
| `Esc` | Close / deselect / exit pick mode (cascading) |
|
|
63
|
+
| `⌘Z` / `Ctrl+Z` | Undo last capture |
|
|
64
|
+
| `↑` `↓` | Move between items in the panel |
|
|
65
|
+
| `↵` | Commit an inline rewrite (`⇧↵` = newline) |
|
|
66
|
+
| `⌘↵` / `Ctrl+↵` | Save a comment/note composer |
|
|
67
|
+
|
|
68
|
+
## Session format & export
|
|
69
|
+
|
|
70
|
+
The panel footer has **Export** (downloads `review-<host>-<date>.json`), **Copy** (session JSON to clipboard), and **Import** (load a session file back — reviews can round-trip between people).
|
|
71
|
+
|
|
72
|
+
**Deletion is permanent and room-wide.** Deleting an item (or using Clear) removes it for everyone in the live room, and the sync server keeps a tombstone for the id, so no stale client — offline, another tab, an old laptop — can upload it back. Tombstones survive server restarts and are pruned after 30 days (capped at 5000 per room). Importing a session file does **not** resurrect items that were deleted in the live room: the tombstone wins and the imported copy disappears again on sync. To genuinely bring a deleted item back, re-create it (it gets a new id).
|
|
73
|
+
|
|
74
|
+
The JSON follows [`schema/review-session.schema.json`](./schema/review-session.schema.json) (`schemaVersion: 1`). Shape:
|
|
75
|
+
|
|
76
|
+
```jsonc
|
|
77
|
+
{
|
|
78
|
+
"schemaVersion": 1,
|
|
79
|
+
"id": "…",
|
|
80
|
+
"createdAt": "…", "updatedAt": "…",
|
|
81
|
+
"site": { "origin": "…", "route": "/", "viewport": { "width": 0, "height": 0 }, "userAgent": "…" },
|
|
82
|
+
"author": "Marta from marketing",
|
|
83
|
+
"items": [
|
|
84
|
+
{
|
|
85
|
+
"id": "…", "createdAt": "…",
|
|
86
|
+
"type": "copy", // copy | image | comment | delete | move | style
|
|
87
|
+
"status": "open", // open | accepted | rejected | applied
|
|
88
|
+
"priority": "must", // must | nice
|
|
89
|
+
"route": "/pricing",
|
|
90
|
+
"target": { // everything needed to find the element again
|
|
91
|
+
"selector": "#hero > p:nth-of-type(1)",
|
|
92
|
+
"tagName": "p", "textSnippet": "…",
|
|
93
|
+
"attrs": { "id": "…", "classList": ["…"], "data": {} },
|
|
94
|
+
"nthOfType": [1, 2, 1],
|
|
95
|
+
"boundingBox": { "x": 0, "y": 0, "width": 0, "height": 0 }
|
|
96
|
+
},
|
|
97
|
+
"payload": { "before": "old text", "after": "new text" },
|
|
98
|
+
"note": "optional free text"
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Payload per type: `copy` `{before, after}` · `image` `{beforeSrc, afterSrc?, afterDataUrl?, description?}` · `comment` `{text}` · `delete` `{reason?}` · `move` `{direction?, targetDescription?}` · `style` `{description}`. Unknown fields and unknown item types are preserved on import (forward compatibility).
|
|
105
|
+
|
|
106
|
+
For `image` items the replacement arrives in exactly one of two forms: **embedded** (`afterDataUrl`, a `data:` URL, capped at 2 MB at capture) or **linked** (`afterSrc`, an external URL — used when the reviewer pasted/dragged a link that couldn't be fetched because of CORS, or the file was too large to embed). An agent applying the session downloads `afterSrc` itself or writes the data URL to a file.
|
|
107
|
+
|
|
108
|
+
Versioning rule: `schemaVersion` (currently `1`) is bumped only for breaking changes — removing/renaming fields or changing their meaning. Additive optional fields (like documenting `afterSrc` above) do not bump it; every valid v1 session stays valid and importable.
|
|
109
|
+
|
|
110
|
+
### Handing the output to a coding agent
|
|
111
|
+
|
|
112
|
+
Export (or Copy) the session and give the file to the agent with a prompt like:
|
|
113
|
+
|
|
114
|
+
> Apply the feedback in `review-agropolio-2026-07-28.json` to this codebase. For each item, locate the source element using `target.selector`, `target.textSnippet` and `target.attrs`, apply the change described by `type`/`payload`/`note` (items with `"priority": "must"` first), and set its `status` to `"applied"`.
|
|
115
|
+
|
|
116
|
+
Programmatic helpers are exported too: `parseSessionJson` / `parseSession` (validation), `isCopyItem` … type guards, and `sessionToJson` / `downloadSession` / `importSessionFile` from `reviewkit` (server-safe schema utilities also via `reviewkit/schema`).
|
|
117
|
+
|
|
118
|
+
## Integrating into an existing Vite + React site
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
// src/main.tsx
|
|
122
|
+
import { StrictMode } from 'react';
|
|
123
|
+
import { createRoot } from 'react-dom/client';
|
|
124
|
+
import { ReviewKit } from '@satanwagen/reviewkit/client';
|
|
125
|
+
import { App } from './App';
|
|
126
|
+
|
|
127
|
+
createRoot(document.getElementById('root')!).render(
|
|
128
|
+
<StrictMode>
|
|
129
|
+
<App />
|
|
130
|
+
<ReviewKit token={import.meta.env.VITE_REVIEW_TOKEN} />
|
|
131
|
+
</StrictMode>,
|
|
132
|
+
);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
No Vite config changes are needed — `reviewkit` ships prebuilt ESM/CJS with types. In dev the layer is always on; in production reviewers open `https://your-site.tld/?review=<token>`.
|
|
136
|
+
|
|
137
|
+
Until the npm release lands you can also depend on it via a file path — this is exactly what the demo does:
|
|
138
|
+
|
|
139
|
+
```jsonc
|
|
140
|
+
// package.json
|
|
141
|
+
"dependencies": { "@satanwagen/reviewkit": "file:../path/to/ReviewKit" }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
npm symlinks the folder, so after editing reviewkit run `npm run build` in it and restart/reload Vite. Note that the deployed site only picks up library changes after a rebuild + deploy of the host app (Fable has `npm run deploy` for the whole chain) — the live sync server only carries review data, never code. Run `npm run build` in `ReviewKit/` at least once before installing, since the package resolves to `dist/`.
|
|
145
|
+
|
|
146
|
+
### Real-world example: token-only activation + lazy loading (Agropolio landing page)
|
|
147
|
+
|
|
148
|
+
The Agropolio landing page (`../Agropolio/Fable` relative to this repo) integrates reviewkit so that **only** `?review=agropolio` ever activates it — dev-build auto-activation is bypassed, and without the token the library code isn't even downloaded (separate lazy chunk):
|
|
149
|
+
|
|
150
|
+
```jsonc
|
|
151
|
+
// Fable/package.json
|
|
152
|
+
"dependencies": { "@satanwagen/reviewkit": "file:../../ReviewKit" }
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
// Fable/vite.config.ts — the linked package has its own node_modules,
|
|
157
|
+
// dedupe guarantees a single React instance (Fable is on React 18):
|
|
158
|
+
resolve: { dedupe: ['react', 'react-dom'] }
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
// Fable/src/ReviewLayer.tsx (rendered next to <App /> in main.tsx)
|
|
163
|
+
import { lazy, Suspense, useEffect, useState } from 'react'
|
|
164
|
+
|
|
165
|
+
const TOKEN = 'agropolio'
|
|
166
|
+
const SESSION_KEY = 'review-kit:active' // same key reviewkit uses internally
|
|
167
|
+
|
|
168
|
+
function shouldLoad(): boolean {
|
|
169
|
+
try {
|
|
170
|
+
const param = new URLSearchParams(window.location.search).get('review')
|
|
171
|
+
if (param === 'off') { window.sessionStorage.removeItem(SESSION_KEY); return false }
|
|
172
|
+
if (param === TOKEN) return true
|
|
173
|
+
return window.sessionStorage.getItem(SESSION_KEY) === '1' // survive reloads in-tab
|
|
174
|
+
} catch { return false }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const ReviewKit = lazy(() => import('@satanwagen/reviewkit/client').then((m) => ({ default: m.ReviewKit })))
|
|
178
|
+
|
|
179
|
+
export function ReviewLayer() {
|
|
180
|
+
const [load, setLoad] = useState(false)
|
|
181
|
+
useEffect(() => { if (shouldLoad()) setLoad(true) }, [])
|
|
182
|
+
if (!load) return null
|
|
183
|
+
return <Suspense fallback={null}><ReviewKit token={TOKEN} /></Suspense>
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The gate outside `<ReviewKit />` is what keeps the layer invisible to normal visitors: no token → the wrapper renders `null`, no shadow root, no listeners, no network request for the reviewkit chunk.
|
|
188
|
+
|
|
189
|
+
## Adding ReviewKit to another site (with live collaboration)
|
|
190
|
+
|
|
191
|
+
Step by step, for any site on any hosting (nginx, IIS, Vercel, …).
|
|
192
|
+
|
|
193
|
+
**1. Install the package** — pick one:
|
|
194
|
+
|
|
195
|
+
```jsonc
|
|
196
|
+
// a) local folder (this repo checked out next to your project):
|
|
197
|
+
"dependencies": { "@satanwagen/reviewkit": "file:../ReviewKit" }
|
|
198
|
+
// b) straight from git:
|
|
199
|
+
"dependencies": { "@satanwagen/reviewkit": "git+ssh://git@your-git-host/review-kit.git" }
|
|
200
|
+
// c) once published to npm:
|
|
201
|
+
"dependencies": { "@satanwagen/reviewkit": "^0.1.0" }
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Run `npm run build` inside ReviewKit at least once for the `file:`/git variants (the package resolves to `dist/`). With React 18 add `resolve.dedupe: ['react','react-dom']` to your bundler config when using a `file:` link.
|
|
205
|
+
|
|
206
|
+
**2. Integrate — two lines, done. Live collaboration included:**
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
import { ReviewKit } from '@satanwagen/reviewkit/client';
|
|
210
|
+
|
|
211
|
+
<ReviewKit token="my-site-token" syncToken="<the sync server's shared secret>" />
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
That's the whole setup. `syncUrl` **defaults to the always-on ReviewKit sync server** (`wss://agropolio.fucking.style/rk-sync`), so presence, live cursors and instantly-shared items work on any site with no server-side step at all — no allowlist entry, no restart, nothing. Rooms are keyed by the page's origin + path, so different sites never see each other's feedback or presence. If the server is unreachable the tool silently runs local-only (no console errors); omit `syncToken` to disable networking entirely.
|
|
215
|
+
|
|
216
|
+
**What the open server implies (honesty section):** the token and `syncToken` are readable in the browser bundle, so anyone holding a review URL can join and create rooms. The server is meant for the operator's own sites, not as a public service; it is guarded by the shared token, per-connection rate limits (40 msg/s, burst 80), 1 MiB frame cap, 16 clients and 2000 items per room, and a live-room ceiling (`RK_SYNC_MAX_ROOMS`, default 500).
|
|
217
|
+
|
|
218
|
+
**3. Advanced: run your own sync server** (optional) — `realtime/server/`: `npm run build`, run `dist/server.js` with `RK_SYNC_TOKEN` (+ optional `RK_SYNC_PORT`, `RK_SYNC_DATA`, `RK_SYNC_MAX_ROOMS`, and `RK_SYNC_ORIGINS` if you *want* an origin allowlist — empty/unset means any origin, which is the default deployment) as a service, point `syncUrl` at it, then put a WebSocket-aware reverse proxy in front:
|
|
219
|
+
|
|
220
|
+
*nginx* (as deployed on satan):
|
|
221
|
+
|
|
222
|
+
```nginx
|
|
223
|
+
location /rk-sync {
|
|
224
|
+
proxy_pass http://127.0.0.1:8787;
|
|
225
|
+
proxy_http_version 1.1;
|
|
226
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
227
|
+
proxy_set_header Connection "upgrade";
|
|
228
|
+
proxy_read_timeout 120s;
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
*IIS*: enable the **WebSocket Protocol** feature (Server Manager → Web Server → Application Development), install **Application Request Routing** + **URL Rewrite**, enable ARR proxying, and add a rewrite rule that forwards `^rk-sync` to `http://127.0.0.1:8787/rk-sync` — ARR carries the WebSocket upgrade automatically once the feature is on.
|
|
233
|
+
|
|
234
|
+
With an `RK_SYNC_ORIGINS` allowlist active, rejected origins are logged (`[origin] rejected …`) and closed with code 4005; the client quietly stays local.
|
|
235
|
+
|
|
236
|
+
**4. Token caveat:** don't reuse secrets that protect anything else, and keep tokens out of the main bundle (load ReviewKit in a lazy chunk like the Agropolio integration does).
|
|
237
|
+
|
|
238
|
+
**5. Verify it works:** open `https://your-site/?review=<token>` — the Review badge appears; the panel header shows **live** (green dot) when the sync server accepted you, or *local only* when not. In the Network tab you should see one WebSocket to `/rk-sync` with a `welcome` frame. Open the same URL in a second browser: avatars, live cursors and instantly-appearing items confirm collaboration.
|
|
239
|
+
|
|
240
|
+
## Pairing with Emblema
|
|
241
|
+
|
|
242
|
+
ReviewKit pairs with the [Emblema](https://emblema.app) desktop app: once paired, the local sync agent streams review items to Emblema, and the paired Emblema shows them as findings in its ReviewKit tile (statuses flow back to the page).
|
|
243
|
+
|
|
244
|
+
Sync is automatic and realtime — no manual step. With `emblema` set in the `ReviewKit` config, the browser itself pushes every added or edited open item to a locally running Emblema (debounced 500 ms, silent-fail: an unreachable Emblema never blocks the reviewer; the panel header shows a small `emblema` status dot). The session menu keeps **Resend all to Emblema** as a one-click fallback that re-dispatches every open item on the page.
|
|
245
|
+
|
|
246
|
+
**Where the key lives:** the pairing key and state are stored by the local sync agent in `<project>/.reviewkit/emblema-sync.json`. The key (`emk_…`) is a secret — the panel only ever displays it shortened (`emk_…last4`) and puts the full value on the clipboard.
|
|
247
|
+
|
|
248
|
+
**Set up and generate a key** (in the project repo):
|
|
249
|
+
|
|
250
|
+
```sh
|
|
251
|
+
npx reviewkit-emblema init --origin https://your-site # creates the state file, prints the key
|
|
252
|
+
npx reviewkit-emblema serve # runs the local API + realtime bridge
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Paste the printed key into Emblema. From the page UI the same lives in the panel's gear menu → **Connect to Emblema…** — the menu entry shows the live pairing status at a glance (green dot = paired, amber = the local agent isn't answering, red = key revoked, grey = not paired), and the dialog has **New key (rotate)**, **Copy key** and **Delete key (revoke)**.
|
|
256
|
+
|
|
257
|
+
**Rotate / revoke from the CLI:**
|
|
258
|
+
|
|
259
|
+
```sh
|
|
260
|
+
npx reviewkit-emblema key # print the current key
|
|
261
|
+
npx reviewkit-emblema rotate # issue a new secret — the old key stops working immediately
|
|
262
|
+
npx reviewkit-emblema revoke # disable the pairing entirely (rotate re-enables)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Rotate and revoke take effect immediately, including against an already-running `serve` process — authenticated requests always check the current on-disk key, so a rotated or revoked key can never keep authenticating.
|
|
266
|
+
|
|
267
|
+
## Demo
|
|
268
|
+
|
|
269
|
+
```sh
|
|
270
|
+
cd examples/vite-demo
|
|
271
|
+
npm install
|
|
272
|
+
npm run dev # http://localhost:3010 — dev build, so the layer is active immediately
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Also try `http://localhost:3010/?review=demo` to exercise URL activation.
|
|
276
|
+
|
|
277
|
+
## Development
|
|
278
|
+
|
|
279
|
+
```sh
|
|
280
|
+
npm install
|
|
281
|
+
npm run typecheck
|
|
282
|
+
npm run lint
|
|
283
|
+
npm run build # tsup → dist/ (ESM + CJS + d.ts)
|
|
284
|
+
```
|