/**
* Pluggable storage for feedback screenshots.
*
* `adapter-prisma` and `adapter-drizzle` accept an optional
* `screenshotStorage` config. When provided, the adapter forwards the
* widget-supplied data URL to `upload()` and persists the returned URL on
* `Feedback.screenshotUrl`. When not provided, the adapter falls back to
* inline base64 (with a one-time warn) — fine for dev and small
* deployments, a footgun for production Postgres.
*
* `@siteping/screenshot-storage` implements it over any S3-compatible
* bucket (AWS S3, Cloudflare R2, Backblaze B2, MinIO…), Cloudflare Images,
* a database table, the local filesystem or memory. Implement it yourself
* for anything else.
*
* @example
* ```ts
* // Minimal S3 implementation
* import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
*
* const s3 = new S3Client({ region: "eu-west-3" });
* const screenshotStorage: ScreenshotStorage = {
* async upload(dataUrl, { mimeType }) {
* const body = Buffer.from(dataUrl.slice(dataUrl.indexOf(",") + 1), "base64");
* const key = `siteping/${crypto.randomUUID()}`; // fresh per upload, see URL ownership
* await s3.send(new PutObjectCommand({
* Bucket: "my-bucket", Key: key, Body: body, ContentType: mimeType,
* }));
* return { url: `https://cdn.example.com/${key}` };
* },
* };
*
* createSitepingHandler({ prisma, screenshotStorage });
* ```
*/
export interface ScreenshotStorage {
/**
* Persist a base64 data URL and return the URL the widget will use as
* `
`. Implementations decide the underlying storage and any
* post-processing (resize, virus scan, content-type sniff).
*
* Adapters call this synchronously inside `createFeedback` — keep it
* fast or move to a queue if needed.
*
* `ctx.feedbackId` identifies the upload. Adapters that upload before the
* record exists pass the *client-generated* `clientId` (Prisma); the
* Drizzle adapter passes the server-generated id the create attempt will
* insert the record under.
*
* **URL ownership:** every call must return a URL no other call returns —
* key the object by a fresh random value (`crypto.randomUUID()`, as the
* example does), never by `ctx.feedbackId` alone nor by a content hash.
* Adapters treat each URL as the property of the one record that stores it
* and may pass it to `delete` once that record is deleted, or once its
* create lost a race: two submissions of one `clientId` both upload, and
* the object of the one that is not stored is deleted. Under Prisma,
* `ctx.feedbackId` is that client-supplied `clientId`, so an object keyed
* by it is rewritten by every replay — a retry, or anyone who learns the
* id — and a URL shared by several records lets deleting one remove the
* object another still points at.
*
* **Security note:** treat `ctx.feedbackId` as attacker-controlled:
* sanitize before using it in filesystem paths or object keys, even though
* server adapters validate its shape upstream.
*/
upload(dataUrl: string, ctx: {
feedbackId: string;
mimeType: string;
}): Promise<{
url: string;
}>;
/**
* Optional cleanup hook called when the feedback is deleted, and for an
* object uploaded by a create whose record was not stored: one rejected
* because its `clientId` is already stored, or — in adapters that key
* uploads per attempt, like Drizzle — one whose insert failed. An object a
* stored row still references (a deterministic key reused by the replay)
* is kept. Receives only URLs returned by {@link ScreenshotStorage.upload}.
* Adapters call this best-effort and swallow errors — orphaned objects are
* preferred over failed deletes.
*/
delete?: (url: string) => Promise;
}
/**
* Most `ScreenshotStorage.delete` calls one cleanup keeps in flight — a
* project delete may free thousands of objects, and firing them all at once
* can exhaust sockets (one per request with `fetch`) or memory and trip
* object-store rate limits. It stays under the 50 sockets the AWS SDK opens
* per client by default, yet a project delete, which waits for its cleanup,
* is not serialised: 1,000 objects take 32 rounds. Adapters run their
* deletes through `settleWithConcurrencyLimit` with this bound.
*/
export declare const SCREENSHOT_DELETE_CONCURRENCY = 32;
/**
* MIME type an adapter reports to {@link ScreenshotStorage.upload}: the one
* an image data URL declares (`data:image/png;base64,…` → `image/png`),
* limited to the JPEG, PNG and WebP the HTTP schema accepts. Stores are
* public and may be fed unvalidated data URLs, and an `image/svg+xml` label
* would make the stored object script-capable when served inline. Anything
* else — including a data URL that declares no type — reports JPEG, the
* widget's capture format.
*/
export declare function screenshotMimeType(dataUrl: string): string;