@satanwagen/reviewkit 0.1.1 → 0.1.3

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 (56) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +134 -63
  3. package/dist/activate.cjs +150 -0
  4. package/dist/activate.cjs.map +1 -0
  5. package/dist/activate.d.cts +59 -0
  6. package/dist/activate.d.ts +59 -0
  7. package/dist/activate.js +21 -0
  8. package/dist/activate.js.map +1 -0
  9. package/dist/{chunk-4YNLMSCK.js → chunk-DG6O5XIC.js} +36 -10
  10. package/dist/chunk-DG6O5XIC.js.map +1 -0
  11. package/dist/chunk-ETAGGIDN.js +119 -0
  12. package/dist/chunk-ETAGGIDN.js.map +1 -0
  13. package/dist/{chunk-YWBFAV57.js → chunk-NSXRFM75.js} +853 -1544
  14. package/dist/chunk-NSXRFM75.js.map +1 -0
  15. package/dist/chunk-Q7U43PXE.js +36 -0
  16. package/dist/chunk-Q7U43PXE.js.map +1 -0
  17. package/dist/client/index.cjs +4530 -4237
  18. package/dist/client/index.cjs.map +1 -1
  19. package/dist/client/index.d.cts +1 -1
  20. package/dist/client/index.d.ts +1 -1
  21. package/dist/client/index.js +49 -3
  22. package/dist/client/index.js.map +1 -1
  23. package/dist/effects-EWKHKUDP.js +851 -0
  24. package/dist/effects-EWKHKUDP.js.map +1 -0
  25. package/dist/index-BUSmAweg.d.ts +77 -0
  26. package/dist/index-Dl2Fl0qr.d.cts +77 -0
  27. package/dist/index.cjs +4544 -4256
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.js +35 -3
  32. package/dist/index.js.map +1 -1
  33. package/dist/lazy.cjs +8988 -0
  34. package/dist/lazy.cjs.map +1 -0
  35. package/dist/lazy.d.cts +7 -0
  36. package/dist/lazy.d.ts +7 -0
  37. package/dist/lazy.js +8 -0
  38. package/dist/lazy.js.map +1 -0
  39. package/dist/next.cjs +8989 -0
  40. package/dist/next.cjs.map +1 -0
  41. package/dist/next.d.cts +4 -0
  42. package/dist/next.d.ts +4 -0
  43. package/dist/next.js +9 -0
  44. package/dist/next.js.map +1 -0
  45. package/dist/schema.cjs +53 -16
  46. package/dist/schema.cjs.map +1 -1
  47. package/dist/schema.d.cts +20 -5
  48. package/dist/schema.d.ts +20 -5
  49. package/dist/schema.js +30 -2
  50. package/dist/schema.js.map +1 -1
  51. package/package.json +21 -8
  52. package/cli/emblema-sync.mjs +0 -645
  53. package/dist/chunk-4YNLMSCK.js.map +0 -1
  54. package/dist/chunk-YWBFAV57.js.map +0 -1
  55. package/dist/index-BbucFgZi.d.ts +0 -49
  56. package/dist/index-CBlJrKkm.d.cts +0 -49
