@siteping/server 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1206 @@
1
+ import { type DeepReadonly, type Prettify, type Serialized } from "./type-utils.js";
2
+ /** FAB anchor — bottom-corner placement supported by the widget. */
3
+ export type SitepingPosition = "bottom-right" | "bottom-left";
4
+ /** Visual theme — `auto` resolves to `light` or `dark` via system preference. */
5
+ export type SitepingTheme = "light" | "dark" | "auto";
6
+ /** Built-in UI locales shipped with the widget. */
7
+ export declare const BUILTIN_LOCALES: readonly ["en", "fr", "de", "es", "it", "pt", "ru", "ja", "ko"];
8
+ export type BuiltinLocale = (typeof BUILTIN_LOCALES)[number];
9
+ /**
10
+ * Locale identifier accepted by the widget. Built-in locales are kept as
11
+ * literal strings so editors auto-complete them, but arbitrary BCP-47 tags
12
+ * are also accepted (custom dictionaries registered via `registerLocale`).
13
+ */
14
+ export type SitepingLocale = BuiltinLocale | (string & {});
15
+ /**
16
+ * Reasons reported through `SitepingConfig.onSkip` — production environment,
17
+ * viewport narrower than `minViewportWidth` (`"mobile"`), or server-side
18
+ * rendering (no `window`/`document`).
19
+ */
20
+ export type SitepingSkipReason = "production" | "mobile" | "ssr";
21
+ /** Per-channel + per-buffer-size diagnostics configuration. */
22
+ export interface DiagnosticsCaptureOptions {
23
+ console?: boolean | undefined;
24
+ network?: boolean | undefined;
25
+ /** Console buffer size — default and maximum 50 (the server's cap; larger values are clamped). */
26
+ maxConsoleEntries?: number | undefined;
27
+ /** Failed-request buffer size — default and maximum 20 (the server's cap; larger values are clamped). */
28
+ maxNetworkEntries?: number | undefined;
29
+ }
30
+ /** Identity payload supplied by the host application — bypasses the modal. */
31
+ export interface SitepingIdentity {
32
+ name: string;
33
+ email: string;
34
+ }
35
+ /**
36
+ * Max length of an identity's `name` and `email` — the HTTP schema's
37
+ * `authorName` / `authorEmail` cap. The widget's modal enforces it so a value
38
+ * it persists is never a 400 on every later submission.
39
+ */
40
+ export declare const IDENTITY_FIELD_MAX_LENGTH = 200;
41
+ /** Deep-link configuration — controls how a feedback id is read from the URL. */
42
+ export interface SitepingDeepLinkOptions {
43
+ /** Query parameter name carrying the feedback id. Defaults to `"siteping"`. */
44
+ param?: string | undefined;
45
+ }
46
+ /**
47
+ * The feedback handed to panel action callbacks: a detached, deeply frozen
48
+ * copy, typed read-only all the way down. Type your own helpers with it — a
49
+ * `FeedbackResponse` parameter does not accept the frozen copy.
50
+ */
51
+ export type SitepingPanelActionFeedback = DeepReadonly<FeedbackResponse>;
52
+ /**
53
+ * Helpers passed to a panel action's `onAction` as its second argument.
54
+ * Both do nothing once the widget has been destroyed.
55
+ */
56
+ export interface SitepingPanelActionContext {
57
+ /**
58
+ * Re-fetch the panel list and markers, then re-render the detail view with
59
+ * the updated feedback — or go back to the list when it no longer matches
60
+ * the panel filters. Call it after your action changed the feedback
61
+ * server-side (e.g. moved it to `in_progress` once a ticket exists).
62
+ */
63
+ refresh: () => Promise<void>;
64
+ /** Close the feedback panel. */
65
+ close: () => void;
66
+ }
67
+ /**
68
+ * Fields shared by both kinds of {@link SitepingPanelAction}.
69
+ *
70
+ * Do not use this type directly — use {@link SitepingPanelAction}.
71
+ */
72
+ export interface SitepingPanelActionBase {
73
+ /** Stable, unique identifier — becomes `data-action-id` on the rendered control. */
74
+ id: string;
75
+ /**
76
+ * Visible label, rendered as plain text. Host-provided verbatim — not
77
+ * routed through the widget i18n, since hosts localize their own strings.
78
+ */
79
+ label: string;
80
+ /**
81
+ * Optional inline SVG markup rendered before the label. It is parsed
82
+ * inertly and reduced to plain shapes — scripts, event handlers, links,
83
+ * styles and external references are dropped — but keep it static markup
84
+ * you control. Markup that is not an `<svg>` is ignored with a warning.
85
+ */
86
+ icon?: string | undefined;
87
+ /**
88
+ * Per-feedback visibility predicate. Return `false` to omit the action
89
+ * for that feedback. Defaults to always visible. A throw hides the action
90
+ * and is reported through `onError`.
91
+ */
92
+ visible?: ((feedback: SitepingPanelActionFeedback) => boolean) | undefined;
93
+ }
94
+ /** A panel action rendered as a button that runs host code. */
95
+ export interface SitepingPanelButtonAction extends SitepingPanelActionBase {
96
+ /**
97
+ * Invoked on click. While a returned promise is pending the detail view's
98
+ * action buttons are disabled and the clicked button shows a spinner —
99
+ * also after `context.refresh()` re-renders the view, and when the user
100
+ * comes back to that feedback.
101
+ * Throws and rejections are reported through `onError` and restore the
102
+ * buttons; the detail view stays open either way.
103
+ */
104
+ onAction: (feedback: SitepingPanelActionFeedback, context: SitepingPanelActionContext) => void | Promise<void>;
105
+ /** Not available on a button action — use either `onAction` or `href`, never both. */
106
+ href?: never;
107
+ }
108
+ /** A panel action rendered as a link. */
109
+ export interface SitepingPanelLinkAction extends SitepingPanelActionBase {
110
+ /**
111
+ * Link target — a URL, or a function building one from the feedback.
112
+ * Relative URLs resolve against the page. Only `http:`, `https:` and
113
+ * `mailto:` are rendered: a static `href` with any other scheme
114
+ * (`javascript:` included) skips the action with a warning; a function
115
+ * returning one hides the action and is reported through `onError`. Web
116
+ * links open in a new tab with `rel="noopener noreferrer"`.
117
+ */
118
+ href: string | ((feedback: SitepingPanelActionFeedback) => string);
119
+ /** Not available on a link action — use either `onAction` or `href`, never both. */
120
+ onAction?: never;
121
+ }
122
+ /**
123
+ * A host-defined action rendered in the feedback detail view, below the
124
+ * built-in Resolve / Delete buttons: a button running your code
125
+ * (`onAction`) or a link (`href`) — never both.
126
+ *
127
+ * Hosts use this to bridge feedbacks into their own systems — create a
128
+ * ticket, dispatch to a bot, open the feedback in their tracker — without
129
+ * forking the panel. Callbacks receive a detached, deeply frozen copy of
130
+ * the feedback: read it freely, it can never alter what the panel displays.
131
+ */
132
+ export type SitepingPanelAction = SitepingPanelButtonAction | SitepingPanelLinkAction;
133
+ /**
134
+ * Extra request headers for HTTP mode — a static map, or a factory (sync or
135
+ * async) invoked once per request to produce fresh values (e.g. a short-lived
136
+ * session token).
137
+ */
138
+ export type SitepingHeadersOption = Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
139
+ /**
140
+ * Options shared by both widget modes (HTTP and direct store).
141
+ *
142
+ * Do not use this type directly — use {@link SitepingConfig}, the
143
+ * discriminated union that adds the mode-specific fields.
144
+ */
145
+ export interface SitepingBaseConfig {
146
+ /** Required — project identifier used to scope feedbacks */
147
+ projectName: string;
148
+ /** FAB position — defaults to 'bottom-right' */
149
+ position?: SitepingPosition | undefined;
150
+ /**
151
+ * Show the "toggle markers visibility" item in the FAB radial menu.
152
+ * Defaults to `true` (current behavior). Set to `false` to hide that
153
+ * item entirely — useful for hosts that always want markers visible
154
+ * (e.g. dedicated review tools) or that find the eye icon redundant
155
+ * when no marker is on screen.
156
+ *
157
+ * Hiding the item also removes its keyboard navigation slot — the
158
+ * remaining two items still respond to ArrowUp/ArrowDown/Home/End.
159
+ * The marker-visibility state itself is unaffected; markers stay
160
+ * visible (the previous default state) and `annotations:toggle` is
161
+ * simply never emitted from the FAB.
162
+ */
163
+ showAnnotationsToggle?: boolean | undefined;
164
+ /** Accent color for the widget UI — defaults to '#0066ff' */
165
+ accentColor?: string | undefined;
166
+ /**
167
+ * Render the widget even when it would normally be skipped — this bypasses
168
+ * BOTH the production-environment guard AND the `minViewportWidth` guard.
169
+ * It does NOT bypass the SSR guard: without `window`/`document` the widget
170
+ * never renders and `onSkip("ssr")` fires instead.
171
+ * Defaults to false. Use it for dedicated review tools, staging environments,
172
+ * or responsive testing where you always want the widget present.
173
+ */
174
+ forceShow?: boolean | undefined;
175
+ /**
176
+ * Minimum viewport width (px) at or above which the widget renders. Below it,
177
+ * the widget is skipped and `onSkip("mobile")` fires. Defaults to `0`: the
178
+ * widget renders at every width, and phones get a compact layout (bottom
179
+ * sheets, touch-sized controls).
180
+ *
181
+ * Set it (e.g. `768`) to keep the widget off small screens, or use
182
+ * `forceShow` to bypass the viewport check entirely.
183
+ */
184
+ minViewportWidth?: number | undefined;
185
+ /** Enable debug logging of lifecycle events — defaults to false */
186
+ debug?: boolean | undefined;
187
+ /** Color theme — defaults to 'light' */
188
+ theme?: SitepingTheme | undefined;
189
+ /** UI locale — defaults to 'en'. Built-in: en, fr, de, es, it, pt (Brazilian), ru, ja, ko. Any other string falls back to English. */
190
+ locale?: SitepingLocale | undefined;
191
+ /**
192
+ * Returns the current page scope for annotations and panel filtering.
193
+ * Called on initial markers load and on `instance.refresh()`.
194
+ *
195
+ * Default: `{ url: window.location.pathname, urlPattern: null }` — annotations
196
+ * are scoped strictly to the current pathname.
197
+ *
198
+ * Apps with parameterized routes (e.g. React Router) should return both the
199
+ * concrete URL and the route template (e.g. `/orders/:orderId`) so the panel
200
+ * can offer a "this type of page" filter that groups feedbacks by template.
201
+ */
202
+ getPageScope?: (() => PageScope) | undefined;
203
+ /**
204
+ * When true (default), the widget filters initial markers and panel results
205
+ * by `feedback.url === scope.url`, so annotations created on one page never
206
+ * leak to other pages — even if their CSS selector accidentally matches.
207
+ * Set to `false` to revert to the legacy project-wide behavior.
208
+ */
209
+ scopeAnnotationsByUrl?: boolean | undefined;
210
+ /**
211
+ * Capture a JPEG screenshot of the annotated area on submit. Defaults to
212
+ * `false` — opt-in because:
213
+ *
214
+ * - it adds runtime weight (~60 KB gzip dynamic chunk for html2canvas-pro,
215
+ * loaded only on first capture),
216
+ * - it embeds page content in the feedback (privacy/GDPR consideration —
217
+ * inform end users in your widget host UI when enabling).
218
+ *
219
+ * `html2canvas-pro` ships as a regular dependency of `@siteping/widget` so the
220
+ * dynamic import always resolves; you don't need to install anything extra.
221
+ *
222
+ * **Masking sensitive elements:** add `data-siteping-ignore="true"` to any
223
+ * element you do NOT want captured (password fields, credit-card forms,
224
+ * API tokens shown in the UI, etc.). The capture predicate skips matching
225
+ * elements *and their descendants*. Do this BEFORE turning on screenshots
226
+ * in production — once a feedback is saved, the screenshot is in your DB
227
+ * (or object storage) regardless of what was on the page.
228
+ */
229
+ enableScreenshot?: boolean | undefined;
230
+ /**
231
+ * Enable right-click (`contextmenu`) to instantly open the comment composer
232
+ * at the cursor location. When enabled, a document-level listener intercepts
233
+ * right-clicks, prevents the browser's native context menu, and enters the
234
+ * annotation flow anchored to the element under the cursor. Defaults to
235
+ * `false` — the browser's native context menu is never hijacked unless the
236
+ * host explicitly opts in.
237
+ *
238
+ * Keyboard-triggered context menus (≣ Menu key, Shift+F10) always get the
239
+ * native menu; only mouse right-click and touch/pen long-press open the
240
+ * composer — a long-press only where the browser fires `contextmenu` for
241
+ * it, which iOS and iPadOS never do (see the note below).
242
+ *
243
+ * **Modifier-key escape hatch:** holding Shift, Ctrl, Alt, or Meta while
244
+ * right-clicking always falls through to the native context menu, giving
245
+ * users (and devtools) an escape hatch regardless of this setting.
246
+ *
247
+ * Right-clicks on SitePing's own UI (FAB, panel, markers, popup) are
248
+ * ignored — the native menu is shown as expected.
249
+ *
250
+ * Note: on Android, `contextmenu` fires on long-press — touch users open the
251
+ * composer by long-pressing, on phones too since the widget renders at every
252
+ * width by default (see `minViewportWidth`). On iPhone and iPad a long-press
253
+ * never fires `contextmenu` (WebKit bug 213953): users there open annotate
254
+ * mode from the floating button and tap the element.
255
+ */
256
+ enableRightClickComment?: boolean | undefined;
257
+ /**
258
+ * Capture the last few `console.*` calls and failed network requests
259
+ * (HTTP >= 400 or network error) at the moment a feedback is submitted.
260
+ *
261
+ * Lets reviewers replay the technical context that led to the report —
262
+ * stack traces, 500 responses, dead third-party scripts. Great for the
263
+ * "the page just doesn't work" feedback that contains zero detail.
264
+ *
265
+ * - `true` — capture with defaults (50 console / 20 network entries).
266
+ * - `false` (default) — no capture, no monkey-patching.
267
+ * - object — per-channel toggles + smaller buffer sizes (values above the
268
+ * 50 / 20 server caps are clamped so submissions never fail validation).
269
+ *
270
+ * **Privacy considerations:** console messages may contain anything the
271
+ * host page logs, including user data. Failed network requests record the
272
+ * URL without its credentials, query string or hash, and never the
273
+ * response body.
274
+ * Inform end users before enabling in environments where they might log
275
+ * sensitive values.
276
+ */
277
+ captureDiagnostics?: boolean | DiagnosticsCaptureOptions | undefined;
278
+ /** Called when the widget is skipped (production mode, viewport under `minViewportWidth`, SSR — no DOM) */
279
+ onSkip?: (reason: SitepingSkipReason) => void;
280
+ /**
281
+ * Auto-focus a specific annotation when its ID appears in the URL query
282
+ * string. Lets hosts deeplink directly into a feedback from external
283
+ * systems (Zammad tickets, Slack notifications, dashboard rows).
284
+ *
285
+ * When enabled, the widget reads the configured query parameter from
286
+ * `window.location.search` right after the initial markers load. If the
287
+ * value matches a visible feedback ID, the widget scrolls the annotation
288
+ * into view, pins its highlight, and pulses the marker — the same visual
289
+ * affordance a marker click produces.
290
+ *
291
+ * - `false` / `undefined` (default): no URL parsing. Existing behavior
292
+ * unchanged, no host URL inspection.
293
+ * - `true`: enabled with default query parameter name `siteping`.
294
+ * - object: enabled with a custom parameter name. Use this to avoid
295
+ * clashes with host-app query keys.
296
+ *
297
+ * Only the initial load triggers focus. Subsequent URL changes (SPA
298
+ * navigation, `history.pushState`, hash updates) are ignored —
299
+ * deliberate, to avoid surprising re-scrolls during normal browsing.
300
+ * Hosts that need re-focus on route change can call
301
+ * `instance.focusFeedback(id)` explicitly.
302
+ */
303
+ deepLink?: boolean | SitepingDeepLinkOptions | undefined;
304
+ /**
305
+ * Automatically re-fetch feedbacks when the page changes during client-side
306
+ * (SPA) navigation. Enabled by default.
307
+ *
308
+ * The widget is normally mounted once (singleton) inside a persistent layout
309
+ * — e.g. a Next.js App Router `layout.tsx`, which does NOT remount on
310
+ * client-side navigation. Without this, init runs a single time and both the
311
+ * panel list and the page markers stay frozen on the page where the widget
312
+ * first mounted. With it on, the widget patches the History API
313
+ * (`pushState`/`replaceState`, which SPA routers call instead of triggering
314
+ * `popstate`) and listens for `popstate`/`hashchange`, then re-fetches when
315
+ * the scope key (`getPageScope().url` + template) actually changes.
316
+ *
317
+ * This re-fetches data only — it deliberately does NOT re-focus or re-scroll
318
+ * to an annotation (deep-link focus stays initial-load only; see `deepLink`),
319
+ * so normal browsing is never interrupted by a surprise scroll.
320
+ *
321
+ * - `true` (default) — watch navigation and re-fetch on route change.
322
+ * - `false` — never touch the History API; hosts drive updates manually via
323
+ * `instance.refresh()`.
324
+ */
325
+ watchNavigation?: boolean | undefined;
326
+ /**
327
+ * Pre-fill author identity from the host application — typically the
328
+ * currently signed-in user. When set, the widget uses these values
329
+ * directly and never shows the identity modal, even on first feedback.
330
+ *
331
+ * Use case: SSO-integrated apps where the end user is already
332
+ * authenticated by the host. Avoids the awkward "enter your name and
333
+ * email" prompt for users the host already knows.
334
+ *
335
+ * When unset (default), the widget falls back to localStorage and shows
336
+ * the modal on first feedback as before — existing behavior unchanged.
337
+ *
338
+ * Note: `config.identity` is **not** persisted to localStorage. It is
339
+ * read at widget init time, not on every render. Hosts that need live
340
+ * identity updates after sign-in/sign-out should currently remount the
341
+ * widget (e.g. via a React `key` on the wrapping component). See
342
+ * https://github.com/NeosiaNexus/SitePing/issues/85 for tracking a
343
+ * future enhancement that propagates identity updates without a remount.
344
+ */
345
+ identity?: SitepingIdentity | undefined;
346
+ /**
347
+ * Host-defined actions rendered in the feedback detail view, below the
348
+ * built-in Resolve and Delete buttons. Read once when the panel loads:
349
+ * entries without a non-empty `id` and `label`, without exactly one of
350
+ * `onAction` / `href`, with an unsafe static `href`, or reusing an earlier
351
+ * `id` are skipped with a console warning. See {@link SitepingPanelAction}.
352
+ */
353
+ panelActions?: readonly SitepingPanelAction[] | undefined;
354
+ /**
355
+ * Reviewer mode: hide the actions that triage feedback — resolve, reopen,
356
+ * delete, the bulk actions, "Delete all" — and keep creating, browsing
357
+ * and replying. Defaults to `false`. The server's `permissions` hide
358
+ * what it would refuse on top of it. It only hides: the server decides
359
+ * what it accepts. Read once when the panel loads.
360
+ */
361
+ readOnly?: boolean | undefined;
362
+ /** Called when the feedback panel is opened. */
363
+ onOpen?: (() => void) | undefined;
364
+ /** Called when the feedback panel is closed. */
365
+ onClose?: (() => void) | undefined;
366
+ /** Called after a feedback is successfully submitted. */
367
+ onFeedbackSent?: ((feedback: FeedbackResponse) => void) | undefined;
368
+ /** Called after a reply is posted from the panel's discussion thread. */
369
+ onCommentAdded?: ((comment: CommentResponse) => void) | undefined;
370
+ /**
371
+ * Called when a feedback API call fails.
372
+ *
373
+ * The widget always emits a `SitepingError` (or a subclass:
374
+ * `SitepingNetworkError`, `SitepingValidationError`, `SitepingAuthError`)
375
+ * for HTTP-mode failures — host apps can `instanceof` to drive retry
376
+ * logic, or read `error.code` (`"NETWORK" | "VALIDATION" | "AUTH" |
377
+ * "SERVER"`) and `error.retryable` — and an `AUTH` error's `status`
378
+ * (`401` for missing or dead credentials, `403` for a refusal). The type
379
+ * is widened to `Error` so direct-store callers can still surface raw
380
+ * errors without breaking the contract.
381
+ *
382
+ * Also receives whatever a `panelActions` callback throws or rejects with
383
+ * (non-`Error` values are wrapped). Those host failures are not API
384
+ * failures, so they are not emitted on the public `feedback:error` event.
385
+ * The widget has no UI for them, so they are also always logged with
386
+ * `console.error`, whether or not `onError` is set.
387
+ */
388
+ onError?: ((error: Error) => void) | undefined;
389
+ /** Called when the user starts drawing an annotation. */
390
+ onAnnotationStart?: (() => void) | undefined;
391
+ /** Called when the user finishes drawing an annotation. */
392
+ onAnnotationEnd?: (() => void) | undefined;
393
+ }
394
+ /**
395
+ * HTTP mode — the widget talks to a server endpoint backed by a store
396
+ * adapter (e.g. `@siteping/adapter-prisma` request handlers).
397
+ */
398
+ export interface SitepingHttpConfig extends SitepingBaseConfig {
399
+ /** HTTP endpoint that receives feedbacks (e.g. '/api/siteping'). */
400
+ endpoint: string;
401
+ /**
402
+ * Convenience auth for HTTP mode — sent as `Authorization: Bearer <apiKey>`
403
+ * on every request to `endpoint`.
404
+ *
405
+ * **WARNING: the widget runs in every visitor's browser, so a static key
406
+ * configured here is public** — anyone can read it from your page source
407
+ * and replay it against your API. Only use `apiKey` for internal tools
408
+ * already behind your own login. On public sites, prefer `headers` with a
409
+ * per-request factory returning a short-lived session token.
410
+ */
411
+ apiKey?: string | undefined;
412
+ /**
413
+ * Extra headers for every HTTP-mode request — a static map, or a factory
414
+ * (sync or async) called once per request (e.g. to fetch a fresh session
415
+ * token). Merged over the widget's generated headers, case-insensitively,
416
+ * so an explicit `Authorization` entry overrides `apiKey`. A factory that
417
+ * throws, rejects, or does not settle within 10 s fails the request like a
418
+ * network error.
419
+ */
420
+ headers?: SitepingHeadersOption | undefined;
421
+ /** Not available in HTTP mode — use either `endpoint` or `store`, never both. */
422
+ store?: never;
423
+ }
424
+ /**
425
+ * Store mode — the widget talks to a `SitepingStore` directly in the
426
+ * browser, no server needed (demos, prototypes, localStorage persistence).
427
+ */
428
+ export interface SitepingStoreConfig extends SitepingBaseConfig {
429
+ /**
430
+ * Direct store for client-side mode. Bypasses HTTP entirely. A send stops
431
+ * waiting on `createFeedback` after 30 s (the call itself cannot be
432
+ * cancelled), so a network-backed store should bound its own calls.
433
+ */
434
+ store: SitepingStore;
435
+ /** Not available in store mode — use either `endpoint` or `store`, never both. */
436
+ endpoint?: never;
437
+ /** HTTP-mode only — meaningless without an `endpoint`. */
438
+ apiKey?: never;
439
+ /** HTTP-mode only — meaningless without an `endpoint`. */
440
+ headers?: never;
441
+ }
442
+ /**
443
+ * Configuration options for the Siteping widget.
444
+ *
445
+ * A discriminated union over the two transport modes: pass `endpoint`
446
+ * (HTTP mode, optionally with `apiKey`/`headers`) **or** `store` (direct
447
+ * client-side mode) — never both, never neither. Invalid combinations are
448
+ * compile errors instead of runtime warnings.
449
+ */
450
+ export type SitepingConfig = SitepingHttpConfig | SitepingStoreConfig;
451
+ /** Instance returned by initSiteping() with lifecycle methods. */
452
+ export interface SitepingInstance {
453
+ /** Remove the widget from the DOM and clean up all listeners. */
454
+ destroy: () => void;
455
+ /** Open the panel programmatically */
456
+ open: () => void;
457
+ /** Close the panel */
458
+ close: () => void;
459
+ /** Reload feedbacks from server */
460
+ refresh: () => void;
461
+ /**
462
+ * Scroll the matching annotation into view, pin its highlight, and
463
+ * pulse its marker. Returns `true` when a visible feedback matched the
464
+ * given ID, `false` otherwise (unknown ID, feedback on another URL when
465
+ * `scopeAnnotationsByUrl` filtered it out, or markers not yet loaded).
466
+ *
467
+ * Counterpart to the `deepLink` config option for hosts that prefer to
468
+ * drive focus from JS (e.g., a notification click handler) instead of a
469
+ * URL query parameter.
470
+ */
471
+ focusFeedback: (feedbackId: string) => boolean;
472
+ /** Subscribe to a public widget event */
473
+ on: <K extends keyof SitepingPublicEvents>(event: K, listener: SitepingPublicEventListener<K>) => SitepingUnsubscribe;
474
+ /** Unsubscribe from a public widget event */
475
+ off: <K extends keyof SitepingPublicEvents>(event: K, listener: SitepingPublicEventListener<K>) => void;
476
+ }
477
+ /** Listener signature for a single `SitepingPublicEvents` key. */
478
+ export type SitepingPublicEventListener<K extends keyof SitepingPublicEvents> = (...args: SitepingPublicEvents[K]) => void;
479
+ /** Disposer returned by `SitepingInstance.on` — call once to detach the listener. */
480
+ export type SitepingUnsubscribe = () => void;
481
+ /** Events exposed to consumers via SitepingInstance.on / .off */
482
+ export interface SitepingPublicEvents {
483
+ "feedback:sent": [FeedbackResponse];
484
+ "feedback:deleted": [FeedbackResponse["id"]];
485
+ /** A reply was posted from the panel's discussion thread. */
486
+ "comment:added": [CommentResponse];
487
+ /**
488
+ * A feedback API call failed. Same payload contract as
489
+ * `SitepingConfig.onError` — a `SitepingError` subclass in HTTP mode,
490
+ * possibly a raw `Error` in store mode.
491
+ */
492
+ "feedback:error": [Error];
493
+ "panel:open": [];
494
+ "panel:close": [];
495
+ /** The user started drawing an annotation. */
496
+ "annotation:start": [];
497
+ /** The user finished drawing an annotation. */
498
+ "annotation:end": [];
499
+ }
500
+ /** Single source of truth for feedback types — used by both TS types and Zod schemas. */
501
+ export declare const FEEDBACK_TYPES: readonly ["question", "change", "bug", "other"];
502
+ export type FeedbackType = (typeof FEEDBACK_TYPES)[number];
503
+ /** Single source of truth for feedback statuses. */
504
+ export declare const FEEDBACK_STATUSES: readonly ["open", "in_progress", "resolved", "wont_fix"];
505
+ export type FeedbackStatus = (typeof FEEDBACK_STATUSES)[number];
506
+ /**
507
+ * Terminal statuses — the feedback needs no further action. `resolvedAt` is
508
+ * the closure timestamp: set when a feedback enters a closed status, null
509
+ * while it is open or in progress. The derivation happens at the edge (HTTP
510
+ * handler, dashboard) — store adapters persist whatever they are given.
511
+ */
512
+ export declare const CLOSED_FEEDBACK_STATUSES: readonly ["resolved", "wont_fix"];
513
+ /** A terminal status — `resolved` or `wont_fix`. */
514
+ export type ClosedFeedbackStatus = (typeof CLOSED_FEEDBACK_STATUSES)[number];
515
+ /** Non-terminal statuses — the feedback still needs attention. */
516
+ export declare const OPEN_FEEDBACK_STATUSES: readonly ["open", "in_progress"];
517
+ /** A non-terminal status — `open` or `in_progress`. */
518
+ export type OpenFeedbackStatus = (typeof OPEN_FEEDBACK_STATUSES)[number];
519
+ /** Whether a status is terminal (`resolved` or `wont_fix`). Narrows the status type. */
520
+ export declare function isClosedStatus(status: FeedbackStatus): status is ClosedFeedbackStatus;
521
+ /**
522
+ * Page scope returned by `SitepingConfig.getPageScope()`.
523
+ *
524
+ * - `url`: concrete page identifier — usually `window.location.pathname`,
525
+ * used as the strict scope for marker rendering.
526
+ * - `urlPattern`: optional parameterized template (e.g. `/orders/:orderId`)
527
+ * used by the panel's "this type of page" filter to group feedbacks across
528
+ * instances of the same page kind.
529
+ */
530
+ export interface PageScope {
531
+ url: string;
532
+ urlPattern: string | null;
533
+ }
534
+ /** Input for creating a feedback record in the store. */
535
+ export interface FeedbackCreateInput {
536
+ projectName: string;
537
+ type: FeedbackType;
538
+ message: string;
539
+ status: FeedbackStatus;
540
+ url: string;
541
+ /**
542
+ * Optional parameterized URL template (e.g. `/orders/:orderId`) for the page
543
+ * where the feedback was created. Allows the panel to filter feedbacks by
544
+ * "this type of page" across different instances. Null when the host did not
545
+ * provide a `getPageScope` callback or the route has no template.
546
+ */
547
+ urlPattern?: string | null | undefined;
548
+ viewport: string;
549
+ userAgent: string;
550
+ authorName: string;
551
+ authorEmail: string;
552
+ clientId: string;
553
+ annotations: AnnotationCreateInput[];
554
+ /**
555
+ * Base64 JPEG `data:` URL captured by the widget at submit time.
556
+ *
557
+ * Adapters with a configured `ScreenshotStorage` are expected to upload
558
+ * this and persist the returned URL on `FeedbackRecord.screenshotUrl`.
559
+ * Adapters without storage may persist the data URL inline (memory /
560
+ * localStorage / dev) — the widget then renders it directly.
561
+ */
562
+ screenshotDataUrl?: string | null | undefined;
563
+ /**
564
+ * Where the client's annotation rect sits within the screenshot image,
565
+ * as fractions [0, 1] of the image dimensions. Present when the widget
566
+ * captured context around the drawn rect; null for legacy captures that
567
+ * were cropped exactly to the rect (dashboards then render the image
568
+ * without an overlay).
569
+ */
570
+ screenshotRegion?: ScreenshotRegion | null | undefined;
571
+ /**
572
+ * Optional console + failed-network snapshot captured by the widget when
573
+ * `SitepingConfig.captureDiagnostics` is enabled. Stored as JSON on
574
+ * `FeedbackRecord.diagnostics` so reviewers can replay the context.
575
+ */
576
+ diagnostics?: DiagnosticsSnapshot | null | undefined;
577
+ }
578
+ /** Input for a single annotation when creating a feedback. */
579
+ export interface AnnotationCreateInput {
580
+ cssSelector: string;
581
+ xpath: string;
582
+ textSnippet: string;
583
+ elementTag: string;
584
+ elementId?: string | undefined;
585
+ textPrefix: string;
586
+ textSuffix: string;
587
+ fingerprint: string;
588
+ neighborText: string;
589
+ /**
590
+ * Semantic anchor identifier from the closest ancestor's `data-feedback-anchor`
591
+ * attribute. When set, this is the most stable re-anchoring signal because
592
+ * hosts deliberately place these on layout/section roots that survive DOM
593
+ * refactors and viewport changes. Null when no semantic ancestor exists.
594
+ */
595
+ anchorKey?: string | null | undefined;
596
+ xPct: number;
597
+ yPct: number;
598
+ wPct: number;
599
+ hPct: number;
600
+ scrollX: number;
601
+ scrollY: number;
602
+ viewportW: number;
603
+ viewportH: number;
604
+ devicePixelRatio: number;
605
+ }
606
+ /** Query parameters for fetching feedbacks. */
607
+ export interface FeedbackQuery {
608
+ projectName: string;
609
+ type?: FeedbackType | undefined;
610
+ /** Exact single-status filter. For "any of a set" (bucket) semantics, use `statuses`. */
611
+ status?: FeedbackStatus | undefined;
612
+ /**
613
+ * Filter to feedbacks whose status is any of the listed values — bucket
614
+ * semantics used by the panel's binary tabs (e.g. "Open" passes
615
+ * `["open", "in_progress"]`). When both `status` and `statuses` are set,
616
+ * `statuses` wins. An empty array is treated as absent (no status filter).
617
+ */
618
+ statuses?: readonly FeedbackStatus[] | undefined;
619
+ search?: string | undefined;
620
+ page?: number | undefined;
621
+ /**
622
+ * Page size. Defaults to `DEFAULT_PAGE_LIMIT` (50) and is capped at
623
+ * `MAX_PAGE_LIMIT` (100) — `clampPagination` implements both, and the
624
+ * conformance suite checks them.
625
+ */
626
+ limit?: number | undefined;
627
+ /**
628
+ * Filter to feedbacks created on this exact URL (path). Used by the panel's
629
+ * "this page" filter and by the markers loader to keep page scopes isolated.
630
+ */
631
+ url?: string | undefined;
632
+ /**
633
+ * Filter to feedbacks created on this URL pattern (e.g. `/orders/:orderId`).
634
+ * Used by the panel's "this type of page" filter to group feedbacks across
635
+ * different concrete instances of the same template.
636
+ */
637
+ urlPattern?: string | undefined;
638
+ }
639
+ /**
640
+ * Update payload for patching a feedback.
641
+ *
642
+ * A discriminated union encoding the closure invariant: a feedback entering
643
+ * a closed status carries its closure timestamp, an open one carries `null`.
644
+ * `{ status: "resolved", resolvedAt: null }` is a compile error instead of a
645
+ * silent data bug. Build it from a plain `FeedbackStatus` with
646
+ * {@link toFeedbackUpdate}.
647
+ */
648
+ export type FeedbackUpdateInput = {
649
+ status: OpenFeedbackStatus;
650
+ resolvedAt: null;
651
+ } | {
652
+ status: ClosedFeedbackStatus;
653
+ resolvedAt: Date;
654
+ };
655
+ /**
656
+ * Derive the {@link FeedbackUpdateInput} for a status change — the closure
657
+ * timestamp is stamped for closed statuses and cleared otherwise. This is
658
+ * the edge derivation described on {@link CLOSED_FEEDBACK_STATUSES}; store
659
+ * adapters persist the result verbatim.
660
+ */
661
+ export declare function toFeedbackUpdate(status: FeedbackStatus, closedAt?: Date): FeedbackUpdateInput;
662
+ /** A persisted feedback record returned by the store. */
663
+ export interface FeedbackRecord {
664
+ id: string;
665
+ type: FeedbackType;
666
+ message: string;
667
+ status: FeedbackStatus;
668
+ projectName: string;
669
+ url: string;
670
+ /**
671
+ * Parameterized URL template the feedback was created on.
672
+ * Null for legacy records or hosts without `getPageScope`.
673
+ */
674
+ urlPattern: string | null;
675
+ authorName: string;
676
+ authorEmail: string;
677
+ viewport: string;
678
+ userAgent: string;
679
+ clientId: string;
680
+ resolvedAt: Date | null;
681
+ createdAt: Date;
682
+ updatedAt: Date;
683
+ annotations: AnnotationRecord[];
684
+ /**
685
+ * Discussion thread, oldest first. A store that implements
686
+ * `SitepingStore.addComment` returns it on every record; a store without
687
+ * comments may leave it out, which reads as an empty thread (HTTP handlers
688
+ * send `[]`).
689
+ */
690
+ comments?: CommentRecord[] | undefined;
691
+ /**
692
+ * URL the widget renders as `<img src>`. Either an `https://...` from a
693
+ * configured `ScreenshotStorage`, or a `data:image/jpeg;base64,...` URL
694
+ * inline-persisted by adapters without storage. Null when no screenshot
695
+ * was captured (legacy records, capture failed, or host disabled it).
696
+ */
697
+ screenshotUrl: string | null;
698
+ /**
699
+ * Annotation rect position within the screenshot image, as fractions of
700
+ * its dimensions. Null for legacy captures cropped exactly to the rect.
701
+ */
702
+ screenshotRegion: ScreenshotRegion | null;
703
+ /**
704
+ * Console + failed-network snapshot captured at submit time. Null when
705
+ * diagnostics weren't enabled on the widget side.
706
+ */
707
+ diagnostics: DiagnosticsSnapshot | null;
708
+ }
709
+ /** A persisted annotation record returned by the store. */
710
+ export interface AnnotationRecord {
711
+ id: string;
712
+ feedbackId: string;
713
+ cssSelector: string;
714
+ xpath: string;
715
+ textSnippet: string;
716
+ elementTag: string;
717
+ elementId: string | null;
718
+ textPrefix: string;
719
+ textSuffix: string;
720
+ fingerprint: string;
721
+ neighborText: string;
722
+ /**
723
+ * Semantic anchor identifier from `data-feedback-anchor`. Null for legacy
724
+ * annotations or those drawn outside any anchored region.
725
+ */
726
+ anchorKey: string | null;
727
+ xPct: number;
728
+ yPct: number;
729
+ wPct: number;
730
+ hPct: number;
731
+ scrollX: number;
732
+ scrollY: number;
733
+ viewportW: number;
734
+ viewportH: number;
735
+ devicePixelRatio: number;
736
+ createdAt: Date;
737
+ }
738
+ /**
739
+ * Who wrote a comment: `client` — the reviewer on the site (the widget) —
740
+ * or `team` — the project side (the dashboard). HTTP handlers stamp
741
+ * `client` unless the server's access policy vouches for the caller, so a
742
+ * visitor cannot pose as the team.
743
+ */
744
+ export declare const COMMENT_AUTHOR_ROLES: readonly ["client", "team"];
745
+ export type CommentAuthorRole = (typeof COMMENT_AUTHOR_ROLES)[number];
746
+ /** Longest comment `body` the HTTP API accepts — the cap of a feedback `message`. */
747
+ export declare const COMMENT_BODY_MAX_LENGTH = 5000;
748
+ /**
749
+ * Most `client` comments one thread holds. List responses embed whole
750
+ * threads, which are not paginated, so this bounds what one feedback weighs
751
+ * however much a public endpoint is spammed. `team` comments — a role only
752
+ * the access policy grants — neither count nor meet it: a thread spammed
753
+ * full still takes the team's answer. Stores enforce it with
754
+ * `StoreLimitError`; a store without an atomic primitive may overshoot it by
755
+ * the posts that race the last free slot.
756
+ */
757
+ export declare const MAX_COMMENTS_PER_FEEDBACK = 100;
758
+ /** A persisted comment returned by the store. */
759
+ export interface CommentRecord {
760
+ id: string;
761
+ /** The feedback whose thread holds this comment. */
762
+ feedbackId: string;
763
+ body: string;
764
+ authorName: string;
765
+ /**
766
+ * Author email, or `""` when the author has none on file (a dashboard
767
+ * user). HTTP handlers redact it exactly like `FeedbackRecord.authorEmail`.
768
+ */
769
+ authorEmail: string;
770
+ authorRole: CommentAuthorRole;
771
+ /** Client-generated id — `addComment` is idempotent on it, so a retried post never duplicates a reply. */
772
+ clientId: string;
773
+ createdAt: Date;
774
+ }
775
+ /** Input of `SitepingStore.addComment`. */
776
+ export interface CommentCreateInput {
777
+ body: string;
778
+ authorName: string;
779
+ authorEmail: string;
780
+ authorRole: CommentAuthorRole;
781
+ clientId: string;
782
+ }
783
+ /**
784
+ * Body of a comment `POST`. `feedbackId` is what tells it apart from a
785
+ * feedback submission on the same endpoint; `projectName` scopes the write
786
+ * to that project's feedbacks.
787
+ */
788
+ export interface CommentPayload extends CommentCreateInput {
789
+ projectName: string;
790
+ feedbackId: string;
791
+ }
792
+ /** Body of a comment `DELETE` — `commentId` is what tells it apart from a feedback delete. */
793
+ export interface CommentDeletePayload {
794
+ projectName: string;
795
+ feedbackId: string;
796
+ commentId: string;
797
+ }
798
+ /**
799
+ * Thrown when a record is not found during update or delete.
800
+ *
801
+ * Handlers translate this to HTTP 404. Adapters MUST throw this (not
802
+ * ORM-specific errors) so the handler layer remains ORM-agnostic.
803
+ */
804
+ export declare class StoreNotFoundError extends Error {
805
+ readonly code: "STORE_NOT_FOUND";
806
+ constructor(message?: string, options?: ErrorOptions);
807
+ }
808
+ /**
809
+ * Thrown when a unique constraint is violated (e.g. duplicate `clientId`).
810
+ *
811
+ * Handlers use this to return the existing record instead of failing.
812
+ */
813
+ export declare class StoreDuplicateError extends Error {
814
+ readonly code: "STORE_DUPLICATE";
815
+ constructor(message?: string, options?: ErrorOptions);
816
+ }
817
+ /**
818
+ * Thrown when a store accepts a mutation but cannot persist it — e.g.
819
+ * `localStorage` is full (QuotaExceededError). Adapters MUST throw this rather
820
+ * than swallow the failure, so callers learn the write was lost instead of
821
+ * seeing a phantom success.
822
+ */
823
+ export declare class StorePersistenceError extends Error {
824
+ readonly code: "STORE_PERSISTENCE";
825
+ constructor(message?: string, options?: ErrorOptions);
826
+ }
827
+ /**
828
+ * Thrown when a write would break a bound of the store contract — a
829
+ * `client` comment on a thread already holding `MAX_COMMENTS_PER_FEEDBACK`
830
+ * of them. Handlers translate this to HTTP 409.
831
+ */
832
+ export declare class StoreLimitError extends Error {
833
+ readonly code: "STORE_LIMIT";
834
+ constructor(message?: string, options?: ErrorOptions);
835
+ }
836
+ /**
837
+ * Thrown when a value is longer than the store can hold, though the HTTP
838
+ * validation accepts it — on MySQL, Prisma maps a plain `String` to
839
+ * `VARCHAR(191)`. Handlers translate this to HTTP 422: the submission can
840
+ * never be stored as it is, so a client must not retry it.
841
+ */
842
+ export declare class StoreValueTooLongError extends Error {
843
+ readonly code: "STORE_VALUE_TOO_LONG";
844
+ constructor(message?: string, options?: ErrorOptions);
845
+ }
846
+ /** Shape of any ORM error that carries a Prisma-style `code` field. */
847
+ type CodedError<C extends string = string> = {
848
+ code: C;
849
+ };
850
+ /**
851
+ * Type guard — works for `StoreNotFoundError` and ORM-specific equivalents
852
+ * (e.g. Prisma P2025). Matches on the stable `code` too, for the same
853
+ * cross-bundle reason as {@link isStorePersistence}.
854
+ */
855
+ export declare function isStoreNotFound(error: unknown): error is StoreNotFoundError | CodedError<"STORE_NOT_FOUND" | "P2025">;
856
+ /**
857
+ * Type guard — works for `StoreDuplicateError` and ORM-specific equivalents
858
+ * (e.g. Prisma P2002). Matches on the stable `code` too, for the same
859
+ * cross-bundle reason as {@link isStorePersistence}.
860
+ */
861
+ export declare function isStoreDuplicate(error: unknown): error is StoreDuplicateError | CodedError<"STORE_DUPLICATE" | "P2002">;
862
+ /**
863
+ * Type guard for `StorePersistenceError`. Matches on the stable `code` field
864
+ * in addition to `instanceof`: every consumer package bundles its own copy of
865
+ * core (tsup `noExternal`), so an instance thrown by one package fails an
866
+ * `instanceof` check against another package's class identity.
867
+ */
868
+ export declare function isStorePersistence(error: unknown): error is StorePersistenceError | CodedError<"STORE_PERSISTENCE">;
869
+ /** Type guard for `StoreLimitError`, matching on `code` for the same cross-bundle reason as {@link isStorePersistence}. */
870
+ export declare function isStoreLimit(error: unknown): error is StoreLimitError | CodedError<"STORE_LIMIT">;
871
+ /** Type guard for `StoreValueTooLongError`, matching on `code` for the same cross-bundle reason as {@link isStorePersistence}. */
872
+ export declare function isStoreValueTooLong(error: unknown): error is StoreValueTooLongError | CodedError<"STORE_VALUE_TOO_LONG">;
873
+ /** Flatten a widget `AnnotationPayload` (nested anchor + rect) into a flat `AnnotationCreateInput`. */
874
+ export declare function flattenAnnotation(ann: AnnotationPayload): AnnotationCreateInput;
875
+ /**
876
+ * Outcome of `SitepingStore.createFeedbackIfAbsent` — the record plus whether
877
+ * this very call inserted it.
878
+ */
879
+ export interface FeedbackCreateOutcome {
880
+ feedback: FeedbackRecord;
881
+ /**
882
+ * `true` when this call inserted the record, `false` when a record with the
883
+ * same `clientId` already existed and is returned instead (a replay, or a
884
+ * concurrent request that won the race).
885
+ */
886
+ created: boolean;
887
+ }
888
+ /** Paginated result returned by `SitepingStore.getFeedbacks`. */
889
+ export interface FeedbackPage {
890
+ feedbacks: FeedbackRecord[];
891
+ total: number;
892
+ }
893
+ /**
894
+ * Abstract storage interface for Siteping.
895
+ *
896
+ * Any adapter (Prisma, Drizzle, raw SQL, localStorage, etc.) implements this
897
+ * interface. The HTTP handler and widget `StoreClient` operate against
898
+ * `SitepingStore`, decoupled from the storage backend.
899
+ *
900
+ * ## Error contract
901
+ *
902
+ * - **`updateFeedback` / `deleteFeedback`**: throw `StoreNotFoundError` when
903
+ * the record does not exist.
904
+ * - **`createFeedback`**: either return the existing record on duplicate
905
+ * `clientId` (idempotent) or throw `StoreDuplicateError`. The handler
906
+ * handles both patterns — but only a throw, or the optional
907
+ * `createFeedbackIfAbsent`, tells it the record was not inserted by this
908
+ * call; stores that return the existing record should implement
909
+ * `createFeedbackIfAbsent` so creation side effects never run twice.
910
+ * - **All mutations**: when a write is accepted but cannot be persisted
911
+ * (e.g. storage quota), throw `StorePersistenceError` instead of reporting
912
+ * a phantom success. Detect it with `isStorePersistence`.
913
+ * - Other methods should not throw on empty results — return empty arrays or `null`.
914
+ */
915
+ export interface SitepingStore {
916
+ /** Create a feedback with its annotations. Idempotent on `clientId` — return existing record on duplicate, or throw `StoreDuplicateError`. Throws `StorePersistenceError` when the write cannot be persisted. */
917
+ createFeedback(data: FeedbackCreateInput): Promise<FeedbackRecord>;
918
+ /** Paginated query with optional filters. Returns empty array (not error) when no results. */
919
+ getFeedbacks(query: FeedbackQuery): Promise<FeedbackPage>;
920
+ /** Lookup by client-generated UUID. Returns `null` (not error) when not found. */
921
+ findByClientId(clientId: string): Promise<FeedbackRecord | null>;
922
+ /** Update status/resolvedAt. Throws `StoreNotFoundError` if `id` does not exist, `StorePersistenceError` when the write cannot be persisted. */
923
+ updateFeedback(id: string, data: FeedbackUpdateInput): Promise<FeedbackRecord>;
924
+ /** Delete a single record. Throws `StoreNotFoundError` if `id` does not exist, `StorePersistenceError` when the write cannot be persisted. */
925
+ deleteFeedback(id: string): Promise<void>;
926
+ /** Bulk delete all feedbacks for a project. No-op (not error) if none exist. Throws `StorePersistenceError` when the write cannot be persisted. */
927
+ deleteAllFeedbacks(projectName: string): Promise<void>;
928
+ /**
929
+ * Optional — return `true` when the record with `id` belongs to
930
+ * `projectName`, `false` otherwise (including when it does not exist).
931
+ *
932
+ * HTTP handlers use this to reject cross-project PATCH/DELETE requests.
933
+ * Implement it whenever your store serves multiple projects; when absent,
934
+ * handlers skip the ownership check and rely on `id` alone.
935
+ */
936
+ verifyProjectOwnership?(id: string, projectName: string): Promise<boolean>;
937
+ /**
938
+ * Optional — `createFeedback` that reports whether this call inserted the
939
+ * record (`created: true`) or found an existing one with the same
940
+ * `clientId` (`created: false`). The dedup check and the insert must be
941
+ * atomic, like `createFeedback`'s: of N concurrent calls with the same
942
+ * `clientId`, exactly one may report `created: true`, and all must return
943
+ * that same record. `createCollectionStore` guarantees this within one
944
+ * store instance by serializing its mutations; stores shared across
945
+ * processes need an atomic backend primitive (unique constraint,
946
+ * transaction, compare-and-set).
947
+ *
948
+ * HTTP handlers prefer it over `createFeedback` to fire creation side
949
+ * effects (webhooks) exactly once when concurrent requests race on the
950
+ * same `clientId`. Stores whose `createFeedback` throws
951
+ * `StoreDuplicateError` on a duplicate already give that signal and may
952
+ * leave it out.
953
+ */
954
+ createFeedbackIfAbsent?(data: FeedbackCreateInput): Promise<FeedbackCreateOutcome>;
955
+ /**
956
+ * Optional — append a comment to the thread of feedback `feedbackId` and
957
+ * return it. Idempotent on `data.clientId`: when a comment with that
958
+ * `clientId` exists, on any thread, return it unchanged (HTTP handlers
959
+ * refuse a replay aimed at another thread).
960
+ *
961
+ * Throws `StoreNotFoundError` when the feedback does not exist,
962
+ * `StoreLimitError` when a `client` comment meets a thread already holding
963
+ * `MAX_COMMENTS_PER_FEEDBACK` of them (a `team` comment is never refused
964
+ * for it), `StorePersistenceError` when the
965
+ * write cannot be persisted. A comment is not a change of the feedback
966
+ * itself: its `updatedAt` stays as it was.
967
+ *
968
+ * A store that implements it returns every feedback's thread on
969
+ * `FeedbackRecord.comments`, oldest first, and deletes a thread with its
970
+ * feedback. A store without it (and `deleteComment`) has no threads:
971
+ * HTTP handlers serve empty threads and answer comment writes with 501.
972
+ */
973
+ addComment?(feedbackId: string, data: CommentCreateInput): Promise<CommentRecord>;
974
+ /**
975
+ * Optional — delete comment `commentId` from the thread of `feedbackId`.
976
+ * Scoped to that thread, which the caller was authorized for: throws
977
+ * `StoreNotFoundError` when the feedback does not exist or the comment is
978
+ * not on its thread, `StorePersistenceError` when the write cannot be
979
+ * persisted.
980
+ */
981
+ deleteComment?(feedbackId: string, commentId: string): Promise<void>;
982
+ }
983
+ /** Payload sent from the widget to the server when submitting feedback. */
984
+ export interface FeedbackPayload {
985
+ projectName: string;
986
+ type: FeedbackType;
987
+ message: string;
988
+ url: string;
989
+ /**
990
+ * Parameterized URL template (e.g. `/orders/:orderId`) supplied by
991
+ * `SitepingConfig.getPageScope()`. Null when the host did not provide one.
992
+ */
993
+ urlPattern?: string | null | undefined;
994
+ viewport: string;
995
+ userAgent: string;
996
+ authorName: string;
997
+ authorEmail: string;
998
+ annotations: AnnotationPayload[];
999
+ /** Client-generated UUID for deduplication */
1000
+ clientId: string;
1001
+ /**
1002
+ * Base64 JPEG `data:` URL of the annotated area. Captured by the widget
1003
+ * when `enableScreenshot: true` is set in `SitepingConfig`. Null when
1004
+ * disabled or when capture failed silently.
1005
+ */
1006
+ screenshotDataUrl?: string | null | undefined;
1007
+ /**
1008
+ * Annotation rect position within the screenshot image — see
1009
+ * `ScreenshotRegion`. Null/absent when no screenshot was captured or the
1010
+ * capture predates contextual framing.
1011
+ */
1012
+ screenshotRegion?: ScreenshotRegion | null | undefined;
1013
+ /**
1014
+ * Snapshot of the last few console messages and failed network requests
1015
+ * captured at submit time when `captureDiagnostics` is enabled.
1016
+ */
1017
+ diagnostics?: DiagnosticsSnapshot | null | undefined;
1018
+ }
1019
+ /** Single source of truth for console diagnostic severity levels. */
1020
+ export declare const CONSOLE_DIAGNOSTIC_LEVELS: readonly ["log", "info", "warn", "error"];
1021
+ /** Severity levels persisted in `ConsoleDiagnosticEntry`. */
1022
+ export type ConsoleDiagnosticLevel = (typeof CONSOLE_DIAGNOSTIC_LEVELS)[number];
1023
+ /** A single console entry captured by `ConsoleBuffer`. */
1024
+ export interface ConsoleDiagnosticEntry {
1025
+ level: ConsoleDiagnosticLevel;
1026
+ /** ISO 8601 timestamp captured at log time. */
1027
+ timestamp: string;
1028
+ /** Best-effort string representation of the original console args. */
1029
+ message: string;
1030
+ }
1031
+ /** A single failed network request captured by `NetworkBuffer`. */
1032
+ export interface NetworkDiagnosticEntry {
1033
+ url: string;
1034
+ method: string;
1035
+ /** HTTP status; 0 when the request never reached the server. */
1036
+ status: number;
1037
+ /** End-to-end duration in ms. */
1038
+ durationMs: number;
1039
+ /** ISO 8601 timestamp at the moment the request was initiated. */
1040
+ timestamp: string;
1041
+ }
1042
+ /**
1043
+ * Diagnostics captured by the widget when `captureDiagnostics` is enabled.
1044
+ *
1045
+ * Both arrays are bounded (default: 50 console / 20 network). Adapters that
1046
+ * support diagnostics should persist this as a JSON blob alongside the
1047
+ * feedback so reviewers can replay the context that led to the report.
1048
+ */
1049
+ export interface DiagnosticsSnapshot {
1050
+ console: ConsoleDiagnosticEntry[];
1051
+ network: NetworkDiagnosticEntry[];
1052
+ }
1053
+ /**
1054
+ * Length caps for `AnchorData.elementTag` / `elementId`, shared by the widget
1055
+ * (which bounds what it captures) and the HTTP adapter (which rejects a longer
1056
+ * tag and drops a longer id) so the two can't drift. 191 fits Prisma's default `String` column
1057
+ * on MySQL (`VARCHAR(191)`); real tag names and ids are far shorter.
1058
+ */
1059
+ export declare const ANCHOR_ELEMENT_TAG_MAX = 191;
1060
+ export declare const ANCHOR_ELEMENT_ID_MAX = 191;
1061
+ /** DOM anchoring data for re-attaching annotations to page elements. */
1062
+ export interface AnchorData {
1063
+ /**
1064
+ * CSS selector generated by @medv/finder — primary anchor. Inside open
1065
+ * shadow roots: one selector per tree, outermost host first, joined by
1066
+ * `" >>> "` (e.g. `"#pricing >>> .plan-title"`).
1067
+ */
1068
+ cssSelector: string;
1069
+ /**
1070
+ * XPath — fallback 1. Inside a shadow root it is relative to that root
1071
+ * (`./…`) and informational only: XPath cannot enter shadow trees.
1072
+ */
1073
+ xpath: string;
1074
+ /** First ~120 chars of element innerText — empty string if none */
1075
+ textSnippet: string;
1076
+ /** Tag name for validation (e.g. "DIV", "SECTION") */
1077
+ elementTag: string;
1078
+ /** Element id attribute if available — most stable */
1079
+ elementId?: string | undefined;
1080
+ /** ~32 chars of text before this element in document flow (disambiguation) */
1081
+ textPrefix: string;
1082
+ /** ~32 chars of text after this element in document flow (disambiguation) */
1083
+ textSuffix: string;
1084
+ /** Structural fingerprint: "childCount:siblingIdx:attrHash" */
1085
+ fingerprint: string;
1086
+ /** Text content of adjacent sibling elements (context) */
1087
+ neighborText: string;
1088
+ /**
1089
+ * Semantic anchor identifier from the closest ancestor's `data-feedback-anchor`
1090
+ * attribute. When set, this is the highest-priority re-anchoring signal —
1091
+ * hosts deliberately place these on layout/section roots that survive
1092
+ * viewport changes and DOM refactors.
1093
+ */
1094
+ anchorKey?: string | null | undefined;
1095
+ }
1096
+ /**
1097
+ * Where the client's annotation rect sits within the captured screenshot,
1098
+ * as fractions [0, 1] of the image dimensions. The widget captures context
1099
+ * around the drawn rect and records the rect's position here so dashboards
1100
+ * can re-render the annotation on top of the image. Survives downscaling
1101
+ * (fractions are resolution-independent).
1102
+ */
1103
+ export interface ScreenshotRegion {
1104
+ /** X offset of the rect as fraction of image width — [0, 1] */
1105
+ xPct: number;
1106
+ /** Y offset of the rect as fraction of image height — [0, 1] */
1107
+ yPct: number;
1108
+ /** Rect width as fraction of image width — [0, 1] */
1109
+ wPct: number;
1110
+ /** Rect height as fraction of image height — [0, 1] */
1111
+ hPct: number;
1112
+ }
1113
+ /** Drawn rectangle coordinates as percentages relative to the anchor element. */
1114
+ export interface RectData {
1115
+ /** X offset as fraction of anchor element width — must be in range [0, 1] */
1116
+ xPct: number;
1117
+ /** Y offset as fraction of anchor element height — must be in range [0, 1] */
1118
+ yPct: number;
1119
+ /** Width as fraction of anchor element width — must be in range [0, 1] */
1120
+ wPct: number;
1121
+ /** Height as fraction of anchor element height — must be in range [0, 1] */
1122
+ hPct: number;
1123
+ }
1124
+ /** Annotation data sent as part of a feedback submission. */
1125
+ export interface AnnotationPayload {
1126
+ anchor: AnchorData;
1127
+ rect: RectData;
1128
+ scrollX: number;
1129
+ scrollY: number;
1130
+ viewportW: number;
1131
+ viewportH: number;
1132
+ devicePixelRatio: number;
1133
+ }
1134
+ /**
1135
+ * Feedback record as returned by the API — derived from
1136
+ * {@link FeedbackRecord}: dates are serialized to ISO strings and `clientId`
1137
+ * is omitted (server-side dedup concern, never exposed on the wire), on the
1138
+ * record and on each comment of its thread. Adding a field to
1139
+ * `FeedbackRecord` updates this type automatically.
1140
+ *
1141
+ * Note: `authorEmail` may be an empty string — HTTP adapters redact it for
1142
+ * unauthenticated requests; the full value requires a Bearer-authenticated
1143
+ * request.
1144
+ */
1145
+ export type FeedbackResponse = Prettify<Serialized<Omit<FeedbackRecord, "clientId" | "comments">> & {
1146
+ /** The thread, oldest first — always sent by `@siteping/server`, absent from servers that predate comments. */
1147
+ comments?: CommentResponse[] | undefined;
1148
+ /**
1149
+ * What the requester may do with this feedback — always sent by
1150
+ * `@siteping/server`. Absent from servers that predate it, and in store
1151
+ * mode: nothing is refused then.
1152
+ */
1153
+ permissions?: FeedbackPermissions | undefined;
1154
+ }>;
1155
+ /**
1156
+ * What a requester may do with one feedback, as the server's access policy
1157
+ * decides — so clients hide the actions it would refuse. The server still
1158
+ * enforces every one of them.
1159
+ */
1160
+ export interface FeedbackPermissions {
1161
+ /** Change its status: resolve, reopen, … */
1162
+ canChangeStatus: boolean;
1163
+ /** Delete it. */
1164
+ canDelete: boolean;
1165
+ /** Reply in its thread. */
1166
+ canComment: boolean;
1167
+ /** Delete replies from its thread. */
1168
+ canDeleteComment: boolean;
1169
+ }
1170
+ /** What a requester may do with a whole project, sent with each list. */
1171
+ export interface FeedbackListPermissions {
1172
+ /** Delete every feedback of the project at once. */
1173
+ canDeleteAll: boolean;
1174
+ }
1175
+ /**
1176
+ * Annotation record as returned by the API — {@link AnnotationRecord} with
1177
+ * `createdAt` serialized to an ISO string.
1178
+ */
1179
+ export type AnnotationResponse = Prettify<Serialized<AnnotationRecord>>;
1180
+ /**
1181
+ * Comment as returned by the API — {@link CommentRecord} with `createdAt`
1182
+ * serialized and `clientId` omitted, like on `FeedbackResponse`. Its
1183
+ * `authorEmail` is redacted on the same terms as the feedback's.
1184
+ */
1185
+ export type CommentResponse = Prettify<Serialized<Omit<CommentRecord, "clientId">>>;
1186
+ /** What the store behind an endpoint supports — advertised on every list response. */
1187
+ export interface SitepingCapabilities {
1188
+ /** Whether comments can be posted: the store implements `addComment`. */
1189
+ comments: boolean;
1190
+ /**
1191
+ * Whether comments can be deleted: the store implements `deleteComment`.
1192
+ * Sent by `@siteping/server`; a client that does not delete (the widget)
1193
+ * leaves it out.
1194
+ */
1195
+ deleteComments?: boolean | undefined;
1196
+ }
1197
+ /** Paginated `FeedbackResponse` shape returned by the API. */
1198
+ export interface FeedbackResponseList {
1199
+ feedbacks: FeedbackResponse[];
1200
+ total: number;
1201
+ /** Always sent by `@siteping/server` — absent from servers that predate it. */
1202
+ capabilities?: SitepingCapabilities | undefined;
1203
+ /** Always sent by `@siteping/server` — absent from servers that predate it. */
1204
+ permissions?: FeedbackListPermissions | undefined;
1205
+ }
1206
+ export {};