@siteping/server 0.1.1 → 0.2.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.
package/README.md CHANGED
@@ -14,7 +14,7 @@ The HTTP endpoint of [SitePing](https://github.com/NeosiaNexus/SitePing) over an
14
14
  npm install @siteping/server
15
15
  ```
16
16
 
17
- Node ≥ 20, or any runtime with the Fetch API.
17
+ Node ≥ 22, or any runtime with the Fetch API.
18
18
 
19
19
  ## Quick start
20
20
 
@@ -5,6 +5,16 @@ import type { FeedbackRecord } from "./types.cjs";
5
5
  * feedback (`javascript:…`, `data:…`) would turn into a live link.
6
6
  */
7
7
  export declare function parseHttpUrl(url: string, base?: string): URL | null;
8
+ /**
9
+ * Whether a screenshot URL is safe to render as an `<img src>`, in the
10
+ * widget's panel and the dashboard alike. A store can hold any string there
11
+ * (a direct store caller, a custom `ScreenshotStorage`, a migrated database):
12
+ * a `javascript:`, `data:text/html` or `data:image/svg+xml` URL — SVG can
13
+ * carry external `<image href>` references — or an arbitrary host would make
14
+ * the viewer fetch from it, leaking their IP and user agent. Browsers never
15
+ * run scripts from an `<img>`, but the fetch itself still happens.
16
+ */
17
+ export declare function isSafeImageUrl(url: string): boolean;
8
18
  /**
9
19
  * Link that opens a feedback's page with the widget focused on it —
10
20
  * `?<param>=<id>`, read by the widget's `deepLink` option. Relative record
@@ -5,6 +5,16 @@ import type { FeedbackRecord } from "./types.js";
5
5
  * feedback (`javascript:…`, `data:…`) would turn into a live link.
6
6
  */
7
7
  export declare function parseHttpUrl(url: string, base?: string): URL | null;
8
+ /**
9
+ * Whether a screenshot URL is safe to render as an `<img src>`, in the
10
+ * widget's panel and the dashboard alike. A store can hold any string there
11
+ * (a direct store caller, a custom `ScreenshotStorage`, a migrated database):
12
+ * a `javascript:`, `data:text/html` or `data:image/svg+xml` URL — SVG can
13
+ * carry external `<image href>` references — or an arbitrary host would make
14
+ * the viewer fetch from it, leaking their IP and user agent. Browsers never
15
+ * run scripts from an `<img>`, but the fetch itself still happens.
16
+ */
17
+ export declare function isSafeImageUrl(url: string): boolean;
8
18
  /**
9
19
  * Link that opens a feedback's page with the widget focused on it —
10
20
  * `?<param>=<id>`, read by the widget's `deepLink` option. Relative record
package/dist/errors.d.cts CHANGED
@@ -1,30 +1,30 @@
1
1
  /**
2
2
  * Typed error hierarchy for Siteping client/server boundaries.
3
3
  *
4
- * Consumers can `instanceof`-check or read `code` / `retryable` instead of
5
- * pattern-matching error messages. Designed to be additive on top of the
6
- * existing store errors (`StoreNotFoundError`, `StoreDuplicateError`) which
7
- * remain the canonical signals for server-side store implementations.
4
+ * Consumers match an error with a guard — `isSitepingError`,
5
+ * `isSitepingConfigError`, a store guard such as `isStoreNotFound` — then
6
+ * read `code` / `retryable` instead of pattern-matching messages. The guards
7
+ * are structural, never `instanceof`: every published package bundles its
8
+ * own copy of these classes (tsup `noExternal`), so an error one package
9
+ * throws fails `instanceof` against another's class. The widget and the
10
+ * dashboard export the guard and the types, not the classes.
8
11
  *
9
12
  * Usage on the widget side (api-client.ts):
10
13
  * - fetch failures / aborts / timeouts → `SitepingNetworkError` (retryable)
11
- * - HTTP 4xx (except 401/403) → `SitepingValidationError` (not retryable)
14
+ * - HTTP 4xx (except 401/403/408/429) → `SitepingValidationError` (not retryable)
12
15
  * - HTTP 401 / 403 → `SitepingAuthError` (not retryable)
13
- * - everything else → `SitepingError` generic
16
+ * - HTTP 5xx / 408 / 429 → `SitepingError` code `"SERVER"` (retryable)
14
17
  *
15
18
  * Store mode (store-client.ts): a `createFeedback` that does not settle in
16
- * time → `SitepingError` with code `"TIMEOUT"` (retryable).
19
+ * time → `SitepingError` with code `"TIMEOUT"` (retryable); a reply to a
20
+ * store that keeps no comments → `"SERVER"`, not retryable.
17
21
  *
18
- * `retryable` is meta information surfaced to host apps that want to wire
19
- * their own retry/queue/backoff strategy — the widget already retries
20
- * network failures via its built-in retry queue.
22
+ * `retryable` says whether the same request may succeed later, unchanged —
23
+ * meta information for host apps that wire their own retry/queue/backoff
24
+ * strategy; the widget already retries and queues transient failures itself.
21
25
  */
22
- /**
23
- * Discriminant string carried by every `SitepingError`. Subclasses pin a
24
- * literal value; the base class accepts a wider string so userland can
25
- * extend the hierarchy without colliding with built-ins.
26
- */
27
- export type SitepingErrorCode = "NETWORK" | "VALIDATION" | "AUTH" | "SERVER" | "TIMEOUT" | (string & {});
26
+ /** Discriminant string carried by every `SitepingError`. */
27
+ export type SitepingErrorCode = "NETWORK" | "VALIDATION" | "AUTH" | "SERVER" | "TIMEOUT";
28
28
  export declare class SitepingError<TCode extends SitepingErrorCode = SitepingErrorCode> extends Error {
29
29
  readonly code: TCode;
30
30
  readonly retryable: boolean;
@@ -53,3 +53,33 @@ export declare class SitepingAuthError extends SitepingError<"AUTH"> {
53
53
  readonly status: 401 | 403;
54
54
  constructor(message: string, status: 401 | 403);
55
55
  }
56
+ /**
57
+ * Every error the widget and the dashboard report for a failed call, told
58
+ * apart by `code`: narrowing on it reaches a subclass's own fields, such as
59
+ * an `AUTH` error's `status`.
60
+ */
61
+ export type SitepingClientError = SitepingNetworkError | SitepingValidationError | SitepingAuthError | SitepingError<"SERVER" | "TIMEOUT">;
62
+ /**
63
+ * Whether `error` is a `SitepingError` (any subclass) — matched on its
64
+ * fields, a known `code` and a boolean `retryable`, so it holds for an error
65
+ * another package's copy of the class threw. Anything else reaching an
66
+ * `onError` (a store's own error, a panel action's) fails it.
67
+ */
68
+ export declare function isSitepingError(error: unknown): error is SitepingClientError;
69
+ /** Stable codes of the errors a handler factory throws for options it cannot run with. */
70
+ export type SitepingConfigErrorCode = "CONFIG_STORE_MISSING" | "CONFIG_MAX_BODY_BYTES_INVALID" | "CONFIG_OWNERSHIP_CHECK_MISSING" | "CONFIG_WEBHOOK_TIMEOUT_INVALID" | "CONFIG_API_KEY_REQUIRED";
71
+ /**
72
+ * Thrown by `createSitepingHandler` while it is created, never per request,
73
+ * when its options cannot run safely. The message says how to fix it; branch
74
+ * on `code`, after {@link isSitepingConfigError}.
75
+ */
76
+ export declare class SitepingConfigError extends Error {
77
+ readonly code: SitepingConfigErrorCode;
78
+ constructor(code: SitepingConfigErrorCode, message: string);
79
+ }
80
+ /**
81
+ * Whether `error` is a `SitepingConfigError` — matched on its `name` and a
82
+ * `CONFIG_` code rather than `instanceof`, for the same cross-bundle reason
83
+ * as {@link isSitepingError}.
84
+ */
85
+ export declare function isSitepingConfigError(error: unknown): error is SitepingConfigError;
package/dist/errors.d.ts CHANGED
@@ -1,30 +1,30 @@
1
1
  /**
2
2
  * Typed error hierarchy for Siteping client/server boundaries.
3
3
  *
4
- * Consumers can `instanceof`-check or read `code` / `retryable` instead of
5
- * pattern-matching error messages. Designed to be additive on top of the
6
- * existing store errors (`StoreNotFoundError`, `StoreDuplicateError`) which
7
- * remain the canonical signals for server-side store implementations.
4
+ * Consumers match an error with a guard — `isSitepingError`,
5
+ * `isSitepingConfigError`, a store guard such as `isStoreNotFound` — then
6
+ * read `code` / `retryable` instead of pattern-matching messages. The guards
7
+ * are structural, never `instanceof`: every published package bundles its
8
+ * own copy of these classes (tsup `noExternal`), so an error one package
9
+ * throws fails `instanceof` against another's class. The widget and the
10
+ * dashboard export the guard and the types, not the classes.
8
11
  *
9
12
  * Usage on the widget side (api-client.ts):
10
13
  * - fetch failures / aborts / timeouts → `SitepingNetworkError` (retryable)
11
- * - HTTP 4xx (except 401/403) → `SitepingValidationError` (not retryable)
14
+ * - HTTP 4xx (except 401/403/408/429) → `SitepingValidationError` (not retryable)
12
15
  * - HTTP 401 / 403 → `SitepingAuthError` (not retryable)
13
- * - everything else → `SitepingError` generic
16
+ * - HTTP 5xx / 408 / 429 → `SitepingError` code `"SERVER"` (retryable)
14
17
  *
15
18
  * Store mode (store-client.ts): a `createFeedback` that does not settle in
16
- * time → `SitepingError` with code `"TIMEOUT"` (retryable).
19
+ * time → `SitepingError` with code `"TIMEOUT"` (retryable); a reply to a
20
+ * store that keeps no comments → `"SERVER"`, not retryable.
17
21
  *
18
- * `retryable` is meta information surfaced to host apps that want to wire
19
- * their own retry/queue/backoff strategy — the widget already retries
20
- * network failures via its built-in retry queue.
22
+ * `retryable` says whether the same request may succeed later, unchanged —
23
+ * meta information for host apps that wire their own retry/queue/backoff
24
+ * strategy; the widget already retries and queues transient failures itself.
21
25
  */
22
- /**
23
- * Discriminant string carried by every `SitepingError`. Subclasses pin a
24
- * literal value; the base class accepts a wider string so userland can
25
- * extend the hierarchy without colliding with built-ins.
26
- */
27
- export type SitepingErrorCode = "NETWORK" | "VALIDATION" | "AUTH" | "SERVER" | "TIMEOUT" | (string & {});
26
+ /** Discriminant string carried by every `SitepingError`. */
27
+ export type SitepingErrorCode = "NETWORK" | "VALIDATION" | "AUTH" | "SERVER" | "TIMEOUT";
28
28
  export declare class SitepingError<TCode extends SitepingErrorCode = SitepingErrorCode> extends Error {
29
29
  readonly code: TCode;
30
30
  readonly retryable: boolean;
@@ -53,3 +53,33 @@ export declare class SitepingAuthError extends SitepingError<"AUTH"> {
53
53
  readonly status: 401 | 403;
54
54
  constructor(message: string, status: 401 | 403);
55
55
  }
56
+ /**
57
+ * Every error the widget and the dashboard report for a failed call, told
58
+ * apart by `code`: narrowing on it reaches a subclass's own fields, such as
59
+ * an `AUTH` error's `status`.
60
+ */
61
+ export type SitepingClientError = SitepingNetworkError | SitepingValidationError | SitepingAuthError | SitepingError<"SERVER" | "TIMEOUT">;
62
+ /**
63
+ * Whether `error` is a `SitepingError` (any subclass) — matched on its
64
+ * fields, a known `code` and a boolean `retryable`, so it holds for an error
65
+ * another package's copy of the class threw. Anything else reaching an
66
+ * `onError` (a store's own error, a panel action's) fails it.
67
+ */
68
+ export declare function isSitepingError(error: unknown): error is SitepingClientError;
69
+ /** Stable codes of the errors a handler factory throws for options it cannot run with. */
70
+ export type SitepingConfigErrorCode = "CONFIG_STORE_MISSING" | "CONFIG_MAX_BODY_BYTES_INVALID" | "CONFIG_OWNERSHIP_CHECK_MISSING" | "CONFIG_WEBHOOK_TIMEOUT_INVALID" | "CONFIG_API_KEY_REQUIRED";
71
+ /**
72
+ * Thrown by `createSitepingHandler` while it is created, never per request,
73
+ * when its options cannot run safely. The message says how to fix it; branch
74
+ * on `code`, after {@link isSitepingConfigError}.
75
+ */
76
+ export declare class SitepingConfigError extends Error {
77
+ readonly code: SitepingConfigErrorCode;
78
+ constructor(code: SitepingConfigErrorCode, message: string);
79
+ }
80
+ /**
81
+ * Whether `error` is a `SitepingConfigError` — matched on its `name` and a
82
+ * `CONFIG_` code rather than `instanceof`, for the same cross-bundle reason
83
+ * as {@link isSitepingError}.
84
+ */
85
+ export declare function isSitepingConfigError(error: unknown): error is SitepingConfigError;
package/dist/i18n.d.cts CHANGED
@@ -12,7 +12,7 @@
12
12
  * dictionary file) exists. Adding a locale cannot silently fall back to
13
13
  * English anymore.
14
14
  */
15
- import { type BuiltinLocale } from "./types.cjs";
15
+ import type { BuiltinLocale } from "./types.cjs";
16
16
  /**
17
17
  * Lazy dictionary loaders for every non-English built-in locale. Use static
18
18
  * `() => import("./xx.cjs").then((m) => m.xx)` thunks — bundlers keep
@@ -26,7 +26,8 @@ export interface I18n<T> {
26
26
  /**
27
27
  * Create a translation function for the given locale.
28
28
  *
29
- * Locale resolution: exact language match > English fallback, per key.
29
+ * Locale resolution, per key: `registerLocale` override > built-in
30
+ * dictionary of the language > English fallback.
30
31
  * Non-English built-in locales are lazy-loaded via `loadLocale` — call
31
32
  * `await loadLocale(locale)` at init if you want the UI to render in the
32
33
  * target language immediately; until the dictionary lands, keys resolve
@@ -34,17 +35,20 @@ export interface I18n<T> {
34
35
  */
35
36
  createT(locale: string): TranslateFunction<T>;
36
37
  /**
37
- * Dynamically import a built-in locale and register it. Returns the
38
- * loaded dictionary, or `null` if the locale isn't a known built-in.
39
- * Custom locales registered via `registerLocale` bypass this loader —
40
- * they are already in the registry (and are returned as registered, so a
41
- * partial custom dictionary comes back partial).
38
+ * Dynamically import a built-in locale (once) and register it. Returns the
39
+ * locale as `createT` resolves it — the built-in dictionary with any
40
+ * `registerLocale` overrides on top — or `null` for a code that is neither
41
+ * built in nor registered. A custom code never hits a loader: it comes
42
+ * back as registered, so a partial custom dictionary comes back partial.
42
43
  */
43
44
  loadLocale(locale: string): Promise<Partial<T> | null>;
44
45
  /**
45
- * Register a custom locale at runtime. Partial dictionaries are welcome —
46
- * missing keys fall back to English per key, so overriding a single
47
- * string never requires copying the whole catalog.
46
+ * Register translations at runtime, for a new locale or over a built-in
47
+ * one. Partial dictionaries are welcome: the keys given override the
48
+ * built-in dictionary (whenever it loads), every other key keeps its
49
+ * built-in value, then falls back to English — so overriding a single
50
+ * string never requires copying the whole catalog. Calls for the same
51
+ * code accumulate.
48
52
  */
49
53
  registerLocale(code: string, translations: Partial<T>): void;
50
54
  }
package/dist/i18n.d.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  * dictionary file) exists. Adding a locale cannot silently fall back to
13
13
  * English anymore.
14
14
  */
15
- import { type BuiltinLocale } from "./types.js";
15
+ import type { BuiltinLocale } from "./types.js";
16
16
  /**
17
17
  * Lazy dictionary loaders for every non-English built-in locale. Use static
18
18
  * `() => import("./xx.js").then((m) => m.xx)` thunks — bundlers keep
@@ -26,7 +26,8 @@ export interface I18n<T> {
26
26
  /**
27
27
  * Create a translation function for the given locale.
28
28
  *
29
- * Locale resolution: exact language match > English fallback, per key.
29
+ * Locale resolution, per key: `registerLocale` override > built-in
30
+ * dictionary of the language > English fallback.
30
31
  * Non-English built-in locales are lazy-loaded via `loadLocale` — call
31
32
  * `await loadLocale(locale)` at init if you want the UI to render in the
32
33
  * target language immediately; until the dictionary lands, keys resolve
@@ -34,17 +35,20 @@ export interface I18n<T> {
34
35
  */
35
36
  createT(locale: string): TranslateFunction<T>;
36
37
  /**
37
- * Dynamically import a built-in locale and register it. Returns the
38
- * loaded dictionary, or `null` if the locale isn't a known built-in.
39
- * Custom locales registered via `registerLocale` bypass this loader —
40
- * they are already in the registry (and are returned as registered, so a
41
- * partial custom dictionary comes back partial).
38
+ * Dynamically import a built-in locale (once) and register it. Returns the
39
+ * locale as `createT` resolves it — the built-in dictionary with any
40
+ * `registerLocale` overrides on top — or `null` for a code that is neither
41
+ * built in nor registered. A custom code never hits a loader: it comes
42
+ * back as registered, so a partial custom dictionary comes back partial.
42
43
  */
43
44
  loadLocale(locale: string): Promise<Partial<T> | null>;
44
45
  /**
45
- * Register a custom locale at runtime. Partial dictionaries are welcome —
46
- * missing keys fall back to English per key, so overriding a single
47
- * string never requires copying the whole catalog.
46
+ * Register translations at runtime, for a new locale or over a built-in
47
+ * one. Partial dictionaries are welcome: the keys given override the
48
+ * built-in dictionary (whenever it loads), every other key keeps its
49
+ * built-in value, then falls back to English — so overriding a single
50
+ * string never requires copying the whole catalog. Calls for the same
51
+ * code accumulate.
48
52
  */
49
53
  registerLocale(code: string, translations: Partial<T>): void;
50
54
  }