package/CHANGELOG.md ADDED
@@ -0,0 +1,105 @@
1
+ # Changelog
2
+
3
+ ## 0.1.3 — 2026-09-02
4
+
5
+ Defaults tightened so a production build behaves the way its code reads.
6
+ Several changes are breaking for hosts that relied on the old conveniences;
7
+ each has a one-line migration below.
8
+
9
+ ### Breaking
10
+
11
+ - **No activation without a token.** Previously an unset `token` meant "any
12
+ `?review=x` turns the layer on" (plus the `reviewkit` localStorage flag).
13
+ Now, without `token`, nothing activates. Migration: pass `token`. For a
14
+ reviewer-less dev loop pass `devAutoOn`.
15
+ - **No `NODE_ENV === 'development'` auto-on.** The library never reads the
16
+ host's NODE_ENV any more. Migration:
17
+ `devAutoOn={import.meta.env.DEV}` (Vite) or
18
+ `devAutoOn={process.env.NODE_ENV === 'development'}` (Next/webpack).
19
+ - **No default `syncUrl`.** The former default
20
+ `wss://agropolio.fucking.style/rk-sync` pointed at an expired domain. Sync
21
+ now runs only when **both** `syncUrl` and `syncToken` are set; otherwise
22
+ the layer is local-only, no socket. Migration: set `syncUrl` explicitly.
23
+ Same for the CLI: `reviewkit-emblema init --sync-url …` (the room bridge
24
+ stays disabled until both `--sync-url` and `--sync-token` are configured).
25
+ - **CLI moved to `@satanwagen/reviewkit-cli`.** The main package no longer
26
+ ships a `bin`, so a host `npm install` does not pull the Emblema agent.
27
+ Migration: `npx @satanwagen/reviewkit-cli init …` / `serve` (the command
28
+ inside is still `reviewkit-emblema`).
29
+ - **`emblema.repoPath` is optional.** The repo path is Emblema's local
30
+ knowledge (advertised in its `/intake/ping` answer, and present in
31
+ `.reviewkit/emblema-sync.json` for the agent) and no longer belongs in host
32
+ code. `emblema={{}}` now enables the bridge. The v1 dispatch payload keeps
33
+ `v: 1`, but `repoPath` became optional: an intake with a strict "required"
34
+ validator must relax it and fall back to its active project.
35
+ - **Effect sounds moved off the expired domain.** `/ondro` now fetches
36
+ `rick.mp3` from `https://2-59-219-26.sslip.io/rk-assets` (the same VPS,
37
+ reached through a sslip.io hostname so no domain renewal can break it).
38
+ New prop `fxAssetsUrl` overrides the base for self-hosting. Add the origin
39
+ to CSP `media-src` if you send a policy.
40
+ - Legacy activation keys (`review-kit:active`, `reviewkit`) are still cleaned
41
+ up on load but never consulted.
42
+ - **Emblema intake must accept a batch without `repoPath`.** An Emblema build
43
+ that still validates it as required answers HTTP 400; the panel now shows
44
+ that as `emblema rejected` (new push status) instead of `emblema offline`.
45
+ - **CLI:** a `.reviewkit/emblema-sync.json` written by 0.1.2 still carries the
46
+ retired default sync host. `serve` now detects it, logs a re-init hint and
47
+ runs with the bridge disabled instead of reconnecting forever; the local
48
+ `/status` endpoint reports `realtime.state: 'off'` whenever either sync
49
+ setting is missing (previously it said `connecting` with no URL).
50
+
51
+ ### Added
52
+
53
+ - `enabled?: boolean` master switch (default `true`) so a wrapper can write
54
+ `<ReviewKit enabled={Boolean(TOKEN)} token={TOKEN} />` instead of
55
+ `if (!TOKEN) return null`.
56
+ - `devAutoOn?: boolean` — host-controlled "always on for this load". Still
57
+ subject to `enabled`, `allowedHosts` and `?review=off`.
58
+ - **`@satanwagen/reviewkit/next`** — `'use client'` component: render it from
59
+ a server `layout.tsx`; it evaluates URL + sticky session after mount and
60
+ only then dynamically imports the core. `examples/next-demo` shows it
61
+ together with the CSP headers.
62
+ - **`@satanwagen/reviewkit/lazy`** — the same code-split component for Vite
63
+ and friends; no more hand-rolled `React.lazy` + `Suspense` gate.
64
+ - **`@satanwagen/reviewkit/activate`** — `shouldActivate({ token, enabled,
65
+ allowedHosts, devAutoOn })`, React-free, ~200 B. The supported way for a
66
+ host to gate its own lazy import; the sessionStorage format stays private.
67
+ - One-time console warning when a URL token activates the layer on a
68
+ non-loopback hostname with no `allowedHosts` (see README "Production
69
+ recipe").
70
+ - The `/` command effects (~40 KB of source) load as a separate chunk on
71
+ first use instead of shipping in the core chunk.
72
+ - `probeEmblema()` returns `{ port, repoPath? }`; `pingEmblema()` stays as a
73
+ port-only wrapper.
74
+ - Activation matrix tests (`test/activate.test.mjs`) run against `dist/`
75
+ (`npm test` builds first via `pretest`).
76
+ - A one-time console warning when `?review=<x>` does not match the configured
77
+ token (kept from 0.1.2, now also emitted by `shouldActivate`).
78
+ - `/lazy` and `/next` are effectively ESM entries: the CJS builds exist for
79
+ `require()` consumers but inline the core (esbuild has no CJS code
80
+ splitting). Every modern bundler picks the ESM build.
81
+ - README: activation-state table, CSP requirements, production recipe.
82
+
83
+ ### Bundle audit (unminified ESM, `dist/`)
84
+
85
+ | Chunk | 0.1.2 | 0.1.3 |
86
+ | --- | ---: | ---: |
87
+ | core client chunk | 313 KB | 267 KB |
88
+ | effects (lazy, on first `/command`) | in core | 41 KB |
89
+ | `activate` entry | — | 0.2 KB |
90
+
91
+ Largest remaining inputs: `styles.ts` (53 KB), `Panel.tsx` (46 KB). Schema
92
+ validation and IndexedDB persistence are under 10 KB each and were not the
93
+ weight.
94
+
95
+ ## 0.1.2
96
+
97
+ - Delete endpoint for Emblema findings, idempotent for unknown ids.
98
+
99
+ ## 0.1.1
100
+
101
+ - `reopen` status verb; realtime health in local state.
102
+
103
+ ## 0.1.0
104
+
105
+ - First scoped release as `@satanwagen/reviewkit`.
package/README.md CHANGED
@@ -13,35 +13,105 @@ npm install @satanwagen/reviewkit
13
13
  ## Two-line integration
14
14
 
15
15
  ```tsx
16
- import { ReviewKit } from '@satanwagen/reviewkit/client';
16
+ import { ReviewKit } from '@satanwagen/reviewkit/lazy'; // Vite, CRA, Remix …
17
+ // import { ReviewKit } from '@satanwagen/reviewkit/next'; // Next.js App Router ('use client' built in)
17
18
 
18
19
  // anywhere in your tree, rendered once:
19
- <ReviewKit />
20
+ <ReviewKit token={import.meta.env.VITE_REVIEW_TOKEN} />
20
21
  ```
21
22
 
22
23
  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
 
25
+ Three entries, same props:
26
+
27
+ | Entry | What it is | Use when |
28
+ | --- | --- | --- |
29
+ | `@satanwagen/reviewkit/lazy` | Component that decides after mount (URL + sticky session) and only then `import()`s the core chunk. | Vite / CRA / Remix / any client bundler. Visitors without a review link never download the core. |
30
+ | `@satanwagen/reviewkit/next` | Same, with `'use client'` baked into the built file so a server `layout.tsx` can render it directly. | Next.js App Router. See `examples/next-demo`. |
31
+ | `@satanwagen/reviewkit/client` | The core itself, eager. | You already have your own lazy boundary, or you want everything in one chunk. |
32
+
33
+ The code split only exists in the ESM builds (what Vite, Next, webpack 5 pick); the CJS files of `/lazy` and `/next` inline the core for `require()` consumers.
34
+
35
+ And a React-free helper for hosts that gate their own lazy import:
36
+
37
+ ```ts
38
+ import { shouldActivate } from '@satanwagen/reviewkit/activate'; // ~200 B, no React, SSR-safe (false)
39
+ if (shouldActivate({ token: TOKEN })) void import('./reviewHost');
40
+ ```
41
+
42
+ `shouldActivate` is the supported API for "should this page load show review?"; the sessionStorage format behind the sticky session is private and may change. Its only side effect: `?review=off` clears the sticky opt-in.
43
+
24
44
  ### Props
25
45
 
26
46
  | Prop | Type | Description |
27
47
  | --- | --- | --- |
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. |
48
+ | `token` | `string` | **Required for any activation.** The layer turns on only via `?review=<token>` (or the per-tab session such a link starts). Without it nothing activates, whatever the URL says. |
49
+ | `enabled` | `boolean` | Master switch, default `true`. `false` renders nothing, installs nothing. Lets you write `enabled={Boolean(TOKEN)}` instead of `if (!TOKEN) return null`. |
50
+ | `devAutoOn` | `boolean` | Force the layer on for this page load, no link needed. Host-controlled dev convenience (`devAutoOn={import.meta.env.DEV}`); the library never reads `NODE_ENV`. Still subject to `enabled`, `allowedHosts` and `?review=off`. |
51
+ | `allowedHosts` | `string[]` | Hostname allowlist with `*` wildcards (`['localhost', '*.staging.example']`). Outside it nothing activates, token or not. Unset = any host (with a one-time console warning on URL activation on a public host). |
52
+ | `syncUrl` | `string` | WebSocket endpoint of your sync server (`realtime/server`), e.g. `wss://your-site.tld/rk-sync`. **No default.** |
53
+ | `syncToken` | `string` | Shared secret of the sync server. Live collaboration (presence, cursors, shared items) runs only when **both** `syncUrl` and `syncToken` are set; otherwise solo mode, no network. Public in the bundle by nature — a gate against casual visitors, not a secret. |
54
+ | `emblema` | `{ repoPath?: string; port?: number }` | Enables the Emblema bridge (`emblema={{}}` is enough). See "Pairing with Emblema". |
30
55
  | `defaultAuthor` | `string` | Pre-fills the reviewer name. |
56
+ | `fxAssetsUrl` | `string` | Base URL for the `/` effect soundtracks (`<base>/rick.mp3` for `/ondro`). Default: the public ReviewKit assets host `https://2-59-219-26.sslip.io/rk-assets`. Override to self-host. Needs `media-src` in CSP. |
31
57
  | `position` | `'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right'` | Initial corner of the floating badge (default `bottom-right`; draggable, persisted). |
32
58
  | `storageKey` | `string` | Persistence key (default: page origin). |
33
59
  | `onSessionChange` | `(session: ReviewSession) => void` | Called (debounced) whenever the session changes. |
34
60
 
35
61
  ## How the layer activates
36
62
 
37
- **With a `token` configured (the production setup) the rules are strict:**
63
+ Rules, in order; the first that applies wins. No environment sniffing anywhere.
64
+
65
+ | # | Condition | Result |
66
+ | --- | --- | --- |
67
+ | 1 | `enabled={false}` | off |
68
+ | 2 | hostname not in `allowedHosts` (when given) | off |
69
+ | 3 | `?review=off` in the URL | off, and the in-tab sticky session is cleared |
70
+ | 4 | `devAutoOn` | **on** |
71
+ | 5 | no `token` configured | off (`?review=x` logs a one-time warning and stays off) |
72
+ | 6 | `?review=<token>` matches | **on**, starts the per-tab sticky session |
73
+ | 7 | `?review=<something else>` | off |
74
+ | 8 | valid sticky session (same token, < 4 h since last active load) | **on**, expiry renewed |
75
+ | 9 | otherwise | off |
76
+
77
+ Activation-state matrix for the common cases:
78
+
79
+ | token | build | URL | sticky | result |
80
+ | --- | --- | --- | --- | --- |
81
+ | unset | dev or prod | none / `?review=x` | any | off |
82
+ | unset | dev, with `devAutoOn` | any except `off` | any | on |
83
+ | set | prod | `?review=<token>` | — | on (sticky starts) |
84
+ | set | prod | none | valid | on |
85
+ | set | prod | none | none / expired / other token | off |
86
+ | set | prod | `?review=wrong` | valid | off |
87
+ | set | any | `?review=off` | valid | off, sticky cleared |
88
+
89
+ The sticky session is per tab (`sessionStorage`), bound to the token and renewed on every active load, so a reviewer who opened a review link keeps reviewing across full page loads on other routes. The gear menu's **Keep review on in this tab** toggles it explicitly; **End review** (or `?review=off`) clears everything and returns the clean site. Leftover keys from pre-0.1.3 versions (`review-kit:active`, `reviewkit`) are removed on load and never consulted.
90
+
91
+ ### Production recipe
92
+
93
+ ```tsx
94
+ <ReviewKit
95
+ enabled={Boolean(TOKEN)}
96
+ token={TOKEN}
97
+ allowedHosts={['staging.your-site.tld', '*.preview.your-site.tld']}
98
+ syncUrl="wss://your-site.tld/rk-sync"
99
+ syncToken={SYNC_TOKEN}
100
+ />
101
+ ```
102
+
103
+ `token` + `allowedHosts` together: the token says *who*, the allowlist says *where*. Without `allowedHosts` a URL activation on a non-loopback host logs one console warning. `TOKEN`/`SYNC_TOKEN` are public in the bundle by design; use the `/lazy` or `/next` entry so they at least live in the review chunk, not the main one.
38
104
 
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.
105
+ ### CSP
41
106
 
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.
107
+ If the host sends a `Content-Security-Policy`, `connect-src` must allow:
43
108
 
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'`).
109
+ - the sync server origin, as `wss://your-site.tld` (or the `ws:` origin in dev),
110
+ - `http://127.0.0.1:*` when the `emblema` prop is set (intake ports 48752–48755, pairing agent 48770–48773).
111
+
112
+ and `media-src` must allow `https://2-59-219-26.sslip.io` (or your `fxAssetsUrl`) for the `/` effect sounds; without it the effects run silent.
113
+
114
+ With `connect-src 'self'` alone the WebSocket is blocked and the layer silently stays local-only. `examples/next-demo/next.config.ts` has a complete header.
45
115
 
46
116
  ## Using it (reviewer flow)
47
117
 
@@ -121,70 +191,64 @@ Programmatic helpers are exported too: `parseSessionJson` / `parseSession` (vali
121
191
  // src/main.tsx
122
192
  import { StrictMode } from 'react';
123
193
  import { createRoot } from 'react-dom/client';
124
- import { ReviewKit } from '@satanwagen/reviewkit/client';
194
+ import { ReviewKit } from '@satanwagen/reviewkit/lazy';
125
195
  import { App } from './App';
126
196
 
127
197
  createRoot(document.getElementById('root')!).render(
128
198
  <StrictMode>
129
199
  <App />
130
- <ReviewKit token={import.meta.env.VITE_REVIEW_TOKEN} />
200
+ <ReviewKit token={import.meta.env.VITE_REVIEW_TOKEN} devAutoOn={import.meta.env.DEV} />
131
201
  </StrictMode>,
132
202
  );
133
203
  ```
134
204
 
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>`.
205
+ No Vite config changes are needed — `reviewkit` ships prebuilt ESM/CJS with types. In `vite dev` the layer is on thanks to `devAutoOn`; in production reviewers open `https://your-site.tld/?review=<token>`, and visitors without the link never download the review chunk (the `/lazy` entry splits it automatically).
136
206
 
137
- Until the npm release lands you can also depend on it via a file path — this is exactly what the demo does:
207
+ Until the npm release lands you can also depend on it via a file path — this is exactly what the demos do:
138
208
 
139
209
  ```jsonc
140
210
  // package.json
141
211
  "dependencies": { "@satanwagen/reviewkit": "file:../path/to/ReviewKit" }
142
212
  ```
143
213
 
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/`.
214
+ npm symlinks the folder, so after editing reviewkit run `npm run build` in it and restart/reload Vite. With React 18 add `resolve: { dedupe: ['react', 'react-dom'] }` to `vite.config.ts` so the linked package's own `node_modules` cannot introduce a second React. Run `npm run build` in `ReviewKit/` at least once before installing, since the package resolves to `dist/`.
145
215
 
146
- ### Real-world example: token-only activation + lazy loading (Agropolio landing page)
216
+ ### Gating a hand-rolled lazy boundary
147
217
 
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):
218
+ If you keep your own wrapper (for example to keep `syncToken` in a chunk you control), gate it with `shouldActivate` instead of reading ReviewKit's storage keys:
149
219
 
150
- ```jsonc
151
- // Fable/package.json
152
- "dependencies": { "@satanwagen/reviewkit": "file:../../ReviewKit" }
153
- ```
220
+ ```tsx
221
+ // ReviewLayer.tsx
222
+ import { lazy, Suspense, useEffect, useState } from 'react';
223
+ import { shouldActivate } from '@satanwagen/reviewkit/activate';
154
224
 
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
- ```
225
+ const TOKEN = import.meta.env.VITE_REVIEW_TOKEN;
226
+ const ReviewHost = lazy(() => import('./reviewHost')); // renders <ReviewKit token={TOKEN} syncUrl=… syncToken=… />
160
227
 
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 }
228
+ export function ReviewLayer() {
229
+ const [load, setLoad] = useState(false);
230
+ useEffect(() => { if (shouldActivate({ token: TOKEN })) setLoad(true); }, []);
231
+ return load ? <Suspense fallback={null}><ReviewHost /></Suspense> : null;
175
232
  }
233
+ ```
176
234
 
177
- const ReviewKit = lazy(() => import('@satanwagen/reviewkit/client').then((m) => ({ default: m.ReviewKit })))
235
+ ## Integrating into a Next.js site (App Router)
178
236
 
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>
237
+ ```tsx
238
+ // app/layout.tsx (a server component — that's fine)
239
+ import { ReviewKit } from '@satanwagen/reviewkit/next';
240
+
241
+ export default function RootLayout({ children }) {
242
+ return (
243
+ <html><body>
244
+ {children}
245
+ <ReviewKit token={process.env.NEXT_PUBLIC_REVIEW_TOKEN} devAutoOn={process.env.NODE_ENV === 'development'} />
246
+ </body></html>
247
+ );
184
248
  }
185
249
  ```
186
250
 
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.
251
+ The `/next` entry is a client component; it renders nothing on the server and evaluates the URL/sticky session after mount before importing the core. Add the CSP `connect-src` entries above if you send a policy. Full example: `examples/next-demo`.
188
252
 
189
253
  ## Adding ReviewKit to another site (with live collaboration)
190
254
 
@@ -203,19 +267,19 @@ Step by step, for any site on any hosting (nginx, IIS, Vercel, …).
203
267
 
204
268
  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
269
 
206
- **2. Integrate — two lines, done. Live collaboration included:**
270
+ **2. Integrate — two lines, done:**
207
271
 
208
272
  ```tsx
209
- import { ReviewKit } from '@satanwagen/reviewkit/client';
273
+ import { ReviewKit } from '@satanwagen/reviewkit/lazy';
210
274
 
211
- <ReviewKit token="my-site-token" syncToken="<the sync server's shared secret>" />
275
+ <ReviewKit token="my-site-token" syncUrl="wss://your-site.tld/rk-sync" syncToken="<the sync server's shared secret>" />
212
276
  ```
213
277
 
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.
278
+ Live collaboration (presence, live cursors, instantly shared items) runs only when **both** `syncUrl` and `syncToken` are setthere is no default server. 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 either prop to disable networking entirely.
215
279
 
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).
280
+ **What a shared 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. Run the server for your own sites only; 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
281
 
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:
282
+ **3. Run the sync server** — `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
283
 
220
284
  *nginx* (as deployed on satan):
221
285
 
@@ -233,7 +297,7 @@ That's the whole setup. `syncUrl` **defaults to the always-on ReviewKit sync ser
233
297
 
234
298
  With an `RK_SYNC_ORIGINS` allowlist active, rejected origins are logged (`[origin] rejected …`) and closed with code 4005; the client quietly stays local.
235
299
 
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).
300
+ **4. Token caveat:** don't reuse secrets that protect anything else, and keep tokens out of the main bundle (use the `/lazy` or `/next` entry). Add `allowedHosts` in production (see "Production recipe").
237
301
 
238
302
  **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
303
 
@@ -241,15 +305,17 @@ That's the whole setup. `syncUrl` **defaults to the always-on ReviewKit sync ser
241
305
 
242
306
  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
307
 
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.
308
+ Sync is automatic and realtime — no manual step. With `emblema={{}}` 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.
309
+
310
+ **Which repo gets edited** is Emblema's knowledge, not the website's: the intake advertises its `repoPath` in the `/intake/ping` answer, and the local agent has it in `.reviewkit/emblema-sync.json` (`init --repo`, default: the directory you ran it in). `emblema.repoPath` remains as an optional override only; an absolute path from a reviewer's laptop has no place in host code.
245
311
 
246
312
  **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
313
 
248
- **Set up and generate a key** (in the project repo):
314
+ **Set up and generate a key** (in the project repo; the CLI is its own package so host installs stay CLI-free):
249
315
 
250
316
  ```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
317
+ npx @satanwagen/reviewkit-cli init --origin https://your-site --sync-url wss://your-site/rk-sync --sync-token <secret>
318
+ npx @satanwagen/reviewkit-cli serve # local API + realtime bridge (bridge needs both sync flags)
253
319
  ```
254
320
 
255
321
  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)**.
@@ -257,22 +323,24 @@ Paste the printed key into Emblema. From the page UI the same lives in the panel
257
323
  **Rotate / revoke from the CLI:**
258
324
 
259
325
  ```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)
