@satanwagen/reviewkit 0.1.2 → 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.
- package/CHANGELOG.md +105 -0
- package/README.md +134 -63
- package/dist/activate.cjs +150 -0
- package/dist/activate.cjs.map +1 -0
- package/dist/activate.d.cts +59 -0
- package/dist/activate.d.ts +59 -0
- package/dist/activate.js +21 -0
- package/dist/activate.js.map +1 -0
- package/dist/{chunk-4YNLMSCK.js → chunk-DG6O5XIC.js} +36 -10
- package/dist/chunk-DG6O5XIC.js.map +1 -0
- package/dist/chunk-ETAGGIDN.js +119 -0
- package/dist/chunk-ETAGGIDN.js.map +1 -0
- package/dist/{chunk-YWBFAV57.js → chunk-NSXRFM75.js} +853 -1544
- package/dist/chunk-NSXRFM75.js.map +1 -0
- package/dist/chunk-Q7U43PXE.js +36 -0
- package/dist/chunk-Q7U43PXE.js.map +1 -0
- package/dist/client/index.cjs +4530 -4237
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +49 -3
- package/dist/client/index.js.map +1 -1
- package/dist/effects-EWKHKUDP.js +851 -0
- package/dist/effects-EWKHKUDP.js.map +1 -0
- package/dist/index-BUSmAweg.d.ts +77 -0
- package/dist/index-Dl2Fl0qr.d.cts +77 -0
- package/dist/index.cjs +4544 -4256
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +35 -3
- package/dist/index.js.map +1 -1
- package/dist/lazy.cjs +8988 -0
- package/dist/lazy.cjs.map +1 -0
- package/dist/lazy.d.cts +7 -0
- package/dist/lazy.d.ts +7 -0
- package/dist/lazy.js +8 -0
- package/dist/lazy.js.map +1 -0
- package/dist/next.cjs +8989 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +4 -0
- package/dist/next.d.ts +4 -0
- package/dist/next.js +9 -0
- package/dist/next.js.map +1 -0
- package/dist/schema.cjs +53 -16
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.d.cts +20 -5
- package/dist/schema.d.ts +20 -5
- package/dist/schema.js +30 -2
- package/dist/schema.js.map +1 -1
- package/package.json +21 -8
- package/cli/emblema-sync.mjs +0 -675
- package/dist/chunk-4YNLMSCK.js.map +0 -1
- package/dist/chunk-YWBFAV57.js.map +0 -1
- package/dist/index-BbucFgZi.d.ts +0 -49
- package/dist/index-CBlJrKkm.d.cts +0 -49
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@satanwagen/reviewkit/activate` — the activation decision, React-free.
|
|
3
|
+
*
|
|
4
|
+
* Hosts that lazy-load the review layer need to know BEFORE importing any
|
|
5
|
+
* React code whether this page load should show the layer at all. This
|
|
6
|
+
* module is that decision, and the only supported way to read ReviewKit's
|
|
7
|
+
* persisted opt-in state (the sessionStorage format is private and may
|
|
8
|
+
* change; `shouldActivate` will not).
|
|
9
|
+
*
|
|
10
|
+
* Rules (no exceptions, no environment sniffing):
|
|
11
|
+
*
|
|
12
|
+
* - `enabled: false` → never.
|
|
13
|
+
* - hostname ∉ `allowedHosts` → never (when an allowlist is given).
|
|
14
|
+
* - `?review=off` → never; also clears the in-tab opt-in.
|
|
15
|
+
* - no `token` configured → never. `?review=x` without a token logs
|
|
16
|
+
* a warning and stays off.
|
|
17
|
+
* - `?review=<token>` → yes.
|
|
18
|
+
* - valid in-tab sticky session → yes (started by an earlier `?review=<token>`
|
|
19
|
+
* load in this tab, bound to the token,
|
|
20
|
+
* ~4 h sliding expiry).
|
|
21
|
+
* - `devAutoOn: true` → yes, regardless of URL/token. The host
|
|
22
|
+
* decides when (e.g. `import.meta.env.DEV`);
|
|
23
|
+
* the library never reads NODE_ENV.
|
|
24
|
+
*/
|
|
25
|
+
interface ActivationOptions {
|
|
26
|
+
/** The review token. Without it the layer never activates (except `devAutoOn`). */
|
|
27
|
+
token?: string;
|
|
28
|
+
/** Master switch. `false` short-circuits everything. Default `true`. */
|
|
29
|
+
enabled?: boolean;
|
|
30
|
+
/** Hostname allowlist with `*` wildcards. Unset = any host. */
|
|
31
|
+
allowedHosts?: string[];
|
|
32
|
+
/** Force the layer on for this page load (dev convenience; host-controlled). */
|
|
33
|
+
devAutoOn?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** @internal sessionStorage key of the per-tab opt-in. Format is private. */
|
|
36
|
+
declare const STICKY_KEY = "review-kit:sticky";
|
|
37
|
+
/** @internal Is the per-tab opt-in present, unexpired and bound to `token`? */
|
|
38
|
+
declare function isStickyEnabled(token: string | undefined): boolean;
|
|
39
|
+
/** @internal Start / renew / end the per-tab opt-in. No-op without a token. */
|
|
40
|
+
declare function setSticky(token: string | undefined, on: boolean): void;
|
|
41
|
+
/** @internal Clear the per-tab opt-in. */
|
|
42
|
+
declare function clearSticky(): void;
|
|
43
|
+
/** @internal The raw `?review` value of the current URL, or null. */
|
|
44
|
+
declare function reviewParam(): string | null;
|
|
45
|
+
/** Does the current hostname match one of the allowed patterns?
|
|
46
|
+
* '*' matches any characters ('*.dev.example' → a.dev.example, a.b.dev.example). */
|
|
47
|
+
declare function hostAllowed(allowedHosts: string[] | undefined): boolean;
|
|
48
|
+
/** Loopback hosts where a missing allowlist is expected, not a smell. */
|
|
49
|
+
declare function isLocalHost(): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Should the review layer be active for this page load?
|
|
52
|
+
*
|
|
53
|
+
* Read-only except for one deliberate side effect: `?review=off` clears the
|
|
54
|
+
* in-tab opt-in, so a host that gates a lazy import on this function still
|
|
55
|
+
* honours "off" on the next load. Safe to call on the server (returns false).
|
|
56
|
+
*/
|
|
57
|
+
declare function shouldActivate(options?: ActivationOptions): boolean;
|
|
58
|
+
|
|
59
|
+
export { type ActivationOptions, STICKY_KEY, clearSticky, hostAllowed, isLocalHost, isStickyEnabled, reviewParam, setSticky, shouldActivate };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@satanwagen/reviewkit/activate` — the activation decision, React-free.
|
|
3
|
+
*
|
|
4
|
+
* Hosts that lazy-load the review layer need to know BEFORE importing any
|
|
5
|
+
* React code whether this page load should show the layer at all. This
|
|
6
|
+
* module is that decision, and the only supported way to read ReviewKit's
|
|
7
|
+
* persisted opt-in state (the sessionStorage format is private and may
|
|
8
|
+
* change; `shouldActivate` will not).
|
|
9
|
+
*
|
|
10
|
+
* Rules (no exceptions, no environment sniffing):
|
|
11
|
+
*
|
|
12
|
+
* - `enabled: false` → never.
|
|
13
|
+
* - hostname ∉ `allowedHosts` → never (when an allowlist is given).
|
|
14
|
+
* - `?review=off` → never; also clears the in-tab opt-in.
|
|
15
|
+
* - no `token` configured → never. `?review=x` without a token logs
|
|
16
|
+
* a warning and stays off.
|
|
17
|
+
* - `?review=<token>` → yes.
|
|
18
|
+
* - valid in-tab sticky session → yes (started by an earlier `?review=<token>`
|
|
19
|
+
* load in this tab, bound to the token,
|
|
20
|
+
* ~4 h sliding expiry).
|
|
21
|
+
* - `devAutoOn: true` → yes, regardless of URL/token. The host
|
|
22
|
+
* decides when (e.g. `import.meta.env.DEV`);
|
|
23
|
+
* the library never reads NODE_ENV.
|
|
24
|
+
*/
|
|
25
|
+
interface ActivationOptions {
|
|
26
|
+
/** The review token. Without it the layer never activates (except `devAutoOn`). */
|
|
27
|
+
token?: string;
|
|
28
|
+
/** Master switch. `false` short-circuits everything. Default `true`. */
|
|
29
|
+
enabled?: boolean;
|
|
30
|
+
/** Hostname allowlist with `*` wildcards. Unset = any host. */
|
|
31
|
+
allowedHosts?: string[];
|
|
32
|
+
/** Force the layer on for this page load (dev convenience; host-controlled). */
|
|
33
|
+
devAutoOn?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** @internal sessionStorage key of the per-tab opt-in. Format is private. */
|
|
36
|
+
declare const STICKY_KEY = "review-kit:sticky";
|
|
37
|
+
/** @internal Is the per-tab opt-in present, unexpired and bound to `token`? */
|
|
38
|
+
declare function isStickyEnabled(token: string | undefined): boolean;
|
|
39
|
+
/** @internal Start / renew / end the per-tab opt-in. No-op without a token. */
|
|
40
|
+
declare function setSticky(token: string | undefined, on: boolean): void;
|
|
41
|
+
/** @internal Clear the per-tab opt-in. */
|
|
42
|
+
declare function clearSticky(): void;
|
|
43
|
+
/** @internal The raw `?review` value of the current URL, or null. */
|
|
44
|
+
declare function reviewParam(): string | null;
|
|
45
|
+
/** Does the current hostname match one of the allowed patterns?
|
|
46
|
+
* '*' matches any characters ('*.dev.example' → a.dev.example, a.b.dev.example). */
|
|
47
|
+
declare function hostAllowed(allowedHosts: string[] | undefined): boolean;
|
|
48
|
+
/** Loopback hosts where a missing allowlist is expected, not a smell. */
|
|
49
|
+
declare function isLocalHost(): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Should the review layer be active for this page load?
|
|
52
|
+
*
|
|
53
|
+
* Read-only except for one deliberate side effect: `?review=off` clears the
|
|
54
|
+
* in-tab opt-in, so a host that gates a lazy import on this function still
|
|
55
|
+
* honours "off" on the next load. Safe to call on the server (returns false).
|
|
56
|
+
*/
|
|
57
|
+
declare function shouldActivate(options?: ActivationOptions): boolean;
|
|
58
|
+
|
|
59
|
+
export { type ActivationOptions, STICKY_KEY, clearSticky, hostAllowed, isLocalHost, isStickyEnabled, reviewParam, setSticky, shouldActivate };
|
package/dist/activate.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
STICKY_KEY,
|
|
3
|
+
clearSticky,
|
|
4
|
+
hostAllowed,
|
|
5
|
+
isLocalHost,
|
|
6
|
+
isStickyEnabled,
|
|
7
|
+
reviewParam,
|
|
8
|
+
setSticky,
|
|
9
|
+
shouldActivate
|
|
10
|
+
} from "./chunk-ETAGGIDN.js";
|
|
11
|
+
export {
|
|
12
|
+
STICKY_KEY,
|
|
13
|
+
clearSticky,
|
|
14
|
+
hostAllowed,
|
|
15
|
+
isLocalHost,
|
|
16
|
+
isStickyEnabled,
|
|
17
|
+
reviewParam,
|
|
18
|
+
setSticky,
|
|
19
|
+
shouldActivate
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=activate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -18,14 +18,15 @@ function orderForDispatch(items) {
|
|
|
18
18
|
return a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0;
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
|
-
function buildDispatch(session, items, config) {
|
|
21
|
+
function buildDispatch(session, items, config, advertisedRepoPath) {
|
|
22
22
|
const origin = window.location.origin;
|
|
23
|
+
const repoPath = config.repoPath ?? advertisedRepoPath;
|
|
23
24
|
return {
|
|
24
25
|
v: REVIEW_DISPATCH_VERSION,
|
|
25
26
|
source: "review-kit",
|
|
26
27
|
batchId: batchId(),
|
|
27
28
|
sentAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
28
|
-
repoPath:
|
|
29
|
+
...repoPath ? { repoPath } : {},
|
|
29
30
|
site: {
|
|
30
31
|
origin,
|
|
31
32
|
reviewUrl: window.location.href
|
|
@@ -43,25 +44,31 @@ async function fetchJson(url, init, timeoutMs) {
|
|
|
43
44
|
window.clearTimeout(timer);
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
|
-
async function
|
|
47
|
+
async function probeEmblema(config) {
|
|
47
48
|
for (const port of portsFor(config)) {
|
|
48
49
|
try {
|
|
49
50
|
const res = await fetchJson(`http://127.0.0.1:${port}/intake/ping`, { method: "GET" }, 1200);
|
|
50
51
|
if (!res.ok) continue;
|
|
51
52
|
const body = await res.json();
|
|
52
53
|
if (typeof body === "object" && body !== null && body.app === "emblema") {
|
|
53
|
-
|
|
54
|
+
const repoPath = body.repoPath;
|
|
55
|
+
return typeof repoPath === "string" && repoPath !== "" ? { port, repoPath } : { port };
|
|
54
56
|
}
|
|
55
57
|
} catch {
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
return null;
|
|
59
61
|
}
|
|
62
|
+
async function pingEmblema(config) {
|
|
63
|
+
const probe = await probeEmblema(config);
|
|
64
|
+
return probe?.port ?? null;
|
|
65
|
+
}
|
|
60
66
|
async function sendToEmblema(session, items, config) {
|
|
61
67
|
if (items.length === 0) return { ok: false, reason: "no-items" };
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
const
|
|
68
|
+
const probe = await probeEmblema(config);
|
|
69
|
+
if (probe === null) return { ok: false, reason: "offline" };
|
|
70
|
+
const { port } = probe;
|
|
71
|
+
const payload = buildDispatch(session, items, config, probe.repoPath);
|
|
65
72
|
try {
|
|
66
73
|
const res = await fetchJson(
|
|
67
74
|
`http://127.0.0.1:${port}/intake/review`,
|
|
@@ -229,6 +236,25 @@ function parseSessionJson(json) {
|
|
|
229
236
|
return parseSession(value);
|
|
230
237
|
}
|
|
231
238
|
|
|
232
|
-
export {
|
|
233
|
-
|
|
234
|
-
|
|
239
|
+
export {
|
|
240
|
+
REVIEW_DISPATCH_VERSION,
|
|
241
|
+
pingEmblema,
|
|
242
|
+
sendToEmblema,
|
|
243
|
+
fetchSyncState,
|
|
244
|
+
generateSyncKey,
|
|
245
|
+
fetchSyncKey,
|
|
246
|
+
revokeSyncKey,
|
|
247
|
+
SCHEMA_VERSION,
|
|
248
|
+
ITEM_STATUSES,
|
|
249
|
+
KNOWN_ITEM_TYPES,
|
|
250
|
+
isKnownItemType,
|
|
251
|
+
isCopyItem,
|
|
252
|
+
isImageItem,
|
|
253
|
+
isCommentItem,
|
|
254
|
+
isDeleteItem,
|
|
255
|
+
isMoveItem,
|
|
256
|
+
isStyleItem,
|
|
257
|
+
parseSession,
|
|
258
|
+
parseSessionJson
|
|
259
|
+
};
|
|
260
|
+
//# sourceMappingURL=chunk-DG6O5XIC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/integrations/emblema.ts","../src/schema.ts"],"sourcesContent":["/**\n * ReviewKit → Emblema dispatch (Workstream B of INTEGRATION-EMBLEMA.md).\n *\n * This file is the AUTHORITATIVE definition of the EmblemaReviewDispatch v1\n * contract — Emblema vendors a copy of the type (its\n * src/lib/review-intake-contract.ts points back here). Do not rename or add\n * payload fields without bumping REVIEW_DISPATCH_VERSION; the receiving end\n * rejects any other `v`.\n *\n * Transport per the spec: GET /intake/ping to find a live app, then one\n * POST /intake/review with the whole batch. One-way in v1 — Emblema shows an\n * approval card; nothing flows back to the page. No queueing: if the app is\n * not running the caller shows a toast and gives up.\n */\n\nimport type { ReviewItem, ReviewSession } from '../schema';\n\nexport const REVIEW_DISPATCH_VERSION = 1;\n\n/** Fixed intake ports, in probe order (spec §3.1: 48752, fallbacks if busy). */\nexport const EMBLEMA_INTAKE_PORTS = [48752, 48753, 48754, 48755] as const;\n\n/**\n * ReviewKitConfig.emblema — enables the Emblema bridge. `{}` is a valid\n * value: the repo Emblema should edit is Emblema's own local knowledge (it\n * knows which project it opened; the `reviewkit-emblema` agent has it in\n * `.reviewkit/emblema-sync.json`), so an absolute path from the reviewer's\n * machine never has to appear in the host site's code.\n */\nexport interface EmblemaConfig {\n /**\n * Override the repo path sent with each batch. Normally omitted: the\n * intake advertises its own `repoPath` in the `/intake/ping` answer, and\n * when it does not, the field is left out and Emblema falls back to its\n * active project.\n */\n repoPath?: string;\n /** Pin the intake to one port instead of probing the default list. */\n port?: number;\n}\n\n/** The v1 payload of POST /intake/review — field names are frozen. */\nexport interface EmblemaReviewDispatch {\n v: typeof REVIEW_DISPATCH_VERSION;\n source: 'review-kit';\n batchId: string;\n /** ISO 8601 */\n sentAt: string;\n /**\n * Absolute repo path — `emblema.repoPath` if configured, else the path the\n * intake advertised on ping. Omitted when neither is known; the receiver\n * then uses its active project. (Optional since 0.1.3; still `v: 1`.)\n */\n repoPath?: string;\n site: {\n origin: string;\n reviewUrl: string;\n };\n sessionAuthor: string;\n /** 1..200 verbatim ReviewItems (spec sends only actionable statuses). */\n items: ReviewItem[];\n}\n\nexport type SendResult =\n | { ok: true; batchId: string; sent: number; port: number }\n | { ok: false; reason: 'offline' | 'rejected' | 'no-items'; detail?: string };\n\n/** Max items per batch (spec §3.2). */\nexport const MAX_DISPATCH_ITEMS = 200;\n\nfunction batchId(): string {\n return `rk-batch-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction portsFor(config: EmblemaConfig): number[] {\n return config.port !== undefined ? [config.port] : [...EMBLEMA_INTAKE_PORTS];\n}\n\n/**\n * Order items the way Emblema's route-grouping consumes them (spec §3.3):\n * grouped by route, `must` before `nice` inside a group, then oldest first.\n * The payload stays a flat array — grouping itself happens on the Emblema side.\n */\nexport function orderForDispatch(items: ReviewItem[]): ReviewItem[] {\n const routeOrder: string[] = [];\n for (const item of items) if (!routeOrder.includes(item.route)) routeOrder.push(item.route);\n return [...items].sort((a, b) => {\n const byRoute = routeOrder.indexOf(a.route) - routeOrder.indexOf(b.route);\n if (byRoute !== 0) return byRoute;\n if (a.priority !== b.priority) return a.priority === 'must' ? -1 : 1;\n return a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0;\n });\n}\n\n/** Build the v1 payload. Caller pre-filters items to the statuses it wants sent. */\nexport function buildDispatch(\n session: ReviewSession | null,\n items: ReviewItem[],\n config: EmblemaConfig,\n advertisedRepoPath?: string,\n): EmblemaReviewDispatch {\n const origin = window.location.origin;\n const repoPath = config.repoPath ?? advertisedRepoPath;\n return {\n v: REVIEW_DISPATCH_VERSION,\n source: 'review-kit',\n batchId: batchId(),\n sentAt: new Date().toISOString(),\n ...(repoPath ? { repoPath } : {}),\n site: {\n origin,\n reviewUrl: window.location.href,\n },\n sessionAuthor: session?.author ?? '',\n items: orderForDispatch(items).slice(0, MAX_DISPATCH_ITEMS),\n };\n}\n\nasync function fetchJson(url: string, init: RequestInit, timeoutMs: number): Promise<Response> {\n const abort = new AbortController();\n const timer = window.setTimeout(() => abort.abort(), timeoutMs);\n try {\n return await fetch(url, { ...init, signal: abort.signal });\n } finally {\n window.clearTimeout(timer);\n }\n}\n\nexport interface EmblemaProbe {\n port: number;\n /** Repo the intake is bound to, when the ping answer advertises one. */\n repoPath?: string;\n}\n\n/**\n * Find a live intake: first port whose /intake/ping answers as Emblema.\n * The answer may carry `repoPath` (the project Emblema has open for this\n * pairing); it becomes the batch's `repoPath` unless the config overrides it.\n */\nexport async function probeEmblema(config: EmblemaConfig): Promise<EmblemaProbe | null> {\n for (const port of portsFor(config)) {\n try {\n const res = await fetchJson(`http://127.0.0.1:${port}/intake/ping`, { method: 'GET' }, 1200);\n if (!res.ok) continue;\n const body: unknown = await res.json();\n if (typeof body === 'object' && body !== null && (body as { app?: unknown }).app === 'emblema') {\n const repoPath = (body as { repoPath?: unknown }).repoPath;\n return typeof repoPath === 'string' && repoPath !== '' ? { port, repoPath } : { port };\n }\n } catch {\n /* connection refused / timeout — try the next port */\n }\n }\n return null;\n}\n\n/** Port of a live intake, or null. Thin wrapper over `probeEmblema`. */\nexport async function pingEmblema(config: EmblemaConfig): Promise<number | null> {\n const probe = await probeEmblema(config);\n return probe?.port ?? null;\n}\n\n/** Ping, then POST the batch. Never queues; never retries. */\nexport async function sendToEmblema(\n session: ReviewSession | null,\n items: ReviewItem[],\n config: EmblemaConfig,\n): Promise<SendResult> {\n if (items.length === 0) return { ok: false, reason: 'no-items' };\n const probe = await probeEmblema(config);\n if (probe === null) return { ok: false, reason: 'offline' };\n const { port } = probe;\n const payload = buildDispatch(session, items, config, probe.repoPath);\n try {\n const res = await fetchJson(\n `http://127.0.0.1:${port}/intake/review`,\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n 10_000,\n );\n if (!res.ok) return { ok: false, reason: 'rejected', detail: `HTTP ${res.status}` };\n const body: unknown = await res.json().catch(() => null);\n const returnedId =\n typeof body === 'object' && body !== null && typeof (body as { batchId?: unknown }).batchId === 'string'\n ? ((body as { batchId: string }).batchId)\n : payload.batchId;\n return { ok: true, batchId: returnedId, sent: payload.items.length, port };\n } catch {\n return { ok: false, reason: 'offline' };\n }\n}\n\n/* ---------------- pairing (contract v2) ---------------- */\n\n/** Local sync agent (reviewkit-emblema serve) management API ports. */\nexport const EMBLEMA_SYNC_PORTS = [48770, 48771, 48772, 48773] as const;\n\nexport interface EmblemaSyncState {\n ok: true;\n paired: boolean;\n revoked: boolean;\n id: string;\n name: string;\n url: string;\n lastAck: string | null;\n findings: number;\n}\n\nasync function localFetch(path: string, init?: RequestInit): Promise<Response | null> {\n for (const port of EMBLEMA_SYNC_PORTS) {\n try {\n const abort = new AbortController();\n const timer = window.setTimeout(() => abort.abort(), 1200);\n const res = await fetch(`http://127.0.0.1:${port}${path}`, { ...init, signal: abort.signal });\n window.clearTimeout(timer);\n return res;\n } catch {\n /* next port */\n }\n }\n return null;\n}\n\n/** State of the local pairing agent; null when it isn't running. */\nexport async function fetchSyncState(): Promise<EmblemaSyncState | null> {\n const res = await localFetch('/api/emblema/local/state');\n if (!res || !res.ok) return null;\n const body: unknown = await res.json().catch(() => null);\n return typeof body === 'object' && body !== null && (body as { ok?: unknown }).ok === true\n ? (body as EmblemaSyncState)\n : null;\n}\n\n/** Generate (or rotate — same operation) the pairing key; returns emk_… */\nexport async function generateSyncKey(): Promise<string | null> {\n const res = await localFetch('/api/emblema/local/generate', { method: 'POST' });\n if (!res || !res.ok) return null;\n const body: unknown = await res.json().catch(() => null);\n const key = (body as { key?: unknown } | null)?.key;\n return typeof key === 'string' ? key : null;\n}\n\n/** Current key without rotating (Copy button). */\nexport async function fetchSyncKey(): Promise<string | null> {\n const res = await localFetch('/api/emblema/local/key');\n if (!res || !res.ok) return null;\n const body: unknown = await res.json().catch(() => null);\n const key = (body as { key?: unknown } | null)?.key;\n return typeof key === 'string' ? key : null;\n}\n\nexport async function revokeSyncKey(): Promise<boolean> {\n const res = await localFetch('/api/emblema/local/revoke', { method: 'POST' });\n return res !== null && res.ok;\n}\n","/**\n * review-kit session schema — the contract everything else depends on.\n *\n * Forward compatibility rules:\n * - Every object carries an index signature, so unknown fields parsed from a\n * file are kept in memory and survive a load → save round-trip.\n * - Item `type` is open-ended: unknown types are preserved as\n * {@link UnknownReviewItem} and never dropped.\n *\n * Kept in sync with `schema/review-session.schema.json`.\n */\n\nexport const SCHEMA_VERSION = 1;\n\nexport type ItemStatus = 'open' | 'accepted' | 'rejected' | 'applied';\nexport type ItemPriority = 'must' | 'nice';\nexport type KnownItemType = 'copy' | 'image' | 'comment' | 'delete' | 'move' | 'style';\n\nexport const ITEM_STATUSES: readonly ItemStatus[] = ['open', 'accepted', 'rejected', 'applied'];\nexport const KNOWN_ITEM_TYPES: readonly KnownItemType[] = [\n 'copy',\n 'image',\n 'comment',\n 'delete',\n 'move',\n 'style',\n];\n\n/** Extra fields from future schema versions are preserved, never dropped. */\nexport interface Extensible {\n [extra: string]: unknown;\n}\n\nexport interface BoundingBox extends Extensible {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface TargetAttrs extends Extensible {\n id?: string;\n classList?: string[];\n alt?: string;\n src?: string;\n href?: string;\n ariaLabel?: string;\n /** All `data-*` attributes, keyed without the `data-` prefix. */\n data?: Record<string, string>;\n}\n\n/**\n * Everything needed to find the element again — by the plugin (to re-pin it)\n * and by a coding agent (to locate it in source).\n */\nexport interface ReviewTarget extends Extensible {\n /** Stable CSS selector path (id-anchored where possible, no hashed classes). */\n selector: string;\n tagName: string;\n /** Trimmed, whitespace-collapsed text content, first ~120 chars. */\n textSnippet: string;\n attrs: TargetAttrs;\n /** nth-of-type index chain from <body> down to the element (1-based). */\n nthOfType: number[];\n /** Viewport-relative bounding box at capture time. */\n boundingBox: BoundingBox;\n /** Reserved for phase 4 (source mapping). */\n sourceFile?: string;\n sourceLine?: number;\n /** Fractional point inside the element's box (0..1) the item refers to. */\n anchor?: { ox: number; oy: number } & Extensible;\n}\n\nexport interface CopyPayload extends Extensible {\n before: string;\n after: string;\n}\n\nexport interface ImagePayload extends Extensible {\n beforeSrc: string;\n /**\n * Replacement image by reference — an external URL that couldn't be\n * embedded (CORS) or exceeded the embed cap. Exclusive with afterDataUrl.\n */\n afterSrc?: string;\n /** Replacement image embedded inline as a data: URL (2 MB cap at capture). */\n afterDataUrl?: string;\n description?: string;\n}\n\nexport interface CommentPayload extends Extensible {\n text: string;\n}\n\nexport interface DeletePayload extends Extensible {\n reason?: string;\n}\n\nexport interface MovePayload extends Extensible {\n direction?: 'up' | 'down' | 'first' | 'last';\n targetDescription?: string;\n}\n\nexport interface StylePayload extends Extensible {\n description: string;\n}\n\nexport interface ReviewItemBase extends Extensible {\n id: string;\n /** ISO 8601 */\n createdAt: string;\n /** Who created the item (additive; older sessions may lack it). */\n author?: { id: string; name: string; color?: string } & Extensible;\n type: string;\n status: ItemStatus;\n /** Pathname where the item was captured. */\n route: string;\n target: ReviewTarget;\n /** Optional free text from the reviewer, in addition to the payload. */\n note?: string;\n priority: ItemPriority;\n}\n\nexport interface CopyItem extends ReviewItemBase {\n type: 'copy';\n payload: CopyPayload;\n}\nexport interface ImageItem extends ReviewItemBase {\n type: 'image';\n payload: ImagePayload;\n}\nexport interface CommentItem extends ReviewItemBase {\n type: 'comment';\n payload: CommentPayload;\n}\nexport interface DeleteItem extends ReviewItemBase {\n type: 'delete';\n payload: DeletePayload;\n}\nexport interface MoveItem extends ReviewItemBase {\n type: 'move';\n payload: MovePayload;\n}\nexport interface StyleItem extends ReviewItemBase {\n type: 'style';\n payload: StylePayload;\n}\n\nexport type KnownReviewItem = CopyItem | ImageItem | CommentItem | DeleteItem | MoveItem | StyleItem;\n\n/** An item of a type this build doesn't know about — carried through untouched. */\nexport interface UnknownReviewItem extends ReviewItemBase {\n payload?: unknown;\n}\n\nexport type ReviewItem = KnownReviewItem | UnknownReviewItem;\n\nexport interface SiteInfo extends Extensible {\n origin: string;\n /** Pathname of the route the session was started on. */\n route: string;\n viewport: { width: number; height: number } & Extensible;\n userAgent: string;\n}\n\nexport interface ReviewSession extends Extensible {\n schemaVersion: number;\n id: string;\n /** ISO 8601 */\n createdAt: string;\n /** ISO 8601 */\n updatedAt: string;\n site: SiteInfo;\n /** Free-text reviewer name, typed once. */\n author: string;\n items: ReviewItem[];\n}\n\n/* ------------------------------------------------------------------ */\n/* Type guards */\n/* ------------------------------------------------------------------ */\n\nexport function isKnownItemType(type: string): type is KnownItemType {\n return (KNOWN_ITEM_TYPES as readonly string[]).includes(type);\n}\n\nexport function isCopyItem(item: ReviewItem): item is CopyItem {\n return item.type === 'copy';\n}\nexport function isImageItem(item: ReviewItem): item is ImageItem {\n return item.type === 'image';\n}\nexport function isCommentItem(item: ReviewItem): item is CommentItem {\n return item.type === 'comment';\n}\nexport function isDeleteItem(item: ReviewItem): item is DeleteItem {\n return item.type === 'delete';\n}\nexport function isMoveItem(item: ReviewItem): item is MoveItem {\n return item.type === 'move';\n}\nexport function isStyleItem(item: ReviewItem): item is StyleItem {\n return item.type === 'style';\n}\n\n/* ------------------------------------------------------------------ */\n/* Validation */\n/* ------------------------------------------------------------------ */\n\nexport type ParseResult =\n | { ok: true; session: ReviewSession }\n | { ok: false; error: string };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction fail(error: string): ParseResult {\n return { ok: false, error };\n}\n\n/**\n * Validate an unknown value (already JSON-parsed) into a ReviewSession.\n * Tolerant of unknown fields and unknown item types (forward compatibility);\n * strict about the structural minimum each item needs to be useful.\n * Missing `status` defaults to 'open', missing `priority` to 'nice'.\n */\nexport function parseSession(value: unknown): ParseResult {\n if (!isRecord(value)) return fail('Not a JSON object.');\n if (typeof value.schemaVersion !== 'number') {\n return fail('Missing or invalid \"schemaVersion\" (expected a number).');\n }\n if (typeof value.id !== 'string' || value.id.length === 0) {\n return fail('Missing or invalid session \"id\".');\n }\n if (!isRecord(value.site) || typeof value.site.origin !== 'string') {\n return fail('Missing or invalid \"site\" (expected { origin, route, viewport, userAgent }).');\n }\n if (!Array.isArray(value.items)) {\n return fail('Missing or invalid \"items\" (expected an array).');\n }\n\n const items: ReviewItem[] = [];\n for (let i = 0; i < value.items.length; i++) {\n const raw: unknown = value.items[i];\n if (!isRecord(raw)) return fail(`Item ${i + 1} is not an object.`);\n if (typeof raw.id !== 'string' || raw.id.length === 0) {\n return fail(`Item ${i + 1} is missing an \"id\".`);\n }\n if (typeof raw.type !== 'string' || raw.type.length === 0) {\n return fail(`Item ${i + 1} (\"${raw.id}\") is missing a \"type\".`);\n }\n if (!isRecord(raw.target) || typeof raw.target.selector !== 'string') {\n return fail(`Item ${i + 1} (\"${raw.id}\") is missing a valid \"target\".`);\n }\n const status: ItemStatus = (ITEM_STATUSES as readonly string[]).includes(String(raw.status))\n ? (raw.status as ItemStatus)\n : 'open';\n const priority: ItemPriority = raw.priority === 'must' ? 'must' : 'nice';\n items.push({\n ...raw,\n status,\n priority,\n createdAt: typeof raw.createdAt === 'string' ? raw.createdAt : new Date(0).toISOString(),\n route: typeof raw.route === 'string' ? raw.route : '/',\n target: raw.target as ReviewTarget,\n } as ReviewItem);\n }\n\n const site = value.site;\n const viewport = isRecord(site.viewport) ? site.viewport : {};\n const session: ReviewSession = {\n ...value,\n schemaVersion: value.schemaVersion,\n id: value.id,\n createdAt: typeof value.createdAt === 'string' ? value.createdAt : new Date(0).toISOString(),\n updatedAt: typeof value.updatedAt === 'string' ? value.updatedAt : new Date(0).toISOString(),\n author: typeof value.author === 'string' ? value.author : '',\n site: {\n ...site,\n origin: typeof site.origin === 'string' ? site.origin : '',\n route: typeof site.route === 'string' ? site.route : '/',\n viewport: {\n ...viewport,\n width: typeof viewport.width === 'number' ? viewport.width : 0,\n height: typeof viewport.height === 'number' ? viewport.height : 0,\n },\n userAgent: typeof site.userAgent === 'string' ? site.userAgent : '',\n },\n items,\n };\n return { ok: true, session };\n}\n\n/** Parse a raw JSON string into a session, with a readable error on failure. */\nexport function parseSessionJson(json: string): ParseResult {\n let value: unknown;\n try {\n value = JSON.parse(json);\n } catch {\n return fail('File is not valid JSON.');\n }\n return parseSession(value);\n}\n\n// Emblema integration contract (authoritative definition lives in\n// src/integrations/emblema.ts; re-exported here so review-kit/schema carries it).\nexport type { EmblemaReviewDispatch, EmblemaConfig } from './integrations/emblema';\nexport { REVIEW_DISPATCH_VERSION } from './integrations/emblema';\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// src/activate.ts
|
|
2
|
+
var STICKY_KEY = "review-kit:sticky";
|
|
3
|
+
var STICKY_TTL_MS = 4 * 60 * 60 * 1e3;
|
|
4
|
+
function safeRemove(storage, key) {
|
|
5
|
+
try {
|
|
6
|
+
storage().removeItem(key);
|
|
7
|
+
} catch {
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function readSticky() {
|
|
11
|
+
try {
|
|
12
|
+
const raw = window.sessionStorage.getItem(STICKY_KEY);
|
|
13
|
+
if (!raw) return null;
|
|
14
|
+
const parsed = JSON.parse(raw);
|
|
15
|
+
if (typeof parsed === "object" && parsed !== null && typeof parsed.token === "string" && typeof parsed.until === "number") {
|
|
16
|
+
return parsed;
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
function isStickyEnabled(token) {
|
|
23
|
+
if (token === void 0 || token === "") return false;
|
|
24
|
+
const sticky = readSticky();
|
|
25
|
+
if (!sticky) return false;
|
|
26
|
+
if (sticky.until < Date.now() || sticky.token !== token) {
|
|
27
|
+
safeRemove(() => window.sessionStorage, STICKY_KEY);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
function setSticky(token, on) {
|
|
33
|
+
try {
|
|
34
|
+
if (on && token !== void 0 && token !== "") {
|
|
35
|
+
const value = { token, until: Date.now() + STICKY_TTL_MS };
|
|
36
|
+
window.sessionStorage.setItem(STICKY_KEY, JSON.stringify(value));
|
|
37
|
+
} else {
|
|
38
|
+
window.sessionStorage.removeItem(STICKY_KEY);
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function clearSticky() {
|
|
44
|
+
safeRemove(() => window.sessionStorage, STICKY_KEY);
|
|
45
|
+
}
|
|
46
|
+
function reviewParam() {
|
|
47
|
+
try {
|
|
48
|
+
return new URLSearchParams(window.location.search).get("review");
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function hostAllowed(allowedHosts) {
|
|
54
|
+
if (!allowedHosts || allowedHosts.length === 0) return true;
|
|
55
|
+
let host = "";
|
|
56
|
+
try {
|
|
57
|
+
host = window.location.hostname.toLowerCase();
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return allowedHosts.some((pattern) => {
|
|
62
|
+
const regex = new RegExp(
|
|
63
|
+
"^" + pattern.toLowerCase().split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$"
|
|
64
|
+
);
|
|
65
|
+
return regex.test(host);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function isLocalHost() {
|
|
69
|
+
try {
|
|
70
|
+
const host = window.location.hostname.toLowerCase();
|
|
71
|
+
return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host.endsWith(".localhost");
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
var warnedNoToken = false;
|
|
77
|
+
var warnedMismatch = false;
|
|
78
|
+
function shouldActivate(options = {}) {
|
|
79
|
+
if (typeof window === "undefined") return false;
|
|
80
|
+
if (options.enabled === false) return false;
|
|
81
|
+
if (!hostAllowed(options.allowedHosts)) return false;
|
|
82
|
+
const param = reviewParam();
|
|
83
|
+
if (param === "off") {
|
|
84
|
+
clearSticky();
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (options.devAutoOn === true) return true;
|
|
88
|
+
const token = options.token;
|
|
89
|
+
if (token === void 0 || token === "") {
|
|
90
|
+
if (param !== null && param !== "" && !warnedNoToken) {
|
|
91
|
+
warnedNoToken = true;
|
|
92
|
+
console.warn(
|
|
93
|
+
"[review-kit] `?review` is present but no `token` is configured; the layer stays off. Pass `token` to <ReviewKit /> (or `devAutoOn` for local development)."
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
if (param !== null && param !== "") {
|
|
99
|
+
if (param === token) return true;
|
|
100
|
+
if (!warnedMismatch) {
|
|
101
|
+
warnedMismatch = true;
|
|
102
|
+
console.warn("[review-kit] `?review` token does not match the configured token; staying inactive.");
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return isStickyEnabled(token);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export {
|
|
110
|
+
STICKY_KEY,
|
|
111
|
+
isStickyEnabled,
|
|
112
|
+
setSticky,
|
|
113
|
+
clearSticky,
|
|
114
|
+
reviewParam,
|
|
115
|
+
hostAllowed,
|
|
116
|
+
isLocalHost,
|
|
117
|
+
shouldActivate
|
|
118
|
+
};
|
|
119
|
+
//# sourceMappingURL=chunk-ETAGGIDN.js.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":[]}
|