import { FeedbackPayload, SitepingStore, ScreenshotStorage, CommentCreateInput, CommentRecord, FeedbackCreateInput, FeedbackRecord, FeedbackQuery, FeedbackPage, FeedbackUpdateInput } from './siteping-core.js'; export { CommentCreateInput, CommentPayload, FeedbackCreateInput, FeedbackRecord, ScreenshotStorage, SitepingStore, StoreDuplicateError, StoreLimitError, StoreNotFoundError, StorePersistenceError, StoreValueTooLongError, flattenAnnotation, isStorePersistence } from './siteping-core.js'; import { SitepingApiKeyHandlerOptions, SitepingPrincipal, SitepingAccessHandlerOptions, SitepingHandler } from '@siteping/server'; export { DiscordWebhookPayload, FeedbackDeleteInput, FeedbackPatchInput, GenericWebhookPayload, GetQueryInput, SitepingAccessControl, SitepingAction, SitepingAuthorizationContext, SitepingDeletionTarget, SitepingHandler, SitepingHandlerBaseOptions, SitepingHttpMethod, SitepingLifecycleHooks, SitepingLogger, SitepingPrincipal, SitepingRequestContext, SlackWebhookPayload, WebhookConfig, WebhookPayloadMap, WebhookType, dispatchWebhook, dispatchWebhooks } from '@siteping/server'; /** * @deprecated The create wire shape is core's `FeedbackPayload` — import * that instead. This alias is kept for one release cycle. */ type FeedbackCreateSchemaInput = FeedbackPayload; /** * Structural type for a Prisma model delegate (`prisma.sitepingFeedback`). * * Arguments are kept `unknown` so any Prisma version's generated client * satisfies the constraint; the adapter assembles type-safe payloads * internally before forwarding them. * * Members use **method syntax** (`create(args)`) rather than function-property * syntax (`create: (args) => ...`) on purpose: under `strictFunctionTypes`, * function-property parameters are checked *contravariantly*, so a real * generated delegate — whose `create(args: SpecificArgs)` takes a type narrower * than `unknown` — would fail to assign to `PrismaModelDelegate`. Method * signatures are checked *bivariantly* on parameters, which is exactly what we * want for structurally matching a third-party generated client (#99). */ interface PrismaModelDelegate { create(args: unknown): Promise; findMany(args: unknown): Promise; findUnique(args: unknown): Promise; update(args: unknown): Promise; delete(args: unknown): Promise; deleteMany(args: unknown): Promise; count(args: unknown): Promise; } /** * Minimal Prisma client shape expected by this adapter. * Consumers pass their own `PrismaClient` instance at runtime — this interface * defines the subset of methods the adapter actually uses, so it can be * referenced in handler option types without importing `@prisma/client`. */ interface SitepingPrismaClient { sitepingFeedback: PrismaModelDelegate; /** * Generated once the schema declares the `SitepingComment` model * (`npx @siteping/cli sync`). Optional, so a client generated from an older * schema keeps type-checking: `PrismaStore` then has no threads and the * handler answers comment writes with 501. */ sitepingComment?: PrismaModelDelegate | undefined; } /** * Options accepted by `PrismaStore`. */ interface PrismaStoreOptions { /** * When `true`, the `?search=` filter is built with `mode: "insensitive"` * (case-insensitive across all letters, including non-ASCII). * * When `false`, the filter is built without `mode` — uses each database's * default `LIKE` semantics (case-insensitive ASCII on SQLite by default; * case-sensitive on PostgreSQL with the standard `LIKE` operator; * collation-driven on MySQL and SQL Server). * * When omitted, the value is auto-detected from the Prisma client's active * provider: providers whose generated client exposes `mode?: QueryMode` * (`postgresql`, `mongodb`, `cockroachdb`) get `true`; others (`mysql`, * `sqlite`, `sqlserver`) get `false`. Unknown / undetectable providers * default to `false` — `contains` without `mode` works on every provider; * `mode: "insensitive"` throws on MySQL/SQLite/SQL Server, so the safer * default is to omit it. */ caseInsensitiveSearch?: boolean; /** * Optional storage backend for screenshots. Without it, the data URL is * persisted inline on `Feedback.screenshotUrl` with a one-time warn. */ screenshotStorage?: ScreenshotStorage | undefined; } /** * Prisma-backed implementation of `SitepingStore`. * * Wraps a PrismaClient to satisfy the abstract store interface. * * Pass `screenshotStorage` to externalise screenshots (S3, R2, B2, …) — the * widget's data URL is uploaded and only the returned URL is persisted, so * the database stays small. Without `screenshotStorage`, the data URL is * persisted inline (logged once on first use as a heads-up). */ declare class PrismaStore implements SitepingStore { /** @internal */ private prisma; private readonly screenshotStorage; /** Module-level flag would leak across PrismaStore instances in tests; use per-instance. */ private inlineFallbackWarned; /** @internal */ private caseInsensitiveSearch; /** Read shape of every feedback query — with the thread once the client has the comment model. */ private readonly include; /** * Add a comment to a feedback's thread — defined only when the client has * the `SitepingComment` delegate, unless a subclass defines its own. Without * it the store has no threads, and the handler answers comment writes with * 501 instead of every post failing with a Prisma error. Declared, not a * field: a field would set it on every instance, hiding a subclass's method. */ readonly addComment?: (feedbackId: string, data: CommentCreateInput) => Promise; /** Delete one comment from a feedback's thread — defined under the same condition as {@link addComment}. */ readonly deleteComment?: (feedbackId: string, commentId: string) => Promise; constructor(prisma: SitepingPrismaClient, options?: PrismaStoreOptions); createFeedback(data: FeedbackCreateInput): Promise; /** * Drop the screenshot uploaded by a replay of a stored clientId — unless the * stored row references it. Uploads are keyed on clientId, so with a * deterministic key (`feedback/${feedbackId}.jpg`) the replay wrote to the * very object the existing row points at: deleting it would strip the * surviving feedback of its screenshot. If the lookup itself fails the * object is kept — an orphan beats data loss. Other insert failures never * get here: with such a key, a retry on another instance may have rewritten * the object and not yet inserted the row that will point at it. */ private discardUnreferencedUpload; private insertFeedback; /** * Resolve the value to persist on `Feedback.screenshotUrl`. * * - No data URL → null * - Storage configured → upload, return remote URL. Upload failures * persist `null` (drop the screenshot) rather than silently inlining * the data URL — an inline fallback would bloat Postgres unnoticed * during a multi-minute storage outage. The feedback message itself is * preserved; only the screenshot is missing, and the warn surfaces it. * - No storage → inline base64, with a one-time warn so prod operators * notice the footgun. * * Operators who prefer the legacy inline-on-failure behaviour can wrap * their `ScreenshotStorage.upload` with their own catch + return the * data URL — the adapter treats whatever the storage returns as final. */ private persistScreenshot; /** * Best-effort cleanup of stored screenshots through `ScreenshotStorage.delete` * — the hook the interface documents for feedback deletion. Failures are * logged and swallowed: an orphaned object is preferable to a delete that * reports failure after the row is already gone. Inline `data:` URLs and * stores without a `delete` hook are skipped. * * Deletes run through a pool of at most {@link SCREENSHOT_DELETE_CONCURRENCY} * concurrent calls: a project delete may free thousands of objects, and one * socket each at once would exhaust the file descriptors of a serverless * function, orphaning most of them. */ private discardScreenshots; /** URLs of the stored screenshots in `projectName` — only fetched when a `delete` hook can use them. */ private storedScreenshotUrls; findByClientId(clientId: string): Promise; getFeedbacks(query: FeedbackQuery): Promise; updateFeedback(id: string, data: FeedbackUpdateInput): Promise; /** * Insert a comment. A replayed `clientId` returns the stored comment, looked * up first so a replay never runs into the thread cap; a concurrent replay * that wins the insert race surfaces as P2002 and is read back the same * way. The `connect` turns a missing feedback into Prisma's P2025 on every * provider, rather than each database's own foreign-key error. The cap on * `client` comments is a count before the insert, so posts racing for the * last free slot may overshoot it; a `team` comment skips it. */ private insertComment; private findComment; /** * Delete one comment. Both ids go in one `deleteMany`, so a comment of * another thread and an unknown one are the same zero-row miss. */ private removeComment; deleteFeedback(id: string): Promise; deleteAllFeedbacks(projectName: string): Promise; /** * Verify that a feedback record with `id` belongs to `projectName`. * Returns `true` when the record exists and matches, `false` otherwise. */ verifyProjectOwnership(id: string, projectName: string): Promise; } /** How the handler reaches its data: a Prisma client, or any store. */ interface PrismaHandlerStoreOptions { /** Prisma client — used when `store` is not provided. Wrapped in a `PrismaStore` internally. */ prisma?: SitepingPrismaClient; /** Abstract store — when provided, takes precedence over `prisma`. */ store?: SitepingStore; /** * Optional storage backend for screenshots. Used only with `prisma` * (ignored when a custom `store` is passed — that store is responsible * for its own screenshot strategy). Without a storage, the data URL is * persisted inline on `Feedback.screenshotUrl` with a one-time warn. */ screenshotStorage?: ScreenshotStorage; /** * Override case-insensitive search behaviour for the built-in `PrismaStore`. * * Only applied when `prisma` is provided (not when a custom `store` is * passed). See `PrismaStoreOptions.caseInsensitiveSearch` for details on * auto-detection and per-provider semantics. */ caseInsensitiveSearch?: boolean; } /** Options of `createSitepingHandler` under the `apiKey` policy — every `@siteping/server` option. */ interface HandlerOptions extends Omit, PrismaHandlerStoreOptions { } /** Options of `createSitepingHandler` under a custom `access` policy (see `@siteping/server`). */ interface PrismaAccessHandlerOptions extends Omit, "store">, PrismaHandlerStoreOptions { } /** * Create request handlers for the Siteping API endpoint — `@siteping/server`'s * `createSitepingHandler` over a Prisma-backed store, with every server option. * * Accepts either a `store` (abstract) or a `prisma` client (backwards compatible). * When `prisma` is provided without `store`, it is wrapped in a `PrismaStore`. * * **Rate limiting** is not handled by this library. Apply rate limiting at the * framework or reverse-proxy level (e.g. Next.js middleware, Nginx, Cloudflare). * The POST endpoint in particular should be rate-limited to prevent abuse, since * the widget typically calls it from unauthenticated browser contexts. * * @example Next.js App Router — `app/api/siteping/route.ts` * ```ts * import { createSitepingHandler } from '@siteping/adapter-prisma' * import { prisma } from '@/lib/prisma' * * export const { GET, POST, PATCH, DELETE, OPTIONS } = createSitepingHandler({ prisma }) * ``` * * @example With abstract store * ```ts * import { createSitepingHandler, PrismaStore } from '@siteping/adapter-prisma' * import { prisma } from '@/lib/prisma' * * const store = new PrismaStore(prisma) * export const { GET, POST, PATCH, DELETE, OPTIONS } = createSitepingHandler({ store }) * ``` */ declare function createSitepingHandler(options: PrismaAccessHandlerOptions): SitepingHandler; declare function createSitepingHandler(options: HandlerOptions): SitepingHandler; /** Options assembled at runtime, either policy. */ declare function createSitepingHandler(options: HandlerOptions | PrismaAccessHandlerOptions): SitepingHandler; export { type FeedbackCreateSchemaInput, type HandlerOptions, type PrismaAccessHandlerOptions, type PrismaModelDelegate, PrismaStore, type PrismaStoreOptions, type SitepingPrismaClient, createSitepingHandler };