326
+ npx @satanwagen/reviewkit-cli key # print the current key
327
+ npx @satanwagen/reviewkit-cli rotate # issue a new secret — the old key stops working immediately
328
+ npx @satanwagen/reviewkit-cli revoke # disable the pairing entirely (rotate re-enables)
263
329
  ```
264
330
 
265
331
  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
332
 
267
- ## Demo
333
+ If the host sends a CSP, the bridge needs `connect-src http://127.0.0.1:*` (see "CSP").
334
+
335
+ ## Demos
268
336
 
269
337
  ```sh
270
338
  cd examples/vite-demo
271
339
  npm install
272
- npm run dev # http://localhost:3010 — dev build, so the layer is active immediately
340
+ npm run dev # http://localhost:3010 — devAutoOn keeps the layer on in dev
273
341
  ```
274
342
 
275
- Also try `http://localhost:3010/?review=demo` to exercise URL activation.
343
+ Also try `http://localhost:3010/?review=demo` to exercise URL activation. `examples/next-demo` is the same site on Next.js (App Router, `/next` entry, CSP headers) on port 3011.
276
344
 
277
345
  ## Development
278
346
 
@@ -281,4 +349,7 @@ npm install
281
349
  npm run typecheck
282
350
  npm run lint
283
351
  npm run build # tsup → dist/ (ESM + CJS + d.ts)
