@solutiondesk/blog 0.0.0 → 1.0.0-69d9f69
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/LICENSE +21 -0
- package/README.md +180 -28
- package/dist/index.cjs +356 -0
- package/dist/index.d.cts +349 -0
- package/dist/index.d.ts +349 -0
- package/dist/index.js +344 -0
- package/package.json +55 -7
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HTTP part shared by the public SolutionDesk SDKs. Runs on any runtime with
|
|
3
|
+
* a WHATWG `fetch` (Node ≥ 18, Deno, Bun, edge runtimes, browsers) — and on
|
|
4
|
+
* purpose names no DOM or Node type, so the published `.d.ts` files need neither
|
|
5
|
+
* `lib: ["dom"]` nor `@types/node`.
|
|
6
|
+
*/
|
|
7
|
+
/** The production API. */
|
|
8
|
+
declare const DEFAULT_BASE_URL = "https://api.solutiondesk.io";
|
|
9
|
+
/** The response parts the SDK reads — what every `fetch` implementation returns. */
|
|
10
|
+
interface FetchResponseLike {
|
|
11
|
+
readonly status: number;
|
|
12
|
+
readonly ok: boolean;
|
|
13
|
+
readonly headers: {
|
|
14
|
+
get(name: string): string | null;
|
|
15
|
+
};
|
|
16
|
+
text(): Promise<string>;
|
|
17
|
+
}
|
|
18
|
+
/** What the SDK passes to `fetch` (plus whatever you set in `fetchOptions`). */
|
|
19
|
+
interface FetchInitLike {
|
|
20
|
+
method: string;
|
|
21
|
+
headers: Record<string, string>;
|
|
22
|
+
body?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A `fetch` implementation. The global `fetch` fits, so do `undici`, `node-fetch`
|
|
26
|
+
* and wrappers (e.g. one that adds tracing). Declared bivariantly so a fetch with
|
|
27
|
+
* a richer `init` type is accepted.
|
|
28
|
+
*/
|
|
29
|
+
type FetchLike = {
|
|
30
|
+
bivarianceHack(input: string, init?: FetchInitLike): Promise<FetchResponseLike>;
|
|
31
|
+
}["bivarianceHack"];
|
|
32
|
+
/** A response kept for `If-None-Match` revalidation. */
|
|
33
|
+
interface CachedResponse {
|
|
34
|
+
etag: string;
|
|
35
|
+
body: unknown;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Where the client keeps answers for ETag revalidation. Any `Map` fits; bring
|
|
39
|
+
* your own to share it between clients or to bound it differently.
|
|
40
|
+
*/
|
|
41
|
+
interface EtagCache {
|
|
42
|
+
get(key: string): CachedResponse | undefined;
|
|
43
|
+
set(key: string, value: CachedResponse): unknown;
|
|
44
|
+
}
|
|
45
|
+
/** Options every client factory takes. */
|
|
46
|
+
interface BaseClientOptions {
|
|
47
|
+
/** The API origin, without `/v1`. Defaults to `https://api.solutiondesk.io`. */
|
|
48
|
+
baseUrl?: string;
|
|
49
|
+
/** A `fetch` implementation; defaults to the global one. */
|
|
50
|
+
fetch?: FetchLike;
|
|
51
|
+
/** Headers sent with every request (e.g. `accept-language` forwarded from your visitor). */
|
|
52
|
+
headers?: Record<string, string>;
|
|
53
|
+
/**
|
|
54
|
+
* Revalidate with `If-None-Match`: the client remembers each answer's `ETag`
|
|
55
|
+
* and, when the API answers `304 Not Modified`, returns the remembered body.
|
|
56
|
+
* `true` keeps up to 500 answers in memory; or pass your own {@link EtagCache}.
|
|
57
|
+
* Off by default (your framework's fetch cache may already do this).
|
|
58
|
+
*/
|
|
59
|
+
etagCache?: boolean | EtagCache;
|
|
60
|
+
}
|
|
61
|
+
/** Per-call options. */
|
|
62
|
+
interface RequestOptions {
|
|
63
|
+
/** Cancels the request (an `AbortSignal`). */
|
|
64
|
+
signal?: unknown;
|
|
65
|
+
/** Extra headers for this request. */
|
|
66
|
+
headers?: Record<string, string>;
|
|
67
|
+
/**
|
|
68
|
+
* Extra `fetch` options for this request, passed through unchanged — e.g.
|
|
69
|
+
* Next.js `{ next: { revalidate: 60, tags: ["legal"] } }` or `{ cache: "no-store" }`.
|
|
70
|
+
*/
|
|
71
|
+
fetchOptions?: Record<string, unknown>;
|
|
72
|
+
}
|
|
73
|
+
/** Any non-2xx answer of the API (and a failed network request, with `status` 0). */
|
|
74
|
+
declare class SolutionDeskError extends Error {
|
|
75
|
+
/** The HTTP status; `0` when no answer arrived (network error, aborted). */
|
|
76
|
+
readonly status: number;
|
|
77
|
+
/** The parsed JSON body of the error answer (usually `{ error: string }`), else its text. */
|
|
78
|
+
readonly body: unknown;
|
|
79
|
+
/** The requested URL. */
|
|
80
|
+
readonly url: string;
|
|
81
|
+
/** Messages per input field on a `400` (e.g. `{ email: "Enter a valid email address." }`). */
|
|
82
|
+
readonly fields: Record<string, string>;
|
|
83
|
+
constructor(message: string, init: {
|
|
84
|
+
status: number;
|
|
85
|
+
body?: unknown;
|
|
86
|
+
url: string;
|
|
87
|
+
cause?: unknown;
|
|
88
|
+
});
|
|
89
|
+
/** `404`: unknown workspace, module turned off or unknown item — on purpose indistinguishable. */
|
|
90
|
+
get isNotFound(): boolean;
|
|
91
|
+
/** `429`: over the rate limit; try again later. */
|
|
92
|
+
get isRateLimited(): boolean;
|
|
93
|
+
}
|
|
94
|
+
/** A bounded in-memory {@link EtagCache} (oldest entry dropped first). */
|
|
95
|
+
declare function createMemoryEtagCache(maxEntries?: number): EtagCache;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Public blog and event images come in a fixed set of widths (`<url>?w=<width>`,
|
|
99
|
+
* WebP, never wider than the original); the API refuses any other width.
|
|
100
|
+
*/
|
|
101
|
+
declare const IMAGE_WIDTHS: readonly [320, 640, 960, 1280, 1920];
|
|
102
|
+
type ImageWidth = (typeof IMAGE_WIDTHS)[number];
|
|
103
|
+
/** The smallest offered width that is at least `width` (1920 for anything wider). */
|
|
104
|
+
declare function imageWidthFor(width: number): ImageWidth;
|
|
105
|
+
/**
|
|
106
|
+
* The address of an image at (at least) `width` pixels — e.g. for a Next.js
|
|
107
|
+
* custom image loader: `({ src, width }) => imageUrl(src, width)`. Without a
|
|
108
|
+
* width the original address is returned.
|
|
109
|
+
*/
|
|
110
|
+
declare function imageUrl(url: string, width?: number): string;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Webhook signature check with Web Crypto only — the same code runs in Node,
|
|
114
|
+
* Deno, Bun, edge runtimes and browsers. The header SolutionDesk sends is
|
|
115
|
+
* `X-SolutionDesk-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>`.
|
|
116
|
+
*/
|
|
117
|
+
/** The header that carries the signature. */
|
|
118
|
+
declare const SIGNATURE_HEADER = "x-solutiondesk-signature";
|
|
119
|
+
/** The header that names the event (also in the body as `event`). */
|
|
120
|
+
declare const EVENT_HEADER = "x-solutiondesk-event";
|
|
121
|
+
interface VerifyWebhookOptions {
|
|
122
|
+
/** How old (or how far in the future) the signed time may be. Default 300 (5 minutes). */
|
|
123
|
+
toleranceSeconds?: number;
|
|
124
|
+
/** "Now" in milliseconds (or a `Date`) — for tests. Default `Date.now()`. */
|
|
125
|
+
now?: number | Date;
|
|
126
|
+
/**
|
|
127
|
+
* A Web Crypto implementation. Default `globalThis.crypto`. On Node 18 pass
|
|
128
|
+
* `require("node:crypto").webcrypto` (Node 19+ has it globally).
|
|
129
|
+
*/
|
|
130
|
+
crypto?: {
|
|
131
|
+
subtle: object;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** The raw request body, exactly as received. */
|
|
135
|
+
type WebhookBody = string | Uint8Array | ArrayBuffer;
|
|
136
|
+
/**
|
|
137
|
+
* Check a SolutionDesk webhook signature. `true` only when the header is well
|
|
138
|
+
* formed, its time lies within the tolerance and the HMAC matches — anything
|
|
139
|
+
* else (missing, tampered, expired, malformed) is `false`, never an exception.
|
|
140
|
+
*
|
|
141
|
+
* @param secret The webhook's signing secret (`whsec_…`), shown once in SolutionDesk.
|
|
142
|
+
* @param rawBody The request body exactly as received — not re-serialized JSON.
|
|
143
|
+
* @param header The `X-SolutionDesk-Signature` header.
|
|
144
|
+
*/
|
|
145
|
+
declare function verifyWebhookSignature(secret: string, rawBody: WebhookBody, header: string | null | undefined, options?: VerifyWebhookOptions): Promise<boolean>;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The answers of the public blog API (`GET /v1/blog/{workspace}/…`). Written by
|
|
149
|
+
* hand so the package has no dependencies; a drift test in the SolutionDesk
|
|
150
|
+
* repository keeps them identical to the API's own contracts.
|
|
151
|
+
*/
|
|
152
|
+
/** An author as readers see them. */
|
|
153
|
+
interface PublicBlogAuthor {
|
|
154
|
+
key: string;
|
|
155
|
+
name: string;
|
|
156
|
+
initials: string;
|
|
157
|
+
kind: "person" | "organization";
|
|
158
|
+
/** The role in the requested language (the main role when not translated). */
|
|
159
|
+
role: string | null;
|
|
160
|
+
}
|
|
161
|
+
interface PublicBlogTag {
|
|
162
|
+
key: string;
|
|
163
|
+
/** The label in the requested language. */
|
|
164
|
+
label: string;
|
|
165
|
+
}
|
|
166
|
+
/** The cover image of a post. */
|
|
167
|
+
interface PublicBlogImage {
|
|
168
|
+
url: string;
|
|
169
|
+
/** Every web-ready width (WebP, `<url>?w=<width>`) for `<img srcset>`. */
|
|
170
|
+
srcset: string;
|
|
171
|
+
width: number | null;
|
|
172
|
+
height: number | null;
|
|
173
|
+
alt: string | null;
|
|
174
|
+
/** A short caption or placeholder text. */
|
|
175
|
+
hint: string | null;
|
|
176
|
+
/** Made with AI: show a disclosure. */
|
|
177
|
+
ai: boolean;
|
|
178
|
+
credit: string | null;
|
|
179
|
+
}
|
|
180
|
+
/** The call to action at the end of a post. */
|
|
181
|
+
interface PublicBlogClosing {
|
|
182
|
+
title: string | null;
|
|
183
|
+
lead: string | null;
|
|
184
|
+
label: string | null;
|
|
185
|
+
href: string | null;
|
|
186
|
+
}
|
|
187
|
+
/** A heading of the text, for a table of contents. */
|
|
188
|
+
interface BlogHeading {
|
|
189
|
+
/** 1–6. */
|
|
190
|
+
level: number;
|
|
191
|
+
text: string;
|
|
192
|
+
/** Unique within the post; `#<id>` links to the heading. */
|
|
193
|
+
id: string;
|
|
194
|
+
}
|
|
195
|
+
/** A post in the list: everything but the text. */
|
|
196
|
+
interface PublicBlogPostSummary {
|
|
197
|
+
slug: string;
|
|
198
|
+
title: string;
|
|
199
|
+
description: string;
|
|
200
|
+
/** ISO time readers see as the publication date. */
|
|
201
|
+
publishedAt: string;
|
|
202
|
+
/** "Updated on" as the team set it, or null. */
|
|
203
|
+
contentUpdatedAt: string | null;
|
|
204
|
+
/** The last change of any kind (for caches and sitemaps). */
|
|
205
|
+
updatedAt: string;
|
|
206
|
+
authors: PublicBlogAuthor[];
|
|
207
|
+
tags: PublicBlogTag[];
|
|
208
|
+
cover: PublicBlogImage | null;
|
|
209
|
+
/** The language the texts above are in — may differ from the one asked for. */
|
|
210
|
+
locale: string;
|
|
211
|
+
/** Every language the post is offered in, main language first. */
|
|
212
|
+
languages: string[];
|
|
213
|
+
readingMinutes: number;
|
|
214
|
+
}
|
|
215
|
+
/** One post with its text. */
|
|
216
|
+
interface PublicBlogPost extends PublicBlogPostSummary {
|
|
217
|
+
markdown: string;
|
|
218
|
+
/** The Markdown as safe HTML (headings carry `id`s). */
|
|
219
|
+
html: string;
|
|
220
|
+
/** The outline, for a table of contents. */
|
|
221
|
+
headings: BlogHeading[];
|
|
222
|
+
closing: PublicBlogClosing | null;
|
|
223
|
+
}
|
|
224
|
+
/** `GET …/posts`. */
|
|
225
|
+
interface PublicBlogPostsPayload {
|
|
226
|
+
posts: PublicBlogPostSummary[];
|
|
227
|
+
/** Pass as `cursor` for the next page; null on the last page. */
|
|
228
|
+
nextCursor: string | null;
|
|
229
|
+
}
|
|
230
|
+
/** `GET …/posts/{slug}` and `GET …/preview`. */
|
|
231
|
+
interface PublicBlogPostPayload {
|
|
232
|
+
post: PublicBlogPost;
|
|
233
|
+
}
|
|
234
|
+
/** `GET …/tags`: every tag with how many published posts carry it, most used first. */
|
|
235
|
+
interface PublicBlogTagsPayload {
|
|
236
|
+
tags: (PublicBlogTag & {
|
|
237
|
+
count: number;
|
|
238
|
+
})[];
|
|
239
|
+
}
|
|
240
|
+
/** `GET …/authors`. */
|
|
241
|
+
interface PublicBlogAuthorsPayload {
|
|
242
|
+
authors: PublicBlogAuthor[];
|
|
243
|
+
}
|
|
244
|
+
/** What a blog webhook announces. */
|
|
245
|
+
type BlogWebhookEvent = "post.published" | "post.updated" | "post.unpublished" | "post.deleted" | "ping";
|
|
246
|
+
/** The body of a blog webhook call (verify it first — see `parseBlogWebhook`). */
|
|
247
|
+
interface BlogWebhookBody {
|
|
248
|
+
event: BlogWebhookEvent;
|
|
249
|
+
/** The workspace slug, as in the public API path. */
|
|
250
|
+
workspace: string;
|
|
251
|
+
/** The post's slug; null for `ping`. */
|
|
252
|
+
slug: string | null;
|
|
253
|
+
/** Earlier slugs of the post — their pages are stale too. */
|
|
254
|
+
previousSlugs: string[];
|
|
255
|
+
/** The languages the post is offered in after the change. */
|
|
256
|
+
locales: string[];
|
|
257
|
+
occurredAt: string;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
interface BlogClientOptions extends BaseClientOptions {
|
|
261
|
+
/** Your workspace's slug — the `acme` in `https://api.solutiondesk.io/v1/blog/acme`. */
|
|
262
|
+
workspace: string;
|
|
263
|
+
/**
|
|
264
|
+
* The language to ask for by default (`de`, `en`, `de-AT` …). Each answer
|
|
265
|
+
* says in `locale` which language it really is in. Override per call.
|
|
266
|
+
*/
|
|
267
|
+
lang?: string;
|
|
268
|
+
}
|
|
269
|
+
interface LangParams {
|
|
270
|
+
/** The language to ask for; overrides the client's `lang`. */
|
|
271
|
+
lang?: string;
|
|
272
|
+
}
|
|
273
|
+
interface ListPostsParams extends LangParams {
|
|
274
|
+
/** Only posts with this tag key. An unknown key matches nothing. */
|
|
275
|
+
tag?: string;
|
|
276
|
+
/** Only posts by this author key. An unknown key matches nothing. */
|
|
277
|
+
author?: string;
|
|
278
|
+
/** Page size, 1–50 (default 20). */
|
|
279
|
+
limit?: number;
|
|
280
|
+
/** `nextCursor` of the previous page. */
|
|
281
|
+
cursor?: string | null;
|
|
282
|
+
}
|
|
283
|
+
interface GetPostParams extends LangParams {
|
|
284
|
+
/**
|
|
285
|
+
* What to do when the slug is an earlier address of a post:
|
|
286
|
+
* - `"follow"` (default): return the post; `movedFrom` holds the slug you asked for.
|
|
287
|
+
* - `"manual"`: return `{ redirectTo }` with the current slug instead.
|
|
288
|
+
*/
|
|
289
|
+
redirect?: "follow" | "manual";
|
|
290
|
+
}
|
|
291
|
+
/** A post fetched with `redirect: "follow"`. */
|
|
292
|
+
interface BlogPostResult extends PublicBlogPostPayload {
|
|
293
|
+
/**
|
|
294
|
+
* The slug you asked for when the post has moved to `post.slug` since
|
|
295
|
+
* (redirect your page there, e.g. with a 301); null when it hasn't.
|
|
296
|
+
*/
|
|
297
|
+
movedFrom: string | null;
|
|
298
|
+
}
|
|
299
|
+
/** The post lives at another slug now (with `redirect: "manual"`). */
|
|
300
|
+
interface BlogPostMoved {
|
|
301
|
+
redirectTo: string;
|
|
302
|
+
}
|
|
303
|
+
interface BlogClient {
|
|
304
|
+
/** The workspace this client reads. */
|
|
305
|
+
readonly workspace: string;
|
|
306
|
+
/** Published posts, newest first, one page (`GET /v1/blog/{workspace}/posts`). */
|
|
307
|
+
listPosts(params?: ListPostsParams, options?: RequestOptions): Promise<PublicBlogPostsPayload>;
|
|
308
|
+
/** Every published post, page by page — e.g. for a sitemap or a static build. */
|
|
309
|
+
allPosts(params?: Omit<ListPostsParams, "cursor">, options?: RequestOptions): AsyncGenerator<PublicBlogPostSummary, void, undefined>;
|
|
310
|
+
/**
|
|
311
|
+
* One published post with its text (`GET /v1/blog/{workspace}/posts/{slug}`).
|
|
312
|
+
* Throws a `SolutionDeskError` with status 404 for an unknown or unpublished post.
|
|
313
|
+
*/
|
|
314
|
+
getPost(slug: string, params?: GetPostParams & {
|
|
315
|
+
redirect?: "follow";
|
|
316
|
+
}, options?: RequestOptions): Promise<BlogPostResult>;
|
|
317
|
+
getPost(slug: string, params: GetPostParams & {
|
|
318
|
+
redirect: "manual";
|
|
319
|
+
}, options?: RequestOptions): Promise<PublicBlogPostPayload | BlogPostMoved>;
|
|
320
|
+
/** Every tag with how many published posts carry it, most used first. */
|
|
321
|
+
listTags(params?: LangParams, options?: RequestOptions): Promise<PublicBlogTagsPayload>;
|
|
322
|
+
/** Every author with their role in the asked-for language. */
|
|
323
|
+
listAuthors(params?: LangParams, options?: RequestOptions): Promise<PublicBlogAuthorsPayload>;
|
|
324
|
+
/**
|
|
325
|
+
* The current state of a post (also a draft) for your preview route — the
|
|
326
|
+
* `token` from your preview address (valid for one hour). The API sends it with
|
|
327
|
+
* `Cache-Control: no-store`; in Next.js also pass `fetchOptions: { cache: "no-store" }`.
|
|
328
|
+
*/
|
|
329
|
+
getPreview(token: string, params?: LangParams, options?: RequestOptions): Promise<PublicBlogPostPayload>;
|
|
330
|
+
/** The address of a blog image at (at least) `width` pixels: 320, 640, 960, 1280 or 1920. */
|
|
331
|
+
imageUrl(url: string, width?: number): string;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* A client for the public blog API — no key, no login; only published posts.
|
|
335
|
+
* Safe in the browser and on the server.
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* const blog = createBlogClient({ workspace: "acme", lang: "en" });
|
|
339
|
+
* const { posts, nextCursor } = await blog.listPosts({ limit: 10 });
|
|
340
|
+
*/
|
|
341
|
+
declare function createBlogClient(options: BlogClientOptions): BlogClient;
|
|
342
|
+
/**
|
|
343
|
+
* Verify a blog webhook call and return its body — or `null` when the
|
|
344
|
+
* `X-SolutionDesk-Signature` header doesn't match (wrong secret, changed body,
|
|
345
|
+
* older than 5 minutes, malformed). Pass the raw body exactly as received.
|
|
346
|
+
*/
|
|
347
|
+
declare function parseBlogWebhook(secret: string, rawBody: WebhookBody, signatureHeader: string | null | undefined, options?: VerifyWebhookOptions): Promise<BlogWebhookBody | null>;
|
|
348
|
+
|
|
349
|
+
export { type BaseClientOptions, type BlogClient, type BlogClientOptions, type BlogHeading, type BlogPostMoved, type BlogPostResult, type BlogWebhookBody, type BlogWebhookEvent, type CachedResponse, DEFAULT_BASE_URL, EVENT_HEADER, type EtagCache, type FetchInitLike, type FetchLike, type FetchResponseLike, type GetPostParams, IMAGE_WIDTHS, type ImageWidth, type LangParams, type ListPostsParams, type PublicBlogAuthor, type PublicBlogAuthorsPayload, type PublicBlogClosing, type PublicBlogImage, type PublicBlogPost, type PublicBlogPostPayload, type PublicBlogPostSummary, type PublicBlogPostsPayload, type PublicBlogTag, type PublicBlogTagsPayload, type RequestOptions, SIGNATURE_HEADER, SolutionDeskError, type VerifyWebhookOptions, type WebhookBody, createBlogClient, createMemoryEtagCache, imageUrl, imageWidthFor, parseBlogWebhook, verifyWebhookSignature };
|