@solutiondesk/blog 0.0.0 → 1.0.0-1cb60bf

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/index.js ADDED
@@ -0,0 +1,434 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ // ../sdk-core/src/http.ts
6
+ var DEFAULT_BASE_URL = "https://api.solutiondesk.io";
7
+ var SolutionDeskError = class extends Error {
8
+ constructor(message, init) {
9
+ super(message);
10
+ /** The HTTP status; `0` when no answer arrived (network error, aborted). */
11
+ __publicField(this, "status");
12
+ /** The parsed JSON body of the error answer (usually `{ error: string }`), else its text. */
13
+ __publicField(this, "body");
14
+ /** The requested URL. */
15
+ __publicField(this, "url");
16
+ /** Messages per input field on a `400` (e.g. `{ email: "Enter a valid email address." }`). */
17
+ __publicField(this, "fields");
18
+ this.name = "SolutionDeskError";
19
+ this.status = init.status;
20
+ this.body = init.body ?? null;
21
+ this.url = init.url;
22
+ this.fields = fieldsOf(init.body);
23
+ if (init.cause !== void 0) {
24
+ Object.defineProperty(this, "cause", { value: init.cause, enumerable: false });
25
+ }
26
+ }
27
+ /** `404`: unknown workspace, module turned off or unknown item — on purpose indistinguishable. */
28
+ get isNotFound() {
29
+ return this.status === 404;
30
+ }
31
+ /** `429`: over the rate limit; try again later. */
32
+ get isRateLimited() {
33
+ return this.status === 429;
34
+ }
35
+ };
36
+ function fieldsOf(body) {
37
+ if (!body || typeof body !== "object") return {};
38
+ const fields = body.fields;
39
+ if (!fields || typeof fields !== "object") return {};
40
+ const out = {};
41
+ for (const [key, value] of Object.entries(fields)) {
42
+ if (typeof value === "string") out[key] = value;
43
+ }
44
+ return out;
45
+ }
46
+ function messageOf(body, status) {
47
+ if (body && typeof body === "object") {
48
+ const { error, message, statusCode } = body;
49
+ if (typeof statusCode === "number" && typeof message === "string" && message) return message;
50
+ if (typeof error === "string" && error) return error;
51
+ if (typeof message === "string" && message) return message;
52
+ if (Array.isArray(message) && typeof message[0] === "string") return message[0];
53
+ }
54
+ if (typeof body === "string" && body.trim() && body.length <= 300) return body.trim();
55
+ return `SolutionDesk answered ${status}.`;
56
+ }
57
+ function createMemoryEtagCache(maxEntries = 500) {
58
+ const entries = /* @__PURE__ */ new Map();
59
+ return {
60
+ get: (key) => entries.get(key),
61
+ set(key, value) {
62
+ entries.delete(key);
63
+ entries.set(key, value);
64
+ if (entries.size > maxEntries) {
65
+ const oldest = entries.keys().next().value;
66
+ if (oldest !== void 0) entries.delete(oldest);
67
+ }
68
+ }
69
+ };
70
+ }
71
+ function queryString(query) {
72
+ if (!query) return "";
73
+ const parts = [];
74
+ for (const [key, value] of Object.entries(query)) {
75
+ if (value === void 0 || value === null || value === "") continue;
76
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
77
+ }
78
+ return parts.length ? `?${parts.join("&")}` : "";
79
+ }
80
+ var seg = (value) => encodeURIComponent(String(value));
81
+ function parseBody(text) {
82
+ if (!text) return null;
83
+ try {
84
+ return JSON.parse(text);
85
+ } catch {
86
+ return text;
87
+ }
88
+ }
89
+ function createHttp(options = {}) {
90
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "").replace(/\/v1$/, "");
91
+ const fetchImpl = options.fetch ?? globalThis.fetch;
92
+ const cache = options.etagCache === true ? createMemoryEtagCache() : options.etagCache ? options.etagCache : void 0;
93
+ const url = (path, query) => `${baseUrl}/v1${path}${queryString(query)}`;
94
+ async function requestWithMeta(req) {
95
+ if (!fetchImpl) {
96
+ throw new Error("No global fetch available: pass `fetch` to the client.");
97
+ }
98
+ const target = url(req.path, req.query);
99
+ const headers = {
100
+ accept: req.accept ?? "application/json",
101
+ ...options.headers,
102
+ ...req.options?.headers
103
+ };
104
+ if (req.body !== void 0) headers["content-type"] = "application/json";
105
+ if (req.apiKey) headers.authorization = `Bearer ${req.apiKey}`;
106
+ const cacheKey = req.method === "GET" && !req.apiKey ? target : void 0;
107
+ const cached = cacheKey && cache ? cache.get(cacheKey) : void 0;
108
+ if (cached && !Object.keys(headers).some((h) => h.toLowerCase() === "if-none-match")) {
109
+ headers["if-none-match"] = cached.etag;
110
+ }
111
+ const init = {
112
+ ...req.options?.fetchOptions,
113
+ method: req.method,
114
+ headers
115
+ };
116
+ if (req.body !== void 0) init.body = JSON.stringify(req.body);
117
+ if (req.options?.signal !== void 0) init.signal = req.options.signal;
118
+ let response;
119
+ try {
120
+ response = await fetchImpl(target, init);
121
+ } catch (cause) {
122
+ const reason = cause instanceof Error ? cause.message : String(cause);
123
+ throw new SolutionDeskError(`The request to SolutionDesk failed: ${reason}`, {
124
+ status: 0,
125
+ url: target,
126
+ cause
127
+ });
128
+ }
129
+ const finalUrl = response.url;
130
+ const meta = {
131
+ status: response.status,
132
+ finalUrl: typeof finalUrl === "string" && finalUrl ? finalUrl : null
133
+ };
134
+ if (response.status === 304 && cached) {
135
+ return { data: cached.body, ...meta };
136
+ }
137
+ const text = await response.text();
138
+ const body = response.ok && req.responseType === "text" ? text : parseBody(text);
139
+ if (!response.ok) {
140
+ throw new SolutionDeskError(messageOf(body, response.status), {
141
+ status: response.status,
142
+ body,
143
+ url: target
144
+ });
145
+ }
146
+ const etag = response.headers.get("etag");
147
+ if (cacheKey && cache && etag) cache.set(cacheKey, { etag, body });
148
+ return { data: body, ...meta };
149
+ }
150
+ return {
151
+ baseUrl,
152
+ url,
153
+ requestWithMeta,
154
+ request: async (req) => (await requestWithMeta(req)).data
155
+ };
156
+ }
157
+ function required(value, name) {
158
+ if (typeof value !== "string" || !value.trim()) {
159
+ throw new TypeError(`SolutionDesk: \`${name}\` is required.`);
160
+ }
161
+ return value;
162
+ }
163
+
164
+ // ../sdk-core/src/images.ts
165
+ var IMAGE_WIDTHS = [320, 640, 960, 1280, 1920];
166
+ function imageWidthFor(width) {
167
+ return IMAGE_WIDTHS.find((w) => w >= width) ?? 1920;
168
+ }
169
+ function imageUrl(url, width) {
170
+ if (width === void 0) return url;
171
+ const base = url.replace(/([?&])w=\d+(&|$)/, (_m, sep, end) => end ? sep : "");
172
+ const join = base.includes("?") ? /[?&]$/.test(base) ? "" : "&" : "?";
173
+ return `${base}${join}w=${imageWidthFor(width)}`;
174
+ }
175
+
176
+ // ../sdk-core/src/webhook.ts
177
+ var SIGNATURE_HEADER = "x-solutiondesk-signature";
178
+ var EVENT_HEADER = "x-solutiondesk-event";
179
+ function encoder() {
180
+ const Encoder = globalThis.TextEncoder;
181
+ if (!Encoder) throw new Error("TextEncoder is not available in this runtime.");
182
+ return new Encoder();
183
+ }
184
+ function bytesOf(body) {
185
+ if (typeof body === "string") return encoder().encode(body);
186
+ return body instanceof Uint8Array ? body : new Uint8Array(body);
187
+ }
188
+ function webhookBodyText(body) {
189
+ if (typeof body === "string") return body;
190
+ const Decoder = globalThis.TextDecoder;
191
+ if (!Decoder) throw new Error("TextDecoder is not available in this runtime.");
192
+ return new Decoder().decode(bytesOf(body));
193
+ }
194
+ function hex(buffer) {
195
+ let out = "";
196
+ for (const byte of new Uint8Array(buffer)) out += byte.toString(16).padStart(2, "0");
197
+ return out;
198
+ }
199
+ function constantTimeEqual(a, b) {
200
+ if (a.length !== b.length) return false;
201
+ let diff = 0;
202
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
203
+ return diff === 0;
204
+ }
205
+ function parseHeader(header) {
206
+ let t = Number.NaN;
207
+ const signatures = [];
208
+ for (const part of header.split(",")) {
209
+ const index = part.indexOf("=");
210
+ if (index < 0) continue;
211
+ const key = part.slice(0, index).trim();
212
+ const value = part.slice(index + 1).trim();
213
+ if (key === "t" && /^\d{1,12}$/.test(value)) t = Number(value);
214
+ else if (key === "v1" && /^[0-9a-f]{64}$/i.test(value)) signatures.push(value.toLowerCase());
215
+ }
216
+ return Number.isFinite(t) && signatures.length > 0 ? { t, signatures } : null;
217
+ }
218
+ async function verifyWebhookSignature(secret, rawBody, header, options = {}) {
219
+ if (!secret || !header || header.length > 1e3) return false;
220
+ const parsed = parseHeader(header);
221
+ if (!parsed) return false;
222
+ const now = options.now instanceof Date ? options.now.getTime() : options.now ?? Date.now();
223
+ const tolerance = options.toleranceSeconds ?? 300;
224
+ if (Math.abs(now / 1e3 - parsed.t) > tolerance) return false;
225
+ const subtle = options.crypto?.subtle ?? globalThis.crypto?.subtle;
226
+ if (!subtle) {
227
+ throw new Error(
228
+ "Web Crypto is not available: pass `crypto` (on Node 18: require('node:crypto').webcrypto)."
229
+ );
230
+ }
231
+ const text = encoder();
232
+ const key = await subtle.importKey(
233
+ "raw",
234
+ text.encode(secret),
235
+ { name: "HMAC", hash: "SHA-256" },
236
+ false,
237
+ ["sign"]
238
+ );
239
+ const prefix = text.encode(`${parsed.t}.`);
240
+ const body = bytesOf(rawBody);
241
+ const signed = new Uint8Array(prefix.length + body.length);
242
+ signed.set(prefix, 0);
243
+ signed.set(body, prefix.length);
244
+ const expected = hex(await subtle.sign("HMAC", key, signed));
245
+ let ok = false;
246
+ for (const signature of parsed.signatures) ok = constantTimeEqual(expected, signature) || ok;
247
+ return ok;
248
+ }
249
+ async function parseWebhook(secret, rawBody, header, options = {}) {
250
+ if (!await verifyWebhookSignature(secret, rawBody, header, options)) return null;
251
+ try {
252
+ return JSON.parse(webhookBodyText(rawBody));
253
+ } catch {
254
+ return null;
255
+ }
256
+ }
257
+
258
+ // src/blocks.ts
259
+ function escapeBlogHtml(value) {
260
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
261
+ }
262
+ function unescapeAttribute(value) {
263
+ return value.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
264
+ }
265
+ var OPEN = /<(span|div) class="sd-block" data-block="([^"]*)" data-props="([^"]*)">/g;
266
+ function closingEnd(html, tag, from) {
267
+ const pattern = new RegExp(`<${tag}[\\s>]|</${tag}>`, "g");
268
+ pattern.lastIndex = from;
269
+ let depth = 1;
270
+ for (let match = pattern.exec(html); match; match = pattern.exec(html)) {
271
+ depth += match[0].startsWith("</") ? -1 : 1;
272
+ if (depth === 0) return match.index + match[0].length;
273
+ }
274
+ return html.length;
275
+ }
276
+ function parseProps(raw) {
277
+ try {
278
+ const parsed = JSON.parse(unescapeAttribute(raw));
279
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
280
+ const props = {};
281
+ for (const [key, value] of Object.entries(parsed)) {
282
+ if (typeof value === "string" || typeof value === "boolean") props[key] = value;
283
+ }
284
+ return props;
285
+ } catch {
286
+ return {};
287
+ }
288
+ }
289
+ function renderBlogBlocks(html, renderers) {
290
+ let out = "";
291
+ let pos = 0;
292
+ OPEN.lastIndex = 0;
293
+ for (let open = OPEN.exec(html); open; open = OPEN.exec(html)) {
294
+ const [whole, tag = "div", name = "", rawProps = ""] = open;
295
+ const start = open.index;
296
+ const contentStart = start + whole.length;
297
+ const end = closingEnd(html, tag, contentStart);
298
+ const inner = html.slice(contentStart, end - `</${tag}>`.length);
299
+ const body = /^(?:<p class="sd-block-text">([\s\S]*?)<\/p>)?<div class="sd-block-body">([\s\S]*)<\/div>$/.exec(
300
+ inner
301
+ );
302
+ const element = {
303
+ block: unescapeAttribute(name),
304
+ props: parseProps(rawProps),
305
+ inline: tag === "span",
306
+ textHtml: body ? body[1] ?? "" : inner,
307
+ childrenHtml: body ? renderBlogBlocks(body[2] ?? "", renderers) : null
308
+ };
309
+ const own = renderers[element.block]?.(element);
310
+ out += html.slice(pos, start);
311
+ if (own !== void 0) out += own;
312
+ else if (body) {
313
+ out += `${whole}${body[1] !== void 0 ? `<p class="sd-block-text">${body[1]}</p>` : ""}<div class="sd-block-body">${element.childrenHtml}</div></${tag}>`;
314
+ } else out += html.slice(start, end);
315
+ pos = end;
316
+ OPEN.lastIndex = end;
317
+ }
318
+ return out + html.slice(pos);
319
+ }
320
+
321
+ // src/client.ts
322
+ function createBlogClient(options) {
323
+ const workspace = required(options.workspace, "workspace");
324
+ const http = createHttp(options);
325
+ const base = `/blog/${seg(workspace)}`;
326
+ const langOf = (params) => params?.lang ?? options.lang;
327
+ const listPosts = (params = {}, requestOptions) => http.request({
328
+ method: "GET",
329
+ path: `${base}/posts`,
330
+ query: {
331
+ lang: langOf(params),
332
+ tag: params.tag,
333
+ author: params.author,
334
+ limit: params.limit,
335
+ cursor: params.cursor
336
+ },
337
+ options: requestOptions
338
+ });
339
+ const fetchPost = (slug, lang, requestOptions) => http.request({
340
+ method: "GET",
341
+ path: `${base}/posts/${seg(slug)}`,
342
+ query: { lang },
343
+ options: requestOptions
344
+ });
345
+ async function getPost(slug, params = {}, requestOptions) {
346
+ required(slug, "slug");
347
+ const lang = langOf(params);
348
+ const asked = slug.toLowerCase();
349
+ if (params.redirect === "manual") {
350
+ try {
351
+ return await fetchPost(slug, lang, {
352
+ ...requestOptions,
353
+ fetchOptions: { ...requestOptions?.fetchOptions, redirect: "manual" }
354
+ });
355
+ } catch (error) {
356
+ if (!(error instanceof SolutionDeskError)) throw error;
357
+ const redirectTo = error.body?.redirectTo;
358
+ if (error.status === 301 && typeof redirectTo === "string") return { redirectTo };
359
+ if (error.status !== 0) throw error;
360
+ }
361
+ const payload2 = await fetchPost(slug, lang, requestOptions);
362
+ return payload2.post.slug !== asked ? { redirectTo: payload2.post.slug } : payload2;
363
+ }
364
+ const payload = await fetchPost(slug, lang, requestOptions);
365
+ return { ...payload, movedFrom: payload.post.slug !== asked ? slug : null };
366
+ }
367
+ return {
368
+ workspace,
369
+ listPosts,
370
+ async *allPosts(params = {}, requestOptions) {
371
+ let cursor = null;
372
+ do {
373
+ const page = await listPosts(
374
+ { ...params, limit: params.limit ?? 50, cursor },
375
+ requestOptions
376
+ );
377
+ yield* page.posts;
378
+ cursor = page.nextCursor;
379
+ } while (cursor);
380
+ },
381
+ getPost,
382
+ listTags: async (params, requestOptions) => http.request({
383
+ method: "GET",
384
+ path: `${base}/tags`,
385
+ query: { lang: langOf(params) },
386
+ options: requestOptions
387
+ }),
388
+ listAuthors: async (params, requestOptions) => http.request({
389
+ method: "GET",
390
+ path: `${base}/authors`,
391
+ query: { lang: langOf(params) },
392
+ options: requestOptions
393
+ }),
394
+ getPreview: async (token, params, requestOptions) => http.request({
395
+ method: "GET",
396
+ path: `${base}/preview`,
397
+ query: { token: required(token, "token"), lang: langOf(params) },
398
+ options: requestOptions
399
+ }),
400
+ imageUrl
401
+ };
402
+ }
403
+ function parseBlogWebhook(secret, rawBody, signatureHeader, options) {
404
+ return parseWebhook(secret, rawBody, signatureHeader, options);
405
+ }
406
+
407
+ // src/head.ts
408
+ function renderBlogJsonLd(data) {
409
+ return JSON.stringify(data).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
410
+ }
411
+ function renderBlogHead(post) {
412
+ const { seo } = post;
413
+ const lines = [`<title>${escapeBlogHtml(seo.title)}</title>`];
414
+ if (seo.canonicalUrl) {
415
+ lines.push(`<link rel="canonical" href="${escapeBlogHtml(seo.canonicalUrl)}">`);
416
+ }
417
+ for (const alternate of seo.alternates) {
418
+ lines.push(
419
+ `<link rel="alternate" hreflang="${escapeBlogHtml(alternate.locale)}" href="${escapeBlogHtml(alternate.url)}">`
420
+ );
421
+ }
422
+ for (const tag of seo.meta) {
423
+ const [attribute, key] = "property" in tag ? ["property", tag.property] : ["name", tag.name];
424
+ lines.push(
425
+ `<meta ${attribute}="${escapeBlogHtml(key)}" content="${escapeBlogHtml(tag.content)}">`
426
+ );
427
+ }
428
+ for (const data of seo.jsonLd) {
429
+ lines.push(`<script type="application/ld+json">${renderBlogJsonLd(data)}</script>`);
430
+ }
431
+ return lines.join("\n");
432
+ }
433
+
434
+ export { DEFAULT_BASE_URL, EVENT_HEADER, IMAGE_WIDTHS, SIGNATURE_HEADER, SolutionDeskError, createBlogClient, createMemoryEtagCache, escapeBlogHtml, imageUrl, imageWidthFor, parseBlogWebhook, renderBlogBlocks, renderBlogHead, renderBlogJsonLd, verifyWebhookSignature };
package/package.json CHANGED
@@ -1,10 +1,58 @@
1
1
  {
2
2
  "name": "@solutiondesk/blog",
3
- "version": "0.0.0",
4
- "description": "OIDC trusted publishing setup package for @solutiondesk/blog",
3
+ "version": "1.0.0-1cb60bf",
4
+ "description": "Typed, zero-dependency client for the SolutionDesk public blog API: posts, tags, authors, previews and webhook verification.",
5
5
  "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
10
- }
6
+ "solutiondesk",
7
+ "blog",
8
+ "cms",
9
+ "headless-cms",
10
+ "sdk",
11
+ "typescript"
12
+ ],
13
+ "homepage": "https://help.solutiondesk.io/docs/team/blog-website",
14
+ "bugs": {
15
+ "url": "https://github.com/wemogy/solutiondesk/issues"
16
+ },
17
+ "license": "MIT",
18
+ "author": "wemogy IT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/wemogy/solutiondesk.git",
22
+ "directory": "packages/sdk-blog"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "main": "./dist/index.cjs",
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "import": {
35
+ "types": "./dist/index.d.ts",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "require": {
39
+ "types": "./dist/index.d.cts",
40
+ "default": "./dist/index.cjs"
41
+ }
42
+ },
43
+ "./package.json": "./package.json"
44
+ },
45
+ "files": [
46
+ "dist",
47
+ "README.md",
48
+ "LICENSE"
49
+ ],
50
+ "engines": {
51
+ "node": ">=18"
52
+ },
53
+ "scripts": {
54
+ "build": "tsup",
55
+ "typecheck": "tsc --noEmit",
56
+ "test": "vitest run"
57
+ }
58
+ }