352
+ npm test # CLI tests + activation matrix (needs dist/)
284
353
  ```
354
+
355
+ See `CHANGELOG.md` for the 0.1.3 default changes and migration notes.
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/activate.ts
21
+ var activate_exports = {};
22
+ __export(activate_exports, {
23
+ STICKY_KEY: () => STICKY_KEY,
24
+ clearSticky: () => clearSticky,
25
+ hostAllowed: () => hostAllowed,
26
+ isLocalHost: () => isLocalHost,
27
+ isStickyEnabled: () => isStickyEnabled,
28
+ reviewParam: () => reviewParam,
29
+ setSticky: () => setSticky,
30
+ shouldActivate: () => shouldActivate
31
+ });
32
+ module.exports = __toCommonJS(activate_exports);
33
+ var STICKY_KEY = "review-kit:sticky";
34
+ var STICKY_TTL_MS = 4 * 60 * 60 * 1e3;
35
+ function safeRemove(storage, key) {
36
+ try {
37
+ storage().removeItem(key);
38
+ } catch {
39
+ }
40
+ }
41
+ function readSticky() {
42
+ try {
43
+ const raw = window.sessionStorage.getItem(STICKY_KEY);
44
+ if (!raw) return null;
45
+ const parsed = JSON.parse(raw);
46
+ if (typeof parsed === "object" && parsed !== null && typeof parsed.token === "string" && typeof parsed.until === "number") {
47
+ return parsed;
48
+ }
49
+ } catch {
50
+ }
51
+ return null;
52
+ }
53
+ function isStickyEnabled(token) {
54
+ if (token === void 0 || token === "") return false;
55
+ const sticky = readSticky();
56
+ if (!sticky) return false;
57
+ if (sticky.until < Date.now() || sticky.token !== token) {
58
+ safeRemove(() => window.sessionStorage, STICKY_KEY);
59
+ return false;
60
+ }
61
+ return true;
62
+ }
63
+ function setSticky(token, on) {
64
+ try {
65
+ if (on && token !== void 0 && token !== "") {
66
+ const value = { token, until: Date.now() + STICKY_TTL_MS };
67
+ window.sessionStorage.setItem(STICKY_KEY, JSON.stringify(value));
68
+ } else {
69
+ window.sessionStorage.removeItem(STICKY_KEY);
70
+ }
71
+ } catch {
72
+ }
73
+ }
74
+ function clearSticky() {
75
+ safeRemove(() => window.sessionStorage, STICKY_KEY);
76
+ }
77
+ function reviewParam() {
78
+ try {
79
+ return new URLSearchParams(window.location.search).get("review");
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+ function hostAllowed(allowedHosts) {
85
+ if (!allowedHosts || allowedHosts.length === 0) return true;
86
+ let host = "";
87
+ try {
88
+ host = window.location.hostname.toLowerCase();
89
+ } catch {
90
+ return false;
91
+ }
92
+ return allowedHosts.some((pattern) => {
93
+ const regex = new RegExp(
94
+ "^" + pattern.toLowerCase().split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$"
95
+ );
96
+ return regex.test(host);
97
+ });
98
+ }
99
+ function isLocalHost() {
100
+ try {
101
+ const host = window.location.hostname.toLowerCase();
102
+ return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host.endsWith(".localhost");
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+ var warnedNoToken = false;
108
+ var warnedMismatch = false;
109
+ function shouldActivate(options = {}) {
110
+ if (typeof window === "undefined") return false;
111
+ if (options.enabled === false) return false;
112
+ if (!hostAllowed(options.allowedHosts)) return false;
113
+ const param = reviewParam();
114
+ if (param === "off") {
115
+ clearSticky();
116
+ return false;
117
+ }
118
+ if (options.devAutoOn === true) return true;
119
+ const token = options.token;
120
+ if (token === void 0 || token === "") {
121
+ if (param !== null && param !== "" && !warnedNoToken) {
122
+ warnedNoToken = true;
123
+ console.warn(
124
+ "[review-kit] `?review` is present but no `token` is configured; the layer stays off. Pass `token` to <ReviewKit /> (or `devAutoOn` for local development)."
125
+ );
126
+ }
127
+ return false;
128
+ }
129
+ if (param !== null && param !== "") {
130
+ if (param === token) return true;
131
+ if (!warnedMismatch) {
132
+ warnedMismatch = true;
133
+ console.warn("[review-kit] `?review` token does not match the configured token; staying inactive.");
134
+ }
135
+ return false;
136
+ }
137
+ return isStickyEnabled(token);
138
+ }
139
+ // Annotate the CommonJS export names for ESM import in node:
140
+ 0 && (module.exports = {
141
+ STICKY_KEY,
142
+ clearSticky,
143
+ hostAllowed,
144
+ isLocalHost,
145
+ isStickyEnabled,
146
+ reviewParam,
147
+ setSticky,
148
+ shouldActivate
149
+ });
150
+ //# sourceMappingURL=activate.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/activate.ts"],"sourcesContent":["/**\n * `@satanwagen/reviewkit/activate` — the activation decision, React-free.\n *\n * Hosts that lazy-load the review layer need to know BEFORE importing any\n * React code whether this page load should show the layer at all. This\n * module is that decision, and the only supported way to read ReviewKit's\n * persisted opt-in state (the sessionStorage format is private and may\n * change; `shouldActivate` will not).\n *\n * Rules (no exceptions, no environment sniffing):\n *\n * - `enabled: false` → never.\n * - hostname ∉ `allowedHosts` → never (when an allowlist is given).\n * - `?review=off` → never; also clears the in-tab opt-in.\n * - no `token` configured → never. `?review=x` without a token logs\n * a warning and stays off.\n * - `?review=<token>` → yes.\n * - valid in-tab sticky session → yes (started by an earlier `?review=<token>`\n * load in this tab, bound to the token,\n * ~4 h sliding expiry).\n * - `devAutoOn: true` → yes, regardless of URL/token. The host\n * decides when (e.g. `import.meta.env.DEV`);\n * the library never reads NODE_ENV.\n */\n\nexport interface ActivationOptions {\n /** The review token. Without it the layer never activates (except `devAutoOn`). */\n token?: string;\n /** Master switch. `false` short-circuits everything. Default `true`. */\n enabled?: boolean;\n /** Hostname allowlist with `*` wildcards. Unset = any host. */\n allowedHosts?: string[];\n /** Force the layer on for this page load (dev convenience; host-controlled). */\n devAutoOn?: boolean;\n}\n\n/** @internal sessionStorage key of the per-tab opt-in. Format is private. */\nexport const STICKY_KEY = 'review-kit:sticky';\nconst STICKY_TTL_MS = 4 * 60 * 60 * 1000;\n\ninterface Sticky {\n token: string;\n until: number;\n}\n\nfunction safeRemove(storage: () => Storage, key: string): void {\n try {\n storage().removeItem(key);\n } catch {\n /* storage unavailable */\n }\n}\n\nfunction readSticky(): Sticky | null {\n try {\n const raw = window.sessionStorage.getItem(STICKY_KEY);\n if (!raw) return null;\n const parsed: unknown = JSON.parse(raw);\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n typeof (parsed as Sticky).token === 'string' &&\n typeof (parsed as Sticky).until === 'number'\n ) {\n return parsed as Sticky;\n }\n } catch {\n /* unreadable */\n }\n return null;\n}\n\n/** @internal Is the per-tab opt-in present, unexpired and bound to `token`? */\nexport function isStickyEnabled(token: string | undefined): boolean {\n if (token === undefined || token === '') return false;\n const sticky = readSticky();\n if (!sticky) return false;\n if (sticky.until < Date.now() || sticky.token !== token) {\n safeRemove(() => window.sessionStorage, STICKY_KEY);\n return false;\n }\n return true;\n}\n\n/** @internal Start / renew / end the per-tab opt-in. No-op without a token. */\nexport function setSticky(token: string | undefined, on: boolean): void {\n try {\n if (on && token !== undefined && token !== '') {\n const value: Sticky = { token, until: Date.now() + STICKY_TTL_MS };\n window.sessionStorage.setItem(STICKY_KEY, JSON.stringify(value));\n } else {\n window.sessionStorage.removeItem(STICKY_KEY);\n }\n } catch {\n /* storage unavailable — the choice just won't persist */\n }\n}\n\n/** @internal Clear the per-tab opt-in. */\nexport function clearSticky(): void {\n safeRemove(() => window.sessionStorage, STICKY_KEY);\n}\n\n/** @internal The raw `?review` value of the current URL, or null. */\nexport function reviewParam(): string | null {\n try {\n return new URLSearchParams(window.location.search).get('review');\n } catch {\n return null;\n }\n}\n\n/** Does the current hostname match one of the allowed patterns?\n * '*' matches any characters ('*.dev.example' → a.dev.example, a.b.dev.example). */\nexport function hostAllowed(allowedHosts: string[] | undefined): boolean {\n if (!allowedHosts || allowedHosts.length === 0) return true;\n let host = '';\n try {\n host = window.location.hostname.toLowerCase();\n } catch {\n return false;\n }\n return allowedHosts.some((pattern) => {\n const regex = new RegExp(\n '^' +\n pattern\n .toLowerCase()\n .split('*')\n .map((part) => part.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'))\n .join('.*') +\n '$',\n );\n return regex.test(host);\n });\n}\n\n/** Loopback hosts where a missing allowlist is expected, not a smell. */\nexport function isLocalHost(): boolean {\n try {\n const host = window.location.hostname.toLowerCase();\n return host === 'localhost' || host === '127.0.0.1' || host === '[::1]' || host.endsWith('.localhost');\n } catch {\n return false;\n }\n}\n\nlet warnedNoToken = false;\nlet warnedMismatch = false;\n\n/**\n * Should the review layer be active for this page load?\n *\n * Read-only except for one deliberate side effect: `?review=off` clears the\n * in-tab opt-in, so a host that gates a lazy import on this function still\n * honours \"off\" on the next load. Safe to call on the server (returns false).\n */\nexport function shouldActivate(options: ActivationOptions = {}): boolean {\n if (typeof window === 'undefined') return false;\n if (options.enabled === false) return false;\n if (!hostAllowed(options.allowedHosts)) return false;\n\n const param = reviewParam();\n if (param === 'off') {\n clearSticky();\n return false;\n }\n if (options.devAutoOn === true) return true;\n\n const token = options.token;\n if (token === undefined || token === '') {\n if (param !== null && param !== '' && !warnedNoToken) {\n warnedNoToken = true;\n console.warn(\n '[review-kit] `?review` is present but no `token` is configured; the layer stays off. ' +\n 'Pass `token` to <ReviewKit /> (or `devAutoOn` for local development).',\n );\n }\n return false;\n }\n if (param !== null && param !== '') {\n if (param === token) return true;\n if (!warnedMismatch) {\n warnedMismatch = true;\n console.warn('[review-kit] `?review` token does not match the configured token; staying inactive.');\n }\n return false;\n }\n return isStickyEnabled(token);\n}\n"],"mappings":"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","names":[]}