@siteping/server 0.1.0 → 0.2.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/dist/types.d.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  import { type DeepReadonly, type Prettify, type Serialized } from "./type-utils.js";
2
2
  /** FAB anchor — bottom-corner placement supported by the widget. */
3
3
  export type SitepingPosition = "bottom-right" | "bottom-left";
4
- /** Visual theme — `auto` resolves to `light` or `dark` via system preference. */
4
+ /**
5
+ * Visual theme of the widget and the dashboard — `auto` follows the system
6
+ * preference, live: an OS switch mid-session re-themes the UI.
7
+ */
5
8
  export type SitepingTheme = "light" | "dark" | "auto";
6
9
  /** Built-in UI locales shipped with the widget. */
7
- export declare const BUILTIN_LOCALES: readonly ["en", "fr", "de", "es", "it", "pt", "ru", "ja"];
10
+ export declare const BUILTIN_LOCALES: readonly ["en", "fr", "de", "es", "it", "pt", "ru", "ja", "ko"];
8
11
  export type BuiltinLocale = (typeof BUILTIN_LOCALES)[number];
9
12
  /**
10
13
  * Locale identifier accepted by the widget. Built-in locales are kept as
@@ -38,6 +41,15 @@ export interface SitepingIdentity {
38
41
  * it persists is never a 400 on every later submission.
39
42
  */
40
43
  export declare const IDENTITY_FIELD_MAX_LENGTH = 200;
44
+ /**
45
+ * Max length of a `projectName` and of a feedback's or comment's `clientId`
46
+ * — the identifiers the stores index. 191 characters is the longest
47
+ * `VARCHAR` every MySQL version indexes under `utf8mb4` (767 bytes), which
48
+ * is why Prisma maps a plain `String` to `VARCHAR(191)` there: the HTTP
49
+ * schema caps them at that length, so their columns need no prefix index.
50
+ */
51
+ export declare const PROJECT_NAME_MAX_LENGTH = 191;
52
+ export declare const CLIENT_ID_MAX_LENGTH = 191;
41
53
  /** Deep-link configuration — controls how a feedback id is read from the URL. */
