@siteping/server 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/deep-link.d.cts +10 -0
- package/dist/deep-link.d.ts +10 -0
- package/dist/errors.d.cts +46 -16
- package/dist/errors.d.ts +46 -16
- package/dist/i18n.d.cts +14 -10
- package/dist/i18n.d.ts +14 -10
- package/dist/index.cjs +536 -259
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +156 -71
- package/dist/index.d.ts +156 -71
- package/dist/index.js +536 -259
- package/dist/index.js.map +1 -1
- package/dist/logger.d.cts +30 -0
- package/dist/logger.d.ts +30 -0
- package/dist/schema.d.cts +31 -0
- package/dist/schema.d.ts +31 -0
- package/dist/screenshot-storage.d.cts +14 -17
- package/dist/screenshot-storage.d.ts +14 -17
- package/dist/siteping-core.d.cts +8 -5
- package/dist/siteping-core.d.ts +8 -5
- package/dist/store-helpers.d.cts +13 -9
- package/dist/store-helpers.d.ts +13 -9
- package/dist/text.d.cts +28 -0
- package/dist/text.d.ts +28 -0
- package/dist/types.d.cts +114 -37
- package/dist/types.d.ts +114 -37
- package/dist/wire.d.cts +6 -1
- package/dist/wire.d.ts +6 -1
- package/openapi.json +1532 -0
- package/package.json +7 -5
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../core/src/email.ts","../../core/src/type-utils.ts","../../core/src/types.ts","../src/constants.ts","../src/access.ts","../src/cors.ts","../src/validation.ts","../src/operations/create-comment.ts","../src/webhooks.ts","../src/operations/create-feedback.ts","../src/operations/delete-comment.ts","../src/operations/delete-feedback.ts","../src/operations/list-feedbacks.ts","../src/operations/update-feedback.ts","../src/csrf.ts","../src/pipeline.ts","../src/handler.ts"],"sourcesContent":["/**\n * The one email pattern every Siteping surface validates against.\n *\n * The widget's identity modal and the HTTP handler's schema used to disagree\n * (a permissive regex on one side, an ASCII-only default on the other), so an\n * address the modal accepted — and persisted in localStorage for good — came\n * back as a 400 on every submission afterwards. One pattern, imported on both\n * sides, makes that drift impossible.\n *\n * Deliberately Unicode-aware: internationalised local parts and domains\n * (`françois@exemple.fr`, `user@münchen.de`, `user@हिंदी.भारत`) are real\n * addresses and common in the audience this widget serves. Combining marks\n * (`\\p{M}`) are allowed alongside letters — scripts like Devanagari need them,\n * and NFD-composed input spells `ç` as `c` + U+0327 — except as the first\n * character of a domain label. Structure follows the usual rules — a local\n * part of 1 to 64 characters with no leading, trailing or doubled dot,\n * dot-separated domain labels of at most 63 characters that neither start nor\n * end with a hyphen, and a final label of at least two characters.\n */\nexport const EMAIL_PATTERN =\n /^(?!\\.)(?!.*\\.\\.)[\\p{L}\\p{M}\\p{N}!#$%&'*+/=?^_`{|}~.-]{0,63}[\\p{L}\\p{M}\\p{N}!#$%&'*+/=?^_`{|}~-]@(?:[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?\\.)+[\\p{L}\\p{N}][\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}]$/u;\n\n/** Whether `value` is an email address Siteping accepts — see {@link EMAIL_PATTERN}. */\nexport function isValidEmail(value: string): boolean {\n return EMAIL_PATTERN.test(value);\n}\n","/**\n * General-purpose TypeScript utility types used across `@siteping/*`.\n *\n * These are kept dependency-free and re-exported from the package entry\n * so adapters and integrators can rely on the same primitives the core\n * uses internally.\n */\n\n/**\n * Force TypeScript to expand a computed type into a flat object literal in\n * tooltips and error messages. Purely cosmetic — same structural type, just\n * easier to read.\n *\n * @example\n * type Raw = Omit<FeedbackRecord, \"annotations\"> & { annotations: number };\n * type Pretty = Prettify<Raw>; // displayed as a flat object\n */\nexport type Prettify<T> = { [K in keyof T]: T[K] } & {};\n\n/**\n * Returns `Y` when `A` is exactly assignable to `B` and vice-versa,\n * otherwise `N`. Powers compile-time equality assertions.\n */\nexport type IfEquals<A, B, Y = true, N = false> =\n (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? Y : N;\n\n/**\n * Compile-time exact-type guard — resolves to `true` when `Actual` and\n * `Expected` are identical, `never` otherwise. Assign the result to a\n * `const _lock: AssertEqual<A, B> = true;` so any drift becomes a compile\n * error at the declaration site.\n */\nexport type AssertEqual<Actual, Expected> = IfEquals<Actual, Expected, true, never>;\n\n/**\n * JSON-serialized shape of `T` — the wire form produced by `Response.json()`\n * / `JSON.stringify`: `Date` becomes ISO `string` (nullability preserved),\n * arrays are serialized element-wise, everything else is untouched.\n *\n * Used to derive the `*Response` API types from the `*Record` store types so\n * the two can never drift: add a field to `FeedbackRecord` and\n * `FeedbackResponse` follows automatically.\n */\nexport type Serialized<T> = {\n [K in keyof T]: T[K] extends Date\n ? string\n : T[K] extends Date | null\n ? string | null\n : T[K] extends (infer U)[]\n ? Serialized<U>[]\n : T[K];\n};\n\n/**\n * Read-only all the way down, for plain data that is deeply frozen at\n * runtime: nested objects get read-only properties, arrays become\n * `readonly` arrays.\n */\nexport type DeepReadonly<T> = T extends readonly (infer U)[]\n ? readonly DeepReadonly<U>[]\n : T extends object\n ? { readonly [K in keyof T]: DeepReadonly<T[K]> }\n : T;\n\n/**\n * Type guard that narrows `value` to a non-null `Record<PropertyKey, unknown>`.\n * Useful when validating arbitrary inputs before reading fields.\n */\nexport function isRecord(value: unknown): value is Record<PropertyKey, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\n/**\n * Returns true when `value` is an object that exposes the requested key.\n * Type-narrows `value` so the property can be accessed without further\n * casting — a strictly typed replacement for `\"k\" in obj`.\n *\n * Named after the standardised `Object.hasOwn` helper rather than the\n * legacy `Object.prototype.hasOwnProperty`, which the linter forbids\n * shadowing.\n */\nexport function hasOwn<K extends PropertyKey>(value: unknown, key: K): value is Record<K, unknown> {\n return isRecord(value) && key in value;\n}\n","import { type AssertEqual, type DeepReadonly, hasOwn, type Prettify, type Serialized } from \"./type-utils.js\";\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\n/** FAB anchor — bottom-corner placement supported by the widget. */\nexport type SitepingPosition = \"bottom-right\" | \"bottom-left\";\n\n/** Visual theme — `auto` resolves to `light` or `dark` via system preference. */\nexport type SitepingTheme = \"light\" | \"dark\" | \"auto\";\n\n/** Built-in UI locales shipped with the widget. */\nexport const BUILTIN_LOCALES = [\"en\", \"fr\", \"de\", \"es\", \"it\", \"pt\", \"ru\", \"ja\", \"ko\"] as const;\nexport type BuiltinLocale = (typeof BUILTIN_LOCALES)[number];\n\n/**\n * Locale identifier accepted by the widget. Built-in locales are kept as\n * literal strings so editors auto-complete them, but arbitrary BCP-47 tags\n * are also accepted (custom dictionaries registered via `registerLocale`).\n */\nexport type SitepingLocale = BuiltinLocale | (string & {});\n\n/**\n * Reasons reported through `SitepingConfig.onSkip` — production environment,\n * viewport narrower than `minViewportWidth` (`\"mobile\"`), or server-side\n * rendering (no `window`/`document`).\n */\nexport type SitepingSkipReason = \"production\" | \"mobile\" | \"ssr\";\n\n/** Per-channel + per-buffer-size diagnostics configuration. */\nexport interface DiagnosticsCaptureOptions {\n console?: boolean | undefined;\n network?: boolean | undefined;\n /** Console buffer size — default and maximum 50 (the server's cap; larger values are clamped). */\n maxConsoleEntries?: number | undefined;\n /** Failed-request buffer size — default and maximum 20 (the server's cap; larger values are clamped). */\n maxNetworkEntries?: number | undefined;\n}\n\n/** Identity payload supplied by the host application — bypasses the modal. */\nexport interface SitepingIdentity {\n name: string;\n email: string;\n}\n\n/**\n * Max length of an identity's `name` and `email` — the HTTP schema's\n * `authorName` / `authorEmail` cap. The widget's modal enforces it so a value\n * it persists is never a 400 on every later submission.\n */\nexport const IDENTITY_FIELD_MAX_LENGTH = 200;\n\n/** Deep-link configuration — controls how a feedback id is read from the URL. */\nexport interface SitepingDeepLinkOptions {\n /** Query parameter name carrying the feedback id. Defaults to `\"siteping\"`. */\n param?: string | undefined;\n}\n\n/**\n * The feedback handed to panel action callbacks: a detached, deeply frozen\n * copy, typed read-only all the way down. Type your own helpers with it — a\n * `FeedbackResponse` parameter does not accept the frozen copy.\n */\nexport type SitepingPanelActionFeedback = DeepReadonly<FeedbackResponse>;\n\n/**\n * Helpers passed to a panel action's `onAction` as its second argument.\n * Both do nothing once the widget has been destroyed.\n */\nexport interface SitepingPanelActionContext {\n /**\n * Re-fetch the panel list and markers, then re-render the detail view with\n * the updated feedback — or go back to the list when it no longer matches\n * the panel filters. Call it after your action changed the feedback\n * server-side (e.g. moved it to `in_progress` once a ticket exists).\n */\n refresh: () => Promise<void>;\n /** Close the feedback panel. */\n close: () => void;\n}\n\n/**\n * Fields shared by both kinds of {@link SitepingPanelAction}.\n *\n * Do not use this type directly — use {@link SitepingPanelAction}.\n */\nexport interface SitepingPanelActionBase {\n /** Stable, unique identifier — becomes `data-action-id` on the rendered control. */\n id: string;\n /**\n * Visible label, rendered as plain text. Host-provided verbatim — not\n * routed through the widget i18n, since hosts localize their own strings.\n */\n label: string;\n /**\n * Optional inline SVG markup rendered before the label. It is parsed\n * inertly and reduced to plain shapes — scripts, event handlers, links,\n * styles and external references are dropped — but keep it static markup\n * you control. Markup that is not an `<svg>` is ignored with a warning.\n */\n icon?: string | undefined;\n /**\n * Per-feedback visibility predicate. Return `false` to omit the action\n * for that feedback. Defaults to always visible. A throw hides the action\n * and is reported through `onError`.\n */\n visible?: ((feedback: SitepingPanelActionFeedback) => boolean) | undefined;\n}\n\n/** A panel action rendered as a button that runs host code. */\nexport interface SitepingPanelButtonAction extends SitepingPanelActionBase {\n /**\n * Invoked on click. While a returned promise is pending the detail view's\n * action buttons are disabled and the clicked button shows a spinner —\n * also after `context.refresh()` re-renders the view, and when the user\n * comes back to that feedback.\n * Throws and rejections are reported through `onError` and restore the\n * buttons; the detail view stays open either way.\n */\n onAction: (feedback: SitepingPanelActionFeedback, context: SitepingPanelActionContext) => void | Promise<void>;\n /** Not available on a button action — use either `onAction` or `href`, never both. */\n href?: never;\n}\n\n/** A panel action rendered as a link. */\nexport interface SitepingPanelLinkAction extends SitepingPanelActionBase {\n /**\n * Link target — a URL, or a function building one from the feedback.\n * Relative URLs resolve against the page. Only `http:`, `https:` and\n * `mailto:` are rendered: a static `href` with any other scheme\n * (`javascript:` included) skips the action with a warning; a function\n * returning one hides the action and is reported through `onError`. Web\n * links open in a new tab with `rel=\"noopener noreferrer\"`.\n */\n href: string | ((feedback: SitepingPanelActionFeedback) => string);\n /** Not available on a link action — use either `onAction` or `href`, never both. */\n onAction?: never;\n}\n\n/**\n * A host-defined action rendered in the feedback detail view, below the\n * built-in Resolve / Delete buttons: a button running your code\n * (`onAction`) or a link (`href`) — never both.\n *\n * Hosts use this to bridge feedbacks into their own systems — create a\n * ticket, dispatch to a bot, open the feedback in their tracker — without\n * forking the panel. Callbacks receive a detached, deeply frozen copy of\n * the feedback: read it freely, it can never alter what the panel displays.\n */\nexport type SitepingPanelAction = SitepingPanelButtonAction | SitepingPanelLinkAction;\n\n/**\n * Extra request headers for HTTP mode — a static map, or a factory (sync or\n * async) invoked once per request to produce fresh values (e.g. a short-lived\n * session token).\n */\nexport type SitepingHeadersOption =\n | Record<string, string>\n | (() => Record<string, string> | Promise<Record<string, string>>);\n\n/**\n * Options shared by both widget modes (HTTP and direct store).\n *\n * Do not use this type directly — use {@link SitepingConfig}, the\n * discriminated union that adds the mode-specific fields.\n */\nexport interface SitepingBaseConfig {\n /** Required — project identifier used to scope feedbacks */\n projectName: string;\n /** FAB position — defaults to 'bottom-right' */\n position?: SitepingPosition | undefined;\n /**\n * Show the \"toggle markers visibility\" item in the FAB radial menu.\n * Defaults to `true` (current behavior). Set to `false` to hide that\n * item entirely — useful for hosts that always want markers visible\n * (e.g. dedicated review tools) or that find the eye icon redundant\n * when no marker is on screen.\n *\n * Hiding the item also removes its keyboard navigation slot — the\n * remaining two items still respond to ArrowUp/ArrowDown/Home/End.\n * The marker-visibility state itself is unaffected; markers stay\n * visible (the previous default state) and `annotations:toggle` is\n * simply never emitted from the FAB.\n */\n showAnnotationsToggle?: boolean | undefined;\n /** Accent color for the widget UI — defaults to '#0066ff' */\n accentColor?: string | undefined;\n /**\n * Render the widget even when it would normally be skipped — this bypasses\n * BOTH the production-environment guard AND the `minViewportWidth` guard.\n * It does NOT bypass the SSR guard: without `window`/`document` the widget\n * never renders and `onSkip(\"ssr\")` fires instead.\n * Defaults to false. Use it for dedicated review tools, staging environments,\n * or responsive testing where you always want the widget present.\n */\n forceShow?: boolean | undefined;\n /**\n * Minimum viewport width (px) at or above which the widget renders. Below it,\n * the widget is skipped and `onSkip(\"mobile\")` fires. Defaults to `0`: the\n * widget renders at every width, and phones get a compact layout (bottom\n * sheets, touch-sized controls).\n *\n * Set it (e.g. `768`) to keep the widget off small screens, or use\n * `forceShow` to bypass the viewport check entirely.\n */\n minViewportWidth?: number | undefined;\n /** Enable debug logging of lifecycle events — defaults to false */\n debug?: boolean | undefined;\n /** Color theme — defaults to 'light' */\n theme?: SitepingTheme | undefined;\n /** UI locale — defaults to 'en'. Built-in: en, fr, de, es, it, pt (Brazilian), ru, ja, ko. Any other string falls back to English. */\n locale?: SitepingLocale | undefined;\n /**\n * Returns the current page scope for annotations and panel filtering.\n * Called on initial markers load and on `instance.refresh()`.\n *\n * Default: `{ url: window.location.pathname, urlPattern: null }` — annotations\n * are scoped strictly to the current pathname.\n *\n * Apps with parameterized routes (e.g. React Router) should return both the\n * concrete URL and the route template (e.g. `/orders/:orderId`) so the panel\n * can offer a \"this type of page\" filter that groups feedbacks by template.\n */\n getPageScope?: (() => PageScope) | undefined;\n /**\n * When true (default), the widget filters initial markers and panel results\n * by `feedback.url === scope.url`, so annotations created on one page never\n * leak to other pages — even if their CSS selector accidentally matches.\n * Set to `false` to revert to the legacy project-wide behavior.\n */\n scopeAnnotationsByUrl?: boolean | undefined;\n /**\n * Capture a JPEG screenshot of the annotated area on submit. Defaults to\n * `false` — opt-in because:\n *\n * - it adds runtime weight (~60 KB gzip dynamic chunk for html2canvas-pro,\n * loaded only on first capture),\n * - it embeds page content in the feedback (privacy/GDPR consideration —\n * inform end users in your widget host UI when enabling).\n *\n * `html2canvas-pro` ships as a regular dependency of `@siteping/widget` so the\n * dynamic import always resolves; you don't need to install anything extra.\n *\n * **Masking sensitive elements:** add `data-siteping-ignore=\"true\"` to any\n * element you do NOT want captured (password fields, credit-card forms,\n * API tokens shown in the UI, etc.). The capture predicate skips matching\n * elements *and their descendants*. Do this BEFORE turning on screenshots\n * in production — once a feedback is saved, the screenshot is in your DB\n * (or object storage) regardless of what was on the page.\n */\n enableScreenshot?: boolean | undefined;\n /**\n * Enable right-click (`contextmenu`) to instantly open the comment composer\n * at the cursor location. When enabled, a document-level listener intercepts\n * right-clicks, prevents the browser's native context menu, and enters the\n * annotation flow anchored to the element under the cursor. Defaults to\n * `false` — the browser's native context menu is never hijacked unless the\n * host explicitly opts in.\n *\n * Keyboard-triggered context menus (≣ Menu key, Shift+F10) always get the\n * native menu; only mouse right-click and touch/pen long-press open the\n * composer — a long-press only where the browser fires `contextmenu` for\n * it, which iOS and iPadOS never do (see the note below).\n *\n * **Modifier-key escape hatch:** holding Shift, Ctrl, Alt, or Meta while\n * right-clicking always falls through to the native context menu, giving\n * users (and devtools) an escape hatch regardless of this setting.\n *\n * Right-clicks on SitePing's own UI (FAB, panel, markers, popup) are\n * ignored — the native menu is shown as expected.\n *\n * Note: on Android, `contextmenu` fires on long-press — touch users open the\n * composer by long-pressing, on phones too since the widget renders at every\n * width by default (see `minViewportWidth`). On iPhone and iPad a long-press\n * never fires `contextmenu` (WebKit bug 213953): users there open annotate\n * mode from the floating button and tap the element.\n */\n enableRightClickComment?: boolean | undefined;\n /**\n * Capture the last few `console.*` calls and failed network requests\n * (HTTP >= 400 or network error) at the moment a feedback is submitted.\n *\n * Lets reviewers replay the technical context that led to the report —\n * stack traces, 500 responses, dead third-party scripts. Great for the\n * \"the page just doesn't work\" feedback that contains zero detail.\n *\n * - `true` — capture with defaults (50 console / 20 network entries).\n * - `false` (default) — no capture, no monkey-patching.\n * - object — per-channel toggles + smaller buffer sizes (values above the\n * 50 / 20 server caps are clamped so submissions never fail validation).\n *\n * **Privacy considerations:** console messages may contain anything the\n * host page logs, including user data. Failed network requests record the\n * URL without its credentials, query string or hash, and never the\n * response body.\n * Inform end users before enabling in environments where they might log\n * sensitive values.\n */\n captureDiagnostics?: boolean | DiagnosticsCaptureOptions | undefined;\n /** Called when the widget is skipped (production mode, viewport under `minViewportWidth`, SSR — no DOM) */\n onSkip?: (reason: SitepingSkipReason) => void;\n /**\n * Auto-focus a specific annotation when its ID appears in the URL query\n * string. Lets hosts deeplink directly into a feedback from external\n * systems (Zammad tickets, Slack notifications, dashboard rows).\n *\n * When enabled, the widget reads the configured query parameter from\n * `window.location.search` right after the initial markers load. If the\n * value matches a visible feedback ID, the widget scrolls the annotation\n * into view, pins its highlight, and pulses the marker — the same visual\n * affordance a marker click produces.\n *\n * - `false` / `undefined` (default): no URL parsing. Existing behavior\n * unchanged, no host URL inspection.\n * - `true`: enabled with default query parameter name `siteping`.\n * - object: enabled with a custom parameter name. Use this to avoid\n * clashes with host-app query keys.\n *\n * Only the initial load triggers focus. Subsequent URL changes (SPA\n * navigation, `history.pushState`, hash updates) are ignored —\n * deliberate, to avoid surprising re-scrolls during normal browsing.\n * Hosts that need re-focus on route change can call\n * `instance.focusFeedback(id)` explicitly.\n */\n deepLink?: boolean | SitepingDeepLinkOptions | undefined;\n /**\n * Automatically re-fetch feedbacks when the page changes during client-side\n * (SPA) navigation. Enabled by default.\n *\n * The widget is normally mounted once (singleton) inside a persistent layout\n * — e.g. a Next.js App Router `layout.tsx`, which does NOT remount on\n * client-side navigation. Without this, init runs a single time and both the\n * panel list and the page markers stay frozen on the page where the widget\n * first mounted. With it on, the widget patches the History API\n * (`pushState`/`replaceState`, which SPA routers call instead of triggering\n * `popstate`) and listens for `popstate`/`hashchange`, then re-fetches when\n * the scope key (`getPageScope().url` + template) actually changes.\n *\n * This re-fetches data only — it deliberately does NOT re-focus or re-scroll\n * to an annotation (deep-link focus stays initial-load only; see `deepLink`),\n * so normal browsing is never interrupted by a surprise scroll.\n *\n * - `true` (default) — watch navigation and re-fetch on route change.\n * - `false` — never touch the History API; hosts drive updates manually via\n * `instance.refresh()`.\n */\n watchNavigation?: boolean | undefined;\n /**\n * Pre-fill author identity from the host application — typically the\n * currently signed-in user. When set, the widget uses these values\n * directly and never shows the identity modal, even on first feedback.\n *\n * Use case: SSO-integrated apps where the end user is already\n * authenticated by the host. Avoids the awkward \"enter your name and\n * email\" prompt for users the host already knows.\n *\n * When unset (default), the widget falls back to localStorage and shows\n * the modal on first feedback as before — existing behavior unchanged.\n *\n * Note: `config.identity` is **not** persisted to localStorage. It is\n * read at widget init time, not on every render. Hosts that need live\n * identity updates after sign-in/sign-out should currently remount the\n * widget (e.g. via a React `key` on the wrapping component). See\n * https://github.com/NeosiaNexus/SitePing/issues/85 for tracking a\n * future enhancement that propagates identity updates without a remount.\n */\n identity?: SitepingIdentity | undefined;\n /**\n * Host-defined actions rendered in the feedback detail view, below the\n * built-in Resolve and Delete buttons. Read once when the panel loads:\n * entries without a non-empty `id` and `label`, without exactly one of\n * `onAction` / `href`, with an unsafe static `href`, or reusing an earlier\n * `id` are skipped with a console warning. See {@link SitepingPanelAction}.\n */\n panelActions?: readonly SitepingPanelAction[] | undefined;\n /**\n * Reviewer mode: hide the actions that triage feedback — resolve, reopen,\n * delete, the bulk actions, \"Delete all\" — and keep creating, browsing\n * and replying. Defaults to `false`. The server's `permissions` hide\n * what it would refuse on top of it. It only hides: the server decides\n * what it accepts. Read once when the panel loads.\n */\n readOnly?: boolean | undefined;\n\n // Events\n /** Called when the feedback panel is opened. */\n onOpen?: (() => void) | undefined;\n /** Called when the feedback panel is closed. */\n onClose?: (() => void) | undefined;\n /** Called after a feedback is successfully submitted. */\n onFeedbackSent?: ((feedback: FeedbackResponse) => void) | undefined;\n /** Called after a reply is posted from the panel's discussion thread. */\n onCommentAdded?: ((comment: CommentResponse) => void) | undefined;\n /**\n * Called when a feedback API call fails.\n *\n * The widget always emits a `SitepingError` (or a subclass:\n * `SitepingNetworkError`, `SitepingValidationError`, `SitepingAuthError`)\n * for HTTP-mode failures — host apps can `instanceof` to drive retry\n * logic, or read `error.code` (`\"NETWORK\" | \"VALIDATION\" | \"AUTH\" |\n * \"SERVER\"`) and `error.retryable` — and an `AUTH` error's `status`\n * (`401` for missing or dead credentials, `403` for a refusal). The type\n * is widened to `Error` so direct-store callers can still surface raw\n * errors without breaking the contract.\n *\n * Also receives whatever a `panelActions` callback throws or rejects with\n * (non-`Error` values are wrapped). Those host failures are not API\n * failures, so they are not emitted on the public `feedback:error` event.\n * The widget has no UI for them, so they are also always logged with\n * `console.error`, whether or not `onError` is set.\n */\n onError?: ((error: Error) => void) | undefined;\n /** Called when the user starts drawing an annotation. */\n onAnnotationStart?: (() => void) | undefined;\n /** Called when the user finishes drawing an annotation. */\n onAnnotationEnd?: (() => void) | undefined;\n}\n\n/**\n * HTTP mode — the widget talks to a server endpoint backed by a store\n * adapter (e.g. `@siteping/adapter-prisma` request handlers).\n */\nexport interface SitepingHttpConfig extends SitepingBaseConfig {\n /** HTTP endpoint that receives feedbacks (e.g. '/api/siteping'). */\n endpoint: string;\n /**\n * Convenience auth for HTTP mode — sent as `Authorization: Bearer <apiKey>`\n * on every request to `endpoint`.\n *\n * **WARNING: the widget runs in every visitor's browser, so a static key\n * configured here is public** — anyone can read it from your page source\n * and replay it against your API. Only use `apiKey` for internal tools\n * already behind your own login. On public sites, prefer `headers` with a\n * per-request factory returning a short-lived session token.\n */\n apiKey?: string | undefined;\n /**\n * Extra headers for every HTTP-mode request — a static map, or a factory\n * (sync or async) called once per request (e.g. to fetch a fresh session\n * token). Merged over the widget's generated headers, case-insensitively,\n * so an explicit `Authorization` entry overrides `apiKey`. A factory that\n * throws, rejects, or does not settle within 10 s fails the request like a\n * network error.\n */\n headers?: SitepingHeadersOption | undefined;\n /** Not available in HTTP mode — use either `endpoint` or `store`, never both. */\n store?: never;\n}\n\n/**\n * Store mode — the widget talks to a `SitepingStore` directly in the\n * browser, no server needed (demos, prototypes, localStorage persistence).\n */\nexport interface SitepingStoreConfig extends SitepingBaseConfig {\n /**\n * Direct store for client-side mode. Bypasses HTTP entirely. A send stops\n * waiting on `createFeedback` after 30 s (the call itself cannot be\n * cancelled), so a network-backed store should bound its own calls.\n */\n store: SitepingStore;\n /** Not available in store mode — use either `endpoint` or `store`, never both. */\n endpoint?: never;\n /** HTTP-mode only — meaningless without an `endpoint`. */\n apiKey?: never;\n /** HTTP-mode only — meaningless without an `endpoint`. */\n headers?: never;\n}\n\n/**\n * Configuration options for the Siteping widget.\n *\n * A discriminated union over the two transport modes: pass `endpoint`\n * (HTTP mode, optionally with `apiKey`/`headers`) **or** `store` (direct\n * client-side mode) — never both, never neither. Invalid combinations are\n * compile errors instead of runtime warnings.\n */\nexport type SitepingConfig = SitepingHttpConfig | SitepingStoreConfig;\n\n/** Instance returned by initSiteping() with lifecycle methods. */\nexport interface SitepingInstance {\n /** Remove the widget from the DOM and clean up all listeners. */\n destroy: () => void;\n /** Open the panel programmatically */\n open: () => void;\n /** Close the panel */\n close: () => void;\n /** Reload feedbacks from server */\n refresh: () => void;\n /**\n * Scroll the matching annotation into view, pin its highlight, and\n * pulse its marker. Returns `true` when a visible feedback matched the\n * given ID, `false` otherwise (unknown ID, feedback on another URL when\n * `scopeAnnotationsByUrl` filtered it out, or markers not yet loaded).\n *\n * Counterpart to the `deepLink` config option for hosts that prefer to\n * drive focus from JS (e.g., a notification click handler) instead of a\n * URL query parameter.\n */\n focusFeedback: (feedbackId: string) => boolean;\n /** Subscribe to a public widget event */\n on: <K extends keyof SitepingPublicEvents>(event: K, listener: SitepingPublicEventListener<K>) => SitepingUnsubscribe;\n /** Unsubscribe from a public widget event */\n off: <K extends keyof SitepingPublicEvents>(event: K, listener: SitepingPublicEventListener<K>) => void;\n}\n\n/** Listener signature for a single `SitepingPublicEvents` key. */\nexport type SitepingPublicEventListener<K extends keyof SitepingPublicEvents> = (\n ...args: SitepingPublicEvents[K]\n) => void;\n\n/** Disposer returned by `SitepingInstance.on` — call once to detach the listener. */\nexport type SitepingUnsubscribe = () => void;\n\n/** Events exposed to consumers via SitepingInstance.on / .off */\nexport interface SitepingPublicEvents {\n \"feedback:sent\": [FeedbackResponse];\n \"feedback:deleted\": [FeedbackResponse[\"id\"]];\n /** A reply was posted from the panel's discussion thread. */\n \"comment:added\": [CommentResponse];\n /**\n * A feedback API call failed. Same payload contract as\n * `SitepingConfig.onError` — a `SitepingError` subclass in HTTP mode,\n * possibly a raw `Error` in store mode.\n */\n \"feedback:error\": [Error];\n \"panel:open\": [];\n \"panel:close\": [];\n /** The user started drawing an annotation. */\n \"annotation:start\": [];\n /** The user finished drawing an annotation. */\n \"annotation:end\": [];\n}\n\n// ---------------------------------------------------------------------------\n// Feedback\n// ---------------------------------------------------------------------------\n\n/** Single source of truth for feedback types — used by both TS types and Zod schemas. */\nexport const FEEDBACK_TYPES = [\"question\", \"change\", \"bug\", \"other\"] as const;\nexport type FeedbackType = (typeof FEEDBACK_TYPES)[number];\n\n/** Single source of truth for feedback statuses. */\nexport const FEEDBACK_STATUSES = [\"open\", \"in_progress\", \"resolved\", \"wont_fix\"] as const;\nexport type FeedbackStatus = (typeof FEEDBACK_STATUSES)[number];\n\n/**\n * Terminal statuses — the feedback needs no further action. `resolvedAt` is\n * the closure timestamp: set when a feedback enters a closed status, null\n * while it is open or in progress. The derivation happens at the edge (HTTP\n * handler, dashboard) — store adapters persist whatever they are given.\n */\nexport const CLOSED_FEEDBACK_STATUSES = [\"resolved\", \"wont_fix\"] as const;\n/** A terminal status — `resolved` or `wont_fix`. */\nexport type ClosedFeedbackStatus = (typeof CLOSED_FEEDBACK_STATUSES)[number];\n\n/** Non-terminal statuses — the feedback still needs attention. */\nexport const OPEN_FEEDBACK_STATUSES = [\"open\", \"in_progress\"] as const;\n/** A non-terminal status — `open` or `in_progress`. */\nexport type OpenFeedbackStatus = (typeof OPEN_FEEDBACK_STATUSES)[number];\n\n// Adding a fifth status without assigning it to exactly one bucket is a\n// compile error here.\nconst _statusBucketsCoverAll: AssertEqual<OpenFeedbackStatus | ClosedFeedbackStatus, FeedbackStatus> = true;\nvoid _statusBucketsCoverAll;\n\n/** Whether a status is terminal (`resolved` or `wont_fix`). Narrows the status type. */\nexport function isClosedStatus(status: FeedbackStatus): status is ClosedFeedbackStatus {\n return (CLOSED_FEEDBACK_STATUSES as readonly FeedbackStatus[]).includes(status);\n}\n\n/**\n * Page scope returned by `SitepingConfig.getPageScope()`.\n *\n * - `url`: concrete page identifier — usually `window.location.pathname`,\n * used as the strict scope for marker rendering.\n * - `urlPattern`: optional parameterized template (e.g. `/orders/:orderId`)\n * used by the panel's \"this type of page\" filter to group feedbacks across\n * instances of the same page kind.\n */\nexport interface PageScope {\n url: string;\n urlPattern: string | null;\n}\n\n// ---------------------------------------------------------------------------\n// Abstract Store — adapter pattern\n// ---------------------------------------------------------------------------\n\n/** Input for creating a feedback record in the store. */\nexport interface FeedbackCreateInput {\n projectName: string;\n type: FeedbackType;\n message: string;\n status: FeedbackStatus;\n url: string;\n /**\n * Optional parameterized URL template (e.g. `/orders/:orderId`) for the page\n * where the feedback was created. Allows the panel to filter feedbacks by\n * \"this type of page\" across different instances. Null when the host did not\n * provide a `getPageScope` callback or the route has no template.\n */\n urlPattern?: string | null | undefined;\n viewport: string;\n userAgent: string;\n authorName: string;\n authorEmail: string;\n clientId: string;\n annotations: AnnotationCreateInput[];\n /**\n * Base64 JPEG `data:` URL captured by the widget at submit time.\n *\n * Adapters with a configured `ScreenshotStorage` are expected to upload\n * this and persist the returned URL on `FeedbackRecord.screenshotUrl`.\n * Adapters without storage may persist the data URL inline (memory /\n * localStorage / dev) — the widget then renders it directly.\n */\n screenshotDataUrl?: string | null | undefined;\n /**\n * Where the client's annotation rect sits within the screenshot image,\n * as fractions [0, 1] of the image dimensions. Present when the widget\n * captured context around the drawn rect; null for legacy captures that\n * were cropped exactly to the rect (dashboards then render the image\n * without an overlay).\n */\n screenshotRegion?: ScreenshotRegion | null | undefined;\n /**\n * Optional console + failed-network snapshot captured by the widget when\n * `SitepingConfig.captureDiagnostics` is enabled. Stored as JSON on\n * `FeedbackRecord.diagnostics` so reviewers can replay the context.\n */\n diagnostics?: DiagnosticsSnapshot | null | undefined;\n}\n\n/** Input for a single annotation when creating a feedback. */\nexport interface AnnotationCreateInput {\n cssSelector: string;\n xpath: string;\n textSnippet: string;\n elementTag: string;\n elementId?: string | undefined;\n textPrefix: string;\n textSuffix: string;\n fingerprint: string;\n neighborText: string;\n /**\n * Semantic anchor identifier from the closest ancestor's `data-feedback-anchor`\n * attribute. When set, this is the most stable re-anchoring signal because\n * hosts deliberately place these on layout/section roots that survive DOM\n * refactors and viewport changes. Null when no semantic ancestor exists.\n */\n anchorKey?: string | null | undefined;\n xPct: number;\n yPct: number;\n wPct: number;\n hPct: number;\n scrollX: number;\n scrollY: number;\n viewportW: number;\n viewportH: number;\n devicePixelRatio: number;\n}\n\n/** Query parameters for fetching feedbacks. */\nexport interface FeedbackQuery {\n projectName: string;\n type?: FeedbackType | undefined;\n /** Exact single-status filter. For \"any of a set\" (bucket) semantics, use `statuses`. */\n status?: FeedbackStatus | undefined;\n /**\n * Filter to feedbacks whose status is any of the listed values — bucket\n * semantics used by the panel's binary tabs (e.g. \"Open\" passes\n * `[\"open\", \"in_progress\"]`). When both `status` and `statuses` are set,\n * `statuses` wins. An empty array is treated as absent (no status filter).\n */\n statuses?: readonly FeedbackStatus[] | undefined;\n search?: string | undefined;\n page?: number | undefined;\n /**\n * Page size. Defaults to `DEFAULT_PAGE_LIMIT` (50) and is capped at\n * `MAX_PAGE_LIMIT` (100) — `clampPagination` implements both, and the\n * conformance suite checks them.\n */\n limit?: number | undefined;\n /**\n * Filter to feedbacks created on this exact URL (path). Used by the panel's\n * \"this page\" filter and by the markers loader to keep page scopes isolated.\n */\n url?: string | undefined;\n /**\n * Filter to feedbacks created on this URL pattern (e.g. `/orders/:orderId`).\n * Used by the panel's \"this type of page\" filter to group feedbacks across\n * different concrete instances of the same template.\n */\n urlPattern?: string | undefined;\n}\n\n/**\n * Update payload for patching a feedback.\n *\n * A discriminated union encoding the closure invariant: a feedback entering\n * a closed status carries its closure timestamp, an open one carries `null`.\n * `{ status: \"resolved\", resolvedAt: null }` is a compile error instead of a\n * silent data bug. Build it from a plain `FeedbackStatus` with\n * {@link toFeedbackUpdate}.\n */\nexport type FeedbackUpdateInput =\n | { status: OpenFeedbackStatus; resolvedAt: null }\n | { status: ClosedFeedbackStatus; resolvedAt: Date };\n\n/**\n * Derive the {@link FeedbackUpdateInput} for a status change — the closure\n * timestamp is stamped for closed statuses and cleared otherwise. This is\n * the edge derivation described on {@link CLOSED_FEEDBACK_STATUSES}; store\n * adapters persist the result verbatim.\n */\nexport function toFeedbackUpdate(status: FeedbackStatus, closedAt: Date = new Date()): FeedbackUpdateInput {\n return isClosedStatus(status) ? { status, resolvedAt: closedAt } : { status, resolvedAt: null };\n}\n\n/** A persisted feedback record returned by the store. */\nexport interface FeedbackRecord {\n id: string;\n type: FeedbackType;\n message: string;\n status: FeedbackStatus;\n projectName: string;\n url: string;\n /**\n * Parameterized URL template the feedback was created on.\n * Null for legacy records or hosts without `getPageScope`.\n */\n urlPattern: string | null;\n authorName: string;\n authorEmail: string;\n viewport: string;\n userAgent: string;\n clientId: string;\n resolvedAt: Date | null;\n createdAt: Date;\n updatedAt: Date;\n annotations: AnnotationRecord[];\n /**\n * Discussion thread, oldest first. A store that implements\n * `SitepingStore.addComment` returns it on every record; a store without\n * comments may leave it out, which reads as an empty thread (HTTP handlers\n * send `[]`).\n */\n comments?: CommentRecord[] | undefined;\n /**\n * URL the widget renders as `<img src>`. Either an `https://...` from a\n * configured `ScreenshotStorage`, or a `data:image/jpeg;base64,...` URL\n * inline-persisted by adapters without storage. Null when no screenshot\n * was captured (legacy records, capture failed, or host disabled it).\n */\n screenshotUrl: string | null;\n /**\n * Annotation rect position within the screenshot image, as fractions of\n * its dimensions. Null for legacy captures cropped exactly to the rect.\n */\n screenshotRegion: ScreenshotRegion | null;\n /**\n * Console + failed-network snapshot captured at submit time. Null when\n * diagnostics weren't enabled on the widget side.\n */\n diagnostics: DiagnosticsSnapshot | null;\n}\n\n/** A persisted annotation record returned by the store. */\nexport interface AnnotationRecord {\n id: string;\n feedbackId: string;\n cssSelector: string;\n xpath: string;\n textSnippet: string;\n elementTag: string;\n elementId: string | null;\n textPrefix: string;\n textSuffix: string;\n fingerprint: string;\n neighborText: string;\n /**\n * Semantic anchor identifier from `data-feedback-anchor`. Null for legacy\n * annotations or those drawn outside any anchored region.\n */\n anchorKey: string | null;\n xPct: number;\n yPct: number;\n wPct: number;\n hPct: number;\n scrollX: number;\n scrollY: number;\n viewportW: number;\n viewportH: number;\n devicePixelRatio: number;\n createdAt: Date;\n}\n\n// ---------------------------------------------------------------------------\n// Comments — the discussion thread of a feedback\n// ---------------------------------------------------------------------------\n\n/**\n * Who wrote a comment: `client` — the reviewer on the site (the widget) —\n * or `team` — the project side (the dashboard). HTTP handlers stamp\n * `client` unless the server's access policy vouches for the caller, so a\n * visitor cannot pose as the team.\n */\nexport const COMMENT_AUTHOR_ROLES = [\"client\", \"team\"] as const;\nexport type CommentAuthorRole = (typeof COMMENT_AUTHOR_ROLES)[number];\n\n/** Longest comment `body` the HTTP API accepts — the cap of a feedback `message`. */\nexport const COMMENT_BODY_MAX_LENGTH = 5000;\n\n/**\n * Most `client` comments one thread holds. List responses embed whole\n * threads, which are not paginated, so this bounds what one feedback weighs\n * however much a public endpoint is spammed. `team` comments — a role only\n * the access policy grants — neither count nor meet it: a thread spammed\n * full still takes the team's answer. Stores enforce it with\n * `StoreLimitError`; a store without an atomic primitive may overshoot it by\n * the posts that race the last free slot.\n */\nexport const MAX_COMMENTS_PER_FEEDBACK = 100;\n\n/** A persisted comment returned by the store. */\nexport interface CommentRecord {\n id: string;\n /** The feedback whose thread holds this comment. */\n feedbackId: string;\n body: string;\n authorName: string;\n /**\n * Author email, or `\"\"` when the author has none on file (a dashboard\n * user). HTTP handlers redact it exactly like `FeedbackRecord.authorEmail`.\n */\n authorEmail: string;\n authorRole: CommentAuthorRole;\n /** Client-generated id — `addComment` is idempotent on it, so a retried post never duplicates a reply. */\n clientId: string;\n createdAt: Date;\n}\n\n/** Input of `SitepingStore.addComment`. */\nexport interface CommentCreateInput {\n body: string;\n authorName: string;\n authorEmail: string;\n authorRole: CommentAuthorRole;\n clientId: string;\n}\n\n/**\n * Body of a comment `POST`. `feedbackId` is what tells it apart from a\n * feedback submission on the same endpoint; `projectName` scopes the write\n * to that project's feedbacks.\n */\nexport interface CommentPayload extends CommentCreateInput {\n projectName: string;\n feedbackId: string;\n}\n\n/** Body of a comment `DELETE` — `commentId` is what tells it apart from a feedback delete. */\nexport interface CommentDeletePayload {\n projectName: string;\n feedbackId: string;\n commentId: string;\n}\n\n// ---------------------------------------------------------------------------\n// Store errors — throw these from adapter implementations\n// ---------------------------------------------------------------------------\n\n/**\n * Thrown when a record is not found during update or delete.\n *\n * Handlers translate this to HTTP 404. Adapters MUST throw this (not\n * ORM-specific errors) so the handler layer remains ORM-agnostic.\n */\nexport class StoreNotFoundError extends Error {\n readonly code = \"STORE_NOT_FOUND\" as const;\n constructor(message = \"Record not found\", options?: ErrorOptions) {\n super(message, options);\n this.name = \"StoreNotFoundError\";\n }\n}\n\n/**\n * Thrown when a unique constraint is violated (e.g. duplicate `clientId`).\n *\n * Handlers use this to return the existing record instead of failing.\n */\nexport class StoreDuplicateError extends Error {\n readonly code = \"STORE_DUPLICATE\" as const;\n constructor(message = \"Duplicate record\", options?: ErrorOptions) {\n super(message, options);\n this.name = \"StoreDuplicateError\";\n }\n}\n\n/**\n * Thrown when a store accepts a mutation but cannot persist it — e.g.\n * `localStorage` is full (QuotaExceededError). Adapters MUST throw this rather\n * than swallow the failure, so callers learn the write was lost instead of\n * seeing a phantom success.\n */\nexport class StorePersistenceError extends Error {\n readonly code = \"STORE_PERSISTENCE\" as const;\n constructor(message = \"Failed to persist store mutation\", options?: ErrorOptions) {\n super(message, options);\n this.name = \"StorePersistenceError\";\n }\n}\n\n/**\n * Thrown when a write would break a bound of the store contract — a\n * `client` comment on a thread already holding `MAX_COMMENTS_PER_FEEDBACK`\n * of them. Handlers translate this to HTTP 409.\n */\nexport class StoreLimitError extends Error {\n readonly code = \"STORE_LIMIT\" as const;\n constructor(message = \"Store limit reached\", options?: ErrorOptions) {\n super(message, options);\n this.name = \"StoreLimitError\";\n }\n}\n\n/**\n * Thrown when a value is longer than the store can hold, though the HTTP\n * validation accepts it — on MySQL, Prisma maps a plain `String` to\n * `VARCHAR(191)`. Handlers translate this to HTTP 422: the submission can\n * never be stored as it is, so a client must not retry it.\n */\nexport class StoreValueTooLongError extends Error {\n readonly code = \"STORE_VALUE_TOO_LONG\" as const;\n constructor(message = \"Value too long for the store\", options?: ErrorOptions) {\n super(message, options);\n this.name = \"StoreValueTooLongError\";\n }\n}\n\n/** Shape of any ORM error that carries a Prisma-style `code` field. */\ntype CodedError<C extends string = string> = { code: C };\n\nfunction hasErrorCode<C extends string>(error: unknown, code: C): error is CodedError<C> {\n return hasOwn(error, \"code\") && error.code === code;\n}\n\n/**\n * Type guard — works for `StoreNotFoundError` and ORM-specific equivalents\n * (e.g. Prisma P2025). Matches on the stable `code` too, for the same\n * cross-bundle reason as {@link isStorePersistence}.\n */\nexport function isStoreNotFound(error: unknown): error is StoreNotFoundError | CodedError<\"STORE_NOT_FOUND\" | \"P2025\"> {\n if (error instanceof StoreNotFoundError) return true;\n // Another bundle's copy of core, or Prisma's P2025 (backwards compat)\n return hasErrorCode(error, \"STORE_NOT_FOUND\") || hasErrorCode(error, \"P2025\");\n}\n\n/**\n * Type guard — works for `StoreDuplicateError` and ORM-specific equivalents\n * (e.g. Prisma P2002). Matches on the stable `code` too, for the same\n * cross-bundle reason as {@link isStorePersistence}.\n */\nexport function isStoreDuplicate(\n error: unknown,\n): error is StoreDuplicateError | CodedError<\"STORE_DUPLICATE\" | \"P2002\"> {\n if (error instanceof StoreDuplicateError) return true;\n // Another bundle's copy of core, or Prisma's P2002 (backwards compat)\n return hasErrorCode(error, \"STORE_DUPLICATE\") || hasErrorCode(error, \"P2002\");\n}\n\n/**\n * Type guard for `StorePersistenceError`. Matches on the stable `code` field\n * in addition to `instanceof`: every consumer package bundles its own copy of\n * core (tsup `noExternal`), so an instance thrown by one package fails an\n * `instanceof` check against another package's class identity.\n */\nexport function isStorePersistence(error: unknown): error is StorePersistenceError | CodedError<\"STORE_PERSISTENCE\"> {\n if (error instanceof StorePersistenceError) return true;\n return hasErrorCode(error, \"STORE_PERSISTENCE\");\n}\n\n/** Type guard for `StoreLimitError`, matching on `code` for the same cross-bundle reason as {@link isStorePersistence}. */\nexport function isStoreLimit(error: unknown): error is StoreLimitError | CodedError<\"STORE_LIMIT\"> {\n if (error instanceof StoreLimitError) return true;\n return hasErrorCode(error, \"STORE_LIMIT\");\n}\n\n/** Type guard for `StoreValueTooLongError`, matching on `code` for the same cross-bundle reason as {@link isStorePersistence}. */\nexport function isStoreValueTooLong(\n error: unknown,\n): error is StoreValueTooLongError | CodedError<\"STORE_VALUE_TOO_LONG\"> {\n if (error instanceof StoreValueTooLongError) return true;\n return hasErrorCode(error, \"STORE_VALUE_TOO_LONG\");\n}\n\n// ---------------------------------------------------------------------------\n// Store helpers — shared conversion logic for adapters\n// ---------------------------------------------------------------------------\n\n/** Flatten a widget `AnnotationPayload` (nested anchor + rect) into a flat `AnnotationCreateInput`. */\nexport function flattenAnnotation(ann: AnnotationPayload): AnnotationCreateInput {\n return {\n cssSelector: ann.anchor.cssSelector,\n xpath: ann.anchor.xpath,\n textSnippet: ann.anchor.textSnippet,\n elementTag: ann.anchor.elementTag,\n elementId: ann.anchor.elementId,\n textPrefix: ann.anchor.textPrefix,\n textSuffix: ann.anchor.textSuffix,\n fingerprint: ann.anchor.fingerprint,\n neighborText: ann.anchor.neighborText,\n anchorKey: ann.anchor.anchorKey ?? null,\n xPct: ann.rect.xPct,\n yPct: ann.rect.yPct,\n wPct: ann.rect.wPct,\n hPct: ann.rect.hPct,\n scrollX: ann.scrollX,\n scrollY: ann.scrollY,\n viewportW: ann.viewportW,\n viewportH: ann.viewportH,\n devicePixelRatio: ann.devicePixelRatio,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Abstract Store — adapter pattern\n// ---------------------------------------------------------------------------\n\n/**\n * Outcome of `SitepingStore.createFeedbackIfAbsent` — the record plus whether\n * this very call inserted it.\n */\nexport interface FeedbackCreateOutcome {\n feedback: FeedbackRecord;\n /**\n * `true` when this call inserted the record, `false` when a record with the\n * same `clientId` already existed and is returned instead (a replay, or a\n * concurrent request that won the race).\n */\n created: boolean;\n}\n\n/** Paginated result returned by `SitepingStore.getFeedbacks`. */\nexport interface FeedbackPage {\n feedbacks: FeedbackRecord[];\n total: number;\n}\n\n/**\n * Abstract storage interface for Siteping.\n *\n * Any adapter (Prisma, Drizzle, raw SQL, localStorage, etc.) implements this\n * interface. The HTTP handler and widget `StoreClient` operate against\n * `SitepingStore`, decoupled from the storage backend.\n *\n * ## Error contract\n *\n * - **`updateFeedback` / `deleteFeedback`**: throw `StoreNotFoundError` when\n * the record does not exist.\n * - **`createFeedback`**: either return the existing record on duplicate\n * `clientId` (idempotent) or throw `StoreDuplicateError`. The handler\n * handles both patterns — but only a throw, or the optional\n * `createFeedbackIfAbsent`, tells it the record was not inserted by this\n * call; stores that return the existing record should implement\n * `createFeedbackIfAbsent` so creation side effects never run twice.\n * - **All mutations**: when a write is accepted but cannot be persisted\n * (e.g. storage quota), throw `StorePersistenceError` instead of reporting\n * a phantom success. Detect it with `isStorePersistence`.\n * - Other methods should not throw on empty results — return empty arrays or `null`.\n */\nexport interface SitepingStore {\n /** 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. */\n createFeedback(data: FeedbackCreateInput): Promise<FeedbackRecord>;\n /** Paginated query with optional filters. Returns empty array (not error) when no results. */\n getFeedbacks(query: FeedbackQuery): Promise<FeedbackPage>;\n /** Lookup by client-generated UUID. Returns `null` (not error) when not found. */\n findByClientId(clientId: string): Promise<FeedbackRecord | null>;\n /** Update status/resolvedAt. Throws `StoreNotFoundError` if `id` does not exist, `StorePersistenceError` when the write cannot be persisted. */\n updateFeedback(id: string, data: FeedbackUpdateInput): Promise<FeedbackRecord>;\n /** Delete a single record. Throws `StoreNotFoundError` if `id` does not exist, `StorePersistenceError` when the write cannot be persisted. */\n deleteFeedback(id: string): Promise<void>;\n /** Bulk delete all feedbacks for a project. No-op (not error) if none exist. Throws `StorePersistenceError` when the write cannot be persisted. */\n deleteAllFeedbacks(projectName: string): Promise<void>;\n /**\n * Optional — return `true` when the record with `id` belongs to\n * `projectName`, `false` otherwise (including when it does not exist).\n *\n * HTTP handlers use this to reject cross-project PATCH/DELETE requests.\n * Implement it whenever your store serves multiple projects; when absent,\n * handlers skip the ownership check and rely on `id` alone.\n */\n verifyProjectOwnership?(id: string, projectName: string): Promise<boolean>;\n /**\n * Optional — `createFeedback` that reports whether this call inserted the\n * record (`created: true`) or found an existing one with the same\n * `clientId` (`created: false`). The dedup check and the insert must be\n * atomic, like `createFeedback`'s: of N concurrent calls with the same\n * `clientId`, exactly one may report `created: true`, and all must return\n * that same record. `createCollectionStore` guarantees this within one\n * store instance by serializing its mutations; stores shared across\n * processes need an atomic backend primitive (unique constraint,\n * transaction, compare-and-set).\n *\n * HTTP handlers prefer it over `createFeedback` to fire creation side\n * effects (webhooks) exactly once when concurrent requests race on the\n * same `clientId`. Stores whose `createFeedback` throws\n * `StoreDuplicateError` on a duplicate already give that signal and may\n * leave it out.\n */\n createFeedbackIfAbsent?(data: FeedbackCreateInput): Promise<FeedbackCreateOutcome>;\n /**\n * Optional — append a comment to the thread of feedback `feedbackId` and\n * return it. Idempotent on `data.clientId`: when a comment with that\n * `clientId` exists, on any thread, return it unchanged (HTTP handlers\n * refuse a replay aimed at another thread).\n *\n * Throws `StoreNotFoundError` when the feedback does not exist,\n * `StoreLimitError` when a `client` comment meets a thread already holding\n * `MAX_COMMENTS_PER_FEEDBACK` of them (a `team` comment is never refused\n * for it), `StorePersistenceError` when the\n * write cannot be persisted. A comment is not a change of the feedback\n * itself: its `updatedAt` stays as it was.\n *\n * A store that implements it returns every feedback's thread on\n * `FeedbackRecord.comments`, oldest first, and deletes a thread with its\n * feedback. A store without it (and `deleteComment`) has no threads:\n * HTTP handlers serve empty threads and answer comment writes with 501.\n */\n addComment?(feedbackId: string, data: CommentCreateInput): Promise<CommentRecord>;\n /**\n * Optional — delete comment `commentId` from the thread of `feedbackId`.\n * Scoped to that thread, which the caller was authorized for: throws\n * `StoreNotFoundError` when the feedback does not exist or the comment is\n * not on its thread, `StorePersistenceError` when the write cannot be\n * persisted.\n */\n deleteComment?(feedbackId: string, commentId: string): Promise<void>;\n}\n\n/** Payload sent from the widget to the server when submitting feedback. */\nexport interface FeedbackPayload {\n projectName: string;\n type: FeedbackType;\n message: string;\n url: string;\n /**\n * Parameterized URL template (e.g. `/orders/:orderId`) supplied by\n * `SitepingConfig.getPageScope()`. Null when the host did not provide one.\n */\n urlPattern?: string | null | undefined;\n viewport: string;\n userAgent: string;\n authorName: string;\n authorEmail: string;\n annotations: AnnotationPayload[];\n /** Client-generated UUID for deduplication */\n clientId: string;\n /**\n * Base64 JPEG `data:` URL of the annotated area. Captured by the widget\n * when `enableScreenshot: true` is set in `SitepingConfig`. Null when\n * disabled or when capture failed silently.\n */\n screenshotDataUrl?: string | null | undefined;\n /**\n * Annotation rect position within the screenshot image — see\n * `ScreenshotRegion`. Null/absent when no screenshot was captured or the\n * capture predates contextual framing.\n */\n screenshotRegion?: ScreenshotRegion | null | undefined;\n /**\n * Snapshot of the last few console messages and failed network requests\n * captured at submit time when `captureDiagnostics` is enabled.\n */\n diagnostics?: DiagnosticsSnapshot | null | undefined;\n}\n\n/** Single source of truth for console diagnostic severity levels. */\nexport const CONSOLE_DIAGNOSTIC_LEVELS = [\"log\", \"info\", \"warn\", \"error\"] as const;\n/** Severity levels persisted in `ConsoleDiagnosticEntry`. */\nexport type ConsoleDiagnosticLevel = (typeof CONSOLE_DIAGNOSTIC_LEVELS)[number];\n\n/** A single console entry captured by `ConsoleBuffer`. */\nexport interface ConsoleDiagnosticEntry {\n level: ConsoleDiagnosticLevel;\n /** ISO 8601 timestamp captured at log time. */\n timestamp: string;\n /** Best-effort string representation of the original console args. */\n message: string;\n}\n\n/** A single failed network request captured by `NetworkBuffer`. */\nexport interface NetworkDiagnosticEntry {\n url: string;\n method: string;\n /** HTTP status; 0 when the request never reached the server. */\n status: number;\n /** End-to-end duration in ms. */\n durationMs: number;\n /** ISO 8601 timestamp at the moment the request was initiated. */\n timestamp: string;\n}\n\n/**\n * Diagnostics captured by the widget when `captureDiagnostics` is enabled.\n *\n * Both arrays are bounded (default: 50 console / 20 network). Adapters that\n * support diagnostics should persist this as a JSON blob alongside the\n * feedback so reviewers can replay the context that led to the report.\n */\nexport interface DiagnosticsSnapshot {\n console: ConsoleDiagnosticEntry[];\n network: NetworkDiagnosticEntry[];\n}\n\n// ---------------------------------------------------------------------------\n// Annotation — multi-selector anchoring (Hypothesis / W3C Web Annotation)\n// ---------------------------------------------------------------------------\n\n/**\n * Length caps for `AnchorData.elementTag` / `elementId`, shared by the widget\n * (which bounds what it captures) and the HTTP adapter (which rejects a longer\n * tag and drops a longer id) so the two can't drift. 191 fits Prisma's default `String` column\n * on MySQL (`VARCHAR(191)`); real tag names and ids are far shorter.\n */\nexport const ANCHOR_ELEMENT_TAG_MAX = 191;\nexport const ANCHOR_ELEMENT_ID_MAX = 191;\n\n/** DOM anchoring data for re-attaching annotations to page elements. */\nexport interface AnchorData {\n /**\n * CSS selector generated by @medv/finder — primary anchor. Inside open\n * shadow roots: one selector per tree, outermost host first, joined by\n * `\" >>> \"` (e.g. `\"#pricing >>> .plan-title\"`).\n */\n cssSelector: string;\n /**\n * XPath — fallback 1. Inside a shadow root it is relative to that root\n * (`./…`) and informational only: XPath cannot enter shadow trees.\n */\n xpath: string;\n /** First ~120 chars of element innerText — empty string if none */\n textSnippet: string;\n /** Tag name for validation (e.g. \"DIV\", \"SECTION\") */\n elementTag: string;\n /** Element id attribute if available — most stable */\n elementId?: string | undefined;\n /** ~32 chars of text before this element in document flow (disambiguation) */\n textPrefix: string;\n /** ~32 chars of text after this element in document flow (disambiguation) */\n textSuffix: string;\n /** Structural fingerprint: \"childCount:siblingIdx:attrHash\" */\n fingerprint: string;\n /** Text content of adjacent sibling elements (context) */\n neighborText: string;\n /**\n * Semantic anchor identifier from the closest ancestor's `data-feedback-anchor`\n * attribute. When set, this is the highest-priority re-anchoring signal —\n * hosts deliberately place these on layout/section roots that survive\n * viewport changes and DOM refactors.\n */\n anchorKey?: string | null | undefined;\n}\n\n/**\n * Where the client's annotation rect sits within the captured screenshot,\n * as fractions [0, 1] of the image dimensions. The widget captures context\n * around the drawn rect and records the rect's position here so dashboards\n * can re-render the annotation on top of the image. Survives downscaling\n * (fractions are resolution-independent).\n */\nexport interface ScreenshotRegion {\n /** X offset of the rect as fraction of image width — [0, 1] */\n xPct: number;\n /** Y offset of the rect as fraction of image height — [0, 1] */\n yPct: number;\n /** Rect width as fraction of image width — [0, 1] */\n wPct: number;\n /** Rect height as fraction of image height — [0, 1] */\n hPct: number;\n}\n\n/** Drawn rectangle coordinates as percentages relative to the anchor element. */\nexport interface RectData {\n /** X offset as fraction of anchor element width — must be in range [0, 1] */\n xPct: number;\n /** Y offset as fraction of anchor element height — must be in range [0, 1] */\n yPct: number;\n /** Width as fraction of anchor element width — must be in range [0, 1] */\n wPct: number;\n /** Height as fraction of anchor element height — must be in range [0, 1] */\n hPct: number;\n}\n\n/** Annotation data sent as part of a feedback submission. */\nexport interface AnnotationPayload {\n anchor: AnchorData;\n rect: RectData;\n scrollX: number;\n scrollY: number;\n viewportW: number;\n viewportH: number;\n devicePixelRatio: number;\n}\n\n// ---------------------------------------------------------------------------\n// API responses\n// ---------------------------------------------------------------------------\n\n/**\n * Feedback record as returned by the API — derived from\n * {@link FeedbackRecord}: dates are serialized to ISO strings and `clientId`\n * is omitted (server-side dedup concern, never exposed on the wire), on the\n * record and on each comment of its thread. Adding a field to\n * `FeedbackRecord` updates this type automatically.\n *\n * Note: `authorEmail` may be an empty string — HTTP adapters redact it for\n * unauthenticated requests; the full value requires a Bearer-authenticated\n * request.\n */\nexport type FeedbackResponse = Prettify<\n Serialized<Omit<FeedbackRecord, \"clientId\" | \"comments\">> & {\n /** The thread, oldest first — always sent by `@siteping/server`, absent from servers that predate comments. */\n comments?: CommentResponse[] | undefined;\n /**\n * What the requester may do with this feedback — always sent by\n * `@siteping/server`. Absent from servers that predate it, and in store\n * mode: nothing is refused then.\n */\n permissions?: FeedbackPermissions | undefined;\n }\n>;\n\n/**\n * What a requester may do with one feedback, as the server's access policy\n * decides — so clients hide the actions it would refuse. The server still\n * enforces every one of them.\n */\nexport interface FeedbackPermissions {\n /** Change its status: resolve, reopen, … */\n canChangeStatus: boolean;\n /** Delete it. */\n canDelete: boolean;\n /** Reply in its thread. */\n canComment: boolean;\n /** Delete replies from its thread. */\n canDeleteComment: boolean;\n}\n\n/** What a requester may do with a whole project, sent with each list. */\nexport interface FeedbackListPermissions {\n /** Delete every feedback of the project at once. */\n canDeleteAll: boolean;\n}\n\n/**\n * Annotation record as returned by the API — {@link AnnotationRecord} with\n * `createdAt` serialized to an ISO string.\n */\nexport type AnnotationResponse = Prettify<Serialized<AnnotationRecord>>;\n\n/**\n * Comment as returned by the API — {@link CommentRecord} with `createdAt`\n * serialized and `clientId` omitted, like on `FeedbackResponse`. Its\n * `authorEmail` is redacted on the same terms as the feedback's.\n */\nexport type CommentResponse = Prettify<Serialized<Omit<CommentRecord, \"clientId\">>>;\n\n/** What the store behind an endpoint supports — advertised on every list response. */\nexport interface SitepingCapabilities {\n /** Whether comments can be posted: the store implements `addComment`. */\n comments: boolean;\n /**\n * Whether comments can be deleted: the store implements `deleteComment`.\n * Sent by `@siteping/server`; a client that does not delete (the widget)\n * leaves it out.\n */\n deleteComments?: boolean | undefined;\n}\n\n/** Paginated `FeedbackResponse` shape returned by the API. */\nexport interface FeedbackResponseList {\n feedbacks: FeedbackResponse[];\n total: number;\n /** Always sent by `@siteping/server` — absent from servers that predate it. */\n capabilities?: SitepingCapabilities | undefined;\n /** Always sent by `@siteping/server` — absent from servers that predate it. */\n permissions?: FeedbackListPermissions | undefined;\n}\n","import { MAX_COMMENTS_PER_FEEDBACK } from \"@siteping/core\";\n\n/** Most annotations accepted on one feedback (the create schema enforces it too). */\nexport const MAX_ANNOTATIONS_PER_FEEDBACK = 50;\n\n/**\n * Default `maxBodyBytes`: twice the largest submission the validation\n * accepts — a 1.5 MB screenshot, 50 annotations and full diagnostics come\n * to about 2 MB.\n */\nexport const DEFAULT_MAX_BODY_BYTES = 4 * 1024 * 1024;\n\n/**\n * Most issues a validation 400 lists. Fifty annotations of `{}` alone make a\n * thousand: listing them all would answer a small body with a large one.\n */\nexport const MAX_VALIDATION_ISSUES = 20;\n\n/**\n * Most `authorize` dry runs of one response in flight at once. A page of 100\n * asks 401 of them, and a database policy's pool holds a handful of\n * connections: unbounded, they would queue there, time out, and read as refusals.\n */\nexport const DRY_RUN_CONCURRENCY = 8;\n\n/**\n * `error` strings of the HTTP API. Part of the wire contract: the widget, the\n * dashboard and existing `@siteping/adapter-prisma` consumers match on some.\n */\nexport const ERROR_MESSAGES = {\n invalidJson: \"Invalid JSON\",\n bodyTooLarge: \"Request body too large\",\n unauthorized: \"Unauthorized\",\n apiKeyRequiredForDestructive: \"apiKey required for destructive operations\",\n forbidden: \"Forbidden\",\n unsupportedMediaType: \"Content-Type must be application/json\",\n feedbackNotFound: \"Feedback not found\",\n commentNotFound: \"Comment not found\",\n clientIdUsedByAnotherProject: \"clientId already used by another project\",\n clientIdUsedByAnotherFeedback: \"clientId already used on another feedback\",\n commentsUnsupported: \"Comments are not supported by this store\",\n tooManyComments: `Too many client comments on this feedback (max ${MAX_COMMENTS_PER_FEEDBACK})`,\n tooManyAnnotations: `Too many annotations (max ${MAX_ANNOTATIONS_PER_FEEDBACK})`,\n valueTooLong: \"A value is too long for this server's database\",\n deletionAborted: \"Deletion aborted: a linked resource could not be cleaned up\",\n internalServerError: \"Internal server error\",\n} as const;\n\n/** Query parameters the list endpoint reads (everything else is ignored). */\nexport const LIST_QUERY_KEYS = [\n \"projectName\",\n \"page\",\n \"limit\",\n \"type\",\n \"status\",\n \"statuses\",\n \"search\",\n \"url\",\n \"urlPattern\",\n] as const;\n","import { ERROR_MESSAGES } from \"./constants.js\";\nimport type {\n SitepingAccessControl,\n SitepingApiKeyHandlerOptions,\n SitepingAuthorizationContext,\n SitepingHttpMethod,\n SitepingPrincipal,\n SitepingRequestContext,\n} from \"./options.js\";\n\n/** Outcome of the access check that opens every request. */\nexport type AccessOutcome<Principal> =\n | { ok: true; principal: Principal; canReadAuthorEmail: boolean }\n | { ok: false; status: 401; error: string };\n\n/** The access policy the operations run, normalised from either option set. */\nexport interface AccessGate<Principal> {\n /**\n * Whether a successful POST echoes `authorEmail` whatever the requester may\n * read. Only the `apiKey` policy does: its public POST has always returned\n * the email to the submitter, who supplied it (or proved ownership of the\n * record by presenting its clientId).\n */\n readonly echoesAuthorEmailOnCreate: boolean;\n /** Whether POST/PATCH/DELETE must pass the CSRF guards (see `access`). */\n readonly guardsMutations: boolean;\n /** `Cache-Control` of the list response. */\n readonly listCacheControl: string;\n authenticate(request: Request, method: SitepingHttpMethod): Promise<AccessOutcome<Principal>>;\n /**\n * Whether the caller of an admitted request would be admitted on `method`\n * too — the first half of each `permissions` answer, `authorize` the second.\n */\n admits(request: Request, method: SitepingHttpMethod): Promise<boolean>;\n authorize(context: SitepingAuthorizationContext<Principal>): Promise<boolean>;\n /**\n * Whether an authenticated caller's comment keeps the `team` role it asks\n * for. Asked only when a comment does, so a GET never runs the policy's\n * callback for it.\n */\n canCommentAsTeam(context: SitepingRequestContext<Principal>, canReadAuthorEmail: boolean): Promise<boolean>;\n}\n\nconst textEncoder = new TextEncoder();\n\n/**\n * Constant-time string comparison for API key validation, without\n * `node:crypto` so the handler runs on any runtime with Web APIs (Node, Bun,\n * Deno, edge workers). Returns `false` early when lengths differ (an\n * unavoidable length leak); the byte comparison itself does not short-circuit.\n *\n * Lengths are compared in BYTES: multi-byte characters make strings of equal\n * `.length` differ in bytes.\n */\nfunction safeCompare(a: string, b: string): boolean {\n const bytesA = textEncoder.encode(a);\n const bytesB = textEncoder.encode(b);\n if (bytesA.length !== bytesB.length) return false;\n let difference = 0;\n for (let index = 0; index < bytesA.length; index++) {\n difference |= (bytesA[index] ?? 0) ^ (bytesB[index] ?? 0);\n }\n return difference === 0;\n}\n\n/** `NODE_ENV === \"production\"`, tolerating runtimes without `process` (edge workers, Deno). */\nfunction isProductionEnvironment(): boolean {\n return typeof process !== \"undefined\" && process.env?.NODE_ENV === \"production\";\n}\n\n/**\n * The shared-secret policy (`apiKey`, `publicEndpoints`,\n * `requireAuthForDestructive`, `redactUnauthenticatedEmails`). It never\n * resolves a principal.\n *\n * @throws Error in production without `apiKey` while destructive endpoints\n * still require it — the factory refuses an unauthenticated destructive surface.\n */\nexport function createApiKeyGate({\n apiKey,\n publicEndpoints = apiKey ? [\"POST\", \"OPTIONS\"] : undefined,\n requireAuthForDestructive = true,\n redactUnauthenticatedEmails = true,\n}: Pick<\n SitepingApiKeyHandlerOptions,\n \"apiKey\" | \"publicEndpoints\" | \"requireAuthForDestructive\" | \"redactUnauthenticatedEmails\"\n>): AccessGate<null> {\n // Without this guard, anyone could `DELETE { deleteAll: true }` against the API.\n if (!apiKey && requireAuthForDestructive && isProductionEnvironment()) {\n throw new Error(\n \"[siteping] createSitepingHandler: apiKey is required in production. \" +\n \"Set `apiKey` to enable destructive endpoints, pass `access` to plug in your own authentication, \" +\n \"or pass `requireAuthForDestructive: false` if SitePing sits behind your own auth middleware.\",\n );\n }\n\n const publicMethods: ReadonlySet<SitepingHttpMethod> | null = publicEndpoints ? new Set(publicEndpoints) : null;\n\n /**\n * True iff `apiKey` is configured AND the request carries a matching Bearer\n * token. A valid token on a public method still counts: it drives PII\n * redaction, not access control.\n */\n const isBearerAuthenticated = (request: Request): boolean => {\n if (!apiKey) return false;\n const header = request.headers.get(\"Authorization\");\n return header !== null && safeCompare(header, `Bearer ${apiKey}`);\n };\n\n const authenticate: AccessGate<null>[\"authenticate\"] = async (request, method) => {\n const canReadAuthorEmail = !redactUnauthenticatedEmails || isBearerAuthenticated(request);\n if (!apiKey) {\n // GET/POST/OPTIONS stay open by default so the widget keeps working in dev without config.\n if (requireAuthForDestructive && (method === \"DELETE\" || method === \"PATCH\")) {\n return { ok: false, status: 401, error: ERROR_MESSAGES.apiKeyRequiredForDestructive };\n }\n return { ok: true, principal: null, canReadAuthorEmail };\n }\n if (publicMethods?.has(method) || isBearerAuthenticated(request)) {\n return { ok: true, principal: null, canReadAuthorEmail };\n }\n return { ok: false, status: 401, error: ERROR_MESSAGES.unauthorized };\n };\n\n return {\n echoesAuthorEmailOnCreate: true,\n // The key travels in a header a forged cross-site request cannot set.\n guardsMutations: false,\n listCacheControl: \"private, max-age=5\",\n authenticate,\n // This policy admits by method: a visitor reads, the key holder triages.\n async admits(request, method) {\n return (await authenticate(request, method)).ok;\n },\n async authorize() {\n return true;\n },\n // The key proves the caller is the project side; a public POST does not.\n async canCommentAsTeam({ request }) {\n return isBearerAuthenticated(request);\n },\n };\n}\n\n/** A custom `access` policy. */\nexport function createAccessGate<Principal extends SitepingPrincipal>(\n access: SitepingAccessControl<Principal>,\n): AccessGate<Principal> {\n return {\n echoesAuthorEmailOnCreate: false,\n guardsMutations: true,\n listCacheControl: \"no-store\",\n async authenticate(request) {\n const principal = await access.authenticate(request);\n // Fail closed on anything falsy: `return session?.user` or a plain\n // JavaScript `false` check must never let a request in.\n if (!principal) {\n return { ok: false, status: 401, error: ERROR_MESSAGES.unauthorized };\n }\n // Emails are personal data: only a `true` answer reveals them — no\n // callback, or `undefined` from a principal missing the flag, hides them.\n return { ok: true, principal, canReadAuthorEmail: (await access.canReadAuthorEmail?.(principal)) === true };\n },\n // A dry run reuses the response's request, so `authenticate` is never asked\n // about another method: `authorize` decides, by action.\n async admits() {\n return true;\n },\n async authorize(context) {\n return access.authorize ? access.authorize(context) : true;\n },\n // Speaking as the team is an impersonation privilege: without the host's\n // word for it (its own callback, or the email access it grants), refuse.\n async canCommentAsTeam({ principal }, canReadAuthorEmail) {\n // Anything but `true` from a callback refuses, as for emails.\n return access.canCommentAsTeam ? (await access.canCommentAsTeam(principal)) === true : canReadAuthorEmail;\n },\n };\n}\n","export type CorsHeaders = Readonly<Record<string, string>>;\n\n/** Request headers every allowlisted origin may send (preflights can add more). */\nconst DEFAULT_ALLOWED_HEADERS: ReadonlyArray<string> = [\"Content-Type\", \"Authorization\"];\n\n/** RFC 9110 `token` — the only valid shape for a header field name. */\nconst HEADER_TOKEN = /^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$/;\n\n/**\n * Build CORS headers for a given request.\n * When `allowedOrigins` is set, only matching origins get reflected.\n * When unset, no CORS headers are added (no permissive wildcard by default).\n */\nexport function buildCorsHeaders(request: Request, allowedOrigins: ReadonlyArray<string> | undefined): CorsHeaders {\n if (!allowedOrigins) return {};\n\n // With an allowlist the response depends on Origin even when it gets no\n // CORS headers (Origin absent or unlisted) — without `Vary`, a shared cache\n // could replay a header-less response to an allowed origin, or vice versa.\n const origin = request.headers.get(\"Origin\");\n if (!origin || !allowedOrigins.includes(origin)) return { Vary: \"Origin\" };\n\n return {\n \"Access-Control-Allow-Origin\": origin,\n \"Access-Control-Allow-Methods\": \"GET, POST, PATCH, DELETE, OPTIONS\",\n \"Access-Control-Allow-Headers\": DEFAULT_ALLOWED_HEADERS.join(\", \"),\n \"Access-Control-Allow-Credentials\": \"true\",\n \"Access-Control-Max-Age\": \"86400\",\n Vary: \"Origin\",\n };\n}\n\n/**\n * Attach CORS headers to an existing Response.\n */\nexport function withCors(response: Response, corsHeaders: CorsHeaders): Response {\n for (const [key, value] of Object.entries(corsHeaders)) {\n response.headers.set(key, value);\n }\n return response;\n}\n\n/**\n * `Access-Control-Allow-Headers` for a preflight from an ALLOWLISTED origin:\n * the defaults plus the header names it asks for — the widget's `headers`\n * option lets hosts send their own (a session token, a tenant id), which the\n * fixed default list would block. Names that are not valid header tokens\n * are dropped.\n */\nfunction preflightAllowedHeaders(request: Request): string {\n const allowed = [...DEFAULT_ALLOWED_HEADERS];\n const seen = new Set(allowed.map((name) => name.toLowerCase()));\n for (const raw of (request.headers.get(\"Access-Control-Request-Headers\") ?? \"\").split(\",\")) {\n const name = raw.trim();\n if (!HEADER_TOKEN.test(name) || seen.has(name.toLowerCase())) continue;\n seen.add(name.toLowerCase());\n allowed.push(name);\n }\n return allowed.join(\", \");\n}\n\n/**\n * Answer a CORS preflight. Configure `allowedOrigins` to restrict which\n * domains can make cross-origin requests to the API: without it, no CORS\n * headers are emitted and browsers block widget requests.\n */\nexport function preflightResponse(request: Request, allowedOrigins: ReadonlyArray<string> | undefined): Response {\n const corsHeaders = buildCorsHeaders(request, allowedOrigins);\n // An allowlisted preflight's answer also depends on the headers it requests.\n const headers = corsHeaders[\"Access-Control-Allow-Origin\"]\n ? {\n ...corsHeaders,\n \"Access-Control-Allow-Headers\": preflightAllowedHeaders(request),\n Vary: \"Origin, Access-Control-Request-Headers\",\n }\n : corsHeaders;\n return new Response(null, { status: 204, headers });\n}\n","import type {\n AssertEqual,\n CommentDeletePayload,\n CommentPayload,\n FeedbackPayload,\n FeedbackStatus,\n FeedbackType,\n Prettify,\n} from \"@siteping/core\";\nimport {\n ANCHOR_ELEMENT_ID_MAX,\n ANCHOR_ELEMENT_TAG_MAX,\n COMMENT_AUTHOR_ROLES,\n COMMENT_BODY_MAX_LENGTH,\n CONSOLE_DIAGNOSTIC_LEVELS,\n EMAIL_PATTERN,\n FEEDBACK_STATUSES,\n FEEDBACK_TYPES,\n IDENTITY_FIELD_MAX_LENGTH,\n} from \"@siteping/core\";\nimport * as zod from \"zod\";\nimport { MAX_VALIDATION_ISSUES } from \"./constants.js\";\n\n// Namespace import required: Zod publishes dual CJS/ESM, and bundlers (tsup, vitest) may\n// resolve the CJS entry where `import { z } from \"zod\"` fails because CJS wraps\n// the entire module under a default/namespace key. This workaround normalizes access\n// regardless of which entry point the bundler resolves.\n// See: https://github.com/colinhacks/zod/issues/2697\nconst z: typeof zod.z = (\"z\" in zod ? zod.z : zod) as typeof zod.z;\n\n/**\n * An array of at most `max` elements, refused on its length before any\n * element is parsed. zod checks every element before `.max()`, so an\n * oversized array would cost a parse of each element and one issue per\n * invalid field: a public POST of 0.6 MB answered with a 120 MB 400.\n */\nfunction boundedArray<Element extends zod.z.ZodType>(element: Element, max: number) {\n return z.array(z.unknown()).max(max).pipe(z.array(element));\n}\n\nconst anchorSchema = z.object({\n cssSelector: z.string().min(1).max(2000),\n xpath: z.string().min(1).max(2000),\n textSnippet: z.string().max(500),\n elementTag: z.string().min(1).max(ANCHOR_ELEMENT_TAG_MAX),\n // A resolver hint: an over-long id is dropped, as the widget now does,\n // rather than failing the whole payload — earlier widgets send it unbounded.\n elementId: z\n .string()\n .transform((id) => (id.length <= ANCHOR_ELEMENT_ID_MAX ? id : undefined))\n .optional(),\n textPrefix: z.string().max(200),\n textSuffix: z.string().max(200),\n fingerprint: z.string().max(200),\n neighborText: z.string().max(500),\n // Optional semantic anchor identifier from `data-feedback-anchor`.\n // Null when no semantic ancestor exists; widget always sends this field.\n anchorKey: z.string().max(200).nullable().optional(),\n});\n\nconst rectSchema = z.object({\n xPct: z.number().min(0).max(1),\n yPct: z.number().min(0).max(1),\n wPct: z.number().min(0).max(1),\n hPct: z.number().min(0).max(1),\n});\n\nconst annotationSchema = z.object({\n anchor: anchorSchema,\n rect: rectSchema,\n // Raw `window.scrollX/Y` — no lower bound: scrollX is negative on a\n // scrolled `dir=\"rtl\"` page. `z.number()` already rejects NaN/±Infinity.\n scrollX: z.number(),\n scrollY: z.number(),\n // Upper bound = the `Int` (INT4) column max, so oversized input is a 400\n // here rather than a database error at insert time.\n viewportW: z.number().int().positive().max(2_147_483_647),\n viewportH: z.number().int().positive().max(2_147_483_647),\n devicePixelRatio: z.number().positive().default(1),\n});\n\n// Diagnostics caps mirror the widget defaults — the widget never sends more\n// than these, but the schema enforces it server-side too so a tampered\n// client can't blow up the JSON column with megabytes of garbage.\nconst consoleEntrySchema = z.object({\n level: z.enum(CONSOLE_DIAGNOSTIC_LEVELS),\n timestamp: z.string().max(50),\n message: z.string().max(600),\n});\n\nconst networkEntrySchema = z.object({\n url: z.string().max(2000),\n method: z.string().max(20),\n status: z.number().int().min(0).max(599),\n durationMs: z.number().min(0).max(600_000),\n timestamp: z.string().max(50),\n});\n\nconst diagnosticsSchema = z.object({\n console: boundedArray(consoleEntrySchema, 50),\n network: boundedArray(networkEntrySchema, 20),\n});\n\n// Annotation rect position within the screenshot image, as fractions [0, 1]\n// of the image dimensions (see `ScreenshotRegion` in core). Strict: this is\n// persisted verbatim into a JSON column, so unknown keys are rejected rather\n// than silently stored.\nexport const screenshotRegionSchema = z.strictObject({\n xPct: z.number().min(0).max(1),\n yPct: z.number().min(0).max(1),\n wPct: z.number().min(0).max(1),\n hPct: z.number().min(0).max(1),\n});\n\n// Restrict to URL-safe identifiers. The widget generates UUIDs (or a\n// Date+Math.random fallback), both of which match. Anything outside this\n// alphabet — `..`, `/`, NUL, etc. — would be a path-traversal vector once\n// the adapter forwards clientId to `screenshotStorage.upload({ feedbackId })`\n// (e.g. an S3 key prefix or a local FS path).\nconst clientIdSchema = z\n .string()\n .min(1)\n .max(200)\n .regex(/^[a-zA-Z0-9_-]+$/, \"clientId must be alphanumeric (a-z, A-Z, 0-9, _, -)\");\n\nexport const feedbackCreateSchema = z.object({\n projectName: z.string().min(1).max(200),\n type: z.enum(FEEDBACK_TYPES),\n message: z.string().min(1).max(5000),\n // Page-scope identifier the widget uses to group feedbacks. Defaults to\n // `window.location.pathname` (\"/orders/42\"), but hosts can override\n // `getPageScope()` to return a full URL, an opaque slug, anything they\n // want. We trim + require non-empty so whitespace-only payloads don't\n // leak into the DB; otherwise the value is opaque to the server and only\n // used as a literal Prisma equality filter, so the loose shape is safe.\n url: z.string().trim().min(1).max(2000),\n // Optional parameterized URL template (e.g. \"/orders/:orderId\") provided\n // by the host via `getPageScope()`. Null when host omits it.\n urlPattern: z.string().max(2000).nullable().optional(),\n viewport: z.string().min(1).max(50),\n userAgent: z.string().min(1).max(500),\n // The widget's identity modal validates against the same core pattern and\n // length cap, so an identity the modal accepts (and persists) is never a\n // 400 here.\n authorName: z.string().min(1).max(IDENTITY_FIELD_MAX_LENGTH),\n authorEmail: z.email({ pattern: EMAIL_PATTERN }).max(IDENTITY_FIELD_MAX_LENGTH),\n annotations: boundedArray(annotationSchema, 50),\n clientId: clientIdSchema,\n // Optional base64 JPEG data URL captured by the widget when\n // `enableScreenshot: true`. ~1.5 MB cap = roughly a 1.1 MB JPEG, well\n // above typical sizes (the widget downscales to 1200px). Rejects abuse\n // without truncating legitimate captures.\n screenshotDataUrl: z\n .string()\n .max(1_500_000)\n .regex(/^data:image\\/(jpeg|png|webp);base64,/, \"screenshotDataUrl must be a data:image/* base64 URL\")\n .nullable()\n .optional(),\n // Optional annotation-rect position within the screenshot. Sent by widgets\n // that capture context around the drawn rect; null + omitted are both\n // accepted so legacy clients (and captures without a screenshot) keep\n // working unchanged.\n screenshotRegion: screenshotRegionSchema.nullable().optional(),\n // Optional console + failed-network snapshot. The widget only attaches\n // this when `captureDiagnostics` is enabled; null + omitted are both\n // accepted so existing clients keep working unchanged.\n diagnostics: diagnosticsSchema.nullable().optional(),\n});\n\n// `feedbackId` names the thread — its presence is what routes a POST here\n// rather than to `feedbackCreateSchema`.\nexport const commentCreateSchema = z.object({\n projectName: z.string().min(1).max(200),\n feedbackId: z.string().min(1).max(200),\n // Trimmed, so a whitespace-only reply is a 400 rather than an empty bubble.\n body: z.string().trim().min(1).max(COMMENT_BODY_MAX_LENGTH),\n authorName: z.string().min(1).max(IDENTITY_FIELD_MAX_LENGTH),\n // The widget's reviewers always have an email; a dashboard user may have\n // none on file, sent as \"\". Anything else must be a real address.\n authorEmail: z.union([z.literal(\"\"), z.email({ pattern: EMAIL_PATTERN }).max(IDENTITY_FIELD_MAX_LENGTH)]),\n // A claim, not a fact: the handler keeps \"team\" only when the access\n // policy vouches for the caller.\n authorRole: z.enum(COMMENT_AUTHOR_ROLES).default(\"client\"),\n clientId: clientIdSchema,\n});\n\nexport const commentDeleteSchema = z.object({\n projectName: z.string().min(1).max(200),\n feedbackId: z.string().min(1).max(200),\n commentId: z.string().min(1).max(200),\n});\n\nexport const feedbackPatchSchema = z.object({\n id: z.string().min(1),\n projectName: z.string().min(1).max(200),\n status: z.enum(FEEDBACK_STATUSES),\n});\n\nexport const feedbackDeleteSchema = z.union([\n z.object({ id: z.string().min(1), projectName: z.string().min(1).max(200) }),\n z.object({ projectName: z.string().min(1).max(200), deleteAll: z.literal(true) }),\n]);\n\nexport const getQuerySchema = z.object({\n projectName: z.string().min(1).max(200),\n page: z.coerce.number().int().min(1).default(1),\n limit: z.coerce.number().int().min(1).max(100).default(50),\n type: z.enum(FEEDBACK_TYPES).optional(),\n status: z.enum(FEEDBACK_STATUSES).optional(),\n // Bucket status filter serialized as CSV over the wire (e.g.\n // `statuses=open,in_progress`). Non-empty strings are split before each value\n // is validated against the known statuses; `statuses` wins over the exact\n // `status` filter downstream. Capped at 4 — the number of known statuses.\n statuses: z\n .preprocess(\n (val) => (typeof val === \"string\" && val.length > 0 ? val.split(\",\") : val),\n boundedArray(z.enum(FEEDBACK_STATUSES), 4),\n )\n .optional(),\n search: z.string().max(200).optional(),\n // Page scope filters — used by the panel's \"this page / this type\" controls\n url: z.string().max(2000).optional(),\n urlPattern: z.string().max(2000).optional(),\n});\n\n// ---------------------------------------------------------------------------\n// Explicit public interfaces — decoupled from Zod to keep .d.ts clean.\n//\n// The create payloads need no local interface at all: the schemas validate\n// core's `FeedbackPayload` / `CommentPayload` / `CommentDeletePayload` wire\n// shapes, and the compile-time locks below keep them identical. Only the wire shapes that exist solely at this HTTP\n// boundary (PATCH / DELETE / GET query) are declared here.\n// ---------------------------------------------------------------------------\n\nexport interface FeedbackPatchInput {\n id: string;\n projectName: string;\n status: FeedbackStatus;\n}\n\nexport interface FeedbackDeleteSingle {\n id: string;\n projectName: string;\n}\n\nexport interface FeedbackDeleteAll {\n projectName: string;\n deleteAll: true;\n}\n\nexport type FeedbackDeleteInput = FeedbackDeleteSingle | FeedbackDeleteAll;\n\nexport interface GetQueryInput {\n projectName: string;\n /** Set to 1 by schema default when omitted from raw input. */\n page: number;\n /** Set to 50 by schema default when omitted from raw input. */\n limit: number;\n type?: FeedbackType | undefined;\n status?: FeedbackStatus | undefined;\n statuses?: FeedbackStatus[] | undefined;\n search?: string | undefined;\n url?: string | undefined;\n urlPattern?: string | undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Type-level locks: the Zod schemas validate exactly the shapes the rest of\n// the monorepo speaks.\n//\n// The create schema is locked against core's `FeedbackPayload` — the actual\n// wire contract the widget sends — so schema drift from the real payload is\n// a compile error, not a runtime 400. The boundary-only shapes (PATCH /\n// DELETE / GET query) are locked against their local interfaces the same\n// way. `Prettify<>` flattens the inferred shape so the equality check\n// compares clean object types rather than intersection chains.\n// ---------------------------------------------------------------------------\n\nconst _createSchemaMatchesPayload: AssertEqual<\n Prettify<zod.z.infer<typeof feedbackCreateSchema>>,\n FeedbackPayload\n> = true;\nvoid _createSchemaMatchesPayload;\nconst _commentSchemaMatchesPayload: AssertEqual<\n Prettify<zod.z.infer<typeof commentCreateSchema>>,\n CommentPayload\n> = true;\nvoid _commentSchemaMatchesPayload;\nconst _commentDeleteSchemaMatchesPayload: AssertEqual<\n Prettify<zod.z.infer<typeof commentDeleteSchema>>,\n CommentDeletePayload\n> = true;\nvoid _commentDeleteSchemaMatchesPayload;\nconst _patchSchemaMatchesInput: AssertEqual<\n Prettify<zod.z.infer<typeof feedbackPatchSchema>>,\n FeedbackPatchInput\n> = true;\nvoid _patchSchemaMatchesInput;\nconst _deleteSchemaMatchesInput: AssertEqual<\n Prettify<zod.z.infer<typeof feedbackDeleteSchema>>,\n FeedbackDeleteInput\n> = true;\nvoid _deleteSchemaMatchesInput;\nconst _querySchemaMatchesInput: AssertEqual<Prettify<zod.z.infer<typeof getQuerySchema>>, GetQueryInput> = true;\nvoid _querySchemaMatchesInput;\n\n/** Single validation issue extracted from a `ZodError`. */\nexport interface ValidationIssue {\n field: string;\n message: string;\n}\n\n/**\n * Map Zod errors to a flat array of `{ field, message }` objects — the first\n * `MAX_VALIDATION_ISSUES` of them.\n * Safe: does not leak input values or schema structure.\n */\nexport function formatValidationErrors(error: zod.z.ZodError): ValidationIssue[] {\n // Zod 4 types `issue.path` as PropertyKey[] (symbols possible in theory);\n // String() keeps the join total instead of throwing on non-string keys.\n return error.issues.slice(0, MAX_VALIDATION_ISSUES).map((issue) => ({\n field: issue.path.map(String).join(\".\"),\n message: issue.message,\n }));\n}\n","import {\n type CommentAuthorRole,\n isStoreLimit,\n isStoreNotFound,\n isStoreValueTooLong,\n type SitepingStore,\n} from \"@siteping/core\";\nimport { ERROR_MESSAGES } from \"../constants.js\";\nimport type { SitepingHandlerBaseOptions } from \"../options.js\";\nimport type { Pipeline, Scope } from \"../pipeline.js\";\nimport { commentCreateSchema } from \"../validation.js\";\n\ninterface CreateCommentDependencies<Principal> {\n store: SitepingStore;\n pipeline: Pipeline<Principal>;\n beforeComment: SitepingHandlerBaseOptions<Principal>[\"beforeComment\"];\n}\n\n/** `POST` with a `feedbackId` — add a comment to that feedback's thread, idempotent on `clientId`. */\nexport function createCommentOperation<Principal>({\n store,\n pipeline,\n beforeComment,\n}: CreateCommentDependencies<Principal>) {\n return async (scope: Scope<Principal>, body: unknown): Promise<Response> => {\n const payload = pipeline.validate(scope, commentCreateSchema, body);\n if (!payload.ok) return payload.response;\n\n try {\n const { projectName, feedbackId, authorRole, ...comment } = beforeComment\n ? await beforeComment(payload.value, scope.context)\n : payload.value;\n const refusal = await pipeline.authorize(scope, { action: \"createComment\", projectName, feedbackId });\n if (refusal) return refusal;\n // 501, not 404: the thread may well exist — this store keeps no comments.\n if (!store.addComment) return pipeline.error(scope, 501, ERROR_MESSAGES.commentsUnsupported);\n\n // Cross-project guard — see the PATCH operation.\n if (store.verifyProjectOwnership && !(await store.verifyProjectOwnership(feedbackId, projectName))) {\n return pipeline.error(scope, 404, ERROR_MESSAGES.feedbackNotFound);\n }\n\n // POST is typically public so the widget can reply from a visitor's\n // browser: the role a request claims is kept only when the access\n // policy vouches for the caller.\n const role: CommentAuthorRole =\n authorRole === \"team\" && (await pipeline.canCommentAsTeam(scope)) ? \"team\" : \"client\";\n const stored = await store.addComment(feedbackId, { ...comment, authorRole: role });\n\n // A clientId is unique across every thread, so a replay that resolves to\n // another feedback's comment is a boundary violation, not a dedup.\n if (stored.feedbackId !== feedbackId) {\n return pipeline.error(scope, 409, ERROR_MESSAGES.clientIdUsedByAnotherFeedback);\n }\n return pipeline.json(scope, pipeline.presentCreatedComment(scope, stored), { status: 201 });\n } catch (error) {\n if (isStoreNotFound(error)) return pipeline.error(scope, 404, ERROR_MESSAGES.feedbackNotFound);\n if (isStoreLimit(error)) return pipeline.error(scope, 409, ERROR_MESSAGES.tooManyComments);\n if (isStoreValueTooLong(error)) return pipeline.refuseTooLong(scope, error);\n return pipeline.fail(scope, \"[siteping] Failed to add comment\", error);\n }\n };\n}\n","/**\n * Outgoing webhook notifications for newly-created feedbacks.\n *\n * Plug a Slack, Discord, or generic HTTP endpoint into `createSitepingHandler`\n * to receive a payload whenever a feedback is successfully persisted. Webhooks\n * are dispatched as fire-and-forget (`void Promise.all(...)`) so a slow or\n * down receiver never blocks the client response — the feedback is already in\n * the DB by the time we dial out.\n *\n * - **Type-specific formatting**: Slack uses `{ text, blocks }`, Discord uses\n * `{ content, embeds }`, generic posts the record as JSON (minus `clientId`).\n * - **Untrusted input**: `message` and `authorName` come from anonymous\n * visitors. Slack text is escaped, Discord markdown is escaped and its\n * mention parsing disabled, so a public feedback form can never be turned\n * into a channel-wide ping or a disguised link.\n * - **Timeout**: 5s by default (overridable per webhook).\n * - **Error handling**: `config.onError(err, feedback.id)` is invoked when\n * present; otherwise we log a one-liner to `console.warn` so the issue is\n * surfaced without crashing the request.\n */\n\nimport type {\n CommentRecord,\n CommentResponse,\n FeedbackRecord,\n FeedbackType,\n Prettify,\n Serialized,\n} from \"@siteping/core\";\n\n/** Supported webhook integrations — drives the JSON body shape. */\nexport type WebhookType = \"slack\" | \"discord\" | \"generic\";\n\n/**\n * Outgoing webhook configuration.\n *\n * - `url` — required, the HTTPS endpoint to POST to.\n * - `type` — payload format. Defaults to `\"generic\"` (raw JSON).\n * - `headers` — extra headers merged on top of `Content-Type: application/json`.\n * Useful for signed-payload schemes (`X-Signature`, bearer tokens, …).\n * - `timeoutMs` — abort the fetch after this many ms: a positive integer of\n * at most 2,147,483,647, the longest delay a timer holds. Defaults to 5000.\n * - `onError` — invoked with the underlying error and the feedback id when\n * the dispatch fails (network error, non-2xx, timeout). The webhook is\n * fire-and-forget, so this is your only chance to observe failures. An\n * async one is awaited: the delivery `waitUntil` holds covers it, and its\n * rejection is reported like a throw, never left unhandled.\n */\nexport interface WebhookConfig {\n url: string;\n type?: WebhookType;\n headers?: Record<string, string>;\n timeoutMs?: number;\n onError?: (err: Error, feedbackId: string) => void | Promise<void>;\n}\n\nconst DEFAULT_TIMEOUT_MS = 5000;\n\n/** Longest delay a timer holds: Node and browsers fire a longer one at once, which would abort every delivery. */\nconst TIMER_MAX_DELAY_MS = 2_147_483_647;\n\n/** Why `timeoutMs` cannot bound a delivery, or `null` when it can (or is unset). */\nfunction timeoutProblem(timeoutMs: number | undefined): string | null {\n if (\n timeoutMs === undefined ||\n (Number.isSafeInteger(timeoutMs) && timeoutMs > 0 && timeoutMs <= TIMER_MAX_DELAY_MS)\n ) {\n return null;\n }\n return `timeoutMs must be a positive integer of at most ${TIMER_MAX_DELAY_MS}, got ${timeoutMs}`;\n}\n\n/**\n * Refuse a webhook whose `timeoutMs` no timer holds, when the handler is\n * created rather than on every delivery.\n *\n * @throws Error naming the webhook by its origin: its path may be a credential.\n */\nexport function checkWebhookTimeouts(configs: readonly WebhookConfig[]): void {\n for (const config of configs) {\n const problem = timeoutProblem(config.timeoutMs);\n if (problem) {\n throw new Error(`[siteping] createSitepingHandler: webhook to ${webhookOrigin(config.url)}: ${problem}.`);\n }\n }\n}\n\n/** Decimal RGB colour table used by Discord embeds — keyed by feedback type. */\nconst DISCORD_COLORS: Readonly<Record<FeedbackType, number>> = {\n bug: 0xef4444,\n question: 0x3b82f6,\n change: 0xf59e0b,\n other: 0x6b7280,\n};\n\nconst DEFAULT_DISCORD_COLOR = 0x6b7280;\n\n// ---------------------------------------------------------------------------\n// Payload shapes — narrow types let TypeScript catch malformed bodies at\n// compile time rather than only at the receiving end.\n// ---------------------------------------------------------------------------\n\n/** Block Kit envelope used by Slack incoming webhooks. */\nexport interface SlackWebhookPayload {\n text: string;\n blocks: ReadonlyArray<SlackHeaderBlock | SlackSectionBlock | SlackContextBlock>;\n}\n\ninterface SlackHeaderBlock {\n type: \"header\";\n text: { type: \"plain_text\"; text: string; emoji: true };\n}\n\ninterface SlackSectionBlock {\n type: \"section\";\n text: { type: \"mrkdwn\"; text: string };\n}\n\ninterface SlackContextBlock {\n type: \"context\";\n elements: ReadonlyArray<{ type: \"mrkdwn\"; text: string }>;\n}\n\n/** Embed envelope used by Discord incoming webhooks. */\nexport interface DiscordWebhookPayload {\n content: string;\n embeds: ReadonlyArray<{\n title: string;\n description: string;\n color: number;\n fields: ReadonlyArray<{ name: string; value: string; inline: boolean }>;\n timestamp: string;\n }>;\n /**\n * Mention parsing is switched off: `content` carries end-user text, so an\n * author called `@everyone` must render as text, never as a notification.\n */\n allowed_mentions: { parse: ReadonlyArray<\"roles\" | \"users\" | \"everyone\"> };\n}\n\n/**\n * Generic webhook body — the stored record as the receiver parses it: dates\n * are ISO strings, and `clientId` is stripped like on every other output, on\n * the record and on each comment of its thread. It is the browser-local\n * dedup secret, and the POST replay path hands the full record to whoever\n * presents it.\n */\nexport type GenericWebhookPayload = Prettify<\n Serialized<Omit<FeedbackRecord, \"clientId\" | \"comments\">> & { comments?: CommentResponse[] | undefined }\n>;\n\n/** What `buildGenericPayload` returns — {@link GenericWebhookPayload} before `JSON.stringify`. */\ntype GenericWebhookBody = Omit<FeedbackRecord, \"clientId\" | \"comments\"> & {\n comments?: Omit<CommentRecord, \"clientId\">[] | undefined;\n};\n\n/** Mapping from webhook type to its concrete body shape. */\nexport interface WebhookPayloadMap {\n slack: SlackWebhookPayload;\n discord: DiscordWebhookPayload;\n generic: GenericWebhookPayload;\n}\n\n/** Truncate a message for chat-platform previews (Slack/Discord look bad with walls of text). */\nfunction truncate(text: string, max = 300): string {\n if (text.length <= max) return text;\n return `${text.slice(0, max - 1)}…`;\n}\n\n/**\n * Join escaped `units` (one per source character) and truncate so the ESCAPED\n * result fits `max`. Truncation drops whole units, so an escape sequence\n * (`&`, `\\[`, `%28`) is never cut in half — and escaping can grow a value\n * several-fold, so sizing the raw text alone would not guarantee fit.\n */\nfunction fitEscaped(units: readonly string[], max: number): string {\n const escaped = units.join(\"\");\n if (escaped.length <= max) return escaped;\n let out = \"\";\n for (const unit of units) {\n if (out.length + unit.length > max - 1) break;\n out += unit;\n }\n return `${out}…`;\n}\n\n/** Block Kit caps `header` text at 150 characters — longer payloads are rejected outright. */\nconst SLACK_HEADER_MAX = 150;\n\n/** Block Kit caps each `mrkdwn` text object at 3000 characters — also a whole-message rejection. */\nconst SLACK_TEXT_MAX = 3000;\n\n/**\n * Escape the three characters Slack parses as control characters in message\n * text (`&`, `<`, `>`) — the exact escaping Slack's formatting rules require\n * for user-provided content. Feedback text is typed by anonymous visitors:\n * unescaped, `<!channel>` notifies the whole channel and\n * `<https://evil.example|Reset your password>` renders as a disguised link.\n */\nfunction escapeSlackText(text: string): string {\n return text.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n}\n\n/**\n * Slack message: text fallback + Block Kit blocks for rich rendering. Every\n * `mrkdwn` field is escaped; the `plain_text` header is rendered verbatim by\n * Slack (no markup parsing), so it keeps the raw author name.\n */\nfunction buildSlackPayload(feedback: FeedbackRecord): SlackWebhookPayload {\n const preview = escapeSlackText(truncate(feedback.message));\n const escapeField = (value: string, max: number) => fitEscaped(Array.from(value, escapeSlackText), max);\n // Two halves + \"*From:* ()\" stay under the text-object limit.\n const fromHalf = Math.floor((SLACK_TEXT_MAX - 11) / 2);\n const headline = `New ${feedback.type} feedback from ${feedback.authorName}`;\n return {\n text: `${escapeSlackText(headline)}: ${preview}`,\n blocks: [\n {\n type: \"header\",\n text: { type: \"plain_text\", text: truncate(headline, SLACK_HEADER_MAX), emoji: true },\n },\n {\n type: \"section\",\n text: { type: \"mrkdwn\", text: preview },\n },\n {\n type: \"context\",\n elements: [\n { type: \"mrkdwn\", text: `*Project:* ${escapeField(feedback.projectName, SLACK_TEXT_MAX - 11)}` },\n { type: \"mrkdwn\", text: `*Type:* ${feedback.type}` },\n { type: \"mrkdwn\", text: `*URL:* ${escapeField(feedback.url, SLACK_TEXT_MAX - 7)}` },\n {\n type: \"mrkdwn\",\n text: `*From:* ${escapeField(feedback.authorName, fromHalf)} (${escapeField(feedback.authorEmail, fromHalf)})`,\n },\n ],\n },\n ],\n };\n}\n\n/**\n * Discord API limits — a payload exceeding any of them is rejected whole\n * (HTTP 400), so the notification is lost rather than truncated.\n */\nconst DISCORD_CONTENT_MAX = 2000;\nconst DISCORD_TITLE_MAX = 256;\nconst DISCORD_FIELD_VALUE_MAX = 1024;\n\n/**\n * Characters Discord's markdown treats as syntax: emphasis, code, spoilers,\n * quotes, headings (and `-#` subtext), masked links (`[text](url)`), and\n * `<…>` mentions / channel links. `\\` is escaped too so a visitor's own\n * backslash can't cancel one of ours. `-` is left alone: outside `-#` it only\n * starts a bullet list — cosmetic, and escaping it would litter every\n * hyphenated name and URL.\n */\nconst DISCORD_MARKDOWN = /[\\\\*_~`|>#[\\]()<]/g;\n\n/**\n * An http(s) URL, bare or in Discord's `<url>` link form, which Discord\n * autolinks. Backslash escapes inside it would land in the link (browsers\n * read `\\` as `/`), so its characters are not escaped: only `[`, `]`, `(`,\n * `)` inside the link are percent-encoded, which keeps the address while\n * making sure no masked link can form (see `discordUrlUnits` for where a\n * bare link ends). `<https://…>` is never a mention. The capture group makes\n * `split` keep the URLs.\n */\nconst DISCORD_AUTOLINK = /(<https?:\\/\\/[^\\s<>]+>|https?:\\/\\/[^\\s<>]+)/i;\n\n/** Punctuation Discord leaves out of the end of a bare autolink. */\nconst DISCORD_LINK_TRAILER = `.,:;\"')]`;\n\nconst escapeMarkdownChar = (char: string) => char.replace(DISCORD_MARKDOWN, \"\\\\$&\");\n// Global so it stays complete on any input, not only the single code\n// points `discordUrlUnits` passes in.\nconst encodeLinkChar = (char: string) =>\n char.replace(/[[\\]()]/g, (bracket) => `%${bracket.charCodeAt(0).toString(16).toUpperCase()}`);\nconst count = (text: string, char: string) => text.split(char).length - 1;\n\n/**\n * Units for a URL token (see `DISCORD_AUTOLINK`). A bare URL's trailing\n * `.,:;\"')]` is the sentence around it — `(see https://…).` — and Discord\n * ends the link before it, so it goes out raw: encoded, it would change the\n * address; escaped, the `\\` would. Raw `)` and `]` are harmless because an\n * unescaped `[` is never sent, so no masked link can start. A `)` or `]`\n * closing a bracket opened inside the URL stays in it (`…/Mercury_(planet)`).\n */\nfunction discordUrlUnits(url: string): string[] {\n // Brackets the link opens minus those it closes, counted once and kept up\n // to date as the trailer is dropped: recounting per character is quadratic.\n const open = { \")\": count(url, \"(\") - count(url, \")\"), \"]\": count(url, \"[\") - count(url, \"]\") };\n let end = url.length;\n while (end > 0) {\n const last = url.charAt(end - 1);\n if (!DISCORD_LINK_TRAILER.includes(last)) break;\n if (last === \")\" || last === \"]\") {\n if (open[last] >= 0) break;\n open[last] += 1;\n }\n end -= 1;\n }\n return [...Array.from(url.slice(0, end), encodeLinkChar), ...url.slice(end)];\n}\n\n/**\n * Escape untrusted text for Discord, sized to `max` (see `fitEscaped`).\n * Feedback text is typed by anonymous visitors: unescaped,\n * `[Reset your password](https://evil.example)` renders as a disguised link —\n * the same threat `escapeSlackText` handles. URLs inside the text stay\n * linkable (see `DISCORD_AUTOLINK`).\n */\nfunction escapeDiscordText(text: string, max: number): string {\n // `split` alternates plain text (even indexes) and URLs (odd indexes).\n const units = text\n .split(DISCORD_AUTOLINK)\n .flatMap((part, index) => (index % 2 === 1 ? discordUrlUnits(part) : Array.from(part, escapeMarkdownChar)));\n return fitEscaped(units, max);\n}\n\n/**\n * Discord message: content fallback + embed for rich rendering. Sent with\n * mention parsing disabled — `content` embeds the author name, and Discord\n * would otherwise turn `@everyone` / `@here` into a server-wide ping. Every\n * user-supplied value is markdown-escaped and sized to its slot's limit.\n */\nfunction buildDiscordPayload(feedback: FeedbackRecord): DiscordWebhookPayload {\n const contentLead = `New **${feedback.type}** feedback from **`;\n const titleLead = `${feedback.type} — `;\n // Two halves + \" ()\" stay under the field-value limit.\n const authorHalf = Math.floor((DISCORD_FIELD_VALUE_MAX - 3) / 2);\n return {\n content: `${contentLead}${escapeDiscordText(feedback.authorName, DISCORD_CONTENT_MAX - contentLead.length - 2)}**`,\n embeds: [\n {\n title: `${titleLead}${escapeDiscordText(feedback.projectName, DISCORD_TITLE_MAX - titleLead.length)}`,\n description: escapeDiscordText(feedback.message, 300),\n color: DISCORD_COLORS[feedback.type] ?? DEFAULT_DISCORD_COLOR,\n fields: [\n { name: \"URL\", value: escapeDiscordText(feedback.url, DISCORD_FIELD_VALUE_MAX), inline: false },\n {\n name: \"Author\",\n value: `${escapeDiscordText(feedback.authorName, authorHalf)} (${escapeDiscordText(feedback.authorEmail, authorHalf)})`,\n inline: true,\n },\n { name: \"Viewport\", value: escapeDiscordText(feedback.viewport, DISCORD_FIELD_VALUE_MAX), inline: true },\n ],\n timestamp: new Date(feedback.createdAt).toISOString(),\n },\n ],\n allowed_mentions: { parse: [] },\n };\n}\n\n/** Generic JSON body — the record minus its `clientId`, and its comments minus theirs. */\nfunction buildGenericPayload(feedback: FeedbackRecord): GenericWebhookBody {\n const { clientId: _clientId, comments, ...payload } = feedback;\n if (!comments) return payload;\n return { ...payload, comments: comments.map(({ clientId: _commentClientId, ...comment }) => comment) };\n}\n\n/**\n * Build the JSON body for a single webhook based on its `type`.\n * Exported for tests; not part of the public API.\n *\n * @internal\n */\nexport function buildWebhookPayload<T extends WebhookType | undefined>(\n type: T,\n feedback: FeedbackRecord,\n): T extends \"slack\" ? SlackWebhookPayload : T extends \"discord\" ? DiscordWebhookPayload : GenericWebhookBody {\n switch (type) {\n case \"slack\":\n return buildSlackPayload(feedback) as never;\n case \"discord\":\n return buildDiscordPayload(feedback) as never;\n default:\n return buildGenericPayload(feedback) as never;\n }\n}\n\n/**\n * Dispatch a single webhook. Fire-and-forget: never throws, never rejects.\n *\n * - Builds the type-specific payload.\n * - POSTs with an `AbortSignal` timeout.\n * - On any error (network, non-2xx, timeout, exception), invokes\n * `config.onError(err, feedbackId)` if provided; otherwise logs a one-liner.\n * A `timeoutMs` no timer holds is such an error: nothing is sent.\n */\nexport async function dispatchWebhook(config: WebhookConfig, feedback: FeedbackRecord): Promise<void> {\n const problem = timeoutProblem(config.timeoutMs);\n if (problem) return reportError(config, new RangeError(problem), feedback.id);\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n // Payload building stays inside the try: it can throw too (an invalid\n // `createdAt` makes Discord's `toISOString()` throw a RangeError), and the\n // handler drops this promise, so a rejection would go unhandled.\n try {\n const body = JSON.stringify(buildWebhookPayload(config.type ?? \"generic\", feedback));\n\n // Build merged headers — caller-supplied entries override `Content-Type`\n // when they explicitly need a different mime (rare for chat webhooks, but\n // possible for some generic receivers). Names are case-insensitive: a\n // `content-type` spread next to our `Content-Type` would make fetch send\n // both values combined, so the default is dropped for any spelling.\n const custom = config.headers ?? {};\n const setsContentType = Object.keys(custom).some((name) => name.toLowerCase() === \"content-type\");\n const headers = setsContentType ? custom : { \"Content-Type\": \"application/json\", ...custom };\n\n // Use AbortSignal.timeout when available (Node 17.3+, all modern browsers).\n // Fall back to a manual controller for environments lacking it.\n timer = setTimeout(() => controller.abort(), timeoutMs);\n\n const response = await fetch(config.url, {\n method: \"POST\",\n headers,\n body,\n signal: controller.signal,\n });\n clearTimeout(timer);\n\n if (!response.ok) {\n const err = new Error(`Webhook responded with HTTP ${response.status}`);\n await reportError(config, err, feedback.id);\n }\n } catch (rawError) {\n clearTimeout(timer);\n const err = rawError instanceof Error ? rawError : new Error(String(rawError));\n await reportError(config, err, feedback.id);\n }\n}\n\nasync function reportError(config: WebhookConfig, err: Error, feedbackId: string): Promise<void> {\n if (config.onError) {\n try {\n // Awaited, so an async callback's rejection lands here like a throw.\n await config.onError(err, feedbackId);\n } catch (callbackErr) {\n // Defense-in-depth: a thrown user callback must not bubble back up\n // and crash the request that already succeeded persisting the\n // feedback. Surface the original error too so it isn't silently lost.\n warnWithoutUrl(\n config.url,\n `webhook onError() callback threw for feedback ${feedbackId}: ${String(callbackErr)} (original error: ${err.message})`,\n );\n }\n return;\n }\n warnWithoutUrl(\n config.url,\n `webhook to ${webhookOrigin(config.url)} failed for feedback ${feedbackId}: ${err.message}`,\n );\n}\n\n/**\n * The part of a webhook URL that is safe to log. Slack and Discord embed the\n * credential in the path (`hooks.slack.com/services/T…/B…/<token>`), and any\n * URL may carry userinfo — only the origin identifies the target harmlessly.\n */\nfunction webhookOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return \"<invalid URL>\";\n }\n}\n\n/**\n * `console.warn` with every copy of the webhook URL reduced to its origin.\n * Node's fetch copies the URL it was given into some errors (\"…a URL that\n * includes credentials: <url>\", \"Failed to parse URL from <url>\"), and an\n * `onError` that rethrows or wraps one carries it into its own error.\n */\nfunction warnWithoutUrl(url: string, message: string): void {\n console.warn(`[siteping] ${url ? message.split(url).join(webhookOrigin(url)) : message}`);\n}\n\n/**\n * Dispatch every configured webhook in parallel. Awaiting the returned promise\n * lets tests synchronize on completion, but production callers should drop the\n * promise on the floor (`void dispatchWebhooks(...)`) so the HTTP response\n * isn't held back on slow receivers.\n */\nexport async function dispatchWebhooks(configs: readonly WebhookConfig[], feedback: FeedbackRecord): Promise<void> {\n if (configs.length === 0) return;\n await Promise.all(configs.map((c) => dispatchWebhook(c, feedback)));\n}\n","import {\n type FeedbackCreateInput,\n type FeedbackPayload,\n type FeedbackRecord,\n flattenAnnotation,\n isStoreDuplicate,\n isStoreValueTooLong,\n type SitepingStore,\n} from \"@siteping/core\";\nimport { ERROR_MESSAGES, MAX_ANNOTATIONS_PER_FEEDBACK } from \"../constants.js\";\nimport type { SitepingHandlerBaseOptions, SitepingLifecycleHooks } from \"../options.js\";\nimport type { Pipeline, Scope } from \"../pipeline.js\";\nimport { feedbackCreateSchema } from \"../validation.js\";\nimport { dispatchWebhooks, type WebhookConfig } from \"../webhooks.js\";\n\ninterface CreateFeedbackDependencies<Principal> {\n store: SitepingStore;\n pipeline: Pipeline<Principal>;\n webhooks: ReadonlyArray<WebhookConfig>;\n waitUntil: SitepingHandlerBaseOptions<Principal>[\"waitUntil\"];\n beforeCreate: SitepingHandlerBaseOptions<Principal>[\"beforeCreate\"];\n onCreated: SitepingLifecycleHooks<Principal>[\"onCreated\"];\n}\n\nconst FAILED_TO_CREATE = \"[siteping] Failed to create feedback\";\n\n/** Outcome of one create: `inserted` is false for a replay. */\ninterface CreateOutcome {\n feedback: FeedbackRecord;\n inserted: boolean;\n}\n\n/** Store input from the validated payload — the store never sees wire-only shapes. */\nfunction toCreateInput(data: FeedbackPayload): FeedbackCreateInput {\n return {\n projectName: data.projectName,\n type: data.type,\n message: data.message,\n status: \"open\",\n url: data.url,\n urlPattern: data.urlPattern ?? null,\n viewport: data.viewport,\n userAgent: data.userAgent,\n authorName: data.authorName,\n authorEmail: data.authorEmail,\n clientId: data.clientId,\n annotations: data.annotations.map(flattenAnnotation),\n screenshotDataUrl: data.screenshotDataUrl ?? null,\n screenshotRegion: data.screenshotRegion ?? null,\n diagnostics: data.diagnostics ?? null,\n };\n}\n\n/** `POST` — create a feedback, idempotent on `clientId`. */\nexport function createFeedbackOperation<Principal>({\n store,\n pipeline,\n webhooks,\n waitUntil,\n beforeCreate,\n onCreated,\n}: CreateFeedbackDependencies<Principal>) {\n /**\n * Creates in flight, keyed by clientId. The widget aborts an attempt after\n * 10 s and resends the same payload, so a retry can reach the server while\n * the first attempt is still being processed. With a store that returns the\n * existing record on a duplicate clientId without implementing\n * `createFeedbackIfAbsent`, both requests would pass the replay check and\n * both \"create\" — notifying the webhooks twice. A request whose clientId is\n * in flight shares that outcome instead, and never runs a second insert or\n * upload. Scoped to this handler instance: across processes, the store\n * decides — `createFeedbackIfAbsent`, or its unique constraint (the\n * duplicate path below).\n */\n const inflightCreates = new Map<string, Promise<CreateOutcome>>();\n\n /** Replay check + insert for one validated input. */\n async function createOrReplay(input: FeedbackCreateInput): Promise<CreateOutcome> {\n // The store reports its own inserts: it arbitrates replays and races on\n // the clientId atomically, across handler instances and processes too.\n if (store.createFeedbackIfAbsent) {\n const { feedback, created } = await store.createFeedbackIfAbsent(input);\n return { feedback, inserted: created };\n }\n\n // Otherwise, replay detection up front: stores that return the existing\n // record on a duplicate clientId are indistinguishable from a fresh\n // insert afterwards, and a replayed submission must not notify the\n // webhooks a second time.\n const replayed = await store.findByClientId(input.clientId);\n if (replayed) return { feedback: replayed, inserted: false };\n\n return { feedback: await store.createFeedback(input), inserted: true };\n }\n\n return async (scope: Scope<Principal>, body: unknown): Promise<Response> => {\n const payload = pipeline.validate(scope, feedbackCreateSchema, body);\n if (!payload.ok) return payload.response;\n\n // Defense-in-depth: enforce annotation limit at handler level in addition to schema validation\n if (payload.value.annotations.length > MAX_ANNOTATIONS_PER_FEEDBACK) {\n return pipeline.error(scope, 400, ERROR_MESSAGES.tooManyAnnotations);\n }\n\n let input: FeedbackCreateInput;\n try {\n const validated = toCreateInput(payload.value);\n input = beforeCreate ? await beforeCreate(validated, scope.context) : validated;\n const refusal = await pipeline.authorize(scope, { action: \"create\", projectName: input.projectName });\n if (refusal) return refusal;\n } catch (error) {\n return pipeline.fail(scope, FAILED_TO_CREATE, error);\n }\n\n // Join an in-flight create of this clientId, or start one. The lookup\n // and the registration run in one synchronous turn, so two overlapping\n // requests can never both miss.\n let pending = inflightCreates.get(input.clientId);\n const owner = pending === undefined;\n if (!pending) {\n pending = createOrReplay(input);\n inflightCreates.set(input.clientId, pending);\n }\n\n let outcome: CreateOutcome;\n try {\n outcome = await pending;\n } catch (error) {\n if (isStoreValueTooLong(error)) return pipeline.refuseTooLong(scope, error);\n // Unique-constraint race: the same clientId landed between the replay\n // check above and the insert. The presenter still owns the record.\n // A failing lookup falls through to the JSON 500 — this catch must not\n // throw, or the request loses its response and CORS headers.\n let existing: FeedbackRecord | null = null;\n if (isStoreDuplicate(error)) {\n try {\n existing = await store.findByClientId(input.clientId);\n } catch (lookupError) {\n pipeline.logError(scope, \"[siteping] Failed to look up the duplicate clientId\", {\n error: lookupError,\n projectName: input.projectName,\n });\n }\n }\n if (!existing) return pipeline.fail(scope, FAILED_TO_CREATE, error);\n outcome = { feedback: existing, inserted: false };\n } finally {\n if (owner) inflightCreates.delete(input.clientId);\n }\n\n const { feedback, inserted } = outcome;\n const subject = { feedbackId: feedback.id, projectName: feedback.projectName };\n try {\n // Creation side effects run once per insert: never for a replay, and\n // never for a request that joined another one's in-flight create.\n if (owner && inserted && feedback.projectName === input.projectName) {\n // Fire-and-forget: never awaited, so the widget isn't held back on\n // slow Slack/Discord/generic receivers. `dispatchWebhooks` traps its\n // own errors and reports them through `WebhookConfig.onError`.\n if (webhooks.length > 0) {\n const delivery = dispatchWebhooks(webhooks, feedback);\n try {\n waitUntil?.(delivery);\n } catch (error) {\n pipeline.logError(scope, \"[siteping] waitUntil failed\", { error, ...subject });\n }\n }\n if (onCreated) await pipeline.runHook(scope, \"onCreated\", subject, () => onCreated(feedback, scope.context));\n }\n\n // A clientId is unique across the whole store, so a replay that\n // resolves to another project's record is a boundary violation, not a\n // dedup: refuse it rather than hand that record (email included) to a\n // request scoped to a different project.\n if (feedback.projectName !== input.projectName) {\n return pipeline.error(scope, 409, ERROR_MESSAGES.clientIdUsedByAnotherProject);\n }\n return pipeline.json(scope, await pipeline.presentCreated(scope, feedback), { status: 201 });\n } catch (error) {\n return pipeline.fail(scope, FAILED_TO_CREATE, error);\n }\n };\n}\n","import { isStoreNotFound, type SitepingStore } from \"@siteping/core\";\nimport { ERROR_MESSAGES } from \"../constants.js\";\nimport type { Pipeline, Scope } from \"../pipeline.js\";\nimport { commentDeleteSchema } from \"../validation.js\";\n\ninterface DeleteCommentDependencies<Principal> {\n store: SitepingStore;\n pipeline: Pipeline<Principal>;\n}\n\n/** `DELETE` with a `commentId` — remove that comment from a feedback's thread. */\nexport function deleteCommentOperation<Principal>({ store, pipeline }: DeleteCommentDependencies<Principal>) {\n return async (scope: Scope<Principal>, body: unknown): Promise<Response> => {\n const payload = pipeline.validate(scope, commentDeleteSchema, body);\n if (!payload.ok) return payload.response;\n const { projectName, feedbackId, commentId } = payload.value;\n\n try {\n const refusal = await pipeline.authorize(scope, { action: \"deleteComment\", projectName, feedbackId, commentId });\n if (refusal) return refusal;\n if (!store.deleteComment) return pipeline.error(scope, 501, ERROR_MESSAGES.commentsUnsupported);\n\n // Cross-project guard — see the PATCH operation. The store then scopes\n // the delete to this feedback's thread.\n if (store.verifyProjectOwnership && !(await store.verifyProjectOwnership(feedbackId, projectName))) {\n return pipeline.error(scope, 404, ERROR_MESSAGES.feedbackNotFound);\n }\n\n await store.deleteComment(feedbackId, commentId);\n return pipeline.json(scope, { deleted: true });\n } catch (error) {\n if (isStoreNotFound(error)) return pipeline.error(scope, 404, ERROR_MESSAGES.commentNotFound);\n return pipeline.fail(scope, \"[siteping] Failed to delete comment\", error);\n }\n };\n}\n","import { isStoreNotFound, type SitepingStore } from \"@siteping/core\";\nimport { ERROR_MESSAGES } from \"../constants.js\";\nimport type { SitepingDeletionTarget, SitepingLifecycleHooks } from \"../options.js\";\nimport type { Pipeline, Scope } from \"../pipeline.js\";\nimport { feedbackDeleteSchema } from \"../validation.js\";\n\ninterface DeleteFeedbackDependencies<Principal> {\n store: SitepingStore;\n pipeline: Pipeline<Principal>;\n onDeleting: SitepingLifecycleHooks<Principal>[\"onDeleting\"];\n onDeleted: SitepingLifecycleHooks<Principal>[\"onDeleted\"];\n}\n\n/** `DELETE` — remove one feedback, or every feedback of a project (`deleteAll`). */\nexport function deleteFeedbackOperation<Principal>({\n store,\n pipeline,\n onDeleting,\n onDeleted,\n}: DeleteFeedbackDependencies<Principal>) {\n return async (scope: Scope<Principal>, body: unknown): Promise<Response> => {\n const payload = pipeline.validate(scope, feedbackDeleteSchema, body);\n if (!payload.ok) return payload.response;\n const target: SitepingDeletionTarget =\n \"deleteAll\" in payload.value\n ? { kind: \"project\", projectName: payload.value.projectName }\n : { kind: \"single\", id: payload.value.id, projectName: payload.value.projectName };\n\n try {\n const refusal = await pipeline.authorize(\n scope,\n target.kind === \"project\"\n ? { action: \"deleteAll\", projectName: target.projectName }\n : { action: \"delete\", projectName: target.projectName, feedbackId: target.id },\n );\n if (refusal) return refusal;\n\n // Cross-project guard — see the PATCH operation.\n if (\n target.kind === \"single\" &&\n store.verifyProjectOwnership &&\n !(await store.verifyProjectOwnership(target.id, target.projectName))\n ) {\n return pipeline.error(scope, 404, ERROR_MESSAGES.feedbackNotFound);\n }\n\n if (onDeleting) {\n try {\n await onDeleting(target, scope.context);\n } catch (error) {\n pipeline.logError(scope, \"[siteping] Hook onDeleting aborted the deletion\", { error, target });\n return pipeline.error(scope, 502, ERROR_MESSAGES.deletionAborted);\n }\n }\n\n if (target.kind === \"project\") await store.deleteAllFeedbacks(target.projectName);\n else await store.deleteFeedback(target.id);\n if (onDeleted) await pipeline.runHook(scope, \"onDeleted\", { target }, () => onDeleted(target, scope.context));\n return pipeline.json(scope, { deleted: true });\n } catch (error) {\n if (isStoreNotFound(error)) return pipeline.error(scope, 404, ERROR_MESSAGES.feedbackNotFound);\n return pipeline.fail(scope, \"[siteping] Failed to delete feedback\", error);\n }\n };\n}\n","import type { FeedbackListPermissions, SitepingCapabilities, SitepingStore } from \"@siteping/core\";\nimport { LIST_QUERY_KEYS } from \"../constants.js\";\nimport type { Pipeline } from \"../pipeline.js\";\nimport { getQuerySchema } from \"../validation.js\";\n\ninterface ListFeedbacksDependencies<Principal> {\n store: SitepingStore;\n pipeline: Pipeline<Principal>;\n}\n\n/** Only the query parameters the endpoint understands, as raw strings. */\nfunction readListQuery(request: Request): Record<string, string> {\n const searchParams = new URL(request.url).searchParams;\n const rawQuery: Record<string, string> = {};\n for (const key of LIST_QUERY_KEYS) {\n const value = searchParams.get(key);\n if (value !== null) rawQuery[key] = value;\n }\n return rawQuery;\n}\n\n/** `GET` — a paginated, filtered page of a project's feedbacks. */\nexport function listFeedbacksOperation<Principal>({ store, pipeline }: ListFeedbacksDependencies<Principal>) {\n return async (request: Request): Promise<Response> => {\n const entry = await pipeline.enter(request, \"GET\");\n if (!entry.ok) return entry.response;\n const scope = entry.value;\n\n const query = pipeline.validate(scope, getQuerySchema, readListQuery(request));\n if (!query.ok) return query.response;\n\n try {\n const { projectName } = query.value;\n const refusal = await pipeline.authorize(scope, { action: \"list\", projectName });\n if (refusal) return refusal;\n\n // GET can be public (no apiKey, or \"GET\" in publicEndpoints for widget\n // hosts) — the scope redacts author emails unless the requester may read them.\n const page = await store.getFeedbacks(query.value);\n const [feedbacks, canDeleteAll] = await Promise.all([\n Promise.all(page.feedbacks.map((feedback) => pipeline.present(scope, feedback))),\n pipeline.may(scope, \"DELETE\", { action: \"deleteAll\", projectName }),\n ]);\n return pipeline.json(\n scope,\n {\n ...page,\n feedbacks,\n // Lets clients hide their comment composer, and delete buttons, up front instead of meeting a 501.\n capabilities: {\n comments: store.addComment !== undefined,\n deleteComments: store.deleteComment !== undefined,\n } satisfies SitepingCapabilities,\n // And the actions this requester would be refused.\n permissions: { canDeleteAll } satisfies FeedbackListPermissions,\n },\n { headers: { \"Cache-Control\": pipeline.listCacheControl } },\n );\n } catch (error) {\n return pipeline.fail(scope, \"[siteping] Failed to fetch feedbacks\", error);\n }\n };\n}\n","import { isStoreNotFound, type SitepingStore, toFeedbackUpdate } from \"@siteping/core\";\nimport { ERROR_MESSAGES } from \"../constants.js\";\nimport type { SitepingLifecycleHooks } from \"../options.js\";\nimport type { Pipeline } from \"../pipeline.js\";\nimport { feedbackPatchSchema } from \"../validation.js\";\n\ninterface UpdateFeedbackDependencies<Principal> {\n store: SitepingStore;\n pipeline: Pipeline<Principal>;\n onUpdated: SitepingLifecycleHooks<Principal>[\"onUpdated\"];\n}\n\n/** `PATCH` — change a feedback's status. */\nexport function updateFeedbackOperation<Principal>({\n store,\n pipeline,\n onUpdated,\n}: UpdateFeedbackDependencies<Principal>) {\n return async (request: Request): Promise<Response> => {\n const entry = await pipeline.enter(request, \"PATCH\");\n if (!entry.ok) return entry.response;\n const scope = entry.value;\n\n const payload = await pipeline.readBody(scope, feedbackPatchSchema);\n if (!payload.ok) return payload.response;\n const { id, projectName, status } = payload.value;\n\n try {\n const refusal = await pipeline.authorize(scope, { action: \"update\", projectName, feedbackId: id });\n if (refusal) return refusal;\n\n // Verify project ownership before updating. Any store implementing\n // the optional SitepingStore.verifyProjectOwnership gets the check;\n // duck-typing instead of `instanceof` keeps it bundling-safe and\n // open to third-party adapters. The handler refuses to start with a\n // custom `authorize` over a store without it.\n if (store.verifyProjectOwnership && !(await store.verifyProjectOwnership(id, projectName))) {\n return pipeline.error(scope, 404, ERROR_MESSAGES.feedbackNotFound);\n }\n\n // resolvedAt is the CLOSURE timestamp — set when the feedback enters\n // a terminal status (resolved / wont_fix), cleared otherwise. The\n // derivation lives here at the edge; stores persist what they're given.\n const feedback = await store.updateFeedback(id, toFeedbackUpdate(status));\n if (onUpdated) {\n const subject = { feedbackId: feedback.id, projectName: feedback.projectName };\n await pipeline.runHook(scope, \"onUpdated\", subject, () => onUpdated(feedback, scope.context));\n }\n\n // PATCH can be made public via publicEndpoints / requireAuthForDestructive:\n // false — the scope keeps the author's email out of the update response.\n return pipeline.json(scope, await pipeline.present(scope, feedback));\n } catch (error) {\n if (isStoreNotFound(error)) return pipeline.error(scope, 404, ERROR_MESSAGES.feedbackNotFound);\n return pipeline.fail(scope, \"[siteping] Failed to update feedback\", error);\n }\n };\n}\n","import type { SitepingHttpMethod } from \"./options.js\";\n\n// Cross-site request forgery guards of the mutating methods, for policies\n// that may authenticate with cookies. CORS only hides a response from a\n// foreign page; it never stops a \"simple\" request (a credentialed POST with\n// `Content-Type: text/plain`) from reaching the handler.\n\n/** Longest refused `Origin` written to the log (untrusted input). */\nconst MAX_LOGGED_ORIGIN_LENGTH = 128;\n\n/** Why a mutation is refused: a foreign origin (403) or a non-JSON body (415). */\nexport type CsrfRefusal = { status: 403; origin: string } | { status: 415 };\n\n/**\n * Why a mutation must be refused, or `null` to let it through.\n *\n * - **Origin** (403), only with an `allowedOrigins` list: listed origins and\n * the endpoint's own pass; anything else — the opaque `null` origin of\n * sandboxed frames included — is refused. A request without `Origin`\n * passes: browsers always send it on cross-origin POST/PATCH/DELETE,\n * server-to-server calls and curl do not.\n * - **Content type** (415): only `application/json` (parameters such as\n * `charset` allowed). It is not CORS-safelisted, so a cross-origin browser\n * must pass a preflight first, which fails without CORS headers.\n */\nexport function csrfRefusal(\n request: Request,\n method: SitepingHttpMethod,\n allowedOrigins: ReadonlyArray<string> | undefined,\n): CsrfRefusal | null {\n if (method !== \"POST\" && method !== \"PATCH\" && method !== \"DELETE\") return null;\n\n const origin = request.headers.get(\"Origin\");\n if (allowedOrigins && origin !== null && !allowedOrigins.includes(origin) && origin !== new URL(request.url).origin) {\n // The refused origin is logged: strip control characters and truncate it.\n // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping control characters is the point\n return { status: 403, origin: origin.replace(/[\\u0000-\\u001f\\u007f]/g, \"\").slice(0, MAX_LOGGED_ORIGIN_LENGTH) };\n }\n\n const [mediaType = \"\"] = (request.headers.get(\"Content-Type\") ?? \"\").split(\";\");\n if (mediaType.trim().toLowerCase() !== \"application/json\") return { status: 415 };\n return null;\n}\n","import type { CommentRecord, FeedbackPermissions, FeedbackRecord } from \"@siteping/core\";\nimport type { AccessGate, AccessOutcome } from \"./access.js\";\nimport { DRY_RUN_CONCURRENCY, ERROR_MESSAGES } from \"./constants.js\";\nimport { buildCorsHeaders, type CorsHeaders, withCors } from \"./cors.js\";\nimport { csrfRefusal } from \"./csrf.js\";\nimport type {\n SitepingAuthorizationContext,\n SitepingHandlerBaseOptions,\n SitepingHttpMethod,\n SitepingLogger,\n SitepingRequestContext,\n} from \"./options.js\";\nimport { formatValidationErrors } from \"./validation.js\";\n\n/** A request that passed the access gate — what every operation works with. */\nexport interface Scope<Principal> {\n context: SitepingRequestContext<Principal>;\n corsHeaders: CorsHeaders;\n /** Whether responses to this request may include `authorEmail`. */\n canReadAuthorEmail: boolean;\n}\n\n/** Either a value to continue with, or the response to send right away. */\nexport type Step<Value> = { ok: true; value: Value } | { ok: false; response: Response };\n\n/** The slice of a zod schema the pipeline validates with. */\ninterface Schema<Output> {\n safeParse(\n input: unknown,\n ): { success: true; data: Output } | { success: false; error: Parameters<typeof formatValidationErrors>[0] };\n}\n\ninterface PipelineDependencies<Principal> {\n gate: AccessGate<Principal>;\n allowedOrigins: ReadonlyArray<string> | undefined;\n logger: SitepingLogger;\n describeError: SitepingHandlerBaseOptions<Principal>[\"describeError\"];\n presentFeedback: SitepingHandlerBaseOptions<Principal>[\"presentFeedback\"];\n maxBodyBytes: number;\n}\n\n/** A comment as it goes on the wire. */\nexport type WireComment = Omit<CommentRecord, \"clientId\">;\n\n/** A feedback as it goes on the wire — its thread and the requester's permissions always present. */\nexport type WireFeedback = Omit<FeedbackRecord, \"clientId\" | \"comments\"> & {\n comments: WireComment[];\n permissions: FeedbackPermissions;\n};\n\n/**\n * Serialize a comment for the HTTP wire — `clientId` stripped and\n * `authorEmail` blanked on the same terms as a feedback's (see below).\n */\nfunction toWireComment(comment: CommentRecord, includeEmail: boolean): WireComment {\n const { clientId: _clientId, ...wire } = comment;\n return includeEmail ? wire : { ...wire, authorEmail: \"\" };\n}\n\n/**\n * Serialize a feedback record for the HTTP wire (edge DTO — stores return raw\n * records, redaction happens here).\n *\n * `clientId` is always stripped: it is a browser-local dedup secret, and the\n * POST dedup path returns the full existing record for whoever presents it —\n * exposing it via responses would turn that into a record-theft oracle.\n * `authorEmail` is PII: blanked unless the requester may read it — on the\n * feedback per `includeEmail`, on its comments per `includeCommentEmails`.\n * A store without comments leaves the thread out: it goes out as `[]`.\n * Never mutates the input — webhooks receive the same record object.\n */\nfunction toWireFeedback(\n feedback: FeedbackRecord,\n includeEmail: boolean,\n includeCommentEmails: boolean,\n): Omit<WireFeedback, \"permissions\"> {\n const { clientId: _clientId, comments, ...wire } = feedback;\n return {\n ...wire,\n ...(includeEmail ? {} : { authorEmail: \"\" }),\n comments: (comments ?? []).map((comment) => toWireComment(comment, includeCommentEmails)),\n };\n}\n\n/** Run tasks at most `size` at a time; a task that finishes hands its slot to the next one waiting. */\nfunction createSlots(size: number): <Result>(task: () => Promise<Result>) => Promise<Result> {\n let running = 0;\n const waiting: Array<() => void> = [];\n return async (task) => {\n if (running < size) running += 1;\n else await new Promise<void>((resolve) => waiting.push(resolve));\n try {\n return await task();\n } finally {\n const next = waiting.shift();\n if (next) next();\n else running -= 1;\n }\n };\n}\n\n/**\n * A request body as text, or `null` past `maxBytes`: refused on its\n * `Content-Length` before any byte is read, and counted as it streams in\n * otherwise, so an oversized body is never held in memory whole.\n */\nasync function readText(request: Request, maxBytes: number): Promise<string | null> {\n if (Number(request.headers.get(\"Content-Length\")) > maxBytes) return null;\n if (!request.body) return \"\";\n const reader = request.body.getReader();\n const decoder = new TextDecoder();\n let text = \"\";\n let received = 0;\n for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {\n received += chunk.value.byteLength;\n if (received > maxBytes) {\n await reader.cancel().catch(() => {});\n return null;\n }\n text += decoder.decode(chunk.value, { stream: true });\n }\n return text + decoder.decode();\n}\n\n/** `text` parsed as JSON, or `null` when it is not JSON. */\nfunction parseJson(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return null;\n }\n}\n\n/** Where a request failed, for the log: method and path — never the query, headers or body. */\nfunction requestContext(request: Request): { method: string; path: string } {\n return { method: request.method, path: new URL(request.url).pathname };\n}\n\n/**\n * The steps every operation shares — access check, parsing, authorization,\n * serialization, failure reporting — so each operation module only holds its\n * own logic. Every response carries the request's CORS headers.\n */\nexport function createPipeline<Principal>({\n gate,\n allowedOrigins,\n logger,\n describeError,\n presentFeedback,\n maxBodyBytes,\n}: PipelineDependencies<Principal>) {\n const json = (scope: Pick<Scope<Principal>, \"corsHeaders\">, body: unknown, init?: ResponseInit): Response =>\n withCors(Response.json(body, init), scope.corsHeaders);\n\n const error = (scope: Pick<Scope<Principal>, \"corsHeaders\">, status: number, message: string): Response =>\n json(scope, { error: message }, { status });\n\n /** Log an unexpected failure and answer a JSON 500 — with `describeError`'s hint when it has one. */\n const fail = (\n request: Request,\n scope: Pick<Scope<Principal>, \"corsHeaders\">,\n message: string,\n failure: unknown,\n ): Response => {\n logger.error(message, { error: failure, ...requestContext(request) });\n return error(scope, 500, describeError?.(failure) ?? ERROR_MESSAGES.internalServerError);\n };\n\n /**\n * Log a failure the response does not report, with what it concerns (a\n * record, a deletion target) and the request's method and path, so an\n * operator can tell which record missed its side effect.\n */\n const logError = (scope: Scope<Principal>, message: string, details: Record<string, unknown>): void => {\n logger.error(message, { ...details, ...requestContext(scope.context.request) });\n };\n\n const validate = <Output>(scope: Scope<Principal>, schema: Schema<Output>, input: unknown): Step<Output> => {\n const parsed = schema.safeParse(input);\n if (parsed.success) return { ok: true, value: parsed.data };\n return { ok: false, response: json(scope, { errors: formatValidationErrors(parsed.error) }, { status: 400 }) };\n };\n\n /** Read a JSON body of at most `maxBodyBytes`, not validated yet. */\n const readJson = async (scope: Scope<Principal>): Promise<Step<unknown>> => {\n // A body that cannot be read (already consumed, a broken stream) is no JSON.\n const text = await readText(scope.context.request, maxBodyBytes).catch(() => \"\");\n if (text === null) return { ok: false, response: error(scope, 413, ERROR_MESSAGES.bodyTooLarge) };\n const body = parseJson(text);\n if (!body) return { ok: false, response: error(scope, 400, ERROR_MESSAGES.invalidJson) };\n return { ok: true, value: body };\n };\n\n /** Requests whose failed dry run is logged already — one line per response, however many fail. */\n const loggedDryRunFailures = new WeakSet<Request>();\n /** Each request's dry-run slots — `DRY_RUN_CONCURRENCY` per response. */\n const dryRunSlots = new WeakMap<Request, ReturnType<typeof createSlots>>();\n\n /**\n * Whether this requester may do what `target` describes: the policy admits\n * the method it takes, and `authorize` allows it in a dry run — a few of a\n * response's at a time. A dry run that throws refuses: a permission only\n * hides a control, and the response it goes out with often answers a write\n * that already happened.\n */\n const may = async (\n scope: Scope<Principal>,\n method: SitepingHttpMethod,\n target: Omit<SitepingAuthorizationContext<Principal>, keyof SitepingRequestContext<Principal>>,\n ): Promise<boolean> => {\n const { request } = scope.context;\n const slots = dryRunSlots.get(request) ?? createSlots(DRY_RUN_CONCURRENCY);\n dryRunSlots.set(request, slots);\n try {\n return await slots(\n async () =>\n (await gate.admits(request, method)) && (await gate.authorize({ ...scope.context, ...target, dryRun: true })),\n );\n } catch (failure) {\n if (!loggedDryRunFailures.has(request)) {\n loggedDryRunFailures.add(request);\n logger.error(\"[siteping] authorize failed on a dry run\", {\n error: failure,\n action: target.action,\n ...requestContext(request),\n });\n }\n return false;\n }\n };\n\n /** What this requester may do with a record — `permissions` on the wire. */\n const permissions = async (scope: Scope<Principal>, feedback: FeedbackRecord): Promise<FeedbackPermissions> => {\n const target = { projectName: feedback.projectName, feedbackId: feedback.id };\n const [canChangeStatus, canDelete, canComment, canDeleteComment] = await Promise.all([\n may(scope, \"PATCH\", { action: \"update\", ...target }),\n may(scope, \"DELETE\", { action: \"delete\", ...target }),\n may(scope, \"POST\", { action: \"createComment\", ...target }),\n may(scope, \"DELETE\", { action: \"deleteComment\", ...target }),\n ]);\n return { canChangeStatus, canDelete, canComment, canDeleteComment };\n };\n\n /** Wire shape of a record for this requester (`presentFeedback`, then redaction), with its permissions. */\n const present = async (\n scope: Scope<Principal>,\n feedback: FeedbackRecord,\n includeEmail = scope.canReadAuthorEmail,\n ): Promise<WireFeedback> => ({\n ...toWireFeedback(\n presentFeedback ? presentFeedback(feedback, scope.context) : feedback,\n includeEmail,\n scope.canReadAuthorEmail,\n ),\n permissions: await permissions(scope, feedback),\n });\n\n return {\n json,\n error,\n validate,\n may,\n present,\n\n /**\n * Open a request: the CSRF guards when the policy needs them, then the\n * access gate. A throwing `access` callback (session store down…)\n * answers the logged 500 with CORS headers rather than rejecting, which\n * a browser would only see as an opaque network error.\n */\n async enter(request: Request, method: SitepingHttpMethod): Promise<Step<Scope<Principal>>> {\n const corsHeaders = buildCorsHeaders(request, allowedOrigins);\n if (gate.guardsMutations) {\n const refusal = csrfRefusal(request, method, allowedOrigins);\n if (refusal?.status === 403) {\n logger.error(\"[siteping] Refused a mutation from an origin outside allowedOrigins\", {\n origin: refusal.origin,\n ...requestContext(request),\n });\n return { ok: false, response: error({ corsHeaders }, 403, ERROR_MESSAGES.forbidden) };\n }\n if (refusal) return { ok: false, response: error({ corsHeaders }, 415, ERROR_MESSAGES.unsupportedMediaType) };\n }\n\n let outcome: AccessOutcome<Principal>;\n try {\n outcome = await gate.authenticate(request, method);\n } catch (failure) {\n return {\n ok: false,\n response: fail(request, { corsHeaders }, \"[siteping] Failed to authenticate request\", failure),\n };\n }\n if (!outcome.ok) return { ok: false, response: error({ corsHeaders }, outcome.status, outcome.error) };\n return {\n ok: true,\n value: {\n context: { request, principal: outcome.principal },\n corsHeaders,\n canReadAuthorEmail: outcome.canReadAuthorEmail,\n },\n };\n },\n\n readJson,\n\n /** Read and validate a JSON body. */\n async readBody<Output>(scope: Scope<Principal>, schema: Schema<Output>): Promise<Step<Output>> {\n const body = await readJson(scope);\n return body.ok ? validate(scope, schema, body.value) : body;\n },\n\n /** `null` when the policy allows the request, its 403 otherwise. */\n async authorize(\n scope: Scope<Principal>,\n target: Omit<SitepingAuthorizationContext<Principal>, keyof SitepingRequestContext<Principal>>,\n ): Promise<Response | null> {\n return (await gate.authorize({ ...scope.context, ...target }))\n ? null\n : error(scope, 403, ERROR_MESSAGES.forbidden);\n },\n\n /**\n * Wire shape of a record answering a POST (fresh or replayed): the\n * requester's email permission, unless the policy echoes the email to\n * its submitter — who supplied the feedback's, never its thread's.\n */\n presentCreated(scope: Scope<Principal>, feedback: FeedbackRecord) {\n return present(scope, feedback, gate.echoesAuthorEmailOnCreate || scope.canReadAuthorEmail);\n },\n\n /** Wire shape of a comment answering a POST — the same echo rule as {@link presentCreated}. */\n presentCreatedComment(scope: Scope<Principal>, comment: CommentRecord): WireComment {\n return toWireComment(comment, gate.echoesAuthorEmailOnCreate || scope.canReadAuthorEmail);\n },\n\n /** Whether this caller's comment keeps the `team` role it asks for — see `AccessGate.canCommentAsTeam`. */\n canCommentAsTeam(scope: Scope<Principal>): Promise<boolean> {\n return gate.canCommentAsTeam(scope.context, scope.canReadAuthorEmail);\n },\n\n /**\n * Answer a value the store cannot hold with a 422, which a client does\n * not retry, and log it: the database's columns are narrower than what\n * the validation accepts, which only the operator can fix.\n */\n refuseTooLong(scope: Scope<Principal>, failure: unknown): Response {\n logError(scope, \"[siteping] A value is too long for the store\", { error: failure });\n return error(scope, 422, ERROR_MESSAGES.valueTooLong);\n },\n\n /** Log an unexpected failure of an operation and answer its JSON 500. */\n fail(scope: Scope<Principal>, message: string, failure: unknown): Response {\n return fail(scope.context.request, scope, message, failure);\n },\n\n logError,\n\n /** Run a lifecycle hook after a write; a failure is logged with `subject`, never surfaced — the write happened. */\n async runHook(\n scope: Scope<Principal>,\n name: string,\n subject: Record<string, unknown>,\n invoke: () => void | Promise<void>,\n ): Promise<void> {\n try {\n await invoke();\n } catch (failure) {\n logError(scope, `[siteping] Hook ${name} failed`, { error: failure, ...subject });\n }\n },\n\n /** The list response's `Cache-Control`. */\n listCacheControl: gate.listCacheControl,\n };\n}\n\nexport type Pipeline<Principal> = ReturnType<typeof createPipeline<Principal>>;\n","import { hasOwn } from \"@siteping/core\";\nimport { type AccessGate, createAccessGate, createApiKeyGate } from \"./access.js\";\nimport { DEFAULT_MAX_BODY_BYTES } from \"./constants.js\";\nimport { preflightResponse } from \"./cors.js\";\nimport { createCommentOperation } from \"./operations/create-comment.js\";\nimport { createFeedbackOperation } from \"./operations/create-feedback.js\";\nimport { deleteCommentOperation } from \"./operations/delete-comment.js\";\nimport { deleteFeedbackOperation } from \"./operations/delete-feedback.js\";\nimport { listFeedbacksOperation } from \"./operations/list-feedbacks.js\";\nimport { updateFeedbackOperation } from \"./operations/update-feedback.js\";\nimport type {\n SitepingAccessHandlerOptions,\n SitepingApiKeyHandlerOptions,\n SitepingHandler,\n SitepingHandlerBaseOptions,\n SitepingHandlerOptions,\n SitepingHttpMethod,\n SitepingLogger,\n SitepingPrincipal,\n} from \"./options.js\";\nimport { createPipeline, type Pipeline, type Scope } from \"./pipeline.js\";\nimport { checkWebhookTimeouts, type WebhookConfig } from \"./webhooks.js\";\n\nconst consoleLogger: SitepingLogger = {\n error(message, context) {\n console.error(message, context);\n },\n};\n\n/**\n * `logger`, made safe to call anywhere: a throw, or a rejection of what it\n * returns — unhandled, fatal in Node by default — falls back to `console.error`.\n */\nfunction safeLogger(logger: SitepingLogger): SitepingLogger {\n return {\n error(message, context) {\n try {\n Promise.resolve(logger.error(message, context)).catch(() => consoleLogger.error(message, context));\n } catch {\n consoleLogger.error(message, context);\n }\n },\n };\n}\n\n/** An operation on a request that passed the access gate, with its JSON body. */\ntype BodyOperation<Principal> = (scope: Scope<Principal>, body: unknown) => Promise<Response>;\n\n/**\n * One method, two resources: a JSON body carrying `commentKey` targets a\n * comment, any other body a feedback — whose payloads never carry that key.\n */\nfunction routeByBody<Principal>(\n pipeline: Pipeline<Principal>,\n method: SitepingHttpMethod,\n commentKey: \"feedbackId\" | \"commentId\",\n comment: BodyOperation<Principal>,\n feedback: BodyOperation<Principal>,\n): (request: Request) => Promise<Response> {\n return async (request) => {\n const entry = await pipeline.enter(request, method);\n if (!entry.ok) return entry.response;\n const body = await pipeline.readJson(entry.value);\n if (!body.ok) return body.response;\n return (hasOwn(body.value, commentKey) ? comment : feedback)(entry.value, body.value);\n };\n}\n\n/**\n * Create the SitePing HTTP API over any `SitepingStore`, using only the Fetch\n * API (`Request` → `Response`): one handler per method, to mount in Next.js\n * route handlers, Hono, Remix, SvelteKit, Bun, Deno or edge workers.\n *\n * **Rate limiting** is not handled by this library. Apply rate limiting at the\n * framework or reverse-proxy level (e.g. Next.js middleware, Nginx, Cloudflare).\n * The POST endpoint in particular should be rate-limited to prevent abuse, since\n * the widget typically calls it from unauthenticated browser contexts.\n *\n * Access is either the built-in `apiKey` policy or your own `access` policy\n * (sessions, JWTs, roles…) — see `SitepingAccessHandlerOptions`.\n *\n * @throws Error without a `store`; with a `maxBodyBytes` that is not a\n * positive integer, or a webhook `timeoutMs` no timer holds; in production\n * without `apiKey` (see `requireAuthForDestructive`); or with\n * `access.authorize` over a store without `verifyProjectOwnership`, since\n * PATCH/DELETE could then reach a record of a project the caller is not\n * authorized for.\n *\n * @example Next.js App Router — `app/api/siteping/route.ts`\n * ```ts\n * import { createSitepingHandler } from '@siteping/server'\n * import { store } from '@/lib/siteping-store'\n *\n * export const { GET, POST, PATCH, DELETE, OPTIONS } = createSitepingHandler({\n * store,\n * apiKey: process.env.SITEPING_API_KEY,\n * })\n * ```\n */\nexport function createSitepingHandler<Principal extends SitepingPrincipal>(\n options: SitepingAccessHandlerOptions<Principal>,\n): SitepingHandler;\nexport function createSitepingHandler(options: SitepingApiKeyHandlerOptions): SitepingHandler;\n/** Options assembled at runtime, either policy. */\nexport function createSitepingHandler<Principal extends SitepingPrincipal>(\n options: SitepingHandlerOptions<Principal>,\n): SitepingHandler;\nexport function createSitepingHandler<Principal extends SitepingPrincipal>(\n options: SitepingHandlerOptions<Principal>,\n): SitepingHandler {\n // Both policies share the callbacks, typed over `Principal` (`null` under apiKey).\n const {\n store,\n allowedOrigins,\n webhooks,\n waitUntil,\n beforeCreate,\n beforeComment,\n presentFeedback,\n hooks = {},\n logger: customLogger,\n describeError,\n maxBodyBytes = DEFAULT_MAX_BODY_BYTES,\n } = options as SitepingHandlerBaseOptions<Principal>;\n if (!store) {\n throw new Error(\"[siteping] createSitepingHandler requires a `store`.\");\n }\n if (!Number.isSafeInteger(maxBodyBytes) || maxBodyBytes <= 0) {\n throw new Error(\n `[siteping] createSitepingHandler: \\`maxBodyBytes\\` must be a positive integer, got ${maxBodyBytes}.`,\n );\n }\n // A custom `authorize` may scope callers to projects, but PATCH/DELETE\n // address records by id: without the ownership check, the project a caller\n // claims (and is authorized for) need not be the record's. Fail closed.\n if (options.access?.authorize && !store.verifyProjectOwnership) {\n throw new Error(\n \"[siteping] createSitepingHandler: `access.authorize` needs a store implementing `verifyProjectOwnership`. \" +\n \"Without it, a caller authorized for one project could PATCH or DELETE another project's feedback by id.\",\n );\n }\n\n // The `apiKey` policy never resolves a principal: its scopes carry `null`.\n const gate = options.access\n ? createAccessGate(options.access)\n : (createApiKeyGate(options) as AccessGate<unknown> as AccessGate<Principal>);\n const logger = customLogger ? safeLogger(customLogger) : consoleLogger;\n const pipeline = createPipeline({ gate, allowedOrigins, logger, describeError, presentFeedback, maxBodyBytes });\n // Normalised once so every POST skips the allocation; an empty list\n // short-circuits dispatch.\n const webhookList: ReadonlyArray<WebhookConfig> = webhooks\n ? Array.isArray(webhooks)\n ? (webhooks as ReadonlyArray<WebhookConfig>)\n : [webhooks as WebhookConfig]\n : [];\n checkWebhookTimeouts(webhookList);\n\n return {\n OPTIONS: (request: Request): Response => preflightResponse(request, allowedOrigins),\n POST: routeByBody(\n pipeline,\n \"POST\",\n \"feedbackId\",\n createCommentOperation({ store, pipeline, beforeComment }),\n createFeedbackOperation({\n store,\n pipeline,\n webhooks: webhookList,\n waitUntil,\n beforeCreate,\n // Bound so hooks written as class methods keep their `this`.\n onCreated: hooks.onCreated?.bind(hooks),\n }),\n ),\n GET: listFeedbacksOperation({ store, pipeline }),\n PATCH: updateFeedbackOperation({ store, pipeline, onUpdated: hooks.onUpdated?.bind(hooks) }),\n DELETE: routeByBody(\n pipeline,\n \"DELETE\",\n \"commentId\",\n deleteCommentOperation({ store, pipeline }),\n deleteFeedbackOperation({\n store,\n pipeline,\n onDeleting: hooks.onDeleting?.bind(hooks),\n onDeleted: hooks.onDeleted?.bind(hooks),\n }),\n ),\n };\n}\n"],"mappings":"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","names":["z"]}
|
|
1
|
+
{"version":3,"sources":["../../core/src/email.ts","../../core/src/type-utils.ts","../../core/src/errors.ts","../../core/src/types.ts","../src/constants.ts","../src/access.ts","../src/cors.ts","../src/webhooks.ts","../src/notify.ts","../src/validation.ts","../src/operations/create-comment.ts","../src/operations/create-feedback.ts","../src/operations/delete-comment.ts","../src/operations/delete-feedback.ts","../src/operations/list-feedbacks.ts","../src/operations/update-feedback.ts","../src/csrf.ts","../src/pipeline.ts","../src/handler.ts"],"sourcesContent":["/**\n * The one email pattern every Siteping surface validates against.\n *\n * The widget's identity modal and the HTTP handler's schema used to disagree\n * (a permissive regex on one side, an ASCII-only default on the other), so an\n * address the modal accepted — and persisted in localStorage for good — came\n * back as a 400 on every submission afterwards. One pattern, imported on both\n * sides, makes that drift impossible.\n *\n * Deliberately Unicode-aware: internationalised local parts and domains\n * (`françois@exemple.fr`, `user@münchen.de`, `user@हिंदी.भारत`) are real\n * addresses and common in the audience this widget serves. Combining marks\n * (`\\p{M}`) are allowed alongside letters — scripts like Devanagari need them,\n * and NFD-composed input spells `ç` as `c` + U+0327 — except as the first\n * character of a domain label. Structure follows the usual rules — a local\n * part of 1 to 64 characters with no leading, trailing or doubled dot,\n * dot-separated domain labels of at most 63 characters that neither start nor\n * end with a hyphen, and a final label of at least two characters.\n */\nexport const EMAIL_PATTERN =\n /^(?!\\.)(?!.*\\.\\.)[\\p{L}\\p{M}\\p{N}!#$%&'*+/=?^_`{|}~.-]{0,63}[\\p{L}\\p{M}\\p{N}!#$%&'*+/=?^_`{|}~-]@(?:[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?\\.)+[\\p{L}\\p{N}][\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}]$/u;\n\n/** Whether `value` is an email address Siteping accepts — see {@link EMAIL_PATTERN}. */\nexport function isValidEmail(value: string): boolean {\n return EMAIL_PATTERN.test(value);\n}\n","/**\n * General-purpose TypeScript utility types used across `@siteping/*`.\n *\n * These are kept dependency-free and re-exported from the package entry\n * so adapters and integrators can rely on the same primitives the core\n * uses internally.\n */\n\n/**\n * Force TypeScript to expand a computed type into a flat object literal in\n * tooltips and error messages. Purely cosmetic — same structural type, just\n * easier to read.\n *\n * @example\n * type Raw = Omit<FeedbackRecord, \"annotations\"> & { annotations: number };\n * type Pretty = Prettify<Raw>; // displayed as a flat object\n */\nexport type Prettify<T> = { [K in keyof T]: T[K] } & {};\n\n/**\n * Returns `Y` when `A` is exactly assignable to `B` and vice-versa,\n * otherwise `N`. Powers compile-time equality assertions.\n */\nexport type IfEquals<A, B, Y = true, N = false> =\n (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? Y : N;\n\n/**\n * Compile-time exact-type guard — resolves to `true` when `Actual` and\n * `Expected` are identical, `never` otherwise. Assign the result to a\n * `const _lock: AssertEqual<A, B> = true;` so any drift becomes a compile\n * error at the declaration site.\n */\nexport type AssertEqual<Actual, Expected> = IfEquals<Actual, Expected, true, never>;\n\n/**\n * JSON-serialized shape of `T` — the wire form produced by `Response.json()`\n * / `JSON.stringify`: `Date` becomes ISO `string` (nullability preserved),\n * arrays are serialized element-wise, everything else is untouched.\n *\n * Used to derive the `*Response` API types from the `*Record` store types so\n * the two can never drift: add a field to `FeedbackRecord` and\n * `FeedbackResponse` follows automatically.\n */\nexport type Serialized<T> = {\n [K in keyof T]: T[K] extends Date\n ? string\n : T[K] extends Date | null\n ? string | null\n : T[K] extends (infer U)[]\n ? Serialized<U>[]\n : T[K];\n};\n\n/**\n * Read-only all the way down, for plain data that is deeply frozen at\n * runtime: nested objects get read-only properties, arrays become\n * `readonly` arrays.\n */\nexport type DeepReadonly<T> = T extends readonly (infer U)[]\n ? readonly DeepReadonly<U>[]\n : T extends object\n ? { readonly [K in keyof T]: DeepReadonly<T[K]> }\n : T;\n\n/**\n * Type guard that narrows `value` to a non-null `Record<PropertyKey, unknown>`.\n * Useful when validating arbitrary inputs before reading fields.\n */\nexport function isRecord(value: unknown): value is Record<PropertyKey, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\n/**\n * Returns true when `value` is an object that exposes the requested key.\n * Type-narrows `value` so the property can be accessed without further\n * casting — a strictly typed replacement for `\"k\" in obj`.\n *\n * Named after the standardised `Object.hasOwn` helper rather than the\n * legacy `Object.prototype.hasOwnProperty`, which the linter forbids\n * shadowing.\n */\nexport function hasOwn<K extends PropertyKey>(value: unknown, key: K): value is Record<K, unknown> {\n return isRecord(value) && key in value;\n}\n","import { hasOwn } from \"./type-utils.js\";\n\n/**\n * Typed error hierarchy for Siteping client/server boundaries.\n *\n * Consumers match an error with a guard — `isSitepingError`,\n * `isSitepingConfigError`, a store guard such as `isStoreNotFound` — then\n * read `code` / `retryable` instead of pattern-matching messages. The guards\n * are structural, never `instanceof`: every published package bundles its\n * own copy of these classes (tsup `noExternal`), so an error one package\n * throws fails `instanceof` against another's class. The widget and the\n * dashboard export the guard and the types, not the classes.\n *\n * Usage on the widget side (api-client.ts):\n * - fetch failures / aborts / timeouts → `SitepingNetworkError` (retryable)\n * - HTTP 4xx (except 401/403/408/429) → `SitepingValidationError` (not retryable)\n * - HTTP 401 / 403 → `SitepingAuthError` (not retryable)\n * - HTTP 5xx / 408 / 429 → `SitepingError` code `\"SERVER\"` (retryable)\n *\n * Store mode (store-client.ts): a `createFeedback` that does not settle in\n * time → `SitepingError` with code `\"TIMEOUT\"` (retryable); a reply to a\n * store that keeps no comments → `\"SERVER\"`, not retryable.\n *\n * `retryable` says whether the same request may succeed later, unchanged —\n * meta information for host apps that wire their own retry/queue/backoff\n * strategy; the widget already retries and queues transient failures itself.\n */\n\n/** Discriminant string carried by every `SitepingError`. */\nexport type SitepingErrorCode = \"NETWORK\" | \"VALIDATION\" | \"AUTH\" | \"SERVER\" | \"TIMEOUT\";\n\nexport class SitepingError<TCode extends SitepingErrorCode = SitepingErrorCode> extends Error {\n readonly code: TCode;\n readonly retryable: boolean;\n\n constructor(message: string, code: TCode, retryable: boolean) {\n super(message);\n this.code = code;\n this.retryable = retryable;\n this.name = \"SitepingError\";\n }\n}\n\n/** Network-level failure: connection refused, DNS, CORS, timeout, abort. Retryable. */\nexport class SitepingNetworkError extends SitepingError<\"NETWORK\"> {\n constructor(message: string) {\n super(message, \"NETWORK\", true);\n this.name = \"SitepingNetworkError\";\n }\n}\n\n/**\n * Server rejected the request (4xx, not auth). Validation problem on the\n * client side. `status` is the HTTP status when a response said so: a 404\n * (the target is gone) and a 409 (a full thread) call for other answers\n * than a 400.\n */\nexport class SitepingValidationError extends SitepingError<\"VALIDATION\"> {\n readonly status: number | undefined;\n\n constructor(message: string, status?: number) {\n super(message, \"VALIDATION\", false);\n this.name = \"SitepingValidationError\";\n this.status = status;\n }\n}\n\n/** Server rejected auth (401 or 403). Not retryable without fresh credentials. */\nexport class SitepingAuthError extends SitepingError<\"AUTH\"> {\n /**\n * `401`: the credentials are missing or no longer work — drop a dead token\n * on this one. `403`: they work, and the server's policy refuses this request.\n */\n readonly status: 401 | 403;\n\n constructor(message: string, status: 401 | 403) {\n super(message, \"AUTH\", false);\n this.name = \"SitepingAuthError\";\n this.status = status;\n }\n}\n\n/**\n * Every error the widget and the dashboard report for a failed call, told\n * apart by `code`: narrowing on it reaches a subclass's own fields, such as\n * an `AUTH` error's `status`.\n */\nexport type SitepingClientError =\n | SitepingNetworkError\n | SitepingValidationError\n | SitepingAuthError\n | SitepingError<\"SERVER\" | \"TIMEOUT\">;\n\nconst SITEPING_ERROR_CODES: readonly string[] = [\n \"NETWORK\",\n \"VALIDATION\",\n \"AUTH\",\n \"SERVER\",\n \"TIMEOUT\",\n] satisfies readonly SitepingErrorCode[];\n\n/**\n * Whether `error` is a `SitepingError` (any subclass) — matched on its\n * fields, a known `code` and a boolean `retryable`, so it holds for an error\n * another package's copy of the class threw. Anything else reaching an\n * `onError` (a store's own error, a panel action's) fails it.\n */\nexport function isSitepingError(error: unknown): error is SitepingClientError {\n return (\n hasOwn(error, \"code\") &&\n hasOwn(error, \"retryable\") &&\n typeof error.retryable === \"boolean\" &&\n typeof error.code === \"string\" &&\n SITEPING_ERROR_CODES.includes(error.code)\n );\n}\n\n/** Stable codes of the errors a handler factory throws for options it cannot run with. */\nexport type SitepingConfigErrorCode =\n | \"CONFIG_STORE_MISSING\"\n | \"CONFIG_MAX_BODY_BYTES_INVALID\"\n | \"CONFIG_OWNERSHIP_CHECK_MISSING\"\n | \"CONFIG_WEBHOOK_TIMEOUT_INVALID\"\n | \"CONFIG_API_KEY_REQUIRED\";\n\n/**\n * Thrown by `createSitepingHandler` while it is created, never per request,\n * when its options cannot run safely. The message says how to fix it; branch\n * on `code`, after {@link isSitepingConfigError}.\n */\nexport class SitepingConfigError extends Error {\n readonly code: SitepingConfigErrorCode;\n\n constructor(code: SitepingConfigErrorCode, message: string) {\n super(message);\n this.code = code;\n this.name = \"SitepingConfigError\";\n }\n}\n\n/**\n * Whether `error` is a `SitepingConfigError` — matched on its `name` and a\n * `CONFIG_` code rather than `instanceof`, for the same cross-bundle reason\n * as {@link isSitepingError}.\n */\nexport function isSitepingConfigError(error: unknown): error is SitepingConfigError {\n return (\n hasOwn(error, \"name\") &&\n error.name === \"SitepingConfigError\" &&\n hasOwn(error, \"code\") &&\n typeof error.code === \"string\" &&\n error.code.startsWith(\"CONFIG_\")\n );\n}\n","import { type AssertEqual, type DeepReadonly, hasOwn, type Prettify, type Serialized } from \"./type-utils.js\";\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\n/** FAB anchor — bottom-corner placement supported by the widget. */\nexport type SitepingPosition = \"bottom-right\" | \"bottom-left\";\n\n/**\n * Visual theme of the widget and the dashboard — `auto` follows the system\n * preference, live: an OS switch mid-session re-themes the UI.\n */\nexport type SitepingTheme = \"light\" | \"dark\" | \"auto\";\n\n/** Built-in UI locales shipped with the widget. */\nexport const BUILTIN_LOCALES = [\"en\", \"fr\", \"de\", \"es\", \"it\", \"pt\", \"ru\", \"ja\", \"ko\"] as const;\nexport type BuiltinLocale = (typeof BUILTIN_LOCALES)[number];\n\n/**\n * Locale identifier accepted by the widget. Built-in locales are kept as\n * literal strings so editors auto-complete them, but arbitrary BCP-47 tags\n * are also accepted (custom dictionaries registered via `registerLocale`).\n */\nexport type SitepingLocale = BuiltinLocale | (string & {});\n\n/**\n * Reasons reported through `SitepingConfig.onSkip` — production environment,\n * viewport narrower than `minViewportWidth` (`\"mobile\"`), or server-side\n * rendering (no `window`/`document`).\n */\nexport type SitepingSkipReason = \"production\" | \"mobile\" | \"ssr\";\n\n/** Per-channel + per-buffer-size diagnostics configuration. */\nexport interface DiagnosticsCaptureOptions {\n console?: boolean | undefined;\n network?: boolean | undefined;\n /** Console buffer size — default and maximum 50 (the server's cap; larger values are clamped). */\n maxConsoleEntries?: number | undefined;\n /** Failed-request buffer size — default and maximum 20 (the server's cap; larger values are clamped). */\n maxNetworkEntries?: number | undefined;\n}\n\n/** Identity payload supplied by the host application — bypasses the modal. */\nexport interface SitepingIdentity {\n name: string;\n email: string;\n}\n\n/**\n * Max length of an identity's `name` and `email` — the HTTP schema's\n * `authorName` / `authorEmail` cap. The widget's modal enforces it so a value\n * it persists is never a 400 on every later submission.\n */\nexport const IDENTITY_FIELD_MAX_LENGTH = 200;\n\n/**\n * Max length of a `projectName` and of a feedback's or comment's `clientId`\n * — the identifiers the stores index. 191 characters is the longest\n * `VARCHAR` every MySQL version indexes under `utf8mb4` (767 bytes), which\n * is why Prisma maps a plain `String` to `VARCHAR(191)` there: the HTTP\n * schema caps them at that length, so their columns need no prefix index.\n */\nexport const PROJECT_NAME_MAX_LENGTH = 191;\nexport const CLIENT_ID_MAX_LENGTH = 191;\n\n/** Deep-link configuration — controls how a feedback id is read from the URL. */\nexport interface SitepingDeepLinkOptions {\n /** Query parameter name carrying the feedback id. Defaults to `\"siteping\"`. */\n param?: string | undefined;\n}\n\n/**\n * The feedback handed to panel action callbacks: a detached, deeply frozen\n * copy, typed read-only all the way down. Type your own helpers with it — a\n * `FeedbackResponse` parameter does not accept the frozen copy.\n */\nexport type SitepingPanelActionFeedback = DeepReadonly<FeedbackResponse>;\n\n/**\n * Helpers passed to a panel action's `onAction` as its second argument.\n * Both do nothing once the widget has been destroyed.\n */\nexport interface SitepingPanelActionContext {\n /**\n * Re-fetch the panel list and markers, then re-render the detail view with\n * the updated feedback — or go back to the list when it no longer matches\n * the panel filters. Call it after your action changed the feedback\n * server-side (e.g. moved it to `in_progress` once a ticket exists).\n */\n refresh: () => Promise<void>;\n /** Close the feedback panel. */\n close: () => void;\n}\n\n/**\n * Fields shared by both kinds of {@link SitepingPanelAction}.\n *\n * Do not use this type directly — use {@link SitepingPanelAction}.\n */\nexport interface SitepingPanelActionBase {\n /** Stable, unique identifier — becomes `data-action-id` on the rendered control. */\n id: string;\n /**\n * Visible label, rendered as plain text. Host-provided verbatim — not\n * routed through the widget i18n, since hosts localize their own strings.\n */\n label: string;\n /**\n * Optional inline SVG markup rendered before the label. It is parsed\n * inertly and reduced to plain shapes — scripts, event handlers, links,\n * styles and external references are dropped — but keep it static markup\n * you control. Markup that is not an `<svg>` is ignored with a warning.\n */\n icon?: string | undefined;\n /**\n * Per-feedback visibility predicate. Return `false` to omit the action\n * for that feedback. Defaults to always visible. A throw hides the action\n * and is reported through `onError`.\n */\n visible?: ((feedback: SitepingPanelActionFeedback) => boolean) | undefined;\n}\n\n/** A panel action rendered as a button that runs host code. */\nexport interface SitepingPanelButtonAction extends SitepingPanelActionBase {\n /**\n * Invoked on click. While a returned promise is pending the detail view's\n * action buttons are disabled and the clicked button shows a spinner —\n * also after `context.refresh()` re-renders the view, and when the user\n * comes back to that feedback.\n * Throws and rejections are reported through `onError` and restore the\n * buttons; the detail view stays open either way.\n */\n onAction: (feedback: SitepingPanelActionFeedback, context: SitepingPanelActionContext) => void | Promise<void>;\n /** Not available on a button action — use either `onAction` or `href`, never both. */\n href?: never;\n}\n\n/** A panel action rendered as a link. */\nexport interface SitepingPanelLinkAction extends SitepingPanelActionBase {\n /**\n * Link target — a URL, or a function building one from the feedback.\n * Relative URLs resolve against the page. Only `http:`, `https:` and\n * `mailto:` are rendered: a static `href` with any other scheme\n * (`javascript:` included) skips the action with a warning; a function\n * returning one hides the action and is reported through `onError`. Web\n * links open in a new tab with `rel=\"noopener noreferrer\"`.\n */\n href: string | ((feedback: SitepingPanelActionFeedback) => string);\n /** Not available on a link action — use either `onAction` or `href`, never both. */\n onAction?: never;\n}\n\n/**\n * A host-defined action rendered in the feedback detail view, below the\n * built-in Resolve / Delete buttons: a button running your code\n * (`onAction`) or a link (`href`) — never both.\n *\n * Hosts use this to bridge feedbacks into their own systems — create a\n * ticket, dispatch to a bot, open the feedback in their tracker — without\n * forking the panel. Callbacks receive a detached, deeply frozen copy of\n * the feedback: read it freely, it can never alter what the panel displays.\n */\nexport type SitepingPanelAction = SitepingPanelButtonAction | SitepingPanelLinkAction;\n\n/**\n * Extra request headers for HTTP mode — a static map, or a factory (sync or\n * async) invoked once per request to produce fresh values (e.g. a short-lived\n * session token).\n */\nexport type SitepingHeadersOption =\n | Record<string, string>\n | (() => Record<string, string> | Promise<Record<string, string>>);\n\n/**\n * Options shared by both widget modes (HTTP and direct store).\n *\n * Do not use this type directly — use {@link SitepingConfig}, the\n * discriminated union that adds the mode-specific fields.\n */\nexport interface SitepingBaseConfig {\n /** Required — project identifier used to scope feedbacks */\n projectName: string;\n /** FAB position — defaults to 'bottom-right' */\n position?: SitepingPosition | undefined;\n /**\n * Show the \"toggle markers visibility\" item in the FAB radial menu.\n * Defaults to `true` (current behavior). Set to `false` to hide that\n * item entirely — useful for hosts that always want markers visible\n * (e.g. dedicated review tools) or that find the eye icon redundant\n * when no marker is on screen.\n *\n * Hiding the item also removes its keyboard navigation slot — the\n * remaining two items still respond to ArrowUp/ArrowDown/Home/End.\n * The marker-visibility state itself is unaffected; markers stay\n * visible (the previous default state) and `annotations:toggle` is\n * simply never emitted from the FAB.\n */\n showAnnotationsToggle?: boolean | undefined;\n /** Accent color for the widget UI — defaults to '#0066ff' */\n accentColor?: string | undefined;\n /**\n * Render the widget even when it would normally be skipped — this bypasses\n * BOTH the production-environment guard AND the `minViewportWidth` guard.\n * It does NOT bypass the SSR guard: without `window`/`document` the widget\n * never renders and `onSkip(\"ssr\")` fires instead.\n * Defaults to false. Use it for dedicated review tools, staging environments,\n * or responsive testing where you always want the widget present.\n */\n forceShow?: boolean | undefined;\n /**\n * Minimum viewport width (px) at or above which the widget renders. Below it,\n * the widget is skipped and `onSkip(\"mobile\")` fires. Defaults to `0`: the\n * widget renders at every width, and phones get a compact layout (bottom\n * sheets, touch-sized controls).\n *\n * Set it (e.g. `768`) to keep the widget off small screens, or use\n * `forceShow` to bypass the viewport check entirely.\n */\n minViewportWidth?: number | undefined;\n /** Enable debug logging of lifecycle events — defaults to false */\n debug?: boolean | undefined;\n /** Color theme — defaults to `light`; `auto` follows the system preference live. */\n theme?: SitepingTheme | undefined;\n /** UI locale — defaults to 'en'. Built-in: en, fr, de, es, it, pt (Brazilian), ru, ja, ko. Any other string falls back to English. */\n locale?: SitepingLocale | undefined;\n /**\n * Returns the current page scope for annotations and panel filtering.\n * Called on initial markers load and on `instance.refresh()`.\n *\n * Default: `{ url: window.location.pathname, urlPattern: null }` — annotations\n * are scoped strictly to the current pathname.\n *\n * Apps with parameterized routes (e.g. React Router) should return both the\n * concrete URL and the route template (e.g. `/orders/:orderId`) so the panel\n * can offer a \"this type of page\" filter that groups feedbacks by template.\n */\n getPageScope?: (() => PageScope) | undefined;\n /**\n * When true (default), the widget filters initial markers and panel results\n * by `feedback.url === scope.url`, so annotations created on one page never\n * leak to other pages — even if their CSS selector accidentally matches.\n * Set to `false` for a project-wide view: the panel lists every feedback of\n * the project, and a marker shows wherever its anchor resolves — for\n * feedback on what every page shares, such as a header or a footer.\n */\n scopeAnnotationsByUrl?: boolean | undefined;\n /**\n * Capture a JPEG screenshot of the annotated area on submit. Defaults to\n * `false` — opt-in because:\n *\n * - it adds runtime weight (~60 KB gzip dynamic chunk for html2canvas-pro,\n * loaded only on first capture),\n * - it embeds page content in the feedback (privacy/GDPR consideration —\n * inform end users in your widget host UI when enabling).\n *\n * `html2canvas-pro` ships as a regular dependency of `@siteping/widget` so the\n * dynamic import always resolves; you don't need to install anything extra.\n *\n * **Masking sensitive elements:** add `data-siteping-ignore=\"true\"` to any\n * element you do NOT want captured (password fields, credit-card forms,\n * API tokens shown in the UI, etc.). The capture predicate skips matching\n * elements *and their descendants*. Do this BEFORE turning on screenshots\n * in production — once a feedback is saved, the screenshot is in your DB\n * (or object storage) regardless of what was on the page.\n */\n enableScreenshot?: boolean | undefined;\n /**\n * Enable right-click (`contextmenu`) to instantly open the comment composer\n * at the cursor location. When enabled, a document-level listener intercepts\n * right-clicks, prevents the browser's native context menu, and enters the\n * annotation flow anchored to the element under the cursor. Defaults to\n * `false` — the browser's native context menu is never hijacked unless the\n * host explicitly opts in.\n *\n * Keyboard-triggered context menus (≣ Menu key, Shift+F10) always get the\n * native menu; only mouse right-click and touch/pen long-press open the\n * composer — a long-press only where the browser fires `contextmenu` for\n * it, which iOS and iPadOS never do (see the note below).\n *\n * **Modifier-key escape hatch:** holding Shift, Ctrl, Alt, or Meta while\n * right-clicking always falls through to the native context menu, giving\n * users (and devtools) an escape hatch regardless of this setting.\n *\n * Right-clicks on SitePing's own UI (FAB, panel, markers, popup) are\n * ignored — the native menu is shown as expected.\n *\n * Note: on Android, `contextmenu` fires on long-press — touch users open the\n * composer by long-pressing, on phones too since the widget renders at every\n * width by default (see `minViewportWidth`). On iPhone and iPad a long-press\n * never fires `contextmenu` (WebKit bug 213953): users there open annotate\n * mode from the floating button and tap the element.\n */\n enableRightClickComment?: boolean | undefined;\n /**\n * Capture the last few `console.*` calls and failed network requests\n * (HTTP >= 400 or network error) at the moment a feedback is submitted.\n *\n * Lets reviewers replay the technical context that led to the report —\n * stack traces, 500 responses, dead third-party scripts. Great for the\n * \"the page just doesn't work\" feedback that contains zero detail.\n *\n * - `true` — capture with defaults (50 console / 20 network entries).\n * - `false` (default) — no capture, no monkey-patching.\n * - object — per-channel toggles + smaller buffer sizes (values above the\n * 50 / 20 server caps are clamped so submissions never fail validation).\n *\n * **Privacy considerations:** console messages may contain anything the\n * host page logs, including user data. Failed network requests record the\n * URL without its credentials, query string or hash, and never the\n * response body.\n * Inform end users before enabling in environments where they might log\n * sensitive values.\n */\n captureDiagnostics?: boolean | DiagnosticsCaptureOptions | undefined;\n /** Called when the widget is skipped (production mode, viewport under `minViewportWidth`, SSR — no DOM) */\n onSkip?: ((reason: SitepingSkipReason) => void) | undefined;\n /**\n * Auto-focus a specific annotation when its ID appears in the URL query\n * string. Lets hosts deeplink directly into a feedback from external\n * systems (Zammad tickets, Slack notifications, dashboard rows).\n *\n * When enabled, the widget reads the configured query parameter from\n * `window.location.search` right after the initial markers load. If the\n * value matches a visible feedback ID, the widget scrolls the annotation\n * into view, pins its highlight, and pulses the marker — the same visual\n * affordance a marker click produces — then opens the panel on that\n * feedback's detail view, where its discussion thread is.\n *\n * - `false` / `undefined` (default): no URL parsing. Existing behavior\n * unchanged, no host URL inspection.\n * - `true`: enabled with default query parameter name `siteping`.\n * - object: enabled with a custom parameter name. Use this to avoid\n * clashes with host-app query keys.\n *\n * Only the initial load triggers focus. Subsequent URL changes (SPA\n * navigation, `history.pushState`, hash updates) are ignored —\n * deliberate, to avoid surprising re-scrolls during normal browsing.\n * Hosts that need re-focus on route change can call\n * `instance.focusFeedback(id)` explicitly.\n */\n deepLink?: boolean | SitepingDeepLinkOptions | undefined;\n /**\n * Automatically re-fetch feedbacks when the page changes during client-side\n * (SPA) navigation. Enabled by default.\n *\n * The widget is normally mounted once (singleton) inside a persistent layout\n * — e.g. a Next.js App Router `layout.tsx`, which does NOT remount on\n * client-side navigation. Without this, init runs a single time and both the\n * panel list and the page markers stay frozen on the page where the widget\n * first mounted. With it on, the widget patches the History API\n * (`pushState`/`replaceState`, which SPA routers call instead of triggering\n * `popstate`) and listens for `popstate`/`hashchange`, then re-fetches when\n * the scope key (`getPageScope().url` + template) actually changes.\n *\n * This re-fetches data only — it deliberately does NOT re-focus or re-scroll\n * to an annotation (deep-link focus stays initial-load only; see `deepLink`),\n * so normal browsing is never interrupted by a surprise scroll.\n *\n * - `true` (default) — watch navigation and re-fetch on route change.\n * - `false` — never touch the History API; hosts drive updates manually via\n * `instance.refresh()`.\n */\n watchNavigation?: boolean | undefined;\n /**\n * Pre-fill author identity from the host application — typically the\n * currently signed-in user. When set, the widget uses these values\n * directly and never shows the identity modal, even on first feedback.\n * It names the reviewer who sends feedback — the dashboard's `author` is\n * the team member who replies, a different person.\n *\n * Use case: SSO-integrated apps where the end user is already\n * authenticated by the host. Avoids the awkward \"enter your name and\n * email\" prompt for users the host already knows.\n *\n * When unset (default), the widget falls back to localStorage and shows\n * the modal on first feedback as before — existing behavior unchanged.\n *\n * Note: `config.identity` is **not** persisted to localStorage. It is\n * read at widget init time, not on every render. Hosts that need live\n * identity updates after sign-in/sign-out should currently remount the\n * widget (e.g. via a React `key` on the wrapping component). See\n * https://github.com/NeosiaNexus/SitePing/issues/85 for tracking a\n * future enhancement that propagates identity updates without a remount.\n */\n identity?: SitepingIdentity | undefined;\n /**\n * Host-defined actions rendered in the feedback detail view, below the\n * built-in Resolve and Delete buttons. Read once when the panel loads:\n * entries without a non-empty `id` and `label`, without exactly one of\n * `onAction` / `href`, with an unsafe static `href`, or reusing an earlier\n * `id` are skipped with a console warning. See {@link SitepingPanelAction}.\n */\n panelActions?: readonly SitepingPanelAction[] | undefined;\n /**\n * Reviewer mode: hide the actions that triage feedback — resolve, reopen,\n * delete, the bulk actions, \"Delete all\" — and keep creating, browsing\n * and replying. Defaults to `false`. The server's `permissions` hide\n * what it would refuse on top of it. It only hides: the server decides\n * what it accepts. Read once when the panel loads.\n */\n readOnly?: boolean | undefined;\n\n // Events\n /** Called when the feedback panel is opened. */\n onOpen?: (() => void) | undefined;\n /** Called when the feedback panel is closed. */\n onClose?: (() => void) | undefined;\n /** Called after a feedback is successfully submitted. */\n onFeedbackSent?: ((feedback: FeedbackResponse) => void) | undefined;\n /** Called after a reply is posted from the panel's discussion thread. */\n onCommentAdded?: ((comment: CommentResponse) => void) | undefined;\n /**\n * Called when a feedback API call fails.\n *\n * The widget always emits a `SitepingError` (or a subclass:\n * `SitepingNetworkError`, `SitepingValidationError`, `SitepingAuthError`)\n * for a failed call — match it with `isSitepingError(error)` (not\n * `instanceof`: the classes aren't exported), then read `error.code`\n * (`\"NETWORK\" | \"VALIDATION\" | \"AUTH\" | \"SERVER\" | \"TIMEOUT\"`),\n * `error.retryable` and an `AUTH` error's `status` (`401` for missing or\n * dead credentials, `403` for a refusal). The type is widened to `Error`\n * because a store in `store` mode may throw its own errors, which reach\n * here unchanged.\n *\n * Also receives whatever a `panelActions` callback throws or rejects with\n * (non-`Error` values are wrapped). Those host failures are not API\n * failures, so they are not emitted on the public `feedback:error` event.\n * The widget has no UI for them, so they are also always logged with\n * `console.error`, whether or not `onError` is set.\n */\n onError?: ((error: Error) => void) | undefined;\n /** Called when the user starts drawing an annotation. */\n onAnnotationStart?: (() => void) | undefined;\n /** Called when the user finishes drawing an annotation. */\n onAnnotationEnd?: (() => void) | undefined;\n}\n\n/**\n * HTTP mode — the widget talks to a server endpoint backed by a store\n * adapter (e.g. `@siteping/adapter-prisma` request handlers).\n */\nexport interface SitepingHttpConfig extends SitepingBaseConfig {\n /** HTTP endpoint that receives feedbacks (e.g. '/api/siteping'). */\n endpoint: string;\n /**\n * Convenience auth for HTTP mode — sent as `Authorization: Bearer <apiKey>`\n * on every request to `endpoint`.\n *\n * **WARNING: the widget runs in every visitor's browser, so a static key\n * configured here is public** — anyone can read it from your page source\n * and replay it against your API. Only use `apiKey` for internal tools\n * already behind your own login. On public sites, prefer `headers` with a\n * per-request factory returning a short-lived session token.\n */\n apiKey?: string | undefined;\n /**\n * Extra headers for every HTTP-mode request — a static map, or a factory\n * (sync or async) called once per request (e.g. to fetch a fresh session\n * token). Merged over the widget's generated headers, case-insensitively,\n * so an explicit `Authorization` entry overrides `apiKey`. A factory that\n * throws, rejects, or does not settle within 10 s fails the request like a\n * network error.\n */\n headers?: SitepingHeadersOption | undefined;\n /** Not available in HTTP mode — use either `endpoint` or `store`, never both. */\n store?: never;\n}\n\n/**\n * Store mode — the widget talks to a `SitepingStore` directly in the\n * browser, no server needed (demos, prototypes, localStorage persistence).\n */\nexport interface SitepingStoreConfig extends SitepingBaseConfig {\n /**\n * Direct store for client-side mode. Bypasses HTTP entirely. A send stops\n * waiting on `createFeedback` after 30 s (the call itself cannot be\n * cancelled), so a network-backed store should bound its own calls.\n */\n store: SitepingStore;\n /** Not available in store mode — use either `endpoint` or `store`, never both. */\n endpoint?: never;\n /** HTTP-mode only — meaningless without an `endpoint`. */\n apiKey?: never;\n /** HTTP-mode only — meaningless without an `endpoint`. */\n headers?: never;\n}\n\n/**\n * Configuration options for the Siteping widget.\n *\n * A discriminated union over the two transport modes: pass `endpoint`\n * (HTTP mode, optionally with `apiKey`/`headers`) **or** `store` (direct\n * client-side mode) — never both, never neither. Invalid combinations are\n * compile errors instead of runtime warnings.\n */\nexport type SitepingConfig = SitepingHttpConfig | SitepingStoreConfig;\n\n/** Instance returned by initSiteping() with lifecycle methods. */\nexport interface SitepingInstance {\n /**\n * Release this handle and detach the listeners added through its `on()`.\n * Every `initSiteping()` call returns its own handle over the one widget,\n * which leaves the DOM, all its listeners cleaned up, once the last handle\n * is destroyed. A repeat call is a no-op, and the handle's other methods do\n * nothing afterwards.\n */\n destroy: () => void;\n /** Open the panel programmatically */\n open: () => void;\n /** Close the panel */\n close: () => void;\n /**\n * Reload feedbacks from the server. Resolves once the pins (and the open\n * panel) show the reloaded list; never rejects — like the dashboard's\n * `refresh()`, a failed reload leaves the current view in place.\n */\n refresh: () => Promise<void>;\n /**\n * Scroll the matching annotation into view, pin its highlight, and\n * pulse its marker. Returns `true` when a visible feedback matched the\n * given ID, `false` otherwise (unknown ID, feedback on another URL when\n * `scopeAnnotationsByUrl` filtered it out, or markers not yet loaded).\n *\n * Counterpart to the `deepLink` config option for hosts that prefer to\n * drive focus from JS (e.g., a notification click handler) instead of a\n * URL query parameter.\n */\n focusFeedback: (feedbackId: string) => boolean;\n /** Subscribe to a public widget event */\n on: <K extends keyof SitepingPublicEvents>(event: K, listener: SitepingPublicEventListener<K>) => SitepingUnsubscribe;\n /** Unsubscribe from a public widget event */\n off: <K extends keyof SitepingPublicEvents>(event: K, listener: SitepingPublicEventListener<K>) => void;\n}\n\n/** Listener signature for a single `SitepingPublicEvents` key. */\nexport type SitepingPublicEventListener<K extends keyof SitepingPublicEvents> = (\n ...args: SitepingPublicEvents[K]\n) => void;\n\n/** Disposer returned by `SitepingInstance.on` — call once to detach the listener. */\nexport type SitepingUnsubscribe = () => void;\n\n/** Events exposed to consumers via SitepingInstance.on / .off */\nexport interface SitepingPublicEvents {\n \"feedback:sent\": [FeedbackResponse];\n \"feedback:deleted\": [FeedbackResponse[\"id\"]];\n /** A reply was posted from the panel's discussion thread. */\n \"comment:added\": [CommentResponse];\n /**\n * A feedback API call failed. Same payload contract as\n * `SitepingConfig.onError` — a `SitepingError` subclass in HTTP mode,\n * possibly a raw `Error` in store mode.\n */\n \"feedback:error\": [Error];\n \"panel:open\": [];\n \"panel:close\": [];\n /** The user started drawing an annotation. */\n \"annotation:start\": [];\n /** The user finished drawing an annotation. */\n \"annotation:end\": [];\n}\n\n// ---------------------------------------------------------------------------\n// Feedback\n// ---------------------------------------------------------------------------\n\n/** Single source of truth for feedback types — used by both TS types and Zod schemas. */\nexport const FEEDBACK_TYPES = [\"question\", \"change\", \"bug\", \"other\"] as const;\nexport type FeedbackType = (typeof FEEDBACK_TYPES)[number];\n\n/** Single source of truth for feedback statuses. */\nexport const FEEDBACK_STATUSES = [\"open\", \"in_progress\", \"resolved\", \"wont_fix\"] as const;\nexport type FeedbackStatus = (typeof FEEDBACK_STATUSES)[number];\n\n/**\n * Terminal statuses — the feedback needs no further action. `resolvedAt` is\n * the closure timestamp: set when a feedback enters a closed status, null\n * while it is open or in progress. The derivation happens at the edge (HTTP\n * handler, dashboard) — store adapters persist whatever they are given.\n */\nexport const CLOSED_FEEDBACK_STATUSES = [\"resolved\", \"wont_fix\"] as const;\n/** A terminal status — `resolved` or `wont_fix`. */\nexport type ClosedFeedbackStatus = (typeof CLOSED_FEEDBACK_STATUSES)[number];\n\n/** Non-terminal statuses — the feedback still needs attention. */\nexport const OPEN_FEEDBACK_STATUSES = [\"open\", \"in_progress\"] as const;\n/** A non-terminal status — `open` or `in_progress`. */\nexport type OpenFeedbackStatus = (typeof OPEN_FEEDBACK_STATUSES)[number];\n\n// Adding a fifth status without assigning it to exactly one bucket is a\n// compile error here.\nconst _statusBucketsCoverAll: AssertEqual<OpenFeedbackStatus | ClosedFeedbackStatus, FeedbackStatus> = true;\nvoid _statusBucketsCoverAll;\n\n/** Whether a status is terminal (`resolved` or `wont_fix`). Narrows the status type. */\nexport function isClosedStatus(status: FeedbackStatus): status is ClosedFeedbackStatus {\n return (CLOSED_FEEDBACK_STATUSES as readonly FeedbackStatus[]).includes(status);\n}\n\n/**\n * Page scope returned by `SitepingConfig.getPageScope()`.\n *\n * - `url`: concrete page identifier — usually `window.location.pathname`,\n * used as the strict scope for marker rendering.\n * - `urlPattern`: optional parameterized template (e.g. `/orders/:orderId`)\n * used by the panel's \"this type of page\" filter to group feedbacks across\n * instances of the same page kind.\n */\nexport interface PageScope {\n url: string;\n urlPattern: string | null;\n}\n\n// ---------------------------------------------------------------------------\n// Abstract Store — adapter pattern\n// ---------------------------------------------------------------------------\n\n/** Input for creating a feedback record in the store. */\nexport interface FeedbackCreateInput {\n projectName: string;\n type: FeedbackType;\n message: string;\n status: FeedbackStatus;\n url: string;\n /**\n * Optional parameterized URL template (e.g. `/orders/:orderId`) for the page\n * where the feedback was created. Allows the panel to filter feedbacks by\n * \"this type of page\" across different instances. Null when the host did not\n * provide a `getPageScope` callback or the route has no template.\n */\n urlPattern?: string | null | undefined;\n viewport: string;\n userAgent: string;\n authorName: string;\n authorEmail: string;\n clientId: string;\n annotations: AnnotationCreateInput[];\n /**\n * Base64 JPEG `data:` URL captured by the widget at submit time.\n *\n * Adapters with a configured `ScreenshotStorage` are expected to upload\n * this and persist the returned URL on `FeedbackRecord.screenshotUrl`.\n * Adapters without storage may persist the data URL inline (memory /\n * localStorage / dev) — the widget then renders it directly.\n */\n screenshotDataUrl?: string | null | undefined;\n /**\n * Where the client's annotation rect sits within the screenshot image,\n * as fractions [0, 1] of the image dimensions. Present when the widget\n * captured context around the drawn rect; null for legacy captures that\n * were cropped exactly to the rect (dashboards then render the image\n * without an overlay).\n */\n screenshotRegion?: ScreenshotRegion | null | undefined;\n /**\n * Optional console + failed-network snapshot captured by the widget when\n * `SitepingConfig.captureDiagnostics` is enabled. Stored as JSON on\n * `FeedbackRecord.diagnostics` so reviewers can replay the context.\n */\n diagnostics?: DiagnosticsSnapshot | null | undefined;\n}\n\n/** Input for a single annotation when creating a feedback. */\nexport interface AnnotationCreateInput {\n cssSelector: string;\n xpath: string;\n textSnippet: string;\n elementTag: string;\n elementId?: string | undefined;\n textPrefix: string;\n textSuffix: string;\n fingerprint: string;\n neighborText: string;\n /**\n * Semantic anchor identifier from the closest ancestor's `data-feedback-anchor`\n * attribute. When set, this is the most stable re-anchoring signal because\n * hosts deliberately place these on layout/section roots that survive DOM\n * refactors and viewport changes. Null when no semantic ancestor exists.\n */\n anchorKey?: string | null | undefined;\n xPct: number;\n yPct: number;\n wPct: number;\n hPct: number;\n scrollX: number;\n scrollY: number;\n viewportW: number;\n viewportH: number;\n devicePixelRatio: number;\n}\n\n/** Query parameters for fetching feedbacks. */\nexport interface FeedbackQuery {\n projectName: string;\n type?: FeedbackType | undefined;\n /** Exact single-status filter. For \"any of a set\" (bucket) semantics, use `statuses`. */\n status?: FeedbackStatus | undefined;\n /**\n * Filter to feedbacks whose status is any of the listed values — bucket\n * semantics used by the panel's binary tabs (e.g. \"Open\" passes\n * `[\"open\", \"in_progress\"]`). When both `status` and `statuses` are set,\n * `statuses` wins. An empty array is treated as absent (no status filter).\n */\n statuses?: readonly FeedbackStatus[] | undefined;\n search?: string | undefined;\n page?: number | undefined;\n /**\n * Page size. Defaults to `DEFAULT_PAGE_LIMIT` (50) and is capped at\n * `MAX_PAGE_LIMIT` (100) — `clampPagination` implements both, and the\n * conformance suite checks them.\n */\n limit?: number | undefined;\n /**\n * Filter to feedbacks created on this exact URL (path). Used by the panel's\n * \"this page\" filter and by the markers loader to keep page scopes isolated.\n */\n url?: string | undefined;\n /**\n * Filter to feedbacks created on this URL pattern (e.g. `/orders/:orderId`).\n * Used by the panel's \"this type of page\" filter to group feedbacks across\n * different concrete instances of the same template.\n */\n urlPattern?: string | undefined;\n}\n\n/**\n * Update payload for patching a feedback.\n *\n * A discriminated union encoding the closure invariant: a feedback entering\n * a closed status carries its closure timestamp, an open one carries `null`.\n * `{ status: \"resolved\", resolvedAt: null }` is a compile error instead of a\n * silent data bug. Build it from a plain `FeedbackStatus` with\n * {@link toFeedbackUpdate}.\n */\nexport type FeedbackUpdateInput =\n | { status: OpenFeedbackStatus; resolvedAt: null }\n | { status: ClosedFeedbackStatus; resolvedAt: Date };\n\n/**\n * Derive the {@link FeedbackUpdateInput} for a status change — the closure\n * timestamp is stamped for closed statuses and cleared otherwise. This is\n * the edge derivation described on {@link CLOSED_FEEDBACK_STATUSES}; store\n * adapters persist the result verbatim.\n */\nexport function toFeedbackUpdate(status: FeedbackStatus, closedAt: Date = new Date()): FeedbackUpdateInput {\n return isClosedStatus(status) ? { status, resolvedAt: closedAt } : { status, resolvedAt: null };\n}\n\n/** A persisted feedback record returned by the store. */\nexport interface FeedbackRecord {\n id: string;\n type: FeedbackType;\n message: string;\n status: FeedbackStatus;\n projectName: string;\n url: string;\n /**\n * Parameterized URL template the feedback was created on.\n * Null for legacy records or hosts without `getPageScope`.\n */\n urlPattern: string | null;\n authorName: string;\n authorEmail: string;\n viewport: string;\n userAgent: string;\n clientId: string;\n resolvedAt: Date | null;\n createdAt: Date;\n updatedAt: Date;\n annotations: AnnotationRecord[];\n /**\n * Discussion thread, oldest first. A store that implements\n * `SitepingStore.addComment` returns it on every record; a store without\n * comments may leave it out, which reads as an empty thread (HTTP handlers\n * send `[]`).\n */\n comments?: CommentRecord[] | undefined;\n /**\n * URL the widget renders as `<img src>`. Either an `https://...` from a\n * configured `ScreenshotStorage`, or a `data:image/jpeg;base64,...` URL\n * inline-persisted by adapters without storage. Null when no screenshot\n * was captured (legacy records, capture failed, or host disabled it).\n */\n screenshotUrl: string | null;\n /**\n * Annotation rect position within the screenshot image, as fractions of\n * its dimensions. Null for legacy captures cropped exactly to the rect.\n */\n screenshotRegion: ScreenshotRegion | null;\n /**\n * Console + failed-network snapshot captured at submit time. Null when\n * diagnostics weren't enabled on the widget side.\n */\n diagnostics: DiagnosticsSnapshot | null;\n}\n\n/** A persisted annotation record returned by the store. */\nexport interface AnnotationRecord {\n id: string;\n feedbackId: string;\n cssSelector: string;\n xpath: string;\n textSnippet: string;\n elementTag: string;\n elementId: string | null;\n textPrefix: string;\n textSuffix: string;\n fingerprint: string;\n neighborText: string;\n /**\n * Semantic anchor identifier from `data-feedback-anchor`. Null for legacy\n * annotations or those drawn outside any anchored region.\n */\n anchorKey: string | null;\n xPct: number;\n yPct: number;\n wPct: number;\n hPct: number;\n scrollX: number;\n scrollY: number;\n viewportW: number;\n viewportH: number;\n devicePixelRatio: number;\n createdAt: Date;\n}\n\n// ---------------------------------------------------------------------------\n// Comments — the discussion thread of a feedback\n// ---------------------------------------------------------------------------\n\n/**\n * Who wrote a comment: `client` — the reviewer on the site (the widget) —\n * or `team` — the project side (the dashboard). HTTP handlers stamp\n * `client` unless the server's access policy vouches for the caller, so a\n * visitor cannot pose as the team.\n */\nexport const COMMENT_AUTHOR_ROLES = [\"client\", \"team\"] as const;\nexport type CommentAuthorRole = (typeof COMMENT_AUTHOR_ROLES)[number];\n\n/** Longest comment `body` the HTTP API accepts — the cap of a feedback `message`. */\nexport const COMMENT_BODY_MAX_LENGTH = 5000;\n\n/**\n * Most `client` comments one thread holds. List responses embed whole\n * threads, which are not paginated, so this bounds what one feedback weighs\n * however much a public endpoint is spammed. `team` comments — a role only\n * the access policy grants — neither count nor meet it: a thread spammed\n * full still takes the team's answer. Stores enforce it with\n * `StoreLimitError`; a store without an atomic primitive may overshoot it by\n * the posts that race the last free slot.\n */\nexport const MAX_COMMENTS_PER_FEEDBACK = 100;\n\n/** A persisted comment returned by the store. */\nexport interface CommentRecord {\n id: string;\n /** The feedback whose thread holds this comment. */\n feedbackId: string;\n body: string;\n authorName: string;\n /**\n * Author email, or `\"\"` when the author has none on file (a dashboard\n * user). HTTP handlers redact it exactly like `FeedbackRecord.authorEmail`.\n */\n authorEmail: string;\n authorRole: CommentAuthorRole;\n /** Client-generated id — `addComment` is idempotent on it, so a retried post never duplicates a reply. */\n clientId: string;\n createdAt: Date;\n}\n\n/** Input of `SitepingStore.addComment`. */\nexport interface CommentCreateInput {\n body: string;\n authorName: string;\n authorEmail: string;\n authorRole: CommentAuthorRole;\n clientId: string;\n}\n\n/**\n * Body of a comment `POST`. `feedbackId` is what tells it apart from a\n * feedback submission on the same endpoint; `projectName` scopes the write\n * to that project's feedbacks.\n */\nexport interface CommentPayload extends CommentCreateInput {\n projectName: string;\n feedbackId: string;\n}\n\n/** Body of a comment `DELETE` — `commentId` is what tells it apart from a feedback delete. */\nexport interface CommentDeletePayload {\n projectName: string;\n feedbackId: string;\n commentId: string;\n}\n\n// ---------------------------------------------------------------------------\n// Store errors — throw these from adapter implementations\n// ---------------------------------------------------------------------------\n\n/**\n * Thrown when a record is not found during update or delete.\n *\n * Handlers translate this to HTTP 404. Adapters MUST throw this (not\n * ORM-specific errors) so the handler layer remains ORM-agnostic.\n */\nexport class StoreNotFoundError extends Error {\n readonly code = \"STORE_NOT_FOUND\" as const;\n constructor(message = \"Record not found\", options?: ErrorOptions) {\n super(message, options);\n this.name = \"StoreNotFoundError\";\n }\n}\n\n/**\n * Thrown when a unique constraint is violated (e.g. duplicate `clientId`).\n *\n * Handlers use this to return the existing record instead of failing.\n */\nexport class StoreDuplicateError extends Error {\n readonly code = \"STORE_DUPLICATE\" as const;\n constructor(message = \"Duplicate record\", options?: ErrorOptions) {\n super(message, options);\n this.name = \"StoreDuplicateError\";\n }\n}\n\n/**\n * Thrown when a store accepts a mutation but cannot persist it — e.g.\n * `localStorage` is full (QuotaExceededError). Adapters MUST throw this rather\n * than swallow the failure, so callers learn the write was lost instead of\n * seeing a phantom success.\n */\nexport class StorePersistenceError extends Error {\n readonly code = \"STORE_PERSISTENCE\" as const;\n constructor(message = \"Failed to persist store mutation\", options?: ErrorOptions) {\n super(message, options);\n this.name = \"StorePersistenceError\";\n }\n}\n\n/**\n * Thrown when a write would break a bound of the store contract — a\n * `client` comment on a thread already holding `MAX_COMMENTS_PER_FEEDBACK`\n * of them. Handlers translate this to HTTP 409.\n */\nexport class StoreLimitError extends Error {\n readonly code = \"STORE_LIMIT\" as const;\n constructor(message = \"Store limit reached\", options?: ErrorOptions) {\n super(message, options);\n this.name = \"StoreLimitError\";\n }\n}\n\n/**\n * Thrown when a value is longer than the store can hold, though the HTTP\n * validation accepts it — a MySQL schema that predates the columns `sync`\n * now sizes from the HTTP caps keeps Prisma's `VARCHAR(191)` for a plain\n * `String`. Handlers translate this to HTTP 422: the submission can never be\n * stored as it is, so a client must not retry it.\n */\nexport class StoreValueTooLongError extends Error {\n readonly code = \"STORE_VALUE_TOO_LONG\" as const;\n constructor(message = \"Value too long for the store\", options?: ErrorOptions) {\n super(message, options);\n this.name = \"StoreValueTooLongError\";\n }\n}\n\n/** Shape of any ORM error that carries a Prisma-style `code` field. */\ntype CodedError<C extends string = string> = { code: C };\n\nfunction hasErrorCode<C extends string>(error: unknown, code: C): error is CodedError<C> {\n return hasOwn(error, \"code\") && error.code === code;\n}\n\n/**\n * Type guard — works for `StoreNotFoundError` and ORM-specific equivalents\n * (e.g. Prisma P2025). Matches on the stable `code` too, for the same\n * cross-bundle reason as {@link isStorePersistence}.\n */\nexport function isStoreNotFound(error: unknown): error is StoreNotFoundError | CodedError<\"STORE_NOT_FOUND\" | \"P2025\"> {\n if (error instanceof StoreNotFoundError) return true;\n // Another bundle's copy of core, or Prisma's P2025 (backwards compat)\n return hasErrorCode(error, \"STORE_NOT_FOUND\") || hasErrorCode(error, \"P2025\");\n}\n\n/**\n * Type guard — works for `StoreDuplicateError` and ORM-specific equivalents\n * (e.g. Prisma P2002). Matches on the stable `code` too, for the same\n * cross-bundle reason as {@link isStorePersistence}.\n */\nexport function isStoreDuplicate(\n error: unknown,\n): error is StoreDuplicateError | CodedError<\"STORE_DUPLICATE\" | \"P2002\"> {\n if (error instanceof StoreDuplicateError) return true;\n // Another bundle's copy of core, or Prisma's P2002 (backwards compat)\n return hasErrorCode(error, \"STORE_DUPLICATE\") || hasErrorCode(error, \"P2002\");\n}\n\n/**\n * Type guard for `StorePersistenceError`. Matches on the stable `code` field\n * in addition to `instanceof`: every consumer package bundles its own copy of\n * core (tsup `noExternal`), so an instance thrown by one package fails an\n * `instanceof` check against another package's class identity.\n */\nexport function isStorePersistence(error: unknown): error is StorePersistenceError | CodedError<\"STORE_PERSISTENCE\"> {\n if (error instanceof StorePersistenceError) return true;\n return hasErrorCode(error, \"STORE_PERSISTENCE\");\n}\n\n/** Type guard for `StoreLimitError`, matching on `code` for the same cross-bundle reason as {@link isStorePersistence}. */\nexport function isStoreLimit(error: unknown): error is StoreLimitError | CodedError<\"STORE_LIMIT\"> {\n if (error instanceof StoreLimitError) return true;\n return hasErrorCode(error, \"STORE_LIMIT\");\n}\n\n/** Type guard for `StoreValueTooLongError`, matching on `code` for the same cross-bundle reason as {@link isStorePersistence}. */\nexport function isStoreValueTooLong(\n error: unknown,\n): error is StoreValueTooLongError | CodedError<\"STORE_VALUE_TOO_LONG\"> {\n if (error instanceof StoreValueTooLongError) return true;\n return hasErrorCode(error, \"STORE_VALUE_TOO_LONG\");\n}\n\n// ---------------------------------------------------------------------------\n// Store helpers — shared conversion logic for adapters\n// ---------------------------------------------------------------------------\n\n/** Flatten a widget `AnnotationPayload` (nested anchor + rect) into a flat `AnnotationCreateInput`. */\nexport function flattenAnnotation(ann: AnnotationPayload): AnnotationCreateInput {\n return {\n cssSelector: ann.anchor.cssSelector,\n xpath: ann.anchor.xpath,\n textSnippet: ann.anchor.textSnippet,\n elementTag: ann.anchor.elementTag,\n elementId: ann.anchor.elementId,\n textPrefix: ann.anchor.textPrefix,\n textSuffix: ann.anchor.textSuffix,\n fingerprint: ann.anchor.fingerprint,\n neighborText: ann.anchor.neighborText,\n anchorKey: ann.anchor.anchorKey ?? null,\n xPct: ann.rect.xPct,\n yPct: ann.rect.yPct,\n wPct: ann.rect.wPct,\n hPct: ann.rect.hPct,\n scrollX: ann.scrollX,\n scrollY: ann.scrollY,\n viewportW: ann.viewportW,\n viewportH: ann.viewportH,\n devicePixelRatio: ann.devicePixelRatio,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Abstract Store — adapter pattern\n// ---------------------------------------------------------------------------\n\n/**\n * Outcome of `SitepingStore.createFeedbackIfAbsent` — the record plus whether\n * this very call inserted it.\n */\nexport interface FeedbackCreateOutcome {\n feedback: FeedbackRecord;\n /**\n * `true` when this call inserted the record, `false` when a record with the\n * same `clientId` already existed and is returned instead (a replay, or a\n * concurrent request that won the race).\n */\n created: boolean;\n}\n\n/**\n * Outcome of `SitepingStore.addCommentIfAbsent` — the comment plus whether\n * this very call inserted it.\n */\nexport interface CommentCreateOutcome {\n comment: CommentRecord;\n /**\n * `true` when this call inserted the comment, `false` when a comment with\n * the same `clientId` already existed and is returned instead (a replay, or\n * a concurrent request that won the race).\n */\n created: boolean;\n}\n\n/** Paginated result returned by `SitepingStore.getFeedbacks`. */\nexport interface FeedbackPage {\n feedbacks: FeedbackRecord[];\n total: number;\n}\n\n/**\n * Abstract storage interface for Siteping.\n *\n * Any adapter (Prisma, Drizzle, raw SQL, localStorage, etc.) implements this\n * interface. The HTTP handler and widget `StoreClient` operate against\n * `SitepingStore`, decoupled from the storage backend.\n *\n * ## Error contract\n *\n * - **`updateFeedback` / `deleteFeedback`**: throw `StoreNotFoundError` when\n * the record does not exist.\n * - **`createFeedback`**: throw `StoreDuplicateError` on a duplicate\n * `clientId` — never return the existing record, so a caller always knows\n * whether this call inserted. The optional `createFeedbackIfAbsent`\n * reports the existing record instead, as `{ created: false }`.\n * - **All mutations**: when a write is accepted but cannot be persisted\n * (e.g. storage quota), throw `StorePersistenceError` instead of reporting\n * a phantom success. Detect it with `isStorePersistence`.\n * - Other methods should not throw on empty results — return empty arrays or `null`.\n */\nexport interface SitepingStore {\n /** Create a feedback with its annotations. Throws `StoreDuplicateError` when a feedback with the same `clientId` exists, `StorePersistenceError` when the write cannot be persisted. */\n createFeedback(data: FeedbackCreateInput): Promise<FeedbackRecord>;\n /** Paginated query with optional filters. Returns empty array (not error) when no results. */\n getFeedbacks(query: FeedbackQuery): Promise<FeedbackPage>;\n /** Lookup by client-generated UUID. Returns `null` (not error) when not found. */\n findByClientId(clientId: string): Promise<FeedbackRecord | null>;\n /** Update status/resolvedAt. Throws `StoreNotFoundError` if `id` does not exist, `StorePersistenceError` when the write cannot be persisted. */\n updateFeedback(id: string, data: FeedbackUpdateInput): Promise<FeedbackRecord>;\n /** Delete a single record. Throws `StoreNotFoundError` if `id` does not exist, `StorePersistenceError` when the write cannot be persisted. */\n deleteFeedback(id: string): Promise<void>;\n /** Bulk delete all feedbacks for a project. No-op (not error) if none exist. Throws `StorePersistenceError` when the write cannot be persisted. */\n deleteAllFeedbacks(projectName: string): Promise<void>;\n /**\n * Return `true` when the record with `id` belongs to `projectName`, `false`\n * otherwise (including when it does not exist). HTTP handlers use it to\n * reject cross-project PATCH/DELETE requests, which address records by id.\n */\n verifyProjectOwnership(id: string, projectName: string): Promise<boolean>;\n /**\n * Optional — `createFeedback` that reports whether this call inserted the\n * record (`created: true`) or found an existing one with the same\n * `clientId` (`created: false`). The dedup check and the insert must be\n * atomic, like `createFeedback`'s: of N concurrent calls with the same\n * `clientId`, exactly one may report `created: true`, and all must return\n * that same record. `createCollectionStore` guarantees this within one\n * store instance by serializing its mutations; stores shared across\n * processes need an atomic backend primitive (unique constraint,\n * transaction, compare-and-set).\n *\n * HTTP handlers prefer it over `createFeedback`: a replayed or racing\n * submission resolves to the stored record in one atomic call, without a\n * `StoreDuplicateError` to catch and a lookup to follow it. Without it,\n * they fall back to `createFeedback` and recover its duplicate error.\n */\n createFeedbackIfAbsent?(data: FeedbackCreateInput): Promise<FeedbackCreateOutcome>;\n /**\n * Optional — append a comment to the thread of feedback `feedbackId` and\n * return it. Idempotent on `data.clientId`: when a comment with that\n * `clientId` exists, on any thread, return it unchanged (HTTP handlers\n * refuse a replay aimed at another thread).\n *\n * Throws `StoreNotFoundError` when the feedback does not exist,\n * `StoreLimitError` when a `client` comment meets a thread already holding\n * `MAX_COMMENTS_PER_FEEDBACK` of them (a `team` comment is never refused\n * for it), `StorePersistenceError` when the\n * write cannot be persisted. A comment is not a change of the feedback\n * itself: its `updatedAt` stays as it was.\n *\n * A store that implements it returns every feedback's thread on\n * `FeedbackRecord.comments`, oldest first, and deletes a thread with its\n * feedback. A store without it (and `deleteComment`) has no threads:\n * HTTP handlers serve empty threads and answer comment writes with 501.\n */\n addComment?(feedbackId: string, data: CommentCreateInput): Promise<CommentRecord>;\n /**\n * Optional, alongside `addComment` — `addComment` that reports whether this\n * call inserted the comment (`created: true`) or found one with the same\n * `clientId` (`created: false`). Same errors as `addComment`, and the dedup\n * check and the insert must be atomic like `createFeedbackIfAbsent`'s: of N\n * concurrent calls with the same `clientId`, exactly one may report\n * `created: true`, and all must return that same comment.\n *\n * HTTP handlers need it to fire the `comment.created` webhook and the\n * `onCommentCreated` hook exactly once per reply — a retried post returns\n * the stored comment, which `addComment` alone cannot tell from a new one.\n */\n addCommentIfAbsent?(feedbackId: string, data: CommentCreateInput): Promise<CommentCreateOutcome>;\n /**\n * Optional — delete comment `commentId` from the thread of `feedbackId`.\n * Scoped to that thread, which the caller was authorized for: throws\n * `StoreNotFoundError` when the feedback does not exist or the comment is\n * not on its thread, `StorePersistenceError` when the write cannot be\n * persisted.\n */\n deleteComment?(feedbackId: string, commentId: string): Promise<void>;\n}\n\n/** Payload sent from the widget to the server when submitting feedback. */\nexport interface FeedbackPayload {\n projectName: string;\n type: FeedbackType;\n message: string;\n url: string;\n /**\n * Parameterized URL template (e.g. `/orders/:orderId`) supplied by\n * `SitepingConfig.getPageScope()`. Null when the host did not provide one.\n */\n urlPattern?: string | null | undefined;\n viewport: string;\n userAgent: string;\n authorName: string;\n authorEmail: string;\n annotations: AnnotationPayload[];\n /** Client-generated UUID for deduplication */\n clientId: string;\n /**\n * Base64 JPEG `data:` URL of the annotated area. Captured by the widget\n * when `enableScreenshot: true` is set in `SitepingConfig`. Null when\n * disabled or when capture failed silently.\n */\n screenshotDataUrl?: string | null | undefined;\n /**\n * Annotation rect position within the screenshot image — see\n * `ScreenshotRegion`. Null/absent when no screenshot was captured or the\n * capture predates contextual framing.\n */\n screenshotRegion?: ScreenshotRegion | null | undefined;\n /**\n * Snapshot of the last few console messages and failed network requests\n * captured at submit time when `captureDiagnostics` is enabled.\n */\n diagnostics?: DiagnosticsSnapshot | null | undefined;\n}\n\n/** Single source of truth for console diagnostic severity levels. */\nexport const CONSOLE_DIAGNOSTIC_LEVELS = [\"log\", \"info\", \"warn\", \"error\"] as const;\n/** Severity levels persisted in `ConsoleDiagnosticEntry`. */\nexport type ConsoleDiagnosticLevel = (typeof CONSOLE_DIAGNOSTIC_LEVELS)[number];\n\n/** A single console entry captured by `ConsoleBuffer`. */\nexport interface ConsoleDiagnosticEntry {\n level: ConsoleDiagnosticLevel;\n /** ISO 8601 timestamp captured at log time. */\n timestamp: string;\n /** Best-effort string representation of the original console args. */\n message: string;\n}\n\n/** A single failed network request captured by `NetworkBuffer`. */\nexport interface NetworkDiagnosticEntry {\n url: string;\n method: string;\n /** HTTP status; 0 when the request never reached the server. */\n status: number;\n /** End-to-end duration in ms. */\n durationMs: number;\n /** ISO 8601 timestamp at the moment the request was initiated. */\n timestamp: string;\n}\n\n/**\n * Diagnostics captured by the widget when `captureDiagnostics` is enabled.\n *\n * Both arrays are bounded (default: 50 console / 20 network). Adapters that\n * support diagnostics should persist this as a JSON blob alongside the\n * feedback so reviewers can replay the context that led to the report.\n */\nexport interface DiagnosticsSnapshot {\n console: ConsoleDiagnosticEntry[];\n network: NetworkDiagnosticEntry[];\n}\n\n// ---------------------------------------------------------------------------\n// Annotation — multi-selector anchoring (Hypothesis / W3C Web Annotation)\n// ---------------------------------------------------------------------------\n\n/**\n * Length caps for `AnchorData.elementTag` / `elementId`, shared by the widget\n * (which bounds what it captures) and the HTTP adapter (which rejects a longer\n * tag and drops a longer id) so the two can't drift. 191 fits Prisma's default `String` column\n * on MySQL (`VARCHAR(191)`); real tag names and ids are far shorter.\n */\nexport const ANCHOR_ELEMENT_TAG_MAX = 191;\nexport const ANCHOR_ELEMENT_ID_MAX = 191;\n\n/** DOM anchoring data for re-attaching annotations to page elements. */\nexport interface AnchorData {\n /**\n * CSS selector generated by @medv/finder — primary anchor. Inside open\n * shadow roots: one selector per tree, outermost host first, joined by\n * `\" >>> \"` (e.g. `\"#pricing >>> .plan-title\"`).\n */\n cssSelector: string;\n /**\n * XPath — fallback 1. Inside a shadow root it is relative to that root\n * (`./…`) and informational only: XPath cannot enter shadow trees.\n */\n xpath: string;\n /** First ~120 chars of element innerText — empty string if none */\n textSnippet: string;\n /** Tag name for validation (e.g. \"DIV\", \"SECTION\") */\n elementTag: string;\n /** Element id attribute if available — most stable */\n elementId?: string | undefined;\n /** ~32 chars of text before this element in document flow (disambiguation) */\n textPrefix: string;\n /** ~32 chars of text after this element in document flow (disambiguation) */\n textSuffix: string;\n /** Structural fingerprint: \"childCount:siblingIdx:attrHash\" */\n fingerprint: string;\n /** Text content of adjacent sibling elements (context) */\n neighborText: string;\n /**\n * Semantic anchor identifier from the closest ancestor's `data-feedback-anchor`\n * attribute. When set, this is the highest-priority re-anchoring signal —\n * hosts deliberately place these on layout/section roots that survive\n * viewport changes and DOM refactors.\n */\n anchorKey?: string | null | undefined;\n}\n\n/**\n * Where the client's annotation rect sits within the captured screenshot,\n * as fractions [0, 1] of the image dimensions. The widget captures context\n * around the drawn rect and records the rect's position here so dashboards\n * can re-render the annotation on top of the image. Survives downscaling\n * (fractions are resolution-independent).\n */\nexport interface ScreenshotRegion {\n /** X offset of the rect as fraction of image width — [0, 1] */\n xPct: number;\n /** Y offset of the rect as fraction of image height — [0, 1] */\n yPct: number;\n /** Rect width as fraction of image width — [0, 1] */\n wPct: number;\n /** Rect height as fraction of image height — [0, 1] */\n hPct: number;\n}\n\n/** Drawn rectangle coordinates as percentages relative to the anchor element. */\nexport interface RectData {\n /** X offset as fraction of anchor element width — must be in range [0, 1] */\n xPct: number;\n /** Y offset as fraction of anchor element height — must be in range [0, 1] */\n yPct: number;\n /** Width as fraction of anchor element width — must be in range [0, 1] */\n wPct: number;\n /** Height as fraction of anchor element height — must be in range [0, 1] */\n hPct: number;\n}\n\n/** Annotation data sent as part of a feedback submission. */\nexport interface AnnotationPayload {\n anchor: AnchorData;\n rect: RectData;\n scrollX: number;\n scrollY: number;\n viewportW: number;\n viewportH: number;\n devicePixelRatio: number;\n}\n\n// ---------------------------------------------------------------------------\n// API responses\n// ---------------------------------------------------------------------------\n\n/**\n * Feedback record as returned by the API — derived from\n * {@link FeedbackRecord}: dates are serialized to ISO strings and `clientId`\n * is omitted (server-side dedup concern, never exposed on the wire), on the\n * record and on each comment of its thread. Adding a field to\n * `FeedbackRecord` updates this type automatically.\n *\n * Note: `authorEmail` may be an empty string — HTTP adapters redact it for\n * unauthenticated requests; the full value requires a Bearer-authenticated\n * request.\n */\nexport type FeedbackResponse = Prettify<\n Serialized<Omit<FeedbackRecord, \"clientId\" | \"comments\">> & {\n /** The thread, oldest first — always sent by `@siteping/server`, absent from servers that predate comments. */\n comments?: CommentResponse[] | undefined;\n /**\n * What the requester may do with this feedback — always sent by\n * `@siteping/server`. Absent from servers that predate it, and in store\n * mode: nothing is refused then.\n */\n permissions?: FeedbackPermissions | undefined;\n }\n>;\n\n/**\n * What a requester may do with one feedback, as the server's access policy\n * decides — so clients hide the actions it would refuse. The server still\n * enforces every one of them.\n */\nexport interface FeedbackPermissions {\n /** Change its status: resolve, reopen, … */\n canChangeStatus: boolean;\n /** Delete it. */\n canDelete: boolean;\n /** Reply in its thread. */\n canComment: boolean;\n /** Delete replies from its thread. */\n canDeleteComment: boolean;\n}\n\n/**\n * What a requester may do with one comment of a thread, as the server's\n * access policy decides — finer than the feedback's `canDeleteComment`, which\n * covers the whole thread: a policy that lets authors delete their own\n * replies only grants it comment by comment.\n */\nexport interface CommentPermissions {\n /** Delete it. */\n canDelete: boolean;\n}\n\n/** What a requester may do with a whole project, sent with each list. */\nexport interface FeedbackListPermissions {\n /** Delete every feedback of the project at once. */\n canDeleteAll: boolean;\n}\n\n/**\n * Annotation record as returned by the API — {@link AnnotationRecord} with\n * `createdAt` serialized to an ISO string.\n */\nexport type AnnotationResponse = Prettify<Serialized<AnnotationRecord>>;\n\n/**\n * Comment as returned by the API — {@link CommentRecord} with `createdAt`\n * serialized and `clientId` omitted, like on `FeedbackResponse`. Its\n * `authorEmail` is redacted on the same terms as the feedback's.\n */\nexport type CommentResponse = Prettify<\n Serialized<Omit<CommentRecord, \"clientId\">> & {\n /**\n * What the requester may do with this comment — always sent by\n * `@siteping/server`. Absent from servers that predate it, and in store\n * mode: the feedback's `canDeleteComment` decides then.\n */\n permissions?: CommentPermissions | undefined;\n }\n>;\n\n/**\n * Version of the HTTP protocol `@siteping/server` speaks, sent as `protocol`\n * on every list response. It rises with each addition a client may rely on —\n * a route, a field, a status — so a client checks `protocol >= N` before\n * using what protocol N added, instead of probing for fields. A server that\n * predates it sends none: read that as 0. The OpenAPI document shipped as\n * `@siteping/server/openapi.json` describes this version.\n */\nexport const SITEPING_PROTOCOL_VERSION = 1;\n\n/** What the store behind an endpoint supports — advertised on every list response. */\nexport interface SitepingCapabilities {\n /** Whether comments can be posted: the store implements `addComment`. */\n comments: boolean;\n /**\n * Whether comments can be deleted: the store implements `deleteComment`.\n * Sent by `@siteping/server`; a client that does not delete (the widget)\n * leaves it out.\n */\n deleteComments?: boolean | undefined;\n}\n\n/** Paginated `FeedbackResponse` shape returned by the API. */\nexport interface FeedbackResponseList {\n feedbacks: FeedbackResponse[];\n total: number;\n /** The server's {@link SITEPING_PROTOCOL_VERSION} — absent from servers that predate it: read that as 0. */\n protocol?: number | undefined;\n /** Always sent by `@siteping/server` — absent from servers that predate it. */\n capabilities?: SitepingCapabilities | undefined;\n /** Always sent by `@siteping/server` — absent from servers that predate it. */\n permissions?: FeedbackListPermissions | undefined;\n}\n","import { MAX_COMMENTS_PER_FEEDBACK } from \"@siteping/core\";\n\n/** Most annotations accepted on one feedback (the create schema enforces it too). */\nexport const MAX_ANNOTATIONS_PER_FEEDBACK = 50;\n\n/**\n * Default `maxBodyBytes`: twice the largest submission the validation\n * accepts — a 1.5 MB screenshot, 50 annotations and full diagnostics come\n * to about 2 MB.\n */\nexport const DEFAULT_MAX_BODY_BYTES = 4 * 1024 * 1024;\n\n/**\n * Most issues a validation 400 lists. Fifty annotations of `{}` alone make a\n * thousand: listing them all would answer a small body with a large one.\n */\nexport const MAX_VALIDATION_ISSUES = 20;\n\n/**\n * Most `authorize` dry runs of one response in flight at once. A page of 100\n * asks 401 of them, and a database policy's pool holds a handful of\n * connections: unbounded, they would queue there, time out, and read as refusals.\n */\nexport const DRY_RUN_CONCURRENCY = 8;\n\n/**\n * `error` strings of the HTTP API. Informational, not part of the protocol\n * contract: clients branch on the status code (the widget and the dashboard\n * match no string). Kept stable where possible, but a minor may reword one.\n */\nexport const ERROR_MESSAGES = {\n invalidJson: \"Invalid JSON\",\n bodyTooLarge: \"Request body too large\",\n unauthorized: \"Unauthorized\",\n apiKeyRequiredForDestructive: \"apiKey required for destructive operations\",\n forbidden: \"Forbidden\",\n unsupportedMediaType: \"Content-Type must be application/json\",\n feedbackNotFound: \"Feedback not found\",\n commentNotFound: \"Comment not found\",\n clientIdUsedByAnotherProject: \"clientId already used by another project\",\n clientIdUsedByAnotherFeedback: \"clientId already used on another feedback\",\n commentsUnsupported: \"Comments are not supported by this store\",\n tooManyComments: `Too many client comments on this feedback (max ${MAX_COMMENTS_PER_FEEDBACK})`,\n tooManyAnnotations: `Too many annotations (max ${MAX_ANNOTATIONS_PER_FEEDBACK})`,\n valueTooLong: \"A value is too long for this server's database\",\n deletionAborted: \"Deletion aborted: a linked resource could not be cleaned up\",\n internalServerError: \"Internal server error\",\n} as const;\n\n/** Query parameters the list endpoint reads (everything else is ignored). */\nexport const LIST_QUERY_KEYS = [\n \"projectName\",\n \"page\",\n \"limit\",\n \"type\",\n \"status\",\n \"statuses\",\n \"search\",\n \"url\",\n \"urlPattern\",\n] as const;\n","import { SitepingConfigError } from \"@siteping/core\";\nimport { ERROR_MESSAGES } from \"./constants.js\";\nimport type {\n SitepingAccessControl,\n SitepingApiKeyHandlerOptions,\n SitepingAuthorizationContext,\n SitepingHttpMethod,\n SitepingPrincipal,\n SitepingRequestContext,\n} from \"./options.js\";\n\n/** Outcome of the access check that opens every request. */\nexport type AccessOutcome<Principal> = { ok: true; principal: Principal } | { ok: false; status: 401; error: string };\n\n/** The access policy the operations run, normalised from either option set. */\nexport interface AccessGate<Principal> {\n /**\n * Whether a successful POST echoes `authorEmail` whatever the requester may\n * read. Only the `apiKey` policy does: its public POST has always returned\n * the email to the submitter, who supplied it (or proved ownership of the\n * record by presenting its clientId).\n */\n readonly echoesAuthorEmailOnCreate: boolean;\n /** Whether POST/PATCH/DELETE must pass the CSRF guards (see `access`). */\n readonly guardsMutations: boolean;\n /** `Cache-Control` of the list response. */\n readonly listCacheControl: string;\n authenticate(request: Request, method: SitepingHttpMethod): Promise<AccessOutcome<Principal>>;\n /**\n * Whether the caller of an admitted request would be admitted on `method`\n * too — the first half of each `permissions` answer, `authorize` the second.\n */\n admits(request: Request, method: SitepingHttpMethod): Promise<boolean>;\n authorize(context: SitepingAuthorizationContext<Principal>): Promise<boolean>;\n /** Whether responses about `projectName` may include `authorEmail` for this caller. */\n canReadAuthorEmail(context: SitepingRequestContext<Principal>, projectName: string): Promise<boolean>;\n /**\n * Whether an authenticated caller's comment in `projectName` keeps the\n * `team` role it asks for. Asked only when a comment does, so a GET never\n * runs the policy's callback for it.\n */\n canCommentAsTeam(\n context: SitepingRequestContext<Principal>,\n projectName: string,\n canReadAuthorEmail: boolean,\n ): Promise<boolean>;\n}\n\nconst textEncoder = new TextEncoder();\n\n/**\n * Constant-time string comparison for API key validation, without\n * `node:crypto` so the handler runs on any runtime with Web APIs (Node, Bun,\n * Deno, edge workers). Returns `false` early when lengths differ (an\n * unavoidable length leak); the byte comparison itself does not short-circuit.\n *\n * Lengths are compared in BYTES: multi-byte characters make strings of equal\n * `.length` differ in bytes.\n */\nfunction safeCompare(a: string, b: string): boolean {\n const bytesA = textEncoder.encode(a);\n const bytesB = textEncoder.encode(b);\n if (bytesA.length !== bytesB.length) return false;\n let difference = 0;\n for (let index = 0; index < bytesA.length; index++) {\n difference |= (bytesA[index] ?? 0) ^ (bytesB[index] ?? 0);\n }\n return difference === 0;\n}\n\n/** `NODE_ENV === \"production\"`, tolerating runtimes without `process` (edge workers, Deno). */\nfunction isProductionEnvironment(): boolean {\n return typeof process !== \"undefined\" && process.env?.NODE_ENV === \"production\";\n}\n\n/**\n * The shared-secret policy (`apiKey`, `publicEndpoints`,\n * `requireAuthForDestructive`, `redactUnauthenticatedEmails`). It never\n * resolves a principal.\n *\n * @throws SitepingConfigError (`CONFIG_API_KEY_REQUIRED`) in production without\n * `apiKey` while destructive endpoints still require it — the factory refuses\n * an unauthenticated destructive surface.\n */\nexport function createApiKeyGate({\n apiKey,\n publicEndpoints = apiKey ? [\"POST\", \"OPTIONS\"] : undefined,\n requireAuthForDestructive = true,\n redactUnauthenticatedEmails = true,\n}: Pick<\n SitepingApiKeyHandlerOptions,\n \"apiKey\" | \"publicEndpoints\" | \"requireAuthForDestructive\" | \"redactUnauthenticatedEmails\"\n>): AccessGate<null> {\n // Without this guard, anyone could `DELETE { deleteAll: true }` against the API.\n if (!apiKey && requireAuthForDestructive && isProductionEnvironment()) {\n throw new SitepingConfigError(\n \"CONFIG_API_KEY_REQUIRED\",\n \"[siteping] createSitepingHandler: apiKey is required in production. \" +\n \"Set `apiKey` to enable destructive endpoints, pass `access` to plug in your own authentication, \" +\n \"or pass `requireAuthForDestructive: false` if SitePing sits behind your own auth middleware.\",\n );\n }\n\n const publicMethods: ReadonlySet<SitepingHttpMethod> | null = publicEndpoints ? new Set(publicEndpoints) : null;\n\n /**\n * True iff `apiKey` is configured AND the request carries a matching Bearer\n * token. A valid token on a public method still counts: it drives PII\n * redaction, not access control.\n */\n const isBearerAuthenticated = (request: Request): boolean => {\n if (!apiKey) return false;\n const header = request.headers.get(\"Authorization\");\n return header !== null && safeCompare(header, `Bearer ${apiKey}`);\n };\n\n const authenticate: AccessGate<null>[\"authenticate\"] = async (request, method) => {\n if (!apiKey) {\n // GET/POST/OPTIONS stay open by default so the widget keeps working in dev without config.\n if (requireAuthForDestructive && (method === \"DELETE\" || method === \"PATCH\")) {\n return { ok: false, status: 401, error: ERROR_MESSAGES.apiKeyRequiredForDestructive };\n }\n return { ok: true, principal: null };\n }\n if (publicMethods?.has(method) || isBearerAuthenticated(request)) {\n return { ok: true, principal: null };\n }\n return { ok: false, status: 401, error: ERROR_MESSAGES.unauthorized };\n };\n\n return {\n echoesAuthorEmailOnCreate: true,\n // The key travels in a header a forged cross-site request cannot set.\n guardsMutations: false,\n listCacheControl: \"private, max-age=5\",\n authenticate,\n // This policy admits by method: a visitor reads, the key holder triages.\n async admits(request, method) {\n return (await authenticate(request, method)).ok;\n },\n async authorize() {\n return true;\n },\n // One key covers every project, so the project changes nothing here.\n async canReadAuthorEmail({ request }) {\n return !redactUnauthenticatedEmails || isBearerAuthenticated(request);\n },\n // The key proves the caller is the project side; a public POST does not.\n async canCommentAsTeam({ request }) {\n return isBearerAuthenticated(request);\n },\n };\n}\n\n/** A custom `access` policy. */\nexport function createAccessGate<Principal extends SitepingPrincipal>(\n access: SitepingAccessControl<Principal>,\n): AccessGate<Principal> {\n return {\n echoesAuthorEmailOnCreate: false,\n guardsMutations: true,\n listCacheControl: \"no-store\",\n async authenticate(request) {\n const principal = await access.authenticate(request);\n // Fail closed on anything falsy: `return session?.user` or a plain\n // JavaScript `false` check must never let a request in.\n if (!principal) {\n return { ok: false, status: 401, error: ERROR_MESSAGES.unauthorized };\n }\n return { ok: true, principal };\n },\n // A dry run reuses the response's request, so `authenticate` is never asked\n // about another method: `authorize` decides, by action.\n async admits() {\n return true;\n },\n async authorize(context) {\n return access.authorize ? access.authorize(context) : true;\n },\n // Emails are personal data: only a `true` answer reveals them — no\n // callback, or `undefined` from a principal missing the flag, hides them.\n async canReadAuthorEmail({ request, principal }, projectName) {\n return (await access.canReadAuthorEmail?.(principal, { request, projectName })) === true;\n },\n // Speaking as the team is an impersonation privilege: without the host's\n // word for it (its own callback, or the email access it grants), refuse.\n async canCommentAsTeam({ request, principal }, projectName, canReadAuthorEmail) {\n // Anything but `true` from a callback refuses, as for emails.\n return access.canCommentAsTeam\n ? (await access.canCommentAsTeam(principal, { request, projectName })) === true\n : canReadAuthorEmail;\n },\n };\n}\n","export type CorsHeaders = Readonly<Record<string, string>>;\n\n/** Request headers every allowlisted origin may send (preflights can add more). */\nconst DEFAULT_ALLOWED_HEADERS: ReadonlyArray<string> = [\"Content-Type\", \"Authorization\"];\n\n/** RFC 9110 `token` — the only valid shape for a header field name. */\nconst HEADER_TOKEN = /^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$/;\n\n/**\n * Build CORS headers for a given request.\n * When `allowedOrigins` is set, only matching origins get reflected.\n * When unset, no CORS headers are added (no permissive wildcard by default).\n */\nexport function buildCorsHeaders(request: Request, allowedOrigins: ReadonlyArray<string> | undefined): CorsHeaders {\n if (!allowedOrigins) return {};\n\n // With an allowlist the response depends on Origin even when it gets no\n // CORS headers (Origin absent or unlisted) — without `Vary`, a shared cache\n // could replay a header-less response to an allowed origin, or vice versa.\n const origin = request.headers.get(\"Origin\");\n if (!origin || !allowedOrigins.includes(origin)) return { Vary: \"Origin\" };\n\n return {\n \"Access-Control-Allow-Origin\": origin,\n \"Access-Control-Allow-Methods\": \"GET, POST, PATCH, DELETE, OPTIONS\",\n \"Access-Control-Allow-Headers\": DEFAULT_ALLOWED_HEADERS.join(\", \"),\n \"Access-Control-Allow-Credentials\": \"true\",\n \"Access-Control-Max-Age\": \"86400\",\n Vary: \"Origin\",\n };\n}\n\n/**\n * Attach CORS headers to an existing Response.\n */\nexport function withCors(response: Response, corsHeaders: CorsHeaders): Response {\n for (const [key, value] of Object.entries(corsHeaders)) {\n response.headers.set(key, value);\n }\n return response;\n}\n\n/**\n * `Access-Control-Allow-Headers` for a preflight from an ALLOWLISTED origin:\n * the defaults plus the header names it asks for — the widget's `headers`\n * option lets hosts send their own (a session token, a tenant id), which the\n * fixed default list would block. Names that are not valid header tokens\n * are dropped.\n */\nfunction preflightAllowedHeaders(request: Request): string {\n const allowed = [...DEFAULT_ALLOWED_HEADERS];\n const seen = new Set(allowed.map((name) => name.toLowerCase()));\n for (const raw of (request.headers.get(\"Access-Control-Request-Headers\") ?? \"\").split(\",\")) {\n const name = raw.trim();\n if (!HEADER_TOKEN.test(name) || seen.has(name.toLowerCase())) continue;\n seen.add(name.toLowerCase());\n allowed.push(name);\n }\n return allowed.join(\", \");\n}\n\n/**\n * Answer a CORS preflight. Configure `allowedOrigins` to restrict which\n * domains can make cross-origin requests to the API: without it, no CORS\n * headers are emitted and browsers block widget requests.\n */\nexport function preflightResponse(request: Request, allowedOrigins: ReadonlyArray<string> | undefined): Response {\n const corsHeaders = buildCorsHeaders(request, allowedOrigins);\n // An allowlisted preflight's answer also depends on the headers it requests.\n const headers = corsHeaders[\"Access-Control-Allow-Origin\"]\n ? {\n ...corsHeaders,\n \"Access-Control-Allow-Headers\": preflightAllowedHeaders(request),\n Vary: \"Origin, Access-Control-Request-Headers\",\n }\n : corsHeaders;\n return new Response(null, { status: 204, headers });\n}\n","/**\n * Outgoing webhook notifications.\n *\n * Plug a Slack, Discord, or generic HTTP endpoint into `createSitepingHandler`\n * to receive a payload once a write succeeds: a feedback created (the\n * default), its status changed, a feedback or a project's feedback deleted,\n * a reply posted — each webhook picks its `events`, and every delivery names\n * its event in the `X-Siteping-Event` header. Webhooks are dispatched as\n * fire-and-forget (`void Promise.all(...)`) so a slow or down receiver never\n * blocks the client response — the write is already in the DB by the time we\n * dial out.\n *\n * - **Type-specific formatting**: Slack uses `{ text, blocks }`, Discord uses\n * `{ content, embeds }`, generic posts the record as JSON (minus `clientId`).\n * - **Untrusted input**: `message` and `authorName` come from anonymous\n * visitors. Slack text is escaped, Discord markdown is escaped and its\n * mention parsing disabled, so a public feedback form can never be turned\n * into a channel-wide ping or a disguised link.\n * - **Timeout**: 5s by default (overridable per webhook).\n * - **Error handling**: `config.onError(err, feedbackId, delivery)` is invoked\n * when present; otherwise we log a one-liner to `console.warn` so the issue\n * is surfaced without crashing the request.\n */\n\nimport {\n type CommentRecord,\n type CommentResponse,\n type FeedbackRecord,\n type FeedbackStatus,\n type FeedbackType,\n type Prettify,\n type Serialized,\n SitepingConfigError,\n} from \"@siteping/core\";\nimport type { SitepingCreatedComment, SitepingDeletionTarget } from \"./options.js\";\n\n/** Supported webhook integrations — drives the JSON body shape. */\nexport type WebhookType = \"slack\" | \"discord\" | \"generic\";\n\n/**\n * What a delivery reports — sent as its `X-Siteping-Event` header, and what a\n * webhook's `events` subscribes to:\n *\n * - `feedback.created` — a feedback was stored (never for a replayed `clientId`).\n * - `feedback.status_changed` — a PATCH set a feedback's status, even the one it had.\n * - `feedback.deleted` — one feedback, or every feedback of a project, was deleted.\n * - `comment.created` — a reply was stored (never for a replayed `clientId`).\n */\nexport type WebhookEvent = keyof GenericWebhookEventMap;\n\n/** Every {@link WebhookEvent}, in the order above — e.g. to validate an `X-Siteping-Event` header. */\nexport const WEBHOOK_EVENTS = [\n \"feedback.created\",\n \"feedback.status_changed\",\n \"feedback.deleted\",\n \"comment.created\",\n] as const satisfies ReadonlyArray<WebhookEvent>;\n\n/** Compile lock: `WEBHOOK_EVENTS` lists every key of {@link GenericWebhookEventMap}. */\nconst _everyEventListed: Exclude<WebhookEvent, (typeof WEBHOOK_EVENTS)[number]> extends never ? true : never = true;\nvoid _everyEventListed;\n\n/** What a webhook receives when it sets no `events`: the creation alone, as before events existed. */\nconst DEFAULT_EVENTS: ReadonlyArray<WebhookEvent> = [\"feedback.created\"];\n\n/** Header naming a delivery's event — set on every request. */\nconst EVENT_HEADER = \"X-Siteping-Event\";\n\n/** What `onError` learns about the failed delivery, beyond the feedback id. */\nexport interface WebhookDeliveryInfo {\n event: WebhookEvent;\n projectName: string;\n}\n\n/**\n * Outgoing webhook configuration.\n *\n * - `url` — required, the HTTPS endpoint to POST to.\n * - `type` — payload format. Defaults to `\"generic\"` (raw JSON).\n * - `headers` — extra headers merged on top of `Content-Type: application/json`.\n * Useful for signed-payload schemes (`X-Signature`, bearer tokens, …).\n * - `timeoutMs` — abort the fetch after this many ms: a positive integer of\n * at most 2,147,483,647, the longest delay a timer holds. Defaults to 5000.\n * - `events` — the {@link WebhookEvent}s this webhook receives: a non-empty\n * list, defaulting to `[\"feedback.created\"]`, so a webhook set up before\n * events existed gets exactly the deliveries it always did.\n * - `onError` — invoked with the underlying error, the id of the feedback the\n * delivery is about (`\"\"` for a project-wide deletion) and the delivery's\n * event and project, when the dispatch fails (network error, non-2xx,\n * timeout). The webhook is fire-and-forget, so this is your only chance to\n * observe failures. An async one is awaited: the delivery `waitUntil` holds\n * covers it, and its rejection is reported like a throw, never left\n * unhandled.\n */\nexport interface WebhookConfig {\n url: string;\n type?: WebhookType;\n headers?: Record<string, string>;\n timeoutMs?: number;\n events?: ReadonlyArray<WebhookEvent>;\n onError?: (err: Error, feedbackId: string, delivery: WebhookDeliveryInfo) => void | Promise<void>;\n}\n\nconst DEFAULT_TIMEOUT_MS = 5000;\n\n/** Longest delay a timer holds: Node and browsers fire a longer one at once, which would abort every delivery. */\nconst TIMER_MAX_DELAY_MS = 2_147_483_647;\n\n/** Why `timeoutMs` cannot bound a delivery, or `null` when it can (or is unset). */\nfunction timeoutProblem(timeoutMs: number | undefined): string | null {\n if (\n timeoutMs === undefined ||\n (Number.isSafeInteger(timeoutMs) && timeoutMs > 0 && timeoutMs <= TIMER_MAX_DELAY_MS)\n ) {\n return null;\n }\n return `timeoutMs must be a positive integer of at most ${TIMER_MAX_DELAY_MS}, got ${timeoutMs}`;\n}\n\n/** Why `events` cannot route deliveries, or `null` when it can (or is unset). */\nfunction eventsProblem(events: ReadonlyArray<WebhookEvent> | undefined): string | null {\n if (events === undefined) return null;\n if (!Array.isArray(events) || events.length === 0) return \"events must list at least one event\";\n const unknown = events.find((event) => !(WEBHOOK_EVENTS as ReadonlyArray<string>).includes(event));\n if (unknown !== undefined)\n return `unknown event ${JSON.stringify(unknown)} in events (one of ${WEBHOOK_EVENTS.join(\", \")})`;\n return null;\n}\n\n/**\n * Refuse a webhook whose `timeoutMs` no timer holds, or whose `events` would\n * route nothing (empty) or name no event (a typo), when the handler is\n * created rather than on every delivery.\n *\n * @throws SitepingConfigError (`CONFIG_WEBHOOK_TIMEOUT_INVALID`) naming the\n * webhook by its origin: its path may be a credential.\n */\nexport function checkWebhookConfigs(configs: readonly WebhookConfig[]): void {\n for (const config of configs) {\n const problem = timeoutProblem(config.timeoutMs) ?? eventsProblem(config.events);\n if (problem) {\n throw new SitepingConfigError(\n \"CONFIG_WEBHOOK_TIMEOUT_INVALID\",\n `[siteping] createSitepingHandler: webhook to ${webhookOrigin(config.url)}: ${problem}.`,\n );\n }\n }\n}\n\n/** Whether `config` receives `event`'s deliveries. */\nexport function subscribesTo(config: WebhookConfig, event: WebhookEvent): boolean {\n return (config.events ?? DEFAULT_EVENTS).includes(event);\n}\n\n/** Decimal RGB colour table used by Discord embeds — keyed by feedback type. */\nconst DISCORD_COLORS: Readonly<Record<FeedbackType, number>> = {\n bug: 0xef4444,\n question: 0x3b82f6,\n change: 0xf59e0b,\n other: 0x6b7280,\n};\n\nconst DEFAULT_DISCORD_COLOR = 0x6b7280;\n\n// ---------------------------------------------------------------------------\n// Payload shapes — narrow types let TypeScript catch malformed bodies at\n// compile time rather than only at the receiving end.\n// ---------------------------------------------------------------------------\n\n/** Block Kit envelope used by Slack incoming webhooks. */\nexport interface SlackWebhookPayload {\n text: string;\n blocks: ReadonlyArray<SlackHeaderBlock | SlackSectionBlock | SlackContextBlock>;\n}\n\ninterface SlackHeaderBlock {\n type: \"header\";\n text: { type: \"plain_text\"; text: string; emoji: true };\n}\n\ninterface SlackSectionBlock {\n type: \"section\";\n text: { type: \"mrkdwn\"; text: string };\n}\n\ninterface SlackContextBlock {\n type: \"context\";\n elements: ReadonlyArray<{ type: \"mrkdwn\"; text: string }>;\n}\n\n/** Embed envelope used by Discord incoming webhooks. */\nexport interface DiscordWebhookPayload {\n content: string;\n embeds: ReadonlyArray<{\n title: string;\n description: string;\n color: number;\n fields: ReadonlyArray<{ name: string; value: string; inline: boolean }>;\n timestamp: string;\n }>;\n /**\n * Mention parsing is switched off: `content` carries end-user text, so an\n * author called `@everyone` must render as text, never as a notification.\n */\n allowed_mentions: { parse: ReadonlyArray<\"roles\" | \"users\" | \"everyone\"> };\n}\n\n/**\n * Generic webhook body — the stored record as the receiver parses it: dates\n * are ISO strings, and `clientId` is stripped like on every other output, on\n * the record and on each comment of its thread. It is the browser-local\n * dedup secret, and the POST replay path hands the full record to whoever\n * presents it.\n */\nexport type GenericWebhookPayload = Prettify<\n Serialized<Omit<FeedbackRecord, \"clientId\" | \"comments\">> & { comments?: CommentResponse[] | undefined }\n>;\n\n/** What `buildGenericPayload` returns — {@link GenericWebhookPayload} before `JSON.stringify`. */\ntype GenericWebhookBody = Omit<FeedbackRecord, \"clientId\" | \"comments\"> & {\n comments?: Omit<CommentRecord, \"clientId\">[] | undefined;\n};\n\n/**\n * Generic body of `feedback.deleted` — what was deleted, as the `onDeleting`\n * and `onDeleted` hooks receive it: `{ kind: \"single\", id, projectName }` for\n * one feedback, `{ kind: \"project\", projectName }` for a `deleteAll`, which\n * fires even when the project held nothing (stores report no count).\n */\nexport type GenericWebhookDeletionPayload = SitepingDeletionTarget;\n\n/**\n * Generic body of `comment.created` — the stored comment as the receiver\n * parses it (`createdAt` an ISO string, `clientId` stripped), plus the\n * project its feedback belongs to. The author's email is unredacted, like\n * the feedback's in {@link GenericWebhookPayload}.\n */\nexport type GenericWebhookCommentPayload = Prettify<Serialized<Omit<SitepingCreatedComment, \"clientId\">>>;\n\n/**\n * The generic body of each {@link WebhookEvent} — what a `\"generic\"` receiver\n * parses, read from its `X-Siteping-Event` header. `feedback.created` and\n * `feedback.status_changed` both carry the feedback as stored after the write.\n */\nexport interface GenericWebhookEventMap {\n \"feedback.created\": GenericWebhookPayload;\n \"feedback.status_changed\": GenericWebhookPayload;\n \"feedback.deleted\": GenericWebhookDeletionPayload;\n \"comment.created\": GenericWebhookCommentPayload;\n}\n\n/** One notification: its event and the record it is about. */\nexport type WebhookDelivery =\n | { event: \"feedback.created\" | \"feedback.status_changed\"; feedback: FeedbackRecord }\n | { event: \"feedback.deleted\"; target: SitepingDeletionTarget }\n | { event: \"comment.created\"; comment: SitepingCreatedComment };\n\n/** The feedback id and project a delivery is about — `\"\"` for a project-wide deletion. */\nexport function deliverySubject(delivery: WebhookDelivery): { feedbackId: string; projectName: string } {\n switch (delivery.event) {\n case \"feedback.deleted\":\n return delivery.target.kind === \"single\"\n ? { feedbackId: delivery.target.id, projectName: delivery.target.projectName }\n : { feedbackId: \"\", projectName: delivery.target.projectName };\n case \"comment.created\":\n return { feedbackId: delivery.comment.feedbackId, projectName: delivery.comment.projectName };\n default:\n return { feedbackId: delivery.feedback.id, projectName: delivery.feedback.projectName };\n }\n}\n\n/** Mapping from webhook type to its concrete body shape. */\nexport interface WebhookPayloadMap {\n slack: SlackWebhookPayload;\n discord: DiscordWebhookPayload;\n generic: GenericWebhookPayload;\n}\n\n/** Truncate a message for chat-platform previews (Slack/Discord look bad with walls of text). */\nfunction truncate(text: string, max = 300): string {\n if (text.length <= max) return text;\n return `${text.slice(0, max - 1)}…`;\n}\n\n/**\n * Join escaped `units` (one per source character) and truncate so the ESCAPED\n * result fits `max`. Truncation drops whole units, so an escape sequence\n * (`&`, `\\[`, `%28`) is never cut in half — and escaping can grow a value\n * several-fold, so sizing the raw text alone would not guarantee fit.\n */\nfunction fitEscaped(units: readonly string[], max: number): string {\n const escaped = units.join(\"\");\n if (escaped.length <= max) return escaped;\n let out = \"\";\n for (const unit of units) {\n if (out.length + unit.length > max - 1) break;\n out += unit;\n }\n return `${out}…`;\n}\n\n/** Block Kit caps `header` text at 150 characters — longer payloads are rejected outright. */\nconst SLACK_HEADER_MAX = 150;\n\n/** Block Kit caps each `mrkdwn` text object at 3000 characters — also a whole-message rejection. */\nconst SLACK_TEXT_MAX = 3000;\n\n/**\n * Escape the three characters Slack parses as control characters in message\n * text (`&`, `<`, `>`) — the exact escaping Slack's formatting rules require\n * for user-provided content. Feedback text is typed by anonymous visitors:\n * unescaped, `<!channel>` notifies the whole channel and\n * `<https://evil.example|Reset your password>` renders as a disguised link.\n */\nfunction escapeSlackText(text: string): string {\n return text.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n}\n\n/** Untrusted text escaped for a Slack `mrkdwn` field, sized to `max` (see `fitEscaped`). */\nconst slackField = (value: string, max: number): string => fitEscaped(Array.from(value, escapeSlackText), max);\n\n/** Two halves + \"*From:* ()\" stay under the text-object limit. */\nconst SLACK_FROM_HALF = Math.floor((SLACK_TEXT_MAX - 11) / 2);\n\n/** `*Project:* …`, sized to the text-object limit. */\nconst slackProject = (projectName: string): string => `*Project:* ${slackField(projectName, SLACK_TEXT_MAX - 11)}`;\n\n/** `*URL:* …`, sized to the text-object limit. */\nconst slackUrl = (url: string): string => `*URL:* ${slackField(url, SLACK_TEXT_MAX - 7)}`;\n\n/**\n * Slack message: text fallback + Block Kit blocks for rich rendering. The\n * `plain_text` header is rendered verbatim by Slack (no markup parsing), so it\n * keeps raw values; `preview` and every `context` field must come escaped.\n */\nfunction slackMessage(headline: string, preview: string, context: readonly string[]): SlackWebhookPayload {\n return {\n text: `${escapeSlackText(headline)}: ${preview}`,\n blocks: [\n {\n type: \"header\",\n text: { type: \"plain_text\", text: truncate(headline, SLACK_HEADER_MAX), emoji: true },\n },\n {\n type: \"section\",\n text: { type: \"mrkdwn\", text: preview },\n },\n {\n type: \"context\",\n elements: context.map((text) => ({ type: \"mrkdwn\", text })),\n },\n ],\n };\n}\n\n/** `*From:* name (email)` — an empty email (a dashboard user's reply) drops its parentheses. */\nfunction slackFrom(authorName: string, authorEmail: string): string {\n const email = authorEmail ? ` (${slackField(authorEmail, SLACK_FROM_HALF)})` : \"\";\n return `*From:* ${slackField(authorName, SLACK_FROM_HALF)}${email}`;\n}\n\n/** How a status change reads in a chat headline. */\nconst STATUS_VERBS: Readonly<Record<FeedbackStatus, string>> = {\n open: \"reopened\",\n in_progress: \"marked in progress\",\n resolved: \"resolved\",\n wont_fix: \"marked won't fix\",\n};\n\n/** Slack message for `feedback.created`. */\nfunction buildSlackPayload(feedback: FeedbackRecord): SlackWebhookPayload {\n return slackMessage(\n `New ${feedback.type} feedback from ${feedback.authorName}`,\n escapeSlackText(truncate(feedback.message)),\n [\n slackProject(feedback.projectName),\n `*Type:* ${feedback.type}`,\n slackUrl(feedback.url),\n `*From:* ${slackField(feedback.authorName, SLACK_FROM_HALF)} (${slackField(feedback.authorEmail, SLACK_FROM_HALF)})`,\n ],\n );\n}\n\n/** Slack message for `feedback.status_changed`. */\nfunction buildSlackStatusPayload(feedback: FeedbackRecord): SlackWebhookPayload {\n return slackMessage(\n `Feedback ${STATUS_VERBS[feedback.status]}: ${feedback.type} from ${feedback.authorName}`,\n escapeSlackText(truncate(feedback.message)),\n [\n slackProject(feedback.projectName),\n `*Status:* ${feedback.status}`,\n slackUrl(feedback.url),\n slackFrom(feedback.authorName, feedback.authorEmail),\n ],\n );\n}\n\n/** Slack message for `feedback.deleted`. */\nfunction buildSlackDeletionPayload(target: SitepingDeletionTarget): SlackWebhookPayload {\n if (target.kind === \"project\") {\n return slackMessage(\n \"All feedback deleted\",\n `Every feedback of ${slackField(target.projectName, SLACK_TEXT_MAX - 30)} was deleted.`,\n [slackProject(target.projectName)],\n );\n }\n return slackMessage(\"Feedback deleted\", `Feedback ${slackField(target.id, SLACK_TEXT_MAX - 30)} was deleted.`, [\n slackProject(target.projectName),\n `*ID:* ${slackField(target.id, SLACK_TEXT_MAX - 6)}`,\n ]);\n}\n\n/** Slack message for `comment.created`. */\nfunction buildSlackCommentPayload(comment: SitepingCreatedComment): SlackWebhookPayload {\n return slackMessage(\n `New ${comment.authorRole} reply from ${comment.authorName}`,\n escapeSlackText(truncate(comment.body)),\n [\n slackProject(comment.projectName),\n `*Feedback:* ${slackField(comment.feedbackId, SLACK_TEXT_MAX - 12)}`,\n slackFrom(comment.authorName, comment.authorEmail),\n ],\n );\n}\n\n/**\n * Discord API limits — a payload exceeding any of them is rejected whole\n * (HTTP 400), so the notification is lost rather than truncated.\n */\nconst DISCORD_CONTENT_MAX = 2000;\nconst DISCORD_TITLE_MAX = 256;\nconst DISCORD_FIELD_VALUE_MAX = 1024;\n\n/**\n * Characters Discord's markdown treats as syntax: emphasis, code, spoilers,\n * quotes, headings (and `-#` subtext), masked links (`[text](url)`), and\n * `<…>` mentions / channel links. `\\` is escaped too so a visitor's own\n * backslash can't cancel one of ours. `-` is left alone: outside `-#` it only\n * starts a bullet list — cosmetic, and escaping it would litter every\n * hyphenated name and URL.\n */\nconst DISCORD_MARKDOWN = /[\\\\*_~`|>#[\\]()<]/g;\n\n/**\n * An http(s) URL, bare or in Discord's `<url>` link form, which Discord\n * autolinks. Backslash escapes inside it would land in the link (browsers\n * read `\\` as `/`), so its characters are not escaped: only `[`, `]`, `(`,\n * `)` inside the link are percent-encoded, which keeps the address while\n * making sure no masked link can form (see `discordUrlUnits` for where a\n * bare link ends). `<https://…>` is never a mention. The capture group makes\n * `split` keep the URLs.\n */\nconst DISCORD_AUTOLINK = /(<https?:\\/\\/[^\\s<>]+>|https?:\\/\\/[^\\s<>]+)/i;\n\n/** Punctuation Discord leaves out of the end of a bare autolink. */\nconst DISCORD_LINK_TRAILER = `.,:;\"')]`;\n\nconst escapeMarkdownChar = (char: string) => char.replace(DISCORD_MARKDOWN, \"\\\\$&\");\n// Global so it stays complete on any input, not only the single code\n// points `discordUrlUnits` passes in.\nconst encodeLinkChar = (char: string) =>\n char.replace(/[[\\]()]/g, (bracket) => `%${bracket.charCodeAt(0).toString(16).toUpperCase()}`);\nconst count = (text: string, char: string) => text.split(char).length - 1;\n\n/**\n * Units for a URL token (see `DISCORD_AUTOLINK`). A bare URL's trailing\n * `.,:;\"')]` is the sentence around it — `(see https://…).` — and Discord\n * ends the link before it, so it goes out raw: encoded, it would change the\n * address; escaped, the `\\` would. Raw `)` and `]` are harmless because an\n * unescaped `[` is never sent, so no masked link can start. A `)` or `]`\n * closing a bracket opened inside the URL stays in it (`…/Mercury_(planet)`).\n */\nfunction discordUrlUnits(url: string): string[] {\n // Brackets the link opens minus those it closes, counted once and kept up\n // to date as the trailer is dropped: recounting per character is quadratic.\n const open = { \")\": count(url, \"(\") - count(url, \")\"), \"]\": count(url, \"[\") - count(url, \"]\") };\n let end = url.length;\n while (end > 0) {\n const last = url.charAt(end - 1);\n if (!DISCORD_LINK_TRAILER.includes(last)) break;\n if (last === \")\" || last === \"]\") {\n if (open[last] >= 0) break;\n open[last] += 1;\n }\n end -= 1;\n }\n return [...Array.from(url.slice(0, end), encodeLinkChar), ...url.slice(end)];\n}\n\n/**\n * Escape untrusted text for Discord, sized to `max` (see `fitEscaped`).\n * Feedback text is typed by anonymous visitors: unescaped,\n * `[Reset your password](https://evil.example)` renders as a disguised link —\n * the same threat `escapeSlackText` handles. URLs inside the text stay\n * linkable (see `DISCORD_AUTOLINK`).\n */\nfunction escapeDiscordText(text: string, max: number): string {\n // `split` alternates plain text (even indexes) and URLs (odd indexes).\n const units = text\n .split(DISCORD_AUTOLINK)\n .flatMap((part, index) => (index % 2 === 1 ? discordUrlUnits(part) : Array.from(part, escapeMarkdownChar)));\n return fitEscaped(units, max);\n}\n\n/**\n * Discord message: content fallback + embed for rich rendering. Sent with\n * mention parsing disabled — `content` embeds the author name, and Discord\n * would otherwise turn `@everyone` / `@here` into a server-wide ping. Every\n * user-supplied value is markdown-escaped and sized to its slot's limit.\n */\nfunction buildDiscordPayload(feedback: FeedbackRecord): DiscordWebhookPayload {\n const contentLead = `New **${feedback.type}** feedback from **`;\n const titleLead = `${feedback.type} — `;\n // Two halves + \" ()\" stay under the field-value limit.\n const authorHalf = Math.floor((DISCORD_FIELD_VALUE_MAX - 3) / 2);\n return {\n content: `${contentLead}${escapeDiscordText(feedback.authorName, DISCORD_CONTENT_MAX - contentLead.length - 2)}**`,\n embeds: [\n {\n title: `${titleLead}${escapeDiscordText(feedback.projectName, DISCORD_TITLE_MAX - titleLead.length)}`,\n description: escapeDiscordText(feedback.message, 300),\n color: DISCORD_COLORS[feedback.type] ?? DEFAULT_DISCORD_COLOR,\n fields: [\n { name: \"URL\", value: escapeDiscordText(feedback.url, DISCORD_FIELD_VALUE_MAX), inline: false },\n {\n name: \"Author\",\n value: `${escapeDiscordText(feedback.authorName, authorHalf)} (${escapeDiscordText(feedback.authorEmail, authorHalf)})`,\n inline: true,\n },\n { name: \"Viewport\", value: escapeDiscordText(feedback.viewport, DISCORD_FIELD_VALUE_MAX), inline: true },\n ],\n timestamp: new Date(feedback.createdAt).toISOString(),\n },\n ],\n allowed_mentions: { parse: [] },\n };\n}\n\n/** One Discord embed. */\ntype DiscordEmbed = DiscordWebhookPayload[\"embeds\"][number];\n\n/** Embed colour of a reply, which has no feedback type of its own. */\nconst COMMENT_DISCORD_COLOR = 0x8b5cf6;\n\n/** A Discord message with mention parsing disabled — every value in it must come escaped. */\nfunction discordMessage(content: string, embed: DiscordEmbed): DiscordWebhookPayload {\n return { content, embeds: [embed], allowed_mentions: { parse: [] } };\n}\n\n/** Discord message for `feedback.status_changed`. */\nfunction buildDiscordStatusPayload(feedback: FeedbackRecord): DiscordWebhookPayload {\n const contentLead = `Feedback **${STATUS_VERBS[feedback.status]}**: ${feedback.type} from **`;\n const titleLead = `${feedback.type} — `;\n return discordMessage(\n `${contentLead}${escapeDiscordText(feedback.authorName, DISCORD_CONTENT_MAX - contentLead.length - 2)}**`,\n {\n title: `${titleLead}${escapeDiscordText(feedback.projectName, DISCORD_TITLE_MAX - titleLead.length)}`,\n description: escapeDiscordText(feedback.message, 300),\n color: DISCORD_COLORS[feedback.type] ?? DEFAULT_DISCORD_COLOR,\n fields: [\n { name: \"Status\", value: feedback.status, inline: true },\n { name: \"URL\", value: escapeDiscordText(feedback.url, DISCORD_FIELD_VALUE_MAX), inline: false },\n ],\n timestamp: new Date(feedback.updatedAt).toISOString(),\n },\n );\n}\n\n/** Discord message for `feedback.deleted`, stamped with the time of the delete. */\nfunction buildDiscordDeletionPayload(target: SitepingDeletionTarget): DiscordWebhookPayload {\n const project = (max: number) => escapeDiscordText(target.projectName, max);\n const contentLead = target.kind === \"project\" ? \"All feedback deleted from **\" : \"Feedback deleted from **\";\n const fields = [{ name: \"Project\", value: project(DISCORD_FIELD_VALUE_MAX), inline: true }];\n if (target.kind === \"single\") {\n fields.push({ name: \"ID\", value: escapeDiscordText(target.id, DISCORD_FIELD_VALUE_MAX), inline: true });\n }\n return discordMessage(`${contentLead}${project(DISCORD_CONTENT_MAX - contentLead.length - 2)}**`, {\n title: target.kind === \"project\" ? \"All feedback deleted\" : \"Feedback deleted\",\n description:\n target.kind === \"project\"\n ? `Every feedback of ${project(300)} was deleted.`\n : `Feedback ${escapeDiscordText(target.id, 300)} was deleted.`,\n color: DEFAULT_DISCORD_COLOR,\n fields,\n timestamp: new Date().toISOString(),\n });\n}\n\n/** Discord message for `comment.created`. */\nfunction buildDiscordCommentPayload(comment: SitepingCreatedComment): DiscordWebhookPayload {\n const contentLead = `New **${comment.authorRole}** reply from **`;\n const titleLead = \"Reply — \";\n // Two halves + \" ()\" stay under the field-value limit; a reply from the\n // dashboard may carry no email at all.\n const authorHalf = Math.floor((DISCORD_FIELD_VALUE_MAX - 3) / 2);\n const author = comment.authorEmail\n ? `${escapeDiscordText(comment.authorName, authorHalf)} (${escapeDiscordText(comment.authorEmail, authorHalf)})`\n : escapeDiscordText(comment.authorName, DISCORD_FIELD_VALUE_MAX);\n return discordMessage(\n `${contentLead}${escapeDiscordText(comment.authorName, DISCORD_CONTENT_MAX - contentLead.length - 2)}**`,\n {\n title: `${titleLead}${escapeDiscordText(comment.projectName, DISCORD_TITLE_MAX - titleLead.length)}`,\n description: escapeDiscordText(comment.body, 300),\n color: COMMENT_DISCORD_COLOR,\n fields: [\n { name: \"Feedback\", value: escapeDiscordText(comment.feedbackId, DISCORD_FIELD_VALUE_MAX), inline: true },\n { name: \"Author\", value: author, inline: true },\n ],\n timestamp: new Date(comment.createdAt).toISOString(),\n },\n );\n}\n\n/** Generic JSON body — the record minus its `clientId`, and its comments minus theirs. */\nfunction buildGenericPayload(feedback: FeedbackRecord): GenericWebhookBody {\n const { clientId: _clientId, comments, ...payload } = feedback;\n if (!comments) return payload;\n return { ...payload, comments: comments.map(({ clientId: _commentClientId, ...comment }) => comment) };\n}\n\n/** Generic JSON body of `comment.created` — the comment minus its `clientId`. */\nfunction buildGenericCommentPayload(comment: SitepingCreatedComment): Omit<SitepingCreatedComment, \"clientId\"> {\n const { clientId: _clientId, ...payload } = comment;\n return payload;\n}\n\n/**\n * Build the JSON body for a single webhook based on its `type`.\n * Exported for tests; not part of the public API.\n *\n * @internal\n */\nexport function buildWebhookPayload<T extends WebhookType | undefined>(\n type: T,\n feedback: FeedbackRecord,\n): T extends \"slack\" ? SlackWebhookPayload : T extends \"discord\" ? DiscordWebhookPayload : GenericWebhookBody {\n return buildDeliveryPayload(type, { event: \"feedback.created\", feedback }) as never;\n}\n\n/**\n * Build the JSON body of `delivery` for a webhook of `type`.\n * Exported for tests; not part of the public API.\n *\n * @internal\n */\nexport function buildDeliveryPayload(\n type: WebhookType | undefined,\n delivery: WebhookDelivery,\n): SlackWebhookPayload | DiscordWebhookPayload | GenericWebhookBody | object {\n switch (delivery.event) {\n case \"feedback.created\":\n if (type === \"slack\") return buildSlackPayload(delivery.feedback);\n if (type === \"discord\") return buildDiscordPayload(delivery.feedback);\n return buildGenericPayload(delivery.feedback);\n case \"feedback.status_changed\":\n if (type === \"slack\") return buildSlackStatusPayload(delivery.feedback);\n if (type === \"discord\") return buildDiscordStatusPayload(delivery.feedback);\n return buildGenericPayload(delivery.feedback);\n case \"feedback.deleted\":\n if (type === \"slack\") return buildSlackDeletionPayload(delivery.target);\n if (type === \"discord\") return buildDiscordDeletionPayload(delivery.target);\n return { ...delivery.target };\n case \"comment.created\":\n if (type === \"slack\") return buildSlackCommentPayload(delivery.comment);\n if (type === \"discord\") return buildDiscordCommentPayload(delivery.comment);\n return buildGenericCommentPayload(delivery.comment);\n }\n}\n\n/**\n * Dispatch a single webhook with `feedback`'s `feedback.created` delivery,\n * whatever its `events` — the handler routes by `events` itself (see\n * {@link dispatchWebhooks}). Fire-and-forget: never throws, never rejects.\n *\n * - Builds the type-specific payload.\n * - POSTs with an `AbortSignal` timeout and an `X-Siteping-Event` header.\n * - On any error (network, non-2xx, timeout, exception), invokes\n * `config.onError(err, feedbackId, delivery)` if provided; otherwise logs a\n * one-liner. A `timeoutMs` no timer holds is such an error: nothing is sent.\n */\nexport async function dispatchWebhook(config: WebhookConfig, feedback: FeedbackRecord): Promise<void> {\n return deliver(config, { event: \"feedback.created\", feedback });\n}\n\n/** POST one delivery to one webhook — see {@link dispatchWebhook}. */\nasync function deliver(config: WebhookConfig, delivery: WebhookDelivery): Promise<void> {\n const problem = timeoutProblem(config.timeoutMs);\n if (problem) return reportError(config, new RangeError(problem), delivery);\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n // Payload building stays inside the try: it can throw too (an invalid\n // `createdAt` makes Discord's `toISOString()` throw a RangeError), and the\n // handler drops this promise, so a rejection would go unhandled.\n try {\n const body = JSON.stringify(buildDeliveryPayload(config.type ?? \"generic\", delivery));\n\n // Build merged headers — caller-supplied entries override `Content-Type`\n // when they explicitly need a different mime (rare for chat webhooks, but\n // possible for some generic receivers), and the event header the same\n // way. Names are case-insensitive: a `content-type` spread next to our\n // `Content-Type` would make fetch send both values combined, so a default\n // is dropped for any spelling.\n const custom = config.headers ?? {};\n const customNames = new Set(Object.keys(custom).map((name) => name.toLowerCase()));\n const headers = {\n ...(customNames.has(\"content-type\") ? {} : { \"Content-Type\": \"application/json\" }),\n ...(customNames.has(EVENT_HEADER.toLowerCase()) ? {} : { [EVENT_HEADER]: delivery.event }),\n ...custom,\n };\n\n // Use AbortSignal.timeout when available (Node 17.3+, all modern browsers).\n // Fall back to a manual controller for environments lacking it.\n timer = setTimeout(() => controller.abort(), timeoutMs);\n\n const response = await fetch(config.url, {\n method: \"POST\",\n headers,\n body,\n signal: controller.signal,\n });\n clearTimeout(timer);\n\n if (!response.ok) {\n const err = new Error(`Webhook responded with HTTP ${response.status}`);\n await reportError(config, err, delivery);\n }\n } catch (rawError) {\n clearTimeout(timer);\n const err = rawError instanceof Error ? rawError : new Error(String(rawError));\n await reportError(config, err, delivery);\n }\n}\n\nasync function reportError(config: WebhookConfig, err: Error, delivery: WebhookDelivery): Promise<void> {\n const { feedbackId, projectName } = deliverySubject(delivery);\n const subject = feedbackId ? `feedback ${feedbackId}` : `project ${projectName}`;\n if (config.onError) {\n try {\n // Awaited, so an async callback's rejection lands here like a throw.\n await config.onError(err, feedbackId, { event: delivery.event, projectName });\n } catch (callbackErr) {\n // Defense-in-depth: a thrown user callback must not bubble back up\n // and crash the request whose write already succeeded. Surface the\n // original error too so it isn't silently lost.\n warnWithoutUrl(\n config.url,\n `webhook onError() callback threw for ${subject}: ${String(callbackErr)} (original error: ${err.message})`,\n );\n }\n return;\n }\n warnWithoutUrl(config.url, `webhook to ${webhookOrigin(config.url)} failed for ${subject}: ${err.message}`);\n}\n\n/**\n * The part of a webhook URL that is safe to log. Slack and Discord embed the\n * credential in the path (`hooks.slack.com/services/T…/B…/<token>`), and any\n * URL may carry userinfo — only the origin identifies the target harmlessly.\n */\nfunction webhookOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return \"<invalid URL>\";\n }\n}\n\n/**\n * `console.warn` with every copy of the webhook URL reduced to its origin.\n * Node's fetch copies the URL it was given into some errors (\"…a URL that\n * includes credentials: <url>\", \"Failed to parse URL from <url>\"), and an\n * `onError` that rethrows or wraps one carries it into its own error.\n */\nfunction warnWithoutUrl(url: string, message: string): void {\n console.warn(`[siteping] ${url ? message.split(url).join(webhookOrigin(url)) : message}`);\n}\n\n/**\n * Dispatch `feedback`'s `feedback.created` delivery to every configured\n * webhook that subscribes to it (every webhook without `events`), in\n * parallel. Awaiting the returned promise lets tests synchronize on\n * completion, but production callers should drop the promise on the floor\n * (`void dispatchWebhooks(...)`) so the HTTP response isn't held back on slow\n * receivers.\n */\nexport async function dispatchWebhooks(configs: readonly WebhookConfig[], feedback: FeedbackRecord): Promise<void> {\n await dispatchDelivery(configs, { event: \"feedback.created\", feedback });\n}\n\n/** Dispatch `delivery` to every webhook of `configs` that subscribes to its event, in parallel. Never rejects. */\nexport async function dispatchDelivery(configs: readonly WebhookConfig[], delivery: WebhookDelivery): Promise<void> {\n const subscribed = configs.filter((config) => subscribesTo(config, delivery.event));\n if (subscribed.length === 0) return;\n await Promise.all(subscribed.map((config) => deliver(config, delivery)));\n}\n","import type { SitepingHandlerBaseOptions } from \"./options.js\";\nimport type { Pipeline, Scope } from \"./pipeline.js\";\nimport {\n deliverySubject,\n dispatchDelivery,\n subscribesTo,\n type WebhookConfig,\n type WebhookDelivery,\n} from \"./webhooks.js\";\n\n/**\n * Notify the webhooks subscribed to `delivery`'s event, once its write has\n * succeeded. Fire-and-forget: never awaited, so the client isn't held back on\n * slow Slack/Discord/generic receivers, and never throws — the delivery traps\n * its own errors and reports them through `WebhookConfig.onError`.\n */\nexport type Notify<Principal> = (scope: Scope<Principal>, delivery: WebhookDelivery) => void;\n\nexport function createNotifier<Principal>(\n webhooks: ReadonlyArray<WebhookConfig>,\n waitUntil: SitepingHandlerBaseOptions<Principal>[\"waitUntil\"],\n pipeline: Pipeline<Principal>,\n): Notify<Principal> {\n return (scope, delivery) => {\n // No subscriber, no work handed to the runtime.\n if (!webhooks.some((config) => subscribesTo(config, delivery.event))) return;\n const pending = dispatchDelivery(webhooks, delivery);\n try {\n waitUntil?.(pending);\n } catch (error) {\n const { feedbackId, projectName } = deliverySubject(delivery);\n pipeline.logError(scope, \"[siteping] waitUntil failed\", {\n error,\n event: delivery.event,\n ...(feedbackId ? { feedbackId } : {}),\n projectName,\n });\n }\n };\n}\n","import type {\n AssertEqual,\n CommentDeletePayload,\n CommentPayload,\n FeedbackPayload,\n FeedbackStatus,\n FeedbackType,\n Prettify,\n} from \"@siteping/core\";\nimport {\n ANCHOR_ELEMENT_ID_MAX,\n ANCHOR_ELEMENT_TAG_MAX,\n CLIENT_ID_MAX_LENGTH,\n COMMENT_AUTHOR_ROLES,\n COMMENT_BODY_MAX_LENGTH,\n CONSOLE_DIAGNOSTIC_LEVELS,\n EMAIL_PATTERN,\n FEEDBACK_STATUSES,\n FEEDBACK_TYPES,\n IDENTITY_FIELD_MAX_LENGTH,\n PROJECT_NAME_MAX_LENGTH,\n} from \"@siteping/core\";\nimport * as zod from \"zod\";\nimport { MAX_VALIDATION_ISSUES } from \"./constants.js\";\n\n// Namespace import required: Zod publishes dual CJS/ESM, and bundlers (tsup, vitest) may\n// resolve the CJS entry where `import { z } from \"zod\"` fails because CJS wraps\n// the entire module under a default/namespace key. This workaround normalizes access\n// regardless of which entry point the bundler resolves.\n// See: https://github.com/colinhacks/zod/issues/2697\nconst z: typeof zod.z = (\"z\" in zod ? zod.z : zod) as typeof zod.z;\n\n/**\n * An array of at most `max` elements, refused on its length before any\n * element is parsed. zod checks every element before `.max()`, so an\n * oversized array would cost a parse of each element and one issue per\n * invalid field: a public POST of 0.6 MB answered with a 120 MB 400.\n */\nfunction boundedArray<Element extends zod.z.ZodType>(element: Element, max: number) {\n return z.array(z.unknown()).max(max).pipe(z.array(element));\n}\n\nconst anchorSchema = z.object({\n cssSelector: z.string().min(1).max(2000),\n xpath: z.string().min(1).max(2000),\n textSnippet: z.string().max(500),\n elementTag: z.string().min(1).max(ANCHOR_ELEMENT_TAG_MAX),\n // A resolver hint: an over-long id is dropped, as the widget now does,\n // rather than failing the whole payload — earlier widgets send it unbounded.\n elementId: z\n .string()\n .transform((id) => (id.length <= ANCHOR_ELEMENT_ID_MAX ? id : undefined))\n .optional(),\n textPrefix: z.string().max(200),\n textSuffix: z.string().max(200),\n fingerprint: z.string().max(200),\n neighborText: z.string().max(500),\n // Optional semantic anchor identifier from `data-feedback-anchor`.\n // Null when no semantic ancestor exists; widget always sends this field.\n anchorKey: z.string().max(200).nullable().optional(),\n});\n\nconst rectSchema = z.object({\n xPct: z.number().min(0).max(1),\n yPct: z.number().min(0).max(1),\n wPct: z.number().min(0).max(1),\n hPct: z.number().min(0).max(1),\n});\n\nconst annotationSchema = z.object({\n anchor: anchorSchema,\n rect: rectSchema,\n // Raw `window.scrollX/Y` — no lower bound: scrollX is negative on a\n // scrolled `dir=\"rtl\"` page. `z.number()` already rejects NaN/±Infinity.\n scrollX: z.number(),\n scrollY: z.number(),\n // Upper bound = the `Int` (INT4) column max, so oversized input is a 400\n // here rather than a database error at insert time.\n viewportW: z.number().int().positive().max(2_147_483_647),\n viewportH: z.number().int().positive().max(2_147_483_647),\n devicePixelRatio: z.number().positive().default(1),\n});\n\n// Diagnostics caps mirror the widget defaults — the widget never sends more\n// than these, but the schema enforces it server-side too so a tampered\n// client can't blow up the JSON column with megabytes of garbage.\nconst consoleEntrySchema = z.object({\n level: z.enum(CONSOLE_DIAGNOSTIC_LEVELS),\n timestamp: z.string().max(50),\n message: z.string().max(600),\n});\n\nconst networkEntrySchema = z.object({\n url: z.string().max(2000),\n method: z.string().max(20),\n status: z.number().int().min(0).max(599),\n durationMs: z.number().min(0).max(600_000),\n timestamp: z.string().max(50),\n});\n\nconst diagnosticsSchema = z.object({\n console: boundedArray(consoleEntrySchema, 50),\n network: boundedArray(networkEntrySchema, 20),\n});\n\n// Annotation rect position within the screenshot image, as fractions [0, 1]\n// of the image dimensions (see `ScreenshotRegion` in core). Strict: this is\n// persisted verbatim into a JSON column, so unknown keys are rejected rather\n// than silently stored.\nexport const screenshotRegionSchema = z.strictObject({\n xPct: z.number().min(0).max(1),\n yPct: z.number().min(0).max(1),\n wPct: z.number().min(0).max(1),\n hPct: z.number().min(0).max(1),\n});\n\n// Restrict to URL-safe identifiers. The widget generates UUIDs (or a\n// Date+Math.random fallback), both of which match. Anything outside this\n// alphabet — `..`, `/`, NUL, etc. — would be a path-traversal vector wherever\n// a custom store or storage puts the clientId in an object key or a local\n// path (the bundled stores hand `screenshotStorage.upload` the stored id).\nconst clientIdSchema = z\n .string()\n .min(1)\n .max(CLIENT_ID_MAX_LENGTH)\n .regex(/^[a-zA-Z0-9_-]+$/, \"clientId must be alphanumeric (a-z, A-Z, 0-9, _, -)\");\n\nexport const feedbackCreateSchema = z.object({\n projectName: z.string().min(1).max(PROJECT_NAME_MAX_LENGTH),\n type: z.enum(FEEDBACK_TYPES),\n message: z.string().min(1).max(5000),\n // Page-scope identifier the widget uses to group feedbacks. Defaults to\n // `window.location.pathname` (\"/orders/42\"), but hosts can override\n // `getPageScope()` to return a full URL, an opaque slug, anything they\n // want. We trim + require non-empty so whitespace-only payloads don't\n // leak into the DB; otherwise the value is opaque to the server and only\n // used as a literal Prisma equality filter, so the loose shape is safe.\n url: z.string().trim().min(1).max(2000),\n // Optional parameterized URL template (e.g. \"/orders/:orderId\") provided\n // by the host via `getPageScope()`. Null when host omits it.\n urlPattern: z.string().max(2000).nullable().optional(),\n viewport: z.string().min(1).max(50),\n userAgent: z.string().min(1).max(500),\n // The widget's identity modal validates against the same core pattern and\n // length cap, so an identity the modal accepts (and persists) is never a\n // 400 here.\n authorName: z.string().min(1).max(IDENTITY_FIELD_MAX_LENGTH),\n authorEmail: z.email({ pattern: EMAIL_PATTERN }).max(IDENTITY_FIELD_MAX_LENGTH),\n annotations: boundedArray(annotationSchema, 50),\n clientId: clientIdSchema,\n // Optional base64 JPEG data URL captured by the widget when\n // `enableScreenshot: true`. ~1.5 MB cap = roughly a 1.1 MB JPEG, well\n // above typical sizes (the widget downscales to 1200px). Rejects abuse\n // without truncating legitimate captures.\n screenshotDataUrl: z\n .string()\n .max(1_500_000)\n .regex(/^data:image\\/(jpeg|png|webp);base64,/, \"screenshotDataUrl must be a data:image/* base64 URL\")\n .nullable()\n .optional(),\n // Optional annotation-rect position within the screenshot. Sent by widgets\n // that capture context around the drawn rect; null + omitted are both\n // accepted so legacy clients (and captures without a screenshot) keep\n // working unchanged.\n screenshotRegion: screenshotRegionSchema.nullable().optional(),\n // Optional console + failed-network snapshot. The widget only attaches\n // this when `captureDiagnostics` is enabled; null + omitted are both\n // accepted so existing clients keep working unchanged.\n diagnostics: diagnosticsSchema.nullable().optional(),\n});\n\n// `feedbackId` names the thread — its presence is what routes a POST here\n// rather than to `feedbackCreateSchema`.\nexport const commentCreateSchema = z.object({\n projectName: z.string().min(1).max(PROJECT_NAME_MAX_LENGTH),\n feedbackId: z.string().min(1).max(200),\n // Trimmed, so a whitespace-only reply is a 400 rather than an empty bubble.\n body: z.string().trim().min(1).max(COMMENT_BODY_MAX_LENGTH),\n authorName: z.string().min(1).max(IDENTITY_FIELD_MAX_LENGTH),\n // The widget's reviewers always have an email; a dashboard user may have\n // none on file, sent as \"\". Anything else must be a real address.\n authorEmail: z.union([z.literal(\"\"), z.email({ pattern: EMAIL_PATTERN }).max(IDENTITY_FIELD_MAX_LENGTH)]),\n // A claim, not a fact: the handler keeps \"team\" only when the access\n // policy vouches for the caller.\n authorRole: z.enum(COMMENT_AUTHOR_ROLES).default(\"client\"),\n clientId: clientIdSchema,\n});\n\nexport const commentDeleteSchema = z.object({\n projectName: z.string().min(1).max(PROJECT_NAME_MAX_LENGTH),\n feedbackId: z.string().min(1).max(200),\n commentId: z.string().min(1).max(200),\n});\n\nexport const feedbackPatchSchema = z.object({\n id: z.string().min(1),\n projectName: z.string().min(1).max(PROJECT_NAME_MAX_LENGTH),\n status: z.enum(FEEDBACK_STATUSES),\n});\n\nexport const feedbackDeleteSchema = z.union([\n z.object({ id: z.string().min(1), projectName: z.string().min(1).max(PROJECT_NAME_MAX_LENGTH) }),\n z.object({ projectName: z.string().min(1).max(PROJECT_NAME_MAX_LENGTH), deleteAll: z.literal(true) }),\n]);\n\nexport const getQuerySchema = z.object({\n projectName: z.string().min(1).max(PROJECT_NAME_MAX_LENGTH),\n page: z.coerce.number().int().min(1).default(1),\n limit: z.coerce.number().int().min(1).max(100).default(50),\n type: z.enum(FEEDBACK_TYPES).optional(),\n status: z.enum(FEEDBACK_STATUSES).optional(),\n // Bucket status filter serialized as CSV over the wire (e.g.\n // `statuses=open,in_progress`). Non-empty strings are split before each value\n // is validated against the known statuses; `statuses` wins over the exact\n // `status` filter downstream. Capped at 4 — the number of known statuses.\n statuses: z\n .preprocess(\n (val) => (typeof val === \"string\" && val.length > 0 ? val.split(\",\") : val),\n boundedArray(z.enum(FEEDBACK_STATUSES), 4),\n )\n .optional(),\n search: z.string().max(200).optional(),\n // Page scope filters — used by the panel's \"this page / this type\" controls\n url: z.string().max(2000).optional(),\n urlPattern: z.string().max(2000).optional(),\n});\n\n// ---------------------------------------------------------------------------\n// Explicit public interfaces — decoupled from Zod to keep .d.ts clean.\n//\n// The create payloads need no local interface at all: the schemas validate\n// core's `FeedbackPayload` / `CommentPayload` / `CommentDeletePayload` wire\n// shapes, and the compile-time locks below keep them identical. Only the wire shapes that exist solely at this HTTP\n// boundary (PATCH / DELETE / GET query) are declared here.\n// ---------------------------------------------------------------------------\n\nexport interface FeedbackPatchInput {\n id: string;\n projectName: string;\n status: FeedbackStatus;\n}\n\nexport interface FeedbackDeleteSingle {\n id: string;\n projectName: string;\n}\n\nexport interface FeedbackDeleteAll {\n projectName: string;\n deleteAll: true;\n}\n\nexport type FeedbackDeleteInput = FeedbackDeleteSingle | FeedbackDeleteAll;\n\nexport interface GetQueryInput {\n projectName: string;\n /** Set to 1 by schema default when omitted from raw input. */\n page: number;\n /** Set to 50 by schema default when omitted from raw input. */\n limit: number;\n type?: FeedbackType | undefined;\n status?: FeedbackStatus | undefined;\n statuses?: FeedbackStatus[] | undefined;\n search?: string | undefined;\n url?: string | undefined;\n urlPattern?: string | undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Type-level locks: the Zod schemas validate exactly the shapes the rest of\n// the monorepo speaks.\n//\n// The create schema is locked against core's `FeedbackPayload` — the actual\n// wire contract the widget sends — so schema drift from the real payload is\n// a compile error, not a runtime 400. The boundary-only shapes (PATCH /\n// DELETE / GET query) are locked against their local interfaces the same\n// way. `Prettify<>` flattens the inferred shape so the equality check\n// compares clean object types rather than intersection chains.\n// ---------------------------------------------------------------------------\n\nconst _createSchemaMatchesPayload: AssertEqual<\n Prettify<zod.z.infer<typeof feedbackCreateSchema>>,\n FeedbackPayload\n> = true;\nvoid _createSchemaMatchesPayload;\nconst _commentSchemaMatchesPayload: AssertEqual<\n Prettify<zod.z.infer<typeof commentCreateSchema>>,\n CommentPayload\n> = true;\nvoid _commentSchemaMatchesPayload;\nconst _commentDeleteSchemaMatchesPayload: AssertEqual<\n Prettify<zod.z.infer<typeof commentDeleteSchema>>,\n CommentDeletePayload\n> = true;\nvoid _commentDeleteSchemaMatchesPayload;\nconst _patchSchemaMatchesInput: AssertEqual<\n Prettify<zod.z.infer<typeof feedbackPatchSchema>>,\n FeedbackPatchInput\n> = true;\nvoid _patchSchemaMatchesInput;\nconst _deleteSchemaMatchesInput: AssertEqual<\n Prettify<zod.z.infer<typeof feedbackDeleteSchema>>,\n FeedbackDeleteInput\n> = true;\nvoid _deleteSchemaMatchesInput;\nconst _querySchemaMatchesInput: AssertEqual<Prettify<zod.z.infer<typeof getQuerySchema>>, GetQueryInput> = true;\nvoid _querySchemaMatchesInput;\n\n/** Single validation issue extracted from a `ZodError`. */\nexport interface ValidationIssue {\n field: string;\n message: string;\n}\n\n/**\n * Map Zod errors to a flat array of `{ field, message }` objects — the first\n * `MAX_VALIDATION_ISSUES` of them.\n * Safe: does not leak input values or schema structure.\n */\nexport function formatValidationErrors(error: zod.z.ZodError): ValidationIssue[] {\n // Zod 4 types `issue.path` as PropertyKey[] (symbols possible in theory);\n // String() keeps the join total instead of throwing on non-string keys.\n return error.issues.slice(0, MAX_VALIDATION_ISSUES).map((issue) => ({\n field: issue.path.map(String).join(\".\"),\n message: issue.message,\n }));\n}\n","import {\n type CommentAuthorRole,\n isStoreLimit,\n isStoreNotFound,\n isStoreValueTooLong,\n type SitepingStore,\n} from \"@siteping/core\";\nimport { ERROR_MESSAGES } from \"../constants.js\";\nimport type { Notify } from \"../notify.js\";\nimport type { SitepingCreatedComment, SitepingHandlerBaseOptions, SitepingLifecycleHooks } from \"../options.js\";\nimport type { Pipeline, Scope } from \"../pipeline.js\";\nimport { commentCreateSchema } from \"../validation.js\";\n\ninterface CreateCommentDependencies<Principal> {\n store: SitepingStore;\n pipeline: Pipeline<Principal>;\n notify: Notify<Principal>;\n beforeComment: SitepingHandlerBaseOptions<Principal>[\"beforeComment\"];\n onCommentCreated: SitepingLifecycleHooks<Principal>[\"onCommentCreated\"];\n}\n\n/** `POST` with a `feedbackId` — add a comment to that feedback's thread, idempotent on `clientId`. */\nexport function createCommentOperation<Principal>({\n store,\n pipeline,\n notify,\n beforeComment,\n onCommentCreated,\n}: CreateCommentDependencies<Principal>) {\n return async (scope: Scope<Principal>, body: unknown): Promise<Response> => {\n const payload = pipeline.validate(scope, commentCreateSchema, body);\n if (!payload.ok) return payload.response;\n\n try {\n const { projectName, feedbackId, authorRole, ...comment } = beforeComment\n ? await beforeComment(payload.value, scope.context)\n : payload.value;\n const refusal = await pipeline.authorize(scope, { action: \"createComment\", projectName, feedbackId });\n if (refusal) return refusal;\n // 501, not 404: the thread may well exist — this store keeps no comments.\n if (!store.addComment) return pipeline.error(scope, 501, ERROR_MESSAGES.commentsUnsupported);\n\n // Cross-project guard — see the PATCH operation.\n if (store.verifyProjectOwnership && !(await store.verifyProjectOwnership(feedbackId, projectName))) {\n return pipeline.error(scope, 404, ERROR_MESSAGES.feedbackNotFound);\n }\n\n // POST is typically public so the widget can reply from a visitor's\n // browser: the role a request claims is kept only when the access\n // policy vouches for the caller.\n const role: CommentAuthorRole =\n authorRole === \"team\" && (await pipeline.canCommentAsTeam(scope, projectName)) ? \"team\" : \"client\";\n const input = { ...comment, authorRole: role };\n // Only a store that reports its inserts can tell a new reply from a\n // retried one: without `addCommentIfAbsent`, no reply counts as created\n // (the handler refuses to start with comment notifications then).\n const { comment: stored, created } = store.addCommentIfAbsent\n ? await store.addCommentIfAbsent(feedbackId, input)\n : { comment: await store.addComment(feedbackId, input), created: false };\n\n // A clientId is unique across every thread, so a replay that resolves to\n // another feedback's comment is a boundary violation, not a dedup.\n if (stored.feedbackId !== feedbackId) {\n return pipeline.error(scope, 409, ERROR_MESSAGES.clientIdUsedByAnotherFeedback);\n }\n if (created) {\n const reply: SitepingCreatedComment = { ...stored, projectName };\n notify(scope, { event: \"comment.created\", comment: reply });\n if (onCommentCreated) {\n await pipeline.runHook(scope, \"onCommentCreated\", { feedbackId, projectName, commentId: stored.id }, () =>\n onCommentCreated(reply, scope.context),\n );\n }\n }\n return pipeline.json(scope, await pipeline.presentCreatedComment(scope, stored, projectName), { status: 201 });\n } catch (error) {\n if (isStoreNotFound(error)) return pipeline.error(scope, 404, ERROR_MESSAGES.feedbackNotFound);\n if (isStoreLimit(error)) return pipeline.error(scope, 409, ERROR_MESSAGES.tooManyComments);\n if (isStoreValueTooLong(error)) return pipeline.refuseTooLong(scope, error);\n return pipeline.fail(scope, \"[siteping] Failed to add comment\", error);\n }\n };\n}\n","import {\n type FeedbackCreateInput,\n type FeedbackPayload,\n type FeedbackRecord,\n flattenAnnotation,\n isStoreDuplicate,\n isStoreValueTooLong,\n type SitepingStore,\n} from \"@siteping/core\";\nimport { ERROR_MESSAGES, MAX_ANNOTATIONS_PER_FEEDBACK } from \"../constants.js\";\nimport type { Notify } from \"../notify.js\";\nimport type { SitepingHandlerBaseOptions, SitepingLifecycleHooks } from \"../options.js\";\nimport type { Pipeline, Scope } from \"../pipeline.js\";\nimport { feedbackCreateSchema } from \"../validation.js\";\n\ninterface CreateFeedbackDependencies<Principal> {\n store: SitepingStore;\n pipeline: Pipeline<Principal>;\n notify: Notify<Principal>;\n beforeCreate: SitepingHandlerBaseOptions<Principal>[\"beforeCreate\"];\n onCreated: SitepingLifecycleHooks<Principal>[\"onCreated\"];\n}\n\nconst FAILED_TO_CREATE = \"[siteping] Failed to create feedback\";\n\n/** Outcome of one create: `inserted` is false for a replay. */\ninterface CreateOutcome {\n feedback: FeedbackRecord;\n inserted: boolean;\n}\n\n/** Store input from the validated payload — the store never sees wire-only shapes. */\nfunction toCreateInput(data: FeedbackPayload): FeedbackCreateInput {\n return {\n projectName: data.projectName,\n type: data.type,\n message: data.message,\n status: \"open\",\n url: data.url,\n urlPattern: data.urlPattern ?? null,\n viewport: data.viewport,\n userAgent: data.userAgent,\n authorName: data.authorName,\n authorEmail: data.authorEmail,\n clientId: data.clientId,\n annotations: data.annotations.map(flattenAnnotation),\n screenshotDataUrl: data.screenshotDataUrl ?? null,\n screenshotRegion: data.screenshotRegion ?? null,\n diagnostics: data.diagnostics ?? null,\n };\n}\n\n/** `POST` — create a feedback, idempotent on `clientId`. */\nexport function createFeedbackOperation<Principal>({\n store,\n pipeline,\n notify,\n beforeCreate,\n onCreated,\n}: CreateFeedbackDependencies<Principal>) {\n /**\n * Creates in flight, keyed by clientId. The widget aborts an attempt after\n * 10 s and resends the same payload, so a retry can reach the server while\n * the first attempt is still being processed. Without\n * `createFeedbackIfAbsent`, both requests would pass the replay check and\n * race to insert: the store throws `StoreDuplicateError` for the loser, but\n * only after it ran its own upload. A request whose clientId is in flight\n * shares that outcome instead, and never runs a second insert or upload.\n * Scoped to this handler instance: across processes, the store decides —\n * `createFeedbackIfAbsent`, or the duplicate error (the path below).\n */\n const inflightCreates = new Map<string, Promise<CreateOutcome>>();\n\n /** Replay check + insert for one validated input. */\n async function createOrReplay(input: FeedbackCreateInput): Promise<CreateOutcome> {\n // The store reports its own inserts: it arbitrates replays and races on\n // the clientId atomically, across handler instances and processes too.\n if (store.createFeedbackIfAbsent) {\n const { feedback, created } = await store.createFeedbackIfAbsent(input);\n return { feedback, inserted: created };\n }\n\n // Otherwise, replay detection up front, so a replayed submission costs a\n // lookup rather than a refused insert; a replay that lands in between\n // surfaces as `StoreDuplicateError`, recovered below.\n const replayed = await store.findByClientId(input.clientId);\n if (replayed) return { feedback: replayed, inserted: false };\n\n return { feedback: await store.createFeedback(input), inserted: true };\n }\n\n return async (scope: Scope<Principal>, body: unknown): Promise<Response> => {\n const payload = pipeline.validate(scope, feedbackCreateSchema, body);\n if (!payload.ok) return payload.response;\n\n // Defense-in-depth: enforce annotation limit at handler level in addition to schema validation\n if (payload.value.annotations.length > MAX_ANNOTATIONS_PER_FEEDBACK) {\n return pipeline.error(scope, 400, ERROR_MESSAGES.tooManyAnnotations);\n }\n\n let input: FeedbackCreateInput;\n try {\n const validated = toCreateInput(payload.value);\n input = beforeCreate ? await beforeCreate(validated, scope.context) : validated;\n const refusal = await pipeline.authorize(scope, { action: \"create\", projectName: input.projectName });\n if (refusal) return refusal;\n } catch (error) {\n return pipeline.fail(scope, FAILED_TO_CREATE, error);\n }\n\n // Join an in-flight create of this clientId, or start one. The lookup\n // and the registration run in one synchronous turn, so two overlapping\n // requests can never both miss.\n let pending = inflightCreates.get(input.clientId);\n const owner = pending === undefined;\n if (!pending) {\n pending = createOrReplay(input);\n inflightCreates.set(input.clientId, pending);\n }\n\n let outcome: CreateOutcome;\n try {\n outcome = await pending;\n } catch (error) {\n if (isStoreValueTooLong(error)) return pipeline.refuseTooLong(scope, error);\n // Duplicate clientId: the same clientId landed between the replay check\n // above and the insert. The presenter still owns the record.\n // A failing lookup falls through to the JSON 500 — this catch must not\n // throw, or the request loses its response and CORS headers.\n let existing: FeedbackRecord | null = null;\n if (isStoreDuplicate(error)) {\n try {\n existing = await store.findByClientId(input.clientId);\n } catch (lookupError) {\n pipeline.logError(scope, \"[siteping] Failed to look up the duplicate clientId\", {\n error: lookupError,\n projectName: input.projectName,\n });\n }\n }\n if (!existing) return pipeline.fail(scope, FAILED_TO_CREATE, error);\n outcome = { feedback: existing, inserted: false };\n } finally {\n if (owner) inflightCreates.delete(input.clientId);\n }\n\n const { feedback, inserted } = outcome;\n const subject = { feedbackId: feedback.id, projectName: feedback.projectName };\n try {\n // Creation side effects run once per insert: never for a replay, and\n // never for a request that joined another one's in-flight create.\n if (owner && inserted && feedback.projectName === input.projectName) {\n notify(scope, { event: \"feedback.created\", feedback });\n if (onCreated) await pipeline.runHook(scope, \"onCreated\", subject, () => onCreated(feedback, scope.context));\n }\n\n // A clientId is unique across the whole store, so a replay that\n // resolves to another project's record is a boundary violation, not a\n // dedup: refuse it rather than hand that record (email included) to a\n // request scoped to a different project.\n if (feedback.projectName !== input.projectName) {\n return pipeline.error(scope, 409, ERROR_MESSAGES.clientIdUsedByAnotherProject);\n }\n return pipeline.json(scope, await pipeline.presentCreated(scope, feedback), { status: 201 });\n } catch (error) {\n return pipeline.fail(scope, FAILED_TO_CREATE, error);\n }\n };\n}\n","import { isStoreNotFound, type SitepingStore } from \"@siteping/core\";\nimport { ERROR_MESSAGES } from \"../constants.js\";\nimport type { Pipeline, Scope } from \"../pipeline.js\";\nimport { commentDeleteSchema } from \"../validation.js\";\n\ninterface DeleteCommentDependencies<Principal> {\n store: SitepingStore;\n pipeline: Pipeline<Principal>;\n}\n\n/** `DELETE` with a `commentId` — remove that comment from a feedback's thread. */\nexport function deleteCommentOperation<Principal>({ store, pipeline }: DeleteCommentDependencies<Principal>) {\n return async (scope: Scope<Principal>, body: unknown): Promise<Response> => {\n const payload = pipeline.validate(scope, commentDeleteSchema, body);\n if (!payload.ok) return payload.response;\n const { projectName, feedbackId, commentId } = payload.value;\n\n try {\n const refusal = await pipeline.authorize(scope, { action: \"deleteComment\", projectName, feedbackId, commentId });\n if (refusal) return refusal;\n if (!store.deleteComment) return pipeline.error(scope, 501, ERROR_MESSAGES.commentsUnsupported);\n\n // Cross-project guard — see the PATCH operation. The store then scopes\n // the delete to this feedback's thread.\n if (store.verifyProjectOwnership && !(await store.verifyProjectOwnership(feedbackId, projectName))) {\n return pipeline.error(scope, 404, ERROR_MESSAGES.feedbackNotFound);\n }\n\n await store.deleteComment(feedbackId, commentId);\n return pipeline.json(scope, { deleted: true });\n } catch (error) {\n if (isStoreNotFound(error)) return pipeline.error(scope, 404, ERROR_MESSAGES.commentNotFound);\n return pipeline.fail(scope, \"[siteping] Failed to delete comment\", error);\n }\n };\n}\n","import { isStoreNotFound, type SitepingStore } from \"@siteping/core\";\nimport { ERROR_MESSAGES } from \"../constants.js\";\nimport type { Notify } from \"../notify.js\";\nimport type { SitepingDeletionTarget, SitepingLifecycleHooks } from \"../options.js\";\nimport type { Pipeline, Scope } from \"../pipeline.js\";\nimport { feedbackDeleteSchema } from \"../validation.js\";\n\ninterface DeleteFeedbackDependencies<Principal> {\n store: SitepingStore;\n pipeline: Pipeline<Principal>;\n notify: Notify<Principal>;\n onDeleting: SitepingLifecycleHooks<Principal>[\"onDeleting\"];\n onDeleted: SitepingLifecycleHooks<Principal>[\"onDeleted\"];\n}\n\n/** `DELETE` — remove one feedback, or every feedback of a project (`deleteAll`). */\nexport function deleteFeedbackOperation<Principal>({\n store,\n pipeline,\n notify,\n onDeleting,\n onDeleted,\n}: DeleteFeedbackDependencies<Principal>) {\n return async (scope: Scope<Principal>, body: unknown): Promise<Response> => {\n const payload = pipeline.validate(scope, feedbackDeleteSchema, body);\n if (!payload.ok) return payload.response;\n const target: SitepingDeletionTarget =\n \"deleteAll\" in payload.value\n ? { kind: \"project\", projectName: payload.value.projectName }\n : { kind: \"single\", id: payload.value.id, projectName: payload.value.projectName };\n\n try {\n const refusal = await pipeline.authorize(\n scope,\n target.kind === \"project\"\n ? { action: \"deleteAll\", projectName: target.projectName }\n : { action: \"delete\", projectName: target.projectName, feedbackId: target.id },\n );\n if (refusal) return refusal;\n\n // Cross-project guard — see the PATCH operation.\n if (\n target.kind === \"single\" &&\n store.verifyProjectOwnership &&\n !(await store.verifyProjectOwnership(target.id, target.projectName))\n ) {\n return pipeline.error(scope, 404, ERROR_MESSAGES.feedbackNotFound);\n }\n\n if (onDeleting) {\n try {\n await onDeleting(target, scope.context);\n } catch (error) {\n pipeline.logError(scope, \"[siteping] Hook onDeleting aborted the deletion\", { error, target });\n return pipeline.error(scope, 502, ERROR_MESSAGES.deletionAborted);\n }\n }\n\n if (target.kind === \"project\") await store.deleteAllFeedbacks(target.projectName);\n else await store.deleteFeedback(target.id);\n notify(scope, { event: \"feedback.deleted\", target });\n if (onDeleted) await pipeline.runHook(scope, \"onDeleted\", { target }, () => onDeleted(target, scope.context));\n return pipeline.json(scope, { deleted: true });\n } catch (error) {\n if (isStoreNotFound(error)) return pipeline.error(scope, 404, ERROR_MESSAGES.feedbackNotFound);\n return pipeline.fail(scope, \"[siteping] Failed to delete feedback\", error);\n }\n };\n}\n","import {\n type FeedbackListPermissions,\n SITEPING_PROTOCOL_VERSION,\n type SitepingCapabilities,\n type SitepingStore,\n} from \"@siteping/core\";\nimport { LIST_QUERY_KEYS } from \"../constants.js\";\nimport type { Pipeline, WireFeedback } from \"../pipeline.js\";\nimport { getQuerySchema } from \"../validation.js\";\n\n/** A list response as it goes on the wire. */\nexport interface WireFeedbackList {\n feedbacks: WireFeedback[];\n total: number;\n /** `SITEPING_PROTOCOL_VERSION`: what a client may rely on from this server. */\n protocol: number;\n /** Every capability, `deleteComments` included. */\n capabilities: { [K in keyof SitepingCapabilities]-?: Exclude<SitepingCapabilities[K], undefined> };\n permissions: FeedbackListPermissions;\n}\n\ninterface ListFeedbacksDependencies<Principal> {\n store: SitepingStore;\n pipeline: Pipeline<Principal>;\n}\n\n/** Only the query parameters the endpoint understands, as raw strings. */\nfunction readListQuery(request: Request): Record<string, string> {\n const searchParams = new URL(request.url).searchParams;\n const rawQuery: Record<string, string> = {};\n for (const key of LIST_QUERY_KEYS) {\n const value = searchParams.get(key);\n if (value !== null) rawQuery[key] = value;\n }\n return rawQuery;\n}\n\n/** `GET` — a paginated, filtered page of a project's feedbacks. */\nexport function listFeedbacksOperation<Principal>({ store, pipeline }: ListFeedbacksDependencies<Principal>) {\n return async (request: Request): Promise<Response> => {\n const entry = await pipeline.enter(request, \"GET\");\n if (!entry.ok) return entry.response;\n const scope = entry.value;\n\n const query = pipeline.validate(scope, getQuerySchema, readListQuery(request));\n if (!query.ok) return query.response;\n\n try {\n const { projectName } = query.value;\n const refusal = await pipeline.authorize(scope, { action: \"list\", projectName });\n if (refusal) return refusal;\n\n // GET can be public (no apiKey, or \"GET\" in publicEndpoints for widget\n // hosts) — the scope redacts author emails unless the requester may read them.\n const page = await store.getFeedbacks(query.value);\n const [feedbacks, canDeleteAll] = await Promise.all([\n Promise.all(page.feedbacks.map((feedback) => pipeline.present(scope, feedback))),\n pipeline.may(scope, \"DELETE\", { action: \"deleteAll\", projectName }),\n ]);\n return pipeline.json(\n scope,\n {\n ...page,\n feedbacks,\n // Lets clients rely on what a protocol version added instead of probing for fields.\n protocol: SITEPING_PROTOCOL_VERSION,\n // Lets clients hide their comment composer, and delete buttons, up front instead of meeting a 501.\n capabilities: {\n comments: store.addComment !== undefined,\n deleteComments: store.deleteComment !== undefined,\n },\n // And the actions this requester would be refused.\n permissions: { canDeleteAll },\n } satisfies WireFeedbackList,\n { headers: { \"Cache-Control\": pipeline.listCacheControl } },\n );\n } catch (error) {\n return pipeline.fail(scope, \"[siteping] Failed to fetch feedbacks\", error);\n }\n };\n}\n","import { isStoreNotFound, type SitepingStore, toFeedbackUpdate } from \"@siteping/core\";\nimport { ERROR_MESSAGES } from \"../constants.js\";\nimport type { Notify } from \"../notify.js\";\nimport type { SitepingLifecycleHooks } from \"../options.js\";\nimport type { Pipeline } from \"../pipeline.js\";\nimport { feedbackPatchSchema } from \"../validation.js\";\n\ninterface UpdateFeedbackDependencies<Principal> {\n store: SitepingStore;\n pipeline: Pipeline<Principal>;\n notify: Notify<Principal>;\n onUpdated: SitepingLifecycleHooks<Principal>[\"onUpdated\"];\n}\n\n/** `PATCH` — change a feedback's status. */\nexport function updateFeedbackOperation<Principal>({\n store,\n pipeline,\n notify,\n onUpdated,\n}: UpdateFeedbackDependencies<Principal>) {\n return async (request: Request): Promise<Response> => {\n const entry = await pipeline.enter(request, \"PATCH\");\n if (!entry.ok) return entry.response;\n const scope = entry.value;\n\n const payload = await pipeline.readBody(scope, feedbackPatchSchema);\n if (!payload.ok) return payload.response;\n const { id, projectName, status } = payload.value;\n\n try {\n const refusal = await pipeline.authorize(scope, { action: \"update\", projectName, feedbackId: id });\n if (refusal) return refusal;\n\n // Verify project ownership before updating. Any store implementing\n // the optional SitepingStore.verifyProjectOwnership gets the check;\n // duck-typing instead of `instanceof` keeps it bundling-safe and\n // open to third-party adapters. The handler refuses to start with a\n // custom `authorize` over a store without it.\n if (store.verifyProjectOwnership && !(await store.verifyProjectOwnership(id, projectName))) {\n return pipeline.error(scope, 404, ERROR_MESSAGES.feedbackNotFound);\n }\n\n // resolvedAt is the CLOSURE timestamp — set when the feedback enters\n // a terminal status (resolved / wont_fix), cleared otherwise. The\n // derivation lives here at the edge; stores persist what they're given.\n const feedback = await store.updateFeedback(id, toFeedbackUpdate(status));\n // Every successful PATCH, the status it already had included: the store\n // keeps no previous status to compare with.\n notify(scope, { event: \"feedback.status_changed\", feedback });\n if (onUpdated) {\n const subject = { feedbackId: feedback.id, projectName: feedback.projectName };\n await pipeline.runHook(scope, \"onUpdated\", subject, () => onUpdated(feedback, scope.context));\n }\n\n // PATCH can be made public via publicEndpoints / requireAuthForDestructive:\n // false — the scope keeps the author's email out of the update response.\n return pipeline.json(scope, await pipeline.present(scope, feedback));\n } catch (error) {\n if (isStoreNotFound(error)) return pipeline.error(scope, 404, ERROR_MESSAGES.feedbackNotFound);\n return pipeline.fail(scope, \"[siteping] Failed to update feedback\", error);\n }\n };\n}\n","import type { SitepingHttpMethod } from \"./options.js\";\n\n// Cross-site request forgery guards of the mutating methods, for policies\n// that may authenticate with cookies. CORS only hides a response from a\n// foreign page; it never stops a \"simple\" request (a credentialed POST with\n// `Content-Type: text/plain`) from reaching the handler.\n\n/** Longest refused `Origin` written to the log (untrusted input). */\nconst MAX_LOGGED_ORIGIN_LENGTH = 128;\n\n/** Why a mutation is refused: a foreign origin (403) or a non-JSON body (415). */\nexport type CsrfRefusal = { status: 403; origin: string } | { status: 415 };\n\n/**\n * Why a mutation must be refused, or `null` to let it through.\n *\n * - **Origin** (403), only with an `allowedOrigins` list: listed origins and\n * the endpoint's own pass; anything else — the opaque `null` origin of\n * sandboxed frames included — is refused. A request without `Origin`\n * passes: browsers always send it on cross-origin POST/PATCH/DELETE,\n * server-to-server calls and curl do not.\n * - **Content type** (415): only `application/json` (parameters such as\n * `charset` allowed). It is not CORS-safelisted, so a cross-origin browser\n * must pass a preflight first, which fails without CORS headers.\n */\nexport function csrfRefusal(\n request: Request,\n method: SitepingHttpMethod,\n allowedOrigins: ReadonlyArray<string> | undefined,\n): CsrfRefusal | null {\n if (method !== \"POST\" && method !== \"PATCH\" && method !== \"DELETE\") return null;\n\n const origin = request.headers.get(\"Origin\");\n if (allowedOrigins && origin !== null && !allowedOrigins.includes(origin) && origin !== new URL(request.url).origin) {\n // The refused origin is logged: strip control characters and truncate it.\n // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping control characters is the point\n return { status: 403, origin: origin.replace(/[\\u0000-\\u001f\\u007f]/g, \"\").slice(0, MAX_LOGGED_ORIGIN_LENGTH) };\n }\n\n const [mediaType = \"\"] = (request.headers.get(\"Content-Type\") ?? \"\").split(\";\");\n if (mediaType.trim().toLowerCase() !== \"application/json\") return { status: 415 };\n return null;\n}\n","import type {\n CommentPermissions,\n CommentRecord,\n FeedbackPermissions,\n FeedbackRecord,\n SitepingLogger,\n} from \"@siteping/core\";\nimport type { AccessGate, AccessOutcome } from \"./access.js\";\nimport { DRY_RUN_CONCURRENCY, ERROR_MESSAGES } from \"./constants.js\";\nimport { buildCorsHeaders, type CorsHeaders, withCors } from \"./cors.js\";\nimport { csrfRefusal } from \"./csrf.js\";\nimport type {\n SitepingAuthorizationContext,\n SitepingHandlerBaseOptions,\n SitepingHttpMethod,\n SitepingRequestContext,\n} from \"./options.js\";\nimport { formatValidationErrors } from \"./validation.js\";\n\n/** A request that passed the access gate — what every operation works with. */\nexport interface Scope<Principal> {\n context: SitepingRequestContext<Principal>;\n corsHeaders: CorsHeaders;\n /**\n * Whether responses about `projectName` may include `authorEmail` — the\n * policy is asked once per project and request, the answer reused.\n */\n canReadAuthorEmail(projectName: string): Promise<boolean>;\n}\n\n/** Either a value to continue with, or the response to send right away. */\nexport type Step<Value> = { ok: true; value: Value } | { ok: false; response: Response };\n\n/** The slice of a zod schema the pipeline validates with. */\ninterface Schema<Output> {\n safeParse(\n input: unknown,\n ): { success: true; data: Output } | { success: false; error: Parameters<typeof formatValidationErrors>[0] };\n}\n\ninterface PipelineDependencies<Principal> {\n gate: AccessGate<Principal>;\n allowedOrigins: ReadonlyArray<string> | undefined;\n logger: Pick<SitepingLogger, \"error\">;\n describeError: SitepingHandlerBaseOptions<Principal>[\"describeError\"];\n presentFeedback: SitepingHandlerBaseOptions<Principal>[\"presentFeedback\"];\n maxBodyBytes: number;\n}\n\n/** A comment as it goes on the wire — the requester's permissions always present. */\nexport type WireComment = Omit<CommentRecord, \"clientId\"> & { permissions: CommentPermissions };\n\n/** A feedback as it goes on the wire — its thread and the requester's permissions always present. */\nexport type WireFeedback = Omit<FeedbackRecord, \"clientId\" | \"comments\"> & {\n comments: WireComment[];\n permissions: FeedbackPermissions;\n};\n\n/**\n * Serialize a comment for the HTTP wire — `clientId` stripped and\n * `authorEmail` blanked on the same terms as a feedback's (see below).\n */\nfunction toWireComment(comment: CommentRecord, includeEmail: boolean): Omit<WireComment, \"permissions\"> {\n const { clientId: _clientId, ...wire } = comment;\n return includeEmail ? wire : { ...wire, authorEmail: \"\" };\n}\n\n/**\n * Serialize a feedback record for the HTTP wire (edge DTO — stores return raw\n * records, redaction happens here).\n *\n * `clientId` is always stripped: it is a browser-local dedup secret, and the\n * POST dedup path returns the full existing record for whoever presents it —\n * exposing it via responses would turn that into a record-theft oracle.\n * `authorEmail` is PII: blanked unless the requester may read it — on the\n * feedback per `includeEmail`, on its comments per `includeCommentEmails`.\n * A store without comments leaves the thread out: it goes out as `[]`.\n * Never mutates the input — webhooks receive the same record object.\n */\nfunction toWireFeedback(\n feedback: FeedbackRecord,\n includeEmail: boolean,\n includeCommentEmails: boolean,\n): Omit<WireFeedback, \"permissions\" | \"comments\"> & { comments: Omit<WireComment, \"permissions\">[] } {\n const { clientId: _clientId, comments, ...wire } = feedback;\n return {\n ...wire,\n ...(includeEmail ? {} : { authorEmail: \"\" }),\n comments: (comments ?? []).map((comment) => toWireComment(comment, includeCommentEmails)),\n };\n}\n\n/** Run tasks at most `size` at a time; a task that finishes hands its slot to the next one waiting. */\nfunction createSlots(size: number): <Result>(task: () => Promise<Result>) => Promise<Result> {\n let running = 0;\n const waiting: Array<() => void> = [];\n return async (task) => {\n if (running < size) running += 1;\n else await new Promise<void>((resolve) => waiting.push(resolve));\n try {\n return await task();\n } finally {\n const next = waiting.shift();\n if (next) next();\n else running -= 1;\n }\n };\n}\n\n/**\n * A request body as text, or `null` past `maxBytes`: refused on its\n * `Content-Length` before any byte is read, and counted as it streams in\n * otherwise, so an oversized body is never held in memory whole.\n */\nasync function readText(request: Request, maxBytes: number): Promise<string | null> {\n if (Number(request.headers.get(\"Content-Length\")) > maxBytes) return null;\n if (!request.body) return \"\";\n const reader = request.body.getReader();\n const decoder = new TextDecoder();\n let text = \"\";\n let received = 0;\n for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {\n received += chunk.value.byteLength;\n if (received > maxBytes) {\n await reader.cancel().catch(() => {});\n return null;\n }\n text += decoder.decode(chunk.value, { stream: true });\n }\n return text + decoder.decode();\n}\n\n/** `text` parsed as JSON, or `null` when it is not JSON. */\nfunction parseJson(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return null;\n }\n}\n\n/** Where a request failed, for the log: method and path — never the query, headers or body. */\nfunction requestContext(request: Request): { method: string; path: string } {\n return { method: request.method, path: new URL(request.url).pathname };\n}\n\n/**\n * The steps every operation shares — access check, parsing, authorization,\n * serialization, failure reporting — so each operation module only holds its\n * own logic. Every response carries the request's CORS headers.\n */\nexport function createPipeline<Principal>({\n gate,\n allowedOrigins,\n logger,\n describeError,\n presentFeedback,\n maxBodyBytes,\n}: PipelineDependencies<Principal>) {\n const json = (scope: Pick<Scope<Principal>, \"corsHeaders\">, body: unknown, init?: ResponseInit): Response =>\n withCors(Response.json(body, init), scope.corsHeaders);\n\n const error = (scope: Pick<Scope<Principal>, \"corsHeaders\">, status: number, message: string): Response =>\n json(scope, { error: message }, { status });\n\n /** Log an unexpected failure and answer a JSON 500 — with `describeError`'s hint when it has one. */\n const fail = (\n request: Request,\n scope: Pick<Scope<Principal>, \"corsHeaders\">,\n message: string,\n failure: unknown,\n ): Response => {\n logger.error(message, { error: failure, ...requestContext(request) });\n return error(scope, 500, describeError?.(failure) ?? ERROR_MESSAGES.internalServerError);\n };\n\n /**\n * Log a failure the response does not report, with what it concerns (a\n * record, a deletion target) and the request's method and path, so an\n * operator can tell which record missed its side effect.\n */\n const logError = (scope: Scope<Principal>, message: string, details: Record<string, unknown>): void => {\n logger.error(message, { ...details, ...requestContext(scope.context.request) });\n };\n\n const validate = <Output>(scope: Scope<Principal>, schema: Schema<Output>, input: unknown): Step<Output> => {\n const parsed = schema.safeParse(input);\n if (parsed.success) return { ok: true, value: parsed.data };\n return { ok: false, response: json(scope, { errors: formatValidationErrors(parsed.error) }, { status: 400 }) };\n };\n\n /** Read a JSON body of at most `maxBodyBytes`, not validated yet. */\n const readJson = async (scope: Scope<Principal>): Promise<Step<unknown>> => {\n // A body that cannot be read (already consumed, a broken stream) is no JSON.\n const text = await readText(scope.context.request, maxBodyBytes).catch(() => \"\");\n if (text === null) return { ok: false, response: error(scope, 413, ERROR_MESSAGES.bodyTooLarge) };\n const body = parseJson(text);\n if (!body) return { ok: false, response: error(scope, 400, ERROR_MESSAGES.invalidJson) };\n return { ok: true, value: body };\n };\n\n /** Requests whose failed dry run is logged already — one line per response, however many fail. */\n const loggedDryRunFailures = new WeakSet<Request>();\n /** Each request's dry-run slots — `DRY_RUN_CONCURRENCY` per response. */\n const dryRunSlots = new WeakMap<Request, ReturnType<typeof createSlots>>();\n\n /**\n * Whether this requester may do what `target` describes: the policy admits\n * the method it takes, and `authorize` allows it in a dry run — a few of a\n * response's at a time. A dry run that throws refuses: a permission only\n * hides a control, and the response it goes out with often answers a write\n * that already happened.\n */\n const may = async (\n scope: Scope<Principal>,\n method: SitepingHttpMethod,\n target: Omit<SitepingAuthorizationContext<Principal>, keyof SitepingRequestContext<Principal>>,\n ): Promise<boolean> => {\n const { request } = scope.context;\n const slots = dryRunSlots.get(request) ?? createSlots(DRY_RUN_CONCURRENCY);\n dryRunSlots.set(request, slots);\n try {\n return await slots(\n async () =>\n (await gate.admits(request, method)) && (await gate.authorize({ ...scope.context, ...target, dryRun: true })),\n );\n } catch (failure) {\n if (!loggedDryRunFailures.has(request)) {\n loggedDryRunFailures.add(request);\n logger.error(\"[siteping] authorize failed on a dry run\", {\n error: failure,\n action: target.action,\n ...requestContext(request),\n });\n }\n return false;\n }\n };\n\n /** What this requester may do with a record — `permissions` on the wire. */\n const permissions = async (scope: Scope<Principal>, feedback: FeedbackRecord): Promise<FeedbackPermissions> => {\n const target = { projectName: feedback.projectName, feedbackId: feedback.id };\n const [canChangeStatus, canDelete, canComment, canDeleteComment] = await Promise.all([\n may(scope, \"PATCH\", { action: \"update\", ...target }),\n may(scope, \"DELETE\", { action: \"delete\", ...target }),\n may(scope, \"POST\", { action: \"createComment\", ...target }),\n may(scope, \"DELETE\", { action: \"deleteComment\", ...target }),\n ]);\n return { canChangeStatus, canDelete, canComment, canDeleteComment };\n };\n\n /**\n * What this requester may do with one comment of a thread. Deleting any\n * reply of the thread covers this one; otherwise the policy is asked about\n * this comment by id — how a per-author rule grants it.\n */\n const commentPermissions = async (\n scope: Scope<Principal>,\n thread: { projectName: string; feedbackId: string },\n commentId: string,\n canDeleteThread: boolean,\n ): Promise<CommentPermissions> => ({\n canDelete: canDeleteThread || (await may(scope, \"DELETE\", { action: \"deleteComment\", ...thread, commentId })),\n });\n\n /**\n * Wire shape of a record for this requester (`presentFeedback`, then\n * redaction), with its permissions and its comments'. `echoEmail` keeps\n * the feedback's `authorEmail` whatever the requester may read.\n */\n const present = async (\n scope: Scope<Principal>,\n feedback: FeedbackRecord,\n echoEmail = false,\n ): Promise<WireFeedback> => {\n const canReadEmails = await scope.canReadAuthorEmail(feedback.projectName);\n const wire = toWireFeedback(\n presentFeedback ? presentFeedback(feedback, scope.context) : feedback,\n echoEmail || canReadEmails,\n canReadEmails,\n );\n const granted = await permissions(scope, feedback);\n const thread = { projectName: feedback.projectName, feedbackId: feedback.id };\n const comments = await Promise.all(\n wire.comments.map(async (comment) => ({\n ...comment,\n permissions: await commentPermissions(scope, thread, comment.id, granted.canDeleteComment),\n })),\n );\n return { ...wire, comments, permissions: granted };\n };\n\n return {\n json,\n error,\n validate,\n may,\n present,\n\n /**\n * Open a request: the CSRF guards when the policy needs them, then the\n * access gate. A throwing `access` callback (session store down…)\n * answers the logged 500 with CORS headers rather than rejecting, which\n * a browser would only see as an opaque network error.\n */\n async enter(request: Request, method: SitepingHttpMethod): Promise<Step<Scope<Principal>>> {\n const corsHeaders = buildCorsHeaders(request, allowedOrigins);\n if (gate.guardsMutations) {\n const refusal = csrfRefusal(request, method, allowedOrigins);\n if (refusal?.status === 403) {\n logger.error(\"[siteping] Refused a mutation from an origin outside allowedOrigins\", {\n origin: refusal.origin,\n ...requestContext(request),\n });\n return { ok: false, response: error({ corsHeaders }, 403, ERROR_MESSAGES.forbidden) };\n }\n if (refusal) return { ok: false, response: error({ corsHeaders }, 415, ERROR_MESSAGES.unsupportedMediaType) };\n }\n\n let outcome: AccessOutcome<Principal>;\n try {\n outcome = await gate.authenticate(request, method);\n } catch (failure) {\n return {\n ok: false,\n response: fail(request, { corsHeaders }, \"[siteping] Failed to authenticate request\", failure),\n };\n }\n if (!outcome.ok) return { ok: false, response: error({ corsHeaders }, outcome.status, outcome.error) };\n const context = { request, principal: outcome.principal };\n const emailAccess = new Map<string, Promise<boolean>>();\n return {\n ok: true,\n value: {\n context,\n corsHeaders,\n canReadAuthorEmail(projectName) {\n let answer = emailAccess.get(projectName);\n if (!answer) {\n answer = gate.canReadAuthorEmail(context, projectName);\n emailAccess.set(projectName, answer);\n }\n return answer;\n },\n },\n };\n },\n\n readJson,\n\n /** Read and validate a JSON body. */\n async readBody<Output>(scope: Scope<Principal>, schema: Schema<Output>): Promise<Step<Output>> {\n const body = await readJson(scope);\n return body.ok ? validate(scope, schema, body.value) : body;\n },\n\n /**\n * `null` when the policy allows the request, its 403 otherwise. An\n * allowed request also settles its email access for the project now,\n * before anything is written: a throwing `canReadAuthorEmail` answers\n * the operation's 500 with nothing stored.\n */\n async authorize(\n scope: Scope<Principal>,\n target: Omit<SitepingAuthorizationContext<Principal>, keyof SitepingRequestContext<Principal>>,\n ): Promise<Response | null> {\n if (!(await gate.authorize({ ...scope.context, ...target }))) return error(scope, 403, ERROR_MESSAGES.forbidden);\n await scope.canReadAuthorEmail(target.projectName);\n return null;\n },\n\n /**\n * Wire shape of a record answering a POST (fresh or replayed): the\n * requester's email permission, unless the policy echoes the email to\n * its submitter — who supplied the feedback's, never its thread's.\n */\n presentCreated(scope: Scope<Principal>, feedback: FeedbackRecord) {\n return present(scope, feedback, gate.echoesAuthorEmailOnCreate);\n },\n\n /**\n * Wire shape of a comment answering a POST, in `projectName`'s thread —\n * the same echo rule as {@link presentCreated}, with its permissions.\n */\n async presentCreatedComment(\n scope: Scope<Principal>,\n comment: CommentRecord,\n projectName: string,\n ): Promise<WireComment> {\n const thread = { projectName, feedbackId: comment.feedbackId };\n const [canReadEmails, canDeleteThread] = await Promise.all([\n scope.canReadAuthorEmail(projectName),\n may(scope, \"DELETE\", { action: \"deleteComment\", ...thread }),\n ]);\n return {\n ...toWireComment(comment, gate.echoesAuthorEmailOnCreate || canReadEmails),\n permissions: await commentPermissions(scope, thread, comment.id, canDeleteThread),\n };\n },\n\n /**\n * Whether this caller's comment in `projectName` keeps the `team` role it\n * asks for — see `AccessGate.canCommentAsTeam`.\n */\n async canCommentAsTeam(scope: Scope<Principal>, projectName: string): Promise<boolean> {\n return gate.canCommentAsTeam(scope.context, projectName, await scope.canReadAuthorEmail(projectName));\n },\n\n /**\n * Answer a value the store cannot hold with a 422, which a client does\n * not retry, and log it: the database's columns are narrower than what\n * the validation accepts, which only the operator can fix.\n */\n refuseTooLong(scope: Scope<Principal>, failure: unknown): Response {\n logError(scope, \"[siteping] A value is too long for the store\", { error: failure });\n return error(scope, 422, ERROR_MESSAGES.valueTooLong);\n },\n\n /** Log an unexpected failure of an operation and answer its JSON 500. */\n fail(scope: Scope<Principal>, message: string, failure: unknown): Response {\n return fail(scope.context.request, scope, message, failure);\n },\n\n logError,\n\n /** Run a lifecycle hook after a write; a failure is logged with `subject`, never surfaced — the write happened. */\n async runHook(\n scope: Scope<Principal>,\n name: string,\n subject: Record<string, unknown>,\n invoke: () => void | Promise<void>,\n ): Promise<void> {\n try {\n await invoke();\n } catch (failure) {\n logError(scope, `[siteping] Hook ${name} failed`, { error: failure, ...subject });\n }\n },\n\n /** The list response's `Cache-Control`. */\n listCacheControl: gate.listCacheControl,\n };\n}\n\nexport type Pipeline<Principal> = ReturnType<typeof createPipeline<Principal>>;\n","import { hasOwn, SitepingConfigError, type SitepingLogger } from \"@siteping/core\";\nimport { type AccessGate, createAccessGate, createApiKeyGate } from \"./access.js\";\nimport { DEFAULT_MAX_BODY_BYTES } from \"./constants.js\";\nimport { preflightResponse } from \"./cors.js\";\nimport { createNotifier } from \"./notify.js\";\nimport { createCommentOperation } from \"./operations/create-comment.js\";\nimport { createFeedbackOperation } from \"./operations/create-feedback.js\";\nimport { deleteCommentOperation } from \"./operations/delete-comment.js\";\nimport { deleteFeedbackOperation } from \"./operations/delete-feedback.js\";\nimport { listFeedbacksOperation } from \"./operations/list-feedbacks.js\";\nimport { updateFeedbackOperation } from \"./operations/update-feedback.js\";\nimport type {\n SitepingAccessHandlerOptions,\n SitepingApiKeyHandlerOptions,\n SitepingHandler,\n SitepingHandlerBaseOptions,\n SitepingHandlerOptions,\n SitepingHttpMethod,\n SitepingPrincipal,\n} from \"./options.js\";\nimport { createPipeline, type Pipeline, type Scope } from \"./pipeline.js\";\nimport { checkWebhookConfigs, subscribesTo, type WebhookConfig } from \"./webhooks.js\";\n\nconst consoleLogger: Pick<SitepingLogger, \"error\"> = {\n error(message, context) {\n console.error(message, context);\n },\n};\n\n/**\n * `logger`, made safe to call anywhere: a throw, or a rejection of what it\n * returns — unhandled, fatal in Node by default — falls back to `console.error`.\n */\nfunction safeLogger(logger: Pick<SitepingLogger, \"error\">): Pick<SitepingLogger, \"error\"> {\n return {\n error(message, context) {\n try {\n Promise.resolve(logger.error(message, context)).catch(() => consoleLogger.error(message, context));\n } catch {\n consoleLogger.error(message, context);\n }\n },\n };\n}\n\n/** An operation on a request that passed the access gate, with its JSON body. */\ntype BodyOperation<Principal> = (scope: Scope<Principal>, body: unknown) => Promise<Response>;\n\n/**\n * One method, two resources: a JSON body carrying `commentKey` targets a\n * comment, any other body a feedback — whose payloads never carry that key.\n */\nfunction routeByBody<Principal>(\n pipeline: Pipeline<Principal>,\n method: SitepingHttpMethod,\n commentKey: \"feedbackId\" | \"commentId\",\n comment: BodyOperation<Principal>,\n feedback: BodyOperation<Principal>,\n): (request: Request) => Promise<Response> {\n return async (request) => {\n const entry = await pipeline.enter(request, method);\n if (!entry.ok) return entry.response;\n const body = await pipeline.readJson(entry.value);\n if (!body.ok) return body.response;\n return (hasOwn(body.value, commentKey) ? comment : feedback)(entry.value, body.value);\n };\n}\n\n/**\n * Create the SitePing HTTP API over any `SitepingStore`, using only the Fetch\n * API (`Request` → `Response`): one handler per method, to mount in Next.js\n * route handlers, Hono, Remix, SvelteKit, Bun, Deno or edge workers.\n *\n * **Rate limiting** is not handled by this library. Apply rate limiting at the\n * framework or reverse-proxy level (e.g. Next.js middleware, Nginx, Cloudflare).\n * The POST endpoint in particular should be rate-limited to prevent abuse, since\n * the widget typically calls it from unauthenticated browser contexts.\n *\n * Access is either the built-in `apiKey` policy or your own `access` policy\n * (sessions, JWTs, roles…) — see `SitepingAccessHandlerOptions`.\n *\n * @throws Error without a `store`; with a `maxBodyBytes` that is not a\n * positive integer, or a webhook `timeoutMs` no timer holds; in production\n * without `apiKey` (see `requireAuthForDestructive`); or with\n * `access.authorize` over a store without `verifyProjectOwnership`, since\n * PATCH/DELETE could then reach a record of a project the caller is not\n * authorized for.\n *\n * @example Next.js App Router — `app/api/siteping/route.ts`\n * ```ts\n * import { createSitepingHandler } from '@siteping/server'\n * import { store } from '@/lib/siteping-store'\n *\n * export const { GET, POST, PATCH, DELETE, OPTIONS } = createSitepingHandler({\n * store,\n * apiKey: process.env.SITEPING_API_KEY,\n * })\n * ```\n */\nexport function createSitepingHandler<Principal extends SitepingPrincipal>(\n options: SitepingAccessHandlerOptions<Principal>,\n): SitepingHandler;\nexport function createSitepingHandler(options: SitepingApiKeyHandlerOptions): SitepingHandler;\n/** Options assembled at runtime, either policy. */\nexport function createSitepingHandler<Principal extends SitepingPrincipal>(\n options: SitepingHandlerOptions<Principal>,\n): SitepingHandler;\nexport function createSitepingHandler<Principal extends SitepingPrincipal>(\n options: SitepingHandlerOptions<Principal>,\n): SitepingHandler {\n // Both policies share the callbacks, typed over `Principal` (`null` under apiKey).\n const {\n store,\n allowedOrigins,\n webhooks,\n waitUntil,\n beforeCreate,\n beforeComment,\n presentFeedback,\n hooks = {},\n logger: customLogger,\n describeError,\n maxBodyBytes = DEFAULT_MAX_BODY_BYTES,\n } = options as SitepingHandlerBaseOptions<Principal>;\n if (!store) {\n throw new SitepingConfigError(\"CONFIG_STORE_MISSING\", \"[siteping] createSitepingHandler requires a `store`.\");\n }\n if (!Number.isSafeInteger(maxBodyBytes) || maxBodyBytes <= 0) {\n throw new SitepingConfigError(\n \"CONFIG_MAX_BODY_BYTES_INVALID\",\n `[siteping] createSitepingHandler: \\`maxBodyBytes\\` must be a positive integer, got ${maxBodyBytes}.`,\n );\n }\n // A custom `authorize` may scope callers to projects, but PATCH/DELETE\n // address records by id: without the ownership check, the project a caller\n // claims (and is authorized for) need not be the record's. Fail closed.\n if (options.access?.authorize && !store.verifyProjectOwnership) {\n throw new SitepingConfigError(\n \"CONFIG_OWNERSHIP_CHECK_MISSING\",\n \"[siteping] createSitepingHandler: `access.authorize` needs a store implementing `verifyProjectOwnership`. \" +\n \"Without it, a caller authorized for one project could PATCH or DELETE another project's feedback by id.\",\n );\n }\n\n // The `apiKey` policy never resolves a principal: its scopes carry `null`.\n const gate = options.access\n ? createAccessGate(options.access)\n : (createApiKeyGate(options) as AccessGate<unknown> as AccessGate<Principal>);\n const logger = customLogger ? safeLogger(customLogger) : consoleLogger;\n const pipeline = createPipeline({ gate, allowedOrigins, logger, describeError, presentFeedback, maxBodyBytes });\n // Normalised once so every POST skips the allocation; an empty list\n // short-circuits dispatch.\n const webhookList: ReadonlyArray<WebhookConfig> = webhooks\n ? Array.isArray(webhooks)\n ? (webhooks as ReadonlyArray<WebhookConfig>)\n : [webhooks as WebhookConfig]\n : [];\n checkWebhookConfigs(webhookList);\n // A reply is notified once per insert, which only a store reporting its\n // inserts can tell from a retried post. Fail closed rather than notify every\n // retry (or none, silently).\n if (\n !store.addCommentIfAbsent &&\n (hooks.onCommentCreated || webhookList.some((config) => subscribesTo(config, \"comment.created\")))\n ) {\n throw new Error(\n \"[siteping] createSitepingHandler: `comment.created` webhooks and the `onCommentCreated` hook need a store \" +\n \"implementing `addCommentIfAbsent`, which tells a new reply from a retried one.\",\n );\n }\n const notify = createNotifier(webhookList, waitUntil, pipeline);\n\n return {\n OPTIONS: (request: Request): Response => preflightResponse(request, allowedOrigins),\n POST: routeByBody(\n pipeline,\n \"POST\",\n \"feedbackId\",\n createCommentOperation({\n store,\n pipeline,\n notify,\n beforeComment,\n onCommentCreated: hooks.onCommentCreated?.bind(hooks),\n }),\n createFeedbackOperation({\n store,\n pipeline,\n notify,\n beforeCreate,\n // Bound so hooks written as class methods keep their `this`.\n onCreated: hooks.onCreated?.bind(hooks),\n }),\n ),\n GET: listFeedbacksOperation({ store, pipeline }),\n PATCH: updateFeedbackOperation({ store, pipeline, notify, onUpdated: hooks.onUpdated?.bind(hooks) }),\n DELETE: routeByBody(\n pipeline,\n \"DELETE\",\n \"commentId\",\n deleteCommentOperation({ store, pipeline }),\n deleteFeedbackOperation({\n store,\n pipeline,\n notify,\n onDeleting: hooks.onDeleting?.bind(hooks),\n onDeleted: hooks.onDeleted?.bind(hooks),\n }),\n ),\n };\n}\n"],"mappings":"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","names":["z"]}
|