@siteping/server 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,488 @@
1
+ import { Prettify, Serialized, FeedbackRecord, CommentResponse, SitepingStore, FeedbackCreateInput, CommentPayload, FeedbackStatus, FeedbackType } from './siteping-core.js';
2
+ export { CommentCreateInput, CommentPayload, FeedbackCreateInput, FeedbackRecord, SitepingStore } from './siteping-core.js';
3
+
4
+ /**
5
+ * Outgoing webhook notifications for newly-created feedbacks.
6
+ *
7
+ * Plug a Slack, Discord, or generic HTTP endpoint into `createSitepingHandler`
8
+ * to receive a payload whenever a feedback is successfully persisted. Webhooks
9
+ * are dispatched as fire-and-forget (`void Promise.all(...)`) so a slow or
10
+ * down receiver never blocks the client response — the feedback is already in
11
+ * the DB by the time we dial out.
12
+ *
13
+ * - **Type-specific formatting**: Slack uses `{ text, blocks }`, Discord uses
14
+ * `{ content, embeds }`, generic posts the record as JSON (minus `clientId`).
15
+ * - **Untrusted input**: `message` and `authorName` come from anonymous
16
+ * visitors. Slack text is escaped, Discord markdown is escaped and its
17
+ * mention parsing disabled, so a public feedback form can never be turned
18
+ * into a channel-wide ping or a disguised link.
19
+ * - **Timeout**: 5s by default (overridable per webhook).
20
+ * - **Error handling**: `config.onError(err, feedback.id)` is invoked when
21
+ * present; otherwise we log a one-liner to `console.warn` so the issue is
22
+ * surfaced without crashing the request.
23
+ */
24
+
25
+ /** Supported webhook integrations — drives the JSON body shape. */
26
+ type WebhookType = "slack" | "discord" | "generic";
27
+ /**
28
+ * Outgoing webhook configuration.
29
+ *
30
+ * - `url` — required, the HTTPS endpoint to POST to.
31
+ * - `type` — payload format. Defaults to `"generic"` (raw JSON).
32
+ * - `headers` — extra headers merged on top of `Content-Type: application/json`.
33
+ * Useful for signed-payload schemes (`X-Signature`, bearer tokens, …).
34
+ * - `timeoutMs` — abort the fetch after this many ms: a positive integer of
35
+ * at most 2,147,483,647, the longest delay a timer holds. Defaults to 5000.
36
+ * - `onError` — invoked with the underlying error and the feedback id when
37
+ * the dispatch fails (network error, non-2xx, timeout). The webhook is
38
+ * fire-and-forget, so this is your only chance to observe failures. An
39
+ * async one is awaited: the delivery `waitUntil` holds covers it, and its
40
+ * rejection is reported like a throw, never left unhandled.
41
+ */
42
+ interface WebhookConfig {
43
+ url: string;
44
+ type?: WebhookType;
45
+ headers?: Record<string, string>;
46
+ timeoutMs?: number;
47
+ onError?: (err: Error, feedbackId: string) => void | Promise<void>;
48
+ }
49
+ /** Block Kit envelope used by Slack incoming webhooks. */
50
+ interface SlackWebhookPayload {
51
+ text: string;
52
+ blocks: ReadonlyArray<SlackHeaderBlock | SlackSectionBlock | SlackContextBlock>;
53
+ }
54
+ interface SlackHeaderBlock {
55
+ type: "header";
56
+ text: {
57
+ type: "plain_text";
58
+ text: string;
59
+ emoji: true;
60
+ };
61
+ }
62
+ interface SlackSectionBlock {
63
+ type: "section";
64
+ text: {
65
+ type: "mrkdwn";
66
+ text: string;
67
+ };
68
+ }
69
+ interface SlackContextBlock {
70
+ type: "context";
71
+ elements: ReadonlyArray<{
72
+ type: "mrkdwn";
73
+ text: string;
74
+ }>;
75
+ }
76
+ /** Embed envelope used by Discord incoming webhooks. */
77
+ interface DiscordWebhookPayload {
78
+ content: string;
79
+ embeds: ReadonlyArray<{
80
+ title: string;
81
+ description: string;
82
+ color: number;
83
+ fields: ReadonlyArray<{
84
+ name: string;
85
+ value: string;
86
+ inline: boolean;
87
+ }>;
88
+ timestamp: string;
89
+ }>;
90
+ /**
91
+ * Mention parsing is switched off: `content` carries end-user text, so an
92
+ * author called `@everyone` must render as text, never as a notification.
93
+ */
94
+ allowed_mentions: {
95
+ parse: ReadonlyArray<"roles" | "users" | "everyone">;
96
+ };
97
+ }
98
+ /**
99
+ * Generic webhook body — the stored record as the receiver parses it: dates
100
+ * are ISO strings, and `clientId` is stripped like on every other output, on
101
+ * the record and on each comment of its thread. It is the browser-local
102
+ * dedup secret, and the POST replay path hands the full record to whoever
103
+ * presents it.
104
+ */
105
+ type GenericWebhookPayload = Prettify<Serialized<Omit<FeedbackRecord, "clientId" | "comments">> & {
106
+ comments?: CommentResponse[] | undefined;
107
+ }>;
108
+ /** Mapping from webhook type to its concrete body shape. */
109
+ interface WebhookPayloadMap {
110
+ slack: SlackWebhookPayload;
111
+ discord: DiscordWebhookPayload;
112
+ generic: GenericWebhookPayload;
113
+ }
114
+ /**
115
+ * Dispatch a single webhook. Fire-and-forget: never throws, never rejects.
116
+ *
117
+ * - Builds the type-specific payload.
118
+ * - POSTs with an `AbortSignal` timeout.
119
+ * - On any error (network, non-2xx, timeout, exception), invokes
120
+ * `config.onError(err, feedbackId)` if provided; otherwise logs a one-liner.
121
+ * A `timeoutMs` no timer holds is such an error: nothing is sent.
122
+ */
123
+ declare function dispatchWebhook(config: WebhookConfig, feedback: FeedbackRecord): Promise<void>;
124
+ /**
125
+ * Dispatch every configured webhook in parallel. Awaiting the returned promise
126
+ * lets tests synchronize on completion, but production callers should drop the
127
+ * promise on the floor (`void dispatchWebhooks(...)`) so the HTTP response
128
+ * isn't held back on slow receivers.
129
+ */
130
+ declare function dispatchWebhooks(configs: readonly WebhookConfig[], feedback: FeedbackRecord): Promise<void>;
131
+
132
+ /** HTTP methods served by `createSitepingHandler`. */
133
+ type SitepingHttpMethod = "GET" | "POST" | "PATCH" | "DELETE" | "OPTIONS";
134
+ /** What a request does, as `SitepingAccessControl.authorize` sees it. */
135
+ type SitepingAction = "create" | "list" | "update" | "delete" | "deleteAll" | "createComment" | "deleteComment";
136
+ /** The request being served, and who sent it. */
137
+ interface SitepingRequestContext<Principal> {
138
+ request: Request;
139
+ /** Whoever `access.authenticate` resolved — always `null` under the `apiKey` policy. */
140
+ principal: Principal;
141
+ }
142
+ /** What `SitepingAccessControl.authorize` decides about. */
143
+ interface SitepingAuthorizationContext<Principal> extends SitepingRequestContext<Principal> {
144
+ action: SitepingAction;
145
+ /** Project the request targets: the body's for writes, the query's for reads. */
146
+ projectName: string;
147
+ /** Target record of `update` and `delete`; the feedback whose thread `createComment` and `deleteComment` target. */
148
+ feedbackId?: string;
149
+ /**
150
+ * Target comment of `deleteComment` — absent on a `dryRun`, whose answer
151
+ * covers the feedback's whole thread.
152
+ */
153
+ commentId?: string;
154
+ /**
155
+ * `true` when nothing is being done: the handler fills in the
156
+ * `permissions` of a response — see `SitepingAccessControl.authorize`.
157
+ * `request` is then the response's own (a `GET`, `POST` or `PATCH`),
158
+ * whatever `action` asks about: decide by `action`, not `request.method`.
159
+ */
160
+ dryRun?: boolean;
161
+ }
162
+ /**
163
+ * Who `access.authenticate` may resolve: a user object, an id, a token's
164
+ * claims. Never a boolean — a `false` check would read as a signed-in caller.
165
+ */
166
+ type SitepingPrincipal = object | string | number;
167
+ /**
168
+ * A custom access policy — sessions, JWTs, roles — resolved from the standard
169
+ * `Request` (a cookie, a header, a proxy identity…), in place of `apiKey`.
170
+ *
171
+ * - `authenticate` resolving a falsy value (`null`, `undefined`, `""`, `0`,
172
+ * or `false` from plain JavaScript) → 401, on every method but `OPTIONS`.
173
+ * - `authorize` resolving `false` → 403. Defaults to allowing every
174
+ * authenticated principal. When set, the store must implement
175
+ * `verifyProjectOwnership`: PATCH/DELETE address records by id, and the
176
+ * check is what binds the authorized `projectName` to the record. It is
177
+ * also asked, with `dryRun: true`, to fill in the `permissions` of every
178
+ * feedback a response carries — `update`, `delete`, `createComment` and
179
+ * `deleteComment` (without a `commentId`) per feedback, plus `deleteAll`
180
+ * once per list — so clients hide what it would refuse. A dry run that
181
+ * throws refuses that permission (logged) rather than failing the
182
+ * response. Keep it fast: a page of 50 feedbacks takes 201 dry runs, 8
183
+ * at a time, sharing the one `request` — cache lookups per request. That
184
+ * request is the response's whatever the action, and `authenticate` is
185
+ * not asked again: a principal it admits on some methods only needs
186
+ * `authorize` to refuse it the other actions.
187
+ * - `canReadAuthorEmail` decides whether responses include `authorEmail`
188
+ * (reviewer PII), on feedbacks and their comments — the list, the PATCH
189
+ * answer and the POST answer alike. Only `true` includes it: without the
190
+ * callback, every response blanks it. A policy that serves anonymous
191
+ * visitors resolves a principal for them too, so being authenticated says
192
+ * nothing about who may read reviewers' emails.
193
+ * - `canCommentAsTeam` decides whether a comment that asks for the `team`
194
+ * role keeps it; otherwise it is stamped `client`. Defaults to the
195
+ * principal's `canReadAuthorEmail` answer (whoever may read reviewer
196
+ * emails is on the project side), so a policy that sets neither never
197
+ * lets a caller speak as the team. Once set, anything but `true`
198
+ * (`undefined` from plain JavaScript included) refuses.
199
+ *
200
+ * A throw from any of them answers a logged 500.
201
+ */
202
+ interface SitepingAccessControl<Principal extends SitepingPrincipal> {
203
+ authenticate(request: Request): Principal | null | undefined | Promise<Principal | null | undefined>;
204
+ authorize?(context: SitepingAuthorizationContext<Principal>): boolean | Promise<boolean>;
205
+ canReadAuthorEmail?(principal: Principal): boolean | Promise<boolean>;
206
+ canCommentAsTeam?(principal: Principal): boolean | Promise<boolean>;
207
+ }
208
+ /**
209
+ * What a DELETE removes: one record, or a whole project (`deleteAll`) —
210
+ * never a comment, whose deletion runs no hook.
211
+ */
212
+ type SitepingDeletionTarget = {
213
+ kind: "single";
214
+ id: string;
215
+ projectName: string;
216
+ } | {
217
+ kind: "project";
218
+ projectName: string;
219
+ };
220
+ /**
221
+ * Side effects around persistence — issue trackers, chat notifications,
222
+ * search indexes, audit logs. Hooks receive the stored record, `clientId` and
223
+ * `authorEmail` included.
224
+ *
225
+ * `onCreated`, `onUpdated` and `onDeleted` are awaited before the response
226
+ * (so serverless runtimes do not freeze them mid-flight); a throw is logged
227
+ * and never fails the request — the write already happened. `onCreated`
228
+ * runs once per inserted feedback, never for a replayed `clientId`.
229
+ * `onDeleting` runs before the delete: throw to abort it (the record is kept
230
+ * and the request answers 502), e.g. when a resource tied to the feedback
231
+ * could not be cleaned up and the delete must be retried.
232
+ */
233
+ interface SitepingLifecycleHooks<Principal> {
234
+ onCreated?(feedback: FeedbackRecord, context: SitepingRequestContext<Principal>): void | Promise<void>;
235
+ onUpdated?(feedback: FeedbackRecord, context: SitepingRequestContext<Principal>): void | Promise<void>;
236
+ onDeleting?(target: SitepingDeletionTarget, context: SitepingRequestContext<Principal>): void | Promise<void>;
237
+ onDeleted?(target: SitepingDeletionTarget, context: SitepingRequestContext<Principal>): void | Promise<void>;
238
+ }
239
+ /**
240
+ * Where the handler reports unexpected failures. Defaults to `console.error`.
241
+ * A logger that throws, or returns a promise that rejects (a log shipper
242
+ * down), falls back to `console.error`: it never fails the request, nor
243
+ * leaves a rejection unhandled.
244
+ *
245
+ * The arguments come in console's and winston's order. pino's `error` takes
246
+ * the context first and type-checks here anyway, then drops the context:
247
+ * adapt it — `{ error: (message, { error, ...context }) => log.error({ err: error, ...context }, message) }`.
248
+ */
249
+ interface SitepingLogger {
250
+ error(message: string, context: Record<string, unknown>): void | Promise<void>;
251
+ }
252
+ /** Options shared by both access policies. */
253
+ interface SitepingHandlerBaseOptions<Principal> {
254
+ /** Persistence backend — any `SitepingStore` (Prisma, Drizzle, memory, your own). */
255
+ store: SitepingStore;
256
+ /**
257
+ * Allowed CORS origins (exact match) — when set, only these origins get CORS
258
+ * headers. When unset, no CORS headers are emitted and browsers block
259
+ * cross-origin widgets. Under `access`, it also bounds which origins may
260
+ * send POST/PATCH/DELETE.
261
+ */
262
+ allowedOrigins?: ReadonlyArray<string> | undefined;
263
+ /**
264
+ * Largest request body accepted, in bytes — 4 MiB by default, twice the
265
+ * largest submission the validation accepts. A longer body answers 413:
266
+ * refused on its `Content-Length`, or once that many bytes have streamed
267
+ * in, before any of it is parsed.
268
+ */
269
+ maxBodyBytes?: number | undefined;
270
+ /**
271
+ * Outgoing webhooks fired after a feedback is successfully persisted.
272
+ *
273
+ * Pass a single config or an array — every entry receives a POST with a
274
+ * type-specific payload (Slack, Discord, or generic JSON). Dispatch is
275
+ * fire-and-forget: the HTTP response is returned to the widget before
276
+ * webhook delivery completes, so a slow receiver never blocks the client.
277
+ * Provide `onError` on each config to observe failures.
278
+ */
279
+ webhooks?: WebhookConfig | ReadonlyArray<WebhookConfig>;
280
+ /**
281
+ * Hand the webhook deliveries to the runtime, which may otherwise freeze
282
+ * or cancel them once the response is sent (serverless functions, edge
283
+ * workers): Next.js `after`, `waitUntil` from `@vercel/functions` or from
284
+ * `cloudflare:workers` — a standalone function, not an unbound method.
285
+ * Called once per notified feedback with a promise that never rejects; a
286
+ * throw is logged and the delivery still runs.
287
+ */
288
+ waitUntil?: ((promise: Promise<unknown>) => void) | undefined;
289
+ /**
290
+ * Rewrite the validated input before it is stored: impose the project or
291
+ * the author from the session, redact secrets from free text, drop fields
292
+ * you do not keep. Runs before `access.authorize`, which sees the effective
293
+ * `projectName`. A throw answers a logged 500 and stores nothing.
294
+ */
295
+ beforeCreate?(input: FeedbackCreateInput, context: SitepingRequestContext<Principal>): FeedbackCreateInput | Promise<FeedbackCreateInput>;
296
+ /**
297
+ * `beforeCreate` for the replies of a thread: rewrite the validated comment
298
+ * before it is stored — impose the author from the session, redact secrets
299
+ * from its body, pin the project. Runs before `access.authorize`, which
300
+ * sees the effective `projectName` and `feedbackId`, so before the feedback
301
+ * is looked up too: it may run for a feedback that turns out not to exist.
302
+ * The `authorRole` it returns is still a claim: `team` is kept only for a
303
+ * caller the policy vouches for. A throw answers a logged 500 and stores
304
+ * nothing.
305
+ */
306
+ beforeComment?(input: CommentPayload, context: SitepingRequestContext<Principal>): CommentPayload | Promise<CommentPayload>;
307
+ /**
308
+ * Transform each record right before it is serialized in a response, e.g.
309
+ * read-time redaction. `clientId` is stripped, and `authorEmail` blanked
310
+ * when the requester may not read it, afterwards — on the record and on
311
+ * each comment of its thread.
312
+ */
313
+ presentFeedback?(feedback: FeedbackRecord, context: SitepingRequestContext<Principal>): FeedbackRecord;
314
+ /** Lifecycle side effects — see `SitepingLifecycleHooks`. */
315
+ hooks?: SitepingLifecycleHooks<Principal>;
316
+ /** Where unexpected failures are reported. Defaults to `console.error`. */
317
+ logger?: SitepingLogger;
318
+ /**
319
+ * Map an unexpected failure to the `error` string sent to the client.
320
+ * Return `undefined` for the default (`"Internal server error"`). Store
321
+ * adapters use it for setup hints (e.g. "table not found, run migrations");
322
+ * never return the failure's own details, which may leak internals.
323
+ */
324
+ describeError?(error: unknown): string | undefined;
325
+ }
326
+ /** The built-in shared-secret policy — `adapter-prisma`'s historical behaviour. */
327
+ interface SitepingApiKeyHandlerOptions extends SitepingHandlerBaseOptions<null> {
328
+ /**
329
+ * Shared secret expected as `Authorization: Bearer {apiKey}`.
330
+ *
331
+ * - **When set:** every request not listed in `publicEndpoints` must include
332
+ * it. Requests without a valid token receive a 401 Unauthorized response.
333
+ * Only a request carrying it may post a comment as the `team`; any other
334
+ * comment is stamped `client`.
335
+ * - **When not set:** the API is public — anyone can create and read
336
+ * feedbacks, and update or delete them once `requireAuthForDestructive`
337
+ * is turned off.
338
+ * - **Recommendation:** always set `apiKey` in production environments.
339
+ */
340
+ apiKey?: string | undefined;
341
+ /**
342
+ * HTTP methods that don't require API key authentication.
343
+ * Defaults to `['POST', 'OPTIONS']` when `apiKey` is set — POST must stay open
344
+ * because the browser widget submits feedback from unauthenticated contexts.
345
+ */
346
+ publicEndpoints?: ReadonlyArray<SitepingHttpMethod>;
347
+ /**
348
+ * Whether destructive endpoints (DELETE, PATCH) require `apiKey`.
349
+ *
350
+ * Defaults to `true`:
351
+ * - `NODE_ENV === "production"` without `apiKey` throws at startup: the
352
+ * factory refuses to return an unauthenticated destructive surface by
353
+ * accident.
354
+ * - `NODE_ENV !== "production"` without `apiKey` keeps the handler running
355
+ * for local dev/tests, but DELETE/PATCH return 401 until you set
356
+ * `apiKey` or explicitly opt out with `requireAuthForDestructive: false`.
357
+ *
358
+ * `false` lifts both, in production too: without `apiKey`, anyone who
359
+ * reaches the endpoint may then PATCH and DELETE, `deleteAll` included.
360
+ * Set it only behind your own middleware that authenticates every method
361
+ * (session, OAuth, etc.).
362
+ */
363
+ requireAuthForDestructive?: boolean;
364
+ /**
365
+ * Blank `authorEmail` — of feedbacks and of their comments — in GET/PATCH
366
+ * responses to requests that do not carry a valid
367
+ * `Authorization: Bearer <apiKey>` header. Defaults to `true`:
368
+ * reviewer emails are PII and the widget needs GET to be reachable, so an
369
+ * unauthenticated response must not enumerate them (issue #105).
370
+ *
371
+ * Set to `false` ONLY when the handler sits behind your own auth layer
372
+ * that covers GET as well (e.g. `requireAuthForDestructive: false` behind
373
+ * session middleware) — the handler cannot see that layer, and without it
374
+ * every visitor who can reach the endpoint can read reviewer emails.
375
+ * `clientId` is stripped from responses regardless of this option.
376
+ */
377
+ redactUnauthenticatedEmails?: boolean;
378
+ /** Use either the `apiKey` policy or `access`, never both. */
379
+ access?: never;
380
+ }
381
+ /** A custom access policy (sessions, JWTs, roles) in place of `apiKey`. */
382
+ interface SitepingAccessHandlerOptions<Principal extends SitepingPrincipal> extends SitepingHandlerBaseOptions<Principal> {
383
+ /**
384
+ * Who is calling and what they may do — see `SitepingAccessControl`.
385
+ *
386
+ * Such a policy may authenticate with cookies, which browsers attach to
387
+ * forged cross-site requests too, so POST/PATCH/DELETE are guarded against
388
+ * CSRF before anything else runs: a body that is not `application/json`
389
+ * answers 415 (only JSON forces a CORS preflight), and with
390
+ * `allowedOrigins` an `Origin` that is neither listed nor the endpoint's
391
+ * own answers 403. Requests without an `Origin` (server-to-server, curl)
392
+ * pass. Behind a proxy that rewrites `request.url`, list your public origin
393
+ * in `allowedOrigins`. Lists are sent with `Cache-Control: no-store`: they
394
+ * depend on the principal, which the browser cache cannot tell apart.
395
+ */
396
+ access: SitepingAccessControl<Principal>;
397
+ /** `apiKey` policy only. */
398
+ apiKey?: never;
399
+ /** `apiKey` policy only. */
400
+ publicEndpoints?: never;
401
+ /** `apiKey` policy only. */
402
+ requireAuthForDestructive?: never;
403
+ /** `apiKey` policy only — use `access.canReadAuthorEmail`. */
404
+ redactUnauthenticatedEmails?: never;
405
+ }
406
+ /** Options of `createSitepingHandler`: the `apiKey` policy XOR a custom `access` policy. */
407
+ type SitepingHandlerOptions<Principal extends SitepingPrincipal = SitepingPrincipal> = SitepingApiKeyHandlerOptions | SitepingAccessHandlerOptions<Principal>;
408
+ /**
409
+ * Object returned by `createSitepingHandler` — one handler per HTTP method.
410
+ */
411
+ interface SitepingHandler {
412
+ OPTIONS: (request: Request) => Response;
413
+ POST: (request: Request) => Promise<Response>;
414
+ GET: (request: Request) => Promise<Response>;
415
+ PATCH: (request: Request) => Promise<Response>;
416
+ DELETE: (request: Request) => Promise<Response>;
417
+ }
418
+
419
+ /**
420
+ * Create the SitePing HTTP API over any `SitepingStore`, using only the Fetch
421
+ * API (`Request` → `Response`): one handler per method, to mount in Next.js
422
+ * route handlers, Hono, Remix, SvelteKit, Bun, Deno or edge workers.
423
+ *
424
+ * **Rate limiting** is not handled by this library. Apply rate limiting at the
425
+ * framework or reverse-proxy level (e.g. Next.js middleware, Nginx, Cloudflare).
426
+ * The POST endpoint in particular should be rate-limited to prevent abuse, since
427
+ * the widget typically calls it from unauthenticated browser contexts.
428
+ *
429
+ * Access is either the built-in `apiKey` policy or your own `access` policy
430
+ * (sessions, JWTs, roles…) — see `SitepingAccessHandlerOptions`.
431
+ *
432
+ * @throws Error without a `store`; with a `maxBodyBytes` that is not a
433
+ * positive integer, or a webhook `timeoutMs` no timer holds; in production
434
+ * without `apiKey` (see `requireAuthForDestructive`); or with
435
+ * `access.authorize` over a store without `verifyProjectOwnership`, since
436
+ * PATCH/DELETE could then reach a record of a project the caller is not
437
+ * authorized for.
438
+ *
439
+ * @example Next.js App Router — `app/api/siteping/route.ts`
440
+ * ```ts
441
+ * import { createSitepingHandler } from '@siteping/server'
442
+ * import { store } from '@/lib/siteping-store'
443
+ *
444
+ * export const { GET, POST, PATCH, DELETE, OPTIONS } = createSitepingHandler({
445
+ * store,
446
+ * apiKey: process.env.SITEPING_API_KEY,
447
+ * })
448
+ * ```
449
+ */
450
+ declare function createSitepingHandler<Principal extends SitepingPrincipal>(options: SitepingAccessHandlerOptions<Principal>): SitepingHandler;
451
+ declare function createSitepingHandler(options: SitepingApiKeyHandlerOptions): SitepingHandler;
452
+ /** Options assembled at runtime, either policy. */
453
+ declare function createSitepingHandler<Principal extends SitepingPrincipal>(options: SitepingHandlerOptions<Principal>): SitepingHandler;
454
+
455
+ interface FeedbackPatchInput {
456
+ id: string;
457
+ projectName: string;
458
+ status: FeedbackStatus;
459
+ }
460
+ interface FeedbackDeleteSingle {
461
+ id: string;
462
+ projectName: string;
463
+ }
464
+ interface FeedbackDeleteAll {
465
+ projectName: string;
466
+ deleteAll: true;
467
+ }
468
+ type FeedbackDeleteInput = FeedbackDeleteSingle | FeedbackDeleteAll;
469
+ interface GetQueryInput {
470
+ projectName: string;
471
+ /** Set to 1 by schema default when omitted from raw input. */
472
+ page: number;
473
+ /** Set to 50 by schema default when omitted from raw input. */
474
+ limit: number;
475
+ type?: FeedbackType | undefined;
476
+ status?: FeedbackStatus | undefined;
477
+ statuses?: FeedbackStatus[] | undefined;
478
+ search?: string | undefined;
479
+ url?: string | undefined;
480
+ urlPattern?: string | undefined;
481
+ }
482
+ /** Single validation issue extracted from a `ZodError`. */
483
+ interface ValidationIssue {
484
+ field: string;
485
+ message: string;
486
+ }
487
+
488
+ export { type DiscordWebhookPayload, type FeedbackDeleteInput, type FeedbackPatchInput, type GenericWebhookPayload, type GetQueryInput, type SitepingAccessControl, type SitepingAccessHandlerOptions, type SitepingAction, type SitepingApiKeyHandlerOptions, type SitepingAuthorizationContext, type SitepingDeletionTarget, type SitepingHandler, type SitepingHandlerBaseOptions, type SitepingHandlerOptions, type SitepingHttpMethod, type SitepingLifecycleHooks, type SitepingLogger, type SitepingPrincipal, type SitepingRequestContext, type SlackWebhookPayload, type ValidationIssue, type WebhookConfig, type WebhookPayloadMap, type WebhookType, createSitepingHandler, dispatchWebhook, dispatchWebhooks };