@siteping/server 0.0.0-stage → 0.1.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 +40 -2
- package/dist/concurrency.d.cts +14 -0
- package/dist/concurrency.d.ts +14 -0
- package/dist/deep-link.d.cts +14 -0
- package/dist/deep-link.d.ts +14 -0
- package/dist/email.d.cts +22 -0
- package/dist/email.d.ts +22 -0
- package/dist/errors.d.cts +55 -0
- package/dist/errors.d.ts +55 -0
- package/dist/filters.d.cts +82 -0
- package/dist/filters.d.ts +82 -0
- package/dist/i18n.d.cts +85 -0
- package/dist/i18n.d.ts +85 -0
- package/dist/index.cjs +1270 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +488 -0
- package/dist/index.d.ts +488 -0
- package/dist/index.js +1237 -0
- package/dist/index.js.map +1 -0
- package/dist/schema.d.cts +317 -0
- package/dist/schema.d.ts +317 -0
- package/dist/screenshot-storage.d.cts +103 -0
- package/dist/screenshot-storage.d.ts +103 -0
- package/dist/siteping-core.d.cts +20 -0
- package/dist/siteping-core.d.ts +20 -0
- package/dist/store-helpers.d.cts +140 -0
- package/dist/store-helpers.d.ts +140 -0
- package/dist/type-utils.d.cts +66 -0
- package/dist/type-utils.d.ts +66 -0
- package/dist/types.d.cts +1206 -0
- package/dist/types.d.ts +1206 -0
- package/dist/wire.d.cts +68 -0
- package/dist/wire.d.ts +68 -0
- package/package.json +64 -4
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import { Prettify, Serialized, FeedbackRecord, CommentResponse, SitepingStore, FeedbackCreateInput, CommentPayload, FeedbackStatus, FeedbackType } from './siteping-core.cjs';
|
|
2
|
+
export { CommentCreateInput, CommentPayload, FeedbackCreateInput, FeedbackRecord, SitepingStore } from './siteping-core.cjs';
|
|
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 };
|