@siteping/adapter-drizzle 0.2.3 → 0.2.4
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/dist/{chunk-6DO75JHR.js → chunk-M5VAOY75.js} +1 -1
- package/dist/chunk-M5VAOY75.js.map +1 -0
- package/dist/libsql.cjs.map +1 -1
- package/dist/libsql.js +1 -1
- package/dist/pg.cjs.map +1 -1
- package/dist/pg.js +1 -1
- package/dist/types.d.cts +16 -6
- package/dist/types.d.ts +16 -6
- package/package.json +2 -2
- package/dist/chunk-6DO75JHR.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shared/replicas.ts","../../core/src/type-utils.ts","../../core/src/types.ts","../../core/src/concurrency.ts","../../core/src/filters.ts","../../core/src/logger.ts","../../core/src/screenshot-storage.ts","../../core/src/store-helpers.ts","../../core/src/text.ts","../src/constants/errors.ts","../src/constants/deletes.ts","../src/constants/screenshots.ts","../src/shared/errors.ts","../src/shared/store.ts","../src/constants/search.ts","../src/constants/sql.ts","../src/shared/comments.ts","../src/shared/deletes.ts","../src/shared/feedbacks.ts","../src/shared/filters.ts","../src/shared/rows.ts","../src/shared/timestamps.ts"],"sourcesContent":["/**\n * The primary of a database built with Drizzle's `withReplicas(primary,\n * replicas)`, or the database itself. Such a database sends `select` and\n * `with` to a replica: the PostgreSQL feedback insert (a data-modifying CTE)\n * would run on a read-only replica, and the store's reads of its own writes\n * (a `clientId` that lost an insert race, a screenshot still referenced)\n * would hit a lagging one. On libSQL it has no `batch` at all. So the store\n * runs everything on the primary.\n */\nexport function primaryOf<Database extends object>(db: Database): Database {\n return \"$primary\" in db ? (db.$primary as Database) : db;\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/**\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 only shows the markers of feedback\n * created on the current page (`feedback.url === scope.url`), so\n * annotations never leak to other pages, even if their CSS selector\n * happens to match there. Set to `false` for project-wide markers: a marker\n * shows wherever its anchor resolves, for feedback on what every page\n * shares, such as a header or a footer. The panel's list has a scope filter\n * of its own: see `defaultPanelScope`.\n */\n scopeAnnotationsByUrl?: boolean | undefined;\n /**\n * The scope the panel's list opens on: `\"page\"` (default) lists the\n * current page's feedback, `\"all\"` every feedback of the project, for\n * reviewers who triage across the whole site. The reviewer can still\n * switch, and their choice holds until the widget is destroyed. The\n * markers on the page follow `scopeAnnotationsByUrl`, whatever the list\n * shows.\n */\n defaultPanelScope?: \"page\" | \"all\" | 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","/**\n * Run `task` over every item with at most `concurrency` calls in flight and\n * settle each call on its own, like `Promise.allSettled` over a bounded pool.\n *\n * Each call runs inside its own promise, so a task that throws synchronously\n * is settled as a rejection instead of stopping the pool, and one failure\n * never skips the remaining items.\n *\n * @param items - Inputs, processed in order.\n * @param concurrency - Maximum number of simultaneous `task` calls (at least 1).\n * @param task - Operation to run for each item.\n * @returns The settled result of every call, in the order of `items`.\n */\nexport async function settleWithConcurrencyLimit<Item, Result>(\n items: readonly Item[],\n concurrency: number,\n task: (item: Item) => Promise<Result>,\n): Promise<Array<PromiseSettledResult<Result>>> {\n const results: Array<PromiseSettledResult<Result>> = new Array(items.length);\n let nextIndex = 0;\n\n const runWorker = async (): Promise<void> => {\n while (nextIndex < items.length) {\n const index = nextIndex;\n nextIndex += 1;\n try {\n results[index] = { status: \"fulfilled\", value: await task(items[index] as Item) };\n } catch (error) {\n results[index] = { status: \"rejected\", reason: error };\n }\n }\n };\n\n const workerCount = Math.min(Math.max(1, concurrency), items.length);\n await Promise.all(Array.from({ length: workerCount }, runWorker));\n return results;\n}\n","/**\n * Shared feedback-record filtering and pagination — extracted from\n * `adapter-memory` and `adapter-localstorage` which previously kept two\n * near-identical copies of the same logic. Any adapter that holds an\n * in-memory snapshot of feedbacks can use it.\n *\n * A record matches when it passes every active filter (see\n * {@link matchesFeedbackQuery}):\n * - projectName (always required)\n * - type\n * - status / statuses (`statuses` bucket wins when both are set)\n * - url\n * - urlPattern\n * - search (lowercase substring match on `message`)\n *\n * Pagination goes through `clampPagination`: `limit` capped at 100, `page`\n * 1-based, both clamped up to 1 rather than indexing backwards from the end\n * of the match set. Query adapters reuse the same helper so every store\n * paginates identically.\n */\n\nimport type { FeedbackQuery, FeedbackRecord } from \"./types.js\";\n\n/** Default page size when the caller omits `query.limit`. */\nexport const DEFAULT_PAGE_LIMIT = 50;\n/** Maximum allowed page size — defends against memory blow-ups on hostile callers. */\nexport const MAX_PAGE_LIMIT = 100;\n\nexport interface FilterResult {\n feedbacks: FeedbackRecord[];\n total: number;\n}\n\n/** Normalised pagination window — see {@link clampPagination}. */\nexport interface Pagination {\n /** 1-based page number, at least 1. */\n page: number;\n /** Page size in `[1, MAX_PAGE_LIMIT]`. */\n limit: number;\n /** Offset of the first row: `(page - 1) * limit`. */\n skip: number;\n}\n\nfunction toPositiveInteger(value: number | undefined, fallback: number): number {\n return value !== undefined && Number.isFinite(value) ? Math.max(1, Math.floor(value)) : fallback;\n}\n\n/**\n * Normalise `page` / `limit` to the store contract: `page` is 1-based and\n * clamped up to 1, `limit` defaults to 50 and is clamped into `[1, 100]`,\n * non-finite values fall back to the defaults. `skip` is the derived row\n * offset for query backends (`OFFSET`, Prisma `skip`).\n *\n * Shared by the in-memory pipeline and query adapters (`PrismaStore`) so\n * every store paginates identically — the HTTP schema clamps the same way,\n * but direct callers (dashboard store mode, server actions) reach the store\n * without a schema in front of them.\n */\nexport function clampPagination(query: Pick<FeedbackQuery, \"page\" | \"limit\">): Pagination {\n const page = toPositiveInteger(query.page, 1);\n const limit = Math.min(toPositiveInteger(query.limit, DEFAULT_PAGE_LIMIT), MAX_PAGE_LIMIT);\n return { page, limit, skip: (page - 1) * limit };\n}\n\n/**\n * Whether a {@link clampPagination} offset lies beyond any row a store can\n * hold. `clampPagination` bounds `page` from below only, so a direct caller's\n * huge `page` yields an offset past `Number.MAX_SAFE_INTEGER` — or `Infinity`\n * — that SQL backends reject (`OFFSET` is a 64-bit integer in PostgreSQL and\n * SQLite; Prisma's `skip` rejects non-integers and 64-bit overflow).\n *\n * Every safe integer fits a signed 64-bit offset and no table holds more rows\n * than that, so query adapters answer such a page as empty — with the real\n * `total` — instead of issuing the query: the same result the in-memory\n * pipeline returns.\n *\n * @param skip - The `skip` returned by {@link clampPagination}.\n * @returns `true` when no row can sit at that offset.\n */\nexport function isUnreachableOffset(skip: number): boolean {\n return !Number.isSafeInteger(skip);\n}\n\n/** `createdAt` in ms for newest-first sorting — an invalid date counts as the oldest. */\nfunction sortTime(record: FeedbackRecord): number {\n const time = record.createdAt.getTime();\n // Below every valid Date (±8.64e15 ms) but finite, so two invalid dates\n // subtract to 0 — `-Infinity - -Infinity` would be NaN again.\n return Number.isNaN(time) ? Number.MIN_SAFE_INTEGER : time;\n}\n\n/**\n * Whether one record passes every filter of `query` — the filter half of\n * {@link applyFeedbackFilters}, exposed so a client holding a single record\n * (e.g. the dashboard deciding whether an optimistic edit still belongs in\n * its list) applies exactly the stores' semantics. Pagination is ignored.\n */\nexport function matchesFeedbackQuery(record: FeedbackRecord, query: FeedbackQuery): boolean {\n const { type, status, statuses, url, urlPattern, search } = query;\n return (\n record.projectName === query.projectName &&\n (!type || record.type === type) &&\n // `statuses` (bucket / any-of) wins over the exact `status` filter when\n // both are present; an empty array is treated as absent.\n (statuses && statuses.length > 0 ? statuses.includes(record.status) : !status || record.status === status) &&\n (!url || record.url === url) &&\n (!urlPattern || record.urlPattern === urlPattern) &&\n (!search || record.message.toLowerCase().includes(search.toLowerCase()))\n );\n}\n\n/**\n * Apply the standard feedback filter + pagination pipeline against an\n * in-memory snapshot. Used by `MemoryStore.getFeedbacks` and\n * `LocalStorageStore.getFeedbacks` so the two never drift.\n *\n * @param items All known feedback records (already include `annotations`).\n * @param query Filter and pagination options. `projectName` is required.\n */\nexport function applyFeedbackFilters(items: readonly FeedbackRecord[], query: FeedbackQuery): FilterResult {\n const results = items.filter((f) => matchesFeedbackQuery(f, query));\n\n // Newest first is part of the store contract (PrismaStore orders by\n // createdAt desc) — sort explicitly instead of relying on insertion order.\n // Array.prototype.sort is stable, so same-millisecond records keep their\n // insertion order (newest inserted first). An invalid date (a hand-edited\n // localStorage blob) sorts as oldest: a NaN comparator result would leave\n // the order of the valid records undefined too.\n results.sort((a, b) => sortTime(b) - sortTime(a));\n\n const total = results.length;\n // Both bounds are clamped (see `clampPagination`): `(page - 1) * limit`\n // goes negative for a non-positive page or limit, and `slice` reads\n // negative indices from the END — so `page: -1` used to return a window\n // whose position depended on how many records happened to match.\n const { limit, skip } = clampPagination(query);\n\n return { feedbacks: results.slice(skip, skip + limit), total };\n}\n","/**\n * Where SitePing reports what goes wrong. The arguments come in console's and\n * winston's order, so `console` and winston satisfy it as they are. pino's\n * methods take the context first and type-check here anyway, then drop the\n * context: adapt them — `{ error: (message, { error, ...context }) =>\n * log.error({ err: error, ...context }, message), warn: … }`.\n *\n * Each option asks only for the level it reports — the handler's `logger`\n * for `error`, the stores' and screenshot storages' for `warn`\n * ({@link SitepingWarnLogger}) — so one object can be handed to all of them.\n */\nexport interface SitepingLogger {\n /** Unexpected failures: a request answered 500, a hook or a lookup failed. */\n error(message: string, context: Record<string, unknown>): void | Promise<void>;\n /**\n * Degraded but handled: a screenshot dropped or left orphaned, screenshots\n * stored inline. The handler never calls it; the stores and screenshot\n * storages require it.\n */\n warn?(message: string, context: Record<string, unknown>): void | Promise<void>;\n}\n\n/** What the stores and screenshot storages report through: a {@link SitepingLogger} with `warn`. */\nexport type SitepingWarnLogger = Required<Pick<SitepingLogger, \"warn\">>;\n\n/**\n * Report through `logger.warn` without letting the logger fail its caller,\n * on paths documented as non-throwing: a throw, or a rejection of what it\n * returns — unhandled, fatal in Node by default — is dropped. A broken\n * logger has no other channel to report through.\n */\nexport function warnSafely(logger: SitepingWarnLogger, message: string, context: Record<string, unknown>): void {\n try {\n Promise.resolve(logger.warn(message, context)).catch(() => {});\n } catch {\n // Dropped: see above.\n }\n}\n","/**\n * Pluggable storage for feedback screenshots.\n *\n * `adapter-prisma` and `adapter-drizzle` accept an optional\n * `screenshotStorage` config. When provided, the adapter forwards the\n * widget-supplied data URL to `upload()` and persists the returned URL on\n * `Feedback.screenshotUrl`. When not provided, the adapter falls back to\n * inline base64 (with a one-time warn) — fine for dev and small\n * deployments, a footgun for production Postgres.\n *\n * `@siteping/screenshot-storage` implements it over any S3-compatible\n * bucket (AWS S3, Cloudflare R2, Backblaze B2, MinIO…), Cloudflare Images,\n * a database table, the local filesystem or memory. Implement it yourself\n * for anything else.\n *\n * @example\n * ```ts\n * // Minimal S3 implementation\n * import { S3Client, PutObjectCommand } from \"@aws-sdk/client-s3\";\n *\n * const s3 = new S3Client({ region: \"eu-west-3\" });\n * const screenshotStorage: ScreenshotStorage = {\n * async upload(dataUrl, { mimeType }) {\n * const body = Buffer.from(dataUrl.slice(dataUrl.indexOf(\",\") + 1), \"base64\");\n * const key = `siteping/${crypto.randomUUID()}`; // fresh per upload, see URL ownership\n * await s3.send(new PutObjectCommand({\n * Bucket: \"my-bucket\", Key: key, Body: body, ContentType: mimeType,\n * }));\n * return { url: `https://cdn.example.com/${key}` };\n * },\n * };\n *\n * createSitepingHandler({ prisma, screenshotStorage });\n * ```\n */\nexport interface ScreenshotStorage {\n /**\n * Persist a base64 data URL and return the URL the widget will use as\n * `<img src>`. Implementations decide the underlying storage and any\n * post-processing (resize, virus scan, content-type sniff).\n *\n * Adapters call this synchronously inside `createFeedback` — keep it\n * fast or move to a queue if needed.\n *\n * `ctx.feedbackId` is the id the feedback is stored under. The record does\n * not exist yet, so the store draws it before the upload — fresh for each\n * create attempt, never client-supplied — and inserts the row under it.\n * Two submissions of one `clientId` therefore upload under two ids, and\n * only one of them is stored.\n *\n * **URL ownership:** every call must return a URL no other call returns —\n * key the object by a fresh random value (`crypto.randomUUID()`, as the\n * example does), never by `ctx.feedbackId` alone nor by a content hash.\n * Adapters treat each URL as the property of the one record that stores it\n * and may pass it to `delete` once that record is deleted, or once its\n * create lost a race: the object of the submission that is not stored is\n * deleted, and a URL shared by several records would let deleting one\n * remove the object another still points at.\n *\n * **Security note:** the bundled stores generate `ctx.feedbackId`, but a\n * third-party one may not honour the contract: sanitize it before using it\n * in a filesystem path or an object key.\n */\n upload(dataUrl: string, ctx: { feedbackId: string; mimeType: string }): Promise<{ url: string }>;\n /**\n * Optional cleanup hook called when the feedback is deleted, and for an\n * object uploaded by a create whose record was not stored: one rejected\n * because its `clientId` is already stored, or one whose insert failed. An\n * object a stored row still references (a storage that broke URL\n * ownership) is kept. Receives only URLs returned by {@link ScreenshotStorage.upload}.\n * Adapters call this best-effort and swallow errors — orphaned objects are\n * preferred over failed deletes.\n */\n delete?: (url: string) => Promise<void>;\n}\n\n/**\n * Most `ScreenshotStorage.delete` calls one cleanup keeps in flight — a\n * project delete may free thousands of objects, and firing them all at once\n * can exhaust sockets (one per request with `fetch`) or memory and trip\n * object-store rate limits. It stays under the 50 sockets the AWS SDK opens\n * per client by default, yet a project delete, which waits for its cleanup,\n * is not serialised: 1,000 objects take 32 rounds. Adapters run their\n * deletes through `settleWithConcurrencyLimit` with this bound.\n */\nexport const SCREENSHOT_DELETE_CONCURRENCY = 32;\n\n/**\n * MIME type an adapter reports to {@link ScreenshotStorage.upload}: the one\n * an image data URL declares (`data:image/png;base64,…` → `image/png`),\n * limited to the JPEG, PNG and WebP the HTTP schema accepts. Stores are\n * public and may be fed unvalidated data URLs, and an `image/svg+xml` label\n * would make the stored object script-capable when served inline. Anything\n * else — including a data URL that declares no type — reports JPEG, the\n * widget's capture format.\n */\nexport function screenshotMimeType(dataUrl: string): string {\n return /^data:(image\\/(?:jpeg|png|webp))[;,]/i.exec(dataUrl)?.[1]?.toLowerCase() ?? \"image/jpeg\";\n}\n","/**\n * Record-construction helpers and the collection-store engine.\n *\n * Every snapshot-style adapter (memory, localStorage, flat file, KV, …)\n * needs the same three ingredients: turn a `FeedbackCreateInput` into a\n * `FeedbackRecord` (null-normalizing optional fields, stamping ids and\n * timestamps), filter/paginate with `applyFeedbackFilters`, and implement\n * the dedup/update/delete choreography of the `SitepingStore` contract.\n *\n * `buildFeedbackRecord` / `buildAnnotationRecord` / `buildCommentRecord`\n * cover the first part for any adapter. `createCollectionStore` covers all\n * of it: give it `load`, `persist`, and `generateId`, and it returns a fully\n * conformant `SitepingStore` — writing a new snapshot adapter is ~20 lines\n * plus its storage specifics.\n */\n\nimport { applyFeedbackFilters } from \"./filters.js\";\nimport type {\n AnnotationCreateInput,\n AnnotationRecord,\n CommentCreateInput,\n CommentCreateOutcome,\n CommentRecord,\n FeedbackCreateInput,\n FeedbackCreateOutcome,\n FeedbackPage,\n FeedbackQuery,\n FeedbackRecord,\n FeedbackUpdateInput,\n SitepingStore,\n} from \"./types.js\";\nimport { MAX_COMMENTS_PER_FEEDBACK, StoreDuplicateError, StoreLimitError, StoreNotFoundError } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Record construction\n// ---------------------------------------------------------------------------\n\n/**\n * Build a persisted `AnnotationRecord` from its create input — normalizes\n * the optional anchor fields to `null` and stamps identity/timestamp.\n */\nexport function buildAnnotationRecord(\n input: AnnotationCreateInput,\n ctx: { id: string; feedbackId: string; now: Date },\n): AnnotationRecord {\n return {\n id: ctx.id,\n feedbackId: ctx.feedbackId,\n cssSelector: input.cssSelector,\n xpath: input.xpath,\n textSnippet: input.textSnippet,\n elementTag: input.elementTag,\n elementId: input.elementId ?? null,\n textPrefix: input.textPrefix,\n textSuffix: input.textSuffix,\n fingerprint: input.fingerprint,\n neighborText: input.neighborText,\n anchorKey: input.anchorKey ?? null,\n xPct: input.xPct,\n yPct: input.yPct,\n wPct: input.wPct,\n hPct: input.hPct,\n scrollX: input.scrollX,\n scrollY: input.scrollY,\n viewportW: input.viewportW,\n viewportH: input.viewportH,\n devicePixelRatio: input.devicePixelRatio,\n createdAt: ctx.now,\n };\n}\n\n/** Build a persisted `CommentRecord` from its create input — stamps identity, thread and timestamp. */\nexport function buildCommentRecord(\n input: CommentCreateInput,\n ctx: { id: string; feedbackId: string; now?: Date },\n): CommentRecord {\n return {\n id: ctx.id,\n feedbackId: ctx.feedbackId,\n body: input.body,\n authorName: input.authorName,\n authorEmail: input.authorEmail,\n authorRole: input.authorRole,\n clientId: input.clientId,\n createdAt: ctx.now ?? new Date(),\n };\n}\n\n/**\n * Build a persisted `FeedbackRecord` (with its annotations) from a create\n * input — normalizes every optional field to `null` and stamps ids and\n * timestamps. Adapters without external screenshot storage keep the data\n * URL inline on `screenshotUrl`, which is what this helper does; adapters\n * with a `ScreenshotStorage` upload first and override `screenshotUrl`.\n * It leaves `comments` out, so the record minus its `annotations` stays an\n * insertable feedback row; a store with threads returns `comments: []` itself.\n */\nexport function buildFeedbackRecord(\n input: FeedbackCreateInput,\n ctx: { id: string; annotationId: () => string; now?: Date },\n): FeedbackRecord {\n const now = ctx.now ?? new Date();\n return {\n id: ctx.id,\n type: input.type,\n message: input.message,\n status: input.status,\n projectName: input.projectName,\n url: input.url,\n urlPattern: input.urlPattern ?? null,\n authorName: input.authorName,\n authorEmail: input.authorEmail,\n viewport: input.viewport,\n userAgent: input.userAgent,\n clientId: input.clientId,\n resolvedAt: null,\n createdAt: now,\n updatedAt: now,\n annotations: input.annotations.map((ann) =>\n buildAnnotationRecord(ann, { id: ctx.annotationId(), feedbackId: ctx.id, now }),\n ),\n screenshotUrl: input.screenshotDataUrl ?? null,\n screenshotRegion: input.screenshotRegion ?? null,\n diagnostics: input.diagnostics ?? null,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Collection-store engine\n// ---------------------------------------------------------------------------\n\n/**\n * Storage primitives behind a collection store. `load`/`persist` may be\n * sync or async, so in-memory arrays, localStorage and async KV stores all\n * fit the same three functions. When both are sync, a mutation runs from\n * `load` to `persist` without yielding, so no other code in the realm can\n * write in between; an async backend is only serialized against the\n * engine's own queue.\n */\nexport interface CollectionStoreBackend {\n /**\n * Return the current full snapshot of feedback records. The engine never\n * mutates this array — handing out a live cache is safe.\n */\n load(): FeedbackRecord[] | Promise<FeedbackRecord[]>;\n /**\n * Persist the full snapshot — always a new array, never the one `load()`\n * returned. Throw `StorePersistenceError` when the write is lost (quota,\n * storage disabled, …) — never swallow the failure. Because the loaded\n * snapshot is left untouched, a throw here leaves a cached `load()` result\n * consistent with durable storage: no phantom record, no half-applied\n * update.\n */\n persist(feedbacks: FeedbackRecord[]): void | Promise<void>;\n /** Generate a unique id for a new feedback, annotation or comment record. */\n generateId(): string;\n /**\n * Keep discussion threads on the records: the store gains `addComment`,\n * `addCommentIfAbsent` and `deleteComment`, and new records start with\n * `comments: []`. Opt in once\n * `load` hands back what `persist` wrote for them, each comment's\n * `createdAt` a `Date` again (a JSON backend revives it like the record's\n * own dates). Off by default, so an adapter written before threads never\n * gains them — over storage never written for them — on an engine update.\n */\n comments?: boolean | undefined;\n}\n\n/**\n * A `SitepingStore` with the optional `verifyProjectOwnership` guaranteed —\n * what `createCollectionStore` returns, which also guarantees\n * `createFeedbackIfAbsent`.\n */\nexport type CollectionStore = SitepingStore & Required<Pick<SitepingStore, \"verifyProjectOwnership\">>;\n\n/**\n * Build a fully conformant `SitepingStore` on top of a snapshot backend.\n *\n * The engine implements the whole store contract: clientId dedup (a repeated\n * `clientId` makes `createFeedback` throw `StoreDuplicateError`, and\n * `createFeedbackIfAbsent` report the existing record), newest-first\n * ordering, the standard filter/pagination pipeline, `StoreNotFoundError` on\n * missing update/delete, project-scoped bulk delete,\n * `verifyProjectOwnership`, and — with `comments: true` — discussion threads\n * (`addComment`, `addCommentIfAbsent` reporting inserts, `deleteComment`)\n * kept on each record. The snapshot returned by `load` is\n * never mutated: every write hands `persist` a new array, so a failed write leaves\n * a cached snapshot exactly as it was. When `persist` fails during `createFeedback`\n * and the record carries an inline screenshot, the engine retries once\n * without the screenshot (by far the heaviest field) so the text feedback\n * survives a storage-quota hit; if that also fails, the error propagates —\n * returning the record would claim a success that was never persisted.\n *\n * Mutations (`createFeedbackIfAbsent`, `createFeedback`, `updateFeedback`,\n * `deleteFeedback`, `deleteAllFeedbacks`, `addComment`, `addCommentIfAbsent`,\n * `deleteComment`) run\n * one at a time through a queue owned by the returned store, so concurrent\n * calls — the widget's `Promise.all` bulk resolve/delete, a comment posted\n * while its feedback is resolved — never start from the same snapshot and\n * overwrite each other, and `createFeedbackIfAbsent` and `addCommentIfAbsent`\n * report `created: true` exactly once per `clientId`. A failed mutation rejects with its own error\n * and does not block the ones queued after it, but `load` and `persist` must\n * always settle: one that never does stalls every later mutation on the\n * store, so give network-backed primitives a timeout. Reads are not queued: they see the\n * last persisted snapshot. The guarantee is scoped to one store instance in\n * one JS realm — two instances over the same storage (two\n * `LocalStorageStore`s on one key, two browser tabs, several server\n * processes sharing a KV or a file) are not coordinated; a backend that\n * needs that must bring its own atomic primitive (a transaction, a\n * compare-and-set).\n *\n * @example\n * ```ts\n * export class MemoryStore implements SitepingStore {\n * private feedbacks: FeedbackRecord[] = [];\n * private readonly store = createCollectionStore({\n * load: () => this.feedbacks,\n * persist: (next) => {\n * this.feedbacks = next;\n * },\n * generateId: () => crypto.randomUUID(),\n * comments: true,\n * });\n * createFeedback = this.store.createFeedback;\n * // …delegate the remaining methods the same way\n * }\n * ```\n */\nexport function createCollectionStore(\n backend: CollectionStoreBackend & { comments: true },\n): CollectionStore &\n Required<Pick<SitepingStore, \"createFeedbackIfAbsent\" | \"addComment\" | \"addCommentIfAbsent\" | \"deleteComment\">>;\nexport function createCollectionStore(\n backend: CollectionStoreBackend,\n): CollectionStore & Required<Pick<SitepingStore, \"createFeedbackIfAbsent\">>;\nexport function createCollectionStore(\n backend: CollectionStoreBackend,\n): CollectionStore & Required<Pick<SitepingStore, \"createFeedbackIfAbsent\">> {\n // Every mutation is a load → modify → persist cycle over the WHOLE\n // snapshot, so two interleaved mutations would start from the same\n // snapshot and the last persist would silently drop the other's change\n // (the widget's bulk resolve/delete fires them all with `Promise.all`).\n // Mutations therefore run one at a time through this queue; `tail` never\n // rejects, so a failed mutation doesn't stall the ones queued after it.\n let tail: Promise<unknown> = Promise.resolve();\n function mutate<T>(mutation: (feedbacks: FeedbackRecord[]) => Promise<T>): Promise<T> {\n const result = tail.then(() => {\n // A sync snapshot goes to the mutation without an `await`, so on a sync\n // backend (memory, localStorage) load → persist is one uninterrupted\n // step: a write outside the queue, like `MemoryStore.clear()`, can't\n // land in between and be undone by a persist of the older snapshot.\n const loaded = backend.load();\n return Array.isArray(loaded) ? mutation(loaded) : loaded.then(mutation);\n });\n tail = result.catch(() => {});\n return result;\n }\n\n // Every mutation below builds a NEW array for `persist` instead of editing\n // the loaded one in place. A backend whose `load()` serves a live cache (an\n // in-memory array, a KV read-through) would otherwise see the change before\n // the write is confirmed — and when `persist` throws, the phantom record\n // stays visible, and the widget's retry of the same clientId dedups against\n // it instead of being written for real.\n const createFeedbackIfAbsent = (data: FeedbackCreateInput): Promise<FeedbackCreateOutcome> =>\n mutate(async (feedbacks) => {\n // ClientId dedup — idempotent\n const existing = feedbacks.find((f) => f.clientId === data.clientId);\n if (existing) return { feedback: existing, created: false };\n\n const record: FeedbackRecord = buildFeedbackRecord(data, {\n id: backend.generateId(),\n annotationId: () => backend.generateId(),\n });\n if (backend.comments) record.comments = [];\n\n const next = [record, ...feedbacks];\n try {\n await backend.persist(next);\n } catch (err) {\n if (!record.screenshotUrl) throw err;\n record.screenshotUrl = null;\n await backend.persist(next);\n }\n return { feedback: record, created: true };\n });\n\n const store: CollectionStore & Required<Pick<SitepingStore, \"createFeedbackIfAbsent\">> = {\n createFeedbackIfAbsent,\n\n async createFeedback(data: FeedbackCreateInput): Promise<FeedbackRecord> {\n const { feedback, created } = await createFeedbackIfAbsent(data);\n if (!created) throw new StoreDuplicateError(`A feedback with clientId ${data.clientId} already exists`);\n return feedback;\n },\n\n async getFeedbacks(query: FeedbackQuery): Promise<FeedbackPage> {\n return applyFeedbackFilters(await backend.load(), query);\n },\n\n async findByClientId(clientId: string): Promise<FeedbackRecord | null> {\n return (await backend.load()).find((f) => f.clientId === clientId) ?? null;\n },\n\n updateFeedback: (id: string, data: FeedbackUpdateInput): Promise<FeedbackRecord> =>\n mutate(async (feedbacks) => {\n const current = feedbacks.find((f) => f.id === id);\n if (!current) throw new StoreNotFoundError();\n\n const updated: FeedbackRecord = {\n ...current,\n status: data.status,\n resolvedAt: data.resolvedAt,\n updatedAt: new Date(),\n };\n await backend.persist(feedbacks.map((f) => (f === current ? updated : f)));\n return updated;\n }),\n\n deleteFeedback: (id: string): Promise<void> =>\n mutate(async (feedbacks) => {\n if (!feedbacks.some((f) => f.id === id)) throw new StoreNotFoundError();\n\n await backend.persist(feedbacks.filter((f) => f.id !== id));\n }),\n\n deleteAllFeedbacks: (projectName: string): Promise<void> =>\n mutate(async (feedbacks) => {\n await backend.persist(feedbacks.filter((f) => f.projectName !== projectName));\n }),\n\n async verifyProjectOwnership(id: string, projectName: string): Promise<boolean> {\n const fb = (await backend.load()).find((f) => f.id === id);\n return fb !== undefined && fb.projectName === projectName;\n },\n };\n if (!backend.comments) return store;\n\n const addCommentIfAbsent = (feedbackId: string, data: CommentCreateInput): Promise<CommentCreateOutcome> =>\n mutate(async (feedbacks) => {\n // ClientId dedup across every thread — idempotent, like createFeedback\n const existing = feedbacks.flatMap((f) => f.comments ?? []).find((c) => c.clientId === data.clientId);\n if (existing) return { comment: existing, created: false };\n\n const current = feedbacks.find((f) => f.id === feedbackId);\n if (!current) throw new StoreNotFoundError();\n const thread = current.comments ?? [];\n if (\n data.authorRole === \"client\" &&\n thread.filter((c) => c.authorRole === \"client\").length >= MAX_COMMENTS_PER_FEEDBACK\n ) {\n throw new StoreLimitError(`A thread holds at most ${MAX_COMMENTS_PER_FEEDBACK} client comments`);\n }\n\n const comment = buildCommentRecord(data, { id: backend.generateId(), feedbackId });\n const updated: FeedbackRecord = { ...current, comments: [...thread, comment] };\n await backend.persist(feedbacks.map((f) => (f === current ? updated : f)));\n return { comment, created: true };\n });\n\n return {\n ...store,\n\n addCommentIfAbsent,\n\n async addComment(feedbackId: string, data: CommentCreateInput): Promise<CommentRecord> {\n return (await addCommentIfAbsent(feedbackId, data)).comment;\n },\n\n deleteComment: (feedbackId: string, commentId: string): Promise<void> =>\n mutate(async (feedbacks) => {\n const current = feedbacks.find((f) => f.id === feedbackId);\n const thread = current?.comments ?? [];\n // A comment of another thread is \"not found\" here: the caller was\n // only authorized for this one.\n if (!current || !thread.some((c) => c.id === commentId)) throw new StoreNotFoundError();\n\n const updated: FeedbackRecord = { ...current, comments: thread.filter((c) => c.id !== commentId) };\n await backend.persist(feedbacks.map((f) => (f === current ? updated : f)));\n }),\n };\n}\n","/**\n * Text a database may refuse — shared by the stores that write to one.\n *\n * PostgreSQL rejects NUL in `text` and `jsonb`, and a surrogate without its\n * pair, which UTF-8 cannot encode: `jsonb` rejects it and `text` turns it\n * into U+FFFD. A string cut inside an emoji ends with such an unpaired\n * surrogate. The store contract accepts either the text as sent or U+FFFD in\n * place of each such code unit (see the conformance suite), so a SQL store\n * normalizes everything it writes or looks up with {@link toStorableText}.\n */\n\n/** The UTF-16 code units a JavaScript string may hold but PostgreSQL cannot store: NUL and an unpaired surrogate. */\nexport const UNSTORABLE_CODE_UNITS = /\\0|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]/g;\n\n/** U+FFFD, the Unicode replacement character — what stands for each unstorable code unit. */\nexport const REPLACEMENT_CHARACTER = \"�\";\n\n/**\n * `text` with every code unit PostgreSQL cannot store (NUL, an unpaired\n * surrogate) replaced by U+FFFD. Applied to everything a store writes or\n * looks up, a submission holding one is stored instead of failing, a record\n * reads back exactly as the store returned it, and a search or lookup\n * holding one matches what was stored for it.\n */\nexport function toStorableText(text: string): string {\n return text.replace(UNSTORABLE_CODE_UNITS, REPLACEMENT_CHARACTER);\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/**\n * `value` with {@link toStorableText} applied to every string it holds, keys\n * included, through arrays and plain objects (annotations, diagnostics…).\n * Other values — numbers, dates, `null` — are kept as they are.\n */\nexport function toStorableValue<Value>(value: Value): Value {\n if (typeof value === \"string\") return toStorableText(value) as Value;\n if (Array.isArray(value)) return value.map(toStorableValue) as Value;\n if (!isPlainObject(value)) return value;\n return Object.fromEntries(\n Object.entries(value).map(([key, item]) => [toStorableText(key), toStorableValue(item)]),\n ) as Value;\n}\n","/** Prefix of every error message and log line emitted by the Drizzle store. */\nexport const DRIZZLE_STORE_MESSAGE_PREFIX = \"[siteping] DrizzleStore\";\n\n/** Store mutations whose database failures surface as `StorePersistenceError`. */\nexport type DrizzleStoreMutation =\n | \"createFeedback\"\n | \"updateFeedback\"\n | \"deleteFeedback\"\n | \"deleteAllFeedbacks\"\n | \"addComment\"\n | \"deleteComment\";\n\n/**\n * Start of the message of Drizzle's `DrizzleQueryError`, which goes on with\n * the statement and every bound parameter.\n */\nexport const DRIZZLE_QUERY_ERROR_MESSAGE_PREFIX = \"Failed query:\";\n\n/**\n * SQLSTATE of a foreign-key violation: what PostgreSQL reports when a\n * comment's feedback is deleted while the comment is being inserted.\n */\nexport const FOREIGN_KEY_VIOLATION_SQLSTATE = \"23503\";\n\n/**\n * Own properties of a driver error that tell what failed without quoting\n * the statement or its values — the only ones the store keeps: Node.js\n * system errors (a lost connection), pg-protocol errors (node-postgres,\n * PGlite, Neon), postgres.js errors and libSQL errors. PostgreSQL's\n * `detail` is left out: for a NOT NULL or CHECK violation it holds the\n * whole failing row.\n */\nexport const DRIVER_ERROR_DIAGNOSTIC_FIELDS = [\n \"code\",\n \"errno\",\n \"syscall\",\n \"severity\",\n \"schema\",\n \"table\",\n \"column\",\n \"dataType\",\n \"constraint\",\n \"routine\",\n \"severity_local\",\n \"schema_name\",\n \"table_name\",\n \"column_name\",\n \"constraint_name\",\n \"extendedCode\",\n \"rawCode\",\n] as const;\n","/**\n * Most feedback rows one statement (or libSQL batch) of `deleteAllFeedbacks`\n * removes while it reads their screenshot URLs back for cleanup. Bounds every\n * driver response — HTTP drivers (Neon HTTP, Turso over HTTP) reject oversized\n * ones, possibly after the deletion committed — and the URLs held in memory\n * between a chunk and its cleanup, however large the project.\n */\nexport const PROJECT_DELETE_CHUNK_SIZE = 500;\n","/** Prefix of inline screenshots — stored as-is, never uploaded, nothing to clean up. */\nexport const INLINE_SCREENSHOT_URL_PREFIX = \"data:\";\n\n/**\n * Screenshot URLs checked per reference lookup before a cleanup — keeps each\n * `IN (…)` list far below the bound-parameter limits of PostgreSQL (65 535)\n * and SQLite (32 766), however many rows a project delete removed.\n */\nexport const SCREENSHOT_REFERENCE_LOOKUP_BATCH_SIZE = 500;\n\n/** SQL `LIKE` pattern matching inline screenshots ({@link INLINE_SCREENSHOT_URL_PREFIX} contains no wildcard). */\nexport const INLINE_SCREENSHOT_URL_LIKE_PATTERN = `${INLINE_SCREENSHOT_URL_PREFIX}%`;\n","import { DrizzleQueryError } from \"drizzle-orm\";\nimport { DRIVER_ERROR_DIAGNOSTIC_FIELDS, DRIZZLE_QUERY_ERROR_MESSAGE_PREFIX } from \"../constants/errors.js\";\nimport type { SitepingSqlGateway } from \"./gateway.js\";\n\n/**\n * Whether `error` is Drizzle's `DrizzleQueryError`, whose message lists the\n * statement and every bound parameter — the submission itself: author\n * emails, messages, inline screenshots — and loggers print an error's\n * `cause` chain. One thrown by another copy of drizzle-orm (its CommonJS\n * build next to the ESM one, a second install) fails `instanceof`, and the\n * class has neither an `entityKind` nor a name of its own: its message\n * gives it away.\n */\nfunction isDrizzleQueryError(error: Error): boolean {\n return error instanceof DrizzleQueryError || error.message.startsWith(DRIZZLE_QUERY_ERROR_MESSAGE_PREFIX);\n}\n\n/**\n * The driver's error behind a failed query, without the statement: a\n * {@link isDrizzleQueryError | Drizzle query error} gives way to its `cause`,\n * and any other error to a copy that keeps what a diagnosis needs — its\n * name, message, stack, {@link DRIVER_ERROR_DIAGNOSTIC_FIELDS} and its\n * `cause` chain, copied the same way — and nothing else. Drivers attach the\n * failed statement and every bound parameter to their own errors: PGlite as\n * plain properties, postgres.js as hidden ones that its `debug` option\n * reveals and that cannot be deleted. The copy is a plain `Error`: an object\n * built on the driver's class would inherit accessors that throw on anything\n * but a genuine instance (`DOMException`'s `code`, when a fetch-based driver\n * times out).\n *\n * @param error - What a gateway call rejected with. Anything but an `Error` is returned as it is.\n * @param above - The errors above this one in the `cause` chain, which end a cycle.\n */\nfunction withoutStatement(error: unknown, above: readonly unknown[] = []): unknown {\n if (!(error instanceof Error)) return error;\n const chain = [...above, error];\n const keepsCause = error.cause !== undefined && !chain.includes(error.cause);\n const cause = keepsCause ? withoutStatement(error.cause, chain) : undefined;\n if (isDrizzleQueryError(error)) return cause;\n const copy = new Error(error.message, keepsCause ? { cause } : undefined);\n const kept = (value: unknown): PropertyDescriptor => ({ value, writable: true, configurable: true });\n Object.defineProperties(copy, { name: kept(error.name), stack: kept(error.stack) });\n const diagnostics = DRIVER_ERROR_DIAGNOSTIC_FIELDS.map((field) => [field, Reflect.get(error, field)] as const);\n return Object.assign(copy, Object.fromEntries(diagnostics.filter(([, value]) => value !== undefined)));\n}\n\n/**\n * `gateway`, with every call rejecting with {@link withoutStatement} its\n * error: no error the store throws, or wraps as a `cause`, carries the\n * statement or its parameters.\n */\nexport function withDriverErrors(gateway: SitepingSqlGateway): SitepingSqlGateway {\n return new Proxy(gateway, {\n get(target, property, receiver) {\n const member: unknown = Reflect.get(target, property, receiver);\n if (typeof member !== \"function\") return member;\n return async (...args: unknown[]) => {\n try {\n return await member.apply(target, args);\n } catch (error) {\n throw withoutStatement(error);\n }\n };\n },\n });\n}\n","import {\n buildCommentRecord,\n buildFeedbackRecord,\n type CommentCreateInput,\n type CommentCreateOutcome,\n type CommentRecord,\n clampPagination,\n type FeedbackCreateInput,\n type FeedbackCreateOutcome,\n type FeedbackPage,\n type FeedbackQuery,\n type FeedbackRecord,\n type FeedbackUpdateInput,\n isStoreDuplicate,\n isStoreNotFound,\n isStorePersistence,\n isUnreachableOffset,\n MAX_COMMENTS_PER_FEEDBACK,\n SCREENSHOT_DELETE_CONCURRENCY,\n type ScreenshotStorage,\n type SitepingStore,\n type SitepingWarnLogger,\n StoreDuplicateError,\n StoreLimitError,\n StoreNotFoundError,\n StorePersistenceError,\n screenshotMimeType,\n settleWithConcurrencyLimit,\n toStorableText,\n toStorableValue,\n warnSafely,\n} from \"@siteping/core\";\nimport { PROJECT_DELETE_CHUNK_SIZE } from \"../constants/deletes.js\";\nimport {\n DRIZZLE_STORE_MESSAGE_PREFIX,\n type DrizzleStoreMutation,\n FOREIGN_KEY_VIOLATION_SQLSTATE,\n} from \"../constants/errors.js\";\nimport { INLINE_SCREENSHOT_URL_PREFIX, SCREENSHOT_REFERENCE_LOOKUP_BATCH_SIZE } from \"../constants/screenshots.js\";\nimport { withDriverErrors } from \"./errors.js\";\nimport type {\n AnnotationRow,\n CommentRow,\n DeleteFeedbacksOptions,\n FeedbackFilter,\n FeedbackRow,\n SitepingSqlGateway,\n} from \"./gateway.js\";\n\n/**\n * The store returned by the dialect factories — the full contract, including\n * the ownership check, the atomic `createFeedbackIfAbsent` and discussion\n * threads with the atomic `addCommentIfAbsent`.\n */\nexport type DrizzleStore = SitepingStore &\n Required<\n Pick<\n SitepingStore,\n \"verifyProjectOwnership\" | \"createFeedbackIfAbsent\" | \"addComment\" | \"addCommentIfAbsent\" | \"deleteComment\"\n >\n >;\n\n/**\n * Where the store reports degraded-but-non-fatal situations (failed\n * screenshot uploads or cleanups, inline screenshots) — SitePing's shared\n * logger, with `warn`: `console` satisfies it, and so does the logger handed\n * to the server. A logger that throws or rejects never fails the store.\n */\nexport type DrizzleStoreLogger = SitepingWarnLogger;\n\nexport interface DrizzleStoreOptions {\n /**\n * Upload screenshots to external storage (S3, R2, Cloudflare Images…) and\n * persist only the returned URL. Without it, the base64 data URL is stored\n * inline (warned once) — fine for development, heavy for production.\n */\n screenshotStorage?: ScreenshotStorage | undefined;\n /**\n * Degraded-path reporting (failed uploads/cleanups, inline screenshots).\n * The store never picks a logging backend itself: without one these events\n * are dropped, so pass your application's logger (or `console`) to see them.\n */\n logger?: DrizzleStoreLogger | undefined;\n /**\n * Clock for record timestamps: `createdAt` of new feedbacks and comments,\n * `updatedAt` of status updates. Defaults to the system clock.\n */\n now?: (() => Date) | undefined;\n}\n\n/** Logger of stores created without one: drops every event, leaving the logging policy to the host application. */\nconst silentLogger: DrizzleStoreLogger = {\n warn() {},\n};\n\n/** Clock of stores created without one. */\nconst systemClock = (): Date => new Date();\n\n/** Whether a store error already carries its contract meaning and must propagate untouched. */\nfunction isStoreContractError(error: unknown): boolean {\n return isStoreNotFound(error) || isStoreDuplicate(error) || isStorePersistence(error);\n}\n\n/**\n * Run database calls a mutation makes (its write, or a read it depends on),\n * reporting any database failure (read-only or full database, lost\n * connection, rejected statement…) as `StorePersistenceError`, the\n * `SitepingStore` mutation contract, with the driver's error as `cause`.\n *\n * @param mutation - Store method being served, for the message.\n * @param identifiers - Minimal ids to debug the failure (never payload data).\n * @param run - The database calls.\n */\nasync function persistMutation<Result>(\n mutation: DrizzleStoreMutation,\n identifiers: Record<string, string>,\n run: () => Promise<Result>,\n): Promise<Result> {\n try {\n return await run();\n } catch (error) {\n if (isStoreContractError(error)) throw error;\n const context = Object.entries(identifiers)\n .map(([name, value]) => `${name}=${value}`)\n .join(\" \");\n throw new StorePersistenceError(`${DRIZZLE_STORE_MESSAGE_PREFIX}.${mutation} failed (${context})`, {\n cause: error,\n });\n }\n}\n\n/** Whether `error` or an error of its `cause` chain is a foreign-key violation. */\nfunction isForeignKeyViolation(error: unknown): boolean {\n const seen: unknown[] = [];\n for (let current = error; current instanceof Error && !seen.includes(current); current = current.cause) {\n if (Reflect.get(current, \"code\") === FOREIGN_KEY_VIOLATION_SQLSTATE) return true;\n seen.push(current);\n }\n return false;\n}\n\n/** Rows grouped by their `feedbackId`, each group in the order the rows came. */\nfunction groupByFeedback<Row extends { feedbackId: string }>(rows: readonly Row[]): Map<string, Row[]> {\n const byFeedback = new Map<string, Row[]>();\n for (const row of rows) {\n const siblings = byFeedback.get(row.feedbackId);\n if (siblings) siblings.push(row);\n else byFeedback.set(row.feedbackId, [row]);\n }\n return byFeedback;\n}\n\n/** Whether a stored `screenshotUrl` points at an object the storage owns (inline data URLs were never uploaded). */\nfunction isUploadedScreenshotUrl(url: string | null | undefined): url is string {\n return typeof url === \"string\" && url.length > 0 && !url.startsWith(INLINE_SCREENSHOT_URL_PREFIX);\n}\n\n/**\n * `SitepingStore` over a dialect gateway: ids, timestamps, clientId\n * idempotency, screenshot upload/cleanup and the error contract live here,\n * SQL lives in the gateway. Every string a method receives goes through\n * {@link toStorableText} before it reaches the gateway.\n * @internal\n */\nexport class DrizzleSitepingStore implements DrizzleStore {\n private readonly gateway: SitepingSqlGateway;\n private readonly screenshotStorage: ScreenshotStorage | undefined;\n private readonly logger: DrizzleStoreLogger;\n private readonly now: () => Date;\n private inlineScreenshotWarned = false;\n\n constructor(gateway: SitepingSqlGateway, options: DrizzleStoreOptions = {}) {\n this.gateway = withDriverErrors(gateway);\n this.screenshotStorage = options.screenshotStorage;\n this.logger = options.logger ?? silentLogger;\n this.now = options.now ?? systemClock;\n }\n\n /** Throws `StoreDuplicateError` when a feedback with the same `clientId` exists. */\n async createFeedback(data: FeedbackCreateInput): Promise<FeedbackRecord> {\n const { feedback, created } = await this.createFeedbackIfAbsent(data);\n if (!created) throw new StoreDuplicateError(`A feedback with clientId ${data.clientId} already exists`);\n return feedback;\n }\n\n /**\n * Insert the feedback unless a row with the same `clientId` exists. The\n * unique `client_id` index plus `ON CONFLICT DO NOTHING` make the check\n * atomic across store instances and processes: of N concurrent calls, only\n * the one whose insert lands reports `created: true`.\n *\n * @throws `StorePersistenceError` when a database call fails: the lookup\n * of the `clientId`, the insert, or — when the insert loses the race — the\n * read-back of the winning row. If that row was deleted in the meantime,\n * an `Error` naming the `clientId` is thrown (the caller may retry the\n * submission). On every failure path, the screenshot this attempt uploaded\n * is discarded first — no committed row references it.\n */\n async createFeedbackIfAbsent(submitted: FeedbackCreateInput): Promise<FeedbackCreateOutcome> {\n const data = toStorableValue(submitted);\n const identifiers = { clientId: data.clientId };\n const existing = await persistMutation(\"createFeedback\", identifiers, () => this.findByClientId(data.clientId));\n if (existing) return { feedback: existing, created: false };\n\n // Fresh per attempt: racing creates of one clientId upload under distinct\n // ids, so the winner's object is never overwritten by a loser's upload.\n const id = crypto.randomUUID();\n const screenshotUrl = await this.persistScreenshot(data.screenshotDataUrl, {\n feedbackId: id,\n clientId: data.clientId,\n });\n const { annotations, ...feedback } = buildFeedbackRecord(data, {\n id,\n annotationId: () => crypto.randomUUID(),\n // The clock value as is: rows created in the same millisecond — by this\n // or any other instance — are ordered by the database-wide insertion\n // ordinal, so a later insert always lists first.\n now: this.now(),\n });\n const row: FeedbackRow = { ...feedback, screenshotUrl };\n\n let inserted: boolean;\n try {\n inserted = await persistMutation(\"createFeedback\", identifiers, () =>\n this.gateway.insertFeedback(row, annotations),\n );\n } catch (error) {\n // A failure reported after the commit (e.g. a dropped connection) may\n // have stored this attempt's row: the reference check keeps its screenshot.\n await this.discardScreenshots([screenshotUrl], { clientId: data.clientId });\n throw error;\n }\n if (inserted) {\n return { feedback: { ...row, annotations, comments: [] }, created: true };\n }\n\n // Lost a race against the same clientId: the stored row keeps its own\n // screenshot, uploaded under its own id, so the one just uploaded is an\n // orphan no row references (screenshot URLs are unique per feedback id).\n // It is discarded however the winner lookup ends — found, deleted in the\n // meantime, or rejected.\n let winner: FeedbackRecord | null;\n try {\n winner = await persistMutation(\"createFeedback\", identifiers, () => this.findByClientId(data.clientId));\n } finally {\n await this.discardScreenshots([screenshotUrl], { clientId: data.clientId });\n }\n if (!winner) {\n throw new Error(\n `${DRIZZLE_STORE_MESSAGE_PREFIX}.createFeedbackIfAbsent: clientId ${data.clientId} conflicted but no row was found (the winning row was deleted before it could be read back)`,\n );\n }\n return { feedback: winner, created: false };\n }\n\n async getFeedbacks(submittedQuery: FeedbackQuery): Promise<FeedbackPage> {\n const query = toStorableValue(submittedQuery);\n const { limit, skip } = clampPagination(query);\n const filter: FeedbackFilter = { projectName: query.projectName };\n if (query.type) filter.type = query.type;\n // A non-empty `statuses` bucket wins over the exact `status` filter.\n if (query.statuses && query.statuses.length > 0) filter.statuses = query.statuses;\n else if (query.status) filter.statuses = [query.status];\n if (query.url) filter.url = query.url;\n if (query.urlPattern) filter.urlPattern = query.urlPattern;\n if (query.search) filter.search = query.search;\n\n // A huge `page` from a direct caller yields an offset PostgreSQL and\n // SQLite reject as `OFFSET`: answer the empty page the in-memory stores\n // return, with the real total, without issuing the `OFFSET` query.\n if (isUnreachableOffset(skip)) {\n return { feedbacks: [], total: await this.gateway.countFeedbacks(filter) };\n }\n\n const { rows, total } = await this.gateway.findFeedbacks(filter, { limit, offset: skip });\n return { feedbacks: await this.withRelations(rows), total };\n }\n\n async findByClientId(clientId: string): Promise<FeedbackRecord | null> {\n const row = await this.gateway.findByClientId(toStorableText(clientId));\n return row ? ((await this.withRelations([row]))[0] ?? null) : null;\n }\n\n /**\n * Update a feedback's status. Its annotations and thread are read before the\n * update — annotations never change after the insert, and the thread is the\n * one at the time of the update: once the update commits, no database call\n * is left that could fail and make the caller retry an applied update.\n *\n * @throws `StoreNotFoundError` when no row has that id.\n * @throws `StorePersistenceError` when reading the annotations, the thread\n * or the update fails — a failed read leaves the row untouched.\n */\n async updateFeedback(submittedId: string, data: FeedbackUpdateInput): Promise<FeedbackRecord> {\n const id = toStorableText(submittedId);\n const { relationsOf, row } = await persistMutation(\"updateFeedback\", { id }, async () => {\n const relationsOf = await this.readRelations([id]);\n // The gateway clamps updatedAt to the row's createdAt, which another\n // process whose clock runs ahead may have stamped later than this clock.\n const row = await this.gateway.updateStatus(id, {\n status: data.status,\n resolvedAt: data.resolvedAt,\n updatedAt: this.now(),\n });\n return { relationsOf, row };\n });\n if (!row) throw new StoreNotFoundError();\n return { ...row, ...relationsOf(id) };\n }\n\n async deleteFeedback(submittedId: string): Promise<void> {\n const id = toStorableText(submittedId);\n const deleted = await persistMutation(\"deleteFeedback\", { id }, () =>\n this.gateway.deleteById(id, this.deleteOptions()),\n );\n if (!deleted) throw new StoreNotFoundError();\n await this.discardScreenshots(deleted.screenshotUrls);\n }\n\n /**\n * Delete every feedback of a project. Rows go first, storage second: orphaned\n * objects are acceptable, rows pointing at deleted screenshots are not.\n *\n * Without a `ScreenshotStorage.delete` hook, one atomic statement (or batch)\n * removes the project and reads nothing back. With one, the removed URLs are\n * needed for cleanup, so rows go in chunks of\n * {@link PROJECT_DELETE_CHUNK_SIZE}, each deleted atomically and its\n * screenshots cleaned up before the next one: every driver response and the\n * URLs held in memory stay bounded however large the project. The call\n * returns once the project has no row left, whatever deletes run alongside.\n *\n * @throws `StorePersistenceError` when a chunk fails. The chunks before it\n * stay deleted and their screenshots are already cleaned up; the delete is\n * idempotent, so retrying it removes the remaining rows. When the failing\n * chunk committed although the driver reported an error, its rows are gone\n * and their screenshots are left in the storage as orphans. Also thrown\n * when the database keeps rows it is told to delete (a row-level security\n * policy or a trigger that skips deletes), instead of retrying forever.\n */\n async deleteAllFeedbacks(submittedProjectName: string): Promise<void> {\n const projectName = toStorableText(submittedProjectName);\n if (!this.deleteOptions().collectScreenshotUrls) {\n await persistMutation(\"deleteAllFeedbacks\", { projectName }, () => this.gateway.deleteByProject(projectName));\n return;\n }\n let remainingAfterEmptyChunk = Number.POSITIVE_INFINITY;\n for (;;) {\n const deleted = await persistMutation(\"deleteAllFeedbacks\", { projectName }, () =>\n this.gateway.deleteProjectChunk(projectName, PROJECT_DELETE_CHUNK_SIZE),\n );\n if (deleted.deletedCount > 0) {\n remainingAfterEmptyChunk = Number.POSITIVE_INFINITY;\n await this.discardScreenshots(deleted.screenshotUrls);\n continue;\n }\n // A short or even empty chunk does not mean the project is empty: on\n // PostgreSQL, a concurrent delete may take some or all of the rows a chunk\n // picked (the statement waits for its locks, then skips the rows it\n // removed). So only a project left empty ends the delete: the next chunk\n // waits for the concurrent delete, and rows keep going. When two empty\n // chunks in a row leave no fewer rows, nothing will remove them.\n const remaining = await persistMutation(\"deleteAllFeedbacks\", { projectName }, () =>\n this.gateway.countFeedbacks({ projectName }),\n );\n if (remaining === 0) return;\n if (remaining >= remainingAfterEmptyChunk) {\n throw new StorePersistenceError(\n `${DRIZZLE_STORE_MESSAGE_PREFIX}.deleteAllFeedbacks: the database kept ${remaining} rows it was told to delete (projectName=${projectName})`,\n );\n }\n remainingAfterEmptyChunk = remaining;\n }\n }\n\n async verifyProjectOwnership(id: string, projectName: string): Promise<boolean> {\n return (await this.gateway.findProjectName(toStorableText(id))) === toStorableText(projectName);\n }\n\n /**\n * Add a comment as the last of its thread, in one statement that also\n * checks the feedback, the cap on `client` comments and the `clientId` —\n * atomic across store instances and processes, except that posts racing\n * for the last free slot may overshoot the cap by the ones that run\n * concurrently.\n *\n * @throws `StoreNotFoundError` when the feedback does not exist, including\n * when it is deleted while the comment is being inserted.\n * @throws `StoreLimitError` when a `client` comment meets a thread already holding `MAX_COMMENTS_PER_FEEDBACK` of them.\n * @throws `StorePersistenceError` when a database call fails.\n */\n async addComment(feedbackId: string, data: CommentCreateInput): Promise<CommentRecord> {\n return (await this.addCommentIfAbsent(feedbackId, data)).comment;\n }\n\n /**\n * {@link addComment}, reporting whether this call inserted the comment: the\n * single statement inserts at most one row per `clientId`, so of racing\n * posts exactly one reports `created: true`.\n */\n async addCommentIfAbsent(submittedFeedbackId: string, submitted: CommentCreateInput): Promise<CommentCreateOutcome> {\n const feedbackId = toStorableText(submittedFeedbackId);\n const data = toStorableValue(submitted);\n const comment = buildCommentRecord(data, { id: crypto.randomUUID(), feedbackId, now: this.now() });\n const identifiers = { feedbackId, clientId: data.clientId };\n let inserted: boolean;\n try {\n inserted = await persistMutation(\"addComment\", identifiers, () =>\n this.gateway.insertComment(comment, MAX_COMMENTS_PER_FEEDBACK),\n );\n } catch (error) {\n // PostgreSQL checks that the feedback exists on the statement's snapshot, and\n // its foreign key after it: a feedback deleted in between fails the insert\n // on the foreign key instead of skipping it. When the feedback is gone, that\n // is a missing feedback; when the lookup fails too, the insert's own failure\n // stands. Any other failure stands at once: on an unreachable database, a\n // lookup would only wait for a second driver timeout.\n if (!isForeignKeyViolation(error)) throw error;\n const feedbackGone = await this.gateway.findProjectName(feedbackId).then(\n (projectName) => projectName === null,\n () => false,\n );\n if (feedbackGone) throw new StoreNotFoundError();\n throw error;\n }\n if (inserted) return { comment, created: true };\n\n // Nothing written: a replay of the clientId, a missing feedback, or a full thread.\n const replayed = await persistMutation(\"addComment\", identifiers, () =>\n this.gateway.findCommentByClientId(data.clientId),\n );\n if (replayed) return { comment: replayed, created: false };\n const projectName = await persistMutation(\"addComment\", identifiers, () =>\n this.gateway.findProjectName(feedbackId),\n );\n if (projectName === null) throw new StoreNotFoundError();\n throw new StoreLimitError(`A thread holds at most ${MAX_COMMENTS_PER_FEEDBACK} client comments`);\n }\n\n async deleteComment(submittedFeedbackId: string, submittedCommentId: string): Promise<void> {\n const feedbackId = toStorableText(submittedFeedbackId);\n const commentId = toStorableText(submittedCommentId);\n const deleted = await persistMutation(\"deleteComment\", { feedbackId, commentId }, () =>\n this.gateway.deleteComment(feedbackId, commentId),\n );\n if (!deleted) throw new StoreNotFoundError();\n }\n\n /**\n * Deletes read the removed screenshot URLs back only when a\n * `ScreenshotStorage.delete` hook can clean them up: without one there is\n * nothing to do with them, and inline data URLs would be materialized for\n * every deleted row.\n */\n private deleteOptions(): DeleteFeedbacksOptions {\n return { collectScreenshotUrls: typeof this.screenshotStorage?.delete === \"function\" };\n }\n\n private async withRelations(rows: readonly FeedbackRow[]): Promise<FeedbackRecord[]> {\n if (rows.length === 0) return [];\n const relationsOf = await this.readRelations(rows.map((row) => row.id));\n return rows.map((row) => ({ ...row, ...relationsOf(row.id) }));\n }\n\n /** Read the annotations and threads of `feedbackIds`, then hand out each feedback's. */\n private async readRelations(\n feedbackIds: readonly string[],\n ): Promise<(feedbackId: string) => { annotations: AnnotationRow[]; comments: CommentRow[] }> {\n const [annotations, comments] = await Promise.all([\n this.gateway.findAnnotations(feedbackIds),\n this.gateway.findComments(feedbackIds),\n ]);\n const annotationsOf = groupByFeedback(annotations);\n const commentsOf = groupByFeedback(comments);\n return (feedbackId) => ({\n annotations: annotationsOf.get(feedbackId) ?? [],\n comments: commentsOf.get(feedbackId) ?? [],\n });\n }\n\n /**\n * Value to persist on `screenshotUrl`: the storage URL, `null` when the\n * upload fails (an inline fallback would bloat the database unnoticed\n * during a storage outage), or the inline data URL without storage.\n *\n * @param dataUrl - Screenshot submitted with the feedback, if any.\n * @param attempt - `feedbackId` is the id this create attempt will insert\n * the row under — unique per attempt and never client-supplied — so\n * racing attempts on one `clientId` never write the same object;\n * `clientId` is only reported in logs.\n */\n private async persistScreenshot(\n dataUrl: string | null | undefined,\n { feedbackId, clientId }: { feedbackId: string; clientId: string },\n ): Promise<string | null> {\n if (!dataUrl) return null;\n if (this.screenshotStorage) {\n try {\n const { url } = await this.screenshotStorage.upload(dataUrl, {\n feedbackId,\n mimeType: screenshotMimeType(dataUrl),\n });\n return url;\n } catch (error) {\n warnSafely(\n this.logger,\n \"[siteping] DrizzleStore: screenshotStorage.upload failed — feedback saved without screenshot\",\n { clientId, feedbackId, error },\n );\n return null;\n }\n }\n if (!this.inlineScreenshotWarned) {\n this.inlineScreenshotWarned = true;\n warnSafely(\n this.logger,\n \"[siteping] DrizzleStore: no screenshotStorage configured — screenshots are stored inline as base64. Configure a ScreenshotStorage for production.\",\n {},\n );\n }\n return dataUrl;\n }\n\n /**\n * Best-effort cleanup through `ScreenshotStorage.delete` of the screenshots\n * no stored feedback references any more; inline data URLs were never\n * uploaded and are skipped.\n *\n * The `ScreenshotStorage` contract makes every URL unique to the feedback id\n * it was uploaded for, and each create attempt uploads under a fresh id, so\n * a URL passed here belongs to one row only and no concurrent create can\n * acquire it: deleting it after the reference check cannot race with a new\n * reference. The check still runs because the owning row may exist after\n * all — an insert that committed although the driver reported a failure —\n * and, as defense in depth, it keeps objects a contract-breaking storage\n * shares across rows (a concurrent create that reuses such a URL after the\n * check is outside the contract and not covered). When the check itself\n * fails, every object is kept (an orphan is acceptable, a row pointing at a\n * deleted screenshot is not).\n *\n * Deletes run through a pool of at most {@link SCREENSHOT_DELETE_CONCURRENCY}\n * concurrent calls, so a project delete freeing thousands of objects does not\n * flood the storage. Failures are logged, never thrown. Each delete runs\n * inside its own promise, so a hook that throws synchronously is settled like\n * a rejection: it neither fails an already committed delete nor skips the\n * remaining objects.\n *\n * @param urls - `screenshotUrl` values of removed rows or of a discarded upload.\n * @param context - Identifiers added to the log lines (never payload data).\n */\n private async discardScreenshots(\n urls: ReadonlyArray<string | null | undefined>,\n context: Record<string, string> = {},\n ): Promise<void> {\n const remove = this.screenshotStorage?.delete?.bind(this.screenshotStorage);\n if (!remove) return;\n const candidates = [...new Set(urls.filter(isUploadedScreenshotUrl))];\n if (candidates.length === 0) return;\n\n let referenced: Set<string>;\n try {\n referenced = await this.findReferencedScreenshotUrls(candidates);\n } catch (lookupError) {\n warnSafely(\n this.logger,\n `${DRIZZLE_STORE_MESSAGE_PREFIX}: screenshot references could not be checked — screenshots kept`,\n { ...context, screenshotUrls: candidates, error: lookupError },\n );\n return;\n }\n const unreferenced = candidates.filter((url) => !referenced.has(url));\n const results = await settleWithConcurrencyLimit(unreferenced, SCREENSHOT_DELETE_CONCURRENCY, async (url) =>\n remove(url),\n );\n results.forEach((result, index) => {\n if (result.status === \"rejected\") {\n warnSafely(\n this.logger,\n `${DRIZZLE_STORE_MESSAGE_PREFIX}: screenshotStorage.delete failed — object left in place`,\n {\n ...context,\n screenshotUrl: unreferenced[index],\n error: result.reason,\n },\n );\n }\n });\n }\n\n /**\n * The URLs among `screenshotUrls` that a stored row still references, looked\n * up in batches of {@link SCREENSHOT_REFERENCE_LOOKUP_BATCH_SIZE}.\n *\n * @param screenshotUrls - Distinct uploaded screenshot URLs.\n */\n private async findReferencedScreenshotUrls(screenshotUrls: readonly string[]): Promise<Set<string>> {\n const referenced = new Set<string>();\n for (let start = 0; start < screenshotUrls.length; start += SCREENSHOT_REFERENCE_LOOKUP_BATCH_SIZE) {\n const batch = screenshotUrls.slice(start, start + SCREENSHOT_REFERENCE_LOOKUP_BATCH_SIZE);\n for (const url of await this.gateway.findReferencedScreenshotUrls(batch)) referenced.add(url);\n }\n return referenced;\n }\n}\n","/** Escape character for LIKE patterns, declared on every search with `ESCAPE`. */\nexport const LIKE_ESCAPE_CHARACTER = \"\\\\\";\n\n/**\n * Characters with a special meaning inside LIKE patterns: the `%` and `_`\n * wildcards plus the escape character itself. Each one is prefixed with\n * {@link LIKE_ESCAPE_CHARACTER} in a single pass, so a literal backslash in the\n * search becomes `\\\\` and never escapes the character after it (or the\n * trailing `%` of the contains pattern).\n */\nexport const LIKE_SPECIAL_CHARACTERS = /[\\\\%_]/g;\n\n/** A SQL substring-match operator. */\nexport type LikeOperator = \"ILIKE\" | \"LIKE\";\n\n/**\n * Operator matching the lowercased search against the `message_search` column.\n * Both operands are already lowercased in JavaScript, so a plain LIKE compares\n * them without any database case folding — identical on every dialect, locale\n * and collation.\n */\nexport const FOLDED_TEXT_LIKE_OPERATOR = \"LIKE\" satisfies LikeOperator;\n\n/**\n * Case-insensitive substring operator per dialect, used only as the fallback\n * for rows without `message_search` (written by the host application).\n * PostgreSQL's LIKE is case-sensitive, so it needs ILIKE, which folds case\n * with the column's collation / the database's `LC_CTYPE` (ASCII-only under\n * `C`). SQLite's LIKE folds only ASCII case.\n */\nexport const CASE_INSENSITIVE_LIKE_OPERATOR = {\n postgres: \"ILIKE\",\n sqlite: \"LIKE\",\n} as const satisfies Record<string, LikeOperator>;\n","/** Name of the scalar \"largest argument\" SQL function in each dialect. */\nexport type GreatestValueFunction = \"GREATEST\" | \"MAX\";\n\n/**\n * Scalar \"largest argument\" function per dialect — keeps a stamped\n * `updatedAt` from landing before the row's own timestamps.\n */\nexport const GREATEST_VALUE_FUNCTION = {\n postgres: \"GREATEST\",\n sqlite: \"MAX\",\n} as const satisfies Record<string, GreatestValueFunction>;\n\n/** CTE holding the feedback row the single-statement PostgreSQL insert actually wrote (none on a `clientId` conflict). */\nexport const INSERTED_FEEDBACK_CTE_ALIAS = \"siteping_inserted_feedback\";\n\n/** CTE of the PostgreSQL annotation insert chained to {@link INSERTED_FEEDBACK_CTE_ALIAS}. */\nexport const INSERTED_ANNOTATIONS_CTE_ALIAS = \"siteping_inserted_annotations\";\n\n/** Alias of the `VALUES` list an insert-select reads its annotation or comment rows from. */\nexport const VALUES_LIST_ALIAS = \"siteping_values\";\n\n/**\n * Longest identifier PostgreSQL keeps (`NAMEDATALEN` - 1), in bytes: it\n * silently truncates longer table, index and constraint names.\n */\nexport const POSTGRES_IDENTIFIER_MAX_BYTES = 63;\n","import type { CommentRecord } from \"@siteping/core\";\nimport { type Column, type SQL, sql, type Table } from \"drizzle-orm\";\n\n/** The columns a comment insert reads besides its own values — satisfied by both dialects' tables. */\nexport interface CommentInsertTables {\n feedbacks: Table & { id: Column };\n comments: Table & { feedbackId: Column; position: Column; authorRole: Column };\n}\n\n/**\n * Values and guard of a comment insert-select, one statement on both\n * dialects: the comment lands as the last of its thread (`position` one past\n * the thread's highest), and only when its feedback exists and — for a\n * `client` comment — its thread holds fewer than `maxComments` of them.\n *\n * @param tables - The feedback and comment tables of the dialect.\n * @param comment - The comment to insert.\n * @param maxComments - Most `client` comments a thread holds.\n */\nexport function guardedCommentInsert(\n { feedbacks, comments }: CommentInsertTables,\n comment: CommentRecord,\n maxComments: number,\n): { values: Record<string, unknown>; condition: SQL } {\n const inThread = sql`${comments.feedbackId} = ${comment.feedbackId}`;\n const exists = sql`EXISTS (SELECT 1 FROM ${feedbacks} WHERE ${feedbacks.id} = ${comment.feedbackId})`;\n return {\n values: {\n ...comment,\n position: sql`(SELECT COALESCE(MAX(${comments.position}) + 1, 0) FROM ${comments} WHERE ${inThread})`,\n },\n condition:\n comment.authorRole === \"client\"\n ? sql`${exists} AND (SELECT COUNT(*) FROM ${comments} WHERE ${inThread} AND ${comments.authorRole} = 'client') < ${maxComments}`\n : exists,\n };\n}\n","import { type Column, type SQL, sql } from \"drizzle-orm\";\nimport { INLINE_SCREENSHOT_URL_LIKE_PATTERN } from \"../constants/screenshots.js\";\nimport type { DeletedFeedbacks, DeleteFeedbacksOptions } from \"./gateway.js\";\n\n/**\n * The stored `screenshotUrl` of a row, or `NULL` when it holds an inline data\n * URL: inline screenshots were never uploaded, so there is nothing to clean up,\n * and reading them back would materialize megabytes per row.\n *\n * @param screenshotUrl - The feedback table's `screenshotUrl` column.\n */\nfunction uploadedScreenshotUrl(screenshotUrl: Column): SQL<string | null> {\n return sql<\n string | null\n >`CASE WHEN ${screenshotUrl} LIKE ${INLINE_SCREENSHOT_URL_LIKE_PATTERN} THEN NULL ELSE ${screenshotUrl} END`;\n}\n\n/**\n * `RETURNING` columns of a feedback delete: the id alone (enough to tell a hit\n * from a miss), plus the uploaded `screenshotUrl` only when the caller will\n * clean up screenshots. Inline base64 data URLs are never read back — they\n * would materialize megabytes per row for nothing, and can exceed an HTTP\n * driver's response-size limit.\n *\n * @param columns - Feedback table columns (`id`, `screenshotUrl`) of the dialect.\n * @param options - Whether the screenshot URLs are needed.\n */\nexport function deletedFeedbackColumns<IdColumn extends Column>(\n columns: { id: IdColumn; screenshotUrl: Column },\n { collectScreenshotUrls }: DeleteFeedbacksOptions,\n): Record<string, IdColumn | SQL<string | null>> {\n return collectScreenshotUrls\n ? { id: columns.id, screenshotUrl: uploadedScreenshotUrl(columns.screenshotUrl) }\n : { id: columns.id };\n}\n\n/**\n * What a delete built with {@link deletedFeedbackColumns} removed.\n *\n * @param rows - Rows returned by that delete.\n */\nexport function toDeletedFeedbacks(rows: ReadonlyArray<Record<string, unknown>>): DeletedFeedbacks {\n return {\n deletedCount: rows.length,\n screenshotUrls: rows.flatMap((row) =>\n \"screenshotUrl\" in row ? [typeof row.screenshotUrl === \"string\" ? row.screenshotUrl : null] : [],\n ),\n };\n}\n","import { type Column, desc, type SQL } from \"drizzle-orm\";\n\n/**\n * Internal feedback columns that are never part of the feedback record: the\n * `creationSequence` ordinal (drives the ordering on PostgreSQL; SQLite uses\n * its implicit `rowid` instead, so the libSQL table has no such column) and the\n * `messageSearch` normalized copy of the message (drives the text search).\n */\nexport interface InternalFeedbackColumns {\n creationSequence?: Column;\n messageSearch: Column;\n}\n\n/**\n * The columns a feedback row is read from — every column except the internal\n * ones (`creationSequence`, `messageSearch`).\n *\n * @param columns - `getTableColumns(feedbacksTable)`.\n * @returns The same columns without the internal ones.\n */\nexport function feedbackRecordColumns<Columns extends InternalFeedbackColumns>(\n columns: Columns,\n): Omit<Columns, keyof InternalFeedbackColumns> {\n const { creationSequence: _creationSequence, messageSearch: _messageSearch, ...recordColumns } = columns;\n return recordColumns;\n}\n\n/**\n * `ORDER BY` of the feedback list: newest `createdAt` first, ties broken by the\n * database-wide insertion ordinal. `createdAt` is the injected clock\n * value as is, so rows created in the same millisecond — by one store\n * instance, several processes or the host application inserting directly —\n * would otherwise come back in an undefined order, and offset pages would\n * overlap or skip rows.\n *\n * @param createdAt - The feedback `createdAt` column.\n * @param insertionOrder - Ordinal the database assigns on every insert, the\n * store's or not: the PostgreSQL identity column, SQLite's `rowid`.\n */\nexport function newestFeedbackFirst(createdAt: Column, insertionOrder: Column | SQL): SQL[] {\n return [desc(createdAt), desc(insertionOrder)];\n}\n","import { and, type Column, eq, inArray, isNull, type SQL, sql } from \"drizzle-orm\";\nimport {\n FOLDED_TEXT_LIKE_OPERATOR,\n LIKE_ESCAPE_CHARACTER,\n LIKE_SPECIAL_CHARACTERS,\n type LikeOperator,\n} from \"../constants/search.js\";\nimport type { FeedbackFilter, FeedbackRow } from \"./gateway.js\";\n\n/** Feedback columns the list filters read — satisfied by both dialects' tables. */\nexport interface FeedbackFilterColumns {\n projectName: Column;\n type: Column;\n status: Column;\n url: Column;\n urlPattern: Column;\n message: Column;\n /** The message lowercased in JavaScript on insert; `NULL` on rows the store did not write. */\n messageSearch: Column;\n}\n\n/** `%search%` with LIKE wildcards escaped, so the search matches literally. */\nexport function toContainsPattern(search: string): string {\n const escaped = search.replace(LIKE_SPECIAL_CHARACTERS, (character) => `${LIKE_ESCAPE_CHARACTER}${character}`);\n return `%${escaped}%`;\n}\n\n/**\n * Case folding of the text search — the same Unicode-aware `toLowerCase()` the\n * standard store filter (`applyFeedbackFilters` in core) applies to both the\n * message and the search, so a SQL store matches exactly what an in-memory one does.\n *\n * @param text - A message to store in its searchable form, or a search term.\n * @returns The lowercased text.\n */\nexport function toSearchableText(text: string): string {\n return text.toLowerCase();\n}\n\n/**\n * `target <operator> '%search%' ESCAPE '\\'` — a literal substring match.\n *\n * @param target - Column or expression searched.\n * @param likeOperator - The dialect's LIKE operator.\n * @param search - The search term, matched literally.\n */\nexport function containsCondition(target: Column | SQL, likeOperator: LikeOperator, search: string): SQL {\n return sql`${target} ${sql.raw(likeOperator)} ${toContainsPattern(search)} ESCAPE ${LIKE_ESCAPE_CHARACTER}`;\n}\n\n/**\n * The feedback row as inserted: the record plus its `messageSearch`, so the\n * text search never depends on the database's case folding.\n *\n * @param feedback - The feedback row the store builds.\n */\nexport function withSearchableMessage<Row extends FeedbackRow>(feedback: Row): Row & { messageSearch: string } {\n return { ...feedback, messageSearch: toSearchableText(feedback.message) };\n}\n\n/**\n * Case-insensitive message search, folded like the standard store filter on\n * every dialect: the lowercased search is matched with a plain LIKE against\n * `message_search` (lowercased in JavaScript on insert), so neither SQLite's\n * ASCII-only LIKE nor a PostgreSQL `C` collation / `LC_CTYPE` changes the\n * result. Rows without `message_search` fall back to the raw message under the\n * dialect's case-insensitive operator, whose folding may be ASCII-only.\n *\n * @param columns - `message` and `messageSearch` of the feedback table.\n * @param fallbackLikeOperator - The dialect's case-insensitive LIKE operator.\n * @param search - The search term, matched literally.\n */\nexport function messageContainsCondition(\n columns: Pick<FeedbackFilterColumns, \"message\" | \"messageSearch\">,\n fallbackLikeOperator: LikeOperator,\n search: string,\n): SQL {\n const foldedSearch = toSearchableText(search);\n const foldedMatch = containsCondition(columns.messageSearch, FOLDED_TEXT_LIKE_OPERATOR, foldedSearch);\n const fallbackMatch = containsCondition(columns.message, fallbackLikeOperator, foldedSearch);\n return sql`(${foldedMatch} OR (${isNull(columns.messageSearch)} AND ${fallbackMatch}))`;\n}\n\n/**\n * WHERE clause of `getFeedbacks`, built only from dialect-agnostic Drizzle\n * operators; the dialect supplies its columns and the case-insensitive LIKE\n * operator of the text-search fallback (see {@link messageContainsCondition}).\n */\nexport function buildFeedbackWhere(\n columns: FeedbackFilterColumns,\n filter: FeedbackFilter,\n fallbackLikeOperator: LikeOperator,\n): SQL | undefined {\n const conditions: SQL[] = [eq(columns.projectName, filter.projectName)];\n if (filter.type) conditions.push(eq(columns.type, filter.type));\n if (filter.statuses) conditions.push(inArray(columns.status, [...filter.statuses]));\n if (filter.url) conditions.push(eq(columns.url, filter.url));\n if (filter.urlPattern) conditions.push(eq(columns.urlPattern, filter.urlPattern));\n if (filter.search) conditions.push(messageContainsCondition(columns, fallbackLikeOperator, filter.search));\n return and(...conditions);\n}\n","import { type Column, getTableColumns, is, SQL, sql, type Table } from \"drizzle-orm\";\nimport { VALUES_LIST_ALIAS } from \"../constants/sql.js\";\n\n/** Wrap a bound parameter so the dialect reads it as the target column's type. */\nexport type EncodeParam = (param: SQL, column: Column) => SQL;\n\nconst keepParamAsIs: EncodeParam = (param) => param;\n\n/**\n * The columns a record is read from — every column except the internal\n * `position` ordinal of annotations and comments, which only drives the\n * ordering.\n *\n * @param columns - `getTableColumns(annotationsTable)` or `getTableColumns(commentsTable)`.\n * @returns The same columns without `position`.\n */\nexport function recordColumns<Columns extends { position: Column }>(columns: Columns): Omit<Columns, \"position\"> {\n const { position: _position, ...record } = columns;\n return record;\n}\n\n/**\n * `SELECT * FROM (VALUES …) AS alias WHERE <condition>` over `rows`, for\n * `db.insert(table).select(…)`: the insert lands only when `condition` holds,\n * inside the same statement or batch as the rest of the write.\n *\n * Values follow `getTableColumns` order — the column list Drizzle writes for\n * an insert-select (these tables have no generated columns) — and go through\n * each column's driver mapping (JSON, timestamps); a value that already is\n * SQL, such as a subquery, is inlined as is.\n *\n * @param table - The table inserted into.\n * @param rows - One value per column key, per row (non-empty).\n * @param condition - Guard evaluated once per statement (e.g. \"the feedback row exists\").\n * @param encodeParam - Dialect hook to type each parameter (PostgreSQL casts, SQLite needs nothing).\n */\nexport function selectValues(\n table: Table,\n rows: ReadonlyArray<Record<string, unknown>>,\n condition: SQL,\n encodeParam: EncodeParam = keepParamAsIs,\n): SQL {\n const columns = Object.entries(getTableColumns(table));\n const tuples = rows.map((row) => {\n const values = columns.map(([key, column]) => {\n const value = row[key];\n return is(value, SQL) ? value : encodeParam(sql`${sql.param(value ?? null, column)}`, column);\n });\n return sql`(${sql.join(values, sql`, `)})`;\n });\n return sql`SELECT * FROM (VALUES ${sql.join(tuples, sql`, `)}) AS ${sql.identifier(VALUES_LIST_ALIAS)} WHERE ${condition}`;\n}\n","import { type Column, type SQL, sql } from \"drizzle-orm\";\nimport type { GreatestValueFunction } from \"../constants/sql.js\";\n\n/**\n * SQL for the `updatedAt` to store on an update: the wall-clock `updatedAt`,\n * raised to the row's own `createdAt` / `updatedAt` when those are later.\n *\n * Why: the row may have been written by another process whose clock runs\n * ahead of this one — an update must still not record `updatedAt < createdAt`\n * or move `updatedAt` backwards. Clamping in SQL holds across store instances\n * and processes, unlike an in-memory clock.\n *\n * @param table - Feedback table columns the clamp reads.\n * @param updatedAt - Wall-clock time of the update.\n * @param greatestFunction - The dialect's \"largest argument\" function.\n */\nexport function monotonicUpdatedAt(\n table: { createdAt: Column; updatedAt: Column },\n updatedAt: Date,\n greatestFunction: GreatestValueFunction,\n): SQL<Date> {\n return sql<Date>`${sql.raw(greatestFunction)}(${sql.param(updatedAt, table.updatedAt)}, ${table.createdAt}, ${table.updatedAt})`;\n}\n"],"mappings":";AASO,SAAS,UAAmC,IAAwB;AACzE,SAAO,cAAc,KAAM,GAAG,WAAwB;AACxD;;;ACyDO,SAAS,SAAS,OAAuD;AAC9E,SAAO,OAAO,UAAU,YAAY,UAAU;AAChD;AAWO,SAAS,OAA8B,OAAgB,KAAqC;AACjG,SAAO,SAAS,KAAK,KAAK,OAAO;AACnC;;;AC2wBO,IAAM,4BAA4B;AAwDlC,IAAM,qBAAN,cAAiC,MAAM;AAAA,EACnC,OAAO;AAAA,EAChB,YAAY,UAAU,oBAAoB,SAAwB;AAChE,UAAM,SAAS,OAAO;AACtB,SAAK,OAAO;AAAA,EACd;AACF;AAOO,IAAM,sBAAN,cAAkC,MAAM;AAAA,EACpC,OAAO;AAAA,EAChB,YAAY,UAAU,oBAAoB,SAAwB;AAChE,UAAM,SAAS,OAAO;AACtB,SAAK,OAAO;AAAA,EACd;AACF;AAQO,IAAM,wBAAN,cAAoC,MAAM;AAAA,EACtC,OAAO;AAAA,EAChB,YAAY,UAAU,oCAAoC,SAAwB;AAChF,UAAM,SAAS,OAAO;AACtB,SAAK,OAAO;AAAA,EACd;AACF;AAOO,IAAM,kBAAN,cAA8B,MAAM;AAAA,EAChC,OAAO;AAAA,EAChB,YAAY,UAAU,uBAAuB,SAAwB;AACnE,UAAM,SAAS,OAAO;AACtB,SAAK,OAAO;AAAA,EACd;AACF;AASO,IAAM,yBAAN,cAAqC,MAAM;AAAA,EACvC,OAAO;AAAA,EAChB,YAAY,UAAU,gCAAgC,SAAwB;AAC5E,UAAM,SAAS,OAAO;AACtB,SAAK,OAAO;AAAA,EACd;AACF;AAKA,SAAS,aAA+B,OAAgB,MAAiC;AACvF,SAAO,OAAO,OAAO,MAAM,KAAK,MAAM,SAAS;AACjD;AAOO,SAAS,gBAAgB,OAAuF;AACrH,MAAI,iBAAiB,mBAAoB,QAAO;AAEhD,SAAO,aAAa,OAAO,iBAAiB,KAAK,aAAa,OAAO,OAAO;AAC9E;AAOO,SAAS,iBACd,OACwE;AACxE,MAAI,iBAAiB,oBAAqB,QAAO;AAEjD,SAAO,aAAa,OAAO,iBAAiB,KAAK,aAAa,OAAO,OAAO;AAC9E;AAQO,SAAS,mBAAmB,OAAkF;AACnH,MAAI,iBAAiB,sBAAuB,QAAO;AACnD,SAAO,aAAa,OAAO,mBAAmB;AAChD;AAGO,SAAS,aAAa,OAAsE;AACjG,MAAI,iBAAiB,gBAAiB,QAAO;AAC7C,SAAO,aAAa,OAAO,aAAa;AAC1C;AAGO,SAAS,oBACd,OACsE;AACtE,MAAI,iBAAiB,uBAAwB,QAAO;AACpD,SAAO,aAAa,OAAO,sBAAsB;AACnD;;;AC9/BA,eAAsB,2BACpB,OACA,aACA,MAC8C;AAC9C,QAAM,UAA+C,IAAI,MAAM,MAAM,MAAM;AAC3E,MAAI,YAAY;AAEhB,QAAM,YAAY,YAA2B;AAC3C,WAAO,YAAY,MAAM,QAAQ;AAC/B,YAAM,QAAQ;AACd,mBAAa;AACb,UAAI;AACF,gBAAQ,KAAK,IAAI,EAAE,QAAQ,aAAa,OAAO,MAAM,KAAK,MAAM,KAAK,CAAS,EAAE;AAAA,MAClF,SAAS,OAAO;AACd,gBAAQ,KAAK,IAAI,EAAE,QAAQ,YAAY,QAAQ,MAAM;AAAA,MACvD;AAAA,IACF;AAAA,EACF;AAEA,QAAM,cAAc,KAAK,IAAI,KAAK,IAAI,GAAG,WAAW,GAAG,MAAM,MAAM;AACnE,QAAM,QAAQ,IAAI,MAAM,KAAK,EAAE,QAAQ,YAAY,GAAG,SAAS,CAAC;AAChE,SAAO;AACT;;;ACZO,IAAM,qBAAqB;AAE3B,IAAM,iBAAiB;AAiB9B,SAAS,kBAAkB,OAA2B,UAA0B;AAC9E,SAAO,UAAU,UAAa,OAAO,SAAS,KAAK,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,CAAC,IAAI;AAC1F;AAaO,SAAS,gBAAgB,OAA0D;AACxF,QAAM,OAAO,kBAAkB,MAAM,MAAM,CAAC;AAC5C,QAAM,QAAQ,KAAK,IAAI,kBAAkB,MAAM,OAAO,kBAAkB,GAAG,cAAc;AACzF,SAAO,EAAE,MAAM,OAAO,OAAO,OAAO,KAAK,MAAM;AACjD;AAiBO,SAAS,oBAAoB,MAAuB;AACzD,SAAO,CAAC,OAAO,cAAc,IAAI;AACnC;;;AClDO,SAAS,WAAW,QAA4B,SAAiB,SAAwC;AAC9G,MAAI;AACF,YAAQ,QAAQ,OAAO,KAAK,SAAS,OAAO,CAAC,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EAC/D,QAAQ;AAAA,EAER;AACF;;;ACgDO,IAAM,gCAAgC;AAWtC,SAAS,mBAAmB,SAAyB;AAC1D,SAAO,wCAAwC,KAAK,OAAO,IAAI,CAAC,GAAG,YAAY,KAAK;AACtF;;;ACzDO,SAAS,sBACd,OACA,KACkB;AAClB,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,YAAY,IAAI;AAAA,IAChB,aAAa,MAAM;AAAA,IACnB,OAAO,MAAM;AAAA,IACb,aAAa,MAAM;AAAA,IACnB,YAAY,MAAM;AAAA,IAClB,WAAW,MAAM,aAAa;AAAA,IAC9B,YAAY,MAAM;AAAA,IAClB,YAAY,MAAM;AAAA,IAClB,aAAa,MAAM;AAAA,IACnB,cAAc,MAAM;AAAA,IACpB,WAAW,MAAM,aAAa;AAAA,IAC9B,MAAM,MAAM;AAAA,IACZ,MAAM,MAAM;AAAA,IACZ,MAAM,MAAM;AAAA,IACZ,MAAM,MAAM;AAAA,IACZ,SAAS,MAAM;AAAA,IACf,SAAS,MAAM;AAAA,IACf,WAAW,MAAM;AAAA,IACjB,WAAW,MAAM;AAAA,IACjB,kBAAkB,MAAM;AAAA,IACxB,WAAW,IAAI;AAAA,EACjB;AACF;AAGO,SAAS,mBACd,OACA,KACe;AACf,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,YAAY,IAAI;AAAA,IAChB,MAAM,MAAM;AAAA,IACZ,YAAY,MAAM;AAAA,IAClB,aAAa,MAAM;AAAA,IACnB,YAAY,MAAM;AAAA,IAClB,UAAU,MAAM;AAAA,IAChB,WAAW,IAAI,OAAO,oBAAI,KAAK;AAAA,EACjC;AACF;AAWO,SAAS,oBACd,OACA,KACgB;AAChB,QAAM,MAAM,IAAI,OAAO,oBAAI,KAAK;AAChC,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,MAAM,MAAM;AAAA,IACZ,SAAS,MAAM;AAAA,IACf,QAAQ,MAAM;AAAA,IACd,aAAa,MAAM;AAAA,IACnB,KAAK,MAAM;AAAA,IACX,YAAY,MAAM,cAAc;AAAA,IAChC,YAAY,MAAM;AAAA,IAClB,aAAa,MAAM;AAAA,IACnB,UAAU,MAAM;AAAA,IAChB,WAAW,MAAM;AAAA,IACjB,UAAU,MAAM;AAAA,IAChB,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,WAAW;AAAA,IACX,aAAa,MAAM,YAAY;AAAA,MAAI,CAAC,QAClC,sBAAsB,KAAK,EAAE,IAAI,IAAI,aAAa,GAAG,YAAY,IAAI,IAAI,IAAI,CAAC;AAAA,IAChF;AAAA,IACA,eAAe,MAAM,qBAAqB;AAAA,IAC1C,kBAAkB,MAAM,oBAAoB;AAAA,IAC5C,aAAa,MAAM,eAAe;AAAA,EACpC;AACF;;;ACjHO,IAAM,wBAAwB;AAG9B,IAAM,wBAAwB;AAS9B,SAAS,eAAe,MAAsB;AACnD,SAAO,KAAK,QAAQ,uBAAuB,qBAAqB;AAClE;AAEA,SAAS,cAAc,OAAkD;AACvE,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,YAAY,OAAO,eAAe,KAAK;AAC7C,SAAO,cAAc,OAAO,aAAa,cAAc;AACzD;AAOO,SAAS,gBAAuB,OAAqB;AAC1D,MAAI,OAAO,UAAU,SAAU,QAAO,eAAe,KAAK;AAC1D,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,eAAe;AAC1D,MAAI,CAAC,cAAc,KAAK,EAAG,QAAO;AAClC,SAAO,OAAO;AAAA,IACZ,OAAO,QAAQ,KAAK,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,MAAM,CAAC,eAAe,GAAG,GAAG,gBAAgB,IAAI,CAAC,CAAC;AAAA,EACzF;AACF;;;AC7CO,IAAM,+BAA+B;AAerC,IAAM,qCAAqC;AAM3C,IAAM,iCAAiC;AAUvC,IAAM,iCAAiC;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;;;AC3CO,IAAM,4BAA4B;;;ACNlC,IAAM,+BAA+B;AAOrC,IAAM,yCAAyC;AAG/C,IAAM,qCAAqC,GAAG,4BAA4B;;;ACXjF,SAAS,yBAAyB;AAalC,SAAS,oBAAoB,OAAuB;AAClD,SAAO,iBAAiB,qBAAqB,MAAM,QAAQ,WAAW,kCAAkC;AAC1G;AAkBA,SAAS,iBAAiB,OAAgB,QAA4B,CAAC,GAAY;AACjF,MAAI,EAAE,iBAAiB,OAAQ,QAAO;AACtC,QAAM,QAAQ,CAAC,GAAG,OAAO,KAAK;AAC9B,QAAM,aAAa,MAAM,UAAU,UAAa,CAAC,MAAM,SAAS,MAAM,KAAK;AAC3E,QAAM,QAAQ,aAAa,iBAAiB,MAAM,OAAO,KAAK,IAAI;AAClE,MAAI,oBAAoB,KAAK,EAAG,QAAO;AACvC,QAAM,OAAO,IAAI,MAAM,MAAM,SAAS,aAAa,EAAE,MAAM,IAAI,MAAS;AACxE,QAAM,OAAO,CAAC,WAAwC,EAAE,OAAO,UAAU,MAAM,cAAc,KAAK;AAClG,SAAO,iBAAiB,MAAM,EAAE,MAAM,KAAK,MAAM,IAAI,GAAG,OAAO,KAAK,MAAM,KAAK,EAAE,CAAC;AAClF,QAAM,cAAc,+BAA+B,IAAI,CAAC,UAAU,CAAC,OAAO,QAAQ,IAAI,OAAO,KAAK,CAAC,CAAU;AAC7G,SAAO,OAAO,OAAO,MAAM,OAAO,YAAY,YAAY,OAAO,CAAC,CAAC,EAAE,KAAK,MAAM,UAAU,MAAS,CAAC,CAAC;AACvG;AAOO,SAAS,iBAAiB,SAAiD;AAChF,SAAO,IAAI,MAAM,SAAS;AAAA,IACxB,IAAI,QAAQ,UAAU,UAAU;AAC9B,YAAM,SAAkB,QAAQ,IAAI,QAAQ,UAAU,QAAQ;AAC9D,UAAI,OAAO,WAAW,WAAY,QAAO;AACzC,aAAO,UAAU,SAAoB;AACnC,YAAI;AACF,iBAAO,MAAM,OAAO,MAAM,QAAQ,IAAI;AAAA,QACxC,SAAS,OAAO;AACd,gBAAM,iBAAiB,KAAK;AAAA,QAC9B;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AACH;;;AC0BA,IAAM,eAAmC;AAAA,EACvC,OAAO;AAAA,EAAC;AACV;AAGA,IAAM,cAAc,MAAY,oBAAI,KAAK;AAGzC,SAAS,qBAAqB,OAAyB;AACrD,SAAO,gBAAgB,KAAK,KAAK,iBAAiB,KAAK,KAAK,mBAAmB,KAAK;AACtF;AAYA,eAAe,gBACb,UACA,aACA,KACiB;AACjB,MAAI;AACF,WAAO,MAAM,IAAI;AAAA,EACnB,SAAS,OAAO;AACd,QAAI,qBAAqB,KAAK,EAAG,OAAM;AACvC,UAAM,UAAU,OAAO,QAAQ,WAAW,EACvC,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM,GAAG,IAAI,IAAI,KAAK,EAAE,EACzC,KAAK,GAAG;AACX,UAAM,IAAI,sBAAsB,GAAG,4BAA4B,IAAI,QAAQ,YAAY,OAAO,KAAK;AAAA,MACjG,OAAO;AAAA,IACT,CAAC;AAAA,EACH;AACF;AAGA,SAAS,sBAAsB,OAAyB;AACtD,QAAM,OAAkB,CAAC;AACzB,WAAS,UAAU,OAAO,mBAAmB,SAAS,CAAC,KAAK,SAAS,OAAO,GAAG,UAAU,QAAQ,OAAO;AACtG,QAAI,QAAQ,IAAI,SAAS,MAAM,MAAM,+BAAgC,QAAO;AAC5E,SAAK,KAAK,OAAO;AAAA,EACnB;AACA,SAAO;AACT;AAGA,SAAS,gBAAoD,MAA0C;AACrG,QAAM,aAAa,oBAAI,IAAmB;AAC1C,aAAW,OAAO,MAAM;AACtB,UAAM,WAAW,WAAW,IAAI,IAAI,UAAU;AAC9C,QAAI,SAAU,UAAS,KAAK,GAAG;AAAA,QAC1B,YAAW,IAAI,IAAI,YAAY,CAAC,GAAG,CAAC;AAAA,EAC3C;AACA,SAAO;AACT;AAGA,SAAS,wBAAwB,KAA+C;AAC9E,SAAO,OAAO,QAAQ,YAAY,IAAI,SAAS,KAAK,CAAC,IAAI,WAAW,4BAA4B;AAClG;AASO,IAAM,uBAAN,MAAmD;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACT,yBAAyB;AAAA,EAEjC,YAAY,SAA6B,UAA+B,CAAC,GAAG;AAC1E,SAAK,UAAU,iBAAiB,OAAO;AACvC,SAAK,oBAAoB,QAAQ;AACjC,SAAK,SAAS,QAAQ,UAAU;AAChC,SAAK,MAAM,QAAQ,OAAO;AAAA,EAC5B;AAAA;AAAA,EAGA,MAAM,eAAe,MAAoD;AACvE,UAAM,EAAE,UAAU,QAAQ,IAAI,MAAM,KAAK,uBAAuB,IAAI;AACpE,QAAI,CAAC,QAAS,OAAM,IAAI,oBAAoB,4BAA4B,KAAK,QAAQ,iBAAiB;AACtG,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,MAAM,uBAAuB,WAAgE;AAC3F,UAAM,OAAO,gBAAgB,SAAS;AACtC,UAAM,cAAc,EAAE,UAAU,KAAK,SAAS;AAC9C,UAAM,WAAW,MAAM,gBAAgB,kBAAkB,aAAa,MAAM,KAAK,eAAe,KAAK,QAAQ,CAAC;AAC9G,QAAI,SAAU,QAAO,EAAE,UAAU,UAAU,SAAS,MAAM;AAI1D,UAAM,KAAK,OAAO,WAAW;AAC7B,UAAM,gBAAgB,MAAM,KAAK,kBAAkB,KAAK,mBAAmB;AAAA,MACzE,YAAY;AAAA,MACZ,UAAU,KAAK;AAAA,IACjB,CAAC;AACD,UAAM,EAAE,aAAa,GAAG,SAAS,IAAI,oBAAoB,MAAM;AAAA,MAC7D;AAAA,MACA,cAAc,MAAM,OAAO,WAAW;AAAA;AAAA;AAAA;AAAA,MAItC,KAAK,KAAK,IAAI;AAAA,IAChB,CAAC;AACD,UAAM,MAAmB,EAAE,GAAG,UAAU,cAAc;AAEtD,QAAI;AACJ,QAAI;AACF,iBAAW,MAAM;AAAA,QAAgB;AAAA,QAAkB;AAAA,QAAa,MAC9D,KAAK,QAAQ,eAAe,KAAK,WAAW;AAAA,MAC9C;AAAA,IACF,SAAS,OAAO;AAGd,YAAM,KAAK,mBAAmB,CAAC,aAAa,GAAG,EAAE,UAAU,KAAK,SAAS,CAAC;AAC1E,YAAM;AAAA,IACR;AACA,QAAI,UAAU;AACZ,aAAO,EAAE,UAAU,EAAE,GAAG,KAAK,aAAa,UAAU,CAAC,EAAE,GAAG,SAAS,KAAK;AAAA,IAC1E;AAOA,QAAI;AACJ,QAAI;AACF,eAAS,MAAM,gBAAgB,kBAAkB,aAAa,MAAM,KAAK,eAAe,KAAK,QAAQ,CAAC;AAAA,IACxG,UAAE;AACA,YAAM,KAAK,mBAAmB,CAAC,aAAa,GAAG,EAAE,UAAU,KAAK,SAAS,CAAC;AAAA,IAC5E;AACA,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI;AAAA,QACR,GAAG,4BAA4B,qCAAqC,KAAK,QAAQ;AAAA,MACnF;AAAA,IACF;AACA,WAAO,EAAE,UAAU,QAAQ,SAAS,MAAM;AAAA,EAC5C;AAAA,EAEA,MAAM,aAAa,gBAAsD;AACvE,UAAM,QAAQ,gBAAgB,cAAc;AAC5C,UAAM,EAAE,OAAO,KAAK,IAAI,gBAAgB,KAAK;AAC7C,UAAM,SAAyB,EAAE,aAAa,MAAM,YAAY;AAChE,QAAI,MAAM,KAAM,QAAO,OAAO,MAAM;AAEpC,QAAI,MAAM,YAAY,MAAM,SAAS,SAAS,EAAG,QAAO,WAAW,MAAM;AAAA,aAChE,MAAM,OAAQ,QAAO,WAAW,CAAC,MAAM,MAAM;AACtD,QAAI,MAAM,IAAK,QAAO,MAAM,MAAM;AAClC,QAAI,MAAM,WAAY,QAAO,aAAa,MAAM;AAChD,QAAI,MAAM,OAAQ,QAAO,SAAS,MAAM;AAKxC,QAAI,oBAAoB,IAAI,GAAG;AAC7B,aAAO,EAAE,WAAW,CAAC,GAAG,OAAO,MAAM,KAAK,QAAQ,eAAe,MAAM,EAAE;AAAA,IAC3E;AAEA,UAAM,EAAE,MAAM,MAAM,IAAI,MAAM,KAAK,QAAQ,cAAc,QAAQ,EAAE,OAAO,QAAQ,KAAK,CAAC;AACxF,WAAO,EAAE,WAAW,MAAM,KAAK,cAAc,IAAI,GAAG,MAAM;AAAA,EAC5D;AAAA,EAEA,MAAM,eAAe,UAAkD;AACrE,UAAM,MAAM,MAAM,KAAK,QAAQ,eAAe,eAAe,QAAQ,CAAC;AACtE,WAAO,OAAQ,MAAM,KAAK,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,OAAQ;AAAA,EAChE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,eAAe,aAAqB,MAAoD;AAC5F,UAAM,KAAK,eAAe,WAAW;AACrC,UAAM,EAAE,aAAa,IAAI,IAAI,MAAM,gBAAgB,kBAAkB,EAAE,GAAG,GAAG,YAAY;AACvF,YAAMA,eAAc,MAAM,KAAK,cAAc,CAAC,EAAE,CAAC;AAGjD,YAAMC,OAAM,MAAM,KAAK,QAAQ,aAAa,IAAI;AAAA,QAC9C,QAAQ,KAAK;AAAA,QACb,YAAY,KAAK;AAAA,QACjB,WAAW,KAAK,IAAI;AAAA,MACtB,CAAC;AACD,aAAO,EAAE,aAAAD,cAAa,KAAAC,KAAI;AAAA,IAC5B,CAAC;AACD,QAAI,CAAC,IAAK,OAAM,IAAI,mBAAmB;AACvC,WAAO,EAAE,GAAG,KAAK,GAAG,YAAY,EAAE,EAAE;AAAA,EACtC;AAAA,EAEA,MAAM,eAAe,aAAoC;AACvD,UAAM,KAAK,eAAe,WAAW;AACrC,UAAM,UAAU,MAAM;AAAA,MAAgB;AAAA,MAAkB,EAAE,GAAG;AAAA,MAAG,MAC9D,KAAK,QAAQ,WAAW,IAAI,KAAK,cAAc,CAAC;AAAA,IAClD;AACA,QAAI,CAAC,QAAS,OAAM,IAAI,mBAAmB;AAC3C,UAAM,KAAK,mBAAmB,QAAQ,cAAc;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsBA,MAAM,mBAAmB,sBAA6C;AACpE,UAAM,cAAc,eAAe,oBAAoB;AACvD,QAAI,CAAC,KAAK,cAAc,EAAE,uBAAuB;AAC/C,YAAM,gBAAgB,sBAAsB,EAAE,YAAY,GAAG,MAAM,KAAK,QAAQ,gBAAgB,WAAW,CAAC;AAC5G;AAAA,IACF;AACA,QAAI,2BAA2B,OAAO;AACtC,eAAS;AACP,YAAM,UAAU,MAAM;AAAA,QAAgB;AAAA,QAAsB,EAAE,YAAY;AAAA,QAAG,MAC3E,KAAK,QAAQ,mBAAmB,aAAa,yBAAyB;AAAA,MACxE;AACA,UAAI,QAAQ,eAAe,GAAG;AAC5B,mCAA2B,OAAO;AAClC,cAAM,KAAK,mBAAmB,QAAQ,cAAc;AACpD;AAAA,MACF;AAOA,YAAM,YAAY,MAAM;AAAA,QAAgB;AAAA,QAAsB,EAAE,YAAY;AAAA,QAAG,MAC7E,KAAK,QAAQ,eAAe,EAAE,YAAY,CAAC;AAAA,MAC7C;AACA,UAAI,cAAc,EAAG;AACrB,UAAI,aAAa,0BAA0B;AACzC,cAAM,IAAI;AAAA,UACR,GAAG,4BAA4B,0CAA0C,SAAS,4CAA4C,WAAW;AAAA,QAC3I;AAAA,MACF;AACA,iCAA2B;AAAA,IAC7B;AAAA,EACF;AAAA,EAEA,MAAM,uBAAuB,IAAY,aAAuC;AAC9E,WAAQ,MAAM,KAAK,QAAQ,gBAAgB,eAAe,EAAE,CAAC,MAAO,eAAe,WAAW;AAAA,EAChG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,MAAM,WAAW,YAAoB,MAAkD;AACrF,YAAQ,MAAM,KAAK,mBAAmB,YAAY,IAAI,GAAG;AAAA,EAC3D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,mBAAmB,qBAA6B,WAA8D;AAClH,UAAM,aAAa,eAAe,mBAAmB;AACrD,UAAM,OAAO,gBAAgB,SAAS;AACtC,UAAM,UAAU,mBAAmB,MAAM,EAAE,IAAI,OAAO,WAAW,GAAG,YAAY,KAAK,KAAK,IAAI,EAAE,CAAC;AACjG,UAAM,cAAc,EAAE,YAAY,UAAU,KAAK,SAAS;AAC1D,QAAI;AACJ,QAAI;AACF,iBAAW,MAAM;AAAA,QAAgB;AAAA,QAAc;AAAA,QAAa,MAC1D,KAAK,QAAQ,cAAc,SAAS,yBAAyB;AAAA,MAC/D;AAAA,IACF,SAAS,OAAO;AAOd,UAAI,CAAC,sBAAsB,KAAK,EAAG,OAAM;AACzC,YAAM,eAAe,MAAM,KAAK,QAAQ,gBAAgB,UAAU,EAAE;AAAA,QAClE,CAACC,iBAAgBA,iBAAgB;AAAA,QACjC,MAAM;AAAA,MACR;AACA,UAAI,aAAc,OAAM,IAAI,mBAAmB;AAC/C,YAAM;AAAA,IACR;AACA,QAAI,SAAU,QAAO,EAAE,SAAS,SAAS,KAAK;AAG9C,UAAM,WAAW,MAAM;AAAA,MAAgB;AAAA,MAAc;AAAA,MAAa,MAChE,KAAK,QAAQ,sBAAsB,KAAK,QAAQ;AAAA,IAClD;AACA,QAAI,SAAU,QAAO,EAAE,SAAS,UAAU,SAAS,MAAM;AACzD,UAAM,cAAc,MAAM;AAAA,MAAgB;AAAA,MAAc;AAAA,MAAa,MACnE,KAAK,QAAQ,gBAAgB,UAAU;AAAA,IACzC;AACA,QAAI,gBAAgB,KAAM,OAAM,IAAI,mBAAmB;AACvD,UAAM,IAAI,gBAAgB,0BAA0B,yBAAyB,kBAAkB;AAAA,EACjG;AAAA,EAEA,MAAM,cAAc,qBAA6B,oBAA2C;AAC1F,UAAM,aAAa,eAAe,mBAAmB;AACrD,UAAM,YAAY,eAAe,kBAAkB;AACnD,UAAM,UAAU,MAAM;AAAA,MAAgB;AAAA,MAAiB,EAAE,YAAY,UAAU;AAAA,MAAG,MAChF,KAAK,QAAQ,cAAc,YAAY,SAAS;AAAA,IAClD;AACA,QAAI,CAAC,QAAS,OAAM,IAAI,mBAAmB;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,gBAAwC;AAC9C,WAAO,EAAE,uBAAuB,OAAO,KAAK,mBAAmB,WAAW,WAAW;AAAA,EACvF;AAAA,EAEA,MAAc,cAAc,MAAyD;AACnF,QAAI,KAAK,WAAW,EAAG,QAAO,CAAC;AAC/B,UAAM,cAAc,MAAM,KAAK,cAAc,KAAK,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC;AACtE,WAAO,KAAK,IAAI,CAAC,SAAS,EAAE,GAAG,KAAK,GAAG,YAAY,IAAI,EAAE,EAAE,EAAE;AAAA,EAC/D;AAAA;AAAA,EAGA,MAAc,cACZ,aAC2F;AAC3F,UAAM,CAAC,aAAa,QAAQ,IAAI,MAAM,QAAQ,IAAI;AAAA,MAChD,KAAK,QAAQ,gBAAgB,WAAW;AAAA,MACxC,KAAK,QAAQ,aAAa,WAAW;AAAA,IACvC,CAAC;AACD,UAAM,gBAAgB,gBAAgB,WAAW;AACjD,UAAM,aAAa,gBAAgB,QAAQ;AAC3C,WAAO,CAAC,gBAAgB;AAAA,MACtB,aAAa,cAAc,IAAI,UAAU,KAAK,CAAC;AAAA,MAC/C,UAAU,WAAW,IAAI,UAAU,KAAK,CAAC;AAAA,IAC3C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAc,kBACZ,SACA,EAAE,YAAY,SAAS,GACC;AACxB,QAAI,CAAC,QAAS,QAAO;AACrB,QAAI,KAAK,mBAAmB;AAC1B,UAAI;AACF,cAAM,EAAE,IAAI,IAAI,MAAM,KAAK,kBAAkB,OAAO,SAAS;AAAA,UAC3D;AAAA,UACA,UAAU,mBAAmB,OAAO;AAAA,QACtC,CAAC;AACD,eAAO;AAAA,MACT,SAAS,OAAO;AACd;AAAA,UACE,KAAK;AAAA,UACL;AAAA,UACA,EAAE,UAAU,YAAY,MAAM;AAAA,QAChC;AACA,eAAO;AAAA,MACT;AAAA,IACF;AACA,QAAI,CAAC,KAAK,wBAAwB;AAChC,WAAK,yBAAyB;AAC9B;AAAA,QACE,KAAK;AAAA,QACL;AAAA,QACA,CAAC;AAAA,MACH;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA6BA,MAAc,mBACZ,MACA,UAAkC,CAAC,GACpB;AACf,UAAM,SAAS,KAAK,mBAAmB,QAAQ,KAAK,KAAK,iBAAiB;AAC1E,QAAI,CAAC,OAAQ;AACb,UAAM,aAAa,CAAC,GAAG,IAAI,IAAI,KAAK,OAAO,uBAAuB,CAAC,CAAC;AACpE,QAAI,WAAW,WAAW,EAAG;AAE7B,QAAI;AACJ,QAAI;AACF,mBAAa,MAAM,KAAK,6BAA6B,UAAU;AAAA,IACjE,SAAS,aAAa;AACpB;AAAA,QACE,KAAK;AAAA,QACL,GAAG,4BAA4B;AAAA,QAC/B,EAAE,GAAG,SAAS,gBAAgB,YAAY,OAAO,YAAY;AAAA,MAC/D;AACA;AAAA,IACF;AACA,UAAM,eAAe,WAAW,OAAO,CAAC,QAAQ,CAAC,WAAW,IAAI,GAAG,CAAC;AACpE,UAAM,UAAU,MAAM;AAAA,MAA2B;AAAA,MAAc;AAAA,MAA+B,OAAO,QACnG,OAAO,GAAG;AAAA,IACZ;AACA,YAAQ,QAAQ,CAAC,QAAQ,UAAU;AACjC,UAAI,OAAO,WAAW,YAAY;AAChC;AAAA,UACE,KAAK;AAAA,UACL,GAAG,4BAA4B;AAAA,UAC/B;AAAA,YACE,GAAG;AAAA,YACH,eAAe,aAAa,KAAK;AAAA,YACjC,OAAO,OAAO;AAAA,UAChB;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,6BAA6B,gBAAyD;AAClG,UAAM,aAAa,oBAAI,IAAY;AACnC,aAAS,QAAQ,GAAG,QAAQ,eAAe,QAAQ,SAAS,wCAAwC;AAClG,YAAM,QAAQ,eAAe,MAAM,OAAO,QAAQ,sCAAsC;AACxF,iBAAW,OAAO,MAAM,KAAK,QAAQ,6BAA6B,KAAK,EAAG,YAAW,IAAI,GAAG;AAAA,IAC9F;AACA,WAAO;AAAA,EACT;AACF;;;ACzlBO,IAAM,wBAAwB;AAS9B,IAAM,0BAA0B;AAWhC,IAAM,4BAA4B;AASlC,IAAM,iCAAiC;AAAA,EAC5C,UAAU;AAAA,EACV,QAAQ;AACV;;;AC1BO,IAAM,0BAA0B;AAAA,EACrC,UAAU;AAAA,EACV,QAAQ;AACV;AAGO,IAAM,8BAA8B;AAGpC,IAAM,iCAAiC;AAGvC,IAAM,oBAAoB;AAM1B,IAAM,gCAAgC;;;ACxB7C,SAAgC,WAAuB;AAkBhD,SAAS,qBACd,EAAE,WAAW,SAAS,GACtB,SACA,aACqD;AACrD,QAAM,WAAW,MAAM,SAAS,UAAU,MAAM,QAAQ,UAAU;AAClE,QAAM,SAAS,4BAA4B,SAAS,UAAU,UAAU,EAAE,MAAM,QAAQ,UAAU;AAClG,SAAO;AAAA,IACL,QAAQ;AAAA,MACN,GAAG;AAAA,MACH,UAAU,2BAA2B,SAAS,QAAQ,kBAAkB,QAAQ,UAAU,QAAQ;AAAA,IACpG;AAAA,IACA,WACE,QAAQ,eAAe,WACnB,MAAM,MAAM,8BAA8B,QAAQ,UAAU,QAAQ,QAAQ,SAAS,UAAU,kBAAkB,WAAW,KAC5H;AAAA,EACR;AACF;;;ACpCA,SAAgC,OAAAC,YAAW;AAW3C,SAAS,sBAAsB,eAA2C;AACxE,SAAOC,iBAEO,aAAa,SAAS,kCAAkC,mBAAmB,aAAa;AACxG;AAYO,SAAS,uBACd,SACA,EAAE,sBAAsB,GACuB;AAC/C,SAAO,wBACH,EAAE,IAAI,QAAQ,IAAI,eAAe,sBAAsB,QAAQ,aAAa,EAAE,IAC9E,EAAE,IAAI,QAAQ,GAAG;AACvB;AAOO,SAAS,mBAAmB,MAAgE;AACjG,SAAO;AAAA,IACL,cAAc,KAAK;AAAA,IACnB,gBAAgB,KAAK;AAAA,MAAQ,CAAC,QAC5B,mBAAmB,MAAM,CAAC,OAAO,IAAI,kBAAkB,WAAW,IAAI,gBAAgB,IAAI,IAAI,CAAC;AAAA,IACjG;AAAA,EACF;AACF;;;AChDA,SAAsB,YAAsB;AAoBrC,SAAS,sBACd,SAC8C;AAC9C,QAAM,EAAE,kBAAkB,mBAAmB,eAAe,gBAAgB,GAAGC,eAAc,IAAI;AACjG,SAAOA;AACT;AAcO,SAAS,oBAAoB,WAAmB,gBAAqC;AAC1F,SAAO,CAAC,KAAK,SAAS,GAAG,KAAK,cAAc,CAAC;AAC/C;;;ACzCA,SAAS,KAAkB,IAAI,SAAS,QAAkB,OAAAC,YAAW;AAsB9D,SAAS,kBAAkB,QAAwB;AACxD,QAAM,UAAU,OAAO,QAAQ,yBAAyB,CAAC,cAAc,GAAG,qBAAqB,GAAG,SAAS,EAAE;AAC7G,SAAO,IAAI,OAAO;AACpB;AAUO,SAAS,iBAAiB,MAAsB;AACrD,SAAO,KAAK,YAAY;AAC1B;AASO,SAAS,kBAAkB,QAAsB,cAA4B,QAAqB;AACvG,SAAOC,OAAM,MAAM,IAAIA,KAAI,IAAI,YAAY,CAAC,IAAI,kBAAkB,MAAM,CAAC,WAAW,qBAAqB;AAC3G;AAQO,SAAS,sBAA+C,UAAgD;AAC7G,SAAO,EAAE,GAAG,UAAU,eAAe,iBAAiB,SAAS,OAAO,EAAE;AAC1E;AAcO,SAAS,yBACd,SACA,sBACA,QACK;AACL,QAAM,eAAe,iBAAiB,MAAM;AAC5C,QAAM,cAAc,kBAAkB,QAAQ,eAAe,2BAA2B,YAAY;AACpG,QAAM,gBAAgB,kBAAkB,QAAQ,SAAS,sBAAsB,YAAY;AAC3F,SAAOA,QAAO,WAAW,QAAQ,OAAO,QAAQ,aAAa,CAAC,QAAQ,aAAa;AACrF;AAOO,SAAS,mBACd,SACA,QACA,sBACiB;AACjB,QAAM,aAAoB,CAAC,GAAG,QAAQ,aAAa,OAAO,WAAW,CAAC;AACtE,MAAI,OAAO,KAAM,YAAW,KAAK,GAAG,QAAQ,MAAM,OAAO,IAAI,CAAC;AAC9D,MAAI,OAAO,SAAU,YAAW,KAAK,QAAQ,QAAQ,QAAQ,CAAC,GAAG,OAAO,QAAQ,CAAC,CAAC;AAClF,MAAI,OAAO,IAAK,YAAW,KAAK,GAAG,QAAQ,KAAK,OAAO,GAAG,CAAC;AAC3D,MAAI,OAAO,WAAY,YAAW,KAAK,GAAG,QAAQ,YAAY,OAAO,UAAU,CAAC;AAChF,MAAI,OAAO,OAAQ,YAAW,KAAK,yBAAyB,SAAS,sBAAsB,OAAO,MAAM,CAAC;AACzG,SAAO,IAAI,GAAG,UAAU;AAC1B;;;ACpGA,SAAsB,iBAAiB,IAAI,KAAK,OAAAC,YAAuB;AAMvE,IAAM,gBAA6B,CAAC,UAAU;AAUvC,SAAS,cAAoD,SAA6C;AAC/G,QAAM,EAAE,UAAU,WAAW,GAAG,OAAO,IAAI;AAC3C,SAAO;AACT;AAiBO,SAAS,aACd,OACA,MACA,WACA,cAA2B,eACtB;AACL,QAAM,UAAU,OAAO,QAAQ,gBAAgB,KAAK,CAAC;AACrD,QAAM,SAAS,KAAK,IAAI,CAAC,QAAQ;AAC/B,UAAM,SAAS,QAAQ,IAAI,CAAC,CAAC,KAAK,MAAM,MAAM;AAC5C,YAAM,QAAQ,IAAI,GAAG;AACrB,aAAO,GAAG,OAAO,GAAG,IAAI,QAAQ,YAAYC,OAAMA,KAAI,MAAM,SAAS,MAAM,MAAM,CAAC,IAAI,MAAM;AAAA,IAC9F,CAAC;AACD,WAAOA,QAAOA,KAAI,KAAK,QAAQA,QAAO,CAAC;AAAA,EACzC,CAAC;AACD,SAAOA,6BAA4BA,KAAI,KAAK,QAAQA,QAAO,CAAC,QAAQA,KAAI,WAAW,iBAAiB,CAAC,UAAU,SAAS;AAC1H;;;ACnDA,SAAgC,OAAAC,YAAW;AAgBpC,SAAS,mBACd,OACA,WACA,kBACW;AACX,SAAOA,OAAYA,KAAI,IAAI,gBAAgB,CAAC,IAAIA,KAAI,MAAM,WAAW,MAAM,SAAS,CAAC,KAAK,MAAM,SAAS,KAAK,MAAM,SAAS;AAC/H;","names":["relationsOf","row","projectName","sql","sql","recordColumns","sql","sql","sql","sql","sql"]}
|