{"version":3,"sources":["../src/index.ts","../../core/src/concurrency.ts","../../core/src/filters.ts","../../core/src/type-utils.ts","../../core/src/types.ts","../../core/src/screenshot-storage.ts"],"sourcesContent":["import {\n  type CommentCreateInput,\n  type CommentRecord,\n  clampPagination,\n  type FeedbackCreateInput,\n  type FeedbackPage,\n  type FeedbackPayload,\n  type FeedbackQuery,\n  type FeedbackRecord,\n  type FeedbackStatus,\n  type FeedbackType,\n  type FeedbackUpdateInput,\n  hasOwn,\n  isStoreDuplicate,\n  isStoreNotFound,\n  isUnreachableOffset,\n  MAX_COMMENTS_PER_FEEDBACK,\n  SCREENSHOT_DELETE_CONCURRENCY,\n  type ScreenshotStorage,\n  type SitepingStore,\n  StoreDuplicateError,\n  StoreLimitError,\n  StoreNotFoundError,\n  StoreValueTooLongError,\n  screenshotMimeType,\n  settleWithConcurrencyLimit,\n} from \"@siteping/core\";\nimport {\n  createSitepingHandler as createServerHandler,\n  type SitepingAccessHandlerOptions,\n  type SitepingApiKeyHandlerOptions,\n  type SitepingHandler,\n  type SitepingPrincipal,\n} from \"@siteping/server\";\n\nexport type {\n  CommentCreateInput,\n  CommentPayload,\n  FeedbackCreateInput,\n  FeedbackRecord,\n  ScreenshotStorage,\n  SitepingStore,\n} from \"@siteping/core\";\nexport {\n  flattenAnnotation,\n  isStorePersistence,\n  StoreDuplicateError,\n  StoreLimitError,\n  StoreNotFoundError,\n  StorePersistenceError,\n  StoreValueTooLongError,\n} from \"@siteping/core\";\nexport type { FeedbackDeleteInput, FeedbackPatchInput, GetQueryInput } from \"@siteping/server\";\n\n/**\n * @deprecated The create wire shape is core's `FeedbackPayload` — import\n * that instead. This alias is kept for one release cycle.\n */\nexport type FeedbackCreateSchemaInput = FeedbackPayload;\n// The server's option types, so a strict linker (pnpm, Bun's isolated\n// installs) never needs @siteping/server as a direct dependency to type them.\nexport type {\n  DiscordWebhookPayload,\n  GenericWebhookPayload,\n  SitepingAccessControl,\n  SitepingAction,\n  SitepingAuthorizationContext,\n  SitepingDeletionTarget,\n  SitepingHandler,\n  SitepingHandlerBaseOptions,\n  SitepingHttpMethod,\n  SitepingLifecycleHooks,\n  SitepingLogger,\n  SitepingPrincipal,\n  SitepingRequestContext,\n  SlackWebhookPayload,\n  WebhookConfig,\n  WebhookPayloadMap,\n  WebhookType,\n} from \"@siteping/server\";\nexport { dispatchWebhook, dispatchWebhooks } from \"@siteping/server\";\n\n// ---------------------------------------------------------------------------\n// Minimal PrismaClient shape expected by this adapter\n// ---------------------------------------------------------------------------\n\n/**\n * Structural type for a Prisma model delegate (`prisma.sitepingFeedback`).\n *\n * Arguments are kept `unknown` so any Prisma version's generated client\n * satisfies the constraint; the adapter assembles type-safe payloads\n * internally before forwarding them.\n *\n * Members use **method syntax** (`create(args)`) rather than function-property\n * syntax (`create: (args) => ...`) on purpose: under `strictFunctionTypes`,\n * function-property parameters are checked *contravariantly*, so a real\n * generated delegate — whose `create(args: SpecificArgs)` takes a type narrower\n * than `unknown` — would fail to assign to `PrismaModelDelegate`. Method\n * signatures are checked *bivariantly* on parameters, which is exactly what we\n * want for structurally matching a third-party generated client (#99).\n */\nexport interface PrismaModelDelegate {\n  create(args: unknown): Promise<unknown>;\n  findMany(args: unknown): Promise<unknown[]>;\n  findUnique(args: unknown): Promise<unknown>;\n  update(args: unknown): Promise<unknown>;\n  delete(args: unknown): Promise<unknown>;\n  deleteMany(args: unknown): Promise<unknown>;\n  count(args: unknown): Promise<number>;\n}\n\n/**\n * Compile-time regression guard for #99 — intentionally in `src/` because the\n * package's `check` script (`tsc --noEmit`) only type-checks `src/`, and\n * vitest transpiles tests without type-checking.\n *\n * `GeneratedDelegateProbe` mirrors a real generated client: every method\n * declares args NARROWER than `unknown`. With method syntax the conditional\n * below resolves to `true`; if `PrismaModelDelegate` ever regresses to\n * function-property syntax (contravariant under `strictFunctionTypes`), it\n * resolves to `false` and the `AssertTrue` constraint fails the build.\n */\ntype AssertTrue<T extends true> = T;\ninterface GeneratedDelegateProbe {\n  create(args: { data: unknown; include?: unknown }): Promise<{ id: string }>;\n  findMany(args: { where?: unknown; include?: unknown }): Promise<{ id: string }[]>;\n  findUnique(args: { where: unknown }): Promise<{ id: string } | null>;\n  update(args: { where: unknown; data: unknown }): Promise<{ id: string }>;\n  delete(args: { where: unknown }): Promise<{ id: string }>;\n  deleteMany(args: { where?: unknown }): Promise<{ count: number }>;\n  count(args: { where?: unknown }): Promise<number>;\n}\ntype _AssertDelegateBivariance = AssertTrue<GeneratedDelegateProbe extends PrismaModelDelegate ? true : false>;\n\n/**\n * Minimal Prisma client shape expected by this adapter.\n * Consumers pass their own `PrismaClient` instance at runtime — this interface\n * defines the subset of methods the adapter actually uses, so it can be\n * referenced in handler option types without importing `@prisma/client`.\n */\nexport interface SitepingPrismaClient {\n  sitepingFeedback: PrismaModelDelegate;\n  /**\n   * Generated once the schema declares the `SitepingComment` model\n   * (`npx @siteping/cli sync`). Optional, so a client generated from an older\n   * schema keeps type-checking: `PrismaStore` then has no threads and the\n   * handler answers comment writes with 501.\n   */\n  sitepingComment?: PrismaModelDelegate | undefined;\n}\n\n// ---------------------------------------------------------------------------\n// PrismaStore — SitepingStore implementation backed by Prisma\n// ---------------------------------------------------------------------------\n\nconst INCLUDE_ANNOTATIONS = { annotations: true } as const;\n/**\n * Read shape once the client has the `SitepingComment` model: the thread\n * oldest first, `id` breaking `createdAt` ties (SQL leaves them unordered).\n */\nconst INCLUDE_ANNOTATIONS_AND_COMMENTS = {\n  annotations: true,\n  comments: { orderBy: [{ createdAt: \"asc\" }, { id: \"asc\" }] },\n} as const;\n\n/**\n * Prisma datasource providers whose generated client exposes `mode?: QueryMode`\n * on string filters. Verified against Prisma 6.x by inspecting the generated\n * `StringFilter` type per provider:\n *   - postgresql, mongodb, cockroachdb → emit `mode?: QueryMode`\n *   - mysql, sqlite, sqlserver → no `mode` field; passing it raises\n *     `PrismaClientValidationError: Unknown argument 'mode'` at runtime.\n * `postgres` is kept as a defensive alias in case `_activeProvider` ever\n * surfaces the legacy spelling.\n */\nconst PROVIDERS_SUPPORTING_INSENSITIVE_MODE: ReadonlySet<string> = new Set([\n  \"postgresql\",\n  \"postgres\",\n  \"mongodb\",\n  \"cockroachdb\",\n]);\n\n/** Internal shape used to probe `PrismaClient` for the active provider. */\ninterface PrismaClientProbe {\n  _activeProvider?: unknown;\n  _engineConfig?: { activeProvider?: unknown };\n  _engine?: { config?: { activeProvider?: unknown } };\n}\n\n/**\n * Best-effort detection of the active Prisma provider for a runtime client.\n *\n * The provider is not part of any public API on `PrismaClient`. We probe a\n * few known internal locations across Prisma 5.x and 6.x and fall back to\n * `null` (treated as \"unknown — assume default Postgres-style behaviour\")\n * when none match.\n */\nfunction detectActiveProvider(prisma: unknown): string | null {\n  try {\n    const candidate = prisma as PrismaClientProbe | null | undefined;\n    const fromActive = candidate?._activeProvider;\n    if (typeof fromActive === \"string\") return fromActive;\n    const fromEngineConfig = candidate?._engineConfig?.activeProvider;\n    if (typeof fromEngineConfig === \"string\") return fromEngineConfig;\n    const fromEngine = candidate?._engine?.config?.activeProvider;\n    if (typeof fromEngine === \"string\") return fromEngine;\n    return null;\n  } catch {\n    return null;\n  }\n}\n\n/**\n * Options accepted by `PrismaStore`.\n */\nexport interface PrismaStoreOptions {\n  /**\n   * When `true`, the `?search=` filter is built with `mode: \"insensitive\"`\n   * (case-insensitive across all letters, including non-ASCII).\n   *\n   * When `false`, the filter is built without `mode` — uses each database's\n   * default `LIKE` semantics (case-insensitive ASCII on SQLite by default;\n   * case-sensitive on PostgreSQL with the standard `LIKE` operator;\n   * collation-driven on MySQL and SQL Server).\n   *\n   * When omitted, the value is auto-detected from the Prisma client's active\n   * provider: providers whose generated client exposes `mode?: QueryMode`\n   * (`postgresql`, `mongodb`, `cockroachdb`) get `true`; others (`mysql`,\n   * `sqlite`, `sqlserver`) get `false`. Unknown / undetectable providers\n   * default to `false` — `contains` without `mode` works on every provider;\n   * `mode: \"insensitive\"` throws on MySQL/SQLite/SQL Server, so the safer\n   * default is to omit it.\n   */\n  caseInsensitiveSearch?: boolean;\n  /**\n   * Optional storage backend for screenshots. Without it, the data URL is\n   * persisted inline on `Feedback.screenshotUrl` with a one-time warn.\n   */\n  screenshotStorage?: ScreenshotStorage | undefined;\n}\n\n/** `where` filter shape passed to `findMany` / `count`. Each field maps to a typed Prisma filter. */\ninterface FeedbackWhereInput {\n  projectName: string;\n  type?: FeedbackType;\n  // Exact match (`status`) or bucket match (`{ in: [...] }` from `statuses`).\n  status?: FeedbackStatus | { in: FeedbackStatus[] };\n  url?: string;\n  urlPattern?: string;\n  message?: { contains: string; mode?: \"insensitive\" };\n}\n\n/**\n * Translate Prisma's coded errors into the store contract's classes (the\n * handler layer and the dashboard are ORM-agnostic and only know these).\n * Anything else — connection failures, validation errors — passes through.\n */\nfunction toStoreError(error: unknown): unknown {\n  if (error instanceof StoreNotFoundError || error instanceof StoreDuplicateError) return error;\n  if (isStoreNotFound(error)) return new StoreNotFoundError(undefined, { cause: error });\n  if (isStoreDuplicate(error)) return new StoreDuplicateError(undefined, { cause: error });\n  // P2000: longer than its column — a plain `String` is `VARCHAR(191)` on MySQL.\n  if (hasOwn(error, \"code\") && error.code === \"P2000\") return new StoreValueTooLongError(undefined, { cause: error });\n  return error;\n}\n\n/**\n * Whether a persisted `screenshotUrl` points at an object a `ScreenshotStorage`\n * owns — inline `data:` URLs were never uploaded, so there is nothing to delete.\n */\nfunction isStoredScreenshotUrl(url: unknown): url is string {\n  return typeof url === \"string\" && url.length > 0 && !url.startsWith(\"data:\");\n}\n\n/**\n * Prisma-backed implementation of `SitepingStore`.\n *\n * Wraps a PrismaClient to satisfy the abstract store interface.\n *\n * Pass `screenshotStorage` to externalise screenshots (S3, R2, B2, …) — the\n * widget's data URL is uploaded and only the returned URL is persisted, so\n * the database stays small. Without `screenshotStorage`, the data URL is\n * persisted inline (logged once on first use as a heads-up).\n */\nexport class PrismaStore implements SitepingStore {\n  /** @internal */\n  private prisma: SitepingPrismaClient;\n  private readonly screenshotStorage: ScreenshotStorage | undefined;\n  /** Module-level flag would leak across PrismaStore instances in tests; use per-instance. */\n  private inlineFallbackWarned = false;\n  /** @internal */\n  private caseInsensitiveSearch: boolean;\n  /** Read shape of every feedback query — with the thread once the client has the comment model. */\n  private readonly include: typeof INCLUDE_ANNOTATIONS | typeof INCLUDE_ANNOTATIONS_AND_COMMENTS;\n\n  /**\n   * Add a comment to a feedback's thread — defined only when the client has\n   * the `SitepingComment` delegate, unless a subclass defines its own. Without\n   * it the store has no threads, and the handler answers comment writes with\n   * 501 instead of every post failing with a Prisma error. Declared, not a\n   * field: a field would set it on every instance, hiding a subclass's method.\n   */\n  declare readonly addComment?: (feedbackId: string, data: CommentCreateInput) => Promise<CommentRecord>;\n  /** Delete one comment from a feedback's thread — defined under the same condition as {@link addComment}. */\n  declare readonly deleteComment?: (feedbackId: string, commentId: string) => Promise<void>;\n\n  constructor(prisma: SitepingPrismaClient, options: PrismaStoreOptions = {}) {\n    this.prisma = prisma;\n    this.screenshotStorage = options.screenshotStorage;\n    const comments = prisma.sitepingComment;\n    this.include = comments ? INCLUDE_ANNOTATIONS_AND_COMMENTS : INCLUDE_ANNOTATIONS;\n    if (comments) {\n      this.addComment ??= (feedbackId, data) => this.insertComment(comments, feedbackId, data);\n      this.deleteComment ??= (feedbackId, commentId) => this.removeComment(comments, feedbackId, commentId);\n    }\n    if (typeof options.caseInsensitiveSearch === \"boolean\") {\n      this.caseInsensitiveSearch = options.caseInsensitiveSearch;\n    } else {\n      const provider = detectActiveProvider(prisma);\n      // When the provider can't be detected, default to `false`: `contains`\n      // without `mode` works on every Prisma provider; `mode: \"insensitive\"`\n      // throws on MySQL/SQLite/SQL Server. Trades non-ASCII case-insensitivity\n      // on undetectable Postgres clients (rare — _activeProvider is set on\n      // every real Prisma 5/6 client) for not crashing on the others.\n      this.caseInsensitiveSearch = provider !== null && PROVIDERS_SUPPORTING_INSENSITIVE_MODE.has(provider);\n    }\n  }\n\n  async createFeedback(data: FeedbackCreateInput): Promise<FeedbackRecord> {\n    const screenshotUrl = await this.persistScreenshot(data.screenshotDataUrl, data.clientId);\n\n    try {\n      return await this.insertFeedback(data, screenshotUrl);\n    } catch (error) {\n      if (isStoreDuplicate(error)) await this.discardUnreferencedUpload(screenshotUrl, data.clientId);\n      throw toStoreError(error);\n    }\n  }\n\n  /**\n   * Drop the screenshot uploaded by a replay of a stored clientId — unless the\n   * stored row references it. Uploads are keyed on clientId, so with a\n   * deterministic key (`feedback/${feedbackId}.jpg`) the replay wrote to the\n   * very object the existing row points at: deleting it would strip the\n   * surviving feedback of its screenshot. If the lookup itself fails the\n   * object is kept — an orphan beats data loss. Other insert failures never\n   * get here: with such a key, a retry on another instance may have rewritten\n   * the object and not yet inserted the row that will point at it.\n   */\n  private async discardUnreferencedUpload(url: string | null, clientId: string): Promise<void> {\n    if (!isStoredScreenshotUrl(url) || !this.screenshotStorage?.delete) return;\n    let existing: { screenshotUrl: string | null } | null;\n    try {\n      existing = (await this.prisma.sitepingFeedback.findUnique({\n        where: { clientId },\n        select: { screenshotUrl: true },\n      })) as { screenshotUrl: string | null } | null;\n    } catch {\n      return;\n    }\n    if (existing?.screenshotUrl === url) return;\n    await this.discardScreenshots([url]);\n  }\n\n  private async insertFeedback(data: FeedbackCreateInput, screenshotUrl: string | null): Promise<FeedbackRecord> {\n    return (await this.prisma.sitepingFeedback.create({\n      data: {\n        projectName: data.projectName,\n        type: data.type,\n        message: data.message,\n        status: data.status,\n        url: data.url,\n        urlPattern: data.urlPattern ?? null,\n        screenshotUrl,\n        // Persisted as JSON when the model has a `screenshotRegion Json?`\n        // column — same omit-when-null contract as `diagnostics` below, so\n        // hosts that haven't run `npx siteping sync` keep working.\n        ...(data.screenshotRegion ? { screenshotRegion: data.screenshotRegion } : {}),\n        // Persisted as JSON when the model has a `diagnostics Json?` column.\n        // Hosts that haven't run `npx siteping sync` keep their schema as-is\n        // and Prisma will throw if we pass an unknown column, so omit the\n        // key entirely when diagnostics is null.\n        ...(data.diagnostics ? { diagnostics: data.diagnostics } : {}),\n        viewport: data.viewport,\n        userAgent: data.userAgent,\n        authorName: data.authorName,\n        authorEmail: data.authorEmail,\n        clientId: data.clientId,\n        annotations: {\n          create: data.annotations.map((ann) => ({\n            cssSelector: ann.cssSelector,\n            xpath: ann.xpath,\n            textSnippet: ann.textSnippet,\n            elementTag: ann.elementTag,\n            elementId: ann.elementId,\n            textPrefix: ann.textPrefix,\n            textSuffix: ann.textSuffix,\n            fingerprint: ann.fingerprint,\n            neighborText: ann.neighborText,\n            anchorKey: ann.anchorKey ?? null,\n            xPct: ann.xPct,\n            yPct: ann.yPct,\n            wPct: ann.wPct,\n            hPct: ann.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      include: this.include,\n    })) as FeedbackRecord;\n  }\n\n  /**\n   * Resolve the value to persist on `Feedback.screenshotUrl`.\n   *\n   * - No data URL → null\n   * - Storage configured → upload, return remote URL. Upload failures\n   *   persist `null` (drop the screenshot) rather than silently inlining\n   *   the data URL — an inline fallback would bloat Postgres unnoticed\n   *   during a multi-minute storage outage. The feedback message itself is\n   *   preserved; only the screenshot is missing, and the warn surfaces it.\n   * - No storage → inline base64, with a one-time warn so prod operators\n   *   notice the footgun.\n   *\n   * Operators who prefer the legacy inline-on-failure behaviour can wrap\n   * their `ScreenshotStorage.upload` with their own catch + return the\n   * data URL — the adapter treats whatever the storage returns as final.\n   */\n  private async persistScreenshot(dataUrl: string | null | undefined, clientId: string): Promise<string | null> {\n    if (!dataUrl) return null;\n\n    if (this.screenshotStorage) {\n      try {\n        // Use clientId as the upload-time identifier — the feedback row's\n        // own id isn't created yet and clientId is unique + stable.\n        // NOTE: clientId is client-supplied; storage implementations that\n        // map it to a filesystem path MUST sanitize against path traversal.\n        const { url } = await this.screenshotStorage.upload(dataUrl, {\n          feedbackId: clientId,\n          mimeType: screenshotMimeType(dataUrl),\n        });\n        return url;\n      } catch (err) {\n        console.warn(\n          \"[siteping] screenshotStorage.upload failed — feedback will be saved without a screenshot. Wrap your storage's upload to handle this differently:\",\n          err,\n        );\n        return null;\n      }\n    }\n\n    if (!this.inlineFallbackWarned) {\n      this.inlineFallbackWarned = true;\n      console.warn(\n        \"[siteping] enableScreenshot is on but no `screenshotStorage` is configured — base64 data URLs will be persisted inline on Feedback.screenshotUrl. Configure a ScreenshotStorage (S3/R2/…) for production.\",\n      );\n    }\n    return dataUrl;\n  }\n\n  /**\n   * Best-effort cleanup of stored screenshots through `ScreenshotStorage.delete`\n   * — the hook the interface documents for feedback deletion. Failures are\n   * logged and swallowed: an orphaned object is preferable to a delete that\n   * reports failure after the row is already gone. Inline `data:` URLs and\n   * stores without a `delete` hook are skipped.\n   *\n   * Deletes run through a pool of at most {@link SCREENSHOT_DELETE_CONCURRENCY}\n   * concurrent calls: a project delete may free thousands of objects, and one\n   * socket each at once would exhaust the file descriptors of a serverless\n   * function, orphaning most of them.\n   */\n  private async discardScreenshots(urls: ReadonlyArray<unknown>): Promise<void> {\n    const remove = this.screenshotStorage?.delete?.bind(this.screenshotStorage);\n    if (!remove) return;\n    const stored = urls.filter(isStoredScreenshotUrl);\n    if (stored.length === 0) return;\n\n    const results = await settleWithConcurrencyLimit(stored, SCREENSHOT_DELETE_CONCURRENCY, async (url) => remove(url));\n    results.forEach((result, index) => {\n      if (result.status === \"rejected\") {\n        console.warn(\n          `[siteping] screenshotStorage.delete failed for ${stored[index]} — object left in place:`,\n          result.reason,\n        );\n      }\n    });\n  }\n\n  /** URLs of the stored screenshots in `projectName` — only fetched when a `delete` hook can use them. */\n  private async storedScreenshotUrls(projectName: string): Promise<string[]> {\n    if (!this.screenshotStorage?.delete) return [];\n    const rows = (await this.prisma.sitepingFeedback.findMany({\n      where: { projectName, screenshotUrl: { not: null } },\n      select: { screenshotUrl: true },\n    })) as ReadonlyArray<{ screenshotUrl: string | null }>;\n    return rows.map((row) => row.screenshotUrl).filter(isStoredScreenshotUrl);\n  }\n\n  async findByClientId(clientId: string): Promise<FeedbackRecord | null> {\n    return (await this.prisma.sitepingFeedback.findUnique({\n      where: { clientId },\n      include: this.include,\n    })) as FeedbackRecord | null;\n  }\n\n  async getFeedbacks(query: FeedbackQuery): Promise<FeedbackPage> {\n    const { projectName, type, status, statuses, search, url, urlPattern } = query;\n    // Same clamp as the in-memory pipeline: the HTTP schema already bounds\n    // page/limit, but direct callers reach the store without it.\n    const { limit, skip } = clampPagination(query);\n\n    const where: FeedbackWhereInput = { projectName };\n    if (type) where.type = type;\n    // Bucket filter (`statuses`) wins over the exact `status` filter; an empty\n    // array is treated as absent so no status constraint is applied.\n    if (statuses && statuses.length > 0) {\n      where.status = { in: [...statuses] };\n    } else if (status) {\n      where.status = status;\n    }\n    if (url) where.url = url;\n    if (urlPattern) where.urlPattern = urlPattern;\n    if (search) {\n      where.message = this.caseInsensitiveSearch ? { contains: search, mode: \"insensitive\" } : { contains: search };\n    }\n\n    // A huge `page` from a direct caller yields a `skip` Prisma rejects\n    // (non-integer or past 64 bits): answer the empty page the in-memory\n    // stores return, with the real total, without issuing `findMany`.\n    if (isUnreachableOffset(skip)) {\n      return { feedbacks: [], total: await this.prisma.sitepingFeedback.count({ where }) };\n    }\n\n    const [feedbacks, total] = await Promise.all([\n      this.prisma.sitepingFeedback.findMany({\n        where,\n        include: this.include,\n        // `id` breaks createdAt ties: SQL leaves equal rows unordered, so\n        // OFFSET pages could otherwise repeat one row and skip another.\n        orderBy: [{ createdAt: \"desc\" }, { id: \"desc\" }],\n        skip,\n        take: limit,\n      }),\n      this.prisma.sitepingFeedback.count({ where }),\n    ]);\n\n    return { feedbacks: feedbacks as FeedbackRecord[], total };\n  }\n\n  async updateFeedback(id: string, data: FeedbackUpdateInput): Promise<FeedbackRecord> {\n    try {\n      return (await this.prisma.sitepingFeedback.update({\n        where: { id },\n        data: {\n          status: data.status,\n          resolvedAt: data.resolvedAt,\n        },\n        include: this.include,\n      })) as FeedbackRecord;\n    } catch (error) {\n      throw toStoreError(error);\n    }\n  }\n\n  /**\n   * Insert a comment. A replayed `clientId` returns the stored comment, looked\n   * up first so a replay never runs into the thread cap; a concurrent replay\n   * that wins the insert race surfaces as P2002 and is read back the same\n   * way. The `connect` turns a missing feedback into Prisma's P2025 on every\n   * provider, rather than each database's own foreign-key error. The cap on\n   * `client` comments is a count before the insert, so posts racing for the\n   * last free slot may overshoot it; a `team` comment skips it.\n   */\n  private async insertComment(\n    comments: PrismaModelDelegate,\n    feedbackId: string,\n    data: CommentCreateInput,\n  ): Promise<CommentRecord> {\n    const replayed = await this.findComment(comments, data.clientId);\n    if (replayed) return replayed;\n    if (\n      data.authorRole === \"client\" &&\n      (await comments.count({ where: { feedbackId, authorRole: \"client\" } })) >= MAX_COMMENTS_PER_FEEDBACK\n    ) {\n      throw new StoreLimitError(`A thread holds at most ${MAX_COMMENTS_PER_FEEDBACK} client comments`);\n    }\n\n    try {\n      return (await comments.create({\n        data: {\n          feedback: { connect: { id: feedbackId } },\n          body: data.body,\n          authorName: data.authorName,\n          authorEmail: data.authorEmail,\n          authorRole: data.authorRole,\n          clientId: data.clientId,\n        },\n      })) as CommentRecord;\n    } catch (error) {\n      const winner = isStoreDuplicate(error) ? await this.findComment(comments, data.clientId) : null;\n      if (winner) return winner;\n      throw toStoreError(error);\n    }\n  }\n\n  private async findComment(comments: PrismaModelDelegate, clientId: string): Promise<CommentRecord | null> {\n    return (await comments.findUnique({ where: { clientId } })) as CommentRecord | null;\n  }\n\n  /**\n   * Delete one comment. Both ids go in one `deleteMany`, so a comment of\n   * another thread and an unknown one are the same zero-row miss.\n   */\n  private async removeComment(comments: PrismaModelDelegate, feedbackId: string, commentId: string): Promise<void> {\n    const { count } = (await comments.deleteMany({ where: { id: commentId, feedbackId } })) as { count: number };\n    if (count === 0) throw new StoreNotFoundError();\n  }\n\n  async deleteFeedback(id: string): Promise<void> {\n    let deleted: { screenshotUrl?: string | null } | null;\n    try {\n      // Prisma returns the deleted row — the only chance to learn which\n      // screenshot object the feedback owned.\n      deleted = (await this.prisma.sitepingFeedback.delete({ where: { id } })) as {\n        screenshotUrl?: string | null;\n      } | null;\n    } catch (error) {\n      throw toStoreError(error);\n    }\n    await this.discardScreenshots([deleted?.screenshotUrl]);\n  }\n\n  async deleteAllFeedbacks(projectName: string): Promise<void> {\n    // Rows first, storage second: a failed storage cleanup leaves orphaned\n    // objects (acceptable), the reverse would leave rows pointing at deleted\n    // screenshots.\n    const screenshotUrls = await this.storedScreenshotUrls(projectName);\n    await this.prisma.sitepingFeedback.deleteMany({ where: { projectName } });\n    await this.discardScreenshots(screenshotUrls);\n  }\n\n  /**\n   * Verify that a feedback record with `id` belongs to `projectName`.\n   * Returns `true` when the record exists and matches, `false` otherwise.\n   */\n  async verifyProjectOwnership(id: string, projectName: string): Promise<boolean> {\n    const record = (await this.prisma.sitepingFeedback.findUnique({\n      where: { id },\n      // Only need projectName for the check — not the annotations, nor an\n      // inline screenshot data URL or the diagnostics JSON\n      select: { projectName: true },\n    })) as { projectName: string } | null;\n    return record !== null && record.projectName === projectName;\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Handler — @siteping/server behind the Prisma store\n// ---------------------------------------------------------------------------\n\n/** How the handler reaches its data: a Prisma client, or any store. */\ninterface PrismaHandlerStoreOptions {\n  /** Prisma client — used when `store` is not provided. Wrapped in a `PrismaStore` internally. */\n  prisma?: SitepingPrismaClient;\n  /** Abstract store — when provided, takes precedence over `prisma`. */\n  store?: SitepingStore;\n  /**\n   * Optional storage backend for screenshots. Used only with `prisma`\n   * (ignored when a custom `store` is passed — that store is responsible\n   * for its own screenshot strategy). Without a storage, the data URL is\n   * persisted inline on `Feedback.screenshotUrl` with a one-time warn.\n   */\n  screenshotStorage?: ScreenshotStorage;\n  /**\n   * Override case-insensitive search behaviour for the built-in `PrismaStore`.\n   *\n   * Only applied when `prisma` is provided (not when a custom `store` is\n   * passed). See `PrismaStoreOptions.caseInsensitiveSearch` for details on\n   * auto-detection and per-provider semantics.\n   */\n  caseInsensitiveSearch?: boolean;\n}\n\n/** Options of `createSitepingHandler` under the `apiKey` policy — every `@siteping/server` option. */\nexport interface HandlerOptions extends Omit<SitepingApiKeyHandlerOptions, \"store\">, PrismaHandlerStoreOptions {}\n\n/** Options of `createSitepingHandler` under a custom `access` policy (see `@siteping/server`). */\nexport interface PrismaAccessHandlerOptions<Principal extends SitepingPrincipal>\n  extends Omit<SitepingAccessHandlerOptions<Principal>, \"store\">,\n    PrismaHandlerStoreOptions {}\n\n/**\n * Setup hint for Prisma's \"table does not exist\" error (P2021) — any SitePing\n * table: a client generated after `sync` also reads `SitepingComment`.\n */\nfunction describePrismaError(error: unknown): string | undefined {\n  if (hasOwn(error, \"code\") && error.code === \"P2021\") {\n    return \"A SitePing table is missing. Run 'npx prisma db push' (or apply your migrations) to create it.\";\n  }\n  return undefined;\n}\n\n/**\n * Create request handlers for the Siteping API endpoint — `@siteping/server`'s\n * `createSitepingHandler` over a Prisma-backed store, with every server option.\n *\n * Accepts either a `store` (abstract) or a `prisma` client (backwards compatible).\n * When `prisma` is provided without `store`, it is wrapped in a `PrismaStore`.\n *\n * **Rate limiting** is not handled by this library. Apply rate limiting at the\n * framework or reverse-proxy level (e.g. Next.js middleware, Nginx, Cloudflare).\n * The POST endpoint in particular should be rate-limited to prevent abuse, since\n * the widget typically calls it from unauthenticated browser contexts.\n *\n * @example Next.js App Router — `app/api/siteping/route.ts`\n * ```ts\n * import { createSitepingHandler } from '@siteping/adapter-prisma'\n * import { prisma } from '@/lib/prisma'\n *\n * export const { GET, POST, PATCH, DELETE, OPTIONS } = createSitepingHandler({ prisma })\n * ```\n *\n * @example With abstract store\n * ```ts\n * import { createSitepingHandler, PrismaStore } from '@siteping/adapter-prisma'\n * import { prisma } from '@/lib/prisma'\n *\n * const store = new PrismaStore(prisma)\n * export const { GET, POST, PATCH, DELETE, OPTIONS } = createSitepingHandler({ store })\n * ```\n */\nexport function createSitepingHandler<Principal extends SitepingPrincipal>(\n  options: PrismaAccessHandlerOptions<Principal>,\n): SitepingHandler;\nexport function createSitepingHandler(options: HandlerOptions): SitepingHandler;\n/** Options assembled at runtime, either policy. */\nexport function createSitepingHandler<Principal extends SitepingPrincipal>(\n  options: HandlerOptions | PrismaAccessHandlerOptions<Principal>,\n): SitepingHandler;\nexport function createSitepingHandler<Principal extends SitepingPrincipal>({\n  prisma,\n  store: providedStore,\n  screenshotStorage,\n  caseInsensitiveSearch,\n  describeError,\n  ...serverOptions\n}: HandlerOptions | PrismaAccessHandlerOptions<Principal>): SitepingHandler {\n  if (!providedStore && !prisma) {\n    throw new Error(\"[siteping] createSitepingHandler requires either `store` or `prisma`.\");\n  }\n\n  // Safe: the throw above guarantees at least one is defined\n  const store: SitepingStore =\n    providedStore ??\n    new PrismaStore(prisma as NonNullable<typeof prisma>, {\n      screenshotStorage,\n      ...(typeof caseInsensitiveSearch === \"boolean\" ? { caseInsensitiveSearch } : {}),\n    });\n\n  return createServerHandler({\n    ...serverOptions,\n    store,\n    describeError: (error) => describeError?.(error) ?? describePrismaError(error),\n  });\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 * General-purpose TypeScript utility types used across `@siteping/*`.\n *\n * These are kept dependency-free and re-exported from the package entry\n * so adapters and integrators can rely on the same primitives the core\n * uses internally.\n */\n\n/**\n * Force TypeScript to expand a computed type into a flat object literal in\n * tooltips and error messages. Purely cosmetic — same structural type, just\n * easier to read.\n *\n * @example\n *   type Raw = Omit<FeedbackRecord, \"annotations\"> & { annotations: number };\n *   type Pretty = Prettify<Raw>; // displayed as a flat object\n */\nexport type Prettify<T> = { [K in keyof T]: T[K] } & {};\n\n/**\n * Returns `Y` when `A` is exactly assignable to `B` and vice-versa,\n * otherwise `N`. Powers compile-time equality assertions.\n */\nexport type IfEquals<A, B, Y = true, N = false> =\n  (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? Y : N;\n\n/**\n * Compile-time exact-type guard — resolves to `true` when `Actual` and\n * `Expected` are identical, `never` otherwise. Assign the result to a\n * `const _lock: AssertEqual<A, B> = true;` so any drift becomes a compile\n * error at the declaration site.\n */\nexport type AssertEqual<Actual, Expected> = IfEquals<Actual, Expected, true, never>;\n\n/**\n * JSON-serialized shape of `T` — the wire form produced by `Response.json()`\n * / `JSON.stringify`: `Date` becomes ISO `string` (nullability preserved),\n * arrays are serialized element-wise, everything else is untouched.\n *\n * Used to derive the `*Response` API types from the `*Record` store types so\n * the two can never drift: add a field to `FeedbackRecord` and\n * `FeedbackResponse` follows automatically.\n */\nexport type Serialized<T> = {\n  [K in keyof T]: T[K] extends Date\n    ? string\n    : T[K] extends Date | null\n      ? string | null\n      : T[K] extends (infer U)[]\n        ? Serialized<U>[]\n        : T[K];\n};\n\n/**\n * Read-only all the way down, for plain data that is deeply frozen at\n * runtime: nested objects get read-only properties, arrays become\n * `readonly` arrays.\n */\nexport type DeepReadonly<T> = T extends readonly (infer U)[]\n  ? readonly DeepReadonly<U>[]\n  : T extends object\n    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }\n    : T;\n\n/**\n * Type guard that narrows `value` to a non-null `Record<PropertyKey, unknown>`.\n * Useful when validating arbitrary inputs before reading fields.\n */\nexport function isRecord(value: unknown): value is Record<PropertyKey, unknown> {\n  return typeof value === \"object\" && value !== null;\n}\n\n/**\n * Returns true when `value` is an object that exposes the requested key.\n * Type-narrows `value` so the property can be accessed without further\n * casting — a strictly typed replacement for `\"k\" in obj`.\n *\n * Named after the standardised `Object.hasOwn` helper rather than the\n * legacy `Object.prototype.hasOwnProperty`, which the linter forbids\n * shadowing.\n */\nexport function hasOwn<K extends PropertyKey>(value: unknown, key: K): value is Record<K, unknown> {\n  return isRecord(value) && key in value;\n}\n","import { type AssertEqual, type DeepReadonly, hasOwn, type Prettify, type Serialized } from \"./type-utils.js\";\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\n/** FAB anchor — bottom-corner placement supported by the widget. */\nexport type SitepingPosition = \"bottom-right\" | \"bottom-left\";\n\n/** Visual theme — `auto` resolves to `light` or `dark` via system preference. */\nexport type SitepingTheme = \"light\" | \"dark\" | \"auto\";\n\n/** Built-in UI locales shipped with the widget. */\nexport const BUILTIN_LOCALES = [\"en\", \"fr\", \"de\", \"es\", \"it\", \"pt\", \"ru\", \"ja\"] as const;\nexport type BuiltinLocale = (typeof BUILTIN_LOCALES)[number];\n\n/**\n * Locale identifier accepted by the widget. Built-in locales are kept as\n * literal strings so editors auto-complete them, but arbitrary BCP-47 tags\n * are also accepted (custom dictionaries registered via `registerLocale`).\n */\nexport type SitepingLocale = BuiltinLocale | (string & {});\n\n/**\n * Reasons reported through `SitepingConfig.onSkip` — production environment,\n * viewport narrower than `minViewportWidth` (`\"mobile\"`), or server-side\n * rendering (no `window`/`document`).\n */\nexport type SitepingSkipReason = \"production\" | \"mobile\" | \"ssr\";\n\n/** Per-channel + per-buffer-size diagnostics configuration. */\nexport interface DiagnosticsCaptureOptions {\n  console?: boolean | undefined;\n  network?: boolean | undefined;\n  /** Console buffer size — default and maximum 50 (the server's cap; larger values are clamped). */\n  maxConsoleEntries?: number | undefined;\n  /** Failed-request buffer size — default and maximum 20 (the server's cap; larger values are clamped). */\n  maxNetworkEntries?: number | undefined;\n}\n\n/** Identity payload supplied by the host application — bypasses the modal. */\nexport interface SitepingIdentity {\n  name: string;\n  email: string;\n}\n\n/**\n * Max length of an identity's `name` and `email` — the HTTP schema's\n * `authorName` / `authorEmail` cap. The widget's modal enforces it so a value\n * it persists is never a 400 on every later submission.\n */\nexport const IDENTITY_FIELD_MAX_LENGTH = 200;\n\n/** Deep-link configuration — controls how a feedback id is read from the URL. */\nexport interface SitepingDeepLinkOptions {\n  /** Query parameter name carrying the feedback id. Defaults to `\"siteping\"`. */\n  param?: string | undefined;\n}\n\n/**\n * The feedback handed to panel action callbacks: a detached, deeply frozen\n * copy, typed read-only all the way down. Type your own helpers with it — a\n * `FeedbackResponse` parameter does not accept the frozen copy.\n */\nexport type SitepingPanelActionFeedback = DeepReadonly<FeedbackResponse>;\n\n/**\n * Helpers passed to a panel action's `onAction` as its second argument.\n * Both do nothing once the widget has been destroyed.\n */\nexport interface SitepingPanelActionContext {\n  /**\n   * Re-fetch the panel list and markers, then re-render the detail view with\n   * the updated feedback — or go back to the list when it no longer matches\n   * the panel filters. Call it after your action changed the feedback\n   * server-side (e.g. moved it to `in_progress` once a ticket exists).\n   */\n  refresh: () => Promise<void>;\n  /** Close the feedback panel. */\n  close: () => void;\n}\n\n/**\n * Fields shared by both kinds of {@link SitepingPanelAction}.\n *\n * Do not use this type directly — use {@link SitepingPanelAction}.\n */\nexport interface SitepingPanelActionBase {\n  /** Stable, unique identifier — becomes `data-action-id` on the rendered control. */\n  id: string;\n  /**\n   * Visible label, rendered as plain text. Host-provided verbatim — not\n   * routed through the widget i18n, since hosts localize their own strings.\n   */\n  label: string;\n  /**\n   * Optional inline SVG markup rendered before the label. It is parsed\n   * inertly and reduced to plain shapes — scripts, event handlers, links,\n   * styles and external references are dropped — but keep it static markup\n   * you control. Markup that is not an `<svg>` is ignored with a warning.\n   */\n  icon?: string | undefined;\n  /**\n   * Per-feedback visibility predicate. Return `false` to omit the action\n   * for that feedback. Defaults to always visible. A throw hides the action\n   * and is reported through `onError`.\n   */\n  visible?: ((feedback: SitepingPanelActionFeedback) => boolean) | undefined;\n}\n\n/** A panel action rendered as a button that runs host code. */\nexport interface SitepingPanelButtonAction extends SitepingPanelActionBase {\n  /**\n   * Invoked on click. While a returned promise is pending the detail view's\n   * action buttons are disabled and the clicked button shows a spinner —\n   * also after `context.refresh()` re-renders the view, and when the user\n   * comes back to that feedback.\n   * Throws and rejections are reported through `onError` and restore the\n   * buttons; the detail view stays open either way.\n   */\n  onAction: (feedback: SitepingPanelActionFeedback, context: SitepingPanelActionContext) => void | Promise<void>;\n  /** Not available on a button action — use either `onAction` or `href`, never both. */\n  href?: never;\n}\n\n/** A panel action rendered as a link. */\nexport interface SitepingPanelLinkAction extends SitepingPanelActionBase {\n  /**\n   * Link target — a URL, or a function building one from the feedback.\n   * Relative URLs resolve against the page. Only `http:`, `https:` and\n   * `mailto:` are rendered: a static `href` with any other scheme\n   * (`javascript:` included) skips the action with a warning; a function\n   * returning one hides the action and is reported through `onError`. Web\n   * links open in a new tab with `rel=\"noopener noreferrer\"`.\n   */\n  href: string | ((feedback: SitepingPanelActionFeedback) => string);\n  /** Not available on a link action — use either `onAction` or `href`, never both. */\n  onAction?: never;\n}\n\n/**\n * A host-defined action rendered in the feedback detail view, below the\n * built-in Resolve / Delete buttons: a button running your code\n * (`onAction`) or a link (`href`) — never both.\n *\n * Hosts use this to bridge feedbacks into their own systems — create a\n * ticket, dispatch to a bot, open the feedback in their tracker — without\n * forking the panel. Callbacks receive a detached, deeply frozen copy of\n * the feedback: read it freely, it can never alter what the panel displays.\n */\nexport type SitepingPanelAction = SitepingPanelButtonAction | SitepingPanelLinkAction;\n\n/**\n * Extra request headers for HTTP mode — a static map, or a factory (sync or\n * async) invoked once per request to produce fresh values (e.g. a short-lived\n * session token).\n */\nexport type SitepingHeadersOption =\n  | Record<string, string>\n  | (() => Record<string, string> | Promise<Record<string, string>>);\n\n/**\n * Options shared by both widget modes (HTTP and direct store).\n *\n * Do not use this type directly — use {@link SitepingConfig}, the\n * discriminated union that adds the mode-specific fields.\n */\nexport interface SitepingBaseConfig {\n  /** Required — project identifier used to scope feedbacks */\n  projectName: string;\n  /** FAB position — defaults to 'bottom-right' */\n  position?: SitepingPosition | undefined;\n  /**\n   * Show the \"toggle markers visibility\" item in the FAB radial menu.\n   * Defaults to `true` (current behavior). Set to `false` to hide that\n   * item entirely — useful for hosts that always want markers visible\n   * (e.g. dedicated review tools) or that find the eye icon redundant\n   * when no marker is on screen.\n   *\n   * Hiding the item also removes its keyboard navigation slot — the\n   * remaining two items still respond to ArrowUp/ArrowDown/Home/End.\n   * The marker-visibility state itself is unaffected; markers stay\n   * visible (the previous default state) and `annotations:toggle` is\n   * simply never emitted from the FAB.\n   */\n  showAnnotationsToggle?: boolean | undefined;\n  /** Accent color for the widget UI — defaults to '#0066ff' */\n  accentColor?: string | undefined;\n  /**\n   * Render the widget even when it would normally be skipped — this bypasses\n   * BOTH the production-environment guard AND the `minViewportWidth` guard.\n   * It does NOT bypass the SSR guard: without `window`/`document` the widget\n   * never renders and `onSkip(\"ssr\")` fires instead.\n   * Defaults to false. Use it for dedicated review tools, staging environments,\n   * or responsive testing where you always want the widget present.\n   */\n  forceShow?: boolean | undefined;\n  /**\n   * Minimum viewport width (px) at or above which the widget renders. Below it,\n   * the widget is skipped and `onSkip(\"mobile\")` fires. Defaults to `0`: the\n   * widget renders at every width, and phones get a compact layout (bottom\n   * sheets, touch-sized controls).\n   *\n   * Set it (e.g. `768`) to keep the widget off small screens, or use\n   * `forceShow` to bypass the viewport check entirely.\n   */\n  minViewportWidth?: number | undefined;\n  /** Enable debug logging of lifecycle events — defaults to false */\n  debug?: boolean | undefined;\n  /** Color theme — defaults to 'light' */\n  theme?: SitepingTheme | undefined;\n  /** UI locale — defaults to 'en'. Built-in: en, fr, de, es, it, pt (Brazilian), ru, ja. Any other string falls back to English. */\n  locale?: SitepingLocale | undefined;\n  /**\n   * Returns the current page scope for annotations and panel filtering.\n   * Called on initial markers load and on `instance.refresh()`.\n   *\n   * Default: `{ url: window.location.pathname, urlPattern: null }` — annotations\n   * are scoped strictly to the current pathname.\n   *\n   * Apps with parameterized routes (e.g. React Router) should return both the\n   * concrete URL and the route template (e.g. `/orders/:orderId`) so the panel\n   * can offer a \"this type of page\" filter that groups feedbacks by template.\n   */\n  getPageScope?: (() => PageScope) | undefined;\n  /**\n   * When true (default), the widget filters initial markers and panel results\n   * by `feedback.url === scope.url`, so annotations created on one page never\n   * leak to other pages — even if their CSS selector accidentally matches.\n   * Set to `false` to revert to the legacy project-wide behavior.\n   */\n  scopeAnnotationsByUrl?: boolean | undefined;\n  /**\n   * Capture a JPEG screenshot of the annotated area on submit. Defaults to\n   * `false` — opt-in because:\n   *\n   * - it adds runtime weight (~60 KB gzip dynamic chunk for html2canvas-pro,\n   *   loaded only on first capture),\n   * - it embeds page content in the feedback (privacy/GDPR consideration —\n   *   inform end users in your widget host UI when enabling).\n   *\n   * `html2canvas-pro` ships as a regular dependency of `@siteping/widget` so the\n   * dynamic import always resolves; you don't need to install anything extra.\n   *\n   * **Masking sensitive elements:** add `data-siteping-ignore=\"true\"` to any\n   * element you do NOT want captured (password fields, credit-card forms,\n   * API tokens shown in the UI, etc.). The capture predicate skips matching\n   * elements *and their descendants*. Do this BEFORE turning on screenshots\n   * in production — once a feedback is saved, the screenshot is in your DB\n   * (or object storage) regardless of what was on the page.\n   */\n  enableScreenshot?: boolean | undefined;\n  /**\n   * Enable right-click (`contextmenu`) to instantly open the comment composer\n   * at the cursor location. When enabled, a document-level listener intercepts\n   * right-clicks, prevents the browser's native context menu, and enters the\n   * annotation flow anchored to the element under the cursor. Defaults to\n   * `false` — the browser's native context menu is never hijacked unless the\n   * host explicitly opts in.\n   *\n   * Keyboard-triggered context menus (≣ Menu key, Shift+F10) always get the\n   * native menu; only mouse right-click and touch/pen long-press open the\n   * composer — a long-press only where the browser fires `contextmenu` for\n   * it, which iOS and iPadOS never do (see the note below).\n   *\n   * **Modifier-key escape hatch:** holding Shift, Ctrl, Alt, or Meta while\n   * right-clicking always falls through to the native context menu, giving\n   * users (and devtools) an escape hatch regardless of this setting.\n   *\n   * Right-clicks on SitePing's own UI (FAB, panel, markers, popup) are\n   * ignored — the native menu is shown as expected.\n   *\n   * Note: on Android, `contextmenu` fires on long-press — touch users open the\n   * composer by long-pressing, on phones too since the widget renders at every\n   * width by default (see `minViewportWidth`). On iPhone and iPad a long-press\n   * never fires `contextmenu` (WebKit bug 213953): users there open annotate\n   * mode from the floating button and tap the element.\n   */\n  enableRightClickComment?: boolean | undefined;\n  /**\n   * Capture the last few `console.*` calls and failed network requests\n   * (HTTP >= 400 or network error) at the moment a feedback is submitted.\n   *\n   * Lets reviewers replay the technical context that led to the report —\n   * stack traces, 500 responses, dead third-party scripts. Great for the\n   * \"the page just doesn't work\" feedback that contains zero detail.\n   *\n   * - `true` — capture with defaults (50 console / 20 network entries).\n   * - `false` (default) — no capture, no monkey-patching.\n   * - object — per-channel toggles + smaller buffer sizes (values above the\n   *   50 / 20 server caps are clamped so submissions never fail validation).\n   *\n   * **Privacy considerations:** console messages may contain anything the\n   * host page logs, including user data. Failed network requests record the\n   * URL without its credentials, query string or hash, and never the\n   * response body.\n   * Inform end users before enabling in environments where they might log\n   * sensitive values.\n   */\n  captureDiagnostics?: boolean | DiagnosticsCaptureOptions | undefined;\n  /** Called when the widget is skipped (production mode, viewport under `minViewportWidth`, SSR — no DOM) */\n  onSkip?: (reason: SitepingSkipReason) => void;\n  /**\n   * Auto-focus a specific annotation when its ID appears in the URL query\n   * string. Lets hosts deeplink directly into a feedback from external\n   * systems (Zammad tickets, Slack notifications, dashboard rows).\n   *\n   * When enabled, the widget reads the configured query parameter from\n   * `window.location.search` right after the initial markers load. If the\n   * value matches a visible feedback ID, the widget scrolls the annotation\n   * into view, pins its highlight, and pulses the marker — the same visual\n   * affordance a marker click produces.\n   *\n   * - `false` / `undefined` (default): no URL parsing. Existing behavior\n   *   unchanged, no host URL inspection.\n   * - `true`: enabled with default query parameter name `siteping`.\n   * - object: enabled with a custom parameter name. Use this to avoid\n   *   clashes with host-app query keys.\n   *\n   * Only the initial load triggers focus. Subsequent URL changes (SPA\n   * navigation, `history.pushState`, hash updates) are ignored —\n   * deliberate, to avoid surprising re-scrolls during normal browsing.\n   * Hosts that need re-focus on route change can call\n   * `instance.focusFeedback(id)` explicitly.\n   */\n  deepLink?: boolean | SitepingDeepLinkOptions | undefined;\n  /**\n   * Automatically re-fetch feedbacks when the page changes during client-side\n   * (SPA) navigation. Enabled by default.\n   *\n   * The widget is normally mounted once (singleton) inside a persistent layout\n   * — e.g. a Next.js App Router `layout.tsx`, which does NOT remount on\n   * client-side navigation. Without this, init runs a single time and both the\n   * panel list and the page markers stay frozen on the page where the widget\n   * first mounted. With it on, the widget patches the History API\n   * (`pushState`/`replaceState`, which SPA routers call instead of triggering\n   * `popstate`) and listens for `popstate`/`hashchange`, then re-fetches when\n   * the scope key (`getPageScope().url` + template) actually changes.\n   *\n   * This re-fetches data only — it deliberately does NOT re-focus or re-scroll\n   * to an annotation (deep-link focus stays initial-load only; see `deepLink`),\n   * so normal browsing is never interrupted by a surprise scroll.\n   *\n   * - `true` (default) — watch navigation and re-fetch on route change.\n   * - `false` — never touch the History API; hosts drive updates manually via\n   *   `instance.refresh()`.\n   */\n  watchNavigation?: boolean | undefined;\n  /**\n   * Pre-fill author identity from the host application — typically the\n   * currently signed-in user. When set, the widget uses these values\n   * directly and never shows the identity modal, even on first feedback.\n   *\n   * Use case: SSO-integrated apps where the end user is already\n   * authenticated by the host. Avoids the awkward \"enter your name and\n   * email\" prompt for users the host already knows.\n   *\n   * When unset (default), the widget falls back to localStorage and shows\n   * the modal on first feedback as before — existing behavior unchanged.\n   *\n   * Note: `config.identity` is **not** persisted to localStorage. It is\n   * read at widget init time, not on every render. Hosts that need live\n   * identity updates after sign-in/sign-out should currently remount the\n   * widget (e.g. via a React `key` on the wrapping component). See\n   * https://github.com/NeosiaNexus/SitePing/issues/85 for tracking a\n   * future enhancement that propagates identity updates without a remount.\n   */\n  identity?: SitepingIdentity | undefined;\n  /**\n   * Host-defined actions rendered in the feedback detail view, below the\n   * built-in Resolve and Delete buttons. Read once when the panel loads:\n   * entries without a non-empty `id` and `label`, without exactly one of\n   * `onAction` / `href`, with an unsafe static `href`, or reusing an earlier\n   * `id` are skipped with a console warning. See {@link SitepingPanelAction}.\n   */\n  panelActions?: readonly SitepingPanelAction[] | undefined;\n  /**\n   * Reviewer mode: hide the actions that triage feedback — resolve, reopen,\n   * delete, the bulk actions, \"Delete all\" — and keep creating, browsing\n   * and replying. Defaults to `false`. The server's `permissions` hide\n   * what it would refuse on top of it. It only hides: the server decides\n   * what it accepts. Read once when the panel loads.\n   */\n  readOnly?: boolean | undefined;\n\n  // Events\n  /** Called when the feedback panel is opened. */\n  onOpen?: (() => void) | undefined;\n  /** Called when the feedback panel is closed. */\n  onClose?: (() => void) | undefined;\n  /** Called after a feedback is successfully submitted. */\n  onFeedbackSent?: ((feedback: FeedbackResponse) => void) | undefined;\n  /** Called after a reply is posted from the panel's discussion thread. */\n  onCommentAdded?: ((comment: CommentResponse) => void) | undefined;\n  /**\n   * Called when a feedback API call fails.\n   *\n   * The widget always emits a `SitepingError` (or a subclass:\n   * `SitepingNetworkError`, `SitepingValidationError`, `SitepingAuthError`)\n   * for HTTP-mode failures — host apps can `instanceof` to drive retry\n   * logic, or read `error.code` (`\"NETWORK\" | \"VALIDATION\" | \"AUTH\" |\n   * \"SERVER\"`) and `error.retryable` — and an `AUTH` error's `status`\n   * (`401` for missing or dead credentials, `403` for a refusal). The type\n   * is widened to `Error` so direct-store callers can still surface raw\n   * errors without breaking the contract.\n   *\n   * Also receives whatever a `panelActions` callback throws or rejects with\n   * (non-`Error` values are wrapped). Those host failures are not API\n   * failures, so they are not emitted on the public `feedback:error` event.\n   * The widget has no UI for them, so they are also always logged with\n   * `console.error`, whether or not `onError` is set.\n   */\n  onError?: ((error: Error) => void) | undefined;\n  /** Called when the user starts drawing an annotation. */\n  onAnnotationStart?: (() => void) | undefined;\n  /** Called when the user finishes drawing an annotation. */\n  onAnnotationEnd?: (() => void) | undefined;\n}\n\n/**\n * HTTP mode — the widget talks to a server endpoint backed by a store\n * adapter (e.g. `@siteping/adapter-prisma` request handlers).\n */\nexport interface SitepingHttpConfig extends SitepingBaseConfig {\n  /** HTTP endpoint that receives feedbacks (e.g. '/api/siteping'). */\n  endpoint: string;\n  /**\n   * Convenience auth for HTTP mode — sent as `Authorization: Bearer <apiKey>`\n   * on every request to `endpoint`.\n   *\n   * **WARNING: the widget runs in every visitor's browser, so a static key\n   * configured here is public** — anyone can read it from your page source\n   * and replay it against your API. Only use `apiKey` for internal tools\n   * already behind your own login. On public sites, prefer `headers` with a\n   * per-request factory returning a short-lived session token.\n   */\n  apiKey?: string | undefined;\n  /**\n   * Extra headers for every HTTP-mode request — a static map, or a factory\n   * (sync or async) called once per request (e.g. to fetch a fresh session\n   * token). Merged over the widget's generated headers, case-insensitively,\n   * so an explicit `Authorization` entry overrides `apiKey`. A factory that\n   * throws, rejects, or does not settle within 10 s fails the request like a\n   * network error.\n   */\n  headers?: SitepingHeadersOption | undefined;\n  /** Not available in HTTP mode — use either `endpoint` or `store`, never both. */\n  store?: never;\n}\n\n/**\n * Store mode — the widget talks to a `SitepingStore` directly in the\n * browser, no server needed (demos, prototypes, localStorage persistence).\n */\nexport interface SitepingStoreConfig extends SitepingBaseConfig {\n  /**\n   * Direct store for client-side mode. Bypasses HTTP entirely. A send stops\n   * waiting on `createFeedback` after 30 s (the call itself cannot be\n   * cancelled), so a network-backed store should bound its own calls.\n   */\n  store: SitepingStore;\n  /** Not available in store mode — use either `endpoint` or `store`, never both. */\n  endpoint?: never;\n  /** HTTP-mode only — meaningless without an `endpoint`. */\n  apiKey?: never;\n  /** HTTP-mode only — meaningless without an `endpoint`. */\n  headers?: never;\n}\n\n/**\n * Configuration options for the Siteping widget.\n *\n * A discriminated union over the two transport modes: pass `endpoint`\n * (HTTP mode, optionally with `apiKey`/`headers`) **or** `store` (direct\n * client-side mode) — never both, never neither. Invalid combinations are\n * compile errors instead of runtime warnings.\n */\nexport type SitepingConfig = SitepingHttpConfig | SitepingStoreConfig;\n\n/** Instance returned by initSiteping() with lifecycle methods. */\nexport interface SitepingInstance {\n  /** Remove the widget from the DOM and clean up all listeners. */\n  destroy: () => void;\n  /** Open the panel programmatically */\n  open: () => void;\n  /** Close the panel */\n  close: () => void;\n  /** Reload feedbacks from server */\n  refresh: () => void;\n  /**\n   * Scroll the matching annotation into view, pin its highlight, and\n   * pulse its marker. Returns `true` when a visible feedback matched the\n   * given ID, `false` otherwise (unknown ID, feedback on another URL when\n   * `scopeAnnotationsByUrl` filtered it out, or markers not yet loaded).\n   *\n   * Counterpart to the `deepLink` config option for hosts that prefer to\n   * drive focus from JS (e.g., a notification click handler) instead of a\n   * URL query parameter.\n   */\n  focusFeedback: (feedbackId: string) => boolean;\n  /** Subscribe to a public widget event */\n  on: <K extends keyof SitepingPublicEvents>(event: K, listener: SitepingPublicEventListener<K>) => SitepingUnsubscribe;\n  /** Unsubscribe from a public widget event */\n  off: <K extends keyof SitepingPublicEvents>(event: K, listener: SitepingPublicEventListener<K>) => void;\n}\n\n/** Listener signature for a single `SitepingPublicEvents` key. */\nexport type SitepingPublicEventListener<K extends keyof SitepingPublicEvents> = (\n  ...args: SitepingPublicEvents[K]\n) => void;\n\n/** Disposer returned by `SitepingInstance.on` — call once to detach the listener. */\nexport type SitepingUnsubscribe = () => void;\n\n/** Events exposed to consumers via SitepingInstance.on / .off */\nexport interface SitepingPublicEvents {\n  \"feedback:sent\": [FeedbackResponse];\n  \"feedback:deleted\": [FeedbackResponse[\"id\"]];\n  /** A reply was posted from the panel's discussion thread. */\n  \"comment:added\": [CommentResponse];\n  /**\n   * A feedback API call failed. Same payload contract as\n   * `SitepingConfig.onError` — a `SitepingError` subclass in HTTP mode,\n   * possibly a raw `Error` in store mode.\n   */\n  \"feedback:error\": [Error];\n  \"panel:open\": [];\n  \"panel:close\": [];\n  /** The user started drawing an annotation. */\n  \"annotation:start\": [];\n  /** The user finished drawing an annotation. */\n  \"annotation:end\": [];\n}\n\n// ---------------------------------------------------------------------------\n// Feedback\n// ---------------------------------------------------------------------------\n\n/** Single source of truth for feedback types — used by both TS types and Zod schemas. */\nexport const FEEDBACK_TYPES = [\"question\", \"change\", \"bug\", \"other\"] as const;\nexport type FeedbackType = (typeof FEEDBACK_TYPES)[number];\n\n/** Single source of truth for feedback statuses. */\nexport const FEEDBACK_STATUSES = [\"open\", \"in_progress\", \"resolved\", \"wont_fix\"] as const;\nexport type FeedbackStatus = (typeof FEEDBACK_STATUSES)[number];\n\n/**\n * Terminal statuses — the feedback needs no further action. `resolvedAt` is\n * the closure timestamp: set when a feedback enters a closed status, null\n * while it is open or in progress. The derivation happens at the edge (HTTP\n * handler, dashboard) — store adapters persist whatever they are given.\n */\nexport const CLOSED_FEEDBACK_STATUSES = [\"resolved\", \"wont_fix\"] as const;\n/** A terminal status — `resolved` or `wont_fix`. */\nexport type ClosedFeedbackStatus = (typeof CLOSED_FEEDBACK_STATUSES)[number];\n\n/** Non-terminal statuses — the feedback still needs attention. */\nexport const OPEN_FEEDBACK_STATUSES = [\"open\", \"in_progress\"] as const;\n/** A non-terminal status — `open` or `in_progress`. */\nexport type OpenFeedbackStatus = (typeof OPEN_FEEDBACK_STATUSES)[number];\n\n// Adding a fifth status without assigning it to exactly one bucket is a\n// compile error here.\nconst _statusBucketsCoverAll: AssertEqual<OpenFeedbackStatus | ClosedFeedbackStatus, FeedbackStatus> = true;\nvoid _statusBucketsCoverAll;\n\n/** Whether a status is terminal (`resolved` or `wont_fix`). Narrows the status type. */\nexport function isClosedStatus(status: FeedbackStatus): status is ClosedFeedbackStatus {\n  return (CLOSED_FEEDBACK_STATUSES as readonly FeedbackStatus[]).includes(status);\n}\n\n/**\n * Page scope returned by `SitepingConfig.getPageScope()`.\n *\n * - `url`: concrete page identifier — usually `window.location.pathname`,\n *   used as the strict scope for marker rendering.\n * - `urlPattern`: optional parameterized template (e.g. `/orders/:orderId`)\n *   used by the panel's \"this type of page\" filter to group feedbacks across\n *   instances of the same page kind.\n */\nexport interface PageScope {\n  url: string;\n  urlPattern: string | null;\n}\n\n// ---------------------------------------------------------------------------\n// Abstract Store — adapter pattern\n// ---------------------------------------------------------------------------\n\n/** Input for creating a feedback record in the store. */\nexport interface FeedbackCreateInput {\n  projectName: string;\n  type: FeedbackType;\n  message: string;\n  status: FeedbackStatus;\n  url: string;\n  /**\n   * Optional parameterized URL template (e.g. `/orders/:orderId`) for the page\n   * where the feedback was created. Allows the panel to filter feedbacks by\n   * \"this type of page\" across different instances. Null when the host did not\n   * provide a `getPageScope` callback or the route has no template.\n   */\n  urlPattern?: string | null | undefined;\n  viewport: string;\n  userAgent: string;\n  authorName: string;\n  authorEmail: string;\n  clientId: string;\n  annotations: AnnotationCreateInput[];\n  /**\n   * Base64 JPEG `data:` URL captured by the widget at submit time.\n   *\n   * Adapters with a configured `ScreenshotStorage` are expected to upload\n   * this and persist the returned URL on `FeedbackRecord.screenshotUrl`.\n   * Adapters without storage may persist the data URL inline (memory /\n   * localStorage / dev) — the widget then renders it directly.\n   */\n  screenshotDataUrl?: string | null | undefined;\n  /**\n   * Where the client's annotation rect sits within the screenshot image,\n   * as fractions [0, 1] of the image dimensions. Present when the widget\n   * captured context around the drawn rect; null for legacy captures that\n   * were cropped exactly to the rect (dashboards then render the image\n   * without an overlay).\n   */\n  screenshotRegion?: ScreenshotRegion | null | undefined;\n  /**\n   * Optional console + failed-network snapshot captured by the widget when\n   * `SitepingConfig.captureDiagnostics` is enabled. Stored as JSON on\n   * `FeedbackRecord.diagnostics` so reviewers can replay the context.\n   */\n  diagnostics?: DiagnosticsSnapshot | null | undefined;\n}\n\n/** Input for a single annotation when creating a feedback. */\nexport interface AnnotationCreateInput {\n  cssSelector: string;\n  xpath: string;\n  textSnippet: string;\n  elementTag: string;\n  elementId?: string | undefined;\n  textPrefix: string;\n  textSuffix: string;\n  fingerprint: string;\n  neighborText: string;\n  /**\n   * Semantic anchor identifier from the closest ancestor's `data-feedback-anchor`\n   * attribute. When set, this is the most stable re-anchoring signal because\n   * hosts deliberately place these on layout/section roots that survive DOM\n   * refactors and viewport changes. Null when no semantic ancestor exists.\n   */\n  anchorKey?: string | null | undefined;\n  xPct: number;\n  yPct: number;\n  wPct: number;\n  hPct: number;\n  scrollX: number;\n  scrollY: number;\n  viewportW: number;\n  viewportH: number;\n  devicePixelRatio: number;\n}\n\n/** Query parameters for fetching feedbacks. */\nexport interface FeedbackQuery {\n  projectName: string;\n  type?: FeedbackType | undefined;\n  /** Exact single-status filter. For \"any of a set\" (bucket) semantics, use `statuses`. */\n  status?: FeedbackStatus | undefined;\n  /**\n   * Filter to feedbacks whose status is any of the listed values — bucket\n   * semantics used by the panel's binary tabs (e.g. \"Open\" passes\n   * `[\"open\", \"in_progress\"]`). When both `status` and `statuses` are set,\n   * `statuses` wins. An empty array is treated as absent (no status filter).\n   */\n  statuses?: readonly FeedbackStatus[] | undefined;\n  search?: string | undefined;\n  page?: number | undefined;\n  /**\n   * Page size. Defaults to `DEFAULT_PAGE_LIMIT` (50) and is capped at\n   * `MAX_PAGE_LIMIT` (100) — `clampPagination` implements both, and the\n   * conformance suite checks them.\n   */\n  limit?: number | undefined;\n  /**\n   * Filter to feedbacks created on this exact URL (path). Used by the panel's\n   * \"this page\" filter and by the markers loader to keep page scopes isolated.\n   */\n  url?: string | undefined;\n  /**\n   * Filter to feedbacks created on this URL pattern (e.g. `/orders/:orderId`).\n   * Used by the panel's \"this type of page\" filter to group feedbacks across\n   * different concrete instances of the same template.\n   */\n  urlPattern?: string | undefined;\n}\n\n/**\n * Update payload for patching a feedback.\n *\n * A discriminated union encoding the closure invariant: a feedback entering\n * a closed status carries its closure timestamp, an open one carries `null`.\n * `{ status: \"resolved\", resolvedAt: null }` is a compile error instead of a\n * silent data bug. Build it from a plain `FeedbackStatus` with\n * {@link toFeedbackUpdate}.\n */\nexport type FeedbackUpdateInput =\n  | { status: OpenFeedbackStatus; resolvedAt: null }\n  | { status: ClosedFeedbackStatus; resolvedAt: Date };\n\n/**\n * Derive the {@link FeedbackUpdateInput} for a status change — the closure\n * timestamp is stamped for closed statuses and cleared otherwise. This is\n * the edge derivation described on {@link CLOSED_FEEDBACK_STATUSES}; store\n * adapters persist the result verbatim.\n */\nexport function toFeedbackUpdate(status: FeedbackStatus, closedAt: Date = new Date()): FeedbackUpdateInput {\n  return isClosedStatus(status) ? { status, resolvedAt: closedAt } : { status, resolvedAt: null };\n}\n\n/** A persisted feedback record returned by the store. */\nexport interface FeedbackRecord {\n  id: string;\n  type: FeedbackType;\n  message: string;\n  status: FeedbackStatus;\n  projectName: string;\n  url: string;\n  /**\n   * Parameterized URL template the feedback was created on.\n   * Null for legacy records or hosts without `getPageScope`.\n   */\n  urlPattern: string | null;\n  authorName: string;\n  authorEmail: string;\n  viewport: string;\n  userAgent: string;\n  clientId: string;\n  resolvedAt: Date | null;\n  createdAt: Date;\n  updatedAt: Date;\n  annotations: AnnotationRecord[];\n  /**\n   * Discussion thread, oldest first. A store that implements\n   * `SitepingStore.addComment` returns it on every record; a store without\n   * comments may leave it out, which reads as an empty thread (HTTP handlers\n   * send `[]`).\n   */\n  comments?: CommentRecord[] | undefined;\n  /**\n   * URL the widget renders as `<img src>`. Either an `https://...` from a\n   * configured `ScreenshotStorage`, or a `data:image/jpeg;base64,...` URL\n   * inline-persisted by adapters without storage. Null when no screenshot\n   * was captured (legacy records, capture failed, or host disabled it).\n   */\n  screenshotUrl: string | null;\n  /**\n   * Annotation rect position within the screenshot image, as fractions of\n   * its dimensions. Null for legacy captures cropped exactly to the rect.\n   */\n  screenshotRegion: ScreenshotRegion | null;\n  /**\n   * Console + failed-network snapshot captured at submit time. Null when\n   * diagnostics weren't enabled on the widget side.\n   */\n  diagnostics: DiagnosticsSnapshot | null;\n}\n\n/** A persisted annotation record returned by the store. */\nexport interface AnnotationRecord {\n  id: string;\n  feedbackId: string;\n  cssSelector: string;\n  xpath: string;\n  textSnippet: string;\n  elementTag: string;\n  elementId: string | null;\n  textPrefix: string;\n  textSuffix: string;\n  fingerprint: string;\n  neighborText: string;\n  /**\n   * Semantic anchor identifier from `data-feedback-anchor`. Null for legacy\n   * annotations or those drawn outside any anchored region.\n   */\n  anchorKey: string | null;\n  xPct: number;\n  yPct: number;\n  wPct: number;\n  hPct: number;\n  scrollX: number;\n  scrollY: number;\n  viewportW: number;\n  viewportH: number;\n  devicePixelRatio: number;\n  createdAt: Date;\n}\n\n// ---------------------------------------------------------------------------\n// Comments — the discussion thread of a feedback\n// ---------------------------------------------------------------------------\n\n/**\n * Who wrote a comment: `client` — the reviewer on the site (the widget) —\n * or `team` — the project side (the dashboard). HTTP handlers stamp\n * `client` unless the server's access policy vouches for the caller, so a\n * visitor cannot pose as the team.\n */\nexport const COMMENT_AUTHOR_ROLES = [\"client\", \"team\"] as const;\nexport type CommentAuthorRole = (typeof COMMENT_AUTHOR_ROLES)[number];\n\n/** Longest comment `body` the HTTP API accepts — the cap of a feedback `message`. */\nexport const COMMENT_BODY_MAX_LENGTH = 5000;\n\n/**\n * Most `client` comments one thread holds. List responses embed whole\n * threads, which are not paginated, so this bounds what one feedback weighs\n * however much a public endpoint is spammed. `team` comments — a role only\n * the access policy grants — neither count nor meet it: a thread spammed\n * full still takes the team's answer. Stores enforce it with\n * `StoreLimitError`; a store without an atomic primitive may overshoot it by\n * the posts that race the last free slot.\n */\nexport const MAX_COMMENTS_PER_FEEDBACK = 100;\n\n/** A persisted comment returned by the store. */\nexport interface CommentRecord {\n  id: string;\n  /** The feedback whose thread holds this comment. */\n  feedbackId: string;\n  body: string;\n  authorName: string;\n  /**\n   * Author email, or `\"\"` when the author has none on file (a dashboard\n   * user). HTTP handlers redact it exactly like `FeedbackRecord.authorEmail`.\n   */\n  authorEmail: string;\n  authorRole: CommentAuthorRole;\n  /** Client-generated id — `addComment` is idempotent on it, so a retried post never duplicates a reply. */\n  clientId: string;\n  createdAt: Date;\n}\n\n/** Input of `SitepingStore.addComment`. */\nexport interface CommentCreateInput {\n  body: string;\n  authorName: string;\n  authorEmail: string;\n  authorRole: CommentAuthorRole;\n  clientId: string;\n}\n\n/**\n * Body of a comment `POST`. `feedbackId` is what tells it apart from a\n * feedback submission on the same endpoint; `projectName` scopes the write\n * to that project's feedbacks.\n */\nexport interface CommentPayload extends CommentCreateInput {\n  projectName: string;\n  feedbackId: string;\n}\n\n/** Body of a comment `DELETE` — `commentId` is what tells it apart from a feedback delete. */\nexport interface CommentDeletePayload {\n  projectName: string;\n  feedbackId: string;\n  commentId: string;\n}\n\n// ---------------------------------------------------------------------------\n// Store errors — throw these from adapter implementations\n// ---------------------------------------------------------------------------\n\n/**\n * Thrown when a record is not found during update or delete.\n *\n * Handlers translate this to HTTP 404. Adapters MUST throw this (not\n * ORM-specific errors) so the handler layer remains ORM-agnostic.\n */\nexport class StoreNotFoundError extends Error {\n  readonly code = \"STORE_NOT_FOUND\" as const;\n  constructor(message = \"Record not found\", options?: ErrorOptions) {\n    super(message, options);\n    this.name = \"StoreNotFoundError\";\n  }\n}\n\n/**\n * Thrown when a unique constraint is violated (e.g. duplicate `clientId`).\n *\n * Handlers use this to return the existing record instead of failing.\n */\nexport class StoreDuplicateError extends Error {\n  readonly code = \"STORE_DUPLICATE\" as const;\n  constructor(message = \"Duplicate record\", options?: ErrorOptions) {\n    super(message, options);\n    this.name = \"StoreDuplicateError\";\n  }\n}\n\n/**\n * Thrown when a store accepts a mutation but cannot persist it — e.g.\n * `localStorage` is full (QuotaExceededError). Adapters MUST throw this rather\n * than swallow the failure, so callers learn the write was lost instead of\n * seeing a phantom success.\n */\nexport class StorePersistenceError extends Error {\n  readonly code = \"STORE_PERSISTENCE\" as const;\n  constructor(message = \"Failed to persist store mutation\", options?: ErrorOptions) {\n    super(message, options);\n    this.name = \"StorePersistenceError\";\n  }\n}\n\n/**\n * Thrown when a write would break a bound of the store contract — a\n * `client` comment on a thread already holding `MAX_COMMENTS_PER_FEEDBACK`\n * of them. Handlers translate this to HTTP 409.\n */\nexport class StoreLimitError extends Error {\n  readonly code = \"STORE_LIMIT\" as const;\n  constructor(message = \"Store limit reached\", options?: ErrorOptions) {\n    super(message, options);\n    this.name = \"StoreLimitError\";\n  }\n}\n\n/**\n * Thrown when a value is longer than the store can hold, though the HTTP\n * validation accepts it — on MySQL, Prisma maps a plain `String` to\n * `VARCHAR(191)`. Handlers translate this to HTTP 422: the submission can\n * never be stored as it is, so a client must not retry it.\n */\nexport class StoreValueTooLongError extends Error {\n  readonly code = \"STORE_VALUE_TOO_LONG\" as const;\n  constructor(message = \"Value too long for the store\", options?: ErrorOptions) {\n    super(message, options);\n    this.name = \"StoreValueTooLongError\";\n  }\n}\n\n/** Shape of any ORM error that carries a Prisma-style `code` field. */\ntype CodedError<C extends string = string> = { code: C };\n\nfunction hasErrorCode<C extends string>(error: unknown, code: C): error is CodedError<C> {\n  return hasOwn(error, \"code\") && error.code === code;\n}\n\n/**\n * Type guard — works for `StoreNotFoundError` and ORM-specific equivalents\n * (e.g. Prisma P2025). Matches on the stable `code` too, for the same\n * cross-bundle reason as {@link isStorePersistence}.\n */\nexport function isStoreNotFound(error: unknown): error is StoreNotFoundError | CodedError<\"STORE_NOT_FOUND\" | \"P2025\"> {\n  if (error instanceof StoreNotFoundError) return true;\n  // Another bundle's copy of core, or Prisma's P2025 (backwards compat)\n  return hasErrorCode(error, \"STORE_NOT_FOUND\") || hasErrorCode(error, \"P2025\");\n}\n\n/**\n * Type guard — works for `StoreDuplicateError` and ORM-specific equivalents\n * (e.g. Prisma P2002). Matches on the stable `code` too, for the same\n * cross-bundle reason as {@link isStorePersistence}.\n */\nexport function isStoreDuplicate(\n  error: unknown,\n): error is StoreDuplicateError | CodedError<\"STORE_DUPLICATE\" | \"P2002\"> {\n  if (error instanceof StoreDuplicateError) return true;\n  // Another bundle's copy of core, or Prisma's P2002 (backwards compat)\n  return hasErrorCode(error, \"STORE_DUPLICATE\") || hasErrorCode(error, \"P2002\");\n}\n\n/**\n * Type guard for `StorePersistenceError`. Matches on the stable `code` field\n * in addition to `instanceof`: every consumer package bundles its own copy of\n * core (tsup `noExternal`), so an instance thrown by one package fails an\n * `instanceof` check against another package's class identity.\n */\nexport function isStorePersistence(error: unknown): error is StorePersistenceError | CodedError<\"STORE_PERSISTENCE\"> {\n  if (error instanceof StorePersistenceError) return true;\n  return hasErrorCode(error, \"STORE_PERSISTENCE\");\n}\n\n/** Type guard for `StoreLimitError`, matching on `code` for the same cross-bundle reason as {@link isStorePersistence}. */\nexport function isStoreLimit(error: unknown): error is StoreLimitError | CodedError<\"STORE_LIMIT\"> {\n  if (error instanceof StoreLimitError) return true;\n  return hasErrorCode(error, \"STORE_LIMIT\");\n}\n\n/** Type guard for `StoreValueTooLongError`, matching on `code` for the same cross-bundle reason as {@link isStorePersistence}. */\nexport function isStoreValueTooLong(\n  error: unknown,\n): error is StoreValueTooLongError | CodedError<\"STORE_VALUE_TOO_LONG\"> {\n  if (error instanceof StoreValueTooLongError) return true;\n  return hasErrorCode(error, \"STORE_VALUE_TOO_LONG\");\n}\n\n// ---------------------------------------------------------------------------\n// Store helpers — shared conversion logic for adapters\n// ---------------------------------------------------------------------------\n\n/** Flatten a widget `AnnotationPayload` (nested anchor + rect) into a flat `AnnotationCreateInput`. */\nexport function flattenAnnotation(ann: AnnotationPayload): AnnotationCreateInput {\n  return {\n    cssSelector: ann.anchor.cssSelector,\n    xpath: ann.anchor.xpath,\n    textSnippet: ann.anchor.textSnippet,\n    elementTag: ann.anchor.elementTag,\n    elementId: ann.anchor.elementId,\n    textPrefix: ann.anchor.textPrefix,\n    textSuffix: ann.anchor.textSuffix,\n    fingerprint: ann.anchor.fingerprint,\n    neighborText: ann.anchor.neighborText,\n    anchorKey: ann.anchor.anchorKey ?? null,\n    xPct: ann.rect.xPct,\n    yPct: ann.rect.yPct,\n    wPct: ann.rect.wPct,\n    hPct: ann.rect.hPct,\n    scrollX: ann.scrollX,\n    scrollY: ann.scrollY,\n    viewportW: ann.viewportW,\n    viewportH: ann.viewportH,\n    devicePixelRatio: ann.devicePixelRatio,\n  };\n}\n\n// ---------------------------------------------------------------------------\n// Abstract Store — adapter pattern\n// ---------------------------------------------------------------------------\n\n/**\n * Outcome of `SitepingStore.createFeedbackIfAbsent` — the record plus whether\n * this very call inserted it.\n */\nexport interface FeedbackCreateOutcome {\n  feedback: FeedbackRecord;\n  /**\n   * `true` when this call inserted the record, `false` when a record with the\n   * same `clientId` already existed and is returned instead (a replay, or a\n   * concurrent request that won the race).\n   */\n  created: boolean;\n}\n\n/** Paginated result returned by `SitepingStore.getFeedbacks`. */\nexport interface FeedbackPage {\n  feedbacks: FeedbackRecord[];\n  total: number;\n}\n\n/**\n * Abstract storage interface for Siteping.\n *\n * Any adapter (Prisma, Drizzle, raw SQL, localStorage, etc.) implements this\n * interface. The HTTP handler and widget `StoreClient` operate against\n * `SitepingStore`, decoupled from the storage backend.\n *\n * ## Error contract\n *\n * - **`updateFeedback` / `deleteFeedback`**: throw `StoreNotFoundError` when\n *   the record does not exist.\n * - **`createFeedback`**: either return the existing record on duplicate\n *   `clientId` (idempotent) or throw `StoreDuplicateError`. The handler\n *   handles both patterns — but only a throw, or the optional\n *   `createFeedbackIfAbsent`, tells it the record was not inserted by this\n *   call; stores that return the existing record should implement\n *   `createFeedbackIfAbsent` so creation side effects never run twice.\n * - **All mutations**: when a write is accepted but cannot be persisted\n *   (e.g. storage quota), throw `StorePersistenceError` instead of reporting\n *   a phantom success. Detect it with `isStorePersistence`.\n * - Other methods should not throw on empty results — return empty arrays or `null`.\n */\nexport interface SitepingStore {\n  /** Create a feedback with its annotations. Idempotent on `clientId` — return existing record on duplicate, or throw `StoreDuplicateError`. Throws `StorePersistenceError` when the write cannot be persisted. */\n  createFeedback(data: FeedbackCreateInput): Promise<FeedbackRecord>;\n  /** Paginated query with optional filters. Returns empty array (not error) when no results. */\n  getFeedbacks(query: FeedbackQuery): Promise<FeedbackPage>;\n  /** Lookup by client-generated UUID. Returns `null` (not error) when not found. */\n  findByClientId(clientId: string): Promise<FeedbackRecord | null>;\n  /** Update status/resolvedAt. Throws `StoreNotFoundError` if `id` does not exist, `StorePersistenceError` when the write cannot be persisted. */\n  updateFeedback(id: string, data: FeedbackUpdateInput): Promise<FeedbackRecord>;\n  /** Delete a single record. Throws `StoreNotFoundError` if `id` does not exist, `StorePersistenceError` when the write cannot be persisted. */\n  deleteFeedback(id: string): Promise<void>;\n  /** Bulk delete all feedbacks for a project. No-op (not error) if none exist. Throws `StorePersistenceError` when the write cannot be persisted. */\n  deleteAllFeedbacks(projectName: string): Promise<void>;\n  /**\n   * Optional — return `true` when the record with `id` belongs to\n   * `projectName`, `false` otherwise (including when it does not exist).\n   *\n   * HTTP handlers use this to reject cross-project PATCH/DELETE requests.\n   * Implement it whenever your store serves multiple projects; when absent,\n   * handlers skip the ownership check and rely on `id` alone.\n   */\n  verifyProjectOwnership?(id: string, projectName: string): Promise<boolean>;\n  /**\n   * Optional — `createFeedback` that reports whether this call inserted the\n   * record (`created: true`) or found an existing one with the same\n   * `clientId` (`created: false`). The dedup check and the insert must be\n   * atomic, like `createFeedback`'s: of N concurrent calls with the same\n   * `clientId`, exactly one may report `created: true`, and all must return\n   * that same record. `createCollectionStore` guarantees this within one\n   * store instance by serializing its mutations; stores shared across\n   * processes need an atomic backend primitive (unique constraint,\n   * transaction, compare-and-set).\n   *\n   * HTTP handlers prefer it over `createFeedback` to fire creation side\n   * effects (webhooks) exactly once when concurrent requests race on the\n   * same `clientId`. Stores whose `createFeedback` throws\n   * `StoreDuplicateError` on a duplicate already give that signal and may\n   * leave it out.\n   */\n  createFeedbackIfAbsent?(data: FeedbackCreateInput): Promise<FeedbackCreateOutcome>;\n  /**\n   * Optional — append a comment to the thread of feedback `feedbackId` and\n   * return it. Idempotent on `data.clientId`: when a comment with that\n   * `clientId` exists, on any thread, return it unchanged (HTTP handlers\n   * refuse a replay aimed at another thread).\n   *\n   * Throws `StoreNotFoundError` when the feedback does not exist,\n   * `StoreLimitError` when a `client` comment meets a thread already holding\n   * `MAX_COMMENTS_PER_FEEDBACK` of them (a `team` comment is never refused\n   * for it), `StorePersistenceError` when the\n   * write cannot be persisted. A comment is not a change of the feedback\n   * itself: its `updatedAt` stays as it was.\n   *\n   * A store that implements it returns every feedback's thread on\n   * `FeedbackRecord.comments`, oldest first, and deletes a thread with its\n   * feedback. A store without it (and `deleteComment`) has no threads:\n   * HTTP handlers serve empty threads and answer comment writes with 501.\n   */\n  addComment?(feedbackId: string, data: CommentCreateInput): Promise<CommentRecord>;\n  /**\n   * Optional — delete comment `commentId` from the thread of `feedbackId`.\n   * Scoped to that thread, which the caller was authorized for: throws\n   * `StoreNotFoundError` when the feedback does not exist or the comment is\n   * not on its thread, `StorePersistenceError` when the write cannot be\n   * persisted.\n   */\n  deleteComment?(feedbackId: string, commentId: string): Promise<void>;\n}\n\n/** Payload sent from the widget to the server when submitting feedback. */\nexport interface FeedbackPayload {\n  projectName: string;\n  type: FeedbackType;\n  message: string;\n  url: string;\n  /**\n   * Parameterized URL template (e.g. `/orders/:orderId`) supplied by\n   * `SitepingConfig.getPageScope()`. Null when the host did not provide one.\n   */\n  urlPattern?: string | null | undefined;\n  viewport: string;\n  userAgent: string;\n  authorName: string;\n  authorEmail: string;\n  annotations: AnnotationPayload[];\n  /** Client-generated UUID for deduplication */\n  clientId: string;\n  /**\n   * Base64 JPEG `data:` URL of the annotated area. Captured by the widget\n   * when `enableScreenshot: true` is set in `SitepingConfig`. Null when\n   * disabled or when capture failed silently.\n   */\n  screenshotDataUrl?: string | null | undefined;\n  /**\n   * Annotation rect position within the screenshot image — see\n   * `ScreenshotRegion`. Null/absent when no screenshot was captured or the\n   * capture predates contextual framing.\n   */\n  screenshotRegion?: ScreenshotRegion | null | undefined;\n  /**\n   * Snapshot of the last few console messages and failed network requests\n   * captured at submit time when `captureDiagnostics` is enabled.\n   */\n  diagnostics?: DiagnosticsSnapshot | null | undefined;\n}\n\n/** Single source of truth for console diagnostic severity levels. */\nexport const CONSOLE_DIAGNOSTIC_LEVELS = [\"log\", \"info\", \"warn\", \"error\"] as const;\n/** Severity levels persisted in `ConsoleDiagnosticEntry`. */\nexport type ConsoleDiagnosticLevel = (typeof CONSOLE_DIAGNOSTIC_LEVELS)[number];\n\n/** A single console entry captured by `ConsoleBuffer`. */\nexport interface ConsoleDiagnosticEntry {\n  level: ConsoleDiagnosticLevel;\n  /** ISO 8601 timestamp captured at log time. */\n  timestamp: string;\n  /** Best-effort string representation of the original console args. */\n  message: string;\n}\n\n/** A single failed network request captured by `NetworkBuffer`. */\nexport interface NetworkDiagnosticEntry {\n  url: string;\n  method: string;\n  /** HTTP status; 0 when the request never reached the server. */\n  status: number;\n  /** End-to-end duration in ms. */\n  durationMs: number;\n  /** ISO 8601 timestamp at the moment the request was initiated. */\n  timestamp: string;\n}\n\n/**\n * Diagnostics captured by the widget when `captureDiagnostics` is enabled.\n *\n * Both arrays are bounded (default: 50 console / 20 network). Adapters that\n * support diagnostics should persist this as a JSON blob alongside the\n * feedback so reviewers can replay the context that led to the report.\n */\nexport interface DiagnosticsSnapshot {\n  console: ConsoleDiagnosticEntry[];\n  network: NetworkDiagnosticEntry[];\n}\n\n// ---------------------------------------------------------------------------\n// Annotation — multi-selector anchoring (Hypothesis / W3C Web Annotation)\n// ---------------------------------------------------------------------------\n\n/**\n * Length caps for `AnchorData.elementTag` / `elementId`, shared by the widget\n * (which bounds what it captures) and the HTTP adapter (which rejects a longer\n * tag and drops a longer id) so the two can't drift. 191 fits Prisma's default `String` column\n * on MySQL (`VARCHAR(191)`); real tag names and ids are far shorter.\n */\nexport const ANCHOR_ELEMENT_TAG_MAX = 191;\nexport const ANCHOR_ELEMENT_ID_MAX = 191;\n\n/** DOM anchoring data for re-attaching annotations to page elements. */\nexport interface AnchorData {\n  /**\n   * CSS selector generated by @medv/finder — primary anchor. Inside open\n   * shadow roots: one selector per tree, outermost host first, joined by\n   * `\" >>> \"` (e.g. `\"#pricing >>> .plan-title\"`).\n   */\n  cssSelector: string;\n  /**\n   * XPath — fallback 1. Inside a shadow root it is relative to that root\n   * (`./…`) and informational only: XPath cannot enter shadow trees.\n   */\n  xpath: string;\n  /** First ~120 chars of element innerText — empty string if none */\n  textSnippet: string;\n  /** Tag name for validation (e.g. \"DIV\", \"SECTION\") */\n  elementTag: string;\n  /** Element id attribute if available — most stable */\n  elementId?: string | undefined;\n  /** ~32 chars of text before this element in document flow (disambiguation) */\n  textPrefix: string;\n  /** ~32 chars of text after this element in document flow (disambiguation) */\n  textSuffix: string;\n  /** Structural fingerprint: \"childCount:siblingIdx:attrHash\" */\n  fingerprint: string;\n  /** Text content of adjacent sibling elements (context) */\n  neighborText: string;\n  /**\n   * Semantic anchor identifier from the closest ancestor's `data-feedback-anchor`\n   * attribute. When set, this is the highest-priority re-anchoring signal —\n   * hosts deliberately place these on layout/section roots that survive\n   * viewport changes and DOM refactors.\n   */\n  anchorKey?: string | null | undefined;\n}\n\n/**\n * Where the client's annotation rect sits within the captured screenshot,\n * as fractions [0, 1] of the image dimensions. The widget captures context\n * around the drawn rect and records the rect's position here so dashboards\n * can re-render the annotation on top of the image. Survives downscaling\n * (fractions are resolution-independent).\n */\nexport interface ScreenshotRegion {\n  /** X offset of the rect as fraction of image width — [0, 1] */\n  xPct: number;\n  /** Y offset of the rect as fraction of image height — [0, 1] */\n  yPct: number;\n  /** Rect width as fraction of image width — [0, 1] */\n  wPct: number;\n  /** Rect height as fraction of image height — [0, 1] */\n  hPct: number;\n}\n\n/** Drawn rectangle coordinates as percentages relative to the anchor element. */\nexport interface RectData {\n  /** X offset as fraction of anchor element width — must be in range [0, 1] */\n  xPct: number;\n  /** Y offset as fraction of anchor element height — must be in range [0, 1] */\n  yPct: number;\n  /** Width as fraction of anchor element width — must be in range [0, 1] */\n  wPct: number;\n  /** Height as fraction of anchor element height — must be in range [0, 1] */\n  hPct: number;\n}\n\n/** Annotation data sent as part of a feedback submission. */\nexport interface AnnotationPayload {\n  anchor: AnchorData;\n  rect: RectData;\n  scrollX: number;\n  scrollY: number;\n  viewportW: number;\n  viewportH: number;\n  devicePixelRatio: number;\n}\n\n// ---------------------------------------------------------------------------\n// API responses\n// ---------------------------------------------------------------------------\n\n/**\n * Feedback record as returned by the API — derived from\n * {@link FeedbackRecord}: dates are serialized to ISO strings and `clientId`\n * is omitted (server-side dedup concern, never exposed on the wire), on the\n * record and on each comment of its thread. Adding a field to\n * `FeedbackRecord` updates this type automatically.\n *\n * Note: `authorEmail` may be an empty string — HTTP adapters redact it for\n * unauthenticated requests; the full value requires a Bearer-authenticated\n * request.\n */\nexport type FeedbackResponse = Prettify<\n  Serialized<Omit<FeedbackRecord, \"clientId\" | \"comments\">> & {\n    /** The thread, oldest first — always sent by `@siteping/server`, absent from servers that predate comments. */\n    comments?: CommentResponse[] | undefined;\n    /**\n     * What the requester may do with this feedback — always sent by\n     * `@siteping/server`. Absent from servers that predate it, and in store\n     * mode: nothing is refused then.\n     */\n    permissions?: FeedbackPermissions | undefined;\n  }\n>;\n\n/**\n * What a requester may do with one feedback, as the server's access policy\n * decides — so clients hide the actions it would refuse. The server still\n * enforces every one of them.\n */\nexport interface FeedbackPermissions {\n  /** Change its status: resolve, reopen, … */\n  canChangeStatus: boolean;\n  /** Delete it. */\n  canDelete: boolean;\n  /** Reply in its thread. */\n  canComment: boolean;\n  /** Delete replies from its thread. */\n  canDeleteComment: boolean;\n}\n\n/** What a requester may do with a whole project, sent with each list. */\nexport interface FeedbackListPermissions {\n  /** Delete every feedback of the project at once. */\n  canDeleteAll: boolean;\n}\n\n/**\n * Annotation record as returned by the API — {@link AnnotationRecord} with\n * `createdAt` serialized to an ISO string.\n */\nexport type AnnotationResponse = Prettify<Serialized<AnnotationRecord>>;\n\n/**\n * Comment as returned by the API — {@link CommentRecord} with `createdAt`\n * serialized and `clientId` omitted, like on `FeedbackResponse`. Its\n * `authorEmail` is redacted on the same terms as the feedback's.\n */\nexport type CommentResponse = Prettify<Serialized<Omit<CommentRecord, \"clientId\">>>;\n\n/** What the store behind an endpoint supports — advertised on every list response. */\nexport interface SitepingCapabilities {\n  /** Whether comments can be posted: the store implements `addComment`. */\n  comments: boolean;\n  /**\n   * Whether comments can be deleted: the store implements `deleteComment`.\n   * Sent by `@siteping/server`; a client that does not delete (the widget)\n   * leaves it out.\n   */\n  deleteComments?: boolean | undefined;\n}\n\n/** Paginated `FeedbackResponse` shape returned by the API. */\nexport interface FeedbackResponseList {\n  feedbacks: FeedbackResponse[];\n  total: number;\n  /** Always sent by `@siteping/server` — absent from servers that predate it. */\n  capabilities?: SitepingCapabilities | undefined;\n  /** Always sent by `@siteping/server` — absent from servers that predate it. */\n  permissions?: FeedbackListPermissions | undefined;\n}\n","/**\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` identifies the upload. Adapters that upload before the\n   * record exists pass the *client-generated* `clientId` (Prisma); the\n   * Drizzle adapter passes the server-generated id the create attempt will\n   * insert the record under.\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: two submissions of one `clientId` both upload, and\n   * the object of the one that is not stored is deleted. Under Prisma,\n   * `ctx.feedbackId` is that client-supplied `clientId`, so an object keyed\n   * by it is rewritten by every replay — a retry, or anyone who learns the\n   * id — and a URL shared by several records lets deleting one remove the\n   * object another still points at.\n   *\n   * **Security note:** treat `ctx.feedbackId` as attacker-controlled:\n   * sanitize before using it in filesystem paths or object keys, even though\n   * server adapters validate its shape upstream.\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 — in adapters that key\n   * uploads per attempt, like Drizzle — one whose insert failed. An object a\n   * stored row still references (a deterministic key reused by the replay)\n   * 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"],"mappings":"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","names":["import_server","createServerHandler"]}