@beezping/adapter-prisma 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/dist/beezping-core.d.cts +20 -0
- package/dist/beezping-core.d.ts +20 -0
- package/dist/concurrency.d.cts +14 -0
- package/dist/concurrency.d.ts +14 -0
- package/dist/constants/schema.d.cts +239 -0
- package/dist/constants/schema.d.ts +239 -0
- package/dist/deep-link.d.cts +14 -0
- package/dist/deep-link.d.ts +14 -0
- package/dist/email.d.cts +8 -5
- package/dist/email.d.ts +8 -5
- package/dist/errors.d.cts +28 -14
- package/dist/errors.d.ts +28 -14
- package/dist/filters.d.cts +15 -7
- package/dist/filters.d.ts +15 -7
- package/dist/i18n.d.cts +18 -0
- package/dist/i18n.d.ts +18 -0
- package/dist/index.cjs +178 -868
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -213
- package/dist/index.d.ts +95 -213
- package/dist/index.js +175 -855
- package/dist/index.js.map +1 -1
- package/dist/schema.d.cts +12 -195
- package/dist/schema.d.ts +12 -195
- package/dist/screenshot-storage.d.cts +55 -30
- package/dist/screenshot-storage.d.ts +55 -30
- package/dist/store-helpers.d.cts +62 -34
- package/dist/store-helpers.d.ts +62 -34
- package/dist/type-utils.d.cts +8 -0
- package/dist/type-utils.d.ts +8 -0
- package/dist/types.d.cts +413 -154
- package/dist/types.d.ts +413 -154
- package/dist/wire.d.cts +42 -9
- package/dist/wire.d.ts +42 -9
- package/package.json +14 -10
- package/dist/siteping-core.d.cts +0 -17
- package/dist/siteping-core.d.ts +0 -17
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../../core/src/filters.ts","../../core/src/type-utils.ts","../../core/src/types.ts","../../server/src/constants/limits.ts","../../server/src/constants/error-messages.ts","../../server/src/access.ts","../../server/src/api-key-access.ts","../../server/src/constants/http.ts","../../server/src/cors.ts","../../server/src/internal-error.ts","../../core/src/email.ts","../../core/src/type-utils.ts","../../core/src/types.ts","../../server/src/validation.ts","../../server/src/webhooks.ts","../../server/src/operations/create-feedback.ts","../../server/src/operations/delete-feedback.ts","../../server/src/operations/list-feedbacks.ts","../../server/src/operations/update-feedback.ts","../../server/src/request-guards.ts","../../server/src/request-pipeline.ts","../../server/src/handler.ts","../../server/src/identity.ts"],"sourcesContent":["import {\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 type ScreenshotStorage,\n type SitepingStore,\n StoreDuplicateError,\n StoreNotFoundError,\n} from \"@beezping/core\";\nimport {\n type ApiKeyAccessOptions,\n createSitepingHandler as createServerHandler,\n type SitepingHandler,\n type WebhookConfig,\n} from \"@beezping/server\";\n\nexport type { ScreenshotStorage, SitepingStore } from \"@beezping/core\";\nexport {\n flattenAnnotation,\n isStorePersistence,\n StoreDuplicateError,\n StoreNotFoundError,\n StorePersistenceError,\n} from \"@beezping/core\";\nexport type {\n FeedbackDeleteInput,\n FeedbackPatchInput,\n GetQueryInput,\n SitepingHandler,\n SitepingHttpMethod,\n} from \"@beezping/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;\nexport type {\n DiscordWebhookPayload,\n GenericWebhookPayload,\n SlackWebhookPayload,\n WebhookConfig,\n WebhookPayloadMap,\n WebhookType,\n} from \"@beezping/server\";\nexport { dispatchWebhook, dispatchWebhooks } from \"@beezping/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\n// ---------------------------------------------------------------------------\n// PrismaStore — SitepingStore implementation backed by Prisma\n// ---------------------------------------------------------------------------\n\nconst INCLUDE_ANNOTATIONS = { annotations: true } 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 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\n constructor(prisma: SitepingPrismaClient, options: PrismaStoreOptions = {}) {\n this.prisma = prisma;\n this.screenshotStorage = options.screenshotStorage;\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 // A replay of an already-stored clientId (the widget's retry queue):\n // the existing row keeps its own screenshot, so the one just uploaded\n // for this attempt is an orphan — drop it before reporting the dup.\n if (isStoreDuplicate(error)) await this.discardScreenshots([screenshotUrl]);\n throw toStoreError(error);\n }\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: INCLUDE_ANNOTATIONS,\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: \"image/jpeg\",\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 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 Promise.allSettled(stored.map((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: INCLUDE_ANNOTATIONS,\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: INCLUDE_ANNOTATIONS,\n orderBy: { createdAt: \"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: INCLUDE_ANNOTATIONS,\n })) as FeedbackRecord;\n } catch (error) {\n throw toStoreError(error);\n }\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 — skip annotations\n })) as { projectName: string } | null;\n return record !== null && record.projectName === projectName;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Handler — thin Prisma wrapper over the store-agnostic @beezping/server\n// ---------------------------------------------------------------------------\n\nexport interface HandlerOptions extends ApiKeyAccessOptions {\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 /** Allowed CORS origins — when set, validates the Origin header */\n allowedOrigins?: ReadonlyArray<string> | undefined;\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 * Outgoing webhooks fired after a feedback is successfully persisted.\n * Fire-and-forget; provide `onError` on each config to observe failures.\n */\n webhooks?: WebhookConfig | ReadonlyArray<WebhookConfig>;\n}\n\n/**\n * Create request handlers for the Siteping API endpoint.\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 * For custom auth (sessions, roles), lifecycle hooks or input transforms use\n * `createSitepingHandler` from `@beezping/server` with 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 *\n * @example Next.js App Router — `app/api/siteping/route.ts`\n * ```ts\n * import { createSitepingHandler } from '@beezping/adapter-prisma'\n * import { prisma } from '@/lib/prisma'\n *\n * export const { GET, POST, PATCH, DELETE, OPTIONS } = createSitepingHandler({ prisma })\n * ```\n */\nexport function createSitepingHandler({\n prisma,\n store: providedStore,\n screenshotStorage,\n caseInsensitiveSearch,\n ...handlerOptions\n}: HandlerOptions): SitepingHandler {\n if (!providedStore && !prisma) {\n throw new Error(\"[siteping] createSitepingHandler requires either `store` or `prisma`.\");\n }\n const store: SitepingStore =\n providedStore ??\n new PrismaStore(prisma as NonNullable<typeof prisma>, {\n screenshotStorage,\n ...(typeof caseInsensitiveSearch === \"boolean\" ? { caseInsensitiveSearch } : {}),\n });\n return createServerHandler({ ...handlerOptions, store, describeError: actionableErrorMessage });\n}\n\nfunction isTableNotFoundError(error: unknown): error is { code: \"P2021\" } {\n return hasOwn(error, \"code\") && (error as { code: unknown }).code === \"P2021\";\n}\n\n/** Actionable message for known Prisma setup errors; `undefined` falls back to the generic one. */\nfunction actionableErrorMessage(error: unknown): string | undefined {\n if (isTableNotFoundError(error)) {\n return \"Table 'SitepingFeedback' not found. Run 'npx prisma db push' to create it.\";\n }\n return undefined;\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 * Filtering order matches the historical adapter behaviour:\n * 1. projectName (always required)\n * 2. type\n * 3. status / statuses (`statuses` bucket wins when both are set)\n * 4. url\n * 5. urlPattern\n * 6. 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/**\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 let results: FeedbackRecord[] = items.filter((f) => f.projectName === query.projectName);\n\n if (query.type) results = results.filter((f) => f.type === query.type);\n // `statuses` (bucket / any-of) wins over the exact `status` filter when both\n // are present; an empty array is treated as absent.\n if (query.statuses && query.statuses.length > 0) {\n const allowed = query.statuses;\n results = results.filter((f) => allowed.includes(f.status));\n } else if (query.status) {\n results = results.filter((f) => f.status === query.status);\n }\n if (query.url) results = results.filter((f) => f.url === query.url);\n if (query.urlPattern) results = results.filter((f) => f.urlPattern === query.urlPattern);\n if (query.search) {\n const s = query.search.toLowerCase();\n results = results.filter((f) => f.message.toLowerCase().includes(s));\n }\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).\n results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());\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 if (isUnreachableOffset(skip)) return { feedbacks: [], total };\n\n return { feedbacks: results.slice(skip, skip + limit), total };\n}\n","/**\n * General-purpose TypeScript utility types used across `@beezping/*`.\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 * 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, 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\"] 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 * mobile viewport, or server-side 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 maxConsoleEntries?: number | undefined;\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/** 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 * 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 * Cookie policy for HTTP-mode requests — forwarded verbatim as the\n * `credentials` option of every `fetch` the widget (or the dashboard's\n * endpoint source) makes. Mirrors the DOM `RequestCredentials` union,\n * declared here so core stays free of DOM lib types.\n *\n * - `\"same-origin\"` (default): cookies only when the endpoint shares the\n * page's origin — the browser's own default.\n * - `\"include\"`: also send cookies to a cross-origin endpoint. Required when a\n * server on another origin authenticates with a session cookie; the server\n * must answer with credentialed CORS (the page's exact origin plus\n * `Access-Control-Allow-Credentials: true`, e.g. `@beezping/server`'s\n * `allowedOrigins`).\n * - `\"omit\"`: never send cookies, even same-origin.\n */\nexport type SitepingRequestCredentials = \"omit\" | \"same-origin\" | \"include\";\n\n/** Every accepted {@link SitepingRequestCredentials} value — runtime guard source for untyped (script-tag) consumers. */\nexport const REQUEST_CREDENTIALS_MODES = [\n \"omit\",\n \"same-origin\",\n \"include\",\n] as const satisfies readonly SitepingRequestCredentials[];\n\n/**\n * Credentials mode used when none is configured — the browser's own `fetch`\n * default, so leaving the option unset never changes cookie behavior (and\n * never opts a cross-origin endpoint into cookie-carrying, CSRF-prone requests).\n */\nexport const DEFAULT_REQUEST_CREDENTIALS = \"same-origin\" satisfies SitepingRequestCredentials;\n\n/**\n * Narrow an untyped config value to a {@link SitepingRequestCredentials} mode.\n *\n * @param value - Raw `credentials` option (may come from an untyped script-tag config).\n * @returns `true` when `value` is one of {@link REQUEST_CREDENTIALS_MODES}.\n */\nexport function isRequestCredentials(value: unknown): value is SitepingRequestCredentials {\n return typeof value === \"string\" && (REQUEST_CREDENTIALS_MODES as readonly string[]).includes(value);\n}\n\n/**\n * Actionable description of a rejected `credentials` option — shared by the\n * widget init guard and the dashboard's endpoint source so both report the\n * same wording.\n *\n * @param value - The rejected raw value (a config value, never user content).\n * @returns e.g. `invalid \\`credentials\\` \"all\". Expected one of \"omit\", \"same-origin\", \"include\".`\n */\nexport function describeInvalidRequestCredentials(value: unknown): string {\n const accepted = REQUEST_CREDENTIALS_MODES.map((mode) => `\"${mode}\"`).join(\", \");\n return `invalid \\`credentials\\` ${String(JSON.stringify(value))}. Expected one of ${accepted}.`;\n}\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 mobile-viewport 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 `768`.\n *\n * Set lower (e.g. `0`) to allow narrow/mobile viewports, or use `forceShow`\n * 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. 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 `@beezping/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.\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. The widget already\n * hides below `minViewportWidth` (default 768 px), but tablets above that\n * threshold will trigger this flow on long-press.\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 + custom buffer sizes.\n *\n * **Privacy considerations:** console messages may contain anything the\n * host page logs, including user data. Failed network requests record the\n * URL (with query string) but never the response body. Inform end users\n * before enabling in environments where they might log sensitive values.\n */\n captureDiagnostics?: boolean | DiagnosticsCaptureOptions | undefined;\n /** Called when the widget is skipped (production mode, mobile viewport, 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 // 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 /**\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`. The type is widened to `Error` so\n * direct-store callers can still surface raw errors without breaking the\n * contract.\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. `@beezping/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, so an explicit\n * `Authorization` entry overrides `apiKey`. A throwing/rejecting factory\n * fails the request like a network error.\n */\n headers?: SitepingHeadersOption | undefined;\n /**\n * Cookie policy for every HTTP-mode request (submit, list, update, delete\n * and the offline retry-queue replay). Defaults to `\"same-origin\"` — the\n * browser default, so existing setups are unchanged.\n *\n * Set `\"include\"` when `endpoint` lives on **another origin** and the\n * server authenticates with a session cookie (e.g. a custom\n * `access.authenticate` in `@beezping/server`): without it the browser\n * never attaches the cookie and every request is rejected as\n * unauthenticated. The server must allow the page's origin explicitly with\n * credentialed CORS (`allowedOrigins`) — a wildcard origin never works\n * with cookies. Only opt in for origins you trust: cookie-authenticated\n * cross-origin requests rely on the server's CSRF defenses (strict origin\n * allowlist, `SameSite` cookies).\n *\n * An unknown value (untyped script-tag config) is rejected at init: the\n * widget logs an error and does not load.\n */\n credentials?: SitepingRequestCredentials | 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 /** Direct store for client-side mode. Bypasses HTTP entirely. */\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 /** HTTP-mode only — meaningless without an `endpoint`. */\n credentials?: 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`/`credentials`) **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 /**\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 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 * 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// 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/** 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 the stable `STORE_NOT_FOUND` code as well as\n * `instanceof`: every consumer package bundles its own copy of core (tsup\n * `noExternal`), so an adapter's instance fails `instanceof` against the\n * server's class identity.\n */\nexport function isStoreNotFound(\n error: unknown,\n): error is StoreNotFoundError | CodedError<\"STORE_NOT_FOUND\"> | CodedError<\"P2025\"> {\n if (error instanceof StoreNotFoundError) return true;\n // Backwards compat: Prisma's P2025\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 the stable `STORE_DUPLICATE` code as well as\n * `instanceof`, for the same cross-bundle reason as {@link isStoreNotFound}.\n */\nexport function isStoreDuplicate(\n error: unknown,\n): error is StoreDuplicateError | CodedError<\"STORE_DUPLICATE\"> | CodedError<\"P2002\"> {\n if (error instanceof StoreDuplicateError) return true;\n // Backwards compat: Prisma's P2002\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// ---------------------------------------------------------------------------\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 rely on `id` alone — so `createSitepingHandler` refuses to start\n * with a custom `access.authorize` (which may scope callers to projects)\n * over a store that lacks it.\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, `onCreated`) exactly once when concurrent requests\n * race on the 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\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/** DOM anchoring data for re-attaching annotations to page elements. */\nexport interface AnchorData {\n /** CSS selector generated by @medv/finder — primary anchor */\n cssSelector: string;\n /** XPath — fallback 1 */\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). Adding\n * a field to `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<Serialized<Omit<FeedbackRecord, \"clientId\">>>;\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/** Paginated `FeedbackResponse` shape returned by the API. */\nexport interface FeedbackResponseList {\n feedbacks: FeedbackResponse[];\n total: number;\n}\n","/** Most annotations accepted on one feedback (also enforced by the create schema). */\nexport const MAX_ANNOTATIONS_PER_FEEDBACK = 50;\n\n/** Longest refused `Origin` value written to the log (untrusted input). */\nexport const MAX_LOGGED_ORIGIN_LENGTH = 128;\n\n/** Longest invalid `allowedHeaders` entry echoed back in a configuration error. */\nexport const MAX_REPORTED_HEADER_NAME_LENGTH = 64;\n","import { MAX_ANNOTATIONS_PER_FEEDBACK } from \"./limits.js\";\n\n/**\n * `error` strings of the HTTP API. Part of the wire contract: widgets and\n * existing `adapter-prisma` consumers match on some of them.\n */\nexport const SITEPING_ERROR_MESSAGES = {\n invalidJson: \"Invalid JSON\",\n unauthorized: \"Unauthorized\",\n apiKeyRequiredForDestructive: \"apiKey required for destructive operations\",\n forbidden: \"Forbidden\",\n unsupportedMediaType: \"Content-Type must be application/json\",\n feedbackNotFound: \"Feedback not found\",\n clientIdUsedByAnotherProject: \"clientId already used by another project\",\n tooManyAnnotations: `Too many annotations (max ${MAX_ANNOTATIONS_PER_FEEDBACK})`,\n deletionAborted: \"Deletion aborted: a linked resource could not be cleaned up\",\n internalServerError: \"Internal server error\",\n} as const satisfies Record<string, string>;\n\n/**\n * Startup errors thrown by `createSitepingHandler` when its options would\n * leave a security guard inoperative. Not part of the wire contract.\n */\nexport const SITEPING_CONFIGURATION_ERROR_MESSAGES = {\n ownershipVerificationRequired:\n \"[siteping] createSitepingHandler: `access.authorize` needs a store implementing `verifyProjectOwnership`. \" +\n \"Without it, a caller authorized for one project could PATCH or DELETE another project's feedback by id. \" +\n \"Implement `verifyProjectOwnership` on the store (createCollectionStore and PrismaStore already do).\",\n invalidAllowedHeader:\n \"[siteping] allowedHeaders: every entry must be a valid HTTP header name (letters, digits and !#$%&'*+-.^_`|~, no spaces or commas). Invalid entry:\",\n} as const satisfies Record<string, string>;\n","import { SITEPING_ERROR_MESSAGES } from \"./constants/error-messages.js\";\n/** HTTP methods served by `createSitepingHandler`. */\nexport type SitepingHttpMethod = \"GET\" | \"POST\" | \"PATCH\" | \"DELETE\" | \"OPTIONS\";\n\n/** Operation a request performs, passed to `SitepingAccessControl.authorize`. */\nexport type SitepingAction = \"create\" | \"list\" | \"update\" | \"delete\" | \"deleteAll\";\n\n/** Context shared by authorization, transforms and lifecycle hooks. */\nexport interface SitepingRequestContext<Principal> {\n request: Request;\n /** Whoever `authenticate` resolved — `null` for requests the access policy leaves anonymous. */\n principal: Principal | null;\n}\n\n/** What `authorize` decides about. */\nexport interface SitepingAuthorizationContext<Principal> extends SitepingRequestContext<Principal> {\n action: SitepingAction;\n /** Project the request targets (from the body for writes, the query for reads). */\n projectName: string;\n /** Target record id for `update` / `delete`. */\n feedbackId?: string;\n}\n\n/**\n * Pluggable access policy — how the handler knows who is calling and what\n * they may do. Framework- and provider-agnostic: resolve the principal from\n * the standard `Request` (session cookie, JWT, API key, proxy header…).\n *\n * - `authenticate` returning `null` → 401.\n * - `authorize` returning `false` → 403. Defaults to allowing every\n * authenticated principal. When set, the store must implement\n * `verifyProjectOwnership`: PATCH/DELETE address records by id, and the\n * check is what keeps the authorized `projectName` bound to the record.\n * - `canReadAuthorEmail` decides whether responses include `authorEmail`\n * (reviewer PII) — every response, including the `POST` answer for a\n * fresh or replayed submission (`beforeCreate` may have replaced the\n * submitted email). Defaults to `true` for authenticated principals; a\n * throw answers `500`.\n */\nexport interface SitepingAccessControl<Principal> {\n authenticate(request: Request): Principal | null | Promise<Principal | null>;\n authorize?(context: SitepingAuthorizationContext<Principal>): boolean | Promise<boolean>;\n canReadAuthorEmail?(principal: Principal): boolean;\n}\n\n/** Outcome of authenticating one request. @internal */\nexport type AuthenticationOutcome<Principal> =\n | { ok: true; principal: Principal | null; canReadAuthorEmail: boolean }\n | { ok: false; status: 401 | 403; error: string };\n\n/**\n * Normalized policy the handler runs: authenticate once per request, then\n * authorize once the target project is known (after body/query parsing).\n * @internal\n */\nexport interface AccessGate<Principal> {\n /**\n * Whether a successful `POST` echoes `authorEmail` whatever the\n * requester's read permission. Only the legacy api-key policy sets it: its\n * public `POST` has always returned the email to the submitter.\n */\n echoesAuthorEmailOnCreate: boolean;\n authenticate(request: Request, method: SitepingHttpMethod): Promise<AuthenticationOutcome<Principal>>;\n authorize(context: SitepingAuthorizationContext<Principal>): Promise<boolean>;\n}\n\n/** Wrap a public `SitepingAccessControl` into the handler's gate. @internal */\nexport function accessGateFromControl<Principal>(access: SitepingAccessControl<Principal>): AccessGate<Principal> {\n return {\n echoesAuthorEmailOnCreate: false,\n async authenticate(request) {\n const principal = await access.authenticate(request);\n if (principal === null) return { ok: false, status: 401, error: SITEPING_ERROR_MESSAGES.unauthorized };\n return { ok: true, principal, canReadAuthorEmail: access.canReadAuthorEmail?.(principal) ?? true };\n },\n async authorize(context) {\n return access.authorize ? access.authorize(context) : true;\n },\n };\n}\n","import type { AccessGate, SitepingHttpMethod } from \"./access.js\";\nimport { SITEPING_ERROR_MESSAGES } from \"./constants/error-messages.js\";\n\n/** Options of the built-in shared-secret policy (the historical `adapter-prisma` behavior). */\nexport interface ApiKeyAccessOptions {\n /**\n * Shared secret expected as `Authorization: Bearer {apiKey}`.\n *\n * - **When set:** every request not listed in `publicEndpoints` must carry it (401 otherwise).\n * - **When not set:** the API is public, except DELETE/PATCH while\n * `requireAuthForDestructive` is on.\n */\n apiKey?: string | undefined;\n /** Methods that skip the key. Defaults to `['POST', 'OPTIONS']` when `apiKey` is set. */\n publicEndpoints?: ReadonlyArray<SitepingHttpMethod>;\n /**\n * Whether DELETE/PATCH require `apiKey`. Defaults to `true`: without `apiKey`\n * the handler refuses to start in production and answers 401 elsewhere.\n * Set `false` only behind your own auth layer — or pass `access` instead.\n */\n requireAuthForDestructive?: boolean;\n /**\n * Blank `authorEmail` in responses to requests without a valid key.\n * Defaults to `true` — reviewer emails are PII and GET must stay reachable\n * by the widget.\n */\n redactUnauthenticatedEmails?: boolean;\n}\n\nconst textEncoder = new TextEncoder();\n\n/**\n * Constant-time comparison without `node:crypto`, so the handler runs on any\n * runtime with Web APIs (Node, Bun, Deno, edge workers). A length difference\n * returns early (unavoidable leak); comparing bytes keeps multi-byte input\n * from turning into an exception.\n */\nfunction constantTimeEqual(received: string, expected: string): boolean {\n const receivedBytes = textEncoder.encode(received);\n const expectedBytes = textEncoder.encode(expected);\n if (receivedBytes.length !== expectedBytes.length) return false;\n let difference = 0;\n for (let index = 0; index < receivedBytes.length; index++) {\n difference |= (receivedBytes[index] ?? 0) ^ (expectedBytes[index] ?? 0);\n }\n return difference === 0;\n}\n\n/** `NODE_ENV === \"production\"`, tolerating runtimes without `process` (edge workers, Deno). */\nfunction isProductionEnvironment(): boolean {\n return typeof process !== \"undefined\" && process.env?.NODE_ENV === \"production\";\n}\n\n/**\n * Build the shared-secret gate. Throws at startup when it would expose an\n * unauthenticated destructive surface in production.\n * @internal\n */\nexport function createApiKeyGate({\n apiKey,\n publicEndpoints = apiKey ? [\"POST\", \"OPTIONS\"] : undefined,\n requireAuthForDestructive = true,\n redactUnauthenticatedEmails = true,\n}: ApiKeyAccessOptions): AccessGate<never> {\n // Without this guard anyone could `DELETE { deleteAll: true }` against the API.\n if (!apiKey && requireAuthForDestructive && isProductionEnvironment()) {\n throw new Error(\n \"[siteping] createSitepingHandler: apiKey is required in production. \" +\n \"Set `apiKey` to enable destructive endpoints, pass `access` to plug your own auth, or pass \" +\n \"`requireAuthForDestructive: false` if SitePing sits behind your own auth middleware.\",\n );\n }\n\n const publicMethods: ReadonlySet<SitepingHttpMethod> | null = publicEndpoints ? new Set(publicEndpoints) : null;\n\n const isBearerAuthenticated = (request: Request): boolean => {\n if (!apiKey) return false;\n const header = request.headers.get(\"Authorization\");\n return header !== null && constantTimeEqual(header, `Bearer ${apiKey}`);\n };\n\n return {\n // Legacy contract: a successful POST echoes the email back to its author.\n echoesAuthorEmailOnCreate: true,\n async authenticate(request, method) {\n const canReadAuthorEmail = !redactUnauthenticatedEmails || isBearerAuthenticated(request);\n if (!apiKey) {\n // GET/POST/OPTIONS stay open by default so the widget works in dev without config.\n if (requireAuthForDestructive && (method === \"DELETE\" || method === \"PATCH\")) {\n return { ok: false, status: 401, error: SITEPING_ERROR_MESSAGES.apiKeyRequiredForDestructive };\n }\n return { ok: true, principal: null, canReadAuthorEmail };\n }\n if (publicMethods?.has(method) || isBearerAuthenticated(request)) {\n return { ok: true, principal: null, canReadAuthorEmail };\n }\n return { ok: false, status: 401, error: SITEPING_ERROR_MESSAGES.unauthorized };\n },\n async authorize() {\n return true;\n },\n };\n}\n","/**\n * `Cache-Control` of the list endpoint, whatever the access policy. The\n * response depends on the caller's credentials — the `Authorization` key\n * (authentication, `authorEmail` redaction) or a session cookie (principal,\n * `presentFeedback`) — none of which the browser cache varies on, so a\n * cached copy could be replayed to a request that lost those credentials.\n */\nexport const LIST_CACHE_CONTROL = \"no-store\";\n\n/** `Cache-Control` of the identity endpoint — depends on the session, never cached. */\nexport const IDENTITY_CACHE_CONTROL = \"no-store\";\n\n/**\n * Methods that change data, guarded against cross-site request forgery\n * (origin allowlist + JSON content type) before any other processing.\n */\nexport const CSRF_PROTECTED_METHODS: ReadonlyArray<string> = [\"POST\", \"PATCH\", \"DELETE\"];\n\n/**\n * Only media type accepted on mutating requests. Not CORS-safelisted, so a\n * cross-origin browser must preflight it — a forged `text/plain` form or\n * `fetch` is refused instead of being parsed as JSON.\n */\nexport const JSON_MEDIA_TYPE = \"application/json\";\n\n/** Methods announced in CORS preflight responses of the feedback endpoint. */\nexport const CORS_ALLOWED_METHODS = \"GET, POST, PATCH, DELETE, OPTIONS\";\n\n/** Methods announced in CORS preflight responses of the identity endpoint. */\nexport const IDENTITY_CORS_ALLOWED_METHODS = \"GET, OPTIONS\";\n\n/**\n * Request headers the widget sends cross-origin — always allowed. The\n * `allowedHeaders` option extends this list (e.g. a custom session header\n * read by `access.authenticate`), never replaces it.\n */\nexport const CORS_DEFAULT_ALLOWED_HEADERS: ReadonlyArray<string> = [\"Content-Type\", \"Authorization\"];\n\n/**\n * Valid HTTP header field name (RFC 9110 `token`). Guards `allowedHeaders`\n * so a typo cannot inject separators into `Access-Control-Allow-Headers`.\n */\nexport const HTTP_HEADER_NAME_PATTERN = /^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$/;\n\n/** How long browsers may cache a preflight answer, in seconds (24 h). */\nexport const CORS_MAX_AGE_SECONDS = 86_400;\n\n/** Query parameters the list endpoint reads (everything else is ignored). */\nexport const LIST_QUERY_KEYS = [\n \"projectName\",\n \"page\",\n \"limit\",\n \"type\",\n \"status\",\n \"statuses\",\n \"search\",\n \"url\",\n \"urlPattern\",\n] as const;\n","import { SITEPING_CONFIGURATION_ERROR_MESSAGES } from \"./constants/error-messages.js\";\nimport { CORS_DEFAULT_ALLOWED_HEADERS, CORS_MAX_AGE_SECONDS, HTTP_HEADER_NAME_PATTERN } from \"./constants/http.js\";\nimport { MAX_REPORTED_HEADER_NAME_LENGTH } from \"./constants/limits.js\";\n\nexport type CorsHeaders = Readonly<Record<string, string>>;\n\n/**\n * Resolved CORS configuration of one endpoint, built once at startup by\n * `createCorsPolicy` so requests never re-validate it.\n */\nexport interface CorsPolicy {\n /** Exact-match origin allowlist; `undefined` disables CORS headers entirely. */\n readonly allowedOrigins: ReadonlyArray<string> | undefined;\n /** `Access-Control-Allow-Methods` value. */\n readonly allowedMethods: string;\n /** `Access-Control-Allow-Headers` value — defaults plus the configured extras. */\n readonly allowedHeaders: string;\n}\n\n/** CORS options an endpoint factory receives from its caller. */\nexport interface CorsPolicyOptions {\n allowedOrigins: ReadonlyArray<string> | undefined;\n /** Extra request headers to allow on top of `CORS_DEFAULT_ALLOWED_HEADERS`. */\n allowedHeaders: ReadonlyArray<string> | undefined;\n /** Methods this endpoint serves, announced to preflights. */\n allowedMethods: string;\n}\n\n/**\n * Merge `extraHeaders` into the default allowlist, case-insensitively and\n * without duplicates (the first spelling wins).\n *\n * @throws Error when an entry is not a valid HTTP header name — a comma or\n * space would otherwise smuggle arbitrary values into the response header.\n */\nexport function resolveAllowedHeaders(extraHeaders: ReadonlyArray<string> | undefined): string {\n const headersByLowercaseName = new Map<string, string>();\n for (const header of [...CORS_DEFAULT_ALLOWED_HEADERS, ...(extraHeaders ?? [])]) {\n if (typeof header !== \"string\" || !HTTP_HEADER_NAME_PATTERN.test(header)) {\n const reported = String(header).slice(0, MAX_REPORTED_HEADER_NAME_LENGTH);\n throw new Error(`${SITEPING_CONFIGURATION_ERROR_MESSAGES.invalidAllowedHeader} ${JSON.stringify(reported)}`);\n }\n const lowercaseName = header.toLowerCase();\n if (!headersByLowercaseName.has(lowercaseName)) headersByLowercaseName.set(lowercaseName, header);\n }\n return [...headersByLowercaseName.values()].join(\", \");\n}\n\n/**\n * Build the CORS policy of an endpoint. The allowed headers are a fixed,\n * validated list: the request's `Access-Control-Request-Headers` is never\n * reflected, so only headers the integrator declared can be preflighted.\n *\n * @throws Error when `allowedHeaders` contains an invalid header name.\n */\nexport function createCorsPolicy({ allowedOrigins, allowedHeaders, allowedMethods }: CorsPolicyOptions): CorsPolicy {\n return { allowedOrigins, allowedMethods, allowedHeaders: resolveAllowedHeaders(allowedHeaders) };\n}\n\n/**\n * CORS headers for `request`: only origins listed in `policy.allowedOrigins`\n * are reflected; without the option no CORS headers are emitted (no\n * permissive wildcard by default).\n */\nexport function buildCorsHeaders(request: Request, policy: CorsPolicy): CorsHeaders {\n if (!policy.allowedOrigins) return {};\n const origin = request.headers.get(\"Origin\");\n if (!origin || !policy.allowedOrigins.includes(origin)) return {};\n return {\n \"Access-Control-Allow-Origin\": origin,\n \"Access-Control-Allow-Methods\": policy.allowedMethods,\n \"Access-Control-Allow-Headers\": policy.allowedHeaders,\n \"Access-Control-Allow-Credentials\": \"true\",\n \"Access-Control-Max-Age\": String(CORS_MAX_AGE_SECONDS),\n Vary: \"Origin\",\n };\n}\n\n/** `204` answer to a CORS preflight (`OPTIONS`), with the policy's headers for allowed origins. */\nexport function preflightResponse(request: Request, policy: CorsPolicy): Response {\n return new Response(null, { status: 204, headers: buildCorsHeaders(request, policy) });\n}\n\n/** Attach CORS headers to an existing Response. */\nexport function withCors(response: Response, corsHeaders: CorsHeaders): Response {\n for (const [key, value] of Object.entries(corsHeaders)) {\n response.headers.set(key, value);\n }\n return response;\n}\n","import { SITEPING_ERROR_MESSAGES } from \"./constants/error-messages.js\";\nimport { type CorsHeaders, withCors } from \"./cors.js\";\nimport type { SitepingLogger } from \"./options.js\";\n\n/** Logs to `console.error` — the logger of handlers created without one. */\nexport const defaultLogger: SitepingLogger = {\n error(message, context) {\n console.error(message, context);\n },\n};\n\n/** Everything needed to report one unexpected failure of a request. */\nexport interface InternalErrorReport {\n logger: SitepingLogger;\n /** Optional safe hint returned to the caller instead of the generic message. */\n describeError?: ((error: unknown) => string | undefined) | undefined;\n /** Factory the failure happened in, e.g. `createSitepingHandler`. */\n source: string;\n /** What was running, e.g. `create feedback` or `authenticate request`. */\n operation: string;\n request: Request;\n corsHeaders: CorsHeaders;\n /** Extra response headers (e.g. `Cache-Control`). */\n headers?: HeadersInit | undefined;\n failure: unknown;\n}\n\n/**\n * Log an unexpected failure with its request context (operation, method,\n * path — never the query, headers or body) and the original error, then\n * answer a JSON 500 carrying the request's CORS headers. The body holds the\n * generic message unless `describeError` supplies a safe hint, so the\n * failure's own details never reach the caller.\n */\nexport function internalErrorResponse({\n logger,\n describeError,\n source,\n operation,\n request,\n corsHeaders,\n headers,\n failure,\n}: InternalErrorReport): Response {\n logger.error(`[siteping] ${source}: ${operation} failed`, {\n error: failure,\n method: request.method,\n path: new URL(request.url).pathname,\n });\n const message = describeError?.(failure) ?? SITEPING_ERROR_MESSAGES.internalServerError;\n return withCors(Response.json({ error: message }, { status: 500, ...(headers ? { headers } : {}) }), corsHeaders);\n}\n","/**\n * The one email pattern every Siteping surface validates against.\n *\n * The widget's identity modal and the HTTP handler's schema used to disagree\n * (a permissive regex on one side, an ASCII-only default on the other), so an\n * address the modal accepted — and persisted in localStorage for good — came\n * back as a 400 on every submission afterwards. One pattern, imported on both\n * sides, makes that drift impossible.\n *\n * Deliberately Unicode-aware: internationalised local parts and domains\n * (`françois@exemple.fr`, `user@münchen.de`) are real addresses and common in\n * the audience this widget serves. Structure follows the usual rules — a\n * local part of 1 to 64 characters with no leading, trailing or doubled dot,\n * dot-separated domain labels of at most 63 characters that neither start nor\n * end with a hyphen, and a final label of at least two characters.\n */\nexport const EMAIL_PATTERN =\n /^(?!\\.)(?!.*\\.\\.)[\\p{L}\\p{N}!#$%&'*+/=?^_`{|}~.-]{0,63}[\\p{L}\\p{N}!#$%&'*+/=?^_`{|}~-]@(?:[\\p{L}\\p{N}](?:[\\p{L}\\p{N}-]{0,61}[\\p{L}\\p{N}])?\\.)+[\\p{L}\\p{N}-]{2,63}$/u;\n\n/** Whether `value` is an email address Siteping accepts — see {@link EMAIL_PATTERN}. */\nexport function isValidEmail(value: string): boolean {\n return EMAIL_PATTERN.test(value);\n}\n","/**\n * General-purpose TypeScript utility types used across `@beezping/*`.\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 * 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, 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\"] 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 * mobile viewport, or server-side 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 maxConsoleEntries?: number | undefined;\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/** 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 * 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 * Cookie policy for HTTP-mode requests — forwarded verbatim as the\n * `credentials` option of every `fetch` the widget (or the dashboard's\n * endpoint source) makes. Mirrors the DOM `RequestCredentials` union,\n * declared here so core stays free of DOM lib types.\n *\n * - `\"same-origin\"` (default): cookies only when the endpoint shares the\n * page's origin — the browser's own default.\n * - `\"include\"`: also send cookies to a cross-origin endpoint. Required when a\n * server on another origin authenticates with a session cookie; the server\n * must answer with credentialed CORS (the page's exact origin plus\n * `Access-Control-Allow-Credentials: true`, e.g. `@beezping/server`'s\n * `allowedOrigins`).\n * - `\"omit\"`: never send cookies, even same-origin.\n */\nexport type SitepingRequestCredentials = \"omit\" | \"same-origin\" | \"include\";\n\n/** Every accepted {@link SitepingRequestCredentials} value — runtime guard source for untyped (script-tag) consumers. */\nexport const REQUEST_CREDENTIALS_MODES = [\n \"omit\",\n \"same-origin\",\n \"include\",\n] as const satisfies readonly SitepingRequestCredentials[];\n\n/**\n * Credentials mode used when none is configured — the browser's own `fetch`\n * default, so leaving the option unset never changes cookie behavior (and\n * never opts a cross-origin endpoint into cookie-carrying, CSRF-prone requests).\n */\nexport const DEFAULT_REQUEST_CREDENTIALS = \"same-origin\" satisfies SitepingRequestCredentials;\n\n/**\n * Narrow an untyped config value to a {@link SitepingRequestCredentials} mode.\n *\n * @param value - Raw `credentials` option (may come from an untyped script-tag config).\n * @returns `true` when `value` is one of {@link REQUEST_CREDENTIALS_MODES}.\n */\nexport function isRequestCredentials(value: unknown): value is SitepingRequestCredentials {\n return typeof value === \"string\" && (REQUEST_CREDENTIALS_MODES as readonly string[]).includes(value);\n}\n\n/**\n * Actionable description of a rejected `credentials` option — shared by the\n * widget init guard and the dashboard's endpoint source so both report the\n * same wording.\n *\n * @param value - The rejected raw value (a config value, never user content).\n * @returns e.g. `invalid \\`credentials\\` \"all\". Expected one of \"omit\", \"same-origin\", \"include\".`\n */\nexport function describeInvalidRequestCredentials(value: unknown): string {\n const accepted = REQUEST_CREDENTIALS_MODES.map((mode) => `\"${mode}\"`).join(\", \");\n return `invalid \\`credentials\\` ${String(JSON.stringify(value))}. Expected one of ${accepted}.`;\n}\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 mobile-viewport 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 `768`.\n *\n * Set lower (e.g. `0`) to allow narrow/mobile viewports, or use `forceShow`\n * 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. 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 `@beezping/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.\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. The widget already\n * hides below `minViewportWidth` (default 768 px), but tablets above that\n * threshold will trigger this flow on long-press.\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 + custom buffer sizes.\n *\n * **Privacy considerations:** console messages may contain anything the\n * host page logs, including user data. Failed network requests record the\n * URL (with query string) but never the response body. Inform end users\n * before enabling in environments where they might log sensitive values.\n */\n captureDiagnostics?: boolean | DiagnosticsCaptureOptions | undefined;\n /** Called when the widget is skipped (production mode, mobile viewport, 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 // 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 /**\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`. The type is widened to `Error` so\n * direct-store callers can still surface raw errors without breaking the\n * contract.\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. `@beezping/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, so an explicit\n * `Authorization` entry overrides `apiKey`. A throwing/rejecting factory\n * fails the request like a network error.\n */\n headers?: SitepingHeadersOption | undefined;\n /**\n * Cookie policy for every HTTP-mode request (submit, list, update, delete\n * and the offline retry-queue replay). Defaults to `\"same-origin\"` — the\n * browser default, so existing setups are unchanged.\n *\n * Set `\"include\"` when `endpoint` lives on **another origin** and the\n * server authenticates with a session cookie (e.g. a custom\n * `access.authenticate` in `@beezping/server`): without it the browser\n * never attaches the cookie and every request is rejected as\n * unauthenticated. The server must allow the page's origin explicitly with\n * credentialed CORS (`allowedOrigins`) — a wildcard origin never works\n * with cookies. Only opt in for origins you trust: cookie-authenticated\n * cross-origin requests rely on the server's CSRF defenses (strict origin\n * allowlist, `SameSite` cookies).\n *\n * An unknown value (untyped script-tag config) is rejected at init: the\n * widget logs an error and does not load.\n */\n credentials?: SitepingRequestCredentials | 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 /** Direct store for client-side mode. Bypasses HTTP entirely. */\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 /** HTTP-mode only — meaningless without an `endpoint`. */\n credentials?: 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`/`credentials`) **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 /**\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 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 * 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// 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/** 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 the stable `STORE_NOT_FOUND` code as well as\n * `instanceof`: every consumer package bundles its own copy of core (tsup\n * `noExternal`), so an adapter's instance fails `instanceof` against the\n * server's class identity.\n */\nexport function isStoreNotFound(\n error: unknown,\n): error is StoreNotFoundError | CodedError<\"STORE_NOT_FOUND\"> | CodedError<\"P2025\"> {\n if (error instanceof StoreNotFoundError) return true;\n // Backwards compat: Prisma's P2025\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 the stable `STORE_DUPLICATE` code as well as\n * `instanceof`, for the same cross-bundle reason as {@link isStoreNotFound}.\n */\nexport function isStoreDuplicate(\n error: unknown,\n): error is StoreDuplicateError | CodedError<\"STORE_DUPLICATE\"> | CodedError<\"P2002\"> {\n if (error instanceof StoreDuplicateError) return true;\n // Backwards compat: Prisma's P2002\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// ---------------------------------------------------------------------------\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 rely on `id` alone — so `createSitepingHandler` refuses to start\n * with a custom `access.authorize` (which may scope callers to projects)\n * over a store that lacks it.\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, `onCreated`) exactly once when concurrent requests\n * race on the 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\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/** DOM anchoring data for re-attaching annotations to page elements. */\nexport interface AnchorData {\n /** CSS selector generated by @medv/finder — primary anchor */\n cssSelector: string;\n /** XPath — fallback 1 */\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). Adding\n * a field to `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<Serialized<Omit<FeedbackRecord, \"clientId\">>>;\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/** Paginated `FeedbackResponse` shape returned by the API. */\nexport interface FeedbackResponseList {\n feedbacks: FeedbackResponse[];\n total: number;\n}\n","import type { AssertEqual, FeedbackPayload, FeedbackStatus, FeedbackType, Prettify } from \"@beezping/core\";\nimport { CONSOLE_DIAGNOSTIC_LEVELS, EMAIL_PATTERN, FEEDBACK_STATUSES, FEEDBACK_TYPES } from \"@beezping/core\";\nimport * as zod from \"zod\";\n\n// Namespace import required: Zod publishes dual CJS/ESM, and bundlers (tsup, vitest) may\n// resolve the CJS entry where `import { z } from \"zod\"` fails because CJS wraps\n// the entire module under a default/namespace key. This workaround normalizes access\n// regardless of which entry point the bundler resolves.\n// See: https://github.com/colinhacks/zod/issues/2697\nconst z: typeof zod.z = (\"z\" in zod ? zod.z : zod) as typeof zod.z;\n\nconst anchorSchema = z.object({\n cssSelector: z.string().min(1).max(2000),\n xpath: z.string().min(1).max(2000),\n textSnippet: z.string().max(500),\n elementTag: z.string().min(1),\n elementId: z.string().optional(),\n textPrefix: z.string().max(200),\n textSuffix: z.string().max(200),\n fingerprint: z.string().max(200),\n neighborText: z.string().max(500),\n // Optional semantic anchor identifier from `data-feedback-anchor`.\n // Null when no semantic ancestor exists; widget always sends this field.\n anchorKey: z.string().max(200).nullable().optional(),\n});\n\nconst rectSchema = z.object({\n xPct: z.number().min(0).max(1),\n yPct: z.number().min(0).max(1),\n wPct: z.number().min(0).max(1),\n hPct: z.number().min(0).max(1),\n});\n\nconst annotationSchema = z.object({\n anchor: anchorSchema,\n rect: rectSchema,\n scrollX: z.number().min(0),\n scrollY: z.number().min(0),\n viewportW: z.number().int().positive(),\n viewportH: z.number().int().positive(),\n devicePixelRatio: z.number().positive().default(1),\n});\n\n// Diagnostics caps mirror the widget defaults — the widget never sends more\n// than these, but the schema enforces it server-side too so a tampered\n// client can't blow up the JSON column with megabytes of garbage.\nconst consoleEntrySchema = z.object({\n level: z.enum(CONSOLE_DIAGNOSTIC_LEVELS),\n timestamp: z.string().max(50),\n message: z.string().max(600),\n});\n\nconst networkEntrySchema = z.object({\n url: z.string().max(2000),\n method: z.string().max(20),\n status: z.number().int().min(0).max(599),\n durationMs: z.number().min(0).max(600_000),\n timestamp: z.string().max(50),\n});\n\nconst diagnosticsSchema = z.object({\n console: z.array(consoleEntrySchema).max(50),\n network: z.array(networkEntrySchema).max(20),\n});\n\n// Annotation rect position within the screenshot image, as fractions [0, 1]\n// of the image dimensions (see `ScreenshotRegion` in core). Strict: this is\n// persisted verbatim into a JSON column, so unknown keys are rejected rather\n// than silently stored.\nexport const screenshotRegionSchema = z.strictObject({\n xPct: z.number().min(0).max(1),\n yPct: z.number().min(0).max(1),\n wPct: z.number().min(0).max(1),\n hPct: z.number().min(0).max(1),\n});\n\nexport const feedbackCreateSchema = z.object({\n projectName: z.string().min(1).max(200),\n type: z.enum(FEEDBACK_TYPES),\n message: z.string().min(1).max(5000),\n // Page-scope identifier the widget uses to group feedbacks. Defaults to\n // `window.location.pathname` (\"/orders/42\"), but hosts can override\n // `getPageScope()` to return a full URL, an opaque slug, anything they\n // want. We trim + require non-empty so whitespace-only payloads don't\n // leak into the DB; otherwise the value is opaque to the server and only\n // used as a literal Prisma equality filter, so the loose shape is safe.\n url: z.string().trim().min(1).max(2000),\n // Optional parameterized URL template (e.g. \"/orders/:orderId\") provided\n // by the host via `getPageScope()`. Null when host omits it.\n urlPattern: z.string().max(2000).nullable().optional(),\n viewport: z.string().min(1).max(50),\n userAgent: z.string().min(1).max(500),\n authorName: z.string().min(1).max(200),\n // The widget's identity modal validates against the same core pattern, so\n // an address the modal accepts (and persists) is never a 400 here.\n authorEmail: z.email({ pattern: EMAIL_PATTERN }).max(200),\n annotations: z.array(annotationSchema).max(50),\n // Restrict to URL-safe identifiers. The widget generates UUIDs (or a\n // Date+Math.random fallback), both of which match. Anything outside this\n // alphabet — `..`, `/`, NUL, etc. — would be a path-traversal vector once\n // the adapter forwards clientId to `screenshotStorage.upload({ feedbackId })`\n // (e.g. an S3 key prefix or a local FS path).\n clientId: z\n .string()\n .min(1)\n .max(200)\n .regex(/^[a-zA-Z0-9_-]+$/, \"clientId must be alphanumeric (a-z, A-Z, 0-9, _, -)\"),\n // Optional base64 JPEG data URL captured by the widget when\n // `enableScreenshot: true`. ~1.5 MB cap = roughly a 1.1 MB JPEG, well\n // above typical sizes (the widget downscales to 1200px). Rejects abuse\n // without truncating legitimate captures.\n screenshotDataUrl: z\n .string()\n .max(1_500_000)\n .regex(/^data:image\\/(jpeg|png|webp);base64,/, \"screenshotDataUrl must be a data:image/* base64 URL\")\n .nullable()\n .optional(),\n // Optional annotation-rect position within the screenshot. Sent by widgets\n // that capture context around the drawn rect; null + omitted are both\n // accepted so legacy clients (and captures without a screenshot) keep\n // working unchanged.\n screenshotRegion: screenshotRegionSchema.nullable().optional(),\n // Optional console + failed-network snapshot. The widget only attaches\n // this when `captureDiagnostics` is enabled; null + omitted are both\n // accepted so existing clients keep working unchanged.\n diagnostics: diagnosticsSchema.nullable().optional(),\n});\n\nexport const feedbackPatchSchema = z.object({\n id: z.string().min(1),\n projectName: z.string().min(1).max(200),\n status: z.enum(FEEDBACK_STATUSES),\n});\n\nexport const feedbackDeleteSchema = z.union([\n z.object({ id: z.string().min(1), projectName: z.string().min(1).max(200) }),\n z.object({ projectName: z.string().min(1).max(200), deleteAll: z.literal(true) }),\n]);\n\nexport const getQuerySchema = z.object({\n projectName: z.string().min(1).max(200),\n page: z.coerce.number().int().min(1).default(1),\n limit: z.coerce.number().int().min(1).max(100).default(50),\n type: z.enum(FEEDBACK_TYPES).optional(),\n status: z.enum(FEEDBACK_STATUSES).optional(),\n // Bucket status filter serialized as CSV over the wire (e.g.\n // `statuses=open,in_progress`). Non-empty strings are split before each value\n // is validated against the known statuses; `statuses` wins over the exact\n // `status` filter downstream. Capped at 4 — the number of known statuses.\n statuses: z\n .preprocess(\n (val) => (typeof val === \"string\" && val.length > 0 ? val.split(\",\") : val),\n z.array(z.enum(FEEDBACK_STATUSES)).max(4),\n )\n .optional(),\n search: z.string().max(200).optional(),\n // Page scope filters — used by the panel's \"this page / this type\" controls\n url: z.string().max(2000).optional(),\n urlPattern: z.string().max(2000).optional(),\n});\n\n// ---------------------------------------------------------------------------\n// Explicit public interfaces — decoupled from Zod to keep .d.ts clean.\n//\n// The create payload needs no local interface at all: the schema validates\n// core's `FeedbackPayload` wire shape, and the compile-time lock below keeps\n// the two identical. Only the wire shapes that exist solely at this HTTP\n// boundary (PATCH / DELETE / GET query) are declared here.\n// ---------------------------------------------------------------------------\n\nexport interface FeedbackPatchInput {\n id: string;\n projectName: string;\n status: FeedbackStatus;\n}\n\nexport interface FeedbackDeleteSingle {\n id: string;\n projectName: string;\n}\n\nexport interface FeedbackDeleteAll {\n projectName: string;\n deleteAll: true;\n}\n\nexport type FeedbackDeleteInput = FeedbackDeleteSingle | FeedbackDeleteAll;\n\nexport interface GetQueryInput {\n projectName: string;\n /** Set to 1 by schema default when omitted from raw input. */\n page: number;\n /** Set to 50 by schema default when omitted from raw input. */\n limit: number;\n type?: FeedbackType | undefined;\n status?: FeedbackStatus | undefined;\n statuses?: FeedbackStatus[] | undefined;\n search?: string | undefined;\n url?: string | undefined;\n urlPattern?: string | undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Type-level locks: the Zod schemas validate exactly the shapes the rest of\n// the monorepo speaks.\n//\n// The create schema is locked against core's `FeedbackPayload` — the actual\n// wire contract the widget sends — so schema drift from the real payload is\n// a compile error, not a runtime 400. The boundary-only shapes (PATCH /\n// DELETE / GET query) are locked against their local interfaces the same\n// way. `Prettify<>` flattens the inferred shape so the equality check\n// compares clean object types rather than intersection chains.\n// ---------------------------------------------------------------------------\n\nconst _createSchemaMatchesPayload: AssertEqual<\n Prettify<zod.z.infer<typeof feedbackCreateSchema>>,\n FeedbackPayload\n> = true;\nvoid _createSchemaMatchesPayload;\nconst _patchSchemaMatchesInput: AssertEqual<\n Prettify<zod.z.infer<typeof feedbackPatchSchema>>,\n FeedbackPatchInput\n> = true;\nvoid _patchSchemaMatchesInput;\nconst _deleteSchemaMatchesInput: AssertEqual<\n Prettify<zod.z.infer<typeof feedbackDeleteSchema>>,\n FeedbackDeleteInput\n> = true;\nvoid _deleteSchemaMatchesInput;\nconst _querySchemaMatchesInput: AssertEqual<Prettify<zod.z.infer<typeof getQuerySchema>>, GetQueryInput> = true;\nvoid _querySchemaMatchesInput;\n\n/** Single validation issue extracted from a `ZodError`. */\nexport interface ValidationIssue {\n field: string;\n message: string;\n}\n\n/**\n * Map Zod errors to a flat array of `{ field, message }` objects.\n * Safe: does not leak input values or schema structure.\n */\nexport function formatValidationErrors(error: zod.z.ZodError): ValidationIssue[] {\n // Zod 4 types `issue.path` as PropertyKey[] (symbols possible in theory);\n // String() keeps the join total instead of throwing on non-string keys.\n return error.issues.map((issue) => ({\n field: issue.path.map(String).join(\".\"),\n message: issue.message,\n }));\n}\n","/**\n * Outgoing webhook notifications for newly-created feedbacks.\n *\n * Plug a Slack, Discord, or generic HTTP endpoint into `createSitepingHandler`\n * to receive a payload whenever a feedback is successfully persisted. Webhooks\n * are dispatched as fire-and-forget (`void Promise.all(...)`) so a slow or\n * down receiver never blocks the client response — the feedback is already in\n * the DB by the time we dial out.\n *\n * - **Type-specific formatting**: Slack uses `{ text, blocks }`, Discord uses\n * `{ content, embeds }`, generic posts the record as JSON (minus `clientId`).\n * - **Untrusted input**: `message` and `authorName` come from anonymous\n * visitors. Slack text is escaped and Discord mention parsing is disabled,\n * so a public feedback form can never be turned into a channel-wide ping.\n * - **Timeout**: 5s by default (overridable per webhook).\n * - **Error handling**: `config.onError(err, feedback.id)` is invoked when\n * present; otherwise we log a one-liner to `console.warn` so the issue is\n * surfaced without crashing the request.\n */\n\nimport type { FeedbackRecord, FeedbackType } from \"@beezping/core\";\n\n/** Supported webhook integrations — drives the JSON body shape. */\nexport type WebhookType = \"slack\" | \"discord\" | \"generic\";\n\n/**\n * Outgoing webhook configuration.\n *\n * - `url` — required, the HTTPS endpoint to POST to.\n * - `type` — payload format. Defaults to `\"generic\"` (raw JSON).\n * - `headers` — extra headers merged on top of `Content-Type: application/json`.\n * Useful for signed-payload schemes (`X-Signature`, bearer tokens, …).\n * - `timeoutMs` — abort the fetch after this many ms. Defaults to 5000.\n * - `onError` — invoked with the underlying error and the feedback id when\n * the dispatch fails (network error, non-2xx, timeout). The webhook is\n * fire-and-forget, so this is your only chance to observe failures.\n */\nexport interface WebhookConfig {\n url: string;\n type?: WebhookType;\n headers?: Record<string, string>;\n timeoutMs?: number;\n onError?: (err: Error, feedbackId: string) => void;\n}\n\nconst DEFAULT_TIMEOUT_MS = 5000;\n\n/** Decimal RGB colour table used by Discord embeds — keyed by feedback type. */\nconst DISCORD_COLORS: Readonly<Record<FeedbackType, number>> = {\n bug: 0xef4444,\n question: 0x3b82f6,\n change: 0xf59e0b,\n other: 0x6b7280,\n};\n\nconst DEFAULT_DISCORD_COLOR = 0x6b7280;\n\n// ---------------------------------------------------------------------------\n// Payload shapes — narrow types let TypeScript catch malformed bodies at\n// compile time rather than only at the receiving end.\n// ---------------------------------------------------------------------------\n\n/** Block Kit envelope used by Slack incoming webhooks. */\nexport interface SlackWebhookPayload {\n text: string;\n blocks: ReadonlyArray<SlackHeaderBlock | SlackSectionBlock | SlackContextBlock>;\n}\n\ninterface SlackHeaderBlock {\n type: \"header\";\n text: { type: \"plain_text\"; text: string; emoji: true };\n}\n\ninterface SlackSectionBlock {\n type: \"section\";\n text: { type: \"mrkdwn\"; text: string };\n}\n\ninterface SlackContextBlock {\n type: \"context\";\n elements: ReadonlyArray<{ type: \"mrkdwn\"; text: string }>;\n}\n\n/** Embed envelope used by Discord incoming webhooks. */\nexport interface DiscordWebhookPayload {\n content: string;\n embeds: ReadonlyArray<{\n title: string;\n description: string;\n color: number;\n fields: ReadonlyArray<{ name: string; value: string; inline: boolean }>;\n timestamp: string;\n }>;\n /**\n * Mention parsing is switched off: `content` carries end-user text, so an\n * author called `@everyone` must render as text, never as a notification.\n */\n allowed_mentions: { parse: ReadonlyArray<\"roles\" | \"users\" | \"everyone\"> };\n}\n\n/**\n * Generic webhook body — the stored record as JSON. `clientId` is stripped like\n * on every other output: it is the browser-local dedup secret and the POST\n * replay path hands the full record to whoever presents it.\n */\nexport type GenericWebhookPayload = Omit<FeedbackRecord, \"clientId\">;\n\n/** Mapping from webhook type to its concrete body shape. */\nexport interface WebhookPayloadMap {\n slack: SlackWebhookPayload;\n discord: DiscordWebhookPayload;\n generic: GenericWebhookPayload;\n}\n\n/** Truncate a message for chat-platform previews (Slack/Discord look bad with walls of text). */\nfunction truncate(text: string, max = 300): string {\n if (text.length <= max) return text;\n return `${text.slice(0, max - 1)}…`;\n}\n\n/** Block Kit caps `header` text at 150 characters — longer payloads are rejected outright. */\nconst SLACK_HEADER_MAX = 150;\n\n/**\n * Escape the three characters Slack parses as control characters in message\n * text (`&`, `<`, `>`) — the exact escaping Slack's formatting rules require\n * for user-provided content. Feedback text is typed by anonymous visitors:\n * unescaped, `<!channel>` notifies the whole channel and\n * `<https://evil.example|Reset your password>` renders as a disguised link.\n */\nfunction escapeSlackText(text: string): string {\n return text.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n}\n\n/**\n * Slack message: text fallback + Block Kit blocks for rich rendering. Every\n * `mrkdwn` field is escaped; the `plain_text` header is rendered verbatim by\n * Slack (no markup parsing), so it keeps the raw author name.\n */\nfunction buildSlackPayload(feedback: FeedbackRecord): SlackWebhookPayload {\n const preview = escapeSlackText(truncate(feedback.message));\n const author = escapeSlackText(feedback.authorName);\n const headline = `New ${feedback.type} feedback from ${feedback.authorName}`;\n return {\n text: `${escapeSlackText(headline)}: ${preview}`,\n blocks: [\n {\n type: \"header\",\n text: { type: \"plain_text\", text: truncate(headline, SLACK_HEADER_MAX), emoji: true },\n },\n {\n type: \"section\",\n text: { type: \"mrkdwn\", text: preview },\n },\n {\n type: \"context\",\n elements: [\n { type: \"mrkdwn\", text: `*Project:* ${escapeSlackText(feedback.projectName)}` },\n { type: \"mrkdwn\", text: `*Type:* ${feedback.type}` },\n { type: \"mrkdwn\", text: `*URL:* ${escapeSlackText(feedback.url)}` },\n { type: \"mrkdwn\", text: `*From:* ${author} (${escapeSlackText(feedback.authorEmail)})` },\n ],\n },\n ],\n };\n}\n\n/**\n * Discord message: content fallback + embed for rich rendering. Sent with\n * mention parsing disabled — `content` embeds the author name, and Discord\n * would otherwise turn `@everyone` / `@here` into a server-wide ping.\n */\nfunction buildDiscordPayload(feedback: FeedbackRecord): DiscordWebhookPayload {\n const preview = truncate(feedback.message);\n return {\n content: `New **${feedback.type}** feedback from **${feedback.authorName}**`,\n embeds: [\n {\n title: `${feedback.type} — ${feedback.projectName}`,\n description: preview,\n color: DISCORD_COLORS[feedback.type] ?? DEFAULT_DISCORD_COLOR,\n fields: [\n { name: \"URL\", value: feedback.url, inline: false },\n { name: \"Author\", value: `${feedback.authorName} (${feedback.authorEmail})`, inline: true },\n { name: \"Viewport\", value: feedback.viewport, inline: true },\n ],\n timestamp: new Date(feedback.createdAt).toISOString(),\n },\n ],\n allowed_mentions: { parse: [] },\n };\n}\n\n/** Generic JSON body — the record minus its `clientId`. */\nfunction buildGenericPayload(feedback: FeedbackRecord): GenericWebhookPayload {\n const { clientId: _clientId, ...payload } = feedback;\n return payload;\n}\n\n/**\n * Build the JSON body for a single webhook based on its `type`.\n * Exported for tests; not part of the public API.\n *\n * @internal\n */\nexport function buildWebhookPayload<T extends WebhookType | undefined>(\n type: T,\n feedback: FeedbackRecord,\n): T extends \"slack\" ? SlackWebhookPayload : T extends \"discord\" ? DiscordWebhookPayload : GenericWebhookPayload {\n switch (type) {\n case \"slack\":\n return buildSlackPayload(feedback) as never;\n case \"discord\":\n return buildDiscordPayload(feedback) as never;\n default:\n return buildGenericPayload(feedback) as never;\n }\n}\n\n/**\n * Dispatch a single webhook. Fire-and-forget: never throws, never rejects.\n *\n * - Builds the type-specific payload.\n * - POSTs with an `AbortSignal` timeout.\n * - On any error (network, non-2xx, timeout, exception), invokes\n * `config.onError(err, feedbackId)` if provided; otherwise logs a one-liner.\n */\nexport async function dispatchWebhook(config: WebhookConfig, feedback: FeedbackRecord): Promise<void> {\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const body = JSON.stringify(buildWebhookPayload(config.type ?? \"generic\", feedback));\n\n // Build merged headers — caller-supplied entries override `Content-Type`\n // when they explicitly need a different mime (rare for chat webhooks, but\n // possible for some generic receivers).\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\", ...(config.headers ?? {}) };\n\n // Use AbortSignal.timeout when available (Node 17.3+, all modern browsers).\n // Fall back to a manual controller for environments lacking it.\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(config.url, {\n method: \"POST\",\n headers,\n body,\n signal: controller.signal,\n });\n clearTimeout(timer);\n\n if (!response.ok) {\n const err = new Error(`Webhook responded with HTTP ${response.status}`);\n reportError(config, err, feedback.id);\n }\n } catch (rawError) {\n clearTimeout(timer);\n const err = rawError instanceof Error ? rawError : new Error(String(rawError));\n reportError(config, err, feedback.id);\n }\n}\n\nfunction reportError(config: WebhookConfig, err: Error, feedbackId: string): void {\n if (config.onError) {\n try {\n config.onError(err, feedbackId);\n } catch (callbackErr) {\n // Defense-in-depth: a thrown user callback must not bubble back up\n // and crash the request that already succeeded persisting the\n // feedback. Surface the original error too so it isn't silently lost.\n console.warn(\n `[siteping] webhook onError() callback threw for feedback ${feedbackId}: ${String(callbackErr)} (original error: ${err.message})`,\n );\n }\n return;\n }\n console.warn(`[siteping] webhook to ${config.url} failed for feedback ${feedbackId}: ${err.message}`);\n}\n\n/**\n * Dispatch every configured webhook in parallel. Awaiting the returned promise\n * lets tests synchronize on completion, but production callers should drop the\n * promise on the floor (`void dispatchWebhooks(...)`) so the HTTP response\n * isn't held back on slow receivers.\n */\nexport async function dispatchWebhooks(configs: readonly WebhookConfig[], feedback: FeedbackRecord): Promise<void> {\n if (configs.length === 0) return;\n await Promise.all(configs.map((c) => dispatchWebhook(c, feedback)));\n}\n","import {\n type FeedbackCreateInput,\n type FeedbackRecord,\n flattenAnnotation,\n isStoreDuplicate,\n type SitepingStore,\n} from \"@beezping/core\";\nimport { SITEPING_ERROR_MESSAGES } from \"../constants/error-messages.js\";\nimport { MAX_ANNOTATIONS_PER_FEEDBACK } from \"../constants/limits.js\";\nimport type { SitepingHandlerBaseOptions } from \"../options.js\";\nimport type { AuthenticatedRequest, RequestPipeline } from \"../request-pipeline.js\";\nimport { feedbackCreateSchema } from \"../validation.js\";\nimport { dispatchWebhooks, type WebhookConfig } from \"../webhooks.js\";\n\ntype FeedbackCreatePayload = ReturnType<typeof feedbackCreateSchema.parse>;\n\nexport interface CreateFeedbackDependencies<Principal> {\n store: SitepingStore;\n pipeline: RequestPipeline<Principal>;\n beforeCreate: SitepingHandlerBaseOptions<Principal>[\"beforeCreate\"];\n onCreated: NonNullable<SitepingHandlerBaseOptions<Principal>[\"hooks\"]>[\"onCreated\"];\n webhooks: ReadonlyArray<WebhookConfig>;\n}\n\n/** Store input from the validated payload — the store never sees wire-only shapes. */\nfunction toCreateInput(payload: FeedbackCreatePayload): FeedbackCreateInput {\n return {\n projectName: payload.projectName,\n type: payload.type,\n message: payload.message,\n status: \"open\",\n url: payload.url,\n urlPattern: payload.urlPattern ?? null,\n viewport: payload.viewport,\n userAgent: payload.userAgent,\n authorName: payload.authorName,\n authorEmail: payload.authorEmail,\n clientId: payload.clientId,\n annotations: payload.annotations.map(flattenAnnotation),\n screenshotDataUrl: payload.screenshotDataUrl ?? null,\n screenshotRegion: payload.screenshotRegion ?? null,\n diagnostics: payload.diagnostics ?? null,\n };\n}\n\n/** `POST` — create a feedback, idempotent on `clientId`. */\nexport function createFeedbackOperation<Principal>({\n store,\n pipeline,\n beforeCreate,\n onCreated,\n webhooks,\n}: CreateFeedbackDependencies<Principal>) {\n /**\n * A clientId is unique across the store, so a replay resolving to another\n * project's record is a boundary violation, not a dedup. `authorEmail`\n * follows the access policy (`canReadAuthorEmail`): `beforeCreate` may have\n * stored an email the requester never sent. The legacy api-key policy\n * still echoes it — see `AccessGate.echoesAuthorEmailOnCreate`.\n */\n const respondCreated = (scope: AuthenticatedRequest<Principal>, feedback: FeedbackRecord, projectName: string) =>\n feedback.projectName === projectName\n ? pipeline.json(scope, pipeline.presentCreated(scope, feedback), { status: 201 })\n : pipeline.error(scope, 409, SITEPING_ERROR_MESSAGES.clientIdUsedByAnotherProject);\n\n /**\n * Insert, resolving a race on clientId to the winning record. `isNew` gates\n * the creation side effects, so it must only be `true` when this call\n * inserted: stores returning the existing record on a duplicate say so\n * through `createFeedbackIfAbsent`; the others throw `StoreDuplicateError`.\n * A store that returns the existing record from plain `createFeedback`\n * cannot be told apart from an insert — see the `SitepingStore` contract.\n */\n const insert = async (input: FeedbackCreateInput): Promise<{ feedback: FeedbackRecord; isNew: boolean }> => {\n try {\n if (store.createFeedbackIfAbsent) {\n const { feedback, created } = await store.createFeedbackIfAbsent(input);\n return { feedback, isNew: created };\n }\n return { feedback: await store.createFeedback(input), isNew: true };\n } catch (error) {\n if (isStoreDuplicate(error)) {\n const existing = await store.findByClientId(input.clientId);\n if (existing) return { feedback: existing, isNew: false };\n }\n throw error;\n }\n };\n\n return async (request: Request): Promise<Response> => {\n const authentication = await pipeline.authenticate(request, \"POST\");\n if (!authentication.ok) return authentication.response;\n const scope = authentication.value;\n\n const payload = await pipeline.readBody(scope, feedbackCreateSchema);\n if (!payload.ok) return payload.response;\n // Defense-in-depth on top of the schema limit.\n if (payload.value.annotations.length > MAX_ANNOTATIONS_PER_FEEDBACK) {\n return pipeline.error(scope, 400, SITEPING_ERROR_MESSAGES.tooManyAnnotations);\n }\n\n try {\n const validatedInput = toCreateInput(payload.value);\n const input = beforeCreate ? await beforeCreate(validatedInput, scope.context) : validatedInput;\n\n const refusal = await pipeline.authorize(scope, { action: \"create\", projectName: input.projectName });\n if (refusal) return refusal;\n\n // Replay detection up front: a replayed submission must not notify\n // hooks or webhooks a second time.\n const replayed = await store.findByClientId(input.clientId);\n if (replayed) return respondCreated(scope, replayed, input.projectName);\n\n const { feedback, isNew } = await insert(input);\n if (isNew && feedback.projectName === input.projectName) {\n if (webhooks.length > 0) void dispatchWebhooks(webhooks, feedback);\n if (onCreated) await pipeline.runHook(\"onCreated\", () => onCreated(feedback, scope.context));\n }\n return respondCreated(scope, feedback, input.projectName);\n } catch (error) {\n return pipeline.internalError(scope, \"create feedback\", error);\n }\n };\n}\n","import { isStoreNotFound, type SitepingStore } from \"@beezping/core\";\nimport { SITEPING_ERROR_MESSAGES } from \"../constants/error-messages.js\";\nimport type { SitepingDeletionTarget, SitepingHandlerBaseOptions } from \"../options.js\";\nimport type { RequestPipeline } from \"../request-pipeline.js\";\nimport { type FeedbackDeleteInput, feedbackDeleteSchema } from \"../validation.js\";\n\ntype LifecycleHooks<Principal> = NonNullable<SitepingHandlerBaseOptions<Principal>[\"hooks\"]>;\n\nexport interface DeleteFeedbackDependencies<Principal> {\n store: SitepingStore;\n pipeline: RequestPipeline<Principal>;\n onDeleting: LifecycleHooks<Principal>[\"onDeleting\"];\n onDeleted: LifecycleHooks<Principal>[\"onDeleted\"];\n}\n\nfunction toDeletionTarget(deletion: FeedbackDeleteInput): SitepingDeletionTarget {\n return \"deleteAll\" in deletion\n ? { kind: \"project\", projectName: deletion.projectName }\n : { kind: \"single\", id: deletion.id, projectName: deletion.projectName };\n}\n\n/** `DELETE` — remove one feedback, or every feedback of a project (`deleteAll`). */\nexport function deleteFeedbackOperation<Principal>({\n store,\n pipeline,\n onDeleting,\n onDeleted,\n}: DeleteFeedbackDependencies<Principal>) {\n const removeTarget = (target: SitepingDeletionTarget): Promise<void> =>\n target.kind === \"project\" ? store.deleteAllFeedbacks(target.projectName) : store.deleteFeedback(target.id);\n\n return async (request: Request): Promise<Response> => {\n const authentication = await pipeline.authenticate(request, \"DELETE\");\n if (!authentication.ok) return authentication.response;\n const scope = authentication.value;\n\n const payload = await pipeline.readBody(scope, feedbackDeleteSchema);\n if (!payload.ok) return payload.response;\n const target = toDeletionTarget(payload.value as FeedbackDeleteInput);\n\n try {\n const refusal = await pipeline.authorize(\n scope,\n target.kind === \"project\"\n ? { action: \"deleteAll\", projectName: target.projectName }\n : { action: \"delete\", projectName: target.projectName, feedbackId: target.id },\n );\n if (refusal) return refusal;\n\n // Cross-project guard — see the PATCH operation for why it may be absent.\n if (\n target.kind === \"single\" &&\n store.verifyProjectOwnership &&\n !(await store.verifyProjectOwnership(target.id, target.projectName))\n ) {\n return pipeline.error(scope, 404, SITEPING_ERROR_MESSAGES.feedbackNotFound);\n }\n\n if (onDeleting) {\n try {\n await onDeleting(target, scope.context);\n } catch (error) {\n pipeline.logger.error(\"[siteping] createSitepingHandler: hook onDeleting aborted the deletion\", {\n error,\n target,\n });\n return pipeline.error(scope, 502, SITEPING_ERROR_MESSAGES.deletionAborted);\n }\n }\n\n await removeTarget(target);\n if (onDeleted) await pipeline.runHook(\"onDeleted\", () => onDeleted(target, scope.context));\n return pipeline.json(scope, { deleted: true });\n } catch (error) {\n if (isStoreNotFound(error)) return pipeline.error(scope, 404, SITEPING_ERROR_MESSAGES.feedbackNotFound);\n return pipeline.internalError(scope, \"delete feedback\", error);\n }\n };\n}\n","import type { SitepingStore } from \"@beezping/core\";\nimport { LIST_CACHE_CONTROL, LIST_QUERY_KEYS } from \"../constants/http.js\";\nimport type { RequestPipeline } from \"../request-pipeline.js\";\nimport { getQuerySchema } from \"../validation.js\";\n\nexport interface ListFeedbacksDependencies<Principal> {\n store: SitepingStore;\n pipeline: RequestPipeline<Principal>;\n}\n\n/** Only the query parameters the endpoint understands, as raw strings. */\nfunction readListQuery(request: Request): Record<string, string> {\n const searchParams = new URL(request.url).searchParams;\n const rawQuery: Record<string, string> = {};\n for (const key of LIST_QUERY_KEYS) {\n const value = searchParams.get(key);\n if (value !== null) rawQuery[key] = value;\n }\n return rawQuery;\n}\n\n/** `GET` — paginated, filtered list of a project's feedbacks. */\nexport function listFeedbacksOperation<Principal>({ store, pipeline }: ListFeedbacksDependencies<Principal>) {\n return async (request: Request): Promise<Response> => {\n const authentication = await pipeline.authenticate(request, \"GET\");\n if (!authentication.ok) return authentication.response;\n const scope = authentication.value;\n\n const query = pipeline.validate(scope, getQuerySchema, readListQuery(request));\n if (!query.ok) return query.response;\n\n try {\n const refusal = await pipeline.authorize(scope, { action: \"list\", projectName: query.value.projectName });\n if (refusal) return refusal;\n const page = await store.getFeedbacks(query.value);\n return pipeline.json(\n scope,\n { ...page, feedbacks: page.feedbacks.map((feedback) => pipeline.present(scope, feedback)) },\n { headers: { \"Cache-Control\": LIST_CACHE_CONTROL } },\n );\n } catch (error) {\n return pipeline.internalError(scope, \"list feedbacks\", error);\n }\n };\n}\n","import { isStoreNotFound, type SitepingStore, toFeedbackUpdate } from \"@beezping/core\";\nimport { SITEPING_ERROR_MESSAGES } from \"../constants/error-messages.js\";\nimport type { SitepingHandlerBaseOptions } from \"../options.js\";\nimport type { RequestPipeline } from \"../request-pipeline.js\";\nimport { feedbackPatchSchema } from \"../validation.js\";\n\nexport interface UpdateFeedbackDependencies<Principal> {\n store: SitepingStore;\n pipeline: RequestPipeline<Principal>;\n onUpdated: NonNullable<SitepingHandlerBaseOptions<Principal>[\"hooks\"]>[\"onUpdated\"];\n}\n\n/** `PATCH` — change a feedback's status. */\nexport function updateFeedbackOperation<Principal>({\n store,\n pipeline,\n onUpdated,\n}: UpdateFeedbackDependencies<Principal>) {\n return async (request: Request): Promise<Response> => {\n const authentication = await pipeline.authenticate(request, \"PATCH\");\n if (!authentication.ok) return authentication.response;\n const scope = authentication.value;\n\n const payload = await pipeline.readBody(scope, feedbackPatchSchema);\n if (!payload.ok) return payload.response;\n const { id, projectName, status } = payload.value;\n\n try {\n const refusal = await pipeline.authorize(scope, { action: \"update\", projectName, feedbackId: id });\n if (refusal) return refusal;\n // Cross-project guard. `createSitepingHandler` refuses to start with a\n // custom `authorize` over a store lacking the check, so it is only ever\n // skipped when no policy scopes callers to projects.\n if (store.verifyProjectOwnership && !(await store.verifyProjectOwnership(id, projectName))) {\n return pipeline.error(scope, 404, SITEPING_ERROR_MESSAGES.feedbackNotFound);\n }\n // resolvedAt (closure timestamp) is derived here at the edge.\n const feedback = await store.updateFeedback(id, toFeedbackUpdate(status));\n if (onUpdated) await pipeline.runHook(\"onUpdated\", () => onUpdated(feedback, scope.context));\n return pipeline.json(scope, pipeline.present(scope, feedback));\n } catch (error) {\n if (isStoreNotFound(error)) return pipeline.error(scope, 404, SITEPING_ERROR_MESSAGES.feedbackNotFound);\n return pipeline.internalError(scope, \"update feedback\", error);\n }\n };\n}\n","import type { SitepingHttpMethod } from \"./access.js\";\nimport { CSRF_PROTECTED_METHODS, JSON_MEDIA_TYPE } from \"./constants/http.js\";\nimport { MAX_LOGGED_ORIGIN_LENGTH } from \"./constants/limits.js\";\nimport type { CorsPolicy } from \"./cors.js\";\n\n/**\n * Cross-site request forgery guards of the mutating methods. CORS only hides\n * a response from a foreign page — it never stops a \"simple\" request (e.g. a\n * credentialed `POST` with `Content-Type: text/plain`) from reaching the\n * handler, so both checks run before the body is parsed, the caller is\n * authenticated or any hook fires.\n */\n\n/** Whether `method` changes data and therefore goes through the CSRF guards. */\nexport function isCsrfProtectedMethod(method: SitepingHttpMethod): boolean {\n return CSRF_PROTECTED_METHODS.includes(method);\n}\n\n/**\n * Whether the request's `Origin` may mutate data under `policy`.\n *\n * - No `allowedOrigins` → no origin check (the JSON content-type guard still\n * forces cross-origin browsers through a preflight, which fails without\n * CORS headers).\n * - No `Origin` header → allowed: server-to-server calls, curl and\n * same-origin navigations of older browsers do not send one, and a browser\n * always sends it on cross-origin `POST`/`PATCH`/`DELETE`.\n * - Same origin as the request URL, or listed in `allowedOrigins` → allowed.\n * - Anything else, including the opaque `null` origin → refused.\n */\nexport function isMutationOriginAllowed(request: Request, policy: CorsPolicy): boolean {\n if (!policy.allowedOrigins) return true;\n const origin = request.headers.get(\"Origin\");\n if (origin === null) return true;\n if (policy.allowedOrigins.includes(origin)) return true;\n return origin === new URL(request.url).origin;\n}\n\n/** Whether the request declares a JSON body (`application/json`, parameters such as `charset` allowed). */\nexport function hasJsonContentType(request: Request): boolean {\n const contentType = request.headers.get(\"Content-Type\");\n if (contentType === null) return false;\n const [mediaType = \"\"] = contentType.split(\";\");\n return mediaType.trim().toLowerCase() === JSON_MEDIA_TYPE;\n}\n\n/** The request's `Origin`, truncated and stripped of control characters, safe to log. */\nexport function describeOriginForLog(request: Request): string {\n const origin = request.headers.get(\"Origin\") ?? \"\";\n // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping control characters is the point\n return origin.replace(/[\\u0000-\\u001f\\u007f]/g, \"\").slice(0, MAX_LOGGED_ORIGIN_LENGTH);\n}\n","import type { FeedbackRecord } from \"@beezping/core\";\nimport type {\n AccessGate,\n AuthenticationOutcome,\n SitepingAuthorizationContext,\n SitepingHttpMethod,\n SitepingRequestContext,\n} from \"./access.js\";\nimport { SITEPING_ERROR_MESSAGES } from \"./constants/error-messages.js\";\nimport { buildCorsHeaders, type CorsHeaders, type CorsPolicy, withCors } from \"./cors.js\";\nimport { internalErrorResponse } from \"./internal-error.js\";\nimport type { SitepingLogger } from \"./options.js\";\nimport {\n describeOriginForLog,\n hasJsonContentType,\n isCsrfProtectedMethod,\n isMutationOriginAllowed,\n} from \"./request-guards.js\";\nimport { formatValidationErrors } from \"./validation.js\";\n\n/** A request that passed authentication — what every operation works with. */\nexport interface AuthenticatedRequest<Principal> {\n context: SitepingRequestContext<Principal>;\n canReadAuthorEmail: boolean;\n corsHeaders: CorsHeaders;\n}\n\n/** Either a value to continue with, or the response to send right away. */\nexport type PipelineStep<Value> = { ok: true; value: Value } | { ok: false; response: Response };\n\n/** Minimal shape of a zod schema the pipeline validates with. */\ninterface ParseableSchema<Output> {\n safeParse(\n input: unknown,\n ): { success: true; data: Output } | { success: false; error: Parameters<typeof formatValidationErrors>[0] };\n}\n\nexport interface RequestPipelineDependencies<Principal> {\n gate: AccessGate<Principal>;\n corsPolicy: CorsPolicy;\n logger: SitepingLogger;\n describeError: ((error: unknown) => string | undefined) | undefined;\n presentFeedback:\n | ((feedback: FeedbackRecord, context: SitepingRequestContext<Principal>) => FeedbackRecord)\n | undefined;\n}\n\n/**\n * Serialize a record for the wire. `clientId` is always stripped — it is a\n * browser-local dedup secret, and the POST dedup path returns the full\n * record to whoever presents it. `authorEmail` is blanked unless allowed.\n */\nfunction toWireFeedback(feedback: FeedbackRecord, includeEmail: boolean): Omit<FeedbackRecord, \"clientId\"> {\n const { clientId: _clientId, ...wire } = feedback;\n return includeEmail ? wire : { ...wire, authorEmail: \"\" };\n}\n\n/**\n * The steps every HTTP operation shares — authenticate, parse, authorize,\n * respond, report failures — so each operation module only holds its own\n * logic. Every response carries the request's CORS headers.\n */\nexport function createRequestPipeline<Principal>({\n gate,\n corsPolicy,\n logger,\n describeError,\n presentFeedback,\n}: RequestPipelineDependencies<Principal>) {\n const json = (scope: Pick<AuthenticatedRequest<Principal>, \"corsHeaders\">, body: unknown, init?: ResponseInit) =>\n withCors(Response.json(body, init), scope.corsHeaders);\n\n const error = (scope: Pick<AuthenticatedRequest<Principal>, \"corsHeaders\">, status: number, message: string) =>\n json(scope, { error: message }, { status });\n\n /** Validate `input` (a parsed body or query) against `schema`. */\n const validate = <Output>(\n scope: AuthenticatedRequest<Principal>,\n schema: ParseableSchema<Output>,\n input: unknown,\n ): PipelineStep<Output> => {\n const parsed = schema.safeParse(input);\n if (parsed.success) return { ok: true, value: parsed.data };\n return { ok: false, response: json(scope, { errors: formatValidationErrors(parsed.error) }, { status: 400 }) };\n };\n\n /** Log an unexpected failure and answer 500 (with `describeError`'s hint when it has one). */\n const internalError = (request: Request, corsHeaders: CorsHeaders, operation: string, failure: unknown): Response =>\n internalErrorResponse({\n logger,\n describeError,\n source: \"createSitepingHandler\",\n operation,\n request,\n corsHeaders,\n failure,\n });\n\n /** Wire shape of a record for this requester (presentFeedback + email policy). */\n const present = (\n scope: AuthenticatedRequest<Principal>,\n feedback: FeedbackRecord,\n includeEmail = scope.canReadAuthorEmail,\n ) => toWireFeedback(presentFeedback ? presentFeedback(feedback, scope.context) : feedback, includeEmail);\n\n return {\n json,\n error,\n\n /**\n * Refuse forged mutations, then run the access gate. Mutating methods\n * from an origin outside `allowedOrigins` answer 403 and non-JSON bodies\n * 415, before the body is parsed, the caller authenticated or any hook\n * run (see `request-guards.ts`). A throwing `authenticate` (session store\n * down…) answers the logged 500 with CORS headers instead of rejecting\n * the handler, which a browser would only see as an opaque network error.\n */\n async authenticate(\n request: Request,\n method: SitepingHttpMethod,\n ): Promise<PipelineStep<AuthenticatedRequest<Principal>>> {\n const corsHeaders = buildCorsHeaders(request, corsPolicy);\n if (isCsrfProtectedMethod(method)) {\n if (!isMutationOriginAllowed(request, corsPolicy)) {\n logger.error(\"[siteping] createSitepingHandler: refused a mutation from an origin outside allowedOrigins\", {\n method,\n path: new URL(request.url).pathname,\n origin: describeOriginForLog(request),\n });\n return { ok: false, response: error({ corsHeaders }, 403, SITEPING_ERROR_MESSAGES.forbidden) };\n }\n if (!hasJsonContentType(request)) {\n return { ok: false, response: error({ corsHeaders }, 415, SITEPING_ERROR_MESSAGES.unsupportedMediaType) };\n }\n }\n let outcome: AuthenticationOutcome<Principal>;\n try {\n outcome = await gate.authenticate(request, method);\n } catch (failure) {\n return { ok: false, response: internalError(request, corsHeaders, \"authenticate request\", failure) };\n }\n if (!outcome.ok) return { ok: false, response: error({ corsHeaders }, outcome.status, outcome.error) };\n return {\n ok: true,\n value: {\n context: { request, principal: outcome.principal },\n canReadAuthorEmail: outcome.canReadAuthorEmail,\n corsHeaders,\n },\n };\n },\n\n validate,\n\n /** Read and validate a JSON body. */\n async readBody<Output>(\n scope: AuthenticatedRequest<Principal>,\n schema: ParseableSchema<Output>,\n ): Promise<PipelineStep<Output>> {\n const body: unknown = await scope.context.request.json().catch(() => null);\n if (!body) return { ok: false, response: error(scope, 400, SITEPING_ERROR_MESSAGES.invalidJson) };\n return validate(scope, schema, body);\n },\n\n /** `null` when allowed, the 403 response otherwise. */\n async authorize(\n scope: AuthenticatedRequest<Principal>,\n authorization: Omit<SitepingAuthorizationContext<Principal>, keyof SitepingRequestContext<Principal>>,\n ): Promise<Response | null> {\n const allowed = await gate.authorize({ ...scope.context, ...authorization });\n return allowed ? null : error(scope, 403, SITEPING_ERROR_MESSAGES.forbidden);\n },\n\n present,\n\n /**\n * Wire shape of a record answering a `POST` (fresh or replayed): the\n * requester's email policy, except under a gate that echoes the email to\n * its submitter (legacy api-key policy).\n */\n presentCreated(scope: AuthenticatedRequest<Principal>, feedback: FeedbackRecord) {\n return present(scope, feedback, gate.echoesAuthorEmailOnCreate || scope.canReadAuthorEmail);\n },\n\n /** Log an unexpected failure and answer 500 (with `describeError`'s hint when it has one). */\n internalError(scope: AuthenticatedRequest<Principal>, operation: string, failure: unknown): Response {\n return internalError(scope.context.request, scope.corsHeaders, operation, failure);\n },\n\n /** Run a post-write hook; its failure is logged, never surfaced. */\n async runHook(name: string, invoke: () => void | Promise<void>): Promise<void> {\n try {\n await invoke();\n } catch (failure) {\n logger.error(`[siteping] createSitepingHandler: hook ${name} failed`, { error: failure });\n }\n },\n\n logger,\n };\n}\n\nexport type RequestPipeline<Principal> = ReturnType<typeof createRequestPipeline<Principal>>;\n","import { type AccessGate, accessGateFromControl } from \"./access.js\";\nimport { createApiKeyGate } from \"./api-key-access.js\";\nimport { SITEPING_CONFIGURATION_ERROR_MESSAGES } from \"./constants/error-messages.js\";\nimport { CORS_ALLOWED_METHODS } from \"./constants/http.js\";\nimport { createCorsPolicy, preflightResponse } from \"./cors.js\";\nimport { defaultLogger } from \"./internal-error.js\";\nimport { createFeedbackOperation } from \"./operations/create-feedback.js\";\nimport { deleteFeedbackOperation } from \"./operations/delete-feedback.js\";\nimport { listFeedbacksOperation } from \"./operations/list-feedbacks.js\";\nimport { updateFeedbackOperation } from \"./operations/update-feedback.js\";\nimport type {\n SitepingAccessHandlerOptions,\n SitepingApiKeyHandlerOptions,\n SitepingHandler,\n SitepingHandlerBaseOptions,\n SitepingHandlerOptions,\n} from \"./options.js\";\nimport { createRequestPipeline } from \"./request-pipeline.js\";\nimport type { WebhookConfig } from \"./webhooks.js\";\n\nfunction toWebhookList(webhooks: SitepingHandlerBaseOptions<unknown>[\"webhooks\"]): ReadonlyArray<WebhookConfig> {\n if (!webhooks) return [];\n return Array.isArray(webhooks) ? (webhooks as ReadonlyArray<WebhookConfig>) : [webhooks as WebhookConfig];\n}\n\n/**\n * Create the SitePing HTTP API over any `SitepingStore`, using only the\n * Fetch API (`Request` / `Response`) — mount it in Next.js route handlers,\n * Hono, Remix, SvelteKit, Bun/Deno servers or edge workers.\n *\n * Rate limiting is not handled here; apply it at the framework or proxy level.\n *\n * @throws Error when `allowedHeaders` contains an invalid header name.\n * @throws Error when `access.authorize` is set and the store lacks\n * `verifyProjectOwnership` — per-record PATCH/DELETE could otherwise target\n * a record of a project the caller is not authorized for.\n *\n * @example Next.js App Router with your own session auth\n * ```ts\n * export const { GET, POST, PATCH, DELETE, OPTIONS } = createSitepingHandler({\n * store,\n * access: {\n * authenticate: (request) => getSessionUser(request),\n * authorize: ({ principal, action }) => action === \"create\" || principal.isAdmin,\n * },\n * });\n * ```\n */\nexport function createSitepingHandler<Principal>(options: SitepingAccessHandlerOptions<Principal>): SitepingHandler;\nexport function createSitepingHandler(options: SitepingApiKeyHandlerOptions): SitepingHandler;\nexport function createSitepingHandler<Principal>(options: SitepingHandlerOptions<Principal>): SitepingHandler {\n // The api-key branch never resolves a principal (always `null`), so both\n // branches share the callbacks typed over `Principal`.\n const {\n store,\n allowedOrigins,\n allowedHeaders,\n beforeCreate,\n presentFeedback,\n hooks = {},\n logger = defaultLogger,\n describeError,\n } = options as SitepingHandlerBaseOptions<Principal>;\n if (!store) {\n throw new Error(\"[siteping] createSitepingHandler requires a `store`.\");\n }\n // A custom `authorize` may scope principals to projects, but PATCH/DELETE\n // address records by id: without an ownership check, the project a caller\n // claims (and is authorized for) need not be the record's. Fail closed at\n // startup rather than letting per-record mutations cross projects.\n if (options.access?.authorize && !store.verifyProjectOwnership) {\n throw new Error(SITEPING_CONFIGURATION_ERROR_MESSAGES.ownershipVerificationRequired);\n }\n const corsPolicy = createCorsPolicy({ allowedOrigins, allowedHeaders, allowedMethods: CORS_ALLOWED_METHODS });\n const gate: AccessGate<Principal> = options.access\n ? accessGateFromControl(options.access)\n : (createApiKeyGate(options) as AccessGate<Principal>);\n const pipeline = createRequestPipeline<Principal>({ gate, corsPolicy, logger, describeError, presentFeedback });\n\n return {\n /** CORS preflight. Configure `allowedOrigins` (and `allowedHeaders`) for cross-origin widgets. */\n OPTIONS: (request: Request): Response => preflightResponse(request, corsPolicy),\n POST: createFeedbackOperation({\n store,\n pipeline,\n beforeCreate,\n onCreated: hooks.onCreated?.bind(hooks),\n webhooks: toWebhookList(options.webhooks),\n }),\n GET: listFeedbacksOperation({ store, pipeline }),\n PATCH: updateFeedbackOperation({ store, pipeline, onUpdated: hooks.onUpdated?.bind(hooks) }),\n DELETE: deleteFeedbackOperation({\n store,\n pipeline,\n onDeleting: hooks.onDeleting?.bind(hooks),\n onDeleted: hooks.onDeleted?.bind(hooks),\n }),\n };\n}\n","import type { SitepingAccessControl } from \"./access.js\";\nimport { IDENTITY_CACHE_CONTROL, IDENTITY_CORS_ALLOWED_METHODS } from \"./constants/http.js\";\nimport { buildCorsHeaders, createCorsPolicy, preflightResponse, withCors } from \"./cors.js\";\nimport { defaultLogger, internalErrorResponse } from \"./internal-error.js\";\nimport type { SitepingLogger } from \"./options.js\";\n\n/** Reviewer identity the widget pre-fills (`SitepingConfig.identity`). */\nexport interface SitepingIdentity {\n name: string;\n email: string;\n}\n\n/**\n * Body of the identity endpoint. The host page fetches it to decide whether\n * to mount the widget and with which identity/project:\n *\n * ```ts\n * const { enabled, identity, projectName } = await (await fetch(\"/api/siteping/identity\")).json();\n * if (enabled && identity) initSiteping({ endpoint, projectName, identity, forceShow: true });\n * ```\n */\nexport interface SitepingIdentityResponse {\n enabled: boolean;\n identity: SitepingIdentity | null;\n projectName: string;\n}\n\nexport interface SitepingIdentityHandlerOptions<Principal> {\n /** Same `authenticate` as the feedback handler's `access`. */\n access: Pick<SitepingAccessControl<Principal>, \"authenticate\">;\n /** Map the principal to the identity shown in the widget; `null` hides the widget. */\n resolveIdentity(principal: Principal): SitepingIdentity | null | Promise<SitepingIdentity | null>;\n /** Project the widget reports into. */\n projectName: string;\n /**\n * Feature flag — globally (`boolean`) or per principal (e.g. an allowlist\n * of reviewers). Defaults to enabled for every resolved identity.\n */\n enabled?: boolean | ((principal: Principal) => boolean | Promise<boolean>);\n /** Allowed CORS origins, as in `createSitepingHandler`. */\n allowedOrigins?: ReadonlyArray<string> | undefined;\n /**\n * Extra request headers cross-origin callers may send, as in\n * `createSitepingHandler` — list the headers `access.authenticate` reads.\n */\n allowedHeaders?: ReadonlyArray<string> | undefined;\n /**\n * Receives failures of `authenticate`, `enabled` or `resolveIdentity`\n * (answered with a generic 500); defaults to `console.error`.\n */\n logger?: SitepingLogger | undefined;\n}\n\n/** Handlers of the identity endpoint — mount both on any Fetch-API router. */\nexport interface SitepingIdentityHandler {\n /** CORS preflight — needed as soon as the identity request carries a non-simple header (e.g. `Authorization`). */\n OPTIONS: (request: Request) => Response;\n GET: (request: Request) => Promise<Response>;\n}\n\n/**\n * `GET` endpoint telling the host page whether the current visitor may give\n * feedback, and as whom. Anonymous or disabled visitors get\n * `{ enabled: false, identity: null }` with 200 — the page simply does not\n * mount the widget. Responses are `no-store`: they depend on the session.\n * `OPTIONS` answers the CORS preflight of cross-origin callers with the\n * same `allowedOrigins` / `allowedHeaders` policy. A throwing callback\n * (session store down…) is logged and answered with a JSON 500 that keeps\n * the CORS headers, so cross-origin callers can read the failure.\n *\n * @throws Error when `allowedHeaders` contains an invalid header name.\n */\nexport function createSitepingIdentityHandler<Principal>({\n access,\n resolveIdentity,\n projectName,\n enabled = true,\n allowedOrigins,\n allowedHeaders,\n logger = defaultLogger,\n}: SitepingIdentityHandlerOptions<Principal>): SitepingIdentityHandler {\n const corsPolicy = createCorsPolicy({\n allowedOrigins,\n allowedHeaders,\n allowedMethods: IDENTITY_CORS_ALLOWED_METHODS,\n });\n const respond = (request: Request, body: SitepingIdentityResponse): Response =>\n withCors(\n Response.json(body, { headers: { \"Cache-Control\": IDENTITY_CACHE_CONTROL } }),\n buildCorsHeaders(request, corsPolicy),\n );\n const disabled = (request: Request) => respond(request, { enabled: false, identity: null, projectName });\n\n const resolveResponse = async (request: Request): Promise<Response> => {\n const principal = await access.authenticate(request);\n if (principal === null) return disabled(request);\n const isEnabled = typeof enabled === \"function\" ? await enabled(principal) : enabled;\n if (!isEnabled) return disabled(request);\n const identity = await resolveIdentity(principal);\n if (!identity) return disabled(request);\n return respond(request, { enabled: true, identity, projectName });\n };\n\n return {\n OPTIONS: (request: Request): Response => preflightResponse(request, corsPolicy),\n GET: async (request: Request): Promise<Response> => {\n try {\n return await resolveResponse(request);\n } catch (failure) {\n return internalErrorResponse({\n logger,\n source: \"createSitepingIdentityHandler\",\n operation: \"resolve identity\",\n request,\n corsHeaders: buildCorsHeaders(request, corsPolicy),\n headers: { \"Cache-Control\": IDENTITY_CACHE_CONTROL },\n failure,\n });\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,+BAAAA;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACuBO,IAAM,qBAAqB;AAE3B,IAAM,iBAAiB;AAiB9B,SAAS,kBAAkB,OAA2B,UAA0B;AAC9E,SAAO,UAAU,UAAa,OAAO,SAAS,KAAK,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,CAAC,IAAI;AAC1F;AAaO,SAAS,gBAAgB,OAA0D;AACxF,QAAM,OAAO,kBAAkB,MAAM,MAAM,CAAC;AAC5C,QAAM,QAAQ,KAAK,IAAI,kBAAkB,MAAM,OAAO,kBAAkB,GAAG,cAAc;AACzF,SAAO,EAAE,MAAM,OAAO,OAAO,OAAO,KAAK,MAAM;AACjD;AAiBO,SAAS,oBAAoB,MAAuB;AACzD,SAAO,CAAC,OAAO,cAAc,IAAI;AACnC;;;ACvBO,SAAS,SAAS,OAAuD;AAC9E,SAAO,OAAO,UAAU,YAAY,UAAU;AAChD;AAWO,SAAS,OAA8B,OAAgB,KAAqC;AACjG,SAAO,SAAS,KAAK,KAAK,OAAO;AACnC;;;ACgpBO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EACnC,OAAO;AAAA,EAChB,YAAY,UAAU,oBAAoB,SAAwB;AAChE,UAAM,SAAS,OAAO;AACtB,SAAK,OAAO;AAAA,EACd;AACF;AAOO,IAAM,sBAAN,cAAkC,MAAM;AAAA,EACpC,OAAO;AAAA,EAChB,YAAY,UAAU,oBAAoB,SAAwB;AAChE,UAAM,SAAS,OAAO;AACtB,SAAK,OAAO;AAAA,EACd;AACF;AAQO,IAAM,wBAAN,cAAoC,MAAM;AAAA,EACtC,OAAO;AAAA,EAChB,YAAY,UAAU,oCAAoC,SAAwB;AAChF,UAAM,SAAS,OAAO;AACtB,SAAK,OAAO;AAAA,EACd;AACF;AAKA,SAAS,aAA+B,OAAgB,MAAiC;AACvF,SAAO,OAAO,OAAO,MAAM,KAAK,MAAM,SAAS;AACjD;AASO,SAAS,gBACd,OACmF;AACnF,MAAI,iBAAiB,mBAAoB,QAAO;AAEhD,SAAO,aAAa,OAAO,iBAAiB,KAAK,aAAa,OAAO,OAAO;AAC9E;AAOO,SAAS,iBACd,OACoF;AACpF,MAAI,iBAAiB,oBAAqB,QAAO;AAEjD,SAAO,aAAa,OAAO,iBAAiB,KAAK,aAAa,OAAO,OAAO;AAC9E;AAQO,SAAS,mBAAmB,OAAkF;AACnH,MAAI,iBAAiB,sBAAuB,QAAO;AACnD,SAAO,aAAa,OAAO,mBAAmB;AAChD;AAOO,SAAS,kBAAkB,KAA+C;AAC/E,SAAO;AAAA,IACL,aAAa,IAAI,OAAO;AAAA,IACxB,OAAO,IAAI,OAAO;AAAA,IAClB,aAAa,IAAI,OAAO;AAAA,IACxB,YAAY,IAAI,OAAO;AAAA,IACvB,WAAW,IAAI,OAAO;AAAA,IACtB,YAAY,IAAI,OAAO;AAAA,IACvB,YAAY,IAAI,OAAO;AAAA,IACvB,aAAa,IAAI,OAAO;AAAA,IACxB,cAAc,IAAI,OAAO;AAAA,IACzB,WAAW,IAAI,OAAO,aAAa;AAAA,IACnC,MAAM,IAAI,KAAK;AAAA,IACf,MAAM,IAAI,KAAK;AAAA,IACf,MAAM,IAAI,KAAK;AAAA,IACf,MAAM,IAAI,KAAK;AAAA,IACf,SAAS,IAAI;AAAA,IACb,SAAS,IAAI;AAAA,IACb,WAAW,IAAI;AAAA,IACf,WAAW,IAAI;AAAA,IACf,kBAAkB,IAAI;AAAA,EACxB;AACF;;;AWl0BA,UAAqB;AVDd,IAAM,+BAA+B;AAGrC,IAAM,2BAA2B;AAGjC,IAAM,kCAAkC;ACDxC,IAAM,0BAA0B;EACrC,aAAa;EACb,cAAc;EACd,8BAA8B;EAC9B,WAAW;EACX,sBAAsB;EACtB,kBAAkB;EAClB,8BAA8B;EAC9B,oBAAoB,6BAA6B,4BAA4B;EAC7E,iBAAiB;EACjB,qBAAqB;AACvB;AAMO,IAAM,wCAAwC;EACnD,+BACE;EAGF,sBACE;AACJ;ACqCO,SAAS,sBAAiC,QAAiE;AAChH,SAAO;IACL,2BAA2B;IAC3B,MAAM,aAAa,SAAS;AAC1B,YAAM,YAAY,MAAM,OAAO,aAAa,OAAO;AACnD,UAAI,cAAc,KAAM,QAAO,EAAE,IAAI,OAAO,QAAQ,KAAK,OAAO,wBAAwB,aAAa;AACrG,aAAO,EAAE,IAAI,MAAM,WAAW,oBAAoB,OAAO,qBAAqB,SAAS,KAAK,KAAK;IACnG;IACA,MAAM,UAAU,SAAS;AACvB,aAAO,OAAO,YAAY,OAAO,UAAU,OAAO,IAAI;IACxD;EACF;AACF;AClDA,IAAM,cAAc,IAAI,YAAY;AAQpC,SAAS,kBAAkB,UAAkB,UAA2B;AACtE,QAAM,gBAAgB,YAAY,OAAO,QAAQ;AACjD,QAAM,gBAAgB,YAAY,OAAO,QAAQ;AACjD,MAAI,cAAc,WAAW,cAAc,OAAQ,QAAO;AAC1D,MAAI,aAAa;AACjB,WAAS,QAAQ,GAAG,QAAQ,cAAc,QAAQ,SAAS;AACzD,mBAAe,cAAc,KAAK,KAAK,MAAM,cAAc,KAAK,KAAK;EACvE;AACA,SAAO,eAAe;AACxB;AAGA,SAAS,0BAAmC;AAC1C,SAAO,OAAO,YAAY,eAAe,QAAQ,KAAK,aAAa;AACrE;AAOO,SAAS,iBAAiB;EAC/B;EACA,kBAAkB,SAAS,CAAC,QAAQ,SAAS,IAAI;EACjD,4BAA4B;EAC5B,8BAA8B;AAChC,GAA2C;AAEzC,MAAI,CAAC,UAAU,6BAA6B,wBAAwB,GAAG;AACrE,UAAM,IAAI;MACR;IAGF;EACF;AAEA,QAAM,gBAAwD,kBAAkB,IAAI,IAAI,eAAe,IAAI;AAE3G,QAAM,wBAAwB,CAAC,YAA8B;AAC3D,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,SAAS,QAAQ,QAAQ,IAAI,eAAe;AAClD,WAAO,WAAW,QAAQ,kBAAkB,QAAQ,UAAU,MAAM,EAAE;EACxE;AAEA,SAAO;;IAEL,2BAA2B;IAC3B,MAAM,aAAa,SAAS,QAAQ;AAClC,YAAM,qBAAqB,CAAC,+BAA+B,sBAAsB,OAAO;AACxF,UAAI,CAAC,QAAQ;AAEX,YAAI,8BAA8B,WAAW,YAAY,WAAW,UAAU;AAC5E,iBAAO,EAAE,IAAI,OAAO,QAAQ,KAAK,OAAO,wBAAwB,6BAA6B;QAC/F;AACA,eAAO,EAAE,IAAI,MAAM,WAAW,MAAM,mBAAmB;MACzD;AACA,UAAI,eAAe,IAAI,MAAM,KAAK,sBAAsB,OAAO,GAAG;AAChE,eAAO,EAAE,IAAI,MAAM,WAAW,MAAM,mBAAmB;MACzD;AACA,aAAO,EAAE,IAAI,OAAO,QAAQ,KAAK,OAAO,wBAAwB,aAAa;IAC/E;IACA,MAAM,YAAY;AAChB,aAAO;IACT;EACF;AACF;AC/FO,IAAM,qBAAqB;AAS3B,IAAM,yBAAgD,CAAC,QAAQ,SAAS,QAAQ;AAOhF,IAAM,kBAAkB;AAGxB,IAAM,uBAAuB;AAU7B,IAAM,+BAAsD,CAAC,gBAAgB,eAAe;AAM5F,IAAM,2BAA2B;AAGjC,IAAM,uBAAuB;AAG7B,IAAM,kBAAkB;EAC7B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AACF;ACvBO,SAAS,sBAAsB,cAAyD;AAC7F,QAAM,yBAAyB,oBAAI,IAAoB;AACvD,aAAW,UAAU,CAAC,GAAG,8BAA8B,GAAI,gBAAgB,CAAC,CAAE,GAAG;AAC/E,QAAI,OAAO,WAAW,YAAY,CAAC,yBAAyB,KAAK,MAAM,GAAG;AACxE,YAAM,WAAW,OAAO,MAAM,EAAE,MAAM,GAAG,+BAA+B;AACxE,YAAM,IAAI,MAAM,GAAG,sCAAsC,oBAAoB,IAAI,KAAK,UAAU,QAAQ,CAAC,EAAE;IAC7G;AACA,UAAM,gBAAgB,OAAO,YAAY;AACzC,QAAI,CAAC,uBAAuB,IAAI,aAAa,EAAG,wBAAuB,IAAI,eAAe,MAAM;EAClG;AACA,SAAO,CAAC,GAAG,uBAAuB,OAAO,CAAC,EAAE,KAAK,IAAI;AACvD;AASO,SAAS,iBAAiB,EAAE,gBAAgB,gBAAgB,eAAe,GAAkC;AAClH,SAAO,EAAE,gBAAgB,gBAAgB,gBAAgB,sBAAsB,cAAc,EAAE;AACjG;AAOO,SAAS,iBAAiB,SAAkB,QAAiC;AAClF,MAAI,CAAC,OAAO,eAAgB,QAAO,CAAC;AACpC,QAAM,SAAS,QAAQ,QAAQ,IAAI,QAAQ;AAC3C,MAAI,CAAC,UAAU,CAAC,OAAO,eAAe,SAAS,MAAM,EAAG,QAAO,CAAC;AAChE,SAAO;IACL,+BAA+B;IAC/B,gCAAgC,OAAO;IACvC,gCAAgC,OAAO;IACvC,oCAAoC;IACpC,0BAA0B,OAAO,oBAAoB;IACrD,MAAM;EACR;AACF;AAGO,SAAS,kBAAkB,SAAkB,QAA8B;AAChF,SAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,SAAS,iBAAiB,SAAS,MAAM,EAAE,CAAC;AACvF;AAGO,SAAS,SAAS,UAAoB,aAAoC;AAC/E,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,WAAW,GAAG;AACtD,aAAS,QAAQ,IAAI,KAAK,KAAK;EACjC;AACA,SAAO;AACT;ACpFO,IAAM,gBAAgC;EAC3C,MAAM,SAAS,SAAS;AACtB,YAAQ,MAAM,SAAS,OAAO;EAChC;AACF;AAyBO,SAAS,sBAAsB;EACpC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AACF,GAAkC;AAChC,SAAO,MAAM,cAAc,MAAM,KAAK,SAAS,WAAW;IACxD,OAAO;IACP,QAAQ,QAAQ;IAChB,MAAM,IAAI,IAAI,QAAQ,GAAG,EAAE;EAC7B,CAAC;AACD,QAAM,UAAU,gBAAgB,OAAO,KAAK,wBAAwB;AACpE,SAAO,SAAS,SAAS,KAAK,EAAE,OAAO,QAAQ,GAAG,EAAE,QAAQ,KAAK,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC,EAAG,CAAC,GAAG,WAAW;AAClH;ACnCO,IAAM,gBACX;ACwCK,SAASC,UAAS,OAAuD;AAC9E,SAAO,OAAO,UAAU,YAAY,UAAU;AAChD;AAWO,SAASC,QAA8B,OAAgB,KAAqC;AACjG,SAAOD,UAAS,KAAK,KAAK,OAAO;AACnC;AC+YO,IAAME,kBAAiB,CAAC,YAAY,UAAU,OAAO,OAAO;AAI5D,IAAMC,qBAAoB,CAAC,QAAQ,eAAe,YAAY,UAAU;AASxE,IAAMC,4BAA2B,CAAC,YAAY,UAAU;AAexD,SAASC,gBAAe,QAAwD;AACrF,SAAQD,0BAAuD,SAAS,MAAM;AAChF;AA8IO,SAASE,kBAAiB,QAAwB,WAAiB,oBAAI,KAAK,GAAwB;AACzG,SAAOD,gBAAe,MAAM,IAAI,EAAE,QAAQ,YAAY,SAAS,IAAI,EAAE,QAAQ,YAAY,KAAK;AAChG;AAmFO,IAAME,sBAAN,cAAiC,MAAM;EACnC,OAAO;EAChB,YAAY,UAAU,oBAAoB,SAAwB;AAChE,UAAM,SAAS,OAAO;AACtB,SAAK,OAAO;EACd;AACF;AAOO,IAAMC,uBAAN,cAAkC,MAAM;EACpC,OAAO;EAChB,YAAY,UAAU,oBAAoB,SAAwB;AAChE,UAAM,SAAS,OAAO;AACtB,SAAK,OAAO;EACd;AACF;AAmBA,SAASC,cAA+B,OAAgB,MAAiC;AACvF,SAAOR,QAAO,OAAO,MAAM,KAAK,MAAM,SAAS;AACjD;AASO,SAASS,iBACd,OACmF;AACnF,MAAI,iBAAiBH,oBAAoB,QAAO;AAEhD,SAAOE,cAAa,OAAO,iBAAiB,KAAKA,cAAa,OAAO,OAAO;AAC9E;AAOO,SAASE,kBACd,OACoF;AACpF,MAAI,iBAAiBH,qBAAqB,QAAO;AAEjD,SAAOC,cAAa,OAAO,iBAAiB,KAAKA,cAAa,OAAO,OAAO;AAC9E;AAkBO,SAASG,mBAAkB,KAA+C;AAC/E,SAAO;IACL,aAAa,IAAI,OAAO;IACxB,OAAO,IAAI,OAAO;IAClB,aAAa,IAAI,OAAO;IACxB,YAAY,IAAI,OAAO;IACvB,WAAW,IAAI,OAAO;IACtB,YAAY,IAAI,OAAO;IACvB,YAAY,IAAI,OAAO;IACvB,aAAa,IAAI,OAAO;IACxB,cAAc,IAAI,OAAO;IACzB,WAAW,IAAI,OAAO,aAAa;IACnC,MAAM,IAAI,KAAK;IACf,MAAM,IAAI,KAAK;IACf,MAAM,IAAI,KAAK;IACf,MAAM,IAAI,KAAK;IACf,SAAS,IAAI;IACb,SAAS,IAAI;IACb,WAAW,IAAI;IACf,WAAW,IAAI;IACf,kBAAkB,IAAI;EACxB;AACF;AAkIO,IAAMC,6BAA4B,CAAC,OAAO,QAAQ,QAAQ,OAAO;AC77BxE,IAAMC,KAAmB,OAAO,MAAU,QAAI;AAE9C,IAAM,eAAeA,GAAE,OAAO;EAC5B,aAAaA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAI;EACvC,OAAOA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAI;EACjC,aAAaA,GAAE,OAAO,EAAE,IAAI,GAAG;EAC/B,YAAYA,GAAE,OAAO,EAAE,IAAI,CAAC;EAC5B,WAAWA,GAAE,OAAO,EAAE,SAAS;EAC/B,YAAYA,GAAE,OAAO,EAAE,IAAI,GAAG;EAC9B,YAAYA,GAAE,OAAO,EAAE,IAAI,GAAG;EAC9B,aAAaA,GAAE,OAAO,EAAE,IAAI,GAAG;EAC/B,cAAcA,GAAE,OAAO,EAAE,IAAI,GAAG;;;EAGhC,WAAWA,GAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS;AACrD,CAAC;AAED,IAAM,aAAaA,GAAE,OAAO;EAC1B,MAAMA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC;EAC7B,MAAMA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC;EAC7B,MAAMA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC;EAC7B,MAAMA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC;AAC/B,CAAC;AAED,IAAM,mBAAmBA,GAAE,OAAO;EAChC,QAAQ;EACR,MAAM;EACN,SAASA,GAAE,OAAO,EAAE,IAAI,CAAC;EACzB,SAASA,GAAE,OAAO,EAAE,IAAI,CAAC;EACzB,WAAWA,GAAE,OAAO,EAAE,IAAI,EAAE,SAAS;EACrC,WAAWA,GAAE,OAAO,EAAE,IAAI,EAAE,SAAS;EACrC,kBAAkBA,GAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,CAAC;AACnD,CAAC;AAKD,IAAM,qBAAqBA,GAAE,OAAO;EAClC,OAAOA,GAAE,KAAKD,0BAAyB;EACvC,WAAWC,GAAE,OAAO,EAAE,IAAI,EAAE;EAC5B,SAASA,GAAE,OAAO,EAAE,IAAI,GAAG;AAC7B,CAAC;AAED,IAAM,qBAAqBA,GAAE,OAAO;EAClC,KAAKA,GAAE,OAAO,EAAE,IAAI,GAAI;EACxB,QAAQA,GAAE,OAAO,EAAE,IAAI,EAAE;EACzB,QAAQA,GAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;EACvC,YAAYA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAO;EACzC,WAAWA,GAAE,OAAO,EAAE,IAAI,EAAE;AAC9B,CAAC;AAED,IAAM,oBAAoBA,GAAE,OAAO;EACjC,SAASA,GAAE,MAAM,kBAAkB,EAAE,IAAI,EAAE;EAC3C,SAASA,GAAE,MAAM,kBAAkB,EAAE,IAAI,EAAE;AAC7C,CAAC;AAMM,IAAM,yBAAyBA,GAAE,aAAa;EACnD,MAAMA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC;EAC7B,MAAMA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC;EAC7B,MAAMA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC;EAC7B,MAAMA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC;AAC/B,CAAC;AAEM,IAAM,uBAAuBA,GAAE,OAAO;EAC3C,aAAaA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;EACtC,MAAMA,GAAE,KAAKZ,eAAc;EAC3B,SAASY,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAI;;;;;;;EAOnC,KAAKA,GAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAI;;;EAGtC,YAAYA,GAAE,OAAO,EAAE,IAAI,GAAI,EAAE,SAAS,EAAE,SAAS;EACrD,UAAUA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE;EAClC,WAAWA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;EACpC,YAAYA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;;;EAGrC,aAAaA,GAAE,MAAM,EAAE,SAAS,cAAc,CAAC,EAAE,IAAI,GAAG;EACxD,aAAaA,GAAE,MAAM,gBAAgB,EAAE,IAAI,EAAE;;;;;;EAM7C,UAAUA,GACP,OAAO,EACP,IAAI,CAAC,EACL,IAAI,GAAG,EACP,MAAM,oBAAoB,qDAAqD;;;;;EAKlF,mBAAmBA,GAChB,OAAO,EACP,IAAI,IAAS,EACb,MAAM,wCAAwC,qDAAqD,EACnG,SAAS,EACT,SAAS;;;;;EAKZ,kBAAkB,uBAAuB,SAAS,EAAE,SAAS;;;;EAI7D,aAAa,kBAAkB,SAAS,EAAE,SAAS;AACrD,CAAC;AAEM,IAAM,sBAAsBA,GAAE,OAAO;EAC1C,IAAIA,GAAE,OAAO,EAAE,IAAI,CAAC;EACpB,aAAaA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;EACtC,QAAQA,GAAE,KAAKX,kBAAiB;AAClC,CAAC;AAEM,IAAM,uBAAuBW,GAAE,MAAM;EAC1CA,GAAE,OAAO,EAAE,IAAIA,GAAE,OAAO,EAAE,IAAI,CAAC,GAAG,aAAaA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,CAAC;EAC3EA,GAAE,OAAO,EAAE,aAAaA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,GAAG,WAAWA,GAAE,QAAQ,IAAI,EAAE,CAAC;AAClF,CAAC;AAEM,IAAM,iBAAiBA,GAAE,OAAO;EACrC,aAAaA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;EACtC,MAAMA,GAAE,OAAO,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;EAC9C,OAAOA,GAAE,OAAO,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE;EACzD,MAAMA,GAAE,KAAKZ,eAAc,EAAE,SAAS;EACtC,QAAQY,GAAE,KAAKX,kBAAiB,EAAE,SAAS;;;;;EAK3C,UAAUW,GACP;IACC,CAAC,QAAS,OAAO,QAAQ,YAAY,IAAI,SAAS,IAAI,IAAI,MAAM,GAAG,IAAI;IACvEA,GAAE,MAAMA,GAAE,KAAKX,kBAAiB,CAAC,EAAE,IAAI,CAAC;EAC1C,EACC,SAAS;EACZ,QAAQW,GAAE,OAAO,EAAE,IAAI,GAAG,EAAE,SAAS;;EAErC,KAAKA,GAAE,OAAO,EAAE,IAAI,GAAI,EAAE,SAAS;EACnC,YAAYA,GAAE,OAAO,EAAE,IAAI,GAAI,EAAE,SAAS;AAC5C,CAAC;AAmFM,SAAS,uBAAuB,OAA0C;AAG/E,SAAO,MAAM,OAAO,IAAI,CAAC,WAAW;IAClC,OAAO,MAAM,KAAK,IAAI,MAAM,EAAE,KAAK,GAAG;IACtC,SAAS,MAAM;EACjB,EAAE;AACJ;AC5MA,IAAM,qBAAqB;AAG3B,IAAM,iBAAyD;EAC7D,KAAK;EACL,UAAU;EACV,QAAQ;EACR,OAAO;AACT;AAEA,IAAM,wBAAwB;AA4D9B,SAAS,SAAS,MAAc,MAAM,KAAa;AACjD,MAAI,KAAK,UAAU,IAAK,QAAO;AAC/B,SAAO,GAAG,KAAK,MAAM,GAAG,MAAM,CAAC,CAAC;AAClC;AAGA,IAAM,mBAAmB;AASzB,SAAS,gBAAgB,MAAsB;AAC7C,SAAO,KAAK,QAAQ,MAAM,OAAO,EAAE,QAAQ,MAAM,MAAM,EAAE,QAAQ,MAAM,MAAM;AAC/E;AAOA,SAAS,kBAAkB,UAA+C;AACxE,QAAM,UAAU,gBAAgB,SAAS,SAAS,OAAO,CAAC;AAC1D,QAAM,SAAS,gBAAgB,SAAS,UAAU;AAClD,QAAM,WAAW,OAAO,SAAS,IAAI,kBAAkB,SAAS,UAAU;AAC1E,SAAO;IACL,MAAM,GAAG,gBAAgB,QAAQ,CAAC,KAAK,OAAO;IAC9C,QAAQ;MACN;QACE,MAAM;QACN,MAAM,EAAE,MAAM,cAAc,MAAM,SAAS,UAAU,gBAAgB,GAAG,OAAO,KAAK;MACtF;MACA;QACE,MAAM;QACN,MAAM,EAAE,MAAM,UAAU,MAAM,QAAQ;MACxC;MACA;QACE,MAAM;QACN,UAAU;UACR,EAAE,MAAM,UAAU,MAAM,cAAc,gBAAgB,SAAS,WAAW,CAAC,GAAG;UAC9E,EAAE,MAAM,UAAU,MAAM,WAAW,SAAS,IAAI,GAAG;UACnD,EAAE,MAAM,UAAU,MAAM,UAAU,gBAAgB,SAAS,GAAG,CAAC,GAAG;UAClE,EAAE,MAAM,UAAU,MAAM,WAAW,MAAM,KAAK,gBAAgB,SAAS,WAAW,CAAC,IAAI;QACzF;MACF;IACF;EACF;AACF;AAOA,SAAS,oBAAoB,UAAiD;AAC5E,QAAM,UAAU,SAAS,SAAS,OAAO;AACzC,SAAO;IACL,SAAS,SAAS,SAAS,IAAI,sBAAsB,SAAS,UAAU;IACxE,QAAQ;MACN;QACE,OAAO,GAAG,SAAS,IAAI,WAAM,SAAS,WAAW;QACjD,aAAa;QACb,OAAO,eAAe,SAAS,IAAI,KAAK;QACxC,QAAQ;UACN,EAAE,MAAM,OAAO,OAAO,SAAS,KAAK,QAAQ,MAAM;UAClD,EAAE,MAAM,UAAU,OAAO,GAAG,SAAS,UAAU,KAAK,SAAS,WAAW,KAAK,QAAQ,KAAK;UAC1F,EAAE,MAAM,YAAY,OAAO,SAAS,UAAU,QAAQ,KAAK;QAC7D;QACA,WAAW,IAAI,KAAK,SAAS,SAAS,EAAE,YAAY;MACtD;IACF;IACA,kBAAkB,EAAE,OAAO,CAAC,EAAE;EAChC;AACF;AAGA,SAAS,oBAAoB,UAAiD;AAC5E,QAAM,EAAE,UAAU,WAAW,GAAG,QAAQ,IAAI;AAC5C,SAAO;AACT;AAQO,SAAS,oBACd,MACA,UAC+G;AAC/G,UAAQ,MAAM;IACZ,KAAK;AACH,aAAO,kBAAkB,QAAQ;IACnC,KAAK;AACH,aAAO,oBAAoB,QAAQ;IACrC;AACE,aAAO,oBAAoB,QAAQ;EACvC;AACF;AAUA,eAAsB,gBAAgB,QAAuB,UAAyC;AACpG,QAAM,YAAY,OAAO,aAAa;AACtC,QAAM,OAAO,KAAK,UAAU,oBAAoB,OAAO,QAAQ,WAAW,QAAQ,CAAC;AAKnF,QAAM,UAAkC,EAAE,gBAAgB,oBAAoB,GAAI,OAAO,WAAW,CAAC,EAAG;AAIxG,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,SAAS;AAE5D,MAAI;AACF,UAAM,WAAW,MAAM,MAAM,OAAO,KAAK;MACvC,QAAQ;MACR;MACA;MACA,QAAQ,WAAW;IACrB,CAAC;AACD,iBAAa,KAAK;AAElB,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,MAAM,IAAI,MAAM,+BAA+B,SAAS,MAAM,EAAE;AACtE,kBAAY,QAAQ,KAAK,SAAS,EAAE;IACtC;EACF,SAAS,UAAU;AACjB,iBAAa,KAAK;AAClB,UAAM,MAAM,oBAAoB,QAAQ,WAAW,IAAI,MAAM,OAAO,QAAQ,CAAC;AAC7E,gBAAY,QAAQ,KAAK,SAAS,EAAE;EACtC;AACF;AAEA,SAAS,YAAY,QAAuB,KAAY,YAA0B;AAChF,MAAI,OAAO,SAAS;AAClB,QAAI;AACF,aAAO,QAAQ,KAAK,UAAU;IAChC,SAAS,aAAa;AAIpB,cAAQ;QACN,4DAA4D,UAAU,KAAK,OAAO,WAAW,CAAC,qBAAqB,IAAI,OAAO;MAChI;IACF;AACA;EACF;AACA,UAAQ,KAAK,yBAAyB,OAAO,GAAG,wBAAwB,UAAU,KAAK,IAAI,OAAO,EAAE;AACtG;AAQA,eAAsB,iBAAiB,SAAmC,UAAyC;AACjH,MAAI,QAAQ,WAAW,EAAG;AAC1B,QAAM,QAAQ,IAAI,QAAQ,IAAI,CAAC,MAAM,gBAAgB,GAAG,QAAQ,CAAC,CAAC;AACpE;ACtQA,SAAS,cAAc,SAAqD;AAC1E,SAAO;IACL,aAAa,QAAQ;IACrB,MAAM,QAAQ;IACd,SAAS,QAAQ;IACjB,QAAQ;IACR,KAAK,QAAQ;IACb,YAAY,QAAQ,cAAc;IAClC,UAAU,QAAQ;IAClB,WAAW,QAAQ;IACnB,YAAY,QAAQ;IACpB,aAAa,QAAQ;IACrB,UAAU,QAAQ;IAClB,aAAa,QAAQ,YAAY,IAAIF,kBAAiB;IACtD,mBAAmB,QAAQ,qBAAqB;IAChD,kBAAkB,QAAQ,oBAAoB;IAC9C,aAAa,QAAQ,eAAe;EACtC;AACF;AAGO,SAAS,wBAAmC;EACjD;EACA;EACA;EACA;EACA;AACF,GAA0C;AAQxC,QAAM,iBAAiB,CAAC,OAAwC,UAA0B,gBACxF,SAAS,gBAAgB,cACrB,SAAS,KAAK,OAAO,SAAS,eAAe,OAAO,QAAQ,GAAG,EAAE,QAAQ,IAAI,CAAC,IAC9E,SAAS,MAAM,OAAO,KAAK,wBAAwB,4BAA4B;AAUrF,QAAM,SAAS,OAAO,UAAsF;AAC1G,QAAI;AACF,UAAI,MAAM,wBAAwB;AAChC,cAAM,EAAE,UAAU,QAAQ,IAAI,MAAM,MAAM,uBAAuB,KAAK;AACtE,eAAO,EAAE,UAAU,OAAO,QAAQ;MACpC;AACA,aAAO,EAAE,UAAU,MAAM,MAAM,eAAe,KAAK,GAAG,OAAO,KAAK;IACpE,SAAS,OAAO;AACd,UAAID,kBAAiB,KAAK,GAAG;AAC3B,cAAM,WAAW,MAAM,MAAM,eAAe,MAAM,QAAQ;AAC1D,YAAI,SAAU,QAAO,EAAE,UAAU,UAAU,OAAO,MAAM;MAC1D;AACA,YAAM;IACR;EACF;AAEA,SAAO,OAAO,YAAwC;AACpD,UAAM,iBAAiB,MAAM,SAAS,aAAa,SAAS,MAAM;AAClE,QAAI,CAAC,eAAe,GAAI,QAAO,eAAe;AAC9C,UAAM,QAAQ,eAAe;AAE7B,UAAM,UAAU,MAAM,SAAS,SAAS,OAAO,oBAAoB;AACnE,QAAI,CAAC,QAAQ,GAAI,QAAO,QAAQ;AAEhC,QAAI,QAAQ,MAAM,YAAY,SAAS,8BAA8B;AACnE,aAAO,SAAS,MAAM,OAAO,KAAK,wBAAwB,kBAAkB;IAC9E;AAEA,QAAI;AACF,YAAM,iBAAiB,cAAc,QAAQ,KAAK;AAClD,YAAM,QAAQ,eAAe,MAAM,aAAa,gBAAgB,MAAM,OAAO,IAAI;AAEjF,YAAM,UAAU,MAAM,SAAS,UAAU,OAAO,EAAE,QAAQ,UAAU,aAAa,MAAM,YAAY,CAAC;AACpG,UAAI,QAAS,QAAO;AAIpB,YAAM,WAAW,MAAM,MAAM,eAAe,MAAM,QAAQ;AAC1D,UAAI,SAAU,QAAO,eAAe,OAAO,UAAU,MAAM,WAAW;AAEtE,YAAM,EAAE,UAAU,MAAM,IAAI,MAAM,OAAO,KAAK;AAC9C,UAAI,SAAS,SAAS,gBAAgB,MAAM,aAAa;AACvD,YAAI,SAAS,SAAS,EAAG,MAAK,iBAAiB,UAAU,QAAQ;AACjE,YAAI,UAAW,OAAM,SAAS,QAAQ,aAAa,MAAM,UAAU,UAAU,MAAM,OAAO,CAAC;MAC7F;AACA,aAAO,eAAe,OAAO,UAAU,MAAM,WAAW;IAC1D,SAAS,OAAO;AACd,aAAO,SAAS,cAAc,OAAO,mBAAmB,KAAK;IAC/D;EACF;AACF;AC5GA,SAAS,iBAAiB,UAAuD;AAC/E,SAAO,eAAe,WAClB,EAAE,MAAM,WAAW,aAAa,SAAS,YAAY,IACrD,EAAE,MAAM,UAAU,IAAI,SAAS,IAAI,aAAa,SAAS,YAAY;AAC3E;AAGO,SAAS,wBAAmC;EACjD;EACA;EACA;EACA;AACF,GAA0C;AACxC,QAAM,eAAe,CAAC,WACpB,OAAO,SAAS,YAAY,MAAM,mBAAmB,OAAO,WAAW,IAAI,MAAM,eAAe,OAAO,EAAE;AAE3G,SAAO,OAAO,YAAwC;AACpD,UAAM,iBAAiB,MAAM,SAAS,aAAa,SAAS,QAAQ;AACpE,QAAI,CAAC,eAAe,GAAI,QAAO,eAAe;AAC9C,UAAM,QAAQ,eAAe;AAE7B,UAAM,UAAU,MAAM,SAAS,SAAS,OAAO,oBAAoB;AACnE,QAAI,CAAC,QAAQ,GAAI,QAAO,QAAQ;AAChC,UAAM,SAAS,iBAAiB,QAAQ,KAA4B;AAEpE,QAAI;AACF,YAAM,UAAU,MAAM,SAAS;QAC7B;QACA,OAAO,SAAS,YACZ,EAAE,QAAQ,aAAa,aAAa,OAAO,YAAY,IACvD,EAAE,QAAQ,UAAU,aAAa,OAAO,aAAa,YAAY,OAAO,GAAG;MACjF;AACA,UAAI,QAAS,QAAO;AAGpB,UACE,OAAO,SAAS,YAChB,MAAM,0BACN,CAAE,MAAM,MAAM,uBAAuB,OAAO,IAAI,OAAO,WAAW,GAClE;AACA,eAAO,SAAS,MAAM,OAAO,KAAK,wBAAwB,gBAAgB;MAC5E;AAEA,UAAI,YAAY;AACd,YAAI;AACF,gBAAM,WAAW,QAAQ,MAAM,OAAO;QACxC,SAAS,OAAO;AACd,mBAAS,OAAO,MAAM,0EAA0E;YAC9F;YACA;UACF,CAAC;AACD,iBAAO,SAAS,MAAM,OAAO,KAAK,wBAAwB,eAAe;QAC3E;MACF;AAEA,YAAM,aAAa,MAAM;AACzB,UAAI,UAAW,OAAM,SAAS,QAAQ,aAAa,MAAM,UAAU,QAAQ,MAAM,OAAO,CAAC;AACzF,aAAO,SAAS,KAAK,OAAO,EAAE,SAAS,KAAK,CAAC;IAC/C,SAAS,OAAO;AACd,UAAID,iBAAgB,KAAK,EAAG,QAAO,SAAS,MAAM,OAAO,KAAK,wBAAwB,gBAAgB;AACtG,aAAO,SAAS,cAAc,OAAO,mBAAmB,KAAK;IAC/D;EACF;AACF;ACnEA,SAAS,cAAc,SAA0C;AAC/D,QAAM,eAAe,IAAI,IAAI,QAAQ,GAAG,EAAE;AAC1C,QAAM,WAAmC,CAAC;AAC1C,aAAW,OAAO,iBAAiB;AACjC,UAAM,QAAQ,aAAa,IAAI,GAAG;AAClC,QAAI,UAAU,KAAM,UAAS,GAAG,IAAI;EACtC;AACA,SAAO;AACT;AAGO,SAAS,uBAAkC,EAAE,OAAO,SAAS,GAAyC;AAC3G,SAAO,OAAO,YAAwC;AACpD,UAAM,iBAAiB,MAAM,SAAS,aAAa,SAAS,KAAK;AACjE,QAAI,CAAC,eAAe,GAAI,QAAO,eAAe;AAC9C,UAAM,QAAQ,eAAe;AAE7B,UAAM,QAAQ,SAAS,SAAS,OAAO,gBAAgB,cAAc,OAAO,CAAC;AAC7E,QAAI,CAAC,MAAM,GAAI,QAAO,MAAM;AAE5B,QAAI;AACF,YAAM,UAAU,MAAM,SAAS,UAAU,OAAO,EAAE,QAAQ,QAAQ,aAAa,MAAM,MAAM,YAAY,CAAC;AACxG,UAAI,QAAS,QAAO;AACpB,YAAM,OAAO,MAAM,MAAM,aAAa,MAAM,KAAK;AACjD,aAAO,SAAS;QACd;QACA,EAAE,GAAG,MAAM,WAAW,KAAK,UAAU,IAAI,CAAC,aAAa,SAAS,QAAQ,OAAO,QAAQ,CAAC,EAAE;QAC1F,EAAE,SAAS,EAAE,iBAAiB,mBAAmB,EAAE;MACrD;IACF,SAAS,OAAO;AACd,aAAO,SAAS,cAAc,OAAO,kBAAkB,KAAK;IAC9D;EACF;AACF;AC/BO,SAAS,wBAAmC;EACjD;EACA;EACA;AACF,GAA0C;AACxC,SAAO,OAAO,YAAwC;AACpD,UAAM,iBAAiB,MAAM,SAAS,aAAa,SAAS,OAAO;AACnE,QAAI,CAAC,eAAe,GAAI,QAAO,eAAe;AAC9C,UAAM,QAAQ,eAAe;AAE7B,UAAM,UAAU,MAAM,SAAS,SAAS,OAAO,mBAAmB;AAClE,QAAI,CAAC,QAAQ,GAAI,QAAO,QAAQ;AAChC,UAAM,EAAE,IAAI,aAAa,OAAO,IAAI,QAAQ;AAE5C,QAAI;AACF,YAAM,UAAU,MAAM,SAAS,UAAU,OAAO,EAAE,QAAQ,UAAU,aAAa,YAAY,GAAG,CAAC;AACjG,UAAI,QAAS,QAAO;AAIpB,UAAI,MAAM,0BAA0B,CAAE,MAAM,MAAM,uBAAuB,IAAI,WAAW,GAAI;AAC1F,eAAO,SAAS,MAAM,OAAO,KAAK,wBAAwB,gBAAgB;MAC5E;AAEA,YAAM,WAAW,MAAM,MAAM,eAAe,IAAIJ,kBAAiB,MAAM,CAAC;AACxE,UAAI,UAAW,OAAM,SAAS,QAAQ,aAAa,MAAM,UAAU,UAAU,MAAM,OAAO,CAAC;AAC3F,aAAO,SAAS,KAAK,OAAO,SAAS,QAAQ,OAAO,QAAQ,CAAC;IAC/D,SAAS,OAAO;AACd,UAAII,iBAAgB,KAAK,EAAG,QAAO,SAAS,MAAM,OAAO,KAAK,wBAAwB,gBAAgB;AACtG,aAAO,SAAS,cAAc,OAAO,mBAAmB,KAAK;IAC/D;EACF;AACF;AC/BO,SAAS,sBAAsB,QAAqC;AACzE,SAAO,uBAAuB,SAAS,MAAM;AAC/C;AAcO,SAAS,wBAAwB,SAAkB,QAA6B;AACrF,MAAI,CAAC,OAAO,eAAgB,QAAO;AACnC,QAAM,SAAS,QAAQ,QAAQ,IAAI,QAAQ;AAC3C,MAAI,WAAW,KAAM,QAAO;AAC5B,MAAI,OAAO,eAAe,SAAS,MAAM,EAAG,QAAO;AACnD,SAAO,WAAW,IAAI,IAAI,QAAQ,GAAG,EAAE;AACzC;AAGO,SAAS,mBAAmB,SAA2B;AAC5D,QAAM,cAAc,QAAQ,QAAQ,IAAI,cAAc;AACtD,MAAI,gBAAgB,KAAM,QAAO;AACjC,QAAM,CAAC,YAAY,EAAE,IAAI,YAAY,MAAM,GAAG;AAC9C,SAAO,UAAU,KAAK,EAAE,YAAY,MAAM;AAC5C;AAGO,SAAS,qBAAqB,SAA0B;AAC7D,QAAM,SAAS,QAAQ,QAAQ,IAAI,QAAQ,KAAK;AAEhD,SAAO,OAAO,QAAQ,0BAA0B,EAAE,EAAE,MAAM,GAAG,wBAAwB;AACvF;ACCA,SAAS,eAAe,UAA0B,cAAyD;AACzG,QAAM,EAAE,UAAU,WAAW,GAAG,KAAK,IAAI;AACzC,SAAO,eAAe,OAAO,EAAE,GAAG,MAAM,aAAa,GAAG;AAC1D;AAOO,SAAS,sBAAiC;EAC/C;EACA;EACA;EACA;EACA;AACF,GAA2C;AACzC,QAAM,OAAO,CAAC,OAA6D,MAAe,SACxF,SAAS,SAAS,KAAK,MAAM,IAAI,GAAG,MAAM,WAAW;AAEvD,QAAM,QAAQ,CAAC,OAA6D,QAAgB,YAC1F,KAAK,OAAO,EAAE,OAAO,QAAQ,GAAG,EAAE,OAAO,CAAC;AAG5C,QAAM,WAAW,CACf,OACA,QACA,UACyB;AACzB,UAAM,SAAS,OAAO,UAAU,KAAK;AACrC,QAAI,OAAO,QAAS,QAAO,EAAE,IAAI,MAAM,OAAO,OAAO,KAAK;AAC1D,WAAO,EAAE,IAAI,OAAO,UAAU,KAAK,OAAO,EAAE,QAAQ,uBAAuB,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC,EAAE;EAC/G;AAGA,QAAM,gBAAgB,CAAC,SAAkB,aAA0B,WAAmB,YACpF,sBAAsB;IACpB;IACA;IACA,QAAQ;IACR;IACA;IACA;IACA;EACF,CAAC;AAGH,QAAM,UAAU,CACd,OACA,UACA,eAAe,MAAM,uBAClB,eAAe,kBAAkB,gBAAgB,UAAU,MAAM,OAAO,IAAI,UAAU,YAAY;AAEvG,SAAO;IACL;IACA;;;;;;;;;IAUA,MAAM,aACJ,SACA,QACwD;AACxD,YAAM,cAAc,iBAAiB,SAAS,UAAU;AACxD,UAAI,sBAAsB,MAAM,GAAG;AACjC,YAAI,CAAC,wBAAwB,SAAS,UAAU,GAAG;AACjD,iBAAO,MAAM,8FAA8F;YACzG;YACA,MAAM,IAAI,IAAI,QAAQ,GAAG,EAAE;YAC3B,QAAQ,qBAAqB,OAAO;UACtC,CAAC;AACD,iBAAO,EAAE,IAAI,OAAO,UAAU,MAAM,EAAE,YAAY,GAAG,KAAK,wBAAwB,SAAS,EAAE;QAC/F;AACA,YAAI,CAAC,mBAAmB,OAAO,GAAG;AAChC,iBAAO,EAAE,IAAI,OAAO,UAAU,MAAM,EAAE,YAAY,GAAG,KAAK,wBAAwB,oBAAoB,EAAE;QAC1G;MACF;AACA,UAAI;AACJ,UAAI;AACF,kBAAU,MAAM,KAAK,aAAa,SAAS,MAAM;MACnD,SAAS,SAAS;AAChB,eAAO,EAAE,IAAI,OAAO,UAAU,cAAc,SAAS,aAAa,wBAAwB,OAAO,EAAE;MACrG;AACA,UAAI,CAAC,QAAQ,GAAI,QAAO,EAAE,IAAI,OAAO,UAAU,MAAM,EAAE,YAAY,GAAG,QAAQ,QAAQ,QAAQ,KAAK,EAAE;AACrG,aAAO;QACL,IAAI;QACJ,OAAO;UACL,SAAS,EAAE,SAAS,WAAW,QAAQ,UAAU;UACjD,oBAAoB,QAAQ;UAC5B;QACF;MACF;IACF;IAEA;;IAGA,MAAM,SACJ,OACA,QAC+B;AAC/B,YAAM,OAAgB,MAAM,MAAM,QAAQ,QAAQ,KAAK,EAAE,MAAM,MAAM,IAAI;AACzE,UAAI,CAAC,KAAM,QAAO,EAAE,IAAI,OAAO,UAAU,MAAM,OAAO,KAAK,wBAAwB,WAAW,EAAE;AAChG,aAAO,SAAS,OAAO,QAAQ,IAAI;IACrC;;IAGA,MAAM,UACJ,OACA,eAC0B;AAC1B,YAAM,UAAU,MAAM,KAAK,UAAU,EAAE,GAAG,MAAM,SAAS,GAAG,cAAc,CAAC;AAC3E,aAAO,UAAU,OAAO,MAAM,OAAO,KAAK,wBAAwB,SAAS;IAC7E;IAEA;;;;;;IAOA,eAAe,OAAwC,UAA0B;AAC/E,aAAO,QAAQ,OAAO,UAAU,KAAK,6BAA6B,MAAM,kBAAkB;IAC5F;;IAGA,cAAc,OAAwC,WAAmB,SAA4B;AACnG,aAAO,cAAc,MAAM,QAAQ,SAAS,MAAM,aAAa,WAAW,OAAO;IACnF;;IAGA,MAAM,QAAQ,MAAc,QAAmD;AAC7E,UAAI;AACF,cAAM,OAAO;MACf,SAAS,SAAS;AAChB,eAAO,MAAM,0CAA0C,IAAI,WAAW,EAAE,OAAO,QAAQ,CAAC;MAC1F;IACF;IAEA;EACF;AACF;ACpLA,SAAS,cAAc,UAAyF;AAC9G,MAAI,CAAC,SAAU,QAAO,CAAC;AACvB,SAAO,MAAM,QAAQ,QAAQ,IAAK,WAA4C,CAAC,QAAyB;AAC1G;AA2BO,SAAS,sBAAiC,SAA6D;AAG5G,QAAM;IACJ;IACA;IACA;IACA;IACA;IACA,QAAQ,CAAC;IACT,SAAS;IACT;EACF,IAAI;AACJ,MAAI,CAAC,OAAO;AACV,UAAM,IAAI,MAAM,sDAAsD;EACxE;AAKA,MAAI,QAAQ,QAAQ,aAAa,CAAC,MAAM,wBAAwB;AAC9D,UAAM,IAAI,MAAM,sCAAsC,6BAA6B;EACrF;AACA,QAAM,aAAa,iBAAiB,EAAE,gBAAgB,gBAAgB,gBAAgB,qBAAqB,CAAC;AAC5G,QAAM,OAA8B,QAAQ,SACxC,sBAAsB,QAAQ,MAAM,IACnC,iBAAiB,OAAO;AAC7B,QAAM,WAAW,sBAAiC,EAAE,MAAM,YAAY,QAAQ,eAAe,gBAAgB,CAAC;AAE9G,SAAO;;IAEL,SAAS,CAAC,YAA+B,kBAAkB,SAAS,UAAU;IAC9E,MAAM,wBAAwB;MAC5B;MACA;MACA;MACA,WAAW,MAAM,WAAW,KAAK,KAAK;MACtC,UAAU,cAAc,QAAQ,QAAQ;IAC1C,CAAC;IACD,KAAK,uBAAuB,EAAE,OAAO,SAAS,CAAC;IAC/C,OAAO,wBAAwB,EAAE,OAAO,UAAU,WAAW,MAAM,WAAW,KAAK,KAAK,EAAE,CAAC;IAC3F,QAAQ,wBAAwB;MAC9B;MACA;MACA,YAAY,MAAM,YAAY,KAAK,KAAK;MACxC,WAAW,MAAM,WAAW,KAAK,KAAK;IACxC,CAAC;EACH;AACF;;;AtByBA,IAAM,sBAAsB,EAAE,aAAa,KAAK;AAYhD,IAAM,wCAA6D,oBAAI,IAAI;AAAA,EACzE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAiBD,SAAS,qBAAqB,QAAgC;AAC5D,MAAI;AACF,UAAM,YAAY;AAClB,UAAM,aAAa,WAAW;AAC9B,QAAI,OAAO,eAAe,SAAU,QAAO;AAC3C,UAAM,mBAAmB,WAAW,eAAe;AACnD,QAAI,OAAO,qBAAqB,SAAU,QAAO;AACjD,UAAM,aAAa,WAAW,SAAS,QAAQ;AAC/C,QAAI,OAAO,eAAe,SAAU,QAAO;AAC3C,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AA+CA,SAAS,aAAa,OAAyB;AAC7C,MAAI,iBAAiB,sBAAsB,iBAAiB,oBAAqB,QAAO;AACxF,MAAI,gBAAgB,KAAK,EAAG,QAAO,IAAI,mBAAmB,QAAW,EAAE,OAAO,MAAM,CAAC;AACrF,MAAI,iBAAiB,KAAK,EAAG,QAAO,IAAI,oBAAoB,QAAW,EAAE,OAAO,MAAM,CAAC;AACvF,SAAO;AACT;AAMA,SAAS,sBAAsB,KAA6B;AAC1D,SAAO,OAAO,QAAQ,YAAY,IAAI,SAAS,KAAK,CAAC,IAAI,WAAW,OAAO;AAC7E;AAYO,IAAM,cAAN,MAA2C;AAAA;AAAA,EAExC;AAAA,EACS;AAAA;AAAA,EAET,uBAAuB;AAAA;AAAA,EAEvB;AAAA,EAER,YAAY,QAA8B,UAA8B,CAAC,GAAG;AAC1E,SAAK,SAAS;AACd,SAAK,oBAAoB,QAAQ;AACjC,QAAI,OAAO,QAAQ,0BAA0B,WAAW;AACtD,WAAK,wBAAwB,QAAQ;AAAA,IACvC,OAAO;AACL,YAAM,WAAW,qBAAqB,MAAM;AAM5C,WAAK,wBAAwB,aAAa,QAAQ,sCAAsC,IAAI,QAAQ;AAAA,IACtG;AAAA,EACF;AAAA,EAEA,MAAM,eAAe,MAAoD;AACvE,UAAM,gBAAgB,MAAM,KAAK,kBAAkB,KAAK,mBAAmB,KAAK,QAAQ;AAExF,QAAI;AACF,aAAO,MAAM,KAAK,eAAe,MAAM,aAAa;AAAA,IACtD,SAAS,OAAO;AAId,UAAI,iBAAiB,KAAK,EAAG,OAAM,KAAK,mBAAmB,CAAC,aAAa,CAAC;AAC1E,YAAM,aAAa,KAAK;AAAA,IAC1B;AAAA,EACF;AAAA,EAEA,MAAc,eAAe,MAA2B,eAAuD;AAC7G,WAAQ,MAAM,KAAK,OAAO,iBAAiB,OAAO;AAAA,MAChD,MAAM;AAAA,QACJ,aAAa,KAAK;AAAA,QAClB,MAAM,KAAK;AAAA,QACX,SAAS,KAAK;AAAA,QACd,QAAQ,KAAK;AAAA,QACb,KAAK,KAAK;AAAA,QACV,YAAY,KAAK,cAAc;AAAA,QAC/B;AAAA;AAAA;AAAA;AAAA,QAIA,GAAI,KAAK,mBAAmB,EAAE,kBAAkB,KAAK,iBAAiB,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,QAK3E,GAAI,KAAK,cAAc,EAAE,aAAa,KAAK,YAAY,IAAI,CAAC;AAAA,QAC5D,UAAU,KAAK;AAAA,QACf,WAAW,KAAK;AAAA,QAChB,YAAY,KAAK;AAAA,QACjB,aAAa,KAAK;AAAA,QAClB,UAAU,KAAK;AAAA,QACf,aAAa;AAAA,UACX,QAAQ,KAAK,YAAY,IAAI,CAAC,SAAS;AAAA,YACrC,aAAa,IAAI;AAAA,YACjB,OAAO,IAAI;AAAA,YACX,aAAa,IAAI;AAAA,YACjB,YAAY,IAAI;AAAA,YAChB,WAAW,IAAI;AAAA,YACf,YAAY,IAAI;AAAA,YAChB,YAAY,IAAI;AAAA,YAChB,aAAa,IAAI;AAAA,YACjB,cAAc,IAAI;AAAA,YAClB,WAAW,IAAI,aAAa;AAAA,YAC5B,MAAM,IAAI;AAAA,YACV,MAAM,IAAI;AAAA,YACV,MAAM,IAAI;AAAA,YACV,MAAM,IAAI;AAAA,YACV,SAAS,IAAI;AAAA,YACb,SAAS,IAAI;AAAA,YACb,WAAW,IAAI;AAAA,YACf,WAAW,IAAI;AAAA,YACf,kBAAkB,IAAI;AAAA,UACxB,EAAE;AAAA,QACJ;AAAA,MACF;AAAA,MACA,SAAS;AAAA,IACX,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBA,MAAc,kBAAkB,SAAoC,UAA0C;AAC5G,QAAI,CAAC,QAAS,QAAO;AAErB,QAAI,KAAK,mBAAmB;AAC1B,UAAI;AAKF,cAAM,EAAE,IAAI,IAAI,MAAM,KAAK,kBAAkB,OAAO,SAAS;AAAA,UAC3D,YAAY;AAAA,UACZ,UAAU;AAAA,QACZ,CAAC;AACD,eAAO;AAAA,MACT,SAAS,KAAK;AACZ,gBAAQ;AAAA,UACN;AAAA,UACA;AAAA,QACF;AACA,eAAO;AAAA,MACT;AAAA,IACF;AAEA,QAAI,CAAC,KAAK,sBAAsB;AAC9B,WAAK,uBAAuB;AAC5B,cAAQ;AAAA,QACN;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,mBAAmB,MAA6C;AAC5E,UAAM,SAAS,KAAK,mBAAmB,QAAQ,KAAK,KAAK,iBAAiB;AAC1E,QAAI,CAAC,OAAQ;AACb,UAAM,SAAS,KAAK,OAAO,qBAAqB;AAChD,QAAI,OAAO,WAAW,EAAG;AAEzB,UAAM,UAAU,MAAM,QAAQ,WAAW,OAAO,IAAI,CAAC,QAAQ,OAAO,GAAG,CAAC,CAAC;AACzE,YAAQ,QAAQ,CAAC,QAAQ,UAAU;AACjC,UAAI,OAAO,WAAW,YAAY;AAChC,gBAAQ;AAAA,UACN,kDAAkD,OAAO,KAAK,CAAC;AAAA,UAC/D,OAAO;AAAA,QACT;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,MAAc,qBAAqB,aAAwC;AACzE,QAAI,CAAC,KAAK,mBAAmB,OAAQ,QAAO,CAAC;AAC7C,UAAM,OAAQ,MAAM,KAAK,OAAO,iBAAiB,SAAS;AAAA,MACxD,OAAO,EAAE,aAAa,eAAe,EAAE,KAAK,KAAK,EAAE;AAAA,MACnD,QAAQ,EAAE,eAAe,KAAK;AAAA,IAChC,CAAC;AACD,WAAO,KAAK,IAAI,CAAC,QAAQ,IAAI,aAAa,EAAE,OAAO,qBAAqB;AAAA,EAC1E;AAAA,EAEA,MAAM,eAAe,UAAkD;AACrE,WAAQ,MAAM,KAAK,OAAO,iBAAiB,WAAW;AAAA,MACpD,OAAO,EAAE,SAAS;AAAA,MAClB,SAAS;AAAA,IACX,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,aAAa,OAA6C;AAC9D,UAAM,EAAE,aAAa,MAAM,QAAQ,UAAU,QAAQ,KAAK,WAAW,IAAI;AAGzE,UAAM,EAAE,OAAO,KAAK,IAAI,gBAAgB,KAAK;AAE7C,UAAM,QAA4B,EAAE,YAAY;AAChD,QAAI,KAAM,OAAM,OAAO;AAGvB,QAAI,YAAY,SAAS,SAAS,GAAG;AACnC,YAAM,SAAS,EAAE,IAAI,CAAC,GAAG,QAAQ,EAAE;AAAA,IACrC,WAAW,QAAQ;AACjB,YAAM,SAAS;AAAA,IACjB;AACA,QAAI,IAAK,OAAM,MAAM;AACrB,QAAI,WAAY,OAAM,aAAa;AACnC,QAAI,QAAQ;AACV,YAAM,UAAU,KAAK,wBAAwB,EAAE,UAAU,QAAQ,MAAM,cAAc,IAAI,EAAE,UAAU,OAAO;AAAA,IAC9G;AAKA,QAAI,oBAAoB,IAAI,GAAG;AAC7B,aAAO,EAAE,WAAW,CAAC,GAAG,OAAO,MAAM,KAAK,OAAO,iBAAiB,MAAM,EAAE,MAAM,CAAC,EAAE;AAAA,IACrF;AAEA,UAAM,CAAC,WAAW,KAAK,IAAI,MAAM,QAAQ,IAAI;AAAA,MAC3C,KAAK,OAAO,iBAAiB,SAAS;AAAA,QACpC;AAAA,QACA,SAAS;AAAA,QACT,SAAS,EAAE,WAAW,OAAO;AAAA,QAC7B;AAAA,QACA,MAAM;AAAA,MACR,CAAC;AAAA,MACD,KAAK,OAAO,iBAAiB,MAAM,EAAE,MAAM,CAAC;AAAA,IAC9C,CAAC;AAED,WAAO,EAAE,WAA0C,MAAM;AAAA,EAC3D;AAAA,EAEA,MAAM,eAAe,IAAY,MAAoD;AACnF,QAAI;AACF,aAAQ,MAAM,KAAK,OAAO,iBAAiB,OAAO;AAAA,QAChD,OAAO,EAAE,GAAG;AAAA,QACZ,MAAM;AAAA,UACJ,QAAQ,KAAK;AAAA,UACb,YAAY,KAAK;AAAA,QACnB;AAAA,QACA,SAAS;AAAA,MACX,CAAC;AAAA,IACH,SAAS,OAAO;AACd,YAAM,aAAa,KAAK;AAAA,IAC1B;AAAA,EACF;AAAA,EAEA,MAAM,eAAe,IAA2B;AAC9C,QAAI;AACJ,QAAI;AAGF,gBAAW,MAAM,KAAK,OAAO,iBAAiB,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;AAAA,IAGxE,SAAS,OAAO;AACd,YAAM,aAAa,KAAK;AAAA,IAC1B;AACA,UAAM,KAAK,mBAAmB,CAAC,SAAS,aAAa,CAAC;AAAA,EACxD;AAAA,EAEA,MAAM,mBAAmB,aAAoC;AAI3D,UAAM,iBAAiB,MAAM,KAAK,qBAAqB,WAAW;AAClE,UAAM,KAAK,OAAO,iBAAiB,WAAW,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC;AACxE,UAAM,KAAK,mBAAmB,cAAc;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,uBAAuB,IAAY,aAAuC;AAC9E,UAAM,SAAU,MAAM,KAAK,OAAO,iBAAiB,WAAW;AAAA,MAC5D,OAAO,EAAE,GAAG;AAAA;AAAA,IAEd,CAAC;AACD,WAAO,WAAW,QAAQ,OAAO,gBAAgB;AAAA,EACnD;AACF;AAsDO,SAASK,uBAAsB;AAAA,EACpC;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoC;AAClC,MAAI,CAAC,iBAAiB,CAAC,QAAQ;AAC7B,UAAM,IAAI,MAAM,uEAAuE;AAAA,EACzF;AACA,QAAM,QACJ,iBACA,IAAI,YAAY,QAAsC;AAAA,IACpD;AAAA,IACA,GAAI,OAAO,0BAA0B,YAAY,EAAE,sBAAsB,IAAI,CAAC;AAAA,EAChF,CAAC;AACH,SAAO,sBAAoB,EAAE,GAAG,gBAAgB,OAAO,eAAe,uBAAuB,CAAC;AAChG;AAEA,SAAS,qBAAqB,OAA4C;AACxE,SAAO,OAAO,OAAO,MAAM,KAAM,MAA4B,SAAS;AACxE;AAGA,SAAS,uBAAuB,OAAoC;AAClE,MAAI,qBAAqB,KAAK,GAAG;AAC/B,WAAO;AAAA,EACT;AACA,SAAO;AACT;","names":["createSitepingHandler","isRecord","hasOwn","FEEDBACK_TYPES","FEEDBACK_STATUSES","CLOSED_FEEDBACK_STATUSES","isClosedStatus","toFeedbackUpdate","StoreNotFoundError","StoreDuplicateError","hasErrorCode","isStoreNotFound","isStoreDuplicate","flattenAnnotation","CONSOLE_DIAGNOSTIC_LEVELS","z","createSitepingHandler"]}
|
|
1
|
+
{"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 BeezpingStore,\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 StoreDuplicateError,\n StoreLimitError,\n StoreNotFoundError,\n StoreValueTooLongError,\n screenshotMimeType,\n settleWithConcurrencyLimit,\n} from \"@beezping/core\";\nimport {\n type BeezpingAccessHandlerOptions,\n type BeezpingApiKeyHandlerOptions,\n type BeezpingHandler,\n type BeezpingPrincipal,\n createBeezpingHandler as createServerHandler,\n} from \"@beezping/server\";\n\nexport type {\n BeezpingStore,\n CommentCreateInput,\n CommentPayload,\n FeedbackCreateInput,\n FeedbackRecord,\n ScreenshotStorage,\n} from \"@beezping/core\";\nexport {\n flattenAnnotation,\n isStorePersistence,\n StoreDuplicateError,\n StoreLimitError,\n StoreNotFoundError,\n StorePersistenceError,\n StoreValueTooLongError,\n} from \"@beezping/core\";\nexport type { FeedbackDeleteInput, FeedbackPatchInput, GetQueryInput } from \"@beezping/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 @beezping/server as a direct dependency to type them.\nexport type {\n BeezpingAccessControl,\n BeezpingAction,\n BeezpingAuthorizationContext,\n BeezpingDeletionTarget,\n BeezpingHandler,\n BeezpingHandlerBaseOptions,\n BeezpingHttpMethod,\n BeezpingLifecycleHooks,\n BeezpingLogger,\n BeezpingPrincipal,\n BeezpingRequestContext,\n DiscordWebhookPayload,\n GenericWebhookPayload,\n SlackWebhookPayload,\n WebhookConfig,\n WebhookPayloadMap,\n WebhookType,\n} from \"@beezping/server\";\nexport { dispatchWebhook, dispatchWebhooks } from \"@beezping/server\";\n\n// ---------------------------------------------------------------------------\n// Minimal PrismaClient shape expected by this adapter\n// ---------------------------------------------------------------------------\n\n/**\n * Structural type for a Prisma model delegate (`prisma.beezpingFeedback`).\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 BeezpingPrismaClient {\n beezpingFeedback: PrismaModelDelegate;\n /**\n * Generated once the schema declares the `BeezpingComment` model\n * (`npx @beezping/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 beezpingComment?: PrismaModelDelegate | undefined;\n}\n\n// ---------------------------------------------------------------------------\n// PrismaStore — BeezpingStore implementation backed by Prisma\n// ---------------------------------------------------------------------------\n\nconst INCLUDE_ANNOTATIONS = { annotations: true } as const;\n/**\n * Read shape once the client has the `BeezpingComment` 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 `BeezpingStore`.\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 BeezpingStore {\n /** @internal */\n private prisma: BeezpingPrismaClient;\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 `BeezpingComment` 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: BeezpingPrismaClient, options: PrismaStoreOptions = {}) {\n this.prisma = prisma;\n this.screenshotStorage = options.screenshotStorage;\n const comments = prisma.beezpingComment;\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.beezpingFeedback.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.beezpingFeedback.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 beezping 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 beezping 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 \"[beezping] 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 \"[beezping] 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 `[beezping] 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.beezpingFeedback.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.beezpingFeedback.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.beezpingFeedback.count({ where }) };\n }\n\n const [feedbacks, total] = await Promise.all([\n this.prisma.beezpingFeedback.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.beezpingFeedback.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.beezpingFeedback.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.beezpingFeedback.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.beezpingFeedback.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.beezpingFeedback.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 — @beezping/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?: BeezpingPrismaClient;\n /** Abstract store — when provided, takes precedence over `prisma`. */\n store?: BeezpingStore;\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 `createBeezpingHandler` under the `apiKey` policy — every `@beezping/server` option. */\nexport interface HandlerOptions extends Omit<BeezpingApiKeyHandlerOptions, \"store\">, PrismaHandlerStoreOptions {}\n\n/** Options of `createBeezpingHandler` under a custom `access` policy (see `@beezping/server`). */\nexport interface PrismaAccessHandlerOptions<Principal extends BeezpingPrincipal>\n extends Omit<BeezpingAccessHandlerOptions<Principal>, \"store\">,\n PrismaHandlerStoreOptions {}\n\n/**\n * Setup hint for Prisma's \"table does not exist\" error (P2021) — any Beezping\n * table: a client generated after `sync` also reads `BeezpingComment`.\n */\nfunction describePrismaError(error: unknown): string | undefined {\n if (hasOwn(error, \"code\") && error.code === \"P2021\") {\n return \"A Beezping 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 Beezping API endpoint — `@beezping/server`'s\n * `createBeezpingHandler` 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/beezping/route.ts`\n * ```ts\n * import { createBeezpingHandler } from '@beezping/adapter-prisma'\n * import { prisma } from '@/lib/prisma'\n *\n * export const { GET, POST, PATCH, DELETE, OPTIONS } = createBeezpingHandler({ prisma })\n * ```\n *\n * @example With abstract store\n * ```ts\n * import { createBeezpingHandler, PrismaStore } from '@beezping/adapter-prisma'\n * import { prisma } from '@/lib/prisma'\n *\n * const store = new PrismaStore(prisma)\n * export const { GET, POST, PATCH, DELETE, OPTIONS } = createBeezpingHandler({ store })\n * ```\n */\nexport function createBeezpingHandler<Principal extends BeezpingPrincipal>(\n options: PrismaAccessHandlerOptions<Principal>,\n): BeezpingHandler;\nexport function createBeezpingHandler(options: HandlerOptions): BeezpingHandler;\n/** Options assembled at runtime, either policy. */\nexport function createBeezpingHandler<Principal extends BeezpingPrincipal>(\n options: HandlerOptions | PrismaAccessHandlerOptions<Principal>,\n): BeezpingHandler;\nexport function createBeezpingHandler<Principal extends BeezpingPrincipal>({\n prisma,\n store: providedStore,\n screenshotStorage,\n caseInsensitiveSearch,\n describeError,\n ...serverOptions\n}: HandlerOptions | PrismaAccessHandlerOptions<Principal>): BeezpingHandler {\n if (!providedStore && !prisma) {\n throw new Error(\"[beezping] createBeezpingHandler requires either `store` or `prisma`.\");\n }\n\n // Safe: the throw above guarantees at least one is defined\n const store: BeezpingStore =\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 `@beezping/*`.\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 BeezpingPosition = \"bottom-right\" | \"bottom-left\";\n\n/** Visual theme — `auto` resolves to `light` or `dark` via system preference. */\nexport type BeezpingTheme = \"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 BeezpingLocale = BuiltinLocale | (string & {});\n\n/**\n * Reasons reported through `BeezpingConfig.onSkip` — production environment,\n * viewport narrower than `minViewportWidth` (`\"mobile\"`), or server-side\n * rendering (no `window`/`document`).\n */\nexport type BeezpingSkipReason = \"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 BeezpingIdentity {\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 BeezpingDeepLinkOptions {\n /** Query parameter name carrying the feedback id. Defaults to `\"beezping\"`. */\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 BeezpingPanelActionFeedback = 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 BeezpingPanelActionContext {\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 BeezpingPanelAction}.\n *\n * Do not use this type directly — use {@link BeezpingPanelAction}.\n */\nexport interface BeezpingPanelActionBase {\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: BeezpingPanelActionFeedback) => boolean) | undefined;\n}\n\n/** A panel action rendered as a button that runs host code. */\nexport interface BeezpingPanelButtonAction extends BeezpingPanelActionBase {\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: BeezpingPanelActionFeedback, context: BeezpingPanelActionContext) => 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 BeezpingPanelLinkAction extends BeezpingPanelActionBase {\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: BeezpingPanelActionFeedback) => 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 BeezpingPanelAction = BeezpingPanelButtonAction | BeezpingPanelLinkAction;\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 BeezpingHeadersOption =\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 BeezpingConfig}, the\n * discriminated union that adds the mode-specific fields.\n */\nexport interface BeezpingBaseConfig {\n /** Required — project identifier used to scope feedbacks */\n projectName: string;\n /** FAB position — defaults to 'bottom-right' */\n position?: BeezpingPosition | 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?: BeezpingTheme | 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?: BeezpingLocale | 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 `@beezping/widget` so the\n * dynamic import always resolves; you don't need to install anything extra.\n *\n * **Masking sensitive elements:** add `data-beezping-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 Beezping'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: BeezpingSkipReason) => 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 `beezping`.\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 | BeezpingDeepLinkOptions | 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/guidomodarelli/beezping/issues/85 for tracking a\n * future enhancement that propagates identity updates without a remount.\n */\n identity?: BeezpingIdentity | 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 BeezpingPanelAction}.\n */\n panelActions?: readonly BeezpingPanelAction[] | 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 `BeezpingError` (or a subclass:\n * `BeezpingNetworkError`, `BeezpingValidationError`, `BeezpingAuthError`)\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. `@beezping/adapter-prisma` request handlers).\n */\nexport interface BeezpingHttpConfig extends BeezpingBaseConfig {\n /** HTTP endpoint that receives feedbacks (e.g. '/api/beezping'). */\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?: BeezpingHeadersOption | 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 `BeezpingStore` directly in the\n * browser, no server needed (demos, prototypes, localStorage persistence).\n */\nexport interface BeezpingStoreConfig extends BeezpingBaseConfig {\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: BeezpingStore;\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 Beezping 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 BeezpingConfig = BeezpingHttpConfig | BeezpingStoreConfig;\n\n/** Instance returned by initBeezping() with lifecycle methods. */\nexport interface BeezpingInstance {\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 BeezpingPublicEvents>(event: K, listener: BeezpingPublicEventListener<K>) => BeezpingUnsubscribe;\n /** Unsubscribe from a public widget event */\n off: <K extends keyof BeezpingPublicEvents>(event: K, listener: BeezpingPublicEventListener<K>) => void;\n}\n\n/** Listener signature for a single `BeezpingPublicEvents` key. */\nexport type BeezpingPublicEventListener<K extends keyof BeezpingPublicEvents> = (\n ...args: BeezpingPublicEvents[K]\n) => void;\n\n/** Disposer returned by `BeezpingInstance.on` — call once to detach the listener. */\nexport type BeezpingUnsubscribe = () => void;\n\n/** Events exposed to consumers via BeezpingInstance.on / .off */\nexport interface BeezpingPublicEvents {\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 * `BeezpingConfig.onError` — a `BeezpingError` 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 `BeezpingConfig.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 * `BeezpingConfig.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 * `BeezpingStore.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 `BeezpingStore.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 `BeezpingStore.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 `BeezpingStore.getFeedbacks`. */\nexport interface FeedbackPage {\n feedbacks: FeedbackRecord[];\n total: number;\n}\n\n/**\n * Abstract storage interface for Beezping.\n *\n * Any adapter (Prisma, Drizzle, raw SQL, localStorage, etc.) implements this\n * interface. The HTTP handler and widget `StoreClient` operate against\n * `BeezpingStore`, 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 BeezpingStore {\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 * `BeezpingConfig.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 `BeezpingConfig`. 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 `@beezping/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 * `@beezping/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 BeezpingCapabilities {\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 `@beezping/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 `@beezping/server` — absent from servers that predate it. */\n capabilities?: BeezpingCapabilities | undefined;\n /** Always sent by `@beezping/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 * `@beezping/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 = `beezping/${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 * createBeezpingHandler({ 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"]}
|