42
54
  export interface SitepingDeepLinkOptions {
43
55
  /** Query parameter name carrying the feedback id. Defaults to `"siteping"`. */
@@ -184,9 +196,9 @@ export interface SitepingBaseConfig {
184
196
  minViewportWidth?: number | undefined;
185
197
  /** Enable debug logging of lifecycle events — defaults to false */
186
198
  debug?: boolean | undefined;
187
- /** Color theme — defaults to 'light' */
199
+ /** Color theme — defaults to `light`; `auto` follows the system preference live. */
188
200
  theme?: SitepingTheme | undefined;
189
- /** UI locale — defaults to 'en'. Built-in: en, fr, de, es, it, pt (Brazilian), ru, ja. Any other string falls back to English. */
201
+ /** UI locale — defaults to 'en'. Built-in: en, fr, de, es, it, pt (Brazilian), ru, ja, ko. Any other string falls back to English. */
190
202
  locale?: SitepingLocale | undefined;
191
203
  /**
192
204
  * Returns the current page scope for annotations and panel filtering.
@@ -204,7 +216,9 @@ export interface SitepingBaseConfig {
204
216
  * When true (default), the widget filters initial markers and panel results
205
217
  * by `feedback.url === scope.url`, so annotations created on one page never
206
218
  * leak to other pages — even if their CSS selector accidentally matches.
207
- * Set to `false` to revert to the legacy project-wide behavior.
219
+ * Set to `false` for a project-wide view: the panel lists every feedback of
220
+ * the project, and a marker shows wherever its anchor resolves — for
221
+ * feedback on what every page shares, such as a header or a footer.
208
222
  */
209
223
  scopeAnnotationsByUrl?: boolean | undefined;
210
224
  /**
@@ -276,7 +290,7 @@ export interface SitepingBaseConfig {
276
290
  */
277
291
  captureDiagnostics?: boolean | DiagnosticsCaptureOptions | undefined;
278
292
  /** Called when the widget is skipped (production mode, viewport under `minViewportWidth`, SSR — no DOM) */
279
- onSkip?: (reason: SitepingSkipReason) => void;
293
+ onSkip?: ((reason: SitepingSkipReason) => void) | undefined;
280
294
  /**
281
295
  * Auto-focus a specific annotation when its ID appears in the URL query
282
296
  * string. Lets hosts deeplink directly into a feedback from external
@@ -286,7 +300,8 @@ export interface SitepingBaseConfig {
286
300
  * `window.location.search` right after the initial markers load. If the
287
301
  * value matches a visible feedback ID, the widget scrolls the annotation
288
302
  * into view, pins its highlight, and pulses the marker — the same visual
289
- * affordance a marker click produces.
303
+ * affordance a marker click produces — then opens the panel on that
304
+ * feedback's detail view, where its discussion thread is.
290
305
  *
291
306
  * - `false` / `undefined` (default): no URL parsing. Existing behavior
292
307
  * unchanged, no host URL inspection.
@@ -327,6 +342,8 @@ export interface SitepingBaseConfig {
327
342
  * Pre-fill author identity from the host application — typically the
328
343
  * currently signed-in user. When set, the widget uses these values
329
344
  * directly and never shows the identity modal, even on first feedback.
345
+ * It names the reviewer who sends feedback — the dashboard's `author` is
346
+ * the team member who replies, a different person.
330
347
  *
331
348
  * Use case: SSO-integrated apps where the end user is already
332
349
  * authenticated by the host. Avoids the awkward "enter your name and
@@ -372,12 +389,13 @@ export interface SitepingBaseConfig {
372
389
  *
373
390
  * The widget always emits a `SitepingError` (or a subclass:
374
391
  * `SitepingNetworkError`, `SitepingValidationError`, `SitepingAuthError`)
375
- * for HTTP-mode failures — host apps can `instanceof` to drive retry
376
- * logic, or read `error.code` (`"NETWORK" | "VALIDATION" | "AUTH" |
377
- * "SERVER"`) and `error.retryable` — and an `AUTH` error's `status`
378
- * (`401` for missing or dead credentials, `403` for a refusal). The type
379
- * is widened to `Error` so direct-store callers can still surface raw
380
- * errors without breaking the contract.
392
+ * for a failed call — match it with `isSitepingError(error)` (not
393
+ * `instanceof`: the classes aren't exported), then read `error.code`
394
+ * (`"NETWORK" | "VALIDATION" | "AUTH" | "SERVER" | "TIMEOUT"`),
395
+ * `error.retryable` and an `AUTH` error's `status` (`401` for missing or
396
+ * dead credentials, `403` for a refusal). The type is widened to `Error`
397
+ * because a store in `store` mode may throw its own errors, which reach
398
+ * here unchanged.
381
399
  *
382
400
  * Also receives whatever a `panelActions` callback throws or rejects with
383
401
  * (non-`Error` values are wrapped). Those host failures are not API
@@ -450,14 +468,24 @@ export interface SitepingStoreConfig extends SitepingBaseConfig {
450
468
  export type SitepingConfig = SitepingHttpConfig | SitepingStoreConfig;
451
469
  /** Instance returned by initSiteping() with lifecycle methods. */
452
470
  export interface SitepingInstance {
453
- /** Remove the widget from the DOM and clean up all listeners. */
471
+ /**
472
+ * Release this handle and detach the listeners added through its `on()`.
473
+ * Every `initSiteping()` call returns its own handle over the one widget,
474
+ * which leaves the DOM, all its listeners cleaned up, once the last handle
475
+ * is destroyed. A repeat call is a no-op, and the handle's other methods do
476
+ * nothing afterwards.
477
+ */
454
478
  destroy: () => void;
455
479
  /** Open the panel programmatically */
456
480
  open: () => void;
457
481
  /** Close the panel */
458
482
  close: () => void;
459
- /** Reload feedbacks from server */
460
- refresh: () => void;
483
+ /**
484
+ * Reload feedbacks from the server. Resolves once the pins (and the open
485
+ * panel) show the reloaded list; never rejects — like the dashboard's
486
+ * `refresh()`, a failed reload leaves the current view in place.
487
+ */
488
+ refresh: () => Promise<void>;
461
489
  /**
462
490
  * Scroll the matching annotation into view, pin its highlight, and
463
491
  * pulse its marker. Returns `true` when a visible feedback matched the
@@ -835,9 +863,10 @@ export declare class StoreLimitError extends Error {
835
863
  }
836
864
  /**
837
865
  * Thrown when a value is longer than the store can hold, though the HTTP
838
- * validation accepts it — on MySQL, Prisma maps a plain `String` to
839
- * `VARCHAR(191)`. Handlers translate this to HTTP 422: the submission can
840
- * never be stored as it is, so a client must not retry it.
866
+ * validation accepts it — a MySQL schema that predates the columns `sync`
867
+ * now sizes from the HTTP caps keeps Prisma's `VARCHAR(191)` for a plain
868
+ * `String`. Handlers translate this to HTTP 422: the submission can never be
869
+ * stored as it is, so a client must not retry it.
841
870
  */
842
871
  export declare class StoreValueTooLongError extends Error {
843
872
  readonly code: "STORE_VALUE_TOO_LONG";
@@ -885,6 +914,19 @@ export interface FeedbackCreateOutcome {
885
914
  */
886
915
  created: boolean;
887
916
  }
917
+ /**
918
+ * Outcome of `SitepingStore.addCommentIfAbsent` — the comment plus whether
919
+ * this very call inserted it.
920
+ */
921
+ export interface CommentCreateOutcome {
922
+ comment: CommentRecord;
923
+ /**
924
+ * `true` when this call inserted the comment, `false` when a comment with
925
+ * the same `clientId` already existed and is returned instead (a replay, or
926
+ * a concurrent request that won the race).
927
+ */
928
+ created: boolean;
929
+ }
888
930
  /** Paginated result returned by `SitepingStore.getFeedbacks`. */
889
931
  export interface FeedbackPage {
890
932
  feedbacks: FeedbackRecord[];
@@ -901,19 +943,17 @@ export interface FeedbackPage {
901
943
  *
902
944
  * - **`updateFeedback` / `deleteFeedback`**: throw `StoreNotFoundError` when
903
945
  * the record does not exist.
904
- * - **`createFeedback`**: either return the existing record on duplicate
905
- * `clientId` (idempotent) or throw `StoreDuplicateError`. The handler
906
- * handles both patterns — but only a throw, or the optional
907
- * `createFeedbackIfAbsent`, tells it the record was not inserted by this
908
- * call; stores that return the existing record should implement
909
- * `createFeedbackIfAbsent` so creation side effects never run twice.
946
+ * - **`createFeedback`**: throw `StoreDuplicateError` on a duplicate
947
+ * `clientId` — never return the existing record, so a caller always knows
948
+ * whether this call inserted. The optional `createFeedbackIfAbsent`
949
+ * reports the existing record instead, as `{ created: false }`.
910
950
  * - **All mutations**: when a write is accepted but cannot be persisted
911
951
  * (e.g. storage quota), throw `StorePersistenceError` instead of reporting
912
952
  * a phantom success. Detect it with `isStorePersistence`.
913
953
  * - Other methods should not throw on empty results — return empty arrays or `null`.
914
954
  */
915
955
  export interface SitepingStore {
916
- /** 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. */
956
+ /** Create a feedback with its annotations. Throws `StoreDuplicateError` when a feedback with the same `clientId` exists, `StorePersistenceError` when the write cannot be persisted. */
917
957
  createFeedback(data: FeedbackCreateInput): Promise<FeedbackRecord>;
918
958
  /** Paginated query with optional filters. Returns empty array (not error) when no results. */
919
959
  getFeedbacks(query: FeedbackQuery): Promise<FeedbackPage>;
@@ -926,14 +966,11 @@ export interface SitepingStore {
926
966
  /** Bulk delete all feedbacks for a project. No-op (not error) if none exist. Throws `StorePersistenceError` when the write cannot be persisted. */
927
967
  deleteAllFeedbacks(projectName: string): Promise<void>;
928
968
  /**
929
- * Optional — return `true` when the record with `id` belongs to
930
- * `projectName`, `false` otherwise (including when it does not exist).
931
- *
932
- * HTTP handlers use this to reject cross-project PATCH/DELETE requests.
933
- * Implement it whenever your store serves multiple projects; when absent,
934
- * handlers skip the ownership check and rely on `id` alone.
969
+ * Return `true` when the record with `id` belongs to `projectName`, `false`
970
+ * otherwise (including when it does not exist). HTTP handlers use it to
971
+ * reject cross-project PATCH/DELETE requests, which address records by id.
935
972
  */
936
- verifyProjectOwnership?(id: string, projectName: string): Promise<boolean>;
973
+ verifyProjectOwnership(id: string, projectName: string): Promise<boolean>;
937
974
  /**
938
975
  * Optional — `createFeedback` that reports whether this call inserted the
939
976
  * record (`created: true`) or found an existing one with the same
@@ -945,11 +982,10 @@ export interface SitepingStore {
945
982
  * processes need an atomic backend primitive (unique constraint,
946
983
  * transaction, compare-and-set).
947
984
  *
948
- * HTTP handlers prefer it over `createFeedback` to fire creation side
949
- * effects (webhooks) exactly once when concurrent requests race on the
950
- * same `clientId`. Stores whose `createFeedback` throws
951
- * `StoreDuplicateError` on a duplicate already give that signal and may
952
- * leave it out.
985
+ * HTTP handlers prefer it over `createFeedback`: a replayed or racing
986
+ * submission resolves to the stored record in one atomic call, without a
987
+ * `StoreDuplicateError` to catch and a lookup to follow it. Without it,
988
+ * they fall back to `createFeedback` and recover its duplicate error.
953
989
  */
954
990
  createFeedbackIfAbsent?(data: FeedbackCreateInput): Promise<FeedbackCreateOutcome>;
955
991
  /**
@@ -971,6 +1007,19 @@ export interface SitepingStore {
971
1007
  * HTTP handlers serve empty threads and answer comment writes with 501.
972
1008
  */
973
1009
  addComment?(feedbackId: string, data: CommentCreateInput): Promise<CommentRecord>;
1010
+ /**
1011
+ * Optional, alongside `addComment` — `addComment` that reports whether this
1012
+ * call inserted the comment (`created: true`) or found one with the same
1013
+ * `clientId` (`created: false`). Same errors as `addComment`, and the dedup
1014
+ * check and the insert must be atomic like `createFeedbackIfAbsent`'s: of N
1015
+ * concurrent calls with the same `clientId`, exactly one may report
1016
+ * `created: true`, and all must return that same comment.
1017
+ *
1018
+ * HTTP handlers need it to fire the `comment.created` webhook and the
1019
+ * `onCommentCreated` hook exactly once per reply — a retried post returns
1020
+ * the stored comment, which `addComment` alone cannot tell from a new one.
1021
+ */
1022
+ addCommentIfAbsent?(feedbackId: string, data: CommentCreateInput): Promise<CommentCreateOutcome>;
974
1023
  /**
975
1024
  * Optional — delete comment `commentId` from the thread of `feedbackId`.
976
1025
  * Scoped to that thread, which the caller was authorized for: throws
@@ -1167,6 +1216,16 @@ export interface FeedbackPermissions {
1167
1216
  /** Delete replies from its thread. */
1168
1217
  canDeleteComment: boolean;
1169
1218
  }
1219
+ /**
1220
+ * What a requester may do with one comment of a thread, as the server's
1221
+ * access policy decides — finer than the feedback's `canDeleteComment`, which
1222
+ * covers the whole thread: a policy that lets authors delete their own
1223
+ * replies only grants it comment by comment.
1224
+ */
1225
+ export interface CommentPermissions {
1226
+ /** Delete it. */
1227
+ canDelete: boolean;
1228
+ }
1170
1229
  /** What a requester may do with a whole project, sent with each list. */
1171
1230
  export interface FeedbackListPermissions {
1172
1231
  /** Delete every feedback of the project at once. */
@@ -1182,7 +1241,23 @@ export type AnnotationResponse = Prettify<Serialized<AnnotationRecord>>;
1182
1241
  * serialized and `clientId` omitted, like on `FeedbackResponse`. Its
1183
1242
  * `authorEmail` is redacted on the same terms as the feedback's.
1184
1243
  */
1185
- export type CommentResponse = Prettify<Serialized<Omit<CommentRecord, "clientId">>>;
1244
+ export type CommentResponse = Prettify<Serialized<Omit<CommentRecord, "clientId">> & {
1245
+ /**
1246
+ * What the requester may do with this comment — always sent by
1247
+ * `@siteping/server`. Absent from servers that predate it, and in store
1248
+ * mode: the feedback's `canDeleteComment` decides then.
1249
+ */
1250
+ permissions?: CommentPermissions | undefined;
1251
+ }>;
1252
+ /**
1253
+ * Version of the HTTP protocol `@siteping/server` speaks, sent as `protocol`
1254
+ * on every list response. It rises with each addition a client may rely on —
1255
+ * a route, a field, a status — so a client checks `protocol >= N` before
1256
+ * using what protocol N added, instead of probing for fields. A server that
1257
+ * predates it sends none: read that as 0. The OpenAPI document shipped as
1258
+ * `@siteping/server/openapi.json` describes this version.
1259
+ */
1260
+ export declare const SITEPING_PROTOCOL_VERSION = 1;
1186
1261
  /** What the store behind an endpoint supports — advertised on every list response. */
1187
1262
  export interface SitepingCapabilities {
1188
1263
  /** Whether comments can be posted: the store implements `addComment`. */
@@ -1198,6 +1273,8 @@ export interface SitepingCapabilities {
1198
1273
  export interface FeedbackResponseList {
1199
1274
  feedbacks: FeedbackResponse[];
1200
1275
  total: number;
1276
+ /** The server's {@link SITEPING_PROTOCOL_VERSION} — absent from servers that predate it: read that as 0. */
1277
+ protocol?: number | undefined;
1201
1278
  /** Always sent by `@siteping/server` — absent from servers that predate it. */
1202
1279
  capabilities?: SitepingCapabilities | undefined;
1203
1280
  /** Always sent by `@siteping/server` — absent from servers that predate it. */
package/dist/wire.d.cts CHANGED
@@ -39,8 +39,13 @@ export declare function withSearchParams(endpoint: string, params: URLSearchPara
39
39
  /**
40
40
  * Map a non-OK `Response` to the appropriate typed error:
41
41
  * - 401 / 403 → `SitepingAuthError`, with its `status`
42
+ * - 5xx / 408 / 429 → generic `SitepingError` (code `"SERVER"`,
43
+ * retryable): a server failure, or a timeout or a rate limit in front of
44
+ * the endpoint, not a verdict on the request — the same request can
45
+ * succeed later
42
46
  * - other 4xx → `SitepingValidationError`, with its `status`
43
- * - 5xx (or anything else) → generic `SitepingError` (code `"SERVER"`)
47
+ * - anything else (a 3xx a manual redirect left unfollowed) → generic
48
+ * `SitepingError` (code `"SERVER"`), not retryable
44
49
  *
45
50
  * The response body is consumed via `.text()` so the caller keeps the
46
51
  * server-supplied message in the thrown error; `.text()` failures fall back
package/dist/wire.d.ts CHANGED
@@ -39,8 +39,13 @@ export declare function withSearchParams(endpoint: string, params: URLSearchPara
39
39
  /**
40
40
  * Map a non-OK `Response` to the appropriate typed error:
41
41
  * - 401 / 403 → `SitepingAuthError`, with its `status`
42
+ * - 5xx / 408 / 429 → generic `SitepingError` (code `"SERVER"`,
43
+ * retryable): a server failure, or a timeout or a rate limit in front of
44
+ * the endpoint, not a verdict on the request — the same request can
45
+ * succeed later
42
46
  * - other 4xx → `SitepingValidationError`, with its `status`
43
- * - 5xx (or anything else) → generic `SitepingError` (code `"SERVER"`)
47
+ * - anything else (a 3xx a manual redirect left unfollowed) → generic
48
+ * `SitepingError` (code `"SERVER"`), not retryable
44
49
  *
45
50
  * The response body is consumed via `.text()` so the caller keeps the
46
51
  * server-supplied message in the thrown error; `.text()` failures fall back